@proofkit/fmodata 0.1.0-alpha.1 → 0.1.0-alpha.10
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 +746 -65
- package/dist/esm/client/base-table.d.ts +117 -5
- package/dist/esm/client/base-table.js +43 -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 +55 -6
- 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 +25 -11
- package/dist/esm/client/entity-set.js +31 -11
- package/dist/esm/client/entity-set.js.map +1 -1
- package/dist/esm/client/filemaker-odata.d.ts +23 -4
- package/dist/esm/client/filemaker-odata.js +124 -29
- 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 +27 -6
- package/dist/esm/client/query-builder.js +457 -210
- package/dist/esm/client/query-builder.js.map +1 -1
- package/dist/esm/client/record-builder.d.ts +96 -9
- package/dist/esm/client/record-builder.js +378 -39
- 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 +48 -1
- package/dist/esm/client/table-occurrence.js +29 -2
- 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 +10 -3
- package/dist/esm/index.js +28 -5
- 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 +158 -8
- package/src/client/batch-builder.ts +265 -0
- package/src/client/batch-request.ts +485 -0
- package/src/client/database.ts +175 -18
- package/src/client/delete-builder.ts +149 -48
- package/src/client/entity-set.ts +114 -23
- package/src/client/filemaker-odata.ts +179 -35
- package/src/client/insert-builder.ts +350 -40
- package/src/client/query-builder.ts +616 -237
- package/src/client/query-builder.ts.bak +1457 -0
- package/src/client/record-builder.ts +692 -65
- package/src/client/response-processor.ts +103 -0
- package/src/client/schema-manager.ts +246 -0
- package/src/client/table-occurrence.ts +78 -3
- package/src/client/update-builder.ts +235 -49
- package/src/errors.ts +217 -0
- package/src/index.ts +59 -2
- package/src/transform.ts +249 -0
- package/src/types.ts +201 -35
- package/src/validation.ts +120 -36
|
@@ -4,7 +4,6 @@ import type {
|
|
|
4
4
|
ExecutionContext,
|
|
5
5
|
ExecutableBuilder,
|
|
6
6
|
WithSystemFields,
|
|
7
|
-
ODataRecordMetadata,
|
|
8
7
|
Result,
|
|
9
8
|
InferSchemaType,
|
|
10
9
|
ExecuteOptions,
|
|
@@ -15,9 +14,24 @@ import type { Filter } from "../filter-types";
|
|
|
15
14
|
import type { TableOccurrence } from "./table-occurrence";
|
|
16
15
|
import type { BaseTable } from "./base-table";
|
|
17
16
|
import { validateListResponse, validateSingleResponse } from "../validation";
|
|
17
|
+
import { RecordCountMismatchError } from "../errors";
|
|
18
18
|
import { type FFetchOptions } from "@fetchkit/ffetch";
|
|
19
|
-
import { z } from "zod/v4";
|
|
20
19
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
20
|
+
import {
|
|
21
|
+
transformFieldNamesArray,
|
|
22
|
+
transformFieldName,
|
|
23
|
+
transformOrderByField,
|
|
24
|
+
transformResponseFields,
|
|
25
|
+
getTableIdentifiers,
|
|
26
|
+
} from "../transform";
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default maximum number of records to return in a list query.
|
|
31
|
+
* This prevents stack overflow issues with large datasets while still
|
|
32
|
+
* allowing substantial data retrieval. Users can override with .top().
|
|
33
|
+
*/
|
|
34
|
+
const DEFAULT_TOP = 1000;
|
|
21
35
|
|
|
22
36
|
// Helper type to extract navigation relation names from an occurrence
|
|
23
37
|
type ExtractNavigationNames<
|
|
@@ -42,19 +56,19 @@ type FindNavigationTarget<
|
|
|
42
56
|
? Name extends keyof Nav
|
|
43
57
|
? ResolveNavigationItem<Nav[Name]>
|
|
44
58
|
: TableOccurrence<
|
|
45
|
-
BaseTable<Record<string,
|
|
59
|
+
BaseTable<Record<string, StandardSchemaV1>, any, any, any>,
|
|
46
60
|
any,
|
|
47
61
|
any,
|
|
48
62
|
any
|
|
49
63
|
>
|
|
50
64
|
: TableOccurrence<
|
|
51
|
-
BaseTable<Record<string,
|
|
65
|
+
BaseTable<Record<string, StandardSchemaV1>, any, any, any>,
|
|
52
66
|
any,
|
|
53
67
|
any,
|
|
54
68
|
any
|
|
55
69
|
>
|
|
56
70
|
: TableOccurrence<
|
|
57
|
-
BaseTable<Record<string,
|
|
71
|
+
BaseTable<Record<string, StandardSchemaV1>, any, any, any>,
|
|
58
72
|
any,
|
|
59
73
|
any,
|
|
60
74
|
any
|
|
@@ -67,7 +81,7 @@ type GetTargetSchemaType<
|
|
|
67
81
|
> = [FindNavigationTarget<O, Rel>] extends [
|
|
68
82
|
TableOccurrence<infer BT, any, any, any>,
|
|
69
83
|
]
|
|
70
|
-
? [BT] extends [BaseTable<infer S, any>]
|
|
84
|
+
? [BT] extends [BaseTable<infer S, any, any, any>]
|
|
71
85
|
? [S] extends [Record<string, StandardSchemaV1>]
|
|
72
86
|
? InferSchemaType<S>
|
|
73
87
|
: Record<string, any>
|
|
@@ -81,7 +95,38 @@ type ExpandConfig = {
|
|
|
81
95
|
};
|
|
82
96
|
|
|
83
97
|
// Type to represent expanded relations
|
|
84
|
-
type ExpandedRelations = Record<string, { schema: any; selected: any }>;
|
|
98
|
+
export type ExpandedRelations = Record<string, { schema: any; selected: any }>;
|
|
99
|
+
|
|
100
|
+
export type QueryReturnType<
|
|
101
|
+
T extends Record<string, any>,
|
|
102
|
+
Selected extends keyof T,
|
|
103
|
+
SingleMode extends "exact" | "maybe" | false,
|
|
104
|
+
IsCount extends boolean,
|
|
105
|
+
Expands extends ExpandedRelations,
|
|
106
|
+
> = IsCount extends true
|
|
107
|
+
? number
|
|
108
|
+
: SingleMode extends "exact"
|
|
109
|
+
? Pick<T, Selected> & {
|
|
110
|
+
[K in keyof Expands]: Pick<
|
|
111
|
+
Expands[K]["schema"],
|
|
112
|
+
Expands[K]["selected"]
|
|
113
|
+
>[];
|
|
114
|
+
}
|
|
115
|
+
: SingleMode extends "maybe"
|
|
116
|
+
?
|
|
117
|
+
| (Pick<T, Selected> & {
|
|
118
|
+
[K in keyof Expands]: Pick<
|
|
119
|
+
Expands[K]["schema"],
|
|
120
|
+
Expands[K]["selected"]
|
|
121
|
+
>[];
|
|
122
|
+
})
|
|
123
|
+
| null
|
|
124
|
+
: (Pick<T, Selected> & {
|
|
125
|
+
[K in keyof Expands]: Pick<
|
|
126
|
+
Expands[K]["schema"],
|
|
127
|
+
Expands[K]["selected"]
|
|
128
|
+
>[];
|
|
129
|
+
})[];
|
|
85
130
|
|
|
86
131
|
export class QueryBuilder<
|
|
87
132
|
T extends Record<string, any>,
|
|
@@ -90,7 +135,11 @@ export class QueryBuilder<
|
|
|
90
135
|
IsCount extends boolean = false,
|
|
91
136
|
Occ extends TableOccurrence<any, any, any, any> | undefined = undefined,
|
|
92
137
|
Expands extends ExpandedRelations = {},
|
|
93
|
-
>
|
|
138
|
+
> implements
|
|
139
|
+
ExecutableBuilder<
|
|
140
|
+
QueryReturnType<T, Selected, SingleMode, IsCount, Expands>
|
|
141
|
+
>
|
|
142
|
+
{
|
|
94
143
|
private queryOptions: Partial<QueryOptions<T>> = {};
|
|
95
144
|
private expandConfigs: ExpandConfig[] = [];
|
|
96
145
|
private singleMode: SingleMode = false as SingleMode;
|
|
@@ -104,16 +153,36 @@ export class QueryBuilder<
|
|
|
104
153
|
private navigateRelation?: string;
|
|
105
154
|
private navigateSourceTableName?: string;
|
|
106
155
|
private navigateBaseRelation?: string;
|
|
156
|
+
private databaseUseEntityIds: boolean;
|
|
157
|
+
|
|
107
158
|
constructor(config: {
|
|
108
159
|
occurrence?: Occ;
|
|
109
160
|
tableName: string;
|
|
110
161
|
databaseName: string;
|
|
111
162
|
context: ExecutionContext;
|
|
163
|
+
databaseUseEntityIds?: boolean;
|
|
112
164
|
}) {
|
|
113
165
|
this.occurrence = config.occurrence;
|
|
114
166
|
this.tableName = config.tableName;
|
|
115
167
|
this.databaseName = config.databaseName;
|
|
116
168
|
this.context = config.context;
|
|
169
|
+
this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Helper to merge database-level useEntityIds with per-request options
|
|
174
|
+
*/
|
|
175
|
+
private mergeExecuteOptions(
|
|
176
|
+
options?: RequestInit & FFetchOptions & ExecuteOptions,
|
|
177
|
+
): RequestInit & FFetchOptions & { useEntityIds?: boolean } {
|
|
178
|
+
// If useEntityIds is not set in options, use the database-level setting
|
|
179
|
+
return {
|
|
180
|
+
...options,
|
|
181
|
+
useEntityIds:
|
|
182
|
+
options?.useEntityIds === undefined
|
|
183
|
+
? this.databaseUseEntityIds
|
|
184
|
+
: options.useEntityIds,
|
|
185
|
+
};
|
|
117
186
|
}
|
|
118
187
|
|
|
119
188
|
/**
|
|
@@ -133,6 +202,31 @@ export class QueryBuilder<
|
|
|
133
202
|
return rest as T;
|
|
134
203
|
}
|
|
135
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Gets the table ID (FMTID) if using entity IDs, otherwise returns the table name
|
|
207
|
+
* @param useEntityIds - Optional override for entity ID usage
|
|
208
|
+
*/
|
|
209
|
+
private getTableId(useEntityIds?: boolean): string {
|
|
210
|
+
if (!this.occurrence) {
|
|
211
|
+
return this.tableName;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const contextDefault = this.context._getUseEntityIds?.() ?? false;
|
|
215
|
+
const shouldUseIds = useEntityIds ?? contextDefault;
|
|
216
|
+
|
|
217
|
+
if (shouldUseIds) {
|
|
218
|
+
const identifiers = getTableIdentifiers(this.occurrence);
|
|
219
|
+
if (!identifiers.id) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`useEntityIds is true but TableOccurrence "${identifiers.name}" does not have an fmtId defined`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return identifiers.id;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return this.occurrence.getTableName();
|
|
228
|
+
}
|
|
229
|
+
|
|
136
230
|
select<K extends keyof T>(
|
|
137
231
|
...fields: K[]
|
|
138
232
|
): QueryBuilder<T, K, SingleMode, IsCount, Occ, Expands> {
|
|
@@ -149,6 +243,7 @@ export class QueryBuilder<
|
|
|
149
243
|
tableName: this.tableName,
|
|
150
244
|
databaseName: this.databaseName,
|
|
151
245
|
context: this.context,
|
|
246
|
+
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
152
247
|
});
|
|
153
248
|
newBuilder.queryOptions = {
|
|
154
249
|
...this.queryOptions,
|
|
@@ -205,16 +300,21 @@ export class QueryBuilder<
|
|
|
205
300
|
const andConditions: any[] = [];
|
|
206
301
|
|
|
207
302
|
for (const [field, value] of Object.entries(filter)) {
|
|
303
|
+
// Transform field name to FMFID if using entity IDs
|
|
304
|
+
const fieldId = this.occurrence?.baseTable
|
|
305
|
+
? transformFieldName(field, this.occurrence.baseTable)
|
|
306
|
+
: field;
|
|
307
|
+
|
|
208
308
|
if (Array.isArray(value)) {
|
|
209
309
|
// Array of operators - convert to AND conditions
|
|
210
310
|
if (value.length === 1) {
|
|
211
311
|
// Single operator in array - unwrap it
|
|
212
|
-
result[
|
|
312
|
+
result[fieldId] = value[0];
|
|
213
313
|
} else {
|
|
214
314
|
// Multiple operators - combine with AND
|
|
215
315
|
// Create separate conditions for each operator
|
|
216
316
|
for (const op of value) {
|
|
217
|
-
andConditions.push({ [
|
|
317
|
+
andConditions.push({ [fieldId]: op });
|
|
218
318
|
}
|
|
219
319
|
}
|
|
220
320
|
} else if (
|
|
@@ -240,14 +340,14 @@ export class QueryBuilder<
|
|
|
240
340
|
|
|
241
341
|
if (isOperatorObject) {
|
|
242
342
|
// Single operator object - pass through
|
|
243
|
-
result[
|
|
343
|
+
result[fieldId] = value;
|
|
244
344
|
} else {
|
|
245
345
|
// Regular object - might be nested filter, pass through
|
|
246
|
-
result[
|
|
346
|
+
result[fieldId] = value;
|
|
247
347
|
}
|
|
248
348
|
} else {
|
|
249
349
|
// Primitive value (shorthand) - pass through
|
|
250
|
-
result[
|
|
350
|
+
result[fieldId] = value;
|
|
251
351
|
}
|
|
252
352
|
}
|
|
253
353
|
|
|
@@ -277,7 +377,21 @@ export class QueryBuilder<
|
|
|
277
377
|
orderBy(
|
|
278
378
|
orderBy: QueryOptions<T>["orderBy"],
|
|
279
379
|
): QueryBuilder<T, Selected, SingleMode, IsCount, Occ, Expands> {
|
|
280
|
-
|
|
380
|
+
// Transform field names to FMFIDs if using entity IDs
|
|
381
|
+
if (this.occurrence?.baseTable && orderBy) {
|
|
382
|
+
if (Array.isArray(orderBy)) {
|
|
383
|
+
this.queryOptions.orderBy = orderBy.map((field) =>
|
|
384
|
+
transformOrderByField(String(field), this.occurrence!.baseTable),
|
|
385
|
+
);
|
|
386
|
+
} else {
|
|
387
|
+
this.queryOptions.orderBy = transformOrderByField(
|
|
388
|
+
String(orderBy),
|
|
389
|
+
this.occurrence.baseTable,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
this.queryOptions.orderBy = orderBy;
|
|
394
|
+
}
|
|
281
395
|
return this;
|
|
282
396
|
}
|
|
283
397
|
|
|
@@ -297,13 +411,26 @@ export class QueryBuilder<
|
|
|
297
411
|
|
|
298
412
|
/**
|
|
299
413
|
* Formats select fields for use in query strings.
|
|
414
|
+
* - Transforms field names to FMFIDs if using entity IDs
|
|
300
415
|
* - Wraps "id" fields in double quotes
|
|
301
416
|
* - URL-encodes special characters but preserves spaces
|
|
302
417
|
*/
|
|
303
|
-
private formatSelectFields(
|
|
418
|
+
private formatSelectFields(
|
|
419
|
+
select: QueryOptions<any>["select"],
|
|
420
|
+
baseTable?: BaseTable<any, any, any, any>,
|
|
421
|
+
): string {
|
|
304
422
|
if (!select) return "";
|
|
305
423
|
const selectFieldsArray = Array.isArray(select) ? select : [select];
|
|
306
|
-
|
|
424
|
+
|
|
425
|
+
// Transform to field IDs if using entity IDs
|
|
426
|
+
const transformedFields = baseTable
|
|
427
|
+
? transformFieldNamesArray(
|
|
428
|
+
selectFieldsArray.map((f) => String(f)),
|
|
429
|
+
baseTable,
|
|
430
|
+
)
|
|
431
|
+
: selectFieldsArray.map((f) => String(f));
|
|
432
|
+
|
|
433
|
+
return transformedFields
|
|
307
434
|
.map((field) => {
|
|
308
435
|
if (field === "id") return `"id"`;
|
|
309
436
|
const encodedField = encodeURIComponent(String(field));
|
|
@@ -335,6 +462,8 @@ export class QueryBuilder<
|
|
|
335
462
|
relation: config.relation,
|
|
336
463
|
targetSchema: targetSchema,
|
|
337
464
|
targetOccurrence: targetOccurrence,
|
|
465
|
+
targetBaseTable: targetOccurrence?.baseTable,
|
|
466
|
+
occurrence: targetOccurrence, // Add occurrence for transformation
|
|
338
467
|
selectedFields: selectedFields,
|
|
339
468
|
nestedExpands: undefined, // TODO: Handle nested expands if needed
|
|
340
469
|
};
|
|
@@ -344,6 +473,7 @@ export class QueryBuilder<
|
|
|
344
473
|
/**
|
|
345
474
|
* Builds OData expand query string from expand configurations.
|
|
346
475
|
* Handles nested expands recursively.
|
|
476
|
+
* Transforms relation names to FMTIDs if using entity IDs.
|
|
347
477
|
*/
|
|
348
478
|
private buildExpandString(configs: ExpandConfig[]): string {
|
|
349
479
|
if (configs.length === 0) {
|
|
@@ -352,20 +482,35 @@ export class QueryBuilder<
|
|
|
352
482
|
|
|
353
483
|
return configs
|
|
354
484
|
.map((config) => {
|
|
485
|
+
// Get target occurrence for this relation
|
|
486
|
+
const targetOccurrence = this.occurrence?.navigation[config.relation];
|
|
487
|
+
|
|
488
|
+
// When using entity IDs, use the target table's FMTID in the expand parameter
|
|
489
|
+
// FileMaker expects FMTID in $expand when Prefer header is set
|
|
490
|
+
const relationName =
|
|
491
|
+
targetOccurrence && targetOccurrence.isUsingTableId()
|
|
492
|
+
? targetOccurrence.getTableId()
|
|
493
|
+
: config.relation;
|
|
494
|
+
|
|
355
495
|
if (!config.options || Object.keys(config.options).length === 0) {
|
|
356
496
|
// Simple expand without options
|
|
357
|
-
return
|
|
497
|
+
return relationName;
|
|
358
498
|
}
|
|
359
499
|
|
|
360
500
|
// Build query options for this expand
|
|
361
501
|
const parts: string[] = [];
|
|
362
502
|
|
|
363
503
|
if (config.options.select) {
|
|
364
|
-
|
|
504
|
+
// Pass target base table for field transformation
|
|
505
|
+
const selectFields = this.formatSelectFields(
|
|
506
|
+
config.options.select,
|
|
507
|
+
targetOccurrence?.baseTable,
|
|
508
|
+
);
|
|
365
509
|
parts.push(`$select=${selectFields}`);
|
|
366
510
|
}
|
|
367
511
|
|
|
368
512
|
if (config.options.filter) {
|
|
513
|
+
// Filter should already be transformed by the nested builder
|
|
369
514
|
// Use odata-query to build filter string
|
|
370
515
|
const filterQuery = buildQuery({ filter: config.options.filter });
|
|
371
516
|
const filterMatch = filterQuery.match(/\$filter=([^&]+)/);
|
|
@@ -375,6 +520,7 @@ export class QueryBuilder<
|
|
|
375
520
|
}
|
|
376
521
|
|
|
377
522
|
if (config.options.orderBy) {
|
|
523
|
+
// OrderBy should already be transformed by the nested builder
|
|
378
524
|
const orderByValue = Array.isArray(config.options.orderBy)
|
|
379
525
|
? config.options.orderBy.join(",")
|
|
380
526
|
: config.options.orderBy;
|
|
@@ -398,10 +544,10 @@ export class QueryBuilder<
|
|
|
398
544
|
}
|
|
399
545
|
|
|
400
546
|
if (parts.length === 0) {
|
|
401
|
-
return
|
|
547
|
+
return relationName;
|
|
402
548
|
}
|
|
403
549
|
|
|
404
|
-
return `${
|
|
550
|
+
return `${relationName}(${parts.join(";")})`;
|
|
405
551
|
})
|
|
406
552
|
.join(",");
|
|
407
553
|
}
|
|
@@ -456,6 +602,7 @@ export class QueryBuilder<
|
|
|
456
602
|
tableName: targetOccurrence?.name ?? (relation as string),
|
|
457
603
|
databaseName: this.databaseName,
|
|
458
604
|
context: this.context,
|
|
605
|
+
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
459
606
|
});
|
|
460
607
|
|
|
461
608
|
// Cast to the expected type for the callback
|
|
@@ -517,6 +664,7 @@ export class QueryBuilder<
|
|
|
517
664
|
tableName: this.tableName,
|
|
518
665
|
databaseName: this.databaseName,
|
|
519
666
|
context: this.context,
|
|
667
|
+
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
520
668
|
});
|
|
521
669
|
newBuilder.queryOptions = { ...this.queryOptions };
|
|
522
670
|
newBuilder.expandConfigs = [...this.expandConfigs];
|
|
@@ -544,6 +692,7 @@ export class QueryBuilder<
|
|
|
544
692
|
tableName: this.tableName,
|
|
545
693
|
databaseName: this.databaseName,
|
|
546
694
|
context: this.context,
|
|
695
|
+
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
547
696
|
});
|
|
548
697
|
newBuilder.queryOptions = { ...this.queryOptions };
|
|
549
698
|
newBuilder.expandConfigs = [...this.expandConfigs];
|
|
@@ -571,6 +720,7 @@ export class QueryBuilder<
|
|
|
571
720
|
tableName: this.tableName,
|
|
572
721
|
databaseName: this.databaseName,
|
|
573
722
|
context: this.context,
|
|
723
|
+
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
574
724
|
});
|
|
575
725
|
newBuilder.queryOptions = { ...this.queryOptions, count: true };
|
|
576
726
|
newBuilder.expandConfigs = [...this.expandConfigs];
|
|
@@ -622,240 +772,180 @@ export class QueryBuilder<
|
|
|
622
772
|
>[]
|
|
623
773
|
>
|
|
624
774
|
> {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
delete queryOptionsWithoutExpand.expand;
|
|
629
|
-
|
|
630
|
-
// Format select fields before building query
|
|
631
|
-
if (queryOptionsWithoutExpand.select) {
|
|
632
|
-
queryOptionsWithoutExpand.select = this.formatSelectFields(
|
|
633
|
-
queryOptionsWithoutExpand.select,
|
|
634
|
-
) as any;
|
|
635
|
-
}
|
|
775
|
+
// Build query without expand (we'll add it manually)
|
|
776
|
+
const queryOptionsWithoutExpand = { ...this.queryOptions };
|
|
777
|
+
delete queryOptionsWithoutExpand.expand;
|
|
636
778
|
|
|
637
|
-
|
|
779
|
+
const mergedOptions = this.mergeExecuteOptions(options);
|
|
638
780
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
781
|
+
// Format select fields before building query
|
|
782
|
+
if (queryOptionsWithoutExpand.select) {
|
|
783
|
+
queryOptionsWithoutExpand.select = this.formatSelectFields(
|
|
784
|
+
queryOptionsWithoutExpand.select,
|
|
785
|
+
this.occurrence?.baseTable,
|
|
786
|
+
) as any;
|
|
787
|
+
}
|
|
645
788
|
|
|
646
|
-
|
|
647
|
-
if (
|
|
648
|
-
this.isNavigate &&
|
|
649
|
-
this.navigateRecordId &&
|
|
650
|
-
this.navigateRelation &&
|
|
651
|
-
this.navigateSourceTableName
|
|
652
|
-
) {
|
|
653
|
-
let url: string;
|
|
654
|
-
if (this.navigateBaseRelation) {
|
|
655
|
-
// Navigation from a navigated EntitySet: /sourceTable/baseRelation('recordId')/relation
|
|
656
|
-
url = `/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateBaseRelation}('${this.navigateRecordId}')/${this.navigateRelation}${queryString}`;
|
|
657
|
-
} else {
|
|
658
|
-
// Normal navigation: /sourceTable('recordId')/relation
|
|
659
|
-
url = `/${this.databaseName}/${this.navigateSourceTableName}('${this.navigateRecordId}')/${this.navigateRelation}${queryString}`;
|
|
660
|
-
}
|
|
661
|
-
const response = await this.context._makeRequest(url, options);
|
|
789
|
+
let queryString = buildQuery(queryOptionsWithoutExpand);
|
|
662
790
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
791
|
+
// Build custom expand string
|
|
792
|
+
const expandString = this.buildExpandString(this.expandConfigs);
|
|
793
|
+
if (expandString) {
|
|
794
|
+
const separator = queryString.includes("?") ? "&" : "?";
|
|
795
|
+
queryString = `${queryString}${separator}$expand=${expandString}`;
|
|
796
|
+
}
|
|
669
797
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
798
|
+
// Handle navigation from RecordBuilder
|
|
799
|
+
if (
|
|
800
|
+
this.isNavigate &&
|
|
801
|
+
this.navigateRecordId &&
|
|
802
|
+
this.navigateRelation &&
|
|
803
|
+
this.navigateSourceTableName
|
|
804
|
+
) {
|
|
805
|
+
let url: string;
|
|
806
|
+
if (this.navigateBaseRelation) {
|
|
807
|
+
// Navigation from a navigated EntitySet: /sourceTable/baseRelation('recordId')/relation
|
|
808
|
+
url = `/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateBaseRelation}('${this.navigateRecordId}')/${this.navigateRelation}${queryString}`;
|
|
809
|
+
} else {
|
|
810
|
+
// Normal navigation: /sourceTable('recordId')/relation
|
|
811
|
+
url = `/${this.databaseName}/${this.navigateSourceTableName}('${this.navigateRecordId}')/${this.navigateRelation}${queryString}`;
|
|
812
|
+
}
|
|
813
|
+
const result = await this.context._makeRequest(url, mergedOptions);
|
|
678
814
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
data: undefined,
|
|
683
|
-
error: new Error(
|
|
684
|
-
"Expected exactly one record, but received none",
|
|
685
|
-
),
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
return { data: null as any, error: undefined };
|
|
689
|
-
}
|
|
815
|
+
if (result.error) {
|
|
816
|
+
return { data: undefined, error: result.error };
|
|
817
|
+
}
|
|
690
818
|
|
|
691
|
-
|
|
692
|
-
const stripped = this.stripODataAnnotationsIfNeeded(
|
|
693
|
-
record,
|
|
694
|
-
options,
|
|
695
|
-
);
|
|
696
|
-
return { data: stripped as any, error: undefined };
|
|
697
|
-
} else {
|
|
698
|
-
const records = resp.value ?? [];
|
|
699
|
-
const stripped = records.map((record: any) =>
|
|
700
|
-
this.stripODataAnnotationsIfNeeded(record, options),
|
|
701
|
-
);
|
|
702
|
-
return { data: stripped as any, error: undefined };
|
|
703
|
-
}
|
|
704
|
-
}
|
|
819
|
+
let response = result.data;
|
|
705
820
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
821
|
+
// Transform response field IDs back to names if using entity IDs
|
|
822
|
+
// Only transform if useEntityIds resolves to true (respects per-request override)
|
|
823
|
+
const shouldUseIds = mergedOptions.useEntityIds ?? false;
|
|
824
|
+
|
|
825
|
+
if (this.occurrence?.baseTable && shouldUseIds) {
|
|
711
826
|
const expandValidationConfigs = this.buildExpandValidationConfigs(
|
|
712
827
|
this.expandConfigs,
|
|
713
828
|
);
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
selectedFields,
|
|
720
|
-
expandValidationConfigs,
|
|
721
|
-
this.singleMode,
|
|
722
|
-
);
|
|
723
|
-
if (!validation.valid) {
|
|
724
|
-
return { data: undefined, error: validation.error };
|
|
725
|
-
}
|
|
726
|
-
const stripped = validation.data
|
|
727
|
-
? this.stripODataAnnotationsIfNeeded(validation.data, options)
|
|
728
|
-
: null;
|
|
729
|
-
return { data: stripped as any, error: undefined };
|
|
730
|
-
} else {
|
|
731
|
-
const validation = await validateListResponse<T>(
|
|
732
|
-
response,
|
|
733
|
-
schema,
|
|
734
|
-
selectedFields,
|
|
735
|
-
expandValidationConfigs,
|
|
736
|
-
);
|
|
737
|
-
if (!validation.valid) {
|
|
738
|
-
return { data: undefined, error: validation.error };
|
|
739
|
-
}
|
|
740
|
-
const stripped = validation.data.map((record) =>
|
|
741
|
-
this.stripODataAnnotationsIfNeeded(record, options),
|
|
742
|
-
);
|
|
743
|
-
return { data: stripped as any, error: undefined };
|
|
744
|
-
}
|
|
829
|
+
response = transformResponseFields(
|
|
830
|
+
response,
|
|
831
|
+
this.occurrence.baseTable,
|
|
832
|
+
expandValidationConfigs,
|
|
833
|
+
);
|
|
745
834
|
}
|
|
746
835
|
|
|
747
|
-
//
|
|
748
|
-
if (
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
) {
|
|
754
|
-
const response = await this.context._makeRequest(
|
|
755
|
-
`/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateRelation}${queryString}`,
|
|
756
|
-
options,
|
|
757
|
-
);
|
|
836
|
+
// Skip validation if requested
|
|
837
|
+
if (options?.skipValidation === true) {
|
|
838
|
+
const resp = response as any;
|
|
839
|
+
if (this.singleMode !== false) {
|
|
840
|
+
const records = resp.value ?? [resp];
|
|
841
|
+
const count = Array.isArray(records) ? records.length : 1;
|
|
758
842
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
843
|
+
if (count > 1) {
|
|
844
|
+
return {
|
|
845
|
+
data: undefined,
|
|
846
|
+
error: new RecordCountMismatchError(
|
|
847
|
+
this.singleMode === "exact" ? "one" : "at-most-one",
|
|
848
|
+
count,
|
|
849
|
+
),
|
|
850
|
+
};
|
|
851
|
+
}
|
|
765
852
|
|
|
766
|
-
|
|
853
|
+
if (count === 0) {
|
|
854
|
+
if (this.singleMode === "exact") {
|
|
767
855
|
return {
|
|
768
856
|
data: undefined,
|
|
769
|
-
error: new
|
|
770
|
-
`Expected ${this.singleMode === "exact" ? "exactly one" : "at most one"} record, but received ${count}`,
|
|
771
|
-
),
|
|
857
|
+
error: new RecordCountMismatchError("one", 0),
|
|
772
858
|
};
|
|
773
859
|
}
|
|
774
|
-
|
|
775
|
-
if (count === 0) {
|
|
776
|
-
if (this.singleMode === "exact") {
|
|
777
|
-
return {
|
|
778
|
-
data: undefined,
|
|
779
|
-
error: new Error(
|
|
780
|
-
"Expected exactly one record, but received none",
|
|
781
|
-
),
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
return { data: null as any, error: undefined };
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
const record = Array.isArray(records) ? records[0] : records;
|
|
788
|
-
const stripped = this.stripODataAnnotationsIfNeeded(
|
|
789
|
-
record,
|
|
790
|
-
options,
|
|
791
|
-
);
|
|
792
|
-
return { data: stripped as any, error: undefined };
|
|
793
|
-
} else {
|
|
794
|
-
const records = resp.value ?? [];
|
|
795
|
-
const stripped = records.map((record: any) =>
|
|
796
|
-
this.stripODataAnnotationsIfNeeded(record, options),
|
|
797
|
-
);
|
|
798
|
-
return { data: stripped as any, error: undefined };
|
|
860
|
+
return { data: null as any, error: undefined };
|
|
799
861
|
}
|
|
800
|
-
}
|
|
801
862
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const selectedFields = this.queryOptions.select as
|
|
805
|
-
| (keyof T)[]
|
|
806
|
-
| undefined;
|
|
807
|
-
const expandValidationConfigs = this.buildExpandValidationConfigs(
|
|
808
|
-
this.expandConfigs,
|
|
809
|
-
);
|
|
810
|
-
|
|
811
|
-
if (this.singleMode !== false) {
|
|
812
|
-
const validation = await validateSingleResponse<T>(
|
|
813
|
-
response,
|
|
814
|
-
schema,
|
|
815
|
-
selectedFields,
|
|
816
|
-
expandValidationConfigs,
|
|
817
|
-
this.singleMode,
|
|
818
|
-
);
|
|
819
|
-
if (!validation.valid) {
|
|
820
|
-
return { data: undefined, error: validation.error };
|
|
821
|
-
}
|
|
822
|
-
const stripped = validation.data
|
|
823
|
-
? this.stripODataAnnotationsIfNeeded(validation.data, options)
|
|
824
|
-
: null;
|
|
863
|
+
const record = Array.isArray(records) ? records[0] : records;
|
|
864
|
+
const stripped = this.stripODataAnnotationsIfNeeded(record, options);
|
|
825
865
|
return { data: stripped as any, error: undefined };
|
|
826
866
|
} else {
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
schema,
|
|
830
|
-
selectedFields,
|
|
831
|
-
expandValidationConfigs,
|
|
832
|
-
);
|
|
833
|
-
if (!validation.valid) {
|
|
834
|
-
return { data: undefined, error: validation.error };
|
|
835
|
-
}
|
|
836
|
-
const stripped = validation.data.map((record) =>
|
|
867
|
+
const records = resp.value ?? [];
|
|
868
|
+
const stripped = records.map((record: any) =>
|
|
837
869
|
this.stripODataAnnotationsIfNeeded(record, options),
|
|
838
870
|
);
|
|
839
871
|
return { data: stripped as any, error: undefined };
|
|
840
872
|
}
|
|
841
873
|
}
|
|
842
874
|
|
|
843
|
-
//
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
875
|
+
// Get schema from occurrence if available
|
|
876
|
+
const schema = this.occurrence?.baseTable?.schema;
|
|
877
|
+
const selectedFields = this.queryOptions.select as
|
|
878
|
+
| (keyof T)[]
|
|
879
|
+
| undefined;
|
|
880
|
+
const expandValidationConfigs = this.buildExpandValidationConfigs(
|
|
881
|
+
this.expandConfigs,
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
if (this.singleMode !== false) {
|
|
885
|
+
const validation = await validateSingleResponse<T>(
|
|
886
|
+
response,
|
|
887
|
+
schema,
|
|
888
|
+
selectedFields,
|
|
889
|
+
expandValidationConfigs,
|
|
890
|
+
this.singleMode,
|
|
891
|
+
);
|
|
892
|
+
if (!validation.valid) {
|
|
893
|
+
return { data: undefined, error: validation.error };
|
|
894
|
+
}
|
|
895
|
+
const stripped = validation.data
|
|
896
|
+
? this.stripODataAnnotationsIfNeeded(validation.data, options)
|
|
897
|
+
: null;
|
|
898
|
+
return { data: stripped as any, error: undefined };
|
|
899
|
+
} else {
|
|
900
|
+
const validation = await validateListResponse<T>(
|
|
901
|
+
response,
|
|
902
|
+
schema,
|
|
903
|
+
selectedFields,
|
|
904
|
+
expandValidationConfigs,
|
|
848
905
|
);
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
906
|
+
if (!validation.valid) {
|
|
907
|
+
return { data: undefined, error: validation.error };
|
|
908
|
+
}
|
|
909
|
+
const stripped = validation.data.map((record) =>
|
|
910
|
+
this.stripODataAnnotationsIfNeeded(record, options),
|
|
911
|
+
);
|
|
912
|
+
return { data: stripped as any, error: undefined };
|
|
852
913
|
}
|
|
914
|
+
}
|
|
853
915
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
916
|
+
// Handle navigation from EntitySet (without record ID)
|
|
917
|
+
if (
|
|
918
|
+
this.isNavigate &&
|
|
919
|
+
!this.navigateRecordId &&
|
|
920
|
+
this.navigateRelation &&
|
|
921
|
+
this.navigateSourceTableName
|
|
922
|
+
) {
|
|
923
|
+
const result = await this.context._makeRequest(
|
|
924
|
+
`/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateRelation}${queryString}`,
|
|
925
|
+
mergedOptions,
|
|
857
926
|
);
|
|
858
927
|
|
|
928
|
+
if (result.error) {
|
|
929
|
+
return { data: undefined, error: result.error };
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
let response = result.data;
|
|
933
|
+
|
|
934
|
+
// Transform response field IDs back to names if using entity IDs
|
|
935
|
+
// Only transform if useEntityIds resolves to true (respects per-request override)
|
|
936
|
+
const shouldUseIds = mergedOptions.useEntityIds ?? false;
|
|
937
|
+
|
|
938
|
+
if (this.occurrence?.baseTable && shouldUseIds) {
|
|
939
|
+
const expandValidationConfigs = this.buildExpandValidationConfigs(
|
|
940
|
+
this.expandConfigs,
|
|
941
|
+
);
|
|
942
|
+
response = transformResponseFields(
|
|
943
|
+
response,
|
|
944
|
+
this.occurrence.baseTable,
|
|
945
|
+
expandValidationConfigs,
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
|
|
859
949
|
// Skip validation if requested
|
|
860
950
|
if (options?.skipValidation === true) {
|
|
861
951
|
const resp = response as any;
|
|
@@ -866,8 +956,9 @@ export class QueryBuilder<
|
|
|
866
956
|
if (count > 1) {
|
|
867
957
|
return {
|
|
868
958
|
data: undefined,
|
|
869
|
-
error: new
|
|
870
|
-
|
|
959
|
+
error: new RecordCountMismatchError(
|
|
960
|
+
this.singleMode === "exact" ? "one" : "at-most-one",
|
|
961
|
+
count,
|
|
871
962
|
),
|
|
872
963
|
};
|
|
873
964
|
}
|
|
@@ -876,9 +967,7 @@ export class QueryBuilder<
|
|
|
876
967
|
if (this.singleMode === "exact") {
|
|
877
968
|
return {
|
|
878
969
|
data: undefined,
|
|
879
|
-
error: new
|
|
880
|
-
"Expected exactly one record, but received none",
|
|
881
|
-
),
|
|
970
|
+
error: new RecordCountMismatchError("one", 0),
|
|
882
971
|
};
|
|
883
972
|
}
|
|
884
973
|
return { data: null as any, error: undefined };
|
|
@@ -888,7 +977,6 @@ export class QueryBuilder<
|
|
|
888
977
|
const stripped = this.stripODataAnnotationsIfNeeded(record, options);
|
|
889
978
|
return { data: stripped as any, error: undefined };
|
|
890
979
|
} else {
|
|
891
|
-
// Handle list response structure
|
|
892
980
|
const records = resp.value ?? [];
|
|
893
981
|
const stripped = records.map((record: any) =>
|
|
894
982
|
this.stripODataAnnotationsIfNeeded(record, options),
|
|
@@ -920,10 +1008,7 @@ export class QueryBuilder<
|
|
|
920
1008
|
const stripped = validation.data
|
|
921
1009
|
? this.stripODataAnnotationsIfNeeded(validation.data, options)
|
|
922
1010
|
: null;
|
|
923
|
-
return {
|
|
924
|
-
data: stripped as any,
|
|
925
|
-
error: undefined,
|
|
926
|
-
};
|
|
1011
|
+
return { data: stripped as any, error: undefined };
|
|
927
1012
|
} else {
|
|
928
1013
|
const validation = await validateListResponse<T>(
|
|
929
1014
|
response,
|
|
@@ -937,15 +1022,136 @@ export class QueryBuilder<
|
|
|
937
1022
|
const stripped = validation.data.map((record) =>
|
|
938
1023
|
this.stripODataAnnotationsIfNeeded(record, options),
|
|
939
1024
|
);
|
|
940
|
-
return {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1025
|
+
return { data: stripped as any, error: undefined };
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Handle $count endpoint
|
|
1030
|
+
if (this.isCountMode) {
|
|
1031
|
+
const tableId = this.getTableId(mergedOptions.useEntityIds);
|
|
1032
|
+
const result = await this.context._makeRequest(
|
|
1033
|
+
`/${this.databaseName}/${tableId}/$count${queryString}`,
|
|
1034
|
+
mergedOptions,
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1037
|
+
if (result.error) {
|
|
1038
|
+
return { data: undefined, error: result.error };
|
|
944
1039
|
}
|
|
945
|
-
|
|
1040
|
+
|
|
1041
|
+
// OData returns count as a string, convert to number
|
|
1042
|
+
const count =
|
|
1043
|
+
typeof result.data === "string" ? Number(result.data) : result.data;
|
|
1044
|
+
return { data: count as number, error: undefined } as any;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const tableId = this.getTableId(mergedOptions.useEntityIds);
|
|
1048
|
+
const result = await this.context._makeRequest(
|
|
1049
|
+
`/${this.databaseName}/${tableId}${queryString}`,
|
|
1050
|
+
mergedOptions,
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
if (result.error) {
|
|
1054
|
+
return { data: undefined, error: result.error };
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
let response = result.data;
|
|
1058
|
+
|
|
1059
|
+
// Transform response field IDs back to names if using entity IDs
|
|
1060
|
+
// Only transform if useEntityIds resolves to true (respects per-request override)
|
|
1061
|
+
const shouldUseIds = mergedOptions.useEntityIds ?? false;
|
|
1062
|
+
|
|
1063
|
+
if (this.occurrence?.baseTable && shouldUseIds) {
|
|
1064
|
+
const expandValidationConfigs = this.buildExpandValidationConfigs(
|
|
1065
|
+
this.expandConfigs,
|
|
1066
|
+
);
|
|
1067
|
+
response = transformResponseFields(
|
|
1068
|
+
response,
|
|
1069
|
+
this.occurrence.baseTable,
|
|
1070
|
+
expandValidationConfigs,
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Skip validation if requested
|
|
1075
|
+
if (options?.skipValidation === true) {
|
|
1076
|
+
const resp = response as any;
|
|
1077
|
+
if (this.singleMode !== false) {
|
|
1078
|
+
const records = resp.value ?? [resp];
|
|
1079
|
+
const count = Array.isArray(records) ? records.length : 1;
|
|
1080
|
+
|
|
1081
|
+
if (count > 1) {
|
|
1082
|
+
return {
|
|
1083
|
+
data: undefined,
|
|
1084
|
+
error: new RecordCountMismatchError(
|
|
1085
|
+
this.singleMode === "exact" ? "one" : "at-most-one",
|
|
1086
|
+
count,
|
|
1087
|
+
),
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (count === 0) {
|
|
1092
|
+
if (this.singleMode === "exact") {
|
|
1093
|
+
return {
|
|
1094
|
+
data: undefined,
|
|
1095
|
+
error: new RecordCountMismatchError("one", 0),
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
return { data: null as any, error: undefined };
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const record = Array.isArray(records) ? records[0] : records;
|
|
1102
|
+
const stripped = this.stripODataAnnotationsIfNeeded(record, options);
|
|
1103
|
+
return { data: stripped as any, error: undefined };
|
|
1104
|
+
} else {
|
|
1105
|
+
// Handle list response structure
|
|
1106
|
+
const records = resp.value ?? [];
|
|
1107
|
+
const stripped = records.map((record: any) =>
|
|
1108
|
+
this.stripODataAnnotationsIfNeeded(record, options),
|
|
1109
|
+
);
|
|
1110
|
+
return { data: stripped as any, error: undefined };
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Get schema from occurrence if available
|
|
1115
|
+
const schema = this.occurrence?.baseTable?.schema;
|
|
1116
|
+
const selectedFields = this.queryOptions.select as (keyof T)[] | undefined;
|
|
1117
|
+
const expandValidationConfigs = this.buildExpandValidationConfigs(
|
|
1118
|
+
this.expandConfigs,
|
|
1119
|
+
);
|
|
1120
|
+
|
|
1121
|
+
if (this.singleMode !== false) {
|
|
1122
|
+
const validation = await validateSingleResponse<T>(
|
|
1123
|
+
response,
|
|
1124
|
+
schema,
|
|
1125
|
+
selectedFields,
|
|
1126
|
+
expandValidationConfigs,
|
|
1127
|
+
this.singleMode,
|
|
1128
|
+
);
|
|
1129
|
+
if (!validation.valid) {
|
|
1130
|
+
return { data: undefined, error: validation.error };
|
|
1131
|
+
}
|
|
1132
|
+
const stripped = validation.data
|
|
1133
|
+
? this.stripODataAnnotationsIfNeeded(validation.data, options)
|
|
1134
|
+
: null;
|
|
946
1135
|
return {
|
|
947
|
-
data:
|
|
948
|
-
error:
|
|
1136
|
+
data: stripped as any,
|
|
1137
|
+
error: undefined,
|
|
1138
|
+
};
|
|
1139
|
+
} else {
|
|
1140
|
+
const validation = await validateListResponse<T>(
|
|
1141
|
+
response,
|
|
1142
|
+
schema,
|
|
1143
|
+
selectedFields,
|
|
1144
|
+
expandValidationConfigs,
|
|
1145
|
+
);
|
|
1146
|
+
if (!validation.valid) {
|
|
1147
|
+
return { data: undefined, error: validation.error };
|
|
1148
|
+
}
|
|
1149
|
+
const stripped = validation.data.map((record) =>
|
|
1150
|
+
this.stripODataAnnotationsIfNeeded(record, options),
|
|
1151
|
+
);
|
|
1152
|
+
return {
|
|
1153
|
+
data: stripped as any,
|
|
1154
|
+
error: undefined,
|
|
949
1155
|
};
|
|
950
1156
|
}
|
|
951
1157
|
}
|
|
@@ -960,6 +1166,7 @@ export class QueryBuilder<
|
|
|
960
1166
|
if (queryOptionsWithoutExpand.select) {
|
|
961
1167
|
queryOptionsWithoutExpand.select = this.formatSelectFields(
|
|
962
1168
|
queryOptionsWithoutExpand.select,
|
|
1169
|
+
this.occurrence?.baseTable,
|
|
963
1170
|
) as any;
|
|
964
1171
|
}
|
|
965
1172
|
|
|
@@ -1026,6 +1233,7 @@ export class QueryBuilder<
|
|
|
1026
1233
|
if (queryOptionsWithoutExpand.select) {
|
|
1027
1234
|
queryOptionsWithoutExpand.select = this.formatSelectFields(
|
|
1028
1235
|
queryOptionsWithoutExpand.select,
|
|
1236
|
+
this.occurrence?.baseTable,
|
|
1029
1237
|
) as any;
|
|
1030
1238
|
}
|
|
1031
1239
|
|
|
@@ -1073,4 +1281,175 @@ export class QueryBuilder<
|
|
|
1073
1281
|
url,
|
|
1074
1282
|
};
|
|
1075
1283
|
}
|
|
1284
|
+
|
|
1285
|
+
toRequest(baseUrl: string): Request {
|
|
1286
|
+
const config = this.getRequestConfig();
|
|
1287
|
+
const fullUrl = `${baseUrl}${config.url}`;
|
|
1288
|
+
|
|
1289
|
+
return new Request(fullUrl, {
|
|
1290
|
+
method: config.method,
|
|
1291
|
+
headers: {
|
|
1292
|
+
"Content-Type": "application/json",
|
|
1293
|
+
Accept: "application/json",
|
|
1294
|
+
},
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
async processResponse(
|
|
1299
|
+
response: Response,
|
|
1300
|
+
options?: ExecuteOptions,
|
|
1301
|
+
): Promise<
|
|
1302
|
+
Result<QueryReturnType<T, Selected, SingleMode, IsCount, Expands>>
|
|
1303
|
+
> {
|
|
1304
|
+
// Handle 204 No Content (shouldn't happen for queries, but handle it gracefully)
|
|
1305
|
+
if (response.status === 204) {
|
|
1306
|
+
// Return empty list for list queries, null for single queries
|
|
1307
|
+
if (this.singleMode !== false) {
|
|
1308
|
+
if (this.singleMode === "maybe") {
|
|
1309
|
+
return { data: null as any, error: undefined };
|
|
1310
|
+
}
|
|
1311
|
+
return {
|
|
1312
|
+
data: undefined,
|
|
1313
|
+
error: new RecordCountMismatchError("one", 0),
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
return { data: [] as any, error: undefined };
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Parse the response body
|
|
1320
|
+
let rawData;
|
|
1321
|
+
try {
|
|
1322
|
+
rawData = await response.json();
|
|
1323
|
+
} catch (err) {
|
|
1324
|
+
// Check if it's an empty body error (common with 204 responses)
|
|
1325
|
+
if (err instanceof SyntaxError && response.status === 204) {
|
|
1326
|
+
// Handled above, but just in case
|
|
1327
|
+
return { data: [] as any, error: undefined };
|
|
1328
|
+
}
|
|
1329
|
+
return {
|
|
1330
|
+
data: undefined,
|
|
1331
|
+
error: {
|
|
1332
|
+
name: "ResponseParseError",
|
|
1333
|
+
message: `Failed to parse response JSON: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
1334
|
+
timestamp: new Date(),
|
|
1335
|
+
} as any,
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if (!rawData) {
|
|
1340
|
+
return {
|
|
1341
|
+
data: undefined,
|
|
1342
|
+
error: {
|
|
1343
|
+
name: "ResponseError",
|
|
1344
|
+
message: "Response body was empty or null",
|
|
1345
|
+
timestamp: new Date(),
|
|
1346
|
+
} as any,
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// Transform response field IDs back to names if using entity IDs
|
|
1351
|
+
// Only transform if useEntityIds resolves to true (respects per-request override)
|
|
1352
|
+
const shouldUseIds = options?.useEntityIds ?? this.databaseUseEntityIds;
|
|
1353
|
+
|
|
1354
|
+
let transformedData = rawData;
|
|
1355
|
+
if (this.occurrence?.baseTable && shouldUseIds) {
|
|
1356
|
+
const expandValidationConfigs = this.buildExpandValidationConfigs(
|
|
1357
|
+
this.expandConfigs,
|
|
1358
|
+
);
|
|
1359
|
+
transformedData = transformResponseFields(
|
|
1360
|
+
rawData,
|
|
1361
|
+
this.occurrence.baseTable,
|
|
1362
|
+
expandValidationConfigs,
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// Skip validation if requested
|
|
1367
|
+
if (options?.skipValidation === true) {
|
|
1368
|
+
const resp = transformedData as any;
|
|
1369
|
+
if (this.singleMode !== false) {
|
|
1370
|
+
const records = resp.value ?? [resp];
|
|
1371
|
+
const count = Array.isArray(records) ? records.length : 1;
|
|
1372
|
+
|
|
1373
|
+
if (count > 1) {
|
|
1374
|
+
return {
|
|
1375
|
+
data: undefined,
|
|
1376
|
+
error: new RecordCountMismatchError(
|
|
1377
|
+
this.singleMode === "exact" ? "one" : "at-most-one",
|
|
1378
|
+
count,
|
|
1379
|
+
),
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
if (count === 0) {
|
|
1384
|
+
if (this.singleMode === "exact") {
|
|
1385
|
+
return {
|
|
1386
|
+
data: undefined,
|
|
1387
|
+
error: new RecordCountMismatchError("one", 0),
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
return { data: null as any, error: undefined };
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const record = Array.isArray(records) ? records[0] : records;
|
|
1394
|
+
const stripped = this.stripODataAnnotationsIfNeeded(record, options);
|
|
1395
|
+
return { data: stripped as any, error: undefined };
|
|
1396
|
+
} else {
|
|
1397
|
+
// Handle list response structure
|
|
1398
|
+
const records = resp.value ?? [];
|
|
1399
|
+
const stripped = records.map((record: any) =>
|
|
1400
|
+
this.stripODataAnnotationsIfNeeded(record, options),
|
|
1401
|
+
);
|
|
1402
|
+
return { data: stripped as any, error: undefined };
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// Get schema from occurrence if available
|
|
1407
|
+
const schema = this.occurrence?.baseTable?.schema;
|
|
1408
|
+
const selectedFields = this.queryOptions.select as (keyof T)[] | undefined;
|
|
1409
|
+
const expandValidationConfigs = this.buildExpandValidationConfigs(
|
|
1410
|
+
this.expandConfigs,
|
|
1411
|
+
);
|
|
1412
|
+
|
|
1413
|
+
if (this.singleMode !== false) {
|
|
1414
|
+
// Single mode (one() or oneOrNull())
|
|
1415
|
+
const validation = await validateSingleResponse<T>(
|
|
1416
|
+
transformedData,
|
|
1417
|
+
schema,
|
|
1418
|
+
selectedFields,
|
|
1419
|
+
expandValidationConfigs,
|
|
1420
|
+
this.singleMode,
|
|
1421
|
+
);
|
|
1422
|
+
|
|
1423
|
+
if (!validation.valid) {
|
|
1424
|
+
return { data: undefined, error: validation.error };
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
if (validation.data === null) {
|
|
1428
|
+
return { data: null as any, error: undefined };
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
const stripped = this.stripODataAnnotationsIfNeeded(
|
|
1432
|
+
validation.data,
|
|
1433
|
+
options,
|
|
1434
|
+
);
|
|
1435
|
+
return { data: stripped as any, error: undefined };
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// List mode
|
|
1439
|
+
const validation = await validateListResponse<T>(
|
|
1440
|
+
transformedData,
|
|
1441
|
+
schema,
|
|
1442
|
+
selectedFields,
|
|
1443
|
+
expandValidationConfigs,
|
|
1444
|
+
);
|
|
1445
|
+
|
|
1446
|
+
if (!validation.valid) {
|
|
1447
|
+
return { data: undefined, error: validation.error };
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
const stripped = validation.data.map((record) =>
|
|
1451
|
+
this.stripODataAnnotationsIfNeeded(record, options),
|
|
1452
|
+
);
|
|
1453
|
+
return { data: stripped as any, error: undefined };
|
|
1454
|
+
}
|
|
1076
1455
|
}
|