metal-orm 1.0.11 → 1.0.13

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 (54) hide show
  1. package/README.md +21 -18
  2. package/dist/decorators/index.cjs +317 -34
  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 +317 -34
  7. package/dist/decorators/index.js.map +1 -1
  8. package/dist/index.cjs +1965 -267
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +273 -23
  11. package/dist/index.d.ts +273 -23
  12. package/dist/index.js +1947 -267
  13. package/dist/index.js.map +1 -1
  14. package/dist/{select-654m4qy8.d.cts → select-CCp1oz9p.d.cts} +254 -4
  15. package/dist/{select-654m4qy8.d.ts → select-CCp1oz9p.d.ts} +254 -4
  16. package/package.json +3 -2
  17. package/src/core/ast/query.ts +40 -22
  18. package/src/core/ddl/dialects/base-schema-dialect.ts +48 -0
  19. package/src/core/ddl/dialects/index.ts +5 -0
  20. package/src/core/ddl/dialects/mssql-schema-dialect.ts +97 -0
  21. package/src/core/ddl/dialects/mysql-schema-dialect.ts +109 -0
  22. package/src/core/ddl/dialects/postgres-schema-dialect.ts +99 -0
  23. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +103 -0
  24. package/src/core/ddl/introspect/mssql.ts +149 -0
  25. package/src/core/ddl/introspect/mysql.ts +99 -0
  26. package/src/core/ddl/introspect/postgres.ts +154 -0
  27. package/src/core/ddl/introspect/sqlite.ts +66 -0
  28. package/src/core/ddl/introspect/types.ts +19 -0
  29. package/src/core/ddl/introspect/utils.ts +27 -0
  30. package/src/core/ddl/schema-diff.ts +179 -0
  31. package/src/core/ddl/schema-generator.ts +229 -0
  32. package/src/core/ddl/schema-introspect.ts +32 -0
  33. package/src/core/ddl/schema-types.ts +39 -0
  34. package/src/core/dialect/abstract.ts +122 -37
  35. package/src/core/dialect/base/sql-dialect.ts +204 -0
  36. package/src/core/dialect/mssql/index.ts +125 -80
  37. package/src/core/dialect/mysql/index.ts +18 -112
  38. package/src/core/dialect/postgres/index.ts +29 -126
  39. package/src/core/dialect/sqlite/index.ts +28 -129
  40. package/src/index.ts +4 -0
  41. package/src/orm/execute.ts +25 -16
  42. package/src/orm/orm-context.ts +60 -55
  43. package/src/orm/query-logger.ts +38 -0
  44. package/src/orm/relations/belongs-to.ts +42 -26
  45. package/src/orm/relations/has-many.ts +41 -25
  46. package/src/orm/relations/many-to-many.ts +43 -27
  47. package/src/orm/unit-of-work.ts +60 -23
  48. package/src/query-builder/hydration-manager.ts +229 -25
  49. package/src/query-builder/query-ast-service.ts +27 -12
  50. package/src/query-builder/select-query-state.ts +24 -12
  51. package/src/query-builder/select.ts +58 -14
  52. package/src/schema/column.ts +206 -27
  53. package/src/schema/table.ts +89 -32
  54. package/src/schema/types.ts +8 -5
