metal-orm 1.0.57 → 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.
@@ -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
- }
481
-
482
- /**
483
- * Convenience alias for selecting specific columns from a relation.
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
+ }
491
+
492
+ /**
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
@@ -1,111 +1,128 @@
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
-
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
+
46
46
  type RelationResult<T extends RelationDef> =
47
47
  T extends HasManyRelation<infer TTarget> ? InferRow<TTarget>[] :
48
48
  T extends HasOneRelation<infer TTarget> ? InferRow<TTarget> | null :
49
49
  T extends BelongsToRelation<infer TTarget> ? InferRow<TTarget> | null :
50
- T extends BelongsToManyRelation<infer TTarget> ? (InferRow<TTarget> & { _pivot?: unknown })[] :
50
+ T extends BelongsToManyRelation<infer TTarget> ? (InferRow<TTarget> & { _pivot?: Record<string, unknown> })[] :
51
51
  never;
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
- };
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;
59
71
 
60
72
  export interface HasManyCollection<TChild> {
73
+ length: number;
74
+ [Symbol.iterator](): Iterator<TChild>;
61
75
  load(): Promise<TChild[]>;
62
76
  getItems(): TChild[];
63
77
  add(data: Partial<TChild>): TChild;
64
78
  attach(entity: TChild): void;
65
79
  remove(entity: TChild): void;
66
- clear(): void;
67
- }
68
-
69
- export interface BelongsToReference<TParent> {
70
- load(): Promise<TParent | null>;
71
- get(): TParent | null;
72
- set(data: Partial<TParent> | TParent | null): TParent | null;
73
- }
74
-
75
- export interface HasOneReference<TChild> {
76
- load(): Promise<TChild | null>;
77
- get(): TChild | null;
78
- set(data: Partial<TChild> | TChild | null): TChild | null;
79
- }
80
-
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
+
81
99
  export interface ManyToManyCollection<TTarget> {
100
+ length: number;
101
+ [Symbol.iterator](): Iterator<TTarget>;
82
102
  load(): Promise<TTarget[]>;
83
103
  getItems(): TTarget[];
84
104
  attach(target: TTarget | number | string): void;
85
105
  detach(target: TTarget | number | string): void;
86
106
  syncByIds(ids: (number | string)[]): Promise<void>;
87
- }
88
-
89
- export type EntityInstance<
90
- TTable extends TableDef,
91
- TRow = InferRow<TTable>
92
- > = TRow & {
93
- [K in keyof RelationMap<TTable>]:
94
- TTable['relations'][K] extends HasManyRelation<infer TTarget>
95
- ? HasManyCollection<EntityInstance<TTarget>>
96
- : TTable['relations'][K] extends HasOneRelation<infer TTarget>
97
- ? HasOneReference<EntityInstance<TTarget>>
98
- : TTable['relations'][K] extends BelongsToManyRelation<infer TTarget>
99
- ? ManyToManyCollection<EntityInstance<TTarget>>
100
- : TTable['relations'][K] extends BelongsToRelation<infer TTarget>
101
- ? BelongsToReference<EntityInstance<TTarget>>
102
- : never;
103
- } & {
104
- $load<K extends keyof RelationMap<TTable>>(relation: K): Promise<RelationMap<TTable>[K]>;
105
- };
106
-
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
+
107
118
  export type Primitive = string | number | boolean | Date | bigint | Buffer | null | undefined;
108
119
 
120
+ type IsAny<T> = 0 extends (1 & T) ? true : false;
121
+
109
122
  export type SelectableKeys<T> = {
110
- [K in keyof T]-?: NonNullable<T[K]> extends Primitive ? K : never
123
+ [K in keyof T]-?: IsAny<T[K]> extends true
124
+ ? never
125
+ : NonNullable<T[K]> extends Primitive
126
+ ? K
127
+ : never
111
128
  }[keyof T];