@proofkit/fmodata 0.1.0-alpha.8 → 0.1.0-beta.23
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/LICENSE.md +21 -0
- package/README.md +651 -449
- package/dist/esm/client/batch-builder.d.ts +10 -9
- package/dist/esm/client/batch-builder.js +119 -56
- package/dist/esm/client/batch-builder.js.map +1 -1
- package/dist/esm/client/batch-request.js +16 -21
- package/dist/esm/client/batch-request.js.map +1 -1
- package/dist/esm/client/builders/default-select.d.ts +10 -0
- package/dist/esm/client/builders/default-select.js +41 -0
- package/dist/esm/client/builders/default-select.js.map +1 -0
- package/dist/esm/client/builders/expand-builder.d.ts +45 -0
- package/dist/esm/client/builders/expand-builder.js +185 -0
- package/dist/esm/client/builders/expand-builder.js.map +1 -0
- package/dist/esm/client/builders/index.d.ts +9 -0
- package/dist/esm/client/builders/query-string-builder.d.ts +18 -0
- package/dist/esm/client/builders/query-string-builder.js +21 -0
- package/dist/esm/client/builders/query-string-builder.js.map +1 -0
- package/dist/esm/client/builders/response-processor.d.ts +43 -0
- package/dist/esm/client/builders/response-processor.js +175 -0
- package/dist/esm/client/builders/response-processor.js.map +1 -0
- package/dist/esm/client/builders/select-mixin.d.ts +25 -0
- package/dist/esm/client/builders/select-mixin.js +28 -0
- package/dist/esm/client/builders/select-mixin.js.map +1 -0
- package/dist/esm/client/builders/select-utils.d.ts +18 -0
- package/dist/esm/client/builders/select-utils.js +30 -0
- package/dist/esm/client/builders/select-utils.js.map +1 -0
- package/dist/esm/client/builders/shared-types.d.ts +40 -0
- package/dist/esm/client/builders/table-utils.d.ts +35 -0
- package/dist/esm/client/builders/table-utils.js +44 -0
- package/dist/esm/client/builders/table-utils.js.map +1 -0
- package/dist/esm/client/database.d.ts +34 -22
- package/dist/esm/client/database.js +48 -84
- package/dist/esm/client/database.js.map +1 -1
- package/dist/esm/client/delete-builder.d.ts +25 -30
- package/dist/esm/client/delete-builder.js +45 -30
- package/dist/esm/client/delete-builder.js.map +1 -1
- package/dist/esm/client/entity-set.d.ts +35 -43
- package/dist/esm/client/entity-set.js +110 -52
- 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 +25 -0
- package/dist/esm/client/error-parser.js.map +1 -0
- package/dist/esm/client/filemaker-odata.d.ts +26 -7
- package/dist/esm/client/filemaker-odata.js +65 -42
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/dist/esm/client/insert-builder.d.ts +19 -24
- package/dist/esm/client/insert-builder.js +94 -58
- 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 +4 -0
- package/dist/esm/client/query/query-builder.d.ts +132 -0
- package/dist/esm/client/query/query-builder.js +456 -0
- package/dist/esm/client/query/query-builder.js.map +1 -0
- package/dist/esm/client/query/response-processor.d.ts +25 -0
- package/dist/esm/client/query/types.d.ts +77 -0
- package/dist/esm/client/query/url-builder.d.ts +71 -0
- package/dist/esm/client/query/url-builder.js +100 -0
- package/dist/esm/client/query/url-builder.js.map +1 -0
- package/dist/esm/client/query-builder.d.ts +2 -115
- package/dist/esm/client/record-builder.d.ts +108 -36
- package/dist/esm/client/record-builder.js +284 -119
- package/dist/esm/client/record-builder.js.map +1 -1
- package/dist/esm/client/response-processor.d.ts +4 -9
- package/dist/esm/client/sanitize-json.d.ts +35 -0
- package/dist/esm/client/sanitize-json.js +27 -0
- package/dist/esm/client/sanitize-json.js.map +1 -0
- package/dist/esm/client/schema-manager.d.ts +5 -5
- package/dist/esm/client/schema-manager.js +45 -31
- package/dist/esm/client/schema-manager.js.map +1 -1
- package/dist/esm/client/update-builder.d.ts +34 -40
- package/dist/esm/client/update-builder.js +99 -58
- package/dist/esm/client/update-builder.js.map +1 -1
- package/dist/esm/client/webhook-builder.d.ts +126 -0
- package/dist/esm/client/webhook-builder.js +189 -0
- package/dist/esm/client/webhook-builder.js.map +1 -0
- package/dist/esm/errors.d.ts +19 -2
- package/dist/esm/errors.js +39 -4
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +10 -8
- package/dist/esm/index.js +40 -10
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/logger.d.ts +47 -0
- package/dist/esm/logger.js +69 -0
- package/dist/esm/logger.js.map +1 -0
- package/dist/esm/logger.test.d.ts +1 -0
- package/dist/esm/orm/column.d.ts +62 -0
- package/dist/esm/orm/column.js +63 -0
- package/dist/esm/orm/column.js.map +1 -0
- package/dist/esm/orm/field-builders.d.ts +164 -0
- package/dist/esm/orm/field-builders.js +158 -0
- package/dist/esm/orm/field-builders.js.map +1 -0
- package/dist/esm/orm/index.d.ts +5 -0
- package/dist/esm/orm/operators.d.ts +173 -0
- package/dist/esm/orm/operators.js +260 -0
- package/dist/esm/orm/operators.js.map +1 -0
- package/dist/esm/orm/table.d.ts +355 -0
- package/dist/esm/orm/table.js +202 -0
- package/dist/esm/orm/table.js.map +1 -0
- package/dist/esm/transform.d.ts +20 -21
- package/dist/esm/transform.js +44 -45
- package/dist/esm/transform.js.map +1 -1
- package/dist/esm/types.d.ts +96 -30
- package/dist/esm/types.js +7 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/validation.d.ts +22 -12
- package/dist/esm/validation.js +132 -85
- package/dist/esm/validation.js.map +1 -1
- package/package.json +28 -20
- package/src/client/batch-builder.ts +153 -89
- package/src/client/batch-request.ts +25 -41
- package/src/client/builders/default-select.ts +75 -0
- package/src/client/builders/expand-builder.ts +246 -0
- package/src/client/builders/index.ts +11 -0
- package/src/client/builders/query-string-builder.ts +46 -0
- package/src/client/builders/response-processor.ts +279 -0
- package/src/client/builders/select-mixin.ts +65 -0
- package/src/client/builders/select-utils.ts +59 -0
- package/src/client/builders/shared-types.ts +45 -0
- package/src/client/builders/table-utils.ts +83 -0
- package/src/client/database.ts +89 -183
- package/src/client/delete-builder.ts +74 -84
- package/src/client/entity-set.ts +266 -293
- package/src/client/error-parser.ts +41 -0
- package/src/client/filemaker-odata.ts +98 -66
- package/src/client/insert-builder.ts +157 -118
- package/src/client/query/expand-builder.ts +160 -0
- package/src/client/query/index.ts +14 -0
- package/src/client/query/query-builder.ts +729 -0
- package/src/client/query/response-processor.ts +226 -0
- package/src/client/query/types.ts +126 -0
- package/src/client/query/url-builder.ts +151 -0
- package/src/client/query-builder.ts +10 -1455
- package/src/client/record-builder.ts +575 -240
- package/src/client/response-processor.ts +15 -42
- package/src/client/sanitize-json.ts +64 -0
- package/src/client/schema-manager.ts +61 -76
- package/src/client/update-builder.ts +161 -143
- package/src/client/webhook-builder.ts +265 -0
- package/src/errors.ts +49 -16
- package/src/index.ts +99 -54
- package/src/logger.test.ts +34 -0
- package/src/logger.ts +116 -0
- package/src/orm/column.ts +106 -0
- package/src/orm/field-builders.ts +250 -0
- package/src/orm/index.ts +61 -0
- package/src/orm/operators.ts +473 -0
- package/src/orm/table.ts +741 -0
- package/src/transform.ts +90 -70
- package/src/types.ts +154 -113
- package/src/validation.ts +200 -115
- package/dist/esm/client/base-table.d.ts +0 -125
- package/dist/esm/client/base-table.js +0 -57
- package/dist/esm/client/base-table.js.map +0 -1
- package/dist/esm/client/query-builder.js +0 -896
- package/dist/esm/client/query-builder.js.map +0 -1
- package/dist/esm/client/table-occurrence.d.ts +0 -72
- package/dist/esm/client/table-occurrence.js +0 -74
- package/dist/esm/client/table-occurrence.js.map +0 -1
- package/dist/esm/filter-types.d.ts +0 -76
- package/src/client/base-table.ts +0 -166
- package/src/client/query-builder.ts.bak +0 -1457
- package/src/client/table-occurrence.ts +0 -175
- package/src/filter-types.ts +0 -97
|
@@ -1,104 +1,163 @@
|
|
|
1
|
+
/** biome-ignore-all lint/complexity/noBannedTypes: Empty object type represents no expands by default */
|
|
2
|
+
import type { FFetchOptions } from "@fetchkit/ffetch";
|
|
3
|
+
import { createLogger, type InternalLogger } from "../logger";
|
|
4
|
+
import type { Column } from "../orm/column";
|
|
5
|
+
import type { ExtractTableName, FMTable, InferSchemaOutputFromFMTable, ValidExpandTarget } from "../orm/table";
|
|
6
|
+
import { getNavigationPaths, getTableName } from "../orm/table";
|
|
1
7
|
import type {
|
|
2
|
-
|
|
8
|
+
ConditionallyWithODataAnnotations,
|
|
9
|
+
ConditionallyWithSpecialColumns,
|
|
3
10
|
ExecutableBuilder,
|
|
4
|
-
|
|
5
|
-
ODataRecordMetadata,
|
|
6
|
-
ODataFieldResponse,
|
|
7
|
-
InferSchemaType,
|
|
11
|
+
ExecuteMethodOptions,
|
|
8
12
|
ExecuteOptions,
|
|
13
|
+
ExecutionContext,
|
|
14
|
+
NormalizeIncludeSpecialColumns,
|
|
15
|
+
ODataFieldResponse,
|
|
16
|
+
Result,
|
|
9
17
|
} from "../types";
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
import {
|
|
19
|
+
buildSelectExpandQueryString,
|
|
20
|
+
createODataRequest,
|
|
21
|
+
ExpandBuilder,
|
|
22
|
+
type ExpandConfig,
|
|
23
|
+
type ExpandedRelations,
|
|
24
|
+
getSchemaFromTable,
|
|
25
|
+
mergeExecuteOptions,
|
|
26
|
+
processODataResponse,
|
|
27
|
+
processSelectWithRenames,
|
|
28
|
+
resolveTableId,
|
|
29
|
+
} from "./builders/index";
|
|
30
|
+
import { parseErrorResponse } from "./error-parser";
|
|
31
|
+
import type { ResolveExpandedRelations, SystemColumnsFromOption, SystemColumnsOption } from "./query/types";
|
|
13
32
|
import { QueryBuilder } from "./query-builder";
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
type
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
33
|
+
import { safeJsonParse } from "./sanitize-json";
|
|
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
|
+
// biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
|
|
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
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
|
|
48
|
+
TSelect extends Record<string, Column<any, any, any, any>>,
|
|
49
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any schema shape
|
|
50
|
+
_TSchema extends Record<string, any>,
|
|
51
|
+
> = {
|
|
52
|
+
[K in keyof TSelect]: ExtractColumnType<TSelect[K]>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Return type for RecordBuilder execute
|
|
56
|
+
export type RecordReturnType<
|
|
57
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any schema shape
|
|
58
|
+
Schema extends Record<string, any>,
|
|
59
|
+
IsSingleField extends boolean,
|
|
60
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
|
|
61
|
+
FieldColumn extends Column<any, any, any, any> | undefined,
|
|
62
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration, accepts any FMTable
|
|
63
|
+
Selected extends keyof Schema | Record<string, Column<any, any, ExtractTableName<FMTable<any, any>>>>,
|
|
64
|
+
Expands extends ExpandedRelations,
|
|
65
|
+
SystemCols extends SystemColumnsOption | undefined = undefined,
|
|
66
|
+
> = IsSingleField extends true
|
|
67
|
+
? // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
|
|
68
|
+
FieldColumn extends Column<infer TOutput, any, any, any>
|
|
69
|
+
? TOutput
|
|
70
|
+
: never
|
|
71
|
+
: // Use tuple wrapping [Selected] extends [...] to prevent distribution over unions
|
|
72
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
|
|
73
|
+
[Selected] extends [Record<string, Column<any, any, any, any>>]
|
|
74
|
+
? MapSelectToReturnType<Selected, Schema> & ResolveExpandedRelations<Expands> & SystemColumnsFromOption<SystemCols>
|
|
75
|
+
: // Use tuple wrapping to prevent distribution over union of keys
|
|
76
|
+
[Selected] extends [keyof Schema]
|
|
77
|
+
? Pick<Schema, Selected> & ResolveExpandedRelations<Expands> & SystemColumnsFromOption<SystemCols>
|
|
78
|
+
: never;
|
|
50
79
|
|
|
51
80
|
export class RecordBuilder<
|
|
52
|
-
|
|
81
|
+
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration, default allows untyped tables
|
|
82
|
+
Occ extends FMTable<any, any> = FMTable<any, any>,
|
|
53
83
|
IsSingleField extends boolean = false,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
|
84
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
|
|
85
|
+
FieldColumn extends Column<any, any, any, any> | undefined = undefined,
|
|
86
|
+
Selected extends
|
|
87
|
+
| keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>
|
|
88
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
|
|
89
|
+
| Record<string, Column<any, any, ExtractTableName<NonNullable<Occ>>>> = keyof InferSchemaOutputFromFMTable<
|
|
90
|
+
NonNullable<Occ>
|
|
91
|
+
>,
|
|
92
|
+
Expands extends ExpandedRelations = {},
|
|
93
|
+
DatabaseIncludeSpecialColumns extends boolean = false,
|
|
94
|
+
SystemCols extends SystemColumnsOption | undefined = undefined,
|
|
58
95
|
> implements
|
|
59
96
|
ExecutableBuilder<
|
|
60
|
-
|
|
97
|
+
RecordReturnType<
|
|
98
|
+
InferSchemaOutputFromFMTable<NonNullable<Occ>>,
|
|
99
|
+
IsSingleField,
|
|
100
|
+
FieldColumn,
|
|
101
|
+
Selected,
|
|
102
|
+
Expands,
|
|
103
|
+
SystemCols
|
|
104
|
+
>
|
|
61
105
|
>
|
|
62
106
|
{
|
|
63
|
-
private
|
|
64
|
-
private
|
|
65
|
-
private
|
|
66
|
-
private
|
|
67
|
-
private
|
|
68
|
-
private
|
|
69
|
-
|
|
70
|
-
private
|
|
71
|
-
private
|
|
72
|
-
private
|
|
73
|
-
|
|
74
|
-
|
|
107
|
+
private readonly table: Occ;
|
|
108
|
+
private readonly databaseName: string;
|
|
109
|
+
private readonly context: ExecutionContext;
|
|
110
|
+
private readonly recordId: string | number;
|
|
111
|
+
private readonly operation?: "getSingleField" | "navigate";
|
|
112
|
+
private readonly operationParam?: string;
|
|
113
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
|
|
114
|
+
private readonly operationColumn?: Column<any, any, any, any>;
|
|
115
|
+
private readonly isNavigateFromEntitySet?: boolean;
|
|
116
|
+
private readonly navigateRelation?: string;
|
|
117
|
+
private readonly navigateSourceTableName?: string;
|
|
118
|
+
|
|
119
|
+
private readonly databaseUseEntityIds: boolean;
|
|
120
|
+
private readonly databaseIncludeSpecialColumns: boolean;
|
|
121
|
+
|
|
122
|
+
// Properties for select/expand support
|
|
123
|
+
private readonly selectedFields?: string[];
|
|
124
|
+
private readonly expandConfigs: ExpandConfig[] = [];
|
|
125
|
+
// Mapping from field names to output keys (for renamed fields in select)
|
|
126
|
+
private readonly fieldMapping?: Record<string, string>;
|
|
127
|
+
// System columns requested via select() second argument
|
|
128
|
+
private readonly systemColumns?: SystemColumnsOption;
|
|
129
|
+
|
|
130
|
+
private readonly logger: InternalLogger;
|
|
75
131
|
|
|
76
132
|
constructor(config: {
|
|
77
|
-
occurrence
|
|
78
|
-
tableName: string;
|
|
133
|
+
occurrence: Occ;
|
|
79
134
|
databaseName: string;
|
|
80
135
|
context: ExecutionContext;
|
|
81
136
|
recordId: string | number;
|
|
82
137
|
databaseUseEntityIds?: boolean;
|
|
138
|
+
databaseIncludeSpecialColumns?: boolean;
|
|
83
139
|
}) {
|
|
84
|
-
this.
|
|
85
|
-
this.tableName = config.tableName;
|
|
140
|
+
this.table = config.occurrence;
|
|
86
141
|
this.databaseName = config.databaseName;
|
|
87
142
|
this.context = config.context;
|
|
88
143
|
this.recordId = config.recordId;
|
|
89
144
|
this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;
|
|
145
|
+
this.databaseIncludeSpecialColumns = config.databaseIncludeSpecialColumns ?? false;
|
|
146
|
+
this.logger = config.context?._getLogger?.() ?? createLogger();
|
|
90
147
|
}
|
|
91
148
|
|
|
92
149
|
/**
|
|
93
|
-
* Helper to merge database-level useEntityIds with per-request options
|
|
150
|
+
* Helper to merge database-level useEntityIds and includeSpecialColumns with per-request options
|
|
94
151
|
*/
|
|
95
|
-
private mergeExecuteOptions(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
152
|
+
private mergeExecuteOptions(options?: RequestInit & FFetchOptions & ExecuteOptions): RequestInit &
|
|
153
|
+
FFetchOptions & {
|
|
154
|
+
useEntityIds?: boolean;
|
|
155
|
+
includeSpecialColumns?: boolean;
|
|
156
|
+
} {
|
|
157
|
+
const merged = mergeExecuteOptions(options, this.databaseUseEntityIds);
|
|
99
158
|
return {
|
|
100
|
-
...
|
|
101
|
-
|
|
159
|
+
...merged,
|
|
160
|
+
includeSpecialColumns: options?.includeSpecialColumns ?? this.databaseIncludeSpecialColumns,
|
|
102
161
|
};
|
|
103
162
|
}
|
|
104
163
|
|
|
@@ -107,114 +166,376 @@ export class RecordBuilder<
|
|
|
107
166
|
* @param useEntityIds - Optional override for entity ID usage
|
|
108
167
|
*/
|
|
109
168
|
private getTableId(useEntityIds?: boolean): string {
|
|
110
|
-
if (!this.
|
|
111
|
-
|
|
169
|
+
if (!this.table) {
|
|
170
|
+
throw new Error("Table occurrence is required");
|
|
112
171
|
}
|
|
172
|
+
return resolveTableId(this.table, getTableName(this.table), this.context, useEntityIds);
|
|
173
|
+
}
|
|
113
174
|
|
|
114
|
-
|
|
115
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Creates a new RecordBuilder with modified configuration.
|
|
177
|
+
* Used by select() to create new instances.
|
|
178
|
+
*/
|
|
179
|
+
private cloneWithChanges<
|
|
180
|
+
NewSelected extends
|
|
181
|
+
| keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>
|
|
182
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
|
|
183
|
+
| Record<string, Column<any, any, ExtractTableName<NonNullable<Occ>>>> = Selected,
|
|
184
|
+
NewSystemCols extends SystemColumnsOption | undefined = SystemCols,
|
|
185
|
+
>(changes: {
|
|
186
|
+
selectedFields?: string[];
|
|
187
|
+
fieldMapping?: Record<string, string>;
|
|
188
|
+
systemColumns?: NewSystemCols;
|
|
189
|
+
}): RecordBuilder<Occ, false, FieldColumn, NewSelected, Expands, DatabaseIncludeSpecialColumns, NewSystemCols> {
|
|
190
|
+
const newBuilder = new RecordBuilder<
|
|
191
|
+
Occ,
|
|
192
|
+
false,
|
|
193
|
+
FieldColumn,
|
|
194
|
+
NewSelected,
|
|
195
|
+
Expands,
|
|
196
|
+
DatabaseIncludeSpecialColumns,
|
|
197
|
+
NewSystemCols
|
|
198
|
+
>({
|
|
199
|
+
occurrence: this.table,
|
|
200
|
+
databaseName: this.databaseName,
|
|
201
|
+
context: this.context,
|
|
202
|
+
recordId: this.recordId,
|
|
203
|
+
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
204
|
+
databaseIncludeSpecialColumns: this.databaseIncludeSpecialColumns,
|
|
205
|
+
});
|
|
206
|
+
// Use type assertion to allow assignment to readonly properties on new instance
|
|
207
|
+
|
|
208
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
|
|
209
|
+
const mutableBuilder = newBuilder as any;
|
|
210
|
+
mutableBuilder.selectedFields = changes.selectedFields ?? this.selectedFields;
|
|
211
|
+
mutableBuilder.fieldMapping = changes.fieldMapping ?? this.fieldMapping;
|
|
212
|
+
mutableBuilder.systemColumns = changes.systemColumns !== undefined ? changes.systemColumns : this.systemColumns;
|
|
213
|
+
mutableBuilder.expandConfigs = [...this.expandConfigs];
|
|
214
|
+
// Preserve navigation context
|
|
215
|
+
mutableBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;
|
|
216
|
+
mutableBuilder.navigateRelation = this.navigateRelation;
|
|
217
|
+
mutableBuilder.navigateSourceTableName = this.navigateSourceTableName;
|
|
218
|
+
mutableBuilder.operationColumn = this.operationColumn;
|
|
219
|
+
return newBuilder;
|
|
220
|
+
}
|
|
116
221
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
222
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
|
|
223
|
+
getSingleField<TColumn extends Column<any, any, ExtractTableName<NonNullable<Occ>>, any>>(
|
|
224
|
+
column: TColumn,
|
|
225
|
+
): RecordBuilder<
|
|
226
|
+
Occ,
|
|
227
|
+
true,
|
|
228
|
+
TColumn,
|
|
229
|
+
keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,
|
|
230
|
+
{},
|
|
231
|
+
DatabaseIncludeSpecialColumns
|
|
232
|
+
> {
|
|
233
|
+
// Runtime validation: ensure column is from the correct table
|
|
234
|
+
const tableName = getTableName(this.table);
|
|
235
|
+
if (!column.isFromTable(tableName)) {
|
|
236
|
+
throw new Error(`Column ${column.toString()} is not from table ${tableName}`);
|
|
125
237
|
}
|
|
126
238
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
239
|
+
const newBuilder = new RecordBuilder<
|
|
240
|
+
Occ,
|
|
241
|
+
true,
|
|
242
|
+
TColumn,
|
|
243
|
+
keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,
|
|
244
|
+
{},
|
|
245
|
+
DatabaseIncludeSpecialColumns
|
|
246
|
+
>({
|
|
247
|
+
occurrence: this.table,
|
|
134
248
|
databaseName: this.databaseName,
|
|
135
249
|
context: this.context,
|
|
136
250
|
recordId: this.recordId,
|
|
251
|
+
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
252
|
+
databaseIncludeSpecialColumns: this.databaseIncludeSpecialColumns,
|
|
137
253
|
});
|
|
138
|
-
|
|
139
|
-
|
|
254
|
+
// Use type assertion to allow assignment to readonly properties on new instance
|
|
255
|
+
|
|
256
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
|
|
257
|
+
const mutableBuilder = newBuilder as any;
|
|
258
|
+
mutableBuilder.operation = "getSingleField";
|
|
259
|
+
mutableBuilder.operationColumn = column;
|
|
260
|
+
mutableBuilder.operationParam = column.getFieldIdentifier(this.databaseUseEntityIds);
|
|
140
261
|
// Preserve navigation context
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
262
|
+
mutableBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;
|
|
263
|
+
mutableBuilder.navigateRelation = this.navigateRelation;
|
|
264
|
+
mutableBuilder.navigateSourceTableName = this.navigateSourceTableName;
|
|
144
265
|
return newBuilder;
|
|
145
266
|
}
|
|
146
267
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Select fields using column references.
|
|
270
|
+
* Allows renaming fields by using different keys in the object.
|
|
271
|
+
* Container fields cannot be selected and will cause a type error.
|
|
272
|
+
*
|
|
273
|
+
* @example
|
|
274
|
+
* db.from(contacts).get("uuid").select({
|
|
275
|
+
* name: contacts.name,
|
|
276
|
+
* userEmail: contacts.email // renamed!
|
|
277
|
+
* })
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* // Include system columns (ROWID, ROWMODID) when using select()
|
|
281
|
+
* db.from(contacts).get("uuid").select(
|
|
282
|
+
* { name: contacts.name },
|
|
283
|
+
* { ROWID: true, ROWMODID: true }
|
|
284
|
+
* )
|
|
285
|
+
*
|
|
286
|
+
* @param fields - Object mapping output keys to column references (container fields excluded)
|
|
287
|
+
* @param systemColumns - Optional object to request system columns (ROWID, ROWMODID)
|
|
288
|
+
* @returns RecordBuilder with updated selected fields
|
|
289
|
+
*/
|
|
290
|
+
select<
|
|
291
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
|
|
292
|
+
TSelect extends Record<string, Column<any, any, ExtractTableName<Occ>, false>>,
|
|
293
|
+
TSystemCols extends SystemColumnsOption = {},
|
|
294
|
+
>(
|
|
295
|
+
fields: TSelect,
|
|
296
|
+
systemColumns?: TSystemCols,
|
|
297
|
+
): RecordBuilder<Occ, false, FieldColumn, TSelect, Expands, DatabaseIncludeSpecialColumns, TSystemCols> {
|
|
298
|
+
const tableName = getTableName(this.table);
|
|
299
|
+
const { selectedFields, fieldMapping } = processSelectWithRenames(fields, tableName, this.logger);
|
|
300
|
+
|
|
301
|
+
// Add system columns to selectedFields if requested
|
|
302
|
+
const finalSelectedFields = [...selectedFields];
|
|
303
|
+
if (systemColumns?.ROWID) {
|
|
304
|
+
finalSelectedFields.push("ROWID");
|
|
305
|
+
}
|
|
306
|
+
if (systemColumns?.ROWMODID) {
|
|
307
|
+
finalSelectedFields.push("ROWMODID");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return this.cloneWithChanges({
|
|
311
|
+
selectedFields: finalSelectedFields,
|
|
312
|
+
fieldMapping: Object.keys(fieldMapping).length > 0 ? fieldMapping : undefined,
|
|
313
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter
|
|
314
|
+
systemColumns: systemColumns as any,
|
|
315
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type
|
|
316
|
+
}) as any;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Expand a navigation property to include related records.
|
|
321
|
+
* Supports nested select, filter, orderBy, and expand operations.
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* ```typescript
|
|
325
|
+
* // Simple expand with FMTable object
|
|
326
|
+
* const contact = await db.from(contacts).get("uuid").expand(users).execute();
|
|
327
|
+
*
|
|
328
|
+
* // Expand with select
|
|
329
|
+
* const contact = await db.from(contacts).get("uuid")
|
|
330
|
+
* .expand(users, b => b.select({ username: users.username, email: users.email }))
|
|
331
|
+
* .execute();
|
|
332
|
+
* ```
|
|
333
|
+
*/
|
|
334
|
+
expand<
|
|
335
|
+
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
|
|
336
|
+
TargetTable extends FMTable<any, any>,
|
|
337
|
+
TSelected extends
|
|
338
|
+
| keyof InferSchemaOutputFromFMTable<TargetTable>
|
|
339
|
+
| Record<
|
|
340
|
+
string,
|
|
341
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
|
|
342
|
+
Column<any, any, ExtractTableName<TargetTable>>
|
|
343
|
+
> = keyof InferSchemaOutputFromFMTable<TargetTable>,
|
|
344
|
+
TNestedExpands extends ExpandedRelations = {},
|
|
345
|
+
>(
|
|
346
|
+
targetTable: ValidExpandTarget<Occ, TargetTable>,
|
|
347
|
+
callback?: (
|
|
348
|
+
builder: QueryBuilder<TargetTable, keyof InferSchemaOutputFromFMTable<TargetTable>, false, false, {}>,
|
|
349
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryBuilder configuration
|
|
350
|
+
) => QueryBuilder<TargetTable, TSelected, any, any, TNestedExpands>,
|
|
351
|
+
): RecordBuilder<
|
|
352
|
+
Occ,
|
|
353
|
+
false,
|
|
354
|
+
FieldColumn,
|
|
355
|
+
Selected,
|
|
356
|
+
Expands & {
|
|
357
|
+
[K in ExtractTableName<TargetTable>]: {
|
|
358
|
+
schema: InferSchemaOutputFromFMTable<TargetTable>;
|
|
359
|
+
selected: TSelected;
|
|
360
|
+
nested: TNestedExpands;
|
|
361
|
+
};
|
|
362
|
+
},
|
|
363
|
+
DatabaseIncludeSpecialColumns,
|
|
364
|
+
SystemCols
|
|
365
|
+
> {
|
|
366
|
+
// Create new builder with updated types
|
|
367
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExpandedRelations
|
|
368
|
+
const newBuilder = new RecordBuilder<Occ, false, FieldColumn, Selected, any, DatabaseIncludeSpecialColumns>({
|
|
369
|
+
occurrence: this.table,
|
|
370
|
+
databaseName: this.databaseName,
|
|
371
|
+
context: this.context,
|
|
372
|
+
recordId: this.recordId,
|
|
373
|
+
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
374
|
+
databaseIncludeSpecialColumns: this.databaseIncludeSpecialColumns,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Use type assertion to allow assignment to readonly properties on new instance
|
|
378
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
|
|
379
|
+
const mutableBuilder = newBuilder as any;
|
|
380
|
+
// Copy existing state
|
|
381
|
+
mutableBuilder.selectedFields = this.selectedFields;
|
|
382
|
+
mutableBuilder.fieldMapping = this.fieldMapping;
|
|
383
|
+
mutableBuilder.systemColumns = this.systemColumns;
|
|
384
|
+
mutableBuilder.expandConfigs = [...this.expandConfigs];
|
|
385
|
+
mutableBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;
|
|
386
|
+
mutableBuilder.navigateRelation = this.navigateRelation;
|
|
387
|
+
mutableBuilder.navigateSourceTableName = this.navigateSourceTableName;
|
|
388
|
+
mutableBuilder.operationColumn = this.operationColumn;
|
|
389
|
+
|
|
390
|
+
// Use ExpandBuilder.processExpand to handle the expand logic
|
|
391
|
+
const expandBuilder = new ExpandBuilder(this.databaseUseEntityIds, this.logger);
|
|
392
|
+
type TargetBuilder = QueryBuilder<TargetTable, keyof InferSchemaOutputFromFMTable<TargetTable>, false, false, {}>;
|
|
393
|
+
const expandConfig = expandBuilder.processExpand<TargetTable, TargetBuilder>(
|
|
394
|
+
targetTable,
|
|
395
|
+
this.table ?? undefined,
|
|
396
|
+
callback as ((builder: TargetBuilder) => TargetBuilder) | undefined,
|
|
397
|
+
() =>
|
|
398
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryBuilder configuration
|
|
399
|
+
new QueryBuilder<TargetTable, any, any, any, any, DatabaseIncludeSpecialColumns, undefined>({
|
|
400
|
+
occurrence: targetTable,
|
|
401
|
+
databaseName: this.databaseName,
|
|
402
|
+
context: this.context,
|
|
403
|
+
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
404
|
+
databaseIncludeSpecialColumns: this.databaseIncludeSpecialColumns,
|
|
405
|
+
}),
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
mutableBuilder.expandConfigs.push(expandConfig);
|
|
409
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion needed as expand changes generic parameters in complex way that TypeScript cannot infer
|
|
410
|
+
return newBuilder as any;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
|
|
414
|
+
navigate<TargetTable extends FMTable<any, any>>(
|
|
415
|
+
targetTable: ValidExpandTarget<Occ, TargetTable>,
|
|
150
416
|
): QueryBuilder<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
417
|
+
TargetTable,
|
|
418
|
+
keyof InferSchemaOutputFromFMTable<TargetTable>,
|
|
419
|
+
false,
|
|
420
|
+
false,
|
|
421
|
+
{},
|
|
422
|
+
DatabaseIncludeSpecialColumns,
|
|
423
|
+
undefined
|
|
424
|
+
> {
|
|
425
|
+
// Extract name and validate
|
|
426
|
+
const relationName = getTableName(targetTable);
|
|
427
|
+
|
|
428
|
+
// Runtime validation: Check if relation name is in navigationPaths
|
|
429
|
+
if (this.table) {
|
|
430
|
+
const navigationPaths = getNavigationPaths(this.table);
|
|
431
|
+
if (navigationPaths && !navigationPaths.includes(relationName)) {
|
|
432
|
+
this.logger.warn(
|
|
433
|
+
`Cannot navigate to "${relationName}". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(", ") : "none"}`,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Create QueryBuilder with target table
|
|
439
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryBuilder configuration
|
|
440
|
+
const builder = new QueryBuilder<TargetTable, any, any, any, any, DatabaseIncludeSpecialColumns, undefined>({
|
|
441
|
+
occurrence: targetTable,
|
|
171
442
|
databaseName: this.databaseName,
|
|
172
443
|
context: this.context,
|
|
444
|
+
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
445
|
+
databaseIncludeSpecialColumns: this.databaseIncludeSpecialColumns,
|
|
173
446
|
});
|
|
174
|
-
// Store the navigation info - we'll use it in execute
|
|
175
|
-
// Transform relation name to FMTID if using entity IDs
|
|
176
|
-
const relationId = targetOccurrence
|
|
177
|
-
? transformTableName(targetOccurrence)
|
|
178
|
-
: relationName;
|
|
179
447
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
448
|
+
// Store the navigation info - we'll use it in execute
|
|
449
|
+
// Use relation name as-is (entity ID handling is done in QueryBuilder)
|
|
450
|
+
const relationId = relationName;
|
|
183
451
|
|
|
184
452
|
// If this RecordBuilder came from a navigated EntitySet, we need to preserve that base path
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
this.navigateRelation
|
|
189
|
-
) {
|
|
453
|
+
let sourceTableName: string;
|
|
454
|
+
let baseRelation: string | undefined;
|
|
455
|
+
if (this.isNavigateFromEntitySet && this.navigateSourceTableName && this.navigateRelation) {
|
|
190
456
|
// Build the base path: /sourceTable/relation('recordId')/newRelation
|
|
191
|
-
|
|
192
|
-
|
|
457
|
+
sourceTableName = this.navigateSourceTableName;
|
|
458
|
+
baseRelation = this.navigateRelation;
|
|
193
459
|
} else {
|
|
194
460
|
// Normal record navigation: /tableName('recordId')/relation
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
(
|
|
461
|
+
// Use table ID if available, otherwise table name
|
|
462
|
+
if (!this.table) {
|
|
463
|
+
throw new Error("Table occurrence is required for navigation");
|
|
464
|
+
}
|
|
465
|
+
sourceTableName = resolveTableId(this.table, getTableName(this.table), this.context, this.databaseUseEntityIds);
|
|
200
466
|
}
|
|
201
467
|
|
|
468
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
|
|
469
|
+
(builder as any).navigation = {
|
|
470
|
+
recordId: this.recordId,
|
|
471
|
+
relation: relationId,
|
|
472
|
+
sourceTableName,
|
|
473
|
+
baseRelation,
|
|
474
|
+
};
|
|
475
|
+
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
|
|
476
|
+
(builder as any).navigation = {
|
|
477
|
+
recordId: this.recordId,
|
|
478
|
+
relation: relationId,
|
|
479
|
+
sourceTableName,
|
|
480
|
+
baseRelation,
|
|
481
|
+
};
|
|
482
|
+
|
|
202
483
|
return builder;
|
|
203
484
|
}
|
|
204
485
|
|
|
205
|
-
|
|
206
|
-
|
|
486
|
+
/**
|
|
487
|
+
* Builds the complete query string including $select and $expand parameters.
|
|
488
|
+
*/
|
|
489
|
+
private buildQueryString(includeSpecialColumns?: boolean, useEntityIds?: boolean): string {
|
|
490
|
+
// Use merged includeSpecialColumns if provided, otherwise use database-level default
|
|
491
|
+
const finalIncludeSpecialColumns = includeSpecialColumns ?? this.databaseIncludeSpecialColumns;
|
|
492
|
+
// Use merged useEntityIds if provided, otherwise use database-level default
|
|
493
|
+
const finalUseEntityIds = useEntityIds ?? this.databaseUseEntityIds;
|
|
494
|
+
|
|
495
|
+
return buildSelectExpandQueryString({
|
|
496
|
+
selectedFields: this.selectedFields,
|
|
497
|
+
expandConfigs: this.expandConfigs,
|
|
498
|
+
table: this.table,
|
|
499
|
+
useEntityIds: finalUseEntityIds,
|
|
500
|
+
logger: this.logger,
|
|
501
|
+
includeSpecialColumns: finalIncludeSpecialColumns,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async execute<EO extends ExecuteOptions>(
|
|
506
|
+
options?: ExecuteMethodOptions<EO>,
|
|
207
507
|
): Promise<
|
|
208
|
-
Result<
|
|
508
|
+
Result<
|
|
509
|
+
ConditionallyWithODataAnnotations<
|
|
510
|
+
ConditionallyWithSpecialColumns<
|
|
511
|
+
RecordReturnType<
|
|
512
|
+
InferSchemaOutputFromFMTable<NonNullable<Occ>>,
|
|
513
|
+
IsSingleField,
|
|
514
|
+
FieldColumn,
|
|
515
|
+
Selected,
|
|
516
|
+
Expands,
|
|
517
|
+
SystemCols
|
|
518
|
+
>,
|
|
519
|
+
// Use the merged value: if explicitly provided in options, use that; otherwise use database default
|
|
520
|
+
NormalizeIncludeSpecialColumns<EO["includeSpecialColumns"], DatabaseIncludeSpecialColumns>,
|
|
521
|
+
// Check if select was applied: if Selected is Record (object select) or a subset of keys, select was applied
|
|
522
|
+
IsSingleField extends true
|
|
523
|
+
? false // Single field operations don't include special columns
|
|
524
|
+
: // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
|
|
525
|
+
Selected extends Record<string, Column<any, any, any>>
|
|
526
|
+
? true
|
|
527
|
+
: Selected extends keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>
|
|
528
|
+
? false
|
|
529
|
+
: true
|
|
530
|
+
>,
|
|
531
|
+
EO["includeODataAnnotations"] extends true ? true : false
|
|
532
|
+
>
|
|
533
|
+
>
|
|
209
534
|
> {
|
|
210
535
|
let url: string;
|
|
211
536
|
|
|
212
537
|
// Build the base URL depending on whether this came from a navigated EntitySet
|
|
213
|
-
if (
|
|
214
|
-
this.isNavigateFromEntitySet &&
|
|
215
|
-
this.navigateSourceTableName &&
|
|
216
|
-
this.navigateRelation
|
|
217
|
-
) {
|
|
538
|
+
if (this.isNavigateFromEntitySet && this.navigateSourceTableName && this.navigateRelation) {
|
|
218
539
|
// From navigated EntitySet: /sourceTable/relation('recordId')
|
|
219
540
|
url = `/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateRelation}('${this.recordId}')`;
|
|
220
541
|
} else {
|
|
@@ -223,71 +544,56 @@ export class RecordBuilder<
|
|
|
223
544
|
url = `/${this.databaseName}/${tableId}('${this.recordId}')`;
|
|
224
545
|
}
|
|
225
546
|
|
|
547
|
+
const mergedOptions = this.mergeExecuteOptions(options);
|
|
548
|
+
|
|
226
549
|
if (this.operation === "getSingleField" && this.operationParam) {
|
|
227
550
|
url += `/${this.operationParam}`;
|
|
551
|
+
} else {
|
|
552
|
+
// Add query string for select/expand (only when not getting a single field)
|
|
553
|
+
const queryString = this.buildQueryString(mergedOptions.includeSpecialColumns, mergedOptions.useEntityIds);
|
|
554
|
+
url += queryString;
|
|
228
555
|
}
|
|
229
|
-
|
|
230
|
-
const mergedOptions = this.mergeExecuteOptions(options);
|
|
231
556
|
const result = await this.context._makeRequest(url, mergedOptions);
|
|
232
557
|
|
|
233
558
|
if (result.error) {
|
|
234
559
|
return { data: undefined, error: result.error };
|
|
235
560
|
}
|
|
236
561
|
|
|
237
|
-
|
|
562
|
+
const response = result.data;
|
|
238
563
|
|
|
239
564
|
// Handle single field operation
|
|
240
565
|
if (this.operation === "getSingleField") {
|
|
241
566
|
// Single field returns a JSON object with @context and value
|
|
242
|
-
|
|
567
|
+
// The type is extracted from the Column stored in FieldColumn generic
|
|
568
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API
|
|
569
|
+
const fieldResponse = response as ODataFieldResponse<any>;
|
|
570
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type extraction
|
|
243
571
|
return { data: fieldResponse.value as any, error: undefined };
|
|
244
572
|
}
|
|
245
573
|
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
// Validate the single record response
|
|
262
|
-
const validation = await validateSingleResponse<any>(
|
|
263
|
-
response,
|
|
264
|
-
schema,
|
|
265
|
-
undefined, // No selected fields for record.get()
|
|
266
|
-
undefined, // No expand configs
|
|
267
|
-
"exact", // Expect exactly one record
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
if (!validation.valid) {
|
|
271
|
-
return { data: undefined, error: validation.error };
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Handle null response
|
|
275
|
-
if (validation.data === null) {
|
|
276
|
-
return { data: null as any, error: undefined };
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return { data: validation.data, error: undefined };
|
|
574
|
+
// Use shared response processor
|
|
575
|
+
const expandBuilder = new ExpandBuilder(mergedOptions.useEntityIds ?? false, this.logger);
|
|
576
|
+
const expandValidationConfigs = expandBuilder.buildValidationConfigs(this.expandConfigs);
|
|
577
|
+
|
|
578
|
+
return processODataResponse(response, {
|
|
579
|
+
table: this.table,
|
|
580
|
+
schema: getSchemaFromTable(this.table),
|
|
581
|
+
singleMode: "exact",
|
|
582
|
+
selectedFields: this.selectedFields,
|
|
583
|
+
expandValidationConfigs,
|
|
584
|
+
skipValidation: options?.skipValidation,
|
|
585
|
+
useEntityIds: mergedOptions.useEntityIds,
|
|
586
|
+
includeSpecialColumns: mergedOptions.includeSpecialColumns,
|
|
587
|
+
fieldMapping: this.fieldMapping,
|
|
588
|
+
});
|
|
280
589
|
}
|
|
281
590
|
|
|
591
|
+
// biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value
|
|
282
592
|
getRequestConfig(): { method: string; url: string; body?: any } {
|
|
283
593
|
let url: string;
|
|
284
594
|
|
|
285
595
|
// Build the base URL depending on whether this came from a navigated EntitySet
|
|
286
|
-
if (
|
|
287
|
-
this.isNavigateFromEntitySet &&
|
|
288
|
-
this.navigateSourceTableName &&
|
|
289
|
-
this.navigateRelation
|
|
290
|
-
) {
|
|
596
|
+
if (this.isNavigateFromEntitySet && this.navigateSourceTableName && this.navigateRelation) {
|
|
291
597
|
// From navigated EntitySet: /sourceTable/relation('recordId')
|
|
292
598
|
url = `/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateRelation}('${this.recordId}')`;
|
|
293
599
|
} else {
|
|
@@ -296,8 +602,16 @@ export class RecordBuilder<
|
|
|
296
602
|
url = `/${this.databaseName}/${tableId}('${this.recordId}')`;
|
|
297
603
|
}
|
|
298
604
|
|
|
299
|
-
if (this.operation === "getSingleField" && this.
|
|
605
|
+
if (this.operation === "getSingleField" && this.operationColumn) {
|
|
606
|
+
// Use the column's getFieldIdentifier to support entity IDs
|
|
607
|
+
url += `/${this.operationColumn.getFieldIdentifier(this.databaseUseEntityIds)}`;
|
|
608
|
+
} else if (this.operation === "getSingleField" && this.operationParam) {
|
|
609
|
+
// Fallback for backwards compatibility (shouldn't happen in normal flow)
|
|
300
610
|
url += `/${this.operationParam}`;
|
|
611
|
+
} else {
|
|
612
|
+
// Add query string for select/expand (only when not getting a single field)
|
|
613
|
+
const queryString = this.buildQueryString();
|
|
614
|
+
url += queryString;
|
|
301
615
|
}
|
|
302
616
|
|
|
303
617
|
return {
|
|
@@ -306,68 +620,89 @@ export class RecordBuilder<
|
|
|
306
620
|
};
|
|
307
621
|
}
|
|
308
622
|
|
|
309
|
-
|
|
623
|
+
/**
|
|
624
|
+
* Returns the query string for this record builder (for testing purposes).
|
|
625
|
+
*/
|
|
626
|
+
getQueryString(options?: { useEntityIds?: boolean }): string {
|
|
627
|
+
const useEntityIds = options?.useEntityIds ?? this.databaseUseEntityIds;
|
|
628
|
+
let path: string;
|
|
629
|
+
|
|
630
|
+
// Build the path depending on navigation context
|
|
631
|
+
if (this.isNavigateFromEntitySet && this.navigateSourceTableName && this.navigateRelation) {
|
|
632
|
+
path = `/${this.navigateSourceTableName}/${this.navigateRelation}('${this.recordId}')`;
|
|
633
|
+
} else {
|
|
634
|
+
// Use getTableId to respect entity ID settings (same as getRequestConfig)
|
|
635
|
+
const tableId = this.getTableId(useEntityIds);
|
|
636
|
+
path = `/${tableId}('${this.recordId}')`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (this.operation === "getSingleField" && this.operationColumn) {
|
|
640
|
+
return `${path}/${this.operationColumn.getFieldIdentifier(useEntityIds)}`;
|
|
641
|
+
}
|
|
642
|
+
if (this.operation === "getSingleField" && this.operationParam) {
|
|
643
|
+
// Fallback for backwards compatibility (shouldn't happen in normal flow)
|
|
644
|
+
return `${path}/${this.operationParam}`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const queryString = this.buildQueryString(undefined, useEntityIds);
|
|
648
|
+
return `${path}${queryString}`;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
toRequest(baseUrl: string, options?: ExecuteOptions): Request {
|
|
310
652
|
const config = this.getRequestConfig();
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
return new Request(fullUrl, {
|
|
314
|
-
method: config.method,
|
|
315
|
-
headers: {
|
|
316
|
-
"Content-Type": "application/json",
|
|
317
|
-
Accept: "application/json",
|
|
318
|
-
},
|
|
319
|
-
});
|
|
653
|
+
return createODataRequest(baseUrl, config, options);
|
|
320
654
|
}
|
|
321
655
|
|
|
322
656
|
async processResponse(
|
|
323
657
|
response: Response,
|
|
324
658
|
options?: ExecuteOptions,
|
|
325
659
|
): Promise<
|
|
326
|
-
Result<
|
|
660
|
+
Result<
|
|
661
|
+
RecordReturnType<
|
|
662
|
+
InferSchemaOutputFromFMTable<NonNullable<Occ>>,
|
|
663
|
+
IsSingleField,
|
|
664
|
+
FieldColumn,
|
|
665
|
+
Selected,
|
|
666
|
+
Expands,
|
|
667
|
+
SystemCols
|
|
668
|
+
>
|
|
669
|
+
>
|
|
327
670
|
> {
|
|
328
|
-
|
|
671
|
+
// Check for error responses (important for batch operations)
|
|
672
|
+
if (!response.ok) {
|
|
673
|
+
const tableName = this.table ? getTableName(this.table) : "unknown";
|
|
674
|
+
const error = await parseErrorResponse(response, response.url || `/${this.databaseName}/${tableName}`);
|
|
675
|
+
return { data: undefined, error };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Use safeJsonParse to handle FileMaker's invalid JSON with unquoted ? values
|
|
679
|
+
const rawResponse = await safeJsonParse(response);
|
|
329
680
|
|
|
330
681
|
// Handle single field operation
|
|
331
682
|
if (this.operation === "getSingleField") {
|
|
332
683
|
// Single field returns a JSON object with @context and value
|
|
333
|
-
|
|
684
|
+
// The type is extracted from the Column stored in FieldColumn generic
|
|
685
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type parameter inferred from FieldColumn generic
|
|
686
|
+
const fieldResponse = rawResponse as ODataFieldResponse<any>;
|
|
687
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type parameter inferred from FieldColumn generic
|
|
334
688
|
return { data: fieldResponse.value as any, error: undefined };
|
|
335
689
|
}
|
|
336
690
|
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
// Validate the single record response
|
|
354
|
-
const validation = await validateSingleResponse<any>(
|
|
355
|
-
transformedResponse,
|
|
356
|
-
schema,
|
|
357
|
-
undefined, // No selected fields for record.get()
|
|
358
|
-
undefined, // No expand configs
|
|
359
|
-
"exact", // Expect exactly one record
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
if (!validation.valid) {
|
|
363
|
-
return { data: undefined, error: validation.error };
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Handle null response
|
|
367
|
-
if (validation.data === null) {
|
|
368
|
-
return { data: null as any, error: undefined };
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return { data: validation.data, error: undefined };
|
|
691
|
+
// Use shared response processor
|
|
692
|
+
const mergedOptions = this.mergeExecuteOptions(options);
|
|
693
|
+
const expandBuilder = new ExpandBuilder(mergedOptions.useEntityIds ?? false, this.logger);
|
|
694
|
+
const expandValidationConfigs = expandBuilder.buildValidationConfigs(this.expandConfigs);
|
|
695
|
+
|
|
696
|
+
return processODataResponse(rawResponse, {
|
|
697
|
+
table: this.table,
|
|
698
|
+
schema: getSchemaFromTable(this.table),
|
|
699
|
+
singleMode: "exact",
|
|
700
|
+
selectedFields: this.selectedFields,
|
|
701
|
+
expandValidationConfigs,
|
|
702
|
+
skipValidation: options?.skipValidation,
|
|
703
|
+
useEntityIds: mergedOptions.useEntityIds,
|
|
704
|
+
includeSpecialColumns: mergedOptions.includeSpecialColumns,
|
|
705
|
+
fieldMapping: this.fieldMapping,
|
|
706
|
+
});
|
|
372
707
|
}
|
|
373
708
|
}
|