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

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 -4
  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 +11 -15
  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 +7 -9
  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 +134 -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 -63
  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 +16 -21
  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 +16 -13
  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 +20 -17
  92. package/src/client/batch-builder.ts +100 -32
  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 +46 -51
  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 +124 -43
  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 +816 -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 +325 -585
  116. package/src/client/response-processor.ts +4 -5
  117. package/src/client/update-builder.ts +102 -73
  118. package/src/errors.ts +22 -1
  119. package/src/index.ts +55 -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 +20 -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,107 @@ 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,
11
8
  } 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";
9
+ import type {
10
+ FMTable,
11
+ InferSchemaOutputFromFMTable,
12
+ ValidExpandTarget,
13
+ ExtractTableName,
14
+ ValidateNoContainerFields,
15
+ } from "../orm/table";
16
+ import { getTableName, getNavigationPaths } from "../orm/table";
21
17
  import { safeJsonParse } from "./sanitize-json";
18
+ import { parseErrorResponse } from "./error-parser";
22
19
  import { QueryBuilder } from "./query-builder";
23
- import {
24
- validateSingleResponse,
25
- type ExpandValidationConfig,
26
- } from "../validation";
27
20
  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>>;
21
+ import { isColumn, type Column } from "../orm/column";
22
+ import {
23
+ type ExpandConfig,
24
+ type ExpandedRelations,
25
+ ExpandBuilder,
26
+ resolveTableId,
27
+ mergeExecuteOptions,
28
+ processODataResponse,
29
+ getSchemaFromTable,
30
+ processSelectWithRenames,
31
+ buildSelectExpandQueryString,
32
+ createODataRequest,
33
+ } from "./builders";
34
+
35
+ /**
36
+ * Extract the value type from a Column.
37
+ * This uses the phantom type stored in Column to get the actual value type.
38
+ */
39
+ type ExtractColumnType<C> = C extends Column<infer T, any> ? T : never;
40
+
41
+ /**
42
+ * Map a select object to its return type.
43
+ * For each key in the select object, extract the type from the corresponding Column.
44
+ */
45
+ type MapSelectToReturnType<
46
+ TSelect extends Record<string, Column<any, any>>,
47
+ TSchema extends Record<string, any>,
48
+ > = {
49
+ [K in keyof TSelect]: ExtractColumnType<TSelect[K]>;
96
50
  };
97
51
 
98
- // Type to represent expanded relations
99
- export type ExpandedRelations = Record<string, { schema: any; selected: any }>;
100
-
101
52
  // Return type for RecordBuilder execute
102
53
  export type RecordReturnType<
103
- T extends Record<string, any>,
54
+ Schema extends Record<string, any>,
104
55
  IsSingleField extends boolean,
105
- FieldKey extends keyof T,
106
- Selected extends keyof T,
56
+ FieldKey extends keyof Schema,
57
+ Selected extends
58
+ | keyof Schema
59
+ | Record<string, Column<any, ExtractTableName<FMTable<any, any>>>>,
107
60
  Expands extends ExpandedRelations,
108
61
  > = 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
- };
62
+ ? Schema[FieldKey]
63
+ : // Use tuple wrapping [Selected] extends [...] to prevent distribution over unions
64
+ [Selected] extends [Record<string, Column<any, any>>]
65
+ ? MapSelectToReturnType<Selected, Schema> & {
66
+ [K in keyof Expands]: Pick<
67
+ Expands[K]["schema"],
68
+ Expands[K]["selected"]
69
+ >[];
70
+ }
71
+ : // Use tuple wrapping to prevent distribution over union of keys
72
+ [Selected] extends [keyof Schema]
73
+ ? Pick<Schema, Selected> & {
74
+ [K in keyof Expands]: Pick<
75
+ Expands[K]["schema"],
76
+ Expands[K]["selected"]
77
+ >[];
78
+ }
79
+ : never;
116
80
 
117
81
  export class RecordBuilder<
118
- T extends Record<string, any>,
82
+ Occ extends FMTable<any, any> = FMTable<any, any>,
119
83
  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,
84
+ FieldKey extends keyof InferSchemaOutputFromFMTable<
85
+ NonNullable<Occ>
86
+ > = keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,
87
+ Selected extends
88
+ | keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>
89
+ | Record<
90
+ string,
91
+ Column<any, ExtractTableName<NonNullable<Occ>>>
92
+ > = keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,
125
93
  Expands extends ExpandedRelations = {},
126
94
  > implements
127
95
  ExecutableBuilder<
