@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.
- package/README.md +746 -65
- package/dist/esm/client/base-table.d.ts +117 -5
- package/dist/esm/client/base-table.js +43 -5
- package/dist/esm/client/base-table.js.map +1 -1
- package/dist/esm/client/batch-builder.d.ts +54 -0
- package/dist/esm/client/batch-builder.js +179 -0
- package/dist/esm/client/batch-builder.js.map +1 -0
- package/dist/esm/client/batch-request.d.ts +61 -0
- package/dist/esm/client/batch-request.js +252 -0
- package/dist/esm/client/batch-request.js.map +1 -0
- package/dist/esm/client/database.d.ts +55 -6
- package/dist/esm/client/database.js +118 -15
- package/dist/esm/client/database.js.map +1 -1
- package/dist/esm/client/delete-builder.d.ts +21 -2
- package/dist/esm/client/delete-builder.js +96 -32
- package/dist/esm/client/delete-builder.js.map +1 -1
- package/dist/esm/client/entity-set.d.ts +25 -11
- package/dist/esm/client/entity-set.js +31 -11
- package/dist/esm/client/entity-set.js.map +1 -1
- package/dist/esm/client/filemaker-odata.d.ts +23 -4
- package/dist/esm/client/filemaker-odata.js +124 -29
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/dist/esm/client/insert-builder.d.ts +38 -3
- package/dist/esm/client/insert-builder.js +231 -34
- package/dist/esm/client/insert-builder.js.map +1 -1
- package/dist/esm/client/query-builder.d.ts +27 -6
- package/dist/esm/client/query-builder.js +457 -210
- package/dist/esm/client/query-builder.js.map +1 -1
- package/dist/esm/client/record-builder.d.ts +96 -9
- package/dist/esm/client/record-builder.js +378 -39
- package/dist/esm/client/record-builder.js.map +1 -1
- package/dist/esm/client/response-processor.d.ts +38 -0
- package/dist/esm/client/schema-manager.d.ts +57 -0
- package/dist/esm/client/schema-manager.js +132 -0
- package/dist/esm/client/schema-manager.js.map +1 -0
- package/dist/esm/client/table-occurrence.d.ts +48 -1
- package/dist/esm/client/table-occurrence.js +29 -2
- package/dist/esm/client/table-occurrence.js.map +1 -1
- package/dist/esm/client/update-builder.d.ts +34 -11
- package/dist/esm/client/update-builder.js +135 -31
- package/dist/esm/client/update-builder.js.map +1 -1
- package/dist/esm/errors.d.ts +73 -0
- package/dist/esm/errors.js +148 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.d.ts +10 -3
- package/dist/esm/index.js +28 -5
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/transform.d.ts +65 -0
- package/dist/esm/transform.js +114 -0
- package/dist/esm/transform.js.map +1 -0
- package/dist/esm/types.d.ts +89 -5
- package/dist/esm/validation.d.ts +6 -3
- package/dist/esm/validation.js +104 -33
- package/dist/esm/validation.js.map +1 -1
- package/package.json +10 -1
- package/src/client/base-table.ts +158 -8
- package/src/client/batch-builder.ts +265 -0
- package/src/client/batch-request.ts +485 -0
- package/src/client/database.ts +175 -18
- package/src/client/delete-builder.ts +149 -48
- package/src/client/entity-set.ts +114 -23
- package/src/client/filemaker-odata.ts +179 -35
- package/src/client/insert-builder.ts +350 -40
- package/src/client/query-builder.ts +616 -237
- package/src/client/query-builder.ts.bak +1457 -0
- package/src/client/record-builder.ts +692 -65
- package/src/client/response-processor.ts +103 -0
- package/src/client/schema-manager.ts +246 -0
- package/src/client/table-occurrence.ts +78 -3
- package/src/client/update-builder.ts +235 -49
- package/src/errors.ts +217 -0
- package/src/index.ts +59 -2
- package/src/transform.ts +249 -0
- package/src/types.ts +201 -35
- package/src/validation.ts +120 -36
package/src/client/database.ts
CHANGED
|
@@ -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?: {
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
105
|
-
`/${this.databaseName}`,
|
|
106
|
-
)) as {
|
|
229
|
+
const result = await this.context._makeRequest<{
|
|
107
230
|
value?: Array<{ name: string }>;
|
|
108
|
-
};
|
|
109
|
-
if (
|
|
110
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
194
|
+
url = `/${this.databaseName}/${tableId}${queryParams}`;
|
|
195
|
+
}
|
|
142
196
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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}/${
|
|
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(`/${
|
|
182
|
-
? queryString.slice(`/${
|
|
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}/${
|
|
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
|
}
|