@proofkit/fmodata 0.1.0-alpha.13 → 0.1.0-alpha.15

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 (143) hide show
  1. package/README.md +489 -334
  2. package/dist/esm/client/batch-builder.d.ts +7 -5
  3. package/dist/esm/client/batch-builder.js +84 -25
  4. package/dist/esm/client/batch-builder.js.map +1 -1
  5. package/dist/esm/client/builders/default-select.d.ts +7 -0
  6. package/dist/esm/client/builders/default-select.js +42 -0
  7. package/dist/esm/client/builders/default-select.js.map +1 -0
  8. package/dist/esm/client/builders/expand-builder.d.ts +43 -0
  9. package/dist/esm/client/builders/expand-builder.js +173 -0
  10. package/dist/esm/client/builders/expand-builder.js.map +1 -0
  11. package/dist/esm/client/builders/index.d.ts +8 -0
  12. package/dist/esm/client/builders/query-string-builder.d.ts +15 -0
  13. package/dist/esm/client/builders/query-string-builder.js +25 -0
  14. package/dist/esm/client/builders/query-string-builder.js.map +1 -0
  15. package/dist/esm/client/builders/response-processor.d.ts +39 -0
  16. package/dist/esm/client/builders/response-processor.js +170 -0
  17. package/dist/esm/client/builders/response-processor.js.map +1 -0
  18. package/dist/esm/client/builders/select-mixin.d.ts +31 -0
  19. package/dist/esm/client/builders/select-mixin.js +30 -0
  20. package/dist/esm/client/builders/select-mixin.js.map +1 -0
  21. package/dist/esm/client/builders/select-utils.d.ts +8 -0
  22. package/dist/esm/client/builders/select-utils.js +15 -0
  23. package/dist/esm/client/builders/select-utils.js.map +1 -0
  24. package/dist/esm/client/builders/shared-types.d.ts +39 -0
  25. package/dist/esm/client/builders/table-utils.d.ts +35 -0
  26. package/dist/esm/client/builders/table-utils.js +45 -0
  27. package/dist/esm/client/builders/table-utils.js.map +1 -0
  28. package/dist/esm/client/database.d.ts +3 -22
  29. package/dist/esm/client/database.js +14 -76
  30. package/dist/esm/client/database.js.map +1 -1
  31. package/dist/esm/client/delete-builder.d.ts +12 -19
  32. package/dist/esm/client/delete-builder.js +26 -26
  33. package/dist/esm/client/delete-builder.js.map +1 -1
  34. package/dist/esm/client/entity-set.d.ts +32 -32
  35. package/dist/esm/client/entity-set.js +92 -69
  36. package/dist/esm/client/entity-set.js.map +1 -1
  37. package/dist/esm/client/error-parser.d.ts +12 -0
  38. package/dist/esm/client/error-parser.js +30 -0
  39. package/dist/esm/client/error-parser.js.map +1 -0
  40. package/dist/esm/client/filemaker-odata.d.ts +2 -4
  41. package/dist/esm/client/filemaker-odata.js +1 -5
  42. package/dist/esm/client/filemaker-odata.js.map +1 -1
  43. package/dist/esm/client/insert-builder.d.ts +9 -12
  44. package/dist/esm/client/insert-builder.js +70 -24
  45. package/dist/esm/client/insert-builder.js.map +1 -1
  46. package/dist/esm/client/query/expand-builder.d.ts +35 -0
  47. package/dist/esm/client/query/index.d.ts +3 -0
  48. package/dist/esm/client/query/query-builder.d.ts +133 -0
  49. package/dist/esm/client/query/query-builder.js +505 -0
  50. package/dist/esm/client/query/query-builder.js.map +1 -0
  51. package/dist/esm/client/query/response-processor.d.ts +22 -0
  52. package/dist/esm/client/query/types.d.ts +52 -0
  53. package/dist/esm/client/query/url-builder.d.ts +71 -0
  54. package/dist/esm/client/query/url-builder.js +107 -0
  55. package/dist/esm/client/query/url-builder.js.map +1 -0
  56. package/dist/esm/client/query-builder.d.ts +1 -111
  57. package/dist/esm/client/record-builder.d.ts +56 -64
  58. package/dist/esm/client/record-builder.js +158 -297
  59. package/dist/esm/client/record-builder.js.map +1 -1
  60. package/dist/esm/client/response-processor.d.ts +3 -3
  61. package/dist/esm/client/update-builder.d.ts +17 -25
  62. package/dist/esm/client/update-builder.js +56 -30
  63. package/dist/esm/client/update-builder.js.map +1 -1
  64. package/dist/esm/errors.d.ts +8 -1
  65. package/dist/esm/errors.js +17 -0
  66. package/dist/esm/errors.js.map +1 -1
  67. package/dist/esm/index.d.ts +3 -7
  68. package/dist/esm/index.js +37 -8
  69. package/dist/esm/index.js.map +1 -1
  70. package/dist/esm/orm/column.d.ts +45 -0
  71. package/dist/esm/orm/column.js +59 -0
  72. package/dist/esm/orm/column.js.map +1 -0
  73. package/dist/esm/orm/field-builders.d.ts +154 -0
  74. package/dist/esm/orm/field-builders.js +152 -0
  75. package/dist/esm/orm/field-builders.js.map +1 -0
  76. package/dist/esm/orm/index.d.ts +4 -0
  77. package/dist/esm/orm/operators.d.ts +175 -0
  78. package/dist/esm/orm/operators.js +221 -0
  79. package/dist/esm/orm/operators.js.map +1 -0
  80. package/dist/esm/orm/table.d.ts +341 -0
  81. package/dist/esm/orm/table.js +211 -0
  82. package/dist/esm/orm/table.js.map +1 -0
  83. package/dist/esm/transform.d.ts +20 -21
  84. package/dist/esm/transform.js +34 -34
  85. package/dist/esm/transform.js.map +1 -1
  86. package/dist/esm/types.d.ts +73 -12
  87. package/dist/esm/types.js.map +1 -1
  88. package/dist/esm/validation.d.ts +14 -4
  89. package/dist/esm/validation.js +45 -1
  90. package/dist/esm/validation.js.map +1 -1
  91. package/package.json +22 -17
  92. package/src/client/batch-builder.ts +102 -33
  93. package/src/client/builders/default-select.ts +69 -0
  94. package/src/client/builders/expand-builder.ts +236 -0
  95. package/src/client/builders/index.ts +11 -0
  96. package/src/client/builders/query-string-builder.ts +41 -0
  97. package/src/client/builders/response-processor.ts +273 -0
  98. package/src/client/builders/select-mixin.ts +74 -0
  99. package/src/client/builders/select-utils.ts +34 -0
  100. package/src/client/builders/shared-types.ts +41 -0
  101. package/src/client/builders/table-utils.ts +87 -0
  102. package/src/client/database.ts +19 -160
  103. package/src/client/delete-builder.ts +48 -52
  104. package/src/client/entity-set.ts +227 -302
  105. package/src/client/error-parser.ts +59 -0
  106. package/src/client/filemaker-odata.ts +3 -14
  107. package/src/client/insert-builder.ts +126 -44
  108. package/src/client/query/expand-builder.ts +164 -0
  109. package/src/client/query/index.ts +13 -0
  110. package/src/client/query/query-builder.ts +826 -0
  111. package/src/client/query/response-processor.ts +244 -0
  112. package/src/client/query/types.ts +102 -0
  113. package/src/client/query/url-builder.ts +179 -0
  114. package/src/client/query-builder.ts +8 -1454
  115. package/src/client/record-builder.ts +336 -586
  116. package/src/client/response-processor.ts +4 -5
  117. package/src/client/update-builder.ts +113 -75
  118. package/src/errors.ts +22 -1
  119. package/src/index.ts +58 -5
  120. package/src/orm/column.ts +78 -0
  121. package/src/orm/field-builders.ts +296 -0
  122. package/src/orm/index.ts +60 -0
  123. package/src/orm/operators.ts +428 -0
  124. package/src/orm/table.ts +759 -0
  125. package/src/transform.ts +62 -48
  126. package/src/types.ts +88 -63
  127. package/src/validation.ts +76 -4
  128. package/LICENSE.md +0 -21
  129. package/dist/esm/client/base-table.d.ts +0 -128
  130. package/dist/esm/client/base-table.js +0 -57
  131. package/dist/esm/client/base-table.js.map +0 -1
  132. package/dist/esm/client/build-occurrences.d.ts +0 -74
  133. package/dist/esm/client/build-occurrences.js +0 -31
  134. package/dist/esm/client/build-occurrences.js.map +0 -1
  135. package/dist/esm/client/query-builder.js +0 -900
  136. package/dist/esm/client/query-builder.js.map +0 -1
  137. package/dist/esm/client/table-occurrence.d.ts +0 -86
  138. package/dist/esm/client/table-occurrence.js +0 -58
  139. package/dist/esm/client/table-occurrence.js.map +0 -1
  140. package/src/client/base-table.ts +0 -178
  141. package/src/client/build-occurrences.ts +0 -155
  142. package/src/client/query-builder.ts.bak +0 -1457
  143. package/src/client/table-occurrence.ts +0 -156
