metal-orm 1.0.55 → 1.0.57

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 (72) hide show
  1. package/README.md +21 -20
  2. package/dist/index.cjs +831 -113
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +524 -71
  5. package/dist/index.d.ts +524 -71
  6. package/dist/index.js +794 -113
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  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 +5 -4
  63. package/src/index.ts +18 -11
  64. package/src/orm/hydration-context.ts +10 -0
  65. package/src/orm/identity-map.ts +19 -0
  66. package/src/orm/interceptor-pipeline.ts +4 -0
  67. package/src/orm/relations/belongs-to.ts +17 -0
  68. package/src/orm/relations/has-one.ts +17 -0
  69. package/src/orm/relations/many-to-many.ts +41 -0
  70. package/src/query-builder/select.ts +68 -68
  71. package/src/schema/table-guards.ts +6 -0
  72. package/src/schema/types.ts +8 -1
@@ -21,10 +21,28 @@ const hideInternal = (obj: object, keys: string[]): void => {
21
21
  }
22
22
  };
23
23
 
24
+ /**
25
+ * Default implementation of a many-to-many collection.
26
+ * Manages the relationship between two entities through a pivot table.
27
+ * Supports lazy loading, attaching/detaching entities, and syncing by IDs.
28
+ *
29
+ * @template TTarget The type of the target entities in the collection.
30
+ */
24
31
  export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollection<TTarget> {
25
32
  private loaded = false;
26
33
  private items: TTarget[] = [];
27
34
 
35
+ /**
36
+ * @param ctx The entity context for tracking changes.
37
+ * @param meta Metadata for the root entity.
38
+ * @param root The root entity instance.
39
+ * @param relationName The name of the relation.
40
+ * @param relation Relation definition.
41
+ * @param rootTable Table definition of the root entity.
42
+ * @param loader Function to load the collection items.
43
+ * @param createEntity Function to create entity instances from rows.
44
+ * @param localKey The local key used for joining.
45
+ */
28
46
  constructor(
29
47
  private readonly ctx: EntityContext,
30
48
  private readonly meta: EntityMeta<TableDef>,
@@ -40,6 +58,10 @@ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollectio
40
58
  this.hydrateFromCache();
41
59
  }
42
60
 
61
+ /**
62
+ * Loads the collection items if not already loaded.
63
+ * @returns A promise that resolves to the array of target entities.
64
+ */
43
65
  async load(): Promise<TTarget[]> {
44
66
  if (this.loaded) return this.items;
45
67
  const map = await this.loader();
@@ -56,10 +78,19 @@ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollectio
56
78
  return this.items;
57
79
  }
58
80
 
81
+ /**
82
+ * Returns the currently loaded items.
83
+ * @returns Array of target entities.
84
+ */
59
85
  getItems(): TTarget[] {
60
86
  return this.items;
61
87
  }
62
88
 
89
+ /**
90
+ * Attaches an entity to the collection.
91
+ * Registers an 'attach' change in the entity context.
92
+ * @param target Entity instance or its primary key value.
93
+ */
63
94
  attach(target: TTarget | number | string): void {
64
95
  const entity = this.ensureEntity(target);
65
96
  const id = this.extractId(entity);
@@ -80,6 +111,11 @@ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollectio
80
111
  );
81
112
  }
82
113
 
114
+ /**
115
+ * Detaches an entity from the collection.
116
+ * Registers a 'detach' change in the entity context.
117
+ * @param target Entity instance or its primary key value.
118
+ */
83
119
  detach(target: TTarget | number | string): void {
84
120
  const id = typeof target === 'number' || typeof target === 'string'
85
121
  ? target
@@ -101,6 +137,11 @@ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollectio
101
137
  );
102
138
  }
103
139
 
140
+ /**
141
+ * Syncs the collection with a list of IDs.
142
+ * Attaches missing IDs and detaches IDs not in the list.
143
+ * @param ids Array of primary key values to sync with.
144
+ */
104
145
  async syncByIds(ids: (number | string)[]): Promise<void> {
105
146
  await this.load();
106
147
  const normalized = new Set(ids.map(id => toKey(id)));
@@ -533,74 +533,74 @@ export class SelectQueryBuilder<T = unknown, TTable extends TableDef = TableDef>
533
533
  * @param ctx - ORM session context
534
534
  * @returns Promise of entity instances
535
535
  */
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
- */
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
+ */
604
604
  async executeWithContexts(execCtx: ExecutionContext, hydCtx: HydrationContext): Promise<EntityInstance<TTable>[]> {
605
605
  return executeHydratedWithContexts(execCtx, hydCtx, this);
606
606
  }
@@ -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,4 +1,5 @@
1
- import { ColumnDef } from './column-types.js';
1
+ import type { ColumnDef } from './column-types.js';
2
+ export type { ColumnDef };
2
3
  import { TableDef } from './table.js';
3
4
  import {
4
5
  RelationDef,
@@ -102,3 +103,9 @@ export type EntityInstance<
102
103
  } & {
103
104
  $load<K extends keyof RelationMap<TTable>>(relation: K): Promise<RelationMap<TTable>[K]>;
104
105
  };
106
+
107
+ export type Primitive = string | number | boolean | Date | bigint | Buffer | null | undefined;
108
+
109
+ export type SelectableKeys<T> = {
110
+ [K in keyof T]-?: NonNullable<T[K]> extends Primitive ? K : never
111
+ }[keyof T];