metal-orm 1.0.16 → 1.0.17

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 (45) hide show
  1. package/README.md +33 -37
  2. package/dist/decorators/index.cjs +152 -23
  3. package/dist/decorators/index.cjs.map +1 -1
  4. package/dist/decorators/index.d.cts +1 -1
  5. package/dist/decorators/index.d.ts +1 -1
  6. package/dist/decorators/index.js +152 -23
  7. package/dist/decorators/index.js.map +1 -1
  8. package/dist/index.cjs +322 -115
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +53 -4
  11. package/dist/index.d.ts +53 -4
  12. package/dist/index.js +316 -115
  13. package/dist/index.js.map +1 -1
  14. package/dist/{select-BKZrMRCQ.d.cts → select-BPCn6MOH.d.cts} +183 -64
  15. package/dist/{select-BKZrMRCQ.d.ts → select-BPCn6MOH.d.ts} +183 -64
  16. package/package.json +2 -1
  17. package/src/core/ast/aggregate-functions.ts +50 -4
  18. package/src/core/ast/expression-builders.ts +22 -15
  19. package/src/core/ast/expression-nodes.ts +6 -0
  20. package/src/core/ddl/introspect/functions/postgres.ts +2 -6
  21. package/src/core/dialect/abstract.ts +12 -8
  22. package/src/core/dialect/mssql/functions.ts +24 -15
  23. package/src/core/dialect/postgres/functions.ts +33 -24
  24. package/src/core/dialect/sqlite/functions.ts +19 -12
  25. package/src/core/functions/datetime.ts +2 -1
  26. package/src/core/functions/numeric.ts +2 -1
  27. package/src/core/functions/standard-strategy.ts +52 -12
  28. package/src/core/functions/text.ts +2 -1
  29. package/src/core/functions/types.ts +8 -8
  30. package/src/index.ts +5 -4
  31. package/src/orm/domain-event-bus.ts +43 -25
  32. package/src/orm/entity-meta.ts +40 -0
  33. package/src/orm/execution-context.ts +6 -0
  34. package/src/orm/hydration-context.ts +6 -4
  35. package/src/orm/orm-session.ts +35 -24
  36. package/src/orm/orm.ts +10 -10
  37. package/src/orm/query-logger.ts +15 -0
  38. package/src/orm/runtime-types.ts +60 -2
  39. package/src/orm/transaction-runner.ts +7 -0
  40. package/src/orm/unit-of-work.ts +1 -0
  41. package/src/query-builder/insert-query-state.ts +13 -3
  42. package/src/query-builder/select-helpers.ts +50 -0
  43. package/src/query-builder/select.ts +122 -30
  44. package/src/query-builder/update-query-state.ts +31 -9
  45. package/src/schema/types.ts +16 -6
@@ -1,12 +1,27 @@
1
1
  import type { DbExecutor } from '../core/execution/db-executor.js';
2
2
 
3
+ /**
4
+ * Represents a single SQL query log entry
5
+ */
3
6
  export interface QueryLogEntry {
7
+ /** The SQL query that was executed */
4
8
  sql: string;
9
+ /** Parameters used in the query */
5
10
  params?: unknown[];
6
11
  }
7
12
 
13
+ /**
14
+ * Function type for query logging callbacks
15
+ * @param entry - The query log entry to process
16
+ */
8
17
  export type QueryLogger = (entry: QueryLogEntry) => void;
9
18
 
