Easy FM v5 contains several re-designs that are intended to bring power to your workflow.
Particularly this includes:
FMHost with Database.create() and DataApiProvider.database.connect() only when eager connection validation is required.Field<T> declarations with concrete field classes.Date field values with Temporal values.FMHost, HostBase, raw database endpoints, fetch(), and fetchJSON() have been
removed. Create a database synchronously with a provider:
// v4
import FMHost from '@jd-data-limited/easy-fm';
const host = new FMHost('https://example.com');
const database = host.database({
database: 'Contacts',
credentials: {
method: 'filemaker',
username: 'api-user',
password: 'secret',
},
externalSources: [],
});
// v5
import { Database, DataApiProvider } from '@jd-data-limited/easy-fm';
const database = Database.create({
provider: new DataApiProvider({
hostname: 'https://example.com',
database: 'Contacts',
credentials: {
method: 'filemaker',
username: 'api-user',
password: 'secret',
},
externalSources: [],
}),
});
Database.create() is synchronous and does not perform network work. Provider connection
and session creation happen lazily on the first operation.
To validate configuration eagerly:
await database.connect();
login() remains an alias for connect(). Normal application code does not need either
call before its first operation.
Database operations are now transport-neutral semantic commands. Layouts and records no
longer construct HTTP paths or RequestInit values.
The built-in DataApiProvider translates these commands to FileMaker Data API HTTP.
Custom integrations implement:
DatabaseProviderProviderConnectionProviderSessionSet ProviderConnection.maxSessions to 1 for a single-session provider or a larger
value for bounded pooling. Core owns session allocation, queuing, reuse, retry policy, and
shutdown.
See provider-api.md for the complete custom-provider contract.
method: "filemaker" opens sessions on demand. Core runs one operation at a time on each
session and opens up to sessionPoolSize sessions. Default is 8.
const provider = new DataApiProvider({
hostname: 'https://example.com',
database: 'Contacts',
credentials: {
method: 'filemaker',
username: 'api-user',
password: 'secret',
sessionPoolSize: 4,
},
externalSources: [],
});
OAuth, Claris, and supplied-token credentials use one serialized session.
When a provider throws ProviderSessionExpiredError, EasyFM retries these read-only
operations once on a replacement session:
EasyFM never automatically replays scripts, creates, updates, duplicates, deletes, or uploads. This avoids repeating side effects.
try {
// Use database.
} finally {
await database.close();
}
close() rejects queued work, aborts active work, closes every session, closes provider
connection, and prevents future operations. logout() remains an alias for close().
FileMaker container references are tied to the session that returned them. V5 stores that session binding with each record and always routes a container download through the same provider session. It never substitutes another pooled session.
Provider method fetchContainer() must return a Web Response. Its underlying transport
may be HTTP or custom. Provider remains responsible for authorization, cookie jars,
redirects, and FileMaker Data API cookie behavior.
const response = await record.fields.photo.webStream();
const contentType = response.headers.get('Content-Type');
const body = response.body;
Download methods accept:
interface ContainerDownloadOptions {
signal?: AbortSignal;
refreshOnSessionLoss?: boolean;
}
If original session is unavailable, download throws ContainerSessionAffinityError. Opt
into one record refetch and one retry when suitable:
const result = await record.fields.photo.arrayBuffer({
signal: controller.signal,
refreshOnSessionLoss: true,
});
stream() remains deprecated and returns a Node.js Readable plus MIME type.
webStream() returns Response; arrayBuffer() returns buffered data plus MIME type.
V4 used generic Field<T>. V5 uses concrete classes:
| v4 declaration | v5 declaration | v5 .value type |
|---|---|---|
Field<string> |
TextField |
string |
Field<number> |
NumberField |
number | null |
Field<Moment> timestamp |
TimeStampField |
Temporal.PlainDateTime | null |
Field<Moment> time |
TimeField |
Temporal.PlainTime | null |
Field<Moment> date |
DateField |
Temporal.PlainDate | null |
Field<Container> |
ContainerField |
container reference string |
Example:
import type {
ContainerField,
DateField,
LayoutInterface,
NumberField,
TextField,
TimeField,
TimeStampField,
} from '@jd-data-limited/easy-fm';
interface ContactsLayout extends LayoutInterface {
fields: {
name: TextField;
balance: NumberField;
birthDate: DateField;
preferredTime: TimeField;
updatedAt: TimeStampField;
photo: ContainerField;
};
portals: {};
}
const contacts = database.layout<ContactsLayout>('Contacts_API');
BaseField is shared abstract base. Field and ValueField are unions, not replacements
for old generic declaration. Calculation and summary fields reject modification before a
request is sent.
FileMaker values do not include time zones. V5 maps them to:
Temporal.PlainDateTemporal.PlainTimeTemporal.PlainDateTimenullimport { Temporal } from 'temporal-polyfill';
record.fields.birthDate.value = Temporal.PlainDate.from('2026-09-23');
record.fields.preferredTime.value = Temporal.PlainTime.from('14:30:00');
record.fields.updatedAt.value = Temporal.PlainDateTime.from('2026-09-23T14:30:00');
No automatic timezone conversion occurs. Apply a zone explicitly when a timestamp represents an instant:
const timestamp = record.fields.updatedAt.value;
if (timestamp !== null) {
const instant = timestamp
.toZonedDateTime('Pacific/Auckland', { disambiguation: 'reject' })
.toInstant();
}
asDate, asTime, and asTimestamp remain deprecated compatibility helpers. New code
should construct Temporal values directly.
External sources remain available only with filemaker credentials:
const database = Database.create({
provider: new DataApiProvider({
hostname: 'https://example.com',
database: 'Main.fmp12',
credentials: {
method: 'filemaker',
username: 'main-user',
password: 'secret',
},
externalSources: [
{
database: 'Related.fmp12',
credentials: {
method: 'filemaker',
username: 'related-user',
password: 'secret',
},
},
],
}),
});
For OAuth, Claris, and token credentials, use an empty externalSources array.