metal-orm 1.0.47 → 1.0.49

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.
@@ -0,0 +1,442 @@
1
+ import path from 'node:path';
2
+ import { createNamingStrategy } from '../naming-strategy.mjs';
3
+ import { buildSchemaMetadata } from './schema.mjs';
4
+
5
+ const escapeJsString = value => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
6
+
7
+ const parseColumnType = colTypeRaw => {
8
+ const type = (colTypeRaw || '').toLowerCase();
9
+ const lengthMatch = type.match(/\((\d+)(?:\s*,\s*(\d+))?\)/);
10
+ const length = lengthMatch ? Number(lengthMatch[1]) : undefined;
11
+ const scale = lengthMatch && lengthMatch[2] ? Number(lengthMatch[2]) : undefined;
12
+
13
+ const base = type.replace(/\(.*\)/, '');
14
+
15
+ if (base === 'bit') return { factory: 'col.boolean()', ts: 'boolean' };
16
+ if (base.includes('bigint')) return { factory: 'col.bigint()', ts: 'number' };
17
+ if (base.includes('int')) return { factory: 'col.int()', ts: 'number' };
18
+ if (base.includes('uuid') || base.includes('uniqueidentifier')) return { factory: 'col.uuid()', ts: 'string' };
19
+ if (base === 'date') return { factory: 'col.date<Date>()', ts: 'Date' };
20
+ if (base.includes('datetime') || base === 'time') return { factory: 'col.datetime<Date>()', ts: 'Date' };
21
+ if (base.includes('char') || base.includes('text')) {
22
+ const lenArg = length ? `${length}` : '255';
23
+ return { factory: `col.varchar(${lenArg})`, ts: 'string' };
24
+ }
25
+ if (base.includes('json')) return { factory: 'col.json()', ts: 'any' };
26
+ if (base.includes('bool') || (base.includes('tinyint') && length === 1)) {
27
+ return { factory: 'col.boolean()', ts: 'boolean' };
28
+ }
29
+ if (base.includes('date') || base.includes('time')) return { factory: 'col.datetime<Date>()', ts: 'Date' };
30
+ if (base.includes('decimal') || base.includes('numeric')) {
31
+ const precision = length ?? 10;
32
+ const scaleVal = scale ?? 0;
33
+ return { factory: `col.decimal(${precision}, ${scaleVal})`, ts: 'number' };
34
+ }
35
+ if (base.includes('double')) return { factory: 'col.float()', ts: 'number' };
36
+ if (base.includes('float') || base.includes('real')) return { factory: 'col.float()', ts: 'number' };
37
+ if (base.includes('blob') || base.includes('binary') || base.includes('bytea')) {
38
+ return { factory: 'col.blob()', ts: 'Buffer' };
39
+ }
40
+
41
+ return { factory: `col.varchar(255) /* TODO: review type ${colTypeRaw} */`, ts: 'any' };
42
+ };
43
+
44
+ const normalizeDefault = value => {
45
+ if (value === undefined) return undefined;
46
+ if (value === null) return { kind: 'value', code: 'null' };
47
+ if (typeof value === 'number' || typeof value === 'boolean') {
48
+ return { kind: 'value', code: JSON.stringify(value) };
49
+ }
50
+ if (typeof value === 'string') {
51
+ const trimmed = value.trim();
52
+ if (/^[-]?\d+(\.\d+)?$/.test(trimmed)) return { kind: 'value', code: trimmed };
53
+ if (/^(true|false)$/i.test(trimmed)) return { kind: 'value', code: trimmed.toLowerCase() };
54
+ if (/^null$/i.test(trimmed)) return { kind: 'value', code: 'null' };
55
+ if (/current_|now\(\)|uuid_generate_v4|uuid\(\)/i.test(trimmed) || trimmed.includes('(')) {
56
+ return { kind: 'raw', code: `'${escapeJsString(trimmed)}'` };
57
+ }
58
+ if (/^'.*'$/.test(trimmed) || /^".*"$/.test(trimmed)) {
59
+ const unquoted = trimmed.slice(1, -1);
60
+ return { kind: 'value', code: `'${escapeJsString(unquoted)}'` };
61
+ }
62
+ return { kind: 'raw', code: `'${escapeJsString(trimmed)}'` };
63
+ }
64
+ return { kind: 'value', code: JSON.stringify(value) };
65
+ };
66
+
67
+ const renderColumnExpression = (column, tablePk) => {
68
+ const base = parseColumnType(column.type);
69
+ let expr = base.factory;
70
+
71
+ if (column.autoIncrement) {
72
+ expr = `col.autoIncrement(${expr})`;
73
+ }
74
+ if (column.notNull) {
75
+ expr = `col.notNull(${expr})`;
76
+ }
77
+ if (column.unique) {
78
+ const name = typeof column.unique === 'string' ? `, '${escapeJsString(column.unique)}'` : '';
79
+ expr = `col.unique(${expr}${name})`;
80
+ }
81
+ if (column.default !== undefined) {
82
+ const def = normalizeDefault(column.default);
83
+ if (def) {
84
+ expr =
85
+ def.kind === 'raw'
86
+ ? `col.defaultRaw(${expr}, ${def.code})`
87
+ : `col.default(${expr}, ${def.code})`;
88
+ }
89
+ }
90
+ if (column.references) {
91
+ const refParts = [
92
+ `table: '${escapeJsString(column.references.table)}'`,
93
+ `column: '${escapeJsString(column.references.column)}'`
94
+ ];
95
+ if (column.references.onDelete) refParts.push(`onDelete: '${escapeJsString(column.references.onDelete)}'`);
96
+ if (column.references.onUpdate) refParts.push(`onUpdate: '${escapeJsString(column.references.onUpdate)}'`);
97
+ expr = `col.references(${expr}, { ${refParts.join(', ')} })`;
98
+ }
99
+
100
+ const isPrimary = Array.isArray(tablePk) && tablePk.includes(column.name);
101
+ const decorator = isPrimary ? 'PrimaryKey' : 'Column';
102
+ const tsType = base.ts || 'any';
103
+ const optional = !column.notNull;
104
+
105
+ return {
106
+ decorator,
107
+ expr,
108
+ tsType,
109
+ optional
110
+ };
111
+ };
112
+
113
+ const METAL_ORM_IMPORT_ORDER = [
114
+ 'col',
115
+ 'Entity',
116
+ 'Column',
117
+ 'PrimaryKey',
118
+ 'HasMany',
119
+ 'HasOne',
120
+ 'BelongsTo',
121
+ 'BelongsToMany',
122
+ 'HasManyCollection',
123
+ 'HasOneReference',
124
+ 'ManyToManyCollection',
125
+ 'bootstrapEntities',
126
+ 'getTableDefFromEntity'
127
+ ];
128
+
129
+ const renderEntityClassLines = ({ table, className, naming, relations, resolveClassName }) => {
130
+ const lines = [];
131
+ const derivedDefault = naming.defaultTableNameFromClass(className);
132
+ const needsTableNameOption = table.name !== derivedDefault;
133
+ const entityOpts = needsTableNameOption ? `{ tableName: '${escapeJsString(table.name)}' }` : '';
134
+ lines.push(`@Entity(${entityOpts})`);
135
+ lines.push(`export class ${className} {`);
136
+
137
+ for (const col of table.columns) {
138
+ const rendered = renderColumnExpression(col, table.primaryKey);
139
+ lines.push(` @${rendered.decorator}(${rendered.expr})`);
140
+ lines.push(` ${col.name}${rendered.optional ? '?:' : '!:'} ${rendered.tsType};`);
141
+ lines.push('');
142
+ }
143
+
144
+ for (const rel of relations) {
145
+ const targetClass = resolveClassName(rel.target);
146
+ if (!targetClass) continue;
147
+ switch (rel.kind) {
148
+ case 'belongsTo':
149
+ lines.push(
150
+ ` @BelongsTo({ target: () => ${targetClass}, foreignKey: '${escapeJsString(rel.foreignKey)}' })`
151
+ );
152
+ lines.push(` ${rel.property}?: ${targetClass};`);
153
+ lines.push('');
154
+ break;
155
+ case 'hasMany':
156
+ lines.push(
157
+ ` @HasMany({ target: () => ${targetClass}, foreignKey: '${escapeJsString(rel.foreignKey)}' })`
158
+ );
159
+ lines.push(` ${rel.property}!: HasManyCollection<${targetClass}>;`);
160
+ lines.push('');
161
+ break;
162
+ case 'hasOne':
163
+ lines.push(
164
+ ` @HasOne({ target: () => ${targetClass}, foreignKey: '${escapeJsString(rel.foreignKey)}' })`
165
+ );
166
+ lines.push(` ${rel.property}!: HasOneReference<${targetClass}>;`);
167
+ lines.push('');
168
+ break;
169
+ case 'belongsToMany': {
170
+ const pivotClass = resolveClassName(rel.pivotTable);
171
+ if (!pivotClass) break;
172
+ lines.push(
173
+ ` @BelongsToMany({ target: () => ${targetClass}, pivotTable: () => ${pivotClass}, pivotForeignKeyToRoot: '${escapeJsString(
174
+ rel.pivotForeignKeyToRoot
175
+ )}', pivotForeignKeyToTarget: '${escapeJsString(rel.pivotForeignKeyToTarget)}' })`
176
+ );
177
+ lines.push(` ${rel.property}!: ManyToManyCollection<${targetClass}>;`);
178
+ lines.push('');
179
+ break;
180
+ }
181
+ default:
182
+ break;
183
+ }
184
+ }
185
+
186
+ lines.push('}');
187
+ lines.push('');
188
+ return lines;
189
+ };
190
+
191
+ const computeTableUsage = (table, relations) => {
192
+ const usage = {
193
+ needsCol: false,
194
+ needsEntity: true,
195
+ needsColumnDecorator: false,
196
+ needsPrimaryKeyDecorator: false,
197
+ needsHasManyDecorator: false,
198
+ needsHasOneDecorator: false,
199
+ needsBelongsToDecorator: false,
200
+ needsBelongsToManyDecorator: false,
201
+ needsHasManyCollection: false,
202
+ needsHasOneReference: false,
203
+ needsManyToManyCollection: false
204
+ };
205
+
206
+ for (const col of table.columns) {
207
+ usage.needsCol = true;
208
+ const rendered = renderColumnExpression(col, table.primaryKey);
209
+ if (rendered.decorator === 'PrimaryKey') {
210
+ usage.needsPrimaryKeyDecorator = true;
211
+ } else {
212
+ usage.needsColumnDecorator = true;
213
+ }
214
+ }
215
+
216
+ for (const rel of relations) {
217
+ if (rel.kind === 'hasMany') {
218
+ usage.needsHasManyDecorator = true;
219
+ usage.needsHasManyCollection = true;
220
+ }
221
+ if (rel.kind === 'hasOne') {
222
+ usage.needsHasOneDecorator = true;
223
+ usage.needsHasOneReference = true;
224
+ }
225
+ if (rel.kind === 'belongsTo') {
226
+ usage.needsBelongsToDecorator = true;
227
+ }
228
+ if (rel.kind === 'belongsToMany') {
229
+ usage.needsBelongsToManyDecorator = true;
230
+ usage.needsManyToManyCollection = true;
231
+ }
232
+ }
233
+
234
+ return usage;
235
+ };
236
+
237
+ const getMetalOrmImportNamesFromUsage = usage => {
238
+ const names = new Set();
239
+ if (usage.needsCol) names.add('col');
240
+ if (usage.needsEntity) names.add('Entity');
241
+ if (usage.needsColumnDecorator) names.add('Column');
242
+ if (usage.needsPrimaryKeyDecorator) names.add('PrimaryKey');
243
+ if (usage.needsHasManyDecorator) names.add('HasMany');
244
+ if (usage.needsHasOneDecorator) names.add('HasOne');
245
+ if (usage.needsBelongsToDecorator) names.add('BelongsTo');
246
+ if (usage.needsBelongsToManyDecorator) names.add('BelongsToMany');
247
+ if (usage.needsHasManyCollection) names.add('HasManyCollection');
248
+ if (usage.needsHasOneReference) names.add('HasOneReference');
249
+ if (usage.needsManyToManyCollection) names.add('ManyToManyCollection');
250
+ return names;
251
+ };
252
+
253
+ const buildMetalOrmImportStatement = names => {
254
+ if (!names || names.size === 0) return '';
255
+ const ordered = METAL_ORM_IMPORT_ORDER.filter(name => names.has(name));
256
+ if (!ordered.length) return '';
257
+ return `import { ${ordered.join(', ')} } from 'metal-orm';`;
258
+ };
259
+
260
+ const getRelativeModuleSpecifier = (from, to) => {
261
+ const rel = path.relative(path.dirname(from), to).replace(/\\/g, '/');
262
+ if (!rel) return './';
263
+ const withoutExt = rel.replace(/\.ts$/i, '');
264
+ return withoutExt.startsWith('.') ? withoutExt : `./${withoutExt}`;
265
+ };
266
+
267
+ export const renderEntityFile = (schema, options) => {
268
+ const naming = options.naming || createNamingStrategy('en');
269
+ const metadata = buildSchemaMetadata(schema, naming);
270
+ const { tables, relations } = metadata;
271
+
272
+ const aggregateUsage = {
273
+ needsCol: false,
274
+ needsEntity: tables.length > 0,
275
+ needsColumnDecorator: false,
276
+ needsPrimaryKeyDecorator: false,
277
+ needsHasManyDecorator: false,
278
+ needsHasOneDecorator: false,
279
+ needsBelongsToDecorator: false,
280
+ needsBelongsToManyDecorator: false,
281
+ needsHasManyCollection: false,
282
+ needsHasOneReference: false,
283
+ needsManyToManyCollection: false
284
+ };
285
+
286
+ for (const table of tables) {
287
+ const rels = relations.get(table.name) || [];
288
+ const tableUsage = computeTableUsage(table, rels);
289
+ aggregateUsage.needsCol ||= tableUsage.needsCol;
290
+ aggregateUsage.needsColumnDecorator ||= tableUsage.needsColumnDecorator;
291
+ aggregateUsage.needsPrimaryKeyDecorator ||= tableUsage.needsPrimaryKeyDecorator;
292
+ aggregateUsage.needsHasManyDecorator ||= tableUsage.needsHasManyDecorator;
293
+ aggregateUsage.needsHasOneDecorator ||= tableUsage.needsHasOneDecorator;
294
+ aggregateUsage.needsBelongsToDecorator ||= tableUsage.needsBelongsToDecorator;
295
+ aggregateUsage.needsBelongsToManyDecorator ||= tableUsage.needsBelongsToManyDecorator;
296
+ aggregateUsage.needsHasManyCollection ||= tableUsage.needsHasManyCollection;
297
+ aggregateUsage.needsHasOneReference ||= tableUsage.needsHasOneReference;
298
+ aggregateUsage.needsManyToManyCollection ||= tableUsage.needsManyToManyCollection;
299
+ }
300
+
301
+ const importNames = getMetalOrmImportNamesFromUsage(aggregateUsage);
302
+ importNames.add('bootstrapEntities');
303
+ importNames.add('getTableDefFromEntity');
304
+ const importStatement = buildMetalOrmImportStatement(importNames);
305
+
306
+ const lines = [
307
+ '// AUTO-GENERATED by scripts/generate-entities.mjs',
308
+ '// Regenerate after schema changes.'
309
+ ];
310
+ if (importStatement) {
311
+ lines.push(importStatement, '');
312
+ }
313
+
314
+ for (const table of tables) {
315
+ const className = metadata.classNames.get(table.name);
316
+ const classLines = renderEntityClassLines({
317
+ table,
318
+ className,
319
+ naming,
320
+ relations: relations.get(table.name) || [],
321
+ resolveClassName: metadata.resolveClassName
322
+ });
323
+ lines.push(...classLines);
324
+ }
325
+
326
+ lines.push(
327
+ 'export const bootstrapEntityTables = () => {',
328
+ ' const tables = bootstrapEntities();',
329
+ ' return {',
330
+ ...tables.map(t => ` ${metadata.classNames.get(t.name)}: getTableDefFromEntity(${metadata.classNames.get(t.name)})!,`),
331
+ ' };',
332
+ '};',
333
+ '',
334
+ 'export const allTables = () => bootstrapEntities();'
335
+ );
336
+
337
+ return lines.join('\n');
338
+ };
339
+
340
+ export const renderSplitEntityFiles = (schema, options) => {
341
+ const naming = options.naming || createNamingStrategy('en');
342
+ const metadata = buildSchemaMetadata(schema, naming);
343
+ const tableFiles = [];
344
+
345
+ for (const table of metadata.tables) {
346
+ const className = metadata.classNames.get(table.name);
347
+ const relations = metadata.relations.get(table.name) || [];
348
+ const usage = computeTableUsage(table, relations);
349
+ const metalImportNames = getMetalOrmImportNamesFromUsage(usage);
350
+ const metalImportStatement = buildMetalOrmImportStatement(metalImportNames);
351
+
352
+ const relationImports = new Set();
353
+ for (const rel of relations) {
354
+ const targetClass = metadata.resolveClassName(rel.target);
355
+ if (targetClass && targetClass !== className) {
356
+ relationImports.add(targetClass);
357
+ }
358
+ if (rel.kind === 'belongsToMany') {
359
+ const pivotClass = metadata.resolveClassName(rel.pivotTable);
360
+ if (pivotClass && pivotClass !== className) {
361
+ relationImports.add(pivotClass);
362
+ }
363
+ }
364
+ }
365
+
366
+ const importLines = [];
367
+ if (metalImportStatement) {
368
+ importLines.push(metalImportStatement);
369
+ }
370
+ for (const targetClass of Array.from(relationImports).sort()) {
371
+ importLines.push(`import { ${targetClass} } from './${targetClass}';`);
372
+ }
373
+
374
+ const lines = [
375
+ '// AUTO-GENERATED by scripts/generate-entities.mjs',
376
+ '// Regenerate after schema changes.'
377
+ ];
378
+ if (importLines.length) {
379
+ lines.push(...importLines, '');
380
+ }
381
+
382
+ const classLines = renderEntityClassLines({
383
+ table,
384
+ className,
385
+ naming,
386
+ relations,
387
+ resolveClassName: metadata.resolveClassName
388
+ });
389
+ lines.push(...classLines);
390
+
391
+ tableFiles.push({
392
+ path: path.join(options.outDir, `${className}.ts`),
393
+ code: lines.join('\n')
394
+ });
395
+ }
396
+
397
+ return { tableFiles, metadata };
398
+ };
399
+
400
+ export const renderSplitIndexFile = (metadata, options) => {
401
+ const importLines = ["import { bootstrapEntities, getTableDefFromEntity } from 'metal-orm';"];
402
+
403
+ const exportedClasses = [];
404
+ for (const table of metadata.tables) {
405
+ const className = metadata.classNames.get(table.name);
406
+ const filePath = path.join(options.outDir, `${className}.ts`);
407
+ const moduleSpecifier = getRelativeModuleSpecifier(options.out, filePath);
408
+ importLines.push(`import { ${className} } from '${moduleSpecifier}';`);
409
+ exportedClasses.push(className);
410
+ }
411
+
412
+ const lines = [
413
+ '// AUTO-GENERATED by scripts/generate-entities.mjs',
414
+ '// Regenerate after schema changes.',
415
+ ...importLines,
416
+ ''
417
+ ];
418
+
419
+ if (exportedClasses.length) {
420
+ lines.push('export {');
421
+ for (const className of exportedClasses) {
422
+ lines.push(` ${className},`);
423
+ }
424
+ lines.push('};', '');
425
+ }
426
+
427
+ lines.push(
428
+ 'export const bootstrapEntityTables = () => {',
429
+ ' const tables = bootstrapEntities();',
430
+ ' return {',
431
+ ...metadata.tables.map(
432
+ t =>
433
+ ` ${metadata.classNames.get(t.name)}: getTableDefFromEntity(${metadata.classNames.get(t.name)})!,`
434
+ ),
435
+ ' };',
436
+ '};',
437
+ '',
438
+ 'export const allTables = () => bootstrapEntities();'
439
+ );
440
+
441
+ return lines.join('\n');
442
+ };
@@ -0,0 +1,178 @@
1
+ const normalizeName = name => (typeof name === 'string' && name.includes('.') ? name.split('.').pop() : name);
2
+
3
+ export const mapRelations = (tables, naming) => {
4
+ const relationMap = new Map();
5
+ const relationKeys = new Map();
6
+ const fkIndex = new Map();
7
+ const uniqueSingleColumns = new Map();
8
+
9
+ for (const table of tables) {
10
+ relationMap.set(table.name, []);
11
+ relationKeys.set(table.name, new Set());
12
+ for (const col of table.columns) {
13
+ if (col.references) {
14
+ const list = fkIndex.get(table.name) || [];
15
+ list.push(col);
16
+ fkIndex.set(table.name, list);
17
+ }
18
+ }
19
+
20
+ const uniqueCols = new Set();
21
+ if (Array.isArray(table.primaryKey) && table.primaryKey.length === 1) {
22
+ uniqueCols.add(table.primaryKey[0]);
23
+ }
24
+ for (const col of table.columns) {
25
+ if (col.unique) uniqueCols.add(col.name);
26
+ }
27
+ for (const idx of table.indexes || []) {
28
+ if (!idx?.unique) continue;
29
+ if (!Array.isArray(idx.columns) || idx.columns.length !== 1 || !idx.columns[0]?.column) continue;
30
+ const columnName = idx.columns[0].column;
31
+ if (idx.where) {
32
+ const predicate = String(idx.where);
33
+ const isNotNullOnly = new RegExp(`\\b${columnName}\\b\\s+is\\s+not\\s+null\\b`, 'i').test(predicate);
34
+ if (!isNotNullOnly) continue;
35
+ }
36
+ uniqueCols.add(columnName);
37
+ }
38
+ uniqueSingleColumns.set(table.name, uniqueCols);
39
+ }
40
+
41
+ const findTable = name => {
42
+ const norm = normalizeName(name);
43
+ return tables.find(t => t.name === name || t.name === norm);
44
+ };
45
+
46
+ const pivotTables = new Set();
47
+ for (const table of tables) {
48
+ const fkCols = fkIndex.get(table.name) || [];
49
+ const distinctTargets = Array.from(new Set(fkCols.map(c => normalizeName(c.references.table))));
50
+ if (fkCols.length === 2 && distinctTargets.length === 2) {
51
+ const [a, b] = fkCols;
52
+ pivotTables.add(table.name);
53
+ const targetA = findTable(a.references.table);
54
+ const targetB = findTable(b.references.table);
55
+ if (targetA && targetB) {
56
+ const aKey = relationKeys.get(targetA.name);
57
+ const bKey = relationKeys.get(targetB.name);
58
+ const aProp = naming.belongsToManyProperty(targetB.name);
59
+ const bProp = naming.belongsToManyProperty(targetA.name);
60
+ if (!aKey.has(aProp)) {
61
+ aKey.add(aProp);
62
+ relationMap.get(targetA.name)?.push({
63
+ kind: 'belongsToMany',
64
+ property: aProp,
65
+ target: targetB.name,
66
+ pivotTable: table.name,
67
+ pivotForeignKeyToRoot: a.name,
68
+ pivotForeignKeyToTarget: b.name
69
+ });
70
+ }
71
+ if (!bKey.has(bProp)) {
72
+ bKey.add(bProp);
73
+ relationMap.get(targetB.name)?.push({
74
+ kind: 'belongsToMany',
75
+ property: bProp,
76
+ target: targetA.name,
77
+ pivotTable: table.name,
78
+ pivotForeignKeyToRoot: b.name,
79
+ pivotForeignKeyToTarget: a.name
80
+ });
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ for (const table of tables) {
87
+ const fkCols = fkIndex.get(table.name) || [];
88
+ for (const fk of fkCols) {
89
+ const targetTable = fk.references.table;
90
+ const targetKey = normalizeName(targetTable);
91
+ const belongsKey = relationKeys.get(table.name);
92
+ const hasManyKey = targetKey ? relationKeys.get(targetKey) : undefined;
93
+
94
+ if (!belongsKey || !hasManyKey) continue;
95
+
96
+ const belongsProp = naming.belongsToProperty(fk.name, targetTable);
97
+ if (!belongsKey.has(belongsProp)) {
98
+ belongsKey.add(belongsProp);
99
+ relationMap.get(table.name)?.push({
100
+ kind: 'belongsTo',
101
+ property: belongsProp,
102
+ target: targetTable,
103
+ foreignKey: fk.name
104
+ });
105
+ }
106
+
107
+ const uniqueCols = uniqueSingleColumns.get(table.name);
108
+ const isHasOne = Boolean(uniqueCols?.has(fk.name));
109
+ const relationKind = isHasOne ? 'hasOne' : 'hasMany';
110
+ const inverseProp = isHasOne ? naming.hasOneProperty(table.name) : naming.hasManyProperty(table.name);
111
+ if (!hasManyKey.has(inverseProp)) {
112
+ hasManyKey.add(inverseProp);
113
+ relationMap.get(targetKey)?.push({
114
+ kind: relationKind,
115
+ property: inverseProp,
116
+ target: table.name,
117
+ foreignKey: fk.name
118
+ });
119
+ }
120
+ }
121
+ }
122
+
123
+ return relationMap;
124
+ };
125
+
126
+ export const buildSchemaMetadata = (schema, naming) => {
127
+ const tables = schema.tables.map(t => {
128
+ const indexes = Array.isArray(t.indexes) ? t.indexes.map(idx => ({ ...idx })) : [];
129
+ const uniqueSingleColumns = new Set(
130
+ indexes
131
+ .filter(idx => idx?.unique && !idx?.where && Array.isArray(idx.columns) && idx.columns.length === 1)
132
+ .map(idx => idx.columns[0]?.column)
133
+ .filter(Boolean)
134
+ );
135
+
136
+ return {
137
+ name: t.name,
138
+ schema: t.schema,
139
+ columns: (t.columns || []).map(col => {
140
+ const unique = col.unique !== undefined ? col.unique : uniqueSingleColumns.has(col.name) ? true : undefined;
141
+ return { ...col, unique };
142
+ }),
143
+ primaryKey: t.primaryKey || [],
144
+ indexes
145
+ };
146
+ });
147
+
148
+ const classNames = new Map();
149
+ tables.forEach(t => {
150
+ const className = naming.classNameFromTable(t.name);
151
+ classNames.set(t.name, className);
152
+ if (t.schema) {
153
+ const qualified = `${t.schema}.${t.name}`;
154
+ if (!classNames.has(qualified)) {
155
+ classNames.set(qualified, className);
156
+ }
157
+ }
158
+ });
159
+
160
+ const resolveClassName = target => {
161
+ if (!target) return undefined;
162
+ if (classNames.has(target)) return classNames.get(target);
163
+ const fallback = target.split('.').pop();
164
+ if (fallback && classNames.has(fallback)) {
165
+ return classNames.get(fallback);
166
+ }
167
+ return undefined;
168
+ };
169
+
170
+ const relations = mapRelations(tables, naming);
171
+
172
+ return {
173
+ tables,
174
+ classNames,
175
+ relations,
176
+ resolveClassName
177
+ };
178
+ };