@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,244 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+ import type { QueryOptions } from "odata-query";
3
+ import type { FMTable } from "../../orm/table";
4
+ import type { Result } from "../../types";
5
+ import { RecordCountMismatchError } from "../../errors";
6
+ import { transformResponseFields } from "../../transform";
7
+ import { validateListResponse, validateSingleResponse } from "../../validation";
8
+ import type { ExpandValidationConfig } from "../../validation";
9
+ import type { ExpandConfig } from "./expand-builder";
10
+ import { FMTable as FMTableClass } from "../../orm/table";
11
+
12
+ /**
13
+ * Configuration for processing query responses
14
+ */
15
+ export interface ProcessQueryResponseConfig<T> {
16
+ occurrence?: FMTable<any, any>;
17
+ singleMode: "exact" | "maybe" | false;
18
+ queryOptions: Partial<QueryOptions<T>>;
19
+ expandConfigs: ExpandConfig[];
20
+ skipValidation?: boolean;
21
+ useEntityIds?: boolean;
22
+ // Mapping from field names to output keys (for renamed fields in select)
23
+ fieldMapping?: Record<string, string>;
24
+ }
25
+
26
+ /**
27
+ * Builds expand validation configs from internal expand configurations.
28
+ * These are used to validate expanded navigation properties.
29
+ */
30
+ function buildExpandValidationConfigs(
31
+ configs: ExpandConfig[],
32
+ ): ExpandValidationConfig[] {
33
+ return configs.map((config) => {
34
+ // Get target table/occurrence from config (stored during expand call)
35
+ const targetTable = config.targetTable;
36
+
37
+ // Extract schema from target table/occurrence
38
+ let targetSchema: Record<string, StandardSchemaV1> | undefined;
39
+ if (targetTable) {
40
+ const tableSchema = (targetTable as any)[FMTableClass.Symbol.Schema];
41
+ if (tableSchema) {
42
+ const zodSchema = tableSchema["~standard"]?.schema;
43
+ if (
44
+ zodSchema &&
45
+ typeof zodSchema === "object" &&
46
+ "shape" in zodSchema
47
+ ) {
48
+ targetSchema = zodSchema.shape as Record<string, StandardSchemaV1>;
49
+ }
50
+ }
51
+ }
52
+
53
+ // Extract selected fields from options
54
+ const selectedFields = config.options?.select
55
+ ? Array.isArray(config.options.select)
56
+ ? config.options.select.map((f) => String(f))
57
+ : [String(config.options.select)]
58
+ : undefined;
59
+
60
+ return {
61
+ relation: config.relation,
62
+ targetSchema: targetSchema,
63
+ targetTable: targetTable,
64
+ table: targetTable, // For transformation
65
+ selectedFields: selectedFields,
66
+ nestedExpands: undefined, // TODO: Handle nested expands if needed
67
+ };
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Extracts records from response data without validation.
73
+ * Handles both single and list responses.
74
+ */
75
+ function extractRecords(
76
+ data: any,
77
+ singleMode: "exact" | "maybe" | false,
78
+ ): Result<any> {
79
+ const resp = data as any;
80
+ if (singleMode !== false) {
81
+ const records = resp.value ?? [resp];
82
+ const count = Array.isArray(records) ? records.length : 1;
83
+
84
+ if (count > 1) {
85
+ return {
86
+ data: undefined,
87
+ error: new RecordCountMismatchError(
88
+ singleMode === "exact" ? "one" : "at-most-one",
89
+ count,
90
+ ),
91
+ };
92
+ }
93
+
94
+ if (count === 0) {
95
+ if (singleMode === "exact") {
96
+ return {
97
+ data: undefined,
98
+ error: new RecordCountMismatchError("one", 0),
99
+ };
100
+ }
101
+ return { data: null as any, error: undefined };
102
+ }
103
+
104
+ const record = Array.isArray(records) ? records[0] : records;
105
+ return { data: record as any, error: undefined };
106
+ } else {
107
+ // Handle list response structure
108
+ const records = resp.value ?? [];
109
+ return { data: records as any, error: undefined };
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Renames fields in response data according to the field mapping.
115
+ * Used when select() is called with renamed fields (e.g., { userEmail: users.email }).
116
+ */
117
+ function renameFieldsInResponse(
118
+ data: any,
119
+ fieldMapping: Record<string, string>,
120
+ ): any {
121
+ if (!data || typeof data !== "object") {
122
+ return data;
123
+ }
124
+
125
+ // Handle array responses
126
+ if (Array.isArray(data)) {
127
+ return data.map((item) => renameFieldsInResponse(item, fieldMapping));
128
+ }
129
+
130
+ // Handle OData list response structure
131
+ if ("value" in data && Array.isArray(data.value)) {
132
+ return {
133
+ ...data,
134
+ value: data.value.map((item: any) =>
135
+ renameFieldsInResponse(item, fieldMapping),
136
+ ),
137
+ };
138
+ }
139
+
140
+ // Handle single record
141
+ const renamed: Record<string, any> = {};
142
+ for (const [key, value] of Object.entries(data)) {
143
+ // Check if this field should be renamed
144
+ const outputKey = fieldMapping[key];
145
+ if (outputKey) {
146
+ renamed[outputKey] = value;
147
+ } else {
148
+ renamed[key] = value;
149
+ }
150
+ }
151
+ return renamed;
152
+ }
153
+
154
+ /**
155
+ * Processes a query response by transforming field IDs and validating the data.
156
+ * This function consolidates the response processing logic that was duplicated
157
+ * across multiple navigation branches in QueryBuilder.execute().
158
+ */
159
+ export async function processQueryResponse<T>(
160
+ response: any,
161
+ config: ProcessQueryResponseConfig<T>,
162
+ ): Promise<Result<any>> {
163
+ const { occurrence, singleMode, skipValidation, useEntityIds, fieldMapping } =
164
+ config;
165
+
166
+ // Transform response if needed
167
+ let data = response;
168
+ if (occurrence && useEntityIds) {
169
+ const expandValidationConfigs = buildExpandValidationConfigs(
170
+ config.expandConfigs,
171
+ );
172
+ data = transformResponseFields(
173
+ response,
174
+ occurrence,
175
+ expandValidationConfigs,
176
+ );
177
+ }
178
+
179
+ // Skip validation path
180
+ if (skipValidation) {
181
+ const result = extractRecords(data, singleMode);
182
+ // Rename fields AFTER extraction (but before returning)
183
+ if (result.data && fieldMapping && Object.keys(fieldMapping).length > 0) {
184
+ return {
185
+ ...result,
186
+ data: renameFieldsInResponse(result.data, fieldMapping),
187
+ };
188
+ }
189
+ return result;
190
+ }
191
+
192
+ // Validation path
193
+ // Get schema from occurrence if available
194
+ let schema: Record<string, StandardSchemaV1> | undefined;
195
+ if (occurrence) {
196
+ const tableSchema = (occurrence as any)[FMTableClass.Symbol.Schema];
197
+ if (tableSchema) {
198
+ const zodSchema = tableSchema["~standard"]?.schema;
199
+ if (zodSchema && typeof zodSchema === "object" && "shape" in zodSchema) {
200
+ schema = zodSchema.shape as Record<string, StandardSchemaV1>;
201
+ }
202
+ }
203
+ }
204
+
205
+ const selectedFields = config.queryOptions.select
206
+ ? ((Array.isArray(config.queryOptions.select)
207
+ ? config.queryOptions.select.map((f) => String(f))
208
+ : [String(config.queryOptions.select)]) as (keyof T)[])
209
+ : undefined;
210
+ const expandValidationConfigs = buildExpandValidationConfigs(
211
+ config.expandConfigs,
212
+ );
213
+
214
+ // Validate with original field names
215
+ const validationResult =
216
+ singleMode !== false
217
+ ? await validateSingleResponse(
218
+ data,
219
+ schema,
220
+ selectedFields as string[] | undefined,
221
+ expandValidationConfigs,
222
+ singleMode,
223
+ )
224
+ : await validateListResponse(
225
+ data,
226
+ schema,
227
+ selectedFields as string[] | undefined,
228
+ expandValidationConfigs,
229
+ );
230
+
231
+ if (!validationResult.valid) {
232
+ return { data: undefined, error: validationResult.error };
233
+ }
234
+
235
+ // Rename fields AFTER validation completes
236
+ if (fieldMapping && Object.keys(fieldMapping).length > 0) {
237
+ return {
238
+ data: renameFieldsInResponse(validationResult.data, fieldMapping),
239
+ error: undefined,
240
+ };
241
+ }
242
+
243
+ return { data: validationResult.data as any, error: undefined };
244
+ }
@@ -0,0 +1,102 @@
1
+ import type { Column } from "../../orm/column";
2
+
3
+ /**
4
+ * Type-safe orderBy type that provides better DX than odata-query's default.
5
+ *
6
+ * Supported forms:
7
+ * - `keyof T` - single field name (defaults to ascending)
8
+ * - `[keyof T, 'asc' | 'desc']` - single field with explicit direction
9
+ * - `Array<[keyof T, 'asc' | 'desc']>` - multiple fields with directions
10
+ *
11
+ * This type intentionally EXCLUDES `Array<keyof T>` to avoid ambiguity
12
+ * between [field1, field2] and [field, direction].
13
+ */
14
+ export type TypeSafeOrderBy<T> =
15
+ | (keyof T & string) // Single field name
16
+ | [keyof T & string, "asc" | "desc"] // Single field with direction
17
+ | Array<[keyof T & string, "asc" | "desc"]>; // Multiple fields with directions
18
+
19
+ // Internal type for expand configuration
20
+ export type ExpandConfig = {
21
+ relation: string;
22
+ options?: Partial<import("odata-query").QueryOptions<any>>;
23
+ targetTable?: import("../../orm/table").FMTable<any, any>;
24
+ };
25
+
26
+ // Type to represent expanded relations
27
+ export type ExpandedRelations = Record<string, { schema: any; selected: any }>;
28
+
29
+ /**
30
+ * Extract the value type from a Column.
31
+ * This uses the phantom type stored in Column to get the actual value type.
32
+ */
33
+ type ExtractColumnType<C> = C extends Column<infer T, any> ? T : never;
34
+
35
+ /**
36
+ * Map a select object to its return type.
37
+ * For each key in the select object, extract the type from the corresponding Column.
38
+ */
39
+ type MapSelectToReturnType<
40
+ TSelect extends Record<string, Column<any, any>>,
41
+ TSchema extends Record<string, any>,
42
+ > = {
43
+ [K in keyof TSelect]: ExtractColumnType<TSelect[K]>;
44
+ };
45
+
46
+ export type QueryReturnType<
47
+ T extends Record<string, any>,
48
+ Selected extends keyof T | Record<string, Column<any, any>>,
49
+ SingleMode extends "exact" | "maybe" | false,
50
+ IsCount extends boolean,
51
+ Expands extends ExpandedRelations,
52
+ > = IsCount extends true
53
+ ? number
54
+ : // Use tuple wrapping [Selected] extends [...] to prevent distribution over unions
55
+ [Selected] extends [Record<string, Column<any, any>>]
56
+ ? SingleMode extends "exact"
57
+ ? MapSelectToReturnType<Selected, T> & {
58
+ [K in keyof Expands]: Pick<
59
+ Expands[K]["schema"],
60
+ Expands[K]["selected"]
61
+ >[];
62
+ }
63
+ : SingleMode extends "maybe"
64
+ ?
65
+ | (MapSelectToReturnType<Selected, T> & {
66
+ [K in keyof Expands]: Pick<
67
+ Expands[K]["schema"],
68
+ Expands[K]["selected"]
69
+ >[];
70
+ })
71
+ | null
72
+ : (MapSelectToReturnType<Selected, T> & {
73
+ [K in keyof Expands]: Pick<
74
+ Expands[K]["schema"],
75
+ Expands[K]["selected"]
76
+ >[];
77
+ })[]
78
+ : // Use tuple wrapping to prevent distribution over union of keys
79
+ [Selected] extends [keyof T]
80
+ ? SingleMode extends "exact"
81
+ ? Pick<T, Selected> & {
82
+ [K in keyof Expands]: Pick<
83
+ Expands[K]["schema"],
84
+ Expands[K]["selected"]
85
+ >[];
86
+ }
87
+ : SingleMode extends "maybe"
88
+ ?
89
+ | (Pick<T, Selected> & {
90
+ [K in keyof Expands]: Pick<
91
+ Expands[K]["schema"],
92
+ Expands[K]["selected"]
93
+ >[];
94
+ })
95
+ | null
96
+ : (Pick<T, Selected> & {
97
+ [K in keyof Expands]: Pick<
98
+ Expands[K]["schema"],
99
+ Expands[K]["selected"]
100
+ >[];
101
+ })[]
102
+ : never;
@@ -0,0 +1,179 @@
1
+ import type { FMTable } from "../../orm/table";
2
+ import { getTableName } from "../../orm/table";
3
+ import { resolveTableId } from "../builders/table-utils";
4
+ import type { ExecutionContext } from "../../types";
5
+
6
+ /**
7
+ * Configuration for navigation from RecordBuilder or EntitySet
8
+ */
9
+ export interface NavigationConfig {
10
+ recordId?: string | number;
11
+ relation: string;
12
+ sourceTableName: string;
13
+ baseRelation?: string; // For chained navigations from navigated EntitySets
14
+ basePath?: string; // Full base path for chained entity set navigations
15
+ }
16
+
17
+ /**
18
+ * Builds OData query URLs for different navigation modes.
19
+ * Handles:
20
+ * - Record navigation: /database/sourceTable('recordId')/relation
21
+ * - Entity set navigation: /database/sourceTable/relation
22
+ * - Count endpoint: /database/tableId/$count
23
+ * - Standard queries: /database/tableId
24
+ */
25
+ export class QueryUrlBuilder {
26
+ constructor(
27
+ private databaseName: string,
28
+ private occurrence: FMTable<any, any>,
29
+ private context: ExecutionContext,
30
+ ) {}
31
+
32
+ /**
33
+ * Builds the full URL for a query request.
34
+ *
35
+ * @param queryString - The OData query string (e.g., "?$filter=...&$select=...")
36
+ * @param options - Options including whether this is a count query, useEntityIds override, and navigation config
37
+ */
38
+ build(
39
+ queryString: string,
40
+ options: {
41
+ isCount?: boolean;
42
+ useEntityIds?: boolean;
43
+ navigation?: NavigationConfig;
44
+ },
45
+ ): string {
46
+ const tableId = resolveTableId(
47
+ this.occurrence,
48
+ getTableName(this.occurrence),
49
+ this.context,
50
+ options.useEntityIds,
51
+ );
52
+
53
+ const navigation = options.navigation;
54
+ if (navigation?.recordId && navigation?.relation) {
55
+ return this.buildRecordNavigation(queryString, tableId, navigation);
56
+ }
57
+ if (navigation?.relation) {
58
+ return this.buildEntitySetNavigation(queryString, tableId, navigation);
59
+ }
60
+ if (options.isCount) {
61
+ return `/${this.databaseName}/${tableId}/$count${queryString}`;
62
+ }
63
+ return `/${this.databaseName}/${tableId}${queryString}`;
64
+ }
65
+
66
+ /**
67
+ * Builds URL for record navigation: /database/sourceTable('recordId')/relation
68
+ * or /database/sourceTable/baseRelation('recordId')/relation for chained navigations
69
+ */
70
+ private buildRecordNavigation(
71
+ queryString: string,
72
+ tableId: string,
73
+ navigation: NavigationConfig,
74
+ ): string {
75
+ const { sourceTableName, baseRelation, recordId, relation } = navigation;
76
+ const base = baseRelation
77
+ ? `${sourceTableName}/${baseRelation}('${recordId}')`
78
+ : `${sourceTableName}('${recordId}')`;
79
+ return `/${this.databaseName}/${base}/${relation}${queryString}`;
80
+ }
81
+
82
+ /**
83
+ * Builds URL for entity set navigation: /database/sourceTable/relation
84
+ * or /database/basePath/relation for chained navigations
85
+ */
86
+ private buildEntitySetNavigation(
87
+ queryString: string,
88
+ tableId: string,
89
+ navigation: NavigationConfig,
90
+ ): string {
91
+ const { sourceTableName, basePath, relation } = navigation;
92
+ const base = basePath || sourceTableName;
93
+ return `/${this.databaseName}/${base}/${relation}${queryString}`;
94
+ }
95
+
96
+ /**
97
+ * Builds a query string path (without database prefix) for getQueryString().
98
+ * Used when the full URL is not needed.
99
+ */
100
+ buildPath(
101
+ queryString: string,
102
+ options?: { useEntityIds?: boolean; navigation?: NavigationConfig },
103
+ ): string {
104
+ const useEntityIds = options?.useEntityIds;
105
+ const navigation = options?.navigation;
106
+ const tableId = resolveTableId(
107
+ this.occurrence,
108
+ getTableName(this.occurrence),
109
+ this.context,
110
+ useEntityIds,
111
+ );
112
+
113
+ if (navigation?.recordId && navigation?.relation) {
114
+ const { sourceTableName, baseRelation, recordId, relation } = navigation;
115
+ const base = baseRelation
116
+ ? `${sourceTableName}/${baseRelation}('${recordId}')`
117
+ : `${sourceTableName}('${recordId}')`;
118
+ return queryString
119
+ ? `/${base}/${relation}${queryString}`
120
+ : `/${base}/${relation}`;
121
+ }
122
+ if (navigation?.relation) {
123
+ const { sourceTableName, basePath, relation } = navigation;
124
+ const base = basePath || sourceTableName;
125
+ return queryString
126
+ ? `/${base}/${relation}${queryString}`
127
+ : `/${base}/${relation}`;
128
+ }
129
+ return queryString ? `/${tableId}${queryString}` : `/${tableId}`;
130
+ }
131
+
132
+ /**
133
+ * Build URL for record operations (single record by ID).
134
+ * Used by RecordBuilder to build URLs like /database/table('id').
135
+ *
136
+ * @param recordId - The record ID
137
+ * @param queryString - The OData query string (e.g., "?$select=...")
138
+ * @param options - Options including operation type and useEntityIds override
139
+ */
140
+ buildRecordUrl(
141
+ recordId: string | number,
142
+ queryString: string,
143
+ options?: {
144
+ operation?: "getSingleField";
145
+ operationParam?: string;
146
+ useEntityIds?: boolean;
147
+ isNavigateFromEntitySet?: boolean;
148
+ navigateSourceTableName?: string;
149
+ navigateRelation?: string;
150
+ },
151
+ ): string {
152
+ const tableId = resolveTableId(
153
+ this.occurrence,
154
+ getTableName(this.occurrence),
155
+ this.context,
156
+ options?.useEntityIds,
157
+ );
158
+
159
+ // Build the base URL depending on whether this came from a navigated EntitySet
160
+ let url: string;
161
+ if (
162
+ options?.isNavigateFromEntitySet &&
163
+ options.navigateSourceTableName &&
164
+ options.navigateRelation
165
+ ) {
166
+ // From navigated EntitySet: /sourceTable/relation('recordId')
167
+ url = `/${this.databaseName}/${options.navigateSourceTableName}/${options.navigateRelation}('${recordId}')`;
168
+ } else {
169
+ // Normal record: /tableName('recordId') - use FMTID if configured
170
+ url = `/${this.databaseName}/${tableId}('${recordId}')`;
171
+ }
172
+
173
+ if (options?.operation === "getSingleField" && options.operationParam) {
174
+ url += `/${options.operationParam}`;
175
+ }
176
+
177
+ return url + queryString;
178
+ }
179
+ }