metal-orm 1.0.56 → 1.0.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +41 -33
  2. package/dist/index.cjs +1461 -195
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +541 -114
  5. package/dist/index.d.ts +541 -114
  6. package/dist/index.js +1424 -195
  7. package/dist/index.js.map +1 -1
  8. package/package.json +69 -69
  9. package/src/codegen/naming-strategy.ts +3 -1
  10. package/src/codegen/typescript.ts +20 -10
  11. package/src/core/ast/aggregate-functions.ts +14 -0
  12. package/src/core/ast/builders.ts +38 -20
  13. package/src/core/ast/expression-builders.ts +70 -2
  14. package/src/core/ast/expression-nodes.ts +305 -274
  15. package/src/core/ast/expression-visitor.ts +11 -1
  16. package/src/core/ast/expression.ts +4 -0
  17. package/src/core/ast/query.ts +3 -0
  18. package/src/core/ddl/introspect/catalogs/mysql.ts +5 -0
  19. package/src/core/ddl/introspect/catalogs/sqlite.ts +3 -0
  20. package/src/core/ddl/introspect/functions/mssql.ts +13 -0
  21. package/src/core/ddl/introspect/mssql.ts +4 -0
  22. package/src/core/ddl/introspect/mysql.ts +4 -0
  23. package/src/core/ddl/introspect/sqlite.ts +4 -0
  24. package/src/core/dialect/abstract.ts +552 -531
  25. package/src/core/dialect/base/function-table-formatter.ts +9 -30
  26. package/src/core/dialect/base/sql-dialect.ts +24 -0
  27. package/src/core/dialect/mssql/functions.ts +40 -2
  28. package/src/core/dialect/mysql/functions.ts +16 -2
  29. package/src/core/dialect/postgres/functions.ts +66 -2
  30. package/src/core/dialect/postgres/index.ts +17 -4
  31. package/src/core/dialect/postgres/table-functions.ts +27 -0
  32. package/src/core/dialect/sqlite/functions.ts +34 -0
  33. package/src/core/dialect/sqlite/index.ts +17 -1
  34. package/src/core/driver/database-driver.ts +9 -1
  35. package/src/core/driver/mssql-driver.ts +3 -0
  36. package/src/core/driver/mysql-driver.ts +3 -0
  37. package/src/core/driver/postgres-driver.ts +3 -0
  38. package/src/core/driver/sqlite-driver.ts +3 -0
  39. package/src/core/execution/executors/mssql-executor.ts +5 -0
  40. package/src/core/execution/executors/mysql-executor.ts +5 -0
  41. package/src/core/execution/executors/postgres-executor.ts +5 -0
  42. package/src/core/execution/executors/sqlite-executor.ts +5 -0
  43. package/src/core/functions/array.ts +26 -0
  44. package/src/core/functions/control-flow.ts +69 -0
  45. package/src/core/functions/datetime.ts +50 -0
  46. package/src/core/functions/definitions/aggregate.ts +16 -0
  47. package/src/core/functions/definitions/control-flow.ts +24 -0
  48. package/src/core/functions/definitions/datetime.ts +36 -0
  49. package/src/core/functions/definitions/helpers.ts +29 -0
  50. package/src/core/functions/definitions/json.ts +49 -0
  51. package/src/core/functions/definitions/numeric.ts +55 -0
  52. package/src/core/functions/definitions/string.ts +43 -0
  53. package/src/core/functions/function-registry.ts +48 -0
  54. package/src/core/functions/group-concat-helpers.ts +57 -0
  55. package/src/core/functions/json.ts +38 -0
  56. package/src/core/functions/numeric.ts +14 -0
  57. package/src/core/functions/standard-strategy.ts +86 -115
  58. package/src/core/functions/standard-table-strategy.ts +13 -0
  59. package/src/core/functions/table-types.ts +15 -0
  60. package/src/core/functions/text.ts +57 -0
  61. package/src/core/sql/sql.ts +59 -38
  62. package/src/decorators/bootstrap.ts +41 -4
  63. package/src/index.ts +18 -11
  64. package/src/orm/entity-meta.ts +6 -3
  65. package/src/orm/entity.ts +81 -14
  66. package/src/orm/execute.ts +87 -20
  67. package/src/orm/hydration-context.ts +10 -0
  68. package/src/orm/identity-map.ts +19 -0
  69. package/src/orm/interceptor-pipeline.ts +4 -0
  70. package/src/orm/lazy-batch.ts +237 -54
  71. package/src/orm/relations/belongs-to.ts +19 -2
  72. package/src/orm/relations/has-many.ts +23 -9
  73. package/src/orm/relations/has-one.ts +19 -2
  74. package/src/orm/relations/many-to-many.ts +59 -4
  75. package/src/orm/save-graph-types.ts +2 -2
  76. package/src/orm/save-graph.ts +18 -18
  77. package/src/query-builder/relation-conditions.ts +80 -59
  78. package/src/query-builder/relation-service.ts +399 -95
  79. package/src/query-builder/relation-types.ts +2 -2
  80. package/src/query-builder/select.ts +124 -106
  81. package/src/schema/table-guards.ts +6 -0
  82. package/src/schema/types.ts +109 -85
