@proofkit/fmodata 0.1.0-alpha.3 → 0.1.0-alpha.6
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 +357 -28
- 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/database.d.ts +20 -3
- package/dist/esm/client/database.js +62 -13
- package/dist/esm/client/database.js.map +1 -1
- package/dist/esm/client/delete-builder.js +24 -27
- package/dist/esm/client/delete-builder.js.map +1 -1
- package/dist/esm/client/entity-set.d.ts +9 -6
- package/dist/esm/client/entity-set.js +5 -1
- package/dist/esm/client/entity-set.js.map +1 -1
- package/dist/esm/client/filemaker-odata.d.ts +17 -4
- package/dist/esm/client/filemaker-odata.js +90 -27
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/dist/esm/client/insert-builder.js +45 -34
- package/dist/esm/client/insert-builder.js.map +1 -1
- package/dist/esm/client/query-builder.d.ts +7 -2
- package/dist/esm/client/query-builder.js +273 -202
- package/dist/esm/client/query-builder.js.map +1 -1
- package/dist/esm/client/record-builder.d.ts +2 -2
- package/dist/esm/client/record-builder.js +50 -40
- package/dist/esm/client/record-builder.js.map +1 -1
- 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.js +39 -35
- package/dist/esm/client/update-builder.js.map +1 -1
- package/dist/esm/errors.d.ts +60 -0
- package/dist/esm/errors.js +122 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.d.ts +6 -3
- package/dist/esm/index.js +26 -4
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/transform.d.ts +56 -0
- package/dist/esm/transform.js +107 -0
- package/dist/esm/transform.js.map +1 -0
- package/dist/esm/types.d.ts +21 -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/database.ts +116 -13
- package/src/client/delete-builder.ts +42 -43
- package/src/client/entity-set.ts +21 -11
- package/src/client/filemaker-odata.ts +132 -34
- package/src/client/insert-builder.ts +69 -37
- package/src/client/query-builder.ts +345 -233
- package/src/client/record-builder.ts +84 -59
- package/src/client/table-occurrence.ts +118 -4
- package/src/client/update-builder.ts +77 -49
- package/src/errors.ts +185 -0
- package/src/index.ts +31 -2
- package/src/transform.ts +236 -0
- package/src/types.ts +112 -34
- package/src/validation.ts +120 -36
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ A strongly-typed FileMaker OData API client.
|
|
|
6
6
|
|
|
7
7
|
Roadmap:
|
|
8
8
|
|
|
9
|
+
- [ ] Crossjoin support
|
|
9
10
|
- [ ] Batch operations
|
|
10
11
|
- [ ] Proper docs at proofkit.dev
|
|
11
12
|
- [ ] @proofkit/typegen integration
|
|
@@ -140,9 +141,9 @@ const contactsBase = new BaseTable({
|
|
|
140
141
|
phone: z.string().optional(),
|
|
141
142
|
createdAt: z.string(),
|
|
142
143
|
},
|
|
143
|
-
idField: "id", // The primary key field
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
idField: "id", // The primary key field (automatically read-only)
|
|
145
|
+
required: ["phone"], // optional: additional required fields for insert (beyond auto-inferred)
|
|
146
|
+
readOnly: ["createdAt"], // optional: fields excluded from insert/update
|
|
146
147
|
});
|
|
147
148
|
```
|
|
148
149
|
|
|
@@ -524,27 +525,30 @@ if (result.data) {
|
|
|
524
525
|
}
|
|
525
526
|
```
|
|
526
527
|
|
|
527
|
-
|
|
528
|
+
Fields are automatically required for insert if their validator doesn't allow `null` or `undefined`. You can specify additional required fields:
|
|
528
529
|
|
|
529
530
|
```typescript
|
|
530
531
|
const usersBase = new BaseTable({
|
|
531
532
|
schema: {
|
|
532
|
-
id: z.string(),
|
|
533
|
-
username: z.string(),
|
|
534
|
-
email: z.string(),
|
|
535
|
-
|
|
533
|
+
id: z.string(), // Auto-required (not nullable), but excluded from insert (idField)
|
|
534
|
+
username: z.string(), // Auto-required (not nullable)
|
|
535
|
+
email: z.string(), // Auto-required (not nullable)
|
|
536
|
+
phone: z.string().nullable(), // Optional by default
|
|
537
|
+
createdAt: z.string(), // Auto-required, but excluded (readOnly)
|
|
536
538
|
},
|
|
537
|
-
idField: "id",
|
|
538
|
-
|
|
539
|
+
idField: "id", // Automatically excluded from insert/update
|
|
540
|
+
required: ["phone"], // Make phone required for inserts despite being nullable
|
|
541
|
+
readOnly: ["createdAt"], // Exclude from insert/update operations
|
|
539
542
|
});
|
|
540
543
|
|
|
541
|
-
// TypeScript
|
|
544
|
+
// TypeScript enforces: username, email, and phone are required
|
|
545
|
+
// TypeScript excludes: id and createdAt cannot be provided
|
|
542
546
|
const result = await db
|
|
543
547
|
.from("users")
|
|
544
548
|
.insert({
|
|
545
549
|
username: "johndoe",
|
|
546
550
|
email: "john@example.com",
|
|
547
|
-
//
|
|
551
|
+
phone: "+1234567890", // Required because specified in 'required' array
|
|
548
552
|
})
|
|
549
553
|
.execute();
|
|
550
554
|
```
|
|
@@ -839,43 +843,97 @@ db.from("users")
|
|
|
839
843
|
.filter({ invalid: { eq: "john" } }); // TS Error
|
|
840
844
|
```
|
|
841
845
|
|
|
842
|
-
### Required Fields
|
|
846
|
+
### Required and Read-Only Fields
|
|
843
847
|
|
|
844
|
-
|
|
848
|
+
The library automatically infers which fields are required based on whether their validator allows `null` or `undefined`:
|
|
845
849
|
|
|
846
850
|
```typescript
|
|
847
851
|
const usersBase = new BaseTable({
|
|
848
852
|
schema: {
|
|
849
|
-
id: z.string(),
|
|
850
|
-
username: z.string(),
|
|
851
|
-
email: z.string(),
|
|
852
|
-
status: z.string(),
|
|
853
|
-
|
|
853
|
+
id: z.string(), // Auto-required, auto-readOnly (idField)
|
|
854
|
+
username: z.string(), // Auto-required (not nullable)
|
|
855
|
+
email: z.string(), // Auto-required (not nullable)
|
|
856
|
+
status: z.string().nullable(), // Optional (nullable)
|
|
857
|
+
createdAt: z.string(), // Read-only system field
|
|
858
|
+
updatedAt: z.string().nullable(), // Optional
|
|
854
859
|
},
|
|
855
|
-
idField: "id",
|
|
856
|
-
|
|
857
|
-
|
|
860
|
+
idField: "id", // Automatically excluded from insert/update
|
|
861
|
+
required: ["status"], // Make status required despite being nullable
|
|
862
|
+
readOnly: ["createdAt"], // Exclude createdAt from insert/update
|
|
858
863
|
});
|
|
859
864
|
|
|
860
|
-
// Insert
|
|
865
|
+
// Insert: username, email, and status are required
|
|
866
|
+
// Insert: id and createdAt are excluded (cannot be provided)
|
|
861
867
|
db.from("users").insert({
|
|
862
868
|
username: "john",
|
|
863
869
|
email: "john@example.com",
|
|
864
|
-
//
|
|
870
|
+
status: "active", // Required due to 'required' array
|
|
871
|
+
updatedAt: new Date().toISOString(), // Optional
|
|
865
872
|
});
|
|
866
873
|
|
|
867
|
-
// Update
|
|
874
|
+
// Update: all fields are optional except id and createdAt are excluded
|
|
868
875
|
db.from("users")
|
|
869
876
|
.update({
|
|
870
|
-
status: "active",
|
|
871
|
-
//
|
|
877
|
+
status: "active", // Optional
|
|
878
|
+
// id and createdAt cannot be modified
|
|
872
879
|
})
|
|
873
880
|
.byId("user-123");
|
|
874
881
|
```
|
|
875
882
|
|
|
883
|
+
**Key Features:**
|
|
884
|
+
|
|
885
|
+
- **Auto-inference:** Non-nullable fields are automatically required for insert
|
|
886
|
+
- **Additional requirements:** Use `required` to make nullable fields required for new records
|
|
887
|
+
- **Read-only fields:** Use `readOnly` to exclude fields from insert/update (e.g., timestamps)
|
|
888
|
+
- **Automatic ID exclusion:** The `idField` is always read-only without needing to specify it
|
|
889
|
+
- **Update flexibility:** All fields are optional for updates (except read-only fields)
|
|
890
|
+
|
|
891
|
+
### Prefer: fmodata.entity-ids
|
|
892
|
+
|
|
893
|
+
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.
|
|
894
|
+
|
|
895
|
+
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.
|
|
896
|
+
|
|
897
|
+
_Note for OttoFMS proxy: This feature requires version 4.14 or later of OttoFMS_
|
|
898
|
+
|
|
899
|
+
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
|
|
900
|
+
|
|
901
|
+
#### Basic Usage
|
|
902
|
+
|
|
903
|
+
```typescript
|
|
904
|
+
import { BaseTableWithIds, TableOccurrenceWithIds } from "@proofkit/fmodata";
|
|
905
|
+
import { z } from "zod/v4";
|
|
906
|
+
|
|
907
|
+
// Define a base table with FileMaker field IDs
|
|
908
|
+
const usersBase = new BaseTableWithIds({
|
|
909
|
+
schema: {
|
|
910
|
+
id: z.string(),
|
|
911
|
+
username: z.string(),
|
|
912
|
+
email: z.string().nullable(),
|
|
913
|
+
createdAt: z.string(),
|
|
914
|
+
},
|
|
915
|
+
idField: "id",
|
|
916
|
+
fmfIds: {
|
|
917
|
+
id: "FMFID:12039485",
|
|
918
|
+
username: "FMFID:34323433",
|
|
919
|
+
email: "FMFID:12232424",
|
|
920
|
+
createdAt: "FMFID:43234355",
|
|
921
|
+
},
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// Create a table occurrence with a FileMaker table occurrence ID
|
|
925
|
+
const usersTO = new TableOccurrenceWithIds({
|
|
926
|
+
name: "users",
|
|
927
|
+
baseTable: usersBase, // Must be a BaseTableWithIds
|
|
928
|
+
fmtId: "FMTID:12432533",
|
|
929
|
+
});
|
|
930
|
+
```
|
|
931
|
+
|
|
876
932
|
### Error Handling
|
|
877
933
|
|
|
878
|
-
All operations return a `Result` type with either `data` or `error
|
|
934
|
+
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.
|
|
935
|
+
|
|
936
|
+
#### Basic Error Checking
|
|
879
937
|
|
|
880
938
|
```typescript
|
|
881
939
|
const result = await db.from("users").list().execute();
|
|
@@ -890,6 +948,277 @@ if (result.data) {
|
|
|
890
948
|
}
|
|
891
949
|
```
|
|
892
950
|
|
|
951
|
+
#### HTTP Errors
|
|
952
|
+
|
|
953
|
+
Handle HTTP status codes (4xx, 5xx) with the `HTTPError` class:
|
|
954
|
+
|
|
955
|
+
```typescript
|
|
956
|
+
import { HTTPError, isHTTPError } from "@proofkit/fmodata";
|
|
957
|
+
|
|
958
|
+
const result = await db.from("users").list().execute();
|
|
959
|
+
|
|
960
|
+
if (result.error) {
|
|
961
|
+
if (isHTTPError(result.error)) {
|
|
962
|
+
// TypeScript knows this is HTTPError
|
|
963
|
+
console.log("HTTP Status:", result.error.status);
|
|
964
|
+
|
|
965
|
+
if (result.error.isNotFound()) {
|
|
966
|
+
console.log("Resource not found");
|
|
967
|
+
} else if (result.error.isUnauthorized()) {
|
|
968
|
+
console.log("Authentication required");
|
|
969
|
+
} else if (result.error.is5xx()) {
|
|
970
|
+
console.log("Server error - try again later");
|
|
971
|
+
} else if (result.error.is4xx()) {
|
|
972
|
+
console.log("Client error:", result.error.statusText);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Access the response body if available
|
|
976
|
+
if (result.error.response) {
|
|
977
|
+
console.log("Error details:", result.error.response);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
#### Network Errors
|
|
984
|
+
|
|
985
|
+
Handle network-level errors (timeouts, connection issues, etc.):
|
|
986
|
+
|
|
987
|
+
```typescript
|
|
988
|
+
import {
|
|
989
|
+
TimeoutError,
|
|
990
|
+
NetworkError,
|
|
991
|
+
RetryLimitError,
|
|
992
|
+
CircuitOpenError,
|
|
993
|
+
} from "@proofkit/fmodata";
|
|
994
|
+
|
|
995
|
+
const result = await db.from("users").list().execute();
|
|
996
|
+
|
|
997
|
+
if (result.error) {
|
|
998
|
+
if (result.error instanceof TimeoutError) {
|
|
999
|
+
console.log("Request timed out");
|
|
1000
|
+
// Show user-friendly timeout message
|
|
1001
|
+
} else if (result.error instanceof NetworkError) {
|
|
1002
|
+
console.log("Network connectivity issue");
|
|
1003
|
+
// Show offline message
|
|
1004
|
+
} else if (result.error instanceof RetryLimitError) {
|
|
1005
|
+
console.log("Request failed after retries");
|
|
1006
|
+
// Log the underlying error: result.error.cause
|
|
1007
|
+
} else if (result.error instanceof CircuitOpenError) {
|
|
1008
|
+
console.log("Service is currently unavailable");
|
|
1009
|
+
// Show maintenance message
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
#### Validation Errors
|
|
1015
|
+
|
|
1016
|
+
When schema validation fails, you get a `ValidationError` with rich context:
|
|
1017
|
+
|
|
1018
|
+
```typescript
|
|
1019
|
+
import { ValidationError, isValidationError } from "@proofkit/fmodata";
|
|
1020
|
+
|
|
1021
|
+
const result = await db.from("users").list().execute();
|
|
1022
|
+
|
|
1023
|
+
if (result.error) {
|
|
1024
|
+
if (isValidationError(result.error)) {
|
|
1025
|
+
// Access validation issues (Standard Schema format)
|
|
1026
|
+
console.log("Validation failed for field:", result.error.field);
|
|
1027
|
+
console.log("Issues:", result.error.issues);
|
|
1028
|
+
console.log("Failed value:", result.error.value);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
**Validator-Agnostic Error Handling**
|
|
1034
|
+
|
|
1035
|
+
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:
|
|
1036
|
+
|
|
1037
|
+
```typescript
|
|
1038
|
+
import { ValidationError } from "@proofkit/fmodata";
|
|
1039
|
+
|
|
1040
|
+
const result = await db.from("users").list().execute();
|
|
1041
|
+
|
|
1042
|
+
if (result.error instanceof ValidationError) {
|
|
1043
|
+
// The cause property (ES2022 Error.cause) contains the Standard Schema issues array
|
|
1044
|
+
// This is validator-agnostic and works with Zod, Valibot, ArkType, etc.
|
|
1045
|
+
console.log("Validation issues:", result.error.cause);
|
|
1046
|
+
console.log("Issues are also available directly:", result.error.issues);
|
|
1047
|
+
|
|
1048
|
+
// Both point to the same array
|
|
1049
|
+
console.log(result.error.cause === result.error.issues); // true
|
|
1050
|
+
|
|
1051
|
+
// Access additional context
|
|
1052
|
+
console.log("Failed field:", result.error.field);
|
|
1053
|
+
console.log("Failed value:", result.error.value);
|
|
1054
|
+
|
|
1055
|
+
// Standard Schema issues have a normalized format
|
|
1056
|
+
result.error.issues.forEach((issue) => {
|
|
1057
|
+
console.log("Path:", issue.path);
|
|
1058
|
+
console.log("Message:", issue.message);
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
**Why Standard Schema Issues Instead of Original Validator Errors?**
|
|
1064
|
+
|
|
1065
|
+
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.
|
|
1066
|
+
|
|
1067
|
+
If you need validator-specific error formatting, you can still access your validator's methods during validation before the data reaches fmodata:
|
|
1068
|
+
|
|
1069
|
+
```typescript
|
|
1070
|
+
import { z } from "zod";
|
|
1071
|
+
|
|
1072
|
+
const userSchema = z.object({
|
|
1073
|
+
email: z.string().email(),
|
|
1074
|
+
age: z.number().min(0).max(150),
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
// Validate early if you need Zod-specific error handling
|
|
1078
|
+
const parseResult = userSchema.safeParse(userData);
|
|
1079
|
+
if (!parseResult.success) {
|
|
1080
|
+
// Use Zod's error formatting
|
|
1081
|
+
const formatted = parseResult.error.flatten();
|
|
1082
|
+
console.log("Zod-specific formatting:", formatted);
|
|
1083
|
+
}
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
#### OData Errors
|
|
1087
|
+
|
|
1088
|
+
Handle OData-specific protocol errors:
|
|
1089
|
+
|
|
1090
|
+
```typescript
|
|
1091
|
+
import { ODataError, isODataError } from "@proofkit/fmodata";
|
|
1092
|
+
|
|
1093
|
+
const result = await db.from("users").list().execute();
|
|
1094
|
+
|
|
1095
|
+
if (result.error) {
|
|
1096
|
+
if (isODataError(result.error)) {
|
|
1097
|
+
console.log("OData Error Code:", result.error.code);
|
|
1098
|
+
console.log("OData Error Message:", result.error.message);
|
|
1099
|
+
console.log("OData Error Details:", result.error.details);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
#### Error Handling Patterns
|
|
1105
|
+
|
|
1106
|
+
**Pattern 1: Using instanceof (like ffetch):**
|
|
1107
|
+
|
|
1108
|
+
```typescript
|
|
1109
|
+
import {
|
|
1110
|
+
HTTPError,
|
|
1111
|
+
ValidationError,
|
|
1112
|
+
TimeoutError,
|
|
1113
|
+
NetworkError,
|
|
1114
|
+
} from "@proofkit/fmodata";
|
|
1115
|
+
|
|
1116
|
+
const result = await db.from("users").list().execute();
|
|
1117
|
+
|
|
1118
|
+
if (result.error) {
|
|
1119
|
+
if (result.error instanceof TimeoutError) {
|
|
1120
|
+
showTimeoutMessage();
|
|
1121
|
+
} else if (result.error instanceof HTTPError) {
|
|
1122
|
+
if (result.error.isNotFound()) {
|
|
1123
|
+
showNotFoundMessage();
|
|
1124
|
+
} else if (result.error.is5xx()) {
|
|
1125
|
+
showServerErrorMessage();
|
|
1126
|
+
}
|
|
1127
|
+
} else if (result.error instanceof ValidationError) {
|
|
1128
|
+
showValidationError(result.error.field, result.error.issues);
|
|
1129
|
+
} else if (result.error instanceof NetworkError) {
|
|
1130
|
+
showOfflineMessage();
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
**Pattern 2: Using kind property (for exhaustive matching):**
|
|
1136
|
+
|
|
1137
|
+
```typescript
|
|
1138
|
+
const result = await db.from("users").list().execute();
|
|
1139
|
+
|
|
1140
|
+
if (result.error) {
|
|
1141
|
+
switch (result.error.kind) {
|
|
1142
|
+
case "TimeoutError":
|
|
1143
|
+
showTimeoutMessage();
|
|
1144
|
+
break;
|
|
1145
|
+
case "HTTPError":
|
|
1146
|
+
handleHTTPError(result.error.status);
|
|
1147
|
+
break;
|
|
1148
|
+
case "ValidationError":
|
|
1149
|
+
showValidationError(result.error.field, result.error.issues);
|
|
1150
|
+
break;
|
|
1151
|
+
case "NetworkError":
|
|
1152
|
+
showOfflineMessage();
|
|
1153
|
+
break;
|
|
1154
|
+
case "ODataError":
|
|
1155
|
+
handleODataError(result.error.code);
|
|
1156
|
+
break;
|
|
1157
|
+
// TypeScript ensures exhaustive matching!
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
**Pattern 3: Using type guards:**
|
|
1163
|
+
|
|
1164
|
+
```typescript
|
|
1165
|
+
import {
|
|
1166
|
+
isHTTPError,
|
|
1167
|
+
isValidationError,
|
|
1168
|
+
isODataError,
|
|
1169
|
+
isNetworkError,
|
|
1170
|
+
} from "@proofkit/fmodata";
|
|
1171
|
+
|
|
1172
|
+
const result = await db.from("users").list().execute();
|
|
1173
|
+
|
|
1174
|
+
if (result.error) {
|
|
1175
|
+
if (isHTTPError(result.error)) {
|
|
1176
|
+
// TypeScript knows this is HTTPError
|
|
1177
|
+
console.log("Status:", result.error.status);
|
|
1178
|
+
} else if (isValidationError(result.error)) {
|
|
1179
|
+
// TypeScript knows this is ValidationError
|
|
1180
|
+
console.log("Field:", result.error.field);
|
|
1181
|
+
console.log("Issues:", result.error.issues);
|
|
1182
|
+
} else if (isODataError(result.error)) {
|
|
1183
|
+
// TypeScript knows this is ODataError
|
|
1184
|
+
console.log("Code:", result.error.code);
|
|
1185
|
+
} else if (isNetworkError(result.error)) {
|
|
1186
|
+
// TypeScript knows this is NetworkError
|
|
1187
|
+
console.log("Network issue:", result.error.cause);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
#### Error Properties
|
|
1193
|
+
|
|
1194
|
+
All errors include helpful metadata:
|
|
1195
|
+
|
|
1196
|
+
```typescript
|
|
1197
|
+
if (result.error) {
|
|
1198
|
+
// All errors have a timestamp
|
|
1199
|
+
console.log("Error occurred at:", result.error.timestamp);
|
|
1200
|
+
|
|
1201
|
+
// All errors have a kind property for discriminated unions
|
|
1202
|
+
console.log("Error kind:", result.error.kind);
|
|
1203
|
+
|
|
1204
|
+
// All errors have a message
|
|
1205
|
+
console.log("Error message:", result.error.message);
|
|
1206
|
+
}
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
#### Available Error Types
|
|
1210
|
+
|
|
1211
|
+
- **`HTTPError`** - HTTP status errors (4xx, 5xx) with helper methods (`is4xx()`, `is5xx()`, `isNotFound()`, etc.)
|
|
1212
|
+
- **`ODataError`** - OData protocol errors with code and details
|
|
1213
|
+
- **`ValidationError`** - Schema validation failures with issues, schema reference, and failed value
|
|
1214
|
+
- **`ResponseStructureError`** - Malformed API responses
|
|
1215
|
+
- **`RecordCountMismatchError`** - When `single()` or `maybeSingle()` expectations aren't met
|
|
1216
|
+
- **`TimeoutError`** - Request timeout (from ffetch)
|
|
1217
|
+
- **`NetworkError`** - Network connectivity issues (from ffetch)
|
|
1218
|
+
- **`RetryLimitError`** - Request failed after retries (from ffetch)
|
|
1219
|
+
- **`CircuitOpenError`** - Circuit breaker is open (from ffetch)
|
|
1220
|
+
- **`AbortError`** - Request was aborted (from ffetch)
|
|
1221
|
+
|
|
893
1222
|
### OData Annotations and Validation
|
|
894
1223
|
|
|
895
1224
|
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`:
|
|
@@ -1,13 +1,130 @@
|
|
|
1
1
|
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* BaseTable defines the schema and configuration for a table.
|
|
4
|
+
*
|
|
5
|
+
* @template Schema - Record of field names to StandardSchemaV1 validators
|
|
6
|
+
* @template IdField - The name of the primary key field (optional, automatically read-only)
|
|
7
|
+
* @template Required - Additional field names to require on insert (beyond auto-inferred required fields)
|
|
8
|
+
* @template ReadOnly - Field names that cannot be modified via insert/update (idField is automatically read-only)
|
|
9
|
+
*
|
|
10
|
+
* @example Basic table with auto-inferred required fields
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { z } from "zod";
|
|
13
|
+
*
|
|
14
|
+
* const usersTable = new BaseTable({
|
|
15
|
+
* schema: {
|
|
16
|
+
* id: z.string(), // Auto-required (not nullable), auto-readOnly (idField)
|
|
17
|
+
* name: z.string(), // Auto-required (not nullable)
|
|
18
|
+
* email: z.string().nullable(), // Optional (nullable)
|
|
19
|
+
* },
|
|
20
|
+
* idField: "id",
|
|
21
|
+
* });
|
|
22
|
+
* // On insert: name is required, email is optional (id is excluded - readOnly)
|
|
23
|
+
* // On update: name and email available (id is excluded - readOnly)
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example Table with additional required and readOnly fields
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { z } from "zod";
|
|
29
|
+
*
|
|
30
|
+
* const usersTable = new BaseTable({
|
|
31
|
+
* schema: {
|
|
32
|
+
* id: z.string(), // Auto-required, auto-readOnly (idField)
|
|
33
|
+
* createdAt: z.string(), // Read-only system field
|
|
34
|
+
* name: z.string(), // Auto-required
|
|
35
|
+
* email: z.string().nullable(), // Optional by default...
|
|
36
|
+
* legacyField: z.string().nullable(), // Optional by default...
|
|
37
|
+
* },
|
|
38
|
+
* idField: "id",
|
|
39
|
+
* required: ["legacyField"], // Make legacyField required for new inserts
|
|
40
|
+
* readOnly: ["createdAt"], // Exclude from insert/update
|
|
41
|
+
* });
|
|
42
|
+
* // On insert: name and legacyField required; email optional (id and createdAt excluded)
|
|
43
|
+
* // On update: all fields optional (id and createdAt excluded)
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @example Table with multiple read-only fields
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { z } from "zod";
|
|
49
|
+
*
|
|
50
|
+
* const usersTable = new BaseTable({
|
|
51
|
+
* schema: {
|
|
52
|
+
* id: z.string(),
|
|
53
|
+
* createdAt: z.string(),
|
|
54
|
+
* modifiedAt: z.string(),
|
|
55
|
+
* createdBy: z.string(),
|
|
56
|
+
* notes: z.string().nullable(),
|
|
57
|
+
* },
|
|
58
|
+
* idField: "id",
|
|
59
|
+
* readOnly: ["createdAt", "modifiedAt", "createdBy"],
|
|
60
|
+
* });
|
|
61
|
+
* // On insert/update: only notes is available (id and system fields excluded)
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare class BaseTable<Schema extends Record<string, StandardSchemaV1> = any, IdField extends keyof Schema | undefined = undefined, Required extends readonly (keyof Schema)[] = readonly [], ReadOnly extends readonly (keyof Schema)[] = readonly []> {
|
|
3
65
|
readonly schema: Schema;
|
|
4
66
|
readonly idField?: IdField;
|
|
5
|
-
readonly
|
|
6
|
-
readonly
|
|
67
|
+
readonly required?: Required;
|
|
68
|
+
readonly readOnly?: ReadOnly;
|
|
69
|
+
readonly fmfIds?: Record<keyof Schema, `FMFID:${string}`>;
|
|
7
70
|
constructor(config: {
|
|
8
71
|
schema: Schema;
|
|
9
72
|
idField?: IdField;
|
|
10
|
-
|
|
11
|
-
|
|
73
|
+
required?: Required;
|
|
74
|
+
readOnly?: ReadOnly;
|
|
75
|
+
});
|
|
76
|
+
/**
|
|
77
|
+
* Returns the FileMaker field ID (FMFID) for a given field name, or the field name itself if not using IDs.
|
|
78
|
+
* @param fieldName - The field name to get the ID for
|
|
79
|
+
* @returns The FMFID string or the original field name
|
|
80
|
+
*/
|
|
81
|
+
getFieldId(fieldName: keyof Schema): string;
|
|
82
|
+
/**
|
|
83
|
+
* Returns the field name for a given FileMaker field ID (FMFID), or the ID itself if not found.
|
|
84
|
+
* @param fieldId - The FMFID to get the field name for
|
|
85
|
+
* @returns The field name or the original ID
|
|
86
|
+
*/
|
|
87
|
+
getFieldName(fieldId: string): string;
|
|
88
|
+
/**
|
|
89
|
+
* Returns true if this BaseTable is using FileMaker field IDs.
|
|
90
|
+
*/
|
|
91
|
+
isUsingFieldIds(): boolean;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* BaseTableWithIds extends BaseTable to require FileMaker field IDs (fmfIds).
|
|
95
|
+
* Use this class when you need to work with FileMaker's internal field identifiers.
|
|
96
|
+
*
|
|
97
|
+
* @template Schema - Record of field names to StandardSchemaV1 validators
|
|
98
|
+
* @template IdField - The name of the primary key field (optional, automatically read-only)
|
|
99
|
+
* @template Required - Additional field names to require on insert (beyond auto-inferred required fields)
|
|
100
|
+
* @template ReadOnly - Field names that cannot be modified via insert/update (idField is automatically read-only)
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* import { z } from "zod";
|
|
105
|
+
*
|
|
106
|
+
* const usersTableWithIds = new BaseTableWithIds({
|
|
107
|
+
* schema: {
|
|
108
|
+
* id: z.string(),
|
|
109
|
+
* name: z.string(),
|
|
110
|
+
* email: z.string().nullable(),
|
|
111
|
+
* },
|
|
112
|
+
* idField: "id",
|
|
113
|
+
* fmfIds: {
|
|
114
|
+
* id: "FMFID:1",
|
|
115
|
+
* name: "FMFID:2",
|
|
116
|
+
* email: "FMFID:3",
|
|
117
|
+
* },
|
|
118
|
+
* });
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export declare class BaseTableWithIds<Schema extends Record<string, StandardSchemaV1> = any, IdField extends keyof Schema | undefined = undefined, Required extends readonly (keyof Schema)[] = readonly [], ReadOnly extends readonly (keyof Schema)[] = readonly []> extends BaseTable<Schema, IdField, Required, ReadOnly> {
|
|
122
|
+
readonly fmfIds: Record<keyof Schema, `FMFID:${string}`>;
|
|
123
|
+
constructor(config: {
|
|
124
|
+
schema: Schema;
|
|
125
|
+
fmfIds: Record<keyof Schema, `FMFID:${string}`>;
|
|
126
|
+
idField?: IdField;
|
|
127
|
+
required?: Required;
|
|
128
|
+
readOnly?: ReadOnly;
|
|
12
129
|
});
|
|
13
130
|
}
|
|
@@ -5,15 +5,56 @@ class BaseTable {
|
|
|
5
5
|
constructor(config) {
|
|
6
6
|
__publicField(this, "schema");
|
|
7
7
|
__publicField(this, "idField");
|
|
8
|
-
__publicField(this, "
|
|
9
|
-
__publicField(this, "
|
|
8
|
+
__publicField(this, "required");
|
|
9
|
+
__publicField(this, "readOnly");
|
|
10
|
+
__publicField(this, "fmfIds");
|
|
10
11
|
this.schema = config.schema;
|
|
11
12
|
this.idField = config.idField;
|
|
12
|
-
this.
|
|
13
|
-
this.
|
|
13
|
+
this.required = config.required;
|
|
14
|
+
this.readOnly = config.readOnly;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Returns the FileMaker field ID (FMFID) for a given field name, or the field name itself if not using IDs.
|
|
18
|
+
* @param fieldName - The field name to get the ID for
|
|
19
|
+
* @returns The FMFID string or the original field name
|
|
20
|
+
*/
|
|
21
|
+
getFieldId(fieldName) {
|
|
22
|
+
if (this.fmfIds && fieldName in this.fmfIds) {
|
|
23
|
+
return this.fmfIds[fieldName];
|
|
24
|
+
}
|
|
25
|
+
return String(fieldName);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Returns the field name for a given FileMaker field ID (FMFID), or the ID itself if not found.
|
|
29
|
+
* @param fieldId - The FMFID to get the field name for
|
|
30
|
+
* @returns The field name or the original ID
|
|
31
|
+
*/
|
|
32
|
+
getFieldName(fieldId) {
|
|
33
|
+
if (this.fmfIds) {
|
|
34
|
+
for (const [fieldName, fmfId] of Object.entries(this.fmfIds)) {
|
|
35
|
+
if (fmfId === fieldId) {
|
|
36
|
+
return fieldName;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return fieldId;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Returns true if this BaseTable is using FileMaker field IDs.
|
|
44
|
+
*/
|
|
45
|
+
isUsingFieldIds() {
|
|
46
|
+
return this.fmfIds !== void 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
class BaseTableWithIds extends BaseTable {
|
|
50
|
+
constructor(config) {
|
|
51
|
+
super(config);
|
|
52
|
+
__publicField(this, "fmfIds");
|
|
53
|
+
this.fmfIds = config.fmfIds;
|
|
14
54
|
}
|
|
15
55
|
}
|
|
16
56
|
export {
|
|
17
|
-
BaseTable
|
|
57
|
+
BaseTable,
|
|
58
|
+
BaseTableWithIds
|
|
18
59
|
};
|
|
19
60
|
//# sourceMappingURL=base-table.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base-table.js","sources":["../../../src/client/base-table.ts"],"sourcesContent":["import { StandardSchemaV1 } from \"@standard-schema/spec\";\n\nexport class BaseTable<\n Schema extends Record<string, StandardSchemaV1> = any,\n IdField extends keyof Schema | undefined = undefined,\n
|
|
1
|
+
{"version":3,"file":"base-table.js","sources":["../../../src/client/base-table.ts"],"sourcesContent":["import { StandardSchemaV1 } from \"@standard-schema/spec\";\n\n/**\n * BaseTable defines the schema and configuration for a table.\n *\n * @template Schema - Record of field names to StandardSchemaV1 validators\n * @template IdField - The name of the primary key field (optional, automatically read-only)\n * @template Required - Additional field names to require on insert (beyond auto-inferred required fields)\n * @template ReadOnly - Field names that cannot be modified via insert/update (idField is automatically read-only)\n *\n * @example Basic table with auto-inferred required fields\n * ```ts\n * import { z } from \"zod\";\n *\n * const usersTable = new BaseTable({\n * schema: {\n * id: z.string(), // Auto-required (not nullable), auto-readOnly (idField)\n * name: z.string(), // Auto-required (not nullable)\n * email: z.string().nullable(), // Optional (nullable)\n * },\n * idField: \"id\",\n * });\n * // On insert: name is required, email is optional (id is excluded - readOnly)\n * // On update: name and email available (id is excluded - readOnly)\n * ```\n *\n * @example Table with additional required and readOnly fields\n * ```ts\n * import { z } from \"zod\";\n *\n * const usersTable = new BaseTable({\n * schema: {\n * id: z.string(), // Auto-required, auto-readOnly (idField)\n * createdAt: z.string(), // Read-only system field\n * name: z.string(), // Auto-required\n * email: z.string().nullable(), // Optional by default...\n * legacyField: z.string().nullable(), // Optional by default...\n * },\n * idField: \"id\",\n * required: [\"legacyField\"], // Make legacyField required for new inserts\n * readOnly: [\"createdAt\"], // Exclude from insert/update\n * });\n * // On insert: name and legacyField required; email optional (id and createdAt excluded)\n * // On update: all fields optional (id and createdAt excluded)\n * ```\n *\n * @example Table with multiple read-only fields\n * ```ts\n * import { z } from \"zod\";\n *\n * const usersTable = new BaseTable({\n * schema: {\n * id: z.string(),\n * createdAt: z.string(),\n * modifiedAt: z.string(),\n * createdBy: z.string(),\n * notes: z.string().nullable(),\n * },\n * idField: \"id\",\n * readOnly: [\"createdAt\", \"modifiedAt\", \"createdBy\"],\n * });\n * // On insert/update: only notes is available (id and system fields excluded)\n * ```\n */\nexport class BaseTable<\n Schema extends Record<string, StandardSchemaV1> = any,\n IdField extends keyof Schema | undefined = undefined,\n Required extends readonly (keyof Schema)[] = readonly [],\n ReadOnly extends readonly (keyof Schema)[] = readonly [],\n> {\n public readonly schema: Schema;\n public readonly idField?: IdField;\n public readonly required?: Required;\n public readonly readOnly?: ReadOnly;\n public readonly fmfIds?: Record<keyof Schema, `FMFID:${string}`>;\n\n constructor(config: {\n schema: Schema;\n idField?: IdField;\n required?: Required;\n readOnly?: ReadOnly;\n }) {\n this.schema = config.schema;\n this.idField = config.idField;\n this.required = config.required;\n this.readOnly = config.readOnly;\n }\n\n /**\n * Returns the FileMaker field ID (FMFID) for a given field name, or the field name itself if not using IDs.\n * @param fieldName - The field name to get the ID for\n * @returns The FMFID string or the original field name\n */\n getFieldId(fieldName: keyof Schema): string {\n if (this.fmfIds && fieldName in this.fmfIds) {\n return this.fmfIds[fieldName];\n }\n return String(fieldName);\n }\n\n /**\n * Returns the field name for a given FileMaker field ID (FMFID), or the ID itself if not found.\n * @param fieldId - The FMFID to get the field name for\n * @returns The field name or the original ID\n */\n getFieldName(fieldId: string): string {\n if (this.fmfIds) {\n // Search for the field name that corresponds to this FMFID\n for (const [fieldName, fmfId] of Object.entries(this.fmfIds)) {\n if (fmfId === fieldId) {\n return fieldName;\n }\n }\n }\n return fieldId;\n }\n\n /**\n * Returns true if this BaseTable is using FileMaker field IDs.\n */\n isUsingFieldIds(): boolean {\n return this.fmfIds !== undefined;\n }\n}\n\n/**\n * BaseTableWithIds extends BaseTable to require FileMaker field IDs (fmfIds).\n * Use this class when you need to work with FileMaker's internal field identifiers.\n *\n * @template Schema - Record of field names to StandardSchemaV1 validators\n * @template IdField - The name of the primary key field (optional, automatically read-only)\n * @template Required - Additional field names to require on insert (beyond auto-inferred required fields)\n * @template ReadOnly - Field names that cannot be modified via insert/update (idField is automatically read-only)\n *\n * @example\n * ```ts\n * import { z } from \"zod\";\n *\n * const usersTableWithIds = new BaseTableWithIds({\n * schema: {\n * id: z.string(),\n * name: z.string(),\n * email: z.string().nullable(),\n * },\n * idField: \"id\",\n * fmfIds: {\n * id: \"FMFID:1\",\n * name: \"FMFID:2\",\n * email: \"FMFID:3\",\n * },\n * });\n * ```\n */\nexport class BaseTableWithIds<\n Schema extends Record<string, StandardSchemaV1> = any,\n IdField extends keyof Schema | undefined = undefined,\n Required extends readonly (keyof Schema)[] = readonly [],\n ReadOnly extends readonly (keyof Schema)[] = readonly [],\n> extends BaseTable<Schema, IdField, Required, ReadOnly> {\n public override readonly fmfIds: Record<keyof Schema, `FMFID:${string}`>;\n\n constructor(config: {\n schema: Schema;\n fmfIds: Record<keyof Schema, `FMFID:${string}`>;\n idField?: IdField;\n required?: Required;\n readOnly?: ReadOnly;\n }) {\n super(config);\n this.fmfIds = config.fmfIds;\n }\n}\n"],"names":[],"mappings":";;;AAgEO,MAAM,UAKX;AAAA,EAOA,YAAY,QAKT;AAXa;AACA;AACA;AACA;AACA;AAQd,SAAK,SAAS,OAAO;AACrB,SAAK,UAAU,OAAO;AACtB,SAAK,WAAW,OAAO;AACvB,SAAK,WAAW,OAAO;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQzB,WAAW,WAAiC;AAC1C,QAAI,KAAK,UAAU,aAAa,KAAK,QAAQ;AACpC,aAAA,KAAK,OAAO,SAAS;AAAA,IAAA;AAE9B,WAAO,OAAO,SAAS;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQzB,aAAa,SAAyB;AACpC,QAAI,KAAK,QAAQ;AAEJ,iBAAA,CAAC,WAAW,KAAK,KAAK,OAAO,QAAQ,KAAK,MAAM,GAAG;AAC5D,YAAI,UAAU,SAAS;AACd,iBAAA;AAAA,QAAA;AAAA,MACT;AAAA,IACF;AAEK,WAAA;AAAA,EAAA;AAAA;AAAA;AAAA;AAAA,EAMT,kBAA2B;AACzB,WAAO,KAAK,WAAW;AAAA,EAAA;AAE3B;AA8BO,MAAM,yBAKH,UAA+C;AAAA,EAGvD,YAAY,QAMT;AACD,UAAM,MAAM;AATW;AAUvB,SAAK,SAAS,OAAO;AAAA,EAAA;AAEzB;"}
|