@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
@@ -0,0 +1,273 @@
1
+ import type { FMTable } from "../../orm/table";
2
+ import type { Result } from "../../types";
3
+ import type { ExpandValidationConfig } from "../../validation";
4
+ import { validateSingleResponse, validateListResponse } from "../../validation";
5
+ import { transformResponseFields } from "../../transform";
6
+ import { RecordCountMismatchError } from "../../errors";
7
+ import { getBaseTableConfig } from "../../orm/table";
8
+ import { ExpandBuilder } from "./expand-builder";
9
+ import type { ExpandConfig } from "./shared-types";
10
+
11
+ export interface ProcessResponseConfig {
12
+ table?: FMTable<any, any>;
13
+ schema?: Record<string, any>;
14
+ singleMode: "exact" | "maybe" | false;
15
+ selectedFields?: string[];
16
+ expandValidationConfigs?: ExpandValidationConfig[];
17
+ skipValidation?: boolean;
18
+ useEntityIds?: boolean;
19
+ // Mapping from field names to output keys (for renamed fields in select)
20
+ fieldMapping?: Record<string, string>;
21
+ }
22
+
23
+ /**
24
+ * Processes OData response with transformation and validation.
25
+ * Shared by QueryBuilder and RecordBuilder.
26
+ */
27
+ export async function processODataResponse<T>(
28
+ rawResponse: any,
29
+ config: ProcessResponseConfig,
30
+ ): Promise<Result<T>> {
31
+ const {
32
+ table,
33
+ schema,
34
+ singleMode,
35
+ selectedFields,
36
+ expandValidationConfigs,
37
+ skipValidation,
38
+ useEntityIds,
39
+ fieldMapping,
40
+ } = config;
41
+
42
+ // Transform field IDs back to names if using entity IDs
43
+ let response = rawResponse;
44
+ if (table && useEntityIds) {
45
+ response = transformResponseFields(
46
+ response,
47
+ table,
48
+ expandValidationConfigs,
49
+ );
50
+ }
51
+
52
+ // Fast path: skip validation
53
+ if (skipValidation) {
54
+ const result = extractRecords(response, singleMode);
55
+ // Rename fields AFTER extraction (but before returning)
56
+ if (result.data && fieldMapping && Object.keys(fieldMapping).length > 0) {
57
+ if (result.error) {
58
+ return { data: undefined, error: result.error } as Result<T>;
59
+ }
60
+ return {
61
+ data: renameFieldsInResponse(result.data, fieldMapping) as T,
62
+ error: undefined,
63
+ };
64
+ }
65
+ return result as Result<T>;
66
+ }
67
+
68
+ // Validation path
69
+ if (singleMode !== false) {
70
+ const validation = await validateSingleResponse<any>(
71
+ response,
72
+ schema,
73
+ selectedFields as any,
74
+ expandValidationConfigs,
75
+ singleMode,
76
+ );
77
+
78
+ if (!validation.valid) {
79
+ return { data: undefined, error: validation.error };
80
+ }
81
+
82
+ // Rename fields AFTER validation completes
83
+ if (fieldMapping && Object.keys(fieldMapping).length > 0) {
84
+ return {
85
+ data: renameFieldsInResponse(validation.data, fieldMapping) as T,
86
+ error: undefined,
87
+ };
88
+ }
89
+
90
+ return { data: validation.data as T, error: undefined };
91
+ }
92
+
93
+ const validation = await validateListResponse<any>(
94
+ response,
95
+ schema,
96
+ selectedFields as any,
97
+ expandValidationConfigs,
98
+ );
99
+
100
+ if (!validation.valid) {
101
+ return { data: undefined, error: validation.error };
102
+ }
103
+
104
+ // Rename fields AFTER validation completes
105
+ if (fieldMapping && Object.keys(fieldMapping).length > 0) {
106
+ return {
107
+ data: renameFieldsInResponse(validation.data, fieldMapping) as T,
108
+ error: undefined,
109
+ };
110
+ }
111
+
112
+ return { data: validation.data as T, error: undefined };
113
+ }
114
+
115
+ /**
116
+ * Extracts records from response without validation.
117
+ */
118
+ function extractRecords<T>(
119
+ response: any,
120
+ singleMode: "exact" | "maybe" | false,
121
+ ): Result<T> {
122
+ if (singleMode === false) {
123
+ const records = response.value ?? [];
124
+ return { data: records as T, error: undefined };
125
+ }
126
+
127
+ const records = response.value ?? [response];
128
+ const count = Array.isArray(records) ? records.length : 1;
129
+
130
+ if (count > 1) {
131
+ return {
132
+ data: undefined,
133
+ error: new RecordCountMismatchError(
134
+ singleMode === "exact" ? "one" : "at-most-one",
135
+ count,
136
+ ),
137
+ };
138
+ }
139
+
140
+ if (count === 0) {
141
+ if (singleMode === "exact") {
142
+ return { data: undefined, error: new RecordCountMismatchError("one", 0) };
143
+ }
144
+ return { data: null as T, error: undefined };
145
+ }
146
+
147
+ const record = Array.isArray(records) ? records[0] : records;
148
+ return { data: record as T, error: undefined };
149
+ }
150
+
151
+ /**
152
+ * Gets schema from a table occurrence, excluding container fields.
153
+ * Container fields are never returned in regular responses (only via getSingleField).
154
+ */
155
+ export function getSchemaFromTable(
156
+ table: FMTable<any, any> | undefined,
157
+ ): Record<string, any> | undefined {
158
+ if (!table) return undefined;
159
+ const baseTableConfig = getBaseTableConfig(table);
160
+ const containerFields = baseTableConfig.containerFields || [];
161
+
162
+ // Filter out container fields from schema
163
+ const schema = { ...baseTableConfig.schema };
164
+ for (const containerField of containerFields) {
165
+ delete schema[containerField as string];
166
+ }
167
+
168
+ return schema;
169
+ }
170
+
171
+ /**
172
+ * Renames fields in response data according to the field mapping.
173
+ * Used when select() is called with renamed fields (e.g., { userEmail: users.email }).
174
+ */
175
+ function renameFieldsInResponse(
176
+ data: any,
177
+ fieldMapping: Record<string, string>,
178
+ ): any {
179
+ if (!data || typeof data !== "object") {
180
+ return data;
181
+ }
182
+
183
+ // Handle array responses
184
+ if (Array.isArray(data)) {
185
+ return data.map((item) => renameFieldsInResponse(item, fieldMapping));
186
+ }
187
+
188
+ // Handle OData list response structure
189
+ if ("value" in data && Array.isArray(data.value)) {
190
+ return {
191
+ ...data,
192
+ value: data.value.map((item: any) =>
193
+ renameFieldsInResponse(item, fieldMapping),
194
+ ),
195
+ };
196
+ }
197
+
198
+ // Handle single record
199
+ const renamed: Record<string, any> = {};
200
+ for (const [key, value] of Object.entries(data)) {
201
+ // Check if this field should be renamed
202
+ const outputKey = fieldMapping[key];
203
+ if (outputKey) {
204
+ renamed[outputKey] = value;
205
+ } else {
206
+ renamed[key] = value;
207
+ }
208
+ }
209
+ return renamed;
210
+ }
211
+
212
+ /**
213
+ * Processes query response with expand configs.
214
+ * This is a convenience wrapper that builds validation configs from expand configs.
215
+ */
216
+ export async function processQueryResponse<T>(
217
+ response: any,
218
+ config: {
219
+ occurrence?: FMTable<any, any>;
220
+ singleMode: "exact" | "maybe" | false;
221
+ queryOptions: { select?: (keyof T)[] | string[] };
222
+ expandConfigs: ExpandConfig[];
223
+ skipValidation?: boolean;
224
+ useEntityIds?: boolean;
225
+ // Mapping from field names to output keys (for renamed fields in select)
226
+ fieldMapping?: Record<string, string>;
227
+ },
228
+ ): Promise<Result<any>> {
229
+ const {
230
+ occurrence,
231
+ singleMode,
232
+ queryOptions,
233
+ expandConfigs,
234
+ skipValidation,
235
+ useEntityIds,
236
+ fieldMapping,
237
+ } = config;
238
+
239
+ const expandBuilder = new ExpandBuilder(useEntityIds ?? false);
240
+ const expandValidationConfigs =
241
+ expandBuilder.buildValidationConfigs(expandConfigs);
242
+
243
+ const selectedFields = queryOptions.select
244
+ ? Array.isArray(queryOptions.select)
245
+ ? queryOptions.select.map(String)
246
+ : [String(queryOptions.select)]
247
+ : undefined;
248
+
249
+ // Process the response first
250
+ let processedResponse = await processODataResponse(response, {
251
+ table: occurrence,
252
+ schema: getSchemaFromTable(occurrence),
253
+ singleMode,
254
+ selectedFields,
255
+ expandValidationConfigs,
256
+ skipValidation,
257
+ useEntityIds,
258
+ });
259
+
260
+ // Rename fields if field mapping is provided (for renamed fields in select)
261
+ if (
262
+ processedResponse.data &&
263
+ fieldMapping &&
264
+ Object.keys(fieldMapping).length > 0
265
+ ) {
266
+ processedResponse = {
267
+ ...processedResponse,
268
+ data: renameFieldsInResponse(processedResponse.data, fieldMapping),
269
+ };
270
+ }
271
+
272
+ return processedResponse;
273
+ }
@@ -0,0 +1,74 @@
1
+ import { isColumn, type Column } from "../../orm/column";
2
+
3
+ /**
4
+ * Utility function for processing select() calls.
5
+ * Used by both QueryBuilder and RecordBuilder to eliminate duplication.
6
+ *
7
+ * @param fields - Field names or Column references
8
+ * @returns Object with selectedFields array
9
+ */
10
+ export function processSelectFields(
11
+ ...fields: (string | Column<any, string>)[]
12
+ ): { selectedFields: string[] } {
13
+ const fieldNames = fields.map((field) => {
14
+ if (isColumn(field)) {
15
+ return field.fieldName as string;
16
+ }
17
+ return String(field);
18
+ });
19
+ return { selectedFields: [...new Set(fieldNames)] };
20
+ }
21
+
22
+ /**
23
+ * Processes select() calls with field renaming support.
24
+ * Validates columns belong to the correct table and builds field mapping for renamed fields.
25
+ * Used by both QueryBuilder and RecordBuilder to eliminate duplication.
26
+ *
27
+ * @param fields - Object mapping output keys to column references
28
+ * @param tableName - Expected table name for validation
29
+ * @returns Object with selectedFields array and fieldMapping for renamed fields
30
+ */
31
+ export function processSelectWithRenames<TTableName extends string>(
32
+ fields: Record<string, Column<any, TTableName>>,
33
+ tableName: string,
34
+ ): { selectedFields: string[]; fieldMapping: Record<string, string> } {
35
+ const selectedFields: string[] = [];
36
+ const fieldMapping: Record<string, string> = {};
37
+
38
+ for (const [outputKey, column] of Object.entries(fields)) {
39
+ if (!isColumn(column)) {
40
+ throw new Error(
41
+ `select() expects column references, but got: ${typeof column}`,
42
+ );
43
+ }
44
+
45
+ // Warn (not throw) on table mismatch for consistency
46
+ if (column.tableName !== tableName) {
47
+ console.warn(
48
+ `Column ${column.toString()} is from table "${column.tableName}", but query is for table "${tableName}"`,
49
+ );
50
+ }
51
+
52
+ const fieldName = column.fieldName;
53
+ selectedFields.push(fieldName);
54
+
55
+ // Build mapping from field name to output key (only if renamed)
56
+ if (fieldName !== outputKey) {
57
+ fieldMapping[fieldName] = outputKey;
58
+ }
59
+ }
60
+
61
+ return {
62
+ selectedFields,
63
+ fieldMapping: Object.keys(fieldMapping).length > 0 ? fieldMapping : {},
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Legacy class name for backward compatibility.
69
+ * @deprecated Use processSelectFields function instead
70
+ */
71
+ export class SelectMixin {
72
+ static processSelect = processSelectFields;
73
+ }
74
+
@@ -0,0 +1,34 @@
1
+ import type { FMTable } from "../../orm/table";
2
+ import { transformFieldNamesArray } from "../../transform";
3
+
4
+ /**
5
+ * Formats select fields for use in OData query strings.
6
+ * - Transforms field names to FMFIDs if using entity IDs
7
+ * - Wraps "id" fields in double quotes (OData reserved)
8
+ * - URL-encodes special characters but preserves spaces
9
+ */
10
+ export function formatSelectFields(
11
+ select: string[] | readonly string[] | undefined,
12
+ table?: FMTable<any, any>,
13
+ useEntityIds?: boolean,
14
+ ): string {
15
+ if (!select || select.length === 0) return "";
16
+
17
+ const selectArray = Array.isArray(select) ? select : [select];
18
+
19
+ // Transform to field IDs if using entity IDs
20
+ const transformedFields =
21
+ table && useEntityIds
22
+ ? transformFieldNamesArray(selectArray.map(String), table)
23
+ : selectArray.map(String);
24
+
25
+ return transformedFields
26
+ .map((field) => {
27
+ if (field === "id") return `"id"`;
28
+ const encoded = encodeURIComponent(field);
29
+ return encoded.replace(/%20/g, " ");
30
+ })
31
+ .join(",");
32
+ }
33
+
34
+
@@ -0,0 +1,41 @@
1
+ import type { QueryOptions } from "odata-query";
2
+ import type { ExecutionContext } from "../../types";
3
+ import type { FMTable } from "../../orm/table";
4
+
5
+ /**
6
+ * Expand configuration used by both QueryBuilder and RecordBuilder
7
+ */
8
+ export type ExpandConfig = {
9
+ relation: string;
10
+ options?: Partial<QueryOptions<any>>;
11
+ targetTable?: FMTable<any, any>;
12
+ };
13
+
14
+ /**
15
+ * Type to represent expanded relations in return types
16
+ */
17
+ export type ExpandedRelations = Record<string, { schema: any; selected: any }>;
18
+
19
+ /**
20
+ * Navigation context shared between builders
21
+ */
22
+ export interface NavigationContext {
23
+ isNavigate?: boolean;
24
+ navigateRecordId?: string | number;
25
+ navigateRelation?: string;
26
+ navigateSourceTableName?: string;
27
+ navigateBaseRelation?: string;
28
+ navigateBasePath?: string;
29
+ }
30
+
31
+ /**
32
+ * Common builder configuration
33
+ */
34
+ export interface BuilderConfig<Occ extends FMTable<any, any> | undefined> {
35
+ occurrence?: Occ;
36
+ tableName: string;
37
+ databaseName: string;
38
+ context: ExecutionContext;
39
+ databaseUseEntityIds?: boolean;
40
+ }
41
+
@@ -0,0 +1,87 @@
1
+ import type { ExecutionContext } from "../../types";
2
+ import { getAcceptHeader } from "../../types";
3
+ import type { FMTable } from "../../orm/table";
4
+ import {
5
+ getTableName,
6
+ getTableId as getTableIdHelper,
7
+ isUsingEntityIds,
8
+ } from "../../orm/table";
9
+ import type { FFetchOptions } from "@fetchkit/ffetch";
10
+ import type { ExecuteOptions } from "../../types";
11
+
12
+ /**
13
+ * Resolves table identifier based on entity ID settings.
14
+ * Used by both QueryBuilder and RecordBuilder.
15
+ */
16
+ export function resolveTableId(
17
+ table: FMTable<any, any> | undefined,
18
+ fallbackTableName: string,
19
+ context: ExecutionContext,
20
+ useEntityIdsOverride?: boolean,
21
+ ): string {
22
+ if (!table) {
23
+ return fallbackTableName;
24
+ }
25
+
26
+ const contextDefault = context._getUseEntityIds?.() ?? false;
27
+ const shouldUseIds = useEntityIdsOverride ?? contextDefault;
28
+
29
+ if (shouldUseIds) {
30
+ if (!isUsingEntityIds(table)) {
31
+ throw new Error(
32
+ `useEntityIds is true but table "${getTableName(table)}" does not have entity IDs configured`,
33
+ );
34
+ }
35
+ return getTableIdHelper(table);
36
+ }
37
+
38
+ return getTableName(table);
39
+ }
40
+
41
+ /**
42
+ * Merges database-level useEntityIds with per-request options.
43
+ */
44
+ export function mergeEntityIdOptions<T extends Record<string, any>>(
45
+ options: T | undefined,
46
+ databaseDefault: boolean,
47
+ ): T & { useEntityIds?: boolean } {
48
+ return {
49
+ ...options,
50
+ useEntityIds: (options as any)?.useEntityIds ?? databaseDefault,
51
+ } as T & { useEntityIds?: boolean };
52
+ }
53
+
54
+ /**
55
+ * Type-safe helper for merging execute options with entity ID settings
56
+ */
57
+ export function mergeExecuteOptions(
58
+ options: (RequestInit & FFetchOptions & ExecuteOptions) | undefined,
59
+ databaseUseEntityIds: boolean,
60
+ ): RequestInit & FFetchOptions & { useEntityIds?: boolean } {
61
+ return mergeEntityIdOptions(options, databaseUseEntityIds);
62
+ }
63
+
64
+ /**
65
+ * Creates an OData Request object with proper headers.
66
+ * Used by both QueryBuilder and RecordBuilder to eliminate duplication.
67
+ *
68
+ * @param baseUrl - Base URL for the request
69
+ * @param config - Request configuration with method and url
70
+ * @param options - Optional execution options
71
+ * @returns Request object ready to use
72
+ */
73
+ export function createODataRequest(
74
+ baseUrl: string,
75
+ config: { method: string; url: string },
76
+ options?: { includeODataAnnotations?: boolean },
77
+ ): Request {
78
+ const fullUrl = `${baseUrl}${config.url}`;
79
+
80
+ return new Request(fullUrl, {
81
+ method: config.method,
82
+ headers: {
83
+ "Content-Type": "application/json",
84
+ Accept: getAcceptHeader(options?.includeODataAnnotations),
85
+ },
86
+ });
87
+ }