@proofkit/fmdapi 5.0.3-beta.1 → 5.1.0-beta.3
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/bin/intent.js +20 -0
- package/dist/esm/adapters/fetch-base.js.map +1 -1
- package/dist/esm/adapters/fetch.js.map +1 -1
- package/dist/esm/adapters/fm-mcp.d.ts +32 -0
- package/dist/esm/adapters/fm-mcp.js +170 -0
- package/dist/esm/adapters/fm-mcp.js.map +1 -0
- package/dist/esm/adapters/otto.js.map +1 -1
- package/dist/esm/client-types.js.map +1 -1
- package/dist/esm/client.d.ts +1 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tokenStore/memory.js.map +1 -1
- package/dist/esm/utils.js.map +1 -1
- package/package.json +30 -17
- package/skills/fmdapi-client/SKILL.md +490 -0
- package/src/adapters/fm-mcp.ts +224 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proofkit/fmdapi",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.1.0-beta.3",
|
|
4
4
|
"description": "FileMaker Data API client",
|
|
5
|
-
"repository":
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/proofgeist/proofkit"
|
|
8
|
+
},
|
|
6
9
|
"author": "Eric <37158449+eluce2@users.noreply.github.com>",
|
|
7
10
|
"license": "MIT",
|
|
8
11
|
"private": false,
|
|
@@ -37,25 +40,26 @@
|
|
|
37
40
|
"./package.json": "./package.json"
|
|
38
41
|
},
|
|
39
42
|
"dependencies": {
|
|
40
|
-
"@standard-schema/spec": "^1.
|
|
41
|
-
"@tanstack/vite-config": "^0.2.
|
|
43
|
+
"@standard-schema/spec": "^1.1.0",
|
|
44
|
+
"@tanstack/vite-config": "^0.2.1",
|
|
42
45
|
"chalk": "5.4.1",
|
|
43
|
-
"commander": "^14.0.
|
|
44
|
-
"dotenv": "^16.
|
|
45
|
-
"fs-extra": "^11.3.
|
|
46
|
+
"commander": "^14.0.2",
|
|
47
|
+
"dotenv": "^16.6.1",
|
|
48
|
+
"fs-extra": "^11.3.3",
|
|
46
49
|
"ts-morph": "^26.0.0",
|
|
47
|
-
"vite": "^6.
|
|
48
|
-
"zod": "^4.
|
|
50
|
+
"vite": "^6.4.1",
|
|
51
|
+
"zod": "^4.3.5"
|
|
49
52
|
},
|
|
50
53
|
"devDependencies": {
|
|
54
|
+
"@tanstack/intent": "^0.0.19",
|
|
51
55
|
"@types/fs-extra": "^11.0.4",
|
|
52
|
-
"@types/node": "^22.
|
|
53
|
-
"@upstash/redis": "^1.
|
|
54
|
-
"knip": "^5.
|
|
55
|
-
"publint": "^0.3.
|
|
56
|
+
"@types/node": "^22.19.5",
|
|
57
|
+
"@upstash/redis": "^1.36.1",
|
|
58
|
+
"knip": "^5.80.2",
|
|
59
|
+
"publint": "^0.3.16",
|
|
56
60
|
"ts-toolbelt": "^9.6.0",
|
|
57
61
|
"typescript": "^5.9.3",
|
|
58
|
-
"vitest": "^4.0.
|
|
62
|
+
"vitest": "^4.0.17"
|
|
59
63
|
},
|
|
60
64
|
"engines": {
|
|
61
65
|
"node": ">=18.0.0"
|
|
@@ -66,7 +70,10 @@
|
|
|
66
70
|
"dist-browser",
|
|
67
71
|
"tokenStore",
|
|
68
72
|
"utils",
|
|
69
|
-
"stubs"
|
|
73
|
+
"stubs",
|
|
74
|
+
"skills",
|
|
75
|
+
"bin",
|
|
76
|
+
"!skills/_artifacts"
|
|
70
77
|
],
|
|
71
78
|
"keywords": [
|
|
72
79
|
"filemaker",
|
|
@@ -77,8 +84,12 @@
|
|
|
77
84
|
"fmrest",
|
|
78
85
|
"fmdapi",
|
|
79
86
|
"proofgeist",
|
|
80
|
-
"fm-dapi"
|
|
87
|
+
"fm-dapi",
|
|
88
|
+
"tanstack-intent"
|
|
81
89
|
],
|
|
90
|
+
"bin": {
|
|
91
|
+
"intent": "./bin/intent.js"
|
|
92
|
+
},
|
|
82
93
|
"scripts": {
|
|
83
94
|
"build": "tsc && vite build && publint --strict",
|
|
84
95
|
"build:watch": "tsc && vite build --watch",
|
|
@@ -87,7 +98,9 @@
|
|
|
87
98
|
"dev": "tsc --watch",
|
|
88
99
|
"ci": "pnpm build && pnpm check-format && pnpm publint --strict && pnpm test",
|
|
89
100
|
"test": "vitest run",
|
|
90
|
-
"
|
|
101
|
+
"test:e2e": "doppler run -- vitest run tests/e2e",
|
|
102
|
+
"capture": "doppler run -- npx tsx scripts/capture-responses.ts",
|
|
103
|
+
"typecheck": "tsc --noEmit",
|
|
91
104
|
"changeset": "changeset",
|
|
92
105
|
"release": "pnpm build && changeset publish --access public",
|
|
93
106
|
"knip": "knip",
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fmdapi-client
|
|
3
|
+
description: >
|
|
4
|
+
DataApi factory, OttoAdapter (dk_ API key), FetchAdapter (username/password
|
|
5
|
+
with token stores), CRUD methods: list, listAll, find, findOne, findFirst,
|
|
6
|
+
maybeFindFirst, findAll, create, update, delete, get, executeScript,
|
|
7
|
+
containerUpload, Standard Schema validation, portal data access, FileMaker
|
|
8
|
+
Data API, layout-bound clients, schema inference
|
|
9
|
+
type: core
|
|
10
|
+
library: proofkit
|
|
11
|
+
library_version: "5.0.3-beta.1"
|
|
12
|
+
requires:
|
|
13
|
+
- typegen-setup
|
|
14
|
+
sources:
|
|
15
|
+
- "proofgeist/proofkit:packages/fmdapi/src/client.ts"
|
|
16
|
+
- "proofgeist/proofkit:packages/fmdapi/src/adapters/otto.ts"
|
|
17
|
+
- "proofgeist/proofkit:packages/fmdapi/src/adapters/fetch.ts"
|
|
18
|
+
- "proofgeist/proofkit:apps/docs/content/docs/fmdapi/*.mdx"
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
Install the package:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pnpm add @proofkit/fmdapi
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
If you want the companion `typegen-setup` skill available locally in this project, also install:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pnpm add -D @proofkit/typegen@*
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### OttoAdapter (recommended)
|
|
36
|
+
|
|
37
|
+
Requires [OttoFMS](https://ottofms.com/) installed on the FileMaker Server. No token management needed — the proxy handles sessions.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { DataApi, OttoAdapter } from "@proofkit/fmdapi";
|
|
41
|
+
|
|
42
|
+
const client = DataApi({
|
|
43
|
+
adapter: new OttoAdapter({
|
|
44
|
+
auth: { apiKey: process.env.OTTO_API_KEY as `dk_${string}` },
|
|
45
|
+
db: process.env.FM_DATABASE,
|
|
46
|
+
server: process.env.FM_SERVER, // must start with https://
|
|
47
|
+
}),
|
|
48
|
+
layout: "API_Contacts",
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
API keys must start with `dk_` (OttoFMS) or `KEY_` (Otto v3). OttoFMS keys use the default HTTPS port with an `/otto` path prefix. Otto v3 keys use port 3030 by default (configurable via `auth.ottoPort`).
|
|
53
|
+
|
|
54
|
+
### FetchAdapter (direct Data API)
|
|
55
|
+
|
|
56
|
+
Authenticates with username/password. Manages Data API session tokens automatically.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { DataApi, FetchAdapter } from "@proofkit/fmdapi";
|
|
60
|
+
|
|
61
|
+
const client = DataApi({
|
|
62
|
+
adapter: new FetchAdapter({
|
|
63
|
+
auth: {
|
|
64
|
+
username: process.env.FM_USERNAME,
|
|
65
|
+
password: process.env.FM_PASSWORD,
|
|
66
|
+
},
|
|
67
|
+
db: process.env.FM_DATABASE,
|
|
68
|
+
server: process.env.FM_SERVER,
|
|
69
|
+
tokenStore: fileTokenStore(), // IMPORTANT for production — see Common Mistakes
|
|
70
|
+
}),
|
|
71
|
+
layout: "API_Contacts",
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### With typegen-generated clients
|
|
76
|
+
|
|
77
|
+
The recommended path is to use `@proofkit/typegen` to generate layout-specific clients with full type safety and schema validation. The generated client file exports a pre-configured `DataApi` instance per layout:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { CustomersLayout } from "./schema/client";
|
|
81
|
+
|
|
82
|
+
const { data } = await CustomersLayout.findOne({
|
|
83
|
+
query: { id: "==abc123" },
|
|
84
|
+
});
|
|
85
|
+
// data.fieldData is fully typed with your FM field names
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Core Patterns
|
|
89
|
+
|
|
90
|
+
### CRUD Operations
|
|
91
|
+
|
|
92
|
+
Every `DataApi` client is bound to a single layout. All methods operate on that layout.
|
|
93
|
+
|
|
94
|
+
**Find records:**
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// Standard find — returns { data: FMRecord[], dataInfo }
|
|
98
|
+
const response = await client.find({
|
|
99
|
+
query: { city: "Portland" },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// OR finds — pass an array of query objects
|
|
103
|
+
const response = await client.find({
|
|
104
|
+
query: [{ city: "Portland" }, { city: "Seattle" }],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// findOne — throws unless exactly 1 record found
|
|
108
|
+
const { data } = await client.findOne({
|
|
109
|
+
query: { email: "==user@example.com" },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// findFirst — returns first record, throws if none found
|
|
113
|
+
const { data } = await client.findFirst({
|
|
114
|
+
query: { status: "Active" },
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// maybeFindFirst — returns first record or null
|
|
118
|
+
const result = await client.maybeFindFirst({
|
|
119
|
+
query: { email: "==user@example.com" },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// findAll — auto-paginates through all results (caution with large datasets)
|
|
123
|
+
const allRecords = await client.findAll({
|
|
124
|
+
query: { status: "==Active" },
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Suppress error on empty result set (FM error 401)
|
|
128
|
+
const response = await client.find({
|
|
129
|
+
query: { email: "==nonexistent@example.com" },
|
|
130
|
+
ignoreEmptyResult: true, // returns empty array instead of throwing
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**List records (no find criteria):**
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
// list — returns up to 100 records by default
|
|
138
|
+
const response = await client.list({
|
|
139
|
+
sort: [{ fieldName: "lastName", sortOrder: "ascend" }],
|
|
140
|
+
limit: 50,
|
|
141
|
+
offset: 1,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// listAll — auto-paginates (caution with large datasets)
|
|
145
|
+
const allRecords = await client.listAll();
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Create:**
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const { recordId, modId } = await client.create({
|
|
152
|
+
fieldData: {
|
|
153
|
+
firstName: "Jane",
|
|
154
|
+
lastName: "Doe",
|
|
155
|
+
email: "jane@example.com",
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Update:**
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// recordId is FileMaker's internal record ID (from find/list/create responses)
|
|
164
|
+
await client.update({
|
|
165
|
+
recordId: 42,
|
|
166
|
+
fieldData: { email: "new@example.com" },
|
|
167
|
+
modId: 5, // optional optimistic locking
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Delete:**
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
await client.delete({ recordId: 42 });
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Get by record ID:**
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const response = await client.get({ recordId: 42 });
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Script Execution
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// Direct execution
|
|
187
|
+
const result = await client.executeScript({
|
|
188
|
+
script: "Process Order",
|
|
189
|
+
scriptParam: JSON.stringify({ orderId: "12345" }),
|
|
190
|
+
});
|
|
191
|
+
console.log(result.scriptResult); // string returned by Exit Script
|
|
192
|
+
|
|
193
|
+
// Scripts attached to CRUD operations
|
|
194
|
+
const { recordId, scriptResult } = await client.create({
|
|
195
|
+
fieldData: { name: "New Record" },
|
|
196
|
+
script: "After Create Hook",
|
|
197
|
+
"script.param": JSON.stringify({ notify: true }),
|
|
198
|
+
// Also available: script.prerequest, script.presort (and their .param variants)
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Portal Data
|
|
203
|
+
|
|
204
|
+
Portal data is returned on every record in the `portalData` property. Each portal row includes its own `recordId` and `modId`.
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
// Type-safe portal access with manual types
|
|
208
|
+
type TOrderRow = {
|
|
209
|
+
"Orders::orderId": string;
|
|
210
|
+
"Orders::orderDate": string;
|
|
211
|
+
"Orders::total": number;
|
|
212
|
+
};
|
|
213
|
+
type TPortals = {
|
|
214
|
+
portal_orders: TOrderRow; // key = portal object name on layout
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const client = DataApi<TContact, TPortals>({
|
|
218
|
+
adapter: new OttoAdapter({ /* ... */ }),
|
|
219
|
+
layout: "API_Contacts",
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const { data } = await client.find({ query: { id: "==123" } });
|
|
223
|
+
for (const row of data) {
|
|
224
|
+
for (const order of row.portalData.portal_orders) {
|
|
225
|
+
console.log(order["Orders::orderId"], order.recordId);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Control portal pagination
|
|
230
|
+
const response = await client.list({
|
|
231
|
+
portalRanges: {
|
|
232
|
+
portal_orders: { offset: 1, limit: 10 },
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Container Upload
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
const file = new Blob(["file contents"], { type: "text/plain" });
|
|
241
|
+
|
|
242
|
+
await client.containerUpload({
|
|
243
|
+
recordId: 42,
|
|
244
|
+
containerFieldName: "photo", // typed to field names if using schema
|
|
245
|
+
file,
|
|
246
|
+
containerFieldRepetition: 1, // optional, defaults to first repetition
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Schema Validation
|
|
251
|
+
|
|
252
|
+
> **Note:** Schema validators are typically generated by `@proofkit/typegen`. Manual schemas are only needed for non-typegen setups. If using typegen, customize via override files (see `typegen-setup` skill).
|
|
253
|
+
|
|
254
|
+
The `schema` option accepts any [Standard Schema](https://standardschema.dev/) compliant validator (Zod, Valibot, ArkType, etc.). When set, every read method validates and transforms each record's `fieldData` (and optionally `portalData`).
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
import { z } from "zod/v4";
|
|
258
|
+
import { DataApi, OttoAdapter } from "@proofkit/fmdapi";
|
|
259
|
+
|
|
260
|
+
const ZContact = z.object({
|
|
261
|
+
firstName: z.string(),
|
|
262
|
+
lastName: z.string(),
|
|
263
|
+
active: z.coerce.boolean(), // transform FM number to boolean
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const client = DataApi({
|
|
267
|
+
adapter: new OttoAdapter({ /* ... */ }),
|
|
268
|
+
layout: "API_Contacts",
|
|
269
|
+
schema: {
|
|
270
|
+
fieldData: ZContact,
|
|
271
|
+
// portalData: { portal_orders: ZOrderRow }, // optional
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// data.fieldData.active is now boolean, not number
|
|
276
|
+
const { data } = await client.findFirst({ query: { id: "==123" } });
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
If validation fails, the client throws with details about which fields mismatched. This catches FileMaker field renames at runtime before they corrupt downstream logic.
|
|
280
|
+
|
|
281
|
+
## Common Mistakes
|
|
282
|
+
|
|
283
|
+
### CRITICAL: Creating DataApi without an adapter
|
|
284
|
+
|
|
285
|
+
Wrong:
|
|
286
|
+
```ts
|
|
287
|
+
import { DataApi } from "@proofkit/fmdapi";
|
|
288
|
+
|
|
289
|
+
const client = DataApi({
|
|
290
|
+
layout: "Contacts",
|
|
291
|
+
server: "https://fm.example.com",
|
|
292
|
+
db: "MyDB.fmp12",
|
|
293
|
+
auth: { apiKey: "dk_abc123" },
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Correct:
|
|
298
|
+
```ts
|
|
299
|
+
import { DataApi, OttoAdapter } from "@proofkit/fmdapi";
|
|
300
|
+
|
|
301
|
+
const client = DataApi({
|
|
302
|
+
adapter: new OttoAdapter({
|
|
303
|
+
server: "https://fm.example.com",
|
|
304
|
+
db: "MyDB.fmp12",
|
|
305
|
+
auth: { apiKey: "dk_abc123" as `dk_${string}` },
|
|
306
|
+
}),
|
|
307
|
+
layout: "Contacts",
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
v5 requires an explicit adapter instance. Connection config (`server`, `db`, `auth`) goes on the adapter, not `DataApi`. `layout` goes on `DataApi`.
|
|
312
|
+
|
|
313
|
+
### CRITICAL: Omitting token store in production (FetchAdapter)
|
|
314
|
+
|
|
315
|
+
Wrong:
|
|
316
|
+
```ts
|
|
317
|
+
const client = DataApi({
|
|
318
|
+
adapter: new FetchAdapter({
|
|
319
|
+
auth: { username: "admin", password: "pass" },
|
|
320
|
+
db: "MyDB.fmp12",
|
|
321
|
+
server: "https://fm.example.com",
|
|
322
|
+
// no tokenStore — defaults to in-memory
|
|
323
|
+
}),
|
|
324
|
+
layout: "Contacts",
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Correct:
|
|
329
|
+
```ts
|
|
330
|
+
import { fileTokenStore } from "@proofkit/fmdapi/tokenStore/file";
|
|
331
|
+
// or for serverless:
|
|
332
|
+
// import { upstashTokenStore } from "@proofkit/fmdapi/tokenStore/upstash";
|
|
333
|
+
|
|
334
|
+
const client = DataApi({
|
|
335
|
+
adapter: new FetchAdapter({
|
|
336
|
+
auth: { username: "admin", password: "pass" },
|
|
337
|
+
db: "MyDB.fmp12",
|
|
338
|
+
server: "https://fm.example.com",
|
|
339
|
+
tokenStore: fileTokenStore(),
|
|
340
|
+
}),
|
|
341
|
+
layout: "Contacts",
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Default `memoryStore` loses tokens on process restart, creating a new session each time. FileMaker allows max 500 concurrent sessions — serverless/edge deployments exhaust this quickly. Use `fileTokenStore()` for persistent servers or `upstashTokenStore()` for edge/serverless. OttoAdapter avoids this entirely.
|
|
346
|
+
|
|
347
|
+
### HIGH: Storing FM recordId as a persistent identifier
|
|
348
|
+
|
|
349
|
+
Wrong:
|
|
350
|
+
```ts
|
|
351
|
+
// Saving recordId to your own database as a foreign key
|
|
352
|
+
const { recordId } = await client.create({ fieldData: { name: "Acme" } });
|
|
353
|
+
await myDb.insert({ fmRecordId: recordId }); // fragile!
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Correct:
|
|
357
|
+
```ts
|
|
358
|
+
// Use a stable primary key field (e.g., UUID) from FileMaker
|
|
359
|
+
const { data } = await client.findOne({ query: { name: "Acme" } });
|
|
360
|
+
const stableId = data.fieldData.primaryKey; // UUID set by auto-enter
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
FileMaker's internal `recordId` can change during imports, migrations, or file recovery. Always use a dedicated primary key field (UUID or serial) for cross-system references. Only use `recordId` for immediate operations (update/delete) within the same request flow.
|
|
364
|
+
|
|
365
|
+
### HIGH: Assuming dynamic layout switching on a single client
|
|
366
|
+
|
|
367
|
+
Wrong:
|
|
368
|
+
```ts
|
|
369
|
+
const client = DataApi({
|
|
370
|
+
adapter: new OttoAdapter({ /* ... */ }),
|
|
371
|
+
layout: "Contacts",
|
|
372
|
+
});
|
|
373
|
+
// Trying to query a different layout
|
|
374
|
+
await client.find({ layout: "Invoices", query: { status: "Open" } });
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Correct:
|
|
378
|
+
```ts
|
|
379
|
+
const contactsClient = DataApi({
|
|
380
|
+
adapter: new OttoAdapter({ /* ... */ }),
|
|
381
|
+
layout: "Contacts",
|
|
382
|
+
});
|
|
383
|
+
const invoicesClient = DataApi({
|
|
384
|
+
adapter: new OttoAdapter({ /* ... */ }),
|
|
385
|
+
layout: "Invoices",
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Each `DataApi` client is bound to one layout at creation. v5 removed per-method layout override. Create a separate client per layout. The adapter instance can be shared.
|
|
390
|
+
|
|
391
|
+
### MEDIUM: Using wrong Otto API key format
|
|
392
|
+
|
|
393
|
+
Wrong:
|
|
394
|
+
```ts
|
|
395
|
+
new OttoAdapter({
|
|
396
|
+
auth: { apiKey: "abc123-def456" }, // no prefix
|
|
397
|
+
db: "MyDB.fmp12",
|
|
398
|
+
server: "https://fm.example.com",
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Correct:
|
|
403
|
+
```ts
|
|
404
|
+
new OttoAdapter({
|
|
405
|
+
auth: { apiKey: "dk_abc123def456" as `dk_${string}` },
|
|
406
|
+
db: "MyDB.fmp12",
|
|
407
|
+
server: "https://fm.example.com",
|
|
408
|
+
});
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
OttoFMS keys start with `dk_`, Otto v3 keys start with `KEY_`. The adapter uses this prefix to determine the connection method (port 3030 for `KEY_`, `/otto` path prefix for `dk_`). An unrecognized prefix throws at construction time.
|
|
412
|
+
|
|
413
|
+
### HIGH: Using deprecated zodValidators option instead of schema
|
|
414
|
+
|
|
415
|
+
Wrong:
|
|
416
|
+
```ts
|
|
417
|
+
import { z } from "zod";
|
|
418
|
+
|
|
419
|
+
const client = DataApi({
|
|
420
|
+
adapter: new OttoAdapter({ /* ... */ }),
|
|
421
|
+
layout: "Contacts",
|
|
422
|
+
zodValidators: {
|
|
423
|
+
fieldData: z.object({ name: z.string() }),
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Correct:
|
|
429
|
+
```ts
|
|
430
|
+
import { z } from "zod/v4";
|
|
431
|
+
|
|
432
|
+
const client = DataApi({
|
|
433
|
+
adapter: new OttoAdapter({ /* ... */ }),
|
|
434
|
+
layout: "Contacts",
|
|
435
|
+
schema: {
|
|
436
|
+
fieldData: z.object({ name: z.string() }),
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
`zodValidators` was removed in v5. Use `schema` instead, which accepts any Standard Schema compliant validator. If upgrading from v4, re-run `npx @proofkit/typegen` to regenerate clients with the new option. The client throws at runtime if `zodValidators` is passed.
|
|
442
|
+
|
|
443
|
+
### CRITICAL: Manually redefining TypeScript types instead of generated types
|
|
444
|
+
|
|
445
|
+
Wrong:
|
|
446
|
+
```ts
|
|
447
|
+
// Hand-writing types that duplicate your FM layout
|
|
448
|
+
type Contact = {
|
|
449
|
+
firstName: string;
|
|
450
|
+
lastName: string;
|
|
451
|
+
email: string;
|
|
452
|
+
};
|
|
453
|
+
const client = DataApi<Contact>({
|
|
454
|
+
adapter: new OttoAdapter({ /* ... */ }),
|
|
455
|
+
layout: "API_Contacts",
|
|
456
|
+
});
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Correct:
|
|
460
|
+
```ts
|
|
461
|
+
// Use typegen-generated client which includes schema + types
|
|
462
|
+
import { ContactsLayout } from "./schema/client";
|
|
463
|
+
|
|
464
|
+
const { data } = await ContactsLayout.find({ query: { email: "==test@example.com" } });
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Manual types drift when FileMaker fields change, with no runtime protection. The typegen-generated client bundles a Standard Schema validator that catches field renames at runtime. Run `npx @proofkit/typegen` after any layout change. See `typegen-setup` skill for more details.
|
|
468
|
+
|
|
469
|
+
### HIGH: Mixing Zod v3 and v4 in the same project
|
|
470
|
+
|
|
471
|
+
Wrong:
|
|
472
|
+
```ts
|
|
473
|
+
import { z } from "zod"; // v3
|
|
474
|
+
import { z as z4 } from "zod/v4"; // v4 in another file
|
|
475
|
+
|
|
476
|
+
// Both installed, schemas from different versions passed to DataApi
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
Correct:
|
|
480
|
+
```ts
|
|
481
|
+
// Use one version consistently. v5 typegen generates zod/v4 imports.
|
|
482
|
+
import { z } from "zod/v4";
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
Zod v3 and v4 have different Standard Schema implementations. Mixing them causes subtle type mismatches and potential runtime validation failures. The typegen tool generates `zod/v4` imports by default. See `typegen-setup` skill for more details.
|
|
486
|
+
|
|
487
|
+
## References
|
|
488
|
+
|
|
489
|
+
- **typegen-setup** -- type generation and client scaffolding that produces the layout-specific clients referenced above
|
|
490
|
+
- **fmodata-client** -- alternative ORM-style client using the OData API (Drizzle-like query builder, different from the REST-based Data API covered here)
|