@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
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;
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Barrel file - exports all public API from the client folder
|
|
2
|
-
export { BaseTable } from "./client/base-table";
|
|
2
|
+
export { BaseTable, BaseTableWithIds } from "./client/base-table";
|
|
3
3
|
export {
|
|
4
4
|
TableOccurrence,
|
|
5
5
|
createTableOccurrence,
|
|
6
|
+
TableOccurrenceWithIds,
|
|
7
|
+
createTableOccurrenceWithIds,
|
|
6
8
|
} from "./client/table-occurrence";
|
|
7
9
|
export { FMServerConnection } from "./client/filemaker-odata";
|
|
8
10
|
|
|
@@ -10,6 +12,16 @@ export { FMServerConnection } from "./client/filemaker-odata";
|
|
|
10
12
|
// Users get these instances from the builder pattern, not by direct instantiation
|
|
11
13
|
export type { Database } from "./client/database";
|
|
12
14
|
export type { EntitySet } from "./client/entity-set";
|
|
15
|
+
export type {
|
|
16
|
+
SchemaManager,
|
|
17
|
+
Field,
|
|
18
|
+
StringField,
|
|
19
|
+
NumericField,
|
|
20
|
+
DateField,
|
|
21
|
+
TimeField,
|
|
22
|
+
TimestampField,
|
|
23
|
+
ContainerField,
|
|
24
|
+
} from "./client/schema-manager";
|
|
13
25
|
|
|
14
26
|
// Utility types for type annotations
|
|
15
27
|
export type {
|
|
@@ -18,6 +30,7 @@ export type {
|
|
|
18
30
|
InsertData,
|
|
19
31
|
UpdateData,
|
|
20
32
|
ODataRecordMetadata,
|
|
33
|
+
Metadata,
|
|
21
34
|
} from "./types";
|
|
22
35
|
|
|
23
36
|
// Filter types
|
|
@@ -31,3 +44,32 @@ export type {
|
|
|
31
44
|
DateOperators,
|
|
32
45
|
LogicalFilter,
|
|
33
46
|
} from "./filter-types";
|
|
47
|
+
|
|
48
|
+
// Re-export ffetch errors
|
|
49
|
+
export {
|
|
50
|
+
TimeoutError,
|
|
51
|
+
AbortError,
|
|
52
|
+
NetworkError,
|
|
53
|
+
RetryLimitError,
|
|
54
|
+
CircuitOpenError,
|
|
55
|
+
} from "@fetchkit/ffetch";
|
|
56
|
+
|
|
57
|
+
// Export our errors
|
|
58
|
+
export {
|
|
59
|
+
FMODataError,
|
|
60
|
+
HTTPError,
|
|
61
|
+
ODataError,
|
|
62
|
+
SchemaLockedError,
|
|
63
|
+
ValidationError,
|
|
64
|
+
ResponseStructureError,
|
|
65
|
+
RecordCountMismatchError,
|
|
66
|
+
isHTTPError,
|
|
67
|
+
isValidationError,
|
|
68
|
+
isODataError,
|
|
69
|
+
isSchemaLockedError,
|
|
70
|
+
isResponseStructureError,
|
|
71
|
+
isRecordCountMismatchError,
|
|
72
|
+
isFMODataError,
|
|
73
|
+
} from "./errors";
|
|
74
|
+
|
|
75
|
+
export type { FMODataErrorType } from "./errors";
|
package/src/transform.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type { BaseTable } from "./client/base-table";
|
|
2
|
+
import type { TableOccurrence } from "./client/table-occurrence";
|
|
3
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Transforms field names to FileMaker field IDs (FMFID) in an object
|
|
7
|
+
* @param data - Object with field names as keys
|
|
8
|
+
* @param baseTable - BaseTable instance to get field IDs from
|
|
9
|
+
* @returns Object with FMFID keys instead of field names
|
|
10
|
+
*/
|
|
11
|
+
export function transformFieldNamesToIds<T extends Record<string, any>>(
|
|
12
|
+
data: T,
|
|
13
|
+
baseTable: BaseTable<any, any, any, any>,
|
|
14
|
+
): Record<string, any> {
|
|
15
|
+
if (!baseTable.isUsingFieldIds()) {
|
|
16
|
+
return data;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const transformed: Record<string, any> = {};
|
|
20
|
+
for (const [fieldName, value] of Object.entries(data)) {
|
|
21
|
+
const fieldId = baseTable.getFieldId(fieldName as any);
|
|
22
|
+
transformed[fieldId] = value;
|
|
23
|
+
}
|
|
24
|
+
return transformed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Transforms FileMaker field IDs (FMFID) to field names in an object
|
|
29
|
+
* @param data - Object with FMFID keys
|
|
30
|
+
* @param baseTable - BaseTable instance to get field names from
|
|
31
|
+
* @returns Object with field names as keys instead of FMFIDs
|
|
32
|
+
*/
|
|
33
|
+
export function transformFieldIdsToNames<T extends Record<string, any>>(
|
|
34
|
+
data: T,
|
|
35
|
+
baseTable: BaseTable<any, any, any, any>,
|
|
36
|
+
): Record<string, any> {
|
|
37
|
+
if (!baseTable.isUsingFieldIds()) {
|
|
38
|
+
return data;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const transformed: Record<string, any> = {};
|
|
42
|
+
for (const [key, value] of Object.entries(data)) {
|
|
43
|
+
// Check if this is an OData metadata field (starts with @)
|
|
44
|
+
if (key.startsWith("@")) {
|
|
45
|
+
transformed[key] = value;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fieldName = baseTable.getFieldName(key);
|
|
50
|
+
transformed[fieldName] = value;
|
|
51
|
+
}
|
|
52
|
+
return transformed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Transforms a field name to FMFID or returns the field name if not using IDs
|
|
57
|
+
* @param fieldName - The field name to transform
|
|
58
|
+
* @param baseTable - BaseTable instance to get field ID from
|
|
59
|
+
* @returns The FMFID or field name
|
|
60
|
+
*/
|
|
61
|
+
export function transformFieldName(
|
|
62
|
+
fieldName: string,
|
|
63
|
+
baseTable: BaseTable<any, any, any, any>,
|
|
64
|
+
): string {
|
|
65
|
+
return baseTable.getFieldId(fieldName as any);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Transforms a table occurrence name to FMTID or returns the name if not using IDs
|
|
70
|
+
* @param occurrence - TableOccurrence instance to get table ID from
|
|
71
|
+
* @returns The FMTID or table name
|
|
72
|
+
*/
|
|
73
|
+
export function transformTableName(
|
|
74
|
+
occurrence: TableOccurrence<any, any, any, any>,
|
|
75
|
+
): string {
|
|
76
|
+
return occurrence.getTableId();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Gets both table name and ID from an occurrence
|
|
81
|
+
* @param occurrence - TableOccurrence instance
|
|
82
|
+
* @returns Object with name (always present) and id (may be undefined if not using IDs)
|
|
83
|
+
*/
|
|
84
|
+
export function getTableIdentifiers(
|
|
85
|
+
occurrence: TableOccurrence<any, any, any, any>,
|
|
86
|
+
): { name: string; id: string | undefined } {
|
|
87
|
+
return {
|
|
88
|
+
name: occurrence.getTableName(),
|
|
89
|
+
id: occurrence.isUsingTableId() ? occurrence.getTableId() : undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Transforms response data by converting field IDs back to field names recursively.
|
|
95
|
+
* Handles both single records and arrays of records, as well as nested expand relationships.
|
|
96
|
+
*
|
|
97
|
+
* @param data - Response data from FileMaker (can be single record, array, or wrapped in value property)
|
|
98
|
+
* @param baseTable - BaseTable instance for the main table
|
|
99
|
+
* @param expandConfigs - Configuration for expanded relations (optional)
|
|
100
|
+
* @returns Transformed data with field names instead of IDs
|
|
101
|
+
*/
|
|
102
|
+
export function transformResponseFields(
|
|
103
|
+
data: any,
|
|
104
|
+
baseTable: BaseTable<any, any, any, any>,
|
|
105
|
+
expandConfigs?: Array<{
|
|
106
|
+
relation: string;
|
|
107
|
+
occurrence?: TableOccurrence<any, any, any, any>;
|
|
108
|
+
}>,
|
|
109
|
+
): any {
|
|
110
|
+
if (!baseTable.isUsingFieldIds()) {
|
|
111
|
+
return data;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Handle null/undefined
|
|
115
|
+
if (data === null || data === undefined) {
|
|
116
|
+
return data;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Handle OData list response with value array
|
|
120
|
+
if (data.value && Array.isArray(data.value)) {
|
|
121
|
+
return {
|
|
122
|
+
...data,
|
|
123
|
+
value: data.value.map((record: any) =>
|
|
124
|
+
transformSingleRecord(record, baseTable, expandConfigs),
|
|
125
|
+
),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Handle array of records
|
|
130
|
+
if (Array.isArray(data)) {
|
|
131
|
+
return data.map((record) =>
|
|
132
|
+
transformSingleRecord(record, baseTable, expandConfigs),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Handle single record
|
|
137
|
+
return transformSingleRecord(data, baseTable, expandConfigs);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Transforms a single record, converting field IDs to names and handling nested expands
|
|
142
|
+
*/
|
|
143
|
+
function transformSingleRecord(
|
|
144
|
+
record: any,
|
|
145
|
+
baseTable: BaseTable<any, any, any, any>,
|
|
146
|
+
expandConfigs?: Array<{
|
|
147
|
+
relation: string;
|
|
148
|
+
occurrence?: TableOccurrence<any, any, any, any>;
|
|
149
|
+
}>,
|
|
150
|
+
): any {
|
|
151
|
+
if (!record || typeof record !== "object") {
|
|
152
|
+
return record;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const transformed: Record<string, any> = {};
|
|
156
|
+
|
|
157
|
+
for (const [key, value] of Object.entries(record)) {
|
|
158
|
+
// Preserve OData metadata fields
|
|
159
|
+
if (key.startsWith("@")) {
|
|
160
|
+
transformed[key] = value;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check if this is an expanded relation (by relation name)
|
|
165
|
+
let expandConfig = expandConfigs?.find((ec) => ec.relation === key);
|
|
166
|
+
|
|
167
|
+
// If not found by relation name, check if this key is a FMTID
|
|
168
|
+
// (FileMaker returns expanded relations with FMTID keys when using entity IDs)
|
|
169
|
+
if (!expandConfig && key.startsWith("FMTID:")) {
|
|
170
|
+
expandConfig = expandConfigs?.find(
|
|
171
|
+
(ec) =>
|
|
172
|
+
ec.occurrence &&
|
|
173
|
+
ec.occurrence.isUsingTableId() &&
|
|
174
|
+
ec.occurrence.getTableId() === key,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (expandConfig && expandConfig.occurrence) {
|
|
179
|
+
// Transform the expanded relation data recursively
|
|
180
|
+
// Use the relation name (not the FMTID) as the key
|
|
181
|
+
const relationKey = expandConfig.relation;
|
|
182
|
+
|
|
183
|
+
if (Array.isArray(value)) {
|
|
184
|
+
transformed[relationKey] = value.map((nestedRecord) =>
|
|
185
|
+
transformSingleRecord(
|
|
186
|
+
nestedRecord,
|
|
187
|
+
expandConfig.occurrence!.baseTable,
|
|
188
|
+
undefined, // Don't pass nested expand configs for now
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
} else if (value && typeof value === "object") {
|
|
192
|
+
transformed[relationKey] = transformSingleRecord(
|
|
193
|
+
value,
|
|
194
|
+
expandConfig.occurrence.baseTable,
|
|
195
|
+
undefined,
|
|
196
|
+
);
|
|
197
|
+
} else {
|
|
198
|
+
transformed[relationKey] = value;
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Transform field ID to field name
|
|
204
|
+
const fieldName = baseTable.getFieldName(key);
|
|
205
|
+
transformed[fieldName] = value;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return transformed;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Transforms an array of field names to FMFIDs
|
|
213
|
+
* @param fieldNames - Array of field names
|
|
214
|
+
* @param baseTable - BaseTable instance to get field IDs from
|
|
215
|
+
* @returns Array of FMFIDs or field names
|
|
216
|
+
*/
|
|
217
|
+
export function transformFieldNamesArray(
|
|
218
|
+
fieldNames: string[],
|
|
219
|
+
baseTable: BaseTable<any, any, any, any>,
|
|
220
|
+
): string[] {
|
|
221
|
+
if (!baseTable.isUsingFieldIds()) {
|
|
222
|
+
return fieldNames;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return fieldNames.map((fieldName) => baseTable.getFieldId(fieldName as any));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Transforms a field name in an orderBy string (e.g., "name desc" -> "FMFID:1 desc")
|
|
230
|
+
* @param orderByString - The orderBy string (field name with optional asc/desc)
|
|
231
|
+
* @param baseTable - BaseTable instance to get field ID from
|
|
232
|
+
* @returns Transformed orderBy string with FMFID
|
|
233
|
+
*/
|
|
234
|
+
export function transformOrderByField(
|
|
235
|
+
orderByString: string,
|
|
236
|
+
baseTable: BaseTable<any, any, any, any>,
|
|
237
|
+
): string {
|
|
238
|
+
if (!baseTable.isUsingFieldIds()) {
|
|
239
|
+
return orderByString;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Parse the orderBy string to extract field name and direction
|
|
243
|
+
const parts = orderByString.trim().split(/\s+/);
|
|
244
|
+
const fieldName = parts[0];
|
|
245
|
+
const direction = parts[1]; // "asc" or "desc" or undefined
|
|
246
|
+
|
|
247
|
+
const fieldId = baseTable.getFieldId(fieldName as any);
|
|
248
|
+
return direction ? `${fieldId} ${direction}` : fieldId;
|
|
249
|
+
}
|