@proofkit/fmodata 0.1.0-alpha.6 → 0.1.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +333 -3
  2. package/dist/esm/client/batch-builder.d.ts +54 -0
  3. package/dist/esm/client/batch-builder.js +179 -0
  4. package/dist/esm/client/batch-builder.js.map +1 -0
  5. package/dist/esm/client/batch-request.d.ts +61 -0
  6. package/dist/esm/client/batch-request.js +252 -0
  7. package/dist/esm/client/batch-request.js.map +1 -0
  8. package/dist/esm/client/database.d.ts +43 -11
  9. package/dist/esm/client/database.js +64 -10
  10. package/dist/esm/client/database.js.map +1 -1
  11. package/dist/esm/client/delete-builder.d.ts +21 -2
  12. package/dist/esm/client/delete-builder.js +76 -9
  13. package/dist/esm/client/delete-builder.js.map +1 -1
  14. package/dist/esm/client/entity-set.d.ts +15 -4
  15. package/dist/esm/client/entity-set.js +23 -7
  16. package/dist/esm/client/entity-set.js.map +1 -1
  17. package/dist/esm/client/filemaker-odata.d.ts +11 -5
  18. package/dist/esm/client/filemaker-odata.js +46 -14
  19. package/dist/esm/client/filemaker-odata.js.map +1 -1
  20. package/dist/esm/client/insert-builder.d.ts +38 -3
  21. package/dist/esm/client/insert-builder.js +195 -9
  22. package/dist/esm/client/insert-builder.js.map +1 -1
  23. package/dist/esm/client/query-builder.d.ts +19 -3
  24. package/dist/esm/client/query-builder.js +193 -17
  25. package/dist/esm/client/query-builder.js.map +1 -1
  26. package/dist/esm/client/record-builder.d.ts +17 -2
  27. package/dist/esm/client/record-builder.js +87 -5
  28. package/dist/esm/client/record-builder.js.map +1 -1
  29. package/dist/esm/client/response-processor.d.ts +38 -0
  30. package/dist/esm/client/schema-manager.d.ts +57 -0
  31. package/dist/esm/client/schema-manager.js +132 -0
  32. package/dist/esm/client/schema-manager.js.map +1 -0
  33. package/dist/esm/client/update-builder.d.ts +34 -11
  34. package/dist/esm/client/update-builder.js +119 -19
  35. package/dist/esm/client/update-builder.js.map +1 -1
  36. package/dist/esm/errors.d.ts +14 -1
  37. package/dist/esm/errors.js +26 -0
  38. package/dist/esm/errors.js.map +1 -1
  39. package/dist/esm/index.d.ts +3 -2
  40. package/dist/esm/index.js +3 -1
  41. package/dist/esm/transform.d.ts +9 -0
  42. package/dist/esm/transform.js +7 -0
  43. package/dist/esm/transform.js.map +1 -1
  44. package/dist/esm/types.d.ts +69 -1
  45. package/package.json +1 -1
  46. package/src/client/batch-builder.ts +265 -0
  47. package/src/client/batch-request.ts +485 -0
  48. package/src/client/database.ts +106 -52
  49. package/src/client/delete-builder.ts +116 -14
  50. package/src/client/entity-set.ts +80 -6
  51. package/src/client/filemaker-odata.ts +65 -19
  52. package/src/client/insert-builder.ts +296 -18
  53. package/src/client/query-builder.ts +278 -17
  54. package/src/client/record-builder.ts +119 -11
  55. package/src/client/response-processor.ts +103 -0
  56. package/src/client/schema-manager.ts +246 -0
  57. package/src/client/update-builder.ts +195 -37
  58. package/src/errors.ts +33 -1
  59. package/src/index.ts +13 -0
  60. package/src/transform.ts +19 -6
  61. package/src/types.ts +89 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proofkit/fmodata",
3
- "version": "0.1.0-alpha.6",
3
+ "version": "0.1.0-alpha.7",
4
4
  "description": "FileMaker OData API client",
