@proofkit/fmodata 0.1.0-alpha.9 → 0.1.0-beta.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +655 -453
  3. package/dist/esm/client/batch-builder.d.ts +10 -9
  4. package/dist/esm/client/batch-builder.js +119 -56
  5. package/dist/esm/client/batch-builder.js.map +1 -1
  6. package/dist/esm/client/batch-request.js +16 -21
  7. package/dist/esm/client/batch-request.js.map +1 -1
  8. package/dist/esm/client/builders/default-select.d.ts +10 -0
  9. package/dist/esm/client/builders/default-select.js +41 -0
  10. package/dist/esm/client/builders/default-select.js.map +1 -0
  11. package/dist/esm/client/builders/expand-builder.d.ts +45 -0
  12. package/dist/esm/client/builders/expand-builder.js +185 -0
  13. package/dist/esm/client/builders/expand-builder.js.map +1 -0
  14. package/dist/esm/client/builders/index.d.ts +9 -0
  15. package/dist/esm/client/builders/query-string-builder.d.ts +18 -0
  16. package/dist/esm/client/builders/query-string-builder.js +21 -0
  17. package/dist/esm/client/builders/query-string-builder.js.map +1 -0
  18. package/dist/esm/client/builders/response-processor.d.ts +43 -0
  19. package/dist/esm/client/builders/response-processor.js +175 -0
  20. package/dist/esm/client/builders/response-processor.js.map +1 -0
  21. package/dist/esm/client/builders/select-mixin.d.ts +25 -0
  22. package/dist/esm/client/builders/select-mixin.js +28 -0
  23. package/dist/esm/client/builders/select-mixin.js.map +1 -0
  24. package/dist/esm/client/builders/select-utils.d.ts +18 -0
  25. package/dist/esm/client/builders/select-utils.js +30 -0
  26. package/dist/esm/client/builders/select-utils.js.map +1 -0
  27. package/dist/esm/client/builders/shared-types.d.ts +40 -0
  28. package/dist/esm/client/builders/table-utils.d.ts +35 -0
  29. package/dist/esm/client/builders/table-utils.js +44 -0
  30. package/dist/esm/client/builders/table-utils.js.map +1 -0
  31. package/dist/esm/client/database.d.ts +34 -22
  32. package/dist/esm/client/database.js +48 -84
  33. package/dist/esm/client/database.js.map +1 -1
  34. package/dist/esm/client/delete-builder.d.ts +25 -30
  35. package/dist/esm/client/delete-builder.js +45 -30
  36. package/dist/esm/client/delete-builder.js.map +1 -1
  37. package/dist/esm/client/entity-set.d.ts +35 -43
  38. package/dist/esm/client/entity-set.js +126 -52
  39. package/dist/esm/client/entity-set.js.map +1 -1
  40. package/dist/esm/client/error-parser.d.ts +12 -0
  41. package/dist/esm/client/error-parser.js +25 -0
  42. package/dist/esm/client/error-parser.js.map +1 -0
  43. package/dist/esm/client/filemaker-odata.d.ts +26 -7
  44. package/dist/esm/client/filemaker-odata.js +65 -42
  45. package/dist/esm/client/filemaker-odata.js.map +1 -1
  46. package/dist/esm/client/insert-builder.d.ts +19 -24
  47. package/dist/esm/client/insert-builder.js +94 -58
  48. package/dist/esm/client/insert-builder.js.map +1 -1
  49. package/dist/esm/client/query/expand-builder.d.ts +35 -0
  50. package/dist/esm/client/query/index.d.ts +4 -0
  51. package/dist/esm/client/query/query-builder.d.ts +132 -0
  52. package/dist/esm/client/query/query-builder.js +456 -0
  53. package/dist/esm/client/query/query-builder.js.map +1 -0
  54. package/dist/esm/client/query/response-processor.d.ts +25 -0
  55. package/dist/esm/client/query/types.d.ts +77 -0
  56. package/dist/esm/client/query/url-builder.d.ts +71 -0
  57. package/dist/esm/client/query/url-builder.js +100 -0
  58. package/dist/esm/client/query/url-builder.js.map +1 -0
  59. package/dist/esm/client/query-builder.d.ts +2 -115
  60. package/dist/esm/client/record-builder.d.ts +108 -36
  61. package/dist/esm/client/record-builder.js +284 -119
  62. package/dist/esm/client/record-builder.js.map +1 -1
  63. package/dist/esm/client/response-processor.d.ts +4 -9
  64. package/dist/esm/client/sanitize-json.d.ts +35 -0
  65. package/dist/esm/client/sanitize-json.js +27 -0
  66. package/dist/esm/client/sanitize-json.js.map +1 -0
  67. package/dist/esm/client/schema-manager.d.ts +5 -5
  68. package/dist/esm/client/schema-manager.js +45 -31
  69. package/dist/esm/client/schema-manager.js.map +1 -1
  70. package/dist/esm/client/update-builder.d.ts +34 -40
  71. package/dist/esm/client/update-builder.js +99 -58
  72. package/dist/esm/client/update-builder.js.map +1 -1
  73. package/dist/esm/client/webhook-builder.d.ts +126 -0
  74. package/dist/esm/client/webhook-builder.js +189 -0
  75. package/dist/esm/client/webhook-builder.js.map +1 -0
  76. package/dist/esm/errors.d.ts +19 -2
  77. package/dist/esm/errors.js +39 -4
  78. package/dist/esm/errors.js.map +1 -1
  79. package/dist/esm/index.d.ts +10 -8
  80. package/dist/esm/index.js +40 -10
  81. package/dist/esm/index.js.map +1 -1
  82. package/dist/esm/logger.d.ts +47 -0
  83. package/dist/esm/logger.js +69 -0
  84. package/dist/esm/logger.js.map +1 -0
  85. package/dist/esm/logger.test.d.ts +1 -0
  86. package/dist/esm/orm/column.d.ts +62 -0
  87. package/dist/esm/orm/column.js +63 -0
  88. package/dist/esm/orm/column.js.map +1 -0
  89. package/dist/esm/orm/field-builders.d.ts +164 -0
  90. package/dist/esm/orm/field-builders.js +158 -0
  91. package/dist/esm/orm/field-builders.js.map +1 -0
  92. package/dist/esm/orm/index.d.ts +5 -0
  93. package/dist/esm/orm/operators.d.ts +173 -0
  94. package/dist/esm/orm/operators.js +260 -0
  95. package/dist/esm/orm/operators.js.map +1 -0
  96. package/dist/esm/orm/table.d.ts +355 -0
  97. package/dist/esm/orm/table.js +202 -0
  98. package/dist/esm/orm/table.js.map +1 -0
  99. package/dist/esm/transform.d.ts +20 -21
  100. package/dist/esm/transform.js +44 -45
  101. package/dist/esm/transform.js.map +1 -1
  102. package/dist/esm/types.d.ts +96 -30
  103. package/dist/esm/types.js +7 -0
  104. package/dist/esm/types.js.map +1 -0
  105. package/dist/esm/validation.d.ts +22 -12
  106. package/dist/esm/validation.js +132 -85
  107. package/dist/esm/validation.js.map +1 -1
  108. package/package.json +34 -29
  109. package/src/client/batch-builder.ts +153 -89
  110. package/src/client/batch-request.ts +25 -41
  111. package/src/client/builders/default-select.ts +75 -0
  112. package/src/client/builders/expand-builder.ts +246 -0
  113. package/src/client/builders/index.ts +11 -0
  114. package/src/client/builders/query-string-builder.ts +46 -0
  115. package/src/client/builders/response-processor.ts +279 -0
  116. package/src/client/builders/select-mixin.ts +65 -0
  117. package/src/client/builders/select-utils.ts +59 -0
  118. package/src/client/builders/shared-types.ts +45 -0
  119. package/src/client/builders/table-utils.ts +83 -0
  120. package/src/client/database.ts +89 -183
  121. package/src/client/delete-builder.ts +74 -84
  122. package/src/client/entity-set.ts +286 -293
  123. package/src/client/error-parser.ts +41 -0
  124. package/src/client/filemaker-odata.ts +98 -66
  125. package/src/client/insert-builder.ts +157 -118
  126. package/src/client/query/expand-builder.ts +160 -0
  127. package/src/client/query/index.ts +14 -0
  128. package/src/client/query/query-builder.ts +729 -0
  129. package/src/client/query/response-processor.ts +226 -0
  130. package/src/client/query/types.ts +126 -0
  131. package/src/client/query/url-builder.ts +151 -0
  132. package/src/client/query-builder.ts +10 -1455
  133. package/src/client/record-builder.ts +575 -240
  134. package/src/client/response-processor.ts +15 -42
  135. package/src/client/sanitize-json.ts +64 -0
  136. package/src/client/schema-manager.ts +61 -76
  137. package/src/client/update-builder.ts +161 -143
  138. package/src/client/webhook-builder.ts +265 -0
  139. package/src/errors.ts +49 -16
  140. package/src/index.ts +99 -54
  141. package/src/logger.test.ts +34 -0
  142. package/src/logger.ts +116 -0
  143. package/src/orm/column.ts +106 -0
  144. package/src/orm/field-builders.ts +250 -0
  145. package/src/orm/index.ts +61 -0
  146. package/src/orm/operators.ts +473 -0
  147. package/src/orm/table.ts +741 -0
  148. package/src/transform.ts +90 -70
  149. package/src/types.ts +154 -113
  150. package/src/validation.ts +200 -115
  151. package/dist/esm/client/base-table.d.ts +0 -125
  152. package/dist/esm/client/base-table.js +0 -57
  153. package/dist/esm/client/base-table.js.map +0 -1
  154. package/dist/esm/client/query-builder.js +0 -896
  155. package/dist/esm/client/query-builder.js.map +0 -1
  156. package/dist/esm/client/table-occurrence.d.ts +0 -72
  157. package/dist/esm/client/table-occurrence.js +0 -74
  158. package/dist/esm/client/table-occurrence.js.map +0 -1
  159. package/dist/esm/filter-types.d.ts +0 -76
  160. package/src/client/base-table.ts +0 -175
  161. package/src/client/query-builder.ts.bak +0 -1457
  162. package/src/client/table-occurrence.ts +0 -175
  163. package/src/filter-types.ts +0 -97
