@proofkit/fmodata 0.1.0-alpha.0 → 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 +1624 -18
- 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/entity-set.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { z } from "zod/v4";
|
|
2
1
|
import type {
|
|
3
2
|
ExecutionContext,
|
|
4
3
|
InferSchemaType,
|
|
@@ -6,6 +5,7 @@ import type {
|
|
|
6
5
|
InsertData,
|
|
7
6
|
UpdateData,
|
|
8
7
|
} from "../types";
|
|
8
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
9
9
|
import type { BaseTable } from "./base-table";
|
|
10
10
|
import type { TableOccurrence } from "./table-occurrence";
|
|
11
11
|
import { QueryBuilder } from "./query-builder";
|
|
@@ -13,6 +13,7 @@ import { RecordBuilder } from "./record-builder";
|
|
|
13
13
|
import { InsertBuilder } from "./insert-builder";
|
|
14
14
|
import { DeleteBuilder } from "./delete-builder";
|
|
15
15
|
import { UpdateBuilder } from "./update-builder";
|
|
16
|
+
import { Database } from "./database";
|
|
16
17
|
|
|
17
18
|
// Helper type to extract navigation relation names from an occurrence
|
|
18
19
|
type ExtractNavigationNames<
|
|
@@ -27,7 +28,7 @@ type ExtractNavigationNames<
|
|
|
27
28
|
// Helper type to extract schema from a TableOccurrence
|
|
28
29
|
type ExtractSchemaFromOccurrence<O> =
|
|
29
30
|
O extends TableOccurrence<infer BT, any, any, any>
|
|
30
|
-
? BT extends BaseTable<infer S, any>
|
|
31
|
+
? BT extends BaseTable<infer S, any, any, any>
|
|
31
32
|
? S
|
|
32
33
|
: never
|
|
33
34
|
: never;
|
|
@@ -35,7 +36,7 @@ type ExtractSchemaFromOccurrence<O> =
|
|
|
35
36
|
// Helper type to extract defaultSelect from a TableOccurrence
|
|
36
37
|
type ExtractDefaultSelect<O> =
|
|
37
38
|
O extends TableOccurrence<infer BT, any, any, infer DefSelect>
|
|
38
|
-
? BT extends BaseTable<infer S, any>
|
|
39
|
+
? BT extends BaseTable<infer S, any, any, any>
|
|
39
40
|
? DefSelect extends "all"
|
|
40
41
|
? keyof S
|
|
41
42
|
: DefSelect extends "schema"
|
|
@@ -59,19 +60,19 @@ type FindNavigationTarget<
|
|
|
59
60
|
? Name extends keyof Nav
|
|
60
61
|
? ResolveNavigationItem<Nav[Name]>
|
|
61
62
|
: TableOccurrence<
|
|
62
|
-
BaseTable<Record<string,
|
|
63
|
+
BaseTable<Record<string, StandardSchemaV1>, any, any, any>,
|
|
63
64
|
any,
|
|
64
65
|
any,
|
|
65
66
|
any
|
|
66
67
|
>
|
|
67
68
|
: TableOccurrence<
|
|
68
|
-
BaseTable<Record<string,
|
|
69
|
+
BaseTable<Record<string, StandardSchemaV1>, any, any, any>,
|
|
69
70
|
any,
|
|
70
71
|
any,
|
|
71
72
|
any
|
|
72
73
|
>
|
|
73
74
|
: TableOccurrence<
|
|
74
|
-
BaseTable<Record<string,
|
|
75
|
+
BaseTable<Record<string, StandardSchemaV1>, any, any, any>,
|
|
75
76
|
any,
|
|
76
77
|
any,
|
|
77
78
|
any
|
|
@@ -84,21 +85,22 @@ type GetTargetSchemaType<
|
|
|
84
85
|
> = [FindNavigationTarget<O, Rel>] extends [
|
|
85
86
|
TableOccurrence<infer BT, any, any, any>,
|
|
86
87
|
]
|
|
87
|
-
? [BT] extends [BaseTable<infer S, any>]
|
|
88
|
-
? [S] extends [Record<string,
|
|
88
|
+
? [BT] extends [BaseTable<infer S, any, any, any>]
|
|
89
|
+
? [S] extends [Record<string, StandardSchemaV1>]
|
|
89
90
|
? InferSchemaType<S>
|
|
90
91
|
: Record<string, any>
|
|
91
92
|
: Record<string, any>
|
|
92
93
|
: Record<string, any>;
|
|
93
94
|
|
|
94
95
|
export class EntitySet<
|
|
95
|
-
Schema extends Record<string,
|
|
96
|
+
Schema extends Record<string, StandardSchemaV1> = any,
|
|
96
97
|
Occ extends TableOccurrence<any, any, any, any> | undefined = undefined,
|
|
97
98
|
> {
|
|
98
99
|
private occurrence?: Occ;
|
|
99
100
|
private tableName: string;
|
|
100
101
|
private databaseName: string;
|
|
101
102
|
private context: ExecutionContext;
|
|
103
|
+
private database: Database<any>; // Database instance for accessing occurrences
|
|
102
104
|
private isNavigateFromEntitySet?: boolean;
|
|
103
105
|
private navigateRelation?: string;
|
|
104
106
|
private navigateSourceTableName?: string;
|
|
@@ -108,30 +110,39 @@ export class EntitySet<
|
|
|
108
110
|
tableName: string;
|
|
109
111
|
databaseName: string;
|
|
110
112
|
context: ExecutionContext;
|
|
113
|
+
database?: any;
|
|
111
114
|
}) {
|
|
112
115
|
this.occurrence = config.occurrence;
|
|
113
116
|
this.tableName = config.tableName;
|
|
114
117
|
this.databaseName = config.databaseName;
|
|
115
118
|
this.context = config.context;
|
|
119
|
+
this.database = config.database;
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
// Type-only method to help TypeScript infer the schema from occurrence
|
|
119
123
|
static create<
|
|
120
|
-
OccurrenceSchema extends Record<string,
|
|
124
|
+
OccurrenceSchema extends Record<string, StandardSchemaV1>,
|
|
121
125
|
Occ extends
|
|
122
|
-
| TableOccurrence<
|
|
126
|
+
| TableOccurrence<
|
|
127
|
+
BaseTable<OccurrenceSchema, any, any, any>,
|
|
128
|
+
any,
|
|
129
|
+
any,
|
|
130
|
+
any
|
|
131
|
+
>
|
|
123
132
|
| undefined = undefined,
|
|
124
133
|
>(config: {
|
|
125
134
|
occurrence?: Occ;
|
|
126
135
|
tableName: string;
|
|
127
136
|
databaseName: string;
|
|
128
137
|
context: ExecutionContext;
|
|
138
|
+
database: Database<any>;
|
|
129
139
|
}): EntitySet<OccurrenceSchema, Occ> {
|
|
130
140
|
return new EntitySet<OccurrenceSchema, Occ>({
|
|
131
141
|
occurrence: config.occurrence,
|
|
132
142
|
tableName: config.tableName,
|
|
133
143
|
databaseName: config.databaseName,
|
|
134
144
|
context: config.context,
|
|
145
|
+
database: config.database,
|
|
135
146
|
});
|
|
136
147
|
}
|
|
137
148
|
|
|
@@ -157,6 +168,7 @@ export class EntitySet<
|
|
|
157
168
|
tableName: this.tableName,
|
|
158
169
|
databaseName: this.databaseName,
|
|
159
170
|
context: this.context,
|
|
171
|
+
databaseUseEntityIds: this.database?.isUsingEntityIds() ?? false,
|
|
160
172
|
});
|
|
161
173
|
|
|
162
174
|
// Apply defaultSelect if occurrence exists and select hasn't been called
|
|
@@ -169,13 +181,13 @@ export class EntitySet<
|
|
|
169
181
|
const fields = Object.keys(schema) as (keyof InferSchemaType<Schema>)[];
|
|
170
182
|
// Deduplicate fields (same as select method)
|
|
171
183
|
const uniqueFields = [...new Set(fields)];
|
|
172
|
-
return builder.select(...uniqueFields);
|
|
184
|
+
return builder.select(...uniqueFields).top(1000);
|
|
173
185
|
} else if (Array.isArray(defaultSelect)) {
|
|
174
186
|
// Use the provided field names, deduplicated
|
|
175
187
|
const uniqueFields = [
|
|
176
188
|
...new Set(defaultSelect),
|
|
177
189
|
] as (keyof InferSchemaType<Schema>)[];
|
|
178
|
-
return builder.select(...uniqueFields);
|
|
190
|
+
return builder.select(...uniqueFields).top(1000);
|
|
179
191
|
}
|
|
180
192
|
// If defaultSelect is "all", no changes needed (current behavior)
|
|
181
193
|
}
|
|
@@ -187,7 +199,10 @@ export class EntitySet<
|
|
|
187
199
|
(builder as any).navigateSourceTableName = this.navigateSourceTableName;
|
|
188
200
|
// navigateRecordId is intentionally not set (undefined) to indicate navigation from EntitySet
|
|
189
201
|
}
|
|
190
|
-
|
|
202
|
+
|
|
203
|
+
// Apply default pagination limit of 1000 records to prevent stack overflow
|
|
204
|
+
// with large datasets. Users can override with .top() if needed.
|
|
205
|
+
return builder.top(1000);
|
|
191
206
|
}
|
|
192
207
|
|
|
193
208
|
get(
|
|
@@ -196,19 +211,24 @@ export class EntitySet<
|
|
|
196
211
|
InferSchemaType<Schema>,
|
|
197
212
|
false,
|
|
198
213
|
keyof InferSchemaType<Schema>,
|
|
199
|
-
Occ
|
|
214
|
+
Occ,
|
|
215
|
+
keyof InferSchemaType<Schema>,
|
|
216
|
+
{}
|
|
200
217
|
> {
|
|
201
218
|
const builder = new RecordBuilder<
|
|
202
219
|
InferSchemaType<Schema>,
|
|
203
220
|
false,
|
|
204
221
|
keyof InferSchemaType<Schema>,
|
|
205
|
-
Occ
|
|
222
|
+
Occ,
|
|
223
|
+
keyof InferSchemaType<Schema>,
|
|
224
|
+
{}
|
|
206
225
|
>({
|
|
207
226
|
occurrence: this.occurrence,
|
|
208
227
|
tableName: this.tableName,
|
|
209
228
|
databaseName: this.databaseName,
|
|
210
229
|
context: this.context,
|
|
211
230
|
recordId: id,
|
|
231
|
+
databaseUseEntityIds: this.database?.isUsingEntityIds() ?? false,
|
|
212
232
|
});
|
|
213
233
|
// Propagate navigation context if present
|
|
214
234
|
if (this.isNavigateFromEntitySet) {
|
|
@@ -219,49 +239,119 @@ export class EntitySet<
|
|
|
219
239
|
return builder;
|
|
220
240
|
}
|
|
221
241
|
|
|
242
|
+
// Overload: when returnFullRecord is explicitly false
|
|
243
|
+
insert(
|
|
244
|
+
data: Occ extends TableOccurrence<infer BT, any, any, any>
|
|
245
|
+
? BT extends BaseTable<any, any, any, any>
|
|
246
|
+
? InsertData<BT>
|
|
247
|
+
: Partial<InferSchemaType<Schema>>
|
|
248
|
+
: Partial<InferSchemaType<Schema>>,
|
|
249
|
+
options: { returnFullRecord: false },
|
|
250
|
+
): InsertBuilder<InferSchemaType<Schema>, Occ, "minimal">;
|
|
251
|
+
|
|
252
|
+
// Overload: when returnFullRecord is true or omitted (default)
|
|
253
|
+
insert(
|
|
254
|
+
data: Occ extends TableOccurrence<infer BT, any, any, any>
|
|
255
|
+
? BT extends BaseTable<any, any, any, any>
|
|
256
|
+
? InsertData<BT>
|
|
257
|
+
: Partial<InferSchemaType<Schema>>
|
|
258
|
+
: Partial<InferSchemaType<Schema>>,
|
|
259
|
+
options?: { returnFullRecord?: true },
|
|
260
|
+
): InsertBuilder<InferSchemaType<Schema>, Occ, "representation">;
|
|
261
|
+
|
|
262
|
+
// Implementation
|
|
222
263
|
insert(
|
|
223
264
|
data: Occ extends TableOccurrence<infer BT, any, any, any>
|
|
224
265
|
? BT extends BaseTable<any, any, any, any>
|
|
225
266
|
? InsertData<BT>
|
|
226
267
|
: Partial<InferSchemaType<Schema>>
|
|
227
268
|
: Partial<InferSchemaType<Schema>>,
|
|
228
|
-
|
|
229
|
-
|
|
269
|
+
options?: { returnFullRecord?: boolean },
|
|
270
|
+
): InsertBuilder<InferSchemaType<Schema>, Occ, "minimal" | "representation"> {
|
|
271
|
+
const returnPref =
|
|
272
|
+
options?.returnFullRecord === false ? "minimal" : "representation";
|
|
273
|
+
return new InsertBuilder<InferSchemaType<Schema>, Occ, typeof returnPref>({
|
|
230
274
|
occurrence: this.occurrence,
|
|
231
275
|
tableName: this.tableName,
|
|
232
276
|
databaseName: this.databaseName,
|
|
233
277
|
context: this.context,
|
|
234
278
|
data: data as Partial<InferSchemaType<Schema>>,
|
|
279
|
+
returnPreference: returnPref as any,
|
|
280
|
+
databaseUseEntityIds: this.database?.isUsingEntityIds() ?? false,
|
|
235
281
|
});
|
|
236
282
|
}
|
|
237
283
|
|
|
284
|
+
// Overload: when returnFullRecord is explicitly true
|
|
238
285
|
update(
|
|
239
286
|
data: Occ extends TableOccurrence<infer BT, any, any, any>
|
|
240
287
|
? BT extends BaseTable<any, any, any, any>
|
|
241
288
|
? UpdateData<BT>
|
|
242
289
|
: Partial<InferSchemaType<Schema>>
|
|
243
290
|
: Partial<InferSchemaType<Schema>>,
|
|
291
|
+
options: { returnFullRecord: true },
|
|
244
292
|
): UpdateBuilder<
|
|
245
293
|
InferSchemaType<Schema>,
|
|
246
294
|
Occ extends TableOccurrence<infer BT, any, any, any>
|
|
247
295
|
? BT extends BaseTable<any, any, any, any>
|
|
248
296
|
? BT
|
|
249
297
|
: BaseTable<Schema, any, any, any>
|
|
250
|
-
: BaseTable<Schema, any, any, any
|
|
298
|
+
: BaseTable<Schema, any, any, any>,
|
|
299
|
+
"representation"
|
|
300
|
+
>;
|
|
301
|
+
|
|
302
|
+
// Overload: when returnFullRecord is false or omitted (default returns count)
|
|
303
|
+
update(
|
|
304
|
+
data: Occ extends TableOccurrence<infer BT, any, any, any>
|
|
305
|
+
? BT extends BaseTable<any, any, any, any>
|
|
306
|
+
? UpdateData<BT>
|
|
307
|
+
: Partial<InferSchemaType<Schema>>
|
|
308
|
+
: Partial<InferSchemaType<Schema>>,
|
|
309
|
+
options?: { returnFullRecord?: false },
|
|
310
|
+
): UpdateBuilder<
|
|
311
|
+
InferSchemaType<Schema>,
|
|
312
|
+
Occ extends TableOccurrence<infer BT, any, any, any>
|
|
313
|
+
? BT extends BaseTable<any, any, any, any>
|
|
314
|
+
? BT
|
|
315
|
+
: BaseTable<Schema, any, any, any>
|
|
316
|
+
: BaseTable<Schema, any, any, any>,
|
|
317
|
+
"minimal"
|
|
318
|
+
>;
|
|
319
|
+
|
|
320
|
+
// Implementation
|
|
321
|
+
update(
|
|
322
|
+
data: Occ extends TableOccurrence<infer BT, any, any, any>
|
|
323
|
+
? BT extends BaseTable<any, any, any, any>
|
|
324
|
+
? UpdateData<BT>
|
|
325
|
+
: Partial<InferSchemaType<Schema>>
|
|
326
|
+
: Partial<InferSchemaType<Schema>>,
|
|
327
|
+
options?: { returnFullRecord?: boolean },
|
|
328
|
+
): UpdateBuilder<
|
|
329
|
+
InferSchemaType<Schema>,
|
|
330
|
+
Occ extends TableOccurrence<infer BT, any, any, any>
|
|
331
|
+
? BT extends BaseTable<any, any, any, any>
|
|
332
|
+
? BT
|
|
333
|
+
: BaseTable<Schema, any, any, any>
|
|
334
|
+
: BaseTable<Schema, any, any, any>,
|
|
335
|
+
"minimal" | "representation"
|
|
251
336
|
> {
|
|
337
|
+
const returnPref =
|
|
338
|
+
options?.returnFullRecord === true ? "representation" : "minimal";
|
|
252
339
|
return new UpdateBuilder<
|
|
253
340
|
InferSchemaType<Schema>,
|
|
254
341
|
Occ extends TableOccurrence<infer BT, any, any, any>
|
|
255
342
|
? BT extends BaseTable<any, any, any, any>
|
|
256
343
|
? BT
|
|
257
344
|
: BaseTable<Schema, any, any, any>
|
|
258
|
-
: BaseTable<Schema, any, any, any
|
|
345
|
+
: BaseTable<Schema, any, any, any>,
|
|
346
|
+
typeof returnPref
|
|
259
347
|
>({
|
|
260
348
|
occurrence: this.occurrence,
|
|
261
349
|
tableName: this.tableName,
|
|
262
350
|
databaseName: this.databaseName,
|
|
263
351
|
context: this.context,
|
|
264
352
|
data: data as Partial<InferSchemaType<Schema>>,
|
|
353
|
+
returnPreference: returnPref as any,
|
|
354
|
+
databaseUseEntityIds: this.database?.isUsingEntityIds() ?? false,
|
|
265
355
|
});
|
|
266
356
|
}
|
|
267
357
|
|
|
@@ -271,6 +361,7 @@ export class EntitySet<
|
|
|
271
361
|
tableName: this.tableName,
|
|
272
362
|
databaseName: this.databaseName,
|
|
273
363
|
context: this.context,
|
|
364
|
+
databaseUseEntityIds: this.database?.isUsingEntityIds() ?? false,
|
|
274
365
|
});
|
|
275
366
|
}
|
|
276
367
|
|
|
@@ -280,15 +371,15 @@ export class EntitySet<
|
|
|
280
371
|
): EntitySet<
|
|
281
372
|
ExtractSchemaFromOccurrence<
|
|
282
373
|
FindNavigationTarget<Occ, RelationName>
|
|
283
|
-
> extends Record<string,
|
|
374
|
+
> extends Record<string, StandardSchemaV1>
|
|
284
375
|
? ExtractSchemaFromOccurrence<FindNavigationTarget<Occ, RelationName>>
|
|
285
|
-
: Record<string,
|
|
376
|
+
: Record<string, StandardSchemaV1>,
|
|
286
377
|
FindNavigationTarget<Occ, RelationName>
|
|
287
378
|
>;
|
|
288
379
|
// Overload for arbitrary strings - returns generic EntitySet
|
|
289
380
|
navigate(
|
|
290
381
|
relationName: string,
|
|
291
|
-
): EntitySet<Record<string,
|
|
382
|
+
): EntitySet<Record<string, StandardSchemaV1>, undefined>;
|
|
292
383
|
// Implementation
|
|
293
384
|
navigate(relationName: string): EntitySet<any, any> {
|
|
294
385
|
// Use the target occurrence if available, otherwise allow untyped navigation
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import createClient, {
|
|
2
|
-
|
|
1
|
+
import createClient, {
|
|
2
|
+
FFetchOptions,
|
|
3
|
+
TimeoutError,
|
|
4
|
+
AbortError,
|
|
5
|
+
NetworkError,
|
|
6
|
+
RetryLimitError,
|
|
7
|
+
CircuitOpenError,
|
|
8
|
+
} from "@fetchkit/ffetch";
|
|
9
|
+
import type { Auth, ExecutionContext, Result } from "../types";
|
|
10
|
+
import { HTTPError, ODataError, SchemaLockedError } from "../errors";
|
|
3
11
|
import { Database } from "./database";
|
|
4
12
|
import { TableOccurrence } from "./table-occurrence";
|
|
5
13
|
|
|
6
|
-
export class
|
|
14
|
+
export class FMServerConnection implements ExecutionContext {
|
|
7
15
|
private fetchClient: ReturnType<typeof createClient>;
|
|
8
16
|
private serverUrl: string;
|
|
9
17
|
private auth: Auth;
|
|
18
|
+
private useEntityIds: boolean = false;
|
|
10
19
|
constructor(config: {
|
|
11
20
|
serverUrl: string;
|
|
12
21
|
auth: Auth;
|
|
@@ -27,14 +36,42 @@ export class FileMakerOData implements ExecutionContext {
|
|
|
27
36
|
this.auth = config.auth;
|
|
28
37
|
}
|
|
29
38
|
|
|
39
|
+
/**
|
|
40
|
+
* @internal
|
|
41
|
+
* Sets whether to use FileMaker entity IDs (FMFID/FMTID) in requests
|
|
42
|
+
*/
|
|
43
|
+
_setUseEntityIds(useEntityIds: boolean): void {
|
|
44
|
+
this.useEntityIds = useEntityIds;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @internal
|
|
49
|
+
* Gets whether to use FileMaker entity IDs (FMFID/FMTID) in requests
|
|
50
|
+
*/
|
|
51
|
+
_getUseEntityIds(): boolean {
|
|
52
|
+
return this.useEntityIds;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @internal
|
|
57
|
+
* Gets the base URL for OData requests
|
|
58
|
+
*/
|
|
59
|
+
_getBaseUrl(): string {
|
|
60
|
+
return `${this.serverUrl}${"apiKey" in this.auth ? `/otto` : ""}/fmi/odata/v4`;
|
|
61
|
+
}
|
|
62
|
+
|
|
30
63
|
/**
|
|
31
64
|
* @internal
|
|
32
65
|
*/
|
|
33
66
|
async _makeRequest<T>(
|
|
34
67
|
url: string,
|
|
35
|
-
options?: RequestInit & FFetchOptions,
|
|
36
|
-
): Promise<T
|
|
68
|
+
options?: RequestInit & FFetchOptions & { useEntityIds?: boolean },
|
|
69
|
+
): Promise<Result<T>> {
|
|
37
70
|
const baseUrl = `${this.serverUrl}${"apiKey" in this.auth ? `/otto` : ""}/fmi/odata/v4`;
|
|
71
|
+
const fullUrl = baseUrl + url;
|
|
72
|
+
|
|
73
|
+
// Use per-request override if provided, otherwise use the database-level setting
|
|
74
|
+
const useEntityIds = options?.useEntityIds ?? this.useEntityIds;
|
|
38
75
|
|
|
39
76
|
const headers = {
|
|
40
77
|
Authorization:
|
|
@@ -43,6 +80,7 @@ export class FileMakerOData implements ExecutionContext {
|
|
|
43
80
|
: `Basic ${btoa(`${this.auth.username}:${this.auth.password}`)}`,
|
|
44
81
|
"Content-Type": "application/json",
|
|
45
82
|
Accept: "application/json",
|
|
83
|
+
...(useEntityIds ? { Prefer: "fmodata.entity-ids" } : {}),
|
|
46
84
|
...(options?.headers || {}),
|
|
47
85
|
};
|
|
48
86
|
|
|
@@ -61,44 +99,147 @@ export class FileMakerOData implements ExecutionContext {
|
|
|
61
99
|
? createClient({ retries: 0, fetchHandler })
|
|
62
100
|
: this.fetchClient;
|
|
63
101
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
102
|
+
try {
|
|
103
|
+
const finalOptions = {
|
|
104
|
+
...restOptions,
|
|
105
|
+
headers,
|
|
106
|
+
};
|
|
68
107
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
108
|
+
// For batch requests, use native fetch to avoid any potential serialization issues with ffetch
|
|
109
|
+
const resp = url.includes("/$batch")
|
|
110
|
+
? await fetch(fullUrl, {
|
|
111
|
+
method: finalOptions.method,
|
|
112
|
+
headers: finalOptions.headers,
|
|
113
|
+
body: finalOptions.body,
|
|
114
|
+
})
|
|
115
|
+
: await clientToUse(fullUrl, finalOptions);
|
|
74
116
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
117
|
+
// Handle HTTP errors
|
|
118
|
+
if (!resp.ok) {
|
|
119
|
+
// Try to parse error body if it's JSON
|
|
120
|
+
let errorBody;
|
|
121
|
+
try {
|
|
122
|
+
if (resp.headers.get("content-type")?.includes("application/json")) {
|
|
123
|
+
errorBody = await resp.json();
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
// Ignore JSON parse errors
|
|
127
|
+
}
|
|
81
128
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
129
|
+
// Check if it's an OData error response
|
|
130
|
+
if (errorBody?.error) {
|
|
131
|
+
const errorCode = errorBody.error.code;
|
|
132
|
+
const errorMessage = errorBody.error.message || resp.statusText;
|
|
133
|
+
|
|
134
|
+
// Check for schema locked error (code 303)
|
|
135
|
+
if (errorCode === "303" || errorCode === 303) {
|
|
136
|
+
return {
|
|
137
|
+
data: undefined,
|
|
138
|
+
error: new SchemaLockedError(
|
|
139
|
+
fullUrl,
|
|
140
|
+
errorMessage,
|
|
141
|
+
errorBody.error,
|
|
142
|
+
),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
data: undefined,
|
|
148
|
+
error: new ODataError(
|
|
149
|
+
fullUrl,
|
|
150
|
+
errorMessage,
|
|
151
|
+
errorCode,
|
|
152
|
+
errorBody.error,
|
|
153
|
+
),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
data: undefined,
|
|
159
|
+
error: new HTTPError(
|
|
160
|
+
fullUrl,
|
|
161
|
+
resp.status,
|
|
162
|
+
resp.statusText,
|
|
163
|
+
errorBody,
|
|
164
|
+
),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check for affected rows header (for DELETE and bulk PATCH operations)
|
|
169
|
+
// FileMaker may return this with 204 No Content or 200 OK
|
|
170
|
+
const affectedRows = resp.headers.get("fmodata.affected_rows");
|
|
171
|
+
if (affectedRows !== null) {
|
|
172
|
+
return { data: parseInt(affectedRows, 10) as T, error: undefined };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Handle 204 No Content with no body
|
|
176
|
+
if (resp.status === 204) {
|
|
177
|
+
// Check for Location header (used for insert with return=minimal)
|
|
178
|
+
// Use optional chaining for safety with mocks that might not have proper headers
|
|
179
|
+
const locationHeader =
|
|
180
|
+
resp.headers?.get?.("Location") || resp.headers?.get?.("location");
|
|
181
|
+
if (locationHeader) {
|
|
182
|
+
// Return the location header so InsertBuilder can extract ROWID
|
|
183
|
+
return { data: { _location: locationHeader } as T, error: undefined };
|
|
184
|
+
}
|
|
185
|
+
return { data: 0 as T, error: undefined };
|
|
186
|
+
}
|
|
86
187
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
188
|
+
// Parse response
|
|
189
|
+
if (resp.headers.get("content-type")?.includes("application/json")) {
|
|
190
|
+
const data = await resp.json();
|
|
191
|
+
|
|
192
|
+
// Check for embedded OData errors
|
|
193
|
+
if (data.error) {
|
|
194
|
+
const errorCode = data.error.code;
|
|
195
|
+
const errorMessage = data.error.message || "Unknown OData error";
|
|
196
|
+
|
|
197
|
+
// Check for schema locked error (code 303)
|
|
198
|
+
if (errorCode === "303" || errorCode === 303) {
|
|
199
|
+
return {
|
|
200
|
+
data: undefined,
|
|
201
|
+
error: new SchemaLockedError(fullUrl, errorMessage, data.error),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
data: undefined,
|
|
207
|
+
error: new ODataError(fullUrl, errorMessage, errorCode, data.error),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { data: data as T, error: undefined };
|
|
91
212
|
}
|
|
92
|
-
|
|
213
|
+
|
|
214
|
+
return { data: (await resp.text()) as T, error: undefined };
|
|
215
|
+
} catch (err) {
|
|
216
|
+
// Map ffetch errors - return them directly (no re-wrapping)
|
|
217
|
+
if (
|
|
218
|
+
err instanceof TimeoutError ||
|
|
219
|
+
err instanceof AbortError ||
|
|
220
|
+
err instanceof NetworkError ||
|
|
221
|
+
err instanceof RetryLimitError ||
|
|
222
|
+
err instanceof CircuitOpenError
|
|
223
|
+
) {
|
|
224
|
+
return { data: undefined, error: err };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Unknown error - wrap it as NetworkError
|
|
228
|
+
return {
|
|
229
|
+
data: undefined,
|
|
230
|
+
error: new NetworkError(fullUrl, err),
|
|
231
|
+
};
|
|
93
232
|
}
|
|
94
|
-
return (await resp.text()) as T;
|
|
95
233
|
}
|
|
96
234
|
|
|
97
235
|
database<
|
|
98
236
|
const Occurrences extends readonly TableOccurrence<any, any, any, any>[],
|
|
99
237
|
>(
|
|
100
238
|
name: string,
|
|
101
|
-
config?: {
|
|
239
|
+
config?: {
|
|
240
|
+
occurrences?: Occurrences | undefined;
|
|
241
|
+
useEntityIds?: boolean;
|
|
242
|
+
},
|
|
102
243
|
): Database<Occurrences> {
|
|
103
244
|
return new Database(name, this, config);
|
|
104
245
|
}
|
|
@@ -108,11 +249,14 @@ export class FileMakerOData implements ExecutionContext {
|
|
|
108
249
|
* @returns Promise resolving to an array of database names
|
|
109
250
|
*/
|
|
110
251
|
async listDatabaseNames(): Promise<string[]> {
|
|
111
|
-
const
|
|
252
|
+
const result = await this._makeRequest<{
|
|
112
253
|
value?: Array<{ name: string }>;
|
|
113
|
-
};
|
|
114
|
-
if (
|
|
115
|
-
|
|
254
|
+
}>("/");
|
|
255
|
+
if (result.error) {
|
|
256
|
+
throw result.error;
|
|
257
|
+
}
|
|
258
|
+
if (result.data.value && Array.isArray(result.data.value)) {
|
|
259
|
+
return result.data.value.map((item) => item.name);
|
|
116
260
|
}
|
|
117
261
|
return [];
|
|
118
262
|
}
|