128
- RecordReturnType<T, IsSingleField, FieldKey, Selected, Expands>
96
+ RecordReturnType<
97
+ InferSchemaOutputFromFMTable<NonNullable<Occ>>,
98
+ IsSingleField,
99
+ FieldKey,
100
+ Selected,
101
+ Expands
102
+ >
129
103
  >
130
104
  {
131
- private occurrence?: Occ;
132
- private tableName: string;
105
+ private table: Occ;
133
106
  private databaseName: string;
134
107
  private context: ExecutionContext;
135
108
  private recordId: string | number;
@@ -141,20 +114,20 @@ export class RecordBuilder<
141
114
 
142
115
  private databaseUseEntityIds: boolean;
143
116
 
144
- // New properties for select/expand support
117
+ // Properties for select/expand support
145
118
  private selectedFields?: string[];
146
119
  private expandConfigs: ExpandConfig[] = [];
120
+ // Mapping from field names to output keys (for renamed fields in select)
121
+ private fieldMapping?: Record<string, string>;
147
122
 
148
123
  constructor(config: {
149
- occurrence?: Occ;
150
- tableName: string;
124
+ occurrence: Occ;
151
125
  databaseName: string;
152
126
  context: ExecutionContext;
153
127
  recordId: string | number;
154
128
  databaseUseEntityIds?: boolean;
155
129
  }) {
156
- this.occurrence = config.occurrence;
157
- this.tableName = config.tableName;
130
+ this.table = config.occurrence;
158
131
  this.databaseName = config.databaseName;
159
132
  this.context = config.context;
160
133
  this.recordId = config.recordId;
@@ -167,11 +140,7 @@ export class RecordBuilder<
167
140
  private mergeExecuteOptions(
168
141
  options?: RequestInit & FFetchOptions & ExecuteOptions,
169
142
  ): 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
- };
143
+ return mergeExecuteOptions(options, this.databaseUseEntityIds);
175
144
  }
176
145
 
177
146
  /**
@@ -179,39 +148,48 @@ export class RecordBuilder<
179
148
  * @param useEntityIds - Optional override for entity ID usage
180
149
  */
181
150
  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;
151
+ if (!this.table) {
152
+ throw new Error("Table occurrence is required");
197
153
  }
198
-
199
- return this.occurrence.getTableName();
154
+ return resolveTableId(
155
+ this.table,
156
+ getTableName(this.table),
157
+ this.context,
158
+ useEntityIds,
159
+ );
200
160
  }
201
161
 
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,
162
+ /**
163
+ * Creates a new RecordBuilder with modified configuration.
164
+ * Used by select() to create new instances.
165
+ */
166
+ private cloneWithChanges<
167
+ NewSelected extends
168
+ | keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>
169
+ | Record<
170
+ string,
171
+ Column<any, ExtractTableName<NonNullable<Occ>>>
172
+ > = Selected,
173
+ >(changes: {
174
+ selectedFields?: string[];
175
+ fieldMapping?: Record<string, string>;
176
+ }): RecordBuilder<Occ, false, FieldKey, NewSelected, Expands> {
177
+ const newBuilder = new RecordBuilder<
178
+ Occ,
179
+ false,
180
+ FieldKey,
181
+ NewSelected,
182
+ Expands
183
+ >({
184
+ occurrence: this.table,
208
185
  databaseName: this.databaseName,
209
186
  context: this.context,
210
187
  recordId: this.recordId,
211
188
  databaseUseEntityIds: this.databaseUseEntityIds,
212
189
  });
213
- newBuilder.operation = "getSingleField";
214
- newBuilder.operationParam = field.toString();
190
+ newBuilder.selectedFields = changes.selectedFields ?? this.selectedFields;
191
+ newBuilder.fieldMapping = changes.fieldMapping ?? this.fieldMapping;
192
+ newBuilder.expandConfigs = [...this.expandConfigs];
215
193
  // Preserve navigation context
216
194
  newBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;
217
195
  newBuilder.navigateRelation = this.navigateRelation;
@@ -219,30 +197,32 @@ export class RecordBuilder<
219
197
  return newBuilder;
220
198
  }
221
199
 
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,
200
+ getSingleField<
201
+ K extends keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,
202
+ >(
203
+ field: K,
204
+ ): RecordBuilder<
205
+ Occ,
206
+ true,
207
+ K,
208
+ keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,
209
+ {}
210
+ > {
211
+ const newBuilder = new RecordBuilder<
212
+ Occ,
213
+ true,
214
+ K,
215
+ keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,
216
+ {}
217
+ >({
218
+ occurrence: this.table,
239
219
  databaseName: this.databaseName,
240
220
  context: this.context,
241
221
  recordId: this.recordId,
242
222
  databaseUseEntityIds: this.databaseUseEntityIds,
243
223
  });
244
- newBuilder.selectedFields = uniqueFields.map((f) => String(f));
245
- newBuilder.expandConfigs = [...this.expandConfigs];
224
+ newBuilder.operation = "getSingleField";
225
+ newBuilder.operationParam = field.toString();
246
226
  // Preserve navigation context
247
227
  newBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;
248
228
  newBuilder.navigateRelation = this.navigateRelation;
@@ -250,93 +230,76 @@ export class RecordBuilder<
250
230
  return newBuilder;
251
231
  }
