@proofkit/fmodata 0.1.0-alpha.9 → 0.1.0-beta.23
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/LICENSE.md +21 -0
- package/README.md +651 -449
- package/dist/esm/client/batch-builder.d.ts +10 -9
- package/dist/esm/client/batch-builder.js +119 -56
- package/dist/esm/client/batch-builder.js.map +1 -1
- package/dist/esm/client/batch-request.js +16 -21
- package/dist/esm/client/batch-request.js.map +1 -1
- package/dist/esm/client/builders/default-select.d.ts +10 -0
- package/dist/esm/client/builders/default-select.js +41 -0
- package/dist/esm/client/builders/default-select.js.map +1 -0
- package/dist/esm/client/builders/expand-builder.d.ts +45 -0
- package/dist/esm/client/builders/expand-builder.js +185 -0
- package/dist/esm/client/builders/expand-builder.js.map +1 -0
- package/dist/esm/client/builders/index.d.ts +9 -0
- package/dist/esm/client/builders/query-string-builder.d.ts +18 -0
- package/dist/esm/client/builders/query-string-builder.js +21 -0
- package/dist/esm/client/builders/query-string-builder.js.map +1 -0
- package/dist/esm/client/builders/response-processor.d.ts +43 -0
- package/dist/esm/client/builders/response-processor.js +175 -0
- package/dist/esm/client/builders/response-processor.js.map +1 -0
- package/dist/esm/client/builders/select-mixin.d.ts +25 -0
- package/dist/esm/client/builders/select-mixin.js +28 -0
- package/dist/esm/client/builders/select-mixin.js.map +1 -0
- package/dist/esm/client/builders/select-utils.d.ts +18 -0
- package/dist/esm/client/builders/select-utils.js +30 -0
- package/dist/esm/client/builders/select-utils.js.map +1 -0
- package/dist/esm/client/builders/shared-types.d.ts +40 -0
- package/dist/esm/client/builders/table-utils.d.ts +35 -0
- package/dist/esm/client/builders/table-utils.js +44 -0
- package/dist/esm/client/builders/table-utils.js.map +1 -0
- package/dist/esm/client/database.d.ts +34 -22
- package/dist/esm/client/database.js +48 -84
- package/dist/esm/client/database.js.map +1 -1
- package/dist/esm/client/delete-builder.d.ts +25 -30
- package/dist/esm/client/delete-builder.js +45 -30
- package/dist/esm/client/delete-builder.js.map +1 -1
- package/dist/esm/client/entity-set.d.ts +35 -43
- package/dist/esm/client/entity-set.js +110 -52
- 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 +25 -0
- package/dist/esm/client/error-parser.js.map +1 -0
- package/dist/esm/client/filemaker-odata.d.ts +26 -7
- package/dist/esm/client/filemaker-odata.js +65 -42
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/dist/esm/client/insert-builder.d.ts +19 -24
- package/dist/esm/client/insert-builder.js +94 -58
- 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 +4 -0
- package/dist/esm/client/query/query-builder.d.ts +132 -0
- package/dist/esm/client/query/query-builder.js +456 -0
- package/dist/esm/client/query/query-builder.js.map +1 -0
- package/dist/esm/client/query/response-processor.d.ts +25 -0
- package/dist/esm/client/query/types.d.ts +77 -0
- package/dist/esm/client/query/url-builder.d.ts +71 -0
- package/dist/esm/client/query/url-builder.js +100 -0
- package/dist/esm/client/query/url-builder.js.map +1 -0
- package/dist/esm/client/query-builder.d.ts +2 -115
- package/dist/esm/client/record-builder.d.ts +108 -36
- package/dist/esm/client/record-builder.js +284 -119
- package/dist/esm/client/record-builder.js.map +1 -1
- package/dist/esm/client/response-processor.d.ts +4 -9
- package/dist/esm/client/sanitize-json.d.ts +35 -0
- package/dist/esm/client/sanitize-json.js +27 -0
- package/dist/esm/client/sanitize-json.js.map +1 -0
- package/dist/esm/client/schema-manager.d.ts +5 -5
- package/dist/esm/client/schema-manager.js +45 -31
- package/dist/esm/client/schema-manager.js.map +1 -1
- package/dist/esm/client/update-builder.d.ts +34 -40
- package/dist/esm/client/update-builder.js +99 -58
- package/dist/esm/client/update-builder.js.map +1 -1
- package/dist/esm/client/webhook-builder.d.ts +126 -0
- package/dist/esm/client/webhook-builder.js +189 -0
- package/dist/esm/client/webhook-builder.js.map +1 -0
- package/dist/esm/errors.d.ts +19 -2
- package/dist/esm/errors.js +39 -4
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +10 -8
- package/dist/esm/index.js +40 -10
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/logger.d.ts +47 -0
- package/dist/esm/logger.js +69 -0
- package/dist/esm/logger.js.map +1 -0
- package/dist/esm/logger.test.d.ts +1 -0
- package/dist/esm/orm/column.d.ts +62 -0
- package/dist/esm/orm/column.js +63 -0
- package/dist/esm/orm/column.js.map +1 -0
- package/dist/esm/orm/field-builders.d.ts +164 -0
- package/dist/esm/orm/field-builders.js +158 -0
- package/dist/esm/orm/field-builders.js.map +1 -0
- package/dist/esm/orm/index.d.ts +5 -0
- package/dist/esm/orm/operators.d.ts +173 -0
- package/dist/esm/orm/operators.js +260 -0
- package/dist/esm/orm/operators.js.map +1 -0
- package/dist/esm/orm/table.d.ts +355 -0
- package/dist/esm/orm/table.js +202 -0
- package/dist/esm/orm/table.js.map +1 -0
- package/dist/esm/transform.d.ts +20 -21
- package/dist/esm/transform.js +44 -45
- package/dist/esm/transform.js.map +1 -1
- package/dist/esm/types.d.ts +96 -30
- package/dist/esm/types.js +7 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/validation.d.ts +22 -12
- package/dist/esm/validation.js +132 -85
- package/dist/esm/validation.js.map +1 -1
- package/package.json +28 -20
- package/src/client/batch-builder.ts +153 -89
- package/src/client/batch-request.ts +25 -41
- package/src/client/builders/default-select.ts +75 -0
- package/src/client/builders/expand-builder.ts +246 -0
- package/src/client/builders/index.ts +11 -0
- package/src/client/builders/query-string-builder.ts +46 -0
- package/src/client/builders/response-processor.ts +279 -0
- package/src/client/builders/select-mixin.ts +65 -0
- package/src/client/builders/select-utils.ts +59 -0
- package/src/client/builders/shared-types.ts +45 -0
- package/src/client/builders/table-utils.ts +83 -0
- package/src/client/database.ts +89 -183
- package/src/client/delete-builder.ts +74 -84
- package/src/client/entity-set.ts +266 -293
- package/src/client/error-parser.ts +41 -0
- package/src/client/filemaker-odata.ts +98 -66
- package/src/client/insert-builder.ts +157 -118
- package/src/client/query/expand-builder.ts +160 -0
- package/src/client/query/index.ts +14 -0
- package/src/client/query/query-builder.ts +729 -0
- package/src/client/query/response-processor.ts +226 -0
- package/src/client/query/types.ts +126 -0
- package/src/client/query/url-builder.ts +151 -0
- package/src/client/query-builder.ts +10 -1455
- package/src/client/record-builder.ts +575 -240
- package/src/client/response-processor.ts +15 -42
- package/src/client/sanitize-json.ts +64 -0
- package/src/client/schema-manager.ts +61 -76
- package/src/client/update-builder.ts +161 -143
- package/src/client/webhook-builder.ts +265 -0
- package/src/errors.ts +49 -16
- package/src/index.ts +99 -54
- package/src/logger.test.ts +34 -0
- package/src/logger.ts +116 -0
- package/src/orm/column.ts +106 -0
- package/src/orm/field-builders.ts +250 -0
- package/src/orm/index.ts +61 -0
- package/src/orm/operators.ts +473 -0
- package/src/orm/table.ts +741 -0
- package/src/transform.ts +90 -70
- package/src/types.ts +154 -113
- package/src/validation.ts +200 -115
- package/dist/esm/client/base-table.d.ts +0 -125
- package/dist/esm/client/base-table.js +0 -57
- package/dist/esm/client/base-table.js.map +0 -1
- package/dist/esm/client/query-builder.js +0 -896
- package/dist/esm/client/query-builder.js.map +0 -1
- package/dist/esm/client/table-occurrence.d.ts +0 -72
- package/dist/esm/client/table-occurrence.js +0 -74
- package/dist/esm/client/table-occurrence.js.map +0 -1
- package/dist/esm/filter-types.d.ts +0 -76
- package/src/client/base-table.ts +0 -175
- package/src/client/query-builder.ts.bak +0 -1457
- package/src/client/table-occurrence.ts +0 -175
- package/src/filter-types.ts +0 -97
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",
|
|
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))),
|
|
56
57
|
});
|
|
57
58
|
|
|
58
|
-
// 3. Create a
|
|
59
|
-
const
|
|
60
|
-
name: "users",
|
|
61
|
-
baseTable: usersBase,
|
|
62
|
-
});
|
|
59
|
+
// 3. Create a database instance
|
|
60
|
+
const db = connection.database("MyDatabase.fmp12");
|
|
63
61
|
|
|
64
|
-
// 4.
|
|
65
|
-
const
|
|
66
|
-
occurrences: [usersTO],
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// 5. Query your data
|
|
70
|
-
const { data, error } = await db.from("users").list().execute();
|
|
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,93 +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.
|
|
131
123
|
|
|
132
|
-
|
|
124
|
+
#### Field Builders
|
|
133
125
|
|
|
134
|
-
|
|
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
|
|
135
127
|
|
|
136
|
-
-
|
|
128
|
+
- `textField()`
|
|
129
|
+
- `numberField()`
|
|
130
|
+
- `dateField()`
|
|
131
|
+
- `timeField()`
|
|
132
|
+
- `timestampField()`
|
|
133
|
+
- `containerField()`
|
|
134
|
+
- `calcField()`
|
|
137
135
|
|
|
138
|
-
|
|
136
|
+
Each field builder supports chainable methods:
|
|
139
137
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
id: z.string(),
|
|
147
|
-
name: z.string(),
|
|
148
|
-
email: z.string(),
|
|
149
|
-
phone: z.string().optional(),
|
|
150
|
-
createdAt: z.string(),
|
|
151
|
-
},
|
|
152
|
-
idField: "id", // The primary key field (automatically read-only)
|
|
153
|
-
required: ["phone"], // optional: additional required fields for insert (beyond auto-inferred)
|
|
154
|
-
readOnly: ["createdAt"], // optional: fields excluded from insert/update
|
|
155
|
-
});
|
|
156
|
-
```
|
|
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
|
|
157
144
|
|
|
158
|
-
|
|
145
|
+
#### Defining Tables
|
|
159
146
|
|
|
160
|
-
|
|
147
|
+
Use `fmTableOccurrence()` to define a table with field builders:
|
|
161
148
|
|
|
162
149
|
```typescript
|
|
163
|
-
import {
|
|
150
|
+
import { z } from "zod/v4";
|
|
151
|
+
import {
|
|
152
|
+
fmTableOccurrence,
|
|
153
|
+
textField,
|
|
154
|
+
numberField,
|
|
155
|
+
timestampField,
|
|
156
|
+
} from "@proofkit/fmodata";
|
|
164
157
|
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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"),
|
|
166
|
+
},
|
|
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
|
+
);
|
|
169
173
|
```
|
|
170
174
|
|
|
175
|
+
The function returns a table object that provides:
|
|
176
|
+
|
|
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)
|
|
180
|
+
|
|
171
181
|
#### Default Field Selection
|
|
172
182
|
|
|
173
|
-
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:
|
|
174
184
|
|
|
175
185
|
```typescript
|
|
176
|
-
// Option 1 (default): "schema" - Select all fields from the schema
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
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
|
+
);
|
|
182
196
|
|
|
183
|
-
// Option 2: "all" - Select all fields (default behavior)
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
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
|
+
);
|
|
189
207
|
|
|
190
|
-
// Option 3:
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
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
|
+
);
|
|
196
221
|
|
|
197
|
-
// When you call list(), the defaultSelect is applied automatically
|
|
198
|
-
const result = await db.from(
|
|
199
|
-
// 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
|
|
200
225
|
|
|
201
226
|
// You can still override with explicit select()
|
|
202
227
|
const result = await db
|
|
203
|
-
.from(
|
|
228
|
+
.from(users)
|
|
204
229
|
.list()
|
|
205
|
-
.select(
|
|
230
|
+
.select({ username: users.username, email: users.email, age: users.age }) // Always overrides at the per-request level
|
|
206
231
|
.execute();
|
|
207
232
|
```
|
|
208
233
|
|
|
209
|
-
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.
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
const db = connection.database("MyDatabase.fmp12", {
|
|
213
|
-
occurrences: [contactsTO, usersTO], // Register your table occurrences
|
|
214
|
-
});
|
|
215
|
-
```
|
|
216
|
-
|
|
217
234
|
## Querying Data
|
|
218
235
|
|
|
219
236
|
### Basic Queries
|
|
@@ -245,9 +262,9 @@ Get a single field value:
|
|
|
245
262
|
|
|
246
263
|
```typescript
|
|
247
264
|
const result = await db
|
|
248
|
-
.from(
|
|
265
|
+
.from(users)
|
|
249
266
|
.get("user-123")
|
|
250
|
-
.getSingleField(
|
|
267
|
+
.getSingleField(users.email)
|
|
251
268
|
.execute();
|
|
252
269
|
|
|
253
270
|
if (result.data) {
|
|
@@ -257,173 +274,103 @@ if (result.data) {
|
|
|
257
274
|
|
|
258
275
|
### Filtering
|
|
259
276
|
|
|
260
|
-
fmodata provides type-safe filter operations that prevent common errors at compile time.
|
|
261
|
-
|
|
262
|
-
#### Operator Syntax
|
|
263
|
-
|
|
264
|
-
You can use filters in three ways:
|
|
265
|
-
|
|
266
|
-
**1. Shorthand (direct value):**
|
|
267
|
-
|
|
268
|
-
```typescript
|
|
269
|
-
.filter({ name: "John" })
|
|
270
|
-
// Equivalent to: { name: [{ eq: "John" }] }
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
**2. Single operator object:**
|
|
274
|
-
|
|
275
|
-
```typescript
|
|
276
|
-
.filter({ age: { gt: 18 } })
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
**3. Array of operators (for multiple operators on same field):**
|
|
280
|
-
|
|
281
|
-
```typescript
|
|
282
|
-
.filter({ age: [{ gt: 18 }, { lt: 65 }] })
|
|
283
|
-
// Result: age gt 18 and age lt 65
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
The array pattern prevents duplicate operators on the same field and allows multiple conditions with implicit AND.
|
|
287
|
-
|
|
288
|
-
#### Available Operators
|
|
289
|
-
|
|
290
|
-
**String fields:**
|
|
291
|
-
|
|
292
|
-
- `eq`, `ne` - equality/inequality
|
|
293
|
-
- `contains`, `startswith`, `endswith` - string functions
|
|
294
|
-
- `gt`, `ge`, `lt`, `le` - comparison
|
|
295
|
-
- `in` - match any value in array
|
|
296
|
-
|
|
297
|
-
**Number fields:**
|
|
298
|
-
|
|
299
|
-
- `eq`, `ne`, `gt`, `ge`, `lt`, `le` - comparisons
|
|
300
|
-
- `in` - match any value in array
|
|
301
|
-
|
|
302
|
-
**Boolean fields:**
|
|
303
|
-
|
|
304
|
-
- `eq`, `ne` - equality only
|
|
305
|
-
|
|
306
|
-
**Date fields:**
|
|
307
|
-
|
|
308
|
-
- `eq`, `ne`, `gt`, `ge`, `lt`, `le` - date comparisons
|
|
309
|
-
- `in` - match any date in array
|
|
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.
|
|
310
278
|
|
|
311
|
-
####
|
|
279
|
+
#### New ORM-Style API (Recommended)
|
|
312
280
|
|
|
313
|
-
|
|
281
|
+
Use the `where()` method with filter operators and column references for type-safe filtering:
|
|
314
282
|
|
|
315
283
|
```typescript
|
|
316
|
-
|
|
317
|
-
// Equivalent to: { name: [{ eq: "John" }] }
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
#### Examples
|
|
321
|
-
|
|
322
|
-
```typescript
|
|
323
|
-
// Equality filter (single operator)
|
|
324
|
-
const activeUsers = await db
|
|
325
|
-
.from("users")
|
|
326
|
-
.list()
|
|
327
|
-
.filter({ active: { eq: true } })
|
|
328
|
-
.execute();
|
|
284
|
+
import { eq, gt, and, or, contains } from "@proofkit/fmodata";
|
|
329
285
|
|
|
330
|
-
//
|
|
331
|
-
const adultUsers = await db
|
|
332
|
-
.from("users")
|
|
333
|
-
.list()
|
|
334
|
-
.filter({ age: { gt: 18 } })
|
|
335
|
-
.execute();
|
|
336
|
-
|
|
337
|
-
// String operators (single operator)
|
|
338
|
-
const johns = await db
|
|
339
|
-
.from("users")
|
|
340
|
-
.list()
|
|
341
|
-
.filter({ name: { contains: "John" } })
|
|
342
|
-
.execute();
|
|
343
|
-
|
|
344
|
-
// Multiple operators on same field (array syntax, implicit AND)
|
|
345
|
-
const rangeQuery = await db
|
|
346
|
-
.from("users")
|
|
347
|
-
.list()
|
|
348
|
-
.filter({ age: [{ gt: 18 }, { lt: 65 }] })
|
|
349
|
-
.execute();
|
|
350
|
-
|
|
351
|
-
// Combine filters with AND
|
|
286
|
+
// Simple equality
|
|
352
287
|
const result = await db
|
|
353
|
-
.from(
|
|
288
|
+
.from(users)
|
|
354
289
|
.list()
|
|
355
|
-
.
|
|
356
|
-
and: [{ active: [{ eq: true }] }, { age: [{ gt: 18 }] }],
|
|
357
|
-
})
|
|
290
|
+
.where(eq(users.active, true))
|
|
358
291
|
.execute();
|
|
359
292
|
|
|
360
|
-
//
|
|
293
|
+
// Comparison operators
|
|
294
|
+
const result = await db.from(users).list().where(gt(users.age, 18)).execute();
|
|
295
|
+
|
|
296
|
+
// String operators
|
|
361
297
|
const result = await db
|
|
362
|
-
.from(
|
|
298
|
+
.from(users)
|
|
363
299
|
.list()
|
|
364
|
-
.
|
|
365
|
-
or: [{ name: [{ eq: "John" }] }, { name: [{ eq: "Jane" }] }],
|
|
366
|
-
})
|
|
300
|
+
.where(contains(users.name, "John"))
|
|
367
301
|
.execute();
|
|
368
302
|
|
|
369
|
-
//
|
|
303
|
+
// Combine with AND
|
|
370
304
|
const result = await db
|
|
371
|
-
.from(
|
|
305
|
+
.from(users)
|
|
372
306
|
.list()
|
|
373
|
-
.
|
|
307
|
+
.where(and(eq(users.active, true), gt(users.age, 18)))
|
|
374
308
|
.execute();
|
|
375
309
|
|
|
376
|
-
//
|
|
310
|
+
// Combine with OR
|
|
377
311
|
const result = await db
|
|
378
|
-
.from(
|
|
312
|
+
.from(users)
|
|
379
313
|
.list()
|
|
380
|
-
.
|
|
314
|
+
.where(or(eq(users.role, "admin"), eq(users.role, "moderator")))
|
|
381
315
|
.execute();
|
|
382
316
|
```
|
|
383
317
|
|
|
384
|
-
|
|
318
|
+
Available operators:
|
|
319
|
+
|
|
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
|
+
### Sorting
|
|
327
|
+
|
|
328
|
+
Sort results using `orderBy()`. The method supports both column references (new ORM API) and string field names (legacy API).
|
|
385
329
|
|
|
386
|
-
|
|
330
|
+
#### Using Column References (New ORM API)
|
|
387
331
|
|
|
388
332
|
```typescript
|
|
333
|
+
import { asc, desc } from "@proofkit/fmodata";
|
|
334
|
+
|
|
335
|
+
// Single field (ascending by default)
|
|
336
|
+
const result = await db.from(users).list().orderBy(users.name).execute();
|
|
337
|
+
|
|
338
|
+
// Single field with explicit direction
|
|
339
|
+
const result = await db.from(users).list().orderBy(asc(users.name)).execute();
|
|
340
|
+
const result = await db.from(users).list().orderBy(desc(users.age)).execute();
|
|
341
|
+
|
|
342
|
+
// Multiple fields (variadic)
|
|
389
343
|
const result = await db
|
|
390
|
-
.from(
|
|
344
|
+
.from(users)
|
|
391
345
|
.list()
|
|
392
|
-
.
|
|
393
|
-
and: [{ name: [{ contains: "John" }] }, { age: [{ gt: 18 }] }],
|
|
394
|
-
})
|
|
346
|
+
.orderBy(asc(users.lastName), desc(users.firstName))
|
|
395
347
|
.execute();
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
#### Escape Hatch
|
|
399
|
-
|
|
400
|
-
For unsupported edge cases, pass a raw OData filter string:
|
|
401
348
|
|
|
402
|
-
|
|
349
|
+
// Multiple fields (array syntax)
|
|
403
350
|
const result = await db
|
|
404
|
-
.from(
|
|
351
|
+
.from(users)
|
|
405
352
|
.list()
|
|
406
|
-
.
|
|
353
|
+
.orderBy([
|
|
354
|
+
[users.lastName, "asc"],
|
|
355
|
+
[users.firstName, "desc"],
|
|
356
|
+
])
|
|
407
357
|
.execute();
|
|
408
358
|
```
|
|
409
359
|
|
|
410
|
-
|
|
360
|
+
#### Type Safety
|
|
411
361
|
|
|
412
|
-
|
|
362
|
+
For typed databases, `orderBy()` provides full type safety:
|
|
413
363
|
|
|
414
364
|
```typescript
|
|
415
|
-
//
|
|
416
|
-
|
|
365
|
+
// ✅ Valid - "name" is a field in the schema
|
|
366
|
+
db.from(users).list().orderBy(users.name);
|
|
417
367
|
|
|
418
|
-
//
|
|
419
|
-
|
|
368
|
+
// ✅ Valid - tuple with field and direction
|
|
369
|
+
db.from(users).list().orderBy(asc(users.name));
|
|
370
|
+
db.from(users).list().orderBy(desc(users.name));
|
|
420
371
|
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
.from("users")
|
|
424
|
-
.list()
|
|
425
|
-
.orderBy("lastName, firstName desc")
|
|
426
|
-
.execute();
|
|
372
|
+
// ✅ Valid - multiple fields
|
|
373
|
+
db.from(users).list().orderBy(asc(users.lastName), desc(users.firstName));
|
|
427
374
|
```
|
|
428
375
|
|
|
429
376
|
### Pagination
|
|
@@ -432,24 +379,29 @@ Control the number of records returned and pagination:
|
|
|
432
379
|
|
|
433
380
|
```typescript
|
|
434
381
|
// Limit results
|
|
435
|
-
const result = await db.from(
|
|
382
|
+
const result = await db.from(users).list().top(10).execute();
|
|
436
383
|
|
|
437
384
|
// Skip records (pagination)
|
|
438
|
-
const result = await db.from(
|
|
385
|
+
const result = await db.from(users).list().top(10).skip(20).execute();
|
|
439
386
|
|
|
440
387
|
// Count total records
|
|
441
|
-
const result = await db.from(
|
|
388
|
+
const result = await db.from(users).list().count().execute();
|
|
442
389
|
```
|
|
443
390
|
|
|
444
391
|
### Selecting Fields
|
|
445
392
|
|
|
446
|
-
Select specific fields to return:
|
|
393
|
+
Select specific fields to return. You can use either column references (new ORM API) or string field names (legacy API):
|
|
447
394
|
|
|
448
395
|
```typescript
|
|
396
|
+
// New ORM API: Using column references (type-safe, supports renaming)
|
|
449
397
|
const result = await db
|
|
450
|
-
.from(
|
|
398
|
+
.from(users)
|
|
451
399
|
.list()
|
|
452
|
-
.select(
|
|
400
|
+
.select({
|
|
401
|
+
username: users.username,
|
|
402
|
+
email: users.email,
|
|
403
|
+
userId: users.id, // Renamed from "id" to "userId"
|
|
404
|
+
})
|
|
453
405
|
.execute();
|
|
454
406
|
|
|
455
407
|
// result.data[0] will only have username and email fields
|
|
@@ -461,9 +413,9 @@ Use `single()` to ensure exactly one record is returned (returns an error if zer
|
|
|
461
413
|
|
|
462
414
|
```typescript
|
|
463
415
|
const result = await db
|
|
464
|
-
.from(
|
|
416
|
+
.from(users)
|
|
465
417
|
.list()
|
|
466
|
-
.
|
|
418
|
+
.where(eq(users.email, "user@example.com"))
|
|
467
419
|
.single()
|
|
468
420
|
.execute();
|
|
469
421
|
|
|
@@ -477,9 +429,9 @@ Use `maybeSingle()` when you want at most one record (returns `null` if no recor
|
|
|
477
429
|
|
|
478
430
|
```typescript
|
|
479
431
|
const result = await db
|
|
480
|
-
.from(
|
|
432
|
+
.from(users)
|
|
481
433
|
.list()
|
|
482
|
-
.
|
|
434
|
+
.where(eq(users.email, "user@example.com"))
|
|
483
435
|
.maybeSingle()
|
|
484
436
|
.execute();
|
|
485
437
|
|
|
@@ -502,12 +454,17 @@ if (result.data) {
|
|
|
502
454
|
All query methods can be chained together:
|
|
503
455
|
|
|
504
456
|
```typescript
|
|
457
|
+
// Using new ORM API
|
|
505
458
|
const result = await db
|
|
506
|
-
.from(
|
|
459
|
+
.from(users)
|
|
507
460
|
.list()
|
|
508
|
-
.select(
|
|
509
|
-
|
|
510
|
-
|
|
461
|
+
.select({
|
|
462
|
+
username: users.username,
|
|
463
|
+
email: users.email,
|
|
464
|
+
age: users.age,
|
|
465
|
+
})
|
|
466
|
+
.where(gt(users.age, 18))
|
|
467
|
+
.orderBy(asc(users.username))
|
|
511
468
|
.top(10)
|
|
512
469
|
.skip(0)
|
|
513
470
|
.execute();
|
|
@@ -522,7 +479,7 @@ Insert new records with type-safe data:
|
|
|
522
479
|
```typescript
|
|
523
480
|
// Insert a new user
|
|
524
481
|
const result = await db
|
|
525
|
-
.from(
|
|
482
|
+
.from(users)
|
|
526
483
|
.insert({
|
|
527
484
|
username: "johndoe",
|
|
528
485
|
email: "john@example.com",
|
|
@@ -535,30 +492,25 @@ if (result.data) {
|
|
|
535
492
|
}
|
|
536
493
|
```
|
|
537
494
|
|
|
538
|
-
Fields are automatically required for insert if
|
|
495
|
+
Fields are automatically required for insert if they use `.notNull()`. Read-only fields (including primary keys) are automatically excluded:
|
|
539
496
|
|
|
540
497
|
```typescript
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
createdAt: z.string(), // Auto-required, but excluded (readOnly)
|
|
548
|
-
},
|
|
549
|
-
idField: "id", // Automatically excluded from insert/update
|
|
550
|
-
required: ["phone"], // Make phone required for inserts despite being nullable
|
|
551
|
-
readOnly: ["createdAt"], // Exclude from insert/update operations
|
|
498
|
+
const users = fmTableOccurrence("users", {
|
|
499
|
+
id: textField().primaryKey(), // Auto-required, but excluded from insert (primaryKey)
|
|
500
|
+
username: textField().notNull(), // Auto-required (notNull)
|
|
501
|
+
email: textField().notNull(), // Auto-required (notNull)
|
|
502
|
+
phone: textField(), // Optional by default (nullable)
|
|
503
|
+
createdAt: timestampField().readOnly(), // Excluded from insert/update
|
|
552
504
|
});
|
|
553
505
|
|
|
554
|
-
// TypeScript enforces: username
|
|
506
|
+
// TypeScript enforces: username and email are required
|
|
555
507
|
// TypeScript excludes: id and createdAt cannot be provided
|
|
556
508
|
const result = await db
|
|
557
|
-
.from(
|
|
509
|
+
.from(users)
|
|
558
510
|
.insert({
|
|
559
511
|
username: "johndoe",
|
|
560
512
|
email: "john@example.com",
|
|
561
|
-
phone: "+1234567890", //
|
|
513
|
+
phone: "+1234567890", // Optional
|
|
562
514
|
})
|
|
563
515
|
.execute();
|
|
564
516
|
```
|
|
@@ -570,7 +522,7 @@ Update records by ID or filter:
|
|
|
570
522
|
```typescript
|
|
571
523
|
// Update by ID
|
|
572
524
|
const result = await db
|
|
573
|
-
.from(
|
|
525
|
+
.from(users)
|
|
574
526
|
.update({ username: "newname" })
|
|
575
527
|
.byId("user-123")
|
|
576
528
|
.execute();
|
|
@@ -579,29 +531,27 @@ if (result.data) {
|
|
|
579
531
|
console.log(`Updated ${result.data.updatedCount} record(s)`);
|
|
580
532
|
}
|
|
581
533
|
|
|
582
|
-
// Update by filter
|
|
534
|
+
// Update by filter (using new ORM API)
|
|
535
|
+
import { lt, and, eq } from "@proofkit/fmodata";
|
|
536
|
+
|
|
583
537
|
const result = await db
|
|
584
|
-
.from(
|
|
538
|
+
.from(users)
|
|
585
539
|
.update({ active: false })
|
|
586
|
-
.where((
|
|
540
|
+
.where(lt(users.lastLogin, "2023-01-01"))
|
|
587
541
|
.execute();
|
|
588
542
|
|
|
589
543
|
// Complex filter example
|
|
590
544
|
const result = await db
|
|
591
|
-
.from(
|
|
545
|
+
.from(users)
|
|
592
546
|
.update({ active: false })
|
|
593
|
-
.where((
|
|
594
|
-
q.filter({
|
|
595
|
-
and: [{ active: true }, { count: { lt: 5 } }],
|
|
596
|
-
}),
|
|
597
|
-
)
|
|
547
|
+
.where(and(eq(users.active, true), lt(users.count, 5)))
|
|
598
548
|
.execute();
|
|
599
549
|
|
|
600
|
-
// Update with additional query options
|
|
550
|
+
// Update with additional query options (legacy filter API)
|
|
601
551
|
const result = await db
|
|
602
552
|
.from("users")
|
|
603
553
|
.update({ active: false })
|
|
604
|
-
.where((q) => q.
|
|
554
|
+
.where((q) => q.where(eq(users.active, true)).top(10))
|
|
605
555
|
.execute();
|
|
606
556
|
```
|
|
607
557
|
|
|
@@ -611,28 +561,26 @@ Delete records by ID or filter:
|
|
|
611
561
|
|
|
612
562
|
```typescript
|
|
613
563
|
// Delete by ID
|
|
614
|
-
const result = await db.from(
|
|
564
|
+
const result = await db.from(users).delete().byId("user-123").execute();
|
|
615
565
|
|
|
616
566
|
if (result.data) {
|
|
617
567
|
console.log(`Deleted ${result.data.deletedCount} record(s)`);
|
|
618
568
|
}
|
|
619
569
|
|
|
620
|
-
// Delete by filter
|
|
570
|
+
// Delete by filter (using new ORM API)
|
|
571
|
+
import { eq, and, lt } from "@proofkit/fmodata";
|
|
572
|
+
|
|
621
573
|
const result = await db
|
|
622
|
-
.from(
|
|
574
|
+
.from(users)
|
|
623
575
|
.delete()
|
|
624
|
-
.where((
|
|
576
|
+
.where(eq(users.active, false))
|
|
625
577
|
.execute();
|
|
626
578
|
|
|
627
579
|
// Delete with complex filters
|
|
628
580
|
const result = await db
|
|
629
|
-
.from(
|
|
581
|
+
.from(users)
|
|
630
582
|
.delete()
|
|
631
|
-
.where((
|
|
632
|
-
q.filter({
|
|
633
|
-
and: [{ active: false }, { lastLogin: { lt: "2023-01-01" } }],
|
|
634
|
-
}),
|
|
635
|
-
)
|
|
583
|
+
.where(and(eq(users.active, false), lt(users.lastLogin, "2023-01-01")))
|
|
636
584
|
.execute();
|
|
637
585
|
```
|
|
638
586
|
|
|
@@ -640,132 +588,145 @@ const result = await db
|
|
|
640
588
|
|
|
641
589
|
### Defining Navigation
|
|
642
590
|
|
|
643
|
-
Define relationships
|
|
591
|
+
Define navigation relationships using the `navigationPaths` option when creating table occurrences:
|
|
644
592
|
|
|
645
593
|
```typescript
|
|
646
|
-
|
|
647
|
-
schema: {
|
|
648
|
-
id: z.string(),
|
|
649
|
-
name: z.string(),
|
|
650
|
-
userId: z.string(),
|
|
651
|
-
},
|
|
652
|
-
idField: "id",
|
|
653
|
-
});
|
|
594
|
+
import { fmTableOccurrence, textField } from "@proofkit/fmodata";
|
|
654
595
|
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
596
|
+
const contacts = fmTableOccurrence(
|
|
597
|
+
"contacts",
|
|
598
|
+
{
|
|
599
|
+
id: textField().primaryKey(),
|
|
600
|
+
name: textField().notNull(),
|
|
601
|
+
userId: textField().notNull(),
|
|
660
602
|
},
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
// Create base occurrences first, then add navigation
|
|
666
|
-
const _contactsTO = defineTableOccurrence({
|
|
667
|
-
name: "contacts",
|
|
668
|
-
baseTable: contactsBase,
|
|
669
|
-
});
|
|
603
|
+
{
|
|
604
|
+
navigationPaths: ["users"], // Valid navigation targets
|
|
605
|
+
},
|
|
606
|
+
);
|
|
670
607
|
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
608
|
+
const users = fmTableOccurrence(
|
|
609
|
+
"users",
|
|
610
|
+
{
|
|
611
|
+
id: textField().primaryKey(),
|
|
612
|
+
username: textField().notNull(),
|
|
613
|
+
email: textField().notNull(),
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
navigationPaths: ["contacts"], // Valid navigation targets
|
|
617
|
+
},
|
|
618
|
+
);
|
|
675
619
|
|
|
676
|
-
//
|
|
677
|
-
const
|
|
678
|
-
|
|
620
|
+
// Use with your database
|
|
621
|
+
const db = connection.database("MyDB", {
|
|
622
|
+
occurrences: [contacts, users],
|
|
679
623
|
});
|
|
624
|
+
```
|
|
680
625
|
|
|
681
|
-
|
|
682
|
-
contacts: () => _contactsTO,
|
|
683
|
-
});
|
|
626
|
+
The `navigationPaths` option:
|
|
684
627
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
});
|
|
689
|
-
```
|
|
628
|
+
- Specifies which table occurrences can be navigated to from this table
|
|
629
|
+
- Enables runtime validation when using `expand()` or `navigate()`
|
|
630
|
+
- Throws descriptive errors if you try to navigate to an invalid path
|
|
690
631
|
|
|
691
632
|
### Navigating Between Tables
|
|
692
633
|
|
|
693
634
|
Navigate to related records:
|
|
694
635
|
|
|
695
636
|
```typescript
|
|
696
|
-
// Navigate from a specific record
|
|
637
|
+
// Navigate from a specific record (using column references)
|
|
697
638
|
const result = await db
|
|
698
|
-
.from(
|
|
639
|
+
.from(contacts)
|
|
699
640
|
.get("contact-123")
|
|
700
|
-
.navigate(
|
|
701
|
-
.select(
|
|
641
|
+
.navigate(users)
|
|
642
|
+
.select({
|
|
643
|
+
username: users.username,
|
|
644
|
+
email: users.email,
|
|
645
|
+
})
|
|
702
646
|
.execute();
|
|
703
647
|
|
|
704
648
|
// Navigate without specifying a record first
|
|
705
|
-
const result = await db.from(
|
|
649
|
+
const result = await db.from(contacts).navigate(users).list().execute();
|
|
706
650
|
|
|
707
|
-
//
|
|
651
|
+
// Using legacy API with string field names
|
|
708
652
|
const result = await db
|
|
709
|
-
.from(
|
|
710
|
-
.
|
|
711
|
-
.
|
|
653
|
+
.from(contacts)
|
|
654
|
+
.get("contact-123")
|
|
655
|
+
.navigate(users)
|
|
656
|
+
.select({ username: users.username, email: users.email })
|
|
712
657
|
.execute();
|
|
713
658
|
```
|
|
714
659
|
|
|
715
660
|
### Expanding Related Records
|
|
716
661
|
|
|
717
|
-
Use `expand()` to include related records in your query results
|
|
662
|
+
Use `expand()` to include related records in your query results. The library validates that the target table is in the source table's `navigationPaths`:
|
|
718
663
|
|
|
719
664
|
```typescript
|
|
720
665
|
// Simple expand
|
|
721
|
-
const result = await db.from(
|
|
666
|
+
const result = await db.from(contacts).list().expand(users).execute();
|
|
722
667
|
|
|
723
|
-
// Expand with field selection
|
|
668
|
+
// Expand with field selection (using column references)
|
|
724
669
|
const result = await db
|
|
725
|
-
.from(
|
|
670
|
+
.from(contacts)
|
|
726
671
|
.list()
|
|
727
|
-
.expand(
|
|
672
|
+
.expand(users, (b) =>
|
|
673
|
+
b.select({
|
|
674
|
+
username: users.username,
|
|
675
|
+
email: users.email,
|
|
676
|
+
}),
|
|
677
|
+
)
|
|
728
678
|
.execute();
|
|
729
679
|
|
|
730
|
-
// Expand with filtering
|
|
680
|
+
// Expand with filtering (using new ORM API)
|
|
681
|
+
import { eq } from "@proofkit/fmodata";
|
|
682
|
+
|
|
731
683
|
const result = await db
|
|
732
|
-
.from(
|
|
684
|
+
.from(contacts)
|
|
733
685
|
.list()
|
|
734
|
-
.expand(
|
|
686
|
+
.expand(users, (b) => b.where(eq(users.active, true)))
|
|
735
687
|
.execute();
|
|
736
688
|
|
|
737
689
|
// Multiple expands
|
|
738
690
|
const result = await db
|
|
739
|
-
.from(
|
|
691
|
+
.from(contacts)
|
|
740
692
|
.list()
|
|
741
|
-
.expand(
|
|
742
|
-
.expand(
|
|
693
|
+
.expand(users, (b) => b.select({ username: users.username }))
|
|
694
|
+
.expand(orders, (b) => b.select({ total: orders.total }).top(5))
|
|
743
695
|
.execute();
|
|
744
696
|
|
|
745
697
|
// Nested expands
|
|
746
698
|
const result = await db
|
|
747
|
-
.from(
|
|
699
|
+
.from(contacts)
|
|
748
700
|
.list()
|
|
749
|
-
.expand(
|
|
701
|
+
.expand(users, (usersBuilder) =>
|
|
750
702
|
usersBuilder
|
|
751
|
-
.select(
|
|
752
|
-
|
|
753
|
-
|
|
703
|
+
.select({
|
|
704
|
+
username: users.username,
|
|
705
|
+
email: users.email,
|
|
706
|
+
})
|
|
707
|
+
.expand(customers, (customerBuilder) =>
|
|
708
|
+
customerBuilder.select({
|
|
709
|
+
name: customers.name,
|
|
710
|
+
tier: customers.tier,
|
|
711
|
+
}),
|
|
754
712
|
),
|
|
755
713
|
)
|
|
756
714
|
.execute();
|
|
757
715
|
|
|
758
716
|
// Complex expand with multiple options
|
|
759
717
|
const result = await db
|
|
760
|
-
.from(
|
|
718
|
+
.from(contacts)
|
|
761
719
|
.list()
|
|
762
|
-
.expand(
|
|
720
|
+
.expand(users, (b) =>
|
|
763
721
|
b
|
|
764
|
-
.select(
|
|
765
|
-
|
|
766
|
-
|
|
722
|
+
.select({
|
|
723
|
+
username: users.username,
|
|
724
|
+
email: users.email,
|
|
725
|
+
})
|
|
726
|
+
.where(eq(users.active, true))
|
|
727
|
+
.orderBy(asc(users.username))
|
|
767
728
|
.top(10)
|
|
768
|
-
.expand(
|
|
729
|
+
.expand(customers, (nested) => nested.select({ name: customers.name })),
|
|
769
730
|
)
|
|
770
731
|
.execute();
|
|
771
732
|
```
|
|
@@ -817,29 +778,226 @@ console.log(result.result.recordId);
|
|
|
817
778
|
|
|
818
779
|
**Note:** OData doesn't support script names with special characters (e.g., `@`, `&`, `/`) or script names beginning with a number. TypeScript will catch these at compile time.
|
|
819
780
|
|
|
781
|
+
## Webhooks
|
|
782
|
+
|
|
783
|
+
Webhooks allow you to receive notifications when data changes in your FileMaker database. The library provides a type-safe API for managing webhooks through the `db.webhook` property.
|
|
784
|
+
|
|
785
|
+
### Adding a Webhook
|
|
786
|
+
|
|
787
|
+
Create a new webhook to monitor a table for changes:
|
|
788
|
+
|
|
789
|
+
```typescript
|
|
790
|
+
// Basic webhook
|
|
791
|
+
const result = await db.webhook.add({
|
|
792
|
+
webhook: "https://example.com/webhook",
|
|
793
|
+
tableName: contactsTable,
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
// Access the created webhook ID
|
|
797
|
+
console.log(result.webHookResult.webHookID);
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
### Webhook Configuration Options
|
|
801
|
+
|
|
802
|
+
Webhooks support various configuration options:
|
|
803
|
+
|
|
804
|
+
```typescript
|
|
805
|
+
// With custom headers
|
|
806
|
+
const result = await db.webhook.add({
|
|
807
|
+
webhook: "https://example.com/webhook",
|
|
808
|
+
tableName: contactsTable,
|
|
809
|
+
headers: {
|
|
810
|
+
"X-Custom-Header": "value",
|
|
811
|
+
Authorization: "Bearer token",
|
|
812
|
+
},
|
|
813
|
+
notifySchemaChanges: true, // Notify when schema changes
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// With field selection (using column references)
|
|
817
|
+
const result = await db.webhook.add({
|
|
818
|
+
webhook: "https://example.com/webhook",
|
|
819
|
+
tableName: contacts,
|
|
820
|
+
select: [contacts.name, contacts.email, contacts.PrimaryKey],
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
// With filtering (using filter expressions)
|
|
824
|
+
import { eq, gt } from "@proofkit/fmodata";
|
|
825
|
+
|
|
826
|
+
const result = await db.webhook.add({
|
|
827
|
+
webhook: "https://example.com/webhook",
|
|
828
|
+
tableName: contacts,
|
|
829
|
+
filter: eq(contacts.active, true),
|
|
830
|
+
select: [contacts.name, contacts.email],
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
// Complex filter example
|
|
834
|
+
const result = await db.webhook.add({
|
|
835
|
+
webhook: "https://example.com/webhook",
|
|
836
|
+
tableName: users,
|
|
837
|
+
filter: and(eq(users.active, true), gt(users.age, 18)),
|
|
838
|
+
select: [users.username, users.email],
|
|
839
|
+
});
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
**Webhook Configuration Properties:**
|
|
843
|
+
|
|
844
|
+
- `webhook` (required) - The URL to call when the webhook is triggered
|
|
845
|
+
- `tableName` (required) - The `FMTable` instance for the table to monitor
|
|
846
|
+
- `headers` (optional) - Custom headers to include in webhook requests
|
|
847
|
+
- `notifySchemaChanges` (optional) - Whether to notify on schema changes
|
|
848
|
+
- `select` (optional) - Field selection as a string or array of `Column` references
|
|
849
|
+
- `filter` (optional) - Filter expression (string or `FilterExpression`) to limit which records trigger the webhook
|
|
850
|
+
|
|
851
|
+
### Listing Webhooks
|
|
852
|
+
|
|
853
|
+
Get all webhooks configured for the database:
|
|
854
|
+
|
|
855
|
+
```typescript
|
|
856
|
+
const result = await db.webhook.list();
|
|
857
|
+
|
|
858
|
+
console.log(result.Status); // Status of the operation
|
|
859
|
+
console.log(result.WebHook); // Array of webhook configurations
|
|
860
|
+
|
|
861
|
+
result.WebHook.forEach((webhook) => {
|
|
862
|
+
console.log(`Webhook ${webhook.webHookID}:`);
|
|
863
|
+
console.log(` Table: ${webhook.tableName}`);
|
|
864
|
+
console.log(` URL: ${webhook.url}`);
|
|
865
|
+
console.log(` Notify Schema Changes: ${webhook.notifySchemaChanges}`);
|
|
866
|
+
console.log(` Select: ${webhook.select}`);
|
|
867
|
+
console.log(` Filter: ${webhook.filter}`);
|
|
868
|
+
console.log(` Pending Operations: ${webhook.pendingOperations.length}`);
|
|
869
|
+
});
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
### Getting a Webhook
|
|
873
|
+
|
|
874
|
+
Retrieve a specific webhook by ID:
|
|
875
|
+
|
|
876
|
+
```typescript
|
|
877
|
+
const webhook = await db.webhook.get(1);
|
|
878
|
+
|
|
879
|
+
console.log(webhook.webHookID);
|
|
880
|
+
console.log(webhook.tableName);
|
|
881
|
+
console.log(webhook.url);
|
|
882
|
+
console.log(webhook.headers);
|
|
883
|
+
console.log(webhook.notifySchemaChanges);
|
|
884
|
+
console.log(webhook.select);
|
|
885
|
+
console.log(webhook.filter);
|
|
886
|
+
console.log(webhook.pendingOperations);
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
### Removing a Webhook
|
|
890
|
+
|
|
891
|
+
Delete a webhook by ID:
|
|
892
|
+
|
|
893
|
+
```typescript
|
|
894
|
+
await db.webhook.remove(1);
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
### Invoking a Webhook
|
|
898
|
+
|
|
899
|
+
Manually trigger a webhook. This is useful for testing or triggering webhooks on-demand:
|
|
900
|
+
|
|
901
|
+
```typescript
|
|
902
|
+
// Invoke for all rows matching the webhook's filter
|
|
903
|
+
await db.webhook.invoke(1);
|
|
904
|
+
|
|
905
|
+
// Invoke for specific row IDs
|
|
906
|
+
await db.webhook.invoke(1, { rowIDs: [63, 61] });
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
### Complete Example
|
|
910
|
+
|
|
911
|
+
Here's a complete example of setting up and managing webhooks:
|
|
912
|
+
|
|
913
|
+
```typescript
|
|
914
|
+
import { eq } from "@proofkit/fmodata";
|
|
915
|
+
|
|
916
|
+
// Add a webhook to monitor active contacts
|
|
917
|
+
const addResult = await db.webhook.add({
|
|
918
|
+
webhook: "https://api.example.com/webhooks/contacts",
|
|
919
|
+
tableName: contacts,
|
|
920
|
+
headers: {
|
|
921
|
+
"X-API-Key": "your-api-key",
|
|
922
|
+
},
|
|
923
|
+
filter: eq(contacts.active, true),
|
|
924
|
+
select: [contacts.name, contacts.email, contacts.PrimaryKey],
|
|
925
|
+
notifySchemaChanges: false,
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
const webhookId = addResult.webHookResult.webHookID;
|
|
929
|
+
console.log(`Created webhook with ID: ${webhookId}`);
|
|
930
|
+
|
|
931
|
+
// List all webhooks
|
|
932
|
+
const listResult = await db.webhook.list();
|
|
933
|
+
console.log(`Total webhooks: ${listResult.WebHook.length}`);
|
|
934
|
+
|
|
935
|
+
// Get the webhook we just created
|
|
936
|
+
const webhook = await db.webhook.get(webhookId);
|
|
937
|
+
console.log(`Webhook URL: ${webhook.url}`);
|
|
938
|
+
|
|
939
|
+
// Manually invoke the webhook for specific records
|
|
940
|
+
await db.webhook.invoke(webhookId, { rowIDs: [1, 2, 3] });
|
|
941
|
+
|
|
942
|
+
// Remove the webhook when done
|
|
943
|
+
await db.webhook.remove(webhookId);
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
**Note:** Webhooks are triggered automatically by FileMaker when records matching the webhook's filter are created, updated, or deleted. The `invoke()` method allows you to manually trigger webhooks for testing or on-demand processing.
|
|
947
|
+
|
|
820
948
|
## Batch Operations
|
|
821
949
|
|
|
822
950
|
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.
|
|
823
951
|
|
|
952
|
+
### Batch Result Structure
|
|
953
|
+
|
|
954
|
+
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:
|
|
955
|
+
|
|
956
|
+
```typescript
|
|
957
|
+
type BatchItemResult<T> = {
|
|
958
|
+
data: T | undefined;
|
|
959
|
+
error: FMODataErrorType | undefined;
|
|
960
|
+
status: number; // HTTP status code (0 for truncated operations)
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
type BatchResult<T extends readonly any[]> = {
|
|
964
|
+
results: { [K in keyof T]: BatchItemResult<T[K]> };
|
|
965
|
+
successCount: number;
|
|
966
|
+
errorCount: number;
|
|
967
|
+
truncated: boolean; // true if FileMaker stopped processing due to an error
|
|
968
|
+
firstErrorIndex: number | null; // Index of the first operation that failed
|
|
969
|
+
};
|
|
970
|
+
```
|
|
971
|
+
|
|
824
972
|
### Basic Batch with Multiple Queries
|
|
825
973
|
|
|
826
974
|
Execute multiple read operations in a single batch:
|
|
827
975
|
|
|
828
976
|
```typescript
|
|
829
977
|
// Create query builders
|
|
830
|
-
const contactsQuery = db.from(
|
|
831
|
-
const usersQuery = db.from(
|
|
978
|
+
const contactsQuery = db.from(contacts).list().top(5);
|
|
979
|
+
const usersQuery = db.from(users).list().top(5);
|
|
832
980
|
|
|
833
981
|
// Execute both queries in a single batch
|
|
834
982
|
const result = await db.batch([contactsQuery, usersQuery]).execute();
|
|
835
983
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
const [contacts, users] = result.data;
|
|
984
|
+
// Access individual results
|
|
985
|
+
const [r1, r2] = result.results;
|
|
839
986
|
|
|
840
|
-
|
|
841
|
-
console.
|
|
987
|
+
if (r1.error) {
|
|
988
|
+
console.error("Contacts query failed:", r1.error);
|
|
989
|
+
} else {
|
|
990
|
+
console.log("Contacts:", r1.data);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (r2.error) {
|
|
994
|
+
console.error("Users query failed:", r2.error);
|
|
995
|
+
} else {
|
|
996
|
+
console.log("Users:", r2.data);
|
|
842
997
|
}
|
|
998
|
+
|
|
999
|
+
// Check summary statistics
|
|
1000
|
+
console.log(`Success: ${result.successCount}, Errors: ${result.errorCount}`);
|
|
843
1001
|
```
|
|
844
1002
|
|
|
845
1003
|
### Mixed Operations (Reads and Writes)
|
|
@@ -848,22 +1006,73 @@ Combine queries, inserts, updates, and deletes in a single batch:
|
|
|
848
1006
|
|
|
849
1007
|
```typescript
|
|
850
1008
|
// Mix different operation types
|
|
851
|
-
const listQuery = db.from(
|
|
852
|
-
const insertOp = db.from(
|
|
1009
|
+
const listQuery = db.from(contacts).list().top(10);
|
|
1010
|
+
const insertOp = db.from(contacts).insert({
|
|
853
1011
|
name: "John Doe",
|
|
854
1012
|
email: "john@example.com",
|
|
855
1013
|
});
|
|
856
|
-
const updateOp = db.from(
|
|
1014
|
+
const updateOp = db.from(users).update({ active: true }).byId("user-123");
|
|
857
1015
|
|
|
858
1016
|
// All operations execute atomically
|
|
859
1017
|
const result = await db.batch([listQuery, insertOp, updateOp]).execute();
|
|
860
1018
|
|
|
861
|
-
|
|
862
|
-
|
|
1019
|
+
// Access individual results
|
|
1020
|
+
const [r1, r2, r3] = result.results;
|
|
1021
|
+
|
|
1022
|
+
if (r1.error) {
|
|
1023
|
+
console.error("List query failed:", r1.error);
|
|
1024
|
+
} else {
|
|
1025
|
+
console.log("Fetched contacts:", r1.data);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (r2.error) {
|
|
1029
|
+
console.error("Insert failed:", r2.error);
|
|
1030
|
+
} else {
|
|
1031
|
+
console.log("Inserted contact:", r2.data);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
if (r3.error) {
|
|
1035
|
+
console.error("Update failed:", r3.error);
|
|
1036
|
+
} else {
|
|
1037
|
+
console.log("Updated user:", r3.data);
|
|
1038
|
+
}
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
### Handling Errors in Batches
|
|
1042
|
+
|
|
1043
|
+
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`:
|
|
1044
|
+
|
|
1045
|
+
```typescript
|
|
1046
|
+
import { BatchTruncatedError, isBatchTruncatedError } from "@proofkit/fmodata";
|
|
1047
|
+
|
|
1048
|
+
const result = await db.batch([query1, query2, query3]).execute();
|
|
1049
|
+
|
|
1050
|
+
const [r1, r2, r3] = result.results;
|
|
1051
|
+
|
|
1052
|
+
// First operation succeeded
|
|
1053
|
+
if (r1.error) {
|
|
1054
|
+
console.error("First query failed:", r1.error);
|
|
1055
|
+
} else {
|
|
1056
|
+
console.log("First query succeeded:", r1.data);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Second operation failed
|
|
1060
|
+
if (r2.error) {
|
|
1061
|
+
console.error("Second query failed:", r2.error);
|
|
1062
|
+
console.log("HTTP Status:", r2.status); // e.g., 404
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Third operation was never executed (truncated)
|
|
1066
|
+
if (r3.error && isBatchTruncatedError(r3.error)) {
|
|
1067
|
+
console.log("Third operation was not executed");
|
|
1068
|
+
console.log(`Failed at operation ${r3.error.failedAtIndex}`);
|
|
1069
|
+
console.log(`This operation index: ${r3.error.operationIndex}`);
|
|
1070
|
+
console.log("Status:", r3.status); // 0 (never executed)
|
|
1071
|
+
}
|
|
863
1072
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
console.log(
|
|
1073
|
+
// Check if batch was truncated
|
|
1074
|
+
if (result.truncated) {
|
|
1075
|
+
console.log(`Batch stopped early at index ${result.firstErrorIndex}`);
|
|
867
1076
|
}
|
|
868
1077
|
```
|
|
869
1078
|
|
|
@@ -874,18 +1083,31 @@ Batch operations are transactional for write operations (inserts, updates, delet
|
|
|
874
1083
|
```typescript
|
|
875
1084
|
const result = await db
|
|
876
1085
|
.batch([
|
|
877
|
-
db.from(
|
|
878
|
-
db.from(
|
|
879
|
-
db.from(
|
|
1086
|
+
db.from(users).insert({ username: "alice", email: "alice@example.com" }),
|
|
1087
|
+
db.from(users).insert({ username: "bob", email: "bob@example.com" }),
|
|
1088
|
+
db.from(users).insert({ username: "charlie", email: "invalid" }), // This fails
|
|
880
1089
|
])
|
|
881
1090
|
.execute();
|
|
882
1091
|
|
|
883
|
-
|
|
1092
|
+
// Check individual results
|
|
1093
|
+
const [r1, r2, r3] = result.results;
|
|
1094
|
+
|
|
1095
|
+
if (r1.error || r2.error || r3.error) {
|
|
884
1096
|
// All three inserts are rolled back - no users were created
|
|
885
|
-
console.error("Batch
|
|
1097
|
+
console.error("Batch had errors:");
|
|
1098
|
+
if (r1.error) console.error("Operation 1:", r1.error);
|
|
1099
|
+
if (r2.error) console.error("Operation 2:", r2.error);
|
|
1100
|
+
if (r3.error) console.error("Operation 3:", r3.error);
|
|
886
1101
|
}
|
|
887
1102
|
```
|
|
888
1103
|
|
|
1104
|
+
### Important Notes
|
|
1105
|
+
|
|
1106
|
+
- **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`.
|
|
1107
|
+
- **Insert operations in batches**: FileMaker ignores `Prefer: return=representation` in batch requests. Insert operations return `{}` or `{ ROWID?: number }` instead of the full created record.
|
|
1108
|
+
- **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).
|
|
1109
|
+
- **Summary statistics**: Use `result.successCount`, `result.errorCount`, `result.truncated`, and `result.firstErrorIndex` for quick batch status checks.
|
|
1110
|
+
|
|
889
1111
|
**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.
|
|
890
1112
|
|
|
891
1113
|
## Schema Management
|
|
@@ -1147,94 +1369,50 @@ await db.schema.createIndex("users", "email");
|
|
|
1147
1369
|
|
|
1148
1370
|
## Advanced Features
|
|
1149
1371
|
|
|
1150
|
-
### Type Safety
|
|
1151
|
-
|
|
1152
|
-
The library provides full TypeScript type inference:
|
|
1153
|
-
|
|
1154
|
-
```typescript
|
|
1155
|
-
const usersBase = defineBaseTable({
|
|
1156
|
-
schema: {
|
|
1157
|
-
id: z.string(),
|
|
1158
|
-
username: z.string(),
|
|
1159
|
-
email: z.string(),
|
|
1160
|
-
},
|
|
1161
|
-
idField: "id",
|
|
1162
|
-
});
|
|
1163
|
-
|
|
1164
|
-
const usersTO = defineTableOccurrence({
|
|
1165
|
-
name: "users",
|
|
1166
|
-
baseTable: usersBase,
|
|
1167
|
-
});
|
|
1168
|
-
|
|
1169
|
-
const db = connection.database("MyDB", {
|
|
1170
|
-
occurrences: [usersTO],
|
|
1171
|
-
});
|
|
1172
|
-
|
|
1173
|
-
// TypeScript knows these are valid field names
|
|
1174
|
-
db.from("users").list().select("username", "email");
|
|
1175
|
-
|
|
1176
|
-
// TypeScript error: "invalid" is not a field name
|
|
1177
|
-
db.from("users").list().select("invalid"); // TS Error
|
|
1178
|
-
|
|
1179
|
-
// Type-safe filters
|
|
1180
|
-
db.from("users")
|
|
1181
|
-
.list()
|
|
1182
|
-
.filter({ username: { eq: "john" } }); // ✓
|
|
1183
|
-
db.from("users")
|
|
1184
|
-
.list()
|
|
1185
|
-
.filter({ invalid: { eq: "john" } }); // TS Error
|
|
1186
|
-
```
|
|
1187
|
-
|
|
1188
1372
|
### Required and Read-Only Fields
|
|
1189
1373
|
|
|
1190
|
-
The library automatically infers which fields are required based on
|
|
1374
|
+
The library automatically infers which fields are required based on field builder configuration:
|
|
1191
1375
|
|
|
1192
1376
|
```typescript
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
updatedAt: z.string().nullable(), // Optional
|
|
1201
|
-
},
|
|
1202
|
-
idField: "id", // Automatically excluded from insert/update
|
|
1203
|
-
required: ["status"], // Make status required despite being nullable
|
|
1204
|
-
readOnly: ["createdAt"], // Exclude createdAt from insert/update
|
|
1377
|
+
const users = fmTableOccurrence("users", {
|
|
1378
|
+
id: textField().primaryKey(), // Auto-required, auto-readOnly (primaryKey)
|
|
1379
|
+
username: textField().notNull(), // Auto-required (notNull)
|
|
1380
|
+
email: textField().notNull(), // Auto-required (notNull)
|
|
1381
|
+
status: textField(), // Optional (nullable by default)
|
|
1382
|
+
createdAt: timestampField().readOnly(), // Read-only system field
|
|
1383
|
+
updatedAt: timestampField(), // Optional (nullable)
|
|
1205
1384
|
});
|
|
1206
1385
|
|
|
1207
|
-
// Insert: username
|
|
1208
|
-
// Insert: id and createdAt are excluded (cannot be provided)
|
|
1209
|
-
db.from(
|
|
1386
|
+
// Insert: username and email are required
|
|
1387
|
+
// Insert: id and createdAt are excluded (cannot be provided - read-only)
|
|
1388
|
+
db.from(users).insert({
|
|
1210
1389
|
username: "john",
|
|
1211
1390
|
email: "john@example.com",
|
|
1212
|
-
status: "active", //
|
|
1391
|
+
status: "active", // Optional
|
|
1213
1392
|
updatedAt: new Date().toISOString(), // Optional
|
|
1214
1393
|
});
|
|
1215
1394
|
|
|
1216
1395
|
// Update: all fields are optional except id and createdAt are excluded
|
|
1217
|
-
db.from(
|
|
1396
|
+
db.from(users)
|
|
1218
1397
|
.update({
|
|
1219
1398
|
status: "active", // Optional
|
|
1220
|
-
// id and createdAt cannot be modified
|
|
1399
|
+
// id and createdAt cannot be modified (read-only)
|
|
1221
1400
|
})
|
|
1222
1401
|
.byId("user-123");
|
|
1223
1402
|
```
|
|
1224
1403
|
|
|
1225
1404
|
**Key Features:**
|
|
1226
1405
|
|
|
1227
|
-
- **Auto-inference:**
|
|
1228
|
-
- **
|
|
1229
|
-
- **Read-only fields:** Use
|
|
1230
|
-
- **Automatic ID exclusion:** The `idField` is always read-only without needing to specify it
|
|
1406
|
+
- **Auto-inference:** Fields with `.notNull()` are automatically required for insert
|
|
1407
|
+
- **Primary keys:** Fields with `.primaryKey()` are automatically read-only
|
|
1408
|
+
- **Read-only fields:** Use `.readOnly()` to exclude fields from insert/update (e.g., timestamps, calculated fields)
|
|
1231
1409
|
- **Update flexibility:** All fields are optional for updates (except read-only fields)
|
|
1232
1410
|
|
|
1233
1411
|
### Prefer: fmodata.entity-ids
|
|
1234
1412
|
|
|
1235
1413
|
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.
|
|
1236
1414
|
|
|
1237
|
-
To enable this feature, simply define your schema with entity IDs using the `
|
|
1415
|
+
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).
|
|
1238
1416
|
|
|
1239
1417
|
_Note for OttoFMS proxy: This feature requires version 4.14 or later of OttoFMS_
|
|
1240
1418
|
|
|
@@ -1243,34 +1421,58 @@ How do I find these ids? They can be found in the XML version of the `$metadata`
|
|
|
1243
1421
|
#### Basic Usage
|
|
1244
1422
|
|
|
1245
1423
|
```typescript
|
|
1246
|
-
import {
|
|
1247
|
-
|
|
1424
|
+
import {
|
|
1425
|
+
fmTableOccurrence,
|
|
1426
|
+
textField,
|
|
1427
|
+
timestampField,
|
|
1428
|
+
} from "@proofkit/fmodata";
|
|
1248
1429
|
|
|
1249
|
-
// Define a
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1430
|
+
// Define a table with FileMaker field IDs and table occurrence ID
|
|
1431
|
+
const users = fmTableOccurrence(
|
|
1432
|
+
"users",
|
|
1433
|
+
{
|
|
1434
|
+
id: textField().primaryKey().entityId("FMFID:12039485"),
|
|
1435
|
+
username: textField().notNull().entityId("FMFID:34323433"),
|
|
1436
|
+
email: textField().entityId("FMFID:12232424"),
|
|
1437
|
+
createdAt: timestampField().readOnly().entityId("FMFID:43234355"),
|
|
1256
1438
|
},
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
id: "FMFID:12039485",
|
|
1260
|
-
username: "FMFID:34323433",
|
|
1261
|
-
email: "FMFID:12232424",
|
|
1262
|
-
createdAt: "FMFID:43234355",
|
|
1439
|
+
{
|
|
1440
|
+
entityId: "FMTID:12432533", // FileMaker table occurrence ID
|
|
1263
1441
|
},
|
|
1442
|
+
);
|
|
1443
|
+
```
|
|
1444
|
+
|
|
1445
|
+
### Special Columns (ROWID and ROWMODID)
|
|
1446
|
+
|
|
1447
|
+
FileMaker provides special columns `ROWID` and `ROWMODID` that uniquely identify records and track modifications. These can be included in query responses when enabled.
|
|
1448
|
+
|
|
1449
|
+
Enable special columns at the database level:
|
|
1450
|
+
|
|
1451
|
+
```typescript
|
|
1452
|
+
const db = connection.database("MyDatabase", {
|
|
1453
|
+
includeSpecialColumns: true,
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
const result = await db.from(users).list().execute();
|
|
1457
|
+
// result.data[0] will have ROWID and ROWMODID properties
|
|
1458
|
+
```
|
|
1459
|
+
|
|
1460
|
+
Override at the request level:
|
|
1461
|
+
|
|
1462
|
+
```typescript
|
|
1463
|
+
// Enable for this request only
|
|
1464
|
+
const result = await db.from(users).list().execute({
|
|
1465
|
+
includeSpecialColumns: true,
|
|
1264
1466
|
});
|
|
1265
1467
|
|
|
1266
|
-
//
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1269
|
-
baseTable: usersBase,
|
|
1270
|
-
fmtId: "FMTID:12432533",
|
|
1468
|
+
// Disable for this request
|
|
1469
|
+
const result = await db.from(users).list().execute({
|
|
1470
|
+
includeSpecialColumns: false,
|
|
1271
1471
|
});
|
|
1272
1472
|
```
|
|
1273
1473
|
|
|
1474
|
+
**Important:** Special columns are only included when no `$select` query is applied (per OData specification). When using `.select()`, special columns are excluded even if `includeSpecialColumns` is enabled.
|
|
1475
|
+
|
|
1274
1476
|
### Error Handling
|
|
1275
1477
|
|
|
1276
1478
|
All operations return a `Result` type with either `data` or `error`. The library provides rich error types that help you handle different error scenarios appropriately.
|
|
@@ -1278,7 +1480,7 @@ All operations return a `Result` type with either `data` or `error`. The library
|
|
|
1278
1480
|
#### Basic Error Checking
|
|
1279
1481
|
|
|
1280
1482
|
```typescript
|
|
1281
|
-
const result = await db.from(
|
|
1483
|
+
const result = await db.from(users).list().execute();
|
|
1282
1484
|
|
|
1283
1485
|
if (result.error) {
|
|
1284
1486
|
console.error("Query failed:", result.error.message);
|
|
@@ -1297,7 +1499,7 @@ Handle HTTP status codes (4xx, 5xx) with the `HTTPError` class:
|
|
|
1297
1499
|
```typescript
|
|
1298
1500
|
import { HTTPError, isHTTPError } from "@proofkit/fmodata";
|
|
1299
1501
|
|
|
1300
|
-
const result = await db.from(
|
|
1502
|
+
const result = await db.from(users).list().execute();
|
|
1301
1503
|
|
|
1302
1504
|
if (result.error) {
|
|
1303
1505
|
if (isHTTPError(result.error)) {
|
|
@@ -1334,7 +1536,7 @@ import {
|
|
|
1334
1536
|
CircuitOpenError,
|
|
1335
1537
|
} from "@proofkit/fmodata";
|
|
1336
1538
|
|
|
1337
|
-
const result = await db.from(
|
|
1539
|
+
const result = await db.from(users).list().execute();
|
|
1338
1540
|
|
|
1339
1541
|
if (result.error) {
|
|
1340
1542
|
if (result.error instanceof TimeoutError) {
|
|
@@ -1360,7 +1562,7 @@ When schema validation fails, you get a `ValidationError` with rich context:
|
|
|
1360
1562
|
```typescript
|
|
1361
1563
|
import { ValidationError, isValidationError } from "@proofkit/fmodata";
|
|
1362
1564
|
|
|
1363
|
-
const result = await db.from(
|
|
1565
|
+
const result = await db.from(users).list().execute();
|
|
1364
1566
|
|
|
1365
1567
|
if (result.error) {
|
|
1366
1568
|
if (isValidationError(result.error)) {
|
|
@@ -1379,7 +1581,7 @@ The library uses [Standard Schema](https://github.com/standard-schema/standard-s
|
|
|
1379
1581
|
```typescript
|
|
1380
1582
|
import { ValidationError } from "@proofkit/fmodata";
|
|
1381
1583
|
|
|
1382
|
-
const result = await db.from(
|
|
1584
|
+
const result = await db.from(users).list().execute();
|
|
1383
1585
|
|
|
1384
1586
|
if (result.error instanceof ValidationError) {
|
|
1385
1587
|
// The cause property (ES2022 Error.cause) contains the Standard Schema issues array
|
|
@@ -1432,7 +1634,7 @@ Handle OData-specific protocol errors:
|
|
|
1432
1634
|
```typescript
|
|
1433
1635
|
import { ODataError, isODataError } from "@proofkit/fmodata";
|
|
1434
1636
|
|
|
1435
|
-
const result = await db.from(
|
|
1637
|
+
const result = await db.from(users).list().execute();
|
|
1436
1638
|
|
|
1437
1639
|
if (result.error) {
|
|
1438
1640
|
if (isODataError(result.error)) {
|
|
@@ -1455,7 +1657,7 @@ import {
|
|
|
1455
1657
|
NetworkError,
|
|
1456
1658
|
} from "@proofkit/fmodata";
|
|
1457
1659
|
|
|
1458
|
-
const result = await db.from(
|
|
1660
|
+
const result = await db.from(users).list().execute();
|
|
1459
1661
|
|
|
1460
1662
|
if (result.error) {
|
|
1461
1663
|
if (result.error instanceof TimeoutError) {
|
|
@@ -1477,7 +1679,7 @@ if (result.error) {
|
|
|
1477
1679
|
**Pattern 2: Using kind property (for exhaustive matching):**
|
|
1478
1680
|
|
|
1479
1681
|
```typescript
|
|
1480
|
-
const result = await db.from(
|
|
1682
|
+
const result = await db.from(users).list().execute();
|
|
1481
1683
|
|
|
1482
1684
|
if (result.error) {
|
|
1483
1685
|
switch (result.error.kind) {
|
|
@@ -1633,7 +1835,7 @@ const queryString = db
|
|
|
1633
1835
|
.from("users")
|
|
1634
1836
|
.list()
|
|
1635
1837
|
.select("username", "email")
|
|
1636
|
-
.
|
|
1838
|
+
.where(eq(users.active, true))
|
|
1637
1839
|
.orderBy("username")
|
|
1638
1840
|
.top(10)
|
|
1639
1841
|
.getQueryString();
|