@proofkit/fmodata 0.1.0-alpha.1 → 0.1.0-alpha.10

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 (75) hide show
  1. package/README.md +746 -65
  2. package/dist/esm/client/base-table.d.ts +117 -5
  3. package/dist/esm/client/base-table.js +43 -5
  4. package/dist/esm/client/base-table.js.map +1 -1
  5. package/dist/esm/client/batch-builder.d.ts +54 -0
  6. package/dist/esm/client/batch-builder.js +179 -0
  7. package/dist/esm/client/batch-builder.js.map +1 -0
  8. package/dist/esm/client/batch-request.d.ts +61 -0
  9. package/dist/esm/client/batch-request.js +252 -0
  10. package/dist/esm/client/batch-request.js.map +1 -0
  11. package/dist/esm/client/database.d.ts +55 -6
  12. package/dist/esm/client/database.js +118 -15
  13. package/dist/esm/client/database.js.map +1 -1
  14. package/dist/esm/client/delete-builder.d.ts +21 -2
  15. package/dist/esm/client/delete-builder.js +96 -32
  16. package/dist/esm/client/delete-builder.js.map +1 -1
  17. package/dist/esm/client/entity-set.d.ts +25 -11
  18. package/dist/esm/client/entity-set.js +31 -11
  19. package/dist/esm/client/entity-set.js.map +1 -1
  20. package/dist/esm/client/filemaker-odata.d.ts +23 -4
  21. package/dist/esm/client/filemaker-odata.js +124 -29
  22. package/dist/esm/client/filemaker-odata.js.map +1 -1
  23. package/dist/esm/client/insert-builder.d.ts +38 -3
  24. package/dist/esm/client/insert-builder.js +231 -34
  25. package/dist/esm/client/insert-builder.js.map +1 -1
  26. package/dist/esm/client/query-builder.d.ts +27 -6
  27. package/dist/esm/client/query-builder.js +457 -210
  28. package/dist/esm/client/query-builder.js.map +1 -1
  29. package/dist/esm/client/record-builder.d.ts +96 -9
  30. package/dist/esm/client/record-builder.js +378 -39
  31. package/dist/esm/client/record-builder.js.map +1 -1
  32. package/dist/esm/client/response-processor.d.ts +38 -0
  33. package/dist/esm/client/schema-manager.d.ts +57 -0
  34. package/dist/esm/client/schema-manager.js +132 -0
  35. package/dist/esm/client/schema-manager.js.map +1 -0
  36. package/dist/esm/client/table-occurrence.d.ts +48 -1
  37. package/dist/esm/client/table-occurrence.js +29 -2
  38. package/dist/esm/client/table-occurrence.js.map +1 -1
  39. package/dist/esm/client/update-builder.d.ts +34 -11
  40. package/dist/esm/client/update-builder.js +135 -31
  41. package/dist/esm/client/update-builder.js.map +1 -1
  42. package/dist/esm/errors.d.ts +73 -0
  43. package/dist/esm/errors.js +148 -0
  44. package/dist/esm/errors.js.map +1 -0
  45. package/dist/esm/index.d.ts +10 -3
  46. package/dist/esm/index.js +28 -5
  47. package/dist/esm/index.js.map +1 -1
  48. package/dist/esm/transform.d.ts +65 -0
  49. package/dist/esm/transform.js +114 -0
  50. package/dist/esm/transform.js.map +1 -0
  51. package/dist/esm/types.d.ts +89 -5
  52. package/dist/esm/validation.d.ts +6 -3
  53. package/dist/esm/validation.js +104 -33
  54. package/dist/esm/validation.js.map +1 -1
  55. package/package.json +10 -1
  56. package/src/client/base-table.ts +158 -8
  57. package/src/client/batch-builder.ts +265 -0
  58. package/src/client/batch-request.ts +485 -0
  59. package/src/client/database.ts +175 -18
  60. package/src/client/delete-builder.ts +149 -48
  61. package/src/client/entity-set.ts +114 -23
  62. package/src/client/filemaker-odata.ts +179 -35
  63. package/src/client/insert-builder.ts +350 -40
  64. package/src/client/query-builder.ts +616 -237
  65. package/src/client/query-builder.ts.bak +1457 -0
  66. package/src/client/record-builder.ts +692 -65
  67. package/src/client/response-processor.ts +103 -0
  68. package/src/client/schema-manager.ts +246 -0
  69. package/src/client/table-occurrence.ts +78 -3
  70. package/src/client/update-builder.ts +235 -49
  71. package/src/errors.ts +217 -0
  72. package/src/index.ts +59 -2
  73. package/src/transform.ts +249 -0
  74. package/src/types.ts +201 -35
  75. package/src/validation.ts +120 -36