@@ -1,5 +1,4 @@
1
- import { ExecutableBuilder, ExecutionContext, Result, ExecuteOptions } from '../types.js';
2
- import { FFetchOptions } from '@fetchkit/ffetch';
1
+ import { BatchResult, ExecutableBuilder, ExecuteMethodOptions, ExecuteOptions, ExecutionContext, Result } from '../types.js';
3
2
  /**
4
3
  * Helper type to extract result types from a tuple of ExecutableBuilders.
5
4
  * Uses a mapped type which TypeScript 4.1+ can handle for tuples.
@@ -10,12 +9,14 @@ type ExtractTupleTypes<T extends readonly ExecutableBuilder<any>[]> = {
10
9
  /**
11
10
  * Builder for batch operations that allows multiple queries to be executed together
12
11
  * in a single transactional request.
12
+ *
13
+ * Note: BatchBuilder does not implement ExecutableBuilder because execute() returns
14
+ * BatchResult instead of Result, which is a different return type structure.
13
15
  */
14
- export declare class BatchBuilder<Builders extends readonly ExecutableBuilder<any>[]> implements ExecutableBuilder<ExtractTupleTypes<Builders>> {
16
+ export declare class BatchBuilder<Builders extends readonly ExecutableBuilder<any>[]> {
17
+ private readonly builders;
15
18
  private readonly databaseName;
16
19
  private readonly context;
17
- private builders;
18
- private readonly originalBuilders;
19
20
  constructor(builders: Builders, databaseName: string, context: ExecutionContext);
20
21
  /**
21
22
  * Add a request to the batch dynamically.
@@ -41,14 +42,14 @@ export declare class BatchBuilder<Builders extends readonly ExecutableBuilder<an
41
42
  url: string;
42
43
  body?: any;
43
44
  };
44
- toRequest(baseUrl: string): Request;
45
- processResponse(response: Response, options?: ExecuteOptions): Promise<Result<any>>;
45
+ toRequest(baseUrl: string, _options?: ExecuteOptions): Request;
46
+ processResponse(_response: Response, _options?: ExecuteOptions): Promise<Result<any>>;
46
47
  /**
47
48
  * Execute the batch operation.
48
49
  *
49
50
  * @param options - Optional fetch options and batch-specific options (includes beforeRequest hook)
50
- * @returns A tuple of results matching the input builders
51
+ * @returns A BatchResult containing individual results for each operation
51
52
  */
52
- execute<EO extends ExecuteOptions>(options?: RequestInit & FFetchOptions & EO): Promise<Result<ExtractTupleTypes<Builders>>>;
53
+ execute<EO extends ExecuteOptions>(options?: ExecuteMethodOptions<EO>): Promise<BatchResult<ExtractTupleTypes<Builders>>>;
53
54
  }
54
55
  export {};
@@ -1,6 +1,7 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ import { BatchTruncatedError } from "../errors.js";
4
5
  import { formatBatchRequestFromNative, parseBatchResponse } from "./batch-request.js";
5
6
  function parsedToResponse(parsed) {
6
7
  const headers = new Headers(parsed.headers);
@@ -24,12 +25,13 @@ function parsedToResponse(parsed) {
24
25
  }
25
26
  class BatchBuilder {
26
27
  constructor(builders, databaseName, context) {
28
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type
27
29
  __publicField(this, "builders");
28
- __publicField(this, "originalBuilders");
30
+ __publicField(this, "databaseName");
31
+ __publicField(this, "context");
32
+ this.builders = [...builders];
29
33
  this.databaseName = databaseName;
30
34
  this.context = context;
31
- this.builders = [...builders];
32
- this.originalBuilders = builders;
33
35
  }
34
36
  /**
35
37
  * Add a request to the batch dynamically.
@@ -53,6 +55,7 @@ class BatchBuilder {
53
55
  * Get the request configuration for this batch operation.
54
56
  * This is used internally by the execution system.
55
57
  */
58
+ // biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value
56
59
  getRequestConfig() {
57
60
  return {
58
61
  method: "POST",
@@ -61,7 +64,7 @@ class BatchBuilder {
61
64
  // Body is constructed in execute()
62
65
  };
63
66
  }
64
- toRequest(baseUrl) {
67
+ toRequest(baseUrl, _options) {
65
68
  const fullUrl = `${baseUrl}/${this.databaseName}/$batch`;
66
69
  return new Request(fullUrl, {
67
70
  method: "POST",
@@ -71,104 +74,164 @@ class BatchBuilder {
71
74
  }
72
75
  });
73
76
  }
74
- async processResponse(response, options) {
75
- return {
77
+ // biome-ignore lint/suspicious/noExplicitAny: Generic return type for interface compliance
78
+ processResponse(_response, _options) {
79
+ return Promise.resolve({
76
80
  data: void 0,
77
81
  error: {
78
82
  name: "NotImplementedError",
79
83
  message: "Batch operations handle response processing internally",
80
84
  timestamp: /* @__PURE__ */ new Date()
85
+ // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object
81
86
  }
82
- };
87
+ });
83
88
  }
84
89
  /**
85
90
  * Execute the batch operation.
86
91
  *
87
92
  * @param options - Optional fetch options and batch-specific options (includes beforeRequest hook)
88
- * @returns A tuple of results matching the input builders
93
+ * @returns A BatchResult containing individual results for each operation
89
94
  */
90
95
  async execute(options) {
91
96
  var _a, _b;
92
97
  const baseUrl = (_b = (_a = this.context)._getBaseUrl) == null ? void 0 : _b.call(_a);
93
98
  if (!baseUrl) {
94
- return {
99
+ const errorCount = this.builders.length;
100
+ const results = this.builders.map((_, _i) => ({
95
101
  data: void 0,
96
102
  error: {
97
103
  name: "ConfigurationError",
98
104
  message: "Base URL not available - execution context must implement _getBaseUrl()",
99
105
  timestamp: /* @__PURE__ */ new Date()
100
- }
106
+ // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object
107
+ },
108
+ status: 0
109
+ }));
110
+ return {
111
+ // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type
112
+ results,
113
+ successCount: 0,
114
+ errorCount,
115
+ truncated: false,
116
+ firstErrorIndex: 0
101
117
  };
102
118
  }
103
119
  try {
104
- const requests = this.builders.map(
105
- (builder) => builder.toRequest(baseUrl)
106
- );
107
- const { body, boundary } = await formatBatchRequestFromNative(
108
- requests,
109
- baseUrl
110
- );
111
- const response = await this.context._makeRequest(
112
- `/${this.databaseName}/$batch`,
113
- {
114
- ...options,
115
- method: "POST",
116
- headers: {
117
- ...options == null ? void 0 : options.headers,
118
- "Content-Type": `multipart/mixed; boundary=${boundary}`,
119
- "OData-Version": "4.0"
120
- },
121
- body
122
- }
123
- );
120
+ const requests = this.builders.map((builder) => builder.toRequest(baseUrl, options));
121
+ const { body, boundary } = await formatBatchRequestFromNative(requests, baseUrl);
122
+ const response = await this.context._makeRequest(`/${this.databaseName}/$batch`, {
123
+ ...options,
124
+ method: "POST",
125
+ headers: {
126
+ ...options == null ? void 0 : options.headers,
127
+ "Content-Type": `multipart/mixed; boundary=${boundary}`,
128
+ "OData-Version": "4.0"
129
+ },
130
+ body
131
+ });
124
132
  if (response.error) {
125
- return { data: void 0, error: response.error };
133
+ const errorCount2 = this.builders.length;
134
+ const results2 = this.builders.map((_, _i) => ({
135
+ data: void 0,
136
+ error: response.error,
137
+ status: 0
138
+ }));
139
+ return {
140
+ // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type
141
+ results: results2,
142
+ successCount: 0,
143
+ errorCount: errorCount2,
144
+ truncated: false,
145
+ firstErrorIndex: 0
146
+ };
126
147
  }
127
148
  const firstLine = response.data.split("\r\n")[0] || response.data.split("\n")[0] || "";
128
149
  const actualBoundary = firstLine.startsWith("--") ? firstLine.substring(2) : boundary;
129
150
  const contentTypeHeader = `multipart/mixed; boundary=${actualBoundary}`;
130
- const parsedResponses = parseBatchResponse(
131
- response.data,
132
- contentTypeHeader
133
- );
134
- if (parsedResponses.length !== this.builders.length) {
135
- return {
136
- data: void 0,
137
- error: {
138
- name: "BatchError",
139
- message: `Expected ${this.builders.length} responses but got ${parsedResponses.length}`,
140
- timestamp: /* @__PURE__ */ new Date()
141
- }
142
- };
143
- }
144
- const processedResults = [];
145
- for (let i = 0; i < this.originalBuilders.length; i++) {
146
- const builder = this.originalBuilders[i];
151
+ const parsedResponses = parseBatchResponse(response.data, contentTypeHeader);
152
+ const results = [];
153
+ let successCount = 0;
154
+ let errorCount = 0;
155
+ let firstErrorIndex = null;
156
+ const truncated = parsedResponses.length < this.builders.length;
157
+ for (let i = 0; i < this.builders.length; i++) {
158
+ const builder = this.builders[i];
147
159
  const parsed = parsedResponses[i];
148
- if (!builder || !parsed) {
149
- processedResults.push(void 0);
160
+ if (!parsed) {
161
+ const failedAtIndex = firstErrorIndex ?? i;
162
+ results.push({
163
+ data: void 0,
164
+ error: new BatchTruncatedError(i, failedAtIndex),
165
+ status: 0
166
+ });
167
+ errorCount++;
168
+ continue;
169
+ }
170
+ if (!builder) {
171
+ results.push({
172
+ data: void 0,
173
+ error: {
174
+ name: "BatchError",
175
+ message: `Builder at index ${i} is undefined`,
176
+ timestamp: /* @__PURE__ */ new Date()
177
+ // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object
178
+ },
179
+ status: parsed.status
180
+ });
181
+ errorCount++;
182
+ if (firstErrorIndex === null) {
183
+ firstErrorIndex = i;
184
+ }
150
185
  continue;
151
186
  }
152
187
  const nativeResponse = parsedToResponse(parsed);
153
188
  const result = await builder.processResponse(nativeResponse, options);
154
189
  if (result.error) {
155
- processedResults.push(void 0);
190
+ results.push({
191
+ data: void 0,
192
+ error: result.error,
193
+ status: parsed.status
194
+ });
195
+ errorCount++;
196
+ if (firstErrorIndex === null) {
197
+ firstErrorIndex = i;
198
+ }
156
199
  } else {
157
- processedResults.push(result.data);
200
+ results.push({
201
+ data: result.data,
202
+ error: void 0,
203
+ status: parsed.status
204
+ });
205
+ successCount++;
158
206
  }
159
207
  }
160
208
  return {
161
- data: processedResults,
162
- error: void 0
209
+ // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type
210
+ results,
211
+ successCount,
212
+ errorCount,
213
+ truncated,
214
+ firstErrorIndex
163
215
  };
164
216
  } catch (err) {
165
- return {
217
+ const errorCount = this.builders.length;
218
+ const results = this.builders.map((_, _i) => ({
166
219
  data: void 0,
167
220
  error: {
168
221
  name: "BatchError",
169
222
  message: err instanceof Error ? err.message : "Unknown error",
170
223
  timestamp: /* @__PURE__ */ new Date()
171
- }
224
+ // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object
225
+ },
226
+ status: 0
227
+ }));
228
+ return {
229
+ // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type
230
+ results,
231
+ successCount: 0,
232
+ errorCount,
233
+ truncated: false,
234
+ firstErrorIndex: 0
172
235
  };
173
236
  }
174
237
  }
@@ -1 +1 @@
1
- {"version":3,"file":"batch-builder.js","sources":["../../../src/client/batch-builder.ts"],"sourcesContent":["import type {\n ExecutableBuilder,\n ExecutionContext,\n Result,\n ExecuteOptions,\n} from \"../types\";\nimport { type FFetchOptions } from \"@fetchkit/ffetch\";\nimport {\n formatBatchRequestFromNative,\n parseBatchResponse,\n type ParsedBatchResponse,\n} from \"./batch-request\";\n\n/**\n * Helper type to extract result types from a tuple of ExecutableBuilders.\n * Uses a mapped type which TypeScript 4.1+ can handle for tuples.\n */\ntype ExtractTupleTypes<T extends readonly ExecutableBuilder<any>[]> = {\n [K in keyof T]: T[K] extends ExecutableBuilder<infer U> ? U : never;\n};\n\n/**\n * Converts a ParsedBatchResponse to a native Response object\n * @param parsed - The parsed batch response\n * @returns A native Response object\n */\nfunction parsedToResponse(parsed: ParsedBatchResponse): Response {\n const headers = new Headers(parsed.headers);\n\n // Handle null body\n if (parsed.body === null || parsed.body === undefined) {\n return new Response(null, {\n status: parsed.status,\n statusText: parsed.statusText,\n headers,\n });\n }\n\n // Convert body to string if it's not already\n const bodyString =\n typeof parsed.body === \"string\" ? parsed.body : JSON.stringify(parsed.body);\n\n // Handle 204 No Content status - it cannot have a body per HTTP spec\n // If FileMaker returns 204 with a body, treat it as 200\n let status = parsed.status;\n if (status === 204 && bodyString && bodyString.trim() !== \"\") {\n status = 200;\n }\n\n return new Response(status === 204 ? null : bodyString, {\n status: status,\n statusText: parsed.statusText,\n headers,\n });\n}\n\n/**\n * Builder for batch operations that allows multiple queries to be executed together\n * in a single transactional request.\n */\nexport class BatchBuilder<Builders extends readonly ExecutableBuilder<any>[]>\n implements ExecutableBuilder<ExtractTupleTypes<Builders>>\n{\n private builders: ExecutableBuilder<any>[];\n private readonly originalBuilders: Builders;\n\n constructor(\n builders: Builders,\n private readonly databaseName: string,\n private readonly context: ExecutionContext,\n ) {\n // Convert readonly tuple to mutable array for dynamic additions\n this.builders = [...builders];\n // Store original tuple for type preservation\n this.originalBuilders = builders;\n }\n\n /**\n * Add a request to the batch dynamically.\n * This allows building up batch operations programmatically.\n *\n * @param builder - An executable builder to add to the batch\n * @returns This BatchBuilder for method chaining\n * @example\n * ```ts\n * const batch = db.batch([]);\n * batch.addRequest(db.from('contacts').list());\n * batch.addRequest(db.from('users').list());\n * const result = await batch.execute();\n * ```\n */\n addRequest<T>(builder: ExecutableBuilder<T>): this {\n this.builders.push(builder);\n return this;\n }\n\n /**\n * Get the request configuration for this batch operation.\n * This is used internally by the execution system.\n */\n getRequestConfig(): { method: string; url: string; body?: any } {\n // Note: This method is kept for compatibility but batch operations\n // should use execute() directly which handles the full Request/Response flow\n return {\n method: \"POST\",\n url: `/${this.databaseName}/$batch`,\n body: undefined, // Body is constructed in execute()\n };\n }\n\n toRequest(baseUrl: string): Request {\n // Batch operations are not designed to be nested, but we provide\n // a basic implementation for interface compliance\n const fullUrl = `${baseUrl}/${this.databaseName}/$batch`;\n return new Request(fullUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"multipart/mixed\",\n \"OData-Version\": \"4.0\",\n },\n });\n }\n\n async processResponse(\n response: Response,\n options?: ExecuteOptions,\n ): Promise<Result<any>> {\n // This should not typically be called for batch operations\n // as they handle their own response processing\n return {\n data: undefined,\n error: {\n name: \"NotImplementedError\",\n message: \"Batch operations handle response processing internally\",\n timestamp: new Date(),\n } as any,\n };\n }\n\n /**\n * Execute the batch operation.\n *\n * @param options - Optional fetch options and batch-specific options (includes beforeRequest hook)\n * @returns A tuple of results matching the input builders\n */\n async execute<EO extends ExecuteOptions>(\n options?: RequestInit & FFetchOptions & EO,\n ): Promise<Result<ExtractTupleTypes<Builders>>> {\n const baseUrl = this.context._getBaseUrl?.();\n if (!baseUrl) {\n return {\n data: undefined,\n error: {\n name: \"ConfigurationError\",\n message:\n \"Base URL not available - execution context must implement _getBaseUrl()\",\n timestamp: new Date(),\n } as any,\n };\n }\n\n try {\n // Convert builders to native Request objects\n const requests: Request[] = this.builders.map((builder) =>\n builder.toRequest(baseUrl),\n );\n\n // Format batch request (automatically groups mutations into changesets)\n const { body, boundary } = await formatBatchRequestFromNative(\n requests,\n baseUrl,\n );\n\n // Execute the batch request\n const response = await this.context._makeRequest<string>(\n `/${this.databaseName}/$batch`,\n {\n ...options,\n method: \"POST\",\n headers: {\n ...options?.headers,\n \"Content-Type\": `multipart/mixed; boundary=${boundary}`,\n \"OData-Version\": \"4.0\",\n },\n body,\n },\n );\n\n if (response.error) {\n return { data: undefined, error: response.error };\n }\n\n // Extract the actual boundary from the response\n // FileMaker uses its own boundary, not the one we sent\n const firstLine =\n response.data.split(\"\\r\\n\")[0] || response.data.split(\"\\n\")[0] || \"\";\n const actualBoundary = firstLine.startsWith(\"--\")\n ? firstLine.substring(2)\n : boundary;\n\n // Parse the multipart response\n const contentTypeHeader = `multipart/mixed; boundary=${actualBoundary}`;\n const parsedResponses = parseBatchResponse(\n response.data,\n contentTypeHeader,\n );\n\n // Check if we got the expected number of responses\n if (parsedResponses.length !== this.builders.length) {\n return {\n data: undefined,\n error: {\n name: \"BatchError\",\n message: `Expected ${this.builders.length} responses but got ${parsedResponses.length}`,\n timestamp: new Date(),\n } as any,\n };\n }\n\n // Process each response using the corresponding builder\n // Build tuple by processing each builder in order\n type ResultTuple = ExtractTupleTypes<Builders>;\n\n // Process builders sequentially to preserve tuple order\n const processedResults: any[] = [];\n for (let i = 0; i < this.originalBuilders.length; i++) {\n const builder = this.originalBuilders[i];\n const parsed = parsedResponses[i];\n\n if (!builder || !parsed) {\n processedResults.push(undefined);\n continue;\n }\n\n // Convert parsed response to native Response\n const nativeResponse = parsedToResponse(parsed);\n\n // Let the builder process its own response\n const result = await builder.processResponse(nativeResponse, options);\n\n if (result.error) {\n processedResults.push(undefined);\n } else {\n processedResults.push(result.data);\n }\n }\n\n // Use a type assertion that TypeScript will respect\n // ExtractTupleTypes ensures this is a proper tuple type\n return {\n data: processedResults as unknown as ResultTuple,\n error: undefined,\n };\n } catch (err) {\n return {\n data: undefined,\n error: {\n name: \"BatchError\",\n message: err instanceof Error ? err.message : \"Unknown error\",\n timestamp: new Date(),\n } as any,\n };\n }\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"batch-builder.js","sources":["../../../src/client/batch-builder.ts"],"sourcesContent":["import { BatchTruncatedError } from \"../errors\";\nimport type {\n BatchItemResult,\n BatchResult,\n ExecutableBuilder,\n ExecuteMethodOptions,\n ExecuteOptions,\n ExecutionContext,\n Result,\n} from \"../types\";\nimport { formatBatchRequestFromNative, type ParsedBatchResponse, parseBatchResponse } from \"./batch-request\";\n\n/**\n * Helper type to extract result types from a tuple of ExecutableBuilders.\n * Uses a mapped type which TypeScript 4.1+ can handle for tuples.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type\ntype ExtractTupleTypes<T extends readonly ExecutableBuilder<any>[]> = {\n [K in keyof T]: T[K] extends ExecutableBuilder<infer U> ? U : never;\n};\n\n/**\n * Converts a ParsedBatchResponse to a native Response object\n * @param parsed - The parsed batch response\n * @returns A native Response object\n */\nfunction parsedToResponse(parsed: ParsedBatchResponse): Response {\n const headers = new Headers(parsed.headers);\n\n // Handle null body\n if (parsed.body === null || parsed.body === undefined) {\n return new Response(null, {\n status: parsed.status,\n statusText: parsed.statusText,\n headers,\n });\n }\n\n // Convert body to string if it's not already\n const bodyString = typeof parsed.body === \"string\" ? parsed.body : JSON.stringify(parsed.body);\n\n // Handle 204 No Content status - it cannot have a body per HTTP spec\n // If FileMaker returns 204 with a body, treat it as 200\n let status = parsed.status;\n if (status === 204 && bodyString && bodyString.trim() !== \"\") {\n status = 200;\n }\n\n return new Response(status === 204 ? null : bodyString, {\n status,\n statusText: parsed.statusText,\n headers,\n });\n}\n\n/**\n * Builder for batch operations that allows multiple queries to be executed together\n * in a single transactional request.\n *\n * Note: BatchBuilder does not implement ExecutableBuilder because execute() returns\n * BatchResult instead of Result, which is a different return type structure.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type\nexport class BatchBuilder<Builders extends readonly ExecutableBuilder<any>[]> {\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type\n private readonly builders: ExecutableBuilder<any>[];\n private readonly databaseName: string;\n private readonly context: ExecutionContext;\n\n constructor(builders: Builders, databaseName: string, context: ExecutionContext) {\n // Convert readonly tuple to mutable array for dynamic additions\n this.builders = [...builders];\n // Store original tuple for type preservation\n this.databaseName = databaseName;\n this.context = context;\n }\n\n /**\n * Add a request to the batch dynamically.\n * This allows building up batch operations programmatically.\n *\n * @param builder - An executable builder to add to the batch\n * @returns This BatchBuilder for method chaining\n * @example\n * ```ts\n * const batch = db.batch([]);\n * batch.addRequest(db.from('contacts').list());\n * batch.addRequest(db.from('users').list());\n * const result = await batch.execute();\n * ```\n */\n addRequest<T>(builder: ExecutableBuilder<T>): this {\n this.builders.push(builder);\n return this;\n }\n\n /**\n * Get the request configuration for this batch operation.\n * This is used internally by the execution system.\n */\n // biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value\n getRequestConfig(): { method: string; url: string; body?: any } {\n // Note: This method is kept for compatibility but batch operations\n // should use execute() directly which handles the full Request/Response flow\n return {\n method: \"POST\",\n url: `/${this.databaseName}/$batch`,\n body: undefined, // Body is constructed in execute()\n };\n }\n\n toRequest(baseUrl: string, _options?: ExecuteOptions): Request {\n // Batch operations are not designed to be nested, but we provide\n // a basic implementation for interface compliance\n const fullUrl = `${baseUrl}/${this.databaseName}/$batch`;\n return new Request(fullUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"multipart/mixed\",\n \"OData-Version\": \"4.0\",\n },\n });\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Generic return type for interface compliance\n processResponse(_response: Response, _options?: ExecuteOptions): Promise<Result<any>> {\n // This should not typically be called for batch operations\n // as they handle their own response processing\n return Promise.resolve({\n data: undefined,\n error: {\n name: \"NotImplementedError\",\n message: \"Batch operations handle response processing internally\",\n timestamp: new Date(),\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object\n } as any,\n });\n }\n\n /**\n * Execute the batch operation.\n *\n * @param options - Optional fetch options and batch-specific options (includes beforeRequest hook)\n * @returns A BatchResult containing individual results for each operation\n */\n async execute<EO extends ExecuteOptions>(\n options?: ExecuteMethodOptions<EO>,\n ): Promise<BatchResult<ExtractTupleTypes<Builders>>> {\n const baseUrl = this.context._getBaseUrl?.();\n if (!baseUrl) {\n // Return BatchResult with all operations marked as failed\n const errorCount = this.builders.length;\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type\n const results: BatchItemResult<any>[] = this.builders.map((_, _i) => ({\n data: undefined,\n error: {\n name: \"ConfigurationError\",\n message: \"Base URL not available - execution context must implement _getBaseUrl()\",\n timestamp: new Date(),\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object\n } as any,\n status: 0,\n }));\n\n return {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type\n results: results as any,\n successCount: 0,\n errorCount,\n truncated: false,\n firstErrorIndex: 0,\n };\n }\n\n try {\n // Convert builders to native Request objects\n const requests: Request[] = this.builders.map((builder) => builder.toRequest(baseUrl, options));\n\n // Format batch request (automatically groups mutations into changesets)\n const { body, boundary } = await formatBatchRequestFromNative(requests, baseUrl);\n\n // Execute the batch request\n const response = await this.context._makeRequest<string>(`/${this.databaseName}/$batch`, {\n ...options,\n method: \"POST\",\n headers: {\n ...options?.headers,\n \"Content-Type\": `multipart/mixed; boundary=${boundary}`,\n \"OData-Version\": \"4.0\",\n },\n body,\n });\n\n if (response.error) {\n // Return BatchResult with all operations marked as failed\n const errorCount = this.builders.length;\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type\n const results: BatchItemResult<any>[] = this.builders.map((_, _i) => ({\n data: undefined,\n error: response.error,\n status: 0,\n }));\n\n return {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type\n results: results as any,\n successCount: 0,\n errorCount,\n truncated: false,\n firstErrorIndex: 0,\n };\n }\n\n // Extract the actual boundary from the response\n // FileMaker uses its own boundary, not the one we sent\n const firstLine = response.data.split(\"\\r\\n\")[0] || response.data.split(\"\\n\")[0] || \"\";\n const actualBoundary = firstLine.startsWith(\"--\") ? firstLine.substring(2) : boundary;\n\n // Parse the multipart response\n const contentTypeHeader = `multipart/mixed; boundary=${actualBoundary}`;\n const parsedResponses = parseBatchResponse(response.data, contentTypeHeader);\n\n // Process each response using the corresponding builder\n // Build BatchResult with per-item results\n type _ResultTuple = ExtractTupleTypes<Builders>;\n\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type\n const results: BatchItemResult<any>[] = [];\n let successCount = 0;\n let errorCount = 0;\n let firstErrorIndex: number | null = null;\n const truncated = parsedResponses.length < this.builders.length;\n\n // Process builders sequentially to preserve tuple order\n for (let i = 0; i < this.builders.length; i++) {\n const builder = this.builders[i];\n const parsed = parsedResponses[i];\n\n if (!parsed) {\n // Truncated - operation never executed\n const failedAtIndex = firstErrorIndex ?? i;\n results.push({\n data: undefined,\n error: new BatchTruncatedError(i, failedAtIndex),\n status: 0,\n });\n errorCount++;\n continue;\n }\n\n if (!builder) {\n // Should not happen, but handle gracefully\n results.push({\n data: undefined,\n error: {\n name: \"BatchError\",\n message: `Builder at index ${i} is undefined`,\n timestamp: new Date(),\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object\n } as any,\n status: parsed.status,\n });\n errorCount++;\n if (firstErrorIndex === null) {\n firstErrorIndex = i;\n }\n continue;\n }\n\n // Convert parsed response to native Response\n const nativeResponse = parsedToResponse(parsed);\n\n // Let the builder process its own response\n const result = await builder.processResponse(nativeResponse, options);\n\n if (result.error) {\n results.push({\n data: undefined,\n error: result.error,\n status: parsed.status,\n });\n errorCount++;\n if (firstErrorIndex === null) {\n firstErrorIndex = i;\n }\n } else {\n results.push({\n data: result.data,\n error: undefined,\n status: parsed.status,\n });\n successCount++;\n }\n }\n\n return {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type\n results: results as any,\n successCount,\n errorCount,\n truncated,\n firstErrorIndex,\n };\n } catch (err) {\n // On exception, return a BatchResult with all operations marked as failed\n const errorCount = this.builders.length;\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type\n const results: BatchItemResult<any>[] = this.builders.map((_, _i) => ({\n data: undefined,\n error: {\n name: \"BatchError\",\n message: err instanceof Error ? err.message : \"Unknown error\",\n timestamp: new Date(),\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object\n } as any,\n status: 0,\n }));\n\n return {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type\n results: results as any,\n successCount: 0,\n errorCount,\n truncated: false,\n firstErrorIndex: 0,\n };\n }\n }\n}\n"],"names":["errorCount","results"],"mappings":"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"}
@@ -1,8 +1,9 @@
1
+ const BOUNDARY_REGEX = /boundary=([^;]+)/;
2
+ const HTTP_STATUS_LINE_REGEX = /HTTP\/\d\.\d\s+(\d+)\s*(.*)/;
3
+ const CRLF_REGEX = /\r\n/;
4
+ const CHANGESET_CONTENT_TYPE_REGEX = /Content-Type: multipart\/mixed;\s*boundary=([^\r\n]+)/;
1
5
  function generateBoundary(prefix = "batch_") {
2
- const randomHex = Array.from(
3
- { length: 32 },
4
- () => Math.floor(Math.random() * 16).toString(16)
5
- ).join("");
6
+ const randomHex = Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join("");
6
7
  return `${prefix}${randomHex}`;
7
8
  }
8
9
  async function requestToConfig(request) {
@@ -37,15 +38,11 @@ function formatSubRequest(request, baseUrl) {
37
38
  }
38
39
  }
39
40
  }
40
- const hasContentType = request.headers && Object.keys(request.headers).some(
41
- (k) => k.toLowerCase() === "content-type"
42
- );
41
+ const hasContentType = request.headers && Object.keys(request.headers).some((k) => k.toLowerCase() === "content-type");
43
42
  if (!hasContentType) {
44
43
  lines.push("Content-Type: application/json");
45
44
  }
46
- const hasContentLength = request.headers && Object.keys(request.headers).some(
47
- (k) => k.toLowerCase() === "content-length"
48
- );
45
+ const hasContentLength = request.headers && Object.keys(request.headers).some((k) => k.toLowerCase() === "content-length");
49
46
  if (!hasContentLength) {
50
47
  lines.push(`Content-Length: ${request.body.length}`);
51
48
  }
@@ -99,17 +96,17 @@ async function formatBatchRequestFromNative(requests, baseUrl, batchBoundary) {
99
96
  };
100
97
  }
101
98
  function extractBoundary(contentType) {
102
- const match = contentType.match(/boundary=([^;]+)/);
103
- return match && match[1] ? match[1].trim() : null;
99
+ const match = contentType.match(BOUNDARY_REGEX);
100
+ return (match == null ? void 0 : match[1]) ? match[1].trim() : null;
104
101
  }
105
102
  function parseStatusLine(line) {
106
103
  var _a;
107
- const match = line.match(/HTTP\/\d\.\d\s+(\d+)\s*(.*)/);
108
- if (!match || !match[1]) {
104
+ const match = line.match(HTTP_STATUS_LINE_REGEX);
105
+ if (!(match == null ? void 0 : match[1])) {
109
106
  return { status: 0, statusText: "" };
110
107
  }
111
108
  return {
112
- status: parseInt(match[1], 10),
109
+ status: Number.parseInt(match[1], 10),
113
110
  statusText: ((_a = match[2]) == null ? void 0 : _a.trim()) || ""
114
111
  };
115
112
  }
@@ -126,11 +123,11 @@ function parseHeaders(lines) {
126
123
  return headers;
127
124
  }
128
125
  function parseHttpResponse(part) {
129
- const lines = part.split(/\r\n/);
126
+ const lines = part.split(CRLF_REGEX);
130
127
  let statusLineIndex = -1;
131
128
  for (let i = 0; i < lines.length; i++) {
132
129
  const line = lines[i];
133
- if (line && line.startsWith("HTTP/")) {
130
+ if (line == null ? void 0 : line.startsWith("HTTP/")) {
134
131
  statusLineIndex = i;
135
132
  break;
136
133
  }
@@ -163,7 +160,7 @@ function parseHttpResponse(part) {
163
160
  foundEmptyLine = true;
164
161
  break;
165
162
  }
166
- if (line && line.startsWith("--")) {
163
+ if (line == null ? void 0 : line.startsWith("--")) {
167
164
  break;
168
165
  }
169
166
  if (line) {
@@ -213,9 +210,7 @@ function parseBatchResponse(responseText, contentType) {
213
210
  continue;
214
211
  }
215
212
  if (trimmedPart.includes("Content-Type: multipart/mixed")) {
216
- const changesetContentTypeMatch = trimmedPart.match(
217
- /Content-Type: multipart\/mixed;\s*boundary=([^\r\n]+)/
218
- );
213
+ const changesetContentTypeMatch = trimmedPart.match(CHANGESET_CONTENT_TYPE_REGEX);
219
214
  if (changesetContentTypeMatch) {
220
215
  const changesetBoundary = (_a = changesetContentTypeMatch == null ? void 0 : changesetContentTypeMatch[1]) == null ? void 0 : _a.trim();
221
216
  const changesetPattern = `--${changesetBoundary}`;
@@ -1 +1 @@
1
- {"version":3,"file":"batch-request.js","sources":["../../../src/client/batch-request.ts"],"sourcesContent":["/**\n * Batch Request Utilities\n *\n * Utilities for formatting and parsing OData batch requests using multipart/mixed format.\n * OData batch requests allow bundling multiple operations into a single HTTP request,\n * with support for transactional changesets.\n */\n\nexport interface RequestConfig {\n method: string;\n url: string;\n body?: string;\n headers?: Record<string, string>;\n}\n\nexport interface ParsedBatchResponse {\n status: number;\n statusText: string;\n headers: Record<string, string>;\n body: any;\n}\n\n/**\n * Generates a random boundary string for multipart requests\n * @param prefix - Prefix for the boundary (e.g., \"batch_\" or \"changeset_\")\n * @returns A boundary string with the prefix and 32 random hex characters\n */\nexport function generateBoundary(prefix: string = \"batch_\"): string {\n const randomHex = Array.from({ length: 32 }, () =>\n Math.floor(Math.random() * 16).toString(16),\n ).join(\"\");\n return `${prefix}${randomHex}`;\n}\n\n/**\n * Converts a native Request object to RequestConfig\n * @param request - Native Request object\n * @returns RequestConfig object\n */\nasync function requestToConfig(request: Request): Promise<RequestConfig> {\n const headers: Record<string, string> = {};\n request.headers.forEach((value, key) => {\n headers[key] = value;\n });\n\n let body: string | undefined;\n if (request.body) {\n // Clone the request to read the body without consuming it\n const clonedRequest = request.clone();\n body = await clonedRequest.text();\n }\n\n return {\n method: request.method,\n url: request.url,\n body,\n headers,\n };\n}\n\n/**\n * Formats a single HTTP request for inclusion in a batch\n * @param request - The request configuration\n * @param baseUrl - The base URL to prepend to relative URLs\n * @returns Formatted request string with CRLF line endings\n *\n * Formatting rules for FileMaker OData:\n * - GET (no body): request line → blank → blank\n * - POST/PATCH (with body): request line → headers → blank → body (NO blank after!)\n */\nfunction formatSubRequest(request: RequestConfig, baseUrl: string): string {\n const lines: string[] = [];\n\n // Add required headers for sub-request\n lines.push(\"Content-Type: application/http\");\n lines.push(\"Content-Transfer-Encoding: binary\");\n lines.push(\"\"); // Empty line after multipart headers\n\n // Construct full URL (convert relative to absolute)\n const fullUrl = request.url.startsWith(\"http\")\n ? request.url\n : `${baseUrl}${request.url}`;\n\n // Add HTTP request line\n lines.push(`${request.method} ${fullUrl} HTTP/1.1`);\n\n // For requests with body, add headers\n if (request.body) {\n // Add request headers (excluding Authorization - it's in the outer request)\n if (request.headers) {\n for (const [key, value] of Object.entries(request.headers)) {\n if (key.toLowerCase() !== \"authorization\") {\n lines.push(`${key}: ${value}`);\n }\n }\n }\n\n // Check if Content-Type is already set\n const hasContentType =\n request.headers &&\n Object.keys(request.headers).some(\n (k) => k.toLowerCase() === \"content-type\",\n );\n\n if (!hasContentType) {\n lines.push(\"Content-Type: application/json\");\n }\n\n // Add Content-Length (required for FileMaker to read the body)\n const hasContentLength =\n request.headers &&\n Object.keys(request.headers).some(\n (k) => k.toLowerCase() === \"content-length\",\n );\n\n if (!hasContentLength) {\n lines.push(`Content-Length: ${request.body.length}`);\n }\n\n lines.push(\"\"); // Empty line between headers and body\n lines.push(request.body);\n // NO blank line after body - the boundary comes immediately\n } else {\n // For GET requests (no body), add TWO blank lines\n lines.push(\"\"); // First blank\n lines.push(\"\"); // Second blank\n }\n\n return lines.join(\"\\r\\n\");\n}\n\n/**\n * Formats a changeset containing multiple non-GET operations\n * @param requests - Array of request configurations (should be non-GET)\n * @param baseUrl - The base URL to prepend to relative URLs\n * @param changesetBoundary - Boundary string for the changeset\n * @returns Formatted changeset string with CRLF line endings\n */\nfunction formatChangeset(\n requests: RequestConfig[],\n baseUrl: string,\n changesetBoundary: string,\n): string {\n const lines: string[] = [];\n\n lines.push(`Content-Type: multipart/mixed; boundary=${changesetBoundary}`);\n lines.push(\"\"); // Empty line after headers\n\n // Add each request in the changeset\n for (const request of requests) {\n lines.push(`--${changesetBoundary}`);\n lines.push(formatSubRequest(request, baseUrl));\n }\n\n // Close the changeset\n lines.push(`--${changesetBoundary}--`);\n\n return lines.join(\"\\r\\n\");\n}\n\n/**\n * Formats multiple requests into a batch request body\n * @param requests - Array of request configurations\n * @param baseUrl - The base URL to prepend to relative URLs\n * @param batchBoundary - Optional boundary string for the batch (generated if not provided)\n * @returns Object containing the formatted body and boundary\n */\nexport function formatBatchRequest(\n requests: RequestConfig[],\n baseUrl: string,\n batchBoundary?: string,\n): { body: string; boundary: string } {\n const boundary = batchBoundary || generateBoundary(\"batch_\");\n const lines: string[] = [];\n\n // Group requests: consecutive non-GET operations go into changesets\n let currentChangeset: RequestConfig[] | null = null;\n\n for (const request of requests) {\n if (request.method === \"GET\") {\n // GET operations break changesets and are added individually\n if (currentChangeset) {\n // Close and add the current changeset\n const changesetBoundary = generateBoundary(\"changeset_\");\n lines.push(`--${boundary}`);\n lines.push(\n formatChangeset(currentChangeset, baseUrl, changesetBoundary),\n );\n currentChangeset = null;\n }\n\n // Add GET request\n lines.push(`--${boundary}`);\n lines.push(formatSubRequest(request, baseUrl));\n } else {\n // Non-GET operations: add to current changeset or create new one\n if (!currentChangeset) {\n currentChangeset = [];\n }\n currentChangeset.push(request);\n }\n }\n\n // Add any remaining changeset\n if (currentChangeset) {\n const changesetBoundary = generateBoundary(\"changeset_\");\n lines.push(`--${boundary}`);\n lines.push(formatChangeset(currentChangeset, baseUrl, changesetBoundary));\n }\n\n // Close the batch\n lines.push(`--${boundary}--`);\n\n return {\n body: lines.join(\"\\r\\n\"),\n boundary,\n };\n}\n\n/**\n * Formats multiple Request objects into a batch request body\n * Supports explicit changesets via Request arrays\n * @param requests - Array of Request objects or Request arrays (for explicit changesets)\n * @param baseUrl - The base URL to prepend to relative URLs\n * @param batchBoundary - Optional boundary string for the batch (generated if not provided)\n * @returns Promise resolving to object containing the formatted body and boundary\n */\nexport async function formatBatchRequestFromNative(\n requests: Array<Request | Request[]>,\n baseUrl: string,\n batchBoundary?: string,\n): Promise<{ body: string; boundary: string }> {\n const boundary = batchBoundary || generateBoundary(\"batch_\");\n const lines: string[] = [];\n\n for (const item of requests) {\n if (Array.isArray(item)) {\n // Explicit changeset - array of Requests\n const changesetBoundary = generateBoundary(\"changeset_\");\n const changesetConfigs: RequestConfig[] = [];\n\n for (const request of item) {\n changesetConfigs.push(await requestToConfig(request));\n }\n\n lines.push(`--${boundary}`);\n lines.push(formatChangeset(changesetConfigs, baseUrl, changesetBoundary));\n } else {\n // Single request\n const config = await requestToConfig(item);\n\n if (config.method === \"GET\") {\n // GET requests are always individual\n lines.push(`--${boundary}`);\n lines.push(formatSubRequest(config, baseUrl));\n } else {\n // Non-GET operations wrapped in a changeset\n const changesetBoundary = generateBoundary(\"changeset_\");\n lines.push(`--${boundary}`);\n lines.push(formatChangeset([config], baseUrl, changesetBoundary));\n }\n }\n }\n\n // Close the batch\n lines.push(`--${boundary}--`);\n\n return {\n body: lines.join(\"\\r\\n\"),\n boundary,\n };\n}\n\n/**\n * Extracts the boundary from a Content-Type header\n * @param contentType - The Content-Type header value\n * @returns The boundary string, or null if not found\n */\nexport function extractBoundary(contentType: string): string | null {\n const match = contentType.match(/boundary=([^;]+)/);\n return match && match[1] ? match[1].trim() : null;\n}\n\n/**\n * Parses an HTTP response line (status line)\n * @param line - The HTTP status line (e.g., \"HTTP/1.1 200 OK\")\n * @returns Object containing status code and status text\n */\nfunction parseStatusLine(line: string): {\n status: number;\n statusText: string;\n} {\n const match = line.match(/HTTP\\/\\d\\.\\d\\s+(\\d+)\\s*(.*)/);\n if (!match || !match[1]) {\n return { status: 0, statusText: \"\" };\n }\n return {\n status: parseInt(match[1], 10),\n statusText: match[2]?.trim() || \"\",\n };\n}\n\n/**\n * Parses headers from an array of header lines\n * @param lines - Array of header lines\n * @returns Object containing parsed headers\n */\nfunction parseHeaders(lines: string[]): Record<string, string> {\n const headers: Record<string, string> = {};\n for (const line of lines) {\n const colonIndex = line.indexOf(\":\");\n if (colonIndex > 0) {\n const key = line.substring(0, colonIndex).trim();\n const value = line.substring(colonIndex + 1).trim();\n headers[key.toLowerCase()] = value;\n }\n }\n return headers;\n}\n\n/**\n * Parses a single HTTP response from a batch part\n * @param part - The raw HTTP response string\n * @returns Parsed response object\n */\nfunction parseHttpResponse(part: string): ParsedBatchResponse {\n const lines = part.split(/\\r\\n/);\n\n // Find the HTTP status line (skip multipart headers)\n let statusLineIndex = -1;\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (line && line.startsWith(\"HTTP/\")) {\n statusLineIndex = i;\n break;\n }\n }\n\n if (statusLineIndex === -1) {\n return {\n status: 0,\n statusText: \"Invalid response\",\n headers: {},\n body: null,\n };\n }\n\n const statusLine = lines[statusLineIndex];\n if (!statusLine) {\n return {\n status: 0,\n statusText: \"Invalid response\",\n headers: {},\n body: null,\n };\n }\n\n const { status, statusText } = parseStatusLine(statusLine);\n\n // Parse headers (between status line and empty line)\n const headerLines: string[] = [];\n let bodyStartIndex = lines.length; // Default to end of lines (no body)\n let foundEmptyLine = false;\n\n for (let i = statusLineIndex + 1; i < lines.length; i++) {\n const line = lines[i];\n if (line === \"\") {\n bodyStartIndex = i + 1;\n foundEmptyLine = true;\n break;\n }\n // Stop at boundary markers (for responses without bodies like 204)\n if (line && line.startsWith(\"--\")) {\n break;\n }\n if (line) {\n headerLines.push(line);\n }\n }\n\n const headers = parseHeaders(headerLines);\n\n // Parse body (everything after the empty line, if there was one)\n let bodyText = \"\";\n if (foundEmptyLine && bodyStartIndex < lines.length) {\n const bodyLines = lines.slice(bodyStartIndex);\n // Stop at boundary markers\n const bodyLinesFiltered: string[] = [];\n for (const line of bodyLines) {\n if (line.startsWith(\"--\")) {\n break;\n }\n bodyLinesFiltered.push(line);\n }\n bodyText = bodyLinesFiltered.join(\"\\r\\n\").trim();\n }\n\n let body: any = null;\n if (bodyText) {\n try {\n body = JSON.parse(bodyText);\n } catch {\n // If not JSON, return as text\n body = bodyText;\n }\n }\n\n return {\n status,\n statusText,\n headers,\n body,\n };\n}\n\n/**\n * Parses a batch response into individual responses\n * @param responseText - The raw batch response text\n * @param contentType - The Content-Type header from the response\n * @returns Array of parsed responses in the same order as the request\n */\nexport function parseBatchResponse(\n responseText: string,\n contentType: string,\n): ParsedBatchResponse[] {\n const boundary = extractBoundary(contentType);\n if (!boundary) {\n throw new Error(\"Could not extract boundary from Content-Type header\");\n }\n\n const results: ParsedBatchResponse[] = [];\n\n // Split by boundary (handle both --boundary and --boundary--)\n const boundaryPattern = `--${boundary}`;\n const parts = responseText.split(boundaryPattern);\n\n for (const part of parts) {\n const trimmedPart = part.trim();\n\n // Skip empty parts and the closing boundary marker\n if (!trimmedPart || trimmedPart === \"--\") {\n continue;\n }\n\n // Check if this part is a changeset (nested multipart)\n if (trimmedPart.includes(\"Content-Type: multipart/mixed\")) {\n // Extract the changeset boundary\n const changesetContentTypeMatch = trimmedPart.match(\n /Content-Type: multipart\\/mixed;\\s*boundary=([^\\r\\n]+)/,\n );\n if (changesetContentTypeMatch) {\n const changesetBoundary = changesetContentTypeMatch?.[1]?.trim();\n const changesetPattern = `--${changesetBoundary}`;\n const changesetParts = trimmedPart.split(changesetPattern);\n\n for (const changesetPart of changesetParts) {\n const trimmedChangesetPart = changesetPart.trim();\n if (!trimmedChangesetPart || trimmedChangesetPart === \"--\") {\n continue;\n }\n\n // Skip the changeset header\n if (\n trimmedChangesetPart.startsWith(\"Content-Type: multipart/mixed\")\n ) {\n continue;\n }\n\n const response = parseHttpResponse(trimmedChangesetPart);\n if (response.status > 0) {\n results.push(response);\n }\n }\n }\n } else {\n // Regular response (not a changeset)\n const response = parseHttpResponse(trimmedPart);\n if (response.status > 0) {\n results.push(response);\n }\n }\n }\n\n return results;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"batch-request.js","sources":["../../../src/client/batch-request.ts"],"sourcesContent":["/**\n * Batch Request Utilities\n *\n * Utilities for formatting and parsing OData batch requests using multipart/mixed format.\n * OData batch requests allow bundling multiple operations into a single HTTP request,\n * with support for transactional changesets.\n */\n\nconst BOUNDARY_REGEX = /boundary=([^;]+)/;\nconst HTTP_STATUS_LINE_REGEX = /HTTP\\/\\d\\.\\d\\s+(\\d+)\\s*(.*)/;\nconst CRLF_REGEX = /\\r\\n/;\nconst CHANGESET_CONTENT_TYPE_REGEX = /Content-Type: multipart\\/mixed;\\s*boundary=([^\\r\\n]+)/;\n\nexport interface RequestConfig {\n method: string;\n url: string;\n body?: string;\n headers?: Record<string, string>;\n}\n\nexport interface ParsedBatchResponse {\n status: number;\n statusText: string;\n headers: Record<string, string>;\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic response body type from OData API\n body: any;\n}\n\n/**\n * Generates a random boundary string for multipart requests\n * @param prefix - Prefix for the boundary (e.g., \"batch_\" or \"changeset_\")\n * @returns A boundary string with the prefix and 32 random hex characters\n */\nexport function generateBoundary(prefix = \"batch_\"): string {\n const randomHex = Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join(\"\");\n return `${prefix}${randomHex}`;\n}\n\n/**\n * Converts a native Request object to RequestConfig\n * @param request - Native Request object\n * @returns RequestConfig object\n */\nasync function requestToConfig(request: Request): Promise<RequestConfig> {\n const headers: Record<string, string> = {};\n request.headers.forEach((value, key) => {\n headers[key] = value;\n });\n\n let body: string | undefined;\n if (request.body) {\n // Clone the request to read the body without consuming it\n const clonedRequest = request.clone();\n body = await clonedRequest.text();\n }\n\n return {\n method: request.method,\n url: request.url,\n body,\n headers,\n };\n}\n\n/**\n * Formats a single HTTP request for inclusion in a batch\n * @param request - The request configuration\n * @param baseUrl - The base URL to prepend to relative URLs\n * @returns Formatted request string with CRLF line endings\n *\n * Formatting rules for FileMaker OData:\n * - GET (no body): request line → blank → blank\n * - POST/PATCH (with body): request line → headers → blank → body (NO blank after!)\n */\nfunction formatSubRequest(request: RequestConfig, baseUrl: string): string {\n const lines: string[] = [];\n\n // Add required headers for sub-request\n lines.push(\"Content-Type: application/http\");\n lines.push(\"Content-Transfer-Encoding: binary\");\n lines.push(\"\"); // Empty line after multipart headers\n\n // Construct full URL (convert relative to absolute)\n const fullUrl = request.url.startsWith(\"http\") ? request.url : `${baseUrl}${request.url}`;\n\n // Add HTTP request line\n lines.push(`${request.method} ${fullUrl} HTTP/1.1`);\n\n // For requests with body, add headers\n if (request.body) {\n // Add request headers (excluding Authorization - it's in the outer request)\n if (request.headers) {\n for (const [key, value] of Object.entries(request.headers)) {\n if (key.toLowerCase() !== \"authorization\") {\n lines.push(`${key}: ${value}`);\n }\n }\n }\n\n // Check if Content-Type is already set\n const hasContentType =\n request.headers && Object.keys(request.headers).some((k) => k.toLowerCase() === \"content-type\");\n\n if (!hasContentType) {\n lines.push(\"Content-Type: application/json\");\n }\n\n // Add Content-Length (required for FileMaker to read the body)\n const hasContentLength =\n request.headers && Object.keys(request.headers).some((k) => k.toLowerCase() === \"content-length\");\n\n if (!hasContentLength) {\n lines.push(`Content-Length: ${request.body.length}`);\n }\n\n lines.push(\"\"); // Empty line between headers and body\n lines.push(request.body);\n // NO blank line after body - the boundary comes immediately\n } else {\n // For GET requests (no body), add TWO blank lines\n lines.push(\"\"); // First blank\n lines.push(\"\"); // Second blank\n }\n\n return lines.join(\"\\r\\n\");\n}\n\n/**\n * Formats a changeset containing multiple non-GET operations\n * @param requests - Array of request configurations (should be non-GET)\n * @param baseUrl - The base URL to prepend to relative URLs\n * @param changesetBoundary - Boundary string for the changeset\n * @returns Formatted changeset string with CRLF line endings\n */\nfunction formatChangeset(requests: RequestConfig[], baseUrl: string, changesetBoundary: string): string {\n const lines: string[] = [];\n\n lines.push(`Content-Type: multipart/mixed; boundary=${changesetBoundary}`);\n lines.push(\"\"); // Empty line after headers\n\n // Add each request in the changeset\n for (const request of requests) {\n lines.push(`--${changesetBoundary}`);\n lines.push(formatSubRequest(request, baseUrl));\n }\n\n // Close the changeset\n lines.push(`--${changesetBoundary}--`);\n\n return lines.join(\"\\r\\n\");\n}\n\n/**\n * Formats multiple requests into a batch request body\n * @param requests - Array of request configurations\n * @param baseUrl - The base URL to prepend to relative URLs\n * @param batchBoundary - Optional boundary string for the batch (generated if not provided)\n * @returns Object containing the formatted body and boundary\n */\nexport function formatBatchRequest(\n requests: RequestConfig[],\n baseUrl: string,\n batchBoundary?: string,\n): { body: string; boundary: string } {\n const boundary = batchBoundary || generateBoundary(\"batch_\");\n const lines: string[] = [];\n\n // Group requests: consecutive non-GET operations go into changesets\n let currentChangeset: RequestConfig[] | null = null;\n\n for (const request of requests) {\n if (request.method === \"GET\") {\n // GET operations break changesets and are added individually\n if (currentChangeset) {\n // Close and add the current changeset\n const changesetBoundary = generateBoundary(\"changeset_\");\n lines.push(`--${boundary}`);\n lines.push(formatChangeset(currentChangeset, baseUrl, changesetBoundary));\n currentChangeset = null;\n }\n\n // Add GET request\n lines.push(`--${boundary}`);\n lines.push(formatSubRequest(request, baseUrl));\n } else {\n // Non-GET operations: add to current changeset or create new one\n if (!currentChangeset) {\n currentChangeset = [];\n }\n currentChangeset.push(request);\n }\n }\n\n // Add any remaining changeset\n if (currentChangeset) {\n const changesetBoundary = generateBoundary(\"changeset_\");\n lines.push(`--${boundary}`);\n lines.push(formatChangeset(currentChangeset, baseUrl, changesetBoundary));\n }\n\n // Close the batch\n lines.push(`--${boundary}--`);\n\n return {\n body: lines.join(\"\\r\\n\"),\n boundary,\n };\n}\n\n/**\n * Formats multiple Request objects into a batch request body\n * Supports explicit changesets via Request arrays\n * @param requests - Array of Request objects or Request arrays (for explicit changesets)\n * @param baseUrl - The base URL to prepend to relative URLs\n * @param batchBoundary - Optional boundary string for the batch (generated if not provided)\n * @returns Promise resolving to object containing the formatted body and boundary\n */\nexport async function formatBatchRequestFromNative(\n requests: Array<Request | Request[]>,\n baseUrl: string,\n batchBoundary?: string,\n): Promise<{ body: string; boundary: string }> {\n const boundary = batchBoundary || generateBoundary(\"batch_\");\n const lines: string[] = [];\n\n for (const item of requests) {\n if (Array.isArray(item)) {\n // Explicit changeset - array of Requests\n const changesetBoundary = generateBoundary(\"changeset_\");\n const changesetConfigs: RequestConfig[] = [];\n\n for (const request of item) {\n changesetConfigs.push(await requestToConfig(request));\n }\n\n lines.push(`--${boundary}`);\n lines.push(formatChangeset(changesetConfigs, baseUrl, changesetBoundary));\n } else {\n // Single request\n const config = await requestToConfig(item);\n\n if (config.method === \"GET\") {\n // GET requests are always individual\n lines.push(`--${boundary}`);\n lines.push(formatSubRequest(config, baseUrl));\n } else {\n // Non-GET operations wrapped in a changeset\n const changesetBoundary = generateBoundary(\"changeset_\");\n lines.push(`--${boundary}`);\n lines.push(formatChangeset([config], baseUrl, changesetBoundary));\n }\n }\n }\n\n // Close the batch\n lines.push(`--${boundary}--`);\n\n return {\n body: lines.join(\"\\r\\n\"),\n boundary,\n };\n}\n\n/**\n * Extracts the boundary from a Content-Type header\n * @param contentType - The Content-Type header value\n * @returns The boundary string, or null if not found\n */\nexport function extractBoundary(contentType: string): string | null {\n const match = contentType.match(BOUNDARY_REGEX);\n return match?.[1] ? match[1].trim() : null;\n}\n\n/**\n * Parses an HTTP response line (status line)\n * @param line - The HTTP status line (e.g., \"HTTP/1.1 200 OK\")\n * @returns Object containing status code and status text\n */\nfunction parseStatusLine(line: string): {\n status: number;\n statusText: string;\n} {\n const match = line.match(HTTP_STATUS_LINE_REGEX);\n if (!match?.[1]) {\n return { status: 0, statusText: \"\" };\n }\n return {\n status: Number.parseInt(match[1], 10),\n statusText: match[2]?.trim() || \"\",\n };\n}\n\n/**\n * Parses headers from an array of header lines\n * @param lines - Array of header lines\n * @returns Object containing parsed headers\n */\nfunction parseHeaders(lines: string[]): Record<string, string> {\n const headers: Record<string, string> = {};\n for (const line of lines) {\n const colonIndex = line.indexOf(\":\");\n if (colonIndex > 0) {\n const key = line.substring(0, colonIndex).trim();\n const value = line.substring(colonIndex + 1).trim();\n headers[key.toLowerCase()] = value;\n }\n }\n return headers;\n}\n\n/**\n * Parses a single HTTP response from a batch part\n * @param part - The raw HTTP response string\n * @returns Parsed response object\n */\nfunction parseHttpResponse(part: string): ParsedBatchResponse {\n const lines = part.split(CRLF_REGEX);\n\n // Find the HTTP status line (skip multipart headers)\n let statusLineIndex = -1;\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (line?.startsWith(\"HTTP/\")) {\n statusLineIndex = i;\n break;\n }\n }\n\n if (statusLineIndex === -1) {\n return {\n status: 0,\n statusText: \"Invalid response\",\n headers: {},\n body: null,\n };\n }\n\n const statusLine = lines[statusLineIndex];\n if (!statusLine) {\n return {\n status: 0,\n statusText: \"Invalid response\",\n headers: {},\n body: null,\n };\n }\n\n const { status, statusText } = parseStatusLine(statusLine);\n\n // Parse headers (between status line and empty line)\n const headerLines: string[] = [];\n let bodyStartIndex = lines.length; // Default to end of lines (no body)\n let foundEmptyLine = false;\n\n for (let i = statusLineIndex + 1; i < lines.length; i++) {\n const line = lines[i];\n if (line === \"\") {\n bodyStartIndex = i + 1;\n foundEmptyLine = true;\n break;\n }\n // Stop at boundary markers (for responses without bodies like 204)\n if (line?.startsWith(\"--\")) {\n break;\n }\n if (line) {\n headerLines.push(line);\n }\n }\n\n const headers = parseHeaders(headerLines);\n\n // Parse body (everything after the empty line, if there was one)\n let bodyText = \"\";\n if (foundEmptyLine && bodyStartIndex < lines.length) {\n const bodyLines = lines.slice(bodyStartIndex);\n // Stop at boundary markers\n const bodyLinesFiltered: string[] = [];\n for (const line of bodyLines) {\n if (line.startsWith(\"--\")) {\n break;\n }\n bodyLinesFiltered.push(line);\n }\n bodyText = bodyLinesFiltered.join(\"\\r\\n\").trim();\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic response body type from OData API\n let body: any = null;\n if (bodyText) {\n try {\n body = JSON.parse(bodyText);\n } catch {\n // If not JSON, return as text\n body = bodyText;\n }\n }\n\n return {\n status,\n statusText,\n headers,\n body,\n };\n}\n\n/**\n * Parses a batch response into individual responses\n * @param responseText - The raw batch response text\n * @param contentType - The Content-Type header from the response\n * @returns Array of parsed responses in the same order as the request\n */\nexport function parseBatchResponse(responseText: string, contentType: string): ParsedBatchResponse[] {\n const boundary = extractBoundary(contentType);\n if (!boundary) {\n throw new Error(\"Could not extract boundary from Content-Type header\");\n }\n\n const results: ParsedBatchResponse[] = [];\n\n // Split by boundary (handle both --boundary and --boundary--)\n const boundaryPattern = `--${boundary}`;\n const parts = responseText.split(boundaryPattern);\n\n for (const part of parts) {\n const trimmedPart = part.trim();\n\n // Skip empty parts and the closing boundary marker\n if (!trimmedPart || trimmedPart === \"--\") {\n continue;\n }\n\n // Check if this part is a changeset (nested multipart)\n if (trimmedPart.includes(\"Content-Type: multipart/mixed\")) {\n // Extract the changeset boundary\n const changesetContentTypeMatch = trimmedPart.match(CHANGESET_CONTENT_TYPE_REGEX);\n if (changesetContentTypeMatch) {\n const changesetBoundary = changesetContentTypeMatch?.[1]?.trim();\n const changesetPattern = `--${changesetBoundary}`;\n const changesetParts = trimmedPart.split(changesetPattern);\n\n for (const changesetPart of changesetParts) {\n const trimmedChangesetPart = changesetPart.trim();\n if (!trimmedChangesetPart || trimmedChangesetPart === \"--\") {\n continue;\n }\n\n // Skip the changeset header\n if (trimmedChangesetPart.startsWith(\"Content-Type: multipart/mixed\")) {\n continue;\n }\n\n const response = parseHttpResponse(trimmedChangesetPart);\n if (response.status > 0) {\n results.push(response);\n }\n }\n }\n } else {\n // Regular response (not a changeset)\n const response = parseHttpResponse(trimmedPart);\n if (response.status > 0) {\n results.push(response);\n }\n }\n }\n\n return results;\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { FMTable } from '../../orm/table.js';
2
+ /**
3
+ * Gets default select fields from a table definition.
4
+ * Returns undefined if defaultSelect is "all".
5
+ * Automatically filters out container fields since they cannot be selected via $select.
6
+ *
7
+ * @param table - The table occurrence
8
+ * @param includeSpecialColumns - If true, includes ROWID and ROWMODID when defaultSelect is "schema"
9
+ */
10
+ export declare function getDefaultSelectFields(table: FMTable<any, any> | undefined, includeSpecialColumns?: boolean): string[] | undefined;
@@ -0,0 +1,41 @@
1
+ import { isColumn } from "../../orm/column.js";
2
+ import { FMTable, getBaseTableConfig } from "../../orm/table.js";
3
+ function getContainerFieldNames(table) {
4
+ const baseTableConfig = getBaseTableConfig(table);
5
+ if (!(baseTableConfig == null ? void 0 : baseTableConfig.containerFields)) {
6
+ return [];
7
+ }
8
+ return baseTableConfig.containerFields;
9
+ }
10
+ function getDefaultSelectFields(table, includeSpecialColumns) {
11
+ if (!table) {
12
+ return void 0;
13
+ }
14
+ const defaultSelect = table[FMTable.Symbol.DefaultSelect];
15
+ const containerFields = getContainerFieldNames(table);
16
+ if (defaultSelect === "schema") {
17
+ const baseTableConfig = getBaseTableConfig(table);
18
+ const allFields = Object.keys(baseTableConfig.schema);
19
+ const fields = [...new Set(allFields.filter((f) => !containerFields.includes(f)))];
20
+ return fields;
21
+ }
22
+ if (Array.isArray(defaultSelect)) {
23
+ return [...new Set(defaultSelect.filter((f) => !containerFields.includes(f)))];
24
+ }
25
+ if (typeof defaultSelect === "object" && defaultSelect !== null && !Array.isArray(defaultSelect)) {
26
+ const fieldNames = [];
27
+ for (const value of Object.values(defaultSelect)) {
28
+ if (isColumn(value)) {
29
+ fieldNames.push(value.fieldName);
30
+ }
31
+ }
32
+ if (fieldNames.length > 0) {
33
+ return [...new Set(fieldNames.filter((f) => !containerFields.includes(f)))];
34
+ }
35
+ }
36
+ return void 0;
37
+ }
38
+ export {
39
+ getDefaultSelectFields
40
+ };
41
+ //# sourceMappingURL=default-select.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-select.js","sources":["../../../../src/client/builders/default-select.ts"],"sourcesContent":["import { isColumn } from \"../../orm/column\";\nimport type { FMTable } from \"../../orm/table\";\nimport { FMTable as FMTableClass, getBaseTableConfig } from \"../../orm/table\";\n\n/**\n * Helper function to get container field names from a table.\n * Container fields cannot be selected via $select in FileMaker OData API.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\nfunction getContainerFieldNames(table: FMTable<any, any>): string[] {\n const baseTableConfig = getBaseTableConfig(table);\n if (!baseTableConfig?.containerFields) {\n return [];\n }\n return baseTableConfig.containerFields as string[];\n}\n\n/**\n * Gets default select fields from a table definition.\n * Returns undefined if defaultSelect is \"all\".\n * Automatically filters out container fields since they cannot be selected via $select.\n *\n * @param table - The table occurrence\n * @param includeSpecialColumns - If true, includes ROWID and ROWMODID when defaultSelect is \"schema\"\n */\nexport function getDefaultSelectFields(\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n table: FMTable<any, any> | undefined,\n includeSpecialColumns?: boolean,\n): string[] | undefined {\n if (!table) {\n return undefined;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access\n const defaultSelect = (table as any)[FMTableClass.Symbol.DefaultSelect];\n const containerFields = getContainerFieldNames(table);\n\n if (defaultSelect === \"schema\") {\n const baseTableConfig = getBaseTableConfig(table);\n const allFields = Object.keys(baseTableConfig.schema);\n // Filter out container fields\n const fields = [...new Set(allFields.filter((f) => !containerFields.includes(f)))];\n\n // Add special columns if requested\n if (includeSpecialColumns) {\n fields.push(\"ROWID\", \"ROWMODID\");\n }\n\n return fields;\n }\n\n if (Array.isArray(defaultSelect)) {\n // Filter out container fields\n return [...new Set(defaultSelect.filter((f) => !containerFields.includes(f)))];\n }\n\n // Check if defaultSelect is a Record<string, Column> (resolved from function)\n if (typeof defaultSelect === \"object\" && defaultSelect !== null && !Array.isArray(defaultSelect)) {\n // Extract field names from Column instances\n const fieldNames: string[] = [];\n for (const value of Object.values(defaultSelect)) {\n if (isColumn(value)) {\n fieldNames.push(value.fieldName);\n }\n }\n if (fieldNames.length > 0) {\n // Filter out container fields\n return [...new Set(fieldNames.filter((f) => !containerFields.includes(f)))];\n }\n }\n\n // defaultSelect is \"all\" or undefined\n return undefined;\n}\n"],"names":["FMTableClass"],"mappings":";;AASA,SAAS,uBAAuB,OAAoC;AAClE,QAAM,kBAAkB,mBAAmB,KAAK;AAChD,MAAI,EAAC,mDAAiB,kBAAiB;AACrC,WAAO,CAAA;AAAA,EACT;AACA,SAAO,gBAAgB;AACzB;AAUO,SAAS,uBAEd,OACA,uBACsB;AACtB,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAGA,QAAM,gBAAiB,MAAcA,QAAa,OAAO,aAAa;AACtE,QAAM,kBAAkB,uBAAuB,KAAK;AAEpD,MAAI,kBAAkB,UAAU;AAC9B,UAAM,kBAAkB,mBAAmB,KAAK;AAChD,UAAM,YAAY,OAAO,KAAK,gBAAgB,MAAM;AAEpD,UAAM,SAAS,CAAC,GAAG,IAAI,IAAI,UAAU,OAAO,CAAC,MAAM,CAAC,gBAAgB,SAAS,CAAC,CAAC,CAAC,CAAC;AAOjF,WAAO;AAAA,EACT;AAEA,MAAI,MAAM,QAAQ,aAAa,GAAG;AAEhC,WAAO,CAAC,GAAG,IAAI,IAAI,cAAc,OAAO,CAAC,MAAM,CAAC,gBAAgB,SAAS,CAAC,CAAC,CAAC,CAAC;AAAA,EAC/E;AAGA,MAAI,OAAO,kBAAkB,YAAY,kBAAkB,QAAQ,CAAC,MAAM,QAAQ,aAAa,GAAG;AAEhG,UAAM,aAAuB,CAAA;AAC7B,eAAW,SAAS,OAAO,OAAO,aAAa,GAAG;AAChD,UAAI,SAAS,KAAK,GAAG;AACnB,mBAAW,KAAK,MAAM,SAAS;AAAA,MACjC;AAAA,IACF;AACA,QAAI,WAAW,SAAS,GAAG;AAEzB,aAAO,CAAC,GAAG,IAAI,IAAI,WAAW,OAAO,CAAC,MAAM,CAAC,gBAAgB,SAAS,CAAC,CAAC,CAAC,CAAC;AAAA,IAC5E;AAAA,EACF;AAGA,SAAO;AACT;"}