@@ -2,134 +2,108 @@ import type {
2
2
  ExecutionContext,
3
3
  ExecutableBuilder,
4
4
  Result,
5
- ODataRecordMetadata,
6
5
  ODataFieldResponse,
7
- InferSchemaType,
8
6
  ExecuteOptions,
9
- WithSystemFields,
10
7
  ConditionallyWithODataAnnotations,
8
+ ExecuteMethodOptions,
11
9
  } from "../types";
12
- import { getAcceptHeader } from "../types";
13
- import type { TableOccurrence } from "./table-occurrence";
14
- import type { BaseTable } from "./base-table";
15
- import {
16
- transformTableName,
17
- transformResponseFields,
18
- getTableIdentifiers,
19
- transformFieldNamesArray,
20
- } from "../transform";
10
+ import type {
11
+ FMTable,
12
+ InferSchemaOutputFromFMTable,
13
+ ValidExpandTarget,
14
+ ExtractTableName,
15
+ ValidateNoContainerFields,
16
+ } from "../orm/table";
17
+ import { getTableName, getNavigationPaths } from "../orm/table";
21
18
  import { safeJsonParse } from "./sanitize-json";
19
+ import { parseErrorResponse } from "./error-parser";
22
20
  import { QueryBuilder } from "./query-builder";
23
- import {
24
- validateSingleResponse,
25
- type ExpandValidationConfig,
26
- } from "../validation";
27
21
  import { type FFetchOptions } from "@fetchkit/ffetch";
