@proofkit/fmodata 0.1.0-alpha.4 → 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.
Files changed (57) hide show
  1. package/README.md +357 -28
  2. package/dist/esm/client/base-table.d.ts +122 -5
  3. package/dist/esm/client/base-table.js +46 -5
  4. package/dist/esm/client/base-table.js.map +1 -1
  5. package/dist/esm/client/database.d.ts +20 -3
  6. package/dist/esm/client/database.js +62 -13
  7. package/dist/esm/client/database.js.map +1 -1
  8. package/dist/esm/client/delete-builder.js +24 -27
  9. package/dist/esm/client/delete-builder.js.map +1 -1
  10. package/dist/esm/client/entity-set.d.ts +9 -6
  11. package/dist/esm/client/entity-set.js +5 -1
  12. package/dist/esm/client/entity-set.js.map +1 -1
  13. package/dist/esm/client/filemaker-odata.d.ts +17 -4
  14. package/dist/esm/client/filemaker-odata.js +90 -27
  15. package/dist/esm/client/filemaker-odata.js.map +1 -1
  16. package/dist/esm/client/insert-builder.js +45 -34
  17. package/dist/esm/client/insert-builder.js.map +1 -1
  18. package/dist/esm/client/query-builder.d.ts +7 -2
  19. package/dist/esm/client/query-builder.js +273 -202
  20. package/dist/esm/client/query-builder.js.map +1 -1
  21. package/dist/esm/client/record-builder.d.ts +2 -2
  22. package/dist/esm/client/record-builder.js +50 -40
  23. package/dist/esm/client/record-builder.js.map +1 -1
  24. package/dist/esm/client/table-occurrence.d.ts +66 -2
  25. package/dist/esm/client/table-occurrence.js +36 -1
  26. package/dist/esm/client/table-occurrence.js.map +1 -1
  27. package/dist/esm/client/update-builder.js +39 -35
  28. package/dist/esm/client/update-builder.js.map +1 -1
  29. package/dist/esm/errors.d.ts +60 -0
  30. package/dist/esm/errors.js +122 -0
  31. package/dist/esm/errors.js.map +1 -0
  32. package/dist/esm/index.d.ts +5 -2
  33. package/dist/esm/index.js +25 -3
  34. package/dist/esm/index.js.map +1 -1
  35. package/dist/esm/transform.d.ts +56 -0
  36. package/dist/esm/transform.js +107 -0
  37. package/dist/esm/transform.js.map +1 -0
  38. package/dist/esm/types.d.ts +21 -5
  39. package/dist/esm/validation.d.ts +6 -3
  40. package/dist/esm/validation.js +104 -33
  41. package/dist/esm/validation.js.map +1 -1
  42. package/package.json +10 -1
  43. package/src/client/base-table.ts +155 -8
  44. package/src/client/database.ts +116 -13
  45. package/src/client/delete-builder.ts +42 -43
  46. package/src/client/entity-set.ts +21 -11
  47. package/src/client/filemaker-odata.ts +132 -34
  48. package/src/client/insert-builder.ts +69 -37
  49. package/src/client/query-builder.ts +345 -233
  50. package/src/client/record-builder.ts +84 -59
  51. package/src/client/table-occurrence.ts +118 -4
  52. package/src/client/update-builder.ts +77 -49
  53. package/src/errors.ts +185 -0
  54. package/src/index.ts +30 -1
  55. package/src/transform.ts +236 -0
  56. package/src/types.ts +112 -34
  57. 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
- insertRequired: ["name", "email"], // optional: fields that are required on insert
145
- updateRequired: ["email"], // optional: fields that are required on update
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
- If you specify `insertRequired` fields in your base table, those fields become required:
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
- createdAt: z.string().optional(),
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
- insertRequired: ["username", "email"], // These fields are required on insert
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 will enforce that username and email are provided
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
- // createdAt is optional
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
- Control which fields are required for insert and update operations:
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
- updatedAt: z.string().optional(),
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
- insertRequired: ["username", "email"], // Required on insert
857
- updateRequired: ["status"], // Required on update
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 requires username and email
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
- // updatedAt is optional
870
+ status: "active", // Required due to 'required' array
871
+ updatedAt: new Date().toISOString(), // Optional
865
872
  });
866
873
 
867
- // Update requires status
874
+ // Update: all fields are optional except id and createdAt are excluded
868
875
  db.from("users")
869
876
  .update({
870
- status: "active",
871
- // other fields are optional
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
- export declare class BaseTable<Schema extends Record<string, StandardSchemaV1> = any, IdField extends keyof Schema | undefined = undefined, InsertRequired extends readonly (keyof Schema)[] = readonly [], UpdateRequired extends readonly (keyof Schema)[] = readonly []> {
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 insertRequired?: InsertRequired;
6
- readonly updateRequired?: UpdateRequired;
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
- insertRequired?: InsertRequired;
11
- updateRequired?: UpdateRequired;
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, "insertRequired");
9
- __publicField(this, "updateRequired");
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.insertRequired = config.insertRequired;
13
- this.updateRequired = config.updateRequired;
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 InsertRequired extends readonly (keyof Schema)[] = readonly [],\n UpdateRequired extends readonly (keyof Schema)[] = readonly [],\n> {\n public readonly schema: Schema;\n public readonly idField?: IdField;\n public readonly insertRequired?: InsertRequired;\n public readonly updateRequired?: UpdateRequired;\n\n constructor(config: {\n schema: Schema;\n idField?: IdField;\n insertRequired?: InsertRequired;\n updateRequired?: UpdateRequired;\n }) {\n this.schema = config.schema;\n this.idField = config.idField;\n this.insertRequired = config.insertRequired;\n this.updateRequired = config.updateRequired;\n }\n}\n"],"names":[],"mappings":";;;AAEO,MAAM,UAKX;AAAA,EAMA,YAAY,QAKT;AAVa;AACA;AACA;AACA;AAQd,SAAK,SAAS,OAAO;AACrB,SAAK,UAAU,OAAO;AACtB,SAAK,iBAAiB,OAAO;AAC7B,SAAK,iBAAiB,OAAO;AAAA,EAAA;AAEjC;"}
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;"}