252
232
 
233
+ /**
234
+ * Select fields using column references.
235
+ * Allows renaming fields by using different keys in the object.
236
+ * Container fields cannot be selected and will cause a type error.
237
+ *
238
+ * @example
239
+ * db.from(contacts).get("uuid").select({
240
+ * name: contacts.name,
241
+ * userEmail: contacts.email // renamed!
242
+ * })
243
+ *
244
+ * @param fields - Object mapping output keys to column references (container fields excluded)
245
+ * @returns RecordBuilder with updated selected fields
246
+ */
247
+ select<
248
+ TSelect extends Record<string, Column<any, ExtractTableName<Occ>, false>>,
249
+ >(fields: TSelect): RecordBuilder<Occ, false, FieldKey, TSelect, Expands> {
250
+ const tableName = getTableName(this.table);
251
+ const { selectedFields, fieldMapping } = processSelectWithRenames(
252
+ fields,
253
+ tableName,
254
+ );
255
+
256
+ return this.cloneWithChanges({
257
+ selectedFields,
258
+ fieldMapping:
259
+ Object.keys(fieldMapping).length > 0 ? fieldMapping : undefined,
260
+ }) as any;
261
+ }
262
+
253
263
  /**
254
264
  * Expand a navigation property to include related records.
255
265
  * Supports nested select, filter, orderBy, and expand operations.
256
266
  *
257
267
  * @example
258
268
  * ```typescript
259
- * // Simple expand
260
- * const contact = await db.from("contacts").get("uuid").expand("users").execute();
269
+ * // Simple expand with FMTable object
270
+ * const contact = await db.from(contacts).get("uuid").expand(users).execute();
261
271
  *
262
272
  * // Expand with select
263
- * const contact = await db.from("contacts").get("uuid")
264
- * .expand("users", b => b.select("username", "email"))
273
+ * const contact = await db.from(contacts).get("uuid")
274
+ * .expand(users, b => b.select({ username: users.username, email: users.email }))
265
275
  * .execute();
266
276
  * ```
267
277
  */
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,
278
+ expand<TargetTable extends FMTable<any, any>>(
279
+ targetTable: ValidExpandTarget<Occ, TargetTable>,
281
280
  callback?: (
282
281
  builder: QueryBuilder<
283
- TargetSchema,
284
- keyof TargetSchema,
285
- false,
282
+ TargetTable,
283
+ keyof InferSchemaOutputFromFMTable<TargetTable>,
286
284
  false,
287
- TargetOcc extends TableOccurrence<any, any, any, any>
288
- ? TargetOcc
289
- : undefined
285
+ false
290
286
  >,
291
- ) => QueryBuilder<
292
- WithSystemFields<TargetSchema>,
293
- TargetSelected,
294
- any,
295
- any,
296
- any
297
- >,
287
+ ) => QueryBuilder<TargetTable, any, any, any, any>,
298
288
  ): RecordBuilder<
299
- T,
289
+ Occ,
300
290
  false,
301
291
  FieldKey,
302
- Occ,
303
292
  Selected,
304
293
  Expands & {
305
- [K in Rel]: { schema: TargetSchema; selected: TargetSelected };
294
+ [K in ExtractTableName<TargetTable>]: {
295
+ schema: InferSchemaOutputFromFMTable<TargetTable>;
296
+ selected: keyof InferSchemaOutputFromFMTable<TargetTable>;
297
+ };
306
298
  }
