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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +651 -449
  3. package/dist/esm/client/batch-builder.d.ts +10 -9
  4. package/dist/esm/client/batch-builder.js +119 -56
  5. package/dist/esm/client/batch-builder.js.map +1 -1
  6. package/dist/esm/client/batch-request.js +16 -21
  7. package/dist/esm/client/batch-request.js.map +1 -1
  8. package/dist/esm/client/builders/default-select.d.ts +10 -0
  9. package/dist/esm/client/builders/default-select.js +41 -0
  10. package/dist/esm/client/builders/default-select.js.map +1 -0
  11. package/dist/esm/client/builders/expand-builder.d.ts +45 -0
  12. package/dist/esm/client/builders/expand-builder.js +185 -0
  13. package/dist/esm/client/builders/expand-builder.js.map +1 -0
  14. package/dist/esm/client/builders/index.d.ts +9 -0
  15. package/dist/esm/client/builders/query-string-builder.d.ts +18 -0
  16. package/dist/esm/client/builders/query-string-builder.js +21 -0
  17. package/dist/esm/client/builders/query-string-builder.js.map +1 -0
  18. package/dist/esm/client/builders/response-processor.d.ts +43 -0
  19. package/dist/esm/client/builders/response-processor.js +175 -0
  20. package/dist/esm/client/builders/response-processor.js.map +1 -0
  21. package/dist/esm/client/builders/select-mixin.d.ts +25 -0
  22. package/dist/esm/client/builders/select-mixin.js +28 -0
  23. package/dist/esm/client/builders/select-mixin.js.map +1 -0
  24. package/dist/esm/client/builders/select-utils.d.ts +18 -0
  25. package/dist/esm/client/builders/select-utils.js +30 -0
  26. package/dist/esm/client/builders/select-utils.js.map +1 -0
  27. package/dist/esm/client/builders/shared-types.d.ts +40 -0
  28. package/dist/esm/client/builders/table-utils.d.ts +35 -0
  29. package/dist/esm/client/builders/table-utils.js +44 -0
  30. package/dist/esm/client/builders/table-utils.js.map +1 -0
  31. package/dist/esm/client/database.d.ts +34 -22
  32. package/dist/esm/client/database.js +48 -84
  33. package/dist/esm/client/database.js.map +1 -1
  34. package/dist/esm/client/delete-builder.d.ts +25 -30
  35. package/dist/esm/client/delete-builder.js +45 -30
  36. package/dist/esm/client/delete-builder.js.map +1 -1
  37. package/dist/esm/client/entity-set.d.ts +35 -43
  38. package/dist/esm/client/entity-set.js +110 -52
  39. package/dist/esm/client/entity-set.js.map +1 -1
  40. package/dist/esm/client/error-parser.d.ts +12 -0
  41. package/dist/esm/client/error-parser.js +25 -0
  42. package/dist/esm/client/error-parser.js.map +1 -0
  43. package/dist/esm/client/filemaker-odata.d.ts +26 -7
  44. package/dist/esm/client/filemaker-odata.js +65 -42
  45. package/dist/esm/client/filemaker-odata.js.map +1 -1
  46. package/dist/esm/client/insert-builder.d.ts +19 -24
  47. package/dist/esm/client/insert-builder.js +94 -58
  48. package/dist/esm/client/insert-builder.js.map +1 -1
  49. package/dist/esm/client/query/expand-builder.d.ts +35 -0
  50. package/dist/esm/client/query/index.d.ts +4 -0
  51. package/dist/esm/client/query/query-builder.d.ts +132 -0
  52. package/dist/esm/client/query/query-builder.js +456 -0
  53. package/dist/esm/client/query/query-builder.js.map +1 -0
  54. package/dist/esm/client/query/response-processor.d.ts +25 -0
  55. package/dist/esm/client/query/types.d.ts +77 -0
  56. package/dist/esm/client/query/url-builder.d.ts +71 -0
  57. package/dist/esm/client/query/url-builder.js +100 -0
  58. package/dist/esm/client/query/url-builder.js.map +1 -0
  59. package/dist/esm/client/query-builder.d.ts +2 -115
  60. package/dist/esm/client/record-builder.d.ts +108 -36
  61. package/dist/esm/client/record-builder.js +284 -119
  62. package/dist/esm/client/record-builder.js.map +1 -1
  63. package/dist/esm/client/response-processor.d.ts +4 -9
  64. package/dist/esm/client/sanitize-json.d.ts +35 -0
  65. package/dist/esm/client/sanitize-json.js +27 -0
  66. package/dist/esm/client/sanitize-json.js.map +1 -0
  67. package/dist/esm/client/schema-manager.d.ts +5 -5
  68. package/dist/esm/client/schema-manager.js +45 -31
  69. package/dist/esm/client/schema-manager.js.map +1 -1
  70. package/dist/esm/client/update-builder.d.ts +34 -40
  71. package/dist/esm/client/update-builder.js +99 -58
  72. package/dist/esm/client/update-builder.js.map +1 -1
  73. package/dist/esm/client/webhook-builder.d.ts +126 -0
  74. package/dist/esm/client/webhook-builder.js +189 -0
  75. package/dist/esm/client/webhook-builder.js.map +1 -0
  76. package/dist/esm/errors.d.ts +19 -2
  77. package/dist/esm/errors.js +39 -4
  78. package/dist/esm/errors.js.map +1 -1
  79. package/dist/esm/index.d.ts +10 -8
  80. package/dist/esm/index.js +40 -10
  81. package/dist/esm/index.js.map +1 -1
  82. package/dist/esm/logger.d.ts +47 -0
  83. package/dist/esm/logger.js +69 -0
  84. package/dist/esm/logger.js.map +1 -0
  85. package/dist/esm/logger.test.d.ts +1 -0
  86. package/dist/esm/orm/column.d.ts +62 -0
  87. package/dist/esm/orm/column.js +63 -0
  88. package/dist/esm/orm/column.js.map +1 -0
  89. package/dist/esm/orm/field-builders.d.ts +164 -0
  90. package/dist/esm/orm/field-builders.js +158 -0
  91. package/dist/esm/orm/field-builders.js.map +1 -0
  92. package/dist/esm/orm/index.d.ts +5 -0
  93. package/dist/esm/orm/operators.d.ts +173 -0
  94. package/dist/esm/orm/operators.js +260 -0
  95. package/dist/esm/orm/operators.js.map +1 -0
  96. package/dist/esm/orm/table.d.ts +355 -0
  97. package/dist/esm/orm/table.js +202 -0
  98. package/dist/esm/orm/table.js.map +1 -0
  99. package/dist/esm/transform.d.ts +20 -21
  100. package/dist/esm/transform.js +44 -45
  101. package/dist/esm/transform.js.map +1 -1
  102. package/dist/esm/types.d.ts +96 -30
  103. package/dist/esm/types.js +7 -0
  104. package/dist/esm/types.js.map +1 -0
  105. package/dist/esm/validation.d.ts +22 -12
  106. package/dist/esm/validation.js +132 -85
  107. package/dist/esm/validation.js.map +1 -1
  108. package/package.json +28 -20
  109. package/src/client/batch-builder.ts +153 -89
  110. package/src/client/batch-request.ts +25 -41
  111. package/src/client/builders/default-select.ts +75 -0
  112. package/src/client/builders/expand-builder.ts +246 -0
  113. package/src/client/builders/index.ts +11 -0
  114. package/src/client/builders/query-string-builder.ts +46 -0
  115. package/src/client/builders/response-processor.ts +279 -0
  116. package/src/client/builders/select-mixin.ts +65 -0
  117. package/src/client/builders/select-utils.ts +59 -0
  118. package/src/client/builders/shared-types.ts +45 -0
  119. package/src/client/builders/table-utils.ts +83 -0
  120. package/src/client/database.ts +89 -183
  121. package/src/client/delete-builder.ts +74 -84
  122. package/src/client/entity-set.ts +266 -293
  123. package/src/client/error-parser.ts +41 -0
  124. package/src/client/filemaker-odata.ts +98 -66
  125. package/src/client/insert-builder.ts +157 -118
  126. package/src/client/query/expand-builder.ts +160 -0
  127. package/src/client/query/index.ts +14 -0
  128. package/src/client/query/query-builder.ts +729 -0
  129. package/src/client/query/response-processor.ts +226 -0
  130. package/src/client/query/types.ts +126 -0
  131. package/src/client/query/url-builder.ts +151 -0
  132. package/src/client/query-builder.ts +10 -1455
  133. package/src/client/record-builder.ts +575 -240
  134. package/src/client/response-processor.ts +15 -42
  135. package/src/client/sanitize-json.ts +64 -0
  136. package/src/client/schema-manager.ts +61 -76
  137. package/src/client/update-builder.ts +161 -143
  138. package/src/client/webhook-builder.ts +265 -0
  139. package/src/errors.ts +49 -16
  140. package/src/index.ts +99 -54
  141. package/src/logger.test.ts +34 -0
  142. package/src/logger.ts +116 -0
  143. package/src/orm/column.ts +106 -0
  144. package/src/orm/field-builders.ts +250 -0
  145. package/src/orm/index.ts +61 -0
  146. package/src/orm/operators.ts +473 -0
  147. package/src/orm/table.ts +741 -0
  148. package/src/transform.ts +90 -70
  149. package/src/types.ts +154 -113
  150. package/src/validation.ts +200 -115
  151. package/dist/esm/client/base-table.d.ts +0 -125
  152. package/dist/esm/client/base-table.js +0 -57
  153. package/dist/esm/client/base-table.js.map +0 -1
  154. package/dist/esm/client/query-builder.js +0 -896
  155. package/dist/esm/client/query-builder.js.map +0 -1
  156. package/dist/esm/client/table-occurrence.d.ts +0 -72
  157. package/dist/esm/client/table-occurrence.js +0 -74
  158. package/dist/esm/client/table-occurrence.js.map +0 -1
  159. package/dist/esm/filter-types.d.ts +0 -76
  160. package/src/client/base-table.ts +0 -166
  161. package/src/client/query-builder.ts.bak +0 -1457
  162. package/src/client/table-occurrence.ts +0 -175
  163. package/src/filter-types.ts +0 -97
