@proofkit/fmodata 0.1.0-alpha.8 → 0.1.0-beta.23

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