@proofkit/fmodata 0.1.0-alpha.13 → 0.1.0-alpha.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +489 -334
- package/dist/esm/client/batch-builder.d.ts +7 -4
- package/dist/esm/client/batch-builder.js +84 -25
- package/dist/esm/client/batch-builder.js.map +1 -1
- package/dist/esm/client/builders/default-select.d.ts +7 -0
- package/dist/esm/client/builders/default-select.js +42 -0
- package/dist/esm/client/builders/default-select.js.map +1 -0
- package/dist/esm/client/builders/expand-builder.d.ts +43 -0
- package/dist/esm/client/builders/expand-builder.js +173 -0
- package/dist/esm/client/builders/expand-builder.js.map +1 -0
- package/dist/esm/client/builders/index.d.ts +8 -0
- package/dist/esm/client/builders/query-string-builder.d.ts +15 -0
- package/dist/esm/client/builders/query-string-builder.js +25 -0
- package/dist/esm/client/builders/query-string-builder.js.map +1 -0
- package/dist/esm/client/builders/response-processor.d.ts +39 -0
- package/dist/esm/client/builders/response-processor.js +170 -0
- package/dist/esm/client/builders/response-processor.js.map +1 -0
- package/dist/esm/client/builders/select-mixin.d.ts +31 -0
- package/dist/esm/client/builders/select-mixin.js +30 -0
- package/dist/esm/client/builders/select-mixin.js.map +1 -0
- package/dist/esm/client/builders/select-utils.d.ts +8 -0
- package/dist/esm/client/builders/select-utils.js +15 -0
- package/dist/esm/client/builders/select-utils.js.map +1 -0
- package/dist/esm/client/builders/shared-types.d.ts +39 -0
- package/dist/esm/client/builders/table-utils.d.ts +35 -0
- package/dist/esm/client/builders/table-utils.js +45 -0
- package/dist/esm/client/builders/table-utils.js.map +1 -0
- package/dist/esm/client/database.d.ts +3 -22
- package/dist/esm/client/database.js +14 -76
- package/dist/esm/client/database.js.map +1 -1
- package/dist/esm/client/delete-builder.d.ts +11 -15
- package/dist/esm/client/delete-builder.js +26 -26
- package/dist/esm/client/delete-builder.js.map +1 -1
- package/dist/esm/client/entity-set.d.ts +32 -32
- package/dist/esm/client/entity-set.js +92 -69
- package/dist/esm/client/entity-set.js.map +1 -1
- package/dist/esm/client/error-parser.d.ts +12 -0
- package/dist/esm/client/error-parser.js +30 -0
- package/dist/esm/client/error-parser.js.map +1 -0
- package/dist/esm/client/filemaker-odata.d.ts +2 -4
- package/dist/esm/client/filemaker-odata.js +1 -5
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/dist/esm/client/insert-builder.d.ts +7 -9
- package/dist/esm/client/insert-builder.js +70 -24
- package/dist/esm/client/insert-builder.js.map +1 -1
- package/dist/esm/client/query/expand-builder.d.ts +35 -0
- package/dist/esm/client/query/index.d.ts +3 -0
- package/dist/esm/client/query/query-builder.d.ts +134 -0
- package/dist/esm/client/query/query-builder.js +505 -0
- package/dist/esm/client/query/query-builder.js.map +1 -0
- package/dist/esm/client/query/response-processor.d.ts +22 -0
- package/dist/esm/client/query/types.d.ts +52 -0
- package/dist/esm/client/query/url-builder.d.ts +71 -0
- package/dist/esm/client/query/url-builder.js +107 -0
- package/dist/esm/client/query/url-builder.js.map +1 -0
- package/dist/esm/client/query-builder.d.ts +1 -111
- package/dist/esm/client/record-builder.d.ts +56 -63
- package/dist/esm/client/record-builder.js +158 -297
- package/dist/esm/client/record-builder.js.map +1 -1
- package/dist/esm/client/response-processor.d.ts +3 -3
- package/dist/esm/client/update-builder.d.ts +16 -21
- package/dist/esm/client/update-builder.js +56 -30
- package/dist/esm/client/update-builder.js.map +1 -1
- package/dist/esm/errors.d.ts +8 -1
- package/dist/esm/errors.js +17 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +3 -7
- package/dist/esm/index.js +37 -8
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/orm/column.d.ts +45 -0
- package/dist/esm/orm/column.js +59 -0
- package/dist/esm/orm/column.js.map +1 -0
- package/dist/esm/orm/field-builders.d.ts +154 -0
- package/dist/esm/orm/field-builders.js +152 -0
- package/dist/esm/orm/field-builders.js.map +1 -0
- package/dist/esm/orm/index.d.ts +4 -0
- package/dist/esm/orm/operators.d.ts +175 -0
- package/dist/esm/orm/operators.js +221 -0
- package/dist/esm/orm/operators.js.map +1 -0
- package/dist/esm/orm/table.d.ts +341 -0
- package/dist/esm/orm/table.js +211 -0
- package/dist/esm/orm/table.js.map +1 -0
- package/dist/esm/transform.d.ts +20 -21
- package/dist/esm/transform.js +34 -34
- package/dist/esm/transform.js.map +1 -1
- package/dist/esm/types.d.ts +16 -13
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/validation.d.ts +14 -4
- package/dist/esm/validation.js +45 -1
- package/dist/esm/validation.js.map +1 -1
- package/package.json +20 -17
- package/src/client/batch-builder.ts +100 -32
- package/src/client/builders/default-select.ts +69 -0
- package/src/client/builders/expand-builder.ts +236 -0
- package/src/client/builders/index.ts +11 -0
- package/src/client/builders/query-string-builder.ts +41 -0
- package/src/client/builders/response-processor.ts +273 -0
- package/src/client/builders/select-mixin.ts +74 -0
- package/src/client/builders/select-utils.ts +34 -0
- package/src/client/builders/shared-types.ts +41 -0
- package/src/client/builders/table-utils.ts +87 -0
- package/src/client/database.ts +19 -160
- package/src/client/delete-builder.ts +46 -51
- package/src/client/entity-set.ts +227 -302
- package/src/client/error-parser.ts +59 -0
- package/src/client/filemaker-odata.ts +3 -14
- package/src/client/insert-builder.ts +124 -43
- package/src/client/query/expand-builder.ts +164 -0
- package/src/client/query/index.ts +13 -0
- package/src/client/query/query-builder.ts +816 -0
- package/src/client/query/response-processor.ts +244 -0
- package/src/client/query/types.ts +102 -0
- package/src/client/query/url-builder.ts +179 -0
- package/src/client/query-builder.ts +8 -1454
- package/src/client/record-builder.ts +325 -585
- package/src/client/response-processor.ts +4 -5
- package/src/client/update-builder.ts +102 -73
- package/src/errors.ts +22 -1
- package/src/index.ts +55 -5
- package/src/orm/column.ts +78 -0
- package/src/orm/field-builders.ts +296 -0
- package/src/orm/index.ts +60 -0
- package/src/orm/operators.ts +428 -0
- package/src/orm/table.ts +759 -0
- package/src/transform.ts +62 -48
- package/src/types.ts +20 -63
- package/src/validation.ts +76 -4
- package/LICENSE.md +0 -21
- package/dist/esm/client/base-table.d.ts +0 -128
- package/dist/esm/client/base-table.js +0 -57
- package/dist/esm/client/base-table.js.map +0 -1
- package/dist/esm/client/build-occurrences.d.ts +0 -74
- package/dist/esm/client/build-occurrences.js +0 -31
- package/dist/esm/client/build-occurrences.js.map +0 -1
- package/dist/esm/client/query-builder.js +0 -900
- package/dist/esm/client/query-builder.js.map +0 -1
- package/dist/esm/client/table-occurrence.d.ts +0 -86
- package/dist/esm/client/table-occurrence.js +0 -58
- package/dist/esm/client/table-occurrence.js.map +0 -1
- package/src/client/base-table.ts +0 -178
- package/src/client/build-occurrences.ts +0 -155
- package/src/client/query-builder.ts.bak +0 -1457
- package/src/client/table-occurrence.ts +0 -156
|
@@ -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
|
|
|
@@ -8,51 +8,59 @@ import type {
|
|
|
8
8
|
ConditionallyWithODataAnnotations,
|
|
9
9
|
} from "../types";
|
|
10
10
|
import { getAcceptHeader } from "../types";
|
|
11
|
-
import type {
|
|
12
|
-
import {
|
|
11
|
+
import type { FMTable } from "../orm/table";
|
|
12
|
+
import {
|
|
13
|
+
getBaseTableConfig,
|
|
14
|
+
getTableName,
|
|
15
|
+
getTableId as getTableIdHelper,
|
|
16
|
+
isUsingEntityIds,
|
|
17
|
+
} from "../orm/table";
|
|
18
|
+
import {
|
|
19
|
+
validateSingleResponse,
|
|
20
|
+
validateAndTransformInput,
|
|
21
|
+
} from "../validation";
|
|
13
22
|
import { type FFetchOptions } from "@fetchkit/ffetch";
|
|
14
23
|
import {
|
|
15
24
|
transformFieldNamesToIds,
|
|
16
|
-
transformTableName,
|
|
17
25
|
transformResponseFields,
|
|
18
|
-
getTableIdentifiers,
|
|
19
26
|
} from "../transform";
|
|
20
27
|
import { InvalidLocationHeaderError } from "../errors";
|
|
21
28
|
import { safeJsonParse } from "./sanitize-json";
|
|
29
|
+
import { parseErrorResponse } from "./error-parser";
|
|
22
30
|
|
|
23
31
|
export type InsertOptions = {
|
|
24
32
|
return?: "minimal" | "representation";
|
|
25
33
|
};
|
|
26
34
|
|
|
35
|
+
import type { InferSchemaOutputFromFMTable } from "../orm/table";
|
|
36
|
+
|
|
27
37
|
export class InsertBuilder<
|
|
28
|
-
|
|
29
|
-
Occ extends TableOccurrence<any, any, any, any> | undefined = undefined,
|
|
38
|
+
Occ extends FMTable<any, any> | undefined = undefined,
|
|
30
39
|
ReturnPreference extends "minimal" | "representation" = "representation",
|
|
31
40
|
> implements
|
|
32
41
|
ExecutableBuilder<
|
|
33
|
-
ReturnPreference extends "minimal"
|
|
42
|
+
ReturnPreference extends "minimal"
|
|
43
|
+
? { ROWID: number }
|
|
44
|
+
: InferSchemaOutputFromFMTable<NonNullable<Occ>>
|
|
34
45
|
>
|
|
35
46
|
{
|
|
36
|
-
private
|
|
37
|
-
private tableName: string;
|
|
47
|
+
private table?: Occ;
|
|
38
48
|
private databaseName: string;
|
|
39
49
|
private context: ExecutionContext;
|
|
40
|
-
private data: Partial<
|
|
50
|
+
private data: Partial<InferSchemaOutputFromFMTable<NonNullable<Occ>>>;
|
|
41
51
|
private returnPreference: ReturnPreference;
|
|
42
52
|
|
|
43
53
|
private databaseUseEntityIds: boolean;
|
|
44
54
|
|
|
45
55
|
constructor(config: {
|
|
46
56
|
occurrence?: Occ;
|
|
47
|
-
tableName: string;
|
|
48
57
|
databaseName: string;
|
|
49
58
|
context: ExecutionContext;
|
|
50
|
-
data: Partial<
|
|
59
|
+
data: Partial<InferSchemaOutputFromFMTable<NonNullable<Occ>>>;
|
|
51
60
|
returnPreference?: ReturnPreference;
|
|
52
61
|
databaseUseEntityIds?: boolean;
|
|
53
62
|
}) {
|
|
54
|
-
this.
|
|
55
|
-
this.tableName = config.tableName;
|
|
63
|
+
this.table = config.occurrence;
|
|
56
64
|
this.databaseName = config.databaseName;
|
|
57
65
|
this.context = config.context;
|
|
58
66
|
this.data = config.data;
|
|
@@ -74,7 +82,6 @@ export class InsertBuilder<
|
|
|
74
82
|
};
|
|
75
83
|
}
|
|
76
84
|
|
|
77
|
-
|
|
78
85
|
/**
|
|
79
86
|
* Parse ROWID from Location header
|
|
80
87
|
* Expected formats:
|
|
@@ -115,24 +122,23 @@ export class InsertBuilder<
|
|
|
115
122
|
* @param useEntityIds - Optional override for entity ID usage
|
|
116
123
|
*/
|
|
117
124
|
private getTableId(useEntityIds?: boolean): string {
|
|
118
|
-
if (!this.
|
|
119
|
-
|
|
125
|
+
if (!this.table) {
|
|
126
|
+
throw new Error("Table occurrence is required");
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
const contextDefault = this.context._getUseEntityIds?.() ?? false;
|
|
123
130
|
const shouldUseIds = useEntityIds ?? contextDefault;
|
|
124
131
|
|
|
125
132
|
if (shouldUseIds) {
|
|
126
|
-
|
|
127
|
-
if (!identifiers.id) {
|
|
133
|
+
if (!isUsingEntityIds(this.table)) {
|
|
128
134
|
throw new Error(
|
|
129
|
-
`useEntityIds is true but
|
|
135
|
+
`useEntityIds is true but table "${getTableName(this.table)}" does not have entity IDs configured`,
|
|
130
136
|
);
|
|
131
137
|
}
|
|
132
|
-
return
|
|
138
|
+
return getTableIdHelper(this.table);
|
|
133
139
|
}
|
|
134
140
|
|
|
135
|
-
return this.
|
|
141
|
+
return getTableName(this.table);
|
|
136
142
|
}
|
|
137
143
|
|
|
138
144
|
async execute<EO extends ExecuteOptions>(
|
|
@@ -142,25 +148,43 @@ export class InsertBuilder<
|
|
|
142
148
|
ReturnPreference extends "minimal"
|
|
143
149
|
? { ROWID: number }
|
|
144
150
|
: ConditionallyWithODataAnnotations<
|
|
145
|
-
|
|
151
|
+
InferSchemaOutputFromFMTable<NonNullable<Occ>>,
|
|
146
152
|
EO["includeODataAnnotations"] extends true ? true : false
|
|
147
153
|
>
|
|
148
154
|
>
|
|
149
155
|
> {
|
|
150
156
|
// Merge database-level useEntityIds with per-request options
|
|
151
157
|
const mergedOptions = this.mergeExecuteOptions(options);
|
|
152
|
-
|
|
158
|
+
|
|
153
159
|
// Get table identifier with override support
|
|
154
160
|
const tableId = this.getTableId(mergedOptions.useEntityIds);
|
|
155
161
|
const url = `/${this.databaseName}/${tableId}`;
|
|
156
162
|
|
|
163
|
+
// Validate and transform input data using input validators (writeValidators)
|
|
164
|
+
let validatedData = this.data;
|
|
165
|
+
if (this.table) {
|
|
166
|
+
const baseTableConfig = getBaseTableConfig(this.table);
|
|
167
|
+
const inputSchema = baseTableConfig.inputSchema;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
validatedData = await validateAndTransformInput(this.data, inputSchema);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
// If validation fails, return error immediately
|
|
173
|
+
return {
|
|
174
|
+
data: undefined,
|
|
175
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
176
|
+
} as any;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
157
180
|
// Transform field names to FMFIDs if using entity IDs
|
|
158
181
|
// Only transform if useEntityIds resolves to true (respects per-request override)
|
|
159
182
|
const shouldUseIds = mergedOptions.useEntityIds ?? false;
|
|
160
|
-
|
|
161
|
-
const transformedData =
|
|
162
|
-
|
|
163
|
-
|
|
183
|
+
|
|
184
|
+
const transformedData =
|
|
185
|
+
this.table && shouldUseIds
|
|
186
|
+
? transformFieldNamesToIds(validatedData, this.table)
|
|
187
|
+
: validatedData;
|
|
164
188
|
|
|
165
189
|
// Set Prefer header based on return preference
|
|
166
190
|
const preferHeader =
|
|
@@ -204,19 +228,31 @@ export class InsertBuilder<
|
|
|
204
228
|
|
|
205
229
|
// Transform response field IDs back to names if using entity IDs
|
|
206
230
|
// Only transform if useEntityIds resolves to true (respects per-request override)
|
|
207
|
-
if (this.
|
|
231
|
+
if (this.table && shouldUseIds) {
|
|
208
232
|
response = transformResponseFields(
|
|
209
233
|
response,
|
|
210
|
-
this.
|
|
234
|
+
this.table,
|
|
211
235
|
undefined, // No expand configs for insert
|
|
212
236
|
);
|
|
213
237
|
}
|
|
214
238
|
|
|
215
|
-
// Get schema from
|
|
216
|
-
|
|
239
|
+
// Get schema from table if available, excluding container fields
|
|
240
|
+
let schema: Record<string, any> | undefined;
|
|
241
|
+
if (this.table) {
|
|
242
|
+
const baseTableConfig = getBaseTableConfig(this.table);
|
|
243
|
+
const containerFields = baseTableConfig.containerFields || [];
|
|
244
|
+
|
|
245
|
+
// Filter out container fields from schema
|
|
246
|
+
schema = { ...baseTableConfig.schema };
|
|
247
|
+
for (const containerField of containerFields) {
|
|
248
|
+
delete schema[containerField as string];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
217
251
|
|
|
218
252
|
// Validate the response (FileMaker returns the created record)
|
|
219
|
-
const validation = await validateSingleResponse<
|
|
253
|
+
const validation = await validateSingleResponse<
|
|
254
|
+
InferSchemaOutputFromFMTable<NonNullable<Occ>>
|
|
255
|
+
>(
|
|
220
256
|
response,
|
|
221
257
|
schema,
|
|
222
258
|
undefined, // No selected fields for insert
|
|
@@ -241,12 +277,14 @@ export class InsertBuilder<
|
|
|
241
277
|
|
|
242
278
|
getRequestConfig(): { method: string; url: string; body?: any } {
|
|
243
279
|
// For batch operations, use database-level setting (no per-request override available here)
|
|
280
|
+
// Note: Input validation happens in execute() and processResponse() for batch operations
|
|
244
281
|
const tableId = this.getTableId(this.databaseUseEntityIds);
|
|
245
282
|
|
|
246
283
|
// Transform field names to FMFIDs if using entity IDs
|
|
247
|
-
const transformedData =
|
|
248
|
-
|
|
249
|
-
|
|
284
|
+
const transformedData =
|
|
285
|
+
this.table && this.databaseUseEntityIds
|
|
286
|
+
? transformFieldNamesToIds(this.data, this.table)
|
|
287
|
+
: this.data;
|
|
250
288
|
|
|
251
289
|
return {
|
|
252
290
|
method: "POST",
|
|
@@ -280,8 +318,22 @@ export class InsertBuilder<
|
|
|
280
318
|
response: Response,
|
|
281
319
|
options?: ExecuteOptions,
|
|
282
320
|
): Promise<
|
|
283
|
-
Result<
|
|
321
|
+
Result<
|
|
322
|
+
ReturnPreference extends "minimal"
|
|
323
|
+
? { ROWID: number }
|
|
324
|
+
: InferSchemaOutputFromFMTable<NonNullable<Occ>>
|
|
325
|
+
>
|
|
284
326
|
> {
|
|
327
|
+
// Check for error responses (important for batch operations)
|
|
328
|
+
if (!response.ok) {
|
|
329
|
+
const tableName = this.table ? getTableName(this.table) : "unknown";
|
|
330
|
+
const error = await parseErrorResponse(
|
|
331
|
+
response,
|
|
332
|
+
response.url || `/${this.databaseName}/${tableName}`,
|
|
333
|
+
);
|
|
334
|
+
return { data: undefined, error };
|
|
335
|
+
}
|
|
336
|
+
|
|
285
337
|
// Handle 204 No Content (common in batch/changeset operations)
|
|
286
338
|
// FileMaker uses return=minimal for changeset operations regardless of Prefer header
|
|
287
339
|
if (response.status === 204) {
|
|
@@ -336,24 +388,53 @@ export class InsertBuilder<
|
|
|
336
388
|
};
|
|
337
389
|
}
|
|
338
390
|
|
|
391
|
+
// Validate and transform input data using input validators (writeValidators)
|
|
392
|
+
// This is needed for processResponse because it's called from batch operations
|
|
393
|
+
// where the data hasn't been validated yet
|
|
394
|
+
let validatedData = this.data;
|
|
395
|
+
if (this.table) {
|
|
396
|
+
const baseTableConfig = getBaseTableConfig(this.table);
|
|
397
|
+
const inputSchema = baseTableConfig.inputSchema;
|
|
398
|
+
try {
|
|
399
|
+
validatedData = await validateAndTransformInput(this.data, inputSchema);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
return {
|
|
402
|
+
data: undefined,
|
|
403
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
404
|
+
} as any;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
339
408
|
// Transform response field IDs back to names if using entity IDs
|
|
340
409
|
// Only transform if useEntityIds resolves to true (respects per-request override)
|
|
341
410
|
const shouldUseIds = options?.useEntityIds ?? this.databaseUseEntityIds;
|
|
342
|
-
|
|
411
|
+
|
|
343
412
|
let transformedResponse = rawResponse;
|
|
344
|
-
if (this.
|
|
413
|
+
if (this.table && shouldUseIds) {
|
|
345
414
|
transformedResponse = transformResponseFields(
|
|
346
415
|
rawResponse,
|
|
347
|
-
this.
|
|
416
|
+
this.table,
|
|
348
417
|
undefined, // No expand configs for insert
|
|
349
418
|
);
|
|
350
419
|
}
|
|
351
420
|
|
|
352
|
-
// Get schema from
|
|
353
|
-
|
|
421
|
+
// Get schema from table if available, excluding container fields
|
|
422
|
+
let schema: Record<string, any> | undefined;
|
|
423
|
+
if (this.table) {
|
|
424
|
+
const baseTableConfig = getBaseTableConfig(this.table);
|
|
425
|
+
const containerFields = baseTableConfig.containerFields || [];
|
|
426
|
+
|
|
427
|
+
// Filter out container fields from schema
|
|
428
|
+
schema = { ...baseTableConfig.schema };
|
|
429
|
+
for (const containerField of containerFields) {
|
|
430
|
+
delete schema[containerField as string];
|
|
431
|
+
}
|
|
432
|
+
}
|
|
354
433
|
|
|
355
434
|
// Validate the response (FileMaker returns the created record)
|
|
356
|
-
const validation = await validateSingleResponse<
|
|
435
|
+
const validation = await validateSingleResponse<
|
|
436
|
+
InferSchemaOutputFromFMTable<NonNullable<Occ>>
|
|
437
|
+
>(
|
|
357
438
|
transformedResponse,
|
|
358
439
|
schema,
|
|
359
440
|
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
|
+
|