@proofkit/fmodata 0.1.0-alpha.9 → 0.1.0-beta.24
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 +655 -453
- 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 +126 -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 +34 -29
- 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 +286 -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 -175
- 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,62 +1,64 @@
|
|
|
1
|
+
import type { FFetchOptions } from "@fetchkit/ffetch";
|
|
2
|
+
import { InvalidLocationHeaderError } from "../errors";
|
|
3
|
+
import type { FMTable } from "../orm/table";
|
|
4
|
+
import { getBaseTableConfig, getTableId as getTableIdHelper, getTableName, isUsingEntityIds } from "../orm/table";
|
|
5
|
+
import { transformFieldNamesToIds, transformResponseFields } from "../transform";
|
|
1
6
|
import type {
|
|
2
|
-
|
|
7
|
+
ConditionallyWithODataAnnotations,
|
|
3
8
|
ExecutableBuilder,
|
|
4
|
-
|
|
5
|
-
ODataRecordMetadata,
|
|
6
|
-
InferSchemaType,
|
|
9
|
+
ExecuteMethodOptions,
|
|
7
10
|
ExecuteOptions,
|
|
8
|
-
|
|
11
|
+
ExecutionContext,
|
|
12
|
+
Result,
|
|
9
13
|
} from "../types";
|
|
10
|
-
import
|
|
11
|
-
import { validateSingleResponse } from "../validation";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
getTableIdentifiers,
|
|
18
|
-
} from "../transform";
|
|
19
|
-
import { InvalidLocationHeaderError } from "../errors";
|
|
14
|
+
import { getAcceptHeader } from "../types";
|
|
15
|
+
import { validateAndTransformInput, validateSingleResponse } from "../validation";
|
|
16
|
+
import { parseErrorResponse } from "./error-parser";
|
|
17
|
+
import { safeJsonParse } from "./sanitize-json";
|
|
18
|
+
|
|
19
|
+
const ROWID_MATCH_REGEX = /ROWID=(\d+)/;
|
|
20
|
+
const PAREN_VALUE_REGEX = /\(['"]?([^'"]+)['"]?\)/;
|
|
20
21
|
|
|
21
|
-
export
|
|
22
|
+
export interface InsertOptions {
|
|
22
23
|
return?: "minimal" | "representation";
|
|
23
|
-
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
import type { InferSchemaOutputFromFMTable } from "../orm/table";
|
|
24
27
|
|
|
25
28
|
export class InsertBuilder<
|
|
26
|
-
|
|
27
|
-
Occ extends
|
|
29
|
+
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
|
|
30
|
+
Occ extends FMTable<any, any> | undefined = undefined,
|
|
28
31
|
ReturnPreference extends "minimal" | "representation" = "representation",
|
|
29
32
|
> implements
|
|
30
33
|
ExecutableBuilder<
|
|
31
|
-
ReturnPreference extends "minimal" ? { ROWID: number } :
|
|
34
|
+
ReturnPreference extends "minimal" ? { ROWID: number } : InferSchemaOutputFromFMTable<NonNullable<Occ>>
|
|
32
35
|
>
|
|
33
36
|
{
|
|
34
|
-
private
|
|
35
|
-
private
|
|
36
|
-
private
|
|
37
|
-
private
|
|
38
|
-
private
|
|
39
|
-
private returnPreference: ReturnPreference;
|
|
37
|
+
private readonly table?: Occ;
|
|
38
|
+
private readonly databaseName: string;
|
|
39
|
+
private readonly context: ExecutionContext;
|
|
40
|
+
private readonly data: Partial<InferSchemaOutputFromFMTable<NonNullable<Occ>>>;
|
|
41
|
+
private readonly returnPreference: ReturnPreference;
|
|
40
42
|
|
|
41
|
-
private databaseUseEntityIds: boolean;
|
|
43
|
+
private readonly databaseUseEntityIds: boolean;
|
|
44
|
+
private readonly databaseIncludeSpecialColumns: boolean;
|
|
42
45
|
|
|
43
46
|
constructor(config: {
|
|
44
47
|
occurrence?: Occ;
|
|
45
|
-
tableName: string;
|
|
46
48
|
databaseName: string;
|
|
47
49
|
context: ExecutionContext;
|
|
48
|
-
data: Partial<
|
|
50
|
+
data: Partial<InferSchemaOutputFromFMTable<NonNullable<Occ>>>;
|
|
49
51
|
returnPreference?: ReturnPreference;
|
|
50
52
|
databaseUseEntityIds?: boolean;
|
|
53
|
+
databaseIncludeSpecialColumns?: boolean;
|
|
51
54
|
}) {
|
|
52
|
-
this.
|
|
53
|
-
this.tableName = config.tableName;
|
|
55
|
+
this.table = config.occurrence;
|
|
54
56
|
this.databaseName = config.databaseName;
|
|
55
57
|
this.context = config.context;
|
|
56
58
|
this.data = config.data;
|
|
57
|
-
this.returnPreference = (config.returnPreference ||
|
|
58
|
-
"representation") as ReturnPreference;
|
|
59
|
+
this.returnPreference = (config.returnPreference || "representation") as ReturnPreference;
|
|
59
60
|
this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;
|
|
61
|
+
this.databaseIncludeSpecialColumns = config.databaseIncludeSpecialColumns ?? false;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
/**
|
|
@@ -72,23 +74,6 @@ export class InsertBuilder<
|
|
|
72
74
|
};
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
/**
|
|
76
|
-
* Helper to conditionally strip OData annotations based on options
|
|
77
|
-
*/
|
|
78
|
-
private stripODataAnnotationsIfNeeded<T extends Record<string, any>>(
|
|
79
|
-
data: T,
|
|
80
|
-
options?: ExecuteOptions,
|
|
81
|
-
): T {
|
|
82
|
-
// Only include annotations if explicitly requested
|
|
83
|
-
if (options?.includeODataAnnotations === true) {
|
|
84
|
-
return data;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Strip OData annotations
|
|
88
|
-
const { "@id": _id, "@editLink": _editLink, ...rest } = data;
|
|
89
|
-
return rest as T;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
77
|
/**
|
|
93
78
|
* Parse ROWID from Location header
|
|
94
79
|
* Expected formats:
|
|
@@ -97,23 +82,21 @@ export class InsertBuilder<
|
|
|
97
82
|
*/
|
|
98
83
|
private parseLocationHeader(locationHeader: string | undefined): number {
|
|
99
84
|
if (!locationHeader) {
|
|
100
|
-
throw new InvalidLocationHeaderError(
|
|
101
|
-
"Location header is required but was not provided",
|
|
102
|
-
);
|
|
85
|
+
throw new InvalidLocationHeaderError("Location header is required but was not provided");
|
|
103
86
|
}
|
|
104
87
|
|
|
105
88
|
// Try to match ROWID=number pattern
|
|
106
|
-
const rowidMatch = locationHeader.match(
|
|
107
|
-
if (rowidMatch
|
|
108
|
-
return parseInt(rowidMatch[1], 10);
|
|
89
|
+
const rowidMatch = locationHeader.match(ROWID_MATCH_REGEX);
|
|
90
|
+
if (rowidMatch?.[1]) {
|
|
91
|
+
return Number.parseInt(rowidMatch[1], 10);
|
|
109
92
|
}
|
|
110
93
|
|
|
111
94
|
// Try to extract value from parentheses and parse as number
|
|
112
|
-
const parenMatch = locationHeader.match(
|
|
113
|
-
if (parenMatch
|
|
95
|
+
const parenMatch = locationHeader.match(PAREN_VALUE_REGEX);
|
|
96
|
+
if (parenMatch?.[1]) {
|
|
114
97
|
const value = parenMatch[1];
|
|
115
|
-
const numValue = parseInt(value, 10);
|
|
116
|
-
if (!isNaN(numValue)) {
|
|
98
|
+
const numValue = Number.parseInt(value, 10);
|
|
99
|
+
if (!Number.isNaN(numValue)) {
|
|
117
100
|
return numValue;
|
|
118
101
|
}
|
|
119
102
|
}
|
|
@@ -129,65 +112,80 @@ export class InsertBuilder<
|
|
|
129
112
|
* @param useEntityIds - Optional override for entity ID usage
|
|
130
113
|
*/
|
|
131
114
|
private getTableId(useEntityIds?: boolean): string {
|
|
132
|
-
if (!this.
|
|
133
|
-
|
|
115
|
+
if (!this.table) {
|
|
116
|
+
throw new Error("Table occurrence is required");
|
|
134
117
|
}
|
|
135
118
|
|
|
136
119
|
const contextDefault = this.context._getUseEntityIds?.() ?? false;
|
|
137
120
|
const shouldUseIds = useEntityIds ?? contextDefault;
|
|
138
121
|
|
|
139
122
|
if (shouldUseIds) {
|
|
140
|
-
|
|
141
|
-
if (!identifiers.id) {
|
|
123
|
+
if (!isUsingEntityIds(this.table)) {
|
|
142
124
|
throw new Error(
|
|
143
|
-
`useEntityIds is true but
|
|
125
|
+
`useEntityIds is true but table "${getTableName(this.table)}" does not have entity IDs configured`,
|
|
144
126
|
);
|
|
145
127
|
}
|
|
146
|
-
return
|
|
128
|
+
return getTableIdHelper(this.table);
|
|
147
129
|
}
|
|
148
130
|
|
|
149
|
-
return this.
|
|
131
|
+
return getTableName(this.table);
|
|
150
132
|
}
|
|
151
133
|
|
|
152
134
|
async execute<EO extends ExecuteOptions>(
|
|
153
|
-
options?:
|
|
135
|
+
options?: ExecuteMethodOptions<EO>,
|
|
154
136
|
): Promise<
|
|
155
137
|
Result<
|
|
156
138
|
ReturnPreference extends "minimal"
|
|
157
139
|
? { ROWID: number }
|
|
158
140
|
: ConditionallyWithODataAnnotations<
|
|
159
|
-
|
|
141
|
+
InferSchemaOutputFromFMTable<NonNullable<Occ>>,
|
|
160
142
|
EO["includeODataAnnotations"] extends true ? true : false
|
|
161
143
|
>
|
|
162
144
|
>
|
|
163
145
|
> {
|
|
164
146
|
// Merge database-level useEntityIds with per-request options
|
|
165
147
|
const mergedOptions = this.mergeExecuteOptions(options);
|
|
166
|
-
|
|
148
|
+
|
|
167
149
|
// Get table identifier with override support
|
|
168
150
|
const tableId = this.getTableId(mergedOptions.useEntityIds);
|
|
169
151
|
const url = `/${this.databaseName}/${tableId}`;
|
|
170
152
|
|
|
153
|
+
// Validate and transform input data using input validators (writeValidators)
|
|
154
|
+
let validatedData = this.data;
|
|
155
|
+
if (this.table) {
|
|
156
|
+
const baseTableConfig = getBaseTableConfig(this.table);
|
|
157
|
+
const inputSchema = baseTableConfig.inputSchema;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
validatedData = await validateAndTransformInput(this.data, inputSchema);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// If validation fails, return error immediately
|
|
163
|
+
return {
|
|
164
|
+
data: undefined,
|
|
165
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
166
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
|
|
167
|
+
} as any;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
171
|
// Transform field names to FMFIDs if using entity IDs
|
|
172
172
|
// Only transform if useEntityIds resolves to true (respects per-request override)
|
|
173
173
|
const shouldUseIds = mergedOptions.useEntityIds ?? false;
|
|
174
|
-
|
|
175
|
-
const transformedData =
|
|
176
|
-
? transformFieldNamesToIds(
|
|
177
|
-
: this.data;
|
|
174
|
+
|
|
175
|
+
const transformedData =
|
|
176
|
+
this.table && shouldUseIds ? transformFieldNamesToIds(validatedData, this.table) : validatedData;
|
|
178
177
|
|
|
179
178
|
// Set Prefer header based on return preference
|
|
180
|
-
const preferHeader =
|
|
181
|
-
this.returnPreference === "minimal"
|
|
182
|
-
? "return=minimal"
|
|
183
|
-
: "return=representation";
|
|
179
|
+
const preferHeader = this.returnPreference === "minimal" ? "return=minimal" : "return=representation";
|
|
184
180
|
|
|
185
181
|
// Make POST request with JSON body
|
|
182
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API
|
|
186
183
|
const result = await this.context._makeRequest<any>(url, {
|
|
187
184
|
method: "POST",
|
|
188
185
|
headers: {
|
|
189
186
|
"Content-Type": "application/json",
|
|
190
187
|
Prefer: preferHeader,
|
|
188
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for headers object
|
|
191
189
|
...((mergedOptions as any)?.headers || {}),
|
|
192
190
|
},
|
|
193
191
|
body: JSON.stringify(transformedData),
|
|
@@ -202,15 +200,17 @@ export class InsertBuilder<
|
|
|
202
200
|
if (this.returnPreference === "minimal") {
|
|
203
201
|
// The response should be empty (204 No Content)
|
|
204
202
|
// _makeRequest will return { _location: string } when there's a Location header
|
|
203
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API
|
|
205
204
|
const responseData = result.data as any;
|
|
206
205
|
|
|
207
|
-
if (!responseData
|
|
206
|
+
if (!responseData?._location) {
|
|
208
207
|
throw new InvalidLocationHeaderError(
|
|
209
208
|
"Location header is required when using return=minimal but was not found in response",
|
|
210
209
|
);
|
|
211
210
|
}
|
|
212
211
|
|
|
213
212
|
const rowid = this.parseLocationHeader(responseData._location);
|
|
213
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
|
|
214
214
|
return { data: { ROWID: rowid } as any, error: undefined };
|
|
215
215
|
}
|
|
216
216
|
|
|
@@ -218,19 +218,30 @@ export class InsertBuilder<
|
|
|
218
218
|
|
|
219
219
|
// Transform response field IDs back to names if using entity IDs
|
|
220
220
|
// Only transform if useEntityIds resolves to true (respects per-request override)
|
|
221
|
-
if (this.
|
|
221
|
+
if (this.table && shouldUseIds) {
|
|
222
222
|
response = transformResponseFields(
|
|
223
223
|
response,
|
|
224
|
-
this.
|
|
224
|
+
this.table,
|
|
225
225
|
undefined, // No expand configs for insert
|
|
226
226
|
);
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
// Get schema from
|
|
230
|
-
|
|
229
|
+
// Get schema from table if available, excluding container fields
|
|
230
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic schema shape from table configuration
|
|
231
|
+
let schema: Record<string, any> | undefined;
|
|
232
|
+
if (this.table) {
|
|
233
|
+
const baseTableConfig = getBaseTableConfig(this.table);
|
|
234
|
+
const containerFields = baseTableConfig.containerFields || [];
|
|
235
|
+
|
|
236
|
+
// Filter out container fields from schema
|
|
237
|
+
schema = { ...baseTableConfig.schema };
|
|
238
|
+
for (const containerField of containerFields) {
|
|
239
|
+
delete schema[containerField as string];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
231
242
|
|
|
232
243
|
// Validate the response (FileMaker returns the created record)
|
|
233
|
-
const validation = await validateSingleResponse<
|
|
244
|
+
const validation = await validateSingleResponse<InferSchemaOutputFromFMTable<NonNullable<Occ>>>(
|
|
234
245
|
response,
|
|
235
246
|
schema,
|
|
236
247
|
undefined, // No selected fields for insert
|
|
@@ -250,23 +261,19 @@ export class InsertBuilder<
|
|
|
250
261
|
};
|
|
251
262
|
}
|
|
252
263
|
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
validation.data,
|
|
256
|
-
options,
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
return { data: finalData as any, error: undefined };
|
|
264
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
|
|
265
|
+
return { data: validation.data as any, error: undefined };
|
|
260
266
|
}
|
|
261
267
|
|
|
268
|
+
// biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value
|
|
262
269
|
getRequestConfig(): { method: string; url: string; body?: any } {
|
|
263
270
|
// For batch operations, use database-level setting (no per-request override available here)
|
|
271
|
+
// Note: Input validation happens in execute() and processResponse() for batch operations
|
|
264
272
|
const tableId = this.getTableId(this.databaseUseEntityIds);
|
|
265
273
|
|
|
266
274
|
// Transform field names to FMFIDs if using entity IDs
|
|
267
|
-
const transformedData =
|
|
268
|
-
? transformFieldNamesToIds(this.data, this.
|
|
269
|
-
: this.data;
|
|
275
|
+
const transformedData =
|
|
276
|
+
this.table && this.databaseUseEntityIds ? transformFieldNamesToIds(this.data, this.table) : this.data;
|
|
270
277
|
|
|
271
278
|
return {
|
|
272
279
|
method: "POST",
|
|
@@ -275,21 +282,18 @@ export class InsertBuilder<
|
|
|
275
282
|
};
|
|
276
283
|
}
|
|
277
284
|
|
|
278
|
-
toRequest(baseUrl: string): Request {
|
|
285
|
+
toRequest(baseUrl: string, options?: ExecuteOptions): Request {
|
|
279
286
|
const config = this.getRequestConfig();
|
|
280
287
|
const fullUrl = `${baseUrl}${config.url}`;
|
|
281
288
|
|
|
282
289
|
// Set Prefer header based on return preference
|
|
283
|
-
const preferHeader =
|
|
284
|
-
this.returnPreference === "minimal"
|
|
285
|
-
? "return=minimal"
|
|
286
|
-
: "return=representation";
|
|
290
|
+
const preferHeader = this.returnPreference === "minimal" ? "return=minimal" : "return=representation";
|
|
287
291
|
|
|
288
292
|
return new Request(fullUrl, {
|
|
289
293
|
method: config.method,
|
|
290
294
|
headers: {
|
|
291
295
|
"Content-Type": "application/json",
|
|
292
|
-
Accept:
|
|
296
|
+
Accept: getAcceptHeader(options?.includeODataAnnotations),
|
|
293
297
|
Prefer: preferHeader,
|
|
294
298
|
},
|
|
295
299
|
body: config.body,
|
|
@@ -300,17 +304,24 @@ export class InsertBuilder<
|
|
|
300
304
|
response: Response,
|
|
301
305
|
options?: ExecuteOptions,
|
|
302
306
|
): Promise<
|
|
303
|
-
Result<ReturnPreference extends "minimal" ? { ROWID: number } :
|
|
307
|
+
Result<ReturnPreference extends "minimal" ? { ROWID: number } : InferSchemaOutputFromFMTable<NonNullable<Occ>>>
|
|
304
308
|
> {
|
|
309
|
+
// Check for error responses (important for batch operations)
|
|
310
|
+
if (!response.ok) {
|
|
311
|
+
const tableName = this.table ? getTableName(this.table) : "unknown";
|
|
312
|
+
const error = await parseErrorResponse(response, response.url || `/${this.databaseName}/${tableName}`);
|
|
313
|
+
return { data: undefined, error };
|
|
314
|
+
}
|
|
315
|
+
|
|
305
316
|
// Handle 204 No Content (common in batch/changeset operations)
|
|
306
317
|
// FileMaker uses return=minimal for changeset operations regardless of Prefer header
|
|
307
318
|
if (response.status === 204) {
|
|
308
319
|
// Check for Location header (for return=minimal)
|
|
309
320
|
if (this.returnPreference === "minimal") {
|
|
310
|
-
const locationHeader =
|
|
311
|
-
response.headers.get("Location") || response.headers.get("location");
|
|
321
|
+
const locationHeader = response.headers.get("Location") || response.headers.get("location");
|
|
312
322
|
if (locationHeader) {
|
|
313
323
|
const rowid = this.parseLocationHeader(locationHeader);
|
|
324
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
|
|
314
325
|
return { data: { ROWID: rowid } as any, error: undefined };
|
|
315
326
|
}
|
|
316
327
|
throw new InvalidLocationHeaderError(
|
|
@@ -322,6 +333,7 @@ export class InsertBuilder<
|
|
|
322
333
|
// This is valid OData behavior for changeset operations
|
|
323
334
|
// We return a success indicator but no actual data
|
|
324
335
|
return {
|
|
336
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
|
|
325
337
|
data: {} as any,
|
|
326
338
|
error: undefined,
|
|
327
339
|
};
|
|
@@ -334,13 +346,15 @@ export class InsertBuilder<
|
|
|
334
346
|
);
|
|
335
347
|
}
|
|
336
348
|
|
|
337
|
-
|
|
349
|
+
// Use safeJsonParse to handle FileMaker's invalid JSON with unquoted ? values
|
|
350
|
+
let rawResponse: unknown;
|
|
338
351
|
try {
|
|
339
|
-
rawResponse = await response
|
|
352
|
+
rawResponse = await safeJsonParse(response);
|
|
340
353
|
} catch (err) {
|
|
341
354
|
// If parsing fails with 204, handle it gracefully
|
|
342
355
|
if (response.status === 204) {
|
|
343
356
|
return {
|
|
357
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
|
|
344
358
|
data: {} as any,
|
|
345
359
|
error: undefined,
|
|
346
360
|
};
|
|
@@ -351,28 +365,58 @@ export class InsertBuilder<
|
|
|
351
365
|
name: "ResponseParseError",
|
|
352
366
|
message: `Failed to parse response JSON: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
353
367
|
timestamp: new Date(),
|
|
368
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object
|
|
354
369
|
} as any,
|
|
355
370
|
};
|
|
356
371
|
}
|
|
357
372
|
|
|
373
|
+
// Validate and transform input data using input validators (writeValidators)
|
|
374
|
+
// This is needed for processResponse because it's called from batch operations
|
|
375
|
+
// where the data hasn't been validated yet
|
|
376
|
+
let _validatedData = this.data;
|
|
377
|
+
if (this.table) {
|
|
378
|
+
const baseTableConfig = getBaseTableConfig(this.table);
|
|
379
|
+
const inputSchema = baseTableConfig.inputSchema;
|
|
380
|
+
try {
|
|
381
|
+
_validatedData = await validateAndTransformInput(this.data, inputSchema);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
return {
|
|
384
|
+
data: undefined,
|
|
385
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
386
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
|
|
387
|
+
} as any;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
358
391
|
// Transform response field IDs back to names if using entity IDs
|
|
359
392
|
// Only transform if useEntityIds resolves to true (respects per-request override)
|
|
360
393
|
const shouldUseIds = options?.useEntityIds ?? this.databaseUseEntityIds;
|
|
361
|
-
|
|
394
|
+
|
|
362
395
|
let transformedResponse = rawResponse;
|
|
363
|
-
if (this.
|
|
396
|
+
if (this.table && shouldUseIds) {
|
|
364
397
|
transformedResponse = transformResponseFields(
|
|
365
398
|
rawResponse,
|
|
366
|
-
this.
|
|
399
|
+
this.table,
|
|
367
400
|
undefined, // No expand configs for insert
|
|
368
401
|
);
|
|
369
402
|
}
|
|
370
403
|
|
|
371
|
-
// Get schema from
|
|
372
|
-
|
|
404
|
+
// Get schema from table if available, excluding container fields
|
|
405
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic schema shape from table configuration
|
|
406
|
+
let schema: Record<string, any> | undefined;
|
|
407
|
+
if (this.table) {
|
|
408
|
+
const baseTableConfig = getBaseTableConfig(this.table);
|
|
409
|
+
const containerFields = baseTableConfig.containerFields || [];
|
|
410
|
+
|
|
411
|
+
// Filter out container fields from schema
|
|
412
|
+
schema = { ...baseTableConfig.schema };
|
|
413
|
+
for (const containerField of containerFields) {
|
|
414
|
+
delete schema[containerField as string];
|
|
415
|
+
}
|
|
416
|
+
}
|
|
373
417
|
|
|
374
418
|
// Validate the response (FileMaker returns the created record)
|
|
375
|
-
const validation = await validateSingleResponse<
|
|
419
|
+
const validation = await validateSingleResponse<InferSchemaOutputFromFMTable<NonNullable<Occ>>>(
|
|
376
420
|
transformedResponse,
|
|
377
421
|
schema,
|
|
378
422
|
undefined, // No selected fields for insert
|
|
@@ -392,12 +436,7 @@ export class InsertBuilder<
|
|
|
392
436
|
};
|
|
393
437
|
}
|
|
394
438
|
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
validation.data,
|
|
398
|
-
options,
|
|
399
|
-
);
|
|
400
|
-
|
|
401
|
-
return { data: finalData as any, error: undefined };
|
|
439
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
|
|
440
|
+
return { data: validation.data as any, error: undefined };
|
|
402
441
|
}
|
|
403
442
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import buildQuery, { type QueryOptions } from "odata-query";
|
|
3
|
+
import { FMTable } from "../../orm/table";
|
|
4
|
+
import type { ExpandValidationConfig } from "../../validation";
|
|
5
|
+
import { formatSelectFields } from "../builders/select-utils";
|
|
6
|
+
|
|
7
|
+
const FILTER_QUERY_REGEX = /\$filter=([^&]+)/;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Internal type for expand configuration
|
|
11
|
+
*/
|
|
12
|
+
export interface ExpandConfig {
|
|
13
|
+
relation: string;
|
|
14
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryOptions configuration
|
|
15
|
+
options?: Partial<QueryOptions<any>>;
|
|
16
|
+
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
|
|
17
|
+
targetTable?: FMTable<any, any>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Builds OData expand query strings and validation configs.
|
|
22
|
+
* Handles nested expands recursively and transforms relation names to FMTIDs
|
|
23
|
+
* when using entity IDs.
|
|
24
|
+
*/
|
|
25
|
+
export class ExpandBuilder {
|
|
26
|
+
private readonly useEntityIds: boolean;
|
|
27
|
+
|
|
28
|
+
constructor(useEntityIds: boolean) {
|
|
29
|
+
this.useEntityIds = useEntityIds;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Builds OData expand query string from expand configurations.
|
|
34
|
+
* Handles nested expands recursively.
|
|
35
|
+
* Transforms relation names to FMTIDs if using entity IDs.
|
|
36
|
+
*/
|
|
37
|
+
buildExpandString(configs: ExpandConfig[]): string {
|
|
38
|
+
if (configs.length === 0) {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return configs.map((config) => this.buildSingleExpand(config)).join(",");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Builds a single expand string with its options.
|
|
47
|
+
*/
|
|
48
|
+
private buildSingleExpand(config: ExpandConfig): string {
|
|
49
|
+
// Get target table/occurrence from config (stored during expand call)
|
|
50
|
+
const targetTable = config.targetTable;
|
|
51
|
+
|
|
52
|
+
// When using entity IDs, use the target table's FMTID in the expand parameter
|
|
53
|
+
// FileMaker expects FMTID in $expand when Prefer header is set
|
|
54
|
+
// Only use FMTID if databaseUseEntityIds is enabled
|
|
55
|
+
let relationName = config.relation;
|
|
56
|
+
if (this.useEntityIds && targetTable && FMTable.Symbol.EntityId in targetTable) {
|
|
57
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access
|
|
58
|
+
const tableId = (targetTable as any)[FMTable.Symbol.EntityId] as `FMTID:${string}` | undefined;
|
|
59
|
+
if (tableId) {
|
|
60
|
+
relationName = tableId;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!config.options || Object.keys(config.options).length === 0) {
|
|
65
|
+
// Simple expand without options
|
|
66
|
+
return relationName;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Build query options for this expand
|
|
70
|
+
const parts: string[] = [];
|
|
71
|
+
|
|
72
|
+
if (config.options.select) {
|
|
73
|
+
// Use shared formatSelectFields function for consistent id field quoting
|
|
74
|
+
const selectArray = Array.isArray(config.options.select)
|
|
75
|
+
? config.options.select.map(String)
|
|
76
|
+
: [String(config.options.select)];
|
|
77
|
+
const selectFields = formatSelectFields(selectArray, targetTable, this.useEntityIds);
|
|
78
|
+
parts.push(`$select=${selectFields}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (config.options.filter) {
|
|
82
|
+
// Filter should already be transformed by the nested builder
|
|
83
|
+
// Use odata-query to build filter string
|
|
84
|
+
const filterQuery = buildQuery({ filter: config.options.filter });
|
|
85
|
+
const filterMatch = filterQuery.match(FILTER_QUERY_REGEX);
|
|
86
|
+
if (filterMatch) {
|
|
87
|
+
parts.push(`$filter=${filterMatch[1]}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (config.options.orderBy) {
|
|
92
|
+
// OrderBy should already be transformed by the nested builder
|
|
93
|
+
const orderByValue = Array.isArray(config.options.orderBy)
|
|
94
|
+
? config.options.orderBy.join(",")
|
|
95
|
+
: config.options.orderBy;
|
|
96
|
+
parts.push(`$orderby=${String(orderByValue)}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (config.options.top !== undefined) {
|
|
100
|
+
parts.push(`$top=${config.options.top}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (config.options.skip !== undefined) {
|
|
104
|
+
parts.push(`$skip=${config.options.skip}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Handle nested expands (from expand configs)
|
|
108
|
+
if (config.options.expand && typeof config.options.expand === "string") {
|
|
109
|
+
// If expand is a string, it's already been built
|
|
110
|
+
parts.push(`$expand=${config.options.expand}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (parts.length === 0) {
|
|
114
|
+
return relationName;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return `${relationName}(${parts.join(";")})`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Builds expand validation configs from internal expand configurations.
|
|
122
|
+
* These are used to validate expanded navigation properties.
|
|
123
|
+
*/
|
|
124
|
+
buildValidationConfigs(configs: ExpandConfig[]): ExpandValidationConfig[] {
|
|
125
|
+
return configs.map((config) => {
|
|
126
|
+
// Get target table/occurrence from config (stored during expand call)
|
|
127
|
+
const targetTable = config.targetTable;
|
|
128
|
+
|
|
129
|
+
// Extract schema from target table/occurrence
|
|
130
|
+
let targetSchema: Record<string, StandardSchemaV1> | undefined;
|
|
131
|
+
if (targetTable) {
|
|
132
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access
|
|
133
|
+
const tableSchema = (targetTable as any)[FMTable.Symbol.Schema];
|
|
134
|
+
if (tableSchema) {
|
|
135
|
+
const zodSchema = tableSchema["~standard"]?.schema;
|
|
136
|
+
if (zodSchema && typeof zodSchema === "object" && "shape" in zodSchema) {
|
|
137
|
+
targetSchema = zodSchema.shape as Record<string, StandardSchemaV1>;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Extract selected fields from options
|
|
143
|
+
let selectedFields: string[] | undefined;
|
|
144
|
+
if (config.options?.select) {
|
|
145
|
+
selectedFields = Array.isArray(config.options.select)
|
|
146
|
+
? config.options.select.map((f) => String(f))
|
|
147
|
+
: [String(config.options.select)];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
relation: config.relation,
|
|
152
|
+
targetSchema,
|
|
153
|
+
targetTable,
|
|
154
|
+
table: targetTable, // For transformation
|
|
155
|
+
selectedFields,
|
|
156
|
+
nestedExpands: undefined, // TODO: Handle nested expands if needed
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** biome-ignore-all lint/performance/noBarrelFile: Re-exporting QueryBuilder and types */
|
|
2
|
+
|
|
3
|
+
// Re-export QueryBuilder as the main export
|
|
4
|
+
|
|
5
|
+
// Export ExpandConfig from expand-builder
|
|
6
|
+
export type { ExpandConfig } from "./expand-builder";
|
|
7
|
+
|
|
8
|
+
// Export types
|
|
9
|
+
export type {
|
|
10
|
+
ExpandedRelations,
|
|
11
|
+
QueryReturnType,
|
|
12
|
+
TypeSafeOrderBy,
|
|
13
|
+
} from "./query-builder";
|
|
14
|
+
export { QueryBuilder } from "./query-builder";
|