@proofkit/fmodata 0.1.0-alpha.4 → 0.1.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +690 -31
- package/dist/esm/client/base-table.d.ts +122 -5
- package/dist/esm/client/base-table.js +46 -5
- package/dist/esm/client/base-table.js.map +1 -1
- package/dist/esm/client/batch-builder.d.ts +54 -0
- package/dist/esm/client/batch-builder.js +179 -0
- package/dist/esm/client/batch-builder.js.map +1 -0
- package/dist/esm/client/batch-request.d.ts +61 -0
- package/dist/esm/client/batch-request.js +252 -0
- package/dist/esm/client/batch-request.js.map +1 -0
- package/dist/esm/client/database.d.ts +54 -5
- package/dist/esm/client/database.js +118 -15
- package/dist/esm/client/database.js.map +1 -1
- package/dist/esm/client/delete-builder.d.ts +21 -2
- package/dist/esm/client/delete-builder.js +96 -32
- package/dist/esm/client/delete-builder.js.map +1 -1
- package/dist/esm/client/entity-set.d.ts +22 -8
- package/dist/esm/client/entity-set.js +28 -8
- package/dist/esm/client/entity-set.js.map +1 -1
- package/dist/esm/client/filemaker-odata.d.ts +22 -3
- package/dist/esm/client/filemaker-odata.js +122 -27
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/dist/esm/client/insert-builder.d.ts +38 -3
- package/dist/esm/client/insert-builder.js +231 -34
- package/dist/esm/client/insert-builder.js.map +1 -1
- package/dist/esm/client/query-builder.d.ts +26 -5
- package/dist/esm/client/query-builder.js +455 -208
- package/dist/esm/client/query-builder.js.map +1 -1
- package/dist/esm/client/record-builder.d.ts +19 -4
- package/dist/esm/client/record-builder.js +132 -40
- package/dist/esm/client/record-builder.js.map +1 -1
- package/dist/esm/client/response-processor.d.ts +38 -0
- package/dist/esm/client/schema-manager.d.ts +57 -0
- package/dist/esm/client/schema-manager.js +132 -0
- package/dist/esm/client/schema-manager.js.map +1 -0
- package/dist/esm/client/table-occurrence.d.ts +66 -2
- package/dist/esm/client/table-occurrence.js +36 -1
- package/dist/esm/client/table-occurrence.js.map +1 -1
- package/dist/esm/client/update-builder.d.ts +34 -11
- package/dist/esm/client/update-builder.js +135 -31
- package/dist/esm/client/update-builder.js.map +1 -1
- package/dist/esm/errors.d.ts +73 -0
- package/dist/esm/errors.js +148 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.d.ts +7 -3
- package/dist/esm/index.js +27 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/transform.d.ts +65 -0
- package/dist/esm/transform.js +114 -0
- package/dist/esm/transform.js.map +1 -0
- package/dist/esm/types.d.ts +89 -5
- package/dist/esm/validation.d.ts +6 -3
- package/dist/esm/validation.js +104 -33
- package/dist/esm/validation.js.map +1 -1
- package/package.json +10 -1
- package/src/client/base-table.ts +155 -8
- package/src/client/batch-builder.ts +265 -0
- package/src/client/batch-request.ts +485 -0
- package/src/client/database.ts +173 -16
- package/src/client/delete-builder.ts +149 -48
- package/src/client/entity-set.ts +99 -15
- package/src/client/filemaker-odata.ts +178 -34
- package/src/client/insert-builder.ts +350 -40
- package/src/client/query-builder.ts +609 -236
- package/src/client/record-builder.ts +186 -53
- package/src/client/response-processor.ts +103 -0
- package/src/client/schema-manager.ts +246 -0
- package/src/client/table-occurrence.ts +118 -4
- package/src/client/update-builder.ts +235 -49
- package/src/errors.ts +217 -0
- package/src/index.ts +43 -1
- package/src/transform.ts +249 -0
- package/src/types.ts +201 -35
- package/src/validation.ts +120 -36
package/src/types.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type FFetchOptions } from "@fetchkit/ffetch";
|
|
2
|
-
import { z } from "zod/v4";
|
|
3
2
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
4
3
|
|
|
5
4
|
export type Auth = { username: string; password: string } | { apiKey: string };
|
|
@@ -7,18 +6,40 @@ export type Auth = { username: string; password: string } | { apiKey: string };
|
|
|
7
6
|
export interface ExecutableBuilder<T> {
|
|
8
7
|
execute(): Promise<Result<T>>;
|
|
9
8
|
getRequestConfig(): { method: string; url: string; body?: any };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convert this builder to a native Request object for batch processing.
|
|
12
|
+
* @param baseUrl - The base URL for the OData service
|
|
13
|
+
* @returns A native Request object
|
|
14
|
+
*/
|
|
15
|
+
toRequest(baseUrl: string): Request;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Process a raw Response object into a typed Result.
|
|
19
|
+
* This allows builders to apply their own validation and transformation logic.
|
|
20
|
+
* @param response - The native Response object from the batch operation
|
|
21
|
+
* @param options - Optional execution options (e.g., skipValidation, includeODataAnnotations)
|
|
22
|
+
* @returns A typed Result with the builder's expected return type
|
|
23
|
+
*/
|
|
24
|
+
processResponse(
|
|
25
|
+
response: Response,
|
|
26
|
+
options?: ExecuteOptions,
|
|
27
|
+
): Promise<Result<T>>;
|
|
10
28
|
}
|
|
11
29
|
|
|
12
30
|
export interface ExecutionContext {
|
|
13
31
|
_makeRequest<T>(
|
|
14
32
|
url: string,
|
|
15
|
-
options?: RequestInit & FFetchOptions,
|
|
16
|
-
): Promise<T
|
|
33
|
+
options?: RequestInit & FFetchOptions & { useEntityIds?: boolean },
|
|
34
|
+
): Promise<Result<T>>;
|
|
35
|
+
_setUseEntityIds?(useEntityIds: boolean): void;
|
|
36
|
+
_getUseEntityIds?(): boolean;
|
|
37
|
+
_getBaseUrl?(): string;
|
|
17
38
|
}
|
|
18
39
|
|
|
19
40
|
export type InferSchemaType<Schema extends Record<string, StandardSchemaV1>> = {
|
|
20
|
-
[K in keyof Schema]: Schema[K] extends StandardSchemaV1<any, infer Output>
|
|
21
|
-
? Output
|
|
41
|
+
[K in keyof Schema]: Schema[K] extends StandardSchemaV1<any, infer Output>
|
|
42
|
+
? Output
|
|
22
43
|
: never;
|
|
23
44
|
};
|
|
24
45
|
|
|
@@ -63,7 +84,7 @@ export type ODataFieldResponse<T> = {
|
|
|
63
84
|
};
|
|
64
85
|
|
|
65
86
|
// Result pattern for execute responses
|
|
66
|
-
export type Result<T, E =
|
|
87
|
+
export type Result<T, E = import("./errors").FMODataErrorType> =
|
|
67
88
|
| { data: T; error: undefined }
|
|
68
89
|
| { data: undefined; error: E };
|
|
69
90
|
|
|
@@ -71,53 +92,198 @@ export type Result<T, E = Error> =
|
|
|
71
92
|
export type MakeFieldsRequired<T, Keys extends keyof T> = Partial<T> &
|
|
72
93
|
Required<Pick<T, Keys>>;
|
|
73
94
|
|
|
95
|
+
// Extract keys from schema where validator doesn't allow null/undefined (auto-required fields)
|
|
96
|
+
export type AutoRequiredKeys<Schema extends Record<string, StandardSchemaV1>> =
|
|
97
|
+
{
|
|
98
|
+
[K in keyof Schema]: Extract<
|
|
99
|
+
StandardSchemaV1.InferOutput<Schema[K]>,
|
|
100
|
+
null | undefined
|
|
101
|
+
> extends never
|
|
102
|
+
? K
|
|
103
|
+
: never;
|
|
104
|
+
}[keyof Schema];
|
|
105
|
+
|
|
106
|
+
// Helper type to compute excluded fields (readOnly fields + idField)
|
|
107
|
+
export type ExcludedFields<
|
|
108
|
+
IdField extends keyof any | undefined,
|
|
109
|
+
ReadOnly extends readonly any[],
|
|
110
|
+
> = IdField extends keyof any ? IdField | ReadOnly[number] : ReadOnly[number];
|
|
111
|
+
|
|
112
|
+
// Helper type for InsertData computation
|
|
113
|
+
type ComputeInsertData<
|
|
114
|
+
Schema extends Record<string, StandardSchemaV1>,
|
|
115
|
+
IdField extends keyof Schema | undefined,
|
|
116
|
+
Required extends readonly any[],
|
|
117
|
+
ReadOnly extends readonly any[],
|
|
118
|
+
> = [Required[number]] extends [keyof InferSchemaType<Schema>]
|
|
119
|
+
? Required extends readonly (keyof InferSchemaType<Schema>)[]
|
|
120
|
+
? MakeFieldsRequired<
|
|
121
|
+
Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>,
|
|
122
|
+
Exclude<
|
|
123
|
+
AutoRequiredKeys<Schema> | Required[number],
|
|
124
|
+
ExcludedFields<IdField, ReadOnly>
|
|
125
|
+
>
|
|
126
|
+
>
|
|
127
|
+
: MakeFieldsRequired<
|
|
128
|
+
Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>,
|
|
129
|
+
Exclude<AutoRequiredKeys<Schema>, ExcludedFields<IdField, ReadOnly>>
|
|
130
|
+
>
|
|
131
|
+
: MakeFieldsRequired<
|
|
132
|
+
Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>,
|
|
133
|
+
Exclude<AutoRequiredKeys<Schema>, ExcludedFields<IdField, ReadOnly>>
|
|
134
|
+
>;
|
|
135
|
+
|
|
74
136
|
// Extract insert data type from BaseTable
|
|
75
|
-
//
|
|
137
|
+
// Auto-infers required fields from validator nullability + user-specified required fields
|
|
138
|
+
// Excludes readOnly fields and idField
|
|
76
139
|
export type InsertData<BT> = BT extends import("./client/base-table").BaseTable<
|
|
77
|
-
infer Schema extends Record<string, z.ZodType>,
|
|
78
140
|
any,
|
|
79
|
-
|
|
141
|
+
any,
|
|
142
|
+
any,
|
|
80
143
|
any
|
|
81
144
|
>
|
|
82
|
-
?
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
145
|
+
? BT extends {
|
|
146
|
+
schema: infer Schema;
|
|
147
|
+
idField?: infer IdField;
|
|
148
|
+
required?: infer Required;
|
|
149
|
+
readOnly?: infer ReadOnly;
|
|
150
|
+
}
|
|
151
|
+
? Schema extends Record<string, StandardSchemaV1>
|
|
152
|
+
? IdField extends keyof Schema | undefined
|
|
153
|
+
? Required extends readonly any[]
|
|
154
|
+
? ReadOnly extends readonly any[]
|
|
155
|
+
? ComputeInsertData<
|
|
156
|
+
Schema,
|
|
157
|
+
Extract<IdField, keyof Schema | undefined>,
|
|
158
|
+
Required,
|
|
159
|
+
ReadOnly
|
|
160
|
+
>
|
|
161
|
+
: Partial<Record<string, any>>
|
|
162
|
+
: Partial<Record<string, any>>
|
|
163
|
+
: Partial<Record<string, any>>
|
|
164
|
+
: Partial<Record<string, any>>
|
|
165
|
+
: Partial<Record<string, any>>
|
|
87
166
|
: Partial<Record<string, any>>;
|
|
88
167
|
|
|
89
168
|
// Extract update data type from BaseTable
|
|
90
|
-
//
|
|
169
|
+
// All fields are optional for updates, excludes readOnly fields and idField
|
|
91
170
|
export type UpdateData<BT> = BT extends import("./client/base-table").BaseTable<
|
|
92
|
-
infer Schema extends Record<string, z.ZodType>,
|
|
93
171
|
any,
|
|
94
172
|
any,
|
|
95
|
-
|
|
173
|
+
any,
|
|
174
|
+
any
|
|
96
175
|
>
|
|
97
|
-
?
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
176
|
+
? BT extends {
|
|
177
|
+
schema: infer Schema;
|
|
178
|
+
idField?: infer IdField;
|
|
179
|
+
readOnly?: infer ReadOnly;
|
|
180
|
+
}
|
|
181
|
+
? Schema extends Record<string, StandardSchemaV1>
|
|
182
|
+
? IdField extends keyof Schema | undefined
|
|
183
|
+
? ReadOnly extends readonly any[]
|
|
184
|
+
? Partial<
|
|
185
|
+
Omit<
|
|
186
|
+
InferSchemaType<Schema>,
|
|
187
|
+
ExcludedFields<
|
|
188
|
+
Extract<IdField, keyof Schema | undefined>,
|
|
189
|
+
ReadOnly
|
|
190
|
+
>
|
|
191
|
+
>
|
|
192
|
+
>
|
|
193
|
+
: Partial<Record<string, any>>
|
|
194
|
+
: Partial<Record<string, any>>
|
|
195
|
+
: Partial<Record<string, any>>
|
|
196
|
+
: Partial<Record<string, any>>
|
|
102
197
|
: Partial<Record<string, any>>;
|
|
103
198
|
|
|
104
199
|
export type ExecuteOptions = {
|
|
105
200
|
includeODataAnnotations?: boolean;
|
|
106
201
|
skipValidation?: boolean;
|
|
202
|
+
/**
|
|
203
|
+
* Overrides the default behavior of the database to use entity IDs (rather than field names) in THIS REQUEST ONLY
|
|
204
|
+
*/
|
|
205
|
+
useEntityIds?: boolean;
|
|
107
206
|
};
|
|
108
207
|
|
|
109
|
-
export type ConditionallyWithODataAnnotations<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
208
|
+
export type ConditionallyWithODataAnnotations<
|
|
209
|
+
T,
|
|
210
|
+
IncludeODataAnnotations extends boolean,
|
|
211
|
+
> = IncludeODataAnnotations extends true
|
|
212
|
+
? T & {
|
|
213
|
+
"@id": string;
|
|
214
|
+
"@editLink": string;
|
|
215
|
+
}
|
|
216
|
+
: T;
|
|
116
217
|
|
|
117
218
|
// Helper type to extract schema from a TableOccurrence
|
|
118
|
-
export type ExtractSchemaFromOccurrence<Occ> =
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
: Record<string, StandardSchemaV1
|
|
219
|
+
export type ExtractSchemaFromOccurrence<Occ> = Occ extends {
|
|
220
|
+
baseTable: { schema: infer S };
|
|
221
|
+
}
|
|
222
|
+
? S extends Record<string, StandardSchemaV1>
|
|
223
|
+
? S
|
|
224
|
+
: Record<string, StandardSchemaV1>
|
|
225
|
+
: Record<string, StandardSchemaV1>;
|
|
226
|
+
|
|
227
|
+
export type GenericFieldMetadata = {
|
|
228
|
+
$Nullable?: boolean;
|
|
229
|
+
"@Index"?: boolean;
|
|
230
|
+
"@Calculation"?: boolean;
|
|
231
|
+
"@Summary"?: boolean;
|
|
232
|
+
"@Global"?: boolean;
|
|
233
|
+
"@Org.OData.Core.V1.Permissions"?: "Org.OData.Core.V1.Permission@Read";
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export type StringFieldMetadata = GenericFieldMetadata & {
|
|
237
|
+
$Type: "Edm.String";
|
|
238
|
+
$DefaultValue?: "USER" | "USERNAME" | "CURRENT_USER";
|
|
239
|
+
$MaxLength?: number;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export type DecimalFieldMetadata = GenericFieldMetadata & {
|
|
243
|
+
$Type: "Edm.Decimal";
|
|
244
|
+
"@AutoGenerated"?: boolean;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export type DateFieldMetadata = GenericFieldMetadata & {
|
|
248
|
+
$Type: "Edm.Date";
|
|
249
|
+
$DefaultValue?: "CURDATE" | "CURRENT_DATE";
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export type TimeOfDayFieldMetadata = GenericFieldMetadata & {
|
|
253
|
+
$Type: "Edm.TimeOfDay";
|
|
254
|
+
$DefaultValue?: "CURTIME" | "CURRENT_TIME";
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export type DateTimeOffsetFieldMetadata = GenericFieldMetadata & {
|
|
258
|
+
$Type: "Edm.Date";
|
|
259
|
+
$DefaultValue?: "CURTIMESTAMP" | "CURRENT_TIMESTAMP";
|
|
260
|
+
"@VersionId"?: boolean;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
export type StreamFieldMetadata = {
|
|
264
|
+
$Type: "Edm.Stream";
|
|
265
|
+
$Nullable?: boolean;
|
|
266
|
+
"@EnclosedPath": string;
|
|
267
|
+
"@ExternalOpenPath": string;
|
|
268
|
+
"@ExternalSecurePath"?: string;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export type FieldMetadata =
|
|
272
|
+
| StringFieldMetadata
|
|
273
|
+
| DecimalFieldMetadata
|
|
274
|
+
| DateFieldMetadata
|
|
275
|
+
| TimeOfDayFieldMetadata
|
|
276
|
+
| DateTimeOffsetFieldMetadata
|
|
277
|
+
| StreamFieldMetadata;
|
|
278
|
+
|
|
279
|
+
export type EntityType = {
|
|
280
|
+
$Kind: "EntityType";
|
|
281
|
+
$Key: string[];
|
|
282
|
+
} & Record<string, FieldMetadata>;
|
|
283
|
+
|
|
284
|
+
export type EntitySet = {
|
|
285
|
+
$Kind: "EntitySet";
|
|
286
|
+
$Type: string;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export type Metadata = Record<string, EntityType | EntitySet>;
|
package/src/validation.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import type { ODataRecordMetadata } from "./types";
|
|
2
2
|
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
3
3
|
import type { TableOccurrence } from "./client/table-occurrence";
|
|
4
|
+
import {
|
|
5
|
+
ValidationError,
|
|
6
|
+
ResponseStructureError,
|
|
7
|
+
RecordCountMismatchError,
|
|
8
|
+
} from "./errors";
|
|
4
9
|
|
|
5
10
|
// Type for expand validation configuration
|
|
6
11
|
export type ExpandValidationConfig = {
|
|
7
12
|
relation: string;
|
|
8
13
|
targetSchema?: Record<string, StandardSchemaV1>;
|
|
9
14
|
targetOccurrence?: TableOccurrence<any, any, any, any>;
|
|
15
|
+
targetBaseTable?: any; // BaseTable instance for transformation
|
|
16
|
+
occurrence?: TableOccurrence<any, any, any, any>; // For transformation
|
|
10
17
|
selectedFields?: string[];
|
|
11
18
|
nestedExpands?: ExpandValidationConfig[];
|
|
12
19
|
};
|
|
@@ -22,7 +29,7 @@ export async function validateRecord<T extends Record<string, any>>(
|
|
|
22
29
|
expandConfigs?: ExpandValidationConfig[],
|
|
23
30
|
): Promise<
|
|
24
31
|
| { valid: true; data: T & ODataRecordMetadata }
|
|
25
|
-
| { valid: false; error:
|
|
32
|
+
| { valid: false; error: ValidationError }
|
|
26
33
|
> {
|
|
27
34
|
// Extract OData metadata fields (don't validate them - include if present)
|
|
28
35
|
const { "@id": id, "@editLink": editLink, ...rest } = record;
|
|
@@ -54,20 +61,42 @@ export async function validateRecord<T extends Record<string, any>>(
|
|
|
54
61
|
|
|
55
62
|
if (fieldSchema) {
|
|
56
63
|
const input = rest[fieldName];
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
try {
|
|
65
|
+
let result = fieldSchema["~standard"].validate(input);
|
|
66
|
+
if (result instanceof Promise) result = await result;
|
|
59
67
|
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
// if the `issues` field exists, the validation failed
|
|
69
|
+
if (result.issues) {
|
|
70
|
+
return {
|
|
71
|
+
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
|
+
),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
validatedRecord[fieldName] = result.value;
|
|
85
|
+
} catch (originalError) {
|
|
86
|
+
// If the validator throws directly, wrap it
|
|
62
87
|
return {
|
|
63
88
|
valid: false,
|
|
64
|
-
error: new
|
|
65
|
-
`Validation failed for field '${fieldName}'
|
|
89
|
+
error: new ValidationError(
|
|
90
|
+
`Validation failed for field '${fieldName}'`,
|
|
91
|
+
[],
|
|
92
|
+
{
|
|
93
|
+
field: fieldName,
|
|
94
|
+
value: input,
|
|
95
|
+
cause: originalError,
|
|
96
|
+
},
|
|
66
97
|
),
|
|
67
98
|
};
|
|
68
99
|
}
|
|
69
|
-
|
|
70
|
-
validatedRecord[fieldName] = result.value;
|
|
71
100
|
} else {
|
|
72
101
|
// For fields not in schema (like when explicitly selecting ROWID/ROWMODID)
|
|
73
102
|
// include them from the original response
|
|
@@ -103,8 +132,12 @@ export async function validateRecord<T extends Record<string, any>>(
|
|
|
103
132
|
if (isRelatedToExpand) {
|
|
104
133
|
return {
|
|
105
134
|
valid: false,
|
|
106
|
-
error: new
|
|
135
|
+
error: new ValidationError(
|
|
107
136
|
`Validation failed for expanded relation '${expandConfig.relation}': ${errorMessage}`,
|
|
137
|
+
[],
|
|
138
|
+
{
|
|
139
|
+
field: expandConfig.relation,
|
|
140
|
+
},
|
|
108
141
|
),
|
|
109
142
|
};
|
|
110
143
|
}
|
|
@@ -129,8 +162,13 @@ export async function validateRecord<T extends Record<string, any>>(
|
|
|
129
162
|
if (!itemValidation.valid) {
|
|
130
163
|
return {
|
|
131
164
|
valid: false,
|
|
132
|
-
error: new
|
|
165
|
+
error: new ValidationError(
|
|
133
166
|
`Validation failed for expanded relation '${expandConfig.relation}' at index ${i}: ${itemValidation.error.message}`,
|
|
167
|
+
itemValidation.error.issues,
|
|
168
|
+
{
|
|
169
|
+
field: expandConfig.relation,
|
|
170
|
+
cause: itemValidation.error.cause,
|
|
171
|
+
},
|
|
134
172
|
),
|
|
135
173
|
};
|
|
136
174
|
}
|
|
@@ -148,8 +186,13 @@ export async function validateRecord<T extends Record<string, any>>(
|
|
|
148
186
|
if (!itemValidation.valid) {
|
|
149
187
|
return {
|
|
150
188
|
valid: false,
|
|
151
|
-
error: new
|
|
189
|
+
error: new ValidationError(
|
|
152
190
|
`Validation failed for expanded relation '${expandConfig.relation}': ${itemValidation.error.message}`,
|
|
191
|
+
itemValidation.error.issues,
|
|
192
|
+
{
|
|
193
|
+
field: expandConfig.relation,
|
|
194
|
+
cause: itemValidation.error.cause,
|
|
195
|
+
},
|
|
153
196
|
),
|
|
154
197
|
};
|
|
155
198
|
}
|
|
@@ -171,20 +214,43 @@ export async function validateRecord<T extends Record<string, any>>(
|
|
|
171
214
|
|
|
172
215
|
for (const [fieldName, fieldSchema] of Object.entries(schema)) {
|
|
173
216
|
const input = rest[fieldName];
|
|
174
|
-
|
|
175
|
-
|
|
217
|
+
try {
|
|
218
|
+
let result = fieldSchema["~standard"].validate(input);
|
|
219
|
+
if (result instanceof Promise) result = await result;
|
|
220
|
+
|
|
221
|
+
// if the `issues` field exists, the validation failed
|
|
222
|
+
if (result.issues) {
|
|
223
|
+
return {
|
|
224
|
+
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
|
+
),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
176
236
|
|
|
177
|
-
|
|
178
|
-
|
|
237
|
+
validatedRecord[fieldName] = result.value;
|
|
238
|
+
} catch (originalError) {
|
|
239
|
+
// If the validator throws an error directly, catch and wrap it
|
|
240
|
+
// This preserves the original error instance for instanceof checks
|
|
179
241
|
return {
|
|
180
242
|
valid: false,
|
|
181
|
-
error: new
|
|
182
|
-
`Validation failed for field '${fieldName}'
|
|
243
|
+
error: new ValidationError(
|
|
244
|
+
`Validation failed for field '${fieldName}'`,
|
|
245
|
+
[],
|
|
246
|
+
{
|
|
247
|
+
field: fieldName,
|
|
248
|
+
value: input,
|
|
249
|
+
cause: originalError,
|
|
250
|
+
},
|
|
183
251
|
),
|
|
184
252
|
};
|
|
185
253
|
}
|
|
186
|
-
|
|
187
|
-
validatedRecord[fieldName] = result.value;
|
|
188
254
|
}
|
|
189
255
|
|
|
190
256
|
// Validate expanded relations even when not using selected fields
|
|
@@ -215,8 +281,12 @@ export async function validateRecord<T extends Record<string, any>>(
|
|
|
215
281
|
if (isRelatedToExpand) {
|
|
216
282
|
return {
|
|
217
283
|
valid: false,
|
|
218
|
-
error: new
|
|
284
|
+
error: new ValidationError(
|
|
219
285
|
`Validation failed for expanded relation '${expandConfig.relation}': ${errorMessage}`,
|
|
286
|
+
[],
|
|
287
|
+
{
|
|
288
|
+
field: expandConfig.relation,
|
|
289
|
+
},
|
|
220
290
|
),
|
|
221
291
|
};
|
|
222
292
|
}
|
|
@@ -241,8 +311,13 @@ export async function validateRecord<T extends Record<string, any>>(
|
|
|
241
311
|
if (!itemValidation.valid) {
|
|
242
312
|
return {
|
|
243
313
|
valid: false,
|
|
244
|
-
error: new
|
|
314
|
+
error: new ValidationError(
|
|
245
315
|
`Validation failed for expanded relation '${expandConfig.relation}' at index ${i}: ${itemValidation.error.message}`,
|
|
316
|
+
itemValidation.error.issues,
|
|
317
|
+
{
|
|
318
|
+
field: expandConfig.relation,
|
|
319
|
+
cause: itemValidation.error.cause,
|
|
320
|
+
},
|
|
246
321
|
),
|
|
247
322
|
};
|
|
248
323
|
}
|
|
@@ -260,8 +335,13 @@ export async function validateRecord<T extends Record<string, any>>(
|
|
|
260
335
|
if (!itemValidation.valid) {
|
|
261
336
|
return {
|
|
262
337
|
valid: false,
|
|
263
|
-
error: new
|
|
338
|
+
error: new ValidationError(
|
|
264
339
|
`Validation failed for expanded relation '${expandConfig.relation}': ${itemValidation.error.message}`,
|
|
340
|
+
itemValidation.error.issues,
|
|
341
|
+
{
|
|
342
|
+
field: expandConfig.relation,
|
|
343
|
+
cause: itemValidation.error.cause,
|
|
344
|
+
},
|
|
265
345
|
),
|
|
266
346
|
};
|
|
267
347
|
}
|
|
@@ -287,13 +367,13 @@ export async function validateListResponse<T extends Record<string, any>>(
|
|
|
287
367
|
expandConfigs?: ExpandValidationConfig[],
|
|
288
368
|
): Promise<
|
|
289
369
|
| { valid: true; data: (T & ODataRecordMetadata)[] }
|
|
290
|
-
| { valid: false; error:
|
|
370
|
+
| { valid: false; error: ResponseStructureError | ValidationError }
|
|
291
371
|
> {
|
|
292
372
|
// Check if response has the expected structure
|
|
293
373
|
if (!response || typeof response !== "object") {
|
|
294
374
|
return {
|
|
295
375
|
valid: false,
|
|
296
|
-
error: new
|
|
376
|
+
error: new ResponseStructureError("an object", response),
|
|
297
377
|
};
|
|
298
378
|
}
|
|
299
379
|
|
|
@@ -303,8 +383,9 @@ export async function validateListResponse<T extends Record<string, any>>(
|
|
|
303
383
|
if (!Array.isArray(value)) {
|
|
304
384
|
return {
|
|
305
385
|
valid: false,
|
|
306
|
-
error: new
|
|
307
|
-
"
|
|
386
|
+
error: new ResponseStructureError(
|
|
387
|
+
"'value' property to be an array",
|
|
388
|
+
value,
|
|
308
389
|
),
|
|
309
390
|
};
|
|
310
391
|
}
|
|
@@ -324,9 +405,7 @@ export async function validateListResponse<T extends Record<string, any>>(
|
|
|
324
405
|
if (!validation.valid) {
|
|
325
406
|
return {
|
|
326
407
|
valid: false,
|
|
327
|
-
error:
|
|
328
|
-
`Validation failed for record at index ${i}: ${validation.error.message}`,
|
|
329
|
-
),
|
|
408
|
+
error: validation.error,
|
|
330
409
|
};
|
|
331
410
|
}
|
|
332
411
|
|
|
@@ -350,14 +429,19 @@ export async function validateSingleResponse<T extends Record<string, any>>(
|
|
|
350
429
|
mode: "exact" | "maybe" = "maybe",
|
|
351
430
|
): Promise<
|
|
352
431
|
| { valid: true; data: (T & ODataRecordMetadata) | null }
|
|
353
|
-
| { valid: false; error:
|
|
432
|
+
| { valid: false; error: RecordCountMismatchError | ValidationError }
|
|
354
433
|
> {
|
|
355
434
|
// Check for multiple records (error in both modes)
|
|
356
|
-
if (
|
|
435
|
+
if (
|
|
436
|
+
response.value &&
|
|
437
|
+
Array.isArray(response.value) &&
|
|
438
|
+
response.value.length > 1
|
|
439
|
+
) {
|
|
357
440
|
return {
|
|
358
441
|
valid: false,
|
|
359
|
-
error: new
|
|
360
|
-
|
|
442
|
+
error: new RecordCountMismatchError(
|
|
443
|
+
mode === "exact" ? "one" : "at-most-one",
|
|
444
|
+
response.value.length,
|
|
361
445
|
),
|
|
362
446
|
};
|
|
363
447
|
}
|
|
@@ -367,7 +451,7 @@ export async function validateSingleResponse<T extends Record<string, any>>(
|
|
|
367
451
|
if (mode === "exact") {
|
|
368
452
|
return {
|
|
369
453
|
valid: false,
|
|
370
|
-
error: new
|
|
454
|
+
error: new RecordCountMismatchError("one", 0),
|
|
371
455
|
};
|
|
372
456
|
}
|
|
373
457
|
// mode === "maybe" - return null for empty
|
|
@@ -387,7 +471,7 @@ export async function validateSingleResponse<T extends Record<string, any>>(
|
|
|
387
471
|
);
|
|
388
472
|
|
|
389
473
|
if (!validation.valid) {
|
|
390
|
-
return validation as { valid: false; error:
|
|
474
|
+
return validation as { valid: false; error: ValidationError };
|
|
391
475
|
}
|
|
392
476
|
|
|
393
477
|
return {
|