FileMaker OData client is a Typescript OData client specifically aimed at the FileMaker OData API. It supports both FileMaker Server and FileMaker Cloud.
-
Install the npm package:
npm install fm-odata-client
-
If you use FileMaker Cloud, you also need to install the following package:
npm install amazon-cognito-identity-js
To get started, you need to create a connection instance. Depending on the FileMaker host type, you have two different options:
-
FileMaker Server
import {BasicAuth, Connection} from 'fm-odata-client'; const connection = new Connection('example.com', new BasicAuth('username', 'password'));
-
FileMaker Cloud
import {Connection} from 'fm-odata-client'; import ClarisId from 'fm-odata-client/claris-id'; const connection = new Connection('example.com', new ClarisId('username', 'password'));
This will give you a connection instance which allows you to issue queries against the OData API.
At the time of writing, the FileMaker OData API suffers an issue where it incorrectly includes unescaped newline
characters in JSON responses. If you are using a version affected by this issue, you can pass an options object as the
third parameter of the Connection
constructor with laxParsing
set to true
to enable lax parsing which will work
around this issue.
You can retrieve a list of all databases available on the server:
const databases = await connection.listDatabases();
console.log('All databases on the host: ', databases);
In order to do actual work on a database, you need to create a database instance:
const database = connection.database('example');
You can retrieve a simple list of all tables in a database, which will include their name and OData URL:
const tables = await database.listTables();
console.log('All tables in the database: ', tables);
To get not only table names, but also field declarations and relationships, you must retrieve all metadata:
const metadata = await database.getMetadata();
console.log('Database metadata: ', metadata);
To actually query and modify table data, you need to create a table instance:
const userTable = database.table('users');
You can create new records through the create()
method. The method takes an object mapping field names to their
value. The value can either be a string, a number (for numeric fields), or a buffer (for container fields):
const newUser = await userTable.create({
id: 1,
username: 'loki',
});
console.log('New user: ', newUser);
Additionally, when addressing a specific repetition of a field, the value can be an object with a repetition
and
value
property:
const newUser = await userTable.create({
name: {repetition: 2, value: 'odin'},
});
console.log('New user: ', newUser);
In the same manner you can update existing records:
const updatedUser = await userTable.update(1, {username: 'thor'});
console.log('Updated user: ', updatedUser);
NOTE: A primary key is usually a string or a number. If a table has multiple primary keys, you must pass an object mapping all primary keys to their value.
If you need to update a bunch of records with the same values, you can also issue an update based on filters:
await userTable.updateMany("startswith(username, 'a')", {name: 'a person'});
You can also delete records either by their primary key or with filters:
// Via primary key:
await userTable.delete(1);
// Via filter:
await userTable.deleteMany("username eq 'loki'");
Instead of passing binary data in a "create" or "update" requests, you can also upload binary data separately:
await userTable.uploadBinary('users', 'photo', dataBuffer);
It is important to note that the OData API limits the types of data you can upload. At the time of writing, these are PEG, GIF, PNG, TIFF and PDF.
You can retrieve a count of all records in a table or just a filtered subset:
const totalRecords = await userTable.count();
const filteredRecords = await userTable.count("startswith(username, 'a')");
console.log('Total records: ', totalRecords);
console.log('Filtered records: ', totalRecords);
To retrieve an individual record from a table, you can fetch it by its primary key. The result will contain all fields except container fields. To retrieve those, see the following section.
const user = await userTable.find(1);
console.log('User: ', user);
Since container fields are never returned in queries, you have to retrieve them individually when needed:
const photo = await userTable.fetchField(1, 'photo');
console.log('Mime-type: ', photo.type);
console.log('Buffer: ', photo.buffer);
You can retrieve multiple records while specifying filters and other query parameters:
const users = await userTable.query({
filter: "startswith(username, 'a')",
top: 5,
});
console.log('Top 5 users: ', users);
You can also request a total count of all records matching your filter while retrieving a limited record set:
const {count, rows: users} = await userTable.query({
filter: "startswith(username, 'a')",
top: 5,
});
console.log('Number of users: ', count);
console.log('Top 5 users: ', users);
Record sets can be paginated with the top
and skip
properties. The skip
property defines the offset, while the
top
property defines the limit.
To change the order of the result set, you can pass in an orderBy
property, which can be one of the following:
- a string which is the name of the field to order by (optionally add
asc
ordesc
) - an object with a
field
and optionallydirection
property - an array of one of the other values
When retrieving large number of records, it might make sense to only retrieve the fields you are actually interested
in. You can specify those fields with the select
property:
const sparseUsers = await userTable.query({
select: ['username'],
});
console.log('Sparse users: ', users);
If you expect your query to only return a single record, you can also use the fetchOne()
method, which takes the same
parameters except count
and top
:
const user = await userTable.fetchOne({filter: "username eq 'loki'"});
console.log('User: ', user);
When your tables have relationships to other tables, you can directly retrieve related records:
const articles = await userTable.query({
relatedTable: {primaryKey: 1, table: 'articles'},
});
console.log('Articles by user with ID 1: ', articles);
The relatedTable
property can either be an object, as shown above, to retrieve all related record of a single record,
or it can just be a string. In the latter case, you'll retrieve all related records to all records, unless limited by
a filter.
To retrieve data from deeper relations, you can pass an array of table names instead of a single table:
const allRelatedComments = await userTable.query({
relatedTable: ['articles', 'comments'],
});
console.log('Comments: ', allRelatedComments);
NOTE: The table names are actually relationship names defined in FileMaker, not the actual table names.
When specifying filters for the relations, you can address them in the filter by prepending the field name with the
table name followed by a slash (/
).
Sometimes you want to collect field data from multiple tables. This can be achieved with a cross-join. A cross-join combines the results of all records from one table with those of another table (or multiple other tables). When making a cross-join, you have to manually match the identities with a filter.
The tables to join can either be a string or an array of strings if you need to cross-join multiple tables.
It is also to note that, by default, the OData API only returns navigation links to each record. To actually get values
back from each table, you need to expand them with the select
property. When selecting two fields with the same name
from different tables, those will be prepended with the table name in the result set:
const result = await userTable.crossJoin(['articles', 'comments'], {
filter: 'articles/userId eq user/id and comments/articleId eq article/id',
select: {
users: ['username'],
comments: ['content'],
},
});
console.log('Cross-join result: ', result);
The crossJoin()
method supports all other properties from the query()
method except relatedTable
.
Filters in OData are simple string expressions. If you just want to write them yourself, you can find more information about the syntax in the OData docs.
Alternatively, you can use a filter builder like odata-filter-builder to programmatically create queries.
Please note the following FileMaker specifics:
- The following built-in functions are not supported:
indexof()
isof()
geo.distance()
geo.length()
geo.intersects()
- Date, time, and timestamp formats conform to ISO 8601. Time zone offsets are relative to the time zone of the server.
- Enclose field names that include special characters, such as spaces or underscores, in double-quotation marks.
The OData API allows executing FileMaker scripts (without table context). The script parameter can be omitted, but if provided must be a number, string, or a JSON serializable object. The return value will always be a string and must be interpreted manually:
const scriptResult = await database.runScript('createUser', 'example-user');
if (scriptResult.code !== 0) {
throw new Error('Script returned with an error');
}
console.log('Script result: ', scriptResult.resultParameter);
You can create and modify tables through the built-in schema manager:
const schemaManager = database.schemaManager();
You can then create tables with the createTable()
method:
await schemaManager.createTable('users', [
{name: 'id', type: 'numeric', primary: true},
{name: 'username', type: 'string'},
]);
Each field must at least specify a name and a type. The type can be one of the following values:
string
numeric
date
time
timestamp
container
All types support the following generic properties:
nullable
- Whether the field accepts null valuesprimary
- Whether the field is a primary keyunique
- Whether the field must be uniqueglobal
- Whether the field is global or localrepetitions
- If defined, specifies the number of allowed repetitions
Some field types allow for additional properties:
string
:maxLength
- Maximum length of valuesdefault
- Can be set toCURRENT_USER
to default to the current user's name
date
:default
- Can be set toCURRENT_DATE
to default to the current date
time
:default
- Can be set toCURRENT_TIME
to default to the current time
timestamp
:default
- Can be set toCURRENT_TIMESTAMP
to default to the current timestamp
container
:externalSecurePath
- Secure path to externally access the contents
Similarly, you can add fields to an existing table:
await schemaManager.addFields('users', [
{name: 'realname', type: 'string'},
]);
Indexes for fields can also be added after the fact:
await schemaManager.createIndex('users', 'username');
Deleting tables, fields or indexes is just as easy:
// Delete an index
await schemaManager.deleteIndex('users', 'username');
// Delete a field
await schemaManager.deleteField('users', 'realname');
// Delete an entire table
await schemaManager.deleteTable('users');
NOTE: At the time of writing, FileMaker OData API does not allow modifying relationships.
The FileMaker OData API allows batching CRUD requests on tables. This will queue up all requests in a single HTTP request and be executed in an atomic operation. This means that when one of the requests fail, the entire batch will be rolled back.
Batched requests do have limitations compared to standard requests:
- They cannot execute schema modifications or retrieve metadata.
- Create and update calls return no response.
In order to create a batch request, it has to be initialized through the database instance. It is important to note that even though the CRUD methods still return promises, these must not be awaited, as they won't be fulfilled until after the batch has executed.
Following is a simple example of inserting multiple rows into a table. All operations will be executed in their call order:
await database.batch(database => {
const userTable = database.table('user');
userTable.create({/* … */});
userTable.create({/* … */});
});
It should be noted that the table instance within the batch must be created from the passed in batched database, and not from the outer database instance.
You might want to include query requests in your batch operation. In order to access the results outside of the batch operation, you need to return their promises in an array:
const [userOne, userTwo] = await database.batch(database => {
const userTable = database.table('user');
return [
userTable.fetchById(1),
userTable.fetchById(2),
];
});