@@ -1,1455 +1,10 @@
1
- import { QueryOptions } from "odata-query";
2
- import buildQuery from "odata-query";
3
- import type {
4
- ExecutionContext,
5
- ExecutableBuilder,
6
- WithSystemFields,
7
- Result,
8
- InferSchemaType,
9
- ExecuteOptions,
10
- ConditionallyWithODataAnnotations,
11
- ExtractSchemaFromOccurrence,
12
- } from "../types";
13
- import type { Filter } from "../filter-types";
14
- import type { TableOccurrence } from "./table-occurrence";
15
- import type { BaseTable } from "./base-table";
16
- import { validateListResponse, validateSingleResponse } from "../validation";
17
- import { RecordCountMismatchError } from "../errors";
18
- import { type FFetchOptions } from "@fetchkit/ffetch";
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;
35
-
36
- // Helper type to extract navigation relation names from an occurrence
37
- type ExtractNavigationNames<
38
- O extends TableOccurrence<any, any, any, any> | undefined,
39
- > =
40
- O extends TableOccurrence<any, any, infer Nav, any>
41
- ? Nav extends Record<string, any>
42
- ? keyof Nav & string
43
- : never
44
- : never;
45
-
46
- // Helper type to resolve a navigation item (handles both direct and lazy-loaded)
47
- type ResolveNavigationItem<T> = T extends () => infer R ? R : T;
48
-
49
- // Helper type to find target occurrence by relation name
50
- type FindNavigationTarget<
51
- O extends TableOccurrence<any, any, any, any> | undefined,
52
- Name extends string,
53
- > =
54
- O extends TableOccurrence<any, any, infer Nav, any>
55
- ? Nav extends Record<string, any>
56
- ? Name extends keyof Nav
57
- ? ResolveNavigationItem<Nav[Name]>
58
- : TableOccurrence<
59
- BaseTable<Record<string, StandardSchemaV1>, any, any, any>,
60
- any,
61
- any,
62
- any
63
- >
64
- : TableOccurrence<
65
- BaseTable<Record<string, StandardSchemaV1>, any, any, any>,
66
- any,
67
- any,
68
- any
69
- >
70
- : TableOccurrence<
71
- BaseTable<Record<string, StandardSchemaV1>, any, any, any>,
72
- any,
73
- any,
74
- any
75
- >;
76
-
77
- // Helper type to get the inferred schema type from a target occurrence
78
- type GetTargetSchemaType<
79
- O extends TableOccurrence<any, any, any, any> | undefined,
80
- Rel extends string,
81
- > = [FindNavigationTarget<O, Rel>] extends [
82
- TableOccurrence<infer BT, any, any, any>,
83
- ]
84
- ? [BT] extends [BaseTable<infer S, any, any, any>]
85
- ? [S] extends [Record<string, StandardSchemaV1>]
86
- ? InferSchemaType<S>
87
- : Record<string, any>
88
- : Record<string, any>
89
- : Record<string, any>;
90
-
91
- // Internal type for expand configuration
92
- type ExpandConfig = {
93
- relation: string;
94
- options?: Partial<QueryOptions<any>>;
95
- };
96
-
97
- // Type to represent expanded relations
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
- })[];
130
-
131
- export class QueryBuilder<
132
- T extends Record<string, any>,
133
- Selected extends keyof T = keyof T,
134
- SingleMode extends "exact" | "maybe" | false = false,
135
- IsCount extends boolean = false,
136
- Occ extends TableOccurrence<any, any, any, any> | undefined = undefined,
137
- Expands extends ExpandedRelations = {},
138
- > implements
139
- ExecutableBuilder<
140
- QueryReturnType<T, Selected, SingleMode, IsCount, Expands>
141
- >
142
- {
143
- private queryOptions: Partial<QueryOptions<T>> = {};
144
- private expandConfigs: ExpandConfig[] = [];
145
- private singleMode: SingleMode = false as SingleMode;
146
- private isCountMode = false as IsCount;
147
- private occurrence?: Occ;
148
- private tableName: string;
149
- private databaseName: string;
150
- private context: ExecutionContext;
151
- private isNavigate?: boolean;
152
- private navigateRecordId?: string | number;
153
- private navigateRelation?: string;
154
- private navigateSourceTableName?: string;
155
- private navigateBaseRelation?: string;
156
- private databaseUseEntityIds: boolean;
157
-
158
- constructor(config: {
159
- occurrence?: Occ;
160
- tableName: string;
161
- databaseName: string;
162
- context: ExecutionContext;
163
- databaseUseEntityIds?: boolean;
164
- }) {
165
- this.occurrence = config.occurrence;
166
- this.tableName = config.tableName;
167
- this.databaseName = config.databaseName;
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
- };
186
- }
187
-
188
- /**
189
- * Helper to conditionally strip OData annotations based on options
190
- */
191
- private stripODataAnnotationsIfNeeded<T extends Record<string, any>>(
192
- data: T,
193
- options?: ExecuteOptions,
194
- ): T {
195
- // Only include annotations if explicitly requested
196
- if (options?.includeODataAnnotations === true) {
197
- return data;
198
- }
199
-
200
- // Strip OData annotations
201
- const { "@id": _id, "@editLink": _editLink, ...rest } = data;
202
- return rest as T;
203
- }
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
-
230
- select<K extends keyof T>(
231
- ...fields: K[]
232
- ): QueryBuilder<T, K, SingleMode, IsCount, Occ, Expands> {
233
- const uniqueFields = [...new Set(fields)];
234
- const newBuilder = new QueryBuilder<
235
- T,
236
- K,
237
- SingleMode,
238
- IsCount,
239
- Occ,
240
- Expands
241
- >({
242
- occurrence: this.occurrence,
243
- tableName: this.tableName,
244
- databaseName: this.databaseName,
245
- context: this.context,
246
- databaseUseEntityIds: this.databaseUseEntityIds,
247
- });
248
- newBuilder.queryOptions = {
249
- ...this.queryOptions,
250
- select: uniqueFields as string[],
251
- };
252
- newBuilder.expandConfigs = [...this.expandConfigs];
253
- newBuilder.singleMode = this.singleMode;
254
- newBuilder.isCountMode = this.isCountMode;
255
- // Preserve navigation metadata
256
- newBuilder.isNavigate = this.isNavigate;
257
- newBuilder.navigateRecordId = this.navigateRecordId;
258
- newBuilder.navigateRelation = this.navigateRelation;
259
- newBuilder.navigateSourceTableName = this.navigateSourceTableName;
260
- newBuilder.navigateBaseRelation = this.navigateBaseRelation;
261
- return newBuilder;
262
- }
263
-
264
- /**
265
- * Transforms our filter format to odata-query's expected format
266
- * - Arrays of operators are converted to AND conditions
267
- * - Single operator objects pass through as-is
268
- * - Shorthand values are handled by odata-query
269
- */
270
- private transformFilter(
271
- filter: Filter<ExtractSchemaFromOccurrence<Occ>>,
272
- ): QueryOptions<T>["filter"] {
273
- if (typeof filter === "string") {
274
- // Raw string filters pass through
275
- return filter;
276
- }
277
-
278
- if (Array.isArray(filter)) {
279
- // Array of filters - odata-query handles this as implicit AND
280
- return filter.map((f) => this.transformFilter(f as any)) as any;
281
- }
282
-
283
- // Check if it's a logical filter (and/or/not)
284
- if ("and" in filter || "or" in filter || "not" in filter) {
285
- const result: any = {};
286
- if ("and" in filter && Array.isArray(filter.and)) {
287
- result.and = filter.and.map((f: any) => this.transformFilter(f));
288
- }
289
- if ("or" in filter && Array.isArray(filter.or)) {
290
- result.or = filter.or.map((f: any) => this.transformFilter(f));
291
- }
292
- if ("not" in filter && filter.not) {
293
- result.not = this.transformFilter(filter.not as any);
294
- }
295
- return result;
296
- }
297
-
298
- // Transform field filters
299
- const result: any = {};
300
- const andConditions: any[] = [];
301
-
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
-
308
- if (Array.isArray(value)) {
309
- // Array of operators - convert to AND conditions
310
- if (value.length === 1) {
311
- // Single operator in array - unwrap it
312
- result[fieldId] = value[0];
313
- } else {
314
- // Multiple operators - combine with AND
315
- // Create separate conditions for each operator
316
- for (const op of value) {
317
- andConditions.push({ [fieldId]: op });
318
- }
319
- }
320
- } else if (
321
- value &&
322
- typeof value === "object" &&
323
- !(value instanceof Date) &&
324
- !Array.isArray(value)
325
- ) {
326
- // Check if it's an operator object (has operator keys like eq, gt, etc.)
327
- const operatorKeys = [
328
- "eq",
329
- "ne",
330
- "gt",
331
- "ge",
332
- "lt",
333
- "le",
334
- "contains",
335
- "startswith",
336
- "endswith",
337
- "in",
338
- ];
339
- const isOperatorObject = operatorKeys.some((key) => key in value);
340
-
341
- if (isOperatorObject) {
342
- // Single operator object - pass through
343
- result[fieldId] = value;
344
- } else {
345
- // Regular object - might be nested filter, pass through
346
- result[fieldId] = value;
347
- }
348
- } else {
349
- // Primitive value (shorthand) - pass through
350
- result[fieldId] = value;
351
- }
352
- }
353
-
354
- // If we have AND conditions from arrays, combine them
355
- if (andConditions.length > 0) {
356
- if (Object.keys(result).length > 0) {
357
- // We have both regular fields and array-derived AND conditions
358
- // Combine everything with AND
359
- return { and: [...andConditions, result] };
360
- } else {
361
- // Only array-derived AND conditions
362
- return { and: andConditions };
363
- }
364
- }
365
-
366
- return result;
367
- }
368
-
369
- filter(
370
- filter: Filter<ExtractSchemaFromOccurrence<Occ>>,
371
- ): QueryBuilder<T, Selected, SingleMode, IsCount, Occ, Expands> {
372
- // Transform our filter format to odata-query's expected format
373
- this.queryOptions.filter = this.transformFilter(filter) as any;
374
- return this;
375
- }
376
-
377
- orderBy(
378
- orderBy: QueryOptions<T>["orderBy"],
379
- ): QueryBuilder<T, Selected, SingleMode, IsCount, Occ, Expands> {
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
- }
395
- return this;
396
- }
397
-
398
- top(
399
- count: number,
400
- ): QueryBuilder<T, Selected, SingleMode, IsCount, Occ, Expands> {
401
- this.queryOptions.top = count;
402
- return this;
403
- }
404
-
405
- skip(
406
- count: number,
407
- ): QueryBuilder<T, Selected, SingleMode, IsCount, Occ, Expands> {
408
- this.queryOptions.skip = count;
409
- return this;
410
- }
411
-
412
- /**
413
- * Formats select fields for use in query strings.
414
- * - Transforms field names to FMFIDs if using entity IDs
415
- * - Wraps "id" fields in double quotes
416
- * - URL-encodes special characters but preserves spaces
417
- */
418
- private formatSelectFields(
419
- select: QueryOptions<any>["select"],
420
- baseTable?: BaseTable<any, any, any, any>,
421
- ): string {
422
- if (!select) return "";
423
- const selectFieldsArray = Array.isArray(select) ? select : [select];
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
434
- .map((field) => {
435
- if (field === "id") return `"id"`;
436
- const encodedField = encodeURIComponent(String(field));
437
- return encodedField.replace(/%20/g, " ");
438
- })
439
- .join(",");
440
- }
441
-
442
- /**
443
- * Builds expand validation configs from internal expand configurations.
444
- * These are used to validate expanded navigation properties.
445
- */
446
- private buildExpandValidationConfigs(
447
- configs: ExpandConfig[],
448
- ): import("../validation").ExpandValidationConfig[] {
449
- return configs.map((config) => {
450
- // Look up target occurrence from navigation
451
- const targetOccurrence = this.occurrence?.navigation[config.relation];
452
- const targetSchema = targetOccurrence?.baseTable?.schema;
453
-
454
- // Extract selected fields from options
455
- const selectedFields = config.options?.select
456
- ? Array.isArray(config.options.select)
457
- ? config.options.select.map((f) => String(f))
458
- : [String(config.options.select)]
459
- : undefined;
460
-
461
- return {
462
- relation: config.relation,
463
- targetSchema: targetSchema,
464
- targetOccurrence: targetOccurrence,
465
- targetBaseTable: targetOccurrence?.baseTable,
466
- occurrence: targetOccurrence, // Add occurrence for transformation
467
- selectedFields: selectedFields,
468
- nestedExpands: undefined, // TODO: Handle nested expands if needed
469
- };
470
- });
471
- }
472
-
473
- /**
474
- * Builds OData expand query string from expand configurations.
475
- * Handles nested expands recursively.
476
- * Transforms relation names to FMTIDs if using entity IDs.
477
- */
478
- private buildExpandString(configs: ExpandConfig[]): string {
479
- if (configs.length === 0) {
480
- return "";
481
- }
482
-
483
- return configs
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
-
495
- if (!config.options || Object.keys(config.options).length === 0) {
496
- // Simple expand without options
497
- return relationName;
498
- }
499
-
500
- // Build query options for this expand
501
- const parts: string[] = [];
502
-
503
- if (config.options.select) {
504
- // Pass target base table for field transformation
505
- const selectFields = this.formatSelectFields(
506
- config.options.select,
507
- targetOccurrence?.baseTable,
508
- );
509
- parts.push(`$select=${selectFields}`);
510
- }
511
-
512
- if (config.options.filter) {
513
- // Filter should already be transformed by the nested builder
514
- // Use odata-query to build filter string
515
- const filterQuery = buildQuery({ filter: config.options.filter });
516
- const filterMatch = filterQuery.match(/\$filter=([^&]+)/);
517
- if (filterMatch) {
518
- parts.push(`$filter=${filterMatch[1]}`);
519
- }
520
- }
521
-
522
- if (config.options.orderBy) {
523
- // OrderBy should already be transformed by the nested builder
524
- const orderByValue = Array.isArray(config.options.orderBy)
525
- ? config.options.orderBy.join(",")
526
- : config.options.orderBy;
527
- parts.push(`$orderby=${String(orderByValue)}`);
528
- }
529
-
530
- if (config.options.top !== undefined) {
531
- parts.push(`$top=${config.options.top}`);
532
- }
533
-
534
- if (config.options.skip !== undefined) {
535
- parts.push(`$skip=${config.options.skip}`);
536
- }
537
-
538
- // Handle nested expands (from expand configs)
539
- if (config.options.expand) {
540
- // If expand is a string, it's already been built
541
- if (typeof config.options.expand === "string") {
542
- parts.push(`$expand=${config.options.expand}`);
543
- }
544
- }
545
-
546
- if (parts.length === 0) {
547
- return relationName;
548
- }
549
-
550
- return `${relationName}(${parts.join(";")})`;
551
- })
552
- .join(",");
553
- }
554
-
555
- expand<
556
- Rel extends ExtractNavigationNames<Occ> | (string & {}),
557
- TargetOcc extends FindNavigationTarget<Occ, Rel> = FindNavigationTarget<
558
- Occ,
559
- Rel
560
- >,
561
- TargetSchema extends GetTargetSchemaType<Occ, Rel> = GetTargetSchemaType<
562
- Occ,
563
- Rel
564
- >,
565
- TargetSelected extends keyof TargetSchema = keyof TargetSchema,
566
- >(
567
- relation: Rel,
568
- callback?: (
569
- builder: QueryBuilder<
570
- TargetSchema,
571
- keyof TargetSchema,
572
- false,
573
- false,
574
- TargetOcc extends TableOccurrence<any, any, any, any>
575
- ? TargetOcc
576
- : undefined
577
- >,
578
- ) => QueryBuilder<
579
- WithSystemFields<TargetSchema>,
580
- TargetSelected,
581
- any,
582
- any,
583
- any
584
- >,
585
- ): QueryBuilder<
586
- T,
587
- Selected,
588
- SingleMode,
589
- IsCount,
590
- Occ,
591
- Expands & {
592
- [K in Rel]: { schema: TargetSchema; selected: TargetSelected };
593
- }
594
- > {
595
- // Look up target occurrence from navigation
596
- const targetOccurrence = this.occurrence?.navigation[relation as string];
597
-
598
- if (callback) {
599
- // Create a new QueryBuilder for the target occurrence
600
- const targetBuilder = new QueryBuilder<any>({
601
- occurrence: targetOccurrence,
602
- tableName: targetOccurrence?.name ?? (relation as string),
603
- databaseName: this.databaseName,
604
- context: this.context,
605
- databaseUseEntityIds: this.databaseUseEntityIds,
606
- });
607
-
608
- // Cast to the expected type for the callback
609
- // At runtime, the builder is untyped (any), but at compile-time we enforce proper types
610
- const typedBuilder = targetBuilder as QueryBuilder<
611
- TargetSchema,
612
- keyof TargetSchema,
613
- false,
614
- false,
615
- TargetOcc extends TableOccurrence<any, any, any, any>
616
- ? TargetOcc
617
- : undefined
618
- >;
619
-
620
- // Pass to callback and get configured builder
621
- const configuredBuilder = callback(typedBuilder);
622
-
623
- // Extract the builder's query options
624
- const expandOptions: Partial<QueryOptions<any>> = {
625
- ...configuredBuilder.queryOptions,
626
- };
627
-
628
- // If the configured builder has nested expands, we need to include them
629
- if (configuredBuilder.expandConfigs.length > 0) {
630
- // Build nested expand string from the configured builder's expand configs
631
- const nestedExpandString = this.buildExpandString(
632
- configuredBuilder.expandConfigs,
633
- );
634
- if (nestedExpandString) {
635
- // Add nested expand to options
636
- expandOptions.expand = nestedExpandString as any;
637
- }
638
- }
639
-
640
- const expandConfig: ExpandConfig = {
641
- relation: relation as string,
642
- options: expandOptions,
643
- };
644
-
645
- this.expandConfigs.push(expandConfig);
646
- } else {
647
- // Simple expand without callback
648
- this.expandConfigs.push({ relation: relation as string });
649
- }
650
-
651
- return this as any;
652
- }
653
-
654
- single(): QueryBuilder<T, Selected, "exact", IsCount, Occ, Expands> {
655
- const newBuilder = new QueryBuilder<
656
- T,
657
- Selected,
658
- "exact",
659
- IsCount,
660
- Occ,
661
- Expands
662
- >({
663
- occurrence: this.occurrence,
664
- tableName: this.tableName,
665
- databaseName: this.databaseName,
666
- context: this.context,
667
- databaseUseEntityIds: this.databaseUseEntityIds,
668
- });
669
- newBuilder.queryOptions = { ...this.queryOptions };
670
- newBuilder.expandConfigs = [...this.expandConfigs];
671
- newBuilder.singleMode = "exact";
672
- newBuilder.isCountMode = this.isCountMode;
673
- // Preserve navigation metadata
674
- newBuilder.isNavigate = this.isNavigate;
675
- newBuilder.navigateRecordId = this.navigateRecordId;
676
- newBuilder.navigateRelation = this.navigateRelation;
677
- newBuilder.navigateSourceTableName = this.navigateSourceTableName;
678
- newBuilder.navigateBaseRelation = this.navigateBaseRelation;
679
- return newBuilder;
680
- }
681
-
682
- maybeSingle(): QueryBuilder<T, Selected, "maybe", IsCount, Occ, Expands> {
683
- const newBuilder = new QueryBuilder<
684
- T,
685
- Selected,
686
- "maybe",
687
- IsCount,
688
- Occ,
689
- Expands
690
- >({
691
- occurrence: this.occurrence,
692
- tableName: this.tableName,
693
- databaseName: this.databaseName,
694
- context: this.context,
695
- databaseUseEntityIds: this.databaseUseEntityIds,
696
- });
697
- newBuilder.queryOptions = { ...this.queryOptions };
698
- newBuilder.expandConfigs = [...this.expandConfigs];
699
- newBuilder.singleMode = "maybe";
700
- newBuilder.isCountMode = this.isCountMode;
701
- // Preserve navigation metadata
702
- newBuilder.isNavigate = this.isNavigate;
703
- newBuilder.navigateRecordId = this.navigateRecordId;
704
- newBuilder.navigateRelation = this.navigateRelation;
705
- newBuilder.navigateSourceTableName = this.navigateSourceTableName;
706
- newBuilder.navigateBaseRelation = this.navigateBaseRelation;
707
- return newBuilder;
708
- }
709
-
710
- count(): QueryBuilder<T, Selected, SingleMode, true, Occ, Expands> {
711
- const newBuilder = new QueryBuilder<
712
- T,
713
- Selected,
714
- SingleMode,
715
- true,
716
- Occ,
717
- Expands
718
- >({
719
- occurrence: this.occurrence,
720
- tableName: this.tableName,
721
- databaseName: this.databaseName,
722
- context: this.context,
723
- databaseUseEntityIds: this.databaseUseEntityIds,
724
- });
725
- newBuilder.queryOptions = { ...this.queryOptions, count: true };
726
- newBuilder.expandConfigs = [...this.expandConfigs];
727
- newBuilder.singleMode = this.singleMode;
728
- newBuilder.isCountMode = true as true;
729
- // Preserve navigation metadata
730
- newBuilder.isNavigate = this.isNavigate;
731
- newBuilder.navigateRecordId = this.navigateRecordId;
732
- newBuilder.navigateRelation = this.navigateRelation;
733
- newBuilder.navigateSourceTableName = this.navigateSourceTableName;
734
- newBuilder.navigateBaseRelation = this.navigateBaseRelation;
735
- return newBuilder;
736
- }
737
-
738
- async execute<EO extends ExecuteOptions>(
739
- options?: RequestInit & FFetchOptions & EO,
740
- ): Promise<
741
- Result<
742
- IsCount extends true
743
- ? number
744
- : SingleMode extends "exact"
745
- ? ConditionallyWithODataAnnotations<
746
- Pick<T, Selected> & {
747
- [K in keyof Expands]: Pick<
748
- Expands[K]["schema"],
749
- Expands[K]["selected"]
750
- >[];
751
- },
752
- EO["includeODataAnnotations"] extends true ? true : false
753
- >
754
- : SingleMode extends "maybe"
755
- ? ConditionallyWithODataAnnotations<
756
- Pick<T, Selected> & {
757
- [K in keyof Expands]: Pick<
758
- Expands[K]["schema"],
759
- Expands[K]["selected"]
760
- >[];
761
- },
762
- EO["includeODataAnnotations"] extends true ? true : false
763
- > | null
764
- : ConditionallyWithODataAnnotations<
765
- Pick<T, Selected> & {
766
- [K in keyof Expands]: Pick<
767
- Expands[K]["schema"],
768
- Expands[K]["selected"]
769
- >[];
770
- },
771
- EO["includeODataAnnotations"] extends true ? true : false
772
- >[]
773
- >
774
- > {
775
- // Build query without expand (we'll add it manually)
776
- const queryOptionsWithoutExpand = { ...this.queryOptions };
777
- delete queryOptionsWithoutExpand.expand;
778
-
779
- const mergedOptions = this.mergeExecuteOptions(options);
780
-
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
- }
788
-
789
- let queryString = buildQuery(queryOptionsWithoutExpand);
790
-
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
- }
797
-
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);
814
-
815
- if (result.error) {
816
- return { data: undefined, error: result.error };
817
- }
818
-
819
- let response = result.data;
820
-
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) {
826
- const expandValidationConfigs = this.buildExpandValidationConfigs(
827
- this.expandConfigs,
828
- );
829
- response = transformResponseFields(
830
- response,
831
- this.occurrence.baseTable,
832
- expandValidationConfigs,
833
- );
834
- }
835
-
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;
842
-
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
- }
852
-
853
- if (count === 0) {
854
- if (this.singleMode === "exact") {
855
- return {
856
- data: undefined,
857
- error: new RecordCountMismatchError("one", 0),
858
- };
859
- }
860
- return { data: null as any, error: undefined };
861
- }
862
-
863
- const record = Array.isArray(records) ? records[0] : records;
864
- const stripped = this.stripODataAnnotationsIfNeeded(record, options);
865
- return { data: stripped as any, error: undefined };
866
- } else {
867
- const records = resp.value ?? [];
868
- const stripped = records.map((record: any) =>
869
- this.stripODataAnnotationsIfNeeded(record, options),
870
- );
871
- return { data: stripped as any, error: undefined };
872
- }
873
- }
874
-
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,
905
- );
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 };
913
- }
914
- }
915
-
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,
926
- );
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
-
949
- // Skip validation if requested
950
- if (options?.skipValidation === true) {
951
- const resp = response as any;
952
- if (this.singleMode !== false) {
953
- const records = resp.value ?? [resp];
954
- const count = Array.isArray(records) ? records.length : 1;
955
-
956
- if (count > 1) {
957
- return {
958
- data: undefined,
959
- error: new RecordCountMismatchError(
960
- this.singleMode === "exact" ? "one" : "at-most-one",
961
- count,
962
- ),
963
- };
964
- }
965
-
966
- if (count === 0) {
967
- if (this.singleMode === "exact") {
968
- return {
969
- data: undefined,
970
- error: new RecordCountMismatchError("one", 0),
971
- };
972
- }
973
- return { data: null as any, error: undefined };
974
- }
975
-
976
- const record = Array.isArray(records) ? records[0] : records;
977
- const stripped = this.stripODataAnnotationsIfNeeded(record, options);
978
- return { data: stripped as any, error: undefined };
979
- } else {
980
- const records = resp.value ?? [];
981
- const stripped = records.map((record: any) =>
982
- this.stripODataAnnotationsIfNeeded(record, options),
983
- );
984
- return { data: stripped as any, error: undefined };
985
- }
986
- }
987
-
988
- // Get schema from occurrence if available
989
- const schema = this.occurrence?.baseTable?.schema;
990
- const selectedFields = this.queryOptions.select as
991
- | (keyof T)[]
992
- | undefined;
993
- const expandValidationConfigs = this.buildExpandValidationConfigs(
994
- this.expandConfigs,
995
- );
996
-
997
- if (this.singleMode !== false) {
998
- const validation = await validateSingleResponse<T>(
999
- response,
1000
- schema,
1001
- selectedFields,
1002
- expandValidationConfigs,
1003
- this.singleMode,
1004
- );
1005
- if (!validation.valid) {
1006
- return { data: undefined, error: validation.error };
1007
- }
1008
- const stripped = validation.data
1009
- ? this.stripODataAnnotationsIfNeeded(validation.data, options)
1010
- : null;
1011
- return { data: stripped as any, error: undefined };
1012
- } else {
1013
- const validation = await validateListResponse<T>(
1014
- response,
1015
- schema,
1016
- selectedFields,
1017
- expandValidationConfigs,
1018
- );
1019
- if (!validation.valid) {
1020
- return { data: undefined, error: validation.error };
1021
- }
1022
- const stripped = validation.data.map((record) =>
1023
- this.stripODataAnnotationsIfNeeded(record, options),
1024
- );
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 };
1039
- }
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;
1135
- return {
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,
1155
- };
1156
- }
1157
- }
1158
-
1159
- getQueryString(): string {
1160
- // Build query without expand (we'll add it manually)
1161
- const queryOptionsWithoutExpand = { ...this.queryOptions };
1162
- delete queryOptionsWithoutExpand.expand;
1163
-
1164
- // Format select fields before building query - buildQuery treats & as separator,
1165
- // so we need to pre-encode special characters. buildQuery preserves encoded values.
1166
- if (queryOptionsWithoutExpand.select) {
1167
- queryOptionsWithoutExpand.select = this.formatSelectFields(
1168
- queryOptionsWithoutExpand.select,
1169
- this.occurrence?.baseTable,
1170
- ) as any;
1171
- }
1172
-
1173
- let queryParams = buildQuery(queryOptionsWithoutExpand);
1174
-
1175
- // Post-process: buildQuery encodes spaces as %20, but we want to preserve spaces
1176
- // Replace %20 with spaces in the $select part
1177
- if (this.queryOptions.select) {
1178
- queryParams = queryParams.replace(
1179
- /\$select=([^&]*)/,
1180
- (match, selectValue) => {
1181
- return `$select=${selectValue.replace(/%20/g, " ")}`;
1182
- },
1183
- );
1184
- }
1185
- const expandString = this.buildExpandString(this.expandConfigs);
1186
- if (expandString) {
1187
- const separator = queryParams.includes("?") ? "&" : "?";
1188
- queryParams = `${queryParams}${separator}$expand=${expandString}`;
1189
- }
1190
-
1191
- // Handle navigation from RecordBuilder (with record ID)
1192
- if (
1193
- this.isNavigate &&
1194
- this.navigateRecordId &&
1195
- this.navigateRelation &&
1196
- this.navigateSourceTableName
1197
- ) {
1198
- let path: string;
1199
- if (this.navigateBaseRelation) {
1200
- // Navigation from a navigated EntitySet: /sourceTable/baseRelation('recordId')/relation
1201
- path = `/${this.navigateSourceTableName}/${this.navigateBaseRelation}('${this.navigateRecordId}')/${this.navigateRelation}`;
1202
- } else {
1203
- // Normal navigation: /sourceTableName('recordId')/relationName
1204
- path = `/${this.navigateSourceTableName}('${this.navigateRecordId}')/${this.navigateRelation}`;
1205
- }
1206
- // Append query params if any exist
1207
- return queryParams ? `${path}${queryParams}` : path;
1208
- }
1209
-
1210
- // Handle navigation from EntitySet (without record ID)
1211
- if (
1212
- this.isNavigate &&
1213
- !this.navigateRecordId &&
1214
- this.navigateRelation &&
1215
- this.navigateSourceTableName
1216
- ) {
1217
- // Return the path portion: /sourceTableName/relationName
1218
- const path = `/${this.navigateSourceTableName}/${this.navigateRelation}`;
1219
- // Append query params if any exist
1220
- return queryParams ? `${path}${queryParams}` : path;
1221
- }
1222
-
1223
- // Default case: return table name with query params
1224
- return `/${this.tableName}${queryParams}`;
1225
- }
1226
-
1227
- getRequestConfig(): { method: string; url: string; body?: any } {
1228
- // Build query without expand (we'll add it manually)
1229
- const queryOptionsWithoutExpand = { ...this.queryOptions };
1230
- delete queryOptionsWithoutExpand.expand;
1231
-
1232
- // Format select fields before building query
1233
- if (queryOptionsWithoutExpand.select) {
1234
- queryOptionsWithoutExpand.select = this.formatSelectFields(
1235
- queryOptionsWithoutExpand.select,
1236
- this.occurrence?.baseTable,
1237
- ) as any;
1238
- }
1239
-
1240
- let queryString = buildQuery(queryOptionsWithoutExpand);
1241
-
1242
- // Build custom expand string
1243
- const expandString = this.buildExpandString(this.expandConfigs);
1244
- if (expandString) {
1245
- const separator = queryString.includes("?") ? "&" : "?";
1246
- queryString = `${queryString}${separator}$expand=${expandString}`;
1247
- }
1248
-
1249
- let url: string;
1250
-
1251
- // Handle navigation from RecordBuilder (with record ID)
1252
- if (
1253
- this.isNavigate &&
1254
- this.navigateRecordId &&
1255
- this.navigateRelation &&
1256
- this.navigateSourceTableName
1257
- ) {
1258
- if (this.navigateBaseRelation) {
1259
- // Navigation from a navigated EntitySet: /sourceTable/baseRelation('recordId')/relation
1260
- url = `/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateBaseRelation}('${this.navigateRecordId}')/${this.navigateRelation}${queryString}`;
1261
- } else {
1262
- // Normal navigation: /sourceTable('recordId')/relation
1263
- url = `/${this.databaseName}/${this.navigateSourceTableName}('${this.navigateRecordId}')/${this.navigateRelation}${queryString}`;
1264
- }
1265
- } else if (
1266
- this.isNavigate &&
1267
- !this.navigateRecordId &&
1268
- this.navigateRelation &&
1269
- this.navigateSourceTableName
1270
- ) {
1271
- // Handle navigation from EntitySet (without record ID)
1272
- url = `/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateRelation}${queryString}`;
1273
- } else if (this.isCountMode) {
1274
- url = `/${this.databaseName}/${this.tableName}/$count${queryString}`;
1275
- } else {
1276
- url = `/${this.databaseName}/${this.tableName}${queryString}`;
1277
- }
1278
-
1279
- return {
1280
- method: "GET",
1281
- url,
1282
- };
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
- }
1455
- }
1
+ /** biome-ignore-all lint/performance/noBarrelFile: Re-exporting QueryBuilder and types */
2
+
3
+ // Re-export QueryBuilder and types from the new modular location
4
+ // This maintains backward compatibility for existing imports
5
+ export {
6
+ type ExpandedRelations,
7
+ QueryBuilder,
8
+ type QueryReturnType,
9
+ type TypeSafeOrderBy,
10
+ } from "./query/index";