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 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
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.5-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
10
+ [![OData](https://img.shields.io/badge/OData-v4-0078D4?logo=data&logoColor=white)](https://www.odata.org/)
11
+ [![FileMaker](https://img.shields.io/badge/FileMaker-19.0--26.0-FF6B00?logo=filemaker&logoColor=white)](https://www.claris.com/filemaker/)
12
+ [![Bundle](https://img.shields.io/badge/gzip-~9.1%20KB-brightgreen)](#)
13
+ [![Deps](https://img.shields.io/badge/runtime%20deps-0-blue)](#)
14
+ [![Node](https://img.shields.io/badge/node-%3E%3D18-339933?logo=node.js&logoColor=white)](#)
15
+ [![License](https://img.shields.io/badge/license-MIT-black)](./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>