@proofkit/fmodata 0.1.0-alpha.9 → 0.1.0-beta.23

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 (163) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +651 -449
  3. package/dist/esm/client/batch-builder.d.ts +10 -9
  4. package/dist/esm/client/batch-builder.js +119 -56
  5. package/dist/esm/client/batch-builder.js.map +1 -1
  6. package/dist/esm/client/batch-request.js +16 -21
  7. package/dist/esm/client/batch-request.js.map +1 -1
  8. package/dist/esm/client/builders/default-select.d.ts +10 -0
  9. package/dist/esm/client/builders/default-select.js +41 -0
  10. package/dist/esm/client/builders/default-select.js.map +1 -0
  11. package/dist/esm/client/builders/expand-builder.d.ts +45 -0
  12. package/dist/esm/client/builders/expand-builder.js +185 -0
  13. package/dist/esm/client/builders/expand-builder.js.map +1 -0
  14. package/dist/esm/client/builders/index.d.ts +9 -0
  15. package/dist/esm/client/builders/query-string-builder.d.ts +18 -0
  16. package/dist/esm/client/builders/query-string-builder.js +21 -0
  17. package/dist/esm/client/builders/query-string-builder.js.map +1 -0
  18. package/dist/esm/client/builders/response-processor.d.ts +43 -0
  19. package/dist/esm/client/builders/response-processor.js +175 -0
  20. package/dist/esm/client/builders/response-processor.js.map +1 -0
  21. package/dist/esm/client/builders/select-mixin.d.ts +25 -0
  22. package/dist/esm/client/builders/select-mixin.js +28 -0
  23. package/dist/esm/client/builders/select-mixin.js.map +1 -0
  24. package/dist/esm/client/builders/select-utils.d.ts +18 -0
  25. package/dist/esm/client/builders/select-utils.js +30 -0
  26. package/dist/esm/client/builders/select-utils.js.map +1 -0
  27. package/dist/esm/client/builders/shared-types.d.ts +40 -0
  28. package/dist/esm/client/builders/table-utils.d.ts +35 -0
  29. package/dist/esm/client/builders/table-utils.js +44 -0
  30. package/dist/esm/client/builders/table-utils.js.map +1 -0
  31. package/dist/esm/client/database.d.ts +34 -22
  32. package/dist/esm/client/database.js +48 -84
  33. package/dist/esm/client/database.js.map +1 -1
  34. package/dist/esm/client/delete-builder.d.ts +25 -30
  35. package/dist/esm/client/delete-builder.js +45 -30
  36. package/dist/esm/client/delete-builder.js.map +1 -1
  37. package/dist/esm/client/entity-set.d.ts +35 -43
  38. package/dist/esm/client/entity-set.js +110 -52
  39. package/dist/esm/client/entity-set.js.map +1 -1
  40. package/dist/esm/client/error-parser.d.ts +12 -0
  41. package/dist/esm/client/error-parser.js +25 -0
  42. package/dist/esm/client/error-parser.js.map +1 -0
  43. package/dist/esm/client/filemaker-odata.d.ts +26 -7
  44. package/dist/esm/client/filemaker-odata.js +65 -42
  45. package/dist/esm/client/filemaker-odata.js.map +1 -1
  46. package/dist/esm/client/insert-builder.d.ts +19 -24
  47. package/dist/esm/client/insert-builder.js +94 -58
  48. package/dist/esm/client/insert-builder.js.map +1 -1
  49. package/dist/esm/client/query/expand-builder.d.ts +35 -0
  50. package/dist/esm/client/query/index.d.ts +4 -0
  51. package/dist/esm/client/query/query-builder.d.ts +132 -0
  52. package/dist/esm/client/query/query-builder.js +456 -0
  53. package/dist/esm/client/query/query-builder.js.map +1 -0
  54. package/dist/esm/client/query/response-processor.d.ts +25 -0
  55. package/dist/esm/client/query/types.d.ts +77 -0
  56. package/dist/esm/client/query/url-builder.d.ts +71 -0
  57. package/dist/esm/client/query/url-builder.js +100 -0
  58. package/dist/esm/client/query/url-builder.js.map +1 -0
  59. package/dist/esm/client/query-builder.d.ts +2 -115
  60. package/dist/esm/client/record-builder.d.ts +108 -36
  61. package/dist/esm/client/record-builder.js +284 -119
  62. package/dist/esm/client/record-builder.js.map +1 -1
  63. package/dist/esm/client/response-processor.d.ts +4 -9
  64. package/dist/esm/client/sanitize-json.d.ts +35 -0
  65. package/dist/esm/client/sanitize-json.js +27 -0
  66. package/dist/esm/client/sanitize-json.js.map +1 -0
  67. package/dist/esm/client/schema-manager.d.ts +5 -5
  68. package/dist/esm/client/schema-manager.js +45 -31
  69. package/dist/esm/client/schema-manager.js.map +1 -1
  70. package/dist/esm/client/update-builder.d.ts +34 -40
  71. package/dist/esm/client/update-builder.js +99 -58
  72. package/dist/esm/client/update-builder.js.map +1 -1
  73. package/dist/esm/client/webhook-builder.d.ts +126 -0
  74. package/dist/esm/client/webhook-builder.js +189 -0
  75. package/dist/esm/client/webhook-builder.js.map +1 -0
  76. package/dist/esm/errors.d.ts +19 -2
  77. package/dist/esm/errors.js +39 -4
  78. package/dist/esm/errors.js.map +1 -1
  79. package/dist/esm/index.d.ts +10 -8
  80. package/dist/esm/index.js +40 -10
  81. package/dist/esm/index.js.map +1 -1
  82. package/dist/esm/logger.d.ts +47 -0
  83. package/dist/esm/logger.js +69 -0
  84. package/dist/esm/logger.js.map +1 -0
  85. package/dist/esm/logger.test.d.ts +1 -0
  86. package/dist/esm/orm/column.d.ts +62 -0
  87. package/dist/esm/orm/column.js +63 -0
  88. package/dist/esm/orm/column.js.map +1 -0
  89. package/dist/esm/orm/field-builders.d.ts +164 -0
  90. package/dist/esm/orm/field-builders.js +158 -0
  91. package/dist/esm/orm/field-builders.js.map +1 -0
  92. package/dist/esm/orm/index.d.ts +5 -0
  93. package/dist/esm/orm/operators.d.ts +173 -0
  94. package/dist/esm/orm/operators.js +260 -0
  95. package/dist/esm/orm/operators.js.map +1 -0
  96. package/dist/esm/orm/table.d.ts +355 -0
  97. package/dist/esm/orm/table.js +202 -0
  98. package/dist/esm/orm/table.js.map +1 -0
  99. package/dist/esm/transform.d.ts +20 -21
  100. package/dist/esm/transform.js +44 -45
  101. package/dist/esm/transform.js.map +1 -1
  102. package/dist/esm/types.d.ts +96 -30
  103. package/dist/esm/types.js +7 -0
  104. package/dist/esm/types.js.map +1 -0
  105. package/dist/esm/validation.d.ts +22 -12
  106. package/dist/esm/validation.js +132 -85
  107. package/dist/esm/validation.js.map +1 -1
  108. package/package.json +28 -20
  109. package/src/client/batch-builder.ts +153 -89
  110. package/src/client/batch-request.ts +25 -41
  111. package/src/client/builders/default-select.ts +75 -0
  112. package/src/client/builders/expand-builder.ts +246 -0
  113. package/src/client/builders/index.ts +11 -0
  114. package/src/client/builders/query-string-builder.ts +46 -0
  115. package/src/client/builders/response-processor.ts +279 -0
  116. package/src/client/builders/select-mixin.ts +65 -0
  117. package/src/client/builders/select-utils.ts +59 -0
  118. package/src/client/builders/shared-types.ts +45 -0
  119. package/src/client/builders/table-utils.ts +83 -0
  120. package/src/client/database.ts +89 -183
  121. package/src/client/delete-builder.ts +74 -84
  122. package/src/client/entity-set.ts +266 -293
  123. package/src/client/error-parser.ts +41 -0
  124. package/src/client/filemaker-odata.ts +98 -66
  125. package/src/client/insert-builder.ts +157 -118
  126. package/src/client/query/expand-builder.ts +160 -0
  127. package/src/client/query/index.ts +14 -0
  128. package/src/client/query/query-builder.ts +729 -0
  129. package/src/client/query/response-processor.ts +226 -0
  130. package/src/client/query/types.ts +126 -0
  131. package/src/client/query/url-builder.ts +151 -0
  132. package/src/client/query-builder.ts +10 -1455
  133. package/src/client/record-builder.ts +575 -240
  134. package/src/client/response-processor.ts +15 -42
  135. package/src/client/sanitize-json.ts +64 -0
  136. package/src/client/schema-manager.ts +61 -76
  137. package/src/client/update-builder.ts +161 -143
  138. package/src/client/webhook-builder.ts +265 -0
  139. package/src/errors.ts +49 -16
  140. package/src/index.ts +99 -54
  141. package/src/logger.test.ts +34 -0
  142. package/src/logger.ts +116 -0
  143. package/src/orm/column.ts +106 -0
  144. package/src/orm/field-builders.ts +250 -0
  145. package/src/orm/index.ts +61 -0
  146. package/src/orm/operators.ts +473 -0
  147. package/src/orm/table.ts +741 -0
  148. package/src/transform.ts +90 -70
  149. package/src/types.ts +154 -113
  150. package/src/validation.ts +200 -115
  151. package/dist/esm/client/base-table.d.ts +0 -125
  152. package/dist/esm/client/base-table.js +0 -57
  153. package/dist/esm/client/base-table.js.map +0 -1
  154. package/dist/esm/client/query-builder.js +0 -896
  155. package/dist/esm/client/query-builder.js.map +0 -1
  156. package/dist/esm/client/table-occurrence.d.ts +0 -72
  157. package/dist/esm/client/table-occurrence.js +0 -74
  158. package/dist/esm/client/table-occurrence.js.map +0 -1
  159. package/dist/esm/filter-types.d.ts +0 -76
  160. package/src/client/base-table.ts +0 -175
  161. package/src/client/query-builder.ts.bak +0 -1457
  162. package/src/client/table-occurrence.ts +0 -175
  163. package/src/filter-types.ts +0 -97
