@proofkit/fmodata 0.1.0-alpha.13 → 0.1.0-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +489 -334
- package/dist/esm/client/batch-builder.d.ts +7 -5
- package/dist/esm/client/batch-builder.js +84 -25
- package/dist/esm/client/batch-builder.js.map +1 -1
- package/dist/esm/client/builders/default-select.d.ts +7 -0
- package/dist/esm/client/builders/default-select.js +42 -0
- package/dist/esm/client/builders/default-select.js.map +1 -0
- package/dist/esm/client/builders/expand-builder.d.ts +43 -0
- package/dist/esm/client/builders/expand-builder.js +173 -0
- package/dist/esm/client/builders/expand-builder.js.map +1 -0
- package/dist/esm/client/builders/index.d.ts +8 -0
- package/dist/esm/client/builders/query-string-builder.d.ts +15 -0
- package/dist/esm/client/builders/query-string-builder.js +25 -0
- package/dist/esm/client/builders/query-string-builder.js.map +1 -0
- package/dist/esm/client/builders/response-processor.d.ts +39 -0
- package/dist/esm/client/builders/response-processor.js +170 -0
- package/dist/esm/client/builders/response-processor.js.map +1 -0
- package/dist/esm/client/builders/select-mixin.d.ts +31 -0
- package/dist/esm/client/builders/select-mixin.js +30 -0
- package/dist/esm/client/builders/select-mixin.js.map +1 -0
- package/dist/esm/client/builders/select-utils.d.ts +8 -0
- package/dist/esm/client/builders/select-utils.js +15 -0
- package/dist/esm/client/builders/select-utils.js.map +1 -0
- package/dist/esm/client/builders/shared-types.d.ts +39 -0
- package/dist/esm/client/builders/table-utils.d.ts +35 -0
- package/dist/esm/client/builders/table-utils.js +45 -0
- package/dist/esm/client/builders/table-utils.js.map +1 -0
- package/dist/esm/client/database.d.ts +3 -22
- package/dist/esm/client/database.js +14 -76
- package/dist/esm/client/database.js.map +1 -1
- package/dist/esm/client/delete-builder.d.ts +12 -19
- package/dist/esm/client/delete-builder.js +26 -26
- package/dist/esm/client/delete-builder.js.map +1 -1
- package/dist/esm/client/entity-set.d.ts +32 -32
- package/dist/esm/client/entity-set.js +92 -69
- package/dist/esm/client/entity-set.js.map +1 -1
- package/dist/esm/client/error-parser.d.ts +12 -0
- package/dist/esm/client/error-parser.js +30 -0
- package/dist/esm/client/error-parser.js.map +1 -0
- package/dist/esm/client/filemaker-odata.d.ts +2 -4
- package/dist/esm/client/filemaker-odata.js +1 -5
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/dist/esm/client/insert-builder.d.ts +9 -12
- package/dist/esm/client/insert-builder.js +70 -24
- package/dist/esm/client/insert-builder.js.map +1 -1
- package/dist/esm/client/query/expand-builder.d.ts +35 -0
- package/dist/esm/client/query/index.d.ts +3 -0
- package/dist/esm/client/query/query-builder.d.ts +133 -0
- package/dist/esm/client/query/query-builder.js +505 -0
- package/dist/esm/client/query/query-builder.js.map +1 -0
- package/dist/esm/client/query/response-processor.d.ts +22 -0
- package/dist/esm/client/query/types.d.ts +52 -0
- package/dist/esm/client/query/url-builder.d.ts +71 -0
- package/dist/esm/client/query/url-builder.js +107 -0
- package/dist/esm/client/query/url-builder.js.map +1 -0
- package/dist/esm/client/query-builder.d.ts +1 -111
- package/dist/esm/client/record-builder.d.ts +56 -64
- package/dist/esm/client/record-builder.js +158 -297
- package/dist/esm/client/record-builder.js.map +1 -1
- package/dist/esm/client/response-processor.d.ts +3 -3
- package/dist/esm/client/update-builder.d.ts +17 -25
- package/dist/esm/client/update-builder.js +56 -30
- package/dist/esm/client/update-builder.js.map +1 -1
- package/dist/esm/errors.d.ts +8 -1
- package/dist/esm/errors.js +17 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +3 -7
- package/dist/esm/index.js +37 -8
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/orm/column.d.ts +45 -0
- package/dist/esm/orm/column.js +59 -0
- package/dist/esm/orm/column.js.map +1 -0
- package/dist/esm/orm/field-builders.d.ts +154 -0
- package/dist/esm/orm/field-builders.js +152 -0
- package/dist/esm/orm/field-builders.js.map +1 -0
- package/dist/esm/orm/index.d.ts +4 -0
- package/dist/esm/orm/operators.d.ts +175 -0
- package/dist/esm/orm/operators.js +221 -0
- package/dist/esm/orm/operators.js.map +1 -0
- package/dist/esm/orm/table.d.ts +341 -0
- package/dist/esm/orm/table.js +211 -0
- package/dist/esm/orm/table.js.map +1 -0
- package/dist/esm/transform.d.ts +20 -21
- package/dist/esm/transform.js +34 -34
- package/dist/esm/transform.js.map +1 -1
- package/dist/esm/types.d.ts +73 -12
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/validation.d.ts +14 -4
- package/dist/esm/validation.js +45 -1
- package/dist/esm/validation.js.map +1 -1
- package/package.json +22 -17
- package/src/client/batch-builder.ts +102 -33
- package/src/client/builders/default-select.ts +69 -0
- package/src/client/builders/expand-builder.ts +236 -0
- package/src/client/builders/index.ts +11 -0
- package/src/client/builders/query-string-builder.ts +41 -0
- package/src/client/builders/response-processor.ts +273 -0
- package/src/client/builders/select-mixin.ts +74 -0
- package/src/client/builders/select-utils.ts +34 -0
- package/src/client/builders/shared-types.ts +41 -0
- package/src/client/builders/table-utils.ts +87 -0
- package/src/client/database.ts +19 -160
- package/src/client/delete-builder.ts +48 -52
- package/src/client/entity-set.ts +227 -302
- package/src/client/error-parser.ts +59 -0
- package/src/client/filemaker-odata.ts +3 -14
- package/src/client/insert-builder.ts +126 -44
- package/src/client/query/expand-builder.ts +164 -0
- package/src/client/query/index.ts +13 -0
- package/src/client/query/query-builder.ts +826 -0
- package/src/client/query/response-processor.ts +244 -0
- package/src/client/query/types.ts +102 -0
- package/src/client/query/url-builder.ts +179 -0
- package/src/client/query-builder.ts +8 -1454
- package/src/client/record-builder.ts +336 -586
- package/src/client/response-processor.ts +4 -5
- package/src/client/update-builder.ts +113 -75
- package/src/errors.ts +22 -1
- package/src/index.ts +58 -5
- package/src/orm/column.ts +78 -0
- package/src/orm/field-builders.ts +296 -0
- package/src/orm/index.ts +60 -0
- package/src/orm/operators.ts +428 -0
- package/src/orm/table.ts +759 -0
- package/src/transform.ts +62 -48
- package/src/types.ts +88 -63
- package/src/validation.ts +76 -4
- package/LICENSE.md +0 -21
- package/dist/esm/client/base-table.d.ts +0 -128
- package/dist/esm/client/base-table.js +0 -57
- package/dist/esm/client/base-table.js.map +0 -1
- package/dist/esm/client/build-occurrences.d.ts +0 -74
- package/dist/esm/client/build-occurrences.js +0 -31
- package/dist/esm/client/build-occurrences.js.map +0 -1
- package/dist/esm/client/query-builder.js +0 -900
- package/dist/esm/client/query-builder.js.map +0 -1
- package/dist/esm/client/table-occurrence.d.ts +0 -86
- package/dist/esm/client/table-occurrence.js +0 -58
- package/dist/esm/client/table-occurrence.js.map +0 -1
- package/src/client/base-table.ts +0 -178
- package/src/client/build-occurrences.ts +0 -155
- package/src/client/query-builder.ts.bak +0 -1457
- package/src/client/table-occurrence.ts +0 -156
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HTTPError,
|
|
3
|
+
ODataError,
|
|
4
|
+
SchemaLockedError,
|
|
5
|
+
FMODataErrorType,
|
|
6
|
+
} from "../errors";
|
|
7
|
+
import { safeJsonParse } from "./sanitize-json";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parses an error response and returns an appropriate error object.
|
|
11
|
+
* This helper is used by builder processResponse methods to handle error responses
|
|
12
|
+
* consistently, particularly important for batch operations where errors need to be
|
|
13
|
+
* properly parsed from the response body.
|
|
14
|
+
*
|
|
15
|
+
* @param response - The Response object (may be from batch or direct request)
|
|
16
|
+
* @param url - The URL that was requested (for error context)
|
|
17
|
+
* @returns An appropriate error object (ODataError, SchemaLockedError, or HTTPError)
|
|
18
|
+
*/
|
|
19
|
+
export async function parseErrorResponse(
|
|
20
|
+
response: Response,
|
|
21
|
+
url: string,
|
|
22
|
+
): Promise<FMODataErrorType> {
|
|
23
|
+
// Try to parse error body if it's JSON
|
|
24
|
+
let errorBody: { error?: { code?: string | number; message?: string } } | undefined;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
28
|
+
errorBody = await safeJsonParse<typeof errorBody>(response);
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Ignore JSON parse errors - we'll fall back to HTTPError
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if it's an OData error response
|
|
35
|
+
if (errorBody?.error) {
|
|
36
|
+
const errorCode = errorBody.error.code;
|
|
37
|
+
const errorMessage = errorBody.error.message || response.statusText;
|
|
38
|
+
|
|
39
|
+
// Check for schema locked error (code 303)
|
|
40
|
+
if (errorCode === "303" || errorCode === 303) {
|
|
41
|
+
return new SchemaLockedError(url, errorMessage, errorBody.error);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return new ODataError(
|
|
45
|
+
url,
|
|
46
|
+
errorMessage,
|
|
47
|
+
String(errorCode),
|
|
48
|
+
errorBody.error,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fall back to generic HTTPError
|
|
53
|
+
return new HTTPError(url, response.status, response.statusText, errorBody);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
ResponseParseError,
|
|
16
16
|
} from "../errors";
|
|
17
17
|
import { Database } from "./database";
|
|
18
|
-
import { TableOccurrence } from "./table-occurrence";
|
|
19
18
|
import { safeJsonParse } from "./sanitize-json";
|
|
20
19
|
import { get } from "es-toolkit/compat";
|
|
21
20
|
|
|
@@ -116,14 +115,7 @@ export class FMServerConnection implements ExecutionContext {
|
|
|
116
115
|
headers,
|
|
117
116
|
};
|
|
118
117
|
|
|
119
|
-
|
|
120
|
-
const resp = url.includes("/$batch")
|
|
121
|
-
? await fetch(fullUrl, {
|
|
122
|
-
method: finalOptions.method,
|
|
123
|
-
headers: finalOptions.headers,
|
|
124
|
-
body: finalOptions.body,
|
|
125
|
-
})
|
|
126
|
-
: await clientToUse(fullUrl, finalOptions);
|
|
118
|
+
const resp = await clientToUse(fullUrl, finalOptions);
|
|
127
119
|
|
|
128
120
|
// Handle HTTP errors
|
|
129
121
|
if (!resp.ok) {
|
|
@@ -261,15 +253,12 @@ export class FMServerConnection implements ExecutionContext {
|
|
|
261
253
|
}
|
|
262
254
|
}
|
|
263
255
|
|
|
264
|
-
database
|
|
265
|
-
const Occurrences extends readonly TableOccurrence<any, any, any, any>[],
|
|
266
|
-
>(
|
|
256
|
+
database(
|
|
267
257
|
name: string,
|
|
268
258
|
config?: {
|
|
269
|
-
occurrences?: Occurrences | undefined;
|
|
270
259
|
useEntityIds?: boolean;
|
|
271
260
|
},
|
|
272
|
-
): Database
|
|
261
|
+
): Database {
|
|
273
262
|
return new Database(name, this, config);
|
|
274
263
|
}
|
|
275
264
|
|
|
@@ -6,53 +6,62 @@ import type {
|
|
|
6
6
|
InferSchemaType,
|
|
7
7
|
ExecuteOptions,
|
|
8
8
|
ConditionallyWithODataAnnotations,
|
|
9
|
+
ExecuteMethodOptions,
|
|
9
10
|
} from "../types";
|
|
10
11
|
import { getAcceptHeader } from "../types";
|
|
11
|
-
import type {
|
|
12
|
-
import {
|
|
12
|
+
import type { FMTable } from "../orm/table";
|
|
13
|
+
import {
|
|
14
|
+
getBaseTableConfig,
|
|
15
|
+
getTableName,
|
|
16
|
+
getTableId as getTableIdHelper,
|
|
17
|
+
isUsingEntityIds,
|
|
18
|
+
} from "../orm/table";
|
|
19
|
+
import {
|
|
20
|
+
validateSingleResponse,
|
|
21
|
+
validateAndTransformInput,
|
|
22
|
+
} from "../validation";
|
|
13
23
|
import { type FFetchOptions } from "@fetchkit/ffetch";
|
|
14
24
|
import {
|
|
15
25
|
transformFieldNamesToIds,
|
|
16
|
-
transformTableName,
|
|
17
26
|
transformResponseFields,
|
|
18
|
-
getTableIdentifiers,
|
|
19
27
|
} from "../transform";
|
|
20
28
|
import { InvalidLocationHeaderError } from "../errors";
|
|
21
29
|
import { safeJsonParse } from "./sanitize-json";
|
|
30
|
+
import { parseErrorResponse } from "./error-parser";
|
|
22
31
|
|
|
23
32
|
export type InsertOptions = {
|
|
24
33
|
return?: "minimal" | "representation";
|
|
25
34
|
};
|
|
26
35
|
|
|
36
|
+
import type { InferSchemaOutputFromFMTable } from "../orm/table";
|
|
37
|
+
|
|
27
38
|
export class InsertBuilder<
|
|
28
|
-
|
|
29
|
-
Occ extends TableOccurrence<any, any, any, any> | undefined = undefined,
|
|
39
|
+
Occ extends FMTable<any, any> | undefined = undefined,
|
|
30
40
|
ReturnPreference extends "minimal" | "representation" = "representation",
|
|
31
41
|
> implements
|
|
32
42
|
ExecutableBuilder<
|
|
33
|
-
ReturnPreference extends "minimal"
|
|
43
|
+
ReturnPreference extends "minimal"
|
|
44
|
+
? { ROWID: number }
|
|
45
|
+
: InferSchemaOutputFromFMTable<NonNullable<Occ>>
|
|
34
46
|
>
|
|
35
47
|
{
|
|
36
|
-
private
|
|
37
|
-
private tableName: string;
|
|
48
|
+
private table?: Occ;
|
|
38
49
|
private databaseName: string;
|
|
39
50
|
private context: ExecutionContext;
|
|
40
|
-
private data: Partial<
|
|
51
|
+
private data: Partial<InferSchemaOutputFromFMTable<NonNullable<Occ>>>;
|
|
41
52
|
private returnPreference: ReturnPreference;
|
|
42
53
|
|
|
43
54
|
private databaseUseEntityIds: boolean;
|
|
44
55
|
|
|
45
56
|
constructor(config: {
|
|
46
57
|
occurrence?: Occ;
|
|
47
|
-
tableName: string;
|
|
48
58
|
databaseName: string;
|
|
49
59
|
context: ExecutionContext;
|
|
50
|
-
data: Partial<
|
|
60
|
+
data: Partial<InferSchemaOutputFromFMTable<NonNullable<Occ>>>;
|
|
51
61
|
returnPreference?: ReturnPreference;
|
|
52
62
|
databaseUseEntityIds?: boolean;
|
|
53
63
|
}) {
|
|
54
|
-
this.
|
|
55
|
-
this.tableName = config.tableName;
|
|
64
|
+
this.table = config.occurrence;
|
|
56
65
|
this.databaseName = config.databaseName;
|
|
57
66
|
this.context = config.context;
|
|
58
67
|
this.data = config.data;
|
|
@@ -74,7 +83,6 @@ export class InsertBuilder<
|
|
|
74
83
|
};
|
|
75
84
|
}
|
|
76
85
|
|
|
77
|
-
|
|
78
86
|
/**
|
|
79
87
|
* Parse ROWID from Location header
|
|
80
88
|
* Expected formats:
|
|
@@ -115,52 +123,69 @@ export class InsertBuilder<
|
|
|
115
123
|
* @param useEntityIds - Optional override for entity ID usage
|
|
116
124
|
*/
|
|
117
125
|
private getTableId(useEntityIds?: boolean): string {
|
|
118
|
-
if (!this.
|
|
119
|
-
|
|
126
|
+
if (!this.table) {
|
|
127
|
+
throw new Error("Table occurrence is required");
|
|
120
128
|
}
|
|
121
129
|
|
|
122
130
|
const contextDefault = this.context._getUseEntityIds?.() ?? false;
|
|
123
131
|
const shouldUseIds = useEntityIds ?? contextDefault;
|
|
124
132
|
|
|
125
133
|
if (shouldUseIds) {
|
|
126
|
-
|
|
127
|
-
if (!identifiers.id) {
|
|
134
|
+
if (!isUsingEntityIds(this.table)) {
|
|
128
135
|
throw new Error(
|
|
129
|
-
`useEntityIds is true but
|
|
136
|
+
`useEntityIds is true but table "${getTableName(this.table)}" does not have entity IDs configured`,
|
|
130
137
|
);
|
|
131
138
|
}
|
|
132
|
-
return
|
|
139
|
+
return getTableIdHelper(this.table);
|
|
133
140
|
}
|
|
134
141
|
|
|
135
|
-
return this.
|
|
142
|
+
return getTableName(this.table);
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
async execute<EO extends ExecuteOptions>(
|
|
139
|
-
options?:
|
|
146
|
+
options?: ExecuteMethodOptions<EO>,
|
|
140
147
|
): Promise<
|
|
141
148
|
Result<
|
|
142
149
|
ReturnPreference extends "minimal"
|
|
143
150
|
? { ROWID: number }
|
|
144
151
|
: ConditionallyWithODataAnnotations<
|
|
145
|
-
|
|
152
|
+
InferSchemaOutputFromFMTable<NonNullable<Occ>>,
|
|
146
153
|
EO["includeODataAnnotations"] extends true ? true : false
|
|
147
154
|
>
|
|
148
155
|
>
|
|
149
156
|
> {
|
|
150
157
|
// Merge database-level useEntityIds with per-request options
|
|
151
158
|
const mergedOptions = this.mergeExecuteOptions(options);
|
|
152
|
-
|
|
159
|
+
|
|
153
160
|
// Get table identifier with override support
|
|
154
161
|
const tableId = this.getTableId(mergedOptions.useEntityIds);
|
|
155
162
|
const url = `/${this.databaseName}/${tableId}`;
|
|
156
163
|
|
|
164
|
+
// Validate and transform input data using input validators (writeValidators)
|
|
165
|
+
let validatedData = this.data;
|
|
166
|
+
if (this.table) {
|
|
167
|
+
const baseTableConfig = getBaseTableConfig(this.table);
|
|
168
|
+
const inputSchema = baseTableConfig.inputSchema;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
validatedData = await validateAndTransformInput(this.data, inputSchema);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
// If validation fails, return error immediately
|
|
174
|
+
return {
|
|
175
|
+
data: undefined,
|
|
176
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
177
|
+
} as any;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
157
181
|
// Transform field names to FMFIDs if using entity IDs
|
|
158
182
|
// Only transform if useEntityIds resolves to true (respects per-request override)
|
|
159
183
|
const shouldUseIds = mergedOptions.useEntityIds ?? false;
|
|
160
|
-
|
|
161
|
-
const transformedData =
|
|
162
|
-
|
|
163
|
-
|
|
184
|
+
|
|
185
|
+
const transformedData =
|
|
186
|
+
this.table && shouldUseIds
|
|
187
|
+
? transformFieldNamesToIds(validatedData, this.table)
|
|
188
|
+
: validatedData;
|
|
164
189
|
|
|
165
190
|
// Set Prefer header based on return preference
|
|
166
191
|
const preferHeader =
|
|
@@ -204,19 +229,31 @@ export class InsertBuilder<
|
|
|
204
229
|
|
|
205
230
|
// Transform response field IDs back to names if using entity IDs
|
|
206
231
|
// Only transform if useEntityIds resolves to true (respects per-request override)
|
|
207
|
-
if (this.
|
|
232
|
+
if (this.table && shouldUseIds) {
|
|
208
233
|
response = transformResponseFields(
|
|
209
234
|
response,
|
|
210
|
-
this.
|
|
235
|
+
this.table,
|
|
211
236
|
undefined, // No expand configs for insert
|
|
212
237
|
);
|
|
213
238
|
}
|
|
214
239
|
|
|
215
|
-
// Get schema from
|
|
216
|
-
|
|
240
|
+
// Get schema from table if available, excluding container fields
|
|
241
|
+
let schema: Record<string, any> | undefined;
|
|
242
|
+
if (this.table) {
|
|
243
|
+
const baseTableConfig = getBaseTableConfig(this.table);
|
|
244
|
+
const containerFields = baseTableConfig.containerFields || [];
|
|
245
|
+
|
|
246
|
+
// Filter out container fields from schema
|
|
247
|
+
schema = { ...baseTableConfig.schema };
|
|
248
|
+
for (const containerField of containerFields) {
|
|
249
|
+
delete schema[containerField as string];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
217
252
|
|
|
218
253
|
// Validate the response (FileMaker returns the created record)
|
|
219
|
-
const validation = await validateSingleResponse<
|
|
254
|
+
const validation = await validateSingleResponse<
|
|
255
|
+
InferSchemaOutputFromFMTable<NonNullable<Occ>>
|
|
256
|
+
>(
|
|
220
257
|
response,
|
|
221
258
|
schema,
|
|
222
259
|
undefined, // No selected fields for insert
|
|
@@ -241,12 +278,14 @@ export class InsertBuilder<
|
|
|
241
278
|
|
|
242
279
|
getRequestConfig(): { method: string; url: string; body?: any } {
|
|
243
280
|
// For batch operations, use database-level setting (no per-request override available here)
|
|
281
|
+
// Note: Input validation happens in execute() and processResponse() for batch operations
|
|
244
282
|
const tableId = this.getTableId(this.databaseUseEntityIds);
|
|
245
283
|
|
|
246
284
|
// Transform field names to FMFIDs if using entity IDs
|
|
247
|
-
const transformedData =
|
|
248
|
-
|
|
249
|
-
|
|
285
|
+
const transformedData =
|
|
286
|
+
this.table && this.databaseUseEntityIds
|
|
287
|
+
? transformFieldNamesToIds(this.data, this.table)
|
|
288
|
+
: this.data;
|
|
250
289
|
|
|
251
290
|
return {
|
|
252
291
|
method: "POST",
|
|
@@ -280,8 +319,22 @@ export class InsertBuilder<
|
|
|
280
319
|
response: Response,
|
|
281
320
|
options?: ExecuteOptions,
|
|
282
321
|
): Promise<
|
|
283
|
-
Result<
|
|
322
|
+
Result<
|
|
323
|
+
ReturnPreference extends "minimal"
|
|
324
|
+
? { ROWID: number }
|
|
325
|
+
: InferSchemaOutputFromFMTable<NonNullable<Occ>>
|
|
326
|
+
>
|
|
284
327
|
> {
|
|
328
|
+
// Check for error responses (important for batch operations)
|
|
329
|
+
if (!response.ok) {
|
|
330
|
+
const tableName = this.table ? getTableName(this.table) : "unknown";
|
|
331
|
+
const error = await parseErrorResponse(
|
|
332
|
+
response,
|
|
333
|
+
response.url || `/${this.databaseName}/${tableName}`,
|
|
334
|
+
);
|
|
335
|
+
return { data: undefined, error };
|
|
336
|
+
}
|
|
337
|
+
|
|
285
338
|
// Handle 204 No Content (common in batch/changeset operations)
|
|
286
339
|
// FileMaker uses return=minimal for changeset operations regardless of Prefer header
|
|
287
340
|
if (response.status === 204) {
|
|
@@ -336,24 +389,53 @@ export class InsertBuilder<
|
|
|
336
389
|
};
|
|
337
390
|
}
|
|
338
391
|
|
|
392
|
+
// Validate and transform input data using input validators (writeValidators)
|
|
393
|
+
// This is needed for processResponse because it's called from batch operations
|
|
394
|
+
// where the data hasn't been validated yet
|
|
395
|
+
let validatedData = this.data;
|
|
396
|
+
if (this.table) {
|
|
397
|
+
const baseTableConfig = getBaseTableConfig(this.table);
|
|
398
|
+
const inputSchema = baseTableConfig.inputSchema;
|
|
399
|
+
try {
|
|
400
|
+
validatedData = await validateAndTransformInput(this.data, inputSchema);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
return {
|
|
403
|
+
data: undefined,
|
|
404
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
405
|
+
} as any;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
339
409
|
// Transform response field IDs back to names if using entity IDs
|
|
340
410
|
// Only transform if useEntityIds resolves to true (respects per-request override)
|
|
341
411
|
const shouldUseIds = options?.useEntityIds ?? this.databaseUseEntityIds;
|
|
342
|
-
|
|
412
|
+
|
|
343
413
|
let transformedResponse = rawResponse;
|
|
344
|
-
if (this.
|
|
414
|
+
if (this.table && shouldUseIds) {
|
|
345
415
|
transformedResponse = transformResponseFields(
|
|
346
416
|
rawResponse,
|
|
347
|
-
this.
|
|
417
|
+
this.table,
|
|
348
418
|
undefined, // No expand configs for insert
|
|
349
419
|
);
|
|
350
420
|
}
|
|
351
421
|
|
|
352
|
-
// Get schema from
|
|
353
|
-
|
|
422
|
+
// Get schema from table if available, excluding container fields
|
|
423
|
+
let schema: Record<string, any> | undefined;
|
|
424
|
+
if (this.table) {
|
|
425
|
+
const baseTableConfig = getBaseTableConfig(this.table);
|
|
426
|
+
const containerFields = baseTableConfig.containerFields || [];
|
|
427
|
+
|
|
428
|
+
// Filter out container fields from schema
|
|
429
|
+
schema = { ...baseTableConfig.schema };
|
|
430
|
+
for (const containerField of containerFields) {
|
|
431
|
+
delete schema[containerField as string];
|
|
432
|
+
}
|
|
433
|
+
}
|
|
354
434
|
|
|
355
435
|
// Validate the response (FileMaker returns the created record)
|
|
356
|
-
const validation = await validateSingleResponse<
|
|
436
|
+
const validation = await validateSingleResponse<
|
|
437
|
+
InferSchemaOutputFromFMTable<NonNullable<Occ>>
|
|
438
|
+
>(
|
|
357
439
|
transformedResponse,
|
|
358
440
|
schema,
|
|
359
441
|
undefined, // No selected fields for insert
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { QueryOptions } from "odata-query";
|
|
2
|
+
import buildQuery from "odata-query";
|
|
3
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
4
|
+
import { FMTable } from "../../orm/table";
|
|
5
|
+
import type { ExpandValidationConfig } from "../../validation";
|
|
6
|
+
import { formatSelectFields } from "../builders/select-utils";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Internal type for expand configuration
|
|
10
|
+
*/
|
|
11
|
+
export type ExpandConfig = {
|
|
12
|
+
relation: string;
|
|
13
|
+
options?: Partial<QueryOptions<any>>;
|
|
14
|
+
targetTable?: FMTable<any, any>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Builds OData expand query strings and validation configs.
|
|
19
|
+
* Handles nested expands recursively and transforms relation names to FMTIDs
|
|
20
|
+
* when using entity IDs.
|
|
21
|
+
*/
|
|
22
|
+
export class ExpandBuilder {
|
|
23
|
+
constructor(private useEntityIds: boolean) {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Builds OData expand query string from expand configurations.
|
|
27
|
+
* Handles nested expands recursively.
|
|
28
|
+
* Transforms relation names to FMTIDs if using entity IDs.
|
|
29
|
+
*/
|
|
30
|
+
buildExpandString(configs: ExpandConfig[]): string {
|
|
31
|
+
if (configs.length === 0) {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return configs.map((config) => this.buildSingleExpand(config)).join(",");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Builds a single expand string with its options.
|
|
40
|
+
*/
|
|
41
|
+
private buildSingleExpand(config: ExpandConfig): string {
|
|
42
|
+
// Get target table/occurrence from config (stored during expand call)
|
|
43
|
+
const targetTable = config.targetTable;
|
|
44
|
+
|
|
45
|
+
// When using entity IDs, use the target table's FMTID in the expand parameter
|
|
46
|
+
// FileMaker expects FMTID in $expand when Prefer header is set
|
|
47
|
+
// Only use FMTID if databaseUseEntityIds is enabled
|
|
48
|
+
let relationName = config.relation;
|
|
49
|
+
if (this.useEntityIds) {
|
|
50
|
+
if (targetTable && FMTable.Symbol.EntityId in targetTable) {
|
|
51
|
+
const tableId = (targetTable as any)[FMTable.Symbol.EntityId] as
|
|
52
|
+
| `FMTID:${string}`
|
|
53
|
+
| undefined;
|
|
54
|
+
if (tableId) {
|
|
55
|
+
relationName = tableId;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!config.options || Object.keys(config.options).length === 0) {
|
|
61
|
+
// Simple expand without options
|
|
62
|
+
return relationName;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Build query options for this expand
|
|
66
|
+
const parts: string[] = [];
|
|
67
|
+
|
|
68
|
+
if (config.options.select) {
|
|
69
|
+
// Use shared formatSelectFields function for consistent id field quoting
|
|
70
|
+
const selectArray = Array.isArray(config.options.select)
|
|
71
|
+
? config.options.select.map(String)
|
|
72
|
+
: [String(config.options.select)];
|
|
73
|
+
const selectFields = formatSelectFields(
|
|
74
|
+
selectArray,
|
|
75
|
+
targetTable,
|
|
76
|
+
this.useEntityIds,
|
|
77
|
+
);
|
|
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=([^&]+)/);
|
|
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) {
|
|
109
|
+
// If expand is a string, it's already been built
|
|
110
|
+
if (typeof config.options.expand === "string") {
|
|
111
|
+
parts.push(`$expand=${config.options.expand}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (parts.length === 0) {
|
|
116
|
+
return relationName;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return `${relationName}(${parts.join(";")})`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Builds expand validation configs from internal expand configurations.
|
|
124
|
+
* These are used to validate expanded navigation properties.
|
|
125
|
+
*/
|
|
126
|
+
buildValidationConfigs(configs: ExpandConfig[]): ExpandValidationConfig[] {
|
|
127
|
+
return configs.map((config) => {
|
|
128
|
+
// Get target table/occurrence from config (stored during expand call)
|
|
129
|
+
const targetTable = config.targetTable;
|
|
130
|
+
|
|
131
|
+
// Extract schema from target table/occurrence
|
|
132
|
+
let targetSchema: Record<string, StandardSchemaV1> | undefined;
|
|
133
|
+
if (targetTable) {
|
|
134
|
+
const tableSchema = (targetTable as any)[FMTable.Symbol.Schema];
|
|
135
|
+
if (tableSchema) {
|
|
136
|
+
const zodSchema = tableSchema["~standard"]?.schema;
|
|
137
|
+
if (
|
|
138
|
+
zodSchema &&
|
|
139
|
+
typeof zodSchema === "object" &&
|
|
140
|
+
"shape" in zodSchema
|
|
141
|
+
) {
|
|
142
|
+
targetSchema = zodSchema.shape as Record<string, StandardSchemaV1>;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Extract selected fields from options
|
|
148
|
+
const selectedFields = config.options?.select
|
|
149
|
+
? Array.isArray(config.options.select)
|
|
150
|
+
? config.options.select.map((f) => String(f))
|
|
151
|
+
: [String(config.options.select)]
|
|
152
|
+
: undefined;
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
relation: config.relation,
|
|
156
|
+
targetSchema: targetSchema,
|
|
157
|
+
targetTable: targetTable,
|
|
158
|
+
table: targetTable, // For transformation
|
|
159
|
+
selectedFields: selectedFields,
|
|
160
|
+
nestedExpands: undefined, // TODO: Handle nested expands if needed
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Re-export QueryBuilder as the main export
|
|
2
|
+
export { QueryBuilder } from "./query-builder";
|
|
3
|
+
|
|
4
|
+
// Export types
|
|
5
|
+
export type {
|
|
6
|
+
TypeSafeOrderBy,
|
|
7
|
+
ExpandedRelations,
|
|
8
|
+
QueryReturnType,
|
|
9
|
+
} from "./query-builder";
|
|
10
|
+
|
|
11
|
+
// Export ExpandConfig from expand-builder
|
|
12
|
+
export type { ExpandConfig } from "./expand-builder";
|
|
13
|
+
|