307
299
  > {
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
300
  // 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,
301
+ const newBuilder = new RecordBuilder<Occ, false, FieldKey, Selected, any>({
302
+ occurrence: this.table,
340
303
  databaseName: this.databaseName,
341
304
  context: this.context,
342
305
  recordId: this.recordId,
@@ -345,319 +308,123 @@ export class RecordBuilder<
345
308
 
346
309
  // Copy existing state
347
310
  newBuilder.selectedFields = this.selectedFields;
311
+ newBuilder.fieldMapping = this.fieldMapping;
348
312
  newBuilder.expandConfigs = [...this.expandConfigs];
349
313
  newBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;
350
314
  newBuilder.navigateRelation = this.navigateRelation;
351
315
  newBuilder.navigateSourceTableName = this.navigateSourceTableName;
352
316
 
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
- };
317
+ // Use ExpandBuilder.processExpand to handle the expand logic
318
+ const expandBuilder = new ExpandBuilder(this.databaseUseEntityIds);
319
+ const expandConfig = expandBuilder.processExpand(
320
+ targetTable,
321
+ this.table ?? undefined,
322
+ callback,
323
+ () =>
324
+ new QueryBuilder<TargetTable>({
325
+ occurrence: targetTable,
326
+ databaseName: this.databaseName,
327
+ context: this.context,
328
+ databaseUseEntityIds: this.databaseUseEntityIds,
329
+ }),
330
+ );
382
331
 
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
- }
332
+ newBuilder.expandConfigs.push(expandConfig);
333
+ return newBuilder as any;
334
+ }
390
335
 
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,
336
+ navigate<TargetTable extends FMTable<any, any>>(
337
+ targetTable: ValidExpandTarget<Occ, TargetTable>,
338
+ ): QueryBuilder<
339
+ TargetTable,
340
+ keyof InferSchemaOutputFromFMTable<TargetTable>,
341
+ false,
342
+ false
343
+ > {
344
+ // Extract name and validate
345
+ const relationName = getTableName(targetTable);
346
+
347
+ // Runtime validation: Check if relation name is in navigationPaths
348
+ if (this.table) {
349
+ const navigationPaths = getNavigationPaths(this.table);
350
+ if (navigationPaths && !navigationPaths.includes(relationName)) {
351
+ console.warn(
352
+ `Cannot navigate to "${relationName}". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(", ") : "none"}`,
396
353
  );
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
354
  }
420
355
  }
421
356
 
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,
357
+ // Create QueryBuilder with target table
358
+ const builder = new QueryBuilder<TargetTable>({
359
+ occurrence: targetTable,
449
360
  databaseName: this.databaseName,
450
361
  context: this.context,
362
+ databaseUseEntityIds: this.databaseUseEntityIds,
451
363
  });
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
364
 
458
- (builder as any).isNavigate = true;
459
- (builder as any).navigateRecordId = this.recordId;
460
- (builder as any).navigateRelation = relationId;
365
+ // Store the navigation info - we'll use it in execute
366
+ // Use relation name as-is (entity ID handling is done in QueryBuilder)
367
+ const relationId = relationName;
461
368
 
462
369
  // If this RecordBuilder came from a navigated EntitySet, we need to preserve that base path
370
+ let sourceTableName: string;
371
+ let baseRelation: string | undefined;
463
372
  if (
464
373
  this.isNavigateFromEntitySet &&
465
374
  this.navigateSourceTableName &&
466
375
  this.navigateRelation
467
376
  ) {
468
377
  // Build the base path: /sourceTable/relation('recordId')/newRelation
469
- (builder as any).navigateSourceTableName = this.navigateSourceTableName;
470
- (builder as any).navigateBaseRelation = this.navigateRelation;
378
+ sourceTableName = this.navigateSourceTableName;
379
+ baseRelation = this.navigateRelation;
471
380
  } else {
472
381
  // 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 "";
382
+ // Use table ID if available, otherwise table name
383
+ if (!this.table) {
384
+ throw new Error("Table occurrence is required for navigation");
385
+ }
386
+ sourceTableName = resolveTableId(
387
+ this.table,
388
+ getTableName(this.table),
389
+ this.context,
390
+ this.databaseUseEntityIds,
391
+ );
550
392
  }
551
393
 
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
- }
394
+ (builder as any).navigation = {
395
+ recordId: this.recordId,
396
+ relation: relationId,
397
+ sourceTableName,
398
+ baseRelation,
399
+ };
618
400
 
619
- return `${relationName}(${parts.join(";")})`;
620
- })
621
- .join(",");
401
+ return builder;
622
402
  }
623
403
 
624
404
  /**
625
405
  * Builds the complete query string including $select and $expand parameters.
626
406
  */
