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