5
5
  "repository": "git@github.com:proofgeist/proofkit.git",
6
6
  "author": "Eric <37158449+eluce2@users.noreply.github.com>",
@@ -0,0 +1,265 @@
1
+ import type {
2
+ ExecutableBuilder,
3
+ ExecutionContext,
4
+ Result,
5
+ ExecuteOptions,
6
+ } from "../types";
7
+ import { type FFetchOptions } from "@fetchkit/ffetch";
8
+ import {
9
+ formatBatchRequestFromNative,
10
+ parseBatchResponse,
11
+ type ParsedBatchResponse,
12
+ } from "./batch-request";
13
+
14
+ /**
15
+ * Helper type to extract result types from a tuple of ExecutableBuilders.
16
+ * Uses a mapped type which TypeScript 4.1+ can handle for tuples.
17
+ */
18
+ type ExtractTupleTypes<T extends readonly ExecutableBuilder<any>[]> = {
19
+ [K in keyof T]: T[K] extends ExecutableBuilder<infer U> ? U : never;
20
+ };
21
+
22
+ /**
23
+ * Converts a ParsedBatchResponse to a native Response object
24
+ * @param parsed - The parsed batch response
25
+ * @returns A native Response object
26
+ */
27
+ function parsedToResponse(parsed: ParsedBatchResponse): Response {
28
+ const headers = new Headers(parsed.headers);
29
+
30
+ // Handle null body
31
+ if (parsed.body === null || parsed.body === undefined) {
32
+ return new Response(null, {
33
+ status: parsed.status,
34
+ statusText: parsed.statusText,
35
+ headers,
36
+ });
37
+ }
38
+
39
+ // Convert body to string if it's not already
40
+ const bodyString =
41
+ typeof parsed.body === "string" ? parsed.body : JSON.stringify(parsed.body);
42
+
43
+ // Handle 204 No Content status - it cannot have a body per HTTP spec
44
+ // If FileMaker returns 204 with a body, treat it as 200
45
+ let status = parsed.status;
46
+ if (status === 204 && bodyString && bodyString.trim() !== "") {
47
+ status = 200;
48
+ }
49
+
50
+ return new Response(status === 204 ? null : bodyString, {
51
+ status: status,
52
+ statusText: parsed.statusText,
53
+ headers,
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Builder for batch operations that allows multiple queries to be executed together
59
+ * in a single transactional request.
60
+ */
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;
66
+
67
+ constructor(
68
+ builders: Builders,
69
+ private readonly databaseName: string,
70
+ private readonly context: ExecutionContext,
71
+ ) {
72
+ // Convert readonly tuple to mutable array for dynamic additions
73
+ this.builders = [...builders];
74
+ // Store original tuple for type preservation
75
+ this.originalBuilders = builders;
76
+ }
77
+
78
+ /**
79
+ * Add a request to the batch dynamically.
80
+ * This allows building up batch operations programmatically.
81
+ *
82
+ * @param builder - An executable builder to add to the batch
83
+ * @returns This BatchBuilder for method chaining
84
+ * @example
85
+ * ```ts
86
+ * const batch = db.batch([]);
87
+ * batch.addRequest(db.from('contacts').list());
88
+ * batch.addRequest(db.from('users').list());
89
+ * const result = await batch.execute();
90
+ * ```
91
+ */
92
+ addRequest<T>(builder: ExecutableBuilder<T>): this {
93
+ this.builders.push(builder);
94
+ return this;
95
+ }
96
+
97
+ /**
98
+ * Get the request configuration for this batch operation.
99
+ * This is used internally by the execution system.
100
+ */
101
+ getRequestConfig(): { method: string; url: string; body?: any } {
102
+ // Note: This method is kept for compatibility but batch operations
103
+ // should use execute() directly which handles the full Request/Response flow
104
+ return {
105
+ method: "POST",
106
+ url: `/${this.databaseName}/$batch`,
107
+ body: undefined, // Body is constructed in execute()
108
+ };
109
+ }
110
+
111
+ toRequest(baseUrl: string): Request {
112
+ // Batch operations are not designed to be nested, but we provide
113
+ // a basic implementation for interface compliance
114
+ const fullUrl = `${baseUrl}/${this.databaseName}/$batch`;
115
+ return new Request(fullUrl, {
116
+ method: "POST",
117
+ headers: {
118
+ "Content-Type": "multipart/mixed",
119
+ "OData-Version": "4.0",
120
+ },
121
+ });
122
+ }
123
+
124
+ async processResponse(
125
+ response: Response,
126
+ options?: ExecuteOptions,
127
+ ): Promise<Result<any>> {
128
+ // This should not typically be called for batch operations
129
+ // as they handle their own response processing
130
+ return {
131
+ data: undefined,
132
+ error: {
133
+ name: "NotImplementedError",
134
+ message: "Batch operations handle response processing internally",
135
+ timestamp: new Date(),
136
+ } as any,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Execute the batch operation.
142
+ *
143
+ * @param options - Optional fetch options and batch-specific options (includes beforeRequest hook)
144
+ * @returns A tuple of results matching the input builders
145
+ */
146
+ async execute<EO extends ExecuteOptions>(
147
+ options?: RequestInit & FFetchOptions & EO,
148
+ ): Promise<Result<ExtractTupleTypes<Builders>>> {
149
+ const baseUrl = this.context._getBaseUrl?.();
150
+ if (!baseUrl) {
151
+ return {
152
+ data: undefined,
153
+ error: {
154
+ name: "ConfigurationError",
155
+ message:
156
+ "Base URL not available - execution context must implement _getBaseUrl()",
157
+ timestamp: new Date(),
158
+ } as any,
159
+ };
160
+ }
161
+
162
+ try {
163
+ // Convert builders to native Request objects
164
+ const requests: Request[] = this.builders.map((builder) =>
165
+ builder.toRequest(baseUrl),
166
+ );
167
+
168
+ // Format batch request (automatically groups mutations into changesets)
169
+ const { body, boundary } = await formatBatchRequestFromNative(
170
+ requests,
171
+ baseUrl,
172
+ );
173
+
174
+ // 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,
186
+ },
187
+ );
188
+
189
+ if (response.error) {
190
+ return { data: undefined, error: response.error };
191
+ }
192
+
193
+ // Extract the actual boundary from the response
194
+ // 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;
200
+
201
+ // Parse the multipart response
202
+ 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
+ }
219
+
220
+ // Process each response using the corresponding builder
221
+ // Build tuple by processing each builder in order
222
+ type ResultTuple = ExtractTupleTypes<Builders>;
223
+
224
+ // 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];
228
+ const parsed = parsedResponses[i];
229
+
230
+ if (!builder || !parsed) {
231
+ processedResults.push(undefined);
232
+ continue;
233
+ }
234
+
235
+ // Convert parsed response to native Response
236
+ const nativeResponse = parsedToResponse(parsed);
237
+
238
+ // Let the builder process its own response
239
+ const result = await builder.processResponse(nativeResponse, options);
240
+
241
+ if (result.error) {
242
+ processedResults.push(undefined);
243
+ } else {
244
+ processedResults.push(result.data);
245
+ }
246
+ }
247
+
248
+ // Use a type assertion that TypeScript will respect
249
+ // ExtractTupleTypes ensures this is a proper tuple type
250
+ return {
251
+ data: processedResults as unknown as ResultTuple,
252
+ error: undefined,
253
+ };
254
+ } catch (err) {
255
+ return {
256
+ data: undefined,
257
+ error: {
258
+ name: "BatchError",
259
+ message: err instanceof Error ? err.message : "Unknown error",
260
+ timestamp: new Date(),
261
+ } as any,
262
+ };
263
+ }
264
+ }
265
+ }