@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,66 +1,56 @@
1
- import type {
2
- ExecutionContext,
3
- ExecutableBuilder,
4
- Result,
5
- WithSystemFields,
6
- ExecuteOptions,
7
- } from "../types";
8
- import type { TableOccurrence } from "./table-occurrence";
9
- import type { BaseTable } from "./base-table";
1
+ import type { FFetchOptions } from "@fetchkit/ffetch";
2
+ import type { FMTable, InferSchemaOutputFromFMTable } from "../orm/table";
3
+ import { getBaseTableConfig, getTableId as getTableIdHelper, getTableName, isUsingEntityIds } from "../orm/table";
4
+ import { transformFieldNamesToIds } from "../transform";
5
+ import type { ExecutableBuilder, ExecuteMethodOptions, ExecuteOptions, ExecutionContext, Result } from "../types";
6
+ import { getAcceptHeader } from "../types";
7
+ import { validateAndTransformInput } from "../validation";
8
+ import { parseErrorResponse } from "./error-parser";
10
9
  import { QueryBuilder } from "./query-builder";
11
- import { type FFetchOptions } from "@fetchkit/ffetch";
12
- import {
13
- transformFieldNamesToIds,
14
- transformTableName,
15
- getTableIdentifiers,
16
- } from "../transform";
17
10
 
18
11
  /**
19
12
  * Initial update builder returned from EntitySet.update(data)
20
13
  * Requires calling .byId() or .where() before .execute() is available
21
14
  */
22
15
  export class UpdateBuilder<
23
- T extends Record<string, any>,
24
- BT extends BaseTable<any, any, any, any>,
16
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
17
+ Occ extends FMTable<any, any>,
25
18
  ReturnPreference extends "minimal" | "representation" = "minimal",
