fms-odata-js 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +160 -0
- package/LICENSE +22 -0
- package/README.md +381 -0
- package/dist/batch.d.ts +154 -0
- package/dist/batch.d.ts.map +1 -0
- package/dist/client.d.ts +160 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/containers.d.ts +190 -0
- package/dist/containers.d.ts.map +1 -0
- package/dist/entity.d.ts +149 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/fms-odata.esm.js +2201 -0
- package/dist/fms-odata.esm.min.js +6 -0
- package/dist/fms-odata.iife.min.js +6 -0
- package/dist/http.d.ts +51 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/metadata.d.ts +85 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/query.d.ts +198 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/scripts.d.ts +114 -0
- package/dist/scripts.d.ts.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/url.d.ts +49 -0
- package/dist/url.d.ts.map +1 -0
- package/package.json +61 -0
- package/src/batch.ts +477 -0
- package/src/client.ts +268 -0
- package/src/containers.ts +457 -0
- package/src/entity.ts +334 -0
- package/src/errors.ts +134 -0
- package/src/http.ts +190 -0
- package/src/index.ts +81 -0
- package/src/metadata.ts +354 -0
- package/src/query.ts +442 -0
- package/src/scripts.ts +279 -0
- package/src/types.ts +30 -0
- package/src/url.ts +98 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.3.0] - 2026-06-23
|
|
11
|
+
|
|
12
|
+
### Changed — Repository and package rename (`fm-odata-js` → `fms-odata-js`)
|
|
13
|
+
|
|
14
|
+
The GitHub repository and npm package have been renamed from `fm-odata-js` to `fms-odata-js` to align with the `fms-odata-spec` reference specification and the `@fms-odata/spec-ts` shared types package.
|
|
15
|
+
|
|
16
|
+
- **Package name:** `fm-odata-js` → `fms-odata-js` on npm. The old package is deprecated and points users to the new name.
|
|
17
|
+
- **Dist bundle filenames:** `fm-odata.esm.js` → `fms-odata.esm.js`, `fm-odata.esm.min.js` → `fms-odata.esm.min.js`, `fm-odata.iife.min.js` → `fms-odata.iife.min.js`.
|
|
18
|
+
- **IIFE global:** `FMODataLib` → `FMSODataLib` (window global for Web Viewer / browser bundles).
|
|
19
|
+
- **Spec dependency:** `@fm-odata/spec-ts` → `@fms-odata/spec-ts` (the spec package was renamed on npm).
|
|
20
|
+
- **Public API class/type identifiers renamed:**
|
|
21
|
+
- `FMOData` → `FMSOData` (entrypoint class)
|
|
22
|
+
- `FMODataError` → `FMSODataError` (error class)
|
|
23
|
+
- `FMODataOptions` → `FMSODataOptions` (constructor options type)
|
|
24
|
+
- `isFMODataError` → `isFMSODataError` (type guard)
|
|
25
|
+
- **Deprecated aliases.** The old names (`FMOData`, `FMODataError`, `FMODataOptions`, `isFMODataError`) are re-exported as deprecated aliases for backward compatibility. Existing code using named imports keeps working. These aliases will be removed in a future major version.
|
|
26
|
+
|
|
27
|
+
### Migration
|
|
28
|
+
|
|
29
|
+
- **npm:** `npm install fms-odata-js` (or update `package.json` dependencies). The old `fm-odata-js` package is deprecated.
|
|
30
|
+
- **Imports:** `import { FMSOData } from 'fms-odata-js'` (new) or `import { FMOData } from 'fms-odata-js'` (deprecated alias, still works).
|
|
31
|
+
- **CDN URLs:** Update `https://cdn.jsdelivr.net/gh/fsans/fm-odata-js@…` to `https://cdn.jsdelivr.net/gh/fsans/fms-odata-js@…` and dist filenames from `fm-odata.esm.min.js` to `fms-odata.esm.min.js`.
|
|
32
|
+
- **IIFE global:** Update `window.FMODataLib` references to `window.FMSODataLib`.
|
|
33
|
+
|
|
34
|
+
## [0.2.0] - 2026-06-22
|
|
35
|
+
|
|
36
|
+
### Added — Spec alignment with @fm-odata/spec-ts
|
|
37
|
+
|
|
38
|
+
This release aligns fm-odata-js with the [fms-odata-spec](https://github.com/fsans/fms-odata-spec) reference specification. The library now depends on `@fm-odata/spec-ts` (as a devDependency — bundled at build time, zero runtime deps) for shared type definitions and the version feature matrix.
|
|
39
|
+
|
|
40
|
+
- **Version detection & feature gating.** `FMOData#version()` lazily fetches `$metadata`, extracts the `Org.OData.Core.V1.ProductVersion` annotation, and caches the detected FileMaker Server major version (`'19'`, `'21'`, `'22'`, `'26'`, or `'future'`). `FMOData#versionInfo()` returns the full version descriptor with feature flags. `FMOData#hasFeature(feature)` checks if the server supports a specific feature (e.g., `applyAggregation`, `scriptsByFMSID`, `webhooks`). 12 unit tests.
|
|
41
|
+
- **`$apply` aggregation support.** `Query#apply(expr)` sets a raw `$apply` expression. `Query#aggregate(expressions)` builds `aggregate(field with function as alias)`. `Query#groupBy(fields, aggregateExpressions?)` builds `groupby((fields),aggregate(...))`. Requires FMS 2024+ (v22). 7 unit tests.
|
|
42
|
+
- **FMSID-based script invocation.** `FMOData#scriptById(fmsid, opts)` invokes a script by its immutable FMSID via `Script.FMSID:<id>` URL. `ScriptInvoker#runById(fmsid, opts)` and `ScriptInvoker#urlById(fmsid)` provide the low-level API. Requires FMS 2026+ (v26). 7 unit tests.
|
|
43
|
+
- **Record references (`$ref`).** `EntityRef#getRefs(navProperty)`, `addRef(navProperty, relatedKey)`, `setRef(navProperty, relatedKey)`, and `removeRef(navProperty, relatedKey?)` provide full OData `$ref` CRUD for navigation properties. 9 unit tests.
|
|
44
|
+
- **FMID auth support.** `fmidAuth(token)` helper builds an `FMID <token>` Authorization header for FileMaker Cloud. The HTTP client now recognizes the `FMID` auth scheme prefix.
|
|
45
|
+
- **Spec type re-exports.** Version types (`FMVersionMajor`, `FMVersionInfo`, `FMFeatureFlags`, `FMQueryOptionFlags`), auth types (`FMAuthScheme`, `FMAuthToken`, `FMAuthTokenProvider`), aggregate types (`AggregateFunction`), and error types (`ODataErrorBody`) are re-exported from `@fm-odata/spec-ts`.
|
|
46
|
+
- **Error helpers.** `isFMODataError(err)` and `isFMScriptError(err)` type guards.
|
|
47
|
+
- **Standardized env var names.** The test harness and probe script now accept standardized env var names (`FM_SERVER`, `FM_DATABASE`, `FM_USER`, `FM_PASSWORD`, `FM_VERIFY_SSL`, `FM_TIMEOUT`, `FM_LIVE`) alongside the legacy `FM_ODATA_*` names. Standardized names take precedence. `.env.sample` updated with both forms documented.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **Query parameter encoding now preserves OData-special characters.** `odataEncode()` (used by `buildQueryString`, `Query#toURL()`, and batch operations) keeps commas (`,`), dollar signs (`$`), equals (`=`), and semicolons (`;`) as literal characters instead of percent-encoding them. FileMaker Server rejects `%2C`, `%24`, `%3D`, and `%3B` in `$select`, `$orderby`, `$expand`, and other system query options—now these parameters work correctly with multiple field names and nested expand options (e.g., `$expand=Orders($select=id,total;$top=5)`). (Ported from FMS-ODATA-MCP fix.)
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- M6 — OData `$batch` multipart request/response:
|
|
56
|
+
- `FMOData#batch()` returns a `Batch` builder for composing multi-operation requests in a single HTTP round-trip.
|
|
57
|
+
- `Batch#add(op)` queues a read operation (GET entity-set with optional `$top`, `$skip`, `$filter`, `$select`). Returns a `BatchHandle<T>`.
|
|
58
|
+
- `Batch#changeset(build)` defines an atomic group of write operations (POST / PATCH / DELETE). All operations in a changeset succeed or fail together.
|
|
59
|
+
- `Changeset#create(entitySet, body)`, `Changeset#patch(entitySet, key, body, opts?)`, `Changeset#delete(entitySet, key, opts?)` add write operations. Each accepts an optional `If-Match` header.
|
|
60
|
+
- `Batch#send(opts?)` serialises the multipart/mixed body, POSTs to `/<db>/$batch`, and parses the multipart response back into per-operation `BatchOpResult` objects.
|
|
61
|
+
- `BatchResult` aggregates all responses in request order with an `ok` boolean (true when all statuses < 400).
|
|
62
|
+
- `BatchHandle<T>` carries a `_promise` that resolves/rejects individually when the batch settles.
|
|
63
|
+
- Exported types: `Batch`, `Changeset`, `BatchHandle`, `BatchReadOp`, `BatchOpResult`, `BatchResult`.
|
|
64
|
+
- OData `$`-prefixed query parameters (`$top`, `$filter`, etc.) are serialised without percent-encoding the `$` sign, which is required by FMS.
|
|
65
|
+
- Multipart MIME parser correctly strips outer `application/http` MIME headers before extracting the inner HTTP status line and body.
|
|
66
|
+
- 14 unit tests covering serialisation (read, changeset, PATCH/DELETE, string-key escaping), response parsing (single/multi, error, mixed read+changeset), header forwarding, `$batch` URL, and `AbortSignal`.
|
|
67
|
+
- Live integration test: sends a batch with a read and a single-create changeset, asserts both responses are successful, and cleans up any created rows.
|
|
68
|
+
|
|
69
|
+
- M5 — OData `$metadata` parser:
|
|
70
|
+
- `FMOData#metadata(opts?)` fetches and parses the CSDL XML into a typed `ODataMetadata` object.
|
|
71
|
+
- `FMOData#metadataXml(opts?)` returns the raw XML (escape hatch for debugging).
|
|
72
|
+
- New exported types: `ODataMetadata`, `EdmEntityType`, `EdmEntitySet`, `EdmProperty`, `EdmAction`.
|
|
73
|
+
- Metadata results are cached by default; pass `refresh: true` to force a refetch.
|
|
74
|
+
- Lightweight ~200 LoC XML parser with zero external dependencies (bundle budget ~1 KB gzipped).
|
|
75
|
+
- 15 unit tests covering entity types, keys, properties, navigation properties, entity sets, and actions.
|
|
76
|
+
- Live integration test fetches metadata from the configured FMS instance.
|
|
77
|
+
|
|
78
|
+
## [0.1.5] - 2026-04-29
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
|
|
82
|
+
- M4 Part 2 — FileMaker container field I/O:
|
|
83
|
+
- `EntityRef#container(fieldName) → ContainerRef` handle for per-field binary operations.
|
|
84
|
+
- `ContainerRef.get()` / `getStream()` download via `GET …/<field>/$value` as a `Blob` or `ReadableStream<Uint8Array>` (with parsed `Content-Type`, `size`, and RFC 6266 `filename`).
|
|
85
|
+
- `ContainerRef.upload(input)` supports two FMS-documented wire formats, selectable via `input.encoding`:
|
|
86
|
+
- `'binary'` (default): `PATCH …/<field>` with raw bytes plus `Content-Type` and (optionally) `Content-Disposition: inline; filename="…"`. Restricted to PNG, JPEG, GIF, TIFF, and PDF.
|
|
87
|
+
- `'base64'`: `PATCH …/<EntitySet>(<key>)` with `{ "<field>": "<base64>", "<field>@com.filemaker.odata.ContentType": "…", "<field>@com.filemaker.odata.Filename": "…" }`. Useful for updating multiple containers (or container + regular fields) in one round-trip.
|
|
88
|
+
- `ContainerRef.delete()` clears the value via `PATCH …/<EntitySet>(<key>)` with `{ "<field>": null }` (no per-field DELETE endpoint exists on FMS).
|
|
89
|
+
- `ContainerDownload` / `ContainerUploadInput` types exported (`encoding` and `contentType` are optional).
|
|
90
|
+
- `ContainerUploadInput.contentType` is now optional. When omitted, the library sniffs the MIME from the payload's magic bytes (PNG, JPEG, GIF, TIFF, PDF) and rejects unrecognised payloads up-front. New exported helper `sniffContainerMime(bytes)`.
|
|
91
|
+
- `image/tiff` added to `FM_CONTAINER_SUPPORTED_MIME_TYPES`. Round-trips correctly through the OData container endpoints.
|
|
92
|
+
- `EntityRef#fieldValue<V>(fieldName)` — `GET /<EntitySet>(<key>)/<fieldName>` and unwrap the OData `{ value: … }` envelope. Useful for fetching a single scalar column without composing a `$select` query.
|
|
93
|
+
- `keepalive: true` on every fetch for HTTP connection reuse.
|
|
94
|
+
- 35 container unit tests + 16 entity unit tests (was 23 + 13). Coverage includes TIFF acceptance, PNG/PDF magic-byte sniffing on upload, every `sniffContainerMime` branch, and `fieldValue` (basic / percent-encoded field name / null value).
|
|
95
|
+
- Live integration test: uploads a 68-byte PNG fixture (`tests/fixtures/pixel.png`) to a `photo_content` container on a throw-away contact row, reads it back, asserts byte equality, and clears. Soft-skips when the field is missing in the solution.
|
|
96
|
+
- `docs/filemaker-quirks.md`: new sections documenting (a) FMS rejects `PUT` on the OData endpoint and has no per-field DELETE for record data, (b) the `Accept: application/octet-stream` quirk on `$value`, (c) the `Untitled.png` auto-generated filename, and (d) field-tested observations on `/$count` URL FMS vs FMC, `OData-Version` enforcement, `Edm.Stream` vs `Edm.Binary`, and single-quote escaping in keys.
|
|
97
|
+
- `docs/container-download-problem.md`: investigation log for the Accept-header quirk, marked Resolved.
|
|
98
|
+
- `docs/filemaker-odata-container-guide.md`: full Claris OData container-operations reference vendored into the repo.
|
|
99
|
+
|
|
100
|
+
### Fixed
|
|
101
|
+
|
|
102
|
+
- **Container downloads now return binary, not the stored filename string.** `ContainerRef.get()` and `getStream()` now send `Accept: */*` (was `application/octet-stream`). FMS 22 returns the field's stored reference value as `text/plain;charset=utf-8` for the `octet-stream` Accept header — every other Accept value returns the actual binary with the correct `Content-Type`. See [`docs/filemaker-quirks.md`](docs/filemaker-quirks.md). The previous "filename causes file reference" hypothesis was a misdiagnosis; filenames round-trip correctly via `Content-Disposition: attachment; filename="…"`.
|
|
103
|
+
- Live integration test for scripts now soft-skips on FMS's actual "Script not found" HTTP response (a plain `FMODataError` with that message), in addition to the originally-anticipated `scriptError: "104"` envelope path.
|
|
104
|
+
|
|
105
|
+
## [0.1.4] - 2026-04-28
|
|
106
|
+
|
|
107
|
+
### Added
|
|
108
|
+
|
|
109
|
+
- M4 Part 1 — FileMaker script execution:
|
|
110
|
+
- `FMOData#script(name, opts?)`, `Query#script(name, opts?)`, `EntityRef#script(name, opts?)` POST to the FMS `Script.<name>` action endpoint at database, entity-set, or single-record scope.
|
|
111
|
+
- `ScriptOptions` (`parameter`, `signal`) maps to the FMS `{ "scriptParameter": "..." }` body when provided.
|
|
112
|
+
- `ScriptResult` envelope (`scriptResult`, `scriptError`, `raw`).
|
|
113
|
+
- `FMScriptError` (subclass of `FMODataError`) thrown when a script returns a non-zero `scriptError`; HTTP-level failures still surface as plain `FMODataError`.
|
|
114
|
+
- Public `ScriptInvoker` for advanced callers that need to build invocation paths manually.
|
|
115
|
+
- 15 new unit tests covering URL construction at all three scopes, parameter encoding, error promotion, envelope unwrapping, 401 retry, and `AbortSignal` propagation.
|
|
116
|
+
- Optional live integration test: define a `Ping` script in the Contacts demo (or set `FM_ODATA_PING_SCRIPT`) and the live suite will exercise it.
|
|
117
|
+
|
|
118
|
+
## [0.1.2] - 2026-04-22
|
|
119
|
+
|
|
120
|
+
### Added
|
|
121
|
+
|
|
122
|
+
- `examples/webviewer/`: standalone HTML demo for the `Contacts` solution, in two variants:
|
|
123
|
+
- `index.html` — loads the library from jsDelivr (`/gh/fsans/fm-odata-js@v0.1.1`).
|
|
124
|
+
- `index-inline.html` — fully inlined bundle; zero runtime network dependencies.
|
|
125
|
+
- `examples/Contacts.fmp12` referenced from the docs as a ready-to-host demo database.
|
|
126
|
+
|
|
127
|
+
### Changed
|
|
128
|
+
|
|
129
|
+
- README overhaul: centered hero, badges, feature bullets, status table, production testimonial blockquote, Docs/Contributing/License sections.
|
|
130
|
+
- `examples/consumer-node/README.md`: added a "Test database" section and sanitized placeholder credentials.
|
|
131
|
+
- `LICENSE`: corrected copyright holder name to `Francesc Sans`.
|
|
132
|
+
|
|
133
|
+
## [0.1.1] - 2026-04-22
|
|
134
|
+
|
|
135
|
+
### Added
|
|
136
|
+
|
|
137
|
+
- M1 scaffold: repo layout, TypeScript/Vitest/Playwright/esbuild configs, empty `FMOData` class, placeholder failing unit test.
|
|
138
|
+
- M2 query + URL layer:
|
|
139
|
+
- `src/url.ts`: `escapeStringLiteral`, `formatDateTime`, `parseDateTime`, `formatLiteral`, `encodePathSegment`, `buildQueryString`.
|
|
140
|
+
- `src/query.ts`: `Query` fluent builder (`select`, `filter`, `or`, `expand`, `orderby`, `top`, `skip`, `count`, `search`, `toURL`), `Filter` class, `filterFactory`.
|
|
141
|
+
- `FMOData#from(entitySet)` returns a `Query`.
|
|
142
|
+
- Public exports: `Query`, `Filter`, `filterFactory`, `FilterFactory`, `FilterInput`, `OrderDir`, `ODataLiteral`.
|
|
143
|
+
- 62 passing unit tests covering every builder path and URL edge case.
|
|
144
|
+
- Minified bundle ~4.2 KB raw / ~1.55 KB gzipped.
|
|
145
|
+
- Dev tooling:
|
|
146
|
+
- `.env.sample` / `.env` for local FMS config; `.env` is git-ignored.
|
|
147
|
+
- `scripts/env.mjs`, `scripts/insecure-fetch.mjs`, `scripts/probe.mjs`.
|
|
148
|
+
- `npm run probe` validates connectivity, auth, and the `Contacts` schema against a live FMS.
|
|
149
|
+
- `FM_ODATA_INSECURE_TLS=1` toggles `NODE_TLS_REJECT_UNAUTHORIZED=0` for dev use with self-signed certs.
|
|
150
|
+
- `docs/filemaker-quirks.md` documenting: Basic-auth-only on OData, `/$count` returning 400, self-signed cert handling.
|
|
151
|
+
- M3 CRUD + auth + errors:
|
|
152
|
+
- `src/http.ts`: shared request executor — `resolveAuthHeader` (Basic/Bearer auto-detect), `basicAuth(user, pass)` helper, `combineSignals`, `executeRequest`, `executeJson`.
|
|
153
|
+
- `src/errors.ts`: `parseErrorResponse` handling OData JSON envelope + FileMaker XML envelope.
|
|
154
|
+
- `src/entity.ts`: `EntityRef` with `.get()`, `.patch()` (with `ifMatch`, `returnRepresentation`), `.delete()`.
|
|
155
|
+
- `Query#create()` (POST), `Query#get()` returning `{ value, count?, nextLink? }`, `Query#byKey(key)` returning `EntityRef`.
|
|
156
|
+
- `FMOData#request()` / `FMOData#rawRequest()` low-level escape hatches.
|
|
157
|
+
- 401 retry via `onUnauthorized` (once), `AbortSignal` + `timeoutMs` composition.
|
|
158
|
+
- 100 passing unit tests (mocked fetch); 3 passing live-integration tests against the `Contacts` DB covering read, full CRUD round-trip, and error handling.
|
|
159
|
+
- Opt-in live suite: `FM_ODATA_LIVE=1 npm test -- tests/integration`.
|
|
160
|
+
- Bundle now ~8.8 KB raw / ~3.2 KB gzipped.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Francesc Sans, nBCN Software, Barcelona
|
|
4
|
+
http://www.ntwk.es — tecnic.at.ntwk.es
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# fms-odata-js
|
|
4
|
+
|
|
5
|
+
**A tiny, type-safe OData v4 client built for FileMaker Server.**
|
|
6
|
+
|
|
7
|
+
Zero runtime dependencies · ~9.1 KB gzipped · ESM + IIFE · Web Viewer / Browser / Node 18+
|
|
8
|
+
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
[](https://www.odata.org/)
|
|
11
|
+
[](https://www.claris.com/filemaker/)
|
|
12
|
+
[](#)
|
|
13
|
+
[](#)
|
|
14
|
+
[](#)
|
|
15
|
+
[](./LICENSE)
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Why fms-odata-js?
|
|
22
|
+
|
|
23
|
+
FileMaker Server speaks OData v4, but the spec has sharp corners and FMS has quirks. `fms-odata-js` smooths both — so you can forget about URL-encoding `$filter` predicates and focus on your data.
|
|
24
|
+
|
|
25
|
+
> **Battle-tested in production.** I've been using this library heavily to let FileMaker Web Viewer instances talk to the *same* hosted database they live in — and the performance has been genuinely impressive. Queries that used to require round-tripping through scripts and set-field loops now resolve in a single OData call, with noticeably lower latency and a much cleaner code path. If you're building rich Web Viewer UIs backed by FMS, this is the fastest route I've found.
|
|
26
|
+
|
|
27
|
+
- **Tiny.** ESM and IIFE bundles, zero runtime dependencies, ~9.1 KB gzipped.
|
|
28
|
+
- **Type-safe.** Fluent, chainable query builder with full TS inference.
|
|
29
|
+
- **Runs anywhere.** Drop it into a FileMaker Web Viewer, a browser, or Node 18+.
|
|
30
|
+
- **FMS-aware.** Handles the documented FMS OData deviations for you.
|
|
31
|
+
- **Version-aware.** Detects the FileMaker Server major version (19, 21, 22, 26) from `$metadata` and gates features accordingly.
|
|
32
|
+
- **Scripts built in.** Invoke FileMaker scripts at database, entity-set, or record scope — by name or by immutable FMSID (v26+).
|
|
33
|
+
- **Containers built in.** Upload, download, stream, or clear container fields with typed helpers — `Blob`, `ArrayBuffer`, and `Uint8Array` all accepted.
|
|
34
|
+
- **Aggregations.** `$apply` builder for `aggregate()` and `groupBy()` — server-side sum, average, min, max, count (FMS 2024+).
|
|
35
|
+
- **Navigation properties.** Full `$ref` CRUD — `getRefs`, `addRef`, `setRef`, `removeRef` for OData relationship links.
|
|
36
|
+
- **Schema introspection.** `$metadata` parsed into typed `ODataMetadata` with entity types, keys, properties, and actions. Cached by default.
|
|
37
|
+
- **Batch requests.** `$batch` builder composes multiple reads and atomic changesets (POST / PATCH / DELETE) into a single HTTP round-trip.
|
|
38
|
+
- **Multi-auth.** Basic, Bearer, and FMID (FileMaker Cloud / Claris ID) auth with 401 retry, `AbortSignal`, and timeouts built in.
|
|
39
|
+
- **Honest errors.** Every failure becomes a normalized `FMSODataError` (or `FMScriptError` for script failures) with `isFMSODataError` / `isFMScriptError` type guards.
|
|
40
|
+
|
|
41
|
+
## Status
|
|
42
|
+
|
|
43
|
+
| Milestone | Scope | State |
|
|
44
|
+
| ----------- | ----------------------------------------------------------------- | :---: |
|
|
45
|
+
| **M1–M3** | Query builder · collection GET · single-entity CRUD · auth · errors | Done |
|
|
46
|
+
| **M4 · 1/4**| Script execution (database / entity-set / record scope) | Done (v0.1.4) |
|
|
47
|
+
| **M4 · 2/4**| Containers (binary upload / download / stream) | Done (v0.1.5) |
|
|
48
|
+
| **M5** | `$metadata` (schema introspection) | Done |
|
|
49
|
+
| **M6** | `$batch` (multipart with changesets) | Done |
|
|
50
|
+
| **v0.2.0** | Spec alignment — version detection, `$apply`, FMSID scripts, `$ref`, FMID auth, IIFE build | Done (v0.2.0) |
|
|
51
|
+
|
|
52
|
+
Full roadmap and changes live in [`CHANGELOG.md`](./CHANGELOG.md).
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
> **Not yet published to npm.** Until the first release hits the registry, install directly from GitHub or a local checkout.
|
|
57
|
+
|
|
58
|
+
From GitHub:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm install github:fsans/fms-odata-js
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
From a local clone:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm install /path/to/fms-odata-js
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Once published, the canonical install will be:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm install fms-odata-js
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Local dev:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm install
|
|
80
|
+
npm test # 227 unit tests, offline
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Quick start
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { FMSOData, basicAuth } from 'fms-odata-js'
|
|
87
|
+
|
|
88
|
+
const db = new FMSOData({
|
|
89
|
+
host: 'https://fms.example.com',
|
|
90
|
+
database: 'Contacts',
|
|
91
|
+
token: basicAuth('admin', 'secret'), // FMS OData requires Basic auth
|
|
92
|
+
timeoutMs: 15_000,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// For FileMaker Cloud, use FMID auth instead:
|
|
96
|
+
// import { fmidAuth } from 'fms-odata-js'
|
|
97
|
+
// token: fmidAuth(clarisIdToken)
|
|
98
|
+
|
|
99
|
+
// Collection read
|
|
100
|
+
const { value, count } = await db
|
|
101
|
+
.from('contact')
|
|
102
|
+
.select('id', 'first_name', 'last_name')
|
|
103
|
+
.filter((f) => f.eq('last_name', 'Smith'))
|
|
104
|
+
.orderby('last_name')
|
|
105
|
+
.top(50)
|
|
106
|
+
.count()
|
|
107
|
+
.get()
|
|
108
|
+
|
|
109
|
+
// Create
|
|
110
|
+
const created = await db.from('contact').create({
|
|
111
|
+
first_name: 'Alice',
|
|
112
|
+
last_name: 'Liddell',
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Read / update / delete a single row
|
|
116
|
+
const row = await db.from('contact').byKey(created.id).get()
|
|
117
|
+
await db.from('contact').byKey(row.id).patch({ first_name: 'A.' })
|
|
118
|
+
await db.from('contact').byKey(row.id).delete()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## FileMaker scripts
|
|
122
|
+
|
|
123
|
+
Invoke FMS-side FileMaker scripts at three scopes. The optional `parameter`
|
|
124
|
+
becomes `Get(ScriptParameter)` inside the script; the script's text result is
|
|
125
|
+
returned as `scriptResult`.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// Database scope
|
|
129
|
+
const { scriptResult } = await db.script('Ping', { parameter: 'hello' })
|
|
130
|
+
|
|
131
|
+
// Entity-set scope (script runs with the table as context)
|
|
132
|
+
await db.from('contact').script('RebuildIndex')
|
|
133
|
+
|
|
134
|
+
// Single-record scope (script's current record is set to this row)
|
|
135
|
+
await db.from('contact').byKey(42).script('Archive')
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
A non-zero `scriptError` becomes an `FMScriptError` (subclass of
|
|
139
|
+
`FMSODataError`), so existing error handlers keep working:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { FMScriptError } from 'fms-odata-js'
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
await db.script('Risky')
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (err instanceof FMScriptError) {
|
|
148
|
+
console.error(`FM script error ${err.scriptError}: ${err.scriptResult}`)
|
|
149
|
+
} else {
|
|
150
|
+
throw err
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### FMSID-based invocation (v26+)
|
|
156
|
+
|
|
157
|
+
On FileMaker Server 2026+, scripts can be invoked by their immutable FMSID
|
|
158
|
+
instead of name. This survives script renames and database migrations:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
const result = await db.scriptById(42, { parameter: 'hello' })
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Use `await db.hasFeature('scriptsByFMSID')` to check before calling.
|
|
165
|
+
|
|
166
|
+
## Container fields
|
|
167
|
+
|
|
168
|
+
Container fields expose their bytes through `EntityRef#container(fieldName)`.
|
|
169
|
+
The handle gives you three I/O shapes plus a clear operation:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const photo = db.from('contact').byKey(42).container('photo')
|
|
173
|
+
|
|
174
|
+
// Upload (default: binary mode — image / PDF only per FMS)
|
|
175
|
+
await photo.upload({
|
|
176
|
+
data: new Uint8Array(await file.arrayBuffer()),
|
|
177
|
+
contentType: 'image/png',
|
|
178
|
+
filename: 'profile.png',
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// Upload any file type (zip, docx, …) via base64 encoding
|
|
182
|
+
await photo.upload({
|
|
183
|
+
data: zipBytes,
|
|
184
|
+
contentType: 'application/zip',
|
|
185
|
+
filename: 'archive.zip',
|
|
186
|
+
encoding: 'base64',
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// Download into memory (good for thumbnails, small assets)
|
|
190
|
+
const { blob, contentType, filename, size } = await photo.get()
|
|
191
|
+
|
|
192
|
+
// Or stream it (good for large files — no buffering)
|
|
193
|
+
const stream = await photo.getStream()
|
|
194
|
+
await stream.pipeTo(someWritable)
|
|
195
|
+
|
|
196
|
+
// Clear the container
|
|
197
|
+
await photo.delete()
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Under the hood the library uses the two FMS-documented wire formats:
|
|
201
|
+
binary mode `PATCH`es `…/<field>` with raw bytes; base64 mode `PATCH`es the
|
|
202
|
+
parent record with `<field>@com.filemaker.odata.ContentType` /
|
|
203
|
+
`…Filename` annotations. `delete()` clears the value via `PATCH` with
|
|
204
|
+
`{ <field>: null }` (FMS has no per-field DELETE for record data).
|
|
205
|
+
|
|
206
|
+
The `Content-Disposition` filename is parsed for you on download, including
|
|
207
|
+
RFC 5987 `filename*=UTF-8''…` for non-ASCII names. On upload, non-ASCII
|
|
208
|
+
filenames are emitted in both plain and RFC 5987 form automatically.
|
|
209
|
+
|
|
210
|
+
## Schema introspection (`$metadata`)
|
|
211
|
+
|
|
212
|
+
Fetch and inspect the OData CSDL schema emitted by FileMaker Server:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
const meta = await db.metadata()
|
|
216
|
+
|
|
217
|
+
console.log(meta.namespace) // e.g. "FileMaker"
|
|
218
|
+
console.log(meta.entitySets) // [{ name: "contact", entityType: "FileMaker.contact" }, …]
|
|
219
|
+
console.log(meta.entityTypes) // [{ name: "contact", keys: ["id"], properties: […] }, …]
|
|
220
|
+
console.log(meta.actions) // FileMaker scripts exposed as OData Actions
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Results are cached by default. Pass `refresh: true` to force a refetch:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const fresh = await db.metadata({ refresh: true })
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Use `metadataXml()` to get the raw CSDL XML for debugging or forward-compat parsing:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
const xml = await db.metadataXml()
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Batch requests (`$batch`)
|
|
236
|
+
|
|
237
|
+
Send multiple operations in a single HTTP round-trip. Reads and atomic changesets
|
|
238
|
+
can be freely mixed:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
const batch = db.batch()
|
|
242
|
+
|
|
243
|
+
// Queue a read (GET)
|
|
244
|
+
const contactsHandle = batch.add({
|
|
245
|
+
op: 'list',
|
|
246
|
+
entitySet: 'contact',
|
|
247
|
+
query: { $top: 10, $filter: "status eq 'active'" },
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// Queue an atomic write group — all succeed or all fail together
|
|
251
|
+
batch.changeset(cs => {
|
|
252
|
+
cs.create('contact', { first_name: 'Alice', last_name: 'Liddell' })
|
|
253
|
+
cs.patch('contact', 42, { status: 'archived' })
|
|
254
|
+
cs.delete('invoice', 99)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const result = await batch.send()
|
|
258
|
+
|
|
259
|
+
if (result.ok) {
|
|
260
|
+
console.log('All operations succeeded')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Per-operation status and body are in result.responses (in request order)
|
|
264
|
+
for (const r of result.responses) {
|
|
265
|
+
console.log(r.status, r.ok, r.body)
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Changeset operations also expose `If-Match` for optimistic concurrency:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
batch.changeset(cs => {
|
|
273
|
+
cs.patch('contact', 42, { status: 'closed' }, { ifMatch: '"abc123"' })
|
|
274
|
+
})
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Version detection & feature gating
|
|
278
|
+
|
|
279
|
+
The library detects the FileMaker Server major version from the
|
|
280
|
+
`Org.OData.Core.V1.ProductVersion` annotation in `$metadata` and caches it
|
|
281
|
+
for the lifetime of the `FMSOData` instance:
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
const v = await db.version() // '19' | '21' | '22' | '26' | 'future' | null
|
|
285
|
+
const info = await db.versionInfo() // full descriptor with feature flags
|
|
286
|
+
const ok = await db.hasFeature('applyAggregation') // boolean
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Feature flags include `applyAggregation`, `scriptsByFMSID`, `webhooks`, and
|
|
290
|
+
more — see `FMFeatureFlags` in the type exports. This lets you write
|
|
291
|
+
conditional code that adapts to the server's capabilities at runtime.
|
|
292
|
+
|
|
293
|
+
## Aggregation (`$apply`)
|
|
294
|
+
|
|
295
|
+
Server-side aggregation via OData `$apply` — requires FileMaker Server 2024+
|
|
296
|
+
(v22). Use `hasFeature('applyAggregation')` to check before calling.
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
// Aggregate: sum, average, min, max, countdistinct
|
|
300
|
+
const { value } = await db
|
|
301
|
+
.from('orders')
|
|
302
|
+
.aggregate([{ field: 'total', function: 'sum', alias: 'totalSum' }])
|
|
303
|
+
.get()
|
|
304
|
+
// $apply=aggregate(total with sum as totalSum)
|
|
305
|
+
|
|
306
|
+
// Group by with aggregation
|
|
307
|
+
const { value } = await db
|
|
308
|
+
.from('orders')
|
|
309
|
+
.groupBy(['customerId'], [
|
|
310
|
+
{ field: 'total', function: 'sum', alias: 'totalSum' },
|
|
311
|
+
{ field: 'total', function: 'average', alias: 'avgTotal' },
|
|
312
|
+
])
|
|
313
|
+
.get()
|
|
314
|
+
// $apply=groupby((customerId),aggregate(total with sum as totalSum,total with average as avgTotal))
|
|
315
|
+
|
|
316
|
+
// Raw $apply for advanced transformations
|
|
317
|
+
const { value } = await db.from('orders').apply('aggregate(total with max as maxTotal)').get()
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Navigation properties (`$ref`)
|
|
321
|
+
|
|
322
|
+
Manage OData relationship links between entities via `$ref`:
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
// List related references
|
|
326
|
+
const refs = await db.from('contact').byKey(7).getRefs('addresses')
|
|
327
|
+
// [{ '@odata.id': 'https://fms.example.com/fmi/odata/v4/DB/address(1)' }, ...]
|
|
328
|
+
|
|
329
|
+
// Add a reference (POST — for collection-valued navigation properties)
|
|
330
|
+
await db.from('contact').byKey(7).addRef('addresses', 42)
|
|
331
|
+
|
|
332
|
+
// Set a reference (PATCH — for single-valued navigation properties)
|
|
333
|
+
await db.from('order').byKey(100).setRef('customer', 7)
|
|
334
|
+
|
|
335
|
+
// Remove a reference
|
|
336
|
+
await db.from('contact').byKey(7).removeRef('addresses', 42)
|
|
337
|
+
await db.from('order').byKey(100).removeRef('customer') // clears single-valued
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Live integration tests
|
|
341
|
+
|
|
342
|
+
Copy `.env.sample` to `.env` and fill in real FMS credentials:
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
npm run probe # quick connectivity check
|
|
346
|
+
FM_LIVE=1 npm test -- tests/integration # full CRUD against real FMS
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
> Using self-signed certs on a LAN box? Set `FM_VERIFY_SSL=0` in `.env`.
|
|
350
|
+
>
|
|
351
|
+
> Standardized env var names (`FM_SERVER`, `FM_DATABASE`, `FM_USER`,
|
|
352
|
+
> `FM_PASSWORD`, `FM_VERIFY_SSL`, `FM_TIMEOUT`, `FM_LIVE`) are preferred.
|
|
353
|
+
> Legacy `FM_ODATA_*` names are still accepted as fallbacks.
|
|
354
|
+
|
|
355
|
+
## Docs
|
|
356
|
+
|
|
357
|
+
- [`docs/README.md`](./docs/README.md) — API reference and deeper guides
|
|
358
|
+
- [`docs/filemaker-quirks.md`](./docs/filemaker-quirks.md) — FMS OData deviations and how this library works around them
|
|
359
|
+
- [`docs/filemaker-odata-container-guide.md`](./docs/filemaker-odata-container-guide.md) — full Claris OData container-operations reference
|
|
360
|
+
- [`CHANGELOG.md`](./CHANGELOG.md) — full release history
|
|
361
|
+
- [fms-odata-spec](https://github.com/fsans/fms-odata-spec) — reference specification this library aligns to
|
|
362
|
+
- [`examples/`](./examples) — runnable sample projects
|
|
363
|
+
- [`consumer-node/`](./examples/consumer-node) — Node CLI consuming the library
|
|
364
|
+
- [`webviewer/`](./examples/webviewer) — **standalone HTML page** ready to drop into a FileMaker Web Viewer (uses the IIFE bundle)
|
|
365
|
+
- [`examples/Contacts.fmp12`](./examples/Contacts.fmp12) — ready-to-host FileMaker test database matching the examples. **Credentials: `admin` / `admin`** (dev use only — change before exposing to any network).
|
|
366
|
+
|
|
367
|
+
### Build formats
|
|
368
|
+
|
|
369
|
+
The library ships three bundle formats:
|
|
370
|
+
|
|
371
|
+
- **ESM** (`dist/fms-odata.esm.js`) — for Node, bundlers, and modern browsers
|
|
372
|
+
- **ESM minified** (`dist/fms-odata.esm.min.js`) — ~9.1 KB gzipped, production use
|
|
373
|
+
- **IIFE** (`dist/fms-odata.iife.min.js`) — global `FMSODataLib`, for FileMaker Web Viewer and `<script>` tag inclusion without a bundler
|
|
374
|
+
|
|
375
|
+
## Contributing
|
|
376
|
+
|
|
377
|
+
Issues and PRs are welcome. Please run `npm test` and `npm run typecheck` before opening a PR.
|
|
378
|
+
|
|
379
|
+
## License
|
|
380
|
+
|
|
381
|
+
[MIT](./LICENSE) © 2026 Francesc Sans, nBCN Software, Barcelona — <http://www.ntwk.es>
|