@zyno-io/dk-server-foundation 26.412.2137 → 26.509.2227
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/dist/resources/proto/generated/devconsole/devconsole.js +1 -1
- package/dist/src/app/config.d.ts +1 -0
- package/dist/src/app/config.d.ts.map +1 -1
- package/dist/src/app/config.js +1 -1
- package/dist/src/app/config.js.map +1 -1
- package/dist/src/database/common.d.ts +3 -0
- package/dist/src/database/common.d.ts.map +1 -1
- package/dist/src/database/common.js +46 -1
- package/dist/src/database/common.js.map +1 -1
- package/dist/src/database/factory.d.ts +17 -0
- package/dist/src/database/factory.d.ts.map +1 -0
- package/dist/src/database/factory.js +53 -0
- package/dist/src/database/factory.js.map +1 -0
- package/dist/src/database/index.d.ts +2 -0
- package/dist/src/database/index.d.ts.map +1 -1
- package/dist/src/database/index.js +2 -0
- package/dist/src/database/index.js.map +1 -1
- package/dist/src/database/migration/MigrationResetCommand.d.ts.map +1 -1
- package/dist/src/database/migration/MigrationResetCommand.js +6 -20
- package/dist/src/database/migration/MigrationResetCommand.js.map +1 -1
- package/dist/src/database/migration/MigrationRunCommand.d.ts.map +1 -1
- package/dist/src/database/migration/MigrationRunCommand.js +2 -0
- package/dist/src/database/migration/MigrationRunCommand.js.map +1 -1
- package/dist/src/database/migration/create/MigrationCreateCommand.d.ts +2 -0
- package/dist/src/database/migration/create/MigrationCreateCommand.d.ts.map +1 -1
- package/dist/src/database/migration/create/MigrationCreateCommand.js +33 -11
- package/dist/src/database/migration/create/MigrationCreateCommand.js.map +1 -1
- package/dist/src/database/migration/create/builder-regenerator.d.ts +12 -0
- package/dist/src/database/migration/create/builder-regenerator.d.ts.map +1 -0
- package/dist/src/database/migration/create/builder-regenerator.js +300 -0
- package/dist/src/database/migration/create/builder-regenerator.js.map +1 -0
- package/dist/src/database/migration/create/db-reader.js +47 -6
- package/dist/src/database/migration/create/db-reader.js.map +1 -1
- package/dist/src/database/migration/create/ddl-generator.d.ts.map +1 -1
- package/dist/src/database/migration/create/ddl-generator.js +36 -206
- package/dist/src/database/migration/create/ddl-generator.js.map +1 -1
- package/dist/src/database/migration/create/file-generator.d.ts +2 -0
- package/dist/src/database/migration/create/file-generator.d.ts.map +1 -1
- package/dist/src/database/migration/create/file-generator.js +7 -2
- package/dist/src/database/migration/create/file-generator.js.map +1 -1
- package/dist/src/database/migration/create/schema-model.d.ts +2 -0
- package/dist/src/database/migration/create/schema-model.d.ts.map +1 -1
- package/dist/src/database/migration/create/schema-model.js +1 -1
- package/dist/src/database/mysql.d.ts.map +1 -1
- package/dist/src/database/mysql.js +5 -11
- package/dist/src/database/mysql.js.map +1 -1
- package/dist/src/database/postgres.d.ts +1 -0
- package/dist/src/database/postgres.d.ts.map +1 -1
- package/dist/src/database/postgres.js +1 -13
- package/dist/src/database/postgres.js.map +1 -1
- package/dist/src/database/schema/AlterBlueprint.d.ts +44 -0
- package/dist/src/database/schema/AlterBlueprint.d.ts.map +1 -0
- package/dist/src/database/schema/AlterBlueprint.js +82 -0
- package/dist/src/database/schema/AlterBlueprint.js.map +1 -0
- package/dist/src/database/schema/Blueprint.d.ts +16 -0
- package/dist/src/database/schema/Blueprint.d.ts.map +1 -0
- package/dist/src/database/schema/Blueprint.js +51 -0
- package/dist/src/database/schema/Blueprint.js.map +1 -0
- package/dist/src/database/schema/BlueprintBase.d.ts +59 -0
- package/dist/src/database/schema/BlueprintBase.d.ts.map +1 -0
- package/dist/src/database/schema/BlueprintBase.js +349 -0
- package/dist/src/database/schema/BlueprintBase.js.map +1 -0
- package/dist/src/database/schema/ColumnDefinition.d.ts +43 -0
- package/dist/src/database/schema/ColumnDefinition.d.ts.map +1 -0
- package/dist/src/database/schema/ColumnDefinition.js +117 -0
- package/dist/src/database/schema/ColumnDefinition.js.map +1 -0
- package/dist/src/database/schema/Schema.d.ts +32 -0
- package/dist/src/database/schema/Schema.d.ts.map +1 -0
- package/dist/src/database/schema/Schema.js +148 -0
- package/dist/src/database/schema/Schema.js.map +1 -0
- package/dist/src/database/schema/grammar/Grammar.d.ts +34 -0
- package/dist/src/database/schema/grammar/Grammar.d.ts.map +1 -0
- package/dist/src/database/schema/grammar/Grammar.js +36 -0
- package/dist/src/database/schema/grammar/Grammar.js.map +1 -0
- package/dist/src/database/schema/grammar/MySQLGrammar.d.ts +28 -0
- package/dist/src/database/schema/grammar/MySQLGrammar.d.ts.map +1 -0
- package/dist/src/database/schema/grammar/MySQLGrammar.js +190 -0
- package/dist/src/database/schema/grammar/MySQLGrammar.js.map +1 -0
- package/dist/src/database/schema/grammar/PostgresGrammar.d.ts +28 -0
- package/dist/src/database/schema/grammar/PostgresGrammar.d.ts.map +1 -0
- package/dist/src/database/schema/grammar/PostgresGrammar.js +223 -0
- package/dist/src/database/schema/grammar/PostgresGrammar.js.map +1 -0
- package/dist/src/database/schema/index.d.ts +9 -0
- package/dist/src/database/schema/index.d.ts.map +1 -0
- package/dist/src/database/schema/index.js +12 -0
- package/dist/src/database/schema/index.js.map +1 -0
- package/docs/content/cli.md +4 -1
- package/docs/content/configuration.md +6 -0
- package/docs/content/database.md +128 -0
- package/package.json +1 -1
- package/src/app/config.ts +2 -0
- package/src/database/common.ts +51 -0
- package/src/database/factory.ts +59 -0
- package/src/database/index.ts +2 -0
- package/src/database/migration/MigrationResetCommand.ts +6 -22
- package/src/database/migration/MigrationRunCommand.ts +2 -0
- package/src/database/migration/create/MigrationCreateCommand.ts +40 -12
- package/src/database/migration/create/builder-regenerator.ts +287 -0
- package/src/database/migration/create/db-reader.ts +43 -6
- package/src/database/migration/create/ddl-generator.ts +35 -212
- package/src/database/migration/create/file-generator.ts +5 -1
- package/src/database/migration/create/schema-model.ts +2 -0
- package/src/database/mysql.ts +7 -15
- package/src/database/postgres.ts +3 -17
- package/src/database/schema/AlterBlueprint.ts +85 -0
- package/src/database/schema/Blueprint.ts +43 -0
- package/src/database/schema/BlueprintBase.ts +386 -0
- package/src/database/schema/ColumnDefinition.ts +131 -0
- package/src/database/schema/Schema.ts +164 -0
- package/src/database/schema/grammar/Grammar.ts +61 -0
- package/src/database/schema/grammar/MySQLGrammar.ts +196 -0
- package/src/database/schema/grammar/PostgresGrammar.ts +232 -0
- package/src/database/schema/index.ts +8 -0
|
@@ -3,14 +3,11 @@ import { SQLDatabaseAdapter } from '@deepkit/sql';
|
|
|
3
3
|
import { existsSync, mkdirSync, readdirSync, unlinkSync, writeFileSync } from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
|
|
6
|
-
import { getAppConfig } from '../../app/resolver';
|
|
7
6
|
import { DBProvider } from '../../app/state';
|
|
8
7
|
import { createLogger } from '../../services';
|
|
9
8
|
import { getDialect } from '../dialect';
|
|
10
|
-
import {
|
|
9
|
+
import { generateBuilderMigrationFile } from './create/builder-regenerator';
|
|
11
10
|
import { readEntitiesSchema } from './create/entity-reader';
|
|
12
|
-
import { buildFileContent } from './create/file-generator';
|
|
13
|
-
import { SchemaDiff } from './create/schema-model';
|
|
14
11
|
import { getSourceMigrationsDir } from './helpers';
|
|
15
12
|
|
|
16
13
|
@cli.controller('migration:reset')
|
|
@@ -21,13 +18,11 @@ export class MigrationResetCommand {
|
|
|
21
18
|
constructor(private dbProvider: DBProvider) {}
|
|
22
19
|
|
|
23
20
|
async execute() {
|
|
24
|
-
// Step 1: Ensure migrations directory exists
|
|
25
21
|
if (!existsSync(this.migrationsDir)) {
|
|
26
22
|
this.logger.info(`Creating migrations directory: ${this.migrationsDir}`);
|
|
27
23
|
mkdirSync(this.migrationsDir, { recursive: true });
|
|
28
24
|
}
|
|
29
25
|
|
|
30
|
-
// Step 2: Remove all .ts files from migrations directory
|
|
31
26
|
const files = readdirSync(this.migrationsDir).filter(f => f.endsWith('.ts'));
|
|
32
27
|
this.logger.info(`Removing ${files.length} migration file(s)`);
|
|
33
28
|
for (const file of files) {
|
|
@@ -36,33 +31,22 @@ export class MigrationResetCommand {
|
|
|
36
31
|
this.logger.info(`Removed ${file}`);
|
|
37
32
|
}
|
|
38
33
|
|
|
39
|
-
// Step 3: Read entity schema from code definitions
|
|
40
34
|
const db = this.dbProvider.db;
|
|
41
35
|
const dialect = getDialect(db.adapter as SQLDatabaseAdapter);
|
|
42
|
-
const pgSchema = dialect === 'postgres' ? (getAppConfig().PG_SCHEMA ?? 'public') : 'public';
|
|
43
36
|
|
|
44
|
-
|
|
37
|
+
// Reset reads entities in the active dialect's canonical form. The generated migration uses
|
|
38
|
+
// dialect-portable builder calls, but cross-dialect parity isn't perfect — see docs.
|
|
39
|
+
this.logger.info(`Reading entity definitions (dialect: ${dialect})...`);
|
|
45
40
|
const entitySchema = readEntitiesSchema(db, dialect);
|
|
46
41
|
const tables = Array.from(entitySchema.values());
|
|
47
42
|
this.logger.info(`Found ${tables.length} entity table(s)`);
|
|
48
43
|
|
|
49
|
-
|
|
50
|
-
const diff: SchemaDiff = {
|
|
51
|
-
dialect,
|
|
52
|
-
pgSchema: dialect === 'postgres' ? pgSchema : undefined,
|
|
53
|
-
addedTables: tables,
|
|
54
|
-
removedTables: [],
|
|
55
|
-
modifiedTables: []
|
|
56
|
-
};
|
|
57
|
-
const statements = generateDDL(diff);
|
|
58
|
-
|
|
59
|
-
if (statements.length === 0) {
|
|
44
|
+
if (tables.length === 0) {
|
|
60
45
|
this.logger.info('No tables found to generate base migration.');
|
|
61
46
|
return;
|
|
62
47
|
}
|
|
63
48
|
|
|
64
|
-
|
|
65
|
-
const migrationContent = buildFileContent(statements);
|
|
49
|
+
const migrationContent = generateBuilderMigrationFile(tables);
|
|
66
50
|
const migrationPath = path.join(this.migrationsDir, '00000000_000000_base.ts');
|
|
67
51
|
|
|
68
52
|
writeFileSync(migrationPath, migrationContent, 'utf8');
|
|
@@ -110,6 +110,8 @@ export class MigrationRunCommand {
|
|
|
110
110
|
|
|
111
111
|
try {
|
|
112
112
|
await migrationModule.default(this.dbProvider.db);
|
|
113
|
+
// Apply deferred schema-builder statements (FKs) and reset per-migration state.
|
|
114
|
+
await this.dbProvider.db.schema.flush();
|
|
113
115
|
} catch (err) {
|
|
114
116
|
this.logger.error('Migration function failed to execute', err, { file });
|
|
115
117
|
throw err;
|
|
@@ -5,11 +5,12 @@ import { getAppConfig } from '../../../app/resolver';
|
|
|
5
5
|
import { DBProvider } from '../../../app/state';
|
|
6
6
|
import { createLogger, pinoLogger } from '../../../services';
|
|
7
7
|
import { getDialect } from '../../dialect';
|
|
8
|
+
import { generateBuilderMigrationFromDiff } from './builder-regenerator';
|
|
8
9
|
import { compareSchemas } from './comparator';
|
|
9
10
|
import { readAllTableNames, readDatabaseSchema } from './db-reader';
|
|
10
11
|
import { generateDDL } from './ddl-generator';
|
|
11
12
|
import { readEntitiesSchema } from './entity-reader';
|
|
12
|
-
import { generateMigrationFile } from './file-generator';
|
|
13
|
+
import { generateMigrationFile, writeMigrationFile } from './file-generator';
|
|
13
14
|
import { promptMigrationDescription, setNonInteractive } from './prompt';
|
|
14
15
|
import { INTERNAL_TABLES } from './schema-model';
|
|
15
16
|
|
|
@@ -19,7 +20,10 @@ export class MigrationCreateCommand {
|
|
|
19
20
|
|
|
20
21
|
constructor(private dbProvider: DBProvider) {}
|
|
21
22
|
|
|
22
|
-
async execute(
|
|
23
|
+
async execute(
|
|
24
|
+
nonInteractive: boolean & Flag<{ description: 'Skip interactive prompts' }> = false,
|
|
25
|
+
raw: boolean & Flag<{ description: 'Emit raw dialect-specific SQL instead of dialect-portable schema-builder calls' }> = false
|
|
26
|
+
) {
|
|
23
27
|
if (nonInteractive) {
|
|
24
28
|
setNonInteractive(true);
|
|
25
29
|
}
|
|
@@ -50,10 +54,27 @@ export class MigrationCreateCommand {
|
|
|
50
54
|
this.logger.info('Comparing schemas...');
|
|
51
55
|
const diff = await compareSchemas(entitySchema, dbSchema, dialect, !nonInteractive, pgSchema);
|
|
52
56
|
|
|
53
|
-
// Step 5:
|
|
54
|
-
const
|
|
57
|
+
// Step 5: Decide if there are any changes
|
|
58
|
+
const hasChanges =
|
|
59
|
+
diff.addedTables.length > 0 ||
|
|
60
|
+
diff.removedTables.length > 0 ||
|
|
61
|
+
diff.modifiedTables.some(
|
|
62
|
+
t =>
|
|
63
|
+
t.addedColumns.length > 0 ||
|
|
64
|
+
t.removedColumns.length > 0 ||
|
|
65
|
+
t.modifiedColumns.length > 0 ||
|
|
66
|
+
t.renamedColumns.length > 0 ||
|
|
67
|
+
t.addedIndexes.length > 0 ||
|
|
68
|
+
t.removedIndexes.length > 0 ||
|
|
69
|
+
t.addedForeignKeys.length > 0 ||
|
|
70
|
+
t.removedForeignKeys.length > 0 ||
|
|
71
|
+
t.primaryKeyChanged ||
|
|
72
|
+
t.addedEnumTypes.length > 0 ||
|
|
73
|
+
t.removedEnumTypes.length > 0 ||
|
|
74
|
+
t.modifiedEnumTypes.length > 0
|
|
75
|
+
);
|
|
55
76
|
|
|
56
|
-
if (
|
|
77
|
+
if (!hasChanges) {
|
|
57
78
|
this.logger.info('No schema changes detected.');
|
|
58
79
|
return;
|
|
59
80
|
}
|
|
@@ -78,16 +99,23 @@ export class MigrationCreateCommand {
|
|
|
78
99
|
this.logger.info(` ${table.tableName}: ${changes.join(', ')}`);
|
|
79
100
|
}
|
|
80
101
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.logger.info(` ${stmt}`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Step 7: Prompt for description and generate file
|
|
102
|
+
// Step 7: Render migration content (builder by default; --raw for legacy SQL)
|
|
103
|
+
let filePath: string;
|
|
87
104
|
pinoLogger.flush();
|
|
88
105
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
89
106
|
const description = await promptMigrationDescription();
|
|
90
|
-
|
|
107
|
+
|
|
108
|
+
if (raw) {
|
|
109
|
+
const statements = generateDDL(diff);
|
|
110
|
+
this.logger.info(`\nDDL statements (${statements.length}):`);
|
|
111
|
+
for (const stmt of statements) this.logger.info(` ${stmt}`);
|
|
112
|
+
filePath = generateMigrationFile(statements, description);
|
|
113
|
+
} else {
|
|
114
|
+
const content = generateBuilderMigrationFromDiff(diff);
|
|
115
|
+
this.logger.info(`\nGenerated builder migration (${content.split('\n').length} lines)`);
|
|
116
|
+
filePath = writeMigrationFile(content, description);
|
|
117
|
+
}
|
|
118
|
+
|
|
91
119
|
this.logger.info(`\nMigration file created: ${filePath}`);
|
|
92
120
|
} finally {
|
|
93
121
|
setNonInteractive(false);
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { ColumnSchema, Dialect, ForeignKeySchema, IndexSchema, SchemaDiff, TableDiff, TableSchema } from './schema-model';
|
|
2
|
+
|
|
3
|
+
const FILE_HEADER = `import { createMigration } from '@zyno-io/dk-server-foundation';\n\nexport default createMigration(async db => {\n`;
|
|
4
|
+
const FILE_FOOTER = `\n});\n`;
|
|
5
|
+
|
|
6
|
+
/** Render a TypeScript migration file that uses db.schema builder to recreate the given tables. */
|
|
7
|
+
export function generateBuilderMigrationFile(tables: TableSchema[]): string {
|
|
8
|
+
const blocks = tables.map(t => renderTableBlock(t));
|
|
9
|
+
return `${FILE_HEADER}${blocks.join('\n\n')}${FILE_FOOTER}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Render a TypeScript migration file from a SchemaDiff. Used by migration:create to emit
|
|
14
|
+
* portable builder-based migrations.
|
|
15
|
+
*
|
|
16
|
+
* Order: drop removed tables → create added tables → alter modified tables. Within an alter:
|
|
17
|
+
* Schema.alter() internally orders operations dependency-safe, so emission order doesn't matter.
|
|
18
|
+
*/
|
|
19
|
+
export function generateBuilderMigrationFromDiff(diff: SchemaDiff): string {
|
|
20
|
+
const blocks: string[] = [];
|
|
21
|
+
|
|
22
|
+
for (const table of diff.removedTables) {
|
|
23
|
+
blocks.push(` await db.schema.drop(${quoteStr(table.name)});`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const table of diff.addedTables) {
|
|
27
|
+
blocks.push(renderTableBlock(table));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const tableDiff of diff.modifiedTables) {
|
|
31
|
+
const block = renderAlterBlock(tableDiff, diff.dialect);
|
|
32
|
+
if (block) blocks.push(block);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (blocks.length === 0) {
|
|
36
|
+
return `${FILE_HEADER} // No schema changes detected.${FILE_FOOTER}`;
|
|
37
|
+
}
|
|
38
|
+
return `${FILE_HEADER}${blocks.join('\n\n')}${FILE_FOOTER}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderAlterBlock(td: TableDiff, dialect: Dialect): string {
|
|
42
|
+
const preLines: string[] = [];
|
|
43
|
+
const innerLines: string[] = [];
|
|
44
|
+
const postLines: string[] = [];
|
|
45
|
+
|
|
46
|
+
// PG enum lifecycle (raw SQL — first-class builder support is post-Phase-2)
|
|
47
|
+
if (dialect === 'postgres') {
|
|
48
|
+
for (const newType of td.addedEnumTypes) {
|
|
49
|
+
preLines.push(` await db.schema.enumType(${quoteStr(newType.typeName)}, [${newType.values.map(quoteStr).join(', ')}]);`);
|
|
50
|
+
}
|
|
51
|
+
for (const mod of td.modifiedEnumTypes) {
|
|
52
|
+
for (const value of mod.added) {
|
|
53
|
+
preLines.push(
|
|
54
|
+
` await db.schema.raw(${quoteStr(`ALTER TYPE "${mod.typeName}" ADD VALUE IF NOT EXISTS '${value.replace(/'/g, "''")}'`)});`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const typeName of td.removedEnumTypes) {
|
|
59
|
+
postLines.push(` await db.schema.raw(${quoteStr(`DROP CAST IF EXISTS (text AS "${typeName}")`)});`);
|
|
60
|
+
postLines.push(` await db.schema.raw(${quoteStr(`DROP TYPE IF EXISTS "${typeName}"`)});`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// PK changes (drop existing first, replacement comes via .primary())
|
|
65
|
+
if (td.primaryKeyChanged && td.oldPrimaryKey && td.oldPrimaryKey.length > 0) {
|
|
66
|
+
innerLines.push(` t.dropPrimary();`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const fk of td.removedForeignKeys) innerLines.push(` t.dropForeign(${quoteStr(fk.name)});`);
|
|
70
|
+
for (const idx of td.removedIndexes) innerLines.push(` t.dropIndex(${quoteStr(idx.name)});`);
|
|
71
|
+
for (const col of td.removedColumns) innerLines.push(` t.dropColumn(${quoteStr(col.name)});`);
|
|
72
|
+
for (const r of td.renamedColumns) innerLines.push(` t.renameColumn(${quoteStr(r.from)}, ${quoteStr(r.to)});`);
|
|
73
|
+
|
|
74
|
+
const stubTable: TableSchema = { name: td.tableName, columns: [], indexes: [], foreignKeys: [] };
|
|
75
|
+
|
|
76
|
+
for (const col of td.addedColumns) {
|
|
77
|
+
let line = renderColumnLine(col, stubTable);
|
|
78
|
+
// MySQL: if we know the entity-order position, emit .after() / .first() to preserve column layout
|
|
79
|
+
if (dialect === 'mysql' && td.entityColumns) {
|
|
80
|
+
const positioning = renderPositioning(col, td.entityColumns, td.addedColumns);
|
|
81
|
+
if (positioning) line = line.replace(/;$/, `${positioning};`);
|
|
82
|
+
}
|
|
83
|
+
innerLines.push(` ${line}`);
|
|
84
|
+
}
|
|
85
|
+
for (const mod of td.modifiedColumns) {
|
|
86
|
+
// Modified column: render as if adding, append .change()
|
|
87
|
+
const line = renderColumnLine(mod.newColumn, stubTable);
|
|
88
|
+
innerLines.push(` ${line.replace(/;$/, '.change();')}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (td.primaryKeyChanged && td.newPrimaryKey && td.newPrimaryKey.length > 0) {
|
|
92
|
+
innerLines.push(` t.primary([${td.newPrimaryKey.map(quoteStr).join(', ')}]);`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const idx of td.addedIndexes) innerLines.push(` ${renderIndexLine(idx, stubTable)}`);
|
|
96
|
+
for (const fk of td.addedForeignKeys) innerLines.push(` ${renderForeignKeyLine(fk, stubTable)}`);
|
|
97
|
+
|
|
98
|
+
if (innerLines.length === 0 && preLines.length === 0 && postLines.length === 0) return '';
|
|
99
|
+
|
|
100
|
+
const parts: string[] = [];
|
|
101
|
+
if (preLines.length > 0) parts.push(preLines.join('\n'));
|
|
102
|
+
if (innerLines.length > 0) {
|
|
103
|
+
parts.push(` await db.schema.alter(${quoteStr(td.tableName)}, t => {\n${innerLines.join('\n')}\n });`);
|
|
104
|
+
}
|
|
105
|
+
if (postLines.length > 0) parts.push(postLines.join('\n'));
|
|
106
|
+
return parts.join('\n\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function renderTableBlock(table: TableSchema): string {
|
|
110
|
+
const lines: string[] = [];
|
|
111
|
+
|
|
112
|
+
for (const col of table.columns) {
|
|
113
|
+
lines.push(` ${renderColumnLine(col, table)}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Composite PK — single-column PK is emitted via .primary() on the column line
|
|
117
|
+
const pkCols = table.columns.filter(c => c.isPrimaryKey).map(c => c.name);
|
|
118
|
+
if (pkCols.length > 1) {
|
|
119
|
+
lines.push(` t.primary([${pkCols.map(quoteStr).join(', ')}]);`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const idx of table.indexes) {
|
|
123
|
+
lines.push(` ${renderIndexLine(idx, table)}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const fk of table.foreignKeys) {
|
|
127
|
+
lines.push(` ${renderForeignKeyLine(fk, table)}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return ` await db.schema.create(${quoteStr(table.name)}, t => {\n${lines.join('\n')}\n });`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Emit `.after('x')` / `.first()` based on entity-order position of an added column. */
|
|
134
|
+
function renderPositioning(col: ColumnSchema, entityColumns: ColumnSchema[], addedColumns: ColumnSchema[]): string {
|
|
135
|
+
const idx = entityColumns.findIndex(c => c.name === col.name);
|
|
136
|
+
if (idx < 0) return '';
|
|
137
|
+
const addedNames = new Set(addedColumns.map(c => c.name));
|
|
138
|
+
// Walk backwards to find the previous column that already exists in the DB (not in the same add batch)
|
|
139
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
140
|
+
const prev = entityColumns[i];
|
|
141
|
+
if (!addedNames.has(prev.name)) return `.after(${quoteStr(prev.name)})`;
|
|
142
|
+
}
|
|
143
|
+
return '.first()';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderColumnLine(col: ColumnSchema, table: TableSchema): string {
|
|
147
|
+
let line = `t.${pickBuilderMethod(col)}`;
|
|
148
|
+
|
|
149
|
+
// Modifier order chosen to read naturally
|
|
150
|
+
if (col.unsigned) line += '.unsigned()';
|
|
151
|
+
if (col.nullable) line += '.nullable()';
|
|
152
|
+
if (col.autoIncrement) line += '.autoIncrement()';
|
|
153
|
+
|
|
154
|
+
const pkCount = table.columns.filter(c => c.isPrimaryKey).length;
|
|
155
|
+
if (col.isPrimaryKey && pkCount === 1) line += '.primary()';
|
|
156
|
+
|
|
157
|
+
if (col.defaultExpression) {
|
|
158
|
+
line += `.defaultRaw(${quoteStr(col.defaultExpression)})`;
|
|
159
|
+
} else if (col.defaultValue !== undefined) {
|
|
160
|
+
line += `.default(${renderJsValue(col.defaultValue)})`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (col.onUpdateExpression) {
|
|
164
|
+
line += `.onUpdate(${quoteStr(col.onUpdateExpression)})`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return line + ';';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function pickBuilderMethod(col: ColumnSchema): string {
|
|
171
|
+
const n = quoteStr(col.name);
|
|
172
|
+
switch (col.type) {
|
|
173
|
+
case 'varchar':
|
|
174
|
+
return `string(${n}, ${col.size ?? 255})`;
|
|
175
|
+
case 'char':
|
|
176
|
+
// CHAR(36) is the canonical string-form UUID storage (dksf:uuid annotation)
|
|
177
|
+
if (col.size === 36) return `uuidString(${n})`;
|
|
178
|
+
return `char(${n}, ${col.size ?? 1})`;
|
|
179
|
+
case 'text':
|
|
180
|
+
return `text(${n})`;
|
|
181
|
+
case 'tinyint':
|
|
182
|
+
// tinyint(1) is the canonical MySQL boolean storage; map back to .boolean()
|
|
183
|
+
if (col.size === 1) return `boolean(${n})`;
|
|
184
|
+
return `tinyint(${n})`;
|
|
185
|
+
case 'smallint':
|
|
186
|
+
return `smallint(${n})`;
|
|
187
|
+
case 'int':
|
|
188
|
+
case 'integer':
|
|
189
|
+
return `integer(${n})`;
|
|
190
|
+
case 'bigint':
|
|
191
|
+
return `bigInteger(${n})`;
|
|
192
|
+
case 'boolean':
|
|
193
|
+
return `boolean(${n})`;
|
|
194
|
+
case 'float':
|
|
195
|
+
case 'real':
|
|
196
|
+
return `float(${n})`;
|
|
197
|
+
case 'double':
|
|
198
|
+
case 'double precision':
|
|
199
|
+
return `double(${n})`;
|
|
200
|
+
case 'decimal':
|
|
201
|
+
case 'numeric': {
|
|
202
|
+
const args = [n];
|
|
203
|
+
if (col.size !== undefined) {
|
|
204
|
+
args.push(String(col.size));
|
|
205
|
+
if (col.scale !== undefined) args.push(String(col.scale));
|
|
206
|
+
}
|
|
207
|
+
return `decimal(${args.join(', ')})`;
|
|
208
|
+
}
|
|
209
|
+
case 'date':
|
|
210
|
+
return `date(${n})`;
|
|
211
|
+
case 'datetime':
|
|
212
|
+
case 'timestamp':
|
|
213
|
+
return `dateTime(${n})`;
|
|
214
|
+
case 'timestamptz':
|
|
215
|
+
return `timestamptz(${n})`;
|
|
216
|
+
case 'binary':
|
|
217
|
+
// BINARY(16) is the canonical UUID storage on MySQL (Deepkit's UUIDv4 annotation)
|
|
218
|
+
if (col.size === 16) return `uuid(${n})`;
|
|
219
|
+
return `binary(${n}, ${col.size ?? 16})`;
|
|
220
|
+
case 'blob':
|
|
221
|
+
case 'bytea':
|
|
222
|
+
return `blob(${n})`;
|
|
223
|
+
case 'json':
|
|
224
|
+
return `json(${n})`;
|
|
225
|
+
case 'jsonb':
|
|
226
|
+
return `jsonb(${n})`;
|
|
227
|
+
case 'uuid':
|
|
228
|
+
return `uuid(${n})`;
|
|
229
|
+
case 'point':
|
|
230
|
+
return `point(${n})`;
|
|
231
|
+
case 'enum': {
|
|
232
|
+
const values = col.enumValues ?? [];
|
|
233
|
+
const valuesArg = `[${values.map(v => quoteStr(v)).join(', ')}]`;
|
|
234
|
+
const typeName = col.enumTypeName;
|
|
235
|
+
const args = typeName ? [n, valuesArg, quoteStr(typeName)] : [n, valuesArg];
|
|
236
|
+
return `enum(${args.join(', ')})`;
|
|
237
|
+
}
|
|
238
|
+
default:
|
|
239
|
+
throw new Error(`Unsupported column type for builder regeneration: '${col.type}' (column ${col.name})`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function renderIndexLine(idx: IndexSchema, table: TableSchema): string {
|
|
244
|
+
const colsArg = renderColumnsArg(idx.columns);
|
|
245
|
+
|
|
246
|
+
const suffix = idx.spatial ? 'spatial' : idx.unique ? 'unique' : 'index';
|
|
247
|
+
const expectedName = `${table.name}_${idx.columns.join('_')}_${suffix}`;
|
|
248
|
+
const nameArg = idx.name === expectedName ? '' : `, ${quoteStr(idx.name)}`;
|
|
249
|
+
|
|
250
|
+
if (idx.spatial) return `t.spatialIndex(${colsArg}${nameArg});`;
|
|
251
|
+
if (idx.unique) return `t.unique(${colsArg}${nameArg});`;
|
|
252
|
+
return `t.index(${colsArg}${nameArg});`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function renderForeignKeyLine(fk: ForeignKeySchema, table: TableSchema): string {
|
|
256
|
+
const colsArg = renderColumnsArg(fk.columns);
|
|
257
|
+
const expectedName = `${table.name}_${fk.columns.join('_')}_foreign`;
|
|
258
|
+
const nameArg = fk.name === expectedName ? '' : `, ${quoteStr(fk.name)}`;
|
|
259
|
+
|
|
260
|
+
let line = `t.foreign(${colsArg}${nameArg})`;
|
|
261
|
+
if (fk.referencedColumns.length === 1) {
|
|
262
|
+
line += `.references(${quoteStr(fk.referencedColumns[0])})`;
|
|
263
|
+
} else {
|
|
264
|
+
line += `.referencesAll([${fk.referencedColumns.map(quoteStr).join(', ')}])`;
|
|
265
|
+
}
|
|
266
|
+
line += `.on(${quoteStr(fk.referencedTable)})`;
|
|
267
|
+
if (fk.onDelete && fk.onDelete.toUpperCase() !== 'RESTRICT') line += `.onDelete(${quoteStr(fk.onDelete)})`;
|
|
268
|
+
if (fk.onUpdate && fk.onUpdate.toUpperCase() !== 'RESTRICT') line += `.onUpdate(${quoteStr(fk.onUpdate)})`;
|
|
269
|
+
return line + ';';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function renderColumnsArg(columns: string[]): string {
|
|
273
|
+
if (columns.length === 1) return quoteStr(columns[0]);
|
|
274
|
+
return `[${columns.map(quoteStr).join(', ')}]`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function quoteStr(s: string): string {
|
|
278
|
+
return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function renderJsValue(value: unknown): string {
|
|
282
|
+
if (value === null) return 'null';
|
|
283
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
284
|
+
if (typeof value === 'number') return String(value);
|
|
285
|
+
if (typeof value === 'string') return quoteStr(value);
|
|
286
|
+
return JSON.stringify(value);
|
|
287
|
+
}
|
|
@@ -47,9 +47,14 @@ async function readMySQLTable(db: BaseDatabase, tableName: string): Promise<Tabl
|
|
|
47
47
|
const columns = await readMySQLColumns(db, tableName);
|
|
48
48
|
if (columns.length === 0) return null;
|
|
49
49
|
|
|
50
|
-
const
|
|
50
|
+
const allIndexes = await readMySQLIndexes(db, tableName);
|
|
51
51
|
const foreignKeys = await readMySQLForeignKeys(db, tableName);
|
|
52
52
|
|
|
53
|
+
// MySQL auto-creates an index for FK columns (same name as the FK) if no covering index exists.
|
|
54
|
+
// Filter those out so they don't show up as spurious added/removed-index diffs.
|
|
55
|
+
const fkNames = new Set(foreignKeys.map(fk => fk.name));
|
|
56
|
+
const indexes = allIndexes.filter(idx => !fkNames.has(idx.name));
|
|
57
|
+
|
|
53
58
|
return { name: tableName, columns, indexes, foreignKeys };
|
|
54
59
|
}
|
|
55
60
|
|
|
@@ -74,7 +79,9 @@ async function readMySQLColumns(db: BaseDatabase, tableName: string): Promise<Co
|
|
|
74
79
|
name: row.COLUMN_NAME,
|
|
75
80
|
type: normalizeMySQLType(dataType, columnType),
|
|
76
81
|
size: inferMySQLSize(dataType, columnType, row.CHARACTER_MAXIMUM_LENGTH, row.NUMERIC_PRECISION),
|
|
77
|
-
|
|
82
|
+
// Scale is only meaningful for decimal/numeric. MySQL reports NUMERIC_SCALE=0
|
|
83
|
+
// for ints, which would cause spurious typeChanged diffs against entity-reader output.
|
|
84
|
+
scale: (dataType === 'decimal' || dataType === 'numeric') && row.NUMERIC_SCALE != null ? Number(row.NUMERIC_SCALE) : undefined,
|
|
78
85
|
unsigned: columnType.includes('unsigned'),
|
|
79
86
|
nullable: row.IS_NULLABLE === 'YES',
|
|
80
87
|
autoIncrement: extra.includes('auto_increment'),
|
|
@@ -230,7 +237,7 @@ async function readPostgresColumns(db: BaseDatabase, tableName: string, pgSchema
|
|
|
230
237
|
name: row.column_name,
|
|
231
238
|
type: dataType,
|
|
232
239
|
size: inferPostgresSize(dataType, row.character_maximum_length, row.numeric_precision),
|
|
233
|
-
scale: row.numeric_scale != null ? Number(row.numeric_scale) : undefined,
|
|
240
|
+
scale: (dataType === 'decimal' || dataType === 'numeric') && row.numeric_scale != null ? Number(row.numeric_scale) : undefined,
|
|
234
241
|
unsigned: false,
|
|
235
242
|
nullable: row.is_nullable === 'YES',
|
|
236
243
|
autoIncrement: isSerial,
|
|
@@ -331,12 +338,42 @@ async function readPostgresIndexes(db: BaseDatabase, tableName: string, pgSchema
|
|
|
331
338
|
|
|
332
339
|
return rows.map(row => ({
|
|
333
340
|
name: row.index_name,
|
|
334
|
-
columns:
|
|
341
|
+
columns: parsePgTextArray(row.columns),
|
|
335
342
|
unique: row.is_unique,
|
|
336
343
|
spatial: false
|
|
337
344
|
}));
|
|
338
345
|
}
|
|
339
346
|
|
|
347
|
+
/** Parse a Postgres text array literal like `{a,b,"c d"}` into a JS string array.
|
|
348
|
+
* Accepts already-parsed arrays unchanged. */
|
|
349
|
+
function parsePgTextArray(value: unknown): string[] {
|
|
350
|
+
if (Array.isArray(value)) return value as string[];
|
|
351
|
+
if (typeof value !== 'string') return [];
|
|
352
|
+
const s = value.trim();
|
|
353
|
+
if (!s.startsWith('{') || !s.endsWith('}')) return [s];
|
|
354
|
+
const inner = s.slice(1, -1);
|
|
355
|
+
if (inner === '') return [];
|
|
356
|
+
// Split on commas not inside quotes; strip surrounding double quotes if present
|
|
357
|
+
const out: string[] = [];
|
|
358
|
+
let cur = '';
|
|
359
|
+
let inQuote = false;
|
|
360
|
+
for (let i = 0; i < inner.length; i++) {
|
|
361
|
+
const ch = inner[i];
|
|
362
|
+
if (ch === '"' && inner[i - 1] !== '\\') {
|
|
363
|
+
inQuote = !inQuote;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (ch === ',' && !inQuote) {
|
|
367
|
+
out.push(cur);
|
|
368
|
+
cur = '';
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
cur += ch;
|
|
372
|
+
}
|
|
373
|
+
if (cur.length > 0 || out.length > 0) out.push(cur);
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
|
|
340
377
|
async function readPostgresForeignKeys(db: BaseDatabase, tableName: string, pgSchema: string = 'public'): Promise<ForeignKeySchema[]> {
|
|
341
378
|
// Use pg_constraint to correctly pair multi-column FK columns via conkey/confkey arrays
|
|
342
379
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -371,9 +408,9 @@ async function readPostgresForeignKeys(db: BaseDatabase, tableName: string, pgSc
|
|
|
371
408
|
|
|
372
409
|
return rows.map(row => ({
|
|
373
410
|
name: row.constraint_name,
|
|
374
|
-
columns:
|
|
411
|
+
columns: parsePgTextArray(row.columns),
|
|
375
412
|
referencedTable: row.referenced_table,
|
|
376
|
-
referencedColumns:
|
|
413
|
+
referencedColumns: parsePgTextArray(row.referenced_columns),
|
|
377
414
|
onDelete: (row.delete_rule || 'RESTRICT').toUpperCase(),
|
|
378
415
|
onUpdate: (row.update_rule || 'RESTRICT').toUpperCase()
|
|
379
416
|
}));
|