@@ -32,7 +32,7 @@ import { QueryAstService } from './query-ast-service.js';
32
32
  import { ColumnSelector } from './column-selector.js';
33
33
  import { RelationManager } from './relation-manager.js';
34
34
  import { RelationIncludeOptions } from './relation-types.js';
35
- import type { RelationDef } from '../schema/relation.js';
35
+ import { RelationKinds, type RelationDef } from '../schema/relation.js';
36
36
  import { JOIN_KINDS, JoinKind, ORDER_DIRECTIONS, OrderDirection } from '../core/sql/sql.js';
37
37
  import { EntityInstance, RelationMap, RelationTargetTable } from '../schema/types.js';
38
38
  import { OrmSession } from '../orm/orm-session.ts';
@@ -76,6 +76,7 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
76
76
  private readonly columnSelector: ColumnSelector;
77
77
  private readonly relationManager: RelationManager;
78
78
  private readonly lazyRelations: Set<string>;
79
+ private readonly lazyRelationOptions: Map<string, RelationIncludeOptions>;
79
80
 
80
81
  /**
81
82
  * Creates a new SelectQueryBuilder instance
@@ -89,7 +90,8 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
89
90
  state?: SelectQueryState,
90
91
  hydration?: HydrationManager,
91
92
  dependencies?: Partial<SelectQueryBuilderDependencies>,
92
- lazyRelations?: Set<string>
93
+ lazyRelations?: Set<string>,
94
+ lazyRelationOptions?: Map<string, RelationIncludeOptions>
93
95
  ) {
94
96
  const deps = resolveSelectQueryBuilderDependencies(dependencies);
95
97
  this.env = { table, deps };
@@ -100,6 +102,7 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
100
102
  hydration: initialHydration
101
103
  };
102
104
  this.lazyRelations = new Set(lazyRelations ?? []);
105
+ this.lazyRelationOptions = new Map(lazyRelationOptions ?? []);
103
106
  this.columnSelector = deps.createColumnSelector(this.env);
104
107
  this.relationManager = deps.createRelationManager(this.env);
105
108
  }
@@ -112,9 +115,17 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
112
115
  */
113
116
  private clone(
114
117
  context: SelectQueryBuilderContext = this.context,
115
- lazyRelations = new Set(this.lazyRelations)
118
+ lazyRelations = new Set(this.lazyRelations),
119
+ lazyRelationOptions = new Map(this.lazyRelationOptions)
116
120
  ): SelectQueryBuilder<T, TTable> {
117
- return new SelectQueryBuilder(this.env.table as TTable, context.state, context.hydration, this.env.deps, lazyRelations);
121
+ return new SelectQueryBuilder(
122
+ this.env.table as TTable,
123
+ context.state,
124
+ context.hydration,
125
+ this.env.deps,
126
+ lazyRelations,
127
+ lazyRelationOptions
128
+ );
118
129
  }
119
130
 
120
131
  /**
@@ -445,42 +456,41 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
445
456
  /**
446
457
  * Includes a relation lazily in the query results
447
458
  * @param relationName - Name of the relation to include lazily
459
+ * @param options - Optional include options for lazy loading
448
460
  * @returns New query builder instance with lazy relation inclusion
449
461
  */
