@proofkit/fmodata 0.1.0-alpha.2 → 0.1.0-alpha.20
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 +1250 -377
- package/dist/esm/client/batch-builder.d.ts +56 -0
- package/dist/esm/client/batch-builder.js +238 -0
- package/dist/esm/client/batch-builder.js.map +1 -0
- package/dist/esm/client/batch-request.d.ts +61 -0
- package/dist/esm/client/batch-request.js +252 -0
- package/dist/esm/client/batch-request.js.map +1 -0
- package/dist/esm/client/builders/default-select.d.ts +10 -0
- package/dist/esm/client/builders/default-select.js +43 -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 +174 -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 +18 -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 +43 -0
- package/dist/esm/client/builders/response-processor.js +176 -0
- package/dist/esm/client/builders/response-processor.js.map +1 -0
- package/dist/esm/client/builders/select-mixin.d.ts +32 -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 +18 -0
- package/dist/esm/client/builders/select-utils.js +23 -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 +45 -0
- package/dist/esm/client/builders/table-utils.js.map +1 -0
- package/dist/esm/client/database.d.ts +68 -15
- package/dist/esm/client/database.js +88 -34
- package/dist/esm/client/database.js.map +1 -1
- package/dist/esm/client/delete-builder.d.ts +31 -17
- package/dist/esm/client/delete-builder.js +114 -47
- package/dist/esm/client/delete-builder.js.map +1 -1
- package/dist/esm/client/entity-set.d.ts +33 -27
- package/dist/esm/client/entity-set.js +123 -45
- 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 +44 -6
- package/dist/esm/client/filemaker-odata.js +172 -28
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/dist/esm/client/insert-builder.d.ts +39 -9
- package/dist/esm/client/insert-builder.js +265 -36
- 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 +139 -0
- package/dist/esm/client/query/query-builder.js +481 -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 +107 -0
- package/dist/esm/client/query/url-builder.js.map +1 -0
- package/dist/esm/client/query-builder.d.ts +1 -94
- package/dist/esm/client/record-builder.d.ts +107 -22
- package/dist/esm/client/record-builder.js +342 -64
- package/dist/esm/client/record-builder.js.map +1 -1
- package/dist/esm/client/response-processor.d.ts +33 -0
- 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 +57 -0
- package/dist/esm/client/schema-manager.js +132 -0
- package/dist/esm/client/schema-manager.js.map +1 -0
- package/dist/esm/client/update-builder.d.ts +42 -25
- package/dist/esm/client/update-builder.js +179 -46
- 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 +197 -0
- package/dist/esm/client/webhook-builder.js.map +1 -0
- package/dist/esm/errors.d.ts +90 -0
- package/dist/esm/errors.js +180 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.d.ts +12 -4
- package/dist/esm/index.js +59 -6
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/logger.d.ts +47 -0
- package/dist/esm/logger.js +72 -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 +62 -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 +168 -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 +242 -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 +200 -0
- package/dist/esm/orm/table.js.map +1 -0
- package/dist/esm/transform.d.ts +64 -0
- package/dist/esm/transform.js +110 -0
- package/dist/esm/transform.js.map +1 -0
- package/dist/esm/types.d.ts +157 -7
- package/dist/esm/types.js +7 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/validation.d.ts +22 -9
- package/dist/esm/validation.js +195 -50
- package/dist/esm/validation.js.map +1 -1
- package/package.json +19 -4
- package/src/client/batch-builder.ts +334 -0
- package/src/client/batch-request.ts +485 -0
- package/src/client/builders/default-select.ts +80 -0
- package/src/client/builders/expand-builder.ts +245 -0
- package/src/client/builders/index.ts +11 -0
- package/src/client/builders/query-string-builder.ts +49 -0
- package/src/client/builders/response-processor.ts +286 -0
- package/src/client/builders/select-mixin.ts +75 -0
- package/src/client/builders/select-utils.ts +56 -0
- package/src/client/builders/shared-types.ts +42 -0
- package/src/client/builders/table-utils.ts +87 -0
- package/src/client/database.ts +147 -89
- package/src/client/delete-builder.ts +189 -87
- package/src/client/entity-set.ts +316 -205
- package/src/client/error-parser.ts +59 -0
- package/src/client/filemaker-odata.ts +254 -41
- package/src/client/insert-builder.ts +420 -49
- package/src/client/query/expand-builder.ts +164 -0
- package/src/client/query/index.ts +13 -0
- package/src/client/query/query-builder.ts +905 -0
- package/src/client/query/response-processor.ts +236 -0
- package/src/client/query/types.ts +128 -0
- package/src/client/query/url-builder.ts +179 -0
- package/src/client/query-builder.ts +8 -1076
- package/src/client/record-builder.ts +704 -139
- package/src/client/response-processor.ts +89 -0
- package/src/client/sanitize-json.ts +66 -0
- package/src/client/schema-manager.ts +246 -0
- package/src/client/update-builder.ts +318 -90
- package/src/client/webhook-builder.ts +285 -0
- package/src/errors.ts +261 -0
- package/src/index.ts +122 -14
- package/src/logger.test.ts +34 -0
- package/src/logger.ts +140 -0
- package/src/orm/column.ts +106 -0
- package/src/orm/field-builders.ts +318 -0
- package/src/orm/index.ts +60 -0
- package/src/orm/operators.ts +487 -0
- package/src/orm/table.ts +759 -0
- package/src/transform.ts +263 -0
- package/src/types.ts +275 -55
- package/src/validation.ts +255 -55
- package/dist/esm/client/base-table.d.ts +0 -13
- package/dist/esm/client/base-table.js +0 -19
- package/dist/esm/client/base-table.js.map +0 -1
- package/dist/esm/client/query-builder.js +0 -649
- package/dist/esm/client/query-builder.js.map +0 -1
- package/dist/esm/client/table-occurrence.d.ts +0 -25
- package/dist/esm/client/table-occurrence.js +0 -47
- 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 -25
- package/src/client/table-occurrence.ts +0 -100
- package/src/filter-types.ts +0 -97
package/README.md
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
A strongly-typed FileMaker OData API client.
|
|
4
4
|
|
|
5
|
-
⚠️ WARNING: This library is in "alpha" status.
|
|
5
|
+
⚠️ WARNING: This library is in "alpha" status. It's still in active development and the API is subject to change. Feedback is welcome on the [community forum](https://community.ottomatic.cloud/c/proofkit/13) or on [GitHub](https://github.com/proofgeist/proofkit/issues).
|
|
6
6
|
|
|
7
7
|
Roadmap:
|
|
8
8
|
|
|
9
|
-
- [ ]
|
|
9
|
+
- [ ] Crossjoin support
|
|
10
|
+
- [x] Batch operations
|
|
11
|
+
- [ ] Automatically chunk requests into smaller batches (e.g. max 512 inserts per batch)
|
|
12
|
+
- [x] Schema updates (add/update tables and fields)
|
|
10
13
|
- [ ] Proper docs at proofkit.dev
|
|
11
14
|
- [ ] @proofkit/typegen integration
|
|
12
15
|
|
|
@@ -23,8 +26,10 @@ Here's a minimal example to get you started:
|
|
|
23
26
|
```typescript
|
|
24
27
|
import {
|
|
25
28
|
FMServerConnection,
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
fmTableOccurrence,
|
|
30
|
+
textField,
|
|
31
|
+
numberField,
|
|
32
|
+
eq,
|
|
28
33
|
} from "@proofkit/fmodata";
|
|
29
34
|
import { z } from "zod/v4";
|
|
30
35
|
|
|
@@ -41,30 +46,21 @@ const connection = new FMServerConnection({
|
|
|
41
46
|
},
|
|
42
47
|
});
|
|
43
48
|
|
|
44
|
-
// 2. Define your table schema
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
idField: "id",
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// 3. Create a table occurrence
|
|
56
|
-
const usersTO = new TableOccurrence({
|
|
57
|
-
name: "users",
|
|
58
|
-
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))),
|
|
59
57
|
});
|
|
60
58
|
|
|
61
|
-
//
|
|
62
|
-
const db = connection.database("MyDatabase.fmp12"
|
|
63
|
-
occurrences: [usersTO],
|
|
64
|
-
});
|
|
59
|
+
// 3. Create a database instance
|
|
60
|
+
const db = connection.database("MyDatabase.fmp12");
|
|
65
61
|
|
|
66
|
-
//
|
|
67
|
-
const { data, error } = await db.from(
|
|
62
|
+
// 4. Query your data
|
|
63
|
+
const { data, error } = await db.from(users).list().execute();
|
|
68
64
|
|
|
69
65
|
if (error) {
|
|
70
66
|
console.error(error);
|
|
@@ -78,13 +74,12 @@ if (data) {
|
|
|
78
74
|
|
|
79
75
|
## Core Concepts
|
|
80
76
|
|
|
81
|
-
This library relies heavily on the builder pattern for defining your queries and operations. Most operations require a final call to `execute()` to send the request to the server. The builder pattern
|
|
77
|
+
This library relies heavily on the builder pattern for defining your queries and operations. Most operations require a final call to `execute()` to send the request to the server. The builder pattern allows you to build complex queries and also supports batch operations, allowing you to execute multiple operations in a single request as supported by the FileMaker OData API. It's also helpful for testing the library, as you can call `getQueryString()` to get the OData query string without executing the request.
|
|
82
78
|
|
|
83
79
|
As such, there are layers to the library to help you build your queries and operations.
|
|
84
80
|
|
|
85
81
|
- `FMServerConnection` - hold server connection details and authentication
|
|
86
|
-
- `
|
|
87
|
-
- `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
|
|
88
83
|
- `Database` - connects the table occurrences to the server connection
|
|
89
84
|
|
|
90
85
|
### FileMaker Server prerequisites
|
|
@@ -97,7 +92,7 @@ To use this library you need:
|
|
|
97
92
|
|
|
98
93
|
A note on best practices:
|
|
99
94
|
|
|
100
|
-
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.
|
|
101
96
|
|
|
102
97
|
### Server Connection
|
|
103
98
|
|
|
@@ -124,85 +119,118 @@ const connection = new FMServerConnection({
|
|
|
124
119
|
|
|
125
120
|
### Schema Definitions
|
|
126
121
|
|
|
127
|
-
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.
|
|
128
123
|
|
|
129
|
-
|
|
124
|
+
#### Field Builders
|
|
130
125
|
|
|
131
|
-
|
|
132
|
-
import { z } from "zod/v4";
|
|
133
|
-
import { BaseTable } from "@proofkit/fmodata";
|
|
134
|
-
|
|
135
|
-
const contactsBase = new BaseTable({
|
|
136
|
-
schema: {
|
|
137
|
-
id: z.string(),
|
|
138
|
-
name: z.string(),
|
|
139
|
-
email: z.string(),
|
|
140
|
-
phone: z.string().optional(),
|
|
141
|
-
createdAt: z.string(),
|
|
142
|
-
},
|
|
143
|
-
idField: "id", // The primary key field
|
|
144
|
-
insertRequired: ["name", "email"], // optional: fields that are required on insert
|
|
145
|
-
updateRequired: ["email"], // optional: fields that are required on update
|
|
146
|
-
});
|
|
147
|
-
```
|
|
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
|
|
148
127
|
|
|
149
|
-
|
|
128
|
+
- `textField()`
|
|
129
|
+
- `numberField()`
|
|
130
|
+
- `dateField()`
|
|
131
|
+
- `timeField()`
|
|
132
|
+
- `timestampField()`
|
|
133
|
+
- `containerField()`
|
|
134
|
+
- `calcField()`
|
|
135
|
+
|
|
136
|
+
Each field builder supports chainable methods:
|
|
137
|
+
|
|
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:
|
|
150
148
|
|
|
151
149
|
```typescript
|
|
152
|
-
import {
|
|
150
|
+
import { z } from "zod/v4";
|
|
151
|
+
import {
|
|
152
|
+
fmTableOccurrence,
|
|
153
|
+
textField,
|
|
154
|
+
numberField,
|
|
155
|
+
timestampField,
|
|
156
|
+
} from "@proofkit/fmodata";
|
|
153
157
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
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"),
|
|
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
|
+
);
|
|
158
173
|
```
|
|
159
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
|
+
|
|
160
181
|
#### Default Field Selection
|
|
161
182
|
|
|
162
|
-
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:
|
|
163
184
|
|
|
164
185
|
```typescript
|
|
165
|
-
// Option 1 (default): "schema" - Select all fields from the schema
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
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
|
+
);
|
|
171
196
|
|
|
172
|
-
// Option 2: "all" - Select all fields (default behavior)
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
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
|
+
);
|
|
178
207
|
|
|
179
|
-
// Option 3:
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
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
|
+
);
|
|
185
221
|
|
|
186
|
-
// When you call list(), the defaultSelect is applied automatically
|
|
187
|
-
const result = await db.from(
|
|
188
|
-
// 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
|
|
189
225
|
|
|
190
226
|
// You can still override with explicit select()
|
|
191
227
|
const result = await db
|
|
192
|
-
.from(
|
|
228
|
+
.from(users)
|
|
193
229
|
.list()
|
|
194
|
-
.select(
|
|
230
|
+
.select({ username: users.username, email: users.email, age: users.age }) // Always overrides at the per-request level
|
|
195
231
|
.execute();
|
|
196
232
|
```
|
|
197
233
|
|
|
198
|
-
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.
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
const db = connection.database("MyDatabase.fmp12", {
|
|
202
|
-
occurrences: [contactsTO, usersTO], // Register your table occurrences
|
|
203
|
-
});
|
|
204
|
-
```
|
|
205
|
-
|
|
206
234
|
## Querying Data
|
|
207
235
|
|
|
208
236
|
### Basic Queries
|
|
@@ -234,9 +262,9 @@ Get a single field value:
|
|
|
234
262
|
|
|
235
263
|
```typescript
|
|
236
264
|
const result = await db
|
|
237
|
-
.from(
|
|
265
|
+
.from(users)
|
|
238
266
|
.get("user-123")
|
|
239
|
-
.getSingleField(
|
|
267
|
+
.getSingleField(users.email)
|
|
240
268
|
.execute();
|
|
241
269
|
|
|
242
270
|
if (result.data) {
|
|
@@ -246,173 +274,103 @@ if (result.data) {
|
|
|
246
274
|
|
|
247
275
|
### Filtering
|
|
248
276
|
|
|
249
|
-
fmodata provides type-safe filter operations that prevent common errors at compile time.
|
|
250
|
-
|
|
251
|
-
#### Operator Syntax
|
|
252
|
-
|
|
253
|
-
You can use filters in three ways:
|
|
254
|
-
|
|
255
|
-
**1. Shorthand (direct value):**
|
|
256
|
-
|
|
257
|
-
```typescript
|
|
258
|
-
.filter({ name: "John" })
|
|
259
|
-
// Equivalent to: { name: [{ eq: "John" }] }
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
**2. Single operator object:**
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
.filter({ age: { gt: 18 } })
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
**3. Array of operators (for multiple operators on same field):**
|
|
269
|
-
|
|
270
|
-
```typescript
|
|
271
|
-
.filter({ age: [{ gt: 18 }, { lt: 65 }] })
|
|
272
|
-
// Result: age gt 18 and age lt 65
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
The array pattern prevents duplicate operators on the same field and allows multiple conditions with implicit AND.
|
|
276
|
-
|
|
277
|
-
#### Available Operators
|
|
278
|
-
|
|
279
|
-
**String fields:**
|
|
280
|
-
|
|
281
|
-
- `eq`, `ne` - equality/inequality
|
|
282
|
-
- `contains`, `startswith`, `endswith` - string functions
|
|
283
|
-
- `gt`, `ge`, `lt`, `le` - comparison
|
|
284
|
-
- `in` - match any value in array
|
|
285
|
-
|
|
286
|
-
**Number fields:**
|
|
287
|
-
|
|
288
|
-
- `eq`, `ne`, `gt`, `ge`, `lt`, `le` - comparisons
|
|
289
|
-
- `in` - match any value in array
|
|
290
|
-
|
|
291
|
-
**Boolean fields:**
|
|
292
|
-
|
|
293
|
-
- `eq`, `ne` - equality only
|
|
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.
|
|
294
278
|
|
|
295
|
-
|
|
279
|
+
#### New ORM-Style API (Recommended)
|
|
296
280
|
|
|
297
|
-
|
|
298
|
-
- `in` - match any date in array
|
|
299
|
-
|
|
300
|
-
#### Shorthand Syntax
|
|
301
|
-
|
|
302
|
-
For simple equality checks, use the shorthand:
|
|
303
|
-
|
|
304
|
-
```typescript
|
|
305
|
-
const result = await db.from("users").list().filter({ name: "John" }).execute();
|
|
306
|
-
// Equivalent to: { name: [{ eq: "John" }] }
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
#### Examples
|
|
281
|
+
Use the `where()` method with filter operators and column references for type-safe filtering:
|
|
310
282
|
|
|
311
283
|
```typescript
|
|
312
|
-
|
|
313
|
-
const activeUsers = await db
|
|
314
|
-
.from("users")
|
|
315
|
-
.list()
|
|
316
|
-
.filter({ active: { eq: true } })
|
|
317
|
-
.execute();
|
|
318
|
-
|
|
319
|
-
// Comparison operators (single operator)
|
|
320
|
-
const adultUsers = await db
|
|
321
|
-
.from("users")
|
|
322
|
-
.list()
|
|
323
|
-
.filter({ age: { gt: 18 } })
|
|
324
|
-
.execute();
|
|
325
|
-
|
|
326
|
-
// String operators (single operator)
|
|
327
|
-
const johns = await db
|
|
328
|
-
.from("users")
|
|
329
|
-
.list()
|
|
330
|
-
.filter({ name: { contains: "John" } })
|
|
331
|
-
.execute();
|
|
332
|
-
|
|
333
|
-
// Multiple operators on same field (array syntax, implicit AND)
|
|
334
|
-
const rangeQuery = await db
|
|
335
|
-
.from("users")
|
|
336
|
-
.list()
|
|
337
|
-
.filter({ age: [{ gt: 18 }, { lt: 65 }] })
|
|
338
|
-
.execute();
|
|
284
|
+
import { eq, gt, and, or, contains } from "@proofkit/fmodata";
|
|
339
285
|
|
|
340
|
-
//
|
|
286
|
+
// Simple equality
|
|
341
287
|
const result = await db
|
|
342
|
-
.from(
|
|
288
|
+
.from(users)
|
|
343
289
|
.list()
|
|
344
|
-
.
|
|
345
|
-
and: [{ active: [{ eq: true }] }, { age: [{ gt: 18 }] }],
|
|
346
|
-
})
|
|
290
|
+
.where(eq(users.active, true))
|
|
347
291
|
.execute();
|
|
348
292
|
|
|
349
|
-
//
|
|
293
|
+
// Comparison operators
|
|
294
|
+
const result = await db.from(users).list().where(gt(users.age, 18)).execute();
|
|
295
|
+
|
|
296
|
+
// String operators
|
|
350
297
|
const result = await db
|
|
351
|
-
.from(
|
|
298
|
+
.from(users)
|
|
352
299
|
.list()
|
|
353
|
-
.
|
|
354
|
-
or: [{ name: [{ eq: "John" }] }, { name: [{ eq: "Jane" }] }],
|
|
355
|
-
})
|
|
300
|
+
.where(contains(users.name, "John"))
|
|
356
301
|
.execute();
|
|
357
302
|
|
|
358
|
-
//
|
|
303
|
+
// Combine with AND
|
|
359
304
|
const result = await db
|
|
360
|
-
.from(
|
|
305
|
+
.from(users)
|
|
361
306
|
.list()
|
|
362
|
-
.
|
|
307
|
+
.where(and(eq(users.active, true), gt(users.age, 18)))
|
|
363
308
|
.execute();
|
|
364
309
|
|
|
365
|
-
//
|
|
310
|
+
// Combine with OR
|
|
366
311
|
const result = await db
|
|
367
|
-
.from(
|
|
312
|
+
.from(users)
|
|
368
313
|
.list()
|
|
369
|
-
.
|
|
314
|
+
.where(or(eq(users.role, "admin"), eq(users.role, "moderator")))
|
|
370
315
|
.execute();
|
|
371
316
|
```
|
|
372
317
|
|
|
373
|
-
|
|
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).
|
|
374
329
|
|
|
375
|
-
|
|
330
|
+
#### Using Column References (New ORM API)
|
|
376
331
|
|
|
377
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)
|
|
378
343
|
const result = await db
|
|
379
|
-
.from(
|
|
344
|
+
.from(users)
|
|
380
345
|
.list()
|
|
381
|
-
.
|
|
382
|
-
and: [{ name: [{ contains: "John" }] }, { age: [{ gt: 18 }] }],
|
|
383
|
-
})
|
|
346
|
+
.orderBy(asc(users.lastName), desc(users.firstName))
|
|
384
347
|
.execute();
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
#### Escape Hatch
|
|
388
|
-
|
|
389
|
-
For unsupported edge cases, pass a raw OData filter string:
|
|
390
348
|
|
|
391
|
-
|
|
349
|
+
// Multiple fields (array syntax)
|
|
392
350
|
const result = await db
|
|
393
|
-
.from(
|
|
351
|
+
.from(users)
|
|
394
352
|
.list()
|
|
395
|
-
.
|
|
353
|
+
.orderBy([
|
|
354
|
+
[users.lastName, "asc"],
|
|
355
|
+
[users.firstName, "desc"],
|
|
356
|
+
])
|
|
396
357
|
.execute();
|
|
397
358
|
```
|
|
398
359
|
|
|
399
|
-
|
|
360
|
+
#### Type Safety
|
|
400
361
|
|
|
401
|
-
|
|
362
|
+
For typed databases, `orderBy()` provides full type safety:
|
|
402
363
|
|
|
403
364
|
```typescript
|
|
404
|
-
//
|
|
405
|
-
|
|
365
|
+
// ✅ Valid - "name" is a field in the schema
|
|
366
|
+
db.from(users).list().orderBy(users.name);
|
|
406
367
|
|
|
407
|
-
//
|
|
408
|
-
|
|
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));
|
|
409
371
|
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
.from("users")
|
|
413
|
-
.list()
|
|
414
|
-
.orderBy("lastName, firstName desc")
|
|
415
|
-
.execute();
|
|
372
|
+
// ✅ Valid - multiple fields
|
|
373
|
+
db.from(users).list().orderBy(asc(users.lastName), desc(users.firstName));
|
|
416
374
|
```
|
|
417
375
|
|
|
418
376
|
### Pagination
|
|
@@ -421,24 +379,29 @@ Control the number of records returned and pagination:
|
|
|
421
379
|
|
|
422
380
|
```typescript
|
|
423
381
|
// Limit results
|
|
424
|
-
const result = await db.from(
|
|
382
|
+
const result = await db.from(users).list().top(10).execute();
|
|
425
383
|
|
|
426
384
|
// Skip records (pagination)
|
|
427
|
-
const result = await db.from(
|
|
385
|
+
const result = await db.from(users).list().top(10).skip(20).execute();
|
|
428
386
|
|
|
429
387
|
// Count total records
|
|
430
|
-
const result = await db.from(
|
|
388
|
+
const result = await db.from(users).list().count().execute();
|
|
431
389
|
```
|
|
432
390
|
|
|
433
391
|
### Selecting Fields
|
|
434
392
|
|
|
435
|
-
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):
|
|
436
394
|
|
|
437
395
|
```typescript
|
|
396
|
+
// New ORM API: Using column references (type-safe, supports renaming)
|
|
438
397
|
const result = await db
|
|
439
|
-
.from(
|
|
398
|
+
.from(users)
|
|
440
399
|
.list()
|
|
441
|
-
.select(
|
|
400
|
+
.select({
|
|
401
|
+
username: users.username,
|
|
402
|
+
email: users.email,
|
|
403
|
+
userId: users.id, // Renamed from "id" to "userId"
|
|
404
|
+
})
|
|
442
405
|
.execute();
|
|
443
406
|
|
|
444
407
|
// result.data[0] will only have username and email fields
|
|
@@ -450,9 +413,9 @@ Use `single()` to ensure exactly one record is returned (returns an error if zer
|
|
|
450
413
|
|
|
451
414
|
```typescript
|
|
452
415
|
const result = await db
|
|
453
|
-
.from(
|
|
416
|
+
.from(users)
|
|
454
417
|
.list()
|
|
455
|
-
.
|
|
418
|
+
.where(eq(users.email, "user@example.com"))
|
|
456
419
|
.single()
|
|
457
420
|
.execute();
|
|
458
421
|
|
|
@@ -466,9 +429,9 @@ Use `maybeSingle()` when you want at most one record (returns `null` if no recor
|
|
|
466
429
|
|
|
467
430
|
```typescript
|
|
468
431
|
const result = await db
|
|
469
|
-
.from(
|
|
432
|
+
.from(users)
|
|
470
433
|
.list()
|
|
471
|
-
.
|
|
434
|
+
.where(eq(users.email, "user@example.com"))
|
|
472
435
|
.maybeSingle()
|
|
473
436
|
.execute();
|
|
474
437
|
|
|
@@ -491,12 +454,17 @@ if (result.data) {
|
|
|
491
454
|
All query methods can be chained together:
|
|
492
455
|
|
|
493
456
|
```typescript
|
|
457
|
+
// Using new ORM API
|
|
494
458
|
const result = await db
|
|
495
|
-
.from(
|
|
459
|
+
.from(users)
|
|
496
460
|
.list()
|
|
497
|
-
.select(
|
|
498
|
-
|
|
499
|
-
|
|
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))
|
|
500
468
|
.top(10)
|
|
501
469
|
.skip(0)
|
|
502
470
|
.execute();
|
|
@@ -511,7 +479,7 @@ Insert new records with type-safe data:
|
|
|
511
479
|
```typescript
|
|
512
480
|
// Insert a new user
|
|
513
481
|
const result = await db
|
|
514
|
-
.from(
|
|
482
|
+
.from(users)
|
|
515
483
|
.insert({
|
|
516
484
|
username: "johndoe",
|
|
517
485
|
email: "john@example.com",
|
|
@@ -524,27 +492,25 @@ if (result.data) {
|
|
|
524
492
|
}
|
|
525
493
|
```
|
|
526
494
|
|
|
527
|
-
|
|
495
|
+
Fields are automatically required for insert if they use `.notNull()`. Read-only fields (including primary keys) are automatically excluded:
|
|
528
496
|
|
|
529
497
|
```typescript
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
},
|
|
537
|
-
idField: "id",
|
|
538
|
-
insertRequired: ["username", "email"], // These fields are required on insert
|
|
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
|
|
539
504
|
});
|
|
540
505
|
|
|
541
|
-
// TypeScript
|
|
506
|
+
// TypeScript enforces: username and email are required
|
|
507
|
+
// TypeScript excludes: id and createdAt cannot be provided
|
|
542
508
|
const result = await db
|
|
543
|
-
.from(
|
|
509
|
+
.from(users)
|
|
544
510
|
.insert({
|
|
545
511
|
username: "johndoe",
|
|
546
512
|
email: "john@example.com",
|
|
547
|
-
|
|
513
|
+
phone: "+1234567890", // Optional
|
|
548
514
|
})
|
|
549
515
|
.execute();
|
|
550
516
|
```
|
|
@@ -556,7 +522,7 @@ Update records by ID or filter:
|
|
|
556
522
|
```typescript
|
|
557
523
|
// Update by ID
|
|
558
524
|
const result = await db
|
|
559
|
-
.from(
|
|
525
|
+
.from(users)
|
|
560
526
|
.update({ username: "newname" })
|
|
561
527
|
.byId("user-123")
|
|
562
528
|
.execute();
|
|
@@ -565,29 +531,27 @@ if (result.data) {
|
|
|
565
531
|
console.log(`Updated ${result.data.updatedCount} record(s)`);
|
|
566
532
|
}
|
|
567
533
|
|
|
568
|
-
// Update by filter
|
|
534
|
+
// Update by filter (using new ORM API)
|
|
535
|
+
import { lt, and, eq } from "@proofkit/fmodata";
|
|
536
|
+
|
|
569
537
|
const result = await db
|
|
570
|
-
.from(
|
|
538
|
+
.from(users)
|
|
571
539
|
.update({ active: false })
|
|
572
|
-
.where((
|
|
540
|
+
.where(lt(users.lastLogin, "2023-01-01"))
|
|
573
541
|
.execute();
|
|
574
542
|
|
|
575
543
|
// Complex filter example
|
|
576
544
|
const result = await db
|
|
577
|
-
.from(
|
|
545
|
+
.from(users)
|
|
578
546
|
.update({ active: false })
|
|
579
|
-
.where((
|
|
580
|
-
q.filter({
|
|
581
|
-
and: [{ active: true }, { count: { lt: 5 } }],
|
|
582
|
-
}),
|
|
583
|
-
)
|
|
547
|
+
.where(and(eq(users.active, true), lt(users.count, 5)))
|
|
584
548
|
.execute();
|
|
585
549
|
|
|
586
|
-
// Update with additional query options
|
|
550
|
+
// Update with additional query options (legacy filter API)
|
|
587
551
|
const result = await db
|
|
588
552
|
.from("users")
|
|
589
553
|
.update({ active: false })
|
|
590
|
-
.where((q) => q.
|
|
554
|
+
.where((q) => q.where(eq(users.active, true)).top(10))
|
|
591
555
|
.execute();
|
|
592
556
|
```
|
|
593
557
|
|
|
@@ -597,28 +561,26 @@ Delete records by ID or filter:
|
|
|
597
561
|
|
|
598
562
|
```typescript
|
|
599
563
|
// Delete by ID
|
|
600
|
-
const result = await db.from(
|
|
564
|
+
const result = await db.from(users).delete().byId("user-123").execute();
|
|
601
565
|
|
|
602
566
|
if (result.data) {
|
|
603
567
|
console.log(`Deleted ${result.data.deletedCount} record(s)`);
|
|
604
568
|
}
|
|
605
569
|
|
|
606
|
-
// Delete by filter
|
|
570
|
+
// Delete by filter (using new ORM API)
|
|
571
|
+
import { eq, and, lt } from "@proofkit/fmodata";
|
|
572
|
+
|
|
607
573
|
const result = await db
|
|
608
|
-
.from(
|
|
574
|
+
.from(users)
|
|
609
575
|
.delete()
|
|
610
|
-
.where((
|
|
576
|
+
.where(eq(users.active, false))
|
|
611
577
|
.execute();
|
|
612
578
|
|
|
613
579
|
// Delete with complex filters
|
|
614
580
|
const result = await db
|
|
615
|
-
.from(
|
|
581
|
+
.from(users)
|
|
616
582
|
.delete()
|
|
617
|
-
.where((
|
|
618
|
-
q.filter({
|
|
619
|
-
and: [{ active: false }, { lastLogin: { lt: "2023-01-01" } }],
|
|
620
|
-
}),
|
|
621
|
-
)
|
|
583
|
+
.where(and(eq(users.active, false), lt(users.lastLogin, "2023-01-01")))
|
|
622
584
|
.execute();
|
|
623
585
|
```
|
|
624
586
|
|
|
@@ -626,128 +588,145 @@ const result = await db
|
|
|
626
588
|
|
|
627
589
|
### Defining Navigation
|
|
628
590
|
|
|
629
|
-
Define relationships
|
|
591
|
+
Define navigation relationships using the `navigationPaths` option when creating table occurrences:
|
|
630
592
|
|
|
631
593
|
```typescript
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
594
|
+
import { fmTableOccurrence, textField } from "@proofkit/fmodata";
|
|
595
|
+
|
|
596
|
+
const contacts = fmTableOccurrence(
|
|
597
|
+
"contacts",
|
|
598
|
+
{
|
|
599
|
+
id: textField().primaryKey(),
|
|
600
|
+
name: textField().notNull(),
|
|
601
|
+
userId: textField().notNull(),
|
|
637
602
|
},
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const usersBase = new BaseTable({
|
|
642
|
-
schema: {
|
|
643
|
-
id: z.string(),
|
|
644
|
-
username: z.string(),
|
|
645
|
-
email: z.string(),
|
|
603
|
+
{
|
|
604
|
+
navigationPaths: ["users"], // Valid navigation targets
|
|
646
605
|
},
|
|
647
|
-
|
|
648
|
-
});
|
|
606
|
+
);
|
|
649
607
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
608
|
+
const users = fmTableOccurrence(
|
|
609
|
+
"users",
|
|
610
|
+
{
|
|
611
|
+
id: textField().primaryKey(),
|
|
612
|
+
username: textField().notNull(),
|
|
613
|
+
email: textField().notNull(),
|
|
656
614
|
},
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const usersTO = new TableOccurrence({
|
|
660
|
-
name: "users",
|
|
661
|
-
baseTable: usersBase,
|
|
662
|
-
navigation: {
|
|
663
|
-
contacts: () => contactsTO, // Relationship to contacts table
|
|
615
|
+
{
|
|
616
|
+
navigationPaths: ["contacts"], // Valid navigation targets
|
|
664
617
|
},
|
|
665
|
-
|
|
618
|
+
);
|
|
666
619
|
|
|
667
|
-
//
|
|
668
|
-
const
|
|
669
|
-
|
|
620
|
+
// Use with your database
|
|
621
|
+
const db = connection.database("MyDB", {
|
|
622
|
+
occurrences: [contacts, users],
|
|
670
623
|
});
|
|
671
624
|
```
|
|
672
625
|
|
|
626
|
+
The `navigationPaths` option:
|
|
627
|
+
|
|
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
|
|
631
|
+
|
|
673
632
|
### Navigating Between Tables
|
|
674
633
|
|
|
675
634
|
Navigate to related records:
|
|
676
635
|
|
|
677
636
|
```typescript
|
|
678
|
-
// Navigate from a specific record
|
|
637
|
+
// Navigate from a specific record (using column references)
|
|
679
638
|
const result = await db
|
|
680
|
-
.from(
|
|
639
|
+
.from(contacts)
|
|
681
640
|
.get("contact-123")
|
|
682
|
-
.navigate(
|
|
683
|
-
.select(
|
|
641
|
+
.navigate(users)
|
|
642
|
+
.select({
|
|
643
|
+
username: users.username,
|
|
644
|
+
email: users.email,
|
|
645
|
+
})
|
|
684
646
|
.execute();
|
|
685
647
|
|
|
686
648
|
// Navigate without specifying a record first
|
|
687
|
-
const result = await db.from(
|
|
649
|
+
const result = await db.from(contacts).navigate(users).list().execute();
|
|
688
650
|
|
|
689
|
-
//
|
|
651
|
+
// Using legacy API with string field names
|
|
690
652
|
const result = await db
|
|
691
|
-
.from(
|
|
692
|
-
.
|
|
693
|
-
.
|
|
653
|
+
.from(contacts)
|
|
654
|
+
.get("contact-123")
|
|
655
|
+
.navigate(users)
|
|
656
|
+
.select({ username: users.username, email: users.email })
|
|
694
657
|
.execute();
|
|
695
658
|
```
|
|
696
659
|
|
|
697
660
|
### Expanding Related Records
|
|
698
661
|
|
|
699
|
-
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`:
|
|
700
663
|
|
|
701
664
|
```typescript
|
|
702
665
|
// Simple expand
|
|
703
|
-
const result = await db.from(
|
|
666
|
+
const result = await db.from(contacts).list().expand(users).execute();
|
|
704
667
|
|
|
705
|
-
// Expand with field selection
|
|
668
|
+
// Expand with field selection (using column references)
|
|
706
669
|
const result = await db
|
|
707
|
-
.from(
|
|
670
|
+
.from(contacts)
|
|
708
671
|
.list()
|
|
709
|
-
.expand(
|
|
672
|
+
.expand(users, (b) =>
|
|
673
|
+
b.select({
|
|
674
|
+
username: users.username,
|
|
675
|
+
email: users.email,
|
|
676
|
+
}),
|
|
677
|
+
)
|
|
710
678
|
.execute();
|
|
711
679
|
|
|
712
|
-
// Expand with filtering
|
|
680
|
+
// Expand with filtering (using new ORM API)
|
|
681
|
+
import { eq } from "@proofkit/fmodata";
|
|
682
|
+
|
|
713
683
|
const result = await db
|
|
714
|
-
.from(
|
|
684
|
+
.from(contacts)
|
|
715
685
|
.list()
|
|
716
|
-
.expand(
|
|
686
|
+
.expand(users, (b) => b.where(eq(users.active, true)))
|
|
717
687
|
.execute();
|
|
718
688
|
|
|
719
689
|
// Multiple expands
|
|
720
690
|
const result = await db
|
|
721
|
-
.from(
|
|
691
|
+
.from(contacts)
|
|
722
692
|
.list()
|
|
723
|
-
.expand(
|
|
724
|
-
.expand(
|
|
693
|
+
.expand(users, (b) => b.select({ username: users.username }))
|
|
694
|
+
.expand(orders, (b) => b.select({ total: orders.total }).top(5))
|
|
725
695
|
.execute();
|
|
726
696
|
|
|
727
697
|
// Nested expands
|
|
728
698
|
const result = await db
|
|
729
|
-
.from(
|
|
699
|
+
.from(contacts)
|
|
730
700
|
.list()
|
|
731
|
-
.expand(
|
|
701
|
+
.expand(users, (usersBuilder) =>
|
|
732
702
|
usersBuilder
|
|
733
|
-
.select(
|
|
734
|
-
|
|
735
|
-
|
|
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
|
+
}),
|
|
736
712
|
),
|
|
737
713
|
)
|
|
738
714
|
.execute();
|
|
739
715
|
|
|
740
716
|
// Complex expand with multiple options
|
|
741
717
|
const result = await db
|
|
742
|
-
.from(
|
|
718
|
+
.from(contacts)
|
|
743
719
|
.list()
|
|
744
|
-
.expand(
|
|
720
|
+
.expand(users, (b) =>
|
|
745
721
|
b
|
|
746
|
-
.select(
|
|
747
|
-
|
|
748
|
-
|
|
722
|
+
.select({
|
|
723
|
+
username: users.username,
|
|
724
|
+
email: users.email,
|
|
725
|
+
})
|
|
726
|
+
.where(eq(users.active, true))
|
|
727
|
+
.orderBy(asc(users.username))
|
|
749
728
|
.top(10)
|
|
750
|
-
.expand(
|
|
729
|
+
.expand(customers, (nested) => nested.select({ name: customers.name })),
|
|
751
730
|
)
|
|
752
731
|
.execute();
|
|
753
732
|
```
|
|
@@ -799,86 +778,709 @@ console.log(result.result.recordId);
|
|
|
799
778
|
|
|
800
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.
|
|
801
780
|
|
|
802
|
-
##
|
|
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.
|
|
803
784
|
|
|
804
|
-
###
|
|
785
|
+
### Adding a Webhook
|
|
805
786
|
|
|
806
|
-
|
|
787
|
+
Create a new webhook to monitor a table for changes:
|
|
807
788
|
|
|
808
789
|
```typescript
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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",
|
|
814
812
|
},
|
|
815
|
-
|
|
813
|
+
notifySchemaChanges: true, // Notify when schema changes
|
|
816
814
|
});
|
|
817
815
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
821
|
});
|
|
822
822
|
|
|
823
|
-
|
|
824
|
-
|
|
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],
|
|
825
831
|
});
|
|
826
832
|
|
|
827
|
-
//
|
|
828
|
-
db.
|
|
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
|
+
```
|
|
829
841
|
|
|
830
|
-
|
|
831
|
-
db.from("users").list().select("invalid"); // TS Error
|
|
842
|
+
**Webhook Configuration Properties:**
|
|
832
843
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
+
});
|
|
840
870
|
```
|
|
841
871
|
|
|
842
|
-
###
|
|
872
|
+
### Getting a Webhook
|
|
843
873
|
|
|
844
|
-
|
|
874
|
+
Retrieve a specific webhook by ID:
|
|
845
875
|
|
|
846
876
|
```typescript
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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",
|
|
854
922
|
},
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
923
|
+
filter: eq(contacts.active, true),
|
|
924
|
+
select: [contacts.name, contacts.email, contacts.PrimaryKey],
|
|
925
|
+
notifySchemaChanges: false,
|
|
858
926
|
});
|
|
859
927
|
|
|
860
|
-
|
|
861
|
-
|
|
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
|
+
|
|
948
|
+
## Batch Operations
|
|
949
|
+
|
|
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.
|
|
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
|
+
|
|
972
|
+
### Basic Batch with Multiple Queries
|
|
973
|
+
|
|
974
|
+
Execute multiple read operations in a single batch:
|
|
975
|
+
|
|
976
|
+
```typescript
|
|
977
|
+
// Create query builders
|
|
978
|
+
const contactsQuery = db.from(contacts).list().top(5);
|
|
979
|
+
const usersQuery = db.from(users).list().top(5);
|
|
980
|
+
|
|
981
|
+
// Execute both queries in a single batch
|
|
982
|
+
const result = await db.batch([contactsQuery, usersQuery]).execute();
|
|
983
|
+
|
|
984
|
+
// Access individual results
|
|
985
|
+
const [r1, r2] = result.results;
|
|
986
|
+
|
|
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);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Check summary statistics
|
|
1000
|
+
console.log(`Success: ${result.successCount}, Errors: ${result.errorCount}`);
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
### Mixed Operations (Reads and Writes)
|
|
1004
|
+
|
|
1005
|
+
Combine queries, inserts, updates, and deletes in a single batch:
|
|
1006
|
+
|
|
1007
|
+
```typescript
|
|
1008
|
+
// Mix different operation types
|
|
1009
|
+
const listQuery = db.from(contacts).list().top(10);
|
|
1010
|
+
const insertOp = db.from(contacts).insert({
|
|
1011
|
+
name: "John Doe",
|
|
1012
|
+
email: "john@example.com",
|
|
1013
|
+
});
|
|
1014
|
+
const updateOp = db.from(users).update({ active: true }).byId("user-123");
|
|
1015
|
+
|
|
1016
|
+
// All operations execute atomically
|
|
1017
|
+
const result = await db.batch([listQuery, insertOp, updateOp]).execute();
|
|
1018
|
+
|
|
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
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Check if batch was truncated
|
|
1074
|
+
if (result.truncated) {
|
|
1075
|
+
console.log(`Batch stopped early at index ${result.firstErrorIndex}`);
|
|
1076
|
+
}
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
### Transactional Behavior
|
|
1080
|
+
|
|
1081
|
+
Batch operations are transactional for write operations (inserts, updates, deletes). If any operation in the batch fails, all write operations are rolled back:
|
|
1082
|
+
|
|
1083
|
+
```typescript
|
|
1084
|
+
const result = await db
|
|
1085
|
+
.batch([
|
|
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
|
|
1089
|
+
])
|
|
1090
|
+
.execute();
|
|
1091
|
+
|
|
1092
|
+
// Check individual results
|
|
1093
|
+
const [r1, r2, r3] = result.results;
|
|
1094
|
+
|
|
1095
|
+
if (r1.error || r2.error || r3.error) {
|
|
1096
|
+
// All three inserts are rolled back - no users were created
|
|
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);
|
|
1101
|
+
}
|
|
1102
|
+
```
|
|
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
|
+
|
|
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.
|
|
1112
|
+
|
|
1113
|
+
## Schema Management
|
|
1114
|
+
|
|
1115
|
+
The library provides methods for managing database schema through the `db.schema` property. You can create and delete tables, add and remove fields, and manage indexes.
|
|
1116
|
+
|
|
1117
|
+
### Creating Tables
|
|
1118
|
+
|
|
1119
|
+
Create a new table with field definitions:
|
|
1120
|
+
|
|
1121
|
+
```typescript
|
|
1122
|
+
import type { Field } from "@proofkit/fmodata";
|
|
1123
|
+
|
|
1124
|
+
const fields: Field[] = [
|
|
1125
|
+
{
|
|
1126
|
+
name: "id",
|
|
1127
|
+
type: "string",
|
|
1128
|
+
primary: true,
|
|
1129
|
+
maxLength: 36,
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
name: "username",
|
|
1133
|
+
type: "string",
|
|
1134
|
+
nullable: false,
|
|
1135
|
+
unique: true,
|
|
1136
|
+
maxLength: 50,
|
|
1137
|
+
},
|
|
1138
|
+
{
|
|
1139
|
+
name: "email",
|
|
1140
|
+
type: "string",
|
|
1141
|
+
nullable: false,
|
|
1142
|
+
maxLength: 255,
|
|
1143
|
+
},
|
|
1144
|
+
{
|
|
1145
|
+
name: "age",
|
|
1146
|
+
type: "numeric",
|
|
1147
|
+
nullable: true,
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
name: "created_at",
|
|
1151
|
+
type: "timestamp",
|
|
1152
|
+
default: "CURRENT_TIMESTAMP",
|
|
1153
|
+
},
|
|
1154
|
+
];
|
|
1155
|
+
|
|
1156
|
+
const tableDefinition = await db.schema.createTable("users", fields);
|
|
1157
|
+
console.log(tableDefinition.tableName); // "users"
|
|
1158
|
+
console.log(tableDefinition.fields); // Array of field definitions
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
### Field Types
|
|
1162
|
+
|
|
1163
|
+
The library supports various field types:
|
|
1164
|
+
|
|
1165
|
+
**String Fields:**
|
|
1166
|
+
|
|
1167
|
+
```typescript
|
|
1168
|
+
{
|
|
1169
|
+
name: "username",
|
|
1170
|
+
type: "string",
|
|
1171
|
+
maxLength: 100, // Optional: varchar(100)
|
|
1172
|
+
nullable: true,
|
|
1173
|
+
unique: true,
|
|
1174
|
+
default: "USER" | "USERNAME" | "CURRENT_USER", // Optional
|
|
1175
|
+
repetitions: 5, // Optional: for repeating fields
|
|
1176
|
+
}
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
**Numeric Fields:**
|
|
1180
|
+
|
|
1181
|
+
```typescript
|
|
1182
|
+
{
|
|
1183
|
+
name: "age",
|
|
1184
|
+
type: "numeric",
|
|
1185
|
+
nullable: true,
|
|
1186
|
+
primary: false,
|
|
1187
|
+
unique: false,
|
|
1188
|
+
}
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
**Date Fields:**
|
|
1192
|
+
|
|
1193
|
+
```typescript
|
|
1194
|
+
{
|
|
1195
|
+
name: "birth_date",
|
|
1196
|
+
type: "date",
|
|
1197
|
+
default: "CURRENT_DATE" | "CURDATE", // Optional
|
|
1198
|
+
nullable: true,
|
|
1199
|
+
}
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
**Time Fields:**
|
|
1203
|
+
|
|
1204
|
+
```typescript
|
|
1205
|
+
{
|
|
1206
|
+
name: "start_time",
|
|
1207
|
+
type: "time",
|
|
1208
|
+
default: "CURRENT_TIME" | "CURTIME", // Optional
|
|
1209
|
+
nullable: true,
|
|
1210
|
+
}
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
**Timestamp Fields:**
|
|
1214
|
+
|
|
1215
|
+
```typescript
|
|
1216
|
+
{
|
|
1217
|
+
name: "created_at",
|
|
1218
|
+
type: "timestamp",
|
|
1219
|
+
default: "CURRENT_TIMESTAMP" | "CURTIMESTAMP", // Optional
|
|
1220
|
+
nullable: false,
|
|
1221
|
+
}
|
|
1222
|
+
```
|
|
1223
|
+
|
|
1224
|
+
**Container Fields:**
|
|
1225
|
+
|
|
1226
|
+
```typescript
|
|
1227
|
+
{
|
|
1228
|
+
name: "avatar",
|
|
1229
|
+
type: "container",
|
|
1230
|
+
externalSecurePath: "/secure/path", // Optional
|
|
1231
|
+
nullable: true,
|
|
1232
|
+
}
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
### Adding Fields to Existing Tables
|
|
1236
|
+
|
|
1237
|
+
Add new fields to an existing table:
|
|
1238
|
+
|
|
1239
|
+
```typescript
|
|
1240
|
+
const newFields: Field[] = [
|
|
1241
|
+
{
|
|
1242
|
+
name: "phone",
|
|
1243
|
+
type: "string",
|
|
1244
|
+
nullable: true,
|
|
1245
|
+
maxLength: 20,
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
name: "bio",
|
|
1249
|
+
type: "string",
|
|
1250
|
+
nullable: true,
|
|
1251
|
+
maxLength: 1000,
|
|
1252
|
+
},
|
|
1253
|
+
];
|
|
1254
|
+
|
|
1255
|
+
const updatedTable = await db.schema.addFields("users", newFields);
|
|
1256
|
+
```
|
|
1257
|
+
|
|
1258
|
+
### Deleting Tables and Fields
|
|
1259
|
+
|
|
1260
|
+
Delete an entire table:
|
|
1261
|
+
|
|
1262
|
+
```typescript
|
|
1263
|
+
await db.schema.deleteTable("old_table");
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
Delete a specific field from a table:
|
|
1267
|
+
|
|
1268
|
+
```typescript
|
|
1269
|
+
await db.schema.deleteField("users", "old_field");
|
|
1270
|
+
```
|
|
1271
|
+
|
|
1272
|
+
### Managing Indexes
|
|
1273
|
+
|
|
1274
|
+
Create an index on a field:
|
|
1275
|
+
|
|
1276
|
+
```typescript
|
|
1277
|
+
const index = await db.schema.createIndex("users", "email");
|
|
1278
|
+
console.log(index.indexName); // "email"
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1281
|
+
Delete an index:
|
|
1282
|
+
|
|
1283
|
+
```typescript
|
|
1284
|
+
await db.schema.deleteIndex("users", "email");
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
### Complete Example
|
|
1288
|
+
|
|
1289
|
+
Here's a complete example of creating a table with various field types:
|
|
1290
|
+
|
|
1291
|
+
```typescript
|
|
1292
|
+
const fields: Field[] = [
|
|
1293
|
+
// Primary key
|
|
1294
|
+
{
|
|
1295
|
+
name: "id",
|
|
1296
|
+
type: "string",
|
|
1297
|
+
primary: true,
|
|
1298
|
+
maxLength: 36,
|
|
1299
|
+
},
|
|
1300
|
+
|
|
1301
|
+
// String fields
|
|
1302
|
+
{
|
|
1303
|
+
name: "username",
|
|
1304
|
+
type: "string",
|
|
1305
|
+
nullable: false,
|
|
1306
|
+
unique: true,
|
|
1307
|
+
maxLength: 50,
|
|
1308
|
+
},
|
|
1309
|
+
{
|
|
1310
|
+
name: "email",
|
|
1311
|
+
type: "string",
|
|
1312
|
+
nullable: false,
|
|
1313
|
+
maxLength: 255,
|
|
1314
|
+
},
|
|
1315
|
+
|
|
1316
|
+
// Numeric field
|
|
1317
|
+
{
|
|
1318
|
+
name: "age",
|
|
1319
|
+
type: "numeric",
|
|
1320
|
+
nullable: true,
|
|
1321
|
+
},
|
|
1322
|
+
|
|
1323
|
+
// Date/time fields
|
|
1324
|
+
{
|
|
1325
|
+
name: "birth_date",
|
|
1326
|
+
type: "date",
|
|
1327
|
+
nullable: true,
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
name: "created_at",
|
|
1331
|
+
type: "timestamp",
|
|
1332
|
+
default: "CURRENT_TIMESTAMP",
|
|
1333
|
+
nullable: false,
|
|
1334
|
+
},
|
|
1335
|
+
|
|
1336
|
+
// Container field
|
|
1337
|
+
{
|
|
1338
|
+
name: "avatar",
|
|
1339
|
+
type: "container",
|
|
1340
|
+
nullable: true,
|
|
1341
|
+
},
|
|
1342
|
+
|
|
1343
|
+
// Repeating field
|
|
1344
|
+
{
|
|
1345
|
+
name: "tags",
|
|
1346
|
+
type: "string",
|
|
1347
|
+
repetitions: 5,
|
|
1348
|
+
maxLength: 50,
|
|
1349
|
+
},
|
|
1350
|
+
];
|
|
1351
|
+
|
|
1352
|
+
// Create the table
|
|
1353
|
+
const table = await db.schema.createTable("users", fields);
|
|
1354
|
+
|
|
1355
|
+
// Later, add more fields
|
|
1356
|
+
await db.schema.addFields("users", [
|
|
1357
|
+
{
|
|
1358
|
+
name: "phone",
|
|
1359
|
+
type: "string",
|
|
1360
|
+
nullable: true,
|
|
1361
|
+
},
|
|
1362
|
+
]);
|
|
1363
|
+
|
|
1364
|
+
// Create an index on email
|
|
1365
|
+
await db.schema.createIndex("users", "email");
|
|
1366
|
+
```
|
|
1367
|
+
|
|
1368
|
+
**Note:** Schema management operations require appropriate access privileges on your FileMaker account. Operations will throw errors if you don't have the necessary permissions.
|
|
1369
|
+
|
|
1370
|
+
## Advanced Features
|
|
1371
|
+
|
|
1372
|
+
### Required and Read-Only Fields
|
|
1373
|
+
|
|
1374
|
+
The library automatically infers which fields are required based on field builder configuration:
|
|
1375
|
+
|
|
1376
|
+
```typescript
|
|
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)
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
// Insert: username and email are required
|
|
1387
|
+
// Insert: id and createdAt are excluded (cannot be provided - read-only)
|
|
1388
|
+
db.from(users).insert({
|
|
862
1389
|
username: "john",
|
|
863
1390
|
email: "john@example.com",
|
|
864
|
-
|
|
1391
|
+
status: "active", // Optional
|
|
1392
|
+
updatedAt: new Date().toISOString(), // Optional
|
|
865
1393
|
});
|
|
866
1394
|
|
|
867
|
-
// Update
|
|
868
|
-
db.from(
|
|
1395
|
+
// Update: all fields are optional except id and createdAt are excluded
|
|
1396
|
+
db.from(users)
|
|
869
1397
|
.update({
|
|
870
|
-
status: "active",
|
|
871
|
-
//
|
|
1398
|
+
status: "active", // Optional
|
|
1399
|
+
// id and createdAt cannot be modified (read-only)
|
|
872
1400
|
})
|
|
873
1401
|
.byId("user-123");
|
|
874
1402
|
```
|
|
875
1403
|
|
|
1404
|
+
**Key Features:**
|
|
1405
|
+
|
|
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)
|
|
1409
|
+
- **Update flexibility:** All fields are optional for updates (except read-only fields)
|
|
1410
|
+
|
|
1411
|
+
### Prefer: fmodata.entity-ids
|
|
1412
|
+
|
|
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.
|
|
1414
|
+
|
|
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).
|
|
1416
|
+
|
|
1417
|
+
_Note for OttoFMS proxy: This feature requires version 4.14 or later of OttoFMS_
|
|
1418
|
+
|
|
1419
|
+
How do I find these ids? They can be found in the XML version of the `$metadata` endpoint for your database, or you can calculate them using these [custom functions](https://github.com/rwu2359/CFforID) from John Renfrew
|
|
1420
|
+
|
|
1421
|
+
#### Basic Usage
|
|
1422
|
+
|
|
1423
|
+
```typescript
|
|
1424
|
+
import {
|
|
1425
|
+
fmTableOccurrence,
|
|
1426
|
+
textField,
|
|
1427
|
+
timestampField,
|
|
1428
|
+
} from "@proofkit/fmodata";
|
|
1429
|
+
|
|
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"),
|
|
1438
|
+
},
|
|
1439
|
+
{
|
|
1440
|
+
entityId: "FMTID:12432533", // FileMaker table occurrence ID
|
|
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,
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
// Disable for this request
|
|
1469
|
+
const result = await db.from(users).list().execute({
|
|
1470
|
+
includeSpecialColumns: false,
|
|
1471
|
+
});
|
|
1472
|
+
```
|
|
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
|
+
|
|
876
1476
|
### Error Handling
|
|
877
1477
|
|
|
878
|
-
All operations return a `Result` type with either `data` or `error
|
|
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.
|
|
1479
|
+
|
|
1480
|
+
#### Basic Error Checking
|
|
879
1481
|
|
|
880
1482
|
```typescript
|
|
881
|
-
const result = await db.from(
|
|
1483
|
+
const result = await db.from(users).list().execute();
|
|
882
1484
|
|
|
883
1485
|
if (result.error) {
|
|
884
1486
|
console.error("Query failed:", result.error.message);
|
|
@@ -890,6 +1492,277 @@ if (result.data) {
|
|
|
890
1492
|
}
|
|
891
1493
|
```
|
|
892
1494
|
|
|
1495
|
+
#### HTTP Errors
|
|
1496
|
+
|
|
1497
|
+
Handle HTTP status codes (4xx, 5xx) with the `HTTPError` class:
|
|
1498
|
+
|
|
1499
|
+
```typescript
|
|
1500
|
+
import { HTTPError, isHTTPError } from "@proofkit/fmodata";
|
|
1501
|
+
|
|
1502
|
+
const result = await db.from(users).list().execute();
|
|
1503
|
+
|
|
1504
|
+
if (result.error) {
|
|
1505
|
+
if (isHTTPError(result.error)) {
|
|
1506
|
+
// TypeScript knows this is HTTPError
|
|
1507
|
+
console.log("HTTP Status:", result.error.status);
|
|
1508
|
+
|
|
1509
|
+
if (result.error.isNotFound()) {
|
|
1510
|
+
console.log("Resource not found");
|
|
1511
|
+
} else if (result.error.isUnauthorized()) {
|
|
1512
|
+
console.log("Authentication required");
|
|
1513
|
+
} else if (result.error.is5xx()) {
|
|
1514
|
+
console.log("Server error - try again later");
|
|
1515
|
+
} else if (result.error.is4xx()) {
|
|
1516
|
+
console.log("Client error:", result.error.statusText);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// Access the response body if available
|
|
1520
|
+
if (result.error.response) {
|
|
1521
|
+
console.log("Error details:", result.error.response);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
```
|
|
1526
|
+
|
|
1527
|
+
#### Network Errors
|
|
1528
|
+
|
|
1529
|
+
Handle network-level errors (timeouts, connection issues, etc.):
|
|
1530
|
+
|
|
1531
|
+
```typescript
|
|
1532
|
+
import {
|
|
1533
|
+
TimeoutError,
|
|
1534
|
+
NetworkError,
|
|
1535
|
+
RetryLimitError,
|
|
1536
|
+
CircuitOpenError,
|
|
1537
|
+
} from "@proofkit/fmodata";
|
|
1538
|
+
|
|
1539
|
+
const result = await db.from(users).list().execute();
|
|
1540
|
+
|
|
1541
|
+
if (result.error) {
|
|
1542
|
+
if (result.error instanceof TimeoutError) {
|
|
1543
|
+
console.log("Request timed out");
|
|
1544
|
+
// Show user-friendly timeout message
|
|
1545
|
+
} else if (result.error instanceof NetworkError) {
|
|
1546
|
+
console.log("Network connectivity issue");
|
|
1547
|
+
// Show offline message
|
|
1548
|
+
} else if (result.error instanceof RetryLimitError) {
|
|
1549
|
+
console.log("Request failed after retries");
|
|
1550
|
+
// Log the underlying error: result.error.cause
|
|
1551
|
+
} else if (result.error instanceof CircuitOpenError) {
|
|
1552
|
+
console.log("Service is currently unavailable");
|
|
1553
|
+
// Show maintenance message
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
```
|
|
1557
|
+
|
|
1558
|
+
#### Validation Errors
|
|
1559
|
+
|
|
1560
|
+
When schema validation fails, you get a `ValidationError` with rich context:
|
|
1561
|
+
|
|
1562
|
+
```typescript
|
|
1563
|
+
import { ValidationError, isValidationError } from "@proofkit/fmodata";
|
|
1564
|
+
|
|
1565
|
+
const result = await db.from(users).list().execute();
|
|
1566
|
+
|
|
1567
|
+
if (result.error) {
|
|
1568
|
+
if (isValidationError(result.error)) {
|
|
1569
|
+
// Access validation issues (Standard Schema format)
|
|
1570
|
+
console.log("Validation failed for field:", result.error.field);
|
|
1571
|
+
console.log("Issues:", result.error.issues);
|
|
1572
|
+
console.log("Failed value:", result.error.value);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
```
|
|
1576
|
+
|
|
1577
|
+
**Validator-Agnostic Error Handling**
|
|
1578
|
+
|
|
1579
|
+
The library uses [Standard Schema](https://github.com/standard-schema/standard-schema) to support any validation library (Zod, Valibot, ArkType, etc.). Following the same pattern as [uploadthing](https://github.com/pingdotgg/uploadthing), the `ValidationError.cause` property contains the normalized Standard Schema issues array:
|
|
1580
|
+
|
|
1581
|
+
```typescript
|
|
1582
|
+
import { ValidationError } from "@proofkit/fmodata";
|
|
1583
|
+
|
|
1584
|
+
const result = await db.from(users).list().execute();
|
|
1585
|
+
|
|
1586
|
+
if (result.error instanceof ValidationError) {
|
|
1587
|
+
// The cause property (ES2022 Error.cause) contains the Standard Schema issues array
|
|
1588
|
+
// This is validator-agnostic and works with Zod, Valibot, ArkType, etc.
|
|
1589
|
+
console.log("Validation issues:", result.error.cause);
|
|
1590
|
+
console.log("Issues are also available directly:", result.error.issues);
|
|
1591
|
+
|
|
1592
|
+
// Both point to the same array
|
|
1593
|
+
console.log(result.error.cause === result.error.issues); // true
|
|
1594
|
+
|
|
1595
|
+
// Access additional context
|
|
1596
|
+
console.log("Failed field:", result.error.field);
|
|
1597
|
+
console.log("Failed value:", result.error.value);
|
|
1598
|
+
|
|
1599
|
+
// Standard Schema issues have a normalized format
|
|
1600
|
+
result.error.issues.forEach((issue) => {
|
|
1601
|
+
console.log("Path:", issue.path);
|
|
1602
|
+
console.log("Message:", issue.message);
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
```
|
|
1606
|
+
|
|
1607
|
+
**Why Standard Schema Issues Instead of Original Validator Errors?**
|
|
1608
|
+
|
|
1609
|
+
By using Standard Schema's normalized issue format in the `cause` property, the library remains truly validator-agnostic. All validation libraries that implement Standard Schema (Zod, Valibot, ArkType, etc.) produce the same issue structure, making error handling consistent regardless of which validator you choose.
|
|
1610
|
+
|
|
1611
|
+
If you need validator-specific error formatting, you can still access your validator's methods during validation before the data reaches fmodata:
|
|
1612
|
+
|
|
1613
|
+
```typescript
|
|
1614
|
+
import { z } from "zod";
|
|
1615
|
+
|
|
1616
|
+
const userSchema = z.object({
|
|
1617
|
+
email: z.string().email(),
|
|
1618
|
+
age: z.number().min(0).max(150),
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
// Validate early if you need Zod-specific error handling
|
|
1622
|
+
const parseResult = userSchema.safeParse(userData);
|
|
1623
|
+
if (!parseResult.success) {
|
|
1624
|
+
// Use Zod's error formatting
|
|
1625
|
+
const formatted = parseResult.error.flatten();
|
|
1626
|
+
console.log("Zod-specific formatting:", formatted);
|
|
1627
|
+
}
|
|
1628
|
+
```
|
|
1629
|
+
|
|
1630
|
+
#### OData Errors
|
|
1631
|
+
|
|
1632
|
+
Handle OData-specific protocol errors:
|
|
1633
|
+
|
|
1634
|
+
```typescript
|
|
1635
|
+
import { ODataError, isODataError } from "@proofkit/fmodata";
|
|
1636
|
+
|
|
1637
|
+
const result = await db.from(users).list().execute();
|
|
1638
|
+
|
|
1639
|
+
if (result.error) {
|
|
1640
|
+
if (isODataError(result.error)) {
|
|
1641
|
+
console.log("OData Error Code:", result.error.code);
|
|
1642
|
+
console.log("OData Error Message:", result.error.message);
|
|
1643
|
+
console.log("OData Error Details:", result.error.details);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
```
|
|
1647
|
+
|
|
1648
|
+
#### Error Handling Patterns
|
|
1649
|
+
|
|
1650
|
+
**Pattern 1: Using instanceof (like ffetch):**
|
|
1651
|
+
|
|
1652
|
+
```typescript
|
|
1653
|
+
import {
|
|
1654
|
+
HTTPError,
|
|
1655
|
+
ValidationError,
|
|
1656
|
+
TimeoutError,
|
|
1657
|
+
NetworkError,
|
|
1658
|
+
} from "@proofkit/fmodata";
|
|
1659
|
+
|
|
1660
|
+
const result = await db.from(users).list().execute();
|
|
1661
|
+
|
|
1662
|
+
if (result.error) {
|
|
1663
|
+
if (result.error instanceof TimeoutError) {
|
|
1664
|
+
showTimeoutMessage();
|
|
1665
|
+
} else if (result.error instanceof HTTPError) {
|
|
1666
|
+
if (result.error.isNotFound()) {
|
|
1667
|
+
showNotFoundMessage();
|
|
1668
|
+
} else if (result.error.is5xx()) {
|
|
1669
|
+
showServerErrorMessage();
|
|
1670
|
+
}
|
|
1671
|
+
} else if (result.error instanceof ValidationError) {
|
|
1672
|
+
showValidationError(result.error.field, result.error.issues);
|
|
1673
|
+
} else if (result.error instanceof NetworkError) {
|
|
1674
|
+
showOfflineMessage();
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
```
|
|
1678
|
+
|
|
1679
|
+
**Pattern 2: Using kind property (for exhaustive matching):**
|
|
1680
|
+
|
|
1681
|
+
```typescript
|
|
1682
|
+
const result = await db.from(users).list().execute();
|
|
1683
|
+
|
|
1684
|
+
if (result.error) {
|
|
1685
|
+
switch (result.error.kind) {
|
|
1686
|
+
case "TimeoutError":
|
|
1687
|
+
showTimeoutMessage();
|
|
1688
|
+
break;
|
|
1689
|
+
case "HTTPError":
|
|
1690
|
+
handleHTTPError(result.error.status);
|
|
1691
|
+
break;
|
|
1692
|
+
case "ValidationError":
|
|
1693
|
+
showValidationError(result.error.field, result.error.issues);
|
|
1694
|
+
break;
|
|
1695
|
+
case "NetworkError":
|
|
1696
|
+
showOfflineMessage();
|
|
1697
|
+
break;
|
|
1698
|
+
case "ODataError":
|
|
1699
|
+
handleODataError(result.error.code);
|
|
1700
|
+
break;
|
|
1701
|
+
// TypeScript ensures exhaustive matching!
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
```
|
|
1705
|
+
|
|
1706
|
+
**Pattern 3: Using type guards:**
|
|
1707
|
+
|
|
1708
|
+
```typescript
|
|
1709
|
+
import {
|
|
1710
|
+
isHTTPError,
|
|
1711
|
+
isValidationError,
|
|
1712
|
+
isODataError,
|
|
1713
|
+
isNetworkError,
|
|
1714
|
+
} from "@proofkit/fmodata";
|
|
1715
|
+
|
|
1716
|
+
const result = await db.from("users").list().execute();
|
|
1717
|
+
|
|
1718
|
+
if (result.error) {
|
|
1719
|
+
if (isHTTPError(result.error)) {
|
|
1720
|
+
// TypeScript knows this is HTTPError
|
|
1721
|
+
console.log("Status:", result.error.status);
|
|
1722
|
+
} else if (isValidationError(result.error)) {
|
|
1723
|
+
// TypeScript knows this is ValidationError
|
|
1724
|
+
console.log("Field:", result.error.field);
|
|
1725
|
+
console.log("Issues:", result.error.issues);
|
|
1726
|
+
} else if (isODataError(result.error)) {
|
|
1727
|
+
// TypeScript knows this is ODataError
|
|
1728
|
+
console.log("Code:", result.error.code);
|
|
1729
|
+
} else if (isNetworkError(result.error)) {
|
|
1730
|
+
// TypeScript knows this is NetworkError
|
|
1731
|
+
console.log("Network issue:", result.error.cause);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
```
|
|
1735
|
+
|
|
1736
|
+
#### Error Properties
|
|
1737
|
+
|
|
1738
|
+
All errors include helpful metadata:
|
|
1739
|
+
|
|
1740
|
+
```typescript
|
|
1741
|
+
if (result.error) {
|
|
1742
|
+
// All errors have a timestamp
|
|
1743
|
+
console.log("Error occurred at:", result.error.timestamp);
|
|
1744
|
+
|
|
1745
|
+
// All errors have a kind property for discriminated unions
|
|
1746
|
+
console.log("Error kind:", result.error.kind);
|
|
1747
|
+
|
|
1748
|
+
// All errors have a message
|
|
1749
|
+
console.log("Error message:", result.error.message);
|
|
1750
|
+
}
|
|
1751
|
+
```
|
|
1752
|
+
|
|
1753
|
+
#### Available Error Types
|
|
1754
|
+
|
|
1755
|
+
- **`HTTPError`** - HTTP status errors (4xx, 5xx) with helper methods (`is4xx()`, `is5xx()`, `isNotFound()`, etc.)
|
|
1756
|
+
- **`ODataError`** - OData protocol errors with code and details
|
|
1757
|
+
- **`ValidationError`** - Schema validation failures with issues, schema reference, and failed value
|
|
1758
|
+
- **`ResponseStructureError`** - Malformed API responses
|
|
1759
|
+
- **`RecordCountMismatchError`** - When `single()` or `maybeSingle()` expectations aren't met
|
|
1760
|
+
- **`TimeoutError`** - Request timeout (from ffetch)
|
|
1761
|
+
- **`NetworkError`** - Network connectivity issues (from ffetch)
|
|
1762
|
+
- **`RetryLimitError`** - Request failed after retries (from ffetch)
|
|
1763
|
+
- **`CircuitOpenError`** - Circuit breaker is open (from ffetch)
|
|
1764
|
+
- **`AbortError`** - Request was aborted (from ffetch)
|
|
1765
|
+
|
|
893
1766
|
### OData Annotations and Validation
|
|
894
1767
|
|
|
895
1768
|
By default, the library automatically strips OData annotations fields (`@id` and `@editLink`) from responses. If you need these fields, you can include them by passing `includeODataAnnotations: true`:
|
|
@@ -962,7 +1835,7 @@ const queryString = db
|
|
|
962
1835
|
.from("users")
|
|
963
1836
|
.list()
|
|
964
1837
|
.select("username", "email")
|
|
965
|
-
.
|
|
1838
|
+
.where(eq(users.active, true))
|
|
966
1839
|
.orderBy("username")
|
|
967
1840
|
.top(10)
|
|
968
1841
|
.getQueryString();
|