19
+ /**
20
+ * Creates a wrapped database executor that logs all SQL queries
21
+ * @param executor - Original database executor to wrap
22
+ * @param logger - Optional logger function to receive query log entries
23
+ * @returns Wrapped executor that logs queries before execution
24
+ */
10
25
  export const createQueryLoggingExecutor = (
11
26
  executor: DbExecutor,
12
27
  logger?: QueryLogger
@@ -1,39 +1,97 @@
1
1
  import { RelationDef } from '../schema/relation.js';
2
2
  import { TableDef } from '../schema/table.js';
3
3
 
4
+ /**
5
+ * Entity status enum representing the lifecycle state of an entity
6
+ */
4
7
  export enum EntityStatus {
8
+ /** Entity is newly created and not yet persisted */
5
9
  New = 'new',
10
+ /** Entity is managed by the ORM and synchronized with the database */
6
11
  Managed = 'managed',
12
+ /** Entity has been modified but not yet persisted */
7
13
  Dirty = 'dirty',
14
+ /** Entity has been marked for removal */
8
15
  Removed = 'removed',
16
+ /** Entity is detached from the ORM context */
9
17
  Detached = 'detached'
10
18
  }
11
19
 
20
+ /**
21
+ * Represents an entity being tracked by the ORM
22
+ */
12
23
  export interface TrackedEntity {
24
+ /** The table definition this entity belongs to */
13
25
  table: TableDef;
26
+ /** The actual entity instance */
14
27
  entity: any;
28
+ /** Primary key value of the entity */
15
29
  pk: string | number | null;
30
+ /** Current status of the entity */
16
31
  status: EntityStatus;
32
+ /** Original values of the entity when it was loaded */
17
33
  original: Record<string, any> | null;
18
34
  }
19
35
 
36
+ /**
37
+ * Type representing a key for relation navigation
38
+ */
20
39
  export type RelationKey = string;
21
40
 
41
+ /**
42
+ * Represents a change operation on a relation
43
+ * @typeParam T - Type of the related entity
44
+ */
22
45
  export type RelationChange<T> =
23
46
  | { kind: 'add'; entity: T }
24
47
  | { kind: 'attach'; entity: T }
25
48
  | { kind: 'remove'; entity: T }
26
49
  | { kind: 'detach'; entity: T };
27
50
 
51
+ /**
52
+ * Represents a relation change entry in the unit of work
53
+ */
28
54
  export interface RelationChangeEntry {
55
+ /** Root entity that owns the relation */
29
56
  root: any;
57
+ /** Key of the relation being changed */
30
58
  relationKey: RelationKey;
59
+ /** Table definition of the root entity */
31
60
  rootTable: TableDef;
61
+ /** Name of the relation */
32
62
  relationName: string;
63
+ /** Relation definition */
33
64
  relation: RelationDef;
65
+ /** The change being applied */
34
66
  change: RelationChange<any>;
35
67
  }
36
68
 
37
- export interface HasDomainEvents {
38
- domainEvents?: any[];
69
+ /**
70
+ * Represents a domain event that can be emitted by entities
71
+ * @typeParam TType - Type of the event (string literal)
72
+ */
73
+ export interface DomainEvent<TType extends string = string> {
74
+ /** Type identifier for the event */
75
+ readonly type: TType;
76
+ /** Timestamp when the event occurred */
77
+ readonly occurredAt?: Date;
78
+ }
79
+
80
+ /**
81
+ * Type representing any domain event
82
+ */
83
+ export type AnyDomainEvent = DomainEvent<string>;
84
+
85
+ /**
86
+ * Type representing ORM-specific domain events
87
+ */
88
+ export type OrmDomainEvent = AnyDomainEvent;
89
+
90
+ /**
91
+ * Interface for entities that can emit domain events
92
+ * @typeParam E - Type of domain events this entity can emit
93
+ */
94
+ export interface HasDomainEvents<E extends DomainEvent = AnyDomainEvent> {
95
+ /** Array of domain events emitted by this entity */
96
+ domainEvents?: E[];
39
97
  }
@@ -1,5 +1,12 @@
1
1
  import type { DbExecutor } from '../core/execution/db-executor.js';
2
2
 
3
+ /**
4
+ * Executes a function within a database transaction
5
+ * @param executor - Database executor to use for transaction operations
6
+ * @param action - Function to execute within the transaction
7
+ * @returns Promise that resolves when the transaction is complete
8
+ * @throws Re-throws any errors that occur during the transaction (after rolling back)
9
+ */
3
10
  export const runInTransaction = async (executor: DbExecutor, action: () => Promise<void>): Promise<void> => {
4
11
  if (!executor.beginTransaction) {
5
12
  await action();
@@ -215,6 +215,7 @@ export class UnitOfWork {
215
215
  private extractColumns(table: TableDef, entity: any): Record<string, unknown> {
216
216
  const payload: Record<string, unknown> = {};
217
217
  for (const column of Object.keys(table.columns)) {
218
+ if (entity[column] === undefined) continue;
218
219
  payload[column] = entity[column];
219
220
  }
220
221
  return payload;
@@ -1,6 +1,6 @@
1
1
  import { TableDef } from '../schema/table.js';
2
2
  import { InsertQueryNode, TableNode } from '../core/ast/query.js';
3
- import { ColumnNode, OperandNode, valueToOperand } from '../core/ast/expression.js';
3
+ import { ColumnNode, OperandNode, isValueOperandInput, valueToOperand } from '../core/ast/expression.js';
4
4
  import { buildColumnNodes, createTableNode } from '../core/ast/builders.js';
5
5
 
6
6
  /**
@@ -31,8 +31,18 @@ export class InsertQueryState {
31
31
  ? this.ast.columns
32
32
  : buildColumnNodes(this.table, Object.keys(rows[0]));
33
33
 
34
- const newRows: OperandNode[][] = rows.map(row =>
35
- definedColumns.map(column => valueToOperand(row[column.name]))
34
+ const newRows: OperandNode[][] = rows.map((row, rowIndex) =>
35
+ definedColumns.map(column => {
36
+ const rawValue = row[column.name];
37
+
38
+ if (!isValueOperandInput(rawValue)) {
39
+ throw new Error(
40
+ `Invalid insert value for column "${column.name}" in row ${rowIndex}: only primitives, null, or OperandNodes are allowed`
41
+ );
42
+ }
43
+
44
+ return valueToOperand(rawValue);
45
+ })
36
46
  );
37
47
 
38
48
  return this.clone({
@@ -0,0 +1,50 @@
1
+ import type { TableDef } from '../schema/table.js';
2
+ import type { ColumnDef } from '../schema/column.js';
3
+ import { getTableDefFromEntity } from '../decorators/bootstrap.js';
4
+
5
+ /**
6
+ * Build a typed selection map from a TableDef.
7
+ */
8
+ export function sel<
9
+ TTable extends TableDef,
10
+ K extends keyof TTable['columns'] & string
11
+ >(table: TTable, ...cols: K[]): Record<K, TTable['columns'][K]> {
12
+ const selection = {} as Record<K, TTable['columns'][K]>;
13
+
14
+ for (const col of cols) {
15
+ const def = table.columns[col] as TTable['columns'][K];
16
+ if (!def) {
17
+ throw new Error(`Column '${col}' not found on table '${table.name}'`);
18
+ }
19
+ selection[col] = def;
20
+ }
21
+
22
+ return selection;
23
+ }
24
+
25
+ type Ctor<T> = { new (...args: any[]): T };
26
+
27
+ /**
28
+ * Build a typed selection map from an entity constructor.
29
+ */
30
+ export function esel<TEntity, K extends keyof TEntity & string>(
31
+ entity: Ctor<TEntity>,
32
+ ...props: K[]
33
+ ): Record<K, ColumnDef> {
34
+ const table = getTableDefFromEntity(entity) as TableDef | undefined;
35
+ if (!table) {
36
+ throw new Error(`No table definition registered for entity '${entity.name}'`);
37
+ }
38
+
39
+ const selection = {} as Record<K, ColumnDef>;
40
+
41
+ for (const prop of props) {
42
+ const col = table.columns[prop];
43
+ if (!col) {
44
+ throw new Error(`No column '${prop}' found for entity '${entity.name}'`);
45
+ }
46
+ selection[prop] = col;
47
+ }
48
+
49
+ return selection;
50
+ }
@@ -54,15 +54,17 @@ import {
54
54
 
55
55
  import { QueryAstService } from './query-ast-service.js';
56
56
 
57
- import { ColumnSelector } from './column-selector.js';
58
-
59
- import { RelationManager } from './relation-manager.js';
60
-
61
- import { RelationIncludeOptions } from './relation-types.js';
62
-
63
- import { JOIN_KINDS, JoinKind, ORDER_DIRECTIONS, OrderDirection } from '../core/sql/sql.js';
64
-
65
- import { Entity, RelationMap } from '../schema/types.js';
57
+ import { ColumnSelector } from './column-selector.js';
58
+
59
+ import { RelationManager } from './relation-manager.js';
60
+
61
+ import { RelationIncludeOptions } from './relation-types.js';
62
+
63
+ import type { RelationDef } from '../schema/relation.js';
64
+
65
+ import { JOIN_KINDS, JoinKind, ORDER_DIRECTIONS, OrderDirection } from '../core/sql/sql.js';
66
+
67
+ import { Entity, RelationMap, RelationTargetTable } from '../schema/types.js';
66
68
 
67
69
  import { OrmSession } from '../orm/orm-session.ts';
68
70
 
@@ -72,11 +74,21 @@ import { HydrationContext } from '../orm/hydration-context.js';
72
74
 
73
75
  import { executeHydrated, executeHydratedWithContexts } from '../orm/execute.js';
74
76
 
75
- import { createJoinNode } from '../core/ast/join-node.js';
76
-
77
-
78
-
79
- /**
77
+ import { createJoinNode } from '../core/ast/join-node.js';
78
+
79
+
80
+ type ColumnSelectionValue = ColumnDef | FunctionNode | CaseExpressionNode | WindowFunctionNode;
81
+
82
+ type DeepSelectConfig<TTable extends TableDef> = {
83
+ root?: (keyof TTable['columns'] & string)[];
84
+ } & {
85
+ [K in keyof TTable['relations'] & string]?: (
86
+ keyof RelationTargetTable<TTable['relations'][K]>['columns'] & string
87
+ )[];
88
+ };
89
+
90
+
91
+ /**
80
92
 
81
93
  * Main query builder class for constructing SQL SELECT queries
82
94
 
@@ -252,12 +264,31 @@ export class SelectQueryBuilder<T = any, TTable extends TableDef = TableDef> {
252
264
 
253
265
  */
254
266
 
255
- select(columns: Record<string, ColumnDef | FunctionNode | CaseExpressionNode | WindowFunctionNode>): SelectQueryBuilder<T, TTable> {
256
-
257
- return this.clone(this.columnSelector.select(this.context, columns));
258
-
259
- }
260
-
267
+ select(columns: Record<string, ColumnSelectionValue>): SelectQueryBuilder<T, TTable> {
268
+
269
+ return this.clone(this.columnSelector.select(this.context, columns));
270
+
271
+ }
272
+
273
+
274
+ /**
275
+ * Selects columns from the root table by name (typed).
276
+ * @param cols - Column names on the root table
277
+ */
278
+ selectColumns<K extends keyof TTable['columns'] & string>(...cols: K[]): SelectQueryBuilder<T, TTable> {
279
+ const selection: Record<string, ColumnDef> = {};
280
+
281
+ for (const key of cols) {
282
+ const col = this.env.table.columns[key];
283
+ if (!col) {
284
+ throw new Error(`Column '${key}' not found on table '${this.env.table.name}'`);
285
+ }
286
+ selection[key] = col;
287
+ }
288
+
289
+ return this.select(selection);
290
+ }
291
+
261
292
 
262
293
 
263
294
  /**
@@ -494,16 +525,77 @@ export class SelectQueryBuilder<T = any, TTable extends TableDef = TableDef> {
494
525
 
495
526
 
496
527
 
497
- includeLazy<K extends keyof RelationMap<TTable>>(relationName: K): SelectQueryBuilder<T, TTable> {
498
-
499
- const nextLazy = new Set(this.lazyRelations);
500
-
501
- nextLazy.add(relationName as string);
502
-
503
- return this.clone(this.context, nextLazy);
504
-
505
- }
506
-
528
+ includeLazy<K extends keyof RelationMap<TTable>>(relationName: K): SelectQueryBuilder<T, TTable> {
529
+
530
+ const nextLazy = new Set(this.lazyRelations);
531
+
532
+ nextLazy.add(relationName as string);
533
+
534
+ return this.clone(this.context, nextLazy);
535
+
536
+ }
537
+
538
+ /**
539
+ * Selects columns for a related table in a single hop.
540
+ */
541
+ selectRelationColumns<
542
+ K extends keyof TTable['relations'] & string,
543
+ TRel extends RelationDef = TTable['relations'][K],
544
+ TTarget extends TableDef = RelationTargetTable<TRel>,
545
+ C extends keyof TTarget['columns'] & string = keyof TTarget['columns'] & string
546
+ >(relationName: K, ...cols: C[]): SelectQueryBuilder<T, TTable> {
547
+ const relation = this.env.table.relations[relationName] as RelationDef | undefined;
548
+ if (!relation) {
549
+ throw new Error(`Relation '${relationName}' not found on table '${this.env.table.name}'`);
550
+ }
551
+ const target = relation.target;
552
+
553
+ for (const col of cols) {
554
+ if (!target.columns[col]) {
555
+ throw new Error(
556
+ `Column '${col}' not found on related table '${target.name}' for relation '${relationName}'`
557
+ );
558
+ }
559
+ }
560
+
561
+ return this.include(relationName as string, { columns: cols as string[] });
562
+ }
563
+
564
+
565
+ /**
566
+ * Convenience alias for selecting specific columns from a relation.
567
+ */
568
+ includePick<
569
+ K extends keyof TTable['relations'] & string,
570
+ TRel extends RelationDef = TTable['relations'][K],
571
+ TTarget extends TableDef = RelationTargetTable<TRel>,
572
+ C extends keyof TTarget['columns'] & string = keyof TTarget['columns'] & string
573
+ >(relationName: K, cols: C[]): SelectQueryBuilder<T, TTable> {
574
+ return this.selectRelationColumns(relationName, ...cols);
575
+ }
576
+
577
+
578
+ /**
579
+ * Selects columns for the root table and relations from a single config object.
580
+ */
581
+ selectColumnsDeep(config: DeepSelectConfig<TTable>): SelectQueryBuilder<T, TTable> {
582
+ let qb: SelectQueryBuilder<T, TTable> = this;
583
+
584
+ if (config.root?.length) {
585
+ qb = qb.selectColumns(...config.root);
586
+ }
587
+
588
+ for (const key of Object.keys(config) as (keyof typeof config)[]) {
589
+ if (key === 'root') continue;
590
+ const relName = key as keyof TTable['relations'] & string;
591
+ const cols = config[relName as keyof DeepSelectConfig<TTable>] as string[] | undefined;
592
+ if (!cols || !cols.length) continue;
593
+ qb = qb.selectRelationColumns(relName, ...(cols as string[]));
594
+ }
595
+
596
+ return qb;
597
+ }
598
+
507
599
 
508
600
 
509
601
  getLazyRelations(): (keyof RelationMap<TTable>)[] {
@@ -1,7 +1,21 @@
1
1
  import { TableDef } from '../schema/table.js';
2
- import { ColumnNode, ExpressionNode, valueToOperand } from '../core/ast/expression.js';
2
+ import { ColumnNode, ExpressionNode, OperandNode, isOperandNode, valueToOperand } from '../core/ast/expression.js';
3
3
  import { TableNode, UpdateQueryNode, UpdateAssignmentNode } from '../core/ast/query.js';
4
4
  import { createTableNode } from '../core/ast/builders.js';
5
+ type LiteralValue = string | number | boolean | null;
6
+ type UpdateValue = OperandNode | LiteralValue;
7
+
8
+ const isUpdateValue = (value: unknown): value is UpdateValue => {
9
+ if (value === null) return true;
10
+ switch (typeof value) {
11
+ case 'string':
12
+ case 'number':
13
+ case 'boolean':
14
+ return true;
15
+ default:
16
+ return isOperandNode(value);
17
+ }
18
+ };
5
19
 
6
20
  /**
7
21
  * Immutable state for UPDATE queries
@@ -24,14 +38,22 @@ export class UpdateQueryState {
24
38
  }
25
39
 
26
40
  withSet(values: Record<string, unknown>): UpdateQueryState {
27
- const assignments: UpdateAssignmentNode[] = Object.entries(values).map(([column, value]) => ({
28
- column: {
29
- type: 'Column',
30
- table: this.table.name,
31
- name: column
32
- },
33
- value: valueToOperand(value)
34
- }));
41
+ const assignments: UpdateAssignmentNode[] = Object.entries(values).map(([column, rawValue]) => {
42
+ if (!isUpdateValue(rawValue)) {
43
+ throw new Error(
44
+ `Invalid update value for column "${column}": only primitives, null, or OperandNodes are allowed`
45
+ );
46
+ }
47
+
48
+ return {
49
+ column: {
50
+ type: 'Column',
51
+ table: this.table.name,
52
+ name: column
53
+ },
54
+ value: valueToOperand(rawValue)
55
+ };
56
+ });
35
57
 
36
58
  return this.clone({
37
59
  ...this.ast,
@@ -1,5 +1,5 @@
1
- import { ColumnDef } from './column.js';
2
- import { TableDef } from './table.js';
1
+ import { ColumnDef } from './column.js';
2
+ import { TableDef } from './table.js';
3
3
  import {
4
4
  RelationDef,
5
5
  HasManyRelation,
@@ -7,10 +7,20 @@ import {
7
7
  BelongsToRelation,
8
8
  BelongsToManyRelation
9
9
  } from './relation.js';
10
-
11
- /**
12
- * Maps a ColumnDef to its TypeScript type representation
13
- */
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
+ /**
22
+ * Maps a ColumnDef to its TypeScript type representation
23
+ */
14
24
  export type ColumnToTs<T extends ColumnDef> =
15
25
  T['type'] extends 'INT' | 'INTEGER' | 'int' | 'integer' ? number :
16
26
  T['type'] extends 'BIGINT' | 'bigint' ? number | bigint :