@@ -1,15 +1,15 @@
1
- import { CompilerContext, Dialect } from '../abstract.js';
2
- import { SelectQueryNode, InsertQueryNode, UpdateQueryNode, DeleteQueryNode } from '../../ast/query.js';
3
- import { JsonPathNode, ColumnNode } from '../../ast/expression.js';
4
-
5
- /**
6
- * SQLite dialect implementation
7
- */
8
- export class SqliteDialect extends Dialect {
9
- /**
10
- * Creates a new SqliteDialect instance
11
- */
12
- public constructor() {
1
+ import { CompilerContext } from '../abstract.js';
2
+ import { JsonPathNode, ColumnNode } from '../../ast/expression.js';
3
+ import { SqlDialectBase } from '../base/sql-dialect.js';
4
+
5
+ /**
6
+ * SQLite dialect implementation
7
+ */
8
+ export class SqliteDialect extends SqlDialectBase {
9
+ /**
10
+ * Creates a new SqliteDialect instance
11
+ */
12
+ public constructor() {
13
13
  super();
14
14
  }
15
15
 
@@ -27,120 +27,19 @@ export class SqliteDialect extends Dialect {
27
27
  * @param node - JSON path node
28
28
  * @returns SQLite JSON path expression
29
29
  */
30
- protected compileJsonPath(node: JsonPathNode): string {
31
- const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
32
- // SQLite uses json_extract(col, '$.path')
33
- return `json_extract(${col}, '${node.path}')`;
34
- }
35
-
36
- /**
37
- * Compiles SELECT query AST to SQLite SQL
38
- * @param ast - Query AST
39
- * @param ctx - Compiler context
40
- * @returns SQLite SQL string
41
- */
42
- protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
43
- const columns = ast.columns.map(c => {
44
- let expr = '';
45
- if (c.type === 'Function') {
46
- expr = this.compileOperand(c, ctx);
47
- } else if (c.type === 'Column') {
48
- expr = `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`;
49
- } else if (c.type === 'ScalarSubquery') {
50
- expr = this.compileOperand(c, ctx);
51
- } else if (c.type === 'CaseExpression') {
52
- expr = this.compileOperand(c, ctx);
53
- } else if (c.type === 'WindowFunction') {
54
- expr = this.compileOperand(c, ctx);
55
- }
56
-
57
- // Handle alias
58
- if (c.alias) {
59
- // Backward compat for the raw string parsing alias hack in playground
60
- if (c.alias.includes('(')) return c.alias;
61
- return `${expr} AS ${this.quoteIdentifier(c.alias)}`;
62
- }
63
- return expr;
64
- }).join(', ');
65
-
66
- const distinct = ast.distinct ? 'DISTINCT ' : '';
67
- const from = `${this.quoteIdentifier(ast.from.name)}`;
68
-
69
- const joins = ast.joins.map(j => {
70
- const table = this.quoteIdentifier(j.table.name);
71
- const cond = this.compileExpression(j.condition, ctx);
72
- return `${j.kind} JOIN ${table} ON ${cond}`;
73
- }).join(' ');
74
- const whereClause = this.compileWhere(ast.where, ctx);
75
-
76
- const groupBy = ast.groupBy && ast.groupBy.length > 0
77
- ? ' GROUP BY ' + ast.groupBy.map(c => `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`).join(', ')
78
- : '';
79
-
80
- const having = ast.having
81
- ? ` HAVING ${this.compileExpression(ast.having, ctx)}`
82
- : '';
83
-
84
- const orderBy = ast.orderBy && ast.orderBy.length > 0
85
- ? ' ORDER BY ' + ast.orderBy.map(o => `${this.quoteIdentifier(o.column.table)}.${this.quoteIdentifier(o.column.name)} ${o.direction}`).join(', ')
86
- : '';
87
-
88
- const limit = ast.limit ? ` LIMIT ${ast.limit}` : '';
89
- const offset = ast.offset ? ` OFFSET ${ast.offset}` : '';
90
-
91
- const ctes = ast.ctes && ast.ctes.length > 0
92
- ? (() => {
93
- const hasRecursive = ast.ctes.some(cte => cte.recursive);
94
- const prefix = hasRecursive ? 'WITH RECURSIVE ' : 'WITH ';
95
- const cteDefs = ast.ctes.map(cte => {
96
- const name = this.quoteIdentifier(cte.name);
97
- const cols = cte.columns ? `(${cte.columns.map(c => this.quoteIdentifier(c)).join(', ')})` : '';
98
- const query = this.compileSelectAst(cte.query, ctx).trim().replace(/;$/, '');
99
- return `${name}${cols} AS (${query})`;
100
- }).join(', ');
101
- return prefix + cteDefs + ' ';
102
- })()
103
- : '';
104
-
105
- return `${ctes}SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy}${limit}${offset};`;
106
- }
107
-
108
- protected compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string {
109
- const table = this.quoteIdentifier(ast.into.name);
110
- const columnList = ast.columns.map(column => `${this.quoteIdentifier(column.table)}.${this.quoteIdentifier(column.name)}`).join(', ');
111
- const values = ast.values.map(row => `(${row.map(value => this.compileOperand(value, ctx)).join(', ')})`).join(', ');
112
- const returning = this.compileReturning(ast.returning, ctx);
113
- return `INSERT INTO ${table} (${columnList}) VALUES ${values}${returning};`;
114
- }
115
-
116
- protected compileUpdateAst(ast: UpdateQueryNode, ctx: CompilerContext): string {
117
- const table = this.quoteIdentifier(ast.table.name);
118
- const assignments = ast.set.map(assignment => {
119
- const col = assignment.column;
120
- const target = `${this.quoteIdentifier(col.table)}.${this.quoteIdentifier(col.name)}`;
121
- const value = this.compileOperand(assignment.value, ctx);
122
- return `${target} = ${value}`;
123
- }).join(', ');
124
- const whereClause = this.compileWhere(ast.where, ctx);
125
- const returning = this.compileReturning(ast.returning, ctx);
126
- return `UPDATE ${table} SET ${assignments}${whereClause}${returning};`;
127
- }
128
-
129
- protected compileDeleteAst(ast: DeleteQueryNode, ctx: CompilerContext): string {
130
- const table = this.quoteIdentifier(ast.from.name);
131
- const whereClause = this.compileWhere(ast.where, ctx);
132
- const returning = this.compileReturning(ast.returning, ctx);
133
- return `DELETE FROM ${table}${whereClause}${returning};`;
134
- }
135
-
136
- protected compileReturning(returning: ColumnNode[] | undefined, ctx: CompilerContext): string {
137
- if (!returning || returning.length === 0) return '';
138
- const columns = returning
139
- .map(column => {
140
- const tablePart = column.table ? `${this.quoteIdentifier(column.table)}.` : '';
141
- return `${tablePart}${this.quoteIdentifier(column.name)}`;
142
- })
143
- .join(', ');
144
- return ` RETURNING ${columns}`;
145
- }
146
- }
30
+ protected compileJsonPath(node: JsonPathNode): string {
31
+ const col = `${this.quoteIdentifier(node.column.table)}.${this.quoteIdentifier(node.column.name)}`;
32
+ // SQLite uses json_extract(col, '$.path')
33
+ return `json_extract(${col}, '${node.path}')`;
34
+ }
35
+
36
+ protected compileReturning(returning: ColumnNode[] | undefined, ctx: CompilerContext): string {
37
+ if (!returning || returning.length === 0) return '';
38
+ const columns = this.formatReturningColumns(returning);
39
+ return ` RETURNING ${columns}`;
40
+ }
41
+
42
+ supportsReturning(): boolean {
43
+ return true;
44
+ }
45
+ }
package/src/index.ts CHANGED
@@ -12,6 +12,10 @@ export * from './core/dialect/mysql/index.js';
12
12
  export * from './core/dialect/mssql/index.js';
13
13
  export * from './core/dialect/sqlite/index.js';
14
14
  export * from './core/dialect/postgres/index.js';
15
+ export * from './core/ddl/schema-generator.js';
16
+ export * from './core/ddl/schema-types.js';
17
+ export * from './core/ddl/schema-diff.js';
18
+ export * from './core/ddl/schema-introspect.js';
15
19
  export * from './orm/als.js';
16
20
  export * from './orm/hydration.js';
17
21
  export * from './codegen/typescript.js';
@@ -1,9 +1,9 @@
1
1
  import { TableDef } from '../schema/table.js';
2
2
  import { Entity } from '../schema/types.js';
3
- import { hydrateRows } from './hydration.js';
4
- import { OrmContext } from './orm-context.js';
5
- import { SelectQueryBuilder } from '../query-builder/select.js';
6
- import { createEntityFromRow } from './entity.js';
3
+ import { hydrateRows } from './hydration.js';
4
+ import { OrmContext } from './orm-context.js';
5
+ import { SelectQueryBuilder } from '../query-builder/select.js';
6
+ import { createEntityFromRow, createEntityProxy } from './entity.js';
7
7
 
8
8
  type Row = Record<string, any>;
9
9
 
@@ -22,15 +22,24 @@ const flattenResults = (results: { columns: string[]; values: unknown[][] }[]):
22
22
  return rows;
23
23
  };
