@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.
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 +6 -3
  33. package/dist/esm/index.js +26 -4
  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 +31 -2
  55. package/src/transform.ts +236 -0
  56. package/src/types.ts +112 -34
  57. package/src/validation.ts +120 -36
@@ -1,5 +1,4 @@
1
1
  import { FFetchOptions } from '@fetchkit/ffetch';
2
- import { z } from 'zod/v4';
3
2
  import { StandardSchemaV1 } from '@standard-schema/spec';
4
3
  export type Auth = {
5
4
  username: string;
@@ -16,7 +15,9 @@ export interface ExecutableBuilder<T> {
16
15
  };
17
16
  }
18
17
  export interface ExecutionContext {
19
- _makeRequest<T>(url: string, options?: RequestInit & FFetchOptions): Promise<T>;
18
+ _makeRequest<T>(url: string, options?: RequestInit & FFetchOptions): Promise<Result<T>>;
19
+ _setUseEntityIds?(useEntityIds: boolean): void;
20
+ _getUseEntityIds?(): boolean;
20
21
  }
21
22
  export type InferSchemaType<Schema extends Record<string, StandardSchemaV1>> = {
22
23
  [K in keyof Schema]: Schema[K] extends StandardSchemaV1<any, infer Output> ? Output : never;
@@ -42,7 +43,7 @@ export type ODataFieldResponse<T> = {
42
43
  "@context": string;
43
44
  value: T;
44
45
  };
45
- export type Result<T, E = Error> = {
46
+ export type Result<T, E = import('./errors.js').FMODataErrorType> = {
46
47
  data: T;
47
48
  error: undefined;
48
49
  } | {
@@ -50,8 +51,22 @@ export type Result<T, E = Error> = {
50
51
  error: E;
51
52
  };
52
53
  export type MakeFieldsRequired<T, Keys extends keyof T> = Partial<T> & Required<Pick<T, Keys>>;
53
- export type InsertData<BT> = BT extends import('./client/base-table.js').BaseTable<infer Schema extends Record<string, z.ZodType>, any, infer InsertRequired extends readonly any[], any> ? [InsertRequired[number]] extends [keyof InferSchemaType<Schema>] ? InsertRequired extends readonly (keyof InferSchemaType<Schema>)[] ? MakeFieldsRequired<InferSchemaType<Schema>, InsertRequired[number]> : Partial<InferSchemaType<Schema>> : Partial<InferSchemaType<Schema>> : Partial<Record<string, any>>;
54
- export type UpdateData<BT> = BT extends import('./client/base-table.js').BaseTable<infer Schema extends Record<string, z.ZodType>, any, any, infer UpdateRequired extends readonly any[]> ? [UpdateRequired[number]] extends [keyof InferSchemaType<Schema>] ? UpdateRequired extends readonly (keyof InferSchemaType<Schema>)[] ? MakeFieldsRequired<InferSchemaType<Schema>, UpdateRequired[number]> : Partial<InferSchemaType<Schema>> : Partial<InferSchemaType<Schema>> : Partial<Record<string, any>>;
54
+ export type AutoRequiredKeys<Schema extends Record<string, StandardSchemaV1>> = {
55
+ [K in keyof Schema]: Extract<StandardSchemaV1.InferOutput<Schema[K]>, null | undefined> extends never ? K : never;
56
+ }[keyof Schema];
57
+ export type ExcludedFields<IdField extends keyof any | undefined, ReadOnly extends readonly any[]> = IdField extends keyof any ? IdField | ReadOnly[number] : ReadOnly[number];
58
+ type ComputeInsertData<Schema extends Record<string, StandardSchemaV1>, IdField extends keyof Schema | undefined, Required extends readonly any[], ReadOnly extends readonly any[]> = [Required[number]] extends [keyof InferSchemaType<Schema>] ? Required extends readonly (keyof InferSchemaType<Schema>)[] ? MakeFieldsRequired<Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>, Exclude<AutoRequiredKeys<Schema> | Required[number], ExcludedFields<IdField, ReadOnly>>> : MakeFieldsRequired<Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>, Exclude<AutoRequiredKeys<Schema>, ExcludedFields<IdField, ReadOnly>>> : MakeFieldsRequired<Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>, Exclude<AutoRequiredKeys<Schema>, ExcludedFields<IdField, ReadOnly>>>;
59
+ export type InsertData<BT> = BT extends import('./client/base-table.js').BaseTable<any, any, any, any> ? BT extends {
60
+ schema: infer Schema;
61
+ idField?: infer IdField;
62
+ required?: infer Required;
63
+ readOnly?: infer ReadOnly;
64
+ } ? Schema extends Record<string, StandardSchemaV1> ? IdField extends keyof Schema | undefined ? Required extends readonly any[] ? ReadOnly extends readonly any[] ? ComputeInsertData<Schema, Extract<IdField, keyof Schema | undefined>, Required, ReadOnly> : Partial<Record<string, any>> : Partial<Record<string, any>> : Partial<Record<string, any>> : Partial<Record<string, any>> : Partial<Record<string, any>> : Partial<Record<string, any>>;
65
+ export type UpdateData<BT> = BT extends import('./client/base-table.js').BaseTable<any, any, any, any> ? BT extends {
66
+ schema: infer Schema;
67
+ idField?: infer IdField;
68
+ readOnly?: infer ReadOnly;
69
+ } ? Schema extends Record<string, StandardSchemaV1> ? IdField extends keyof Schema | undefined ? ReadOnly extends readonly any[] ? Partial<Omit<InferSchemaType<Schema>, ExcludedFields<Extract<IdField, keyof Schema | undefined>, ReadOnly>>> : Partial<Record<string, any>> : Partial<Record<string, any>> : Partial<Record<string, any>> : Partial<Record<string, any>> : Partial<Record<string, any>>;
55
70
  export type ExecuteOptions = {
56
71
  includeODataAnnotations?: boolean;
57
72
  skipValidation?: boolean;
@@ -65,3 +80,4 @@ export type ExtractSchemaFromOccurrence<Occ> = Occ extends {
65
80
  schema: infer S;
66
81
  };
67
82
  } ? S extends Record<string, StandardSchemaV1> ? S : Record<string, StandardSchemaV1> : Record<string, StandardSchemaV1>;
83
+ export {};
@@ -1,10 +1,13 @@
1
1
  import { ODataRecordMetadata } from './types.js';
2
2
  import { StandardSchemaV1 } from '@standard-schema/spec';
3
3
  import { TableOccurrence } from './client/table-occurrence.js';
4
+ import { ValidationError, ResponseStructureError, RecordCountMismatchError } from './errors.js';
4
5
  export type ExpandValidationConfig = {
5
6
  relation: string;
6
7
  targetSchema?: Record<string, StandardSchemaV1>;
7
8
  targetOccurrence?: TableOccurrence<any, any, any, any>;
9
+ targetBaseTable?: any;
10
+ occurrence?: TableOccurrence<any, any, any, any>;
8
11
  selectedFields?: string[];
9
12
  nestedExpands?: ExpandValidationConfig[];
10
13
  };
@@ -17,7 +20,7 @@ export declare function validateRecord<T extends Record<string, any>>(record: an
17
20
  data: T & ODataRecordMetadata;
18
21
  } | {
19
22
  valid: false;
20
- error: Error;
23
+ error: ValidationError;
21
24
  }>;
22
25
  /**
23
26
  * Validates a list response against a schema.
@@ -27,7 +30,7 @@ export declare function validateListResponse<T extends Record<string, any>>(resp
27
30
  data: (T & ODataRecordMetadata)[];
28
31
  } | {
29
32
  valid: false;
30
- error: Error;
33
+ error: ResponseStructureError | ValidationError;
31
34
  }>;
32
35
  /**
33
36
  * Validates a single record response against a schema.
@@ -37,5 +40,5 @@ export declare function validateSingleResponse<T extends Record<string, any>>(re
37
40
  data: (T & ODataRecordMetadata) | null;
38
41
  } | {
39
42
  valid: false;
40
- error: Error;
43
+ error: RecordCountMismatchError | ValidationError;
41
44
  }>;
@@ -1,3 +1,4 @@
1
+ import { RecordCountMismatchError, ResponseStructureError, ValidationError } from "./errors.js";
1
2
  async function validateRecord(record, schema, selectedFields, expandConfigs) {
2
3
  var _a, _b;
3
4
  const { "@id": id, "@editLink": editLink, ...rest } = record;
@@ -19,17 +20,38 @@ async function validateRecord(record, schema, selectedFields, expandConfigs) {
19
20
  const fieldSchema = schema[fieldName];
20
21
  if (fieldSchema) {
21
22
  const input = rest[fieldName];
22
- let result = fieldSchema["~standard"].validate(input);
23
- if (result instanceof Promise) result = await result;
24
- if (result.issues) {
23
+ try {
24
+ let result = fieldSchema["~standard"].validate(input);
25
+ if (result instanceof Promise) result = await result;
26
+ if (result.issues) {
27
+ return {
28
+ valid: false,
29
+ error: new ValidationError(
30
+ `Validation failed for field '${fieldName}'`,
31
+ result.issues,
32
+ {
33
+ field: fieldName,
34
+ value: input,
35
+ cause: result.issues
36
+ }
37
+ )
38
+ };
39
+ }
40
+ validatedRecord2[fieldName] = result.value;
41
+ } catch (originalError) {
25
42
  return {
26
43
  valid: false,
27
- error: new Error(
28
- `Validation failed for field '${fieldName}': ${JSON.stringify(result.issues, null, 2)}`
44
+ error: new ValidationError(
45
+ `Validation failed for field '${fieldName}'`,
46
+ [],
47
+ {
48
+ field: fieldName,
49
+ value: input,
50
+ cause: originalError
51
+ }
29
52
  )
30
53
  };
31
54
  }
32
- validatedRecord2[fieldName] = result.value;
33
55
  } else {
34
56
  validatedRecord2[fieldName] = rest[fieldName];
35
57
  }
@@ -48,8 +70,12 @@ async function validateRecord(record, schema, selectedFields, expandConfigs) {
48
70
  if (isRelatedToExpand) {
49
71
  return {
50
72
  valid: false,
51
- error: new Error(
52
- `Validation failed for expanded relation '${expandConfig.relation}': ${errorMessage}`
73
+ error: new ValidationError(
74
+ `Validation failed for expanded relation '${expandConfig.relation}': ${errorMessage}`,
75
+ [],
76
+ {
77
+ field: expandConfig.relation
78
+ }
53
79
  )
54
80
  };
55
81
  }
@@ -69,8 +95,13 @@ async function validateRecord(record, schema, selectedFields, expandConfigs) {
69
95
  if (!itemValidation.valid) {
70
96
  return {
71
97
  valid: false,
72
- error: new Error(
73
- `Validation failed for expanded relation '${expandConfig.relation}' at index ${i}: ${itemValidation.error.message}`
98
+ error: new ValidationError(
99
+ `Validation failed for expanded relation '${expandConfig.relation}' at index ${i}: ${itemValidation.error.message}`,
100
+ itemValidation.error.issues,
101
+ {
102
+ field: expandConfig.relation,
103
+ cause: itemValidation.error.cause
104
+ }
74
105
  )
75
106
  };
76
107
  }
@@ -87,8 +118,13 @@ async function validateRecord(record, schema, selectedFields, expandConfigs) {
87
118
  if (!itemValidation.valid) {
88
119
  return {
89
120
  valid: false,
90
- error: new Error(
91
- `Validation failed for expanded relation '${expandConfig.relation}': ${itemValidation.error.message}`
121
+ error: new ValidationError(
122
+ `Validation failed for expanded relation '${expandConfig.relation}': ${itemValidation.error.message}`,
123
+ itemValidation.error.issues,
124
+ {
125
+ field: expandConfig.relation,
126
+ cause: itemValidation.error.cause
127
+ }
92
128
  )
93
129
  };
94
130
  }
@@ -105,17 +141,38 @@ async function validateRecord(record, schema, selectedFields, expandConfigs) {
105
141
  const validatedRecord = { ...restWithoutSystemFields };
106
142
  for (const [fieldName, fieldSchema] of Object.entries(schema)) {
107
143
  const input = rest[fieldName];
108
- let result = fieldSchema["~standard"].validate(input);
109
- if (result instanceof Promise) result = await result;
110
- if (result.issues) {
144
+ try {
145
+ let result = fieldSchema["~standard"].validate(input);
146
+ if (result instanceof Promise) result = await result;
147
+ if (result.issues) {
148
+ return {
149
+ valid: false,
150
+ error: new ValidationError(
151
+ `Validation failed for field '${fieldName}'`,
152
+ result.issues,
153
+ {
154
+ field: fieldName,
155
+ value: input,
156
+ cause: result.issues
157
+ }
158
+ )
159
+ };
160
+ }
161
+ validatedRecord[fieldName] = result.value;
162
+ } catch (originalError) {
111
163
  return {
112
164
  valid: false,
113
- error: new Error(
114
- `Validation failed for field '${fieldName}': ${JSON.stringify(result.issues, null, 2)}`
165
+ error: new ValidationError(
166
+ `Validation failed for field '${fieldName}'`,
167
+ [],
168
+ {
169
+ field: fieldName,
170
+ value: input,
171
+ cause: originalError
172
+ }
115
173
  )
116
174
  };
117
175
  }
118
- validatedRecord[fieldName] = result.value;
119
176
  }
120
177
  if (expandConfigs && expandConfigs.length > 0) {
121
178
  for (const expandConfig of expandConfigs) {
@@ -131,8 +188,12 @@ async function validateRecord(record, schema, selectedFields, expandConfigs) {
131
188
  if (isRelatedToExpand) {
132
189
  return {
133
190
  valid: false,
134
- error: new Error(
135
- `Validation failed for expanded relation '${expandConfig.relation}': ${errorMessage}`
191
+ error: new ValidationError(
192
+ `Validation failed for expanded relation '${expandConfig.relation}': ${errorMessage}`,
193
+ [],
194
+ {
195
+ field: expandConfig.relation
196
+ }
136
197
  )
137
198
  };
138
199
  }
@@ -152,8 +213,13 @@ async function validateRecord(record, schema, selectedFields, expandConfigs) {
152
213
  if (!itemValidation.valid) {
153
214
  return {
154
215
  valid: false,
155
- error: new Error(
156
- `Validation failed for expanded relation '${expandConfig.relation}' at index ${i}: ${itemValidation.error.message}`
216
+ error: new ValidationError(
217
+ `Validation failed for expanded relation '${expandConfig.relation}' at index ${i}: ${itemValidation.error.message}`,
218
+ itemValidation.error.issues,
219
+ {
220
+ field: expandConfig.relation,
221
+ cause: itemValidation.error.cause
222
+ }
157
223
  )
158
224
  };
159
225
  }
@@ -170,8 +236,13 @@ async function validateRecord(record, schema, selectedFields, expandConfigs) {
170
236
  if (!itemValidation.valid) {
171
237
  return {
172
238
  valid: false,
173
- error: new Error(
174
- `Validation failed for expanded relation '${expandConfig.relation}': ${itemValidation.error.message}`
239
+ error: new ValidationError(
240
+ `Validation failed for expanded relation '${expandConfig.relation}': ${itemValidation.error.message}`,
241
+ itemValidation.error.issues,
242
+ {
243
+ field: expandConfig.relation,
244
+ cause: itemValidation.error.cause
245
+ }
175
246
  )
176
247
  };
177
248
  }
@@ -189,15 +260,16 @@ async function validateListResponse(response, schema, selectedFields, expandConf
189
260
  if (!response || typeof response !== "object") {
190
261
  return {
191
262
  valid: false,
192
- error: new Error("Invalid response: expected an object")
263
+ error: new ResponseStructureError("an object", response)
193
264
  };
194
265
  }
195
266
  const { "@context": context, value, ...rest } = response;
196
267
  if (!Array.isArray(value)) {
197
268
  return {
198
269
  valid: false,
199
- error: new Error(
200
- "Invalid response: expected 'value' property to be an array"
270
+ error: new ResponseStructureError(
271
+ "'value' property to be an array",
272
+ value
201
273
  )
202
274
  };
203
275
  }
@@ -213,9 +285,7 @@ async function validateListResponse(response, schema, selectedFields, expandConf
213
285
  if (!validation.valid) {
214
286
  return {
215
287
  valid: false,
216
- error: new Error(
217
- `Validation failed for record at index ${i}: ${validation.error.message}`
218
- )
288
+ error: validation.error
219
289
  };
220
290
  }
221
291
  validatedRecords.push(validation.data);
@@ -230,8 +300,9 @@ async function validateSingleResponse(response, schema, selectedFields, expandCo
230
300
  if (response.value && Array.isArray(response.value) && response.value.length > 1) {
231
301
  return {
232
302
  valid: false,
233
- error: new Error(
234
- `Expected ${mode === "exact" ? "exactly one" : "at most one"} record, but received ${response.value.length}`
303
+ error: new RecordCountMismatchError(
304
+ mode === "exact" ? "one" : "at-most-one",
305
+ response.value.length
235
306
  )
236
307
  };
237
308
  }
@@ -239,7 +310,7 @@ async function validateSingleResponse(response, schema, selectedFields, expandCo
239
310
  if (mode === "exact") {
240
311
  return {
241
312
  valid: false,
242
- error: new Error("Expected exactly one record, but received none")
313
+ error: new RecordCountMismatchError("one", 0)
243
314
  };
244
315
  }
245
316
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"validation.js","sources":["../../src/validation.ts"],"sourcesContent":["import type { ODataRecordMetadata } from \"./types\";\nimport { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { TableOccurrence } from \"./client/table-occurrence\";\n\n// Type for expand validation configuration\nexport type ExpandValidationConfig = {\n relation: string;\n targetSchema?: Record<string, StandardSchemaV1>;\n targetOccurrence?: TableOccurrence<any, any, any, any>;\n selectedFields?: string[];\n nestedExpands?: ExpandValidationConfig[];\n};\n\n/**\n * Validates a single record against a schema, only validating selected fields.\n * Also validates expanded relations if expandConfigs are provided.\n */\nexport async function validateRecord<T extends Record<string, any>>(\n record: any,\n schema: Record<string, StandardSchemaV1> | undefined,\n selectedFields?: (keyof T)[],\n expandConfigs?: ExpandValidationConfig[],\n): Promise<\n | { valid: true; data: T & ODataRecordMetadata }\n | { valid: false; error: Error }\n> {\n // Extract OData metadata fields (don't validate them - include if present)\n const { \"@id\": id, \"@editLink\": editLink, ...rest } = record;\n\n // Include metadata fields if present (don't validate they exist)\n const metadata: ODataRecordMetadata = {\n \"@id\": id || \"\",\n \"@editLink\": editLink || \"\",\n };\n\n // If no schema, just return the data with metadata\n if (!schema) {\n return {\n valid: true,\n data: { ...rest, ...metadata } as T & ODataRecordMetadata,\n };\n }\n\n // Filter out FileMaker system fields that shouldn't be in responses by default\n const { ROWID, ROWMODID, ...restWithoutSystemFields } = rest;\n\n // If selected fields are specified, validate only those fields\n if (selectedFields && selectedFields.length > 0) {\n const validatedRecord: Record<string, any> = {};\n\n for (const field of selectedFields) {\n const fieldName = String(field);\n const fieldSchema = schema[fieldName];\n\n if (fieldSchema) {\n const input = rest[fieldName];\n let result = fieldSchema[\"~standard\"].validate(input);\n if (result instanceof Promise) result = await result;\n\n // if the `issues` field exists, the validation failed\n if (result.issues) {\n return {\n valid: false,\n error: new Error(\n `Validation failed for field '${fieldName}': ${JSON.stringify(result.issues, null, 2)}`,\n ),\n };\n }\n\n validatedRecord[fieldName] = result.value;\n } else {\n // For fields not in schema (like when explicitly selecting ROWID/ROWMODID)\n // include them from the original response\n validatedRecord[fieldName] = rest[fieldName];\n }\n }\n\n // Validate expanded relations\n if (expandConfigs && expandConfigs.length > 0) {\n for (const expandConfig of expandConfigs) {\n const expandValue = rest[expandConfig.relation];\n\n // Check if expand field is missing\n if (expandValue === undefined) {\n // Check for inline error array (FileMaker returns errors inline when expand fails)\n if (Array.isArray(rest.error) && rest.error.length > 0) {\n // Extract error message from inline error\n const errorDetail = rest.error[0]?.error;\n if (errorDetail?.message) {\n const errorMessage = errorDetail.message;\n // Check if the error is related to this expand by checking if:\n // 1. The error mentions the relation name, OR\n // 2. The error mentions any of the selected fields\n const isRelatedToExpand =\n errorMessage\n .toLowerCase()\n .includes(expandConfig.relation.toLowerCase()) ||\n (expandConfig.selectedFields &&\n expandConfig.selectedFields.some((field) =>\n errorMessage.toLowerCase().includes(field.toLowerCase()),\n ));\n\n if (isRelatedToExpand) {\n return {\n valid: false,\n error: new Error(\n `Validation failed for expanded relation '${expandConfig.relation}': ${errorMessage}`,\n ),\n };\n }\n }\n }\n // If no inline error but expand was expected, that's also an issue\n // However, this might be a legitimate case (e.g., no related records)\n // So we'll only fail if there's an explicit error array\n } else {\n // Original validation logic for when expand exists\n if (Array.isArray(expandValue)) {\n // Validate each item in the expanded array\n const validatedExpandedItems: any[] = [];\n for (let i = 0; i < expandValue.length; i++) {\n const item = expandValue[i];\n const itemValidation = await validateRecord(\n item,\n expandConfig.targetSchema,\n expandConfig.selectedFields as string[] | undefined,\n expandConfig.nestedExpands,\n );\n if (!itemValidation.valid) {\n return {\n valid: false,\n error: new Error(\n `Validation failed for expanded relation '${expandConfig.relation}' at index ${i}: ${itemValidation.error.message}`,\n ),\n };\n }\n validatedExpandedItems.push(itemValidation.data);\n }\n validatedRecord[expandConfig.relation] = validatedExpandedItems;\n } else {\n // Single expanded item (shouldn't happen in OData, but handle it)\n const itemValidation = await validateRecord(\n expandValue,\n expandConfig.targetSchema,\n expandConfig.selectedFields as string[] | undefined,\n expandConfig.nestedExpands,\n );\n if (!itemValidation.valid) {\n return {\n valid: false,\n error: new Error(\n `Validation failed for expanded relation '${expandConfig.relation}': ${itemValidation.error.message}`,\n ),\n };\n }\n validatedRecord[expandConfig.relation] = itemValidation.data;\n }\n }\n }\n }\n\n // Merge validated data with metadata\n return {\n valid: true,\n data: { ...validatedRecord, ...metadata } as T & ODataRecordMetadata,\n };\n }\n\n // Validate all fields in schema, but exclude ROWID/ROWMODID by default\n const validatedRecord: Record<string, any> = { ...restWithoutSystemFields };\n\n for (const [fieldName, fieldSchema] of Object.entries(schema)) {\n const input = rest[fieldName];\n let result = fieldSchema[\"~standard\"].validate(input);\n if (result instanceof Promise) result = await result;\n\n // if the `issues` field exists, the validation failed\n if (result.issues) {\n return {\n valid: false,\n error: new Error(\n `Validation failed for field '${fieldName}': ${JSON.stringify(result.issues, null, 2)}`,\n ),\n };\n }\n\n validatedRecord[fieldName] = result.value;\n }\n\n // Validate expanded relations even when not using selected fields\n if (expandConfigs && expandConfigs.length > 0) {\n for (const expandConfig of expandConfigs) {\n const expandValue = rest[expandConfig.relation];\n\n // Check if expand field is missing\n if (expandValue === undefined) {\n // Check for inline error array (FileMaker returns errors inline when expand fails)\n if (Array.isArray(rest.error) && rest.error.length > 0) {\n // Extract error message from inline error\n const errorDetail = rest.error[0]?.error;\n if (errorDetail?.message) {\n const errorMessage = errorDetail.message;\n // Check if the error is related to this expand by checking if:\n // 1. The error mentions the relation name, OR\n // 2. The error mentions any of the selected fields\n const isRelatedToExpand =\n errorMessage\n .toLowerCase()\n .includes(expandConfig.relation.toLowerCase()) ||\n (expandConfig.selectedFields &&\n expandConfig.selectedFields.some((field) =>\n errorMessage.toLowerCase().includes(field.toLowerCase()),\n ));\n\n if (isRelatedToExpand) {\n return {\n valid: false,\n error: new Error(\n `Validation failed for expanded relation '${expandConfig.relation}': ${errorMessage}`,\n ),\n };\n }\n }\n }\n // If no inline error but expand was expected, that's also an issue\n // However, this might be a legitimate case (e.g., no related records)\n // So we'll only fail if there's an explicit error array\n } else {\n // Original validation logic for when expand exists\n if (Array.isArray(expandValue)) {\n // Validate each item in the expanded array\n const validatedExpandedItems: any[] = [];\n for (let i = 0; i < expandValue.length; i++) {\n const item = expandValue[i];\n const itemValidation = await validateRecord(\n item,\n expandConfig.targetSchema,\n expandConfig.selectedFields as string[] | undefined,\n expandConfig.nestedExpands,\n );\n if (!itemValidation.valid) {\n return {\n valid: false,\n error: new Error(\n `Validation failed for expanded relation '${expandConfig.relation}' at index ${i}: ${itemValidation.error.message}`,\n ),\n };\n }\n validatedExpandedItems.push(itemValidation.data);\n }\n validatedRecord[expandConfig.relation] = validatedExpandedItems;\n } else {\n // Single expanded item (shouldn't happen in OData, but handle it)\n const itemValidation = await validateRecord(\n expandValue,\n expandConfig.targetSchema,\n expandConfig.selectedFields as string[] | undefined,\n expandConfig.nestedExpands,\n );\n if (!itemValidation.valid) {\n return {\n valid: false,\n error: new Error(\n `Validation failed for expanded relation '${expandConfig.relation}': ${itemValidation.error.message}`,\n ),\n };\n }\n validatedRecord[expandConfig.relation] = itemValidation.data;\n }\n }\n }\n }\n\n return {\n valid: true,\n data: { ...validatedRecord, ...metadata } as T & ODataRecordMetadata,\n };\n}\n\n/**\n * Validates a list response against a schema.\n */\nexport async function validateListResponse<T extends Record<string, any>>(\n response: any,\n schema: Record<string, StandardSchemaV1> | undefined,\n selectedFields?: (keyof T)[],\n expandConfigs?: ExpandValidationConfig[],\n): Promise<\n | { valid: true; data: (T & ODataRecordMetadata)[] }\n | { valid: false; error: Error }\n> {\n // Check if response has the expected structure\n if (!response || typeof response !== \"object\") {\n return {\n valid: false,\n error: new Error(\"Invalid response: expected an object\"),\n };\n }\n\n // Extract @context (for internal validation, but we won't return it)\n const { \"@context\": context, value, ...rest } = response;\n\n if (!Array.isArray(value)) {\n return {\n valid: false,\n error: new Error(\n \"Invalid response: expected 'value' property to be an array\",\n ),\n };\n }\n\n // Validate each record in the array\n const validatedRecords: (T & ODataRecordMetadata)[] = [];\n\n for (let i = 0; i < value.length; i++) {\n const record = value[i];\n const validation = await validateRecord<T>(\n record,\n schema,\n selectedFields,\n expandConfigs,\n );\n\n if (!validation.valid) {\n return {\n valid: false,\n error: new Error(\n `Validation failed for record at index ${i}: ${validation.error.message}`,\n ),\n };\n }\n\n validatedRecords.push(validation.data);\n }\n\n return {\n valid: true,\n data: validatedRecords,\n };\n}\n\n/**\n * Validates a single record response against a schema.\n */\nexport async function validateSingleResponse<T extends Record<string, any>>(\n response: any,\n schema: Record<string, StandardSchemaV1> | undefined,\n selectedFields?: (keyof T)[],\n expandConfigs?: ExpandValidationConfig[],\n mode: \"exact\" | \"maybe\" = \"maybe\",\n): Promise<\n | { valid: true; data: (T & ODataRecordMetadata) | null }\n | { valid: false; error: Error }\n> {\n // Check for multiple records (error in both modes)\n if (response.value && Array.isArray(response.value) && response.value.length > 1) {\n return {\n valid: false,\n error: new Error(\n `Expected ${mode === \"exact\" ? \"exactly one\" : \"at most one\"} record, but received ${response.value.length}`\n ),\n };\n }\n\n // Handle empty responses\n if (!response || (response.value && response.value.length === 0)) {\n if (mode === \"exact\") {\n return {\n valid: false,\n error: new Error(\"Expected exactly one record, but received none\"),\n };\n }\n // mode === \"maybe\" - return null for empty\n return {\n valid: true,\n data: null,\n };\n }\n\n // Single record validation\n const record = response.value?.[0] ?? response;\n const validation = await validateRecord<T>(\n record,\n schema,\n selectedFields,\n expandConfigs,\n );\n\n if (!validation.valid) {\n return validation as { valid: false; error: Error };\n }\n\n return {\n valid: true,\n data: validation.data,\n };\n}\n"],"names":["validatedRecord"],"mappings":"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"}
1
+ {"version":3,"file":"validation.js","sources":["../../src/validation.ts"],"sourcesContent":["import type { ODataRecordMetadata } from \"./types\";\nimport { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { TableOccurrence } from \"./client/table-occurrence\";\nimport {\n ValidationError,\n ResponseStructureError,\n RecordCountMismatchError,\n} from \"./errors\";\n\n// Type for expand validation configuration\nexport type ExpandValidationConfig = {\n relation: string;\n targetSchema?: Record<string, StandardSchemaV1>;\n targetOccurrence?: TableOccurrence<any, any, any, any>;\n targetBaseTable?: any; // BaseTable instance for transformation\n occurrence?: TableOccurrence<any, any, any, any>; // For transformation\n selectedFields?: string[];\n nestedExpands?: ExpandValidationConfig[];\n};\n\n/**\n * Validates a single record against a schema, only validating selected fields.\n * Also validates expanded relations if expandConfigs are provided.\n */\nexport async function validateRecord<T extends Record<string, any>>(\n record: any,\n schema: Record<string, StandardSchemaV1> | undefined,\n selectedFields?: (keyof T)[],\n expandConfigs?: ExpandValidationConfig[],\n): Promise<\n | { valid: true; data: T & ODataRecordMetadata }\n | { valid: false; error: ValidationError }\n> {\n // Extract OData metadata fields (don't validate them - include if present)\n const { \"@id\": id, \"@editLink\": editLink, ...rest } = record;\n\n // Include metadata fields if present (don't validate they exist)\n const metadata: ODataRecordMetadata = {\n \"@id\": id || \"\",\n \"@editLink\": editLink || \"\",\n };\n\n // If no schema, just return the data with metadata\n if (!schema) {\n return {\n valid: true,\n data: { ...rest, ...metadata } as T & ODataRecordMetadata,\n };\n }\n\n // Filter out FileMaker system fields that shouldn't be in responses by default\n const { ROWID, ROWMODID, ...restWithoutSystemFields } = rest;\n\n // If selected fields are specified, validate only those fields\n if (selectedFields && selectedFields.length > 0) {\n const validatedRecord: Record<string, any> = {};\n\n for (const field of selectedFields) {\n const fieldName = String(field);\n const fieldSchema = schema[fieldName];\n\n if (fieldSchema) {\n const input = rest[fieldName];\n try {\n let result = fieldSchema[\"~standard\"].validate(input);\n if (result instanceof Promise) result = await result;\n\n // if the `issues` field exists, the validation failed\n if (result.issues) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for field '${fieldName}'`,\n result.issues,\n {\n field: fieldName,\n value: input,\n cause: result.issues,\n },\n ),\n };\n }\n\n validatedRecord[fieldName] = result.value;\n } catch (originalError) {\n // If the validator throws directly, wrap it\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for field '${fieldName}'`,\n [],\n {\n field: fieldName,\n value: input,\n cause: originalError,\n },\n ),\n };\n }\n } else {\n // For fields not in schema (like when explicitly selecting ROWID/ROWMODID)\n // include them from the original response\n validatedRecord[fieldName] = rest[fieldName];\n }\n }\n\n // Validate expanded relations\n if (expandConfigs && expandConfigs.length > 0) {\n for (const expandConfig of expandConfigs) {\n const expandValue = rest[expandConfig.relation];\n\n // Check if expand field is missing\n if (expandValue === undefined) {\n // Check for inline error array (FileMaker returns errors inline when expand fails)\n if (Array.isArray(rest.error) && rest.error.length > 0) {\n // Extract error message from inline error\n const errorDetail = rest.error[0]?.error;\n if (errorDetail?.message) {\n const errorMessage = errorDetail.message;\n // Check if the error is related to this expand by checking if:\n // 1. The error mentions the relation name, OR\n // 2. The error mentions any of the selected fields\n const isRelatedToExpand =\n errorMessage\n .toLowerCase()\n .includes(expandConfig.relation.toLowerCase()) ||\n (expandConfig.selectedFields &&\n expandConfig.selectedFields.some((field) =>\n errorMessage.toLowerCase().includes(field.toLowerCase()),\n ));\n\n if (isRelatedToExpand) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}': ${errorMessage}`,\n [],\n {\n field: expandConfig.relation,\n },\n ),\n };\n }\n }\n }\n // If no inline error but expand was expected, that's also an issue\n // However, this might be a legitimate case (e.g., no related records)\n // So we'll only fail if there's an explicit error array\n } else {\n // Original validation logic for when expand exists\n if (Array.isArray(expandValue)) {\n // Validate each item in the expanded array\n const validatedExpandedItems: any[] = [];\n for (let i = 0; i < expandValue.length; i++) {\n const item = expandValue[i];\n const itemValidation = await validateRecord(\n item,\n expandConfig.targetSchema,\n expandConfig.selectedFields as string[] | undefined,\n expandConfig.nestedExpands,\n );\n if (!itemValidation.valid) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}' at index ${i}: ${itemValidation.error.message}`,\n itemValidation.error.issues,\n {\n field: expandConfig.relation,\n cause: itemValidation.error.cause,\n },\n ),\n };\n }\n validatedExpandedItems.push(itemValidation.data);\n }\n validatedRecord[expandConfig.relation] = validatedExpandedItems;\n } else {\n // Single expanded item (shouldn't happen in OData, but handle it)\n const itemValidation = await validateRecord(\n expandValue,\n expandConfig.targetSchema,\n expandConfig.selectedFields as string[] | undefined,\n expandConfig.nestedExpands,\n );\n if (!itemValidation.valid) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}': ${itemValidation.error.message}`,\n itemValidation.error.issues,\n {\n field: expandConfig.relation,\n cause: itemValidation.error.cause,\n },\n ),\n };\n }\n validatedRecord[expandConfig.relation] = itemValidation.data;\n }\n }\n }\n }\n\n // Merge validated data with metadata\n return {\n valid: true,\n data: { ...validatedRecord, ...metadata } as T & ODataRecordMetadata,\n };\n }\n\n // Validate all fields in schema, but exclude ROWID/ROWMODID by default\n const validatedRecord: Record<string, any> = { ...restWithoutSystemFields };\n\n for (const [fieldName, fieldSchema] of Object.entries(schema)) {\n const input = rest[fieldName];\n try {\n let result = fieldSchema[\"~standard\"].validate(input);\n if (result instanceof Promise) result = await result;\n\n // if the `issues` field exists, the validation failed\n if (result.issues) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for field '${fieldName}'`,\n result.issues,\n {\n field: fieldName,\n value: input,\n cause: result.issues,\n },\n ),\n };\n }\n\n validatedRecord[fieldName] = result.value;\n } catch (originalError) {\n // If the validator throws an error directly, catch and wrap it\n // This preserves the original error instance for instanceof checks\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for field '${fieldName}'`,\n [],\n {\n field: fieldName,\n value: input,\n cause: originalError,\n },\n ),\n };\n }\n }\n\n // Validate expanded relations even when not using selected fields\n if (expandConfigs && expandConfigs.length > 0) {\n for (const expandConfig of expandConfigs) {\n const expandValue = rest[expandConfig.relation];\n\n // Check if expand field is missing\n if (expandValue === undefined) {\n // Check for inline error array (FileMaker returns errors inline when expand fails)\n if (Array.isArray(rest.error) && rest.error.length > 0) {\n // Extract error message from inline error\n const errorDetail = rest.error[0]?.error;\n if (errorDetail?.message) {\n const errorMessage = errorDetail.message;\n // Check if the error is related to this expand by checking if:\n // 1. The error mentions the relation name, OR\n // 2. The error mentions any of the selected fields\n const isRelatedToExpand =\n errorMessage\n .toLowerCase()\n .includes(expandConfig.relation.toLowerCase()) ||\n (expandConfig.selectedFields &&\n expandConfig.selectedFields.some((field) =>\n errorMessage.toLowerCase().includes(field.toLowerCase()),\n ));\n\n if (isRelatedToExpand) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}': ${errorMessage}`,\n [],\n {\n field: expandConfig.relation,\n },\n ),\n };\n }\n }\n }\n // If no inline error but expand was expected, that's also an issue\n // However, this might be a legitimate case (e.g., no related records)\n // So we'll only fail if there's an explicit error array\n } else {\n // Original validation logic for when expand exists\n if (Array.isArray(expandValue)) {\n // Validate each item in the expanded array\n const validatedExpandedItems: any[] = [];\n for (let i = 0; i < expandValue.length; i++) {\n const item = expandValue[i];\n const itemValidation = await validateRecord(\n item,\n expandConfig.targetSchema,\n expandConfig.selectedFields as string[] | undefined,\n expandConfig.nestedExpands,\n );\n if (!itemValidation.valid) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}' at index ${i}: ${itemValidation.error.message}`,\n itemValidation.error.issues,\n {\n field: expandConfig.relation,\n cause: itemValidation.error.cause,\n },\n ),\n };\n }\n validatedExpandedItems.push(itemValidation.data);\n }\n validatedRecord[expandConfig.relation] = validatedExpandedItems;\n } else {\n // Single expanded item (shouldn't happen in OData, but handle it)\n const itemValidation = await validateRecord(\n expandValue,\n expandConfig.targetSchema,\n expandConfig.selectedFields as string[] | undefined,\n expandConfig.nestedExpands,\n );\n if (!itemValidation.valid) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}': ${itemValidation.error.message}`,\n itemValidation.error.issues,\n {\n field: expandConfig.relation,\n cause: itemValidation.error.cause,\n },\n ),\n };\n }\n validatedRecord[expandConfig.relation] = itemValidation.data;\n }\n }\n }\n }\n\n return {\n valid: true,\n data: { ...validatedRecord, ...metadata } as T & ODataRecordMetadata,\n };\n}\n\n/**\n * Validates a list response against a schema.\n */\nexport async function validateListResponse<T extends Record<string, any>>(\n response: any,\n schema: Record<string, StandardSchemaV1> | undefined,\n selectedFields?: (keyof T)[],\n expandConfigs?: ExpandValidationConfig[],\n): Promise<\n | { valid: true; data: (T & ODataRecordMetadata)[] }\n | { valid: false; error: ResponseStructureError | ValidationError }\n> {\n // Check if response has the expected structure\n if (!response || typeof response !== \"object\") {\n return {\n valid: false,\n error: new ResponseStructureError(\"an object\", response),\n };\n }\n\n // Extract @context (for internal validation, but we won't return it)\n const { \"@context\": context, value, ...rest } = response;\n\n if (!Array.isArray(value)) {\n return {\n valid: false,\n error: new ResponseStructureError(\n \"'value' property to be an array\",\n value,\n ),\n };\n }\n\n // Validate each record in the array\n const validatedRecords: (T & ODataRecordMetadata)[] = [];\n\n for (let i = 0; i < value.length; i++) {\n const record = value[i];\n const validation = await validateRecord<T>(\n record,\n schema,\n selectedFields,\n expandConfigs,\n );\n\n if (!validation.valid) {\n return {\n valid: false,\n error: validation.error,\n };\n }\n\n validatedRecords.push(validation.data);\n }\n\n return {\n valid: true,\n data: validatedRecords,\n };\n}\n\n/**\n * Validates a single record response against a schema.\n */\nexport async function validateSingleResponse<T extends Record<string, any>>(\n response: any,\n schema: Record<string, StandardSchemaV1> | undefined,\n selectedFields?: (keyof T)[],\n expandConfigs?: ExpandValidationConfig[],\n mode: \"exact\" | \"maybe\" = \"maybe\",\n): Promise<\n | { valid: true; data: (T & ODataRecordMetadata) | null }\n | { valid: false; error: RecordCountMismatchError | ValidationError }\n> {\n // Check for multiple records (error in both modes)\n if (\n response.value &&\n Array.isArray(response.value) &&\n response.value.length > 1\n ) {\n return {\n valid: false,\n error: new RecordCountMismatchError(\n mode === \"exact\" ? \"one\" : \"at-most-one\",\n response.value.length,\n ),\n };\n }\n\n // Handle empty responses\n if (!response || (response.value && response.value.length === 0)) {\n if (mode === \"exact\") {\n return {\n valid: false,\n error: new RecordCountMismatchError(\"one\", 0),\n };\n }\n // mode === \"maybe\" - return null for empty\n return {\n valid: true,\n data: null,\n };\n }\n\n // Single record validation\n const record = response.value?.[0] ?? response;\n const validation = await validateRecord<T>(\n record,\n schema,\n selectedFields,\n expandConfigs,\n );\n\n if (!validation.valid) {\n return validation as { valid: false; error: ValidationError };\n }\n\n return {\n valid: true,\n data: validation.data,\n };\n}\n"],"names":["validatedRecord"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proofkit/fmodata",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.6",
4
4
  "description": "FileMaker OData API client",
5
5
  "repository": "git@github.com:proofgeist/proofkit.git",
6
6
  "author": "Eric <37158449+eluce2@users.noreply.github.com>",
@@ -38,6 +38,14 @@
38
38
  "neverthrow": "^8.2.0",
39
39
  "odata-query": "^8.0.4"
40
40
  },
41
+ "peerDependencies": {
42
+ "zod": ">=4.0.0"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "zod": {
46
+ "optional": true
47
+ }
48
+ },
41
49
  "devDependencies": {
42
50
  "@standard-schema/spec": "^1.0.0",
43
51
  "@tanstack/vite-config": "^0.2.0",
@@ -47,6 +55,7 @@
47
55
  "tsx": "^4.19.2",
48
56
  "typescript": "^5.9.3",
49
57
  "vite": "^6.3.4",
58
+ "vite-plugin-dts": "^4.5.4",
50
59
  "vitest": "^4.0.7",
51
60
  "zod": "4.1.12"
52
61
  },
@@ -1,25 +1,172 @@
1
1
  import { StandardSchemaV1 } from "@standard-schema/spec";
2
2
 
3
+ /**
4
+ * BaseTable defines the schema and configuration for a table.
5
+ *
6
+ * @template Schema - Record of field names to StandardSchemaV1 validators
7
+ * @template IdField - The name of the primary key field (optional, automatically read-only)
8
+ * @template Required - Additional field names to require on insert (beyond auto-inferred required fields)
9
+ * @template ReadOnly - Field names that cannot be modified via insert/update (idField is automatically read-only)
10
+ *
11
+ * @example Basic table with auto-inferred required fields
12
+ * ```ts
13
+ * import { z } from "zod";
14
+ *
15
+ * const usersTable = new BaseTable({
16
+ * schema: {
17
+ * id: z.string(), // Auto-required (not nullable), auto-readOnly (idField)
18
+ * name: z.string(), // Auto-required (not nullable)
19
+ * email: z.string().nullable(), // Optional (nullable)
20
+ * },
21
+ * idField: "id",
22
+ * });
23
+ * // On insert: name is required, email is optional (id is excluded - readOnly)
24
+ * // On update: name and email available (id is excluded - readOnly)
25
+ * ```
26
+ *
27
+ * @example Table with additional required and readOnly fields
28
+ * ```ts
29
+ * import { z } from "zod";
30
+ *
31
+ * const usersTable = new BaseTable({
32
+ * schema: {
33
+ * id: z.string(), // Auto-required, auto-readOnly (idField)
34
+ * createdAt: z.string(), // Read-only system field
35
+ * name: z.string(), // Auto-required
36
+ * email: z.string().nullable(), // Optional by default...
37
+ * legacyField: z.string().nullable(), // Optional by default...
38
+ * },
39
+ * idField: "id",
40
+ * required: ["legacyField"], // Make legacyField required for new inserts
41
+ * readOnly: ["createdAt"], // Exclude from insert/update
42
+ * });
43
+ * // On insert: name and legacyField required; email optional (id and createdAt excluded)
44
+ * // On update: all fields optional (id and createdAt excluded)
45
+ * ```
46
+ *
47
+ * @example Table with multiple read-only fields
48
+ * ```ts
49
+ * import { z } from "zod";
50
+ *
51
+ * const usersTable = new BaseTable({
52
+ * schema: {
53
+ * id: z.string(),
54
+ * createdAt: z.string(),
55
+ * modifiedAt: z.string(),
56
+ * createdBy: z.string(),
57
+ * notes: z.string().nullable(),
58
+ * },
59
+ * idField: "id",
60
+ * readOnly: ["createdAt", "modifiedAt", "createdBy"],
61
+ * });
62
+ * // On insert/update: only notes is available (id and system fields excluded)
63
+ * ```
64
+ */
3
65
  export class BaseTable<
4
66
  Schema extends Record<string, StandardSchemaV1> = any,
5
67
  IdField extends keyof Schema | undefined = undefined,
6
- InsertRequired extends readonly (keyof Schema)[] = readonly [],
7
- UpdateRequired extends readonly (keyof Schema)[] = readonly [],
68
+ Required extends readonly (keyof Schema)[] = readonly [],
69
+ ReadOnly extends readonly (keyof Schema)[] = readonly [],
8
70
  > {
9
71
  public readonly schema: Schema;
10
72
  public readonly idField?: IdField;
11
- public readonly insertRequired?: InsertRequired;
12
- public readonly updateRequired?: UpdateRequired;
73
+ public readonly required?: Required;
74
+ public readonly readOnly?: ReadOnly;
75
+ public readonly fmfIds?: Record<keyof Schema, `FMFID:${string}`>;
13
76
 
14
77
  constructor(config: {
15
78
  schema: Schema;
16
79
  idField?: IdField;
17
- insertRequired?: InsertRequired;
18
- updateRequired?: UpdateRequired;
80
+ required?: Required;
81
+ readOnly?: ReadOnly;
19
82
  }) {
20
83
  this.schema = config.schema;
21
84
  this.idField = config.idField;
22
- this.insertRequired = config.insertRequired;
23
- this.updateRequired = config.updateRequired;
85
+ this.required = config.required;
86
+ this.readOnly = config.readOnly;
87
+ }
88
+
89
+ /**
90
+ * Returns the FileMaker field ID (FMFID) for a given field name, or the field name itself if not using IDs.
91
+ * @param fieldName - The field name to get the ID for
92
+ * @returns The FMFID string or the original field name
93
+ */
94
+ getFieldId(fieldName: keyof Schema): string {
95
+ if (this.fmfIds && fieldName in this.fmfIds) {
96
+ return this.fmfIds[fieldName];
97
+ }
98
+ return String(fieldName);
99
+ }
100
+
101
+ /**
102
+ * Returns the field name for a given FileMaker field ID (FMFID), or the ID itself if not found.
103
+ * @param fieldId - The FMFID to get the field name for
104
+ * @returns The field name or the original ID
105
+ */
106
+ getFieldName(fieldId: string): string {
107
+ if (this.fmfIds) {
108
+ // Search for the field name that corresponds to this FMFID
109
+ for (const [fieldName, fmfId] of Object.entries(this.fmfIds)) {
110
+ if (fmfId === fieldId) {
111
+ return fieldName;
112
+ }
113
+ }
114
+ }
115
+ return fieldId;
116
+ }
117
+
118
+ /**
119
+ * Returns true if this BaseTable is using FileMaker field IDs.
120
+ */
121
+ isUsingFieldIds(): boolean {
122
+ return this.fmfIds !== undefined;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * BaseTableWithIds extends BaseTable to require FileMaker field IDs (fmfIds).
128
+ * Use this class when you need to work with FileMaker's internal field identifiers.
129
+ *
130
+ * @template Schema - Record of field names to StandardSchemaV1 validators
131
+ * @template IdField - The name of the primary key field (optional, automatically read-only)
132
+ * @template Required - Additional field names to require on insert (beyond auto-inferred required fields)
133
+ * @template ReadOnly - Field names that cannot be modified via insert/update (idField is automatically read-only)
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * import { z } from "zod";
138
+ *
139
+ * const usersTableWithIds = new BaseTableWithIds({
140
+ * schema: {
141
+ * id: z.string(),
142
+ * name: z.string(),
143
+ * email: z.string().nullable(),
144
+ * },
145
+ * idField: "id",
146
+ * fmfIds: {
147
+ * id: "FMFID:1",
148
+ * name: "FMFID:2",
149
+ * email: "FMFID:3",
150
+ * },
151
+ * });
152
+ * ```
153
+ */
154
+ export class BaseTableWithIds<
155
+ Schema extends Record<string, StandardSchemaV1> = any,
156
+ IdField extends keyof Schema | undefined = undefined,
157
+ Required extends readonly (keyof Schema)[] = readonly [],
158
+ ReadOnly extends readonly (keyof Schema)[] = readonly [],
159
+ > extends BaseTable<Schema, IdField, Required, ReadOnly> {
160
+ public override readonly fmfIds: Record<keyof Schema, `FMFID:${string}`>;
161
+
162
+ constructor(config: {
163
+ schema: Schema;
164
+ fmfIds: Record<keyof Schema, `FMFID:${string}`>;
165
+ idField?: IdField;
166
+ required?: Required;
167
+ readOnly?: ReadOnly;
168
+ }) {
169
+ super(config);
170
+ this.fmfIds = config.fmfIds;
24
171
  }
25
172
  }