package/src/validation.ts CHANGED
@@ -1,58 +1,158 @@
1
+ /** biome-ignore-all lint/style/useCollapsedElseIf: easier to read */
2
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
3
+ import { RecordCountMismatchError, ResponseStructureError, ValidationError } from "./errors";
4
+ import type { FMTable } from "./orm/table";
1
5
  import type { ODataRecordMetadata } from "./types";
2
- import { StandardSchemaV1 } from "@standard-schema/spec";
3
- import type { TableOccurrence } from "./client/table-occurrence";
4
- import {
5
- ValidationError,
6
- ResponseStructureError,
7
- RecordCountMismatchError,
8
- } from "./errors";
6
+
7
+ /**
8
+ * Validates and transforms input data for insert/update operations.
9
+ * Applies input validators (writeValidators) to transform user input to database format.
10
+ * Fields without input validators are passed through unchanged.
11
+ *
12
+ * @param data - The input data to validate and transform
13
+ * @param inputSchema - Optional schema containing input validators for each field
14
+ * @returns Transformed data ready to send to the server
15
+ * @throws ValidationError if any field fails validation
16
+ */
17
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape
18
+ export async function validateAndTransformInput<T extends Record<string, any>>(
19
+ data: Partial<T>,
20
+ inputSchema?: Partial<Record<string, StandardSchemaV1>>,
21
+ ): Promise<Partial<T>> {
22
+ // If no input schema, return data as-is
23
+ if (!inputSchema) {
24
+ return data;
25
+ }
26
+
27
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic field transformation
28
+ const transformedData: Record<string, any> = { ...data };
29
+
30
+ // Process each field that has an input validator
31
+ for (const [fieldName, fieldSchema] of Object.entries(inputSchema)) {
32
+ // Skip if no schema for this field
33
+ if (!fieldSchema) {
34
+ continue;
35
+ }
36
+
37
+ // Only process fields that are present in the input data
38
+ if (fieldName in data) {
39
+ const inputValue = data[fieldName];
40
+
41
+ try {
42
+ // Run the input validator to transform the value
43
+ let result = fieldSchema["~standard"].validate(inputValue);
44
+ if (result instanceof Promise) {
45
+ result = await result;
46
+ }
47
+
48
+ // Check for validation errors
49
+ if (result.issues) {
50
+ throw new ValidationError(`Input validation failed for field '${fieldName}'`, result.issues, {
51
+ field: fieldName,
52
+ value: inputValue,
53
+ cause: result.issues,
54
+ });
55
+ }
56
+
57
+ // Store the transformed value
58
+ transformedData[fieldName] = result.value;
59
+ } catch (error) {
60
+ // If it's already a ValidationError, re-throw it
61
+ if (error instanceof ValidationError) {
62
+ throw error;
63
+ }
64
+
65
+ // Otherwise, wrap the error
66
+ throw new ValidationError(`Input validation failed for field '${fieldName}'`, [], {
67
+ field: fieldName,
68
+ value: inputValue,
69
+ cause: error,
70
+ });
71
+ }
72
+ }
73
+ }
74
+
75
+ // Fields without input validators are already in transformedData (passed through)
76
+ return transformedData as Partial<T>;
77
+ }
9
78
 