450
- includeLazy<K extends keyof RelationMap<TTable>>(relationName: K): SelectQueryBuilder<T, TTable> {
451
- const nextLazy = new Set(this.lazyRelations);
452
- nextLazy.add(relationName as string);
453
- return this.clone(this.context, nextLazy);
454
- }
455
-
456
- /**
457
- * Selects columns for a related table in a single hop.
458
- */
459
- selectRelationColumns<
460
- K extends keyof TTable['relations'] & string,
461
- TRel extends RelationDef = TTable['relations'][K],
462
- TTarget extends TableDef = RelationTargetTable<TRel>,
463
- C extends keyof TTarget['columns'] & string = keyof TTarget['columns'] & string
464
- >(relationName: K, ...cols: C[]): SelectQueryBuilder<T, TTable> {
465
- const relation = this.env.table.relations[relationName] as RelationDef | undefined;
466
- if (!relation) {
467
- throw new Error(`Relation '${relationName}' not found on table '${this.env.table.name}'`);
468
- }
469
- const target = relation.target;
470
-
471
- for (const col of cols) {
472
- if (!target.columns[col]) {
473
- throw new Error(
474
- `Column '${col}' not found on related table '${target.name}' for relation '${relationName}'`
475
- );
476
- }
477
- }
478
-
479
- return this.include(relationName as string, { columns: cols as string[] });
480
- }
462
+ includeLazy<K extends keyof RelationMap<TTable>>(
463
+ relationName: K,
464
+ options?: RelationIncludeOptions
465
+ ): SelectQueryBuilder<T, TTable> {
466
+ let nextContext = this.context;
467
+ const relation = this.env.table.relations[relationName as string];
468
+ if (relation?.type === RelationKinds.BelongsTo) {
469
+ const foreignKey = relation.foreignKey;
470
+ const fkColumn = this.env.table.columns[foreignKey];
471
+ if (fkColumn) {
472
+ const hasAlias = nextContext.state.ast.columns.some(col => {
473
+ const node = col as { alias?: string; name?: string };
474
+ return (node.alias ?? node.name) === foreignKey;
475
+ });
476
+ if (!hasAlias) {
477
+ nextContext = this.columnSelector.select(nextContext, { [foreignKey]: fkColumn });
478
+ }
479
+ }
480
+ }
481
+ const nextLazy = new Set(this.lazyRelations);
482
+ nextLazy.add(relationName as string);
483
+ const nextOptions = new Map(this.lazyRelationOptions);
484
+ if (options) {
485
+ nextOptions.set(relationName as string, options);
486
+ } else {
487
+ nextOptions.delete(relationName as string);
488
+ }
489
+ return this.clone(nextContext, nextLazy, nextOptions);
490
+ }
481
491
 
482
492
  /**
483
- * Convenience alias for selecting specific columns from a relation.
493
+ * Convenience alias for including only specific columns from a relation.
484
494
  */
485
495
  includePick<
486
496
  K extends keyof TTable['relations'] & string,
@@ -488,7 +498,7 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
488
498
  TTarget extends TableDef = RelationTargetTable<TRel>,
489
499
  C extends keyof TTarget['columns'] & string = keyof TTarget['columns'] & string
490
500
  >(relationName: K, cols: C[]): SelectQueryBuilder<T, TTable> {
491
- return this.selectRelationColumns(relationName, ...cols);
501
+ return this.include(relationName, { columns: cols as readonly string[] });
492
502
  }
493
503
 
494
504
 
@@ -505,7 +515,7 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
505
515
  if (entry.type === 'root') {
506
516
  currBuilder = currBuilder.select(...entry.columns);
507
517
  } else {
508
- currBuilder = currBuilder.selectRelationColumns(entry.relationName, ...(entry.columns as string[]));
518
+ currBuilder = currBuilder.include(entry.relationName, { columns: entry.columns as string[] });
509
519
  }
510
520
  }
511
521
 
@@ -520,6 +530,14 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
520
530
  return Array.from(this.lazyRelations) as (keyof RelationMap<TTable>)[];
521
531
  }
522
532
 