24
24
 
25
- export async function executeHydrated<TTable extends TableDef>(
26
- ctx: OrmContext,
27
- qb: SelectQueryBuilder<any, TTable>
28
- ): Promise<Entity<TTable>[]> {
29
- const compiled = ctx.dialect.compileSelect(qb.getAST());
30
- const executed = await ctx.executor.executeSql(compiled.sql, compiled.params);
31
- const rows = flattenResults(executed);
32
- const hydrated = hydrateRows(rows, qb.getHydrationPlan());
33
- return hydrated.map(row =>
34
- createEntityFromRow(ctx, qb.getTable(), row, qb.getLazyRelations())
35
- );
36
- }
25
+ export async function executeHydrated<TTable extends TableDef>(
26
+ ctx: OrmContext,
27
+ qb: SelectQueryBuilder<any, TTable>
28
+ ): Promise<Entity<TTable>[]> {
29
+ const ast = qb.getAST();
30
+ const compiled = ctx.dialect.compileSelect(ast);
31
+ const executed = await ctx.executor.executeSql(compiled.sql, compiled.params);
32
+ const rows = flattenResults(executed);
33
+
34
+ // Set-operation queries cannot be reliably hydrated and should not collapse duplicates.
35
+ if (ast.setOps && ast.setOps.length > 0) {
36
+ return rows.map(row =>
37
+ createEntityProxy(ctx, qb.getTable(), row, qb.getLazyRelations())
38
+ );
39
+ }
40
+
41
+ const hydrated = hydrateRows(rows, qb.getHydrationPlan());
42
+ return hydrated.map(row =>
43
+ createEntityFromRow(ctx, qb.getTable(), row, qb.getLazyRelations())
44
+ );
45
+ }
@@ -1,20 +1,21 @@
1
1
  import type { Dialect } from '../core/dialect/abstract.js';
