@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/README.md CHANGED
@@ -2,12 +2,14 @@
2
2
 
3
3
  A strongly-typed FileMaker OData API client.
4
4
 
5
- ⚠️ WARNING: This library is in "alpha" status. The API is subject to change. Feedback is welcome on the [community forum](https://community.ottomatic.cloud/c/proofkit/13) or on [GitHub](https://github.com/proofgeist/proofkit/issues).
5
+ ⚠️ WARNING: This library is in "alpha" status. It's still in active development and the API is subject to change. Feedback is welcome on the [community forum](https://community.ottomatic.cloud/c/proofkit/13) or on [GitHub](https://github.com/proofgeist/proofkit/issues).
6
6
 
7
7
  Roadmap:
8
8
 
9
9
  - [ ] Crossjoin support
10
- - [ ] Batch operations
10
+ - [x] Batch operations
11
+ - [ ] Automatically chunk requests into smaller batches (e.g. max 512 inserts per batch)
12
+ - [x] Schema updates (add/update tables and fields)
11
13
  - [ ] Proper docs at proofkit.dev
12
14
  - [ ] @proofkit/typegen integration
13
15
 
@@ -79,7 +81,7 @@ if (data) {
79
81
 
80
82
  ## Core Concepts
81
83
 
82
- This library relies heavily on the builder pattern for defining your queries and operations. Most operations require a final call to `execute()` to send the request to the server. The builder pattern is designed to support batch operations in the future, allowing you to execute multiple operations in a single request as supported by the FileMaker OData API. **Note:** Batch operations are not yet supported but are planned before the production release. It's also helpful for testing the library, as you can call `getQueryString()` to get the OData query string without executing the request.
84
+ This library relies heavily on the builder pattern for defining your queries and operations. Most operations require a final call to `execute()` to send the request to the server. The builder pattern allows you to build complex queries and also supports batch operations, allowing you to execute multiple operations in a single request as supported by the FileMaker OData API. It's also helpful for testing the library, as you can call `getQueryString()` to get the OData query string without executing the request.
83
85
 
84
86
  As such, there are layers to the library to help you build your queries and operations.
85
87
 
@@ -803,6 +805,334 @@ console.log(result.result.recordId);
803
805
 
804
806
  **Note:** OData doesn't support script names with special characters (e.g., `@`, `&`, `/`) or script names beginning with a number. TypeScript will catch these at compile time.
805
807
 
808
+ ## Batch Operations
809
+
810
+ Batch operations allow you to execute multiple queries and operations together in a single request. All operations in a batch are executed atomically - they all succeed or all fail together. This is both more efficient (fewer network round-trips) and ensures data consistency across related operations.
811
+
812
+ ### Basic Batch with Multiple Queries
813
+
814
+ Execute multiple read operations in a single batch:
815
+
816
+ ```typescript
817
+ // Create query builders
818
+ const contactsQuery = db.from("contacts").list().top(5);
819
+ const usersQuery = db.from("users").list().top(5);
820
+
821
+ // Execute both queries in a single batch
822
+ const result = await db.batch([contactsQuery, usersQuery]).execute();
823
+
824
+ if (result.data) {
825
+ // Result is a tuple matching the input builders
826
+ const [contacts, users] = result.data;
827
+
828
+ console.log("Contacts:", contacts);
829
+ console.log("Users:", users);
830
+ }
831
+ ```
832
+
833
+ ### Mixed Operations (Reads and Writes)
834
+
835
+ Combine queries, inserts, updates, and deletes in a single batch:
836
+
837
+ ```typescript
838
+ // Mix different operation types
839
+ const listQuery = db.from("contacts").list().top(10);
840
+ const insertOp = db.from("contacts").insert({
841
+ name: "John Doe",
842
+ email: "john@example.com",
843
+ });
844
+ const updateOp = db.from("users").update({ active: true }).byId("user-123");
845
+
846
+ // All operations execute atomically
847
+ const result = await db.batch([listQuery, insertOp, updateOp]).execute();
848
+
849
+ if (result.data) {
850
+ const [contactsList, insertResult, updateResult] = result.data;
851
+
852
+ console.log("Fetched contacts:", contactsList);
853
+ console.log("Inserted contact:", insertResult);
854
+ console.log("Updated user:", updateResult);
855
+ }
856
+ ```
857
+
858
+ ### Transactional Behavior
859
+
860
+ Batch operations are transactional for write operations (inserts, updates, deletes). If any operation in the batch fails, all write operations are rolled back:
861
+
862
+ ```typescript
863
+ const result = await db
864
+ .batch([
865
+ db.from("users").insert({ username: "alice", email: "alice@example.com" }),
866
+ db.from("users").insert({ username: "bob", email: "bob@example.com" }),
867
+ db.from("users").insert({ username: "charlie", email: "invalid" }), // This fails
868
+ ])
869
+ .execute();
870
+
871
+ if (result.error) {
872
+ // All three inserts are rolled back - no users were created
873
+ console.error("Batch failed:", result.error);
874
+ }
875
+ ```
876
+
877
+ **Note:** Batch operations automatically group write operations (POST, PATCH, DELETE) into changesets for transactional behavior, while read operations (GET) are executed individually within the batch.
878
+
879
+ ## Schema Management
880
+
881
+ The library provides methods for managing database schema through the `db.schema` property. You can create and delete tables, add and remove fields, and manage indexes.
882
+
883
+ ### Creating Tables
884
+
885
+ Create a new table with field definitions:
886
+
887
+ ```typescript
888
+ import type { Field } from "@proofkit/fmodata";
889
+
890
+ const fields: Field[] = [
891
+ {
892
+ name: "id",
893
+ type: "string",
894
+ primary: true,
895
+ maxLength: 36,
896
+ },
897
+ {
898
+ name: "username",
899
+ type: "string",
900
+ nullable: false,
901
+ unique: true,
902
+ maxLength: 50,
903
+ },
904
+ {
905
+ name: "email",
906
+ type: "string",
907
+ nullable: false,
908
+ maxLength: 255,
909
+ },
910
+ {
911
+ name: "age",
912
+ type: "numeric",
913
+ nullable: true,
914
+ },
915
+ {
916
+ name: "created_at",
917
+ type: "timestamp",
918
+ default: "CURRENT_TIMESTAMP",
919
+ },
920
+ ];
921
+
922
+ const tableDefinition = await db.schema.createTable("users", fields);
923
+ console.log(tableDefinition.tableName); // "users"
924
+ console.log(tableDefinition.fields); // Array of field definitions
925
+ ```
926
+
927
+ ### Field Types
928
+
929
+ The library supports various field types:
930
+
931
+ **String Fields:**
932
+
933
+ ```typescript
934
+ {
935
+ name: "username",
936
+ type: "string",
937
+ maxLength: 100, // Optional: varchar(100)
938
+ nullable: true,
939
+ unique: true,
940
+ default: "USER" | "USERNAME" | "CURRENT_USER", // Optional
941
+ repetitions: 5, // Optional: for repeating fields
942
+ }
943
+ ```
944
+
945
+ **Numeric Fields:**
946
+
947
+ ```typescript
948
+ {
949
+ name: "age",
950
+ type: "numeric",
951
+ nullable: true,
952
+ primary: false,
953
+ unique: false,
954
+ }
955
+ ```
956
+
957
+ **Date Fields:**
958
+
959
+ ```typescript
960
+ {
961
+ name: "birth_date",
962
+ type: "date",
963
+ default: "CURRENT_DATE" | "CURDATE", // Optional
964
+ nullable: true,
965
+ }
966
+ ```
967
+
968
+ **Time Fields:**
969
+
970
+ ```typescript
971
+ {
972
+ name: "start_time",
973
+ type: "time",
974
+ default: "CURRENT_TIME" | "CURTIME", // Optional
975
+ nullable: true,
976
+ }
977
+ ```
978
+
979
+ **Timestamp Fields:**
980
+
981
+ ```typescript
982
+ {
983
+ name: "created_at",
984
+ type: "timestamp",
985
+ default: "CURRENT_TIMESTAMP" | "CURTIMESTAMP", // Optional
986
+ nullable: false,
987
+ }
988
+ ```
989
+
990
+ **Container Fields:**
991
+
992
+ ```typescript
993
+ {
994
+ name: "avatar",
995
+ type: "container",
996
+ externalSecurePath: "/secure/path", // Optional
997
+ nullable: true,
998
+ }
999
+ ```
1000
+
1001
+ ### Adding Fields to Existing Tables
1002
+
1003
+ Add new fields to an existing table:
1004
+
1005
+ ```typescript
1006
+ const newFields: Field[] = [
1007
+ {
1008
+ name: "phone",
1009
+ type: "string",
1010
+ nullable: true,
1011
+ maxLength: 20,
1012
+ },
1013
+ {
1014
+ name: "bio",
1015
+ type: "string",
1016
+ nullable: true,
1017
+ maxLength: 1000,
1018
+ },
1019
+ ];
1020
+
1021
+ const updatedTable = await db.schema.addFields("users", newFields);
1022
+ ```
1023
+
1024
+ ### Deleting Tables and Fields
1025
+
1026
+ Delete an entire table:
1027
+
1028
+ ```typescript
1029
+ await db.schema.deleteTable("old_table");
1030
+ ```
1031
+
1032
+ Delete a specific field from a table:
1033
+
1034
+ ```typescript
1035
+ await db.schema.deleteField("users", "old_field");
1036
+ ```
1037
+
1038
+ ### Managing Indexes
1039
+
1040
+ Create an index on a field:
1041
+
1042
+ ```typescript
1043
+ const index = await db.schema.createIndex("users", "email");
1044
+ console.log(index.indexName); // "email"
1045
+ ```
1046
+
1047
+ Delete an index:
1048
+
1049
+ ```typescript
1050
+ await db.schema.deleteIndex("users", "email");
1051
+ ```
1052
+
1053
+ ### Complete Example
1054
+
1055
+ Here's a complete example of creating a table with various field types:
1056
+
1057
+ ```typescript
1058
+ const fields: Field[] = [
1059
+ // Primary key
1060
+ {
1061
+ name: "id",
1062
+ type: "string",
1063
+ primary: true,
1064
+ maxLength: 36,
1065
+ },
1066
+
1067
+ // String fields
1068
+ {
1069
+ name: "username",
1070
+ type: "string",
1071
+ nullable: false,
1072
+ unique: true,
1073
+ maxLength: 50,
1074
+ },
1075
+ {
1076
+ name: "email",
1077
+ type: "string",
1078
+ nullable: false,
1079
+ maxLength: 255,
1080
+ },
1081
+
1082
+ // Numeric field
1083
+ {
1084
+ name: "age",
1085
+ type: "numeric",
1086
+ nullable: true,
1087
+ },
1088
+
1089
+ // Date/time fields
1090
+ {
1091
+ name: "birth_date",
1092
+ type: "date",
1093
+ nullable: true,
1094
+ },
1095
+ {
1096
+ name: "created_at",
1097
+ type: "timestamp",
1098
+ default: "CURRENT_TIMESTAMP",
1099
+ nullable: false,
1100
+ },
1101
+
1102
+ // Container field
1103
+ {
1104
+ name: "avatar",
1105
+ type: "container",
1106
+ nullable: true,
1107
+ },
1108
+
1109
+ // Repeating field
1110
+ {
1111
+ name: "tags",
1112
+ type: "string",
1113
+ repetitions: 5,
1114
+ maxLength: 50,
1115
+ },
1116
+ ];
1117
+
1118
+ // Create the table
1119
+ const table = await db.schema.createTable("users", fields);
1120
+
1121
+ // Later, add more fields
1122
+ await db.schema.addFields("users", [
1123
+ {
1124
+ name: "phone",
1125
+ type: "string",
1126
+ nullable: true,
1127
+ },
1128
+ ]);
1129
+
1130
+ // Create an index on email
1131
+ await db.schema.createIndex("users", "email");
1132
+ ```
1133
+
1134
+ **Note:** Schema management operations require appropriate access privileges on your FileMaker account. Operations will throw errors if you don't have the necessary permissions.
1135
+
806
1136
  ## Advanced Features
807
1137
 
808
1138
  ### Type Safety
@@ -0,0 +1,54 @@
1
+ import { ExecutableBuilder, ExecutionContext, Result, ExecuteOptions } from '../types.js';
2
+ import { FFetchOptions } from '@fetchkit/ffetch';
3
+ /**
4
+ * Helper type to extract result types from a tuple of ExecutableBuilders.
5
+ * Uses a mapped type which TypeScript 4.1+ can handle for tuples.
6
+ */
7
+ type ExtractTupleTypes<T extends readonly ExecutableBuilder<any>[]> = {
8
+ [K in keyof T]: T[K] extends ExecutableBuilder<infer U> ? U : never;
9
+ };
10
+ /**
11
+ * Builder for batch operations that allows multiple queries to be executed together
12
+ * in a single transactional request.
13
+ */
14
+ export declare class BatchBuilder<Builders extends readonly ExecutableBuilder<any>[]> implements ExecutableBuilder<ExtractTupleTypes<Builders>> {
15
+ private readonly databaseName;
16
+ private readonly context;
17
+ private builders;
18
+ private readonly originalBuilders;
19
+ constructor(builders: Builders, databaseName: string, context: ExecutionContext);
20
+ /**
21
+ * Add a request to the batch dynamically.
22
+ * This allows building up batch operations programmatically.
23
+ *
24
+ * @param builder - An executable builder to add to the batch
25
+ * @returns This BatchBuilder for method chaining
26
+ * @example
27
+ * ```ts
28
+ * const batch = db.batch([]);
29
+ * batch.addRequest(db.from('contacts').list());
30
+ * batch.addRequest(db.from('users').list());
31
+ * const result = await batch.execute();
32
+ * ```
33
+ */
34
+ addRequest<T>(builder: ExecutableBuilder<T>): this;
35
+ /**
36
+ * Get the request configuration for this batch operation.
37
+ * This is used internally by the execution system.
38
+ */
39
+ getRequestConfig(): {
40
+ method: string;
41
+ url: string;
42
+ body?: any;
43
+ };
44
+ toRequest(baseUrl: string): Request;
45
+ processResponse(response: Response, options?: ExecuteOptions): Promise<Result<any>>;
46
+ /**
47
+ * Execute the batch operation.
48
+ *
49
+ * @param options - Optional fetch options and batch-specific options (includes beforeRequest hook)
50
+ * @returns A tuple of results matching the input builders
51
+ */
52
+ execute<EO extends ExecuteOptions>(options?: RequestInit & FFetchOptions & EO): Promise<Result<ExtractTupleTypes<Builders>>>;
53
+ }
54
+ export {};
@@ -0,0 +1,179 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ import { formatBatchRequestFromNative, parseBatchResponse } from "./batch-request.js";
5
+ function parsedToResponse(parsed) {
6
+ const headers = new Headers(parsed.headers);
7
+ if (parsed.body === null || parsed.body === void 0) {
8
+ return new Response(null, {
9
+ status: parsed.status,
10
+ statusText: parsed.statusText,
11
+ headers
12
+ });
13
+ }
14
+ const bodyString = typeof parsed.body === "string" ? parsed.body : JSON.stringify(parsed.body);
15
+ let status = parsed.status;
16
+ if (status === 204 && bodyString && bodyString.trim() !== "") {
17
+ status = 200;
18
+ }
19
+ return new Response(status === 204 ? null : bodyString, {
20
+ status,
21
+ statusText: parsed.statusText,
22
+ headers
23
+ });
24
+ }
25
+ class BatchBuilder {
26
+ constructor(builders, databaseName, context) {
27
+ __publicField(this, "builders");
28
+ __publicField(this, "originalBuilders");
29
+ this.databaseName = databaseName;
30
+ this.context = context;
31
+ this.builders = [...builders];
32
+ this.originalBuilders = builders;
33
+ }
34
+ /**
35
+ * Add a request to the batch dynamically.
36
+ * This allows building up batch operations programmatically.
37
+ *
38
+ * @param builder - An executable builder to add to the batch
39
+ * @returns This BatchBuilder for method chaining
40
+ * @example
41
+ * ```ts
42
+ * const batch = db.batch([]);
43
+ * batch.addRequest(db.from('contacts').list());
44
+ * batch.addRequest(db.from('users').list());
45
+ * const result = await batch.execute();
46
+ * ```
47
+ */
48
+ addRequest(builder) {
49
+ this.builders.push(builder);
50
+ return this;
51
+ }
52
+ /**
53
+ * Get the request configuration for this batch operation.
54
+ * This is used internally by the execution system.
55
+ */
56
+ getRequestConfig() {
57
+ return {
58
+ method: "POST",
59
+ url: `/${this.databaseName}/$batch`,
60
+ body: void 0
61
+ // Body is constructed in execute()
62
+ };
63
+ }
64
+ toRequest(baseUrl) {
65
+ const fullUrl = `${baseUrl}/${this.databaseName}/$batch`;
66
+ return new Request(fullUrl, {
67
+ method: "POST",
68
+ headers: {
69
+ "Content-Type": "multipart/mixed",
70
+ "OData-Version": "4.0"
71
+ }
72
+ });
73
+ }
74
+ async processResponse(response, options) {
75
+ return {
76
+ data: void 0,
77
+ error: {
78
+ name: "NotImplementedError",
79
+ message: "Batch operations handle response processing internally",
80
+ timestamp: /* @__PURE__ */ new Date()
81
+ }
82
+ };
83
+ }
84
+ /**
85
+ * Execute the batch operation.
86
+ *
87
+ * @param options - Optional fetch options and batch-specific options (includes beforeRequest hook)
88
+ * @returns A tuple of results matching the input builders
89
+ */
90
+ async execute(options) {
91
+ var _a, _b;
92
+ const baseUrl = (_b = (_a = this.context)._getBaseUrl) == null ? void 0 : _b.call(_a);
93
+ if (!baseUrl) {
94
+ return {
95
+ data: void 0,
96
+ error: {
97
+ name: "ConfigurationError",
98
+ message: "Base URL not available - execution context must implement _getBaseUrl()",
99
+ timestamp: /* @__PURE__ */ new Date()
100
+ }
101
+ };
102
+ }
103
+ 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
+ );
124
+ if (response.error) {
125
+ return { data: void 0, error: response.error };
126
+ }
127
+ const firstLine = response.data.split("\r\n")[0] || response.data.split("\n")[0] || "";
128
+ const actualBoundary = firstLine.startsWith("--") ? firstLine.substring(2) : boundary;
129
+ 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];
147
+ const parsed = parsedResponses[i];
148
+ if (!builder || !parsed) {
149
+ processedResults.push(void 0);
150
+ continue;
151
+ }
152
+ const nativeResponse = parsedToResponse(parsed);
153
+ const result = await builder.processResponse(nativeResponse, options);
154
+ if (result.error) {
155
+ processedResults.push(void 0);
156
+ } else {
157
+ processedResults.push(result.data);
158
+ }
159
+ }
160
+ return {
161
+ data: processedResults,
162
+ error: void 0
163
+ };
164
+ } catch (err) {
165
+ return {
166
+ data: void 0,
167
+ error: {
168
+ name: "BatchError",
169
+ message: err instanceof Error ? err.message : "Unknown error",
170
+ timestamp: /* @__PURE__ */ new Date()
171
+ }
172
+ };
173
+ }
174
+ }
175
+ }
176
+ export {
177
+ BatchBuilder
178
+ };
179
+ //# sourceMappingURL=batch-builder.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Batch Request Utilities
3
+ *
4
+ * Utilities for formatting and parsing OData batch requests using multipart/mixed format.
5
+ * OData batch requests allow bundling multiple operations into a single HTTP request,
6
+ * with support for transactional changesets.
7
+ */
8
+ export interface RequestConfig {
9
+ method: string;
10
+ url: string;
11
+ body?: string;
12
+ headers?: Record<string, string>;
13
+ }
14
+ export interface ParsedBatchResponse {
15
+ status: number;
16
+ statusText: string;
17
+ headers: Record<string, string>;
18
+ body: any;
19
+ }
20
+ /**
21
+ * Generates a random boundary string for multipart requests
22
+ * @param prefix - Prefix for the boundary (e.g., "batch_" or "changeset_")
23
+ * @returns A boundary string with the prefix and 32 random hex characters
24
+ */
25
+ export declare function generateBoundary(prefix?: string): string;
26
+ /**
27
+ * Formats multiple requests into a batch request body
28
+ * @param requests - Array of request configurations
29
+ * @param baseUrl - The base URL to prepend to relative URLs
30
+ * @param batchBoundary - Optional boundary string for the batch (generated if not provided)
31
+ * @returns Object containing the formatted body and boundary
32
+ */
33
+ export declare function formatBatchRequest(requests: RequestConfig[], baseUrl: string, batchBoundary?: string): {
34
+ body: string;
35
+ boundary: string;
36
+ };
37
+ /**
38
+ * Formats multiple Request objects into a batch request body
39
+ * Supports explicit changesets via Request arrays
40
+ * @param requests - Array of Request objects or Request arrays (for explicit changesets)
41
+ * @param baseUrl - The base URL to prepend to relative URLs
42
+ * @param batchBoundary - Optional boundary string for the batch (generated if not provided)
43
+ * @returns Promise resolving to object containing the formatted body and boundary
44
+ */
45
+ export declare function formatBatchRequestFromNative(requests: Array<Request | Request[]>, baseUrl: string, batchBoundary?: string): Promise<{
46
+ body: string;
47
+ boundary: string;
48
+ }>;
49
+ /**
50
+ * Extracts the boundary from a Content-Type header
51
+ * @param contentType - The Content-Type header value
52
+ * @returns The boundary string, or null if not found
53
+ */
54
+ export declare function extractBoundary(contentType: string): string | null;
55
+ /**
56
+ * Parses a batch response into individual responses
57
+ * @param responseText - The raw batch response text
58
+ * @param contentType - The Content-Type header from the response
59
+ * @returns Array of parsed responses in the same order as the request
60
+ */
61
+ export declare function parseBatchResponse(responseText: string, contentType: string): ParsedBatchResponse[];