@proofkit/fmodata 0.1.0-alpha.4 → 0.1.0-alpha.7
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 +690 -31
- package/dist/esm/client/base-table.d.ts +122 -5
- package/dist/esm/client/base-table.js +46 -5
- package/dist/esm/client/base-table.js.map +1 -1
- package/dist/esm/client/batch-builder.d.ts +54 -0
- package/dist/esm/client/batch-builder.js +179 -0
- package/dist/esm/client/batch-builder.js.map +1 -0
- package/dist/esm/client/batch-request.d.ts +61 -0
- package/dist/esm/client/batch-request.js +252 -0
- package/dist/esm/client/batch-request.js.map +1 -0
- package/dist/esm/client/database.d.ts +54 -5
- package/dist/esm/client/database.js +118 -15
- package/dist/esm/client/database.js.map +1 -1
- package/dist/esm/client/delete-builder.d.ts +21 -2
- package/dist/esm/client/delete-builder.js +96 -32
- package/dist/esm/client/delete-builder.js.map +1 -1
- package/dist/esm/client/entity-set.d.ts +22 -8
- package/dist/esm/client/entity-set.js +28 -8
- package/dist/esm/client/entity-set.js.map +1 -1
- package/dist/esm/client/filemaker-odata.d.ts +22 -3
- package/dist/esm/client/filemaker-odata.js +122 -27
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/dist/esm/client/insert-builder.d.ts +38 -3
- package/dist/esm/client/insert-builder.js +231 -34
- package/dist/esm/client/insert-builder.js.map +1 -1
- package/dist/esm/client/query-builder.d.ts +26 -5
- package/dist/esm/client/query-builder.js +455 -208
- package/dist/esm/client/query-builder.js.map +1 -1
- package/dist/esm/client/record-builder.d.ts +19 -4
- package/dist/esm/client/record-builder.js +132 -40
- package/dist/esm/client/record-builder.js.map +1 -1
- package/dist/esm/client/response-processor.d.ts +38 -0
- package/dist/esm/client/schema-manager.d.ts +57 -0
- package/dist/esm/client/schema-manager.js +132 -0
- package/dist/esm/client/schema-manager.js.map +1 -0
- package/dist/esm/client/table-occurrence.d.ts +66 -2
- package/dist/esm/client/table-occurrence.js +36 -1
- package/dist/esm/client/table-occurrence.js.map +1 -1
- package/dist/esm/client/update-builder.d.ts +34 -11
- package/dist/esm/client/update-builder.js +135 -31
- package/dist/esm/client/update-builder.js.map +1 -1
- package/dist/esm/errors.d.ts +73 -0
- package/dist/esm/errors.js +148 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.d.ts +7 -3
- package/dist/esm/index.js +27 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/transform.d.ts +65 -0
- package/dist/esm/transform.js +114 -0
- package/dist/esm/transform.js.map +1 -0
- package/dist/esm/types.d.ts +89 -5
- package/dist/esm/validation.d.ts +6 -3
- package/dist/esm/validation.js +104 -33
- package/dist/esm/validation.js.map +1 -1
- package/package.json +10 -1
- package/src/client/base-table.ts +155 -8
- package/src/client/batch-builder.ts +265 -0
- package/src/client/batch-request.ts +485 -0
- package/src/client/database.ts +173 -16
- package/src/client/delete-builder.ts +149 -48
- package/src/client/entity-set.ts +99 -15
- package/src/client/filemaker-odata.ts +178 -34
- package/src/client/insert-builder.ts +350 -40
- package/src/client/query-builder.ts +609 -236
- package/src/client/record-builder.ts +186 -53
- package/src/client/response-processor.ts +103 -0
- package/src/client/schema-manager.ts +246 -0
- package/src/client/table-occurrence.ts +118 -4
- package/src/client/update-builder.ts +235 -49
- package/src/errors.ts +217 -0
- package/src/index.ts +43 -1
- package/src/transform.ts +249 -0
- package/src/types.ts +201 -35
- package/src/validation.ts +120 -36
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch Request Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for formatting and parsing OData batch requests using multipart/mixed format.
|
|
5
|
+
* OData batch requests allow bundling multiple operations into a single HTTP request,
|
|
6
|
+
* with support for transactional changesets.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface RequestConfig {
|
|
10
|
+
method: string;
|
|
11
|
+
url: string;
|
|
12
|
+
body?: string;
|
|
13
|
+
headers?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ParsedBatchResponse {
|
|
17
|
+
status: number;
|
|
18
|
+
statusText: string;
|
|
19
|
+
headers: Record<string, string>;
|
|
20
|
+
body: any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generates a random boundary string for multipart requests
|
|
25
|
+
* @param prefix - Prefix for the boundary (e.g., "batch_" or "changeset_")
|
|
26
|
+
* @returns A boundary string with the prefix and 32 random hex characters
|
|
27
|
+
*/
|
|
28
|
+
export function generateBoundary(prefix: string = "batch_"): string {
|
|
29
|
+
const randomHex = Array.from({ length: 32 }, () =>
|
|
30
|
+
Math.floor(Math.random() * 16).toString(16),
|
|
31
|
+
).join("");
|
|
32
|
+
return `${prefix}${randomHex}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Converts a native Request object to RequestConfig
|
|
37
|
+
* @param request - Native Request object
|
|
38
|
+
* @returns RequestConfig object
|
|
39
|
+
*/
|
|
40
|
+
async function requestToConfig(request: Request): Promise<RequestConfig> {
|
|
41
|
+
const headers: Record<string, string> = {};
|
|
42
|
+
request.headers.forEach((value, key) => {
|
|
43
|
+
headers[key] = value;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let body: string | undefined;
|
|
47
|
+
if (request.body) {
|
|
48
|
+
// Clone the request to read the body without consuming it
|
|
49
|
+
const clonedRequest = request.clone();
|
|
50
|
+
body = await clonedRequest.text();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
method: request.method,
|
|
55
|
+
url: request.url,
|
|
56
|
+
body,
|
|
57
|
+
headers,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Formats a single HTTP request for inclusion in a batch
|
|
63
|
+
* @param request - The request configuration
|
|
64
|
+
* @param baseUrl - The base URL to prepend to relative URLs
|
|
65
|
+
* @returns Formatted request string with CRLF line endings
|
|
66
|
+
*
|
|
67
|
+
* Formatting rules for FileMaker OData:
|
|
68
|
+
* - GET (no body): request line → blank → blank
|
|
69
|
+
* - POST/PATCH (with body): request line → headers → blank → body (NO blank after!)
|
|
70
|
+
*/
|
|
71
|
+
function formatSubRequest(request: RequestConfig, baseUrl: string): string {
|
|
72
|
+
const lines: string[] = [];
|
|
73
|
+
|
|
74
|
+
// Add required headers for sub-request
|
|
75
|
+
lines.push("Content-Type: application/http");
|
|
76
|
+
lines.push("Content-Transfer-Encoding: binary");
|
|
77
|
+
lines.push(""); // Empty line after multipart headers
|
|
78
|
+
|
|
79
|
+
// Construct full URL (convert relative to absolute)
|
|
80
|
+
const fullUrl = request.url.startsWith("http")
|
|
81
|
+
? request.url
|
|
82
|
+
: `${baseUrl}${request.url}`;
|
|
83
|
+
|
|
84
|
+
// Add HTTP request line
|
|
85
|
+
lines.push(`${request.method} ${fullUrl} HTTP/1.1`);
|
|
86
|
+
|
|
87
|
+
// For requests with body, add headers
|
|
88
|
+
if (request.body) {
|
|
89
|
+
// Add request headers (excluding Authorization - it's in the outer request)
|
|
90
|
+
if (request.headers) {
|
|
91
|
+
for (const [key, value] of Object.entries(request.headers)) {
|
|
92
|
+
if (key.toLowerCase() !== "authorization") {
|
|
93
|
+
lines.push(`${key}: ${value}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check if Content-Type is already set
|
|
99
|
+
const hasContentType =
|
|
100
|
+
request.headers &&
|
|
101
|
+
Object.keys(request.headers).some(
|
|
102
|
+
(k) => k.toLowerCase() === "content-type",
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (!hasContentType) {
|
|
106
|
+
lines.push("Content-Type: application/json");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Add Content-Length (required for FileMaker to read the body)
|
|
110
|
+
const hasContentLength =
|
|
111
|
+
request.headers &&
|
|
112
|
+
Object.keys(request.headers).some(
|
|
113
|
+
(k) => k.toLowerCase() === "content-length",
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (!hasContentLength) {
|
|
117
|
+
lines.push(`Content-Length: ${request.body.length}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
lines.push(""); // Empty line between headers and body
|
|
121
|
+
lines.push(request.body);
|
|
122
|
+
// NO blank line after body - the boundary comes immediately
|
|
123
|
+
} else {
|
|
124
|
+
// For GET requests (no body), add TWO blank lines
|
|
125
|
+
lines.push(""); // First blank
|
|
126
|
+
lines.push(""); // Second blank
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return lines.join("\r\n");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Formats a changeset containing multiple non-GET operations
|
|
134
|
+
* @param requests - Array of request configurations (should be non-GET)
|
|
135
|
+
* @param baseUrl - The base URL to prepend to relative URLs
|
|
136
|
+
* @param changesetBoundary - Boundary string for the changeset
|
|
137
|
+
* @returns Formatted changeset string with CRLF line endings
|
|
138
|
+
*/
|
|
139
|
+
function formatChangeset(
|
|
140
|
+
requests: RequestConfig[],
|
|
141
|
+
baseUrl: string,
|
|
142
|
+
changesetBoundary: string,
|
|
143
|
+
): string {
|
|
144
|
+
const lines: string[] = [];
|
|
145
|
+
|
|
146
|
+
lines.push(`Content-Type: multipart/mixed; boundary=${changesetBoundary}`);
|
|
147
|
+
lines.push(""); // Empty line after headers
|
|
148
|
+
|
|
149
|
+
// Add each request in the changeset
|
|
150
|
+
for (const request of requests) {
|
|
151
|
+
lines.push(`--${changesetBoundary}`);
|
|
152
|
+
lines.push(formatSubRequest(request, baseUrl));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Close the changeset
|
|
156
|
+
lines.push(`--${changesetBoundary}--`);
|
|
157
|
+
|
|
158
|
+
return lines.join("\r\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Formats multiple requests into a batch request body
|
|
163
|
+
* @param requests - Array of request configurations
|
|
164
|
+
* @param baseUrl - The base URL to prepend to relative URLs
|
|
165
|
+
* @param batchBoundary - Optional boundary string for the batch (generated if not provided)
|
|
166
|
+
* @returns Object containing the formatted body and boundary
|
|
167
|
+
*/
|
|
168
|
+
export function formatBatchRequest(
|
|
169
|
+
requests: RequestConfig[],
|
|
170
|
+
baseUrl: string,
|
|
171
|
+
batchBoundary?: string,
|
|
172
|
+
): { body: string; boundary: string } {
|
|
173
|
+
const boundary = batchBoundary || generateBoundary("batch_");
|
|
174
|
+
const lines: string[] = [];
|
|
175
|
+
|
|
176
|
+
// Group requests: consecutive non-GET operations go into changesets
|
|
177
|
+
let currentChangeset: RequestConfig[] | null = null;
|
|
178
|
+
|
|
179
|
+
for (const request of requests) {
|
|
180
|
+
if (request.method === "GET") {
|
|
181
|
+
// GET operations break changesets and are added individually
|
|
182
|
+
if (currentChangeset) {
|
|
183
|
+
// Close and add the current changeset
|
|
184
|
+
const changesetBoundary = generateBoundary("changeset_");
|
|
185
|
+
lines.push(`--${boundary}`);
|
|
186
|
+
lines.push(
|
|
187
|
+
formatChangeset(currentChangeset, baseUrl, changesetBoundary),
|
|
188
|
+
);
|
|
189
|
+
currentChangeset = null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Add GET request
|
|
193
|
+
lines.push(`--${boundary}`);
|
|
194
|
+
lines.push(formatSubRequest(request, baseUrl));
|
|
195
|
+
} else {
|
|
196
|
+
// Non-GET operations: add to current changeset or create new one
|
|
197
|
+
if (!currentChangeset) {
|
|
198
|
+
currentChangeset = [];
|
|
199
|
+
}
|
|
200
|
+
currentChangeset.push(request);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Add any remaining changeset
|
|
205
|
+
if (currentChangeset) {
|
|
206
|
+
const changesetBoundary = generateBoundary("changeset_");
|
|
207
|
+
lines.push(`--${boundary}`);
|
|
208
|
+
lines.push(formatChangeset(currentChangeset, baseUrl, changesetBoundary));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Close the batch
|
|
212
|
+
lines.push(`--${boundary}--`);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
body: lines.join("\r\n"),
|
|
216
|
+
boundary,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Formats multiple Request objects into a batch request body
|
|
222
|
+
* Supports explicit changesets via Request arrays
|
|
223
|
+
* @param requests - Array of Request objects or Request arrays (for explicit changesets)
|
|
224
|
+
* @param baseUrl - The base URL to prepend to relative URLs
|
|
225
|
+
* @param batchBoundary - Optional boundary string for the batch (generated if not provided)
|
|
226
|
+
* @returns Promise resolving to object containing the formatted body and boundary
|
|
227
|
+
*/
|
|
228
|
+
export async function formatBatchRequestFromNative(
|
|
229
|
+
requests: Array<Request | Request[]>,
|
|
230
|
+
baseUrl: string,
|
|
231
|
+
batchBoundary?: string,
|
|
232
|
+
): Promise<{ body: string; boundary: string }> {
|
|
233
|
+
const boundary = batchBoundary || generateBoundary("batch_");
|
|
234
|
+
const lines: string[] = [];
|
|
235
|
+
|
|
236
|
+
for (const item of requests) {
|
|
237
|
+
if (Array.isArray(item)) {
|
|
238
|
+
// Explicit changeset - array of Requests
|
|
239
|
+
const changesetBoundary = generateBoundary("changeset_");
|
|
240
|
+
const changesetConfigs: RequestConfig[] = [];
|
|
241
|
+
|
|
242
|
+
for (const request of item) {
|
|
243
|
+
changesetConfigs.push(await requestToConfig(request));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
lines.push(`--${boundary}`);
|
|
247
|
+
lines.push(formatChangeset(changesetConfigs, baseUrl, changesetBoundary));
|
|
248
|
+
} else {
|
|
249
|
+
// Single request
|
|
250
|
+
const config = await requestToConfig(item);
|
|
251
|
+
|
|
252
|
+
if (config.method === "GET") {
|
|
253
|
+
// GET requests are always individual
|
|
254
|
+
lines.push(`--${boundary}`);
|
|
255
|
+
lines.push(formatSubRequest(config, baseUrl));
|
|
256
|
+
} else {
|
|
257
|
+
// Non-GET operations wrapped in a changeset
|
|
258
|
+
const changesetBoundary = generateBoundary("changeset_");
|
|
259
|
+
lines.push(`--${boundary}`);
|
|
260
|
+
lines.push(formatChangeset([config], baseUrl, changesetBoundary));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Close the batch
|
|
266
|
+
lines.push(`--${boundary}--`);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
body: lines.join("\r\n"),
|
|
270
|
+
boundary,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Extracts the boundary from a Content-Type header
|
|
276
|
+
* @param contentType - The Content-Type header value
|
|
277
|
+
* @returns The boundary string, or null if not found
|
|
278
|
+
*/
|
|
279
|
+
export function extractBoundary(contentType: string): string | null {
|
|
280
|
+
const match = contentType.match(/boundary=([^;]+)/);
|
|
281
|
+
return match && match[1] ? match[1].trim() : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Parses an HTTP response line (status line)
|
|
286
|
+
* @param line - The HTTP status line (e.g., "HTTP/1.1 200 OK")
|
|
287
|
+
* @returns Object containing status code and status text
|
|
288
|
+
*/
|
|
289
|
+
function parseStatusLine(line: string): {
|
|
290
|
+
status: number;
|
|
291
|
+
statusText: string;
|
|
292
|
+
} {
|
|
293
|
+
const match = line.match(/HTTP\/\d\.\d\s+(\d+)\s*(.*)/);
|
|
294
|
+
if (!match || !match[1]) {
|
|
295
|
+
return { status: 0, statusText: "" };
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
status: parseInt(match[1], 10),
|
|
299
|
+
statusText: match[2]?.trim() || "",
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Parses headers from an array of header lines
|
|
305
|
+
* @param lines - Array of header lines
|
|
306
|
+
* @returns Object containing parsed headers
|
|
307
|
+
*/
|
|
308
|
+
function parseHeaders(lines: string[]): Record<string, string> {
|
|
309
|
+
const headers: Record<string, string> = {};
|
|
310
|
+
for (const line of lines) {
|
|
311
|
+
const colonIndex = line.indexOf(":");
|
|
312
|
+
if (colonIndex > 0) {
|
|
313
|
+
const key = line.substring(0, colonIndex).trim();
|
|
314
|
+
const value = line.substring(colonIndex + 1).trim();
|
|
315
|
+
headers[key.toLowerCase()] = value;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return headers;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Parses a single HTTP response from a batch part
|
|
323
|
+
* @param part - The raw HTTP response string
|
|
324
|
+
* @returns Parsed response object
|
|
325
|
+
*/
|
|
326
|
+
function parseHttpResponse(part: string): ParsedBatchResponse {
|
|
327
|
+
const lines = part.split(/\r\n/);
|
|
328
|
+
|
|
329
|
+
// Find the HTTP status line (skip multipart headers)
|
|
330
|
+
let statusLineIndex = -1;
|
|
331
|
+
for (let i = 0; i < lines.length; i++) {
|
|
332
|
+
const line = lines[i];
|
|
333
|
+
if (line && line.startsWith("HTTP/")) {
|
|
334
|
+
statusLineIndex = i;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (statusLineIndex === -1) {
|
|
340
|
+
return {
|
|
341
|
+
status: 0,
|
|
342
|
+
statusText: "Invalid response",
|
|
343
|
+
headers: {},
|
|
344
|
+
body: null,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const statusLine = lines[statusLineIndex];
|
|
349
|
+
if (!statusLine) {
|
|
350
|
+
return {
|
|
351
|
+
status: 0,
|
|
352
|
+
statusText: "Invalid response",
|
|
353
|
+
headers: {},
|
|
354
|
+
body: null,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const { status, statusText } = parseStatusLine(statusLine);
|
|
359
|
+
|
|
360
|
+
// Parse headers (between status line and empty line)
|
|
361
|
+
const headerLines: string[] = [];
|
|
362
|
+
let bodyStartIndex = lines.length; // Default to end of lines (no body)
|
|
363
|
+
let foundEmptyLine = false;
|
|
364
|
+
|
|
365
|
+
for (let i = statusLineIndex + 1; i < lines.length; i++) {
|
|
366
|
+
const line = lines[i];
|
|
367
|
+
if (line === "") {
|
|
368
|
+
bodyStartIndex = i + 1;
|
|
369
|
+
foundEmptyLine = true;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
// Stop at boundary markers (for responses without bodies like 204)
|
|
373
|
+
if (line && line.startsWith("--")) {
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
if (line) {
|
|
377
|
+
headerLines.push(line);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const headers = parseHeaders(headerLines);
|
|
382
|
+
|
|
383
|
+
// Parse body (everything after the empty line, if there was one)
|
|
384
|
+
let bodyText = "";
|
|
385
|
+
if (foundEmptyLine && bodyStartIndex < lines.length) {
|
|
386
|
+
const bodyLines = lines.slice(bodyStartIndex);
|
|
387
|
+
// Stop at boundary markers
|
|
388
|
+
const bodyLinesFiltered: string[] = [];
|
|
389
|
+
for (const line of bodyLines) {
|
|
390
|
+
if (line.startsWith("--")) {
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
bodyLinesFiltered.push(line);
|
|
394
|
+
}
|
|
395
|
+
bodyText = bodyLinesFiltered.join("\r\n").trim();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
let body: any = null;
|
|
399
|
+
if (bodyText) {
|
|
400
|
+
try {
|
|
401
|
+
body = JSON.parse(bodyText);
|
|
402
|
+
} catch {
|
|
403
|
+
// If not JSON, return as text
|
|
404
|
+
body = bodyText;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
status,
|
|
410
|
+
statusText,
|
|
411
|
+
headers,
|
|
412
|
+
body,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Parses a batch response into individual responses
|
|
418
|
+
* @param responseText - The raw batch response text
|
|
419
|
+
* @param contentType - The Content-Type header from the response
|
|
420
|
+
* @returns Array of parsed responses in the same order as the request
|
|
421
|
+
*/
|
|
422
|
+
export function parseBatchResponse(
|
|
423
|
+
responseText: string,
|
|
424
|
+
contentType: string,
|
|
425
|
+
): ParsedBatchResponse[] {
|
|
426
|
+
const boundary = extractBoundary(contentType);
|
|
427
|
+
if (!boundary) {
|
|
428
|
+
throw new Error("Could not extract boundary from Content-Type header");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const results: ParsedBatchResponse[] = [];
|
|
432
|
+
|
|
433
|
+
// Split by boundary (handle both --boundary and --boundary--)
|
|
434
|
+
const boundaryPattern = `--${boundary}`;
|
|
435
|
+
const parts = responseText.split(boundaryPattern);
|
|
436
|
+
|
|
437
|
+
for (const part of parts) {
|
|
438
|
+
const trimmedPart = part.trim();
|
|
439
|
+
|
|
440
|
+
// Skip empty parts and the closing boundary marker
|
|
441
|
+
if (!trimmedPart || trimmedPart === "--") {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Check if this part is a changeset (nested multipart)
|
|
446
|
+
if (trimmedPart.includes("Content-Type: multipart/mixed")) {
|
|
447
|
+
// Extract the changeset boundary
|
|
448
|
+
const changesetContentTypeMatch = trimmedPart.match(
|
|
449
|
+
/Content-Type: multipart\/mixed;\s*boundary=([^\r\n]+)/,
|
|
450
|
+
);
|
|
451
|
+
if (changesetContentTypeMatch) {
|
|
452
|
+
const changesetBoundary = changesetContentTypeMatch?.[1]?.trim();
|
|
453
|
+
const changesetPattern = `--${changesetBoundary}`;
|
|
454
|
+
const changesetParts = trimmedPart.split(changesetPattern);
|
|
455
|
+
|
|
456
|
+
for (const changesetPart of changesetParts) {
|
|
457
|
+
const trimmedChangesetPart = changesetPart.trim();
|
|
458
|
+
if (!trimmedChangesetPart || trimmedChangesetPart === "--") {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Skip the changeset header
|
|
463
|
+
if (
|
|
464
|
+
trimmedChangesetPart.startsWith("Content-Type: multipart/mixed")
|
|
465
|
+
) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const response = parseHttpResponse(trimmedChangesetPart);
|
|
470
|
+
if (response.status > 0) {
|
|
471
|
+
results.push(response);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
// Regular response (not a changeset)
|
|
477
|
+
const response = parseHttpResponse(trimmedPart);
|
|
478
|
+
if (response.status > 0) {
|
|
479
|
+
results.push(response);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return results;
|
|
485
|
+
}
|