{"_id":"@contip/fm-odata-client","name":"@contip/fm-odata-client","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@contip/fm-odata-client","version":"1.0.0","description":"Enhanced Fork of FileMaker OData client with Repeating Fields support","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./claris-id":{"types":"./dist/ClarisId.d.ts","import":"./dist/ClarisId.js","require":"./dist/ClarisId.cjs"}},"type":"module","repository":{"type":"git","url":"git+https://github.com/contip/fm-odata-client.git"},"scripts":{"test":"mocha --loader=ts-node/esm --extension=ts test/**/*.ts","coverage":"nyc npm test","test-ci":"nyc --reporter=lcov npm test","build":"tsc --noEmit && tsup","lint":"eslint .","prepare":"npm run build"},"lint-staged":{"*.ts":"eslint --cache --fix"},"author":{"name":"Peter Conti","email":"petertconti@gmail.com"},"keywords":["FileMaker","REST","API","OData","Typescript"],"license":"MIT","engines":{"node":">=0.18"},"dependencies":{"file-type":"^18.2.1","undici":"^5.21.0"},"peerDependencies":{"amazon-cognito-identity-js":"^4.5.12"},"peerDependenciesMeta":{"amazon-cognito-identity-js":{"optional":true}},"devDependencies":{"@commitlint/cli":"^17.0.2","@commitlint/config-conventional":"^17.0.2","@tsconfig/node16":"^1.0.3","@types/chai":"^4.2.15","@types/chai-as-promised":"^7.1.3","@types/mocha":"^10.0.1","@types/node":"^18.15.7","@types/sinon":"^10.0.11","amazon-cognito-identity-js":"^6.2.0","chai":"^4.3.0","chai-as-promised":"^7.1.1","eslint":"^8.17.0","eslint-config-dasprid":"^0.1.12","husky":"^8.0.1","lint-staged":"^13.0.1","mocha":"^10.0.0","nyc":"^15.1.0","sinon":"^15.0.1","ts-node":"^10.8.1","tsup":"^6.7.0","typescript":"^5.0.2"},"_id":"@contip/fm-odata-client@1.0.0","gitHead":"b6369cf2b2ddd7128134ff6df3e33fda9b749c58","bugs":{"url":"https://github.com/contip/fm-odata-client/issues"},"homepage":"https://github.com/contip/fm-odata-client#readme","_nodeVersion":"21.6.2","_npmVersion":"10.8.3","dist":{"integrity":"sha512-Z9JpkkYCnRH96MRE0PEa6yCjumoJ+MuT5j8C2CCpkp3+d8K/0S0YBEB43A4xAvfLWjscwDhTQlp8c5g26Gpp9w==","shasum":"b914b0609b4bbedc1d376d91244db9119fc21a81","tarball":"https://registry.npmjs.org/@contip/fm-odata-client/-/fm-odata-client-1.0.0.tgz","fileCount":14,"unpackedSize":219130,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHjowMLJcElQfzb8XwYUL2UIdpnkw81WWbvxQhAJuDsOAiAuaJymtDpUifZhg9EvClGyS3/8ZFW07M8l9lon1mYZ7g=="}]},"_npmUser":{"name":"contip","email":"petertconti@gmail.com"},"directories":{},"maintainers":[{"name":"contip","email":"petertconti@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/fm-odata-client_1.0.0_1743607927593_0.2232030274891703"},"_hasShrinkwrap":false}},"time":{"created":"2025-04-02T15:32:07.490Z","1.0.0":"2025-04-02T15:32:07.865Z","modified":"2025-04-02T15:32:08.137Z"},"maintainers":[{"name":"contip","email":"petertconti@gmail.com"}],"description":"Enhanced Fork of FileMaker OData client with Repeating Fields support","homepage":"https://github.com/contip/fm-odata-client#readme","keywords":["FileMaker","REST","API","OData","Typescript"],"repository":{"type":"git","url":"git+https://github.com/contip/fm-odata-client.git"},"author":{"name":"Peter Conti","email":"petertconti@gmail.com"},"bugs":{"url":"https://github.com/contip/fm-odata-client/issues"},"license":"MIT","readme":"# FileMaker OData client\n\n[![npm version](https://badge.fury.io/js/fm-odata-client.svg)](https://badge.fury.io/js/fm-odata-client)\n[![Release](https://github.com/soliantconsulting/fm-odata-client/actions/workflows/release.yml/badge.svg)](https://github.com/soliantconsulting/fm-odata-client/actions/workflows/release.yml)\n[![Coverage Status](https://coveralls.io/repos/github/soliantconsulting/fm-odata-client/badge.svg?branch=main)](https://coveralls.io/github/soliantconsulting/fm-odata-client?branch=main)\n\nFileMaker OData client is a Typescript [OData](https://www.odata.org/) client specifically aimed at the\n[FileMaker OData API](https://help.claris.com/en/odata-guide/). It supports both FileMaker Server and FileMaker Cloud. \n\n## Installation\n\n- Install the npm package:\n\n    `npm install fm-odata-client`\n  \n- If you use FileMaker Cloud, you also need to install the following package:\n\n    `npm install amazon-cognito-identity-js`\n\n## Quick Start\n\nTo get started, you need to create a connection instance. Depending on the FileMaker host type, you have two different\noptions:\n\n- FileMaker Server\n    ```typescript\n    import {BasicAuth, Connection} from 'fm-odata-client';\n  \n    const connection = new Connection('example.com', new BasicAuth('username', 'password'));\n    ````\n  \n- FileMaker Cloud\n    ```typescript\n    import {Connection} from 'fm-odata-client';\n    import ClarisId from 'fm-odata-client/claris-id';\n  \n    const connection = new Connection('example.com', new ClarisId('username', 'password'));\n    ````  \n\nThis will give you a connection instance which allows you to issue queries against the OData API.\n\n### Note about FileMaker related OData issues\n\nAt the time of writing, the FileMaker OData API suffers an issue where it incorrectly includes unescaped newline\ncharacters in JSON responses. If you are using a version affected by this issue, you can pass an options object as the \nthird parameter of the `Connection` constructor with `laxParsing` set to `true` to enable lax parsing which will work\naround this issue.\n\n### Listing all databases\n\nYou can retrieve a list of all databases available on the server:\n\n```typescript\nconst databases = await connection.listDatabases();\nconsole.log('All databases on the host: ', databases);\n```\n\n### Working with a database\n\nIn order to do actual work on a database, you need to create a database instance:\n\n```typescript\nconst database = connection.database('example');\n```\n\n#### Listing all tables of a database\n\nYou can retrieve a simple list of all tables in a database, which will include their name and OData URL: \n\n```typescript\nconst tables = await database.listTables();\nconsole.log('All tables in the database: ', tables);\n```\n\n#### Retrieving table metadata\n\nTo get not only table names, but also field declarations and relationships, you must retrieve all metadata:\n\n```typescript\nconst metadata = await database.getMetadata();\nconsole.log('Database metadata: ', metadata);\n```\n\n### Working with table data\n\nTo actually query and modify table data, you need to create a table instance:\n\n```typescript\nconst userTable = database.table('users');\n```\n\n#### Creating records\n\nYou can create new records through the `create()` method. The method takes an object mapping field names to their\nvalue. The value can either be a string, a number (for numeric fields), or a buffer (for container fields):\n\n```typescript\nconst newUser = await userTable.create({\n    id: 1,\n    username: 'loki',\n});\n\nconsole.log('New user: ', newUser);\n```\n\nAdditionally, when addressing a specific repetition of a field, the value can be an object with a `repetition` and\n`value` property:\n\n```typescript\nconst newUser = await userTable.create({\n    name: {repetition: 2, value: 'odin'},\n});\n\nconsole.log('New user: ', newUser);\n```\n\n#### Updating records\n\nIn the same manner you can update existing records:\n\n```typescript\nconst updatedUser = await userTable.update(1, {username: 'thor'});\n\nconsole.log('Updated user: ', updatedUser);\n```\n\n> **__NOTE:__** A primary key is usually a string or a number. If a table has multiple primary keys, you must pass an\n> object mapping all primary keys to their value.\n\nIf you need to update a bunch of records with the same values, you can also issue an update based on\n[filters](#filters):\n\n```typescript\nawait userTable.updateMany(\"startswith(username, 'a')\", {name: 'a person'});\n```\n\n#### Deleting records\n\nYou can also delete records either by their primary key or with [filters](#filters):\n\n```typescript\n// Via primary key:\nawait userTable.delete(1);\n\n// Via filter:\nawait userTable.deleteMany(\"username eq 'loki'\");\n```\n\n#### Uploading binary data\n\nInstead of passing binary data in a \"create\" or \"update\" requests, you can also upload binary data separately:\n\n```typescript\nawait userTable.uploadBinary('users', 'photo', dataBuffer);\n``` \n\nIt is important to note that the OData API limits the types of data you can upload. At the time of writing, these are\nPEG, GIF, PNG, TIFF and PDF.\n\n#### Counting records in a table\n\nYou can retrieve a count of all records in a table or just a filtered subset:\n\n```typescript\nconst totalRecords = await userTable.count();\nconst filteredRecords = await userTable.count(\"startswith(username, 'a')\");\n\nconsole.log('Total records: ', totalRecords);\nconsole.log('Filtered records: ', totalRecords);\n```\n\n#### Retrieving a single record\n\nTo retrieve an individual record from a table, you can fetch it by its primary key. The result will contain all fields\nexcept container fields. To retrieve those, see the following section.\n\n```typescript\nconst user = await userTable.find(1);\nconsole.log('User: ', user);\n```\n\n#### Retrieving individual fields\n\nSince container fields are never returned in queries, you have to retrieve them individually when needed:\n\n```typescript\nconst photo = await userTable.fetchField(1, 'photo');\n\nconsole.log('Mime-type: ', photo.type);\nconsole.log('Buffer: ', photo.buffer);\n``` \n\n#### Retrieving multiple records\n\nYou can retrieve multiple records while specifying [filters](#filters) and other query parameters:\n\n```typescript\nconst users = await userTable.query({\n    filter: \"startswith(username, 'a')\",\n    top: 5,\n});\n\nconsole.log('Top 5 users: ', users);\n```\n\nYou can also request a total count of all records matching your filter while retrieving a limited record set:\n\n```typescript\nconst {count, rows: users} = await userTable.query({\n    filter: \"startswith(username, 'a')\",\n    top: 5,\n});\n\nconsole.log('Number of users: ', count);\nconsole.log('Top 5 users: ', users);\n```\n\n##### Limiting the result set\n\nRecord sets can be paginated with the `top` and `skip` properties. The `skip` property defines the offset, while the\n`top` property defines the limit.\n\n##### Changing the order of the result set\n\nTo change the order of the result set, you can pass in an `orderBy` property, which can be one of the following:\n\n- a string which is the name of the field to order by (optionally add `asc` or `desc`)\n- an object with a `field` and optionally `direction` property\n- an array of one of the other values\n\n##### Selecting a subset of fields\n\nWhen retrieving large number of records, it might make sense to only retrieve the fields you are actually interested\nin. You can specify those fields with the `select` property:\n\n```typescript\nconst sparseUsers = await userTable.query({\n    select: ['username'],\n});\n\nconsole.log('Sparse users: ', users);\n```\n\n##### Retrieving the first record of a result set\n\nIf you expect your query to only return a single record, you can also use the `fetchOne()` method, which takes the same\nparameters except `count` and `top`:\n\n```typescript\nconst user = await userTable.fetchOne({filter: \"username eq 'loki'\"});\nconsole.log('User: ', user);\n```\n\n##### Retrieving related records\n\nWhen your tables have relationships to other tables, you can directly retrieve related records:\n\n```typescript\nconst articles = await userTable.query({\n    relatedTable: {primaryKey: 1, table: 'articles'},\n});\n\nconsole.log('Articles by user with ID 1: ', articles);\n```\n\nThe `relatedTable` property can either be an object, as shown above, to retrieve all related record of a single record,\nor it can just be a string. In the latter case, you'll retrieve all related records to all records, unless limited by\na filter.\n\nTo retrieve data from deeper relations, you can pass an array of table names instead of a single table:\n\n```typescript\nconst allRelatedComments = await userTable.query({\n    relatedTable: ['articles', 'comments'],\n});\n\nconsole.log('Comments: ', allRelatedComments);\n```\n\n> **__NOTE:__** The table names are actually relationship names defined in FileMaker, not the actual table names.\n\nWhen specifying filters for the relations, you can address them in the filter by prepending the field name with the\ntable name followed by a slash (`/`).\n\n##### Cross-joining tables\n\nSometimes you want to collect field data from multiple tables. This can be achieved with a cross-join. A cross-join\ncombines the results of all records from one table with those of another table (or multiple other tables). When\nmaking a cross-join, you have to manually match the identities with a filter.\n\nThe tables to join can either be a string or an array of strings if you need to cross-join multiple tables.\n\nIt is also to note that, by default, the OData API only returns navigation links to each record. To actually get values\nback from each table, you need to expand them with the `select` property. When selecting two fields with the same name\nfrom different tables, those will be prepended with the table name in the result set:\n\n```typescript\nconst result = await userTable.crossJoin(['articles', 'comments'], {\n    filter: 'articles/userId eq user/id and comments/articleId eq article/id',\n    select: {\n        users: ['username'],\n        comments: ['content'],\n    },\n});\n\nconsole.log('Cross-join result: ', result);\n```\n\nThe `crossJoin()` method supports all other properties from the `query()` method except `relatedTable`. \n\n#### Filters\n\nFilters in OData are simple string expressions. If you just want to write them yourself, you can find more information\nabout the syntax in the [OData docs](http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part1-protocol/odata-v4.0-errata03-os-part1-protocol-complete.html#_The_$filter_System).\n\nAlternatively, you can use a filter builder like [odata-filter-builder](https://www.npmjs.com/package/odata-filter-builder)\nto programmatically create queries. \n\nPlease note the following FileMaker specifics:\n\n- The following built-in functions are not supported:\n    - `indexof()`\n    - `isof()`\n    - `geo.distance()`\n    - `geo.length()`\n    - `geo.intersects()`\n- Date, time, and timestamp formats conform to ISO 8601. Time zone offsets are relative to the time zone of the server.\n- Enclose field names that include special characters, such as spaces or underscores, in double-quotation marks.\n\n### Run FileMaker scripts\n\nThe OData API allows executing FileMaker scripts (without table context). The script parameter can be omitted, but if\nprovided must be a number, string, or a JSON serializable object. The return value will always be a string and must\nbe interpreted manually:\n\n```typescript\nconst scriptResult = await database.runScript('createUser', 'example-user');\n\nif (scriptResult.code !== 0) {\n    throw new Error('Script returned with an error');\n}\n\nconsole.log('Script result: ', scriptResult.resultParameter);\n```\n\n### Modifying the database schema\n\nYou can create and modify tables through the built-in schema manager:\n\n```typescript\nconst schemaManager = database.schemaManager();\n``` \n\nYou can then create tables with the `createTable()` method:\n\n```typescript\nawait schemaManager.createTable('users', [\n    {name: 'id', type: 'numeric', primary: true},\n    {name: 'username', type: 'string'},\n]);\n```\n\nEach field must at least specify a name and a type. The type can be one of the following values:\n\n- `string`\n- `numeric`\n- `date`\n- `time`\n- `timestamp`\n- `container`\n\nAll types support the following generic properties:\n\n- `nullable` - Whether the field accepts null values \n- `primary` - Whether the field is a primary key\n- `unique` - Whether the field must be unique\n- `global` - Whether the field is global or local\n- `repetitions` - If defined, specifies the number of allowed repetitions\n\nSome field types allow for additional properties:\n\n- `string`:\n    - `maxLength` - Maximum length of values\n    - `default` - Can be set to `CURRENT_USER` to default to the current user's name\n- `date`:\n    - `default` - Can be set to `CURRENT_DATE` to default to the current date\n- `time`:\n    - `default` - Can be set to `CURRENT_TIME` to default to the current time\n- `timestamp`:\n    - `default` - Can be set to `CURRENT_TIMESTAMP` to default to the current timestamp\n- `container`:\n    - `externalSecurePath` - Secure path to externally access the contents\n\nSimilarly, you can add fields to an existing table:\n\n```typescript\nawait schemaManager.addFields('users', [\n    {name: 'realname', type: 'string'},\n]);\n```\n\nIndexes for fields can also be added after the fact:\n\n```typescript\nawait schemaManager.createIndex('users', 'username');\n```\n\nDeleting tables, fields or indexes is just as easy:\n\n```typescript\n// Delete an index\nawait schemaManager.deleteIndex('users', 'username');\n\n// Delete a field\nawait schemaManager.deleteField('users', 'realname');\n\n// Delete an entire table\nawait schemaManager.deleteTable('users');\n```\n\n> **__NOTE:__** At the time of writing, FileMaker OData API does not allow modifying relationships. \n\n### Batching requests\n\nThe FileMaker OData API allows batching CRUD requests on tables. This will queue up all requests in a single HTTP\nrequest and be executed in an atomic operation. This means that when one of the requests fail, the entire batch will\nbe rolled back.\n\nBatched requests do have limitations compared to standard requests:\n\n- They cannot execute schema modifications or retrieve metadata.\n- Create and update calls return no response.\n\nIn order to create a batch request, it has to be initialized through the database instance. It is important to note\nthat even though the CRUD methods still return promises, these must not be awaited, as they won't be fulfilled until\nafter the batch has executed.\n\nFollowing is a simple example of inserting multiple rows into a table. All operations will be executed in their call\norder:\n\n```typescript\nawait database.batch(database => {\n    const userTable = database.table('user');\n    userTable.create({/* … */});\n    userTable.create({/* … */});\n});\n```\n\nIt should be noted that the table instance within the batch must be created from the passed in batched database, and\nnot from the outer database instance.\n\nYou might want to include query requests in your batch operation. In order to access the results outside of the batch\noperation, you need to return their promises in an array:\n\n```typescript\nconst [userOne, userTwo] = await database.batch(database => {\n    const userTable = database.table('user');\n    return [\n        userTable.fetchById(1),\n        userTable.fetchById(2),\n    ];\n});\n```\n","readmeFilename":"README.md","_rev":"1-9becc557ba65bcdc14a6d956c806fec8"}