10
79
  // Type for expand validation configuration
11
- export type ExpandValidationConfig = {
80
+ export interface ExpandValidationConfig {
12
81
  relation: string;
13
- targetSchema?: Record<string, StandardSchemaV1>;
14
- targetOccurrence?: TableOccurrence<any, any, any, any>;
15
- targetBaseTable?: any; // BaseTable instance for transformation
16
- occurrence?: TableOccurrence<any, any, any, any>; // For transformation
82
+ targetSchema?: Partial<Record<string, StandardSchemaV1>>;
83
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
84
+ targetTable?: FMTable<any, any>;
85
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
86
+ table?: FMTable<any, any>; // For transformation
17
87
  selectedFields?: string[];
18
88
  nestedExpands?: ExpandValidationConfig[];
19
- };
89
+ }
20
90
 
21
91
  /**
22
92
  * Validates a single record against a schema, only validating selected fields.
23
93
  * Also validates expanded relations if expandConfigs are provided.
24
94
  */
95
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape
25
96
  export async function validateRecord<T extends Record<string, any>>(
97
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic record validation
26
98
  record: any,
27
- schema: Record<string, StandardSchemaV1> | undefined,
99
+ schema: Partial<Record<string, StandardSchemaV1>> | undefined,
28
100
  selectedFields?: (keyof T)[],
29
101
  expandConfigs?: ExpandValidationConfig[],
30
- ): Promise<
31
- | { valid: true; data: T & ODataRecordMetadata }
32
- | { valid: false; error: ValidationError }
33
- > {
102
+ includeSpecialColumns?: boolean,
103
+ ): Promise<{ valid: true; data: T & ODataRecordMetadata } | { valid: false; error: ValidationError }> {
34
104
  // Extract OData metadata fields (don't validate them - include if present)
35
105
  const { "@id": id, "@editLink": editLink, ...rest } = record;
36
106
 
37
- // Include metadata fields if present (don't validate they exist)
38
- const metadata: ODataRecordMetadata = {
39
- "@id": id || "",
40
- "@editLink": editLink || "",
41
- };
107
+ // Only include metadata fields if they actually exist and have values
108
+ const metadata: Partial<ODataRecordMetadata> = {};
109
+ if (id) {
110
+ metadata["@id"] = id;
111
+ }
112
+ if (editLink) {
113
+ metadata["@editLink"] = editLink;
114
+ }
42
115
 
43
116
  // If no schema, just return the data with metadata
117
+ // Exclude special columns if includeSpecialColumns is false
44
118
  if (!schema) {
119
+ const { ROWID, ROWMODID, ...restWithoutSystemFields } = rest;
120
+ const specialColumns: { ROWID?: number; ROWMODID?: number } = {};
121
+ if (includeSpecialColumns) {
122
+ if (ROWID !== undefined) {
123
+ specialColumns.ROWID = ROWID;
124
+ }
125
+ if (ROWMODID !== undefined) {
126
+ specialColumns.ROWMODID = ROWMODID;
127
+ }
128
+ }
45
129
  return {
46
130
  valid: true,
47
- data: { ...rest, ...metadata } as T & ODataRecordMetadata,
131
+ data: {
132
+ ...restWithoutSystemFields,
133
+ ...specialColumns,
134
+ ...metadata,
135
+ } as T & ODataRecordMetadata,
48
136
  };
49
137
  }
50
138
 
51
- // Filter out FileMaker system fields that shouldn't be in responses by default
139
+ // Extract FileMaker special columns - preserve them if includeSpecialColumns is enabled
140
+ // Note: Special columns are excluded when using single() method (per OData spec behavior)
52
141
  const { ROWID, ROWMODID, ...restWithoutSystemFields } = rest;
142
+ const specialColumns: { ROWID?: number; ROWMODID?: number } = {};
143
+ // Only include special columns if explicitly enabled (they're excluded for single() by design)
144
+ if (includeSpecialColumns) {
145
+ if (ROWID !== undefined) {
146
+ specialColumns.ROWID = ROWID;
147
+ }
148
+ if (ROWMODID !== undefined) {
149
+ specialColumns.ROWMODID = ROWMODID;
150
+ }
151
+ }
53
152
 
54
153
  // If selected fields are specified, validate only those fields
55
154
  if (selectedFields && selectedFields.length > 0) {
155
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic field validation
56
156
  const validatedRecord: Record<string, any> = {};
57
157
 
58
158
  for (const field of selectedFields) {
@@ -63,21 +163,19 @@ export async function validateRecord<T extends Record<string, any>>(
63
163
  const input = rest[fieldName];
64
164
  try {
65
165
  let result = fieldSchema["~standard"].validate(input);
66
- if (result instanceof Promise) result = await result;
166
+ if (result instanceof Promise) {
167
+ result = await result;
168
+ }
67
169
 
68
170
  // if the `issues` field exists, the validation failed
69
171
  if (result.issues) {
70
172
  return {
71
173
  valid: false,
72
- error: new ValidationError(
73
- `Validation failed for field '${fieldName}'`,
74
- result.issues,
75
- {
76
- field: fieldName,
77
- value: input,
78
- cause: result.issues,
79
- },
80
- ),
174
+ error: new ValidationError(`Validation failed for field '${fieldName}'`, result.issues, {
175
+ field: fieldName,
176
+ value: input,
177
+ cause: result.issues,
178
+ }),
81
179
  };
82
180
  }
83
181
 
@@ -86,21 +184,27 @@ export async function validateRecord<T extends Record<string, any>>(
86
184
  // If the validator throws directly, wrap it
87
185
  return {
88
186
  valid: false,
89
- error: new ValidationError(
90
- `Validation failed for field '${fieldName}'`,
91
- [],
92
- {
93
- field: fieldName,
94
- value: input,
95
- cause: originalError,
96
- },
97
- ),
187
+ error: new ValidationError(`Validation failed for field '${fieldName}'`, [], {
188
+ field: fieldName,
189
+ value: input,
190
+ cause: originalError,
191
+ }),
98
192
  };
99
193
  }
100
194
  } else {
101
195
  // For fields not in schema (like when explicitly selecting ROWID/ROWMODID)
102
- // include them from the original response
103
- validatedRecord[fieldName] = rest[fieldName];
196
+ // Check if it's a special column that was destructured earlier
197
+ if (fieldName === "ROWID" || fieldName === "ROWMODID") {
198
+ // Use the destructured value since it was removed from rest
199
+ if (fieldName === "ROWID" && ROWID !== undefined) {
200
+ validatedRecord[fieldName] = ROWID;
201
+ } else if (fieldName === "ROWMODID" && ROWMODID !== undefined) {
202
+ validatedRecord[fieldName] = ROWMODID;
203
+ }
204
+ } else {
205
+ // For other fields not in schema, include them from the original response
206
+ validatedRecord[fieldName] = rest[fieldName];
207
+ }
104
208
  }
105
209
  }
106
210
 
@@ -121,13 +225,8 @@ export async function validateRecord<T extends Record<string, any>>(
121
225
  // 1. The error mentions the relation name, OR
122
226
  // 2. The error mentions any of the selected fields
123
227
  const isRelatedToExpand =
124
- errorMessage
125
- .toLowerCase()
126
- .includes(expandConfig.relation.toLowerCase()) ||
127
- (expandConfig.selectedFields &&
128
- expandConfig.selectedFields.some((field) =>
129
- errorMessage.toLowerCase().includes(field.toLowerCase()),
130
- ));
228
+ errorMessage.toLowerCase().includes(expandConfig.relation.toLowerCase()) ||
229
+ expandConfig.selectedFields?.some((field) => errorMessage.toLowerCase().includes(field.toLowerCase()));
131
230
 
132
231
  if (isRelatedToExpand) {
133
232
  return {
@@ -150,6 +249,7 @@ export async function validateRecord<T extends Record<string, any>>(
150
249
  // Original validation logic for when expand exists
151
250
  if (Array.isArray(expandValue)) {
152
251
  // Validate each item in the expanded array
252
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic expanded items validation
153
253
  const validatedExpandedItems: any[] = [];
154
254
  for (let i = 0; i < expandValue.length; i++) {
155
255
  const item = expandValue[i];
@@ -158,6 +258,7 @@ export async function validateRecord<T extends Record<string, any>>(
158
258
  expandConfig.targetSchema,
159
259
  expandConfig.selectedFields as string[] | undefined,
160
260
  expandConfig.nestedExpands,
261
+ includeSpecialColumns,
161
262
  );
162
263
  if (!itemValidation.valid) {
163
264
  return {
@@ -182,6 +283,7 @@ export async function validateRecord<T extends Record<string, any>>(
182
283
  expandConfig.targetSchema,
183
284
  expandConfig.selectedFields as string[] | undefined,
184
285
  expandConfig.nestedExpands,
286
+ includeSpecialColumns,
185
287
  );
186
288
  if (!itemValidation.valid) {
187
289
  return {
@@ -202,35 +304,40 @@ export async function validateRecord<T extends Record<string, any>>(
202
304
  }
203
305
  }
204
306
 
205
- // Merge validated data with metadata
307
+ // Return only the validated fields plus metadata and special columns
308
+ // Do NOT merge restWithoutSystemFields as that would overwrite validated values
206
309
  return {
207
310
  valid: true,
208
- data: { ...validatedRecord, ...metadata } as T & ODataRecordMetadata,
311
+ data: { ...validatedRecord, ...specialColumns, ...metadata } as T & ODataRecordMetadata,
209
312
  };
210
313
  }
211
314
 
212
- // Validate all fields in schema, but exclude ROWID/ROWMODID by default
315
+ // Validate all fields in schema, but exclude ROWID/ROWMODID by default (unless includeSpecialColumns is enabled)
316
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic field validation
213
317
  const validatedRecord: Record<string, any> = { ...restWithoutSystemFields };
214
318
 
215
319
  for (const [fieldName, fieldSchema] of Object.entries(schema)) {
320
+ // Skip if no schema for this field
321
+ if (!fieldSchema) {
322
+ continue;
323
+ }
324
+
216
325
  const input = rest[fieldName];
217
326
  try {
218
327
  let result = fieldSchema["~standard"].validate(input);
219
- if (result instanceof Promise) result = await result;
328
+ if (result instanceof Promise) {
329
+ result = await result;
330
+ }
220
331
 
221
332
  // if the `issues` field exists, the validation failed
222
333
  if (result.issues) {
223
334
  return {
224
335
  valid: false,
225
- error: new ValidationError(
226
- `Validation failed for field '${fieldName}'`,
227
- result.issues,
228
- {
229
- field: fieldName,
230
- value: input,
231
- cause: result.issues,
232
- },
233
- ),
336
+ error: new ValidationError(`Validation failed for field '${fieldName}'`, result.issues, {
337
+ field: fieldName,
338
+ value: input,
339
+ cause: result.issues,
340
+ }),
234
341
  };
235
342
  }
236
343
 
@@ -240,15 +347,11 @@ export async function validateRecord<T extends Record<string, any>>(
240
347
  // This preserves the original error instance for instanceof checks
241
348
  return {
242
349
  valid: false,
243
- error: new ValidationError(
244
- `Validation failed for field '${fieldName}'`,
245
- [],
246
- {
247
- field: fieldName,
248
- value: input,
249
- cause: originalError,
250
- },
251
- ),
350
+ error: new ValidationError(`Validation failed for field '${fieldName}'`, [], {
351
+ field: fieldName,
352
+ value: input,
353
+ cause: originalError,
354
+ }),
252
355
  };
253
356
  }
254
357
  }
@@ -270,13 +373,8 @@ export async function validateRecord<T extends Record<string, any>>(
270
373
  // 1. The error mentions the relation name, OR
271
374
  // 2. The error mentions any of the selected fields
272
375
  const isRelatedToExpand =
273
- errorMessage
274
- .toLowerCase()
275
- .includes(expandConfig.relation.toLowerCase()) ||
276
- (expandConfig.selectedFields &&
277
- expandConfig.selectedFields.some((field) =>
278
- errorMessage.toLowerCase().includes(field.toLowerCase()),
279
- ));
376
+ errorMessage.toLowerCase().includes(expandConfig.relation.toLowerCase()) ||
377
+ expandConfig.selectedFields?.some((field) => errorMessage.toLowerCase().includes(field.toLowerCase()));
280
378
 
281
379
  if (isRelatedToExpand) {
282
380
  return {
@@ -299,6 +397,7 @@ export async function validateRecord<T extends Record<string, any>>(
299
397
  // Original validation logic for when expand exists
300
398
  if (Array.isArray(expandValue)) {
301
399
  // Validate each item in the expanded array
400
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic expanded items validation
302
401
  const validatedExpandedItems: any[] = [];
303
402
  for (let i = 0; i < expandValue.length; i++) {
304
403
  const item = expandValue[i];
@@ -307,6 +406,7 @@ export async function validateRecord<T extends Record<string, any>>(
307
406
  expandConfig.targetSchema,
308
407
  expandConfig.selectedFields as string[] | undefined,
309
408
  expandConfig.nestedExpands,
409
+ includeSpecialColumns,
310
410
  );
311
411
  if (!itemValidation.valid) {
312
412
  return {
@@ -331,6 +431,7 @@ export async function validateRecord<T extends Record<string, any>>(
331
431
  expandConfig.targetSchema,
332
432
  expandConfig.selectedFields as string[] | undefined,
333
433
  expandConfig.nestedExpands,
434
+ includeSpecialColumns,
334
435
  );
335
436
  if (!itemValidation.valid) {
336
437
  return {
@@ -353,21 +454,23 @@ export async function validateRecord<T extends Record<string, any>>(
353
454
 
354
455
  return {
355
456
  valid: true,
356
- data: { ...validatedRecord, ...metadata } as T & ODataRecordMetadata,
457
+ data: { ...validatedRecord, ...specialColumns, ...metadata } as T & ODataRecordMetadata,
357
458
  };
358
459
  }
359
460
 
360
461
  /**
361
462
  * Validates a list response against a schema.
362
463
  */
464
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape
363
465
  export async function validateListResponse<T extends Record<string, any>>(
466
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic response validation
364
467
  response: any,
365
- schema: Record<string, StandardSchemaV1> | undefined,
468
+ schema: Partial<Record<string, StandardSchemaV1>> | undefined,
366
469
  selectedFields?: (keyof T)[],
367
470
  expandConfigs?: ExpandValidationConfig[],
471
+ includeSpecialColumns?: boolean,
368
472
  ): Promise<
369
- | { valid: true; data: (T & ODataRecordMetadata)[] }
370
- | { valid: false; error: ResponseStructureError | ValidationError }
473
+ { valid: true; data: (T & ODataRecordMetadata)[] } | { valid: false; error: ResponseStructureError | ValidationError }
371
474
  > {
372
475
  // Check if response has the expected structure
373
476
  if (!response || typeof response !== "object") {
@@ -378,29 +481,20 @@ export async function validateListResponse<T extends Record<string, any>>(
378
481
  }
379
482
 
380
483
  // Extract @context (for internal validation, but we won't return it)
381
- const { "@context": context, value, ...rest } = response;
484
+ const { "@context": context, value, ..._rest } = response;
382
485
 
383
486
  if (!Array.isArray(value)) {
384
487
  return {
385
488
  valid: false,
386
- error: new ResponseStructureError(
387
- "'value' property to be an array",
388
- value,
389
- ),
489
+ error: new ResponseStructureError("'value' property to be an array", value),
390
490
  };
391
491
  }
392
492
 
393
493
  // Validate each record in the array
394
494
  const validatedRecords: (T & ODataRecordMetadata)[] = [];
395
495
 
396
- for (let i = 0; i < value.length; i++) {
397
- const record = value[i];
398
- const validation = await validateRecord<T>(
399
- record,
400
- schema,
401
- selectedFields,
402
- expandConfigs,
403
- );
496
+ for (const record of value) {
497
+ const validation = await validateRecord<T>(record, schema, selectedFields, expandConfigs, includeSpecialColumns);
404
498
 
405
499
  if (!validation.valid) {
406
500
  return {
@@ -421,28 +515,24 @@ export async function validateListResponse<T extends Record<string, any>>(
421
515
  /**
422
516
  * Validates a single record response against a schema.
423
517
  */
518
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape
424
519
  export async function validateSingleResponse<T extends Record<string, any>>(
520
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic response validation
425
521
  response: any,
426
- schema: Record<string, StandardSchemaV1> | undefined,
522
+ schema: Partial<Record<string, StandardSchemaV1>> | undefined,
427
523
  selectedFields?: (keyof T)[],
428
524
  expandConfigs?: ExpandValidationConfig[],
429
525
  mode: "exact" | "maybe" = "maybe",
526
+ includeSpecialColumns?: boolean,
430
527
  ): Promise<
431
528
  | { valid: true; data: (T & ODataRecordMetadata) | null }
432
529
  | { valid: false; error: RecordCountMismatchError | ValidationError }
433
530
  > {
434
531
  // Check for multiple records (error in both modes)
435
- if (
436
- response.value &&
437
- Array.isArray(response.value) &&
438
- response.value.length > 1
439
- ) {
532
+ if (response.value && Array.isArray(response.value) && response.value.length > 1) {
440
533
  return {
441
534
  valid: false,
442
- error: new RecordCountMismatchError(
443
- mode === "exact" ? "one" : "at-most-one",
444
- response.value.length,
445
- ),
535
+ error: new RecordCountMismatchError(mode === "exact" ? "one" : "at-most-one", response.value.length),
446
536
  };
447
537
  }
448
538
 
@@ -463,12 +553,7 @@ export async function validateSingleResponse<T extends Record<string, any>>(
463
553
 
464
554
  // Single record validation
465
555
  const record = response.value?.[0] ?? response;
466
- const validation = await validateRecord<T>(
467
- record,
468
- schema,
469
- selectedFields,
470
- expandConfigs,
471
- );
556
+ const validation = await validateRecord<T>(record, schema, selectedFields, expandConfigs, includeSpecialColumns);
472
557
 
473
558
  if (!validation.valid) {
474
559
  return validation as { valid: false; error: ValidationError };
@@ -1,125 +0,0 @@
1
- import { StandardSchemaV1 } from '@standard-schema/spec';
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 | (string & {}))[] = readonly [], ReadOnly extends readonly (keyof Schema | (string & {}))[] = readonly []> {
65
- readonly schema: Schema;
66
- readonly idField?: IdField;
67
- readonly required?: Required;
68
- readonly readOnly?: ReadOnly;
69
- readonly fmfIds?: Record<keyof Schema | (string & {}), `FMFID:${string}`>;
70
- constructor(config: {
71
- schema: Schema;
72
- idField?: IdField;
73
- required?: Required;
74
- readOnly?: ReadOnly;
75
- fmfIds?: Record<string, `FMFID:${string}`>;
76
- });
77
- /**
78
- * Returns the FileMaker field ID (FMFID) for a given field name, or the field name itself if not using IDs.
79
- * @param fieldName - The field name to get the ID for
80
- * @returns The FMFID string or the original field name
81
- */
82
- getFieldId(fieldName: keyof Schema): string;
83
- /**
84
- * Returns the field name for a given FileMaker field ID (FMFID), or the ID itself if not found.
85
- * @param fieldId - The FMFID to get the field name for
86
- * @returns The field name or the original ID
87
- */
88
- getFieldName(fieldId: string): string;
89
- /**
90
- * Returns true if this BaseTable is using FileMaker field IDs.
91
- */
92
- isUsingFieldIds(): boolean;
93
- }
94
- /**
95
- * Creates a BaseTable with proper TypeScript type inference.
96
- *
97
- * This function should be used instead of `new BaseTable()` to ensure
98
- * field names are properly typed throughout the library.
99
- *
100
- * @example Without entity IDs
101
- * ```ts
102
- * const users = defineBaseTable({
103
- * schema: { id: z.string(), name: z.string() },
104
- * idField: "id",
105
- * });
106
- * ```
107
- *
108
- * @example With entity IDs (FileMaker field IDs)
109
- * ```ts
110
- * const products = defineBaseTable({
111
- * schema: { id: z.string(), name: z.string() },
112
- * idField: "id",
113
- * fmfIds: { id: "FMFID:1", name: "FMFID:2" },
114
- * });
115
- * ```
116
- */
117
- export declare function defineBaseTable<const Schema extends Record<string, StandardSchemaV1>, IdField extends keyof Schema | undefined = undefined, const Required extends readonly (keyof Schema | (string & {}))[] = readonly [], const ReadOnly extends readonly (keyof Schema | (string & {}))[] = readonly []>(config: {
118
- schema: Schema;
119
- idField?: IdField;
120
- required?: Required;
121
- readOnly?: ReadOnly;
122
- fmfIds?: {
123
- [K in keyof Schema | (string & {})]: `FMFID:${string}`;
124
- };
125
- }): BaseTable<Schema, IdField, Required, ReadOnly>;