28
- import { StandardSchemaV1 } from "@standard-schema/spec";
29
- import { QueryOptions } from "odata-query";
30
- import buildQuery from "odata-query";
31
-
32
- // Helper type to extract schema from a TableOccurrence
33
- type ExtractSchemaFromOccurrence<O> =
34
- O extends TableOccurrence<infer BT, any, any, any>
35
- ? BT extends BaseTable<infer S, any, any, any>
36
- ? S
37
- : never
38
- : never;
39
-
40
- // Helper type to extract navigation relation names from an occurrence
41
- type ExtractNavigationNames<
42
- O extends TableOccurrence<any, any, any, any> | undefined,
43
- > =
44
- O extends TableOccurrence<any, any, infer Nav, any>
45
- ? Nav extends Record<string, any>
46
- ? keyof Nav & string
47
- : never
48
- : never;
49
-
50
- // Helper type to find target occurrence by relation name
51
- type FindNavigationTarget<
52
- O extends TableOccurrence<any, any, any, any> | undefined,
53
- Name extends string,
54
- > =
55
- O extends TableOccurrence<any, any, infer Nav, any>
56
- ? Nav extends Record<string, any>
57
- ? Name extends keyof Nav
58
- ? Nav[Name]
59
- : TableOccurrence<
60
- BaseTable<Record<string, StandardSchemaV1>, any, any, any>,
61
- any,
62
- any,
63
- any
64
- >
65
- : TableOccurrence<
66
- BaseTable<Record<string, StandardSchemaV1>, any, any, any>,
67
- any,
68
- any,
69
- any
70
- >
71
- : TableOccurrence<
72
- BaseTable<Record<string, StandardSchemaV1>, any, any, any>,
73
- any,
74
- any,
75
- any
76
- >;
77
-
78
- // Helper type to get the inferred schema type from a target occurrence
79
- type GetTargetSchemaType<
80
- O extends TableOccurrence<any, any, any, any> | undefined,
81
- Rel extends string,
82
- > = [FindNavigationTarget<O, Rel>] extends [
83
- TableOccurrence<infer BT, any, any, any>,
84
- ]
85
- ? [BT] extends [BaseTable<infer S, any, any, any>]
86
- ? [S] extends [Record<string, StandardSchemaV1>]
87
- ? InferSchemaType<S>
88
- : Record<string, any>
89
- : Record<string, any>
90
- : Record<string, any>;
91
-
92
- // Internal type for expand configuration
93
- type ExpandConfig = {
94
- relation: string;
95
- options?: Partial<QueryOptions<any>>;
22
+ import { isColumn, type Column } from "../orm/column";
23
+ import {
24
+ type ExpandConfig,
25
+ type ExpandedRelations,
26
+ ExpandBuilder,
27
+ resolveTableId,
28
+ mergeExecuteOptions,
29
+ processODataResponse,
30
+ getSchemaFromTable,
31
+ processSelectWithRenames,
32
+ buildSelectExpandQueryString,
33
+ createODataRequest,
34
+ } from "./builders/index";
35
+
36
+ /**
37
+ * Extract the value type from a Column.
38
+ * This uses the phantom type stored in Column to get the actual value type.
39
+ */
40
+ type ExtractColumnType<C> = C extends Column<infer T, any> ? T : never;
41
+
42
+ /**
43
+ * Map a select object to its return type.
44
+ * For each key in the select object, extract the type from the corresponding Column.
45
+ */
46
+ type MapSelectToReturnType<
47
+ TSelect extends Record<string, Column<any, any>>,
48
+ TSchema extends Record<string, any>,
49
+ > = {
50
+ [K in keyof TSelect]: ExtractColumnType<TSelect[K]>;
96
51
  };
97
52
 
98
- // Type to represent expanded relations
99
- export type ExpandedRelations = Record<string, { schema: any; selected: any }>;
100
-
101
53
  // Return type for RecordBuilder execute
102
54
  export type RecordReturnType<
103
- T extends Record<string, any>,
55
+ Schema extends Record<string, any>,
104
56
  IsSingleField extends boolean,
105
- FieldKey extends keyof T,
106
- Selected extends keyof T,
57
+ FieldKey extends keyof Schema,
58
+ Selected extends
59
+ | keyof Schema
60
+ | Record<string, Column<any, ExtractTableName<FMTable<any, any>>>>,
107
61
  Expands extends ExpandedRelations,
108
62
  > = IsSingleField extends true
109
- ? T[FieldKey]
110
- : Pick<T, Selected> & {
111
- [K in keyof Expands]: Pick<
112
- Expands[K]["schema"],
113
- Expands[K]["selected"]
114
- >[];
115
- };
63
+ ? Schema[FieldKey]
64
+ : // Use tuple wrapping [Selected] extends [...] to prevent distribution over unions
65
+ [Selected] extends [Record<string, Column<any, any>>]
66
+ ? MapSelectToReturnType<Selected, Schema> & {
67
+ [K in keyof Expands]: Pick<
68
+ Expands[K]["schema"],
69
+ Expands[K]["selected"]
70
+ >[];
71
+ }
72
+ : // Use tuple wrapping to prevent distribution over union of keys
73
+ [Selected] extends [keyof Schema]
74
+ ? Pick<Schema, Selected> & {
75
+ [K in keyof Expands]: Pick<
76
+ Expands[K]["schema"],
77
+ Expands[K]["selected"]
78
+ >[];
79
+ }
80
+ : never;
116
81
 
117
82
  export class RecordBuilder<
118
- T extends Record<string, any>,
83
+ Occ extends FMTable<any, any> = FMTable<any, any>,
119
84
  IsSingleField extends boolean = false,
120
- FieldKey extends keyof T = keyof T,
121
- Occ extends TableOccurrence<any, any, any, any> | undefined =
122
- | TableOccurrence<any, any, any, any>
123
- | undefined,
124
- Selected extends keyof T = keyof T,
85
+ FieldKey extends keyof InferSchemaOutputFromFMTable<
86
+ NonNullable<Occ>
87
+ > = keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,
88
+ Selected extends
89
+ | keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>
90
+ | Record<
91
+ string,
92
+ Column<any, ExtractTableName<NonNullable<Occ>>>
93
+ > = keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,
125
94
  Expands extends ExpandedRelations = {},
126
95
  > implements
127
96
  ExecutableBuilder<
128
- RecordReturnType<T, IsSingleField, FieldKey, Selected, Expands>
97
+ RecordReturnType<
98
+ InferSchemaOutputFromFMTable<NonNullable<Occ>>,
99
+ IsSingleField,
100
+ FieldKey,
101
+ Selected,
102
+ Expands
103
+ >
129
104
  >
130
105
  {
131
- private occurrence?: Occ;
132
- private tableName: string;
106
+ private table: Occ;
133
107
  private databaseName: string;
134
108
  private context: ExecutionContext;
135
109
  private recordId: string | number;
@@ -141,20 +115,20 @@ export class RecordBuilder<
141
115
 
142
116
  private databaseUseEntityIds: boolean;
143
117
 
144
- // New properties for select/expand support
118
+ // Properties for select/expand support
145
119
  private selectedFields?: string[];
146
120
  private expandConfigs: ExpandConfig[] = [];
121
+ // Mapping from field names to output keys (for renamed fields in select)
122
+ private fieldMapping?: Record<string, string>;
147
123
 
148
124
  constructor(config: {
149
- occurrence?: Occ;
150
- tableName: string;
125
+ occurrence: Occ;
151
126
  databaseName: string;
152
127
  context: ExecutionContext;
153
128
  recordId: string | number;
154
129
  databaseUseEntityIds?: boolean;
155
130
  }) {
156
- this.occurrence = config.occurrence;
157
- this.tableName = config.tableName;
131
+ this.table = config.occurrence;
158
132
  this.databaseName = config.databaseName;
159
133
  this.context = config.context;
160
134
  this.recordId = config.recordId;
@@ -167,11 +141,7 @@ export class RecordBuilder<
167
141
  private mergeExecuteOptions(
168
142
  options?: RequestInit & FFetchOptions & ExecuteOptions,
169
143
  ): RequestInit & FFetchOptions & { useEntityIds?: boolean } {
170
- // If useEntityIds is not set in options, use the database-level setting
171
- return {
172
- ...options,
173
- useEntityIds: options?.useEntityIds ?? this.databaseUseEntityIds,
174
- };
144
+ return mergeExecuteOptions(options, this.databaseUseEntityIds);
175
145
  }
176
146
 
177
147
  /**
@@ -179,39 +149,48 @@ export class RecordBuilder<
179
149
  * @param useEntityIds - Optional override for entity ID usage
180
150
  */
181
151
  private getTableId(useEntityIds?: boolean): string {
182
- if (!this.occurrence) {
183
- return this.tableName;
184
- }
185
-
186
- const contextDefault = this.context._getUseEntityIds?.() ?? false;
187
- const shouldUseIds = useEntityIds ?? contextDefault;
188
-
189
- if (shouldUseIds) {
190
- const identifiers = getTableIdentifiers(this.occurrence);
191
- if (!identifiers.id) {
192
- throw new Error(
193
- `useEntityIds is true but TableOccurrence "${identifiers.name}" does not have an fmtId defined`,
194
- );
195
- }
196
- return identifiers.id;
152
+ if (!this.table) {
153
+ throw new Error("Table occurrence is required");
197
154
  }
198
-
199
- return this.occurrence.getTableName();
155
+ return resolveTableId(
156
+ this.table,
157
+ getTableName(this.table),
158
+ this.context,
159
+ useEntityIds,
160
+ );
200
161
  }
201
162
 
202
- getSingleField<K extends keyof T>(
203
- field: K,
204
- ): RecordBuilder<T, true, K, Occ, keyof T, {}> {
205
- const newBuilder = new RecordBuilder<T, true, K, Occ, keyof T, {}>({
206
- occurrence: this.occurrence,
207
- tableName: this.tableName,
163
+ /**
164
+ * Creates a new RecordBuilder with modified configuration.
165
+ * Used by select() to create new instances.
166
+ */
167
+ private cloneWithChanges<
168
+ NewSelected extends
169
+ | keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>
170
+ | Record<
171
+ string,
172
+ Column<any, ExtractTableName<NonNullable<Occ>>>
173
+ > = Selected,
174
+ >(changes: {
175
+ selectedFields?: string[];
176
+ fieldMapping?: Record<string, string>;
177
+ }): RecordBuilder<Occ, false, FieldKey, NewSelected, Expands> {
178
+ const newBuilder = new RecordBuilder<
179
+ Occ,
180
+ false,
181
+ FieldKey,
182
+ NewSelected,
183
+ Expands
184
+ >({
185
+ occurrence: this.table,
208
186
  databaseName: this.databaseName,
209
187
  context: this.context,
210
188
  recordId: this.recordId,
211
189
  databaseUseEntityIds: this.databaseUseEntityIds,
212
190
  });
213
- newBuilder.operation = "getSingleField";
214
- newBuilder.operationParam = field.toString();
191
+ newBuilder.selectedFields = changes.selectedFields ?? this.selectedFields;
192
+ newBuilder.fieldMapping = changes.fieldMapping ?? this.fieldMapping;
193
+ newBuilder.expandConfigs = [...this.expandConfigs];
215
194
  // Preserve navigation context
216
195
  newBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;
217
196
  newBuilder.navigateRelation = this.navigateRelation;
@@ -219,30 +198,32 @@ export class RecordBuilder<
219
198
  return newBuilder;
220
199
  }
221
200
 
222
- /**
223
- * Select specific fields to retrieve from the record.
224
- * Only the selected fields will be returned in the response.
225
- *
226
- * @example
227
- * ```typescript
228
- * const contact = await db.from("contacts").get("uuid").select("name", "email").execute();
229
- * // contact.data has type { name: string; email: string }
230
- * ```
231
- */
232
- select<K extends keyof T>(
233
- ...fields: K[]
234
- ): RecordBuilder<T, false, FieldKey, Occ, K, Expands> {
235
- const uniqueFields = [...new Set(fields)];
236
- const newBuilder = new RecordBuilder<T, false, FieldKey, Occ, K, Expands>({
237
- occurrence: this.occurrence,
238
- tableName: this.tableName,
201
+ getSingleField<
202
+ K extends keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,
203
+ >(
204
+ field: K,
205
+ ): RecordBuilder<
206
+ Occ,
207
+ true,
208
+ K,
209
+ keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,
210
+ {}
211
+ > {
212
+ const newBuilder = new RecordBuilder<
213
+ Occ,
214
+ true,
215
+ K,
216
+ keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,
217
+ {}
218
+ >({
219
+ occurrence: this.table,
239
220
  databaseName: this.databaseName,
240
221
  context: this.context,
241
222
  recordId: this.recordId,
242
223
  databaseUseEntityIds: this.databaseUseEntityIds,
243
224
  });
244
- newBuilder.selectedFields = uniqueFields.map((f) => String(f));
245
- newBuilder.expandConfigs = [...this.expandConfigs];
225
+ newBuilder.operation = "getSingleField";
226
+ newBuilder.operationParam = field.toString();
246
227
  // Preserve navigation context
247
228
  newBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;
248
229
  newBuilder.navigateRelation = this.navigateRelation;
@@ -250,93 +231,76 @@ export class RecordBuilder<
250
231
  return newBuilder;
251
232
  }
252
233
 
234
+ /**
235
+ * Select fields using column references.
236
+ * Allows renaming fields by using different keys in the object.
237
+ * Container fields cannot be selected and will cause a type error.
238
+ *
239
+ * @example
240
+ * db.from(contacts).get("uuid").select({
241
+ * name: contacts.name,
242
+ * userEmail: contacts.email // renamed!
243
+ * })
244
+ *
245
+ * @param fields - Object mapping output keys to column references (container fields excluded)
246
+ * @returns RecordBuilder with updated selected fields
247
+ */
248
+ select<
249
+ TSelect extends Record<string, Column<any, ExtractTableName<Occ>, false>>,
250
+ >(fields: TSelect): RecordBuilder<Occ, false, FieldKey, TSelect, Expands> {
251
+ const tableName = getTableName(this.table);
252
+ const { selectedFields, fieldMapping } = processSelectWithRenames(
253
+ fields,
254
+ tableName,
255
+ );
256
+
257
+ return this.cloneWithChanges({
258
+ selectedFields,
259
+ fieldMapping:
260
+ Object.keys(fieldMapping).length > 0 ? fieldMapping : undefined,
261
+ }) as any;
262
+ }
263
+
253
264
  /**
254
265
  * Expand a navigation property to include related records.
255
266
  * Supports nested select, filter, orderBy, and expand operations.
256
267
  *
257
268
  * @example
258
269
  * ```typescript
259
- * // Simple expand
260
- * const contact = await db.from("contacts").get("uuid").expand("users").execute();
270
+ * // Simple expand with FMTable object
271
+ * const contact = await db.from(contacts).get("uuid").expand(users).execute();
261
272
  *
262
273
  * // Expand with select
263
- * const contact = await db.from("contacts").get("uuid")
264
- * .expand("users", b => b.select("username", "email"))
274
+ * const contact = await db.from(contacts).get("uuid")
275
+ * .expand(users, b => b.select({ username: users.username, email: users.email }))
265
276
  * .execute();
266
277
  * ```
267
278
  */
268
- expand<
269
- Rel extends ExtractNavigationNames<Occ> | (string & {}),
270
- TargetOcc extends FindNavigationTarget<Occ, Rel> = FindNavigationTarget<
271
- Occ,
272
- Rel
273
- >,
274
- TargetSchema extends GetTargetSchemaType<Occ, Rel> = GetTargetSchemaType<
275
- Occ,
276
- Rel
277
- >,
278
- TargetSelected extends keyof TargetSchema = keyof TargetSchema,
279
- >(
280
- relation: Rel,
279
+ expand<TargetTable extends FMTable<any, any>>(
280
+ targetTable: ValidExpandTarget<Occ, TargetTable>,
281
281
  callback?: (
282
282
  builder: QueryBuilder<
283
- TargetSchema,
284
- keyof TargetSchema,
285
- false,
283
+ TargetTable,
284
+ keyof InferSchemaOutputFromFMTable<TargetTable>,
286
285
  false,
287
- TargetOcc extends TableOccurrence<any, any, any, any>
288
- ? TargetOcc
289
- : undefined
286
+ false
290
287
  >,
291
- ) => QueryBuilder<
292
- WithSystemFields<TargetSchema>,
293
- TargetSelected,
294
- any,
295
- any,
296
- any
297
- >,
288
+ ) => QueryBuilder<TargetTable, any, any, any, any>,
298
289
  ): RecordBuilder<
299
- T,
290
+ Occ,
300
291
  false,
301
292
  FieldKey,
302
- Occ,
303
293
  Selected,
304
294
  Expands & {
305
- [K in Rel]: { schema: TargetSchema; selected: TargetSelected };
295
+ [K in ExtractTableName<TargetTable>]: {
296
+ schema: InferSchemaOutputFromFMTable<TargetTable>;
297
+ selected: keyof InferSchemaOutputFromFMTable<TargetTable>;
298
+ };
306
299
  }
307
300
  > {
308
- // Look up target occurrence from navigation
309
- const targetOccurrence = this.occurrence?.navigation[relation as string];
310
-
311
- // Helper function to get defaultSelect fields from target occurrence
312
- const getDefaultSelectFields = (): string[] | undefined => {
313
- if (!targetOccurrence) return undefined;
314
- const defaultSelect = targetOccurrence.defaultSelect;
315
- if (defaultSelect === "schema") {
316
- const schema = targetOccurrence.baseTable?.schema;
317
- if (schema) {
318
- return [...new Set(Object.keys(schema))];
319
- }
320
- } else if (Array.isArray(defaultSelect)) {
321
- return [...new Set(defaultSelect)];
322
- }
323
- // If "all", return undefined (no select restriction)
324
- return undefined;
325
- };
326
-
327
301
  // Create new builder with updated types
328
- const newBuilder = new RecordBuilder<
329
- T,
330
- false,
331
- FieldKey,
332
- Occ,
333
- Selected,
334
- Expands & {
335
- [K in Rel]: { schema: TargetSchema; selected: TargetSelected };
336
- }
337
- >({
338
- occurrence: this.occurrence,
339
- tableName: this.tableName,
302
+ const newBuilder = new RecordBuilder<Occ, false, FieldKey, Selected, any>({
303
+ occurrence: this.table,
340
304
  databaseName: this.databaseName,
341
305
  context: this.context,
342
306
  recordId: this.recordId,
@@ -345,319 +309,132 @@ export class RecordBuilder<
345
309
 
346
310
  // Copy existing state
347
311
  newBuilder.selectedFields = this.selectedFields;
312
+ newBuilder.fieldMapping = this.fieldMapping;
348
313
  newBuilder.expandConfigs = [...this.expandConfigs];
349
314
  newBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;
350
315
  newBuilder.navigateRelation = this.navigateRelation;
351
316
  newBuilder.navigateSourceTableName = this.navigateSourceTableName;
352
317
 
353
- if (callback) {
354
- // Create a new QueryBuilder for the target occurrence
355
- const targetBuilder = new QueryBuilder<any>({
356
- occurrence: targetOccurrence,
357
- tableName: targetOccurrence?.name ?? (relation as string),
358
- databaseName: this.databaseName,
359
- context: this.context,
360
- databaseUseEntityIds: this.databaseUseEntityIds,
361
- });
362
-
363
- // Cast to the expected type for the callback
364
- // At runtime, the builder is untyped (any), but at compile-time we enforce proper types
365
- const typedBuilder = targetBuilder as QueryBuilder<
366
- TargetSchema,
367
- keyof TargetSchema,
368
- false,
369
- false,
370
- TargetOcc extends TableOccurrence<any, any, any, any>
371
- ? TargetOcc
372
- : undefined
373
- >;
374
-
375
- // Pass to callback and get configured builder
376
- const configuredBuilder = callback(typedBuilder);
377
-
378
- // Extract the builder's query options
379
- const expandOptions: Partial<QueryOptions<any>> = {
380
- ...(configuredBuilder as any).queryOptions,
381
- };
318
+ // Use ExpandBuilder.processExpand to handle the expand logic
319
+ const expandBuilder = new ExpandBuilder(this.databaseUseEntityIds);
320
+ type TargetBuilder = QueryBuilder<
321
+ TargetTable,
322
+ keyof InferSchemaOutputFromFMTable<TargetTable>,
323
+ false,
324
+ false
325
+ >;
326
+ const expandConfig = expandBuilder.processExpand<
327
+ TargetTable,
328
+ TargetBuilder
329
+ >(
330
+ targetTable,
331
+ this.table ?? undefined,
332
+ callback,
333
+ () =>
334
+ new QueryBuilder<TargetTable>({
335
+ occurrence: targetTable,
336
+ databaseName: this.databaseName,
337
+ context: this.context,
338
+ databaseUseEntityIds: this.databaseUseEntityIds,
339
+ }),
340
+ );
382
341
 
383
- // If callback didn't provide select, apply defaultSelect from target occurrence
384
- if (!expandOptions.select) {
385
- const defaultFields = getDefaultSelectFields();
386
- if (defaultFields) {
387
- expandOptions.select = defaultFields;
388
- }
389
- }
342
+ newBuilder.expandConfigs.push(expandConfig);
343
+ return newBuilder as any;
344
+ }
390
345
 
391
- // If the configured builder has nested expands, we need to include them
392
- if ((configuredBuilder as any).expandConfigs?.length > 0) {
393
- // Build nested expand string from the configured builder's expand configs
394
- const nestedExpandString = this.buildExpandString(
395
- (configuredBuilder as any).expandConfigs,
346
+ navigate<TargetTable extends FMTable<any, any>>(
347
+ targetTable: ValidExpandTarget<Occ, TargetTable>,
348
+ ): QueryBuilder<
349
+ TargetTable,
350
+ keyof InferSchemaOutputFromFMTable<TargetTable>,
351
+ false,
352
+ false
353
+ > {
354
+ // Extract name and validate
355
+ const relationName = getTableName(targetTable);
356
+
357
+ // Runtime validation: Check if relation name is in navigationPaths
358
+ if (this.table) {
359
+ const navigationPaths = getNavigationPaths(this.table);
360
+ if (navigationPaths && !navigationPaths.includes(relationName)) {
361
+ console.warn(
362
+ `Cannot navigate to "${relationName}". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(", ") : "none"}`,
396
363
  );
397
- if (nestedExpandString) {
398
- // Add nested expand to options
399
- expandOptions.expand = nestedExpandString as any;
400
- }
401
- }
402
-
403
- const expandConfig: ExpandConfig = {
404
- relation: relation as string,
405
- options: expandOptions,
406
- };
407
-
408
- newBuilder.expandConfigs.push(expandConfig);
409
- } else {
410
- // Simple expand without callback - apply defaultSelect if available
411
- const defaultFields = getDefaultSelectFields();
412
- if (defaultFields) {
413
- newBuilder.expandConfigs.push({
414
- relation: relation as string,
415
- options: { select: defaultFields },
416
- });
417
- } else {
418
- newBuilder.expandConfigs.push({ relation: relation as string });
419
364
  }
420
365
  }
421
366
 
422
- return newBuilder;
423
- }
424
-
425
- // Overload for valid relation names - returns typed QueryBuilder
426
- navigate<RelationName extends ExtractNavigationNames<Occ>>(
427
- relationName: RelationName,
428
- ): QueryBuilder<
429
- ExtractSchemaFromOccurrence<
430
- FindNavigationTarget<Occ, RelationName>
431
- > extends Record<string, StandardSchemaV1>
432
- ? InferSchemaType<
433
- ExtractSchemaFromOccurrence<FindNavigationTarget<Occ, RelationName>>
434
- >
435
- : Record<string, any>
436
- >;
437
- // Overload for arbitrary strings - returns generic QueryBuilder with system fields
438
- navigate(
439
- relationName: string,
440
- ): QueryBuilder<{ ROWID: number; ROWMODID: number; [key: string]: any }>;
441
- // Implementation
442
- navigate(relationName: string): QueryBuilder<any> {
443
- // Use the target occurrence if available, otherwise allow untyped navigation
444
- // (useful when types might be incomplete)
445
- const targetOccurrence = this.occurrence?.navigation[relationName];
446
- const builder = new QueryBuilder<any>({
447
- occurrence: targetOccurrence,
448
- tableName: targetOccurrence?.name ?? relationName,
367
+ // Create QueryBuilder with target table
368
+ const builder = new QueryBuilder<TargetTable>({
369
+ occurrence: targetTable,
449
370
  databaseName: this.databaseName,
450
371
  context: this.context,
372
+ databaseUseEntityIds: this.databaseUseEntityIds,
451
373
  });
452
- // Store the navigation info - we'll use it in execute
453
- // Transform relation name to FMTID if using entity IDs
454
- const relationId = targetOccurrence
455
- ? transformTableName(targetOccurrence)
456
- : relationName;
457
374
 
458
- (builder as any).isNavigate = true;
459
- (builder as any).navigateRecordId = this.recordId;
460
- (builder as any).navigateRelation = relationId;
375
+ // Store the navigation info - we'll use it in execute
376
+ // Use relation name as-is (entity ID handling is done in QueryBuilder)
377
+ const relationId = relationName;
461
378
 
462
379
  // If this RecordBuilder came from a navigated EntitySet, we need to preserve that base path
380
+ let sourceTableName: string;
381
+ let baseRelation: string | undefined;
463
382
  if (
464
383
  this.isNavigateFromEntitySet &&
465
384
  this.navigateSourceTableName &&
466
385
  this.navigateRelation
467
386
  ) {
468
387
  // Build the base path: /sourceTable/relation('recordId')/newRelation
469
- (builder as any).navigateSourceTableName = this.navigateSourceTableName;
470
- (builder as any).navigateBaseRelation = this.navigateRelation;
388
+ sourceTableName = this.navigateSourceTableName;
389
+ baseRelation = this.navigateRelation;
471
390
  } else {
472
391
  // Normal record navigation: /tableName('recordId')/relation
473
- // Transform source table name to FMTID if using entity IDs
474
- const sourceTableId = this.occurrence
475
- ? transformTableName(this.occurrence)
476
- : this.tableName;
477
- (builder as any).navigateSourceTableName = sourceTableId;
478
- }
479
-
480
- return builder;
481
- }
482
-
483
- /**
484
- * Formats select fields for use in query strings.
485
- * - Transforms field names to FMFIDs if using entity IDs
486
- * - Wraps "id" fields in double quotes
487
- * - URL-encodes special characters but preserves spaces
488
- */
489
- private formatSelectFields(
490
- select: string[] | undefined,
491
- baseTable?: BaseTable<any, any, any, any>,
492
- useEntityIds?: boolean,
493
- ): string {
494
- if (!select || select.length === 0) return "";
495
-
496
- // Transform to field IDs if using entity IDs AND the feature is enabled
497
- const shouldTransform = baseTable && (useEntityIds ?? this.databaseUseEntityIds);
498
- const transformedFields = shouldTransform
499
- ? transformFieldNamesArray(select, baseTable)
500
- : select;
501
-
502
- return transformedFields
503
- .map((field) => {
504
- if (field === "id") return `"id"`;
505
- const encodedField = encodeURIComponent(String(field));
506
- return encodedField.replace(/%20/g, " ");
507
- })
508
- .join(",");
509
- }
510
-
511
- /**
512
- * Builds expand validation configs from internal expand configurations.
513
- * These are used to validate expanded navigation properties.
514
- */
515
- private buildExpandValidationConfigs(
516
- configs: ExpandConfig[],
517
- ): ExpandValidationConfig[] {
518
- return configs.map((config) => {
519
- // Look up target occurrence from navigation
520
- const targetOccurrence = this.occurrence?.navigation[config.relation];
521
- const targetSchema = targetOccurrence?.baseTable?.schema;
522
-
523
- // Extract selected fields from options
524
- const selectedFields = config.options?.select
525
- ? Array.isArray(config.options.select)
526
- ? config.options.select.map((f) => String(f))
527
- : [String(config.options.select)]
528
- : undefined;
529
-
530
- return {
531
- relation: config.relation,
532
- targetSchema: targetSchema,
533
- targetOccurrence: targetOccurrence,
534
- targetBaseTable: targetOccurrence?.baseTable,
535
- occurrence: targetOccurrence, // For transformation
536
- selectedFields: selectedFields,
537
- nestedExpands: undefined, // TODO: Handle nested expands if needed
538
- };
539
- });
540
- }
541
-
542
- /**
543
- * Builds OData expand query string from expand configurations.
544
- * Handles nested expands recursively.
545
- * Transforms relation names to FMTIDs if using entity IDs.
546
- */
547
- private buildExpandString(configs: ExpandConfig[]): string {
548
- if (configs.length === 0) {
549
- return "";
392
+ // Use table ID if available, otherwise table name
393
+ if (!this.table) {
394
+ throw new Error("Table occurrence is required for navigation");
395
+ }
396
+ sourceTableName = resolveTableId(
397
+ this.table,
398
+ getTableName(this.table),
399
+ this.context,
400
+ this.databaseUseEntityIds,
401
+ );
550
402
  }
551
403
 
552
- return configs
553
- .map((config) => {
554
- // Get target occurrence for this relation
555
- const targetOccurrence = this.occurrence?.navigation[config.relation];
556
-
557
- // When using entity IDs, use the target table's FMTID in the expand parameter
558
- // FileMaker expects FMTID in $expand when Prefer header is set
559
- const relationName =
560
- targetOccurrence && targetOccurrence.isUsingTableId?.()
561
- ? targetOccurrence.getTableId()
562
- : config.relation;
563
-
564
- if (!config.options || Object.keys(config.options).length === 0) {
565
- // Simple expand without options
566
- return relationName;
567
- }
568
-
569
- // Build query options for this expand
570
- const parts: string[] = [];
571
-
572
- if (config.options.select) {
573
- // Pass target base table for field transformation
574
- const selectFields = this.formatSelectFields(
575
- Array.isArray(config.options.select)
576
- ? config.options.select.map((f) => String(f))
577
- : [String(config.options.select)],
578
- targetOccurrence?.baseTable,
579
- );
580
- parts.push(`$select=${selectFields}`);
581
- }
582
-
583
- if (config.options.filter) {
584
- // Filter should already be transformed by the nested builder
585
- // Use odata-query to build filter string
586
- const filterQuery = buildQuery({ filter: config.options.filter });
587
- const filterMatch = filterQuery.match(/\$filter=([^&]+)/);
588
- if (filterMatch) {
589
- parts.push(`$filter=${filterMatch[1]}`);
590
- }
591
- }
592
-
593
- if (config.options.orderBy) {
594
- const orderByQuery = buildQuery({ orderBy: config.options.orderBy });
595
- const orderByMatch = orderByQuery.match(/\$orderby=([^&]+)/);
596
- if (orderByMatch) {
597
- parts.push(`$orderby=${orderByMatch[1]}`);
598
- }
599
- }
600
-
601
- if (config.options.top !== undefined) {
602
- parts.push(`$top=${config.options.top}`);
603
- }
604
-
605
- if (config.options.skip !== undefined) {
606
- parts.push(`$skip=${config.options.skip}`);
607
- }
608
-
609
- // Handle nested expand
610
- if (config.options.expand) {
611
- // Nested expand is already a string from buildExpandString
612
- parts.push(`$expand=${String(config.options.expand)}`);
613
- }
614
-
615
- if (parts.length === 0) {
616
- return relationName;
617
- }
404
+ (builder as any).navigation = {
405
+ recordId: this.recordId,
406
+ relation: relationId,
407
+ sourceTableName,
408
+ baseRelation,
409
+ };
618
410
 
619
- return `${relationName}(${parts.join(";")})`;
620
- })
621
- .join(",");
411
+ return builder;
622
412
  }
623
413
 
624
414
  /**
625
415
  * Builds the complete query string including $select and $expand parameters.
626
416
  */
627
417
  private buildQueryString(): string {
628
- const parts: string[] = [];
629
-
630
- // Build $select
631
- if (this.selectedFields && this.selectedFields.length > 0) {
632
- const selectString = this.formatSelectFields(
633
- this.selectedFields,
634
- this.occurrence?.baseTable,
635
- );
636
- if (selectString) {
637
- parts.push(`$select=${selectString}`);
638
- }
639
- }
640
-
641
- // Build $expand
642
- const expandString = this.buildExpandString(this.expandConfigs);
643
- if (expandString) {
644
- parts.push(`$expand=${expandString}`);
645
- }
646
-
647
- if (parts.length === 0) {
648
- return "";
649
- }
650
-
651
- return `?${parts.join("&")}`;
418
+ return buildSelectExpandQueryString({
419
+ selectedFields: this.selectedFields,
420
+ expandConfigs: this.expandConfigs,
421
+ table: this.table,
422
+ useEntityIds: this.databaseUseEntityIds,
423
+ });
652
424
  }
653
425
 
654
-
655
426
  async execute<EO extends ExecuteOptions>(
656
- options?: RequestInit & FFetchOptions & EO,
427
+ options?: ExecuteMethodOptions<EO>,
657
428
  ): Promise<
658
429
  Result<
659
430
  ConditionallyWithODataAnnotations<
660
- RecordReturnType<T, IsSingleField, FieldKey, Selected, Expands>,
431
+ RecordReturnType<
432
+ InferSchemaOutputFromFMTable<NonNullable<Occ>>,
433
+ IsSingleField,
434
+ FieldKey,
435
+ Selected,
436
+ Expands
437
+ >,
661
438
  EO["includeODataAnnotations"] extends true ? true : false
662
439
  >
663
440
  >
@@ -700,50 +477,30 @@ export class RecordBuilder<
700
477
  // Handle single field operation
701
478
  if (this.operation === "getSingleField") {
702
479
  // Single field returns a JSON object with @context and value
703
- const fieldResponse = response as ODataFieldResponse<T>;
480
+ const fieldResponse = response as ODataFieldResponse<
481
+ InferSchemaOutputFromFMTable<NonNullable<Occ>>[FieldKey]
482
+ >;
704
483
  return { data: fieldResponse.value as any, error: undefined };
705
484
  }
706
485
 
707
- // Transform response field IDs back to names if using entity IDs
708
- // Only transform if useEntityIds resolves to true (respects per-request override)
709
- const shouldUseIds = mergedOptions.useEntityIds ?? false;
710
-
711
- // Build expand validation configs for transformation and validation
712
- const expandValidationConfigs =
713
- this.expandConfigs.length > 0
714
- ? this.buildExpandValidationConfigs(this.expandConfigs)
715
- : undefined;
716
-
717
- if (this.occurrence?.baseTable && shouldUseIds) {
718
- response = transformResponseFields(
719
- response,
720
- this.occurrence.baseTable,
721
- expandValidationConfigs,
722
- );
723
- }
724
-
725
- // Get schema from occurrence if available
726
- const schema = this.occurrence?.baseTable?.schema;
727
-
728
- // Validate the single record response
729
- const validation = await validateSingleResponse<any>(
730
- response,
731
- schema,
732
- this.selectedFields as (keyof T)[] | undefined,
733
- expandValidationConfigs,
734
- "exact", // Expect exactly one record
486
+ // Use shared response processor
487
+ const expandBuilder = new ExpandBuilder(
488
+ mergedOptions.useEntityIds ?? false,
489
+ );
490
+ const expandValidationConfigs = expandBuilder.buildValidationConfigs(
491
+ this.expandConfigs,
735
492
  );
736
493
 
737
- if (!validation.valid) {
738
- return { data: undefined, error: validation.error };
739
- }
740
-
741
- // Handle null response
742
- if (validation.data === null) {
743
- return { data: null as any, error: undefined };
744
- }
745
-
746
- return { data: validation.data as any, error: undefined };
494
+ return processODataResponse(response, {
495
+ table: this.table,
496
+ schema: getSchemaFromTable(this.table),
497
+ singleMode: "exact",
498
+ selectedFields: this.selectedFields,
499
+ expandValidationConfigs,
500
+ skipValidation: options?.skipValidation,
501
+ useEntityIds: mergedOptions.useEntityIds,
502
+ fieldMapping: this.fieldMapping,
503
+ });
747
504
  }
748
505
 
749
506
  getRequestConfig(): { method: string; url: string; body?: any } {
@@ -806,73 +563,66 @@ export class RecordBuilder<
806
563
 
807
564
  toRequest(baseUrl: string, options?: ExecuteOptions): Request {
808
565
  const config = this.getRequestConfig();
809
- const fullUrl = `${baseUrl}${config.url}`;
810
-
811
- return new Request(fullUrl, {
812
- method: config.method,
813
- headers: {
814
- "Content-Type": "application/json",
815
- Accept: getAcceptHeader(options?.includeODataAnnotations),
816
- },
817
- });
566
+ return createODataRequest(baseUrl, config, options);
818
567
  }
819
568
 
820
569
  async processResponse(
821
570
  response: Response,
822
571
  options?: ExecuteOptions,
823
572
  ): Promise<
824
- Result<RecordReturnType<T, IsSingleField, FieldKey, Selected, Expands>>
573
+ Result<
574
+ RecordReturnType<
575
+ InferSchemaOutputFromFMTable<NonNullable<Occ>>,
576
+ IsSingleField,
577
+ FieldKey,
578
+ Selected,
579
+ Expands
580
+ >
581
+ >
825
582
  > {
583
+ // Check for error responses (important for batch operations)
584
+ if (!response.ok) {
585
+ const tableName = this.table ? getTableName(this.table) : "unknown";
586
+ const error = await parseErrorResponse(
587
+ response,
588
+ response.url || `/${this.databaseName}/${tableName}`,
589
+ );
590
+ return { data: undefined, error };
591
+ }
592
+
826
593
  // Use safeJsonParse to handle FileMaker's invalid JSON with unquoted ? values
827
594
  const rawResponse = await safeJsonParse(response);
828
595
 
829
596
  // Handle single field operation
830
597
  if (this.operation === "getSingleField") {
831
598
  // Single field returns a JSON object with @context and value
832
- const fieldResponse = rawResponse as ODataFieldResponse<T>;
599
+ const fieldResponse = rawResponse as ODataFieldResponse<
600
+ InferSchemaOutputFromFMTable<NonNullable<Occ>>[FieldKey]
601
+ >;
833
602
  return { data: fieldResponse.value as any, error: undefined };
834
603
  }
835
604
 
836
- // Transform response field IDs back to names if using entity IDs
837
- // Only transform if useEntityIds resolves to true (respects per-request override)
838
- const shouldUseIds = options?.useEntityIds ?? this.databaseUseEntityIds;
839
-
840
- // Build expand validation configs for transformation and validation
841
- const expandValidationConfigs =
842
- this.expandConfigs.length > 0
843
- ? this.buildExpandValidationConfigs(this.expandConfigs)
844
- : undefined;
845
-
846
- let transformedResponse = rawResponse;
847
- if (this.occurrence?.baseTable && shouldUseIds) {
848
- transformedResponse = transformResponseFields(
849
- rawResponse,
850
- this.occurrence.baseTable,
851
- expandValidationConfigs,
852
- );
853
- }
854
-
855
- // Get schema from occurrence if available
856
- const schema = this.occurrence?.baseTable?.schema;
857
-
858
- // Validate the single record response
859
- const validation = await validateSingleResponse<any>(
860
- transformedResponse,
861
- schema,
862
- this.selectedFields as (keyof T)[] | undefined,
863
- expandValidationConfigs,
864
- "exact", // Expect exactly one record
605
+ // Use shared response processor
606
+ const mergedOptions = mergeExecuteOptions(
607
+ options,
608
+ this.databaseUseEntityIds,
609
+ );
610
+ const expandBuilder = new ExpandBuilder(
611
+ mergedOptions.useEntityIds ?? false,
612
+ );
613
+ const expandValidationConfigs = expandBuilder.buildValidationConfigs(
614
+ this.expandConfigs,
865
615
  );
866
616
 
867
- if (!validation.valid) {
868
- return { data: undefined, error: validation.error };
869
- }
870
-
871
- // Handle null response
872
- if (validation.data === null) {
873
- return { data: null as any, error: undefined };
874
- }
875
-
876
- return { data: validation.data as any, error: undefined };
617
+ return processODataResponse(rawResponse, {
618
+ table: this.table,
619
+ schema: getSchemaFromTable(this.table),
620
+ singleMode: "exact",
621
+ selectedFields: this.selectedFields,
622
+ expandValidationConfigs,
623
+ skipValidation: options?.skipValidation,
624
+ useEntityIds: mergedOptions.useEntityIds,
625
+ fieldMapping: this.fieldMapping,
626
+ });
877
627
  }
878
628
  }