@proofkit/fmodata 0.1.0-alpha.1 → 0.1.0-alpha.11
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 +760 -69
- package/dist/esm/client/base-table.d.ts +120 -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/build-occurrences.d.ts +74 -0
- package/dist/esm/client/build-occurrences.js +31 -0
- package/dist/esm/client/build-occurrences.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 +26 -12
- package/dist/esm/client/entity-set.js +43 -12
- 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 +28 -7
- package/dist/esm/client/query-builder.js +470 -212
- package/dist/esm/client/query-builder.js.map +1 -1
- package/dist/esm/client/record-builder.d.ts +96 -10
- 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 +69 -8
- package/dist/esm/client/table-occurrence.js +35 -24
- 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 +13 -3
- package/dist/esm/index.js +29 -7
- 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 +161 -8
- package/src/client/batch-builder.ts +265 -0
- package/src/client/batch-request.ts +485 -0
- package/src/client/build-occurrences.ts +155 -0
- package/src/client/database.ts +175 -18
- package/src/client/delete-builder.ts +149 -48
- package/src/client/entity-set.ts +134 -28
- package/src/client/filemaker-odata.ts +179 -35
- package/src/client/insert-builder.ts +350 -40
- package/src/client/query-builder.ts +632 -244
- package/src/client/query-builder.ts.bak +1457 -0
- package/src/client/record-builder.ts +692 -68
- package/src/client/response-processor.ts +103 -0
- package/src/client/schema-manager.ts +246 -0
- package/src/client/table-occurrence.ts +107 -51
- package/src/client/update-builder.ts +235 -49
- package/src/errors.ts +217 -0
- package/src/index.ts +63 -6
- package/src/transform.ts +249 -0
- package/src/types.ts +201 -35
- package/src/validation.ts +120 -36
|
@@ -3,11 +3,17 @@ 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 type { BaseTable } from "./base-table";
|
|
9
10
|
import { QueryBuilder } from "./query-builder";
|
|
10
11
|
import { type FFetchOptions } from "@fetchkit/ffetch";
|
|
12
|
+
import {
|
|
13
|
+
transformFieldNamesToIds,
|
|
14
|
+
transformTableName,
|
|
15
|
+
getTableIdentifiers,
|
|
16
|
+
} from "../transform";
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Initial update builder returned from EntitySet.update(data)
|
|
@@ -16,12 +22,16 @@ import { type FFetchOptions } from "@fetchkit/ffetch";
|
|
|
16
22
|
export class UpdateBuilder<
|
|
17
23
|
T extends Record<string, any>,
|
|
18
24
|
BT extends BaseTable<any, any, any, any>,
|
|
25
|
+
ReturnPreference extends "minimal" | "representation" = "minimal",
|
|
19
26
|
> {
|
|
20
27
|
private tableName: string;
|
|
21
28
|
private databaseName: string;
|
|
22
29
|
private context: ExecutionContext;
|
|
23
30
|
private occurrence?: TableOccurrence<any, any, any, any>;
|
|
24
31
|
private data: Partial<T>;
|
|
32
|
+
private returnPreference: ReturnPreference;
|
|
33
|
+
|
|
34
|
+
private databaseUseEntityIds: boolean;
|
|
25
35
|
|
|
26
36
|
constructor(config: {
|
|
27
37
|
occurrence?: TableOccurrence<any, any, any, any>;
|
|
@@ -29,20 +39,26 @@ export class UpdateBuilder<
|
|
|
29
39
|
databaseName: string;
|
|
30
40
|
context: ExecutionContext;
|
|
31
41
|
data: Partial<T>;
|
|
42
|
+
returnPreference: ReturnPreference;
|
|
43
|
+
databaseUseEntityIds?: boolean;
|
|
32
44
|
}) {
|
|
33
45
|
this.occurrence = config.occurrence;
|
|
34
46
|
this.tableName = config.tableName;
|
|
35
47
|
this.databaseName = config.databaseName;
|
|
36
48
|
this.context = config.context;
|
|
37
49
|
this.data = config.data;
|
|
50
|
+
this.returnPreference = config.returnPreference;
|
|
51
|
+
this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
/**
|
|
41
55
|
* Update a single record by ID
|
|
42
|
-
* Returns
|
|
56
|
+
* Returns updated count by default, or full record if returnFullRecord was set to true
|
|
43
57
|
*/
|
|
44
|
-
byId(
|
|
45
|
-
|
|
58
|
+
byId(
|
|
59
|
+
id: string | number,
|
|
60
|
+
): ExecutableUpdateBuilder<T, true, ReturnPreference> {
|
|
61
|
+
return new ExecutableUpdateBuilder<T, true, ReturnPreference>({
|
|
46
62
|
occurrence: this.occurrence,
|
|
47
63
|
tableName: this.tableName,
|
|
48
64
|
databaseName: this.databaseName,
|
|
@@ -50,19 +66,21 @@ export class UpdateBuilder<
|
|
|
50
66
|
data: this.data,
|
|
51
67
|
mode: "byId",
|
|
52
68
|
recordId: id,
|
|
69
|
+
returnPreference: this.returnPreference,
|
|
70
|
+
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
53
71
|
});
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
/**
|
|
57
75
|
* Update records matching a filter query
|
|
58
|
-
* Returns
|
|
76
|
+
* Returns updated count by default, or full record if returnFullRecord was set to true
|
|
59
77
|
* @param fn Callback that receives a QueryBuilder for building the filter
|
|
60
78
|
*/
|
|
61
79
|
where(
|
|
62
80
|
fn: (
|
|
63
81
|
q: QueryBuilder<WithSystemFields<T>>,
|
|
64
82
|
) => QueryBuilder<WithSystemFields<T>>,
|
|
65
|
-
): ExecutableUpdateBuilder<T, true> {
|
|
83
|
+
): ExecutableUpdateBuilder<T, true, ReturnPreference> {
|
|
66
84
|
// Create a QueryBuilder for the user to configure
|
|
67
85
|
const queryBuilder = new QueryBuilder<
|
|
68
86
|
WithSystemFields<T>,
|
|
@@ -80,7 +98,7 @@ export class UpdateBuilder<
|
|
|
80
98
|
// Let the user configure it
|
|
81
99
|
const configuredBuilder = fn(queryBuilder);
|
|
82
100
|
|
|
83
|
-
return new ExecutableUpdateBuilder<T, true>({
|
|
101
|
+
return new ExecutableUpdateBuilder<T, true, ReturnPreference>({
|
|
84
102
|
occurrence: this.occurrence,
|
|
85
103
|
tableName: this.tableName,
|
|
86
104
|
databaseName: this.databaseName,
|
|
@@ -88,6 +106,8 @@ export class UpdateBuilder<
|
|
|
88
106
|
data: this.data,
|
|
89
107
|
mode: "byFilter",
|
|
90
108
|
queryBuilder: configuredBuilder,
|
|
109
|
+
returnPreference: this.returnPreference,
|
|
110
|
+
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
91
111
|
});
|
|
92
112
|
}
|
|
93
113
|
}
|
|
@@ -95,12 +115,16 @@ export class UpdateBuilder<
|
|
|
95
115
|
/**
|
|
96
116
|
* Executable update builder - has execute() method
|
|
97
117
|
* Returned after calling .byId() or .where()
|
|
98
|
-
*
|
|
118
|
+
* Can return either updated count or full record based on returnFullRecord option
|
|
99
119
|
*/
|
|
100
120
|
export class ExecutableUpdateBuilder<
|
|
101
121
|
T extends Record<string, any>,
|
|
102
122
|
IsByFilter extends boolean,
|
|
103
|
-
|
|
123
|
+
ReturnPreference extends "minimal" | "representation" = "minimal",
|
|
124
|
+
> implements
|
|
125
|
+
ExecutableBuilder<
|
|
126
|
+
ReturnPreference extends "minimal" ? { updatedCount: number } : T
|
|
127
|
+
>
|
|
104
128
|
{
|
|
105
129
|
private tableName: string;
|
|
106
130
|
private databaseName: string;
|
|
@@ -110,6 +134,8 @@ export class ExecutableUpdateBuilder<
|
|
|
110
134
|
private mode: "byId" | "byFilter";
|
|
111
135
|
private recordId?: string | number;
|
|
112
136
|
private queryBuilder?: QueryBuilder<any>;
|
|
137
|
+
private returnPreference: ReturnPreference;
|
|
138
|
+
private databaseUseEntityIds: boolean;
|
|
113
139
|
|
|
114
140
|
constructor(config: {
|
|
115
141
|
occurrence?: TableOccurrence<any, any, any, any>;
|
|
@@ -120,6 +146,8 @@ export class ExecutableUpdateBuilder<
|
|
|
120
146
|
mode: "byId" | "byFilter";
|
|
121
147
|
recordId?: string | number;
|
|
122
148
|
queryBuilder?: QueryBuilder<any>;
|
|
149
|
+
returnPreference: ReturnPreference;
|
|
150
|
+
databaseUseEntityIds?: boolean;
|
|
123
151
|
}) {
|
|
124
152
|
this.occurrence = config.occurrence;
|
|
125
153
|
this.tableName = config.tableName;
|
|
@@ -129,44 +157,126 @@ export class ExecutableUpdateBuilder<
|
|
|
129
157
|
this.mode = config.mode;
|
|
130
158
|
this.recordId = config.recordId;
|
|
131
159
|
this.queryBuilder = config.queryBuilder;
|
|
160
|
+
this.returnPreference = config.returnPreference;
|
|
161
|
+
this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Helper to merge database-level useEntityIds with per-request options
|
|
166
|
+
*/
|
|
167
|
+
private mergeExecuteOptions(
|
|
168
|
+
options?: RequestInit & FFetchOptions & ExecuteOptions,
|
|
169
|
+
): RequestInit & FFetchOptions & { useEntityIds?: boolean } {
|
|
170
|
+
// If useEntityIds is not set in options, use the database-level setting
|
|
171
|
+
return {
|
|
172
|
+
...options,
|
|
173
|
+
useEntityIds: options?.useEntityIds ?? this.databaseUseEntityIds,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Gets the table ID (FMTID) if using entity IDs, otherwise returns the table name
|
|
179
|
+
* @param useEntityIds - Optional override for entity ID usage
|
|
180
|
+
*/
|
|
181
|
+
private getTableId(useEntityIds?: boolean): string {
|
|
182
|
+
if (!this.occurrence) {
|
|
183
|
+
return this.tableName;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const contextDefault = this.context._getUseEntityIds?.() ?? false;
|
|
187
|
+
const shouldUseIds = useEntityIds ?? contextDefault;
|
|
188
|
+
|
|
189
|
+
if (shouldUseIds) {
|
|
190
|
+
const identifiers = getTableIdentifiers(this.occurrence);
|
|
191
|
+
if (!identifiers.id) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`useEntityIds is true but TableOccurrence "${identifiers.name}" does not have an fmtId defined`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return identifiers.id;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return this.occurrence.getTableName();
|
|
132
200
|
}
|
|
133
201
|
|
|
134
202
|
async execute(
|
|
135
|
-
options?: RequestInit & FFetchOptions,
|
|
136
|
-
): Promise<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
203
|
+
options?: RequestInit & FFetchOptions & { useEntityIds?: boolean },
|
|
204
|
+
): Promise<
|
|
205
|
+
Result<ReturnPreference extends "minimal" ? { updatedCount: number } : T>
|
|
206
|
+
> {
|
|
207
|
+
// Merge database-level useEntityIds with per-request options
|
|
208
|
+
const mergedOptions = this.mergeExecuteOptions(options);
|
|
209
|
+
|
|
210
|
+
// Get table identifier with override support
|
|
211
|
+
const tableId = this.getTableId(mergedOptions.useEntityIds);
|
|
212
|
+
|
|
213
|
+
// Transform field names to FMFIDs if using entity IDs
|
|
214
|
+
// Only transform if useEntityIds resolves to true (respects per-request override)
|
|
215
|
+
const shouldUseIds = mergedOptions.useEntityIds ?? false;
|
|
216
|
+
|
|
217
|
+
const transformedData =
|
|
218
|
+
this.occurrence?.baseTable && shouldUseIds
|
|
219
|
+
? transformFieldNamesToIds(this.data, this.occurrence.baseTable)
|
|
220
|
+
: this.data;
|
|
221
|
+
|
|
222
|
+
let url: string;
|
|
223
|
+
|
|
224
|
+
if (this.mode === "byId") {
|
|
225
|
+
// Update single record by ID: PATCH /{database}/{table}('id')
|
|
226
|
+
url = `/${this.databaseName}/${tableId}('${this.recordId}')`;
|
|
227
|
+
} else {
|
|
228
|
+
// Update by filter: PATCH /{database}/{table}?$filter=...
|
|
229
|
+
if (!this.queryBuilder) {
|
|
230
|
+
throw new Error("Query builder is required for filter-based update");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Get the query string from the configured QueryBuilder
|
|
234
|
+
const queryString = this.queryBuilder.getQueryString();
|
|
235
|
+
// The query string will have the tableId already transformed by QueryBuilder
|
|
236
|
+
// Remove the leading "/" and table name from the query string as we'll build our own URL
|
|
237
|
+
const queryParams = queryString.startsWith(`/${tableId}`)
|
|
238
|
+
? queryString.slice(`/${tableId}`.length)
|
|
239
|
+
: queryString.startsWith(`/${this.tableName}`)
|
|
153
240
|
? queryString.slice(`/${this.tableName}`.length)
|
|
154
241
|
: queryString;
|
|
155
242
|
|
|
156
|
-
|
|
157
|
-
|
|
243
|
+
url = `/${this.databaseName}/${tableId}${queryParams}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Set Prefer header based on returnPreference
|
|
247
|
+
const headers: Record<string, string> = {
|
|
248
|
+
"Content-Type": "application/json",
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (this.returnPreference === "representation") {
|
|
252
|
+
headers["Prefer"] = "return=representation";
|
|
253
|
+
}
|
|
158
254
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
255
|
+
// Make PATCH request with JSON body
|
|
256
|
+
const result = await this.context._makeRequest(url, {
|
|
257
|
+
method: "PATCH",
|
|
258
|
+
headers,
|
|
259
|
+
body: JSON.stringify(transformedData),
|
|
260
|
+
...mergedOptions,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (result.error) {
|
|
264
|
+
return { data: undefined, error: result.error };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const response = result.data;
|
|
268
|
+
|
|
269
|
+
// Handle based on return preference
|
|
270
|
+
if (this.returnPreference === "representation") {
|
|
271
|
+
// Return the full updated record
|
|
272
|
+
return {
|
|
273
|
+
data: response as ReturnPreference extends "minimal"
|
|
274
|
+
? { updatedCount: number }
|
|
275
|
+
: T,
|
|
276
|
+
error: undefined,
|
|
277
|
+
};
|
|
278
|
+
} else {
|
|
279
|
+
// Return updated count (minimal)
|
|
170
280
|
let updatedCount = 0;
|
|
171
281
|
|
|
172
282
|
if (typeof response === "number") {
|
|
@@ -176,37 +286,113 @@ export class ExecutableUpdateBuilder<
|
|
|
176
286
|
updatedCount = (response as any).updatedCount || 0;
|
|
177
287
|
}
|
|
178
288
|
|
|
179
|
-
return { data: { updatedCount }, error: undefined };
|
|
180
|
-
} catch (error) {
|
|
181
289
|
return {
|
|
182
|
-
data:
|
|
183
|
-
|
|
290
|
+
data: { updatedCount } as ReturnPreference extends "minimal"
|
|
291
|
+
? { updatedCount: number }
|
|
292
|
+
: T,
|
|
293
|
+
error: undefined,
|
|
184
294
|
};
|
|
185
295
|
}
|
|
186
296
|
}
|
|
187
297
|
|
|
188
298
|
getRequestConfig(): { method: string; url: string; body?: any } {
|
|
299
|
+
// For batch operations, use database-level setting (no per-request override available here)
|
|
300
|
+
const tableId = this.getTableId(this.databaseUseEntityIds);
|
|
301
|
+
|
|
302
|
+
// Transform field names to FMFIDs if using entity IDs
|
|
303
|
+
const transformedData =
|
|
304
|
+
this.occurrence?.baseTable && this.databaseUseEntityIds
|
|
305
|
+
? transformFieldNamesToIds(this.data, this.occurrence.baseTable)
|
|
306
|
+
: this.data;
|
|
307
|
+
|
|
189
308
|
let url: string;
|
|
190
309
|
|
|
191
310
|
if (this.mode === "byId") {
|
|
192
|
-
url = `/${this.databaseName}/${
|
|
311
|
+
url = `/${this.databaseName}/${tableId}('${this.recordId}')`;
|
|
193
312
|
} else {
|
|
194
313
|
if (!this.queryBuilder) {
|
|
195
314
|
throw new Error("Query builder is required for filter-based update");
|
|
196
315
|
}
|
|
197
316
|
|
|
198
317
|
const queryString = this.queryBuilder.getQueryString();
|
|
199
|
-
const queryParams = queryString.startsWith(`/${
|
|
200
|
-
? queryString.slice(`/${
|
|
201
|
-
: queryString
|
|
318
|
+
const queryParams = queryString.startsWith(`/${tableId}`)
|
|
319
|
+
? queryString.slice(`/${tableId}`.length)
|
|
320
|
+
: queryString.startsWith(`/${this.tableName}`)
|
|
321
|
+
? queryString.slice(`/${this.tableName}`.length)
|
|
322
|
+
: queryString;
|
|
202
323
|
|
|
203
|
-
url = `/${this.databaseName}/${
|
|
324
|
+
url = `/${this.databaseName}/${tableId}${queryParams}`;
|
|
204
325
|
}
|
|
205
326
|
|
|
206
327
|
return {
|
|
207
328
|
method: "PATCH",
|
|
208
329
|
url,
|
|
209
|
-
body: JSON.stringify(
|
|
330
|
+
body: JSON.stringify(transformedData),
|
|
210
331
|
};
|
|
211
332
|
}
|
|
333
|
+
|
|
334
|
+
toRequest(baseUrl: string): Request {
|
|
335
|
+
const config = this.getRequestConfig();
|
|
336
|
+
const fullUrl = `${baseUrl}${config.url}`;
|
|
337
|
+
|
|
338
|
+
return new Request(fullUrl, {
|
|
339
|
+
method: config.method,
|
|
340
|
+
headers: {
|
|
341
|
+
"Content-Type": "application/json",
|
|
342
|
+
Accept: "application/json",
|
|
343
|
+
},
|
|
344
|
+
body: config.body,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async processResponse(
|
|
349
|
+
response: Response,
|
|
350
|
+
options?: ExecuteOptions,
|
|
351
|
+
): Promise<
|
|
352
|
+
Result<ReturnPreference extends "minimal" ? { updatedCount: number } : T>
|
|
353
|
+
> {
|
|
354
|
+
// Check for empty response (204 No Content)
|
|
355
|
+
const text = await response.text();
|
|
356
|
+
if (!text || text.trim() === "") {
|
|
357
|
+
// For 204 No Content, check the fmodata.affected_rows header
|
|
358
|
+
const affectedRows = response.headers.get("fmodata.affected_rows");
|
|
359
|
+
const updatedCount = affectedRows ? parseInt(affectedRows, 10) : 1;
|
|
360
|
+
return {
|
|
361
|
+
data: { updatedCount } as ReturnPreference extends "minimal"
|
|
362
|
+
? { updatedCount: number }
|
|
363
|
+
: T,
|
|
364
|
+
error: undefined,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const rawResponse = JSON.parse(text);
|
|
369
|
+
|
|
370
|
+
// Handle based on return preference
|
|
371
|
+
if (this.returnPreference === "representation") {
|
|
372
|
+
// Return the full updated record
|
|
373
|
+
return {
|
|
374
|
+
data: rawResponse as ReturnPreference extends "minimal"
|
|
375
|
+
? { updatedCount: number }
|
|
376
|
+
: T,
|
|
377
|
+
error: undefined,
|
|
378
|
+
};
|
|
379
|
+
} else {
|
|
380
|
+
// Return updated count (minimal)
|
|
381
|
+
let updatedCount = 0;
|
|
382
|
+
|
|
383
|
+
if (typeof rawResponse === "number") {
|
|
384
|
+
updatedCount = rawResponse;
|
|
385
|
+
} else if (rawResponse && typeof rawResponse === "object") {
|
|
386
|
+
// Check if the response has a count property (fallback)
|
|
387
|
+
updatedCount = (rawResponse as any).updatedCount || 0;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
data: { updatedCount } as ReturnPreference extends "minimal"
|
|
392
|
+
? { updatedCount: number }
|
|
393
|
+
: T,
|
|
394
|
+
error: undefined,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
}
|
|
212
398
|
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base class for all fmodata errors
|
|
5
|
+
*/
|
|
6
|
+
export abstract class FMODataError extends Error {
|
|
7
|
+
abstract readonly kind: string;
|
|
8
|
+
readonly timestamp: Date;
|
|
9
|
+
|
|
10
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.name = this.constructor.name;
|
|
13
|
+
this.timestamp = new Date();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ============================================
|
|
18
|
+
// HTTP Errors (with status codes)
|
|
19
|
+
// ============================================
|
|
20
|
+
|
|
21
|
+
export class HTTPError extends FMODataError {
|
|
22
|
+
readonly kind = "HTTPError" as const;
|
|
23
|
+
readonly url: string;
|
|
24
|
+
readonly status: number;
|
|
25
|
+
readonly statusText: string;
|
|
26
|
+
readonly response?: any;
|
|
27
|
+
|
|
28
|
+
constructor(url: string, status: number, statusText: string, response?: any) {
|
|
29
|
+
super(`HTTP ${status} ${statusText} for ${url}`);
|
|
30
|
+
this.url = url;
|
|
31
|
+
this.status = status;
|
|
32
|
+
this.statusText = statusText;
|
|
33
|
+
this.response = response;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Helper methods for common status checks
|
|
37
|
+
is4xx(): boolean {
|
|
38
|
+
return this.status >= 400 && this.status < 500;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
is5xx(): boolean {
|
|
42
|
+
return this.status >= 500 && this.status < 600;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
isNotFound(): boolean {
|
|
46
|
+
return this.status === 404;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
isUnauthorized(): boolean {
|
|
50
|
+
return this.status === 401;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
isForbidden(): boolean {
|
|
54
|
+
return this.status === 403;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================
|
|
59
|
+
// OData Specific Errors
|
|
60
|
+
// ============================================
|
|
61
|
+
|
|
62
|
+
export class ODataError extends FMODataError {
|
|
63
|
+
readonly kind = "ODataError" as const;
|
|
64
|
+
readonly url: string;
|
|
65
|
+
readonly code?: string;
|
|
66
|
+
readonly details?: any;
|
|
67
|
+
|
|
68
|
+
constructor(url: string, message: string, code?: string, details?: any) {
|
|
69
|
+
super(`OData error: ${message}`);
|
|
70
|
+
this.url = url;
|
|
71
|
+
this.code = code;
|
|
72
|
+
this.details = details;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class SchemaLockedError extends FMODataError {
|
|
77
|
+
readonly kind = "SchemaLockedError" as const;
|
|
78
|
+
readonly url: string;
|
|
79
|
+
readonly code: string;
|
|
80
|
+
readonly details?: any;
|
|
81
|
+
|
|
82
|
+
constructor(url: string, message: string, details?: any) {
|
|
83
|
+
super(`OData error: ${message}`);
|
|
84
|
+
this.url = url;
|
|
85
|
+
this.code = "303";
|
|
86
|
+
this.details = details;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================
|
|
91
|
+
// Validation Errors
|
|
92
|
+
// ============================================
|
|
93
|
+
|
|
94
|
+
export class ValidationError extends FMODataError {
|
|
95
|
+
readonly kind = "ValidationError" as const;
|
|
96
|
+
readonly field?: string;
|
|
97
|
+
readonly issues: readonly StandardSchemaV1.Issue[];
|
|
98
|
+
readonly value?: unknown;
|
|
99
|
+
|
|
100
|
+
constructor(
|
|
101
|
+
message: string,
|
|
102
|
+
issues: readonly StandardSchemaV1.Issue[],
|
|
103
|
+
options?: {
|
|
104
|
+
field?: string;
|
|
105
|
+
value?: unknown;
|
|
106
|
+
cause?: Error["cause"];
|
|
107
|
+
},
|
|
108
|
+
) {
|
|
109
|
+
super(
|
|
110
|
+
message,
|
|
111
|
+
options?.cause !== undefined ? { cause: options.cause } : undefined,
|
|
112
|
+
);
|
|
113
|
+
this.field = options?.field;
|
|
114
|
+
this.issues = issues;
|
|
115
|
+
this.value = options?.value;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class ResponseStructureError extends FMODataError {
|
|
120
|
+
readonly kind = "ResponseStructureError" as const;
|
|
121
|
+
readonly expected: string;
|
|
122
|
+
readonly received: any;
|
|
123
|
+
|
|
124
|
+
constructor(expected: string, received: any) {
|
|
125
|
+
super(`Invalid response structure: expected ${expected}`);
|
|
126
|
+
this.expected = expected;
|
|
127
|
+
this.received = received;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export class RecordCountMismatchError extends FMODataError {
|
|
132
|
+
readonly kind = "RecordCountMismatchError" as const;
|
|
133
|
+
readonly expected: number | "one" | "at-most-one";
|
|
134
|
+
readonly received: number;
|
|
135
|
+
|
|
136
|
+
constructor(expected: number | "one" | "at-most-one", received: number) {
|
|
137
|
+
const expectedStr = typeof expected === "number" ? expected : expected;
|
|
138
|
+
super(`Expected ${expectedStr} record(s), but received ${received}`);
|
|
139
|
+
this.expected = expected;
|
|
140
|
+
this.received = received;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export class InvalidLocationHeaderError extends FMODataError {
|
|
145
|
+
readonly kind = "InvalidLocationHeaderError" as const;
|
|
146
|
+
readonly locationHeader?: string;
|
|
147
|
+
|
|
148
|
+
constructor(message: string, locationHeader?: string) {
|
|
149
|
+
super(message);
|
|
150
|
+
this.locationHeader = locationHeader;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================
|
|
155
|
+
// Type Guards
|
|
156
|
+
// ============================================
|
|
157
|
+
|
|
158
|
+
export function isHTTPError(error: unknown): error is HTTPError {
|
|
159
|
+
return error instanceof HTTPError;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function isValidationError(error: unknown): error is ValidationError {
|
|
163
|
+
return error instanceof ValidationError;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function isODataError(error: unknown): error is ODataError {
|
|
167
|
+
return error instanceof ODataError;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function isSchemaLockedError(
|
|
171
|
+
error: unknown,
|
|
172
|
+
): error is SchemaLockedError {
|
|
173
|
+
return error instanceof SchemaLockedError;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function isResponseStructureError(
|
|
177
|
+
error: unknown,
|
|
178
|
+
): error is ResponseStructureError {
|
|
179
|
+
return error instanceof ResponseStructureError;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function isRecordCountMismatchError(
|
|
183
|
+
error: unknown,
|
|
184
|
+
): error is RecordCountMismatchError {
|
|
185
|
+
return error instanceof RecordCountMismatchError;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function isFMODataError(error: unknown): error is FMODataError {
|
|
189
|
+
return error instanceof FMODataError;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ============================================
|
|
193
|
+
// Union type for all possible errors
|
|
194
|
+
// ============================================
|
|
195
|
+
|
|
196
|
+
// Re-export ffetch errors (they'll be imported from @fetchkit/ffetch)
|
|
197
|
+
export type {
|
|
198
|
+
TimeoutError,
|
|
199
|
+
AbortError,
|
|
200
|
+
NetworkError,
|
|
201
|
+
RetryLimitError,
|
|
202
|
+
CircuitOpenError,
|
|
203
|
+
} from "@fetchkit/ffetch";
|
|
204
|
+
|
|
205
|
+
export type FMODataErrorType =
|
|
206
|
+
| import("@fetchkit/ffetch").TimeoutError
|
|
207
|
+
| import("@fetchkit/ffetch").AbortError
|
|
208
|
+
| import("@fetchkit/ffetch").NetworkError
|
|
209
|
+
| import("@fetchkit/ffetch").RetryLimitError
|
|
210
|
+
| import("@fetchkit/ffetch").CircuitOpenError
|
|
211
|
+
| HTTPError
|
|
212
|
+
| ODataError
|
|
213
|
+
| SchemaLockedError
|
|
214
|
+
| ValidationError
|
|
215
|
+
| ResponseStructureError
|
|
216
|
+
| RecordCountMismatchError
|
|
217
|
+
| InvalidLocationHeaderError;
|