@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.
Files changed (75) hide show
  1. package/README.md +1624 -18
  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,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, z.ZodTypeAny>, any>,
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, z.ZodTypeAny>, any>,
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, z.ZodTypeAny>, any>,
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, z.ZodType>]
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, z.ZodType> = any,
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, z.ZodType>,
124
+ OccurrenceSchema extends Record<string, StandardSchemaV1>,
121
125
  Occ extends
122
- | TableOccurrence<BaseTable<OccurrenceSchema, any>, any, any, any>
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
- return builder;
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
- ): InsertBuilder<InferSchemaType<Schema>, Occ> {
229
- return new InsertBuilder<InferSchemaType<Schema>, Occ>({
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, z.ZodType>
374
+ > extends Record<string, StandardSchemaV1>
284
375
  ? ExtractSchemaFromOccurrence<FindNavigationTarget<Occ, RelationName>>
285
- : Record<string, z.ZodTypeAny>,
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, z.ZodTypeAny>, undefined>;
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, { FFetchOptions } from "@fetchkit/ffetch";
2
- import type { Auth, ExecutionContext } from "../types";
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 FileMakerOData implements ExecutionContext {
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
- const resp = await clientToUse(baseUrl + url, {
65
- ...restOptions,
66
- headers,
67
- });
102
+ try {
103
+ const finalOptions = {
104
+ ...restOptions,
105
+ headers,
106
+ };
68
107
 
69
- if (!resp.ok) {
70
- throw new Error(
71
- `Failed to make request to ${baseUrl + url}: ${resp.statusText}`,
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
- // Check for affected rows header (for DELETE and bulk PATCH operations)
76
- // FileMaker may return this with 204 No Content or 200 OK
77
- const affectedRows = resp.headers.get("fmodata.affected_rows");
78
- if (affectedRows !== null) {
79
- return parseInt(affectedRows, 10) as T;
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
- // Handle 204 No Content with no body
83
- if (resp.status === 204) {
84
- return 0 as T;
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
- if (resp.headers.get("content-type")?.includes("application/json")) {
88
- let data = await resp.json();
89
- if (data.error) {
90
- throw new Error(data.error);
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
- return data as T;
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?: { occurrences?: Occurrences },
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 response = (await this._makeRequest("/")) as {
252
+ const result = await this._makeRequest<{
112
253
  value?: Array<{ name: string }>;
113
- };
114
- if (response.value && Array.isArray(response.value)) {
115
- return response.value.map((item) => item.name);
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
  }