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.
Files changed (40) hide show
  1. package/README.md +34 -31
  2. package/dist/index.cjs +1463 -1003
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +148 -129
  5. package/dist/index.d.ts +148 -129
  6. package/dist/index.js +1459 -1003
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/core/ddl/schema-generator.ts +44 -1
  10. package/src/decorators/bootstrap.ts +183 -146
  11. package/src/decorators/column-decorator.ts +8 -49
  12. package/src/decorators/decorator-metadata.ts +10 -46
  13. package/src/decorators/entity.ts +30 -40
  14. package/src/decorators/relations.ts +30 -56
  15. package/src/orm/entity-hydration.ts +72 -0
  16. package/src/orm/entity-meta.ts +13 -11
  17. package/src/orm/entity-metadata.ts +240 -238
  18. package/src/orm/entity-relation-cache.ts +39 -0
  19. package/src/orm/entity-relations.ts +207 -0
  20. package/src/orm/entity.ts +124 -410
  21. package/src/orm/execute.ts +4 -4
  22. package/src/orm/lazy-batch/belongs-to-many.ts +134 -0
  23. package/src/orm/lazy-batch/belongs-to.ts +108 -0
  24. package/src/orm/lazy-batch/has-many.ts +69 -0
  25. package/src/orm/lazy-batch/has-one.ts +68 -0
  26. package/src/orm/lazy-batch/shared.ts +125 -0
  27. package/src/orm/lazy-batch.ts +4 -492
  28. package/src/orm/relations/many-to-many.ts +2 -1
  29. package/src/query-builder/relation-cte-builder.ts +63 -0
  30. package/src/query-builder/relation-filter-utils.ts +159 -0
  31. package/src/query-builder/relation-include-strategies.ts +177 -0
  32. package/src/query-builder/relation-join-planner.ts +80 -0
  33. package/src/query-builder/relation-service.ts +119 -479
  34. package/src/query-builder/relation-types.ts +41 -10
  35. package/src/query-builder/select/projection-facet.ts +23 -23
  36. package/src/query-builder/select/select-operations.ts +145 -0
  37. package/src/query-builder/select.ts +351 -422
  38. package/src/schema/relation.ts +22 -18
  39. package/src/schema/table.ts +22 -9
  40. package/src/schema/types.ts +14 -12
@@ -7,7 +7,7 @@ import {
7
7
  ensureEntityMetadata,
8
8
  setEntityTableName
9
9
  } from '../orm/entity-metadata.js';
10
- import { DualModeClassDecorator, isStandardDecoratorContext, readMetadataBag } from './decorator-metadata.js';
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
- const decorator: DualModeClassDecorator = value => {
47
- const tableName = options.tableName ?? deriveTableNameFromConstructor(value);
48
- setEntityTableName(value as EntityConstructor, tableName, options.hooks);
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
- return value;
51
- };
52
-
53
- const decoratorWithContext: DualModeClassDecorator = (value, context?) => {
54
- const ctor = value as EntityConstructor;
55
- decorator(ctor);
56
-
57
- if (context && isStandardDecoratorContext(context)) {
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
- for (const entry of bag.relations) {
70
- if (meta.relations[entry.propertyName]) {
71
- throw new Error(
72
- `Relation '${entry.propertyName}' is already defined on entity '${ctor.name}'.`
73
- );
74
- }
75
- const relationCopy =
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
- addRelationMetadata,
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: string;
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: string;
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: string;
33
+ foreignKey?: string;
40
34
  }
41
35
 
42
36
  /**
43
37
  * Options for BelongsToMany relation.
44
38
  */
45
- export interface BelongsToManyOptions {
46
- target: EntityOrTableTargetResolver;
47
- pivotTable: EntityOrTableTargetResolver;
48
- pivotForeignKeyToRoot: string;
49
- pivotForeignKeyToTarget: string;
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 resolveConstructor = (instanceOrCtor: unknown): EntityConstructor | undefined => {
65
- if (typeof instanceOrCtor === 'function') {
66
- return instanceOrCtor as EntityConstructor;
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
- const propertyName = normalizePropertyName(propertyKeyOrContext);
98
- const ctor = resolveConstructor(targetOrValue);
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(options: BelongsToManyOptions) {
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
+ };
@@ -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
- * Metadata stored on entity instances for ORM internal use
15
- * @typeParam TTable - Table definition type
16
- */
17
- export interface EntityMeta<TTable extends TableDef> {
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: (keyof RelationMap<TTable>)[];
25
+ lazyRelations: RelationKey<TTable>[];
24
26
  /** Include options for lazy relations */
25
27
  lazyRelationOptions: Map<string, RelationIncludeOptions>;
26
28
  /** Cache for relation promises */