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