@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.
- package/LICENSE.md +21 -0
- package/README.md +651 -449
- package/dist/esm/client/batch-builder.d.ts +10 -9
- package/dist/esm/client/batch-builder.js +119 -56
- package/dist/esm/client/batch-builder.js.map +1 -1
- package/dist/esm/client/batch-request.js +16 -21
- package/dist/esm/client/batch-request.js.map +1 -1
- package/dist/esm/client/builders/default-select.d.ts +10 -0
- package/dist/esm/client/builders/default-select.js +41 -0
- package/dist/esm/client/builders/default-select.js.map +1 -0
- package/dist/esm/client/builders/expand-builder.d.ts +45 -0
- package/dist/esm/client/builders/expand-builder.js +185 -0
- package/dist/esm/client/builders/expand-builder.js.map +1 -0
- package/dist/esm/client/builders/index.d.ts +9 -0
- package/dist/esm/client/builders/query-string-builder.d.ts +18 -0
- package/dist/esm/client/builders/query-string-builder.js +21 -0
- package/dist/esm/client/builders/query-string-builder.js.map +1 -0
- package/dist/esm/client/builders/response-processor.d.ts +43 -0
- package/dist/esm/client/builders/response-processor.js +175 -0
- package/dist/esm/client/builders/response-processor.js.map +1 -0
- package/dist/esm/client/builders/select-mixin.d.ts +25 -0
- package/dist/esm/client/builders/select-mixin.js +28 -0
- package/dist/esm/client/builders/select-mixin.js.map +1 -0
- package/dist/esm/client/builders/select-utils.d.ts +18 -0
- package/dist/esm/client/builders/select-utils.js +30 -0
- package/dist/esm/client/builders/select-utils.js.map +1 -0
- package/dist/esm/client/builders/shared-types.d.ts +40 -0
- package/dist/esm/client/builders/table-utils.d.ts +35 -0
- package/dist/esm/client/builders/table-utils.js +44 -0
- package/dist/esm/client/builders/table-utils.js.map +1 -0
- package/dist/esm/client/database.d.ts +34 -22
- package/dist/esm/client/database.js +48 -84
- package/dist/esm/client/database.js.map +1 -1
- package/dist/esm/client/delete-builder.d.ts +25 -30
- package/dist/esm/client/delete-builder.js +45 -30
- package/dist/esm/client/delete-builder.js.map +1 -1
- package/dist/esm/client/entity-set.d.ts +35 -43
- package/dist/esm/client/entity-set.js +110 -52
- package/dist/esm/client/entity-set.js.map +1 -1
- package/dist/esm/client/error-parser.d.ts +12 -0
- package/dist/esm/client/error-parser.js +25 -0
- package/dist/esm/client/error-parser.js.map +1 -0
- package/dist/esm/client/filemaker-odata.d.ts +26 -7
- package/dist/esm/client/filemaker-odata.js +65 -42
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/dist/esm/client/insert-builder.d.ts +19 -24
- package/dist/esm/client/insert-builder.js +94 -58
- package/dist/esm/client/insert-builder.js.map +1 -1
- package/dist/esm/client/query/expand-builder.d.ts +35 -0
- package/dist/esm/client/query/index.d.ts +4 -0
- package/dist/esm/client/query/query-builder.d.ts +132 -0
- package/dist/esm/client/query/query-builder.js +456 -0
- package/dist/esm/client/query/query-builder.js.map +1 -0
- package/dist/esm/client/query/response-processor.d.ts +25 -0
- package/dist/esm/client/query/types.d.ts +77 -0
- package/dist/esm/client/query/url-builder.d.ts +71 -0
- package/dist/esm/client/query/url-builder.js +100 -0
- package/dist/esm/client/query/url-builder.js.map +1 -0
- package/dist/esm/client/query-builder.d.ts +2 -115
- package/dist/esm/client/record-builder.d.ts +108 -36
- package/dist/esm/client/record-builder.js +284 -119
- package/dist/esm/client/record-builder.js.map +1 -1
- package/dist/esm/client/response-processor.d.ts +4 -9
- package/dist/esm/client/sanitize-json.d.ts +35 -0
- package/dist/esm/client/sanitize-json.js +27 -0
- package/dist/esm/client/sanitize-json.js.map +1 -0
- package/dist/esm/client/schema-manager.d.ts +5 -5
- package/dist/esm/client/schema-manager.js +45 -31
- package/dist/esm/client/schema-manager.js.map +1 -1
- package/dist/esm/client/update-builder.d.ts +34 -40
- package/dist/esm/client/update-builder.js +99 -58
- package/dist/esm/client/update-builder.js.map +1 -1
- package/dist/esm/client/webhook-builder.d.ts +126 -0
- package/dist/esm/client/webhook-builder.js +189 -0
- package/dist/esm/client/webhook-builder.js.map +1 -0
- package/dist/esm/errors.d.ts +19 -2
- package/dist/esm/errors.js +39 -4
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +10 -8
- package/dist/esm/index.js +40 -10
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/logger.d.ts +47 -0
- package/dist/esm/logger.js +69 -0
- package/dist/esm/logger.js.map +1 -0
- package/dist/esm/logger.test.d.ts +1 -0
- package/dist/esm/orm/column.d.ts +62 -0
- package/dist/esm/orm/column.js +63 -0
- package/dist/esm/orm/column.js.map +1 -0
- package/dist/esm/orm/field-builders.d.ts +164 -0
- package/dist/esm/orm/field-builders.js +158 -0
- package/dist/esm/orm/field-builders.js.map +1 -0
- package/dist/esm/orm/index.d.ts +5 -0
- package/dist/esm/orm/operators.d.ts +173 -0
- package/dist/esm/orm/operators.js +260 -0
- package/dist/esm/orm/operators.js.map +1 -0
- package/dist/esm/orm/table.d.ts +355 -0
- package/dist/esm/orm/table.js +202 -0
- package/dist/esm/orm/table.js.map +1 -0
- package/dist/esm/transform.d.ts +20 -21
- package/dist/esm/transform.js +44 -45
- package/dist/esm/transform.js.map +1 -1
- package/dist/esm/types.d.ts +96 -30
- package/dist/esm/types.js +7 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/validation.d.ts +22 -12
- package/dist/esm/validation.js +132 -85
- package/dist/esm/validation.js.map +1 -1
- package/package.json +28 -20
- package/src/client/batch-builder.ts +153 -89
- package/src/client/batch-request.ts +25 -41
- package/src/client/builders/default-select.ts +75 -0
- package/src/client/builders/expand-builder.ts +246 -0
- package/src/client/builders/index.ts +11 -0
- package/src/client/builders/query-string-builder.ts +46 -0
- package/src/client/builders/response-processor.ts +279 -0
- package/src/client/builders/select-mixin.ts +65 -0
- package/src/client/builders/select-utils.ts +59 -0
- package/src/client/builders/shared-types.ts +45 -0
- package/src/client/builders/table-utils.ts +83 -0
- package/src/client/database.ts +89 -183
- package/src/client/delete-builder.ts +74 -84
- package/src/client/entity-set.ts +266 -293
- package/src/client/error-parser.ts +41 -0
- package/src/client/filemaker-odata.ts +98 -66
- package/src/client/insert-builder.ts +157 -118
- package/src/client/query/expand-builder.ts +160 -0
- package/src/client/query/index.ts +14 -0
- package/src/client/query/query-builder.ts +729 -0
- package/src/client/query/response-processor.ts +226 -0
- package/src/client/query/types.ts +126 -0
- package/src/client/query/url-builder.ts +151 -0
- package/src/client/query-builder.ts +10 -1455
- package/src/client/record-builder.ts +575 -240
- package/src/client/response-processor.ts +15 -42
- package/src/client/sanitize-json.ts +64 -0
- package/src/client/schema-manager.ts +61 -76
- package/src/client/update-builder.ts +161 -143
- package/src/client/webhook-builder.ts +265 -0
- package/src/errors.ts +49 -16
- package/src/index.ts +99 -54
- package/src/logger.test.ts +34 -0
- package/src/logger.ts +116 -0
- package/src/orm/column.ts +106 -0
- package/src/orm/field-builders.ts +250 -0
- package/src/orm/index.ts +61 -0
- package/src/orm/operators.ts +473 -0
- package/src/orm/table.ts +741 -0
- package/src/transform.ts +90 -70
- package/src/types.ts +154 -113
- package/src/validation.ts +200 -115
- package/dist/esm/client/base-table.d.ts +0 -125
- package/dist/esm/client/base-table.js +0 -57
- package/dist/esm/client/base-table.js.map +0 -1
- package/dist/esm/client/query-builder.js +0 -896
- package/dist/esm/client/query-builder.js.map +0 -1
- package/dist/esm/client/table-occurrence.d.ts +0 -72
- package/dist/esm/client/table-occurrence.js +0 -74
- package/dist/esm/client/table-occurrence.js.map +0 -1
- package/dist/esm/filter-types.d.ts +0 -76
- package/src/client/base-table.ts +0 -175
- package/src/client/query-builder.ts.bak +0 -1457
- package/src/client/table-occurrence.ts +0 -175
- 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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
80
|
+
export interface ExpandValidationConfig {
|
|
12
81
|
relation: string;
|
|
13
|
-
targetSchema?: Record<string, StandardSchemaV1
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
99
|
+
schema: Partial<Record<string, StandardSchemaV1>> | undefined,
|
|
28
100
|
selectedFields?: (keyof T)[],
|
|
29
101
|
expandConfigs?: ExpandValidationConfig[],
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
//
|
|
38
|
-
const metadata: ODataRecordMetadata = {
|
|
39
|
-
|
|
40
|
-
"@
|
|
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: {
|
|
131
|
+
data: {
|
|
132
|
+
...restWithoutSystemFields,
|
|
133
|
+
...specialColumns,
|
|
134
|
+
...metadata,
|
|
135
|
+
} as T & ODataRecordMetadata,
|
|
48
136
|
};
|
|
49
137
|
}
|
|
50
138
|
|
|
51
|
-
//
|
|
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)
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
//
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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)
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
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
|
|
468
|
+
schema: Partial<Record<string, StandardSchemaV1>> | undefined,
|
|
366
469
|
selectedFields?: (keyof T)[],
|
|
367
470
|
expandConfigs?: ExpandValidationConfig[],
|
|
471
|
+
includeSpecialColumns?: boolean,
|
|
368
472
|
): Promise<
|
|
369
|
-
|
|
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, ...
|
|
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 (
|
|
397
|
-
const
|
|
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
|
|
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>;
|