627
407
  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("&")}`;
408
+ return buildSelectExpandQueryString({
409
+ selectedFields: this.selectedFields,
410
+ expandConfigs: this.expandConfigs,
411
+ table: this.table,
412
+ useEntityIds: this.databaseUseEntityIds,
413
+ });
652
414
  }
653
415
 
654
-
655
416
  async execute<EO extends ExecuteOptions>(
656
417
  options?: RequestInit & FFetchOptions & EO,
657
418
  ): Promise<
658
419
  Result<
659
420
  ConditionallyWithODataAnnotations<
660
- RecordReturnType<T, IsSingleField, FieldKey, Selected, Expands>,
421
+ RecordReturnType<
422
+ InferSchemaOutputFromFMTable<NonNullable<Occ>>,
423
+ IsSingleField,
424
+ FieldKey,
425
+ Selected,
426
+ Expands
427
+ >,
661
428
  EO["includeODataAnnotations"] extends true ? true : false
662
429
  >
663
430
  >
@@ -700,50 +467,30 @@ export class RecordBuilder<
700
467
  // Handle single field operation
701
468
  if (this.operation === "getSingleField") {
702
469
  // Single field returns a JSON object with @context and value
703
- const fieldResponse = response as ODataFieldResponse<T>;
470
+ const fieldResponse = response as ODataFieldResponse<
471
+ InferSchemaOutputFromFMTable<NonNullable<Occ>>[FieldKey]
472
+ >;
704
473
  return { data: fieldResponse.value as any, error: undefined };
705
474
  }
706
475
 
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
476
+ // Use shared response processor
477
+ const expandBuilder = new ExpandBuilder(
478
+ mergedOptions.useEntityIds ?? false,
479
+ );
480
+ const expandValidationConfigs = expandBuilder.buildValidationConfigs(
481
+ this.expandConfigs,
735
482
  );
736
483
 
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 };
484
+ return processODataResponse(response, {
485
+ table: this.table,
486
+ schema: getSchemaFromTable(this.table),
487
+ singleMode: "exact",
488
+ selectedFields: this.selectedFields,
489
+ expandValidationConfigs,
490
+ skipValidation: options?.skipValidation,
491
+ useEntityIds: mergedOptions.useEntityIds,
492
+ fieldMapping: this.fieldMapping,
493
+ });
747
494
  }
748
495
 
749
496
  getRequestConfig(): { method: string; url: string; body?: any } {
@@ -806,73 +553,66 @@ export class RecordBuilder<
806
553
 
807
554
  toRequest(baseUrl: string, options?: ExecuteOptions): Request {
808
555
  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
- });
556
+ return createODataRequest(baseUrl, config, options);
818
557
  }
819
558
 
820
559
  async processResponse(
821
560
  response: Response,
822
561
  options?: ExecuteOptions,
823
562
  ): Promise<
824
- Result<RecordReturnType<T, IsSingleField, FieldKey, Selected, Expands>>
563
+ Result<
564
+ RecordReturnType<
565
+ InferSchemaOutputFromFMTable<NonNullable<Occ>>,
566
+ IsSingleField,
567
+ FieldKey,
568
+ Selected,
569
+ Expands
570
+ >
571
+ >
825
572
  > {
573
+ // Check for error responses (important for batch operations)
574
+ if (!response.ok) {
575
+ const tableName = this.table ? getTableName(this.table) : "unknown";
576
+ const error = await parseErrorResponse(
577
+ response,
578
+ response.url || `/${this.databaseName}/${tableName}`,
579
+ );
580
+ return { data: undefined, error };
581
+ }
582
+
826
583
  // Use safeJsonParse to handle FileMaker's invalid JSON with unquoted ? values
827
584
  const rawResponse = await safeJsonParse(response);
828
585
 
829
586
  // Handle single field operation
830
587
  if (this.operation === "getSingleField") {
831
588
  // Single field returns a JSON object with @context and value
832
- const fieldResponse = rawResponse as ODataFieldResponse<T>;
589
+ const fieldResponse = rawResponse as ODataFieldResponse<
590
+ InferSchemaOutputFromFMTable<NonNullable<Occ>>[FieldKey]
591
+ >;
833
592
  return { data: fieldResponse.value as any, error: undefined };
834
593
  }
835
594
 
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
595
+ // Use shared response processor
596
+ const mergedOptions = mergeExecuteOptions(
597
+ options,
598
+ this.databaseUseEntityIds,
599
+ );
600
+ const expandBuilder = new ExpandBuilder(
601
+ mergedOptions.useEntityIds ?? false,
602
+ );
603
+ const expandValidationConfigs = expandBuilder.buildValidationConfigs(
604
+ this.expandConfigs,
865
605
  );
866
606
 
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 };
607
+ return processODataResponse(rawResponse, {
608
+ table: this.table,
609
+ schema: getSchemaFromTable(this.table),
610
+ singleMode: "exact",
611
+ selectedFields: this.selectedFields,
612
+ expandValidationConfigs,
613
+ skipValidation: options?.skipValidation,
614
+ useEntityIds: mergedOptions.useEntityIds,
615
+ fieldMapping: this.fieldMapping,
616
+ });
877
617
  }
878
618
  }