2
2
  import type { RelationDef } from '../schema/relation.js';
3
3
  import type { TableDef } from '../schema/table.js';
4
- import type { DbExecutor, QueryResult } from './db-executor.js';
5
- import { DomainEventBus, DomainEventHandler as DomainEventHandlerFn, addDomainEvent } from './domain-event-bus.js';
6
- import { IdentityMap } from './identity-map.js';
7
- import { RelationChangeProcessor } from './relation-change-processor.js';
8
- import { runInTransaction } from './transaction-runner.js';
9
- import { UnitOfWork } from './unit-of-work.js';
10
- import {
11
- EntityStatus,
12
- HasDomainEvents,
13
- RelationChange,
14
- RelationChangeEntry,
15
- RelationKey,
16
- TrackedEntity
17
- } from './runtime-types.js';
4
+ import type { DbExecutor, QueryResult } from './db-executor.js';
5
+ import { DomainEventBus, DomainEventHandler as DomainEventHandlerFn, addDomainEvent } from './domain-event-bus.js';
6
+ import { IdentityMap } from './identity-map.js';
7
+ import { RelationChangeProcessor } from './relation-change-processor.js';
8
+ import { runInTransaction } from './transaction-runner.js';
9
+ import { UnitOfWork } from './unit-of-work.js';
10
+ import {
11
+ EntityStatus,
12
+ HasDomainEvents,
13
+ RelationChange,
14
+ RelationChangeEntry,
15
+ RelationKey,
16
+ TrackedEntity
17
+ } from './runtime-types.js';
18
+ import { createQueryLoggingExecutor, QueryLogger } from './query-logger.js';
18
19
 
