@proofkit/fmodata 0.1.0-alpha.1 → 0.1.0-alpha.11
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 +760 -69
- package/dist/esm/client/base-table.d.ts +120 -5
- package/dist/esm/client/base-table.js +43 -5
- package/dist/esm/client/base-table.js.map +1 -1
- package/dist/esm/client/batch-builder.d.ts +54 -0
- package/dist/esm/client/batch-builder.js +179 -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/build-occurrences.d.ts +74 -0
- package/dist/esm/client/build-occurrences.js +31 -0
- package/dist/esm/client/build-occurrences.js.map +1 -0
- package/dist/esm/client/database.d.ts +55 -6
- package/dist/esm/client/database.js +118 -15
- package/dist/esm/client/database.js.map +1 -1
- package/dist/esm/client/delete-builder.d.ts +21 -2
- package/dist/esm/client/delete-builder.js +96 -32
- package/dist/esm/client/delete-builder.js.map +1 -1
- package/dist/esm/client/entity-set.d.ts +26 -12
- package/dist/esm/client/entity-set.js +43 -12
- package/dist/esm/client/entity-set.js.map +1 -1
- package/dist/esm/client/filemaker-odata.d.ts +23 -4
- package/dist/esm/client/filemaker-odata.js +124 -29
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/dist/esm/client/insert-builder.d.ts +38 -3
- package/dist/esm/client/insert-builder.js +231 -34
- package/dist/esm/client/insert-builder.js.map +1 -1
- package/dist/esm/client/query-builder.d.ts +28 -7
- package/dist/esm/client/query-builder.js +470 -212
- package/dist/esm/client/query-builder.js.map +1 -1
- package/dist/esm/client/record-builder.d.ts +96 -10
- package/dist/esm/client/record-builder.js +378 -39
- package/dist/esm/client/record-builder.js.map +1 -1
- package/dist/esm/client/response-processor.d.ts +38 -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/table-occurrence.d.ts +69 -8
- package/dist/esm/client/table-occurrence.js +35 -24
- package/dist/esm/client/table-occurrence.js.map +1 -1
- package/dist/esm/client/update-builder.d.ts +34 -11
- package/dist/esm/client/update-builder.js +135 -31
- package/dist/esm/client/update-builder.js.map +1 -1
- package/dist/esm/errors.d.ts +73 -0
- package/dist/esm/errors.js +148 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.d.ts +13 -3
- package/dist/esm/index.js +29 -7
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/transform.d.ts +65 -0
- package/dist/esm/transform.js +114 -0
- package/dist/esm/transform.js.map +1 -0
- package/dist/esm/types.d.ts +89 -5
- package/dist/esm/validation.d.ts +6 -3
- package/dist/esm/validation.js +104 -33
- package/dist/esm/validation.js.map +1 -1
- package/package.json +10 -1
- package/src/client/base-table.ts +161 -8
- package/src/client/batch-builder.ts +265 -0
- package/src/client/batch-request.ts +485 -0
- package/src/client/build-occurrences.ts +155 -0
- package/src/client/database.ts +175 -18
- package/src/client/delete-builder.ts +149 -48
- package/src/client/entity-set.ts +134 -28
- package/src/client/filemaker-odata.ts +179 -35
- package/src/client/insert-builder.ts +350 -40
- package/src/client/query-builder.ts +632 -244
- package/src/client/query-builder.ts.bak +1457 -0
- package/src/client/record-builder.ts +692 -68
- package/src/client/response-processor.ts +103 -0
- package/src/client/schema-manager.ts +246 -0
- package/src/client/table-occurrence.ts +107 -51
- package/src/client/update-builder.ts +235 -49
- package/src/errors.ts +217 -0
- package/src/index.ts +63 -6
- package/src/transform.ts +249 -0
- package/src/types.ts +201 -35
- package/src/validation.ts +120 -36
package/README.md
CHANGED
|
@@ -2,12 +2,21 @@
|
|
|
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
|
+
|
|
7
|
+
Roadmap:
|
|
8
|
+
|
|
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)
|
|
13
|
+
- [ ] Proper docs at proofkit.dev
|
|
14
|
+
- [ ] @proofkit/typegen integration
|
|
6
15
|
|
|
7
16
|
## Installation
|
|
8
17
|
|
|
9
18
|
```bash
|
|
10
|
-
pnpm add @proofkit/fmodata
|
|
19
|
+
pnpm add @proofkit/fmodata@alpha
|
|
11
20
|
```
|
|
12
21
|
|
|
13
22
|
## Quick Start
|
|
@@ -15,11 +24,15 @@ pnpm add @proofkit/fmodata
|
|
|
15
24
|
Here's a minimal example to get you started:
|
|
16
25
|
|
|
17
26
|
```typescript
|
|
18
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
FMServerConnection,
|
|
29
|
+
defineBaseTable,
|
|
30
|
+
defineTableOccurrence,
|
|
31
|
+
} from "@proofkit/fmodata";
|
|
19
32
|
import { z } from "zod/v4";
|
|
20
33
|
|
|
21
|
-
// 1. Create a
|
|
22
|
-
const
|
|
34
|
+
// 1. Create a connection to the server
|
|
35
|
+
const connection = new FMServerConnection({
|
|
23
36
|
serverUrl: "https://your-server.com",
|
|
24
37
|
auth: {
|
|
25
38
|
// OttoFMS API key
|
|
@@ -32,7 +45,7 @@ const client = new FileMakerOData({
|
|
|
32
45
|
});
|
|
33
46
|
|
|
34
47
|
// 2. Define your table schema
|
|
35
|
-
const usersBase =
|
|
48
|
+
const usersBase = defineBaseTable({
|
|
36
49
|
schema: {
|
|
37
50
|
id: z.string(),
|
|
38
51
|
username: z.string(),
|
|
@@ -43,13 +56,13 @@ const usersBase = new BaseTable({
|
|
|
43
56
|
});
|
|
44
57
|
|
|
45
58
|
// 3. Create a table occurrence
|
|
46
|
-
const usersTO =
|
|
59
|
+
const usersTO = defineTableOccurrence({
|
|
47
60
|
name: "users",
|
|
48
61
|
baseTable: usersBase,
|
|
49
62
|
});
|
|
50
63
|
|
|
51
64
|
// 4. Create a database instance
|
|
52
|
-
const db =
|
|
65
|
+
const db = connection.database("MyDatabase.fmp12", {
|
|
53
66
|
occurrences: [usersTO],
|
|
54
67
|
});
|
|
55
68
|
|
|
@@ -68,11 +81,11 @@ if (data) {
|
|
|
68
81
|
|
|
69
82
|
## Core Concepts
|
|
70
83
|
|
|
71
|
-
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.
|
|
84
|
+
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.
|
|
72
85
|
|
|
73
86
|
As such, there are layers to the library to help you build your queries and operations.
|
|
74
87
|
|
|
75
|
-
- `
|
|
88
|
+
- `FMServerConnection` - hold server connection details and authentication
|
|
76
89
|
- `BaseTable` - defines the fields and validators for a base table
|
|
77
90
|
- `TableOccurrence` - references a base table, and other table occurrences for navigation
|
|
78
91
|
- `Database` - connects the table occurrences to the server connection
|
|
@@ -95,7 +108,7 @@ The client can authenticate using username/password or API key:
|
|
|
95
108
|
|
|
96
109
|
```typescript
|
|
97
110
|
// Username and password authentication
|
|
98
|
-
const
|
|
111
|
+
const connection = new FMServerConnection({
|
|
99
112
|
serverUrl: "https://api.example.com",
|
|
100
113
|
auth: {
|
|
101
114
|
username: "test",
|
|
@@ -104,7 +117,7 @@ const client = new FileMakerOData({
|
|
|
104
117
|
});
|
|
105
118
|
|
|
106
119
|
// API key authentication
|
|
107
|
-
const
|
|
120
|
+
const connection = new FMServerConnection({
|
|
108
121
|
serverUrl: "https://api.example.com",
|
|
109
122
|
auth: {
|
|
110
123
|
apiKey: "your-api-key",
|
|
@@ -114,15 +127,17 @@ const client = new FileMakerOData({
|
|
|
114
127
|
|
|
115
128
|
### Schema Definitions
|
|
116
129
|
|
|
117
|
-
This library relies on a schema-first approach for good type-safety and optional runtime validation. These are
|
|
130
|
+
This library relies on a schema-first approach for good type-safety and optional runtime validation. These are abstracted into BaseTable and TableOccurrence types to match FileMaker concepts.
|
|
131
|
+
|
|
132
|
+
Use **`defineBaseTable()`** and **`defineTableOccurrence()`** to create your schemas. These functions provide full TypeScript type inference for field names in queries.
|
|
118
133
|
|
|
119
134
|
A `BaseTable` defines the schema for your FileMaker table using Standard Schema. These examples show zod, but you can use any other validation library that supports Standard Schema.
|
|
120
135
|
|
|
121
136
|
```typescript
|
|
122
137
|
import { z } from "zod/v4";
|
|
123
|
-
import {
|
|
138
|
+
import { defineBaseTable } from "@proofkit/fmodata";
|
|
124
139
|
|
|
125
|
-
const contactsBase =
|
|
140
|
+
const contactsBase = defineBaseTable({
|
|
126
141
|
schema: {
|
|
127
142
|
id: z.string(),
|
|
128
143
|
name: z.string(),
|
|
@@ -130,18 +145,18 @@ const contactsBase = new BaseTable({
|
|
|
130
145
|
phone: z.string().optional(),
|
|
131
146
|
createdAt: z.string(),
|
|
132
147
|
},
|
|
133
|
-
idField: "id", // The primary key field
|
|
134
|
-
|
|
135
|
-
|
|
148
|
+
idField: "id", // The primary key field (automatically read-only)
|
|
149
|
+
required: ["phone"], // optional: additional required fields for insert (beyond auto-inferred)
|
|
150
|
+
readOnly: ["createdAt"], // optional: fields excluded from insert/update
|
|
136
151
|
});
|
|
137
152
|
```
|
|
138
153
|
|
|
139
|
-
A `TableOccurrence` is the actual entry point for the OData service on the FileMaker server. It
|
|
154
|
+
A `TableOccurrence` is the actual entry point for the OData service on the FileMaker server. It allows you to reference the same base table multiple times with different names.
|
|
140
155
|
|
|
141
156
|
```typescript
|
|
142
|
-
import {
|
|
157
|
+
import { defineTableOccurrence } from "@proofkit/fmodata";
|
|
143
158
|
|
|
144
|
-
const contactsTO =
|
|
159
|
+
const contactsTO = defineTableOccurrence({
|
|
145
160
|
name: "contacts", // The table occurrence name in FileMaker
|
|
146
161
|
baseTable: contactsBase,
|
|
147
162
|
});
|
|
@@ -153,21 +168,21 @@ FileMaker will automatically return all non-container fields from a schema if yo
|
|
|
153
168
|
|
|
154
169
|
```typescript
|
|
155
170
|
// Option 1 (default): "schema" - Select all fields from the schema (same as "all" but more explicit)
|
|
156
|
-
const usersTO =
|
|
171
|
+
const usersTO = defineTableOccurrence({
|
|
157
172
|
name: "users",
|
|
158
173
|
baseTable: usersBase,
|
|
159
174
|
defaultSelect: "schema", // a $select parameter will be always be added to the query for only the fields you've defined in the BaseTable schema
|
|
160
175
|
});
|
|
161
176
|
|
|
162
177
|
// Option 2: "all" - Select all fields (default behavior)
|
|
163
|
-
const usersTO =
|
|
178
|
+
const usersTO = defineTableOccurrence({
|
|
164
179
|
name: "users",
|
|
165
180
|
baseTable: usersBase,
|
|
166
181
|
defaultSelect: "all", // Don't always a $select parameter to the query; FileMaker will return all non-container fields from the table
|
|
167
182
|
});
|
|
168
183
|
|
|
169
184
|
// Option 3: Array of field names - Select only specific fields by default
|
|
170
|
-
const usersTO =
|
|
185
|
+
const usersTO = defineTableOccurrence({
|
|
171
186
|
name: "users",
|
|
172
187
|
baseTable: usersBase,
|
|
173
188
|
defaultSelect: ["username", "email"], // Only select these fields by default
|
|
@@ -185,10 +200,10 @@ const result = await db
|
|
|
185
200
|
.execute();
|
|
186
201
|
```
|
|
187
202
|
|
|
188
|
-
Lastly, you can combine all table occurrences into a database instance for the full type-safe experience. This is a method on the main `
|
|
203
|
+
Lastly, you can combine all table occurrences into a database instance for the full type-safe experience. This is a method on the main `FMServerConnection` client class.
|
|
189
204
|
|
|
190
205
|
```typescript
|
|
191
|
-
const db =
|
|
206
|
+
const db = connection.database("MyDatabase.fmp12", {
|
|
192
207
|
occurrences: [contactsTO, usersTO], // Register your table occurrences
|
|
193
208
|
});
|
|
194
209
|
```
|
|
@@ -514,27 +529,30 @@ if (result.data) {
|
|
|
514
529
|
}
|
|
515
530
|
```
|
|
516
531
|
|
|
517
|
-
|
|
532
|
+
Fields are automatically required for insert if their validator doesn't allow `null` or `undefined`. You can specify additional required fields:
|
|
518
533
|
|
|
519
534
|
```typescript
|
|
520
|
-
const usersBase =
|
|
535
|
+
const usersBase = defineBaseTable({
|
|
521
536
|
schema: {
|
|
522
|
-
id: z.string(),
|
|
523
|
-
username: z.string(),
|
|
524
|
-
email: z.string(),
|
|
525
|
-
|
|
537
|
+
id: z.string(), // Auto-required (not nullable), but excluded from insert (idField)
|
|
538
|
+
username: z.string(), // Auto-required (not nullable)
|
|
539
|
+
email: z.string(), // Auto-required (not nullable)
|
|
540
|
+
phone: z.string().nullable(), // Optional by default
|
|
541
|
+
createdAt: z.string(), // Auto-required, but excluded (readOnly)
|
|
526
542
|
},
|
|
527
|
-
idField: "id",
|
|
528
|
-
|
|
543
|
+
idField: "id", // Automatically excluded from insert/update
|
|
544
|
+
required: ["phone"], // Make phone required for inserts despite being nullable
|
|
545
|
+
readOnly: ["createdAt"], // Exclude from insert/update operations
|
|
529
546
|
});
|
|
530
547
|
|
|
531
|
-
// TypeScript
|
|
548
|
+
// TypeScript enforces: username, email, and phone are required
|
|
549
|
+
// TypeScript excludes: id and createdAt cannot be provided
|
|
532
550
|
const result = await db
|
|
533
551
|
.from("users")
|
|
534
552
|
.insert({
|
|
535
553
|
username: "johndoe",
|
|
536
554
|
email: "john@example.com",
|
|
537
|
-
//
|
|
555
|
+
phone: "+1234567890", // Required because specified in 'required' array
|
|
538
556
|
})
|
|
539
557
|
.execute();
|
|
540
558
|
```
|
|
@@ -616,10 +634,16 @@ const result = await db
|
|
|
616
634
|
|
|
617
635
|
### Defining Navigation
|
|
618
636
|
|
|
619
|
-
|
|
637
|
+
Use `buildOccurrences()` to define relationships between tables. This function takes an array of table occurrences and a configuration object that specifies navigation relationships using type-safe string references:
|
|
620
638
|
|
|
621
639
|
```typescript
|
|
622
|
-
|
|
640
|
+
import {
|
|
641
|
+
defineBaseTable,
|
|
642
|
+
defineTableOccurrence,
|
|
643
|
+
buildOccurrences,
|
|
644
|
+
} from "@proofkit/fmodata";
|
|
645
|
+
|
|
646
|
+
const contactsBase = defineBaseTable({
|
|
623
647
|
schema: {
|
|
624
648
|
id: z.string(),
|
|
625
649
|
name: z.string(),
|
|
@@ -628,7 +652,7 @@ const contactsBase = new BaseTable({
|
|
|
628
652
|
idField: "id",
|
|
629
653
|
});
|
|
630
654
|
|
|
631
|
-
const usersBase =
|
|
655
|
+
const usersBase = defineBaseTable({
|
|
632
656
|
schema: {
|
|
633
657
|
id: z.string(),
|
|
634
658
|
username: z.string(),
|
|
@@ -637,29 +661,43 @@ const usersBase = new BaseTable({
|
|
|
637
661
|
idField: "id",
|
|
638
662
|
});
|
|
639
663
|
|
|
640
|
-
//
|
|
641
|
-
const
|
|
664
|
+
// Step 1: Define base table occurrences (without navigation)
|
|
665
|
+
const _contactsTO = defineTableOccurrence({
|
|
642
666
|
name: "contacts",
|
|
643
667
|
baseTable: contactsBase,
|
|
644
|
-
navigation: {
|
|
645
|
-
users: () => usersTO, // Relationship to users table
|
|
646
|
-
},
|
|
647
668
|
});
|
|
648
669
|
|
|
649
|
-
const
|
|
670
|
+
const _usersTO = defineTableOccurrence({
|
|
650
671
|
name: "users",
|
|
651
672
|
baseTable: usersBase,
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// Step 2: Build occurrences with navigation using string references
|
|
676
|
+
// The strings autocomplete to valid table occurrence names!
|
|
677
|
+
const occurrences = buildOccurrences({
|
|
678
|
+
occurrences: [_contactsTO, _usersTO],
|
|
652
679
|
navigation: {
|
|
653
|
-
contacts:
|
|
680
|
+
contacts: ["users"],
|
|
681
|
+
users: ["contacts"],
|
|
654
682
|
},
|
|
655
683
|
});
|
|
656
684
|
|
|
657
|
-
//
|
|
658
|
-
const
|
|
659
|
-
|
|
685
|
+
// Use with your database
|
|
686
|
+
const db = connection.database("MyDB", {
|
|
687
|
+
occurrences: occurrences,
|
|
660
688
|
});
|
|
661
689
|
```
|
|
662
690
|
|
|
691
|
+
The `buildOccurrences` function accepts an object with:
|
|
692
|
+
|
|
693
|
+
- `occurrences` - Array of TableOccurrences to build
|
|
694
|
+
- `navigation` - Optional object mapping TO names to arrays of navigation targets
|
|
695
|
+
|
|
696
|
+
It returns a tuple in the same order as the input array, with full autocomplete for navigation target names. Self-navigation is prevented at the type level.
|
|
697
|
+
|
|
698
|
+
- Handles circular references automatically
|
|
699
|
+
- Returns fully typed `TableOccurrence` instances with resolved navigation
|
|
700
|
+
|
|
663
701
|
### Navigating Between Tables
|
|
664
702
|
|
|
665
703
|
Navigate to related records:
|
|
@@ -789,6 +827,334 @@ console.log(result.result.recordId);
|
|
|
789
827
|
|
|
790
828
|
**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.
|
|
791
829
|
|
|
830
|
+
## Batch Operations
|
|
831
|
+
|
|
832
|
+
Batch operations allow you to execute multiple queries and operations together in a single request. All operations in a batch are executed atomically - they all succeed or all fail together. This is both more efficient (fewer network round-trips) and ensures data consistency across related operations.
|
|
833
|
+
|
|
834
|
+
### Basic Batch with Multiple Queries
|
|
835
|
+
|
|
836
|
+
Execute multiple read operations in a single batch:
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
// Create query builders
|
|
840
|
+
const contactsQuery = db.from("contacts").list().top(5);
|
|
841
|
+
const usersQuery = db.from("users").list().top(5);
|
|
842
|
+
|
|
843
|
+
// Execute both queries in a single batch
|
|
844
|
+
const result = await db.batch([contactsQuery, usersQuery]).execute();
|
|
845
|
+
|
|
846
|
+
if (result.data) {
|
|
847
|
+
// Result is a tuple matching the input builders
|
|
848
|
+
const [contacts, users] = result.data;
|
|
849
|
+
|
|
850
|
+
console.log("Contacts:", contacts);
|
|
851
|
+
console.log("Users:", users);
|
|
852
|
+
}
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
### Mixed Operations (Reads and Writes)
|
|
856
|
+
|
|
857
|
+
Combine queries, inserts, updates, and deletes in a single batch:
|
|
858
|
+
|
|
859
|
+
```typescript
|
|
860
|
+
// Mix different operation types
|
|
861
|
+
const listQuery = db.from("contacts").list().top(10);
|
|
862
|
+
const insertOp = db.from("contacts").insert({
|
|
863
|
+
name: "John Doe",
|
|
864
|
+
email: "john@example.com",
|
|
865
|
+
});
|
|
866
|
+
const updateOp = db.from("users").update({ active: true }).byId("user-123");
|
|
867
|
+
|
|
868
|
+
// All operations execute atomically
|
|
869
|
+
const result = await db.batch([listQuery, insertOp, updateOp]).execute();
|
|
870
|
+
|
|
871
|
+
if (result.data) {
|
|
872
|
+
const [contactsList, insertResult, updateResult] = result.data;
|
|
873
|
+
|
|
874
|
+
console.log("Fetched contacts:", contactsList);
|
|
875
|
+
console.log("Inserted contact:", insertResult);
|
|
876
|
+
console.log("Updated user:", updateResult);
|
|
877
|
+
}
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
### Transactional Behavior
|
|
881
|
+
|
|
882
|
+
Batch operations are transactional for write operations (inserts, updates, deletes). If any operation in the batch fails, all write operations are rolled back:
|
|
883
|
+
|
|
884
|
+
```typescript
|
|
885
|
+
const result = await db
|
|
886
|
+
.batch([
|
|
887
|
+
db.from("users").insert({ username: "alice", email: "alice@example.com" }),
|
|
888
|
+
db.from("users").insert({ username: "bob", email: "bob@example.com" }),
|
|
889
|
+
db.from("users").insert({ username: "charlie", email: "invalid" }), // This fails
|
|
890
|
+
])
|
|
891
|
+
.execute();
|
|
892
|
+
|
|
893
|
+
if (result.error) {
|
|
894
|
+
// All three inserts are rolled back - no users were created
|
|
895
|
+
console.error("Batch failed:", result.error);
|
|
896
|
+
}
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
**Note:** Batch operations automatically group write operations (POST, PATCH, DELETE) into changesets for transactional behavior, while read operations (GET) are executed individually within the batch.
|
|
900
|
+
|
|
901
|
+
## Schema Management
|
|
902
|
+
|
|
903
|
+
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.
|
|
904
|
+
|
|
905
|
+
### Creating Tables
|
|
906
|
+
|
|
907
|
+
Create a new table with field definitions:
|
|
908
|
+
|
|
909
|
+
```typescript
|
|
910
|
+
import type { Field } from "@proofkit/fmodata";
|
|
911
|
+
|
|
912
|
+
const fields: Field[] = [
|
|
913
|
+
{
|
|
914
|
+
name: "id",
|
|
915
|
+
type: "string",
|
|
916
|
+
primary: true,
|
|
917
|
+
maxLength: 36,
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
name: "username",
|
|
921
|
+
type: "string",
|
|
922
|
+
nullable: false,
|
|
923
|
+
unique: true,
|
|
924
|
+
maxLength: 50,
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
name: "email",
|
|
928
|
+
type: "string",
|
|
929
|
+
nullable: false,
|
|
930
|
+
maxLength: 255,
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
name: "age",
|
|
934
|
+
type: "numeric",
|
|
935
|
+
nullable: true,
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
name: "created_at",
|
|
939
|
+
type: "timestamp",
|
|
940
|
+
default: "CURRENT_TIMESTAMP",
|
|
941
|
+
},
|
|
942
|
+
];
|
|
943
|
+
|
|
944
|
+
const tableDefinition = await db.schema.createTable("users", fields);
|
|
945
|
+
console.log(tableDefinition.tableName); // "users"
|
|
946
|
+
console.log(tableDefinition.fields); // Array of field definitions
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
### Field Types
|
|
950
|
+
|
|
951
|
+
The library supports various field types:
|
|
952
|
+
|
|
953
|
+
**String Fields:**
|
|
954
|
+
|
|
955
|
+
```typescript
|
|
956
|
+
{
|
|
957
|
+
name: "username",
|
|
958
|
+
type: "string",
|
|
959
|
+
maxLength: 100, // Optional: varchar(100)
|
|
960
|
+
nullable: true,
|
|
961
|
+
unique: true,
|
|
962
|
+
default: "USER" | "USERNAME" | "CURRENT_USER", // Optional
|
|
963
|
+
repetitions: 5, // Optional: for repeating fields
|
|
964
|
+
}
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
**Numeric Fields:**
|
|
968
|
+
|
|
969
|
+
```typescript
|
|
970
|
+
{
|
|
971
|
+
name: "age",
|
|
972
|
+
type: "numeric",
|
|
973
|
+
nullable: true,
|
|
974
|
+
primary: false,
|
|
975
|
+
unique: false,
|
|
976
|
+
}
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
**Date Fields:**
|
|
980
|
+
|
|
981
|
+
```typescript
|
|
982
|
+
{
|
|
983
|
+
name: "birth_date",
|
|
984
|
+
type: "date",
|
|
985
|
+
default: "CURRENT_DATE" | "CURDATE", // Optional
|
|
986
|
+
nullable: true,
|
|
987
|
+
}
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
**Time Fields:**
|
|
991
|
+
|
|
992
|
+
```typescript
|
|
993
|
+
{
|
|
994
|
+
name: "start_time",
|
|
995
|
+
type: "time",
|
|
996
|
+
default: "CURRENT_TIME" | "CURTIME", // Optional
|
|
997
|
+
nullable: true,
|
|
998
|
+
}
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
**Timestamp Fields:**
|
|
1002
|
+
|
|
1003
|
+
```typescript
|
|
1004
|
+
{
|
|
1005
|
+
name: "created_at",
|
|
1006
|
+
type: "timestamp",
|
|
1007
|
+
default: "CURRENT_TIMESTAMP" | "CURTIMESTAMP", // Optional
|
|
1008
|
+
nullable: false,
|
|
1009
|
+
}
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
**Container Fields:**
|
|
1013
|
+
|
|
1014
|
+
```typescript
|
|
1015
|
+
{
|
|
1016
|
+
name: "avatar",
|
|
1017
|
+
type: "container",
|
|
1018
|
+
externalSecurePath: "/secure/path", // Optional
|
|
1019
|
+
nullable: true,
|
|
1020
|
+
}
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
### Adding Fields to Existing Tables
|
|
1024
|
+
|
|
1025
|
+
Add new fields to an existing table:
|
|
1026
|
+
|
|
1027
|
+
```typescript
|
|
1028
|
+
const newFields: Field[] = [
|
|
1029
|
+
{
|
|
1030
|
+
name: "phone",
|
|
1031
|
+
type: "string",
|
|
1032
|
+
nullable: true,
|
|
1033
|
+
maxLength: 20,
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
name: "bio",
|
|
1037
|
+
type: "string",
|
|
1038
|
+
nullable: true,
|
|
1039
|
+
maxLength: 1000,
|
|
1040
|
+
},
|
|
1041
|
+
];
|
|
1042
|
+
|
|
1043
|
+
const updatedTable = await db.schema.addFields("users", newFields);
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
### Deleting Tables and Fields
|
|
1047
|
+
|
|
1048
|
+
Delete an entire table:
|
|
1049
|
+
|
|
1050
|
+
```typescript
|
|
1051
|
+
await db.schema.deleteTable("old_table");
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
Delete a specific field from a table:
|
|
1055
|
+
|
|
1056
|
+
```typescript
|
|
1057
|
+
await db.schema.deleteField("users", "old_field");
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
### Managing Indexes
|
|
1061
|
+
|
|
1062
|
+
Create an index on a field:
|
|
1063
|
+
|
|
1064
|
+
```typescript
|
|
1065
|
+
const index = await db.schema.createIndex("users", "email");
|
|
1066
|
+
console.log(index.indexName); // "email"
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
Delete an index:
|
|
1070
|
+
|
|
1071
|
+
```typescript
|
|
1072
|
+
await db.schema.deleteIndex("users", "email");
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
### Complete Example
|
|
1076
|
+
|
|
1077
|
+
Here's a complete example of creating a table with various field types:
|
|
1078
|
+
|
|
1079
|
+
```typescript
|
|
1080
|
+
const fields: Field[] = [
|
|
1081
|
+
// Primary key
|
|
1082
|
+
{
|
|
1083
|
+
name: "id",
|
|
1084
|
+
type: "string",
|
|
1085
|
+
primary: true,
|
|
1086
|
+
maxLength: 36,
|
|
1087
|
+
},
|
|
1088
|
+
|
|
1089
|
+
// String fields
|
|
1090
|
+
{
|
|
1091
|
+
name: "username",
|
|
1092
|
+
type: "string",
|
|
1093
|
+
nullable: false,
|
|
1094
|
+
unique: true,
|
|
1095
|
+
maxLength: 50,
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
name: "email",
|
|
1099
|
+
type: "string",
|
|
1100
|
+
nullable: false,
|
|
1101
|
+
maxLength: 255,
|
|
1102
|
+
},
|
|
1103
|
+
|
|
1104
|
+
// Numeric field
|
|
1105
|
+
{
|
|
1106
|
+
name: "age",
|
|
1107
|
+
type: "numeric",
|
|
1108
|
+
nullable: true,
|
|
1109
|
+
},
|
|
1110
|
+
|
|
1111
|
+
// Date/time fields
|
|
1112
|
+
{
|
|
1113
|
+
name: "birth_date",
|
|
1114
|
+
type: "date",
|
|
1115
|
+
nullable: true,
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
name: "created_at",
|
|
1119
|
+
type: "timestamp",
|
|
1120
|
+
default: "CURRENT_TIMESTAMP",
|
|
1121
|
+
nullable: false,
|
|
1122
|
+
},
|
|
1123
|
+
|
|
1124
|
+
// Container field
|
|
1125
|
+
{
|
|
1126
|
+
name: "avatar",
|
|
1127
|
+
type: "container",
|
|
1128
|
+
nullable: true,
|
|
1129
|
+
},
|
|
1130
|
+
|
|
1131
|
+
// Repeating field
|
|
1132
|
+
{
|
|
1133
|
+
name: "tags",
|
|
1134
|
+
type: "string",
|
|
1135
|
+
repetitions: 5,
|
|
1136
|
+
maxLength: 50,
|
|
1137
|
+
},
|
|
1138
|
+
];
|
|
1139
|
+
|
|
1140
|
+
// Create the table
|
|
1141
|
+
const table = await db.schema.createTable("users", fields);
|
|
1142
|
+
|
|
1143
|
+
// Later, add more fields
|
|
1144
|
+
await db.schema.addFields("users", [
|
|
1145
|
+
{
|
|
1146
|
+
name: "phone",
|
|
1147
|
+
type: "string",
|
|
1148
|
+
nullable: true,
|
|
1149
|
+
},
|
|
1150
|
+
]);
|
|
1151
|
+
|
|
1152
|
+
// Create an index on email
|
|
1153
|
+
await db.schema.createIndex("users", "email");
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
**Note:** Schema management operations require appropriate access privileges on your FileMaker account. Operations will throw errors if you don't have the necessary permissions.
|
|
1157
|
+
|
|
792
1158
|
## Advanced Features
|
|
793
1159
|
|
|
794
1160
|
### Type Safety
|
|
@@ -796,7 +1162,7 @@ console.log(result.result.recordId);
|
|
|
796
1162
|
The library provides full TypeScript type inference:
|
|
797
1163
|
|
|
798
1164
|
```typescript
|
|
799
|
-
const usersBase =
|
|
1165
|
+
const usersBase = defineBaseTable({
|
|
800
1166
|
schema: {
|
|
801
1167
|
id: z.string(),
|
|
802
1168
|
username: z.string(),
|
|
@@ -805,12 +1171,12 @@ const usersBase = new BaseTable({
|
|
|
805
1171
|
idField: "id",
|
|
806
1172
|
});
|
|
807
1173
|
|
|
808
|
-
const usersTO =
|
|
1174
|
+
const usersTO = defineTableOccurrence({
|
|
809
1175
|
name: "users",
|
|
810
1176
|
baseTable: usersBase,
|
|
811
1177
|
});
|
|
812
1178
|
|
|
813
|
-
const db =
|
|
1179
|
+
const db = connection.database("MyDB", {
|
|
814
1180
|
occurrences: [usersTO],
|
|
815
1181
|
});
|
|
816
1182
|
|
|
@@ -829,43 +1195,97 @@ db.from("users")
|
|
|
829
1195
|
.filter({ invalid: { eq: "john" } }); // TS Error
|
|
830
1196
|
```
|
|
831
1197
|
|
|
832
|
-
### Required Fields
|
|
1198
|
+
### Required and Read-Only Fields
|
|
833
1199
|
|
|
834
|
-
|
|
1200
|
+
The library automatically infers which fields are required based on whether their validator allows `null` or `undefined`:
|
|
835
1201
|
|
|
836
1202
|
```typescript
|
|
837
|
-
const usersBase =
|
|
1203
|
+
const usersBase = defineBaseTable({
|
|
838
1204
|
schema: {
|
|
839
|
-
id: z.string(),
|
|
840
|
-
username: z.string(),
|
|
841
|
-
email: z.string(),
|
|
842
|
-
status: z.string(),
|
|
843
|
-
|
|
1205
|
+
id: z.string(), // Auto-required, auto-readOnly (idField)
|
|
1206
|
+
username: z.string(), // Auto-required (not nullable)
|
|
1207
|
+
email: z.string(), // Auto-required (not nullable)
|
|
1208
|
+
status: z.string().nullable(), // Optional (nullable)
|
|
1209
|
+
createdAt: z.string(), // Read-only system field
|
|
1210
|
+
updatedAt: z.string().nullable(), // Optional
|
|
844
1211
|
},
|
|
845
|
-
idField: "id",
|
|
846
|
-
|
|
847
|
-
|
|
1212
|
+
idField: "id", // Automatically excluded from insert/update
|
|
1213
|
+
required: ["status"], // Make status required despite being nullable
|
|
1214
|
+
readOnly: ["createdAt"], // Exclude createdAt from insert/update
|
|
848
1215
|
});
|
|
849
1216
|
|
|
850
|
-
// Insert
|
|
1217
|
+
// Insert: username, email, and status are required
|
|
1218
|
+
// Insert: id and createdAt are excluded (cannot be provided)
|
|
851
1219
|
db.from("users").insert({
|
|
852
1220
|
username: "john",
|
|
853
1221
|
email: "john@example.com",
|
|
854
|
-
//
|
|
1222
|
+
status: "active", // Required due to 'required' array
|
|
1223
|
+
updatedAt: new Date().toISOString(), // Optional
|
|
855
1224
|
});
|
|
856
1225
|
|
|
857
|
-
// Update
|
|
1226
|
+
// Update: all fields are optional except id and createdAt are excluded
|
|
858
1227
|
db.from("users")
|
|
859
1228
|
.update({
|
|
860
|
-
status: "active",
|
|
861
|
-
//
|
|
1229
|
+
status: "active", // Optional
|
|
1230
|
+
// id and createdAt cannot be modified
|
|
862
1231
|
})
|
|
863
1232
|
.byId("user-123");
|
|
864
1233
|
```
|
|
865
1234
|
|
|
1235
|
+
**Key Features:**
|
|
1236
|
+
|
|
1237
|
+
- **Auto-inference:** Non-nullable fields are automatically required for insert
|
|
1238
|
+
- **Additional requirements:** Use `required` to make nullable fields required for new records
|
|
1239
|
+
- **Read-only fields:** Use `readOnly` to exclude fields from insert/update (e.g., timestamps)
|
|
1240
|
+
- **Automatic ID exclusion:** The `idField` is always read-only without needing to specify it
|
|
1241
|
+
- **Update flexibility:** All fields are optional for updates (except read-only fields)
|
|
1242
|
+
|
|
1243
|
+
### Prefer: fmodata.entity-ids
|
|
1244
|
+
|
|
1245
|
+
This library supports using FileMaker's internal field identifiers (FMFID) and table occurrence identifiers (FMTID) instead of names. This protects your integration from both field and table occurrence name changes.
|
|
1246
|
+
|
|
1247
|
+
To enable this feature, simply define your schema with entity IDs using the `defineBaseTable` and `defineTableOccurrence` functions. Behind the scenes, the library will transform your request and the response back to the names you specify in these schemas. 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 `fmfIds` on the base table and `fmtId` on the table occurrence).
|
|
1248
|
+
|
|
1249
|
+
_Note for OttoFMS proxy: This feature requires version 4.14 or later of OttoFMS_
|
|
1250
|
+
|
|
1251
|
+
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
|
|
1252
|
+
|
|
1253
|
+
#### Basic Usage
|
|
1254
|
+
|
|
1255
|
+
```typescript
|
|
1256
|
+
import { defineBaseTable, defineTableOccurrence } from "@proofkit/fmodata";
|
|
1257
|
+
import { z } from "zod/v4";
|
|
1258
|
+
|
|
1259
|
+
// Define a base table with FileMaker field IDs
|
|
1260
|
+
const usersBase = defineBaseTable({
|
|
1261
|
+
schema: {
|
|
1262
|
+
id: z.string(),
|
|
1263
|
+
username: z.string(),
|
|
1264
|
+
email: z.string().nullable(),
|
|
1265
|
+
createdAt: z.string(),
|
|
1266
|
+
},
|
|
1267
|
+
idField: "id",
|
|
1268
|
+
fmfIds: {
|
|
1269
|
+
id: "FMFID:12039485",
|
|
1270
|
+
username: "FMFID:34323433",
|
|
1271
|
+
email: "FMFID:12232424",
|
|
1272
|
+
createdAt: "FMFID:43234355",
|
|
1273
|
+
},
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
// Create a table occurrence with a FileMaker table occurrence ID
|
|
1277
|
+
const usersTO = defineTableOccurrence({
|
|
1278
|
+
name: "users",
|
|
1279
|
+
baseTable: usersBase,
|
|
1280
|
+
fmtId: "FMTID:12432533",
|
|
1281
|
+
});
|
|
1282
|
+
```
|
|
1283
|
+
|
|
866
1284
|
### Error Handling
|
|
867
1285
|
|
|
868
|
-
All operations return a `Result` type with either `data` or `error
|
|
1286
|
+
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.
|
|
1287
|
+
|
|
1288
|
+
#### Basic Error Checking
|
|
869
1289
|
|
|
870
1290
|
```typescript
|
|
871
1291
|
const result = await db.from("users").list().execute();
|
|
@@ -880,6 +1300,277 @@ if (result.data) {
|
|
|
880
1300
|
}
|
|
881
1301
|
```
|
|
882
1302
|
|
|
1303
|
+
#### HTTP Errors
|
|
1304
|
+
|
|
1305
|
+
Handle HTTP status codes (4xx, 5xx) with the `HTTPError` class:
|
|
1306
|
+
|
|
1307
|
+
```typescript
|
|
1308
|
+
import { HTTPError, isHTTPError } from "@proofkit/fmodata";
|
|
1309
|
+
|
|
1310
|
+
const result = await db.from("users").list().execute();
|
|
1311
|
+
|
|
1312
|
+
if (result.error) {
|
|
1313
|
+
if (isHTTPError(result.error)) {
|
|
1314
|
+
// TypeScript knows this is HTTPError
|
|
1315
|
+
console.log("HTTP Status:", result.error.status);
|
|
1316
|
+
|
|
1317
|
+
if (result.error.isNotFound()) {
|
|
1318
|
+
console.log("Resource not found");
|
|
1319
|
+
} else if (result.error.isUnauthorized()) {
|
|
1320
|
+
console.log("Authentication required");
|
|
1321
|
+
} else if (result.error.is5xx()) {
|
|
1322
|
+
console.log("Server error - try again later");
|
|
1323
|
+
} else if (result.error.is4xx()) {
|
|
1324
|
+
console.log("Client error:", result.error.statusText);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// Access the response body if available
|
|
1328
|
+
if (result.error.response) {
|
|
1329
|
+
console.log("Error details:", result.error.response);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
```
|
|
1334
|
+
|
|
1335
|
+
#### Network Errors
|
|
1336
|
+
|
|
1337
|
+
Handle network-level errors (timeouts, connection issues, etc.):
|
|
1338
|
+
|
|
1339
|
+
```typescript
|
|
1340
|
+
import {
|
|
1341
|
+
TimeoutError,
|
|
1342
|
+
NetworkError,
|
|
1343
|
+
RetryLimitError,
|
|
1344
|
+
CircuitOpenError,
|
|
1345
|
+
} from "@proofkit/fmodata";
|
|
1346
|
+
|
|
1347
|
+
const result = await db.from("users").list().execute();
|
|
1348
|
+
|
|
1349
|
+
if (result.error) {
|
|
1350
|
+
if (result.error instanceof TimeoutError) {
|
|
1351
|
+
console.log("Request timed out");
|
|
1352
|
+
// Show user-friendly timeout message
|
|
1353
|
+
} else if (result.error instanceof NetworkError) {
|
|
1354
|
+
console.log("Network connectivity issue");
|
|
1355
|
+
// Show offline message
|
|
1356
|
+
} else if (result.error instanceof RetryLimitError) {
|
|
1357
|
+
console.log("Request failed after retries");
|
|
1358
|
+
// Log the underlying error: result.error.cause
|
|
1359
|
+
} else if (result.error instanceof CircuitOpenError) {
|
|
1360
|
+
console.log("Service is currently unavailable");
|
|
1361
|
+
// Show maintenance message
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
#### Validation Errors
|
|
1367
|
+
|
|
1368
|
+
When schema validation fails, you get a `ValidationError` with rich context:
|
|
1369
|
+
|
|
1370
|
+
```typescript
|
|
1371
|
+
import { ValidationError, isValidationError } from "@proofkit/fmodata";
|
|
1372
|
+
|
|
1373
|
+
const result = await db.from("users").list().execute();
|
|
1374
|
+
|
|
1375
|
+
if (result.error) {
|
|
1376
|
+
if (isValidationError(result.error)) {
|
|
1377
|
+
// Access validation issues (Standard Schema format)
|
|
1378
|
+
console.log("Validation failed for field:", result.error.field);
|
|
1379
|
+
console.log("Issues:", result.error.issues);
|
|
1380
|
+
console.log("Failed value:", result.error.value);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
**Validator-Agnostic Error Handling**
|
|
1386
|
+
|
|
1387
|
+
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:
|
|
1388
|
+
|
|
1389
|
+
```typescript
|
|
1390
|
+
import { ValidationError } from "@proofkit/fmodata";
|
|
1391
|
+
|
|
1392
|
+
const result = await db.from("users").list().execute();
|
|
1393
|
+
|
|
1394
|
+
if (result.error instanceof ValidationError) {
|
|
1395
|
+
// The cause property (ES2022 Error.cause) contains the Standard Schema issues array
|
|
1396
|
+
// This is validator-agnostic and works with Zod, Valibot, ArkType, etc.
|
|
1397
|
+
console.log("Validation issues:", result.error.cause);
|
|
1398
|
+
console.log("Issues are also available directly:", result.error.issues);
|
|
1399
|
+
|
|
1400
|
+
// Both point to the same array
|
|
1401
|
+
console.log(result.error.cause === result.error.issues); // true
|
|
1402
|
+
|
|
1403
|
+
// Access additional context
|
|
1404
|
+
console.log("Failed field:", result.error.field);
|
|
1405
|
+
console.log("Failed value:", result.error.value);
|
|
1406
|
+
|
|
1407
|
+
// Standard Schema issues have a normalized format
|
|
1408
|
+
result.error.issues.forEach((issue) => {
|
|
1409
|
+
console.log("Path:", issue.path);
|
|
1410
|
+
console.log("Message:", issue.message);
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
**Why Standard Schema Issues Instead of Original Validator Errors?**
|
|
1416
|
+
|
|
1417
|
+
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.
|
|
1418
|
+
|
|
1419
|
+
If you need validator-specific error formatting, you can still access your validator's methods during validation before the data reaches fmodata:
|
|
1420
|
+
|
|
1421
|
+
```typescript
|
|
1422
|
+
import { z } from "zod";
|
|
1423
|
+
|
|
1424
|
+
const userSchema = z.object({
|
|
1425
|
+
email: z.string().email(),
|
|
1426
|
+
age: z.number().min(0).max(150),
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
// Validate early if you need Zod-specific error handling
|
|
1430
|
+
const parseResult = userSchema.safeParse(userData);
|
|
1431
|
+
if (!parseResult.success) {
|
|
1432
|
+
// Use Zod's error formatting
|
|
1433
|
+
const formatted = parseResult.error.flatten();
|
|
1434
|
+
console.log("Zod-specific formatting:", formatted);
|
|
1435
|
+
}
|
|
1436
|
+
```
|
|
1437
|
+
|
|
1438
|
+
#### OData Errors
|
|
1439
|
+
|
|
1440
|
+
Handle OData-specific protocol errors:
|
|
1441
|
+
|
|
1442
|
+
```typescript
|
|
1443
|
+
import { ODataError, isODataError } from "@proofkit/fmodata";
|
|
1444
|
+
|
|
1445
|
+
const result = await db.from("users").list().execute();
|
|
1446
|
+
|
|
1447
|
+
if (result.error) {
|
|
1448
|
+
if (isODataError(result.error)) {
|
|
1449
|
+
console.log("OData Error Code:", result.error.code);
|
|
1450
|
+
console.log("OData Error Message:", result.error.message);
|
|
1451
|
+
console.log("OData Error Details:", result.error.details);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
```
|
|
1455
|
+
|
|
1456
|
+
#### Error Handling Patterns
|
|
1457
|
+
|
|
1458
|
+
**Pattern 1: Using instanceof (like ffetch):**
|
|
1459
|
+
|
|
1460
|
+
```typescript
|
|
1461
|
+
import {
|
|
1462
|
+
HTTPError,
|
|
1463
|
+
ValidationError,
|
|
1464
|
+
TimeoutError,
|
|
1465
|
+
NetworkError,
|
|
1466
|
+
} from "@proofkit/fmodata";
|
|
1467
|
+
|
|
1468
|
+
const result = await db.from("users").list().execute();
|
|
1469
|
+
|
|
1470
|
+
if (result.error) {
|
|
1471
|
+
if (result.error instanceof TimeoutError) {
|
|
1472
|
+
showTimeoutMessage();
|
|
1473
|
+
} else if (result.error instanceof HTTPError) {
|
|
1474
|
+
if (result.error.isNotFound()) {
|
|
1475
|
+
showNotFoundMessage();
|
|
1476
|
+
} else if (result.error.is5xx()) {
|
|
1477
|
+
showServerErrorMessage();
|
|
1478
|
+
}
|
|
1479
|
+
} else if (result.error instanceof ValidationError) {
|
|
1480
|
+
showValidationError(result.error.field, result.error.issues);
|
|
1481
|
+
} else if (result.error instanceof NetworkError) {
|
|
1482
|
+
showOfflineMessage();
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
```
|
|
1486
|
+
|
|
1487
|
+
**Pattern 2: Using kind property (for exhaustive matching):**
|
|
1488
|
+
|
|
1489
|
+
```typescript
|
|
1490
|
+
const result = await db.from("users").list().execute();
|
|
1491
|
+
|
|
1492
|
+
if (result.error) {
|
|
1493
|
+
switch (result.error.kind) {
|
|
1494
|
+
case "TimeoutError":
|
|
1495
|
+
showTimeoutMessage();
|
|
1496
|
+
break;
|
|
1497
|
+
case "HTTPError":
|
|
1498
|
+
handleHTTPError(result.error.status);
|
|
1499
|
+
break;
|
|
1500
|
+
case "ValidationError":
|
|
1501
|
+
showValidationError(result.error.field, result.error.issues);
|
|
1502
|
+
break;
|
|
1503
|
+
case "NetworkError":
|
|
1504
|
+
showOfflineMessage();
|
|
1505
|
+
break;
|
|
1506
|
+
case "ODataError":
|
|
1507
|
+
handleODataError(result.error.code);
|
|
1508
|
+
break;
|
|
1509
|
+
// TypeScript ensures exhaustive matching!
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
```
|
|
1513
|
+
|
|
1514
|
+
**Pattern 3: Using type guards:**
|
|
1515
|
+
|
|
1516
|
+
```typescript
|
|
1517
|
+
import {
|
|
1518
|
+
isHTTPError,
|
|
1519
|
+
isValidationError,
|
|
1520
|
+
isODataError,
|
|
1521
|
+
isNetworkError,
|
|
1522
|
+
} from "@proofkit/fmodata";
|
|
1523
|
+
|
|
1524
|
+
const result = await db.from("users").list().execute();
|
|
1525
|
+
|
|
1526
|
+
if (result.error) {
|
|
1527
|
+
if (isHTTPError(result.error)) {
|
|
1528
|
+
// TypeScript knows this is HTTPError
|
|
1529
|
+
console.log("Status:", result.error.status);
|
|
1530
|
+
} else if (isValidationError(result.error)) {
|
|
1531
|
+
// TypeScript knows this is ValidationError
|
|
1532
|
+
console.log("Field:", result.error.field);
|
|
1533
|
+
console.log("Issues:", result.error.issues);
|
|
1534
|
+
} else if (isODataError(result.error)) {
|
|
1535
|
+
// TypeScript knows this is ODataError
|
|
1536
|
+
console.log("Code:", result.error.code);
|
|
1537
|
+
} else if (isNetworkError(result.error)) {
|
|
1538
|
+
// TypeScript knows this is NetworkError
|
|
1539
|
+
console.log("Network issue:", result.error.cause);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
```
|
|
1543
|
+
|
|
1544
|
+
#### Error Properties
|
|
1545
|
+
|
|
1546
|
+
All errors include helpful metadata:
|
|
1547
|
+
|
|
1548
|
+
```typescript
|
|
1549
|
+
if (result.error) {
|
|
1550
|
+
// All errors have a timestamp
|
|
1551
|
+
console.log("Error occurred at:", result.error.timestamp);
|
|
1552
|
+
|
|
1553
|
+
// All errors have a kind property for discriminated unions
|
|
1554
|
+
console.log("Error kind:", result.error.kind);
|
|
1555
|
+
|
|
1556
|
+
// All errors have a message
|
|
1557
|
+
console.log("Error message:", result.error.message);
|
|
1558
|
+
}
|
|
1559
|
+
```
|
|
1560
|
+
|
|
1561
|
+
#### Available Error Types
|
|
1562
|
+
|
|
1563
|
+
- **`HTTPError`** - HTTP status errors (4xx, 5xx) with helper methods (`is4xx()`, `is5xx()`, `isNotFound()`, etc.)
|
|
1564
|
+
- **`ODataError`** - OData protocol errors with code and details
|
|
1565
|
+
- **`ValidationError`** - Schema validation failures with issues, schema reference, and failed value
|
|
1566
|
+
- **`ResponseStructureError`** - Malformed API responses
|
|
1567
|
+
- **`RecordCountMismatchError`** - When `single()` or `maybeSingle()` expectations aren't met
|
|
1568
|
+
- **`TimeoutError`** - Request timeout (from ffetch)
|
|
1569
|
+
- **`NetworkError`** - Network connectivity issues (from ffetch)
|
|
1570
|
+
- **`RetryLimitError`** - Request failed after retries (from ffetch)
|
|
1571
|
+
- **`CircuitOpenError`** - Circuit breaker is open (from ffetch)
|
|
1572
|
+
- **`AbortError`** - Request was aborted (from ffetch)
|
|
1573
|
+
|
|
883
1574
|
### OData Annotations and Validation
|
|
884
1575
|
|
|
885
1576
|
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`:
|