metal-orm 1.1.7 → 1.1.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -1,203 +1,204 @@
1
- import { detectTreeTable, mapTreeTables } from './tree-detection.mjs';
2
-
3
- const normalizeName = name => (typeof name === 'string' && name.includes('.') ? name.split('.').pop() : name);
4
-
5
- export const mapRelations = (tables, naming) => {
6
- const relationMap = new Map();
7
- const relationKeys = new Map();
8
- const fkIndex = new Map();
9
- const uniqueSingleColumns = new Map();
10
-
11
- for (const table of tables) {
12
- relationMap.set(table.name, []);
13
- relationKeys.set(table.name, new Set());
14
- for (const col of table.columns) {
15
- if (col.references) {
16
- const list = fkIndex.get(table.name) || [];
17
- list.push(col);
18
- fkIndex.set(table.name, list);
19
- }
20
- }
21
-
22
- const uniqueCols = new Set();
23
- if (Array.isArray(table.primaryKey) && table.primaryKey.length === 1) {
24
- uniqueCols.add(table.primaryKey[0]);
25
- }
26
- for (const col of table.columns) {
27
- if (col.unique) uniqueCols.add(col.name);
28
- }
29
- for (const idx of table.indexes || []) {
30
- if (!idx?.unique) continue;
31
- if (!Array.isArray(idx.columns) || idx.columns.length !== 1 || !idx.columns[0]?.column) continue;
32
- const columnName = idx.columns[0].column;
33
- if (idx.where) {
34
- const predicate = String(idx.where);
35
- const isNotNullOnly = new RegExp(`\\b${columnName}\\b\\s+is\\s+not\\s+null\\b`, 'i').test(predicate);
36
- if (!isNotNullOnly) continue;
37
- }
38
- uniqueCols.add(columnName);
39
- }
40
- uniqueSingleColumns.set(table.name, uniqueCols);
41
- }
42
-
43
- const findTable = name => {
44
- const norm = normalizeName(name);
45
- return tables.find(t => t.name === name || t.name === norm);
46
- };
47
-
1
+ import { detectTreeTable, mapTreeTables } from './tree-detection.mjs';
2
+
3
+ const normalizeName = name => (typeof name === 'string' && name.includes('.') ? name.split('.').pop() : name);
4
+
5
+ export const mapRelations = (tables, naming) => {
6
+ const relationMap = new Map();
7
+ const relationKeys = new Map();
8
+ const fkIndex = new Map();
9
+ const uniqueSingleColumns = new Map();
10
+
11
+ for (const table of tables) {
12
+ relationMap.set(table.name, []);
13
+ relationKeys.set(table.name, new Set());
14
+ for (const col of table.columns) {
15
+ if (col.references) {
16
+ const list = fkIndex.get(table.name) || [];
17
+ list.push(col);
18
+ fkIndex.set(table.name, list);
19
+ }
20
+ }
21
+
22
+ const uniqueCols = new Set();
23
+ if (Array.isArray(table.primaryKey) && table.primaryKey.length === 1) {
24
+ uniqueCols.add(table.primaryKey[0]);
25
+ }
26
+ for (const col of table.columns) {
27
+ if (col.unique) uniqueCols.add(col.name);
28
+ }
29
+ for (const idx of table.indexes || []) {
30
+ if (!idx?.unique) continue;
31
+ if (!Array.isArray(idx.columns) || idx.columns.length !== 1 || !idx.columns[0]?.column) continue;
32
+ const columnName = idx.columns[0].column;
33
+ if (idx.where) {
34
+ const predicate = String(idx.where);
35
+ const isNotNullOnly = new RegExp(`\\b${columnName}\\b\\s+is\\s+not\\s+null\\b`, 'i').test(predicate);
36
+ if (!isNotNullOnly) continue;
37
+ }
38
+ uniqueCols.add(columnName);
39
+ }
40
+ uniqueSingleColumns.set(table.name, uniqueCols);
41
+ }
42
+
43
+ const findTable = name => {
44
+ const norm = normalizeName(name);
45
+ return tables.find(t => t.name === name || t.name === norm);
46
+ };
47
+
48
48
  const pivotTables = new Set();
49
49
  for (const table of tables) {
50
50
  const fkCols = fkIndex.get(table.name) || [];
51
+ const hasSelfReference = fkCols.some(c => normalizeName(c.references.table) === table.name);
51
52
  const distinctTargets = Array.from(new Set(fkCols.map(c => normalizeName(c.references.table))));
52
- if (fkCols.length === 2 && distinctTargets.length === 2) {
53
+ if (!hasSelfReference && fkCols.length === 2 && distinctTargets.length === 2) {
53
54
  const [a, b] = fkCols;
54
55
  pivotTables.add(table.name);
55
56
  const targetA = findTable(a.references.table);
56
57
  const targetB = findTable(b.references.table);
57
- if (targetA && targetB) {
58
- const aKey = relationKeys.get(targetA.name);
59
- const bKey = relationKeys.get(targetB.name);
60
- const aProp = naming.belongsToManyProperty(targetB.name);
61
- const bProp = naming.belongsToManyProperty(targetA.name);
62
- if (!aKey.has(aProp)) {
63
- aKey.add(aProp);
64
- relationMap.get(targetA.name)?.push({
65
- kind: 'belongsToMany',
66
- property: aProp,
67
- target: targetB.name,
68
- pivotTable: table.name,
69
- pivotForeignKeyToRoot: a.name,
70
- pivotForeignKeyToTarget: b.name
71
- });
72
- }
73
- if (!bKey.has(bProp)) {
74
- bKey.add(bProp);
75
- relationMap.get(targetB.name)?.push({
76
- kind: 'belongsToMany',
77
- property: bProp,
78
- target: targetA.name,
79
- pivotTable: table.name,
80
- pivotForeignKeyToRoot: b.name,
81
- pivotForeignKeyToTarget: a.name
82
- });
83
- }
84
- }
85
- }
86
- }
87
-
88
- for (const table of tables) {
89
- const fkCols = fkIndex.get(table.name) || [];
90
- for (const fk of fkCols) {
91
- const targetTable = fk.references.table;
92
- const targetKey = normalizeName(targetTable);
93
- const belongsKey = relationKeys.get(table.name);
94
- const hasManyKey = targetKey ? relationKeys.get(targetKey) : undefined;
95
-
96
- if (!belongsKey || !hasManyKey) continue;
97
-
98
- const belongsProp = naming.belongsToProperty(fk.name, targetTable);
99
- if (!belongsKey.has(belongsProp)) {
100
- belongsKey.add(belongsProp);
101
- relationMap.get(table.name)?.push({
102
- kind: 'belongsTo',
103
- property: belongsProp,
104
- target: targetTable,
105
- foreignKey: fk.name
106
- });
107
- }
108
-
109
- // Skip generating HasMany/HasOne relations TO pivot tables
110
- // (pivot data is accessible via _pivot on BelongsToMany)
111
- if (pivotTables.has(table.name)) continue;
112
-
113
- const uniqueCols = uniqueSingleColumns.get(table.name);
114
- const isHasOne = Boolean(uniqueCols?.has(fk.name));
115
- const relationKind = isHasOne ? 'hasOne' : 'hasMany';
116
- const inverseProp = isHasOne ? naming.hasOneProperty(table.name) : naming.hasManyProperty(table.name);
117
- if (!hasManyKey.has(inverseProp)) {
118
- hasManyKey.add(inverseProp);
119
- relationMap.get(targetKey)?.push({
120
- kind: relationKind,
121
- property: inverseProp,
122
- target: table.name,
123
- foreignKey: fk.name
124
- });
125
- }
126
- }
127
- }
128
-
129
- return relationMap;
130
- };
131
-
132
- export const buildSchemaMetadata = (schema, naming) => {
133
- const tables = schema.tables.map(t => {
134
- const indexes = Array.isArray(t.indexes) ? t.indexes.map(idx => ({ ...idx })) : [];
135
- const uniqueSingleColumns = new Set(
136
- indexes
137
- .filter(idx => idx?.unique && !idx?.where && Array.isArray(idx.columns) && idx.columns.length === 1)
138
- .map(idx => idx.columns[0]?.column)
139
- .filter(Boolean)
140
- );
141
-
142
- return {
143
- name: t.name,
144
- schema: t.schema,
145
- columns: (t.columns || []).map(col => {
146
- const unique = col.unique !== undefined ? col.unique : uniqueSingleColumns.has(col.name) ? true : undefined;
147
- return { ...col, unique };
148
- }),
149
- primaryKey: t.primaryKey || [],
150
- indexes,
151
- isView: false
152
- };
153
- });
154
-
155
- const views = (schema.views || []).map(v => ({
156
- name: v.name,
157
- schema: v.schema,
158
- columns: (v.columns || []).map(col => ({ ...col })),
159
- primaryKey: [],
160
- indexes: [],
161
- isView: true,
162
- definition: v.definition,
163
- comment: v.comment
164
- }));
165
-
166
- const allEntities = [...tables, ...views];
167
-
168
- const classNames = new Map();
169
- allEntities.forEach(t => {
170
- const className = naming.classNameFromTable(t.name);
171
- classNames.set(t.name, className);
172
- if (t.schema) {
173
- const qualified = `${t.schema}.${t.name}`;
174
- if (!classNames.has(qualified)) {
175
- classNames.set(qualified, className);
176
- }
177
- }
178
- });
179
-
180
- const resolveClassName = target => {
181
- if (!target) return undefined;
182
- if (classNames.has(target)) return classNames.get(target);
183
- const fallback = target.split('.').pop();
184
- if (fallback && classNames.has(fallback)) {
185
- return classNames.get(fallback);
186
- }
187
- return undefined;
188
- };
189
-
190
- const relations = mapRelations(tables, naming);
191
-
192
- // Detect tree tables
193
- const treeConfigs = mapTreeTables(tables, naming);
194
-
195
- return {
196
- tables,
197
- views,
198
- classNames,
199
- relations,
200
- resolveClassName,
201
- treeConfigs
202
- };
203
- };
58
+ if (targetA && targetB) {
59
+ const aKey = relationKeys.get(targetA.name);
60
+ const bKey = relationKeys.get(targetB.name);
61
+ const aProp = naming.belongsToManyProperty(targetB.name);
62
+ const bProp = naming.belongsToManyProperty(targetA.name);
63
+ if (!aKey.has(aProp)) {
64
+ aKey.add(aProp);
65
+ relationMap.get(targetA.name)?.push({
66
+ kind: 'belongsToMany',
67
+ property: aProp,
68
+ target: targetB.name,
69
+ pivotTable: table.name,
70
+ pivotForeignKeyToRoot: a.name,
71
+ pivotForeignKeyToTarget: b.name
72
+ });
73
+ }
74
+ if (!bKey.has(bProp)) {
75
+ bKey.add(bProp);
76
+ relationMap.get(targetB.name)?.push({
77
+ kind: 'belongsToMany',
78
+ property: bProp,
79
+ target: targetA.name,
80
+ pivotTable: table.name,
81
+ pivotForeignKeyToRoot: b.name,
82
+ pivotForeignKeyToTarget: a.name
83
+ });
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ for (const table of tables) {
90
+ const fkCols = fkIndex.get(table.name) || [];
91
+ for (const fk of fkCols) {
92
+ const targetTable = fk.references.table;
93
+ const targetKey = normalizeName(targetTable);
94
+ const belongsKey = relationKeys.get(table.name);
95
+ const hasManyKey = targetKey ? relationKeys.get(targetKey) : undefined;
96
+
97
+ if (!belongsKey || !hasManyKey) continue;
98
+
99
+ const belongsProp = naming.belongsToProperty(fk.name, targetTable);
100
+ if (!belongsKey.has(belongsProp)) {
101
+ belongsKey.add(belongsProp);
102
+ relationMap.get(table.name)?.push({
103
+ kind: 'belongsTo',
104
+ property: belongsProp,
105
+ target: targetTable,
106
+ foreignKey: fk.name
107
+ });
108
+ }
109
+
110
+ // Skip generating HasMany/HasOne relations TO pivot tables
111
+ // (pivot data is accessible via _pivot on BelongsToMany)
112
+ if (pivotTables.has(table.name)) continue;
113
+
114
+ const uniqueCols = uniqueSingleColumns.get(table.name);
115
+ const isHasOne = Boolean(uniqueCols?.has(fk.name));
116
+ const relationKind = isHasOne ? 'hasOne' : 'hasMany';
117
+ const inverseProp = isHasOne ? naming.hasOneProperty(table.name) : naming.hasManyProperty(table.name);
118
+ if (!hasManyKey.has(inverseProp)) {
119
+ hasManyKey.add(inverseProp);
120
+ relationMap.get(targetKey)?.push({
121
+ kind: relationKind,
122
+ property: inverseProp,
123
+ target: table.name,
124
+ foreignKey: fk.name
125
+ });
126
+ }
127
+ }
128
+ }
129
+
130
+ return relationMap;
131
+ };
132
+
133
+ export const buildSchemaMetadata = (schema, naming) => {
134
+ const tables = schema.tables.map(t => {
135
+ const indexes = Array.isArray(t.indexes) ? t.indexes.map(idx => ({ ...idx })) : [];
136
+ const uniqueSingleColumns = new Set(
137
+ indexes
138
+ .filter(idx => idx?.unique && !idx?.where && Array.isArray(idx.columns) && idx.columns.length === 1)
139
+ .map(idx => idx.columns[0]?.column)
140
+ .filter(Boolean)
141
+ );
142
+
143
+ return {
144
+ name: t.name,
145
+ schema: t.schema,
146
+ columns: (t.columns || []).map(col => {
147
+ const unique = col.unique !== undefined ? col.unique : uniqueSingleColumns.has(col.name) ? true : undefined;
148
+ return { ...col, unique };
149
+ }),
150
+ primaryKey: t.primaryKey || [],
151
+ indexes,
152
+ isView: false
153
+ };
154
+ });
155
+
156
+ const views = (schema.views || []).map(v => ({
157
+ name: v.name,
158
+ schema: v.schema,
159
+ columns: (v.columns || []).map(col => ({ ...col })),
160
+ primaryKey: [],
161
+ indexes: [],
162
+ isView: true,
163
+ definition: v.definition,
164
+ comment: v.comment
165
+ }));
166
+
167
+ const allEntities = [...tables, ...views];
168
+
169
+ const classNames = new Map();
170
+ allEntities.forEach(t => {
171
+ const className = naming.classNameFromTable(t.name);
172
+ classNames.set(t.name, className);
173
+ if (t.schema) {
174
+ const qualified = `${t.schema}.${t.name}`;
175
+ if (!classNames.has(qualified)) {
176
+ classNames.set(qualified, className);
177
+ }
178
+ }
179
+ });
180
+
181
+ const resolveClassName = target => {
182
+ if (!target) return undefined;
183
+ if (classNames.has(target)) return classNames.get(target);
184
+ const fallback = target.split('.').pop();
185
+ if (fallback && classNames.has(fallback)) {
186
+ return classNames.get(fallback);
187
+ }
188
+ return undefined;
189
+ };
190
+
191
+ const relations = mapRelations(tables, naming);
192
+
193
+ // Detect tree tables
194
+ const treeConfigs = mapTreeTables(tables, naming);
195
+
196
+ return {
197
+ tables,
198
+ views,
199
+ classNames,
200
+ relations,
201
+ resolveClassName,
202
+ treeConfigs
203
+ };
204
+ };
@@ -1,28 +1,34 @@
1
- import { ColumnDefLike, RelationMetadata } from '../orm/entity-metadata.js';
2
- import type { TransformerMetadata } from './transformers/transformer-metadata.js';
3
-
1
+ import { ColumnDefLike, RelationMetadata } from '../orm/entity-metadata.js';
2
+ import type { TransformerMetadata } from './transformers/transformer-metadata.js';
3
+
4
4
  /**
5
5
  * Bag for storing decorator metadata during the decoration phase.
6
6
  */
7
+ export interface DecoratorTreeMetadata {
8
+ parentProperty?: string;
9
+ childrenProperty?: string;
10
+ }
11
+
7
12
  export interface DecoratorMetadataBag {
8
13
  columns: Array<{ propertyName: string; column: ColumnDefLike }>;
9
14
  relations: Array<{ propertyName: string; relation: RelationMetadata }>;
10
15
  transformers: Array<{ propertyName: string; metadata: TransformerMetadata }>;
16
+ tree?: DecoratorTreeMetadata;
11
17
  }
12
-
13
- const METADATA_KEY = 'metal-orm:decorators';
14
-
15
- type MetadataCarrier = {
16
- metadata?: Record<PropertyKey, unknown>;
17
- };
18
-
19
- /**
20
- * Gets or creates a metadata bag for the given decorator context.
21
- * @param context - The decorator context with metadata support.
22
- * @returns The metadata bag.
23
- */
24
- export const getOrCreateMetadataBag = (context: MetadataCarrier): DecoratorMetadataBag => {
25
- const metadata = context.metadata || (context.metadata = {} as Record<PropertyKey, unknown>);
18
+
19
+ const METADATA_KEY = 'metal-orm:decorators';
20
+
21
+ type MetadataCarrier = {
22
+ metadata?: Record<PropertyKey, unknown>;
23
+ };
24
+
25
+ /**
26
+ * Gets or creates a metadata bag for the given decorator context.
27
+ * @param context - The decorator context with metadata support.
28
+ * @returns The metadata bag.
29
+ */
30
+ export const getOrCreateMetadataBag = (context: MetadataCarrier): DecoratorMetadataBag => {
31
+ const metadata = context.metadata || (context.metadata = {} as Record<PropertyKey, unknown>);
26
32
  let bag = metadata[METADATA_KEY] as DecoratorMetadataBag | undefined;
27
33
  if (!bag) {
28
34
  bag = { columns: [], relations: [], transformers: [] };
@@ -30,32 +36,32 @@ export const getOrCreateMetadataBag = (context: MetadataCarrier): DecoratorMetad
30
36
  }
31
37
  return bag;
32
38
  };
33
-
34
- /**
35
- * Reads the metadata bag from the given decorator context.
36
- * @param context - The decorator context with metadata support.
37
- * @returns The metadata bag if present.
38
- */
39
- export const readMetadataBag = (context: MetadataCarrier): DecoratorMetadataBag | undefined => {
40
- return context.metadata?.[METADATA_KEY] as DecoratorMetadataBag | undefined;
41
- };
42
-
43
- /**
44
- * Reads the metadata bag from a decorated constructor when using standard decorators.
45
- * @param ctor - The entity constructor.
46
- * @returns The metadata bag if present.
47
- */
48
- export const readMetadataBagFromConstructor = (ctor: object): DecoratorMetadataBag | undefined => {
49
- const metadataSymbol = (Symbol as { metadata?: symbol }).metadata;
50
- if (!metadataSymbol) return undefined;
51
- const metadata = Reflect.get(ctor, metadataSymbol) as Record<PropertyKey, unknown> | undefined;
52
- return metadata?.[METADATA_KEY] as DecoratorMetadataBag | undefined;
53
- };
54
-
55
- /**
56
- * Public helper to read decorator metadata from a class constructor.
57
- * @param ctor - The entity constructor.
58
- * @returns The metadata bag if present.
59
- */
60
- export const getDecoratorMetadata = (ctor: object): DecoratorMetadataBag | undefined =>
61
- readMetadataBagFromConstructor(ctor);
39
+
40
+ /**
41
+ * Reads the metadata bag from the given decorator context.
42
+ * @param context - The decorator context with metadata support.
43
+ * @returns The metadata bag if present.
44
+ */
45
+ export const readMetadataBag = (context: MetadataCarrier): DecoratorMetadataBag | undefined => {
46
+ return context.metadata?.[METADATA_KEY] as DecoratorMetadataBag | undefined;
47
+ };
48
+
49
+ /**
50
+ * Reads the metadata bag from a decorated constructor when using standard decorators.
51
+ * @param ctor - The entity constructor.
52
+ * @returns The metadata bag if present.
53
+ */
54
+ export const readMetadataBagFromConstructor = (ctor: object): DecoratorMetadataBag | undefined => {
55
+ const metadataSymbol = (Symbol as { metadata?: symbol }).metadata;
56
+ if (!metadataSymbol) return undefined;
57
+ const metadata = Reflect.get(ctor, metadataSymbol) as Record<PropertyKey, unknown> | undefined;
58
+ return metadata?.[METADATA_KEY] as DecoratorMetadataBag | undefined;
59
+ };
60
+
61
+ /**
62
+ * Public helper to read decorator metadata from a class constructor.
63
+ * @param ctor - The entity constructor.
64
+ * @returns The metadata bag if present.
65
+ */
66
+ export const getDecoratorMetadata = (ctor: object): DecoratorMetadataBag | undefined =>
67
+ readMetadataBagFromConstructor(ctor);