19
20
  export interface OrmInterceptor {
20
21
  beforeFlush?(ctx: OrmContext): Promise<void> | void;
@@ -23,43 +24,46 @@ export interface OrmInterceptor {
23
24
 
24
25
  export type DomainEventHandler = DomainEventHandlerFn<OrmContext>;
25
26
 
26
- export interface OrmContextOptions {
27
- dialect: Dialect;
28
- executor: DbExecutor;
29
- interceptors?: OrmInterceptor[];
30
- domainEventHandlers?: Record<string, DomainEventHandler[]>;
31
- }
32
-
33
- export class OrmContext {
34
- private readonly identityMap = new IdentityMap();
35
- private readonly unitOfWork: UnitOfWork;
36
- private readonly relationChanges: RelationChangeProcessor;
37
- private readonly interceptors: OrmInterceptor[];
38
- private readonly domainEvents: DomainEventBus<OrmContext>;
39
-
40
- constructor(private readonly options: OrmContextOptions) {
41
- this.interceptors = [...(options.interceptors ?? [])];
42
- this.unitOfWork = new UnitOfWork(
43
- options.dialect,
44
- options.executor,
45
- this.identityMap,
46
- () => this
47
- );
48
- this.relationChanges = new RelationChangeProcessor(
49
- this.unitOfWork,
50
- options.dialect,
51
- options.executor
52
- );
53
- this.domainEvents = new DomainEventBus<OrmContext>(options.domainEventHandlers);
54
- }
27
+ export interface OrmContextOptions {
28
+ dialect: Dialect;
29
+ executor: DbExecutor;
30
+ interceptors?: OrmInterceptor[];
31
+ domainEventHandlers?: Record<string, DomainEventHandler[]>;
32
+ queryLogger?: QueryLogger;
33
+ }
34
+
35
+ export class OrmContext {
36
+ private readonly identityMap = new IdentityMap();
37
+ private readonly executorWithLogging: DbExecutor;
38
+ private readonly unitOfWork: UnitOfWork;
39
+ private readonly relationChanges: RelationChangeProcessor;
40
+ private readonly interceptors: OrmInterceptor[];
41
+ private readonly domainEvents: DomainEventBus<OrmContext>;
42
+
43
+ constructor(private readonly options: OrmContextOptions) {
44
+ this.interceptors = [...(options.interceptors ?? [])];
45
+ this.executorWithLogging = createQueryLoggingExecutor(options.executor, options.queryLogger);
46
+ this.unitOfWork = new UnitOfWork(
47
+ options.dialect,
48
+ this.executorWithLogging,
49
+ this.identityMap,
50
+ () => this
51
+ );
52
+ this.relationChanges = new RelationChangeProcessor(
53
+ this.unitOfWork,
54
+ options.dialect,
55
+ this.executorWithLogging
56
+ );
57
+ this.domainEvents = new DomainEventBus<OrmContext>(options.domainEventHandlers);
58
+ }
55
59
 
56
60
  get dialect(): Dialect {
57
61
  return this.options.dialect;
58
62
  }
59
63
 
60
- get executor(): DbExecutor {
61
- return this.options.executor;
62
- }
64
+ get executor(): DbExecutor {
65
+ return this.executorWithLogging;
66
+ }
63
67
 
64
68
  get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
65
69
  return this.unitOfWork.identityBuckets;
@@ -143,12 +147,13 @@ export class OrmContext {
143
147
  }
144
148
  }
145
149
 
146
- export { addDomainEvent };
147
- export { EntityStatus };
148
- export type {
149
- QueryResult,
150
- DbExecutor,
151
- RelationKey,
152
- RelationChange,
153
- HasDomainEvents
154
- };
150
+ export { addDomainEvent };
151
+ export { EntityStatus };
152
+ export type {
153
+ QueryResult,
154
+ DbExecutor,
155
+ RelationKey,
156
+ RelationChange,
157
+ HasDomainEvents
158
+ };
159
+ export type { QueryLogEntry, QueryLogger } from './query-logger.js';
@@ -0,0 +1,38 @@
1
+ import type { DbExecutor } from './db-executor.js';
2
+
3
+ export interface QueryLogEntry {
4
+ sql: string;
5
+ params?: unknown[];
6
+ }
7
+
8
+ export type QueryLogger = (entry: QueryLogEntry) => void;
9
+
10
+ export const createQueryLoggingExecutor = (
11
+ executor: DbExecutor,
12
+ logger?: QueryLogger
13
+ ): DbExecutor => {
14
+ if (!logger) {
15
+ return executor;
16
+ }
17
+
18
+ const wrapped: DbExecutor = {
19
+ async executeSql(sql, params) {
20
+ logger({ sql, params });
21
+ return executor.executeSql(sql, params);
22
+ }
23
+ };
24
+
25
+ if (executor.beginTransaction) {
26
+ wrapped.beginTransaction = executor.beginTransaction.bind(executor);
27
+ }
28
+
29
+ if (executor.commitTransaction) {
30
+ wrapped.commitTransaction = executor.commitTransaction.bind(executor);
31
+ }
32
+
33
+ if (executor.rollbackTransaction) {
34
+ wrapped.rollbackTransaction = executor.rollbackTransaction.bind(executor);
35
+ }
36
+
37
+ return wrapped;
38
+ };
@@ -4,15 +4,26 @@ import { BelongsToRelation } from '../../schema/relation.js';
4
4
  import { TableDef } from '../../schema/table.js';
5
5
  import { EntityMeta, getHydrationRecord, hasEntityMeta } from '../entity-meta.js';
6
6
 
7
- type Rows = Record<string, any>;
8
-
9
- const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
10
-
11
- export class DefaultBelongsToReference<TParent> implements BelongsToReference<TParent> {
12
- private loaded = false;
13
- private current: TParent | null = null;
14
-
15
- constructor(
7
+ type Rows = Record<string, any>;
8
+
9
+ const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
10
+
11
+ const hideInternal = (obj: any, keys: string[]): void => {
12
+ for (const key of keys) {
13
+ Object.defineProperty(obj, key, {
14
+ value: obj[key],
15
+ writable: false,
16
+ configurable: false,
17
+ enumerable: false
18
+ });
19
+ }
20
+ };
21
+
22
+ export class DefaultBelongsToReference<TParent> implements BelongsToReference<TParent> {
23
+ private loaded = false;
24
+ private current: TParent | null = null;
25
+
26
+ constructor(
16
27
  private readonly ctx: OrmContext,
17
28
  private readonly meta: EntityMeta<any>,
18
29
  private readonly root: any,
@@ -20,14 +31,15 @@ export class DefaultBelongsToReference<TParent> implements BelongsToReference<TP
20
31
  private readonly relation: BelongsToRelation,
21
32
  private readonly rootTable: TableDef,
22
33
  private readonly loader: () => Promise<Map<string, Rows>>,
23
- private readonly createEntity: (row: Record<string, any>) => TParent,
24
- private readonly targetKey: string
25
- ) {
26
- this.populateFromHydrationCache();
27
- }
28
-
29
- async load(): Promise<TParent | null> {
30
- if (this.loaded) return this.current;
34
+ private readonly createEntity: (row: Record<string, any>) => TParent,
35
+ private readonly targetKey: string
36
+ ) {
37
+ hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'targetKey']);
38
+ this.populateFromHydrationCache();
39
+ }
40
+
41
+ async load(): Promise<TParent | null> {
42
+ if (this.loaded) return this.current;
31
43
  const map = await this.loader();
32
44
  const fkValue = this.root[this.relation.foreignKey];
33
45
  if (fkValue === null || fkValue === undefined) {
@@ -81,12 +93,16 @@ export class DefaultBelongsToReference<TParent> implements BelongsToReference<TP
81
93
  return `${this.rootTable.name}.${this.relationName}`;
82
94
  }
83
95
 
84
- private populateFromHydrationCache(): void {
85
- const fkValue = this.root[this.relation.foreignKey];
86
- if (fkValue === undefined || fkValue === null) return;
87
- const row = getHydrationRecord(this.meta, this.relationName, fkValue);
88
- if (!row) return;
89
- this.current = this.createEntity(row);
90
- this.loaded = true;
91
- }
92
- }
96
+ private populateFromHydrationCache(): void {
97
+ const fkValue = this.root[this.relation.foreignKey];
98
+ if (fkValue === undefined || fkValue === null) return;
99
+ const row = getHydrationRecord(this.meta, this.relationName, fkValue);
100
+ if (!row) return;
101
+ this.current = this.createEntity(row);
102
+ this.loaded = true;
103
+ }
104
+
105
+ toJSON(): TParent | null {
106
+ return this.current;
107
+ }
108
+ }
@@ -4,15 +4,26 @@ import { HasManyRelation } from '../../schema/relation.js';
4
4
  import { TableDef } from '../../schema/table.js';
5
5
  import { EntityMeta, getHydrationRows } from '../entity-meta.js';
6
6
 
7
- type Rows = Record<string, any>[];
8
-
9
- const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
10
-
11
- export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChild> {
12
- private loaded = false;
13
- private items: TChild[] = [];
14
- private readonly added = new Set<TChild>();
15
- private readonly removed = new Set<TChild>();
7
+ type Rows = Record<string, any>[];
8
+
9
+ const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
10
+
11
+ const hideInternal = (obj: any, keys: string[]): void => {
12
+ for (const key of keys) {
13
+ Object.defineProperty(obj, key, {
14
+ value: obj[key],
15
+ writable: false,
16
+ configurable: false,
17
+ enumerable: false
18
+ });
19
+ }
20
+ };
21
+
22
+ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChild> {
23
+ private loaded = false;
24
+ private items: TChild[] = [];
25
+ private readonly added = new Set<TChild>();
26
+ private readonly removed = new Set<TChild>();
16
27
 
17
28
  constructor(
18
29
  private readonly ctx: OrmContext,
@@ -23,13 +34,14 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
23
34
  private readonly rootTable: TableDef,
24
35
  private readonly loader: () => Promise<Map<string, Rows>>,
25
36
  private readonly createEntity: (row: Record<string, any>) => TChild,
26
- private readonly localKey: string
27
- ) {
28
- this.hydrateFromCache();
29
- }
30
-
31
- async load(): Promise<TChild[]> {
32
- if (this.loaded) return this.items;
37
+ private readonly localKey: string
38
+ ) {
39
+ hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'localKey']);
40
+ this.hydrateFromCache();
41
+ }
42
+
43
+ async load(): Promise<TChild[]> {
44
+ if (this.loaded) return this.items;
33
45
  const map = await this.loader();
34
46
  const key = toKey(this.root[this.localKey]);
35
47
  const rows = map.get(key) ?? [];
@@ -100,12 +112,16 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
100
112
  return `${this.rootTable.name}.${this.relationName}`;
101
113
  }
102
114
 
103
- private hydrateFromCache(): void {
104
- const keyValue = this.root[this.localKey];
105
- if (keyValue === undefined || keyValue === null) return;
106
- const rows = getHydrationRows(this.meta, this.relationName, keyValue);
107
- if (!rows?.length) return;
108
- this.items = rows.map(row => this.createEntity(row));
109
- this.loaded = true;
110
- }
111
- }
115
+ private hydrateFromCache(): void {
116
+ const keyValue = this.root[this.localKey];
117
+ if (keyValue === undefined || keyValue === null) return;
118
+ const rows = getHydrationRows(this.meta, this.relationName, keyValue);
119
+ if (!rows?.length) return;
120
+ this.items = rows.map(row => this.createEntity(row));
121
+ this.loaded = true;
122
+ }
123
+
124
+ toJSON(): TChild[] {
125
+ return this.items;
126
+ }
127
+ }
@@ -5,15 +5,26 @@ import { TableDef } from '../../schema/table.js';
5
5
  import { findPrimaryKey } from '../../query-builder/hydration-planner.js';
6
6
  import { EntityMeta, getHydrationRows } from '../entity-meta.js';
7
7
 
8
- type Rows = Record<string, any>[];
9
-
10
- const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
11
-
12
- export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollection<TTarget> {
13
- private loaded = false;
14
- private items: TTarget[] = [];
15
-
16
- constructor(
8
+ type Rows = Record<string, any>[];
9
+
10
+ const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
11
+
12
+ const hideInternal = (obj: any, keys: string[]): void => {
13
+ for (const key of keys) {
14
+ Object.defineProperty(obj, key, {
15
+ value: obj[key],
16
+ writable: false,
17
+ configurable: false,
18
+ enumerable: false
19
+ });
20
+ }
21
+ };
22
+
23
+ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollection<TTarget> {
24
+ private loaded = false;
25
+ private items: TTarget[] = [];
26
+
27
+ constructor(
17
28
  private readonly ctx: OrmContext,
18
29
  private readonly meta: EntityMeta<any>,
19
30
  private readonly root: any,
@@ -21,14 +32,15 @@ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollectio
21
32
  private readonly relation: BelongsToManyRelation,
22
33
  private readonly rootTable: TableDef,
23
34
  private readonly loader: () => Promise<Map<string, Rows>>,
24
- private readonly createEntity: (row: Record<string, any>) => TTarget,
25
- private readonly localKey: string
26
- ) {
27
- this.hydrateFromCache();
28
- }
29
-
30
- async load(): Promise<TTarget[]> {
31
- if (this.loaded) return this.items;
35
+ private readonly createEntity: (row: Record<string, any>) => TTarget,
36
+ private readonly localKey: string
37
+ ) {
38
+ hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'localKey']);
39
+ this.hydrateFromCache();
40
+ }
41
+
42
+ async load(): Promise<TTarget[]> {
43
+ if (this.loaded) return this.items;
32
44
  const map = await this.loader();
33
45
  const key = toKey(this.root[this.localKey]);
34
46
  const rows = map.get(key) ?? [];
@@ -132,18 +144,22 @@ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollectio
132
144
  return this.relation.targetKey || findPrimaryKey(this.relation.target);
133
145
  }
134
146
 
135
- private hydrateFromCache(): void {
136
- const keyValue = this.root[this.localKey];
137
- if (keyValue === undefined || keyValue === null) return;
138
- const rows = getHydrationRows(this.meta, this.relationName, keyValue);
139
- if (!rows?.length) return;
147
+ private hydrateFromCache(): void {
148
+ const keyValue = this.root[this.localKey];
149
+ if (keyValue === undefined || keyValue === null) return;
150
+ const rows = getHydrationRows(this.meta, this.relationName, keyValue);
151
+ if (!rows?.length) return;
140
152
  this.items = rows.map(row => {
141
153
  const entity = this.createEntity(row);
142
154
  if ((row as any)._pivot) {
143
155
  (entity as any)._pivot = (row as any)._pivot;
144
156
  }
145
- return entity;
146
- });
147
- this.loaded = true;
148
- }
149
- }
157
+ return entity;
158
+ });
159
+ this.loaded = true;
160
+ }
161
+
162
+ toJSON(): TTarget[] {
163
+ return this.items;
164
+ }
165
+ }