533
+ /**
534
+ * Gets lazy relation include options
535
+ * @returns Map of relation names to include options
536
+ */
537
+ getLazyRelationOptions(): Map<string, RelationIncludeOptions> {
538
+ return new Map(this.lazyRelationOptions);
539
+ }
540
+
523
541
  /**
524
542
  * Gets the table definition for this query builder
525
543
  * @returns Table definition
@@ -533,74 +551,74 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
533
551
  * @param ctx - ORM session context
534
552
  * @returns Promise of entity instances
535
553
  */
536
- async execute(ctx: OrmSession): Promise<EntityInstance<TTable>[]> {
537
- return executeHydrated(ctx, this);
538
- }
539
-
540
- private withAst(ast: SelectQueryNode): SelectQueryBuilder<T, TTable> {
541
- const nextState = new SelectQueryState(this.env.table as TTable, ast);
542
- const nextContext: SelectQueryBuilderContext = {
543
- ...this.context,
544
- state: nextState
545
- };
546
- return this.clone(nextContext);
547
- }
548
-
549
- async count(session: OrmSession): Promise<number> {
550
- const unpagedAst: SelectQueryNode = {
551
- ...this.context.state.ast,
552
- orderBy: undefined,
553
- limit: undefined,
554
- offset: undefined
555
- };
556
-
557
- const subAst = this.withAst(unpagedAst).getAST();
558
-
559
- const countQuery: SelectQueryNode = {
560
- type: 'SelectQuery',
561
- from: derivedTable(subAst, '__metal_count'),
562
- columns: [{ type: 'Function', name: 'COUNT', args: [], alias: 'total' } as FunctionNode],
563
- joins: []
564
- };
565
-
566
- const execCtx = session.getExecutionContext();
567
- const compiled = execCtx.dialect.compileSelect(countQuery);
568
- const results = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
569
- const value = results[0]?.values?.[0]?.[0];
570
-
571
- if (typeof value === 'number') return value;
572
- if (typeof value === 'bigint') return Number(value);
573
- if (typeof value === 'string') return Number(value);
574
- return value === null || value === undefined ? 0 : Number(value);
575
- }
576
-
577
- async executePaged(
578
- session: OrmSession,
579
- options: { page: number; pageSize: number }
580
- ): Promise<{ items: EntityInstance<TTable>[]; totalItems: number }> {
581
- const { page, pageSize } = options;
582
- if (!Number.isInteger(page) || page < 1) {
583
- throw new Error('executePaged: page must be an integer >= 1');
584
- }
585
- if (!Number.isInteger(pageSize) || pageSize < 1) {
586
- throw new Error('executePaged: pageSize must be an integer >= 1');
587
- }
588
-
589
- const offset = (page - 1) * pageSize;
590
- const [items, totalItems] = await Promise.all([
591
- this.limit(pageSize).offset(offset).execute(session),
592
- this.count(session)
593
- ]);
594
-
595
- return { items, totalItems };
596
- }
597
-
598
- /**
599
- * Executes the query with provided execution and hydration contexts
600
- * @param execCtx - Execution context
601
- * @param hydCtx - Hydration context
602
- * @returns Promise of entity instances
603
- */
554
+ async execute(ctx: OrmSession): Promise<EntityInstance<TTable>[]> {
555
+ return executeHydrated(ctx, this);
556
+ }
557
+
558
+ private withAst(ast: SelectQueryNode): SelectQueryBuilder<T, TTable> {
559
+ const nextState = new SelectQueryState(this.env.table as TTable, ast);
560
+ const nextContext: SelectQueryBuilderContext = {
561
+ ...this.context,
562
+ state: nextState
563
+ };
564
+ return this.clone(nextContext);
565
+ }
566
+
567
+ async count(session: OrmSession): Promise<number> {
568
+ const unpagedAst: SelectQueryNode = {
569
+ ...this.context.state.ast,
570
+ orderBy: undefined,
571
+ limit: undefined,
572
+ offset: undefined
573
+ };
574
+
575
+ const subAst = this.withAst(unpagedAst).getAST();
576
+
577
+ const countQuery: SelectQueryNode = {
578
+ type: 'SelectQuery',
579
+ from: derivedTable(subAst, '__metal_count'),
580
+ columns: [{ type: 'Function', name: 'COUNT', args: [], alias: 'total' } as FunctionNode],
581
+ joins: []
582
+ };
583
+
584
+ const execCtx = session.getExecutionContext();
585
+ const compiled = execCtx.dialect.compileSelect(countQuery);
586
+ const results = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
587
+ const value = results[0]?.values?.[0]?.[0];
588
+
589
+ if (typeof value === 'number') return value;
590
+ if (typeof value === 'bigint') return Number(value);
591
+ if (typeof value === 'string') return Number(value);
592
+ return value === null || value === undefined ? 0 : Number(value);
593
+ }
594
+
595
+ async executePaged(
596
+ session: OrmSession,
597
+ options: { page: number; pageSize: number }
598
+ ): Promise<{ items: EntityInstance<TTable>[]; totalItems: number }> {
599
+ const { page, pageSize } = options;
600
+ if (!Number.isInteger(page) || page < 1) {
601
+ throw new Error('executePaged: page must be an integer >= 1');
602
+ }
603
+ if (!Number.isInteger(pageSize) || pageSize < 1) {
604
+ throw new Error('executePaged: pageSize must be an integer >= 1');
605
+ }
606
+
607
+ const offset = (page - 1) * pageSize;
608
+ const [items, totalItems] = await Promise.all([
609
+ this.limit(pageSize).offset(offset).execute(session),
610
+ this.count(session)
611
+ ]);
612
+
613
+ return { items, totalItems };
614
+ }
615
+
616
+ /**
617
+ * Executes the query with provided execution and hydration contexts
618
+ * @param execCtx - Execution context
619
+ * @param hydCtx - Hydration context
620
+ * @returns Promise of entity instances
621
+ */
604
622
  async executeWithContexts(execCtx: ExecutionContext, hydCtx: HydrationContext): Promise<EntityInstance<TTable>[]> {
605
623
  return executeHydratedWithContexts(execCtx, hydCtx, this);
606
624
  }
