@proofkit/fmodata 0.1.0-alpha.4 → 0.1.0-alpha.7
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 +690 -31
- package/dist/esm/client/base-table.d.ts +122 -5
- package/dist/esm/client/base-table.js +46 -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 +54 -5
- 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 +22 -8
- package/dist/esm/client/entity-set.js +28 -8
- package/dist/esm/client/entity-set.js.map +1 -1
- package/dist/esm/client/filemaker-odata.d.ts +22 -3
- package/dist/esm/client/filemaker-odata.js +122 -27
- 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 +26 -5
- package/dist/esm/client/query-builder.js +455 -208
- package/dist/esm/client/query-builder.js.map +1 -1
- package/dist/esm/client/record-builder.d.ts +19 -4
- package/dist/esm/client/record-builder.js +132 -40
- 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 +66 -2
- package/dist/esm/client/table-occurrence.js +36 -1
- 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 +7 -3
- package/dist/esm/index.js +27 -3
- 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 +155 -8
- package/src/client/batch-builder.ts +265 -0
- package/src/client/batch-request.ts +485 -0
- package/src/client/database.ts +173 -16
- package/src/client/delete-builder.ts +149 -48
- package/src/client/entity-set.ts +99 -15
- package/src/client/filemaker-odata.ts +178 -34
- package/src/client/insert-builder.ts +350 -40
- package/src/client/query-builder.ts +609 -236
- package/src/client/record-builder.ts +186 -53
- package/src/client/response-processor.ts +103 -0
- package/src/client/schema-manager.ts +246 -0
- package/src/client/table-occurrence.ts +118 -4
- package/src/client/update-builder.ts +235 -49
- package/src/errors.ts +217 -0
- package/src/index.ts +43 -1
- package/src/transform.ts +249 -0
- package/src/types.ts +201 -35
- package/src/validation.ts +120 -36
|
@@ -5,13 +5,16 @@ import type {
|
|
|
5
5
|
ODataRecordMetadata,
|
|
6
6
|
ODataFieldResponse,
|
|
7
7
|
InferSchemaType,
|
|
8
|
+
ExecuteOptions,
|
|
8
9
|
} from "../types";
|
|
9
10
|
import type { TableOccurrence } from "./table-occurrence";
|
|
10
11
|
import type { BaseTable } from "./base-table";
|
|
12
|
+
import { transformTableName, transformResponseFields, getTableIdentifiers } from "../transform";
|
|
11
13
|
import { QueryBuilder } from "./query-builder";
|
|
12
14
|
import { validateSingleResponse } from "../validation";
|
|
13
15
|
import { type FFetchOptions } from "@fetchkit/ffetch";
|
|
14
|
-
import {
|
|
16
|
+
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
17
|
+
// import type { z } from "zod/v4";
|
|
15
18
|
|
|
16
19
|
// Helper type to extract schema from a TableOccurrence
|
|
17
20
|
type ExtractSchemaFromOccurrence<O> =
|
|
@@ -68,18 +71,60 @@ export class RecordBuilder<
|
|
|
68
71
|
private navigateRelation?: string;
|
|
69
72
|
private navigateSourceTableName?: string;
|
|
70
73
|
|
|
74
|
+
private databaseUseEntityIds: boolean;
|
|
75
|
+
|
|
71
76
|
constructor(config: {
|
|
72
77
|
occurrence?: Occ;
|
|
73
78
|
tableName: string;
|
|
74
79
|
databaseName: string;
|
|
75
80
|
context: ExecutionContext;
|
|
76
81
|
recordId: string | number;
|
|
82
|
+
databaseUseEntityIds?: boolean;
|
|
77
83
|
}) {
|
|
78
84
|
this.occurrence = config.occurrence;
|
|
79
85
|
this.tableName = config.tableName;
|
|
80
86
|
this.databaseName = config.databaseName;
|
|
81
87
|
this.context = config.context;
|
|
82
88
|
this.recordId = config.recordId;
|
|
89
|
+
this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Helper to merge database-level useEntityIds with per-request options
|
|
94
|
+
*/
|
|
95
|
+
private mergeExecuteOptions(
|
|
96
|
+
options?: RequestInit & FFetchOptions & ExecuteOptions,
|
|
97
|
+
): RequestInit & FFetchOptions & { useEntityIds?: boolean } {
|
|
98
|
+
// If useEntityIds is not set in options, use the database-level setting
|
|
99
|
+
return {
|
|
100
|
+
...options,
|
|
101
|
+
useEntityIds: options?.useEntityIds ?? this.databaseUseEntityIds,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Gets the table ID (FMTID) if using entity IDs, otherwise returns the table name
|
|
107
|
+
* @param useEntityIds - Optional override for entity ID usage
|
|
108
|
+
*/
|
|
109
|
+
private getTableId(useEntityIds?: boolean): string {
|
|
110
|
+
if (!this.occurrence) {
|
|
111
|
+
return this.tableName;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const contextDefault = this.context._getUseEntityIds?.() ?? false;
|
|
115
|
+
const shouldUseIds = useEntityIds ?? contextDefault;
|
|
116
|
+
|
|
117
|
+
if (shouldUseIds) {
|
|
118
|
+
const identifiers = getTableIdentifiers(this.occurrence);
|
|
119
|
+
if (!identifiers.id) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`useEntityIds is true but TableOccurrence "${identifiers.name}" does not have an fmtId defined`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return identifiers.id;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return this.occurrence.getTableName();
|
|
83
128
|
}
|
|
84
129
|
|
|
85
130
|
getSingleField<K extends keyof T>(field: K): RecordBuilder<T, true, K, Occ> {
|
|
@@ -105,7 +150,7 @@ export class RecordBuilder<
|
|
|
105
150
|
): QueryBuilder<
|
|
106
151
|
ExtractSchemaFromOccurrence<
|
|
107
152
|
FindNavigationTarget<Occ, RelationName>
|
|
108
|
-
> extends Record<string,
|
|
153
|
+
> extends Record<string, StandardSchemaV1>
|
|
109
154
|
? InferSchemaType<
|
|
110
155
|
ExtractSchemaFromOccurrence<FindNavigationTarget<Occ, RelationName>>
|
|
111
156
|
>
|
|
@@ -127,9 +172,14 @@ export class RecordBuilder<
|
|
|
127
172
|
context: this.context,
|
|
128
173
|
});
|
|
129
174
|
// Store the navigation info - we'll use it in execute
|
|
175
|
+
// Transform relation name to FMTID if using entity IDs
|
|
176
|
+
const relationId = targetOccurrence
|
|
177
|
+
? transformTableName(targetOccurrence)
|
|
178
|
+
: relationName;
|
|
179
|
+
|
|
130
180
|
(builder as any).isNavigate = true;
|
|
131
181
|
(builder as any).navigateRecordId = this.recordId;
|
|
132
|
-
(builder as any).navigateRelation =
|
|
182
|
+
(builder as any).navigateRelation = relationId;
|
|
133
183
|
|
|
134
184
|
// If this RecordBuilder came from a navigated EntitySet, we need to preserve that base path
|
|
135
185
|
if (
|
|
@@ -142,74 +192,91 @@ export class RecordBuilder<
|
|
|
142
192
|
(builder as any).navigateBaseRelation = this.navigateRelation;
|
|
143
193
|
} else {
|
|
144
194
|
// Normal record navigation: /tableName('recordId')/relation
|
|
145
|
-
|
|
195
|
+
// Transform source table name to FMTID if using entity IDs
|
|
196
|
+
const sourceTableId = this.occurrence
|
|
197
|
+
? transformTableName(this.occurrence)
|
|
198
|
+
: this.tableName;
|
|
199
|
+
(builder as any).navigateSourceTableName = sourceTableId;
|
|
146
200
|
}
|
|
147
201
|
|
|
148
202
|
return builder;
|
|
149
203
|
}
|
|
150
204
|
|
|
151
205
|
async execute(
|
|
152
|
-
options?: RequestInit & FFetchOptions,
|
|
206
|
+
options?: RequestInit & FFetchOptions & { useEntityIds?: boolean },
|
|
153
207
|
): Promise<
|
|
154
208
|
Result<IsSingleField extends true ? T[FieldKey] : T & ODataRecordMetadata>
|
|
155
209
|
> {
|
|
156
|
-
|
|
157
|
-
let url: string;
|
|
158
|
-
|
|
159
|
-
// Build the base URL depending on whether this came from a navigated EntitySet
|
|
160
|
-
if (
|
|
161
|
-
this.isNavigateFromEntitySet &&
|
|
162
|
-
this.navigateSourceTableName &&
|
|
163
|
-
this.navigateRelation
|
|
164
|
-
) {
|
|
165
|
-
// From navigated EntitySet: /sourceTable/relation('recordId')
|
|
166
|
-
url = `/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateRelation}('${this.recordId}')`;
|
|
167
|
-
} else {
|
|
168
|
-
// Normal record: /tableName('recordId')
|
|
169
|
-
url = `/${this.databaseName}/${this.tableName}('${this.recordId}')`;
|
|
170
|
-
}
|
|
210
|
+
let url: string;
|
|
171
211
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
212
|
+
// Build the base URL depending on whether this came from a navigated EntitySet
|
|
213
|
+
if (
|
|
214
|
+
this.isNavigateFromEntitySet &&
|
|
215
|
+
this.navigateSourceTableName &&
|
|
216
|
+
this.navigateRelation
|
|
217
|
+
) {
|
|
218
|
+
// From navigated EntitySet: /sourceTable/relation('recordId')
|
|
219
|
+
url = `/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateRelation}('${this.recordId}')`;
|
|
220
|
+
} else {
|
|
221
|
+
// Normal record: /tableName('recordId') - use FMTID if configured
|
|
222
|
+
const tableId = this.getTableId(options?.useEntityIds ?? this.databaseUseEntityIds);
|
|
223
|
+
url = `/${this.databaseName}/${tableId}('${this.recordId}')`;
|
|
224
|
+
}
|
|
175
225
|
|
|
176
|
-
|
|
226
|
+
if (this.operation === "getSingleField" && this.operationParam) {
|
|
227
|
+
url += `/${this.operationParam}`;
|
|
228
|
+
}
|
|
177
229
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// Single field returns a JSON object with @context and value
|
|
181
|
-
const fieldResponse = response as ODataFieldResponse<T>;
|
|
182
|
-
return { data: fieldResponse.value as any, error: undefined };
|
|
183
|
-
}
|
|
230
|
+
const mergedOptions = this.mergeExecuteOptions(options);
|
|
231
|
+
const result = await this.context._makeRequest(url, mergedOptions);
|
|
184
232
|
|
|
185
|
-
|
|
186
|
-
|
|
233
|
+
if (result.error) {
|
|
234
|
+
return { data: undefined, error: result.error };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let response = result.data;
|
|
238
|
+
|
|
239
|
+
// Handle single field operation
|
|
240
|
+
if (this.operation === "getSingleField") {
|
|
241
|
+
// Single field returns a JSON object with @context and value
|
|
242
|
+
const fieldResponse = response as ODataFieldResponse<T>;
|
|
243
|
+
return { data: fieldResponse.value as any, error: undefined };
|
|
244
|
+
}
|
|
187
245
|
|
|
188
|
-
|
|
189
|
-
|
|
246
|
+
// Transform response field IDs back to names if using entity IDs
|
|
247
|
+
// Only transform if useEntityIds resolves to true (respects per-request override)
|
|
248
|
+
const shouldUseIds = mergedOptions.useEntityIds ?? false;
|
|
249
|
+
|
|
250
|
+
if (this.occurrence?.baseTable && shouldUseIds) {
|
|
251
|
+
response = transformResponseFields(
|
|
190
252
|
response,
|
|
191
|
-
|
|
192
|
-
undefined, // No
|
|
193
|
-
undefined, // No expand configs
|
|
194
|
-
"exact", // Expect exactly one record
|
|
253
|
+
this.occurrence.baseTable,
|
|
254
|
+
undefined, // No expand configs for simple get
|
|
195
255
|
);
|
|
256
|
+
}
|
|
196
257
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
258
|
+
// Get schema from occurrence if available
|
|
259
|
+
const schema = this.occurrence?.baseTable?.schema;
|
|
200
260
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
261
|
+
// Validate the single record response
|
|
262
|
+
const validation = await validateSingleResponse<any>(
|
|
263
|
+
response,
|
|
264
|
+
schema,
|
|
265
|
+
undefined, // No selected fields for record.get()
|
|
266
|
+
undefined, // No expand configs
|
|
267
|
+
"exact", // Expect exactly one record
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
if (!validation.valid) {
|
|
271
|
+
return { data: undefined, error: validation.error };
|
|
272
|
+
}
|
|
205
273
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
-
data: undefined,
|
|
210
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
211
|
-
};
|
|
274
|
+
// Handle null response
|
|
275
|
+
if (validation.data === null) {
|
|
276
|
+
return { data: null as any, error: undefined };
|
|
212
277
|
}
|
|
278
|
+
|
|
279
|
+
return { data: validation.data, error: undefined };
|
|
213
280
|
}
|
|
214
281
|
|
|
215
282
|
getRequestConfig(): { method: string; url: string; body?: any } {
|
|
@@ -224,8 +291,9 @@ export class RecordBuilder<
|
|
|
224
291
|
// From navigated EntitySet: /sourceTable/relation('recordId')
|
|
225
292
|
url = `/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateRelation}('${this.recordId}')`;
|
|
226
293
|
} else {
|
|
227
|
-
//
|
|
228
|
-
|
|
294
|
+
// For batch operations, use database-level setting (no per-request override available here)
|
|
295
|
+
const tableId = this.getTableId(this.databaseUseEntityIds);
|
|
296
|
+
url = `/${this.databaseName}/${tableId}('${this.recordId}')`;
|
|
229
297
|
}
|
|
230
298
|
|
|
231
299
|
if (this.operation === "getSingleField" && this.operationParam) {
|
|
@@ -237,4 +305,69 @@ export class RecordBuilder<
|
|
|
237
305
|
url,
|
|
238
306
|
};
|
|
239
307
|
}
|
|
308
|
+
|
|
309
|
+
toRequest(baseUrl: string): Request {
|
|
310
|
+
const config = this.getRequestConfig();
|
|
311
|
+
const fullUrl = `${baseUrl}${config.url}`;
|
|
312
|
+
|
|
313
|
+
return new Request(fullUrl, {
|
|
314
|
+
method: config.method,
|
|
315
|
+
headers: {
|
|
316
|
+
"Content-Type": "application/json",
|
|
317
|
+
Accept: "application/json",
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async processResponse(
|
|
323
|
+
response: Response,
|
|
324
|
+
options?: ExecuteOptions,
|
|
325
|
+
): Promise<
|
|
326
|
+
Result<IsSingleField extends true ? T[FieldKey] : T & ODataRecordMetadata>
|
|
327
|
+
> {
|
|
328
|
+
const rawResponse = await response.json();
|
|
329
|
+
|
|
330
|
+
// Handle single field operation
|
|
331
|
+
if (this.operation === "getSingleField") {
|
|
332
|
+
// Single field returns a JSON object with @context and value
|
|
333
|
+
const fieldResponse = rawResponse as ODataFieldResponse<T>;
|
|
334
|
+
return { data: fieldResponse.value as any, error: undefined };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Transform response field IDs back to names if using entity IDs
|
|
338
|
+
// Only transform if useEntityIds resolves to true (respects per-request override)
|
|
339
|
+
const shouldUseIds = options?.useEntityIds ?? this.databaseUseEntityIds;
|
|
340
|
+
|
|
341
|
+
let transformedResponse = rawResponse;
|
|
342
|
+
if (this.occurrence?.baseTable && shouldUseIds) {
|
|
343
|
+
transformedResponse = transformResponseFields(
|
|
344
|
+
rawResponse,
|
|
345
|
+
this.occurrence.baseTable,
|
|
346
|
+
undefined, // No expand configs for simple get
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Get schema from occurrence if available
|
|
351
|
+
const schema = this.occurrence?.baseTable?.schema;
|
|
352
|
+
|
|
353
|
+
// Validate the single record response
|
|
354
|
+
const validation = await validateSingleResponse<any>(
|
|
355
|
+
transformedResponse,
|
|
356
|
+
schema,
|
|
357
|
+
undefined, // No selected fields for record.get()
|
|
358
|
+
undefined, // No expand configs
|
|
359
|
+
"exact", // Expect exactly one record
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
if (!validation.valid) {
|
|
363
|
+
return { data: undefined, error: validation.error };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Handle null response
|
|
367
|
+
if (validation.data === null) {
|
|
368
|
+
return { data: null as any, error: undefined };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return { data: validation.data, error: undefined };
|
|
372
|
+
}
|
|
240
373
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
import type { BaseTable } from "./base-table";
|
|
3
|
+
import type { ExecuteOptions } from "../types";
|
|
4
|
+
import type { ExpandValidationConfig } from "../validation";
|
|
5
|
+
import { ValidationError, ResponseStructureError } from "../errors";
|
|
6
|
+
import { transformResponseFields } from "../transform";
|
|
7
|
+
import { validateListResponse, validateRecord } from "../validation";
|
|
8
|
+
|
|
9
|
+
// Type for raw OData responses
|
|
10
|
+
export type ODataResponse<T = unknown> = T & {
|
|
11
|
+
"@odata.context"?: string;
|
|
12
|
+
"@odata.count"?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ODataListResponse<T = unknown> = ODataResponse<{
|
|
16
|
+
value: T[];
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
export type ODataRecordResponse<T = unknown> = ODataResponse<
|
|
20
|
+
T & {
|
|
21
|
+
"@id"?: string;
|
|
22
|
+
"@editLink"?: string;
|
|
23
|
+
}
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Strip OData annotations from a single record
|
|
28
|
+
*/
|
|
29
|
+
export function stripODataAnnotations<T extends Record<string, unknown>>(
|
|
30
|
+
record: ODataRecordResponse<T>,
|
|
31
|
+
options?: ExecuteOptions,
|
|
32
|
+
): T {
|
|
33
|
+
if (options?.includeODataAnnotations === true) {
|
|
34
|
+
return record as T;
|
|
35
|
+
}
|
|
36
|
+
const { "@id": _id, "@editLink": _editLink, ...rest } = record;
|
|
37
|
+
return rest as T;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Transform field IDs back to names using the base table configuration
|
|
42
|
+
*/
|
|
43
|
+
export function applyFieldTransformation<T extends Record<string, unknown>>(
|
|
44
|
+
response: ODataResponse<T> | ODataListResponse<T>,
|
|
45
|
+
baseTable: BaseTable<Record<string, StandardSchemaV1>, any, any, any>,
|
|
46
|
+
expandConfigs?: ExpandValidationConfig[],
|
|
47
|
+
): ODataResponse<T> | ODataListResponse<T> {
|
|
48
|
+
return transformResponseFields(response, baseTable, expandConfigs) as
|
|
49
|
+
| ODataResponse<T>
|
|
50
|
+
| ODataListResponse<T>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Apply schema validation and transformation to data
|
|
55
|
+
*/
|
|
56
|
+
export async function applyValidation<T extends Record<string, unknown>>(
|
|
57
|
+
data: T | T[],
|
|
58
|
+
schema?: Record<string, StandardSchemaV1>,
|
|
59
|
+
selectedFields?: (keyof T)[],
|
|
60
|
+
expandConfigs?: ExpandValidationConfig[],
|
|
61
|
+
): Promise<
|
|
62
|
+
| { valid: true; data: T | T[] }
|
|
63
|
+
| { valid: false; error: ValidationError | ResponseStructureError }
|
|
64
|
+
> {
|
|
65
|
+
if (Array.isArray(data)) {
|
|
66
|
+
// Validate as a list
|
|
67
|
+
const validation = await validateListResponse<T>(
|
|
68
|
+
{ value: data },
|
|
69
|
+
schema,
|
|
70
|
+
selectedFields as string[] | undefined,
|
|
71
|
+
expandConfigs,
|
|
72
|
+
);
|
|
73
|
+
if (!validation.valid) {
|
|
74
|
+
return { valid: false, error: validation.error };
|
|
75
|
+
}
|
|
76
|
+
return { valid: true, data: validation.data };
|
|
77
|
+
} else {
|
|
78
|
+
// Validate as a single record
|
|
79
|
+
const validation = await validateRecord<T>(
|
|
80
|
+
data,
|
|
81
|
+
schema,
|
|
82
|
+
selectedFields,
|
|
83
|
+
expandConfigs,
|
|
84
|
+
);
|
|
85
|
+
if (!validation.valid) {
|
|
86
|
+
return { valid: false, error: validation.error };
|
|
87
|
+
}
|
|
88
|
+
return { valid: true, data: validation.data };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extract value array from OData list response, or wrap single record in array
|
|
94
|
+
*/
|
|
95
|
+
export function extractListValue<T>(
|
|
96
|
+
response: ODataListResponse<T> | ODataRecordResponse<T>,
|
|
97
|
+
): T[] {
|
|
98
|
+
if ("value" in response && Array.isArray(response.value)) {
|
|
99
|
+
return response.value;
|
|
100
|
+
}
|
|
101
|
+
// Single record responses return the record directly
|
|
102
|
+
return [response as T];
|
|
103
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import type { FFetchOptions } from "@fetchkit/ffetch";
|
|
2
|
+
import type { ExecutionContext } from "../types";
|
|
3
|
+
|
|
4
|
+
type GenericField = {
|
|
5
|
+
name: string;
|
|
6
|
+
nullable?: boolean;
|
|
7
|
+
primary?: boolean;
|
|
8
|
+
unique?: boolean;
|
|
9
|
+
global?: boolean;
|
|
10
|
+
repetitions?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type StringField = GenericField & {
|
|
14
|
+
type: "string";
|
|
15
|
+
maxLength?: number;
|
|
16
|
+
default?: "USER" | "USERNAME" | "CURRENT_USER";
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type NumericField = GenericField & {
|
|
20
|
+
type: "numeric";
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type DateField = GenericField & {
|
|
24
|
+
type: "date";
|
|
25
|
+
default?: "CURRENT_DATE" | "CURDATE";
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type TimeField = GenericField & {
|
|
29
|
+
type: "time";
|
|
30
|
+
default?: "CURRENT_TIME" | "CURTIME";
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type TimestampField = GenericField & {
|
|
34
|
+
type: "timestamp";
|
|
35
|
+
default?: "CURRENT_TIMESTAMP" | "CURTIMESTAMP";
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type ContainerField = GenericField & {
|
|
39
|
+
type: "container";
|
|
40
|
+
externalSecurePath?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type Field =
|
|
44
|
+
| StringField
|
|
45
|
+
| NumericField
|
|
46
|
+
| DateField
|
|
47
|
+
| TimeField
|
|
48
|
+
| TimestampField
|
|
49
|
+
| ContainerField;
|
|
50
|
+
|
|
51
|
+
export type {
|
|
52
|
+
StringField,
|
|
53
|
+
NumericField,
|
|
54
|
+
DateField,
|
|
55
|
+
TimeField,
|
|
56
|
+
TimestampField,
|
|
57
|
+
ContainerField,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type FileMakerField = Omit<Field, "type" | "repetitions" | "maxLength"> & {
|
|
61
|
+
type: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type TableDefinition = {
|
|
65
|
+
tableName: string;
|
|
66
|
+
fields: FileMakerField[];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export class SchemaManager {
|
|
70
|
+
public constructor(
|
|
71
|
+
private readonly databaseName: string,
|
|
72
|
+
private readonly context: ExecutionContext,
|
|
73
|
+
) {}
|
|
74
|
+
|
|
75
|
+
public async createTable(
|
|
76
|
+
tableName: string,
|
|
77
|
+
fields: Field[],
|
|
78
|
+
options?: RequestInit & FFetchOptions,
|
|
79
|
+
): Promise<TableDefinition> {
|
|
80
|
+
const result = await this.context._makeRequest<TableDefinition>(
|
|
81
|
+
`/${this.databaseName}/FileMaker_Tables`,
|
|
82
|
+
{
|
|
83
|
+
method: "POST",
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
tableName,
|
|
86
|
+
fields: fields.map(SchemaManager.compileFieldDefinition),
|
|
87
|
+
}),
|
|
88
|
+
...options,
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (result.error) {
|
|
93
|
+
throw result.error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result.data;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public async addFields(
|
|
100
|
+
tableName: string,
|
|
101
|
+
fields: Field[],
|
|
102
|
+
options?: RequestInit & FFetchOptions,
|
|
103
|
+
): Promise<TableDefinition> {
|
|
104
|
+
const result = await this.context._makeRequest<TableDefinition>(
|
|
105
|
+
`/${this.databaseName}/FileMaker_Tables/${tableName}`,
|
|
106
|
+
{
|
|
107
|
+
method: "PATCH",
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
fields: fields.map(SchemaManager.compileFieldDefinition),
|
|
110
|
+
}),
|
|
111
|
+
...options,
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (result.error) {
|
|
116
|
+
throw result.error;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result.data;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public async deleteTable(
|
|
123
|
+
tableName: string,
|
|
124
|
+
options?: RequestInit & FFetchOptions,
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
const result = await this.context._makeRequest(
|
|
127
|
+
`/${this.databaseName}/FileMaker_Tables/${tableName}`,
|
|
128
|
+
{ method: "DELETE", ...options },
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (result.error) {
|
|
132
|
+
throw result.error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public async deleteField(
|
|
137
|
+
tableName: string,
|
|
138
|
+
fieldName: string,
|
|
139
|
+
options?: RequestInit & FFetchOptions,
|
|
140
|
+
): Promise<void> {
|
|
141
|
+
const result = await this.context._makeRequest(
|
|
142
|
+
`/${this.databaseName}/FileMaker_Tables/${tableName}/${fieldName}`,
|
|
143
|
+
{
|
|
144
|
+
method: "DELETE",
|
|
145
|
+
...options,
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (result.error) {
|
|
150
|
+
throw result.error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public async createIndex(
|
|
155
|
+
tableName: string,
|
|
156
|
+
fieldName: string,
|
|
157
|
+
options?: RequestInit & FFetchOptions,
|
|
158
|
+
): Promise<{ indexName: string }> {
|
|
159
|
+
const result = await this.context._makeRequest<{ indexName: string }>(
|
|
160
|
+
`/${this.databaseName}/FileMaker_Indexes/${tableName}`,
|
|
161
|
+
{
|
|
162
|
+
method: "POST",
|
|
163
|
+
body: JSON.stringify({ indexName: fieldName }),
|
|
164
|
+
...options,
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
if (result.error) {
|
|
169
|
+
throw result.error;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return result.data;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public async deleteIndex(
|
|
176
|
+
tableName: string,
|
|
177
|
+
fieldName: string,
|
|
178
|
+
options?: RequestInit & FFetchOptions,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
const result = await this.context._makeRequest(
|
|
181
|
+
`/${this.databaseName}/FileMaker_Indexes/${tableName}/${fieldName}`,
|
|
182
|
+
{
|
|
183
|
+
method: "DELETE",
|
|
184
|
+
...options,
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (result.error) {
|
|
189
|
+
throw result.error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private static compileFieldDefinition(field: Field): FileMakerField {
|
|
194
|
+
let type: string = field.type;
|
|
195
|
+
const repetitions = field.repetitions;
|
|
196
|
+
|
|
197
|
+
// Handle string fields - convert to varchar and add maxLength if present
|
|
198
|
+
if (field.type === "string") {
|
|
199
|
+
type = "varchar";
|
|
200
|
+
const stringField = field as StringField;
|
|
201
|
+
if (stringField.maxLength !== undefined) {
|
|
202
|
+
type += `(${stringField.maxLength})`;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Add repetitions suffix if present
|
|
207
|
+
if (repetitions !== undefined) {
|
|
208
|
+
type += `[${repetitions}]`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Build the result object, excluding type, maxLength, and repetitions
|
|
212
|
+
const result: any = {
|
|
213
|
+
name: field.name,
|
|
214
|
+
type,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Add optional properties that FileMaker expects
|
|
218
|
+
if (field.nullable !== undefined) result.nullable = field.nullable;
|
|
219
|
+
if (field.primary !== undefined) result.primary = field.primary;
|
|
220
|
+
if (field.unique !== undefined) result.unique = field.unique;
|
|
221
|
+
if (field.global !== undefined) result.global = field.global;
|
|
222
|
+
|
|
223
|
+
// Add type-specific properties
|
|
224
|
+
if (field.type === "string") {
|
|
225
|
+
const stringField = field as StringField;
|
|
226
|
+
if (stringField.default !== undefined)
|
|
227
|
+
result.default = stringField.default;
|
|
228
|
+
} else if (field.type === "date") {
|
|
229
|
+
const dateField = field as DateField;
|
|
230
|
+
if (dateField.default !== undefined) result.default = dateField.default;
|
|
231
|
+
} else if (field.type === "time") {
|
|
232
|
+
const timeField = field as TimeField;
|
|
233
|
+
if (timeField.default !== undefined) result.default = timeField.default;
|
|
234
|
+
} else if (field.type === "timestamp") {
|
|
235
|
+
const timestampField = field as TimestampField;
|
|
236
|
+
if (timestampField.default !== undefined)
|
|
237
|
+
result.default = timestampField.default;
|
|
238
|
+
} else if (field.type === "container") {
|
|
239
|
+
const containerField = field as ContainerField;
|
|
240
|
+
if (containerField.externalSecurePath !== undefined)
|
|
241
|
+
result.externalSecurePath = containerField.externalSecurePath;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return result as FileMakerField;
|
|
245
|
+
}
|
|
246
|
+
}
|