26
19
  > {
27
- private tableName: string;
28
- private databaseName: string;
29
- private context: ExecutionContext;
30
- private occurrence?: TableOccurrence<any, any, any, any>;
31
- private data: Partial<T>;
32
- private returnPreference: ReturnPreference;
20
+ private readonly databaseName: string;
21
+ private readonly context: ExecutionContext;
22
+ private readonly table: Occ;
23
+ private readonly data: Partial<InferSchemaOutputFromFMTable<Occ>>;
24
+ private readonly returnPreference: ReturnPreference;
33
25
 
34
- private databaseUseEntityIds: boolean;
26
+ private readonly databaseUseEntityIds: boolean;
27
+ private readonly databaseIncludeSpecialColumns: boolean;
35
28
 
36
29
  constructor(config: {
37
- occurrence?: TableOccurrence<any, any, any, any>;
38
- tableName: string;
30
+ occurrence: Occ;
39
31
  databaseName: string;
40
32
  context: ExecutionContext;
41
- data: Partial<T>;
33
+ data: Partial<InferSchemaOutputFromFMTable<Occ>>;
42
34
  returnPreference: ReturnPreference;
43
35
  databaseUseEntityIds?: boolean;
36
+ databaseIncludeSpecialColumns?: boolean;
44
37
  }) {
45
- this.occurrence = config.occurrence;
46
- this.tableName = config.tableName;
38
+ this.table = config.occurrence;
47
39
  this.databaseName = config.databaseName;
48
40
  this.context = config.context;
49
41
  this.data = config.data;
50
42
  this.returnPreference = config.returnPreference;
51
43
  this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;
44
+ this.databaseIncludeSpecialColumns = config.databaseIncludeSpecialColumns ?? false;
52
45
  }
53
46
 
54
47
  /**
55
48
  * Update a single record by ID
56
49
  * Returns updated count by default, or full record if returnFullRecord was set to true
57
50
  */
58
- byId(
59
- id: string | number,
60
- ): ExecutableUpdateBuilder<T, true, ReturnPreference> {
61
- return new ExecutableUpdateBuilder<T, true, ReturnPreference>({
62
- occurrence: this.occurrence,
63
- tableName: this.tableName,
51
+ byId(id: string | number): ExecutableUpdateBuilder<Occ, true, ReturnPreference> {
52
+ return new ExecutableUpdateBuilder<Occ, true, ReturnPreference>({
53
+ occurrence: this.table,
64
54
  databaseName: this.databaseName,
65
55
  context: this.context,
66
56
  data: this.data,
@@ -76,21 +66,10 @@ export class UpdateBuilder<
76
66
  * Returns updated count by default, or full record if returnFullRecord was set to true
77
67
  * @param fn Callback that receives a QueryBuilder for building the filter
78
68
  */
79
- where(
80
- fn: (
81
- q: QueryBuilder<WithSystemFields<T>>,
82
- ) => QueryBuilder<WithSystemFields<T>>,
83
- ): ExecutableUpdateBuilder<T, true, ReturnPreference> {
69
+ where(fn: (q: QueryBuilder<Occ>) => QueryBuilder<Occ>): ExecutableUpdateBuilder<Occ, true, ReturnPreference> {
84
70
  // Create a QueryBuilder for the user to configure
85
- const queryBuilder = new QueryBuilder<
86
- WithSystemFields<T>,
87
- keyof WithSystemFields<T>,
88
- false,
89
- false,
90
- undefined
91
- >({
92
- occurrence: undefined,
93
- tableName: this.tableName,
71
+ const queryBuilder = new QueryBuilder<Occ>({
72
+ occurrence: this.table,
94
73
  databaseName: this.databaseName,
95
74
  context: this.context,
96
75
  });
@@ -98,9 +77,8 @@ export class UpdateBuilder<
98
77
  // Let the user configure it
99
78
  const configuredBuilder = fn(queryBuilder);
100
79
 
101
- return new ExecutableUpdateBuilder<T, true, ReturnPreference>({
102
- occurrence: this.occurrence,
103
- tableName: this.tableName,
80
+ return new ExecutableUpdateBuilder<Occ, true, ReturnPreference>({
81
+ occurrence: this.table,
104
82
  databaseName: this.databaseName,
105
83
  context: this.context,
106
84
  data: this.data,
@@ -118,39 +96,35 @@ export class UpdateBuilder<
118
96
  * Can return either updated count or full record based on returnFullRecord option
119
97
  */
120
98
  export class ExecutableUpdateBuilder<
121
- T extends Record<string, any>,
122
- IsByFilter extends boolean,
99
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
100
+ Occ extends FMTable<any, any>,
101
+ _IsByFilter extends boolean,
123
102
  ReturnPreference extends "minimal" | "representation" = "minimal",
124
103
  > implements
125
- ExecutableBuilder<
126
- ReturnPreference extends "minimal" ? { updatedCount: number } : T
127
- >
104
+ ExecutableBuilder<ReturnPreference extends "minimal" ? { updatedCount: number } : InferSchemaOutputFromFMTable<Occ>>
128
105
  {
129
- private tableName: string;
130
- private databaseName: string;
131
- private context: ExecutionContext;
132
- private occurrence?: TableOccurrence<any, any, any, any>;
133
- private data: Partial<T>;
134
- private mode: "byId" | "byFilter";
135
- private recordId?: string | number;
136
- private queryBuilder?: QueryBuilder<any>;
137
- private returnPreference: ReturnPreference;
138
- private databaseUseEntityIds: boolean;
106
+ private readonly databaseName: string;
107
+ private readonly context: ExecutionContext;
108
+ private readonly table: Occ;
109
+ private readonly data: Partial<InferSchemaOutputFromFMTable<Occ>>;
110
+ private readonly mode: "byId" | "byFilter";
111
+ private readonly recordId?: string | number;
112
+ private readonly queryBuilder?: QueryBuilder<Occ>;
113
+ private readonly returnPreference: ReturnPreference;
114
+ private readonly databaseUseEntityIds: boolean;
139
115
 
140
116
  constructor(config: {
141
- occurrence?: TableOccurrence<any, any, any, any>;
142
- tableName: string;
117
+ occurrence: Occ;
143
118
  databaseName: string;
144
119
  context: ExecutionContext;
145
- data: Partial<T>;
120
+ data: Partial<InferSchemaOutputFromFMTable<Occ>>;
146
121
  mode: "byId" | "byFilter";
147
122
  recordId?: string | number;
148
- queryBuilder?: QueryBuilder<any>;
123
+ queryBuilder?: QueryBuilder<Occ>;
149
124
  returnPreference: ReturnPreference;
150
125
  databaseUseEntityIds?: boolean;
151
126
  }) {
152
- this.occurrence = config.occurrence;
153
- this.tableName = config.tableName;
127
+ this.table = config.occurrence;
154
128
  this.databaseName = config.databaseName;
155
129
  this.context = config.context;
156
130
  this.data = config.data;
@@ -179,30 +153,25 @@ export class ExecutableUpdateBuilder<
179
153
  * @param useEntityIds - Optional override for entity ID usage
180
154
  */
181
155
  private getTableId(useEntityIds?: boolean): string {
182
- if (!this.occurrence) {
183
- return this.tableName;
184
- }
185
-
186
156
  const contextDefault = this.context._getUseEntityIds?.() ?? false;
187
157
  const shouldUseIds = useEntityIds ?? contextDefault;
188
158
 
189
159
  if (shouldUseIds) {
190
- const identifiers = getTableIdentifiers(this.occurrence);
191
- if (!identifiers.id) {
160
+ if (!isUsingEntityIds(this.table)) {
192
161
  throw new Error(
193
- `useEntityIds is true but TableOccurrence "${identifiers.name}" does not have an fmtId defined`,
162
+ `useEntityIds is true but table "${getTableName(this.table)}" does not have entity IDs configured`,
194
163
  );
195
164
  }
196
- return identifiers.id;
165
+ return getTableIdHelper(this.table);
197
166
  }
198
167
 
199
- return this.occurrence.getTableName();
168
+ return getTableName(this.table);
200
169
  }
201
170
 
202
171
  async execute(
203
- options?: RequestInit & FFetchOptions & { useEntityIds?: boolean },
172
+ options?: ExecuteMethodOptions<ExecuteOptions>,
204
173
  ): Promise<
205
- Result<ReturnPreference extends "minimal" ? { updatedCount: number } : T>
174
+ Result<ReturnPreference extends "minimal" ? { updatedCount: number } : InferSchemaOutputFromFMTable<Occ>>
206
175
  > {
207
176
  // Merge database-level useEntityIds with per-request options
208
177
  const mergedOptions = this.mergeExecuteOptions(options);
@@ -210,14 +179,30 @@ export class ExecutableUpdateBuilder<
210
179
  // Get table identifier with override support
211
180
  const tableId = this.getTableId(mergedOptions.useEntityIds);
212
181
 
182
+ // Validate and transform input data using input validators (writeValidators)
183
+ let validatedData = this.data;
184
+ if (this.table) {
185
+ const baseTableConfig = getBaseTableConfig(this.table);
186
+ const inputSchema = baseTableConfig.inputSchema;
187
+
188
+ try {
189
+ validatedData = await validateAndTransformInput(this.data, inputSchema);
190
+ } catch (error) {
191
+ // If validation fails, return error immediately
192
+ return {
193
+ data: undefined,
194
+ error: error instanceof Error ? error : new Error(String(error)),
195
+ // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
196
+ } as any;
197
+ }
198
+ }
199
+
213
200
  // Transform field names to FMFIDs if using entity IDs
214
201
  // Only transform if useEntityIds resolves to true (respects per-request override)
215
202
  const shouldUseIds = mergedOptions.useEntityIds ?? false;
216
203
 
217
204
  const transformedData =
218
- this.occurrence?.baseTable && shouldUseIds
219
- ? transformFieldNamesToIds(this.data, this.occurrence.baseTable)
220
- : this.data;
205
+ this.table && shouldUseIds ? transformFieldNamesToIds(validatedData, this.table) : validatedData;
221
206
 
222
207
  let url: string;
223
208
 
@@ -234,11 +219,15 @@ export class ExecutableUpdateBuilder<
234
219
  const queryString = this.queryBuilder.getQueryString();
235
220
  // The query string will have the tableId already transformed by QueryBuilder
236
221
  // Remove the leading "/" and table name from the query string as we'll build our own URL
237
- const queryParams = queryString.startsWith(`/${tableId}`)
238
- ? queryString.slice(`/${tableId}`.length)
239
- : queryString.startsWith(`/${this.tableName}`)
240
- ? queryString.slice(`/${this.tableName}`.length)
241
- : queryString;
222
+ const tableName = getTableName(this.table);
223
+ let queryParams: string;
224
+ if (queryString.startsWith(`/${tableId}`)) {
225
+ queryParams = queryString.slice(`/${tableId}`.length);
226
+ } else if (queryString.startsWith(`/${tableName}`)) {
227
+ queryParams = queryString.slice(`/${tableName}`.length);
228
+ } else {
229
+ queryParams = queryString;
230
+ }
242
231
 
243
232
  url = `/${this.databaseName}/${tableId}${queryParams}`;
244
233
  }
@@ -249,7 +238,7 @@ export class ExecutableUpdateBuilder<
249
238
  };
250
239
 
251
240
  if (this.returnPreference === "representation") {
252
- headers["Prefer"] = "return=representation";
241
+ headers.Prefer = "return=representation";
253
242
  }
254
243
 
255
244
  // Make PATCH request with JSON body
@@ -272,38 +261,38 @@ export class ExecutableUpdateBuilder<
272
261
  return {
273
262
  data: response as ReturnPreference extends "minimal"
274
263
  ? { updatedCount: number }
275
- : T,
276
- error: undefined,
277
- };
278
- } else {
279
- // Return updated count (minimal)
280
- let updatedCount = 0;
281
-
282
- if (typeof response === "number") {
283
- updatedCount = response;
284
- } else if (response && typeof response === "object") {
285
- // Check if the response has a count property (fallback)
286
- updatedCount = (response as any).updatedCount || 0;
287
- }
288
-
289
- return {
290
- data: { updatedCount } as ReturnPreference extends "minimal"
291
- ? { updatedCount: number }
292
- : T,
264
+ : InferSchemaOutputFromFMTable<Occ>,
293
265
  error: undefined,
294
266
  };
295
267
  }
268
+ // Return updated count (minimal)
269
+ let updatedCount = 0;
270
+
271
+ if (typeof response === "number") {
272
+ updatedCount = response;
273
+ } else if (response && typeof response === "object") {
274
+ // Check if the response has a count property (fallback)
275
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API
276
+ updatedCount = (response as any).updatedCount || 0;
277
+ }
278
+
279
+ return {
280
+ data: { updatedCount } as ReturnPreference extends "minimal"
281
+ ? { updatedCount: number }
282
+ : InferSchemaOutputFromFMTable<Occ>,
283
+ error: undefined,
284
+ };
296
285
  }
297
286
 
287
+ // biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value
298
288
  getRequestConfig(): { method: string; url: string; body?: any } {
299
289
  // For batch operations, use database-level setting (no per-request override available here)
290
+ // Note: Input validation happens in execute() and processResponse() for batch operations
300
291
  const tableId = this.getTableId(this.databaseUseEntityIds);
301
292
 
302
293
  // Transform field names to FMFIDs if using entity IDs
303
294
  const transformedData =
304
- this.occurrence?.baseTable && this.databaseUseEntityIds
305
- ? transformFieldNamesToIds(this.data, this.occurrence.baseTable)
306
- : this.data;
295
+ this.table && this.databaseUseEntityIds ? transformFieldNamesToIds(this.data, this.table) : this.data;
307
296
 
308
297
  let url: string;
309
298
 
@@ -315,11 +304,15 @@ export class ExecutableUpdateBuilder<
315
304
  }
316
305
 
317
306
  const queryString = this.queryBuilder.getQueryString();
318
- const queryParams = queryString.startsWith(`/${tableId}`)
319
- ? queryString.slice(`/${tableId}`.length)
320
- : queryString.startsWith(`/${this.tableName}`)
321
- ? queryString.slice(`/${this.tableName}`.length)
322
- : queryString;
307
+ const tableName = getTableName(this.table);
308
+ let queryParams: string;
309
+ if (queryString.startsWith(`/${tableId}`)) {
310
+ queryParams = queryString.slice(`/${tableId}`.length);
311
+ } else if (queryString.startsWith(`/${tableName}`)) {
312
+ queryParams = queryString.slice(`/${tableName}`.length);
313
+ } else {
314
+ queryParams = queryString;
315
+ }
323
316
 
324
317
  url = `/${this.databaseName}/${tableId}${queryParams}`;
325
318
  }
@@ -331,7 +324,7 @@ export class ExecutableUpdateBuilder<
331
324
  };
332
325
  }
333
326
 
334
- toRequest(baseUrl: string): Request {
327
+ toRequest(baseUrl: string, options?: ExecuteOptions): Request {
335
328
  const config = this.getRequestConfig();
336
329
  const fullUrl = `${baseUrl}${config.url}`;
337
330
 
@@ -339,7 +332,7 @@ export class ExecutableUpdateBuilder<
339
332
  method: config.method,
340
333
  headers: {
341
334
  "Content-Type": "application/json",
342
- Accept: "application/json",
335
+ Accept: getAcceptHeader(options?.includeODataAnnotations),
343
336
  },
344
337
  body: config.body,
345
338
  });
@@ -347,52 +340,77 @@ export class ExecutableUpdateBuilder<
347
340
 
348
341
  async processResponse(
349
342
  response: Response,
350
- options?: ExecuteOptions,
343
+ _options?: ExecuteOptions,
351
344
  ): Promise<
352
- Result<ReturnPreference extends "minimal" ? { updatedCount: number } : T>
345
+ Result<ReturnPreference extends "minimal" ? { updatedCount: number } : InferSchemaOutputFromFMTable<Occ>>
353
346
  > {
347
+ // Check for error responses (important for batch operations)
348
+ if (!response.ok) {
349
+ const tableName = getTableName(this.table);
350
+ const error = await parseErrorResponse(response, response.url || `/${this.databaseName}/${tableName}`);
351
+ return { data: undefined, error };
352
+ }
353
+
354
354
  // Check for empty response (204 No Content)
355
355
  const text = await response.text();
356
356
  if (!text || text.trim() === "") {
357
357
  // For 204 No Content, check the fmodata.affected_rows header
358
358
  const affectedRows = response.headers.get("fmodata.affected_rows");
359
- const updatedCount = affectedRows ? parseInt(affectedRows, 10) : 1;
359
+ const updatedCount = affectedRows ? Number.parseInt(affectedRows, 10) : 1;
360
360
  return {
361
361
  data: { updatedCount } as ReturnPreference extends "minimal"
362
362
  ? { updatedCount: number }
363
- : T,
363
+ : InferSchemaOutputFromFMTable<Occ>,
364
364
  error: undefined,
365
365
  };
366
366
  }
367
367
 
368
368
  const rawResponse = JSON.parse(text);
369
369
 
370
+ // Validate and transform input data using input validators (writeValidators)
371
+ // This is needed for processResponse because it's called from batch operations
372
+ // where the data hasn't been validated yet
373
+ let _validatedData = this.data;
374
+ if (this.table) {
375
+ const baseTableConfig = getBaseTableConfig(this.table);
376
+ const inputSchema = baseTableConfig.inputSchema;
377
+ try {
378
+ _validatedData = await validateAndTransformInput(this.data, inputSchema);
379
+ } catch (error) {
380
+ return {
381
+ data: undefined,
382
+ error: error instanceof Error ? error : new Error(String(error)),
383
+ // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
384
+ } as any;
385
+ }
386
+ }
387
+
370
388
  // Handle based on return preference
371
389
  if (this.returnPreference === "representation") {
372
390
  // Return the full updated record
373
391
  return {
374
392
  data: rawResponse as ReturnPreference extends "minimal"
375
393
  ? { updatedCount: number }
376
- : T,
377
- error: undefined,
378
- };
379
- } else {
380
- // Return updated count (minimal)
381
- let updatedCount = 0;
382
-
383
- if (typeof rawResponse === "number") {
384
- updatedCount = rawResponse;
385
- } else if (rawResponse && typeof rawResponse === "object") {
386
- // Check if the response has a count property (fallback)
387
- updatedCount = (rawResponse as any).updatedCount || 0;
388
- }
389
-
390
- return {
391
- data: { updatedCount } as ReturnPreference extends "minimal"
392
- ? { updatedCount: number }
393
- : T,
394
+ : InferSchemaOutputFromFMTable<Occ>,
394
395
  error: undefined,
395
396
  };
396
397
  }
398
+ // Return updated count (minimal)
399
+ let updatedCount = 0;
400
+
401
+ if (typeof rawResponse === "number") {
402
+ updatedCount = rawResponse;
403
+ } else if (rawResponse && typeof rawResponse === "object") {
404
+ // Check if the response has a count property (fallback)
405
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API
406
+ updatedCount = (rawResponse as any).updatedCount || 0;
407
+ }
408
+
409
+ return {
410
+ data: { updatedCount } as ReturnPreference extends "minimal"
411
+ ? { updatedCount: number }
412
+ : InferSchemaOutputFromFMTable<Occ>,
413
+ error: undefined,
414
+ };
397
415
  }
398
416
  }