metal-orm 1.0.15 → 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.
- package/README.md +64 -61
- package/dist/decorators/index.cjs +490 -175
- package/dist/decorators/index.cjs.map +1 -1
- package/dist/decorators/index.d.cts +1 -5
- package/dist/decorators/index.d.ts +1 -5
- package/dist/decorators/index.js +490 -175
- package/dist/decorators/index.js.map +1 -1
- package/dist/index.cjs +1044 -483
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -15
- package/dist/index.d.ts +67 -15
- package/dist/index.js +1033 -482
- package/dist/index.js.map +1 -1
- package/dist/{select-Bkv8g8u_.d.cts → select-BPCn6MOH.d.cts} +486 -32
- package/dist/{select-Bkv8g8u_.d.ts → select-BPCn6MOH.d.ts} +486 -32
- package/package.json +2 -1
- package/src/codegen/naming-strategy.ts +64 -0
- package/src/codegen/typescript.ts +48 -53
- package/src/core/ast/aggregate-functions.ts +50 -4
- package/src/core/ast/expression-builders.ts +22 -15
- package/src/core/ast/expression-nodes.ts +6 -0
- package/src/core/ddl/introspect/functions/postgres.ts +2 -6
- package/src/core/ddl/schema-generator.ts +3 -2
- package/src/core/ddl/schema-introspect.ts +1 -1
- package/src/core/dialect/abstract.ts +40 -8
- package/src/core/dialect/mssql/functions.ts +24 -15
- package/src/core/dialect/postgres/functions.ts +33 -24
- package/src/core/dialect/sqlite/functions.ts +19 -12
- package/src/core/functions/datetime.ts +2 -1
- package/src/core/functions/numeric.ts +2 -1
- package/src/core/functions/standard-strategy.ts +52 -12
- package/src/core/functions/text.ts +2 -1
- package/src/core/functions/types.ts +8 -8
- package/src/decorators/column.ts +13 -4
- package/src/index.ts +13 -5
- package/src/orm/domain-event-bus.ts +43 -25
- package/src/orm/entity-context.ts +30 -0
- package/src/orm/entity-meta.ts +42 -2
- package/src/orm/entity-metadata.ts +1 -6
- package/src/orm/entity.ts +88 -88
- package/src/orm/execute.ts +42 -25
- package/src/orm/execution-context.ts +18 -0
- package/src/orm/hydration-context.ts +16 -0
- package/src/orm/identity-map.ts +4 -0
- package/src/orm/interceptor-pipeline.ts +29 -0
- package/src/orm/lazy-batch.ts +6 -6
- package/src/orm/orm-session.ts +245 -0
- package/src/orm/orm.ts +58 -0
- package/src/orm/query-logger.ts +15 -0
- package/src/orm/relation-change-processor.ts +5 -1
- package/src/orm/relations/belongs-to.ts +45 -44
- package/src/orm/relations/has-many.ts +44 -43
- package/src/orm/relations/has-one.ts +140 -139
- package/src/orm/relations/many-to-many.ts +46 -45
- package/src/orm/runtime-types.ts +60 -2
- package/src/orm/transaction-runner.ts +7 -0
- package/src/orm/unit-of-work.ts +7 -1
- package/src/query-builder/insert-query-state.ts +13 -3
- package/src/query-builder/select-helpers.ts +50 -0
- package/src/query-builder/select.ts +616 -18
- package/src/query-builder/update-query-state.ts +31 -9
- package/src/schema/types.ts +16 -6
- package/src/orm/orm-context.ts +0 -159
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { FunctionStrategy, FunctionRenderer, FunctionRenderContext } from './types.js';
|
|
1
|
+
import { FunctionStrategy, FunctionRenderer, FunctionRenderContext } from './types.js';
|
|
2
|
+
import { LiteralNode, OperandNode } from '../ast/expression.js';
|
|
2
3
|
|
|
3
4
|
export class StandardFunctionStrategy implements FunctionStrategy {
|
|
4
5
|
protected renderers: Map<string, FunctionRenderer> = new Map();
|
|
@@ -7,11 +8,16 @@ export class StandardFunctionStrategy implements FunctionStrategy {
|
|
|
7
8
|
this.registerStandard();
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
protected registerStandard() {
|
|
11
|
-
// Register ANSI standard implementations
|
|
12
|
-
this.add('
|
|
13
|
-
this.add('
|
|
14
|
-
this.add('
|
|
11
|
+
protected registerStandard() {
|
|
12
|
+
// Register ANSI standard implementations
|
|
13
|
+
this.add('COUNT', ({ compiledArgs }) => `COUNT(${compiledArgs.join(', ')})`);
|
|
14
|
+
this.add('SUM', ({ compiledArgs }) => `SUM(${compiledArgs[0]})`);
|
|
15
|
+
this.add('AVG', ({ compiledArgs }) => `AVG(${compiledArgs[0]})`);
|
|
16
|
+
this.add('MIN', ({ compiledArgs }) => `MIN(${compiledArgs[0]})`);
|
|
17
|
+
this.add('MAX', ({ compiledArgs }) => `MAX(${compiledArgs[0]})`);
|
|
18
|
+
this.add('ABS', ({ compiledArgs }) => `ABS(${compiledArgs[0]})`);
|
|
19
|
+
this.add('UPPER', ({ compiledArgs }) => `UPPER(${compiledArgs[0]})`);
|
|
20
|
+
this.add('LOWER', ({ compiledArgs }) => `LOWER(${compiledArgs[0]})`);
|
|
15
21
|
this.add('LENGTH', ({ compiledArgs }) => `LENGTH(${compiledArgs[0]})`);
|
|
16
22
|
this.add('TRIM', ({ compiledArgs }) => `TRIM(${compiledArgs[0]})`);
|
|
17
23
|
this.add('LTRIM', ({ compiledArgs }) => `LTRIM(${compiledArgs[0]})`);
|
|
@@ -33,15 +39,49 @@ export class StandardFunctionStrategy implements FunctionStrategy {
|
|
|
33
39
|
this.add('FROM_UNIXTIME', ({ compiledArgs }) => `FROM_UNIXTIME(${compiledArgs[0]})`);
|
|
34
40
|
this.add('END_OF_MONTH', ({ compiledArgs }) => `LAST_DAY(${compiledArgs[0]})`);
|
|
35
41
|
this.add('DAY_OF_WEEK', ({ compiledArgs }) => `DAYOFWEEK(${compiledArgs[0]})`);
|
|
36
|
-
this.add('WEEK_OF_YEAR', ({ compiledArgs }) => `WEEKOFYEAR(${compiledArgs[0]})`);
|
|
37
|
-
this.add('DATE_TRUNC', ({ compiledArgs }) => `DATE_TRUNC(${compiledArgs[0]}, ${compiledArgs[1]})`);
|
|
42
|
+
this.add('WEEK_OF_YEAR', ({ compiledArgs }) => `WEEKOFYEAR(${compiledArgs[0]})`);
|
|
43
|
+
this.add('DATE_TRUNC', ({ compiledArgs }) => `DATE_TRUNC(${compiledArgs[0]}, ${compiledArgs[1]})`);
|
|
44
|
+
this.add('GROUP_CONCAT', ctx => this.renderGroupConcat(ctx));
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
protected add(name: string, renderer: FunctionRenderer) {
|
|
41
48
|
this.renderers.set(name, renderer);
|
|
42
49
|
}
|
|
43
50
|
|
|
44
|
-
getRenderer(name: string): FunctionRenderer | undefined {
|
|
45
|
-
return this.renderers.get(name);
|
|
46
|
-
}
|
|
47
|
-
|
|
51
|
+
getRenderer(name: string): FunctionRenderer | undefined {
|
|
52
|
+
return this.renderers.get(name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private renderGroupConcat(ctx: FunctionRenderContext): string {
|
|
56
|
+
const arg = ctx.compiledArgs[0];
|
|
57
|
+
const orderClause = this.buildOrderByExpression(ctx);
|
|
58
|
+
const orderSegment = orderClause ? ` ${orderClause}` : '';
|
|
59
|
+
const separatorClause = this.formatGroupConcatSeparator(ctx);
|
|
60
|
+
return `GROUP_CONCAT(${arg}${orderSegment}${separatorClause})`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
protected buildOrderByExpression(ctx: FunctionRenderContext): string {
|
|
64
|
+
const orderBy = ctx.node.orderBy;
|
|
65
|
+
if (!orderBy || orderBy.length === 0) {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
const parts = orderBy.map(order => `${ctx.compileOperand(order.column)} ${order.direction}`);
|
|
69
|
+
return `ORDER BY ${parts.join(', ')}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
protected formatGroupConcatSeparator(ctx: FunctionRenderContext): string {
|
|
73
|
+
if (!ctx.node.separator) {
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
return ` SEPARATOR ${ctx.compileOperand(ctx.node.separator)}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
protected getGroupConcatSeparatorOperand(ctx: FunctionRenderContext): OperandNode {
|
|
80
|
+
return ctx.node.separator ?? StandardFunctionStrategy.DEFAULT_GROUP_CONCAT_SEPARATOR;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
protected static readonly DEFAULT_GROUP_CONCAT_SEPARATOR: LiteralNode = {
|
|
84
|
+
type: 'Literal',
|
|
85
|
+
value: ','
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -11,7 +11,8 @@ const isColumnDef = (val: any): val is ColumnDef => !!val && typeof val === 'obj
|
|
|
11
11
|
const toOperand = (input: OperandInput): OperandNode => {
|
|
12
12
|
if (isOperandNode(input)) return input;
|
|
13
13
|
if (isColumnDef(input)) return columnOperand(input);
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
return valueToOperand(input);
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
const fn = (key: string, args: OperandInput[]): FunctionNode => ({
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { FunctionNode } from '../ast/expression.js';
|
|
2
|
-
|
|
3
|
-
export interface FunctionRenderContext {
|
|
4
|
-
node: FunctionNode;
|
|
5
|
-
compiledArgs: string[];
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
1
|
+
import { FunctionNode, OperandNode } from '../ast/expression.js';
|
|
2
|
+
|
|
3
|
+
export interface FunctionRenderContext {
|
|
4
|
+
node: FunctionNode;
|
|
5
|
+
compiledArgs: string[];
|
|
6
|
+
/** Helper to compile additional operands (e.g., separators or ORDER BY columns) */
|
|
7
|
+
compileOperand: (operand: OperandNode) => string;
|
|
8
|
+
}
|
|
9
9
|
|
|
10
10
|
export type FunctionRenderer = (ctx: FunctionRenderContext) => string;
|
|
11
11
|
|
package/src/decorators/column.ts
CHANGED
|
@@ -16,11 +16,20 @@ export interface ColumnOptions {
|
|
|
16
16
|
export type ColumnInput = ColumnOptions | ColumnDef;
|
|
17
17
|
|
|
18
18
|
const normalizeColumnInput = (input: ColumnInput): ColumnDefLike => {
|
|
19
|
+
const asOptions = input as ColumnOptions;
|
|
20
|
+
const asDefinition = input as ColumnDef;
|
|
19
21
|
const column: ColumnDefLike = {
|
|
20
|
-
type:
|
|
21
|
-
args:
|
|
22
|
-
notNull:
|
|
23
|
-
primary:
|
|
22
|
+
type: asOptions.type ?? asDefinition.type,
|
|
23
|
+
args: asOptions.args ?? asDefinition.args,
|
|
24
|
+
notNull: asOptions.notNull ?? asDefinition.notNull,
|
|
25
|
+
primary: asOptions.primary ?? asDefinition.primary,
|
|
26
|
+
unique: asDefinition.unique,
|
|
27
|
+
default: asDefinition.default,
|
|
28
|
+
autoIncrement: asDefinition.autoIncrement,
|
|
29
|
+
generated: asDefinition.generated,
|
|
30
|
+
check: asDefinition.check,
|
|
31
|
+
references: asDefinition.references,
|
|
32
|
+
comment: asDefinition.comment
|
|
24
33
|
};
|
|
25
34
|
|
|
26
35
|
if (!column.type) {
|
package/src/index.ts
CHANGED
|
@@ -2,10 +2,11 @@ export * from './schema/table.js';
|
|
|
2
2
|
export * from './schema/column.js';
|
|
3
3
|
export * from './schema/relation.js';
|
|
4
4
|
export * from './schema/types.js';
|
|
5
|
-
export * from './query-builder/select.js';
|
|
6
|
-
export * from './query-builder/
|
|
7
|
-
export * from './query-builder/
|
|
8
|
-
export * from './query-builder/
|
|
5
|
+
export * from './query-builder/select.js';
|
|
6
|
+
export * from './query-builder/select-helpers.js';
|
|
7
|
+
export * from './query-builder/insert.js';
|
|
8
|
+
export * from './query-builder/update.js';
|
|
9
|
+
export * from './query-builder/delete.js';
|
|
9
10
|
export * from './core/ast/expression.js';
|
|
10
11
|
export * from './core/hydration/types.js';
|
|
11
12
|
export * from './core/dialect/mysql/index.js';
|
|
@@ -23,13 +24,20 @@ export * from './core/functions/datetime.js';
|
|
|
23
24
|
export * from './orm/als.js';
|
|
24
25
|
export * from './orm/hydration.js';
|
|
25
26
|
export * from './codegen/typescript.js';
|
|
26
|
-
export * from './orm/orm-
|
|
27
|
+
export * from './orm/orm-session.js';
|
|
28
|
+
export * from './orm/orm.js';
|
|
27
29
|
export * from './orm/entity.js';
|
|
28
30
|
export * from './orm/lazy-batch.js';
|
|
29
31
|
export * from './orm/relations/has-many.js';
|
|
30
32
|
export * from './orm/relations/belongs-to.js';
|
|
31
33
|
export * from './orm/relations/many-to-many.js';
|
|
32
34
|
export * from './orm/execute.js';
|
|
35
|
+
export * from './orm/entity-context.js';
|
|
36
|
+
export * from './orm/execution-context.js';
|
|
37
|
+
export * from './orm/hydration-context.js';
|
|
38
|
+
export * from './orm/domain-event-bus.js';
|
|
39
|
+
export * from './orm/runtime-types.js';
|
|
40
|
+
export * from './orm/query-logger.js';
|
|
33
41
|
|
|
34
42
|
// NEW: execution abstraction + helpers
|
|
35
43
|
export * from './core/execution/db-executor.js';
|
|
@@ -1,32 +1,53 @@
|
|
|
1
|
-
import type { HasDomainEvents, TrackedEntity } from './runtime-types.js';
|
|
1
|
+
import type { DomainEvent, HasDomainEvents, TrackedEntity } from './runtime-types.js';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type EventOfType<E extends DomainEvent, TType extends E['type']> =
|
|
4
|
+
Extract<E, { type: TType }>;
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
-
|
|
6
|
+
export type DomainEventHandler<E extends DomainEvent, Context> =
|
|
7
|
+
(event: E, ctx: Context) => Promise<void> | void;
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
export type InitialHandlers<E extends DomainEvent, Context> = {
|
|
10
|
+
[K in E['type']]?: DomainEventHandler<EventOfType<E, K>, Context>[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export class DomainEventBus<E extends DomainEvent, Context> {
|
|
14
|
+
private readonly handlers = new Map<E['type'], DomainEventHandler<E, Context>[]>();
|
|
15
|
+
|
|
16
|
+
constructor(initialHandlers?: InitialHandlers<E, Context>) {
|
|
17
|
+
if (initialHandlers) {
|
|
18
|
+
for (const key in initialHandlers) {
|
|
19
|
+
const type = key as E['type'];
|
|
20
|
+
const list = initialHandlers[type] ?? [];
|
|
21
|
+
this.handlers.set(type, [...(list as DomainEventHandler<E, Context>[])]);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
13
24
|
}
|
|
14
25
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
on<TType extends E['type']>(
|
|
27
|
+
type: TType,
|
|
28
|
+
handler: DomainEventHandler<EventOfType<E, TType>, Context>
|
|
29
|
+
): void {
|
|
30
|
+
const key = type as E['type'];
|
|
31
|
+
const existing = this.handlers.get(key) ?? [];
|
|
32
|
+
existing.push(handler as unknown as DomainEventHandler<E, Context>);
|
|
33
|
+
this.handlers.set(key, existing);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
register<TType extends E['type']>(
|
|
37
|
+
type: TType,
|
|
38
|
+
handler: DomainEventHandler<EventOfType<E, TType>, Context>
|
|
39
|
+
): void {
|
|
40
|
+
this.on(type, handler);
|
|
19
41
|
}
|
|
20
42
|
|
|
21
43
|
async dispatch(trackedEntities: Iterable<TrackedEntity>, ctx: Context): Promise<void> {
|
|
22
44
|
for (const tracked of trackedEntities) {
|
|
23
|
-
const entity = tracked.entity as HasDomainEvents
|
|
24
|
-
if (!entity.domainEvents
|
|
45
|
+
const entity = tracked.entity as HasDomainEvents<E>;
|
|
46
|
+
if (!entity.domainEvents?.length) continue;
|
|
25
47
|
|
|
26
48
|
for (const event of entity.domainEvents) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
if (!handlers) continue;
|
|
49
|
+
const handlers = this.handlers.get(event.type as E['type']);
|
|
50
|
+
if (!handlers?.length) continue;
|
|
30
51
|
|
|
31
52
|
for (const handler of handlers) {
|
|
32
53
|
await handler(event, ctx);
|
|
@@ -36,15 +57,12 @@ export class DomainEventBus<Context> {
|
|
|
36
57
|
entity.domainEvents = [];
|
|
37
58
|
}
|
|
38
59
|
}
|
|
39
|
-
|
|
40
|
-
private getEventName(event: any): string {
|
|
41
|
-
if (!event) return 'Unknown';
|
|
42
|
-
if (typeof event === 'string') return event;
|
|
43
|
-
return event.constructor?.name ?? 'Unknown';
|
|
44
|
-
}
|
|
45
60
|
}
|
|
46
61
|
|
|
47
|
-
export const addDomainEvent =
|
|
62
|
+
export const addDomainEvent = <E extends DomainEvent>(
|
|
63
|
+
entity: HasDomainEvents<E>,
|
|
64
|
+
event: E
|
|
65
|
+
): void => {
|
|
48
66
|
if (!entity.domainEvents) {
|
|
49
67
|
entity.domainEvents = [];
|
|
50
68
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Dialect } from '../core/dialect/abstract.js';
|
|
2
|
+
import type { DbExecutor } from '../core/execution/db-executor.js';
|
|
3
|
+
import { TableDef } from '../schema/table.js';
|
|
4
|
+
import { RelationDef } from '../schema/relation.js';
|
|
5
|
+
import { RelationChange, RelationKey, TrackedEntity } from './runtime-types.js';
|
|
6
|
+
|
|
7
|
+
export interface EntityContext {
|
|
8
|
+
dialect: Dialect;
|
|
9
|
+
executor: DbExecutor;
|
|
10
|
+
|
|
11
|
+
getEntity(table: TableDef, pk: any): any;
|
|
12
|
+
setEntity(table: TableDef, pk: any, entity: any): void;
|
|
13
|
+
|
|
14
|
+
trackNew(table: TableDef, entity: any, pk?: any): void;
|
|
15
|
+
trackManaged(table: TableDef, pk: any, entity: any): void;
|
|
16
|
+
|
|
17
|
+
markDirty(entity: any): void;
|
|
18
|
+
markRemoved(entity: any): void;
|
|
19
|
+
|
|
20
|
+
getEntitiesForTable(table: TableDef): TrackedEntity[];
|
|
21
|
+
|
|
22
|
+
registerRelationChange(
|
|
23
|
+
root: any,
|
|
24
|
+
relationKey: RelationKey,
|
|
25
|
+
rootTable: TableDef,
|
|
26
|
+
relationName: string,
|
|
27
|
+
relation: RelationDef,
|
|
28
|
+
change: RelationChange<any>
|
|
29
|
+
): void;
|
|
30
|
+
}
|
package/src/orm/entity-meta.ts
CHANGED
|
@@ -1,20 +1,41 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table.js';
|
|
2
|
-
import {
|
|
2
|
+
import { EntityContext } from './entity-context.js';
|
|
3
3
|
import { RelationMap } from '../schema/types.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Symbol used to store entity metadata on entity instances
|
|
7
|
+
*/
|
|
5
8
|
export const ENTITY_META = Symbol('EntityMeta');
|
|
6
9
|
|
|
7
10
|
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
8
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Metadata stored on entity instances for ORM internal use
|
|
14
|
+
* @typeParam TTable - Table definition type
|
|
15
|
+
*/
|
|
9
16
|
export interface EntityMeta<TTable extends TableDef> {
|
|
10
|
-
|
|
17
|
+
/** Entity context */
|
|
18
|
+
ctx: EntityContext;
|
|
19
|
+
/** Table definition */
|
|
11
20
|
table: TTable;
|
|
21
|
+
/** Relations that should be loaded lazily */
|
|
12
22
|
lazyRelations: (keyof RelationMap<TTable>)[];
|
|
23
|
+
/** Cache for relation promises */
|
|
13
24
|
relationCache: Map<string, Promise<any>>;
|
|
25
|
+
/** Hydration data for relations */
|
|
14
26
|
relationHydration: Map<string, Map<string, any>>;
|
|
27
|
+
/** Relation wrapper instances */
|
|
15
28
|
relationWrappers: Map<string, unknown>;
|
|
16
29
|
}
|
|
17
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Gets hydration rows for a specific relation and key
|
|
33
|
+
* @param meta - Entity metadata
|
|
34
|
+
* @param relationName - Name of the relation
|
|
35
|
+
* @param key - Key to look up in the hydration map
|
|
36
|
+
* @returns Array of hydration rows or undefined if not found
|
|
37
|
+
* @typeParam TTable - Table definition type
|
|
38
|
+
*/
|
|
18
39
|
export const getHydrationRows = <TTable extends TableDef>(
|
|
19
40
|
meta: EntityMeta<TTable>,
|
|
20
41
|
relationName: string,
|
|
@@ -27,6 +48,14 @@ export const getHydrationRows = <TTable extends TableDef>(
|
|
|
27
48
|
return Array.isArray(rows) ? rows : undefined;
|
|
28
49
|
};
|
|
29
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Gets a single hydration record for a specific relation and key
|
|
53
|
+
* @param meta - Entity metadata
|
|
54
|
+
* @param relationName - Name of the relation
|
|
55
|
+
* @param key - Key to look up in the hydration map
|
|
56
|
+
* @returns Single hydration record or undefined if not found
|
|
57
|
+
* @typeParam TTable - Table definition type
|
|
58
|
+
*/
|
|
30
59
|
export const getHydrationRecord = <TTable extends TableDef>(
|
|
31
60
|
meta: EntityMeta<TTable>,
|
|
32
61
|
relationName: string,
|
|
@@ -42,11 +71,22 @@ export const getHydrationRecord = <TTable extends TableDef>(
|
|
|
42
71
|
return value;
|
|
43
72
|
};
|
|
44
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Gets entity metadata from an entity instance
|
|
76
|
+
* @param entity - Entity instance to get metadata from
|
|
77
|
+
* @returns Entity metadata or undefined if not found
|
|
78
|
+
* @typeParam TTable - Table definition type
|
|
79
|
+
*/
|
|
45
80
|
export const getEntityMeta = <TTable extends TableDef>(entity: any): EntityMeta<TTable> | undefined => {
|
|
46
81
|
if (!entity || typeof entity !== 'object') return undefined;
|
|
47
82
|
return (entity as any)[ENTITY_META];
|
|
48
83
|
};
|
|
49
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Checks if an entity has metadata attached
|
|
87
|
+
* @param entity - Entity instance to check
|
|
88
|
+
* @returns True if the entity has metadata, false otherwise
|
|
89
|
+
*/
|
|
50
90
|
export const hasEntityMeta = (entity: any): entity is { [ENTITY_META]: EntityMeta<TableDef> } => {
|
|
51
91
|
return Boolean(getEntityMeta(entity));
|
|
52
92
|
};
|
|
@@ -6,12 +6,7 @@ export type EntityConstructor = new (...args: any[]) => any;
|
|
|
6
6
|
export type EntityOrTableTarget = EntityConstructor | TableDef;
|
|
7
7
|
export type EntityOrTableTargetResolver = EntityOrTableTarget | (() => EntityOrTableTarget);
|
|
8
8
|
|
|
9
|
-
export
|
|
10
|
-
type: ColumnType;
|
|
11
|
-
args?: ColumnDef['args'];
|
|
12
|
-
primary?: boolean;
|
|
13
|
-
notNull?: boolean;
|
|
14
|
-
}
|
|
9
|
+
export type ColumnDefLike = Omit<ColumnDef, 'name' | 'table'>;
|
|
15
10
|
|
|
16
11
|
interface BaseRelationMetadata {
|
|
17
12
|
propertyKey: string;
|
package/src/orm/entity.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table.js';
|
|
2
|
-
import { Entity, RelationMap, HasManyCollection, HasOneReference, BelongsToReference, ManyToManyCollection } from '../schema/types.js';
|
|
3
|
-
import {
|
|
4
|
-
import { ENTITY_META, EntityMeta, getEntityMeta } from './entity-meta.js';
|
|
5
|
-
import { DefaultHasManyCollection } from './relations/has-many.js';
|
|
6
|
-
import { DefaultHasOneReference } from './relations/has-one.js';
|
|
7
|
-
import { DefaultBelongsToReference } from './relations/belongs-to.js';
|
|
8
|
-
import { DefaultManyToManyCollection } from './relations/many-to-many.js';
|
|
9
|
-
import { HasManyRelation, HasOneRelation, BelongsToRelation, BelongsToManyRelation, RelationKinds } from '../schema/relation.js';
|
|
10
|
-
import { loadHasManyRelation, loadHasOneRelation, loadBelongsToRelation, loadBelongsToManyRelation } from './lazy-batch.js';
|
|
2
|
+
import { Entity, RelationMap, HasManyCollection, HasOneReference, BelongsToReference, ManyToManyCollection } from '../schema/types.js';
|
|
3
|
+
import { EntityContext } from './entity-context.js';
|
|
4
|
+
import { ENTITY_META, EntityMeta, getEntityMeta } from './entity-meta.js';
|
|
5
|
+
import { DefaultHasManyCollection } from './relations/has-many.js';
|
|
6
|
+
import { DefaultHasOneReference } from './relations/has-one.js';
|
|
7
|
+
import { DefaultBelongsToReference } from './relations/belongs-to.js';
|
|
8
|
+
import { DefaultManyToManyCollection } from './relations/many-to-many.js';
|
|
9
|
+
import { HasManyRelation, HasOneRelation, BelongsToRelation, BelongsToManyRelation, RelationKinds } from '../schema/relation.js';
|
|
10
|
+
import { loadHasManyRelation, loadHasOneRelation, loadBelongsToRelation, loadBelongsToManyRelation } from './lazy-batch.js';
|
|
11
11
|
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
12
12
|
|
|
13
13
|
type Rows = Record<string, any>[];
|
|
@@ -45,7 +45,7 @@ export const createEntityProxy = <
|
|
|
45
45
|
TTable extends TableDef,
|
|
46
46
|
TLazy extends keyof RelationMap<TTable> = keyof RelationMap<TTable>
|
|
47
47
|
>(
|
|
48
|
-
ctx:
|
|
48
|
+
ctx: EntityContext,
|
|
49
49
|
table: TTable,
|
|
50
50
|
row: Record<string, any>,
|
|
51
51
|
lazyRelations: TLazy[] = [] as TLazy[]
|
|
@@ -105,7 +105,7 @@ export const createEntityProxy = <
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
export const createEntityFromRow = <TTable extends TableDef>(
|
|
108
|
-
ctx:
|
|
108
|
+
ctx: EntityContext,
|
|
109
109
|
table: TTable,
|
|
110
110
|
row: Record<string, any>,
|
|
111
111
|
lazyRelations: (keyof RelationMap<TTable>)[] = []
|
|
@@ -129,60 +129,60 @@ export const createEntityFromRow = <TTable extends TableDef>(
|
|
|
129
129
|
|
|
130
130
|
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
132
|
+
const populateHydrationCache = <TTable extends TableDef>(
|
|
133
|
+
entity: any,
|
|
134
|
+
row: Record<string, any>,
|
|
135
|
+
meta: EntityMeta<TTable>
|
|
136
|
+
): void => {
|
|
137
|
+
for (const relationName of Object.keys(meta.table.relations)) {
|
|
138
|
+
const relation = meta.table.relations[relationName];
|
|
139
|
+
const data = row[relationName];
|
|
140
|
+
if (relation.type === RelationKinds.HasOne) {
|
|
141
|
+
const localKey = relation.localKey || findPrimaryKey(meta.table);
|
|
142
|
+
const rootValue = entity[localKey];
|
|
143
|
+
if (rootValue === undefined || rootValue === null) continue;
|
|
144
|
+
if (!data || typeof data !== 'object') continue;
|
|
145
|
+
const cache = new Map<string, Record<string, any>>();
|
|
146
|
+
cache.set(toKey(rootValue), data as Record<string, any>);
|
|
147
|
+
meta.relationHydration.set(relationName, cache);
|
|
148
|
+
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!Array.isArray(data)) continue;
|
|
153
|
+
|
|
154
|
+
if (relation.type === RelationKinds.HasMany || relation.type === RelationKinds.BelongsToMany) {
|
|
155
|
+
const localKey = relation.localKey || findPrimaryKey(meta.table);
|
|
156
|
+
const rootValue = entity[localKey];
|
|
157
|
+
if (rootValue === undefined || rootValue === null) continue;
|
|
158
|
+
const cache = new Map<string, Rows>();
|
|
159
|
+
cache.set(toKey(rootValue), data as Rows);
|
|
160
|
+
meta.relationHydration.set(relationName, cache);
|
|
161
|
+
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (relation.type === RelationKinds.BelongsTo) {
|
|
166
|
+
const targetKey = relation.localKey || findPrimaryKey(relation.target);
|
|
167
|
+
const cache = new Map<string, Record<string, any>>();
|
|
168
|
+
for (const item of data) {
|
|
169
|
+
const pkValue = item[targetKey];
|
|
170
|
+
if (pkValue === undefined || pkValue === null) continue;
|
|
171
|
+
cache.set(toKey(pkValue), item);
|
|
172
|
+
}
|
|
173
|
+
if (cache.size) {
|
|
160
174
|
meta.relationHydration.set(relationName, cache);
|
|
161
175
|
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (relation.type === RelationKinds.BelongsTo) {
|
|
166
|
-
const targetKey = relation.localKey || findPrimaryKey(relation.target);
|
|
167
|
-
const cache = new Map<string, Record<string, any>>();
|
|
168
|
-
for (const item of data) {
|
|
169
|
-
const pkValue = item[targetKey];
|
|
170
|
-
if (pkValue === undefined || pkValue === null) continue;
|
|
171
|
-
cache.set(toKey(pkValue), item);
|
|
172
|
-
}
|
|
173
|
-
if (cache.size) {
|
|
174
|
-
meta.relationHydration.set(relationName, cache);
|
|
175
|
-
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
176
|
-
}
|
|
177
176
|
}
|
|
178
177
|
}
|
|
179
|
-
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
180
|
|
|
181
|
-
const getRelationWrapper = (
|
|
182
|
-
meta: EntityMeta<any>,
|
|
183
|
-
relationName: string,
|
|
184
|
-
owner: any
|
|
185
|
-
): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
|
|
181
|
+
const getRelationWrapper = (
|
|
182
|
+
meta: EntityMeta<any>,
|
|
183
|
+
relationName: string,
|
|
184
|
+
owner: any
|
|
185
|
+
): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
|
|
186
186
|
if (meta.relationWrappers.has(relationName)) {
|
|
187
187
|
return meta.relationWrappers.get(relationName) as HasManyCollection<any>;
|
|
188
188
|
}
|
|
@@ -198,34 +198,34 @@ const getRelationWrapper = (
|
|
|
198
198
|
return wrapper;
|
|
199
199
|
};
|
|
200
200
|
|
|
201
|
-
const instantiateWrapper = (
|
|
202
|
-
meta: EntityMeta<any>,
|
|
203
|
-
relationName: string,
|
|
204
|
-
relation: HasManyRelation | HasOneRelation | BelongsToRelation | BelongsToManyRelation,
|
|
205
|
-
owner: any
|
|
206
|
-
): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
|
|
207
|
-
switch (relation.type) {
|
|
208
|
-
case RelationKinds.HasOne: {
|
|
209
|
-
const hasOne = relation as HasOneRelation;
|
|
210
|
-
const localKey = hasOne.localKey || findPrimaryKey(meta.table);
|
|
211
|
-
const loader = () => relationLoaderCache(meta, relationName, () =>
|
|
212
|
-
loadHasOneRelation(meta.ctx, meta.table, relationName, hasOne)
|
|
213
|
-
);
|
|
214
|
-
return new DefaultHasOneReference(
|
|
215
|
-
meta.ctx,
|
|
216
|
-
meta,
|
|
217
|
-
owner,
|
|
218
|
-
relationName,
|
|
219
|
-
hasOne,
|
|
220
|
-
meta.table,
|
|
221
|
-
loader,
|
|
222
|
-
(row: Record<string, any>) => createEntityFromRow(meta.ctx, hasOne.target, row),
|
|
223
|
-
localKey
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
case RelationKinds.HasMany: {
|
|
227
|
-
const hasMany = relation as HasManyRelation;
|
|
228
|
-
const localKey = hasMany.localKey || findPrimaryKey(meta.table);
|
|
201
|
+
const instantiateWrapper = (
|
|
202
|
+
meta: EntityMeta<any>,
|
|
203
|
+
relationName: string,
|
|
204
|
+
relation: HasManyRelation | HasOneRelation | BelongsToRelation | BelongsToManyRelation,
|
|
205
|
+
owner: any
|
|
206
|
+
): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
|
|
207
|
+
switch (relation.type) {
|
|
208
|
+
case RelationKinds.HasOne: {
|
|
209
|
+
const hasOne = relation as HasOneRelation;
|
|
210
|
+
const localKey = hasOne.localKey || findPrimaryKey(meta.table);
|
|
211
|
+
const loader = () => relationLoaderCache(meta, relationName, () =>
|
|
212
|
+
loadHasOneRelation(meta.ctx, meta.table, relationName, hasOne)
|
|
213
|
+
);
|
|
214
|
+
return new DefaultHasOneReference(
|
|
215
|
+
meta.ctx,
|
|
216
|
+
meta,
|
|
217
|
+
owner,
|
|
218
|
+
relationName,
|
|
219
|
+
hasOne,
|
|
220
|
+
meta.table,
|
|
221
|
+
loader,
|
|
222
|
+
(row: Record<string, any>) => createEntityFromRow(meta.ctx, hasOne.target, row),
|
|
223
|
+
localKey
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
case RelationKinds.HasMany: {
|
|
227
|
+
const hasMany = relation as HasManyRelation;
|
|
228
|
+
const localKey = hasMany.localKey || findPrimaryKey(meta.table);
|
|
229
229
|
const loader = () => relationLoaderCache(meta, relationName, () =>
|
|
230
230
|
loadHasManyRelation(meta.ctx, meta.table, relationName, hasMany)
|
|
231
231
|
);
|