@proofkit/fmodata 0.1.0-alpha.8 → 0.1.0-beta.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +651 -449
- package/dist/esm/client/batch-builder.d.ts +10 -9
- package/dist/esm/client/batch-builder.js +119 -56
- package/dist/esm/client/batch-builder.js.map +1 -1
- package/dist/esm/client/batch-request.js +16 -21
- package/dist/esm/client/batch-request.js.map +1 -1
- package/dist/esm/client/builders/default-select.d.ts +10 -0
- package/dist/esm/client/builders/default-select.js +41 -0
- package/dist/esm/client/builders/default-select.js.map +1 -0
- package/dist/esm/client/builders/expand-builder.d.ts +45 -0
- package/dist/esm/client/builders/expand-builder.js +185 -0
- package/dist/esm/client/builders/expand-builder.js.map +1 -0
- package/dist/esm/client/builders/index.d.ts +9 -0
- package/dist/esm/client/builders/query-string-builder.d.ts +18 -0
- package/dist/esm/client/builders/query-string-builder.js +21 -0
- package/dist/esm/client/builders/query-string-builder.js.map +1 -0
- package/dist/esm/client/builders/response-processor.d.ts +43 -0
- package/dist/esm/client/builders/response-processor.js +175 -0
- package/dist/esm/client/builders/response-processor.js.map +1 -0
- package/dist/esm/client/builders/select-mixin.d.ts +25 -0
- package/dist/esm/client/builders/select-mixin.js +28 -0
- package/dist/esm/client/builders/select-mixin.js.map +1 -0
- package/dist/esm/client/builders/select-utils.d.ts +18 -0
- package/dist/esm/client/builders/select-utils.js +30 -0
- package/dist/esm/client/builders/select-utils.js.map +1 -0
- package/dist/esm/client/builders/shared-types.d.ts +40 -0
- package/dist/esm/client/builders/table-utils.d.ts +35 -0
- package/dist/esm/client/builders/table-utils.js +44 -0
- package/dist/esm/client/builders/table-utils.js.map +1 -0
- package/dist/esm/client/database.d.ts +34 -22
- package/dist/esm/client/database.js +48 -84
- package/dist/esm/client/database.js.map +1 -1
- package/dist/esm/client/delete-builder.d.ts +25 -30
- package/dist/esm/client/delete-builder.js +45 -30
- package/dist/esm/client/delete-builder.js.map +1 -1
- package/dist/esm/client/entity-set.d.ts +35 -43
- package/dist/esm/client/entity-set.js +110 -52
- package/dist/esm/client/entity-set.js.map +1 -1
- package/dist/esm/client/error-parser.d.ts +12 -0
- package/dist/esm/client/error-parser.js +25 -0
- package/dist/esm/client/error-parser.js.map +1 -0
- package/dist/esm/client/filemaker-odata.d.ts +26 -7
- package/dist/esm/client/filemaker-odata.js +65 -42
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/dist/esm/client/insert-builder.d.ts +19 -24
- package/dist/esm/client/insert-builder.js +94 -58
- package/dist/esm/client/insert-builder.js.map +1 -1
- package/dist/esm/client/query/expand-builder.d.ts +35 -0
- package/dist/esm/client/query/index.d.ts +4 -0
- package/dist/esm/client/query/query-builder.d.ts +132 -0
- package/dist/esm/client/query/query-builder.js +456 -0
- package/dist/esm/client/query/query-builder.js.map +1 -0
- package/dist/esm/client/query/response-processor.d.ts +25 -0
- package/dist/esm/client/query/types.d.ts +77 -0
- package/dist/esm/client/query/url-builder.d.ts +71 -0
- package/dist/esm/client/query/url-builder.js +100 -0
- package/dist/esm/client/query/url-builder.js.map +1 -0
- package/dist/esm/client/query-builder.d.ts +2 -115
- package/dist/esm/client/record-builder.d.ts +108 -36
- package/dist/esm/client/record-builder.js +284 -119
- package/dist/esm/client/record-builder.js.map +1 -1
- package/dist/esm/client/response-processor.d.ts +4 -9
- package/dist/esm/client/sanitize-json.d.ts +35 -0
- package/dist/esm/client/sanitize-json.js +27 -0
- package/dist/esm/client/sanitize-json.js.map +1 -0
- package/dist/esm/client/schema-manager.d.ts +5 -5
- package/dist/esm/client/schema-manager.js +45 -31
- package/dist/esm/client/schema-manager.js.map +1 -1
- package/dist/esm/client/update-builder.d.ts +34 -40
- package/dist/esm/client/update-builder.js +99 -58
- package/dist/esm/client/update-builder.js.map +1 -1
- package/dist/esm/client/webhook-builder.d.ts +126 -0
- package/dist/esm/client/webhook-builder.js +189 -0
- package/dist/esm/client/webhook-builder.js.map +1 -0
- package/dist/esm/errors.d.ts +19 -2
- package/dist/esm/errors.js +39 -4
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +10 -8
- package/dist/esm/index.js +40 -10
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/logger.d.ts +47 -0
- package/dist/esm/logger.js +69 -0
- package/dist/esm/logger.js.map +1 -0
- package/dist/esm/logger.test.d.ts +1 -0
- package/dist/esm/orm/column.d.ts +62 -0
- package/dist/esm/orm/column.js +63 -0
- package/dist/esm/orm/column.js.map +1 -0
- package/dist/esm/orm/field-builders.d.ts +164 -0
- package/dist/esm/orm/field-builders.js +158 -0
- package/dist/esm/orm/field-builders.js.map +1 -0
- package/dist/esm/orm/index.d.ts +5 -0
- package/dist/esm/orm/operators.d.ts +173 -0
- package/dist/esm/orm/operators.js +260 -0
- package/dist/esm/orm/operators.js.map +1 -0
- package/dist/esm/orm/table.d.ts +355 -0
- package/dist/esm/orm/table.js +202 -0
- package/dist/esm/orm/table.js.map +1 -0
- package/dist/esm/transform.d.ts +20 -21
- package/dist/esm/transform.js +44 -45
- package/dist/esm/transform.js.map +1 -1
- package/dist/esm/types.d.ts +96 -30
- package/dist/esm/types.js +7 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/validation.d.ts +22 -12
- package/dist/esm/validation.js +132 -85
- package/dist/esm/validation.js.map +1 -1
- package/package.json +28 -20
- package/src/client/batch-builder.ts +153 -89
- package/src/client/batch-request.ts +25 -41
- package/src/client/builders/default-select.ts +75 -0
- package/src/client/builders/expand-builder.ts +246 -0
- package/src/client/builders/index.ts +11 -0
- package/src/client/builders/query-string-builder.ts +46 -0
- package/src/client/builders/response-processor.ts +279 -0
- package/src/client/builders/select-mixin.ts +65 -0
- package/src/client/builders/select-utils.ts +59 -0
- package/src/client/builders/shared-types.ts +45 -0
- package/src/client/builders/table-utils.ts +83 -0
- package/src/client/database.ts +89 -183
- package/src/client/delete-builder.ts +74 -84
- package/src/client/entity-set.ts +266 -293
- package/src/client/error-parser.ts +41 -0
- package/src/client/filemaker-odata.ts +98 -66
- package/src/client/insert-builder.ts +157 -118
- package/src/client/query/expand-builder.ts +160 -0
- package/src/client/query/index.ts +14 -0
- package/src/client/query/query-builder.ts +729 -0
- package/src/client/query/response-processor.ts +226 -0
- package/src/client/query/types.ts +126 -0
- package/src/client/query/url-builder.ts +151 -0
- package/src/client/query-builder.ts +10 -1455
- package/src/client/record-builder.ts +575 -240
- package/src/client/response-processor.ts +15 -42
- package/src/client/sanitize-json.ts +64 -0
- package/src/client/schema-manager.ts +61 -76
- package/src/client/update-builder.ts +161 -143
- package/src/client/webhook-builder.ts +265 -0
- package/src/errors.ts +49 -16
- package/src/index.ts +99 -54
- package/src/logger.test.ts +34 -0
- package/src/logger.ts +116 -0
- package/src/orm/column.ts +106 -0
- package/src/orm/field-builders.ts +250 -0
- package/src/orm/index.ts +61 -0
- package/src/orm/operators.ts +473 -0
- package/src/orm/table.ts +741 -0
- package/src/transform.ts +90 -70
- package/src/types.ts +154 -113
- package/src/validation.ts +200 -115
- package/dist/esm/client/base-table.d.ts +0 -125
- package/dist/esm/client/base-table.js +0 -57
- package/dist/esm/client/base-table.js.map +0 -1
- package/dist/esm/client/query-builder.js +0 -896
- package/dist/esm/client/query-builder.js.map +0 -1
- package/dist/esm/client/table-occurrence.d.ts +0 -72
- package/dist/esm/client/table-occurrence.js +0 -74
- package/dist/esm/client/table-occurrence.js.map +0 -1
- package/dist/esm/filter-types.d.ts +0 -76
- package/src/client/base-table.ts +0 -166
- package/src/client/query-builder.ts.bak +0 -1457
- package/src/client/table-occurrence.ts +0 -175
- package/src/filter-types.ts +0 -97
|
@@ -1,20 +1,20 @@
|
|
|
1
|
+
import { BatchTruncatedError } from "../errors";
|
|
1
2
|
import type {
|
|
3
|
+
BatchItemResult,
|
|
4
|
+
BatchResult,
|
|
2
5
|
ExecutableBuilder,
|
|
6
|
+
ExecuteMethodOptions,
|
|
7
|
+
ExecuteOptions,
|
|
3
8
|
ExecutionContext,
|
|
4
9
|
Result,
|
|
5
|
-
ExecuteOptions,
|
|
6
10
|
} from "../types";
|
|
7
|
-
import { type
|
|
8
|
-
import {
|
|
9
|
-
formatBatchRequestFromNative,
|
|
10
|
-
parseBatchResponse,
|
|
11
|
-
type ParsedBatchResponse,
|
|
12
|
-
} from "./batch-request";
|
|
11
|
+
import { formatBatchRequestFromNative, type ParsedBatchResponse, parseBatchResponse } from "./batch-request";
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* Helper type to extract result types from a tuple of ExecutableBuilders.
|
|
16
15
|
* Uses a mapped type which TypeScript 4.1+ can handle for tuples.
|
|
17
16
|
*/
|
|
17
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type
|
|
18
18
|
type ExtractTupleTypes<T extends readonly ExecutableBuilder<any>[]> = {
|
|
19
19
|
[K in keyof T]: T[K] extends ExecutableBuilder<infer U> ? U : never;
|
|
20
20
|
};
|
|
@@ -37,8 +37,7 @@ function parsedToResponse(parsed: ParsedBatchResponse): Response {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Convert body to string if it's not already
|
|
40
|
-
const bodyString =
|
|
41
|
-
typeof parsed.body === "string" ? parsed.body : JSON.stringify(parsed.body);
|
|
40
|
+
const bodyString = typeof parsed.body === "string" ? parsed.body : JSON.stringify(parsed.body);
|
|
42
41
|
|
|
43
42
|
// Handle 204 No Content status - it cannot have a body per HTTP spec
|
|
44
43
|
// If FileMaker returns 204 with a body, treat it as 200
|
|
@@ -48,7 +47,7 @@ function parsedToResponse(parsed: ParsedBatchResponse): Response {
|
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
return new Response(status === 204 ? null : bodyString, {
|
|
51
|
-
status
|
|
50
|
+
status,
|
|
52
51
|
statusText: parsed.statusText,
|
|
53
52
|
headers,
|
|
54
53
|
});
|
|
@@ -57,22 +56,23 @@ function parsedToResponse(parsed: ParsedBatchResponse): Response {
|
|
|
57
56
|
/**
|
|
58
57
|
* Builder for batch operations that allows multiple queries to be executed together
|
|
59
58
|
* in a single transactional request.
|
|
59
|
+
*
|
|
60
|
+
* Note: BatchBuilder does not implement ExecutableBuilder because execute() returns
|
|
61
|
+
* BatchResult instead of Result, which is a different return type structure.
|
|
60
62
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
private builders: ExecutableBuilder<any>[];
|
|
65
|
-
private readonly
|
|
63
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type
|
|
64
|
+
export class BatchBuilder<Builders extends readonly ExecutableBuilder<any>[]> {
|
|
65
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type
|
|
66
|
+
private readonly builders: ExecutableBuilder<any>[];
|
|
67
|
+
private readonly databaseName: string;
|
|
68
|
+
private readonly context: ExecutionContext;
|
|
66
69
|
|
|
67
|
-
constructor(
|
|
68
|
-
builders: Builders,
|
|
69
|
-
private readonly databaseName: string,
|
|
70
|
-
private readonly context: ExecutionContext,
|
|
71
|
-
) {
|
|
70
|
+
constructor(builders: Builders, databaseName: string, context: ExecutionContext) {
|
|
72
71
|
// Convert readonly tuple to mutable array for dynamic additions
|
|
73
72
|
this.builders = [...builders];
|
|
74
73
|
// Store original tuple for type preservation
|
|
75
|
-
this.
|
|
74
|
+
this.databaseName = databaseName;
|
|
75
|
+
this.context = context;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
@@ -98,6 +98,7 @@ export class BatchBuilder<Builders extends readonly ExecutableBuilder<any>[]>
|
|
|
98
98
|
* Get the request configuration for this batch operation.
|
|
99
99
|
* This is used internally by the execution system.
|
|
100
100
|
*/
|
|
101
|
+
// biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value
|
|
101
102
|
getRequestConfig(): { method: string; url: string; body?: any } {
|
|
102
103
|
// Note: This method is kept for compatibility but batch operations
|
|
103
104
|
// should use execute() directly which handles the full Request/Response flow
|
|
@@ -108,7 +109,7 @@ export class BatchBuilder<Builders extends readonly ExecutableBuilder<any>[]>
|
|
|
108
109
|
};
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
toRequest(baseUrl: string): Request {
|
|
112
|
+
toRequest(baseUrl: string, _options?: ExecuteOptions): Request {
|
|
112
113
|
// Batch operations are not designed to be nested, but we provide
|
|
113
114
|
// a basic implementation for interface compliance
|
|
114
115
|
const fullUrl = `${baseUrl}/${this.databaseName}/$batch`;
|
|
@@ -121,114 +122,148 @@ export class BatchBuilder<Builders extends readonly ExecutableBuilder<any>[]>
|
|
|
121
122
|
});
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
options?: ExecuteOptions,
|
|
127
|
-
): Promise<Result<any>> {
|
|
125
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic return type for interface compliance
|
|
126
|
+
processResponse(_response: Response, _options?: ExecuteOptions): Promise<Result<any>> {
|
|
128
127
|
// This should not typically be called for batch operations
|
|
129
128
|
// as they handle their own response processing
|
|
130
|
-
return {
|
|
129
|
+
return Promise.resolve({
|
|
131
130
|
data: undefined,
|
|
132
131
|
error: {
|
|
133
132
|
name: "NotImplementedError",
|
|
134
133
|
message: "Batch operations handle response processing internally",
|
|
135
134
|
timestamp: new Date(),
|
|
135
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object
|
|
136
136
|
} as any,
|
|
137
|
-
};
|
|
137
|
+
});
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* Execute the batch operation.
|
|
142
142
|
*
|
|
143
143
|
* @param options - Optional fetch options and batch-specific options (includes beforeRequest hook)
|
|
144
|
-
* @returns A
|
|
144
|
+
* @returns A BatchResult containing individual results for each operation
|
|
145
145
|
*/
|
|
146
146
|
async execute<EO extends ExecuteOptions>(
|
|
147
|
-
options?:
|
|
148
|
-
): Promise<
|
|
147
|
+
options?: ExecuteMethodOptions<EO>,
|
|
148
|
+
): Promise<BatchResult<ExtractTupleTypes<Builders>>> {
|
|
149
149
|
const baseUrl = this.context._getBaseUrl?.();
|
|
150
150
|
if (!baseUrl) {
|
|
151
|
-
|
|
151
|
+
// Return BatchResult with all operations marked as failed
|
|
152
|
+
const errorCount = this.builders.length;
|
|
153
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type
|
|
154
|
+
const results: BatchItemResult<any>[] = this.builders.map((_, _i) => ({
|
|
152
155
|
data: undefined,
|
|
153
156
|
error: {
|
|
154
157
|
name: "ConfigurationError",
|
|
155
|
-
message:
|
|
156
|
-
"Base URL not available - execution context must implement _getBaseUrl()",
|
|
158
|
+
message: "Base URL not available - execution context must implement _getBaseUrl()",
|
|
157
159
|
timestamp: new Date(),
|
|
160
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object
|
|
158
161
|
} as any,
|
|
162
|
+
status: 0,
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type
|
|
167
|
+
results: results as any,
|
|
168
|
+
successCount: 0,
|
|
169
|
+
errorCount,
|
|
170
|
+
truncated: false,
|
|
171
|
+
firstErrorIndex: 0,
|
|
159
172
|
};
|
|
160
173
|
}
|
|
161
174
|
|
|
162
175
|
try {
|
|
163
176
|
// Convert builders to native Request objects
|
|
164
|
-
const requests: Request[] = this.builders.map((builder) =>
|
|
165
|
-
builder.toRequest(baseUrl),
|
|
166
|
-
);
|
|
177
|
+
const requests: Request[] = this.builders.map((builder) => builder.toRequest(baseUrl, options));
|
|
167
178
|
|
|
168
179
|
// Format batch request (automatically groups mutations into changesets)
|
|
169
|
-
const { body, boundary } = await formatBatchRequestFromNative(
|
|
170
|
-
requests,
|
|
171
|
-
baseUrl,
|
|
172
|
-
);
|
|
180
|
+
const { body, boundary } = await formatBatchRequestFromNative(requests, baseUrl);
|
|
173
181
|
|
|
174
182
|
// Execute the batch request
|
|
175
|
-
const response = await this.context._makeRequest<string>(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
"Content-Type": `multipart/mixed; boundary=${boundary}`,
|
|
183
|
-
"OData-Version": "4.0",
|
|
184
|
-
},
|
|
185
|
-
body,
|
|
183
|
+
const response = await this.context._makeRequest<string>(`/${this.databaseName}/$batch`, {
|
|
184
|
+
...options,
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: {
|
|
187
|
+
...options?.headers,
|
|
188
|
+
"Content-Type": `multipart/mixed; boundary=${boundary}`,
|
|
189
|
+
"OData-Version": "4.0",
|
|
186
190
|
},
|
|
187
|
-
|
|
191
|
+
body,
|
|
192
|
+
});
|
|
188
193
|
|
|
189
194
|
if (response.error) {
|
|
190
|
-
|
|
195
|
+
// Return BatchResult with all operations marked as failed
|
|
196
|
+
const errorCount = this.builders.length;
|
|
197
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type
|
|
198
|
+
const results: BatchItemResult<any>[] = this.builders.map((_, _i) => ({
|
|
199
|
+
data: undefined,
|
|
200
|
+
error: response.error,
|
|
201
|
+
status: 0,
|
|
202
|
+
}));
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type
|
|
206
|
+
results: results as any,
|
|
207
|
+
successCount: 0,
|
|
208
|
+
errorCount,
|
|
209
|
+
truncated: false,
|
|
210
|
+
firstErrorIndex: 0,
|
|
211
|
+
};
|
|
191
212
|
}
|
|
192
213
|
|
|
193
214
|
// Extract the actual boundary from the response
|
|
194
215
|
// FileMaker uses its own boundary, not the one we sent
|
|
195
|
-
const firstLine =
|
|
196
|
-
|
|
197
|
-
const actualBoundary = firstLine.startsWith("--")
|
|
198
|
-
? firstLine.substring(2)
|
|
199
|
-
: boundary;
|
|
216
|
+
const firstLine = response.data.split("\r\n")[0] || response.data.split("\n")[0] || "";
|
|
217
|
+
const actualBoundary = firstLine.startsWith("--") ? firstLine.substring(2) : boundary;
|
|
200
218
|
|
|
201
219
|
// Parse the multipart response
|
|
202
220
|
const contentTypeHeader = `multipart/mixed; boundary=${actualBoundary}`;
|
|
203
|
-
const parsedResponses = parseBatchResponse(
|
|
204
|
-
response.data,
|
|
205
|
-
contentTypeHeader,
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
// Check if we got the expected number of responses
|
|
209
|
-
if (parsedResponses.length !== this.builders.length) {
|
|
210
|
-
return {
|
|
211
|
-
data: undefined,
|
|
212
|
-
error: {
|
|
213
|
-
name: "BatchError",
|
|
214
|
-
message: `Expected ${this.builders.length} responses but got ${parsedResponses.length}`,
|
|
215
|
-
timestamp: new Date(),
|
|
216
|
-
} as any,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
221
|
+
const parsedResponses = parseBatchResponse(response.data, contentTypeHeader);
|
|
219
222
|
|
|
220
223
|
// Process each response using the corresponding builder
|
|
221
|
-
// Build
|
|
222
|
-
type
|
|
224
|
+
// Build BatchResult with per-item results
|
|
225
|
+
type _ResultTuple = ExtractTupleTypes<Builders>;
|
|
226
|
+
|
|
227
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type
|
|
228
|
+
const results: BatchItemResult<any>[] = [];
|
|
229
|
+
let successCount = 0;
|
|
230
|
+
let errorCount = 0;
|
|
231
|
+
let firstErrorIndex: number | null = null;
|
|
232
|
+
const truncated = parsedResponses.length < this.builders.length;
|
|
223
233
|
|
|
224
234
|
// Process builders sequentially to preserve tuple order
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const builder = this.originalBuilders[i];
|
|
235
|
+
for (let i = 0; i < this.builders.length; i++) {
|
|
236
|
+
const builder = this.builders[i];
|
|
228
237
|
const parsed = parsedResponses[i];
|
|
229
238
|
|
|
230
|
-
if (!
|
|
231
|
-
|
|
239
|
+
if (!parsed) {
|
|
240
|
+
// Truncated - operation never executed
|
|
241
|
+
const failedAtIndex = firstErrorIndex ?? i;
|
|
242
|
+
results.push({
|
|
243
|
+
data: undefined,
|
|
244
|
+
error: new BatchTruncatedError(i, failedAtIndex),
|
|
245
|
+
status: 0,
|
|
246
|
+
});
|
|
247
|
+
errorCount++;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!builder) {
|
|
252
|
+
// Should not happen, but handle gracefully
|
|
253
|
+
results.push({
|
|
254
|
+
data: undefined,
|
|
255
|
+
error: {
|
|
256
|
+
name: "BatchError",
|
|
257
|
+
message: `Builder at index ${i} is undefined`,
|
|
258
|
+
timestamp: new Date(),
|
|
259
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object
|
|
260
|
+
} as any,
|
|
261
|
+
status: parsed.status,
|
|
262
|
+
});
|
|
263
|
+
errorCount++;
|
|
264
|
+
if (firstErrorIndex === null) {
|
|
265
|
+
firstErrorIndex = i;
|
|
266
|
+
}
|
|
232
267
|
continue;
|
|
233
268
|
}
|
|
234
269
|
|
|
@@ -239,26 +274,55 @@ export class BatchBuilder<Builders extends readonly ExecutableBuilder<any>[]>
|
|
|
239
274
|
const result = await builder.processResponse(nativeResponse, options);
|
|
240
275
|
|
|
241
276
|
if (result.error) {
|
|
242
|
-
|
|
277
|
+
results.push({
|
|
278
|
+
data: undefined,
|
|
279
|
+
error: result.error,
|
|
280
|
+
status: parsed.status,
|
|
281
|
+
});
|
|
282
|
+
errorCount++;
|
|
283
|
+
if (firstErrorIndex === null) {
|
|
284
|
+
firstErrorIndex = i;
|
|
285
|
+
}
|
|
243
286
|
} else {
|
|
244
|
-
|
|
287
|
+
results.push({
|
|
288
|
+
data: result.data,
|
|
289
|
+
error: undefined,
|
|
290
|
+
status: parsed.status,
|
|
291
|
+
});
|
|
292
|
+
successCount++;
|
|
245
293
|
}
|
|
246
294
|
}
|
|
247
295
|
|
|
248
|
-
// Use a type assertion that TypeScript will respect
|
|
249
|
-
// ExtractTupleTypes ensures this is a proper tuple type
|
|
250
296
|
return {
|
|
251
|
-
|
|
252
|
-
|
|
297
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type
|
|
298
|
+
results: results as any,
|
|
299
|
+
successCount,
|
|
300
|
+
errorCount,
|
|
301
|
+
truncated,
|
|
302
|
+
firstErrorIndex,
|
|
253
303
|
};
|
|
254
304
|
} catch (err) {
|
|
255
|
-
return
|
|
305
|
+
// On exception, return a BatchResult with all operations marked as failed
|
|
306
|
+
const errorCount = this.builders.length;
|
|
307
|
+
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type
|
|
308
|
+
const results: BatchItemResult<any>[] = this.builders.map((_, _i) => ({
|
|
256
309
|
data: undefined,
|
|
257
310
|
error: {
|
|
258
311
|
name: "BatchError",
|
|
259
312
|
message: err instanceof Error ? err.message : "Unknown error",
|
|
260
313
|
timestamp: new Date(),
|
|
314
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object
|
|
261
315
|
} as any,
|
|
316
|
+
status: 0,
|
|
317
|
+
}));
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type
|
|
321
|
+
results: results as any,
|
|
322
|
+
successCount: 0,
|
|
323
|
+
errorCount,
|
|
324
|
+
truncated: false,
|
|
325
|
+
firstErrorIndex: 0,
|
|
262
326
|
};
|
|
263
327
|
}
|
|
264
328
|
}
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* with support for transactional changesets.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
const BOUNDARY_REGEX = /boundary=([^;]+)/;
|
|
10
|
+
const HTTP_STATUS_LINE_REGEX = /HTTP\/\d\.\d\s+(\d+)\s*(.*)/;
|
|
11
|
+
const CRLF_REGEX = /\r\n/;
|
|
12
|
+
const CHANGESET_CONTENT_TYPE_REGEX = /Content-Type: multipart\/mixed;\s*boundary=([^\r\n]+)/;
|
|
13
|
+
|
|
9
14
|
export interface RequestConfig {
|
|
10
15
|
method: string;
|
|
11
16
|
url: string;
|
|
@@ -17,6 +22,7 @@ export interface ParsedBatchResponse {
|
|
|
17
22
|
status: number;
|
|
18
23
|
statusText: string;
|
|
19
24
|
headers: Record<string, string>;
|
|
25
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic response body type from OData API
|
|
20
26
|
body: any;
|
|
21
27
|
}
|
|
22
28
|
|
|
@@ -25,10 +31,8 @@ export interface ParsedBatchResponse {
|
|
|
25
31
|
* @param prefix - Prefix for the boundary (e.g., "batch_" or "changeset_")
|
|
26
32
|
* @returns A boundary string with the prefix and 32 random hex characters
|
|
27
33
|
*/
|
|
28
|
-
export function generateBoundary(prefix
|
|
29
|
-
const randomHex = Array.from({ length: 32 }, () =>
|
|
30
|
-
Math.floor(Math.random() * 16).toString(16),
|
|
31
|
-
).join("");
|
|
34
|
+
export function generateBoundary(prefix = "batch_"): string {
|
|
35
|
+
const randomHex = Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join("");
|
|
32
36
|
return `${prefix}${randomHex}`;
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -77,9 +81,7 @@ function formatSubRequest(request: RequestConfig, baseUrl: string): string {
|
|
|
77
81
|
lines.push(""); // Empty line after multipart headers
|
|
78
82
|
|
|
79
83
|
// Construct full URL (convert relative to absolute)
|
|
80
|
-
const fullUrl = request.url.startsWith("http")
|
|
81
|
-
? request.url
|
|
82
|
-
: `${baseUrl}${request.url}`;
|
|
84
|
+
const fullUrl = request.url.startsWith("http") ? request.url : `${baseUrl}${request.url}`;
|
|
83
85
|
|
|
84
86
|
// Add HTTP request line
|
|
85
87
|
lines.push(`${request.method} ${fullUrl} HTTP/1.1`);
|
|
@@ -97,10 +99,7 @@ function formatSubRequest(request: RequestConfig, baseUrl: string): string {
|
|
|
97
99
|
|
|
98
100
|
// Check if Content-Type is already set
|
|
99
101
|
const hasContentType =
|
|
100
|
-
request.headers &&
|
|
101
|
-
Object.keys(request.headers).some(
|
|
102
|
-
(k) => k.toLowerCase() === "content-type",
|
|
103
|
-
);
|
|
102
|
+
request.headers && Object.keys(request.headers).some((k) => k.toLowerCase() === "content-type");
|
|
104
103
|
|
|
105
104
|
if (!hasContentType) {
|
|
106
105
|
lines.push("Content-Type: application/json");
|
|
@@ -108,10 +107,7 @@ function formatSubRequest(request: RequestConfig, baseUrl: string): string {
|
|
|
108
107
|
|
|
109
108
|
// Add Content-Length (required for FileMaker to read the body)
|
|
110
109
|
const hasContentLength =
|
|
111
|
-
request.headers &&
|
|
112
|
-
Object.keys(request.headers).some(
|
|
113
|
-
(k) => k.toLowerCase() === "content-length",
|
|
114
|
-
);
|
|
110
|
+
request.headers && Object.keys(request.headers).some((k) => k.toLowerCase() === "content-length");
|
|
115
111
|
|
|
116
112
|
if (!hasContentLength) {
|
|
117
113
|
lines.push(`Content-Length: ${request.body.length}`);
|
|
@@ -136,11 +132,7 @@ function formatSubRequest(request: RequestConfig, baseUrl: string): string {
|
|
|
136
132
|
* @param changesetBoundary - Boundary string for the changeset
|
|
137
133
|
* @returns Formatted changeset string with CRLF line endings
|
|
138
134
|
*/
|
|
139
|
-
function formatChangeset(
|
|
140
|
-
requests: RequestConfig[],
|
|
141
|
-
baseUrl: string,
|
|
142
|
-
changesetBoundary: string,
|
|
143
|
-
): string {
|
|
135
|
+
function formatChangeset(requests: RequestConfig[], baseUrl: string, changesetBoundary: string): string {
|
|
144
136
|
const lines: string[] = [];
|
|
145
137
|
|
|
146
138
|
lines.push(`Content-Type: multipart/mixed; boundary=${changesetBoundary}`);
|
|
@@ -183,9 +175,7 @@ export function formatBatchRequest(
|
|
|
183
175
|
// Close and add the current changeset
|
|
184
176
|
const changesetBoundary = generateBoundary("changeset_");
|
|
185
177
|
lines.push(`--${boundary}`);
|
|
186
|
-
lines.push(
|
|
187
|
-
formatChangeset(currentChangeset, baseUrl, changesetBoundary),
|
|
188
|
-
);
|
|
178
|
+
lines.push(formatChangeset(currentChangeset, baseUrl, changesetBoundary));
|
|
189
179
|
currentChangeset = null;
|
|
190
180
|
}
|
|
191
181
|
|
|
@@ -277,8 +267,8 @@ export async function formatBatchRequestFromNative(
|
|
|
277
267
|
* @returns The boundary string, or null if not found
|
|
278
268
|
*/
|
|
279
269
|
export function extractBoundary(contentType: string): string | null {
|
|
280
|
-
const match = contentType.match(
|
|
281
|
-
return match
|
|
270
|
+
const match = contentType.match(BOUNDARY_REGEX);
|
|
271
|
+
return match?.[1] ? match[1].trim() : null;
|
|
282
272
|
}
|
|
283
273
|
|
|
284
274
|
/**
|
|
@@ -290,12 +280,12 @@ function parseStatusLine(line: string): {
|
|
|
290
280
|
status: number;
|
|
291
281
|
statusText: string;
|
|
292
282
|
} {
|
|
293
|
-
const match = line.match(
|
|
294
|
-
if (!match
|
|
283
|
+
const match = line.match(HTTP_STATUS_LINE_REGEX);
|
|
284
|
+
if (!match?.[1]) {
|
|
295
285
|
return { status: 0, statusText: "" };
|
|
296
286
|
}
|
|
297
287
|
return {
|
|
298
|
-
status: parseInt(match[1], 10),
|
|
288
|
+
status: Number.parseInt(match[1], 10),
|
|
299
289
|
statusText: match[2]?.trim() || "",
|
|
300
290
|
};
|
|
301
291
|
}
|
|
@@ -324,13 +314,13 @@ function parseHeaders(lines: string[]): Record<string, string> {
|
|
|
324
314
|
* @returns Parsed response object
|
|
325
315
|
*/
|
|
326
316
|
function parseHttpResponse(part: string): ParsedBatchResponse {
|
|
327
|
-
const lines = part.split(
|
|
317
|
+
const lines = part.split(CRLF_REGEX);
|
|
328
318
|
|
|
329
319
|
// Find the HTTP status line (skip multipart headers)
|
|
330
320
|
let statusLineIndex = -1;
|
|
331
321
|
for (let i = 0; i < lines.length; i++) {
|
|
332
322
|
const line = lines[i];
|
|
333
|
-
if (line
|
|
323
|
+
if (line?.startsWith("HTTP/")) {
|
|
334
324
|
statusLineIndex = i;
|
|
335
325
|
break;
|
|
336
326
|
}
|
|
@@ -370,7 +360,7 @@ function parseHttpResponse(part: string): ParsedBatchResponse {
|
|
|
370
360
|
break;
|
|
371
361
|
}
|
|
372
362
|
// Stop at boundary markers (for responses without bodies like 204)
|
|
373
|
-
if (line
|
|
363
|
+
if (line?.startsWith("--")) {
|
|
374
364
|
break;
|
|
375
365
|
}
|
|
376
366
|
if (line) {
|
|
@@ -395,6 +385,7 @@ function parseHttpResponse(part: string): ParsedBatchResponse {
|
|
|
395
385
|
bodyText = bodyLinesFiltered.join("\r\n").trim();
|
|
396
386
|
}
|
|
397
387
|
|
|
388
|
+
// biome-ignore lint/suspicious/noExplicitAny: Dynamic response body type from OData API
|
|
398
389
|
let body: any = null;
|
|
399
390
|
if (bodyText) {
|
|
400
391
|
try {
|
|
@@ -419,10 +410,7 @@ function parseHttpResponse(part: string): ParsedBatchResponse {
|
|
|
419
410
|
* @param contentType - The Content-Type header from the response
|
|
420
411
|
* @returns Array of parsed responses in the same order as the request
|
|
421
412
|
*/
|
|
422
|
-
export function parseBatchResponse(
|
|
423
|
-
responseText: string,
|
|
424
|
-
contentType: string,
|
|
425
|
-
): ParsedBatchResponse[] {
|
|
413
|
+
export function parseBatchResponse(responseText: string, contentType: string): ParsedBatchResponse[] {
|
|
426
414
|
const boundary = extractBoundary(contentType);
|
|
427
415
|
if (!boundary) {
|
|
428
416
|
throw new Error("Could not extract boundary from Content-Type header");
|
|
@@ -445,9 +433,7 @@ export function parseBatchResponse(
|
|
|
445
433
|
// Check if this part is a changeset (nested multipart)
|
|
446
434
|
if (trimmedPart.includes("Content-Type: multipart/mixed")) {
|
|
447
435
|
// Extract the changeset boundary
|
|
448
|
-
const changesetContentTypeMatch = trimmedPart.match(
|
|
449
|
-
/Content-Type: multipart\/mixed;\s*boundary=([^\r\n]+)/,
|
|
450
|
-
);
|
|
436
|
+
const changesetContentTypeMatch = trimmedPart.match(CHANGESET_CONTENT_TYPE_REGEX);
|
|
451
437
|
if (changesetContentTypeMatch) {
|
|
452
438
|
const changesetBoundary = changesetContentTypeMatch?.[1]?.trim();
|
|
453
439
|
const changesetPattern = `--${changesetBoundary}`;
|
|
@@ -460,9 +446,7 @@ export function parseBatchResponse(
|
|
|
460
446
|
}
|
|
461
447
|
|
|
462
448
|
// Skip the changeset header
|
|
463
|
-
if (
|
|
464
|
-
trimmedChangesetPart.startsWith("Content-Type: multipart/mixed")
|
|
465
|
-
) {
|
|
449
|
+
if (trimmedChangesetPart.startsWith("Content-Type: multipart/mixed")) {
|
|
466
450
|
continue;
|
|
467
451
|
}
|
|
468
452
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { isColumn } from "../../orm/column";
|
|
2
|
+
import type { FMTable } from "../../orm/table";
|
|
3
|
+
import { FMTable as FMTableClass, getBaseTableConfig } from "../../orm/table";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Helper function to get container field names from a table.
|
|
7
|
+
* Container fields cannot be selected via $select in FileMaker OData API.
|
|
8
|
+
*/
|
|
9
|
+
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
|
|
10
|
+
function getContainerFieldNames(table: FMTable<any, any>): string[] {
|
|
11
|
+
const baseTableConfig = getBaseTableConfig(table);
|
|
12
|
+
if (!baseTableConfig?.containerFields) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return baseTableConfig.containerFields as string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Gets default select fields from a table definition.
|
|
20
|
+
* Returns undefined if defaultSelect is "all".
|
|
21
|
+
* Automatically filters out container fields since they cannot be selected via $select.
|
|
22
|
+
*
|
|
23
|
+
* @param table - The table occurrence
|
|
24
|
+
* @param includeSpecialColumns - If true, includes ROWID and ROWMODID when defaultSelect is "schema"
|
|
25
|
+
*/
|
|
26
|
+
export function getDefaultSelectFields(
|
|
27
|
+
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
|
|
28
|
+
table: FMTable<any, any> | undefined,
|
|
29
|
+
includeSpecialColumns?: boolean,
|
|
30
|
+
): string[] | undefined {
|
|
31
|
+
if (!table) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access
|
|
36
|
+
const defaultSelect = (table as any)[FMTableClass.Symbol.DefaultSelect];
|
|
37
|
+
const containerFields = getContainerFieldNames(table);
|
|
38
|
+
|
|
39
|
+
if (defaultSelect === "schema") {
|
|
40
|
+
const baseTableConfig = getBaseTableConfig(table);
|
|
41
|
+
const allFields = Object.keys(baseTableConfig.schema);
|
|
42
|
+
// Filter out container fields
|
|
43
|
+
const fields = [...new Set(allFields.filter((f) => !containerFields.includes(f)))];
|
|
44
|
+
|
|
45
|
+
// Add special columns if requested
|
|
46
|
+
if (includeSpecialColumns) {
|
|
47
|
+
fields.push("ROWID", "ROWMODID");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return fields;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (Array.isArray(defaultSelect)) {
|
|
54
|
+
// Filter out container fields
|
|
55
|
+
return [...new Set(defaultSelect.filter((f) => !containerFields.includes(f)))];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check if defaultSelect is a Record<string, Column> (resolved from function)
|
|
59
|
+
if (typeof defaultSelect === "object" && defaultSelect !== null && !Array.isArray(defaultSelect)) {
|
|
60
|
+
// Extract field names from Column instances
|
|
61
|
+
const fieldNames: string[] = [];
|
|
62
|
+
for (const value of Object.values(defaultSelect)) {
|
|
63
|
+
if (isColumn(value)) {
|
|
64
|
+
fieldNames.push(value.fieldName);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (fieldNames.length > 0) {
|
|
68
|
+
// Filter out container fields
|
|
69
|
+
return [...new Set(fieldNames.filter((f) => !containerFields.includes(f)))];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// defaultSelect is "all" or undefined
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|