metal-orm 1.0.57 → 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 (46) hide show
  1. package/README.md +23 -13
  2. package/dist/index.cjs +1750 -733
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +244 -157
  5. package/dist/index.d.ts +244 -157
  6. package/dist/index.js +1745 -733
  7. package/dist/index.js.map +1 -1
  8. package/package.json +69 -69
  9. package/src/core/ddl/schema-generator.ts +44 -1
  10. package/src/decorators/bootstrap.ts +186 -113
  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 +18 -13
  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 -343
  21. package/src/orm/execute.ts +87 -20
  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 -309
  28. package/src/orm/relations/belongs-to.ts +2 -2
  29. package/src/orm/relations/has-many.ts +23 -9
  30. package/src/orm/relations/has-one.ts +2 -2
  31. package/src/orm/relations/many-to-many.ts +29 -14
  32. package/src/orm/save-graph-types.ts +2 -2
  33. package/src/orm/save-graph.ts +18 -18
  34. package/src/query-builder/relation-conditions.ts +80 -59
  35. package/src/query-builder/relation-cte-builder.ts +63 -0
  36. package/src/query-builder/relation-filter-utils.ts +159 -0
  37. package/src/query-builder/relation-include-strategies.ts +177 -0
  38. package/src/query-builder/relation-join-planner.ts +80 -0
  39. package/src/query-builder/relation-service.ts +103 -159
  40. package/src/query-builder/relation-types.ts +43 -12
  41. package/src/query-builder/select/projection-facet.ts +23 -23
  42. package/src/query-builder/select/select-operations.ts +145 -0
  43. package/src/query-builder/select.ts +373 -426
  44. package/src/schema/relation.ts +22 -18
  45. package/src/schema/table.ts +22 -9
  46. package/src/schema/types.ts +103 -84
@@ -1,34 +1,5 @@
1
1
  import { ColumnDefLike, RelationMetadata } from '../orm/entity-metadata.js';
2
2
 
3
- /**
4
- * Context object provided by standard decorators in newer TypeScript versions.
5
- */
6
- export interface StandardDecoratorContext {
7
- kind: string;
8
- name?: string | symbol;
9
- metadata?: Record<PropertyKey, unknown>;
10
- static?: boolean;
11
- private?: boolean;
12
- }
13
-
14
- /**
15
- * Dual-mode property decorator that supports both legacy and standard decorator syntax.
16
- */
17
- export interface DualModePropertyDecorator {
18
- (target: object, propertyKey: string | symbol): void;
19
- (value: unknown, context: StandardDecoratorContext): void;
20
- }
21
-
22
- /**
23
- * Dual-mode class decorator that supports both legacy and standard decorator syntax.
24
- */
25
- export interface DualModeClassDecorator {
26
- // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
27
- <TFunction extends Function>(value: TFunction): void | TFunction;
28
- // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
29
- <TFunction extends Function>(value: TFunction, context: StandardDecoratorContext): void | TFunction;
30
- }
31
-
32
3
  /**
33
4
  * Bag for storing decorator metadata during the decoration phase.
34
5
  */
@@ -39,37 +10,31 @@ export interface DecoratorMetadataBag {
39
10
 
40
11
  const METADATA_KEY = 'metal-orm:decorators';
41
12
 
42
- /**
43
- * Checks if a value is a StandardDecoratorContext.
44
- * @param value - The value to check.
45
- * @returns True if the value is a StandardDecoratorContext.
46
- */
47
- export const isStandardDecoratorContext = (value: unknown): value is StandardDecoratorContext => {
48
- return typeof value === 'object' && value !== null && 'kind' in (value as object);
13
+ type MetadataCarrier = {
14
+ metadata?: Record<PropertyKey, unknown>;
49
15
  };
50
16
 
51
17
  /**
52
18
  * Gets or creates a metadata bag for the given decorator context.
53
- * @param context - The decorator context.
19
+ * @param context - The decorator context with metadata support.
54
20
  * @returns The metadata bag.
55
21
  */
56
- export const getOrCreateMetadataBag = (context: StandardDecoratorContext): DecoratorMetadataBag => {
22
+ export const getOrCreateMetadataBag = (context: MetadataCarrier): DecoratorMetadataBag => {
57
23
  const metadata = context.metadata || (context.metadata = {} as Record<PropertyKey, unknown>);
58
- const existing = metadata[METADATA_KEY] as DecoratorMetadataBag | undefined;
59
- if (existing) {
60
- return existing;
24
+ let bag = metadata[METADATA_KEY] as DecoratorMetadataBag | undefined;
25
+ if (!bag) {
26
+ bag = { columns: [], relations: [] };
27
+ metadata[METADATA_KEY] = bag;
61
28
  }
62
- const bag: DecoratorMetadataBag = { columns: [], relations: [] };
63
- metadata[METADATA_KEY] = bag;
64
29
  return bag;
65
30
  };
66
31
 
67
32
  /**
68
33
  * Reads the metadata bag from the given decorator context.
69
- * @param context - The decorator context.
34
+ * @param context - The decorator context with metadata support.
70
35
  * @returns The metadata bag if present.
71
36
  */
72
- export const readMetadataBag = (context: StandardDecoratorContext): DecoratorMetadataBag | undefined => {
37
+ export const readMetadataBag = (context: MetadataCarrier): DecoratorMetadataBag | undefined => {
73
38
  return context.metadata?.[METADATA_KEY] as DecoratorMetadataBag | undefined;
74
39
  };
75
40
 
@@ -87,7 +52,6 @@ export const readMetadataBagFromConstructor = (ctor: object): DecoratorMetadataB
87
52
 
88
53
  /**
89
54
  * Public helper to read decorator metadata from a class constructor.
90
- * Standard decorators only; legacy metadata is intentionally ignored.
91
55
  * @param ctor - The entity constructor.
92
56
  * @returns The metadata bag if present.
93
57
  */
@@ -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,25 +1,30 @@
1
- import { TableDef } from '../schema/table.js';
1
+ import { TableDef } from '../schema/table.js';
2
+ import { RelationIncludeOptions } from '../query-builder/relation-types.js';
2
3
  import { EntityContext } from './entity-context.js';
3
- import { RelationMap } from '../schema/types.js';
4
+ import { RelationMap } from '../schema/types.js';
4
5
 
5
6
  /**
6
7
  * Symbol used to store entity metadata on entity instances
7
8
  */
8
- export const ENTITY_META = Symbol('EntityMeta');
9
-
10
- const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
11
-
12
- /**
13
- * Metadata stored on entity instances for ORM internal use
14
- * @typeParam TTable - Table definition type
15
- */
16
- 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> {
17
20
  /** Entity context */
18
21
  ctx: EntityContext;
19
22
  /** Table definition */
20
23
  table: TTable;
21
- /** Relations that should be loaded lazily */
22
- lazyRelations: (keyof RelationMap<TTable>)[];
24
+ /** Relations that should be loaded lazily */
25
+ lazyRelations: RelationKey<TTable>[];
26
+ /** Include options for lazy relations */
27
+ lazyRelationOptions: Map<string, RelationIncludeOptions>;
23
28
  /** Cache for relation promises */
24
29
  relationCache: Map<string, Promise<unknown>>;
25
30
  /** Hydration data for relations */