-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathentity-set.ts
More file actions
386 lines (353 loc) · 17.5 KB
/
entity-set.ts
File metadata and controls
386 lines (353 loc) · 17.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
import type { InternalLogger } from "../logger";
import type { FieldBuilder } from "../orm/field-builders";
import type {
ColumnMap,
FMTable,
InferSchemaOutputFromFMTable,
InsertDataFromFMTable,
UpdateDataFromFMTable,
ValidExpandTarget,
} from "../orm/table";
import {
FMTable as FMTableClass,
getDefaultSelect,
getTableColumns,
getTableName,
getTableSchema,
isUsingEntityIds,
} from "../orm/table";
import type { FMODataLayer, ODataConfig } from "../services";
import { resolveTableId } from "./builders/table-utils";
import { CountBuilder } from "./count-builder";
import type { Database } from "./database";
import { DeleteBuilder } from "./delete-builder";
import { InsertBuilder } from "./insert-builder";
import { QueryBuilder } from "./query/index";
import { RecordBuilder } from "./record-builder";
import { createClientRuntime } from "./runtime";
import { UpdateBuilder } from "./update-builder";
// Helper type to extract defaultSelect from an FMTable
// Since TypeScript can't extract Symbol-indexed properties at the type level,
// we simplify to return keyof InferSchemaFromFMTable<O> when O is an FMTable.
// The actual defaultSelect logic is handled at runtime.
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
type _ExtractDefaultSelect<O> = O extends FMTable<any, any> ? keyof InferSchemaOutputFromFMTable<O> : never;
/**
* Helper type to extract properly-typed columns from an FMTable.
* This preserves the specific column types instead of widening to `any`.
*/
type ExtractColumnsFromOcc<T> =
// biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
T extends FMTable<infer TFields, infer TName, any>
? // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
TFields extends Record<string, FieldBuilder<any, any, any, any>>
? ColumnMap<TFields, TName>
: never
: never;
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
export class EntitySet<Occ extends FMTable<any, any>, DatabaseIncludeSpecialColumns extends boolean = false> {
private readonly occurrence: Occ;
private readonly layer: FMODataLayer;
private readonly config: ODataConfig;
private readonly logger: InternalLogger;
private readonly database: Database<DatabaseIncludeSpecialColumns>; // Database instance for accessing occurrences
private readonly isNavigateFromEntitySet?: boolean;
private readonly navigateRelation?: string;
private readonly navigateRelationEntityId?: string;
private readonly navigateSourceTableName?: string;
private readonly navigateSourceTableEntityId?: string;
private readonly navigateBasePath?: string; // Full base path for chained navigations
private readonly navigateBasePathEntityId?: string;
constructor(config: {
occurrence: Occ;
layer: FMODataLayer;
// biome-ignore lint/suspicious/noExplicitAny: Database type is optional and can be any Database instance
database?: any;
}) {
this.occurrence = config.occurrence;
this.database = config.database;
// Extract config and logger from the layer for sync access
const runtime = createClientRuntime(config.layer);
this.layer = runtime.layer;
this.config = runtime.config;
this.logger = runtime.logger;
}
// Type-only method to help TypeScript infer the schema from table
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
static create<Occ extends FMTable<any, any>, DatabaseIncludeSpecialColumns extends boolean = false>(config: {
occurrence: Occ;
layer: FMODataLayer;
database: Database<DatabaseIncludeSpecialColumns>;
}): EntitySet<Occ, DatabaseIncludeSpecialColumns> {
return new EntitySet<Occ, DatabaseIncludeSpecialColumns>({
occurrence: config.occurrence,
layer: config.layer,
database: config.database,
});
}
private applyNavigationContext<T>(builder: T): T {
if (this.isNavigateFromEntitySet && this.navigateRelation && this.navigateSourceTableName) {
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
(builder as any).navigation = {
relation: this.navigateRelation,
relationEntityId: this.navigateRelationEntityId,
sourceTableName: this.navigateSourceTableName,
sourceTableEntityId: this.navigateSourceTableEntityId,
basePath: this.navigateBasePath,
basePathEntityId: this.navigateBasePathEntityId,
};
}
return builder;
}
list(): QueryBuilder<
Occ,
keyof InferSchemaOutputFromFMTable<Occ>,
false,
false,
// biome-ignore lint/complexity/noBannedTypes: Empty object type represents no expands by default
{},
false,
DatabaseIncludeSpecialColumns
> {
const builder = new QueryBuilder<
Occ,
keyof InferSchemaOutputFromFMTable<Occ>,
false,
false,
// biome-ignore lint/complexity/noBannedTypes: Empty object type represents no expands by default
{},
false,
DatabaseIncludeSpecialColumns
>({
occurrence: this.occurrence as Occ,
layer: this.layer,
});
// Apply defaultSelect if occurrence exists and select hasn't been called
if (this.occurrence) {
// FMTable - access via helper functions
const defaultSelectValue = getDefaultSelect(this.occurrence);
// Schema is stored directly as Partial<Record<keyof TFields, StandardSchemaV1>>
const _schema = getTableSchema(this.occurrence);
if (defaultSelectValue === "schema") {
// Use getTableColumns to get all columns and select them
// This is equivalent to select(getTableColumns(occurrence))
// Cast to the declared return type - runtime behavior handles the actual selection
const allColumns = getTableColumns(this.occurrence) as ExtractColumnsFromOcc<Occ>;
const selectedBuilder = this.applyNavigationContext(
this.config.includeSpecialColumns
? builder.select(allColumns, { ROWID: true, ROWMODID: true })
: builder.select(allColumns),
).top(1000);
return selectedBuilder as QueryBuilder<
Occ,
keyof InferSchemaOutputFromFMTable<Occ>,
false,
false,
// biome-ignore lint/complexity/noBannedTypes: Empty object type represents no expands by default
{},
false,
DatabaseIncludeSpecialColumns
>;
}
if (typeof defaultSelectValue === "object") {
// defaultSelectValue is a select object (Record<string, Column>)
// Cast to the declared return type - runtime behavior handles the actual selection
const selectedBuilder = this.applyNavigationContext(
builder.select(defaultSelectValue as ExtractColumnsFromOcc<Occ>),
).top(1000);
return selectedBuilder as QueryBuilder<
Occ,
keyof InferSchemaOutputFromFMTable<Occ>,
false,
false,
// biome-ignore lint/complexity/noBannedTypes: Empty object type represents no expands by default
{},
false,
DatabaseIncludeSpecialColumns
>;
}
// If defaultSelect is "all", no changes needed (current behavior)
}
// Apply default pagination limit of 1000 records to prevent stack overflow
// with large datasets. Users can override with .top() if needed.
return this.applyNavigationContext(builder).top(1000);
}
count(): CountBuilder<Occ, DatabaseIncludeSpecialColumns> {
const builder = new CountBuilder<Occ, DatabaseIncludeSpecialColumns>({
occurrence: this.occurrence,
layer: this.layer,
});
return this.applyNavigationContext(builder);
}
get(
id: string | number,
// biome-ignore lint/complexity/noBannedTypes: Empty object type represents no expands by default
): RecordBuilder<Occ, false, undefined, keyof InferSchemaOutputFromFMTable<Occ>, {}, DatabaseIncludeSpecialColumns> {
const builder = new RecordBuilder<
Occ,
false,
undefined,
keyof InferSchemaOutputFromFMTable<Occ>,
// biome-ignore lint/complexity/noBannedTypes: Empty object type represents no expands by default
{},
DatabaseIncludeSpecialColumns
>({
occurrence: this.occurrence,
layer: this.layer,
recordId: id,
});
// Apply defaultSelect if occurrence exists
if (this.occurrence) {
// FMTable - access via helper functions
const defaultSelectValue = getDefaultSelect(this.occurrence);
// Schema is stored directly as Partial<Record<keyof TFields, StandardSchemaV1>>
const _schema = getTableSchema(this.occurrence);
if (defaultSelectValue === "schema") {
// Use getTableColumns to get all columns and select them
// This is equivalent to select(getTableColumns(occurrence))
// Use ExtractColumnsFromOcc to preserve the properly-typed column types
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter
const allColumns = getTableColumns(this.occurrence as any) as ExtractColumnsFromOcc<Occ>;
const selectedBuilder = this.applyNavigationContext(
this.config.includeSpecialColumns
? builder.select(allColumns, { ROWID: true, ROWMODID: true })
: builder.select(allColumns),
);
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type
return selectedBuilder as any;
}
if (typeof defaultSelectValue === "object" && defaultSelectValue !== null && !Array.isArray(defaultSelectValue)) {
// defaultSelectValue is a select object (Record<string, Column>)
// Use it directly with select()
// Use ExtractColumnsFromOcc to preserve the properly-typed column types
const selectedBuilder = this.applyNavigationContext(
builder.select(defaultSelectValue as ExtractColumnsFromOcc<Occ>),
);
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type
return selectedBuilder as any;
}
// If defaultSelect is "all", no changes needed (current behavior)
}
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type
return this.applyNavigationContext(builder) as any;
}
// Overload: when returnFullRecord is false
insert(data: InsertDataFromFMTable<Occ>, options: { returnFullRecord: false }): InsertBuilder<Occ, "minimal">;
// Overload: when returnFullRecord is true or omitted (default)
insert(data: InsertDataFromFMTable<Occ>, options?: { returnFullRecord?: true }): InsertBuilder<Occ, "representation">;
// Implementation
insert(
data: InsertDataFromFMTable<Occ>,
options?: { returnFullRecord?: boolean },
): InsertBuilder<Occ, "minimal" | "representation"> {
const returnPreference = options?.returnFullRecord === false ? "minimal" : "representation";
return new InsertBuilder<Occ, typeof returnPreference>({
occurrence: this.occurrence,
layer: this.layer,
// biome-ignore lint/suspicious/noExplicitAny: Input type is validated/transformed at runtime
data: data as any,
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter
returnPreference: returnPreference as any,
});
}
// Overload: when returnFullRecord is explicitly true
update(data: UpdateDataFromFMTable<Occ>, options: { returnFullRecord: true }): UpdateBuilder<Occ, "representation">;
// Overload: when returnFullRecord is false or omitted (default)
update(data: UpdateDataFromFMTable<Occ>, options?: { returnFullRecord?: false }): UpdateBuilder<Occ, "minimal">;
// Implementation
update(
data: UpdateDataFromFMTable<Occ>,
options?: { returnFullRecord?: boolean },
): UpdateBuilder<Occ, "minimal" | "representation"> {
const returnPreference = options?.returnFullRecord === true ? "representation" : "minimal";
return new UpdateBuilder<Occ, typeof returnPreference>({
occurrence: this.occurrence,
layer: this.layer,
// biome-ignore lint/suspicious/noExplicitAny: Input type is validated/transformed at runtime
data: data as any,
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter
returnPreference: returnPreference as any,
});
}
delete(): DeleteBuilder<Occ> {
return new DeleteBuilder<Occ>({
occurrence: this.occurrence,
layer: this.layer,
// biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type
}) as any;
}
// Implementation
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
navigate<TargetTable extends FMTable<any, any>>(
targetTable: ValidExpandTarget<Occ, TargetTable>,
// biome-ignore lint/suspicious/noExplicitAny: Required for conditional type inference
): EntitySet<TargetTable extends FMTable<any, any> ? TargetTable : never, DatabaseIncludeSpecialColumns> {
// Check if it's an FMTable object or a string
let relationName: string;
// FMTable object - extract name and validate
relationName = getTableName(targetTable);
// Runtime validation: Check if relation name is in navigationPaths
if (this.occurrence && FMTableClass.Symbol.NavigationPaths in this.occurrence) {
// biome-ignore lint/suspicious/noExplicitAny: Symbol property access for internal property
const navigationPaths = (this.occurrence as any)[FMTableClass.Symbol.NavigationPaths] as readonly string[];
if (navigationPaths && !navigationPaths.includes(relationName)) {
this.logger.warn(
`Cannot navigate to "${relationName}". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(", ") : "none"}`,
);
}
}
// Create EntitySet with target table
// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FMTable configuration
const entitySet = new EntitySet<any, DatabaseIncludeSpecialColumns>({
occurrence: targetTable,
layer: this.layer,
database: this.database,
});
// Resolve entity IDs lazily at request time by storing both name and ID forms
const relationEntityId = isUsingEntityIds(targetTable)
? resolveTableId(targetTable, relationName, true)
: relationName;
const sourceTableName = getTableName(this.occurrence);
const sourceTableEntityId = isUsingEntityIds(this.occurrence)
? resolveTableId(this.occurrence, sourceTableName, true)
: sourceTableName;
// Store the navigation info in the EntitySet
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
(entitySet as any).isNavigateFromEntitySet = true;
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
(entitySet as any).navigateRelation = relationName;
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
(entitySet as any).navigateRelationEntityId = relationEntityId;
// Build the full base path for chained navigations
if (this.isNavigateFromEntitySet && this.navigateBasePath) {
// Already have a base path from previous navigation - extend it with current relation
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
(entitySet as any).navigateBasePath = `${this.navigateBasePath}/${this.navigateRelation}`;
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
(entitySet as any).navigateBasePathEntityId =
`${this.navigateBasePathEntityId ?? this.navigateBasePath}/${this.navigateRelationEntityId ?? this.navigateRelation}`;
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
(entitySet as any).navigateSourceTableName = this.navigateSourceTableName;
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
(entitySet as any).navigateSourceTableEntityId = this.navigateSourceTableEntityId;
} else if (this.isNavigateFromEntitySet && this.navigateRelation) {
// First chained navigation - create base path from source/relation
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
(entitySet as any).navigateBasePath = `${this.navigateSourceTableName}/${this.navigateRelation}`;
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
(entitySet as any).navigateBasePathEntityId =
`${this.navigateSourceTableEntityId ?? this.navigateSourceTableName}/${this.navigateRelationEntityId ?? this.navigateRelation}`;
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
(entitySet as any).navigateSourceTableName = this.navigateSourceTableName;
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
(entitySet as any).navigateSourceTableEntityId = this.navigateSourceTableEntityId;
} else {
// Initial navigation - source is just the table name (resolved to entity ID if needed)
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
(entitySet as any).navigateSourceTableName = sourceTableName;
// biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
(entitySet as any).navigateSourceTableEntityId = sourceTableEntityId;
}
return entitySet;
}
}