@proofkit/fmodata 0.1.0-alpha.4 → 0.1.0-alpha.7
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 +690 -31
- package/dist/esm/client/base-table.d.ts +122 -5
- package/dist/esm/client/base-table.js +46 -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 +54 -5
- 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 +22 -8
- package/dist/esm/client/entity-set.js +28 -8
- package/dist/esm/client/entity-set.js.map +1 -1
- package/dist/esm/client/filemaker-odata.d.ts +22 -3
- package/dist/esm/client/filemaker-odata.js +122 -27
- 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 +26 -5
- package/dist/esm/client/query-builder.js +455 -208
- package/dist/esm/client/query-builder.js.map +1 -1
- package/dist/esm/client/record-builder.d.ts +19 -4
- package/dist/esm/client/record-builder.js +132 -40
- 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 +66 -2
- package/dist/esm/client/table-occurrence.js +36 -1
- 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 +7 -3
- package/dist/esm/index.js +27 -3
- 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 +155 -8
- package/src/client/batch-builder.ts +265 -0
- package/src/client/batch-request.ts +485 -0
- package/src/client/database.ts +173 -16
- package/src/client/delete-builder.ts +149 -48
- package/src/client/entity-set.ts +99 -15
- package/src/client/filemaker-odata.ts +178 -34
- package/src/client/insert-builder.ts +350 -40
- package/src/client/query-builder.ts +609 -236
- package/src/client/record-builder.ts +186 -53
- package/src/client/response-processor.ts +103 -0
- package/src/client/schema-manager.ts +246 -0
- package/src/client/table-occurrence.ts +118 -4
- package/src/client/update-builder.ts +235 -49
- package/src/errors.ts +217 -0
- package/src/index.ts +43 -1
- package/src/transform.ts +249 -0
- package/src/types.ts +201 -35
- package/src/validation.ts +120 -36
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
|
|
|
@@ -78,7 +81,7 @@ if (data) {
|
|
|
78
81
|
|
|
79
82
|
## Core Concepts
|
|
80
83
|
|
|
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
|
|
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.
|
|
82
85
|
|
|
83
86
|
As such, there are layers to the library to help you build your queries and operations.
|
|
84
87
|
|
|
@@ -140,9 +143,9 @@ const contactsBase = new BaseTable({
|
|
|
140
143
|
phone: z.string().optional(),
|
|
141
144
|
createdAt: z.string(),
|
|
142
145
|
},
|
|
143
|
-
idField: "id", // The primary key field
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
idField: "id", // The primary key field (automatically read-only)
|
|
147
|
+
required: ["phone"], // optional: additional required fields for insert (beyond auto-inferred)
|
|
148
|
+
readOnly: ["createdAt"], // optional: fields excluded from insert/update
|
|
146
149
|
});
|
|
147
150
|
```
|
|
148
151
|
|
|
@@ -524,27 +527,30 @@ if (result.data) {
|
|
|
524
527
|
}
|
|
525
528
|
```
|
|
526
529
|
|
|
527
|
-
|
|
530
|
+
Fields are automatically required for insert if their validator doesn't allow `null` or `undefined`. You can specify additional required fields:
|
|
528
531
|
|
|
529
532
|
```typescript
|
|
530
533
|
const usersBase = new BaseTable({
|
|
531
534
|
schema: {
|
|
532
|
-
id: z.string(),
|
|
533
|
-
username: z.string(),
|
|
534
|
-
email: z.string(),
|
|
535
|
-
|
|
535
|
+
id: z.string(), // Auto-required (not nullable), but excluded from insert (idField)
|
|
536
|
+
username: z.string(), // Auto-required (not nullable)
|
|
537
|
+
email: z.string(), // Auto-required (not nullable)
|
|
538
|
+
phone: z.string().nullable(), // Optional by default
|
|
539
|
+
createdAt: z.string(), // Auto-required, but excluded (readOnly)
|
|
536
540
|
},
|
|
537
|
-
idField: "id",
|
|
538
|
-
|
|
541
|
+
idField: "id", // Automatically excluded from insert/update
|
|
542
|
+
required: ["phone"], // Make phone required for inserts despite being nullable
|
|
543
|
+
readOnly: ["createdAt"], // Exclude from insert/update operations
|
|
539
544
|
});
|
|
540
545
|
|
|
541
|
-
// TypeScript
|
|
546
|
+
// TypeScript enforces: username, email, and phone are required
|
|
547
|
+
// TypeScript excludes: id and createdAt cannot be provided
|
|
542
548
|
const result = await db
|
|
543
549
|
.from("users")
|
|
544
550
|
.insert({
|
|
545
551
|
username: "johndoe",
|
|
546
552
|
email: "john@example.com",
|
|
547
|
-
//
|
|
553
|
+
phone: "+1234567890", // Required because specified in 'required' array
|
|
548
554
|
})
|
|
549
555
|
.execute();
|
|
550
556
|
```
|
|
@@ -799,6 +805,334 @@ console.log(result.result.recordId);
|
|
|
799
805
|
|
|
800
806
|
**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
807
|
|
|
808
|
+
## Batch Operations
|
|
809
|
+
|
|
810
|
+
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.
|
|
811
|
+
|
|
812
|
+
### Basic Batch with Multiple Queries
|
|
813
|
+
|
|
814
|
+
Execute multiple read operations in a single batch:
|
|
815
|
+
|
|
816
|
+
```typescript
|
|
817
|
+
// Create query builders
|
|
818
|
+
const contactsQuery = db.from("contacts").list().top(5);
|
|
819
|
+
const usersQuery = db.from("users").list().top(5);
|
|
820
|
+
|
|
821
|
+
// Execute both queries in a single batch
|
|
822
|
+
const result = await db.batch([contactsQuery, usersQuery]).execute();
|
|
823
|
+
|
|
824
|
+
if (result.data) {
|
|
825
|
+
// Result is a tuple matching the input builders
|
|
826
|
+
const [contacts, users] = result.data;
|
|
827
|
+
|
|
828
|
+
console.log("Contacts:", contacts);
|
|
829
|
+
console.log("Users:", users);
|
|
830
|
+
}
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
### Mixed Operations (Reads and Writes)
|
|
834
|
+
|
|
835
|
+
Combine queries, inserts, updates, and deletes in a single batch:
|
|
836
|
+
|
|
837
|
+
```typescript
|
|
838
|
+
// Mix different operation types
|
|
839
|
+
const listQuery = db.from("contacts").list().top(10);
|
|
840
|
+
const insertOp = db.from("contacts").insert({
|
|
841
|
+
name: "John Doe",
|
|
842
|
+
email: "john@example.com",
|
|
843
|
+
});
|
|
844
|
+
const updateOp = db.from("users").update({ active: true }).byId("user-123");
|
|
845
|
+
|
|
846
|
+
// All operations execute atomically
|
|
847
|
+
const result = await db.batch([listQuery, insertOp, updateOp]).execute();
|
|
848
|
+
|
|
849
|
+
if (result.data) {
|
|
850
|
+
const [contactsList, insertResult, updateResult] = result.data;
|
|
851
|
+
|
|
852
|
+
console.log("Fetched contacts:", contactsList);
|
|
853
|
+
console.log("Inserted contact:", insertResult);
|
|
854
|
+
console.log("Updated user:", updateResult);
|
|
855
|
+
}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
### Transactional Behavior
|
|
859
|
+
|
|
860
|
+
Batch operations are transactional for write operations (inserts, updates, deletes). If any operation in the batch fails, all write operations are rolled back:
|
|
861
|
+
|
|
862
|
+
```typescript
|
|
863
|
+
const result = await db
|
|
864
|
+
.batch([
|
|
865
|
+
db.from("users").insert({ username: "alice", email: "alice@example.com" }),
|
|
866
|
+
db.from("users").insert({ username: "bob", email: "bob@example.com" }),
|
|
867
|
+
db.from("users").insert({ username: "charlie", email: "invalid" }), // This fails
|
|
868
|
+
])
|
|
869
|
+
.execute();
|
|
870
|
+
|
|
871
|
+
if (result.error) {
|
|
872
|
+
// All three inserts are rolled back - no users were created
|
|
873
|
+
console.error("Batch failed:", result.error);
|
|
874
|
+
}
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
**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.
|
|
878
|
+
|
|
879
|
+
## Schema Management
|
|
880
|
+
|
|
881
|
+
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.
|
|
882
|
+
|
|
883
|
+
### Creating Tables
|
|
884
|
+
|
|
885
|
+
Create a new table with field definitions:
|
|
886
|
+
|
|
887
|
+
```typescript
|
|
888
|
+
import type { Field } from "@proofkit/fmodata";
|
|
889
|
+
|
|
890
|
+
const fields: Field[] = [
|
|
891
|
+
{
|
|
892
|
+
name: "id",
|
|
893
|
+
type: "string",
|
|
894
|
+
primary: true,
|
|
895
|
+
maxLength: 36,
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
name: "username",
|
|
899
|
+
type: "string",
|
|
900
|
+
nullable: false,
|
|
901
|
+
unique: true,
|
|
902
|
+
maxLength: 50,
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
name: "email",
|
|
906
|
+
type: "string",
|
|
907
|
+
nullable: false,
|
|
908
|
+
maxLength: 255,
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
name: "age",
|
|
912
|
+
type: "numeric",
|
|
913
|
+
nullable: true,
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
name: "created_at",
|
|
917
|
+
type: "timestamp",
|
|
918
|
+
default: "CURRENT_TIMESTAMP",
|
|
919
|
+
},
|
|
920
|
+
];
|
|
921
|
+
|
|
922
|
+
const tableDefinition = await db.schema.createTable("users", fields);
|
|
923
|
+
console.log(tableDefinition.tableName); // "users"
|
|
924
|
+
console.log(tableDefinition.fields); // Array of field definitions
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
### Field Types
|
|
928
|
+
|
|
929
|
+
The library supports various field types:
|
|
930
|
+
|
|
931
|
+
**String Fields:**
|
|
932
|
+
|
|
933
|
+
```typescript
|
|
934
|
+
{
|
|
935
|
+
name: "username",
|
|
936
|
+
type: "string",
|
|
937
|
+
maxLength: 100, // Optional: varchar(100)
|
|
938
|
+
nullable: true,
|
|
939
|
+
unique: true,
|
|
940
|
+
default: "USER" | "USERNAME" | "CURRENT_USER", // Optional
|
|
941
|
+
repetitions: 5, // Optional: for repeating fields
|
|
942
|
+
}
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
**Numeric Fields:**
|
|
946
|
+
|
|
947
|
+
```typescript
|
|
948
|
+
{
|
|
949
|
+
name: "age",
|
|
950
|
+
type: "numeric",
|
|
951
|
+
nullable: true,
|
|
952
|
+
primary: false,
|
|
953
|
+
unique: false,
|
|
954
|
+
}
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
**Date Fields:**
|
|
958
|
+
|
|
959
|
+
```typescript
|
|
960
|
+
{
|
|
961
|
+
name: "birth_date",
|
|
962
|
+
type: "date",
|
|
963
|
+
default: "CURRENT_DATE" | "CURDATE", // Optional
|
|
964
|
+
nullable: true,
|
|
965
|
+
}
|
|
966
|
+
```
|
|
967
|
+
|
|
968
|
+
**Time Fields:**
|
|
969
|
+
|
|
970
|
+
```typescript
|
|
971
|
+
{
|
|
972
|
+
name: "start_time",
|
|
973
|
+
type: "time",
|
|
974
|
+
default: "CURRENT_TIME" | "CURTIME", // Optional
|
|
975
|
+
nullable: true,
|
|
976
|
+
}
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
**Timestamp Fields:**
|
|
980
|
+
|
|
981
|
+
```typescript
|
|
982
|
+
{
|
|
983
|
+
name: "created_at",
|
|
984
|
+
type: "timestamp",
|
|
985
|
+
default: "CURRENT_TIMESTAMP" | "CURTIMESTAMP", // Optional
|
|
986
|
+
nullable: false,
|
|
987
|
+
}
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
**Container Fields:**
|
|
991
|
+
|
|
992
|
+
```typescript
|
|
993
|
+
{
|
|
994
|
+
name: "avatar",
|
|
995
|
+
type: "container",
|
|
996
|
+
externalSecurePath: "/secure/path", // Optional
|
|
997
|
+
nullable: true,
|
|
998
|
+
}
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
### Adding Fields to Existing Tables
|
|
1002
|
+
|
|
1003
|
+
Add new fields to an existing table:
|
|
1004
|
+
|
|
1005
|
+
```typescript
|
|
1006
|
+
const newFields: Field[] = [
|
|
1007
|
+
{
|
|
1008
|
+
name: "phone",
|
|
1009
|
+
type: "string",
|
|
1010
|
+
nullable: true,
|
|
1011
|
+
maxLength: 20,
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
name: "bio",
|
|
1015
|
+
type: "string",
|
|
1016
|
+
nullable: true,
|
|
1017
|
+
maxLength: 1000,
|
|
1018
|
+
},
|
|
1019
|
+
];
|
|
1020
|
+
|
|
1021
|
+
const updatedTable = await db.schema.addFields("users", newFields);
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
### Deleting Tables and Fields
|
|
1025
|
+
|
|
1026
|
+
Delete an entire table:
|
|
1027
|
+
|
|
1028
|
+
```typescript
|
|
1029
|
+
await db.schema.deleteTable("old_table");
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
Delete a specific field from a table:
|
|
1033
|
+
|
|
1034
|
+
```typescript
|
|
1035
|
+
await db.schema.deleteField("users", "old_field");
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
### Managing Indexes
|
|
1039
|
+
|
|
1040
|
+
Create an index on a field:
|
|
1041
|
+
|
|
1042
|
+
```typescript
|
|
1043
|
+
const index = await db.schema.createIndex("users", "email");
|
|
1044
|
+
console.log(index.indexName); // "email"
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
Delete an index:
|
|
1048
|
+
|
|
1049
|
+
```typescript
|
|
1050
|
+
await db.schema.deleteIndex("users", "email");
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
### Complete Example
|
|
1054
|
+
|
|
1055
|
+
Here's a complete example of creating a table with various field types:
|
|
1056
|
+
|
|
1057
|
+
```typescript
|
|
1058
|
+
const fields: Field[] = [
|
|
1059
|
+
// Primary key
|
|
1060
|
+
{
|
|
1061
|
+
name: "id",
|
|
1062
|
+
type: "string",
|
|
1063
|
+
primary: true,
|
|
1064
|
+
maxLength: 36,
|
|
1065
|
+
},
|
|
1066
|
+
|
|
1067
|
+
// String fields
|
|
1068
|
+
{
|
|
1069
|
+
name: "username",
|
|
1070
|
+
type: "string",
|
|
1071
|
+
nullable: false,
|
|
1072
|
+
unique: true,
|
|
1073
|
+
maxLength: 50,
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
name: "email",
|
|
1077
|
+
type: "string",
|
|
1078
|
+
nullable: false,
|
|
1079
|
+
maxLength: 255,
|
|
1080
|
+
},
|
|
1081
|
+
|
|
1082
|
+
// Numeric field
|
|
1083
|
+
{
|
|
1084
|
+
name: "age",
|
|
1085
|
+
type: "numeric",
|
|
1086
|
+
nullable: true,
|
|
1087
|
+
},
|
|
1088
|
+
|
|
1089
|
+
// Date/time fields
|
|
1090
|
+
{
|
|
1091
|
+
name: "birth_date",
|
|
1092
|
+
type: "date",
|
|
1093
|
+
nullable: true,
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
name: "created_at",
|
|
1097
|
+
type: "timestamp",
|
|
1098
|
+
default: "CURRENT_TIMESTAMP",
|
|
1099
|
+
nullable: false,
|
|
1100
|
+
},
|
|
1101
|
+
|
|
1102
|
+
// Container field
|
|
1103
|
+
{
|
|
1104
|
+
name: "avatar",
|
|
1105
|
+
type: "container",
|
|
1106
|
+
nullable: true,
|
|
1107
|
+
},
|
|
1108
|
+
|
|
1109
|
+
// Repeating field
|
|
1110
|
+
{
|
|
1111
|
+
name: "tags",
|
|
1112
|
+
type: "string",
|
|
1113
|
+
repetitions: 5,
|
|
1114
|
+
maxLength: 50,
|
|
1115
|
+
},
|
|
1116
|
+
];
|
|
1117
|
+
|
|
1118
|
+
// Create the table
|
|
1119
|
+
const table = await db.schema.createTable("users", fields);
|
|
1120
|
+
|
|
1121
|
+
// Later, add more fields
|
|
1122
|
+
await db.schema.addFields("users", [
|
|
1123
|
+
{
|
|
1124
|
+
name: "phone",
|
|
1125
|
+
type: "string",
|
|
1126
|
+
nullable: true,
|
|
1127
|
+
},
|
|
1128
|
+
]);
|
|
1129
|
+
|
|
1130
|
+
// Create an index on email
|
|
1131
|
+
await db.schema.createIndex("users", "email");
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
**Note:** Schema management operations require appropriate access privileges on your FileMaker account. Operations will throw errors if you don't have the necessary permissions.
|
|
1135
|
+
|
|
802
1136
|
## Advanced Features
|
|
803
1137
|
|
|
804
1138
|
### Type Safety
|
|
@@ -839,43 +1173,97 @@ db.from("users")
|
|
|
839
1173
|
.filter({ invalid: { eq: "john" } }); // TS Error
|
|
840
1174
|
```
|
|
841
1175
|
|
|
842
|
-
### Required Fields
|
|
1176
|
+
### Required and Read-Only Fields
|
|
843
1177
|
|
|
844
|
-
|
|
1178
|
+
The library automatically infers which fields are required based on whether their validator allows `null` or `undefined`:
|
|
845
1179
|
|
|
846
1180
|
```typescript
|
|
847
1181
|
const usersBase = new BaseTable({
|
|
848
1182
|
schema: {
|
|
849
|
-
id: z.string(),
|
|
850
|
-
username: z.string(),
|
|
851
|
-
email: z.string(),
|
|
852
|
-
status: z.string(),
|
|
853
|
-
|
|
1183
|
+
id: z.string(), // Auto-required, auto-readOnly (idField)
|
|
1184
|
+
username: z.string(), // Auto-required (not nullable)
|
|
1185
|
+
email: z.string(), // Auto-required (not nullable)
|
|
1186
|
+
status: z.string().nullable(), // Optional (nullable)
|
|
1187
|
+
createdAt: z.string(), // Read-only system field
|
|
1188
|
+
updatedAt: z.string().nullable(), // Optional
|
|
854
1189
|
},
|
|
855
|
-
idField: "id",
|
|
856
|
-
|
|
857
|
-
|
|
1190
|
+
idField: "id", // Automatically excluded from insert/update
|
|
1191
|
+
required: ["status"], // Make status required despite being nullable
|
|
1192
|
+
readOnly: ["createdAt"], // Exclude createdAt from insert/update
|
|
858
1193
|
});
|
|
859
1194
|
|
|
860
|
-
// Insert
|
|
1195
|
+
// Insert: username, email, and status are required
|
|
1196
|
+
// Insert: id and createdAt are excluded (cannot be provided)
|
|
861
1197
|
db.from("users").insert({
|
|
862
1198
|
username: "john",
|
|
863
1199
|
email: "john@example.com",
|
|
864
|
-
//
|
|
1200
|
+
status: "active", // Required due to 'required' array
|
|
1201
|
+
updatedAt: new Date().toISOString(), // Optional
|
|
865
1202
|
});
|
|
866
1203
|
|
|
867
|
-
// Update
|
|
1204
|
+
// Update: all fields are optional except id and createdAt are excluded
|
|
868
1205
|
db.from("users")
|
|
869
1206
|
.update({
|
|
870
|
-
status: "active",
|
|
871
|
-
//
|
|
1207
|
+
status: "active", // Optional
|
|
1208
|
+
// id and createdAt cannot be modified
|
|
872
1209
|
})
|
|
873
1210
|
.byId("user-123");
|
|
874
1211
|
```
|
|
875
1212
|
|
|
1213
|
+
**Key Features:**
|
|
1214
|
+
|
|
1215
|
+
- **Auto-inference:** Non-nullable fields are automatically required for insert
|
|
1216
|
+
- **Additional requirements:** Use `required` to make nullable fields required for new records
|
|
1217
|
+
- **Read-only fields:** Use `readOnly` to exclude fields from insert/update (e.g., timestamps)
|
|
1218
|
+
- **Automatic ID exclusion:** The `idField` is always read-only without needing to specify it
|
|
1219
|
+
- **Update flexibility:** All fields are optional for updates (except read-only fields)
|
|
1220
|
+
|
|
1221
|
+
### Prefer: fmodata.entity-ids
|
|
1222
|
+
|
|
1223
|
+
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.
|
|
1224
|
+
|
|
1225
|
+
To enable this feature, simply define your schema with the `BaseTableWithIds` and `TableOccurrenceWithIds` classes. 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 the `TableOccurrenceWithIds` class.
|
|
1226
|
+
|
|
1227
|
+
_Note for OttoFMS proxy: This feature requires version 4.14 or later of OttoFMS_
|
|
1228
|
+
|
|
1229
|
+
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
|
|
1230
|
+
|
|
1231
|
+
#### Basic Usage
|
|
1232
|
+
|
|
1233
|
+
```typescript
|
|
1234
|
+
import { BaseTableWithIds, TableOccurrenceWithIds } from "@proofkit/fmodata";
|
|
1235
|
+
import { z } from "zod/v4";
|
|
1236
|
+
|
|
1237
|
+
// Define a base table with FileMaker field IDs
|
|
1238
|
+
const usersBase = new BaseTableWithIds({
|
|
1239
|
+
schema: {
|
|
1240
|
+
id: z.string(),
|
|
1241
|
+
username: z.string(),
|
|
1242
|
+
email: z.string().nullable(),
|
|
1243
|
+
createdAt: z.string(),
|
|
1244
|
+
},
|
|
1245
|
+
idField: "id",
|
|
1246
|
+
fmfIds: {
|
|
1247
|
+
id: "FMFID:12039485",
|
|
1248
|
+
username: "FMFID:34323433",
|
|
1249
|
+
email: "FMFID:12232424",
|
|
1250
|
+
createdAt: "FMFID:43234355",
|
|
1251
|
+
},
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
// Create a table occurrence with a FileMaker table occurrence ID
|
|
1255
|
+
const usersTO = new TableOccurrenceWithIds({
|
|
1256
|
+
name: "users",
|
|
1257
|
+
baseTable: usersBase, // Must be a BaseTableWithIds
|
|
1258
|
+
fmtId: "FMTID:12432533",
|
|
1259
|
+
});
|
|
1260
|
+
```
|
|
1261
|
+
|
|
876
1262
|
### Error Handling
|
|
877
1263
|
|
|
878
|
-
All operations return a `Result` type with either `data` or `error
|
|
1264
|
+
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.
|
|
1265
|
+
|
|
1266
|
+
#### Basic Error Checking
|
|
879
1267
|
|
|
880
1268
|
```typescript
|
|
881
1269
|
const result = await db.from("users").list().execute();
|
|
@@ -890,6 +1278,277 @@ if (result.data) {
|
|
|
890
1278
|
}
|
|
891
1279
|
```
|
|
892
1280
|
|
|
1281
|
+
#### HTTP Errors
|
|
1282
|
+
|
|
1283
|
+
Handle HTTP status codes (4xx, 5xx) with the `HTTPError` class:
|
|
1284
|
+
|
|
1285
|
+
```typescript
|
|
1286
|
+
import { HTTPError, isHTTPError } from "@proofkit/fmodata";
|
|
1287
|
+
|
|
1288
|
+
const result = await db.from("users").list().execute();
|
|
1289
|
+
|
|
1290
|
+
if (result.error) {
|
|
1291
|
+
if (isHTTPError(result.error)) {
|
|
1292
|
+
// TypeScript knows this is HTTPError
|
|
1293
|
+
console.log("HTTP Status:", result.error.status);
|
|
1294
|
+
|
|
1295
|
+
if (result.error.isNotFound()) {
|
|
1296
|
+
console.log("Resource not found");
|
|
1297
|
+
} else if (result.error.isUnauthorized()) {
|
|
1298
|
+
console.log("Authentication required");
|
|
1299
|
+
} else if (result.error.is5xx()) {
|
|
1300
|
+
console.log("Server error - try again later");
|
|
1301
|
+
} else if (result.error.is4xx()) {
|
|
1302
|
+
console.log("Client error:", result.error.statusText);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// Access the response body if available
|
|
1306
|
+
if (result.error.response) {
|
|
1307
|
+
console.log("Error details:", result.error.response);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
#### Network Errors
|
|
1314
|
+
|
|
1315
|
+
Handle network-level errors (timeouts, connection issues, etc.):
|
|
1316
|
+
|
|
1317
|
+
```typescript
|
|
1318
|
+
import {
|
|
1319
|
+
TimeoutError,
|
|
1320
|
+
NetworkError,
|
|
1321
|
+
RetryLimitError,
|
|
1322
|
+
CircuitOpenError,
|
|
1323
|
+
} from "@proofkit/fmodata";
|
|
1324
|
+
|
|
1325
|
+
const result = await db.from("users").list().execute();
|
|
1326
|
+
|
|
1327
|
+
if (result.error) {
|
|
1328
|
+
if (result.error instanceof TimeoutError) {
|
|
1329
|
+
console.log("Request timed out");
|
|
1330
|
+
// Show user-friendly timeout message
|
|
1331
|
+
} else if (result.error instanceof NetworkError) {
|
|
1332
|
+
console.log("Network connectivity issue");
|
|
1333
|
+
// Show offline message
|
|
1334
|
+
} else if (result.error instanceof RetryLimitError) {
|
|
1335
|
+
console.log("Request failed after retries");
|
|
1336
|
+
// Log the underlying error: result.error.cause
|
|
1337
|
+
} else if (result.error instanceof CircuitOpenError) {
|
|
1338
|
+
console.log("Service is currently unavailable");
|
|
1339
|
+
// Show maintenance message
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
```
|
|
1343
|
+
|
|
1344
|
+
#### Validation Errors
|
|
1345
|
+
|
|
1346
|
+
When schema validation fails, you get a `ValidationError` with rich context:
|
|
1347
|
+
|
|
1348
|
+
```typescript
|
|
1349
|
+
import { ValidationError, isValidationError } from "@proofkit/fmodata";
|
|
1350
|
+
|
|
1351
|
+
const result = await db.from("users").list().execute();
|
|
1352
|
+
|
|
1353
|
+
if (result.error) {
|
|
1354
|
+
if (isValidationError(result.error)) {
|
|
1355
|
+
// Access validation issues (Standard Schema format)
|
|
1356
|
+
console.log("Validation failed for field:", result.error.field);
|
|
1357
|
+
console.log("Issues:", result.error.issues);
|
|
1358
|
+
console.log("Failed value:", result.error.value);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
```
|
|
1362
|
+
|
|
1363
|
+
**Validator-Agnostic Error Handling**
|
|
1364
|
+
|
|
1365
|
+
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:
|
|
1366
|
+
|
|
1367
|
+
```typescript
|
|
1368
|
+
import { ValidationError } from "@proofkit/fmodata";
|
|
1369
|
+
|
|
1370
|
+
const result = await db.from("users").list().execute();
|
|
1371
|
+
|
|
1372
|
+
if (result.error instanceof ValidationError) {
|
|
1373
|
+
// The cause property (ES2022 Error.cause) contains the Standard Schema issues array
|
|
1374
|
+
// This is validator-agnostic and works with Zod, Valibot, ArkType, etc.
|
|
1375
|
+
console.log("Validation issues:", result.error.cause);
|
|
1376
|
+
console.log("Issues are also available directly:", result.error.issues);
|
|
1377
|
+
|
|
1378
|
+
// Both point to the same array
|
|
1379
|
+
console.log(result.error.cause === result.error.issues); // true
|
|
1380
|
+
|
|
1381
|
+
// Access additional context
|
|
1382
|
+
console.log("Failed field:", result.error.field);
|
|
1383
|
+
console.log("Failed value:", result.error.value);
|
|
1384
|
+
|
|
1385
|
+
// Standard Schema issues have a normalized format
|
|
1386
|
+
result.error.issues.forEach((issue) => {
|
|
1387
|
+
console.log("Path:", issue.path);
|
|
1388
|
+
console.log("Message:", issue.message);
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
```
|
|
1392
|
+
|
|
1393
|
+
**Why Standard Schema Issues Instead of Original Validator Errors?**
|
|
1394
|
+
|
|
1395
|
+
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.
|
|
1396
|
+
|
|
1397
|
+
If you need validator-specific error formatting, you can still access your validator's methods during validation before the data reaches fmodata:
|
|
1398
|
+
|
|
1399
|
+
```typescript
|
|
1400
|
+
import { z } from "zod";
|
|
1401
|
+
|
|
1402
|
+
const userSchema = z.object({
|
|
1403
|
+
email: z.string().email(),
|
|
1404
|
+
age: z.number().min(0).max(150),
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
// Validate early if you need Zod-specific error handling
|
|
1408
|
+
const parseResult = userSchema.safeParse(userData);
|
|
1409
|
+
if (!parseResult.success) {
|
|
1410
|
+
// Use Zod's error formatting
|
|
1411
|
+
const formatted = parseResult.error.flatten();
|
|
1412
|
+
console.log("Zod-specific formatting:", formatted);
|
|
1413
|
+
}
|
|
1414
|
+
```
|
|
1415
|
+
|
|
1416
|
+
#### OData Errors
|
|
1417
|
+
|
|
1418
|
+
Handle OData-specific protocol errors:
|
|
1419
|
+
|
|
1420
|
+
```typescript
|
|
1421
|
+
import { ODataError, isODataError } from "@proofkit/fmodata";
|
|
1422
|
+
|
|
1423
|
+
const result = await db.from("users").list().execute();
|
|
1424
|
+
|
|
1425
|
+
if (result.error) {
|
|
1426
|
+
if (isODataError(result.error)) {
|
|
1427
|
+
console.log("OData Error Code:", result.error.code);
|
|
1428
|
+
console.log("OData Error Message:", result.error.message);
|
|
1429
|
+
console.log("OData Error Details:", result.error.details);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
#### Error Handling Patterns
|
|
1435
|
+
|
|
1436
|
+
**Pattern 1: Using instanceof (like ffetch):**
|
|
1437
|
+
|
|
1438
|
+
```typescript
|
|
1439
|
+
import {
|
|
1440
|
+
HTTPError,
|
|
1441
|
+
ValidationError,
|
|
1442
|
+
TimeoutError,
|
|
1443
|
+
NetworkError,
|
|
1444
|
+
} from "@proofkit/fmodata";
|
|
1445
|
+
|
|
1446
|
+
const result = await db.from("users").list().execute();
|
|
1447
|
+
|
|
1448
|
+
if (result.error) {
|
|
1449
|
+
if (result.error instanceof TimeoutError) {
|
|
1450
|
+
showTimeoutMessage();
|
|
1451
|
+
} else if (result.error instanceof HTTPError) {
|
|
1452
|
+
if (result.error.isNotFound()) {
|
|
1453
|
+
showNotFoundMessage();
|
|
1454
|
+
} else if (result.error.is5xx()) {
|
|
1455
|
+
showServerErrorMessage();
|
|
1456
|
+
}
|
|
1457
|
+
} else if (result.error instanceof ValidationError) {
|
|
1458
|
+
showValidationError(result.error.field, result.error.issues);
|
|
1459
|
+
} else if (result.error instanceof NetworkError) {
|
|
1460
|
+
showOfflineMessage();
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
```
|
|
1464
|
+
|
|
1465
|
+
**Pattern 2: Using kind property (for exhaustive matching):**
|
|
1466
|
+
|
|
1467
|
+
```typescript
|
|
1468
|
+
const result = await db.from("users").list().execute();
|
|
1469
|
+
|
|
1470
|
+
if (result.error) {
|
|
1471
|
+
switch (result.error.kind) {
|
|
1472
|
+
case "TimeoutError":
|
|
1473
|
+
showTimeoutMessage();
|
|
1474
|
+
break;
|
|
1475
|
+
case "HTTPError":
|
|
1476
|
+
handleHTTPError(result.error.status);
|
|
1477
|
+
break;
|
|
1478
|
+
case "ValidationError":
|
|
1479
|
+
showValidationError(result.error.field, result.error.issues);
|
|
1480
|
+
break;
|
|
1481
|
+
case "NetworkError":
|
|
1482
|
+
showOfflineMessage();
|
|
1483
|
+
break;
|
|
1484
|
+
case "ODataError":
|
|
1485
|
+
handleODataError(result.error.code);
|
|
1486
|
+
break;
|
|
1487
|
+
// TypeScript ensures exhaustive matching!
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
```
|
|
1491
|
+
|
|
1492
|
+
**Pattern 3: Using type guards:**
|
|
1493
|
+
|
|
1494
|
+
```typescript
|
|
1495
|
+
import {
|
|
1496
|
+
isHTTPError,
|
|
1497
|
+
isValidationError,
|
|
1498
|
+
isODataError,
|
|
1499
|
+
isNetworkError,
|
|
1500
|
+
} 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("Status:", result.error.status);
|
|
1508
|
+
} else if (isValidationError(result.error)) {
|
|
1509
|
+
// TypeScript knows this is ValidationError
|
|
1510
|
+
console.log("Field:", result.error.field);
|
|
1511
|
+
console.log("Issues:", result.error.issues);
|
|
1512
|
+
} else if (isODataError(result.error)) {
|
|
1513
|
+
// TypeScript knows this is ODataError
|
|
1514
|
+
console.log("Code:", result.error.code);
|
|
1515
|
+
} else if (isNetworkError(result.error)) {
|
|
1516
|
+
// TypeScript knows this is NetworkError
|
|
1517
|
+
console.log("Network issue:", result.error.cause);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
```
|
|
1521
|
+
|
|
1522
|
+
#### Error Properties
|
|
1523
|
+
|
|
1524
|
+
All errors include helpful metadata:
|
|
1525
|
+
|
|
1526
|
+
```typescript
|
|
1527
|
+
if (result.error) {
|
|
1528
|
+
// All errors have a timestamp
|
|
1529
|
+
console.log("Error occurred at:", result.error.timestamp);
|
|
1530
|
+
|
|
1531
|
+
// All errors have a kind property for discriminated unions
|
|
1532
|
+
console.log("Error kind:", result.error.kind);
|
|
1533
|
+
|
|
1534
|
+
// All errors have a message
|
|
1535
|
+
console.log("Error message:", result.error.message);
|
|
1536
|
+
}
|
|
1537
|
+
```
|
|
1538
|
+
|
|
1539
|
+
#### Available Error Types
|
|
1540
|
+
|
|
1541
|
+
- **`HTTPError`** - HTTP status errors (4xx, 5xx) with helper methods (`is4xx()`, `is5xx()`, `isNotFound()`, etc.)
|
|
1542
|
+
- **`ODataError`** - OData protocol errors with code and details
|
|
1543
|
+
- **`ValidationError`** - Schema validation failures with issues, schema reference, and failed value
|
|
1544
|
+
- **`ResponseStructureError`** - Malformed API responses
|
|
1545
|
+
- **`RecordCountMismatchError`** - When `single()` or `maybeSingle()` expectations aren't met
|
|
1546
|
+
- **`TimeoutError`** - Request timeout (from ffetch)
|
|
1547
|
+
- **`NetworkError`** - Network connectivity issues (from ffetch)
|
|
1548
|
+
- **`RetryLimitError`** - Request failed after retries (from ffetch)
|
|
1549
|
+
- **`CircuitOpenError`** - Circuit breaker is open (from ffetch)
|
|
1550
|
+
- **`AbortError`** - Request was aborted (from ffetch)
|
|
1551
|
+
|
|
893
1552
|
### OData Annotations and Validation
|
|
894
1553
|
|
|
895
1554
|
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`:
|