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