metal-orm 1.0.58 → 1.0.59
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 +34 -31
- package/dist/index.cjs +1463 -1003
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +148 -129
- package/dist/index.d.ts +148 -129
- package/dist/index.js +1459 -1003
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ddl/schema-generator.ts +44 -1
- package/src/decorators/bootstrap.ts +183 -146
- package/src/decorators/column-decorator.ts +8 -49
- package/src/decorators/decorator-metadata.ts +10 -46
- package/src/decorators/entity.ts +30 -40
- package/src/decorators/relations.ts +30 -56
- package/src/orm/entity-hydration.ts +72 -0
- package/src/orm/entity-meta.ts +13 -11
- package/src/orm/entity-metadata.ts +240 -238
- package/src/orm/entity-relation-cache.ts +39 -0
- package/src/orm/entity-relations.ts +207 -0
- package/src/orm/entity.ts +124 -410
- package/src/orm/execute.ts +4 -4
- package/src/orm/lazy-batch/belongs-to-many.ts +134 -0
- package/src/orm/lazy-batch/belongs-to.ts +108 -0
- package/src/orm/lazy-batch/has-many.ts +69 -0
- package/src/orm/lazy-batch/has-one.ts +68 -0
- package/src/orm/lazy-batch/shared.ts +125 -0
- package/src/orm/lazy-batch.ts +4 -492
- package/src/orm/relations/many-to-many.ts +2 -1
- package/src/query-builder/relation-cte-builder.ts +63 -0
- package/src/query-builder/relation-filter-utils.ts +159 -0
- package/src/query-builder/relation-include-strategies.ts +177 -0
- package/src/query-builder/relation-join-planner.ts +80 -0
- package/src/query-builder/relation-service.ts +119 -479
- package/src/query-builder/relation-types.ts +41 -10
- package/src/query-builder/select/projection-facet.ts +23 -23
- package/src/query-builder/select/select-operations.ts +145 -0
- package/src/query-builder/select.ts +351 -422
- package/src/schema/relation.ts +22 -18
- package/src/schema/table.ts +22 -9
- package/src/schema/types.ts +14 -12
package/src/decorators/entity.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
ensureEntityMetadata,
|
|
8
8
|
setEntityTableName
|
|
9
9
|
} from '../orm/entity-metadata.js';
|
|
10
|
-
import {
|
|
10
|
+
import { readMetadataBag } from './decorator-metadata.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Options for defining an entity.
|
|
@@ -43,51 +43,41 @@ const deriveTableNameFromConstructor = (ctor: EntityConstructor<unknown>): strin
|
|
|
43
43
|
* @returns A class decorator that registers the entity metadata.
|
|
44
44
|
*/
|
|
45
45
|
export function Entity(options: EntityOptions = {}) {
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
46
|
+
return function <T extends EntityConstructor>(value: T, context: ClassDecoratorContext): T {
|
|
47
|
+
const ctor = value;
|
|
48
|
+
const tableName = options.tableName ?? deriveTableNameFromConstructor(ctor);
|
|
49
|
+
setEntityTableName(ctor, tableName, options.hooks);
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const bag = readMetadataBag(context);
|
|
59
|
-
if (bag) {
|
|
60
|
-
const meta = ensureEntityMetadata(ctor);
|
|
61
|
-
for (const entry of bag.columns) {
|
|
62
|
-
if (meta.columns[entry.propertyName]) {
|
|
63
|
-
throw new Error(
|
|
64
|
-
`Column '${entry.propertyName}' is already defined on entity '${ctor.name}'.`
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
addColumnMetadata(ctor, entry.propertyName, { ...entry.column });
|
|
51
|
+
const bag = readMetadataBag(context);
|
|
52
|
+
if (bag) {
|
|
53
|
+
const meta = ensureEntityMetadata(ctor);
|
|
54
|
+
for (const entry of bag.columns) {
|
|
55
|
+
if (meta.columns[entry.propertyName]) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Column '${entry.propertyName}' is already defined on entity '${ctor.name}'.`
|
|
58
|
+
);
|
|
68
59
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
entry.relation.kind === RelationKinds.BelongsToMany
|
|
77
|
-
? {
|
|
78
|
-
...entry.relation,
|
|
79
|
-
defaultPivotColumns: entry.relation.defaultPivotColumns
|
|
80
|
-
? [...entry.relation.defaultPivotColumns]
|
|
81
|
-
: undefined
|
|
82
|
-
}
|
|
83
|
-
: { ...entry.relation };
|
|
84
|
-
addRelationMetadata(ctor, entry.propertyName, relationCopy);
|
|
60
|
+
addColumnMetadata(ctor, entry.propertyName, { ...entry.column });
|
|
61
|
+
}
|
|
62
|
+
for (const entry of bag.relations) {
|
|
63
|
+
if (meta.relations[entry.propertyName]) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Relation '${entry.propertyName}' is already defined on entity '${ctor.name}'.`
|
|
66
|
+
);
|
|
85
67
|
}
|
|
68
|
+
const relationCopy =
|
|
69
|
+
entry.relation.kind === RelationKinds.BelongsToMany
|
|
70
|
+
? {
|
|
71
|
+
...entry.relation,
|
|
72
|
+
defaultPivotColumns: entry.relation.defaultPivotColumns
|
|
73
|
+
? [...entry.relation.defaultPivotColumns]
|
|
74
|
+
: undefined
|
|
75
|
+
}
|
|
76
|
+
: { ...entry.relation };
|
|
77
|
+
addRelationMetadata(ctor, entry.propertyName, relationCopy);
|
|
86
78
|
}
|
|
87
79
|
}
|
|
88
80
|
|
|
89
81
|
return ctor;
|
|
90
82
|
};
|
|
91
|
-
|
|
92
|
-
return decoratorWithContext;
|
|
93
83
|
}
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import { CascadeMode, RelationKinds } from '../schema/relation.js';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
EntityConstructor,
|
|
3
|
+
EntityOrTableTarget,
|
|
5
4
|
EntityOrTableTargetResolver,
|
|
6
5
|
RelationMetadata
|
|
7
6
|
} from '../orm/entity-metadata.js';
|
|
8
|
-
import {
|
|
9
|
-
DualModePropertyDecorator,
|
|
10
|
-
getOrCreateMetadataBag,
|
|
11
|
-
isStandardDecoratorContext,
|
|
12
|
-
StandardDecoratorContext
|
|
13
|
-
} from './decorator-metadata.js';
|
|
7
|
+
import { getOrCreateMetadataBag } from './decorator-metadata.js';
|
|
14
8
|
|
|
15
9
|
interface BaseRelationOptions {
|
|
16
10
|
target: EntityOrTableTargetResolver;
|
|
@@ -22,31 +16,34 @@ interface BaseRelationOptions {
|
|
|
22
16
|
* Options for HasMany relation.
|
|
23
17
|
*/
|
|
24
18
|
export interface HasManyOptions extends BaseRelationOptions {
|
|
25
|
-
foreignKey
|
|
19
|
+
foreignKey?: string;
|
|
26
20
|
}
|
|
27
21
|
|
|
28
22
|
/**
|
|
29
23
|
* Options for HasOne relation.
|
|
30
24
|
*/
|
|
31
25
|
export interface HasOneOptions extends BaseRelationOptions {
|
|
32
|
-
foreignKey
|
|
26
|
+
foreignKey?: string;
|
|
33
27
|
}
|
|
34
28
|
|
|
35
29
|
/**
|
|
36
30
|
* Options for BelongsTo relation.
|
|
37
31
|
*/
|
|
38
32
|
export interface BelongsToOptions extends BaseRelationOptions {
|
|
39
|
-
foreignKey
|
|
33
|
+
foreignKey?: string;
|
|
40
34
|
}
|
|
41
35
|
|
|
42
36
|
/**
|
|
43
37
|
* Options for BelongsToMany relation.
|
|
44
38
|
*/
|
|
45
|
-
export interface BelongsToManyOptions
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
39
|
+
export interface BelongsToManyOptions<
|
|
40
|
+
TTarget extends EntityOrTableTarget = EntityOrTableTarget,
|
|
41
|
+
TPivot extends EntityOrTableTarget = EntityOrTableTarget
|
|
42
|
+
> {
|
|
43
|
+
target: EntityOrTableTargetResolver<TTarget>;
|
|
44
|
+
pivotTable: EntityOrTableTargetResolver<TPivot>;
|
|
45
|
+
pivotForeignKeyToRoot?: string;
|
|
46
|
+
pivotForeignKeyToTarget?: string;
|
|
50
47
|
localKey?: string;
|
|
51
48
|
targetKey?: string;
|
|
52
49
|
pivotPrimaryKey?: string;
|
|
@@ -61,48 +58,22 @@ const normalizePropertyName = (name: string | symbol): string => {
|
|
|
61
58
|
return name;
|
|
62
59
|
};
|
|
63
60
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (instanceOrCtor && typeof (instanceOrCtor as { constructor: new (...args: unknown[]) => unknown }).constructor === 'function') {
|
|
69
|
-
return (instanceOrCtor as { constructor: new (...args: unknown[]) => unknown }).constructor as EntityConstructor;
|
|
70
|
-
}
|
|
71
|
-
return undefined;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const registerRelation = (ctor: EntityConstructor, propertyName: string, metadata: RelationMetadata): void => {
|
|
75
|
-
addRelationMetadata(ctor, propertyName, metadata);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const createFieldDecorator = (
|
|
79
|
-
metadataFactory: (propertyName: string) => RelationMetadata
|
|
80
|
-
) => {
|
|
81
|
-
const decorator: DualModePropertyDecorator = (targetOrValue, propertyKeyOrContext) => {
|
|
82
|
-
if (isStandardDecoratorContext(propertyKeyOrContext)) {
|
|
83
|
-
const ctx = propertyKeyOrContext as StandardDecoratorContext;
|
|
84
|
-
if (!ctx.name) {
|
|
85
|
-
throw new Error('Relation decorator requires a property name');
|
|
86
|
-
}
|
|
87
|
-
const propertyName = normalizePropertyName(ctx.name);
|
|
88
|
-
const bag = getOrCreateMetadataBag(ctx);
|
|
89
|
-
const relationMetadata = metadataFactory(propertyName);
|
|
90
|
-
|
|
91
|
-
if (!bag.relations.some(entry => entry.propertyName === propertyName)) {
|
|
92
|
-
bag.relations.push({ propertyName, relation: relationMetadata });
|
|
93
|
-
}
|
|
94
|
-
return;
|
|
61
|
+
const createFieldDecorator = (metadataFactory: (propertyName: string) => RelationMetadata) => {
|
|
62
|
+
return function (_value: unknown, context: ClassFieldDecoratorContext) {
|
|
63
|
+
if (!context.name) {
|
|
64
|
+
throw new Error('Relation decorator requires a property name');
|
|
95
65
|
}
|
|
66
|
+
if (context.private) {
|
|
67
|
+
throw new Error('Relation decorator does not support private fields');
|
|
68
|
+
}
|
|
69
|
+
const propertyName = normalizePropertyName(context.name);
|
|
70
|
+
const bag = getOrCreateMetadataBag(context);
|
|
71
|
+
const relationMetadata = metadataFactory(propertyName);
|
|
96
72
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (!ctor) {
|
|
100
|
-
throw new Error('Unable to resolve constructor when registering relation metadata');
|
|
73
|
+
if (!bag.relations.some(entry => entry.propertyName === propertyName)) {
|
|
74
|
+
bag.relations.push({ propertyName, relation: relationMetadata });
|
|
101
75
|
}
|
|
102
|
-
registerRelation(ctor, propertyName, metadataFactory(propertyName));
|
|
103
76
|
};
|
|
104
|
-
|
|
105
|
-
return decorator;
|
|
106
77
|
};
|
|
107
78
|
|
|
108
79
|
/**
|
|
@@ -147,7 +118,7 @@ export function BelongsTo(options: BelongsToOptions) {
|
|
|
147
118
|
kind: RelationKinds.BelongsTo,
|
|
148
119
|
propertyKey: propertyName,
|
|
149
120
|
target: options.target,
|
|
150
|
-
foreignKey: options.foreignKey
|
|
121
|
+
foreignKey: options.foreignKey ?? `${propertyName}_id`,
|
|
151
122
|
localKey: options.localKey,
|
|
152
123
|
cascade: options.cascade
|
|
153
124
|
}));
|
|
@@ -158,7 +129,10 @@ export function BelongsTo(options: BelongsToOptions) {
|
|
|
158
129
|
* @param options - The relation options.
|
|
159
130
|
* @returns A property decorator that registers the relation metadata.
|
|
160
131
|
*/
|
|
161
|
-
export function BelongsToMany
|
|
132
|
+
export function BelongsToMany<
|
|
133
|
+
TTarget extends EntityOrTableTarget = EntityOrTableTarget,
|
|
134
|
+
TPivot extends EntityOrTableTarget = EntityOrTableTarget
|
|
135
|
+
>(options: BelongsToManyOptions<TTarget, TPivot>) {
|
|
162
136
|
return createFieldDecorator(propertyName => ({
|
|
163
137
|
kind: RelationKinds.BelongsToMany,
|
|
164
138
|
propertyKey: propertyName,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { TableDef } from '../schema/table.js';
|
|
2
|
+
import { RelationKinds } from '../schema/relation.js';
|
|
3
|
+
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
4
|
+
import { EntityMeta } from './entity-meta.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Type representing an array of database rows.
|
|
8
|
+
*/
|
|
9
|
+
type Rows = Record<string, unknown>[];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Converts a value to a string key.
|
|
13
|
+
* @param value - The value to convert
|
|
14
|
+
* @returns String representation of the value
|
|
15
|
+
*/
|
|
16
|
+
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Populates the hydration cache with relation data from the database row.
|
|
20
|
+
* @template TTable - The table type
|
|
21
|
+
* @param entity - The entity instance
|
|
22
|
+
* @param row - The database row
|
|
23
|
+
* @param meta - The entity metadata
|
|
24
|
+
*/
|
|
25
|
+
export const populateHydrationCache = <TTable extends TableDef>(
|
|
26
|
+
entity: Record<string, unknown>,
|
|
27
|
+
row: Record<string, unknown>,
|
|
28
|
+
meta: EntityMeta<TTable>
|
|
29
|
+
): void => {
|
|
30
|
+
for (const relationName of Object.keys(meta.table.relations)) {
|
|
31
|
+
const relation = meta.table.relations[relationName];
|
|
32
|
+
const data = row[relationName];
|
|
33
|
+
if (relation.type === RelationKinds.HasOne) {
|
|
34
|
+
const localKey = relation.localKey || findPrimaryKey(meta.table);
|
|
35
|
+
const rootValue = entity[localKey];
|
|
36
|
+
if (rootValue === undefined || rootValue === null) continue;
|
|
37
|
+
if (!data || typeof data !== 'object') continue;
|
|
38
|
+
const cache = new Map<string, Record<string, unknown>>();
|
|
39
|
+
cache.set(toKey(rootValue), data as Record<string, unknown>);
|
|
40
|
+
meta.relationHydration.set(relationName, cache);
|
|
41
|
+
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!Array.isArray(data)) continue;
|
|
46
|
+
|
|
47
|
+
if (relation.type === RelationKinds.HasMany || relation.type === RelationKinds.BelongsToMany) {
|
|
48
|
+
const localKey = relation.localKey || findPrimaryKey(meta.table);
|
|
49
|
+
const rootValue = entity[localKey];
|
|
50
|
+
if (rootValue === undefined || rootValue === null) continue;
|
|
51
|
+
const cache = new Map<string, Rows>();
|
|
52
|
+
cache.set(toKey(rootValue), data as Rows);
|
|
53
|
+
meta.relationHydration.set(relationName, cache);
|
|
54
|
+
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (relation.type === RelationKinds.BelongsTo) {
|
|
59
|
+
const targetKey = relation.localKey || findPrimaryKey(relation.target);
|
|
60
|
+
const cache = new Map<string, Record<string, unknown>>();
|
|
61
|
+
for (const item of data) {
|
|
62
|
+
const pkValue = item[targetKey];
|
|
63
|
+
if (pkValue === undefined || pkValue === null) continue;
|
|
64
|
+
cache.set(toKey(pkValue), item);
|
|
65
|
+
}
|
|
66
|
+
if (cache.size) {
|
|
67
|
+
meta.relationHydration.set(relationName, cache);
|
|
68
|
+
meta.relationCache.set(relationName, Promise.resolve(cache));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
package/src/orm/entity-meta.ts
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table.js';
|
|
2
2
|
import { RelationIncludeOptions } from '../query-builder/relation-types.js';
|
|
3
3
|
import { EntityContext } from './entity-context.js';
|
|
4
|
-
import { RelationMap } from '../schema/types.js';
|
|
4
|
+
import { RelationMap } from '../schema/types.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Symbol used to store entity metadata on entity instances
|
|
8
8
|
*/
|
|
9
|
-
export const ENTITY_META = Symbol('EntityMeta');
|
|
10
|
-
|
|
11
|
-
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
export const ENTITY_META = Symbol('EntityMeta');
|
|
10
|
+
|
|
11
|
+
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
12
|
+
|
|
13
|
+
export type RelationKey<TTable extends TableDef> = Extract<keyof RelationMap<TTable>, string>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Metadata stored on entity instances for ORM internal use
|
|
17
|
+
* @typeParam TTable - Table definition type
|
|
18
|
+
*/
|
|
19
|
+
export interface EntityMeta<TTable extends TableDef> {
|
|
18
20
|
/** Entity context */
|
|
19
21
|
ctx: EntityContext;
|
|
20
22
|
/** Table definition */
|
|
21
23
|
table: TTable;
|
|
22
24
|
/** Relations that should be loaded lazily */
|
|
23
|
-
lazyRelations:
|
|
25
|
+
lazyRelations: RelationKey<TTable>[];
|
|
24
26
|
/** Include options for lazy relations */
|
|
25
27
|
lazyRelationOptions: Map<string, RelationIncludeOptions>;
|
|
26
28
|
/** Cache for relation promises */
|