@proofkit/fmodata 0.1.0-alpha.12 → 0.1.0-alpha.14
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/README.md +489 -334
- package/dist/esm/client/batch-builder.d.ts +7 -4
- package/dist/esm/client/batch-builder.js +84 -25
- package/dist/esm/client/batch-builder.js.map +1 -1
- package/dist/esm/client/builders/default-select.d.ts +7 -0
- package/dist/esm/client/builders/default-select.js +42 -0
- package/dist/esm/client/builders/default-select.js.map +1 -0
- package/dist/esm/client/builders/expand-builder.d.ts +43 -0
- package/dist/esm/client/builders/expand-builder.js +173 -0
- package/dist/esm/client/builders/expand-builder.js.map +1 -0
- package/dist/esm/client/builders/index.d.ts +8 -0
- package/dist/esm/client/builders/query-string-builder.d.ts +15 -0
- package/dist/esm/client/builders/query-string-builder.js +25 -0
- package/dist/esm/client/builders/query-string-builder.js.map +1 -0
- package/dist/esm/client/builders/response-processor.d.ts +39 -0
- package/dist/esm/client/builders/response-processor.js +170 -0
- package/dist/esm/client/builders/response-processor.js.map +1 -0
- package/dist/esm/client/builders/select-mixin.d.ts +31 -0
- package/dist/esm/client/builders/select-mixin.js +30 -0
- package/dist/esm/client/builders/select-mixin.js.map +1 -0
- package/dist/esm/client/builders/select-utils.d.ts +8 -0
- package/dist/esm/client/builders/select-utils.js +15 -0
- package/dist/esm/client/builders/select-utils.js.map +1 -0
- package/dist/esm/client/builders/shared-types.d.ts +39 -0
- package/dist/esm/client/builders/table-utils.d.ts +35 -0
- package/dist/esm/client/builders/table-utils.js +45 -0
- package/dist/esm/client/builders/table-utils.js.map +1 -0
- package/dist/esm/client/database.d.ts +3 -22
- package/dist/esm/client/database.js +14 -76
- package/dist/esm/client/database.js.map +1 -1
- package/dist/esm/client/delete-builder.d.ts +11 -15
- package/dist/esm/client/delete-builder.js +26 -26
- package/dist/esm/client/delete-builder.js.map +1 -1
- package/dist/esm/client/entity-set.d.ts +32 -32
- package/dist/esm/client/entity-set.js +92 -69
- package/dist/esm/client/entity-set.js.map +1 -1
- package/dist/esm/client/error-parser.d.ts +12 -0
- package/dist/esm/client/error-parser.js +30 -0
- package/dist/esm/client/error-parser.js.map +1 -0
- package/dist/esm/client/filemaker-odata.d.ts +2 -4
- package/dist/esm/client/filemaker-odata.js +1 -5
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/dist/esm/client/insert-builder.d.ts +7 -9
- package/dist/esm/client/insert-builder.js +70 -24
- package/dist/esm/client/insert-builder.js.map +1 -1
- package/dist/esm/client/query/expand-builder.d.ts +35 -0
- package/dist/esm/client/query/index.d.ts +3 -0
- package/dist/esm/client/query/query-builder.d.ts +134 -0
- package/dist/esm/client/query/query-builder.js +505 -0
- package/dist/esm/client/query/query-builder.js.map +1 -0
- package/dist/esm/client/query/response-processor.d.ts +22 -0
- package/dist/esm/client/query/types.d.ts +52 -0
- package/dist/esm/client/query/url-builder.d.ts +71 -0
- package/dist/esm/client/query/url-builder.js +107 -0
- package/dist/esm/client/query/url-builder.js.map +1 -0
- package/dist/esm/client/query-builder.d.ts +1 -111
- package/dist/esm/client/record-builder.d.ts +56 -63
- package/dist/esm/client/record-builder.js +158 -296
- package/dist/esm/client/record-builder.js.map +1 -1
- package/dist/esm/client/response-processor.d.ts +3 -3
- package/dist/esm/client/update-builder.d.ts +16 -21
- package/dist/esm/client/update-builder.js +56 -30
- package/dist/esm/client/update-builder.js.map +1 -1
- package/dist/esm/errors.d.ts +8 -1
- package/dist/esm/errors.js +17 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +3 -7
- package/dist/esm/index.js +37 -8
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/orm/column.d.ts +45 -0
- package/dist/esm/orm/column.js +59 -0
- package/dist/esm/orm/column.js.map +1 -0
- package/dist/esm/orm/field-builders.d.ts +154 -0
- package/dist/esm/orm/field-builders.js +152 -0
- package/dist/esm/orm/field-builders.js.map +1 -0
- package/dist/esm/orm/index.d.ts +4 -0
- package/dist/esm/orm/operators.d.ts +175 -0
- package/dist/esm/orm/operators.js +221 -0
- package/dist/esm/orm/operators.js.map +1 -0
- package/dist/esm/orm/table.d.ts +341 -0
- package/dist/esm/orm/table.js +211 -0
- package/dist/esm/orm/table.js.map +1 -0
- package/dist/esm/transform.d.ts +20 -21
- package/dist/esm/transform.js +34 -34
- package/dist/esm/transform.js.map +1 -1
- package/dist/esm/types.d.ts +16 -13
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/validation.d.ts +14 -4
- package/dist/esm/validation.js +45 -1
- package/dist/esm/validation.js.map +1 -1
- package/package.json +5 -2
- package/src/client/batch-builder.ts +100 -32
- package/src/client/builders/default-select.ts +69 -0
- package/src/client/builders/expand-builder.ts +236 -0
- package/src/client/builders/index.ts +11 -0
- package/src/client/builders/query-string-builder.ts +41 -0
- package/src/client/builders/response-processor.ts +273 -0
- package/src/client/builders/select-mixin.ts +74 -0
- package/src/client/builders/select-utils.ts +34 -0
- package/src/client/builders/shared-types.ts +41 -0
- package/src/client/builders/table-utils.ts +87 -0
- package/src/client/database.ts +19 -160
- package/src/client/delete-builder.ts +46 -51
- package/src/client/entity-set.ts +227 -302
- package/src/client/error-parser.ts +59 -0
- package/src/client/filemaker-odata.ts +3 -14
- package/src/client/insert-builder.ts +124 -43
- package/src/client/query/expand-builder.ts +164 -0
- package/src/client/query/index.ts +13 -0
- package/src/client/query/query-builder.ts +816 -0
- package/src/client/query/response-processor.ts +244 -0
- package/src/client/query/types.ts +102 -0
- package/src/client/query/url-builder.ts +179 -0
- package/src/client/query-builder.ts +8 -1447
- package/src/client/record-builder.ts +325 -583
- package/src/client/response-processor.ts +4 -5
- package/src/client/update-builder.ts +102 -73
- package/src/errors.ts +22 -1
- package/src/index.ts +55 -5
- package/src/orm/column.ts +78 -0
- package/src/orm/field-builders.ts +296 -0
- package/src/orm/index.ts +60 -0
- package/src/orm/operators.ts +428 -0
- package/src/orm/table.ts +759 -0
- package/src/transform.ts +62 -48
- package/src/types.ts +20 -63
- package/src/validation.ts +76 -4
- package/dist/esm/client/base-table.d.ts +0 -128
- package/dist/esm/client/base-table.js +0 -57
- package/dist/esm/client/base-table.js.map +0 -1
- package/dist/esm/client/build-occurrences.d.ts +0 -74
- package/dist/esm/client/build-occurrences.js +0 -31
- package/dist/esm/client/build-occurrences.js.map +0 -1
- package/dist/esm/client/query-builder.js +0 -897
- package/dist/esm/client/query-builder.js.map +0 -1
- package/dist/esm/client/table-occurrence.d.ts +0 -86
- package/dist/esm/client/table-occurrence.js +0 -58
- package/dist/esm/client/table-occurrence.js.map +0 -1
- package/src/client/base-table.ts +0 -178
- package/src/client/build-occurrences.ts +0 -155
- package/src/client/query-builder.ts.bak +0 -1457
- package/src/client/table-occurrence.ts +0 -156
package/README.md
CHANGED
|
@@ -26,8 +26,10 @@ Here's a minimal example to get you started:
|
|
|
26
26
|
```typescript
|
|
27
27
|
import {
|
|
28
28
|
FMServerConnection,
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
fmTableOccurrence,
|
|
30
|
+
textField,
|
|
31
|
+
numberField,
|
|
32
|
+
eq,
|
|
31
33
|
} from "@proofkit/fmodata";
|
|
32
34
|
import { z } from "zod/v4";
|
|
33
35
|
|
|
@@ -44,30 +46,21 @@ const connection = new FMServerConnection({
|
|
|
44
46
|
},
|
|
45
47
|
});
|
|
46
48
|
|
|
47
|
-
// 2. Define your table schema
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
idField: "id",
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// 3. Create a table occurrence
|
|
59
|
-
const usersTO = defineTableOccurrence({
|
|
60
|
-
name: "users",
|
|
61
|
-
baseTable: usersBase,
|
|
49
|
+
// 2. Define your table schema using field builders
|
|
50
|
+
const users = fmTableOccurrence("users", {
|
|
51
|
+
id: textField().primaryKey(),
|
|
52
|
+
username: textField().notNull(),
|
|
53
|
+
email: textField().notNull(),
|
|
54
|
+
active: numberField()
|
|
55
|
+
.readValidator(z.coerce.boolean())
|
|
56
|
+
.writeValidator(z.boolean().transform((v) => (v ? 1 : 0))),
|
|
62
57
|
});
|
|
63
58
|
|
|
64
|
-
//
|
|
65
|
-
const db = connection.database("MyDatabase.fmp12"
|
|
66
|
-
occurrences: [usersTO],
|
|
67
|
-
});
|
|
59
|
+
// 3. Create a database instance
|
|
60
|
+
const db = connection.database("MyDatabase.fmp12");
|
|
68
61
|
|
|
69
|
-
//
|
|
70
|
-
const { data, error } = await db.from(
|
|
62
|
+
// 4. Query your data
|
|
63
|
+
const { data, error } = await db.from(users).list().execute();
|
|
71
64
|
|
|
72
65
|
if (error) {
|
|
73
66
|
console.error(error);
|
|
@@ -86,8 +79,7 @@ This library relies heavily on the builder pattern for defining your queries and
|
|
|
86
79
|
As such, there are layers to the library to help you build your queries and operations.
|
|
87
80
|
|
|
88
81
|
- `FMServerConnection` - hold server connection details and authentication
|
|
89
|
-
- `
|
|
90
|
-
- `TableOccurrence` - references a base table, and other table occurrences for navigation
|
|
82
|
+
- `FMTable` (created via `fmTableOccurrence()`) - defines the fields, validators, and metadata for a table occurrence
|
|
91
83
|
- `Database` - connects the table occurrences to the server connection
|
|
92
84
|
|
|
93
85
|
### FileMaker Server prerequisites
|
|
@@ -100,7 +92,7 @@ To use this library you need:
|
|
|
100
92
|
|
|
101
93
|
A note on best practices:
|
|
102
94
|
|
|
103
|
-
OData relies entirely on the table occurances in the relationship graph for data access. Relationships between table occurrences are also used, but maybe not as you expect (in short, only the simplest relationships are supported). Given these constraints, it may be best for you to have a seperate FileMaker file for your OData connection, using external data sources to link to your actual data. We've found this especially helpful for larger projects that have very large graphs with lots of
|
|
95
|
+
OData relies entirely on the table occurances in the relationship graph for data access. Relationships between table occurrences are also used, but maybe not as you expect (in short, only the simplest relationships are supported). Given these constraints, it may be best for you to have a seperate FileMaker file for your OData connection, using external data sources to link to your actual data file. We've found this especially helpful for larger projects that have very large graphs with lots of redundant table occurances compared to actual number of base tables.
|
|
104
96
|
|
|
105
97
|
### Server Connection
|
|
106
98
|
|
|
@@ -127,87 +119,118 @@ const connection = new FMServerConnection({
|
|
|
127
119
|
|
|
128
120
|
### Schema Definitions
|
|
129
121
|
|
|
130
|
-
This library relies on a schema-first approach for good type-safety and optional runtime validation.
|
|
122
|
+
This library relies on a schema-first approach for good type-safety and optional runtime validation. Use **`fmTableOccurrence()`** with field builders to create your schemas. This provides full TypeScript type inference for field names in queries.
|
|
123
|
+
|
|
124
|
+
#### Field Builders
|
|
125
|
+
|
|
126
|
+
Field builders provide a fluent API for defining table fields with type-safe metadata. These field types map directly to the FileMaker field types
|
|
127
|
+
|
|
128
|
+
- `textField()`
|
|
129
|
+
- `numberField()`
|
|
130
|
+
- `dateField()`
|
|
131
|
+
- `timeField()`
|
|
132
|
+
- `timestampField()`
|
|
133
|
+
- `containerField()`
|
|
134
|
+
- `calcField()`
|
|
131
135
|
|
|
132
|
-
|
|
136
|
+
Each field builder supports chainable methods:
|
|
133
137
|
|
|
134
|
-
|
|
138
|
+
- `.primaryKey()` - Mark as primary key (automatically read-only)
|
|
139
|
+
- `.notNull()` - Make field non-nullable (required for inserts)
|
|
140
|
+
- `.readOnly()` - Exclude from insert/update operations
|
|
141
|
+
- `.entityId(id)` - Assign FileMaker field ID (FMFID), allowing your API calls to survive FileMaker name changes
|
|
142
|
+
- `.readValidator(validator)` - Transform/validate data when reading from database
|
|
143
|
+
- `.writeValidator(validator)` - Transform/validate data when writing to database
|
|
144
|
+
|
|
145
|
+
#### Defining Tables
|
|
146
|
+
|
|
147
|
+
Use `fmTableOccurrence()` to define a table with field builders:
|
|
135
148
|
|
|
136
149
|
```typescript
|
|
137
150
|
import { z } from "zod/v4";
|
|
138
|
-
import {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
import {
|
|
152
|
+
fmTableOccurrence,
|
|
153
|
+
textField,
|
|
154
|
+
numberField,
|
|
155
|
+
timestampField,
|
|
156
|
+
} from "@proofkit/fmodata";
|
|
157
|
+
|
|
158
|
+
const contacts = fmTableOccurrence(
|
|
159
|
+
"contacts",
|
|
160
|
+
{
|
|
161
|
+
id: textField().primaryKey().entityId("FMFID:1"),
|
|
162
|
+
name: textField().notNull().entityId("FMFID:2"),
|
|
163
|
+
email: textField().notNull().entityId("FMFID:3"),
|
|
164
|
+
phone: textField().entityId("FMFID:4"), // Optional (nullable by default)
|
|
165
|
+
createdAt: timestampField().readOnly().entityId("FMFID:5"),
|
|
147
166
|
},
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
167
|
+
{
|
|
168
|
+
entityId: "FMTID:100", // Optional: FileMaker table occurrence ID
|
|
169
|
+
defaultSelect: "schema", // Optional: "all", "schema", or function. Defaults to "schema".
|
|
170
|
+
navigationPaths: ["users"], // Optional: valid navigation targets to provide type-errors when navigating/expanding
|
|
171
|
+
},
|
|
172
|
+
);
|
|
152
173
|
```
|
|
153
174
|
|
|
154
|
-
|
|
175
|
+
The function returns a table object that provides:
|
|
155
176
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const contactsTO = defineTableOccurrence({
|
|
160
|
-
name: "contacts", // The table occurrence name in FileMaker
|
|
161
|
-
baseTable: contactsBase,
|
|
162
|
-
});
|
|
163
|
-
```
|
|
177
|
+
- Column references for each field (e.g., `contacts.id`, `contacts.name`)
|
|
178
|
+
- Type-safe schema for queries and operations
|
|
179
|
+
- Metadata stored via Symbols (hidden from IDE autocomplete)
|
|
164
180
|
|
|
165
181
|
#### Default Field Selection
|
|
166
182
|
|
|
167
|
-
FileMaker will automatically return all non-container fields from a schema if you don't specify a $select parameter in your query. This library
|
|
183
|
+
FileMaker will automatically return all non-container fields from a schema if you don't specify a $select parameter in your query. This library allows you to configure default field selection behavior using the `defaultSelect` option:
|
|
168
184
|
|
|
169
185
|
```typescript
|
|
170
|
-
// Option 1 (default): "schema" - Select all fields from the schema
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
186
|
+
// Option 1 (default): "schema" - Select all fields from the schema
|
|
187
|
+
const users = fmTableOccurrence(
|
|
188
|
+
"users",
|
|
189
|
+
{
|
|
190
|
+
/* fields */
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
defaultSelect: "schema", // A $select parameter will always be added for only the fields defined in the schema
|
|
194
|
+
},
|
|
195
|
+
);
|
|
176
196
|
|
|
177
|
-
// Option 2: "all" - Select all fields (default behavior)
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
197
|
+
// Option 2: "all" - Select all fields (FileMaker default behavior)
|
|
198
|
+
const users = fmTableOccurrence(
|
|
199
|
+
"users",
|
|
200
|
+
{
|
|
201
|
+
/* fields */
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
defaultSelect: "all", // No $select parameter by default; FileMaker returns all non-container fields
|
|
205
|
+
},
|
|
206
|
+
);
|
|
183
207
|
|
|
184
|
-
// Option 3:
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
208
|
+
// Option 3: Function - Select specific columns by default
|
|
209
|
+
const users = fmTableOccurrence(
|
|
210
|
+
"users",
|
|
211
|
+
{
|
|
212
|
+
/* fields */
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
defaultSelect: (cols) => ({
|
|
216
|
+
username: cols.username,
|
|
217
|
+
email: cols.email,
|
|
218
|
+
}), // Only select these fields by default
|
|
219
|
+
},
|
|
220
|
+
);
|
|
190
221
|
|
|
191
|
-
// When you call list(), the defaultSelect is applied automatically
|
|
192
|
-
const result = await db.from(
|
|
193
|
-
// If defaultSelect is
|
|
222
|
+
// When you call list() or get(), the defaultSelect is applied automatically
|
|
223
|
+
const result = await db.from(users).list().execute();
|
|
224
|
+
// If defaultSelect is a function returning { username, email }, result.data will only contain those fields
|
|
194
225
|
|
|
195
226
|
// You can still override with explicit select()
|
|
196
227
|
const result = await db
|
|
197
|
-
.from(
|
|
228
|
+
.from(users)
|
|
198
229
|
.list()
|
|
199
|
-
.select(
|
|
230
|
+
.select({ username: users.username, email: users.email, age: users.age }) // Always overrides at the per-request level
|
|
200
231
|
.execute();
|
|
201
232
|
```
|
|
202
233
|
|
|
203
|
-
Lastly, you can combine all table occurrences into a database instance for the full type-safe experience. This is a method on the main `FMServerConnection` client class.
|
|
204
|
-
|
|
205
|
-
```typescript
|
|
206
|
-
const db = connection.database("MyDatabase.fmp12", {
|
|
207
|
-
occurrences: [contactsTO, usersTO], // Register your table occurrences
|
|
208
|
-
});
|
|
209
|
-
```
|
|
210
|
-
|
|
211
234
|
## Querying Data
|
|
212
235
|
|
|
213
236
|
### Basic Queries
|
|
@@ -251,11 +274,60 @@ if (result.data) {
|
|
|
251
274
|
|
|
252
275
|
### Filtering
|
|
253
276
|
|
|
254
|
-
fmodata provides type-safe filter operations that prevent common errors at compile time.
|
|
277
|
+
fmodata provides type-safe filter operations that prevent common errors at compile time. You can use either the new ORM-style API with operators and column references, or the legacy filter API.
|
|
278
|
+
|
|
279
|
+
#### New ORM-Style API (Recommended)
|
|
280
|
+
|
|
281
|
+
Use the `where()` method with filter operators and column references for type-safe filtering:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
import { eq, gt, and, or, contains } from "@proofkit/fmodata";
|
|
285
|
+
|
|
286
|
+
// Simple equality
|
|
287
|
+
const result = await db
|
|
288
|
+
.from(users)
|
|
289
|
+
.list()
|
|
290
|
+
.where(eq(users.active, true))
|
|
291
|
+
.execute();
|
|
292
|
+
|
|
293
|
+
// Comparison operators
|
|
294
|
+
const result = await db.from(users).list().where(gt(users.age, 18)).execute();
|
|
295
|
+
|
|
296
|
+
// String operators
|
|
297
|
+
const result = await db
|
|
298
|
+
.from(users)
|
|
299
|
+
.list()
|
|
300
|
+
.where(contains(users.name, "John"))
|
|
301
|
+
.execute();
|
|
302
|
+
|
|
303
|
+
// Combine with AND
|
|
304
|
+
const result = await db
|
|
305
|
+
.from(users)
|
|
306
|
+
.list()
|
|
307
|
+
.where(and(eq(users.active, true), gt(users.age, 18)))
|
|
308
|
+
.execute();
|
|
309
|
+
|
|
310
|
+
// Combine with OR
|
|
311
|
+
const result = await db
|
|
312
|
+
.from(users)
|
|
313
|
+
.list()
|
|
314
|
+
.where(or(eq(users.role, "admin"), eq(users.role, "moderator")))
|
|
315
|
+
.execute();
|
|
316
|
+
```
|
|
255
317
|
|
|
256
|
-
|
|
318
|
+
Available operators:
|
|
257
319
|
|
|
258
|
-
|
|
320
|
+
- **Comparison**: `eq()`, `ne()`, `gt()`, `gte()`, `lt()`, `lte()`
|
|
321
|
+
- **String**: `contains()`, `startsWith()`, `endsWith()`
|
|
322
|
+
- **Array**: `inArray()`, `notInArray()`
|
|
323
|
+
- **Null**: `isNull()`, `isNotNull()`
|
|
324
|
+
- **Logical**: `and()`, `or()`, `not()`
|
|
325
|
+
|
|
326
|
+
#### Legacy Filter API (DO NOT USE, will be removed shortly)
|
|
327
|
+
|
|
328
|
+
The filter system supports three syntaxes: shorthand, single operator objects, and arrays for multiple operators.
|
|
329
|
+
|
|
330
|
+
You can use the legacy `filter()` method in three ways:
|
|
259
331
|
|
|
260
332
|
**1. Shorthand (direct value):**
|
|
261
333
|
|
|
@@ -403,21 +475,52 @@ const result = await db
|
|
|
403
475
|
|
|
404
476
|
### Sorting
|
|
405
477
|
|
|
406
|
-
Sort results using `orderBy()
|
|
478
|
+
Sort results using `orderBy()`. The method supports both column references (new ORM API) and string field names (legacy API).
|
|
479
|
+
|
|
480
|
+
#### Using Column References (New ORM API)
|
|
407
481
|
|
|
408
482
|
```typescript
|
|
409
|
-
|
|
410
|
-
const result = await db.from("users").list().orderBy("name").execute();
|
|
483
|
+
import { asc, desc } from "@proofkit/fmodata";
|
|
411
484
|
|
|
412
|
-
//
|
|
413
|
-
const result = await db.from(
|
|
485
|
+
// Single field (ascending by default)
|
|
486
|
+
const result = await db.from(users).list().orderBy(users.name).execute();
|
|
414
487
|
|
|
415
|
-
//
|
|
488
|
+
// Single field with explicit direction
|
|
489
|
+
const result = await db.from(users).list().orderBy(asc(users.name)).execute();
|
|
490
|
+
const result = await db.from(users).list().orderBy(desc(users.age)).execute();
|
|
491
|
+
|
|
492
|
+
// Multiple fields (variadic)
|
|
416
493
|
const result = await db
|
|
417
|
-
.from(
|
|
494
|
+
.from(users)
|
|
418
495
|
.list()
|
|
419
|
-
.orderBy(
|
|
496
|
+
.orderBy(asc(users.lastName), desc(users.firstName))
|
|
420
497
|
.execute();
|
|
498
|
+
|
|
499
|
+
// Multiple fields (array syntax)
|
|
500
|
+
const result = await db
|
|
501
|
+
.from(users)
|
|
502
|
+
.list()
|
|
503
|
+
.orderBy([
|
|
504
|
+
[users.lastName, "asc"],
|
|
505
|
+
[users.firstName, "desc"],
|
|
506
|
+
])
|
|
507
|
+
.execute();
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
#### Type Safety
|
|
511
|
+
|
|
512
|
+
For typed databases, `orderBy()` provides full type safety:
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
// ✅ Valid - "name" is a field in the schema
|
|
516
|
+
db.from(users).list().orderBy(users.name);
|
|
517
|
+
|
|
518
|
+
// ✅ Valid - tuple with field and direction
|
|
519
|
+
db.from(users).list().orderBy(asc(users.name));
|
|
520
|
+
db.from(users).list().orderBy(desc(users.name));
|
|
521
|
+
|
|
522
|
+
// ✅ Valid - multiple fields
|
|
523
|
+
db.from(users).list().orderBy(asc(users.lastName), desc(users.firstName));
|
|
421
524
|
```
|
|
422
525
|
|
|
423
526
|
### Pagination
|
|
@@ -426,24 +529,29 @@ Control the number of records returned and pagination:
|
|
|
426
529
|
|
|
427
530
|
```typescript
|
|
428
531
|
// Limit results
|
|
429
|
-
const result = await db.from(
|
|
532
|
+
const result = await db.from(users).list().top(10).execute();
|
|
430
533
|
|
|
431
534
|
// Skip records (pagination)
|
|
432
|
-
const result = await db.from(
|
|
535
|
+
const result = await db.from(users).list().top(10).skip(20).execute();
|
|
433
536
|
|
|
434
537
|
// Count total records
|
|
435
|
-
const result = await db.from(
|
|
538
|
+
const result = await db.from(users).list().count().execute();
|
|
436
539
|
```
|
|
437
540
|
|
|
438
541
|
### Selecting Fields
|
|
439
542
|
|
|
440
|
-
Select specific fields to return:
|
|
543
|
+
Select specific fields to return. You can use either column references (new ORM API) or string field names (legacy API):
|
|
441
544
|
|
|
442
545
|
```typescript
|
|
546
|
+
// New ORM API: Using column references (type-safe, supports renaming)
|
|
443
547
|
const result = await db
|
|
444
|
-
.from(
|
|
548
|
+
.from(users)
|
|
445
549
|
.list()
|
|
446
|
-
.select(
|
|
550
|
+
.select({
|
|
551
|
+
username: users.username,
|
|
552
|
+
email: users.email,
|
|
553
|
+
userId: users.id, // Renamed from "id" to "userId"
|
|
554
|
+
})
|
|
447
555
|
.execute();
|
|
448
556
|
|
|
449
557
|
// result.data[0] will only have username and email fields
|
|
@@ -455,9 +563,9 @@ Use `single()` to ensure exactly one record is returned (returns an error if zer
|
|
|
455
563
|
|
|
456
564
|
```typescript
|
|
457
565
|
const result = await db
|
|
458
|
-
.from(
|
|
566
|
+
.from(users)
|
|
459
567
|
.list()
|
|
460
|
-
.filter(
|
|
568
|
+
.filter(eq(users.email, "user@example.com"))
|
|
461
569
|
.single()
|
|
462
570
|
.execute();
|
|
463
571
|
|
|
@@ -471,9 +579,9 @@ Use `maybeSingle()` when you want at most one record (returns `null` if no recor
|
|
|
471
579
|
|
|
472
580
|
```typescript
|
|
473
581
|
const result = await db
|
|
474
|
-
.from(
|
|
582
|
+
.from(users)
|
|
475
583
|
.list()
|
|
476
|
-
.filter(
|
|
584
|
+
.filter(eq(users.email, "user@example.com"))
|
|
477
585
|
.maybeSingle()
|
|
478
586
|
.execute();
|
|
479
587
|
|
|
@@ -496,6 +604,22 @@ if (result.data) {
|
|
|
496
604
|
All query methods can be chained together:
|
|
497
605
|
|
|
498
606
|
```typescript
|
|
607
|
+
// Using new ORM API
|
|
608
|
+
const result = await db
|
|
609
|
+
.from(users)
|
|
610
|
+
.list()
|
|
611
|
+
.select({
|
|
612
|
+
username: users.username,
|
|
613
|
+
email: users.email,
|
|
614
|
+
age: users.age,
|
|
615
|
+
})
|
|
616
|
+
.where(gt(users.age, 18))
|
|
617
|
+
.orderBy(asc(users.username))
|
|
618
|
+
.top(10)
|
|
619
|
+
.skip(0)
|
|
620
|
+
.execute();
|
|
621
|
+
|
|
622
|
+
// Using legacy API
|
|
499
623
|
const result = await db
|
|
500
624
|
.from("users")
|
|
501
625
|
.list()
|
|
@@ -516,7 +640,7 @@ Insert new records with type-safe data:
|
|
|
516
640
|
```typescript
|
|
517
641
|
// Insert a new user
|
|
518
642
|
const result = await db
|
|
519
|
-
.from(
|
|
643
|
+
.from(users)
|
|
520
644
|
.insert({
|
|
521
645
|
username: "johndoe",
|
|
522
646
|
email: "john@example.com",
|
|
@@ -529,30 +653,25 @@ if (result.data) {
|
|
|
529
653
|
}
|
|
530
654
|
```
|
|
531
655
|
|
|
532
|
-
Fields are automatically required for insert if
|
|
656
|
+
Fields are automatically required for insert if they use `.notNull()`. Read-only fields (including primary keys) are automatically excluded:
|
|
533
657
|
|
|
534
658
|
```typescript
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
createdAt: z.string(), // Auto-required, but excluded (readOnly)
|
|
542
|
-
},
|
|
543
|
-
idField: "id", // Automatically excluded from insert/update
|
|
544
|
-
required: ["phone"], // Make phone required for inserts despite being nullable
|
|
545
|
-
readOnly: ["createdAt"], // Exclude from insert/update operations
|
|
659
|
+
const users = fmTableOccurrence("users", {
|
|
660
|
+
id: textField().primaryKey(), // Auto-required, but excluded from insert (primaryKey)
|
|
661
|
+
username: textField().notNull(), // Auto-required (notNull)
|
|
662
|
+
email: textField().notNull(), // Auto-required (notNull)
|
|
663
|
+
phone: textField(), // Optional by default (nullable)
|
|
664
|
+
createdAt: timestampField().readOnly(), // Excluded from insert/update
|
|
546
665
|
});
|
|
547
666
|
|
|
548
|
-
// TypeScript enforces: username
|
|
667
|
+
// TypeScript enforces: username and email are required
|
|
549
668
|
// TypeScript excludes: id and createdAt cannot be provided
|
|
550
669
|
const result = await db
|
|
551
|
-
.from(
|
|
670
|
+
.from(users)
|
|
552
671
|
.insert({
|
|
553
672
|
username: "johndoe",
|
|
554
673
|
email: "john@example.com",
|
|
555
|
-
phone: "+1234567890", //
|
|
674
|
+
phone: "+1234567890", // Optional
|
|
556
675
|
})
|
|
557
676
|
.execute();
|
|
558
677
|
```
|
|
@@ -564,7 +683,7 @@ Update records by ID or filter:
|
|
|
564
683
|
```typescript
|
|
565
684
|
// Update by ID
|
|
566
685
|
const result = await db
|
|
567
|
-
.from(
|
|
686
|
+
.from(users)
|
|
568
687
|
.update({ username: "newname" })
|
|
569
688
|
.byId("user-123")
|
|
570
689
|
.execute();
|
|
@@ -573,25 +692,23 @@ if (result.data) {
|
|
|
573
692
|
console.log(`Updated ${result.data.updatedCount} record(s)`);
|
|
574
693
|
}
|
|
575
694
|
|
|
576
|
-
// Update by filter
|
|
695
|
+
// Update by filter (using new ORM API)
|
|
696
|
+
import { lt, and, eq } from "@proofkit/fmodata";
|
|
697
|
+
|
|
577
698
|
const result = await db
|
|
578
|
-
.from(
|
|
699
|
+
.from(users)
|
|
579
700
|
.update({ active: false })
|
|
580
|
-
.where((
|
|
701
|
+
.where(lt(users.lastLogin, "2023-01-01"))
|
|
581
702
|
.execute();
|
|
582
703
|
|
|
583
704
|
// Complex filter example
|
|
584
705
|
const result = await db
|
|
585
|
-
.from(
|
|
706
|
+
.from(users)
|
|
586
707
|
.update({ active: false })
|
|
587
|
-
.where((
|
|
588
|
-
q.filter({
|
|
589
|
-
and: [{ active: true }, { count: { lt: 5 } }],
|
|
590
|
-
}),
|
|
591
|
-
)
|
|
708
|
+
.where(and(eq(users.active, true), lt(users.count, 5)))
|
|
592
709
|
.execute();
|
|
593
710
|
|
|
594
|
-
// Update with additional query options
|
|
711
|
+
// Update with additional query options (legacy filter API)
|
|
595
712
|
const result = await db
|
|
596
713
|
.from("users")
|
|
597
714
|
.update({ active: false })
|
|
@@ -605,28 +722,26 @@ Delete records by ID or filter:
|
|
|
605
722
|
|
|
606
723
|
```typescript
|
|
607
724
|
// Delete by ID
|
|
608
|
-
const result = await db.from(
|
|
725
|
+
const result = await db.from(users).delete().byId("user-123").execute();
|
|
609
726
|
|
|
610
727
|
if (result.data) {
|
|
611
728
|
console.log(`Deleted ${result.data.deletedCount} record(s)`);
|
|
612
729
|
}
|
|
613
730
|
|
|
614
|
-
// Delete by filter
|
|
731
|
+
// Delete by filter (using new ORM API)
|
|
732
|
+
import { eq, and, lt } from "@proofkit/fmodata";
|
|
733
|
+
|
|
615
734
|
const result = await db
|
|
616
|
-
.from(
|
|
735
|
+
.from(users)
|
|
617
736
|
.delete()
|
|
618
|
-
.where((
|
|
737
|
+
.where(eq(users.active, false))
|
|
619
738
|
.execute();
|
|
620
739
|
|
|
621
740
|
// Delete with complex filters
|
|
622
741
|
const result = await db
|
|
623
|
-
.from(
|
|
742
|
+
.from(users)
|
|
624
743
|
.delete()
|
|
625
|
-
.where((
|
|
626
|
-
q.filter({
|
|
627
|
-
and: [{ active: false }, { lastLogin: { lt: "2023-01-01" } }],
|
|
628
|
-
}),
|
|
629
|
-
)
|
|
744
|
+
.where(and(eq(users.active, false), lt(users.lastLogin, "2023-01-01")))
|
|
630
745
|
.execute();
|
|
631
746
|
```
|
|
632
747
|
|
|
@@ -634,148 +749,145 @@ const result = await db
|
|
|
634
749
|
|
|
635
750
|
### Defining Navigation
|
|
636
751
|
|
|
637
|
-
|
|
752
|
+
Define navigation relationships using the `navigationPaths` option when creating table occurrences:
|
|
638
753
|
|
|
639
754
|
```typescript
|
|
640
|
-
import {
|
|
641
|
-
defineBaseTable,
|
|
642
|
-
defineTableOccurrence,
|
|
643
|
-
buildOccurrences,
|
|
644
|
-
} from "@proofkit/fmodata";
|
|
755
|
+
import { fmTableOccurrence, textField } from "@proofkit/fmodata";
|
|
645
756
|
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
757
|
+
const contacts = fmTableOccurrence(
|
|
758
|
+
"contacts",
|
|
759
|
+
{
|
|
760
|
+
id: textField().primaryKey(),
|
|
761
|
+
name: textField().notNull(),
|
|
762
|
+
userId: textField().notNull(),
|
|
651
763
|
},
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
const usersBase = defineBaseTable({
|
|
656
|
-
schema: {
|
|
657
|
-
id: z.string(),
|
|
658
|
-
username: z.string(),
|
|
659
|
-
email: z.string(),
|
|
764
|
+
{
|
|
765
|
+
navigationPaths: ["users"], // Valid navigation targets
|
|
660
766
|
},
|
|
661
|
-
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
// Step 1: Define base table occurrences (without navigation)
|
|
665
|
-
const _contactsTO = defineTableOccurrence({
|
|
666
|
-
name: "contacts",
|
|
667
|
-
baseTable: contactsBase,
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
const _usersTO = defineTableOccurrence({
|
|
671
|
-
name: "users",
|
|
672
|
-
baseTable: usersBase,
|
|
673
|
-
});
|
|
767
|
+
);
|
|
674
768
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
users: ["contacts"],
|
|
769
|
+
const users = fmTableOccurrence(
|
|
770
|
+
"users",
|
|
771
|
+
{
|
|
772
|
+
id: textField().primaryKey(),
|
|
773
|
+
username: textField().notNull(),
|
|
774
|
+
email: textField().notNull(),
|
|
682
775
|
},
|
|
683
|
-
|
|
776
|
+
{
|
|
777
|
+
navigationPaths: ["contacts"], // Valid navigation targets
|
|
778
|
+
},
|
|
779
|
+
);
|
|
684
780
|
|
|
685
781
|
// Use with your database
|
|
686
782
|
const db = connection.database("MyDB", {
|
|
687
|
-
occurrences:
|
|
783
|
+
occurrences: [contacts, users],
|
|
688
784
|
});
|
|
689
785
|
```
|
|
690
786
|
|
|
691
|
-
The `
|
|
787
|
+
The `navigationPaths` option:
|
|
692
788
|
|
|
693
|
-
-
|
|
694
|
-
-
|
|
695
|
-
|
|
696
|
-
It returns a tuple in the same order as the input array, with full autocomplete for navigation target names. Self-navigation is prevented at the type level.
|
|
697
|
-
|
|
698
|
-
- Handles circular references automatically
|
|
699
|
-
- Returns fully typed `TableOccurrence` instances with resolved navigation
|
|
789
|
+
- Specifies which table occurrences can be navigated to from this table
|
|
790
|
+
- Enables runtime validation when using `expand()` or `navigate()`
|
|
791
|
+
- Throws descriptive errors if you try to navigate to an invalid path
|
|
700
792
|
|
|
701
793
|
### Navigating Between Tables
|
|
702
794
|
|
|
703
795
|
Navigate to related records:
|
|
704
796
|
|
|
705
797
|
```typescript
|
|
706
|
-
// Navigate from a specific record
|
|
798
|
+
// Navigate from a specific record (using column references)
|
|
707
799
|
const result = await db
|
|
708
|
-
.from(
|
|
800
|
+
.from(contacts)
|
|
709
801
|
.get("contact-123")
|
|
710
|
-
.navigate(
|
|
711
|
-
.select(
|
|
802
|
+
.navigate(users)
|
|
803
|
+
.select({
|
|
804
|
+
username: users.username,
|
|
805
|
+
email: users.email,
|
|
806
|
+
})
|
|
712
807
|
.execute();
|
|
713
808
|
|
|
714
809
|
// Navigate without specifying a record first
|
|
715
|
-
const result = await db.from(
|
|
810
|
+
const result = await db.from(contacts).navigate(users).list().execute();
|
|
716
811
|
|
|
717
|
-
//
|
|
812
|
+
// Using legacy API with string field names
|
|
718
813
|
const result = await db
|
|
719
|
-
.from(
|
|
720
|
-
.
|
|
721
|
-
.
|
|
814
|
+
.from(contacts)
|
|
815
|
+
.get("contact-123")
|
|
816
|
+
.navigate(users)
|
|
817
|
+
.select({ username: users.username, email: users.email })
|
|
722
818
|
.execute();
|
|
723
819
|
```
|
|
724
820
|
|
|
725
821
|
### Expanding Related Records
|
|
726
822
|
|
|
727
|
-
Use `expand()` to include related records in your query results
|
|
823
|
+
Use `expand()` to include related records in your query results. The library validates that the target table is in the source table's `navigationPaths`:
|
|
728
824
|
|
|
729
825
|
```typescript
|
|
730
826
|
// Simple expand
|
|
731
|
-
const result = await db.from(
|
|
827
|
+
const result = await db.from(contacts).list().expand(users).execute();
|
|
732
828
|
|
|
733
|
-
// Expand with field selection
|
|
829
|
+
// Expand with field selection (using column references)
|
|
734
830
|
const result = await db
|
|
735
|
-
.from(
|
|
831
|
+
.from(contacts)
|
|
736
832
|
.list()
|
|
737
|
-
.expand(
|
|
833
|
+
.expand(users, (b) =>
|
|
834
|
+
b.select({
|
|
835
|
+
username: users.username,
|
|
836
|
+
email: users.email,
|
|
837
|
+
}),
|
|
838
|
+
)
|
|
738
839
|
.execute();
|
|
739
840
|
|
|
740
|
-
// Expand with filtering
|
|
841
|
+
// Expand with filtering (using new ORM API)
|
|
842
|
+
import { eq } from "@proofkit/fmodata";
|
|
843
|
+
|
|
741
844
|
const result = await db
|
|
742
|
-
.from(
|
|
845
|
+
.from(contacts)
|
|
743
846
|
.list()
|
|
744
|
-
.expand(
|
|
847
|
+
.expand(users, (b) => b.where(eq(users.active, true)))
|
|
745
848
|
.execute();
|
|
746
849
|
|
|
747
850
|
// Multiple expands
|
|
748
851
|
const result = await db
|
|
749
|
-
.from(
|
|
852
|
+
.from(contacts)
|
|
750
853
|
.list()
|
|
751
|
-
.expand(
|
|
752
|
-
.expand(
|
|
854
|
+
.expand(users, (b) => b.select({ username: users.username }))
|
|
855
|
+
.expand(orders, (b) => b.select({ total: orders.total }).top(5))
|
|
753
856
|
.execute();
|
|
754
857
|
|
|
755
858
|
// Nested expands
|
|
756
859
|
const result = await db
|
|
757
|
-
.from(
|
|
860
|
+
.from(contacts)
|
|
758
861
|
.list()
|
|
759
|
-
.expand(
|
|
862
|
+
.expand(users, (usersBuilder) =>
|
|
760
863
|
usersBuilder
|
|
761
|
-
.select(
|
|
762
|
-
|
|
763
|
-
|
|
864
|
+
.select({
|
|
865
|
+
username: users.username,
|
|
866
|
+
email: users.email,
|
|
867
|
+
})
|
|
868
|
+
.expand(customers, (customerBuilder) =>
|
|
869
|
+
customerBuilder.select({
|
|
870
|
+
name: customers.name,
|
|
871
|
+
tier: customers.tier,
|
|
872
|
+
}),
|
|
764
873
|
),
|
|
765
874
|
)
|
|
766
875
|
.execute();
|
|
767
876
|
|
|
768
877
|
// Complex expand with multiple options
|
|
769
878
|
const result = await db
|
|
770
|
-
.from(
|
|
879
|
+
.from(contacts)
|
|
771
880
|
.list()
|
|
772
|
-
.expand(
|
|
881
|
+
.expand(users, (b) =>
|
|
773
882
|
b
|
|
774
|
-
.select(
|
|
775
|
-
|
|
776
|
-
|
|
883
|
+
.select({
|
|
884
|
+
username: users.username,
|
|
885
|
+
email: users.email,
|
|
886
|
+
})
|
|
887
|
+
.where(eq(users.active, true))
|
|
888
|
+
.orderBy(asc(users.username))
|
|
777
889
|
.top(10)
|
|
778
|
-
.expand(
|
|
890
|
+
.expand(customers, (nested) => nested.select({ name: customers.name })),
|
|
779
891
|
)
|
|
780
892
|
.execute();
|
|
781
893
|
```
|
|
@@ -831,25 +943,55 @@ console.log(result.result.recordId);
|
|
|
831
943
|
|
|
832
944
|
Batch operations allow you to execute multiple queries and operations together in a single request. All operations in a batch are executed atomically - they all succeed or all fail together. This is both more efficient (fewer network round-trips) and ensures data consistency across related operations.
|
|
833
945
|
|
|
946
|
+
### Batch Result Structure
|
|
947
|
+
|
|
948
|
+
Batch operations return a `BatchResult` object that contains individual results for each operation. Each result has its own `data`, `error`, and `status` properties, allowing you to handle success and failure on a per-operation basis:
|
|
949
|
+
|
|
950
|
+
```typescript
|
|
951
|
+
type BatchItemResult<T> = {
|
|
952
|
+
data: T | undefined;
|
|
953
|
+
error: FMODataErrorType | undefined;
|
|
954
|
+
status: number; // HTTP status code (0 for truncated operations)
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
type BatchResult<T extends readonly any[]> = {
|
|
958
|
+
results: { [K in keyof T]: BatchItemResult<T[K]> };
|
|
959
|
+
successCount: number;
|
|
960
|
+
errorCount: number;
|
|
961
|
+
truncated: boolean; // true if FileMaker stopped processing due to an error
|
|
962
|
+
firstErrorIndex: number | null; // Index of the first operation that failed
|
|
963
|
+
};
|
|
964
|
+
```
|
|
965
|
+
|
|
834
966
|
### Basic Batch with Multiple Queries
|
|
835
967
|
|
|
836
968
|
Execute multiple read operations in a single batch:
|
|
837
969
|
|
|
838
970
|
```typescript
|
|
839
971
|
// Create query builders
|
|
840
|
-
const contactsQuery = db.from(
|
|
841
|
-
const usersQuery = db.from(
|
|
972
|
+
const contactsQuery = db.from(contacts).list().top(5);
|
|
973
|
+
const usersQuery = db.from(users).list().top(5);
|
|
842
974
|
|
|
843
975
|
// Execute both queries in a single batch
|
|
844
976
|
const result = await db.batch([contactsQuery, usersQuery]).execute();
|
|
845
977
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
const [contacts, users] = result.data;
|
|
978
|
+
// Access individual results
|
|
979
|
+
const [r1, r2] = result.results;
|
|
849
980
|
|
|
850
|
-
|
|
851
|
-
console.
|
|
981
|
+
if (r1.error) {
|
|
982
|
+
console.error("Contacts query failed:", r1.error);
|
|
983
|
+
} else {
|
|
984
|
+
console.log("Contacts:", r1.data);
|
|
852
985
|
}
|
|
986
|
+
|
|
987
|
+
if (r2.error) {
|
|
988
|
+
console.error("Users query failed:", r2.error);
|
|
989
|
+
} else {
|
|
990
|
+
console.log("Users:", r2.data);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Check summary statistics
|
|
994
|
+
console.log(`Success: ${result.successCount}, Errors: ${result.errorCount}`);
|
|
853
995
|
```
|
|
854
996
|
|
|
855
997
|
### Mixed Operations (Reads and Writes)
|
|
@@ -858,22 +1000,73 @@ Combine queries, inserts, updates, and deletes in a single batch:
|
|
|
858
1000
|
|
|
859
1001
|
```typescript
|
|
860
1002
|
// Mix different operation types
|
|
861
|
-
const listQuery = db.from(
|
|
862
|
-
const insertOp = db.from(
|
|
1003
|
+
const listQuery = db.from(contacts).list().top(10);
|
|
1004
|
+
const insertOp = db.from(contacts).insert({
|
|
863
1005
|
name: "John Doe",
|
|
864
1006
|
email: "john@example.com",
|
|
865
1007
|
});
|
|
866
|
-
const updateOp = db.from(
|
|
1008
|
+
const updateOp = db.from(users).update({ active: true }).byId("user-123");
|
|
867
1009
|
|
|
868
1010
|
// All operations execute atomically
|
|
869
1011
|
const result = await db.batch([listQuery, insertOp, updateOp]).execute();
|
|
870
1012
|
|
|
871
|
-
|
|
872
|
-
|
|
1013
|
+
// Access individual results
|
|
1014
|
+
const [r1, r2, r3] = result.results;
|
|
873
1015
|
|
|
874
|
-
|
|
875
|
-
console.
|
|
876
|
-
|
|
1016
|
+
if (r1.error) {
|
|
1017
|
+
console.error("List query failed:", r1.error);
|
|
1018
|
+
} else {
|
|
1019
|
+
console.log("Fetched contacts:", r1.data);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (r2.error) {
|
|
1023
|
+
console.error("Insert failed:", r2.error);
|
|
1024
|
+
} else {
|
|
1025
|
+
console.log("Inserted contact:", r2.data);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (r3.error) {
|
|
1029
|
+
console.error("Update failed:", r3.error);
|
|
1030
|
+
} else {
|
|
1031
|
+
console.log("Updated user:", r3.data);
|
|
1032
|
+
}
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
### Handling Errors in Batches
|
|
1036
|
+
|
|
1037
|
+
When FileMaker encounters an error in a batch operation, it **stops processing** subsequent operations. Operations that were never executed due to an earlier error will have a `BatchTruncatedError`:
|
|
1038
|
+
|
|
1039
|
+
```typescript
|
|
1040
|
+
import { BatchTruncatedError, isBatchTruncatedError } from "@proofkit/fmodata";
|
|
1041
|
+
|
|
1042
|
+
const result = await db.batch([query1, query2, query3]).execute();
|
|
1043
|
+
|
|
1044
|
+
const [r1, r2, r3] = result.results;
|
|
1045
|
+
|
|
1046
|
+
// First operation succeeded
|
|
1047
|
+
if (r1.error) {
|
|
1048
|
+
console.error("First query failed:", r1.error);
|
|
1049
|
+
} else {
|
|
1050
|
+
console.log("First query succeeded:", r1.data);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// Second operation failed
|
|
1054
|
+
if (r2.error) {
|
|
1055
|
+
console.error("Second query failed:", r2.error);
|
|
1056
|
+
console.log("HTTP Status:", r2.status); // e.g., 404
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Third operation was never executed (truncated)
|
|
1060
|
+
if (r3.error && isBatchTruncatedError(r3.error)) {
|
|
1061
|
+
console.log("Third operation was not executed");
|
|
1062
|
+
console.log(`Failed at operation ${r3.error.failedAtIndex}`);
|
|
1063
|
+
console.log(`This operation index: ${r3.error.operationIndex}`);
|
|
1064
|
+
console.log("Status:", r3.status); // 0 (never executed)
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Check if batch was truncated
|
|
1068
|
+
if (result.truncated) {
|
|
1069
|
+
console.log(`Batch stopped early at index ${result.firstErrorIndex}`);
|
|
877
1070
|
}
|
|
878
1071
|
```
|
|
879
1072
|
|
|
@@ -884,18 +1077,31 @@ Batch operations are transactional for write operations (inserts, updates, delet
|
|
|
884
1077
|
```typescript
|
|
885
1078
|
const result = await db
|
|
886
1079
|
.batch([
|
|
887
|
-
db.from(
|
|
888
|
-
db.from(
|
|
889
|
-
db.from(
|
|
1080
|
+
db.from(users).insert({ username: "alice", email: "alice@example.com" }),
|
|
1081
|
+
db.from(users).insert({ username: "bob", email: "bob@example.com" }),
|
|
1082
|
+
db.from(users).insert({ username: "charlie", email: "invalid" }), // This fails
|
|
890
1083
|
])
|
|
891
1084
|
.execute();
|
|
892
1085
|
|
|
893
|
-
|
|
1086
|
+
// Check individual results
|
|
1087
|
+
const [r1, r2, r3] = result.results;
|
|
1088
|
+
|
|
1089
|
+
if (r1.error || r2.error || r3.error) {
|
|
894
1090
|
// All three inserts are rolled back - no users were created
|
|
895
|
-
console.error("Batch
|
|
1091
|
+
console.error("Batch had errors:");
|
|
1092
|
+
if (r1.error) console.error("Operation 1:", r1.error);
|
|
1093
|
+
if (r2.error) console.error("Operation 2:", r2.error);
|
|
1094
|
+
if (r3.error) console.error("Operation 3:", r3.error);
|
|
896
1095
|
}
|
|
897
1096
|
```
|
|
898
1097
|
|
|
1098
|
+
### Important Notes
|
|
1099
|
+
|
|
1100
|
+
- **FileMaker stops on first error**: When an error occurs, FileMaker stops processing subsequent operations in the batch. Truncated operations will have `BatchTruncatedError` with `status: 0`.
|
|
1101
|
+
- **Insert operations in batches**: FileMaker ignores `Prefer: return=representation` in batch requests. Insert operations return `{}` or `{ ROWID?: number }` instead of the full created record.
|
|
1102
|
+
- **All results are always defined**: Every operation in the batch will have a corresponding result in `result.results`, even if it was never executed (truncated operations).
|
|
1103
|
+
- **Summary statistics**: Use `result.successCount`, `result.errorCount`, `result.truncated`, and `result.firstErrorIndex` for quick batch status checks.
|
|
1104
|
+
|
|
899
1105
|
**Note:** Batch operations automatically group write operations (POST, PATCH, DELETE) into changesets for transactional behavior, while read operations (GET) are executed individually within the batch.
|
|
900
1106
|
|
|
901
1107
|
## Schema Management
|
|
@@ -1157,94 +1363,50 @@ await db.schema.createIndex("users", "email");
|
|
|
1157
1363
|
|
|
1158
1364
|
## Advanced Features
|
|
1159
1365
|
|
|
1160
|
-
### Type Safety
|
|
1161
|
-
|
|
1162
|
-
The library provides full TypeScript type inference:
|
|
1163
|
-
|
|
1164
|
-
```typescript
|
|
1165
|
-
const usersBase = defineBaseTable({
|
|
1166
|
-
schema: {
|
|
1167
|
-
id: z.string(),
|
|
1168
|
-
username: z.string(),
|
|
1169
|
-
email: z.string(),
|
|
1170
|
-
},
|
|
1171
|
-
idField: "id",
|
|
1172
|
-
});
|
|
1173
|
-
|
|
1174
|
-
const usersTO = defineTableOccurrence({
|
|
1175
|
-
name: "users",
|
|
1176
|
-
baseTable: usersBase,
|
|
1177
|
-
});
|
|
1178
|
-
|
|
1179
|
-
const db = connection.database("MyDB", {
|
|
1180
|
-
occurrences: [usersTO],
|
|
1181
|
-
});
|
|
1182
|
-
|
|
1183
|
-
// TypeScript knows these are valid field names
|
|
1184
|
-
db.from("users").list().select("username", "email");
|
|
1185
|
-
|
|
1186
|
-
// TypeScript error: "invalid" is not a field name
|
|
1187
|
-
db.from("users").list().select("invalid"); // TS Error
|
|
1188
|
-
|
|
1189
|
-
// Type-safe filters
|
|
1190
|
-
db.from("users")
|
|
1191
|
-
.list()
|
|
1192
|
-
.filter({ username: { eq: "john" } }); // ✓
|
|
1193
|
-
db.from("users")
|
|
1194
|
-
.list()
|
|
1195
|
-
.filter({ invalid: { eq: "john" } }); // TS Error
|
|
1196
|
-
```
|
|
1197
|
-
|
|
1198
1366
|
### Required and Read-Only Fields
|
|
1199
1367
|
|
|
1200
|
-
The library automatically infers which fields are required based on
|
|
1368
|
+
The library automatically infers which fields are required based on field builder configuration:
|
|
1201
1369
|
|
|
1202
1370
|
```typescript
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
updatedAt: z.string().nullable(), // Optional
|
|
1211
|
-
},
|
|
1212
|
-
idField: "id", // Automatically excluded from insert/update
|
|
1213
|
-
required: ["status"], // Make status required despite being nullable
|
|
1214
|
-
readOnly: ["createdAt"], // Exclude createdAt from insert/update
|
|
1371
|
+
const users = fmTableOccurrence("users", {
|
|
1372
|
+
id: textField().primaryKey(), // Auto-required, auto-readOnly (primaryKey)
|
|
1373
|
+
username: textField().notNull(), // Auto-required (notNull)
|
|
1374
|
+
email: textField().notNull(), // Auto-required (notNull)
|
|
1375
|
+
status: textField(), // Optional (nullable by default)
|
|
1376
|
+
createdAt: timestampField().readOnly(), // Read-only system field
|
|
1377
|
+
updatedAt: timestampField(), // Optional (nullable)
|
|
1215
1378
|
});
|
|
1216
1379
|
|
|
1217
|
-
// Insert: username
|
|
1218
|
-
// Insert: id and createdAt are excluded (cannot be provided)
|
|
1219
|
-
db.from(
|
|
1380
|
+
// Insert: username and email are required
|
|
1381
|
+
// Insert: id and createdAt are excluded (cannot be provided - read-only)
|
|
1382
|
+
db.from(users).insert({
|
|
1220
1383
|
username: "john",
|
|
1221
1384
|
email: "john@example.com",
|
|
1222
|
-
status: "active", //
|
|
1385
|
+
status: "active", // Optional
|
|
1223
1386
|
updatedAt: new Date().toISOString(), // Optional
|
|
1224
1387
|
});
|
|
1225
1388
|
|
|
1226
1389
|
// Update: all fields are optional except id and createdAt are excluded
|
|
1227
|
-
db.from(
|
|
1390
|
+
db.from(users)
|
|
1228
1391
|
.update({
|
|
1229
1392
|
status: "active", // Optional
|
|
1230
|
-
// id and createdAt cannot be modified
|
|
1393
|
+
// id and createdAt cannot be modified (read-only)
|
|
1231
1394
|
})
|
|
1232
1395
|
.byId("user-123");
|
|
1233
1396
|
```
|
|
1234
1397
|
|
|
1235
1398
|
**Key Features:**
|
|
1236
1399
|
|
|
1237
|
-
- **Auto-inference:**
|
|
1238
|
-
- **
|
|
1239
|
-
- **Read-only fields:** Use
|
|
1240
|
-
- **Automatic ID exclusion:** The `idField` is always read-only without needing to specify it
|
|
1400
|
+
- **Auto-inference:** Fields with `.notNull()` are automatically required for insert
|
|
1401
|
+
- **Primary keys:** Fields with `.primaryKey()` are automatically read-only
|
|
1402
|
+
- **Read-only fields:** Use `.readOnly()` to exclude fields from insert/update (e.g., timestamps, calculated fields)
|
|
1241
1403
|
- **Update flexibility:** All fields are optional for updates (except read-only fields)
|
|
1242
1404
|
|
|
1243
1405
|
### Prefer: fmodata.entity-ids
|
|
1244
1406
|
|
|
1245
1407
|
This library supports using FileMaker's internal field identifiers (FMFID) and table occurrence identifiers (FMTID) instead of names. This protects your integration from both field and table occurrence name changes.
|
|
1246
1408
|
|
|
1247
|
-
To enable this feature, simply define your schema with entity IDs using the `
|
|
1409
|
+
To enable this feature, simply define your schema with entity IDs using the `.entityId()` method on field builders and the `entityId` option in `fmTableOccurrence()`. Behind the scenes, the library will transform your request and the response back to the names you specify in your schema. This is an all-or-nothing feature. For it to work properly, you must define all table occurrences passed to a `Database` with entity IDs (both field IDs via `.entityId()` and table ID via the `entityId` option).
|
|
1248
1410
|
|
|
1249
1411
|
_Note for OttoFMS proxy: This feature requires version 4.14 or later of OttoFMS_
|
|
1250
1412
|
|
|
@@ -1253,32 +1415,25 @@ How do I find these ids? They can be found in the XML version of the `$metadata`
|
|
|
1253
1415
|
#### Basic Usage
|
|
1254
1416
|
|
|
1255
1417
|
```typescript
|
|
1256
|
-
import {
|
|
1257
|
-
|
|
1418
|
+
import {
|
|
1419
|
+
fmTableOccurrence,
|
|
1420
|
+
textField,
|
|
1421
|
+
timestampField,
|
|
1422
|
+
} from "@proofkit/fmodata";
|
|
1258
1423
|
|
|
1259
|
-
// Define a
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1424
|
+
// Define a table with FileMaker field IDs and table occurrence ID
|
|
1425
|
+
const users = fmTableOccurrence(
|
|
1426
|
+
"users",
|
|
1427
|
+
{
|
|
1428
|
+
id: textField().primaryKey().entityId("FMFID:12039485"),
|
|
1429
|
+
username: textField().notNull().entityId("FMFID:34323433"),
|
|
1430
|
+
email: textField().entityId("FMFID:12232424"),
|
|
1431
|
+
createdAt: timestampField().readOnly().entityId("FMFID:43234355"),
|
|
1266
1432
|
},
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
id: "FMFID:12039485",
|
|
1270
|
-
username: "FMFID:34323433",
|
|
1271
|
-
email: "FMFID:12232424",
|
|
1272
|
-
createdAt: "FMFID:43234355",
|
|
1433
|
+
{
|
|
1434
|
+
entityId: "FMTID:12432533", // FileMaker table occurrence ID
|
|
1273
1435
|
},
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
// Create a table occurrence with a FileMaker table occurrence ID
|
|
1277
|
-
const usersTO = defineTableOccurrence({
|
|
1278
|
-
name: "users",
|
|
1279
|
-
baseTable: usersBase,
|
|
1280
|
-
fmtId: "FMTID:12432533",
|
|
1281
|
-
});
|
|
1436
|
+
);
|
|
1282
1437
|
```
|
|
1283
1438
|
|
|
1284
1439
|
### Error Handling
|
|
@@ -1288,7 +1443,7 @@ All operations return a `Result` type with either `data` or `error`. The library
|
|
|
1288
1443
|
#### Basic Error Checking
|
|
1289
1444
|
|
|
1290
1445
|
```typescript
|
|
1291
|
-
const result = await db.from(
|
|
1446
|
+
const result = await db.from(users).list().execute();
|
|
1292
1447
|
|
|
1293
1448
|
if (result.error) {
|
|
1294
1449
|
console.error("Query failed:", result.error.message);
|
|
@@ -1307,7 +1462,7 @@ Handle HTTP status codes (4xx, 5xx) with the `HTTPError` class:
|
|
|
1307
1462
|
```typescript
|
|
1308
1463
|
import { HTTPError, isHTTPError } from "@proofkit/fmodata";
|
|
1309
1464
|
|
|
1310
|
-
const result = await db.from(
|
|
1465
|
+
const result = await db.from(users).list().execute();
|
|
1311
1466
|
|
|
1312
1467
|
if (result.error) {
|
|
1313
1468
|
if (isHTTPError(result.error)) {
|
|
@@ -1344,7 +1499,7 @@ import {
|
|
|
1344
1499
|
CircuitOpenError,
|
|
1345
1500
|
} from "@proofkit/fmodata";
|
|
1346
1501
|
|
|
1347
|
-
const result = await db.from(
|
|
1502
|
+
const result = await db.from(users).list().execute();
|
|
1348
1503
|
|
|
1349
1504
|
if (result.error) {
|
|
1350
1505
|
if (result.error instanceof TimeoutError) {
|
|
@@ -1370,7 +1525,7 @@ When schema validation fails, you get a `ValidationError` with rich context:
|
|
|
1370
1525
|
```typescript
|
|
1371
1526
|
import { ValidationError, isValidationError } from "@proofkit/fmodata";
|
|
1372
1527
|
|
|
1373
|
-
const result = await db.from(
|
|
1528
|
+
const result = await db.from(users).list().execute();
|
|
1374
1529
|
|
|
1375
1530
|
if (result.error) {
|
|
1376
1531
|
if (isValidationError(result.error)) {
|
|
@@ -1389,7 +1544,7 @@ The library uses [Standard Schema](https://github.com/standard-schema/standard-s
|
|
|
1389
1544
|
```typescript
|
|
1390
1545
|
import { ValidationError } from "@proofkit/fmodata";
|
|
1391
1546
|
|
|
1392
|
-
const result = await db.from(
|
|
1547
|
+
const result = await db.from(users).list().execute();
|
|
1393
1548
|
|
|
1394
1549
|
if (result.error instanceof ValidationError) {
|
|
1395
1550
|
// The cause property (ES2022 Error.cause) contains the Standard Schema issues array
|
|
@@ -1442,7 +1597,7 @@ Handle OData-specific protocol errors:
|
|
|
1442
1597
|
```typescript
|
|
1443
1598
|
import { ODataError, isODataError } from "@proofkit/fmodata";
|
|
1444
1599
|
|
|
1445
|
-
const result = await db.from(
|
|
1600
|
+
const result = await db.from(users).list().execute();
|
|
1446
1601
|
|
|
1447
1602
|
if (result.error) {
|
|
1448
1603
|
if (isODataError(result.error)) {
|
|
@@ -1465,7 +1620,7 @@ import {
|
|
|
1465
1620
|
NetworkError,
|
|
1466
1621
|
} from "@proofkit/fmodata";
|
|
1467
1622
|
|
|
1468
|
-
const result = await db.from(
|
|
1623
|
+
const result = await db.from(users).list().execute();
|
|
1469
1624
|
|
|
1470
1625
|
if (result.error) {
|
|
1471
1626
|
if (result.error instanceof TimeoutError) {
|
|
@@ -1487,7 +1642,7 @@ if (result.error) {
|
|
|
1487
1642
|
**Pattern 2: Using kind property (for exhaustive matching):**
|
|
1488
1643
|
|
|
1489
1644
|
```typescript
|
|
1490
|
-
const result = await db.from(
|
|
1645
|
+
const result = await db.from(users).list().execute();
|
|
1491
1646
|
|
|
1492
1647
|
if (result.error) {
|
|
1493
1648
|
switch (result.error.kind) {
|