@@ -1,14 +1,15 @@
1
- import { z } from "zod/v4";
2
1
  import type { StandardSchemaV1 } from "@standard-schema/spec";
3
- import type { ExecutionContext } from "../types";
2
+ import type { ExecutionContext, ExecutableBuilder, Metadata } from "../types";
4
3
  import type { BaseTable } from "./base-table";
5
4
  import type { TableOccurrence } from "./table-occurrence";
6
5
  import { EntitySet } from "./entity-set";
6
+ import { BatchBuilder } from "./batch-builder";
7
+ import { SchemaManager } from "./schema-manager";
7
8
 
8
9
  // Helper type to extract schema from a TableOccurrence
9
10
  type ExtractSchemaFromOccurrence<O> =
10
11
  O extends TableOccurrence<infer BT, any, any, any>
11
- ? BT extends BaseTable<infer S, any>
12
+ ? BT extends BaseTable<infer S, any, any, any>
12
13
  ? S
13
14
  : never
14
15
  : never;
@@ -44,30 +45,122 @@ export class Database<
44
45
  >[] = readonly [],
45
46
  > {
46
47
  private occurrenceMap: Map<string, TableOccurrence<any, any, any, any>>;
48
+ private _useEntityIds: boolean = false;
49
+ public readonly schema: SchemaManager;
47
50
 
48
51
  constructor(
49
52
  private readonly databaseName: string,
50
53
  private readonly context: ExecutionContext,
51
- config?: { occurrences?: Occurrences },
54
+ config?: {
55
+ occurrences?: Occurrences | undefined;
56
+ /**
57
+ * Whether to use entity IDs instead of field names in the actual requests to the server
58
+ * Defaults to true if all occurrences use entity IDs, false otherwise
59
+ * If set to false but some occurrences do not use entity IDs, an error will be thrown
60
+ */
61
+ useEntityIds?: boolean;
62
+ },
52
63
  ) {
53
64
  this.occurrenceMap = new Map();
54
65
  if (config?.occurrences) {
66
+ // Validate consistency: either all occurrences use entity IDs or none do
67
+ const occurrencesWithIds: string[] = [];
68
+ const occurrencesWithoutIds: string[] = [];
69
+
55
70
  for (const occ of config.occurrences) {
56
71
  this.occurrenceMap.set(occ.name, occ);
72
+
73
+ const hasTableId = occ.isUsingTableId();
74
+ const hasFieldIds = occ.baseTable.isUsingFieldIds();
75
+
76
+ // An occurrence uses entity IDs if it has both fmtId and fmfIds
77
+ if (hasTableId && hasFieldIds) {
78
+ occurrencesWithIds.push(occ.name);
79
+ } else if (!hasTableId && !hasFieldIds) {
80
+ occurrencesWithoutIds.push(occ.name);
81
+ } else {
82
+ // Partial entity ID usage (only one of fmtId or fmfIds) - this is an error
83
+ throw new Error(
84
+ `TableOccurrence "${occ.name}" has inconsistent entity ID configuration. ` +
85
+ `Both fmtId (${hasTableId ? "present" : "missing"}) and fmfIds (${hasFieldIds ? "present" : "missing"}) must be defined together.`,
86
+ );
87
+ }
57
88
  }
89
+
90
+ // Determine default value: true if all occurrences use entity IDs, false otherwise
91
+ const allOccurrencesUseEntityIds =
92
+ occurrencesWithIds.length > 0 && occurrencesWithoutIds.length === 0;
93
+ const hasMixedUsage =
94
+ occurrencesWithIds.length > 0 && occurrencesWithoutIds.length > 0;
95
+
96
+ // Handle explicit useEntityIds config
97
+ if (config.useEntityIds !== undefined) {
98
+ if (config.useEntityIds === false) {
99
+ // If explicitly set to false, allow mixed usage and use false
100
+ this._useEntityIds = false;
101
+ } else if (config.useEntityIds === true) {
102
+ // If explicitly set to true, validate that all occurrences use entity IDs
103
+ if (hasMixedUsage || occurrencesWithoutIds.length > 0) {
104
+ throw new Error(
105
+ `useEntityIds is set to true but some occurrences do not use entity IDs. ` +
106
+ `Occurrences without entity IDs: [${occurrencesWithoutIds.join(", ")}]. ` +
107
+ `Either set useEntityIds to false or configure all occurrences with entity IDs.`,
108
+ );
109
+ }
110
+ this._useEntityIds = true;
111
+ }
112
+ } else {
113
+ // Default: true if all occurrences use entity IDs, false otherwise
114
+ // But throw error if there's mixed usage when using defaults
115
+ if (hasMixedUsage) {
116
+ throw new Error(
117
+ `Cannot mix TableOccurrence instances with and without entity IDs in the same database. ` +
118
+ `Occurrences with entity IDs: [${occurrencesWithIds.join(", ")}]. ` +
119
+ `Occurrences without entity IDs: [${occurrencesWithoutIds.join(", ")}]. ` +
120
+ `Either all table occurrences must use entity IDs (fmtId + fmfIds), none should, or explicitly set useEntityIds to false.`,
121
+ );
122
+ }
123
+ this._useEntityIds = allOccurrencesUseEntityIds;
124
+ }
125
+ } else {
126
+ // No occurrences provided, use explicit config or default to false
127
+ this._useEntityIds = config?.useEntityIds ?? false;
128
+ }
129
+
130
+ // Inform the execution context whether to use entity IDs
131
+ if (this.context._setUseEntityIds) {
132
+ this.context._setUseEntityIds(this._useEntityIds);
58
133
  }
134
+
135
+ // Initialize schema manager
136
+ this.schema = new SchemaManager(this.databaseName, this.context);
137
+ }
138
+
139
+ /**
140
+ * Returns true if any table occurrence in this database is using entity IDs.
141
+ */
142
+ isUsingEntityIds(): boolean {
143
+ return this._useEntityIds;
144
+ }
145
+
146
+ /**
147
+ * Gets a table occurrence by name.
148
+ * @internal
149
+ */
150
+ getOccurrence(name: string): TableOccurrence<any, any, any, any> | undefined {
151
+ return this.occurrenceMap.get(name);
59
152
  }
60
153
 
61
154
  from<Name extends ExtractOccurrenceNames<Occurrences> | (string & {})>(
62
155
  name: Name,
63
156
  ): Occurrences extends readonly []
64
- ? EntitySet<Record<string, z.ZodTypeAny>, undefined>
157
+ ? EntitySet<Record<string, StandardSchemaV1>, undefined>
65
158
  : Name extends ExtractOccurrenceNames<Occurrences>
66
159
  ? EntitySet<
67
160
  ExtractSchemaFromOccurrence<FindOccurrenceByName<Occurrences, Name>>,
68
161
  FindOccurrenceByName<Occurrences, Name>
69
162
  >
70
- : EntitySet<Record<string, z.ZodTypeAny>, undefined> {
163
+ : EntitySet<Record<string, StandardSchemaV1>, undefined> {
71
164
  const occurrence = this.occurrenceMap.get(name as string);
72
165
 
73
166
  if (occurrence) {
@@ -76,24 +169,56 @@ export class Database<
76
169
  type SchemaType = ExtractSchemaFromOccurrence<OccType>;
77
170
 
78
171
  return EntitySet.create<SchemaType, OccType>({
79
- occurrence: occurrence as any,
172
+ occurrence: occurrence as OccType,
80
173
  tableName: name as string,
81
174
  databaseName: this.databaseName,
82
175
  context: this.context,
176
+ database: this,
83
177
  }) as any;
84
178
  } else {
85
179
  // Return untyped EntitySet for dynamic table access
86
- return new EntitySet<Record<string, z.ZodTypeAny>, undefined>({
180
+ return new EntitySet<Record<string, StandardSchemaV1>, undefined>({
87
181
  tableName: name as string,
88
182
  databaseName: this.databaseName,
89
183
  context: this.context,
184
+ database: this,
90
185
  }) as any;
91
186
  }
92
187
  }
93
188
 
94
- // Example method showing how to use the request method
95
- async getMetadata() {
96
- return this.context._makeRequest(`/${this.databaseName}/$metadata`);
189
+ /**
190
+ * Retrieves the OData metadata for this database.
191
+ * @param args Optional configuration object
192
+ * @param args.format The format to retrieve metadata in. Defaults to "json".
193
+ * @returns The metadata in the specified format
194
+ */
195
+ async getMetadata(args: { format: "xml" }): Promise<string>;
196
+ async getMetadata(args?: { format?: "json" }): Promise<Metadata>;
197
+ async getMetadata(args?: {
198
+ format?: "xml" | "json";
199
+ }): Promise<string | Metadata> {
200
+ const result = await this.context._makeRequest<
201
+ Record<string, Metadata> | string
202
+ >(`/${this.databaseName}/$metadata`, {
203
+ headers: {
204
+ Accept: args?.format === "xml" ? "application/xml" : "application/json",
205
+ },
206
+ });
207
+ if (result.error) {
208
+ throw result.error;
209
+ }
210
+
211
+ if (args?.format === "json") {
212
+ const data = result.data as Record<string, Metadata>;
213
+ const metadata = data[this.databaseName];
214
+ if (!metadata) {
215
+ throw new Error(
216
+ `Metadata for database "${this.databaseName}" not found in response`,
217
+ );
218
+ }
219
+ return metadata;
220
+ }
221
+ return result.data as string;
97
222
  }
98
223
 
99
224
  /**
@@ -101,13 +226,14 @@ export class Database<
101
226
  * @returns Promise resolving to an array of table names
102
227
  */
103
228
  async listTableNames(): Promise<string[]> {
104
- const response = (await this.context._makeRequest(
105
- `/${this.databaseName}`,
106
- )) as {
229
+ const result = await this.context._makeRequest<{
107
230
  value?: Array<{ name: string }>;
108
- };
109
- if (response.value && Array.isArray(response.value)) {
110
- return response.value.map((item) => item.name);
231
+ }>(`/${this.databaseName}`);
232
+ if (result.error) {
233
+ throw result.error;
234
+ }
235
+ if (result.data.value && Array.isArray(result.data.value)) {
236
+ return result.data.value.map((item) => item.name);
111
237
  }
112
238
  return [];
113
239
  }
@@ -136,7 +262,7 @@ export class Database<
136
262
  body.scriptParameterValue = options.scriptParam;
137
263
  }
138
264
 
139
- const response = await this.context._makeRequest<{
265
+ const result = await this.context._makeRequest<{
140
266
  scriptResult: {
141
267
  code: number;
142
268
  resultParameter?: string;
@@ -146,6 +272,12 @@ export class Database<
146
272
  body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined,
147
273
  });
148
274
 
275
+ if (result.error) {
276
+ throw result.error;
277
+ }
278
+
279
+ const response = result.data;
280
+
149
281
  // If resultSchema is provided, validate the result through it
150
282
  if (options?.resultSchema && response.scriptResult !== undefined) {
151
283
  const validationResult = options.resultSchema["~standard"].validate(
@@ -174,4 +306,29 @@ export class Database<
174
306
  result: response.scriptResult.resultParameter,
175
307
  } as any;
176
308
  }
309
+
310
+ /**
311
+ * Create a batch operation builder that allows multiple queries to be executed together
312
+ * in a single atomic request. All operations succeed or fail together (transactional).
313
+ *
314
+ * @param builders - Array of executable query builders to batch
315
+ * @returns A BatchBuilder that can be executed
316
+ * @example
317
+ * ```ts
318
+ * const result = await db.batch([
319
+ * db.from('contacts').list().top(5),
320
+ * db.from('users').list().top(5),
321
+ * db.from('contacts').insert({ name: 'John' })
322
+ * ]).execute();
323
+ *
324
+ * if (result.data) {
325
+ * const [contacts, users, insertResult] = result.data;
326
+ * }
327
+ * ```
328
+ */
329
+ batch<const Builders extends readonly ExecutableBuilder<any>[]>(
330
+ builders: Builders,
331
+ ): BatchBuilder<Builders> {
332
+ return new BatchBuilder(builders, this.databaseName, this.context);
333
+ }
177
334
  }
@@ -3,11 +3,12 @@ import type {
3
3
  ExecutableBuilder,
4
4
  Result,
5
5
  WithSystemFields,
6
+ ExecuteOptions,
6
7
  } from "../types";
7
8
  import type { TableOccurrence } from "./table-occurrence";
8
9
  import { QueryBuilder } from "./query-builder";
9
10
  import { type FFetchOptions } from "@fetchkit/ffetch";
10
- import buildQuery from "odata-query";
11
+ import { getTableIdentifiers } from "../transform";
11
12
 
12
13
  /**
13
14
  * Initial delete builder returned from EntitySet.delete()
@@ -18,17 +19,20 @@ export class DeleteBuilder<T extends Record<string, any>> {
18
19
  private databaseName: string;
19
20
  private context: ExecutionContext;
20
21
  private occurrence?: TableOccurrence<any, any, any, any>;
22
+ private databaseUseEntityIds: boolean;
21
23
 
22
24
  constructor(config: {
23
25
  occurrence?: TableOccurrence<any, any, any, any>;
24
26
  tableName: string;
25
27
  databaseName: string;
26
28
  context: ExecutionContext;
29
+ databaseUseEntityIds?: boolean;
27
30
  }) {
28
31
  this.occurrence = config.occurrence;
29
32
  this.tableName = config.tableName;
30
33
  this.databaseName = config.databaseName;
31
34
  this.context = config.context;
35
+ this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;
32
36
  }
33
37
 
34
38
  /**
@@ -42,6 +46,7 @@ export class DeleteBuilder<T extends Record<string, any>> {
42
46
  context: this.context,
43
47
  mode: "byId",
44
48
  recordId: id,
49
+ databaseUseEntityIds: this.databaseUseEntityIds,
45
50
  });
46
51
  }
47
52
 
@@ -78,6 +83,7 @@ export class DeleteBuilder<T extends Record<string, any>> {
78
83
  context: this.context,
79
84
  mode: "byFilter",
80
85
  queryBuilder: configuredBuilder,
86
+ databaseUseEntityIds: this.databaseUseEntityIds,
81
87
  });
82
88
  }
83
89
  }
@@ -96,6 +102,7 @@ export class ExecutableDeleteBuilder<T extends Record<string, any>>
96
102
  private mode: "byId" | "byFilter";
97
103
  private recordId?: string | number;
98
104
  private queryBuilder?: QueryBuilder<any>;
105
+ private databaseUseEntityIds: boolean;
99
106
 
100
107
  constructor(config: {
101
108
  occurrence?: TableOccurrence<any, any, any, any>;
@@ -105,6 +112,7 @@ export class ExecutableDeleteBuilder<T extends Record<string, any>>
105
112
  mode: "byId" | "byFilter";
106
113
  recordId?: string | number;
107
114
  queryBuilder?: QueryBuilder<any>;
115
+ databaseUseEntityIds?: boolean;
108
116
  }) {
109
117
  this.occurrence = config.occurrence;
110
118
  this.tableName = config.tableName;
@@ -113,76 +121,127 @@ export class ExecutableDeleteBuilder<T extends Record<string, any>>
113
121
  this.mode = config.mode;
114
122
  this.recordId = config.recordId;
115
123
  this.queryBuilder = config.queryBuilder;
124
+ this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;
125
+ }
126
+
127
+ /**
128
+ * Helper to merge database-level useEntityIds with per-request options
129
+ */
130
+ private mergeExecuteOptions(
131
+ options?: RequestInit & FFetchOptions & ExecuteOptions,
132
+ ): RequestInit & FFetchOptions & { useEntityIds?: boolean } {
133
+ // If useEntityIds is not set in options, use the database-level setting
134
+ return {
135
+ ...options,
136
+ useEntityIds: options?.useEntityIds ?? this.databaseUseEntityIds,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Gets the table ID (FMTID) if using entity IDs, otherwise returns the table name
142
+ * @param useEntityIds - Optional override for entity ID usage
143
+ */
144
+ private getTableId(useEntityIds?: boolean): string {
145
+ if (!this.occurrence) {
146
+ return this.tableName;
147
+ }
148
+
149
+ const contextDefault = this.context._getUseEntityIds?.() ?? false;
150
+ const shouldUseIds = useEntityIds ?? contextDefault;
151
+
152
+ if (shouldUseIds) {
153
+ const identifiers = getTableIdentifiers(this.occurrence);
154
+ if (!identifiers.id) {
155
+ throw new Error(
156
+ `useEntityIds is true but TableOccurrence "${identifiers.name}" does not have an fmtId defined`,
157
+ );
158
+ }
159
+ return identifiers.id;
160
+ }
161
+
162
+ return this.occurrence.getTableName();
116
163
  }
117
164
 
118
165
  async execute(
119
- options?: RequestInit & FFetchOptions,
166
+ options?: RequestInit & FFetchOptions & { useEntityIds?: boolean },
120
167
  ): Promise<Result<{ deletedCount: number }>> {
121
- try {
122
- let url: string;
123
-
124
- if (this.mode === "byId") {
125
- // Delete single record by ID: DELETE /{database}/{table}('id')
126
- url = `/${this.databaseName}/${this.tableName}('${this.recordId}')`;
127
- } else {
128
- // Delete by filter: DELETE /{database}/{table}?$filter=...
129
- if (!this.queryBuilder) {
130
- throw new Error("Query builder is required for filter-based delete");
131
- }
132
-
133
- // Get the query string from the configured QueryBuilder
134
- const queryString = this.queryBuilder.getQueryString();
135
- // Remove the leading "/" from the query string as we'll build our own URL
136
- const queryParams = queryString.startsWith(`/${this.tableName}`)
168
+ // Merge database-level useEntityIds with per-request options
169
+ const mergedOptions = this.mergeExecuteOptions(options);
170
+
171
+ // Get table identifier with override support
172
+ const tableId = this.getTableId(mergedOptions.useEntityIds);
173
+
174
+ let url: string;
175
+
176
+ if (this.mode === "byId") {
177
+ // Delete single record by ID: DELETE /{database}/{table}('id')
178
+ url = `/${this.databaseName}/${tableId}('${this.recordId}')`;
179
+ } else {
180
+ // Delete by filter: DELETE /{database}/{table}?$filter=...
181
+ if (!this.queryBuilder) {
182
+ throw new Error("Query builder is required for filter-based delete");
183
+ }
184
+
185
+ // Get the query string from the configured QueryBuilder
186
+ const queryString = this.queryBuilder.getQueryString();
187
+ // Remove the leading "/" and table name from the query string as we'll build our own URL
188
+ const queryParams = queryString.startsWith(`/${tableId}`)
189
+ ? queryString.slice(`/${tableId}`.length)
190
+ : queryString.startsWith(`/${this.tableName}`)
137
191
  ? queryString.slice(`/${this.tableName}`.length)
138
192
  : queryString;
139
193
 
140
- url = `/${this.databaseName}/${this.tableName}${queryParams}`;
141
- }
194
+ url = `/${this.databaseName}/${tableId}${queryParams}`;
195
+ }
142
196
 
143
- // Make DELETE request
144
- const response = await this.context._makeRequest(url, {
145
- method: "DELETE",
146
- ...options,
147
- });
148
-
149
- // OData returns 204 No Content with fmodata.affected_rows header
150
- // The _makeRequest should handle extracting the header value
151
- // For now, we'll check if response contains the count
152
- let deletedCount = 0;
153
-
154
- if (typeof response === "number") {
155
- deletedCount = response;
156
- } else if (response && typeof response === "object") {
157
- // Check if the response has a count property (fallback)
158
- deletedCount = (response as any).deletedCount || 0;
159
- }
197
+ // Make DELETE request
198
+ const result = await this.context._makeRequest(url, {
199
+ method: "DELETE",
200
+ ...mergedOptions,
201
+ });
160
202
 
161
- return { data: { deletedCount }, error: undefined };
162
- } catch (error) {
163
- return {
164
- data: undefined,
165
- error: error instanceof Error ? error : new Error(String(error)),
166
- };
203
+ if (result.error) {
204
+ return { data: undefined, error: result.error };
205
+ }
206
+
207
+ const response = result.data;
208
+
209
+ // OData returns 204 No Content with fmodata.affected_rows header
210
+ // The _makeRequest should handle extracting the header value
211
+ // For now, we'll check if response contains the count
212
+ let deletedCount = 0;
213
+
214
+ if (typeof response === "number") {
215
+ deletedCount = response;
216
+ } else if (response && typeof response === "object") {
217
+ // Check if the response has a count property (fallback)
218
+ deletedCount = (response as any).deletedCount || 0;
167
219
  }
220
+
221
+ return { data: { deletedCount }, error: undefined };
168
222
  }
169
223
 
170
224
  getRequestConfig(): { method: string; url: string; body?: any } {
225
+ // For batch operations, use database-level setting (no per-request override available here)
226
+ const tableId = this.getTableId(this.databaseUseEntityIds);
227
+
171
228
  let url: string;
172
229
 
173
230
  if (this.mode === "byId") {
174
- url = `/${this.databaseName}/${this.tableName}('${this.recordId}')`;
231
+ url = `/${this.databaseName}/${tableId}('${this.recordId}')`;
175
232
  } else {
176
233
  if (!this.queryBuilder) {
177
234
  throw new Error("Query builder is required for filter-based delete");
178
235
  }
179
236
 
180
237
  const queryString = this.queryBuilder.getQueryString();
181
- const queryParams = queryString.startsWith(`/${this.tableName}`)
182
- ? queryString.slice(`/${this.tableName}`.length)
183
- : queryString;
238
+ const queryParams = queryString.startsWith(`/${tableId}`)
239
+ ? queryString.slice(`/${tableId}`.length)
240
+ : queryString.startsWith(`/${this.tableName}`)
241
+ ? queryString.slice(`/${this.tableName}`.length)
242
+ : queryString;
184
243
 
185
- url = `/${this.databaseName}/${this.tableName}${queryParams}`;
244
+ url = `/${this.databaseName}/${tableId}${queryParams}`;
186
245
  }
187
246
 
188
247
  return {
@@ -190,4 +249,46 @@ export class ExecutableDeleteBuilder<T extends Record<string, any>>
190
249
  url,
191
250
  };
192
251
  }
252
+
253
+ toRequest(baseUrl: string): Request {
254
+ const config = this.getRequestConfig();
255
+ const fullUrl = `${baseUrl}${config.url}`;
256
+
257
+ return new Request(fullUrl, {
258
+ method: config.method,
259
+ headers: {
260
+ Accept: "application/json",
261
+ },
262
+ });
263
+ }
264
+
265
+ async processResponse(
266
+ response: Response,
267
+ options?: ExecuteOptions,
268
+ ): Promise<Result<{ deletedCount: number }>> {
269
+ // Check for empty response (204 No Content)
270
+ const text = await response.text();
271
+ if (!text || text.trim() === "") {
272
+ // For 204 No Content, check the fmodata.affected_rows header
273
+ const affectedRows = response.headers.get("fmodata.affected_rows");
274
+ const deletedCount = affectedRows ? parseInt(affectedRows, 10) : 1;
275
+ return { data: { deletedCount }, error: undefined };
276
+ }
277
+
278
+ const rawResponse = JSON.parse(text);
279
+
280
+ // OData returns 204 No Content with fmodata.affected_rows header
281
+ // The _makeRequest should handle extracting the header value
282
+ // For now, we'll check if response contains the count
283
+ let deletedCount = 0;
284
+
285
+ if (typeof rawResponse === "number") {
286
+ deletedCount = rawResponse;
287
+ } else if (rawResponse && typeof rawResponse === "object") {
288
+ // Check if the response has a count property (fallback)
289
+ deletedCount = (rawResponse as any).deletedCount || 0;
290
+ }
291
+
292
+ return { data: { deletedCount }, error: undefined };
293
+ }
193
294
  }