@@ -9,6 +9,12 @@ const isRelationsRecord = (relations: unknown): relations is Record<string, unkn
9
9
  return typeof relations === 'object' && relations !== null;
10
10
  };
11
11
 
12
+ /**
13
+ * Type guard that checks if a value is a TableDef.
14
+ *
15
+ * @param value The value to check.
16
+ * @returns True if the value follows the TableDef structure.
17
+ */
12
18
  export const isTableDef = (value: unknown): value is TableDef => {
13
19
  if (typeof value !== 'object' || value === null) {
14
20
  return false;
@@ -1,104 +1,128 @@
1
- import { ColumnDef } from './column-types.js';
2
- import { TableDef } from './table.js';
3
- import {
4
- RelationDef,
5
- HasManyRelation,
6
- HasOneRelation,
7
- BelongsToRelation,
8
- BelongsToManyRelation
9
- } from './relation.js';
10
-
11
- /**
12
- * Resolves a relation definition to its target table type.
13
- */
14
- export type RelationTargetTable<TRel extends RelationDef> =
15
- TRel extends HasManyRelation<infer TTarget> ? TTarget :
16
- TRel extends HasOneRelation<infer TTarget> ? TTarget :
17
- TRel extends BelongsToRelation<infer TTarget> ? TTarget :
18
- TRel extends BelongsToManyRelation<infer TTarget> ? TTarget :
19
- never;
20
-
21
- type NormalizedColumnType<T extends ColumnDef> = Lowercase<T['type'] & string>;
22
-
23
- /**
24
- * Maps a ColumnDef to its TypeScript type representation
25
- */
26
- export type ColumnToTs<T extends ColumnDef> =
27
- [unknown] extends [T['tsType']]
28
- ? NormalizedColumnType<T> extends 'int' | 'integer' ? number :
29
- NormalizedColumnType<T> extends 'bigint' ? number | bigint :
30
- NormalizedColumnType<T> extends 'decimal' | 'float' | 'double' ? number :
31
- NormalizedColumnType<T> extends 'boolean' ? boolean :
32
- NormalizedColumnType<T> extends 'json' ? unknown :
33
- NormalizedColumnType<T> extends 'blob' | 'binary' | 'varbinary' | 'bytea' ? Buffer :
34
- NormalizedColumnType<T> extends 'date' | 'datetime' | 'timestamp' | 'timestamptz' ? string :
35
- string
36
- : Exclude<T['tsType'], undefined>;
37
-
38
- /**
39
- * Infers a row shape from a table definition
40
- */
41
- export type InferRow<TTable extends TableDef> = {
42
- [K in keyof TTable['columns']]: ColumnToTs<TTable['columns'][K]>;
43
- };
44
-
1
+ import type { ColumnDef } from './column-types.js';
2
+ export type { ColumnDef };
3
+ import { TableDef } from './table.js';
4
+ import {
5
+ RelationDef,
6
+ HasManyRelation,
7
+ HasOneRelation,
8
+ BelongsToRelation,
9
+ BelongsToManyRelation
10
+ } from './relation.js';
11
+
12
+ /**
13
+ * Resolves a relation definition to its target table type.
14
+ */
15
+ export type RelationTargetTable<TRel extends RelationDef> =
16
+ TRel extends HasManyRelation<infer TTarget> ? TTarget :
17
+ TRel extends HasOneRelation<infer TTarget> ? TTarget :
18
+ TRel extends BelongsToRelation<infer TTarget> ? TTarget :
19
+ TRel extends BelongsToManyRelation<infer TTarget> ? TTarget :
20
+ never;
21
+
22
+ type NormalizedColumnType<T extends ColumnDef> = Lowercase<T['type'] & string>;
23
+
24
+ /**
25
+ * Maps a ColumnDef to its TypeScript type representation
26
+ */
27
+ export type ColumnToTs<T extends ColumnDef> =
28
+ [unknown] extends [T['tsType']]
29
+ ? NormalizedColumnType<T> extends 'int' | 'integer' ? number :
30
+ NormalizedColumnType<T> extends 'bigint' ? number | bigint :
31
+ NormalizedColumnType<T> extends 'decimal' | 'float' | 'double' ? number :
32
+ NormalizedColumnType<T> extends 'boolean' ? boolean :
33
+ NormalizedColumnType<T> extends 'json' ? unknown :
34
+ NormalizedColumnType<T> extends 'blob' | 'binary' | 'varbinary' | 'bytea' ? Buffer :
35
+ NormalizedColumnType<T> extends 'date' | 'datetime' | 'timestamp' | 'timestamptz' ? string :
36
+ string
37
+ : Exclude<T['tsType'], undefined>;
38
+
39
+ /**
40
+ * Infers a row shape from a table definition
41
+ */
42
+ export type InferRow<TTable extends TableDef> = {
43
+ [K in keyof TTable['columns']]: ColumnToTs<TTable['columns'][K]>;
44
+ };
45
+
45
46
  type RelationResult<T extends RelationDef> =
46
47
  T extends HasManyRelation<infer TTarget> ? InferRow<TTarget>[] :
47
48
  T extends HasOneRelation<infer TTarget> ? InferRow<TTarget> | null :
48
49
  T extends BelongsToRelation<infer TTarget> ? InferRow<TTarget> | null :
49
- T extends BelongsToManyRelation<infer TTarget> ? (InferRow<TTarget> & { _pivot?: unknown })[] :
50
+ T extends BelongsToManyRelation<infer TTarget> ? (InferRow<TTarget> & { _pivot?: Record<string, unknown> })[] :
50
51
  never;
51
-
52
- /**
53
- * Maps relation names to the expected row results
54
- */
55
- export type RelationMap<TTable extends TableDef> = {
56
- [K in keyof TTable['relations']]: RelationResult<TTable['relations'][K]>;
57
- };
52
+
53
+ /**
54
+ * Maps relation names to the expected row results
55
+ */
56
+ export type RelationMap<TTable extends TableDef> = {
57
+ [K in keyof TTable['relations']]: RelationResult<TTable['relations'][K]>;
58
+ };
59
+
60
+ type RelationWrapper<TRel extends RelationDef> =
61
+ TRel extends HasManyRelation<infer TTarget>
62
+ ? HasManyCollection<EntityInstance<TTarget>> & ReadonlyArray<EntityInstance<TTarget>>
63
+ : TRel extends HasOneRelation<infer TTarget>
64
+ ? HasOneReference<EntityInstance<TTarget>>
65
+ : TRel extends BelongsToManyRelation<infer TTarget>
66
+ ? ManyToManyCollection<EntityInstance<TTarget> & { _pivot?: Record<string, unknown> }>
67
+ & ReadonlyArray<EntityInstance<TTarget> & { _pivot?: Record<string, unknown> }>
68
+ : TRel extends BelongsToRelation<infer TTarget>
69
+ ? BelongsToReference<EntityInstance<TTarget>>
70
+ : never;
58
71
 
59
72
  export interface HasManyCollection<TChild> {
73
+ length: number;
74
+ [Symbol.iterator](): Iterator<TChild>;
60
75
  load(): Promise<TChild[]>;
61
76
  getItems(): TChild[];
62
77
  add(data: Partial<TChild>): TChild;
63
78
  attach(entity: TChild): void;
64
79
  remove(entity: TChild): void;
65
- clear(): void;
66
- }
67
-
68
- export interface BelongsToReference<TParent> {
69
- load(): Promise<TParent | null>;
70
- get(): TParent | null;
71
- set(data: Partial<TParent> | TParent | null): TParent | null;
72
- }
73
-
74
- export interface HasOneReference<TChild> {
75
- load(): Promise<TChild | null>;
76
- get(): TChild | null;
77
- set(data: Partial<TChild> | TChild | null): TChild | null;
78
- }
79
-
80
+ clear(): void;
81
+ }
82
+
83
+ export interface BelongsToReferenceApi<TParent extends object = object> {
84
+ load(): Promise<TParent | null>;
85
+ get(): TParent | null;
86
+ set(data: Partial<TParent> | TParent | null): TParent | null;
87
+ }
88
+
89
+ export type BelongsToReference<TParent extends object = object> = BelongsToReferenceApi<TParent> & Partial<TParent>;
90
+
91
+ export interface HasOneReferenceApi<TChild extends object = object> {
92
+ load(): Promise<TChild | null>;
93
+ get(): TChild | null;
94
+ set(data: Partial<TChild> | TChild | null): TChild | null;
95
+ }
96
+
97
+ export type HasOneReference<TChild extends object = object> = HasOneReferenceApi<TChild> & Partial<TChild>;
98
+
80
99
  export interface ManyToManyCollection<TTarget> {
100
+ length: number;
101
+ [Symbol.iterator](): Iterator<TTarget>;
81
102
  load(): Promise<TTarget[]>;
82
103
  getItems(): TTarget[];
83
104
  attach(target: TTarget | number | string): void;
84
105
  detach(target: TTarget | number | string): void;
85
106
  syncByIds(ids: (number | string)[]): Promise<void>;
86
- }
107
+ }
108
+
109
+ export type EntityInstance<
110
+ TTable extends TableDef,
111
+ TRow = InferRow<TTable>
112
+ > = TRow & {
113
+ [K in keyof RelationMap<TTable>]: RelationWrapper<TTable['relations'][K]>;
114
+ } & {
115
+ $load<K extends keyof RelationMap<TTable>>(relation: K): Promise<RelationMap<TTable>[K]>;
116
+ };
117
+
118
+ export type Primitive = string | number | boolean | Date | bigint | Buffer | null | undefined;
119
+
120
+ type IsAny<T> = 0 extends (1 & T) ? true : false;
87
121
 
88
- export type EntityInstance<
89
- TTable extends TableDef,
90
- TRow = InferRow<TTable>
91
- > = TRow & {
92
- [K in keyof RelationMap<TTable>]:
93
- TTable['relations'][K] extends HasManyRelation<infer TTarget>
94
- ? HasManyCollection<EntityInstance<TTarget>>
95
- : TTable['relations'][K] extends HasOneRelation<infer TTarget>
96
- ? HasOneReference<EntityInstance<TTarget>>
97
- : TTable['relations'][K] extends BelongsToManyRelation<infer TTarget>
98
- ? ManyToManyCollection<EntityInstance<TTarget>>
99
- : TTable['relations'][K] extends BelongsToRelation<infer TTarget>
100
- ? BelongsToReference<EntityInstance<TTarget>>
101
- : never;
102
- } & {
103
- $load<K extends keyof RelationMap<TTable>>(relation: K): Promise<RelationMap<TTable>[K]>;
104
- };
122
+ export type SelectableKeys<T> = {
123
+ [K in keyof T]-?: IsAny<T[K]> extends true
124
+ ? never
125
+ : NonNullable<T[K]> extends Primitive
126
+ ? K
127
+ : never
128
+ }[keyof T];