@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.
- package/README.md +489 -334
- package/dist/esm/client/batch-builder.d.ts +7 -4
- package/dist/esm/client/batch-builder.js +84 -25
- package/dist/esm/client/batch-builder.js.map +1 -1
- package/dist/esm/client/builders/default-select.d.ts +7 -0
- package/dist/esm/client/builders/default-select.js +42 -0
- package/dist/esm/client/builders/default-select.js.map +1 -0
- package/dist/esm/client/builders/expand-builder.d.ts +43 -0
- package/dist/esm/client/builders/expand-builder.js +173 -0
- package/dist/esm/client/builders/expand-builder.js.map +1 -0
- package/dist/esm/client/builders/index.d.ts +8 -0
- package/dist/esm/client/builders/query-string-builder.d.ts +15 -0
- package/dist/esm/client/builders/query-string-builder.js +25 -0
- package/dist/esm/client/builders/query-string-builder.js.map +1 -0
- package/dist/esm/client/builders/response-processor.d.ts +39 -0
- package/dist/esm/client/builders/response-processor.js +170 -0
- package/dist/esm/client/builders/response-processor.js.map +1 -0
- package/dist/esm/client/builders/select-mixin.d.ts +31 -0
- package/dist/esm/client/builders/select-mixin.js +30 -0
- package/dist/esm/client/builders/select-mixin.js.map +1 -0
- package/dist/esm/client/builders/select-utils.d.ts +8 -0
- package/dist/esm/client/builders/select-utils.js +15 -0
- package/dist/esm/client/builders/select-utils.js.map +1 -0
- package/dist/esm/client/builders/shared-types.d.ts +39 -0
- package/dist/esm/client/builders/table-utils.d.ts +35 -0
- package/dist/esm/client/builders/table-utils.js +45 -0
- package/dist/esm/client/builders/table-utils.js.map +1 -0
- package/dist/esm/client/database.d.ts +3 -22
- package/dist/esm/client/database.js +14 -76
- package/dist/esm/client/database.js.map +1 -1
- package/dist/esm/client/delete-builder.d.ts +11 -15
- package/dist/esm/client/delete-builder.js +26 -26
- package/dist/esm/client/delete-builder.js.map +1 -1
- package/dist/esm/client/entity-set.d.ts +32 -32
- package/dist/esm/client/entity-set.js +92 -69
- package/dist/esm/client/entity-set.js.map +1 -1
- package/dist/esm/client/error-parser.d.ts +12 -0
- package/dist/esm/client/error-parser.js +30 -0
- package/dist/esm/client/error-parser.js.map +1 -0
- package/dist/esm/client/filemaker-odata.d.ts +2 -4
- package/dist/esm/client/filemaker-odata.js +1 -5
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/dist/esm/client/insert-builder.d.ts +7 -9
- package/dist/esm/client/insert-builder.js +70 -24
- package/dist/esm/client/insert-builder.js.map +1 -1
- package/dist/esm/client/query/expand-builder.d.ts +35 -0
- package/dist/esm/client/query/index.d.ts +3 -0
- package/dist/esm/client/query/query-builder.d.ts +134 -0
- package/dist/esm/client/query/query-builder.js +505 -0
- package/dist/esm/client/query/query-builder.js.map +1 -0
- package/dist/esm/client/query/response-processor.d.ts +22 -0
- package/dist/esm/client/query/types.d.ts +52 -0
- package/dist/esm/client/query/url-builder.d.ts +71 -0
- package/dist/esm/client/query/url-builder.js +107 -0
- package/dist/esm/client/query/url-builder.js.map +1 -0
- package/dist/esm/client/query-builder.d.ts +1 -111
- package/dist/esm/client/record-builder.d.ts +56 -63
- package/dist/esm/client/record-builder.js +158 -297
- package/dist/esm/client/record-builder.js.map +1 -1
- package/dist/esm/client/response-processor.d.ts +3 -3
- package/dist/esm/client/update-builder.d.ts +16 -21
- package/dist/esm/client/update-builder.js +56 -30
- package/dist/esm/client/update-builder.js.map +1 -1
- package/dist/esm/errors.d.ts +8 -1
- package/dist/esm/errors.js +17 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +3 -7
- package/dist/esm/index.js +37 -8
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/orm/column.d.ts +45 -0
- package/dist/esm/orm/column.js +59 -0
- package/dist/esm/orm/column.js.map +1 -0
- package/dist/esm/orm/field-builders.d.ts +154 -0
- package/dist/esm/orm/field-builders.js +152 -0
- package/dist/esm/orm/field-builders.js.map +1 -0
- package/dist/esm/orm/index.d.ts +4 -0
- package/dist/esm/orm/operators.d.ts +175 -0
- package/dist/esm/orm/operators.js +221 -0
- package/dist/esm/orm/operators.js.map +1 -0
- package/dist/esm/orm/table.d.ts +341 -0
- package/dist/esm/orm/table.js +211 -0
- package/dist/esm/orm/table.js.map +1 -0
- package/dist/esm/transform.d.ts +20 -21
- package/dist/esm/transform.js +34 -34
- package/dist/esm/transform.js.map +1 -1
- package/dist/esm/types.d.ts +16 -13
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/validation.d.ts +14 -4
- package/dist/esm/validation.js +45 -1
- package/dist/esm/validation.js.map +1 -1
- package/package.json +20 -17
- package/src/client/batch-builder.ts +100 -32
- package/src/client/builders/default-select.ts +69 -0
- package/src/client/builders/expand-builder.ts +236 -0
- package/src/client/builders/index.ts +11 -0
- package/src/client/builders/query-string-builder.ts +41 -0
- package/src/client/builders/response-processor.ts +273 -0
- package/src/client/builders/select-mixin.ts +74 -0
- package/src/client/builders/select-utils.ts +34 -0
- package/src/client/builders/shared-types.ts +41 -0
- package/src/client/builders/table-utils.ts +87 -0
- package/src/client/database.ts +19 -160
- package/src/client/delete-builder.ts +46 -51
- package/src/client/entity-set.ts +227 -302
- package/src/client/error-parser.ts +59 -0
- package/src/client/filemaker-odata.ts +3 -14
- package/src/client/insert-builder.ts +124 -43
- package/src/client/query/expand-builder.ts +164 -0
- package/src/client/query/index.ts +13 -0
- package/src/client/query/query-builder.ts +816 -0
- package/src/client/query/response-processor.ts +244 -0
- package/src/client/query/types.ts +102 -0
- package/src/client/query/url-builder.ts +179 -0
- package/src/client/query-builder.ts +8 -1454
- package/src/client/record-builder.ts +325 -585
- package/src/client/response-processor.ts +4 -5
- package/src/client/update-builder.ts +102 -73
- package/src/errors.ts +22 -1
- package/src/index.ts +55 -5
- package/src/orm/column.ts +78 -0
- package/src/orm/field-builders.ts +296 -0
- package/src/orm/index.ts +60 -0
- package/src/orm/operators.ts +428 -0
- package/src/orm/table.ts +759 -0
- package/src/transform.ts +62 -48
- package/src/types.ts +20 -63
- package/src/validation.ts +76 -4
- package/LICENSE.md +0 -21
- package/dist/esm/client/base-table.d.ts +0 -128
- package/dist/esm/client/base-table.js +0 -57
- package/dist/esm/client/base-table.js.map +0 -1
- package/dist/esm/client/build-occurrences.d.ts +0 -74
- package/dist/esm/client/build-occurrences.js +0 -31
- package/dist/esm/client/build-occurrences.js.map +0 -1
- package/dist/esm/client/query-builder.js +0 -900
- package/dist/esm/client/query-builder.js.map +0 -1
- package/dist/esm/client/table-occurrence.d.ts +0 -86
- package/dist/esm/client/table-occurrence.js +0 -58
- package/dist/esm/client/table-occurrence.js.map +0 -1
- package/src/client/base-table.ts +0 -178
- package/src/client/build-occurrences.ts +0 -155
- package/src/client/query-builder.ts.bak +0 -1457
- 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
|
+
}
|