@zyno-io/dk-server-foundation 26.412.2137 → 26.601.2229
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 +36 -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 +308 -0
- package/dist/src/database/migration/create/builder-regenerator.js.map +1 -0
- package/dist/src/database/migration/create/comparator.js +33 -4
- package/dist/src/database/migration/create/comparator.js.map +1 -1
- package/dist/src/database/migration/create/db-reader.js +63 -9
- 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 +44 -206
- package/dist/src/database/migration/create/ddl-generator.js.map +1 -1
- package/dist/src/database/migration/create/entity-reader.js +47 -5
- package/dist/src/database/migration/create/entity-reader.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 +9 -0
- package/dist/src/database/migration/create/schema-model.d.ts.map +1 -1
- package/dist/src/database/migration/create/schema-model.js +3 -3
- 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 +50 -0
- package/dist/src/database/schema/AlterBlueprint.d.ts.map +1 -0
- package/dist/src/database/schema/AlterBlueprint.js +88 -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 +65 -0
- package/dist/src/database/schema/BlueprintBase.d.ts.map +1 -0
- package/dist/src/database/schema/BlueprintBase.js +385 -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 +151 -0
- package/dist/src/database/schema/Schema.js.map +1 -0
- package/dist/src/database/schema/grammar/Grammar.d.ts +35 -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 +29 -0
- package/dist/src/database/schema/grammar/MySQLGrammar.d.ts.map +1 -0
- package/dist/src/database/schema/grammar/MySQLGrammar.js +199 -0
- package/dist/src/database/schema/grammar/MySQLGrammar.js.map +1 -0
- package/dist/src/database/schema/grammar/PostgresGrammar.d.ts +29 -0
- package/dist/src/database/schema/grammar/PostgresGrammar.d.ts.map +1 -0
- package/dist/src/database/schema/grammar/PostgresGrammar.js +233 -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/dist/src/database/types.d.ts +8 -0
- package/dist/src/database/types.d.ts.map +1 -1
- package/dist/src/database/types.js +3 -1
- package/dist/src/database/types.js.map +1 -1
- package/docs/content/cli.md +4 -1
- package/docs/content/configuration.md +6 -0
- package/docs/content/database.md +137 -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 +42 -12
- package/src/database/migration/create/builder-regenerator.ts +294 -0
- package/src/database/migration/create/comparator.ts +34 -4
- package/src/database/migration/create/db-reader.ts +60 -9
- package/src/database/migration/create/ddl-generator.ts +45 -212
- package/src/database/migration/create/entity-reader.ts +46 -5
- package/src/database/migration/create/file-generator.ts +5 -1
- package/src/database/migration/create/schema-model.ts +6 -0
- package/src/database/mysql.ts +7 -15
- package/src/database/postgres.ts +3 -17
- package/src/database/schema/AlterBlueprint.ts +92 -0
- package/src/database/schema/Blueprint.ts +43 -0
- package/src/database/schema/BlueprintBase.ts +425 -0
- package/src/database/schema/ColumnDefinition.ts +131 -0
- package/src/database/schema/Schema.ts +167 -0
- package/src/database/schema/grammar/Grammar.ts +62 -0
- package/src/database/schema/grammar/MySQLGrammar.ts +206 -0
- package/src/database/schema/grammar/PostgresGrammar.ts +243 -0
- package/src/database/schema/index.ts +8 -0
- package/src/database/types.ts +6 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ClassType } from '@deepkit/core';
|
|
2
|
+
import { Database } from '@deepkit/orm';
|
|
3
|
+
|
|
4
|
+
import { BaseDatabase } from './common';
|
|
5
|
+
import { Dialect } from './dialect';
|
|
6
|
+
import { createMySQLDatabase, IMySQLDatabaseAdapterConfig, MySQLDatabaseAdapter } from './mysql';
|
|
7
|
+
import { createPostgresDatabase, IPostgresDatabaseAdapterConfig, PostgresDatabaseAdapter } from './postgres';
|
|
8
|
+
|
|
9
|
+
export type AnyDatabaseAdapter = MySQLDatabaseAdapter | PostgresDatabaseAdapter;
|
|
10
|
+
|
|
11
|
+
export interface ICommonDatabaseAdapterConfig {
|
|
12
|
+
enableLocksTable?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type Entities = ConstructorParameters<typeof Database>[1];
|
|
16
|
+
|
|
17
|
+
export function createDatabase(config: ICommonDatabaseAdapterConfig, entities?: Entities): ClassType<BaseDatabase<AnyDatabaseAdapter>>;
|
|
18
|
+
export function createDatabase(
|
|
19
|
+
dialect: 'mysql',
|
|
20
|
+
config: IMySQLDatabaseAdapterConfig,
|
|
21
|
+
entities?: Entities
|
|
22
|
+
): ClassType<BaseDatabase<MySQLDatabaseAdapter>>;
|
|
23
|
+
export function createDatabase(
|
|
24
|
+
dialect: 'postgres',
|
|
25
|
+
config: IPostgresDatabaseAdapterConfig,
|
|
26
|
+
entities?: Entities
|
|
27
|
+
): ClassType<BaseDatabase<PostgresDatabaseAdapter>>;
|
|
28
|
+
export function createDatabase(
|
|
29
|
+
dialectOrConfig: Dialect | ICommonDatabaseAdapterConfig,
|
|
30
|
+
configOrEntities?: IMySQLDatabaseAdapterConfig | IPostgresDatabaseAdapterConfig | Entities,
|
|
31
|
+
maybeEntities?: Entities
|
|
32
|
+
): ClassType<BaseDatabase<AnyDatabaseAdapter>> {
|
|
33
|
+
let dialect: Dialect;
|
|
34
|
+
let config: IMySQLDatabaseAdapterConfig | IPostgresDatabaseAdapterConfig;
|
|
35
|
+
let entities: Entities;
|
|
36
|
+
|
|
37
|
+
if (typeof dialectOrConfig === 'string') {
|
|
38
|
+
dialect = dialectOrConfig;
|
|
39
|
+
config = configOrEntities as IMySQLDatabaseAdapterConfig | IPostgresDatabaseAdapterConfig;
|
|
40
|
+
entities = maybeEntities ?? [];
|
|
41
|
+
} else {
|
|
42
|
+
dialect = resolveDialectFromEnv();
|
|
43
|
+
config = dialectOrConfig;
|
|
44
|
+
entities = (configOrEntities as Entities | undefined) ?? [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return dialect === 'postgres'
|
|
48
|
+
? createPostgresDatabase(config as IPostgresDatabaseAdapterConfig, entities)
|
|
49
|
+
: createMySQLDatabase(config as IMySQLDatabaseAdapterConfig, entities);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveDialectFromEnv(): Dialect {
|
|
53
|
+
const value = process.env.DB_ADAPTER;
|
|
54
|
+
if (value === 'mysql' || value === 'postgres') return value;
|
|
55
|
+
throw new Error(
|
|
56
|
+
`createDatabase(sharedConfig) requires the DB_ADAPTER env var to be 'mysql' or 'postgres' (got: ${value ?? 'undefined'}). ` +
|
|
57
|
+
`Either set DB_ADAPTER or call createDatabase(dialect, config) explicitly.`
|
|
58
|
+
);
|
|
59
|
+
}
|
package/src/database/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export * from './common';
|
|
2
2
|
export * from './dialect';
|
|
3
3
|
export * from './entity';
|
|
4
|
+
export * from './factory';
|
|
4
5
|
export { createMigration, runMigrations, standardizeDbCollation } from './migration';
|
|
6
|
+
export * from './schema';
|
|
5
7
|
export * from './mysql';
|
|
6
8
|
export * from './postgres';
|
|
7
9
|
export * from './types';
|
|
@@ -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,28 @@ 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.renamedIndexes.length > 0 ||
|
|
70
|
+
t.addedForeignKeys.length > 0 ||
|
|
71
|
+
t.removedForeignKeys.length > 0 ||
|
|
72
|
+
t.primaryKeyChanged ||
|
|
73
|
+
t.addedEnumTypes.length > 0 ||
|
|
74
|
+
t.removedEnumTypes.length > 0 ||
|
|
75
|
+
t.modifiedEnumTypes.length > 0
|
|
76
|
+
);
|
|
55
77
|
|
|
56
|
-
if (
|
|
78
|
+
if (!hasChanges) {
|
|
57
79
|
this.logger.info('No schema changes detected.');
|
|
58
80
|
return;
|
|
59
81
|
}
|
|
@@ -74,20 +96,28 @@ export class MigrationCreateCommand {
|
|
|
74
96
|
if (table.renamedColumns.length > 0) changes.push(`${table.renamedColumns.length} renamed`);
|
|
75
97
|
if (table.addedIndexes.length > 0) changes.push(`+${table.addedIndexes.length} idx`);
|
|
76
98
|
if (table.removedIndexes.length > 0) changes.push(`-${table.removedIndexes.length} idx`);
|
|
99
|
+
if (table.renamedIndexes.length > 0) changes.push(`~${table.renamedIndexes.length} idx name`);
|
|
77
100
|
if (table.primaryKeyChanged) changes.push('PK changed');
|
|
78
101
|
this.logger.info(` ${table.tableName}: ${changes.join(', ')}`);
|
|
79
102
|
}
|
|
80
103
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.logger.info(` ${stmt}`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Step 7: Prompt for description and generate file
|
|
104
|
+
// Step 7: Render migration content (builder by default; --raw for legacy SQL)
|
|
105
|
+
let filePath: string;
|
|
87
106
|
pinoLogger.flush();
|
|
88
107
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
89
108
|
const description = await promptMigrationDescription();
|
|
90
|
-
|
|
109
|
+
|
|
110
|
+
if (raw) {
|
|
111
|
+
const statements = generateDDL(diff);
|
|
112
|
+
this.logger.info(`\nDDL statements (${statements.length}):`);
|
|
113
|
+
for (const stmt of statements) this.logger.info(` ${stmt}`);
|
|
114
|
+
filePath = generateMigrationFile(statements, description);
|
|
115
|
+
} else {
|
|
116
|
+
const content = generateBuilderMigrationFromDiff(diff);
|
|
117
|
+
this.logger.info(`\nGenerated builder migration (${content.split('\n').length} lines)`);
|
|
118
|
+
filePath = writeMigrationFile(content, description);
|
|
119
|
+
}
|
|
120
|
+
|
|
91
121
|
this.logger.info(`\nMigration file created: ${filePath}`);
|
|
92
122
|
} finally {
|
|
93
123
|
setNonInteractive(false);
|
|
@@ -0,0 +1,294 @@
|
|
|
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 r of td.renamedIndexes) innerLines.push(` t.renameIndex(${quoteStr(r.from)}, ${quoteStr(r.to)});`);
|
|
72
|
+
for (const col of td.removedColumns) innerLines.push(` t.dropColumn(${quoteStr(col.name)});`);
|
|
73
|
+
for (const r of td.renamedColumns) innerLines.push(` t.renameColumn(${quoteStr(r.from)}, ${quoteStr(r.to)});`);
|
|
74
|
+
|
|
75
|
+
const stubTable: TableSchema = { name: td.tableName, columns: [], indexes: [], foreignKeys: [] };
|
|
76
|
+
|
|
77
|
+
for (const col of td.addedColumns) {
|
|
78
|
+
let line = renderColumnLine(col, stubTable);
|
|
79
|
+
// MySQL: if we know the entity-order position, emit .after() / .first() to preserve column layout
|
|
80
|
+
if (dialect === 'mysql' && td.entityColumns) {
|
|
81
|
+
const positioning = renderPositioning(col, td.entityColumns, td.addedColumns);
|
|
82
|
+
if (positioning) line = line.replace(/;$/, `${positioning};`);
|
|
83
|
+
}
|
|
84
|
+
innerLines.push(` ${line}`);
|
|
85
|
+
}
|
|
86
|
+
for (const mod of td.modifiedColumns) {
|
|
87
|
+
// Modified column: render as if adding, append .change()
|
|
88
|
+
const line = renderColumnLine(mod.newColumn, stubTable);
|
|
89
|
+
innerLines.push(` ${line.replace(/;$/, '.change();')}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (td.primaryKeyChanged && td.newPrimaryKey && td.newPrimaryKey.length > 0) {
|
|
93
|
+
innerLines.push(` t.primary([${td.newPrimaryKey.map(quoteStr).join(', ')}]);`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const idx of td.addedIndexes) innerLines.push(` ${renderIndexLine(idx, stubTable)}`);
|
|
97
|
+
for (const fk of td.addedForeignKeys) innerLines.push(` ${renderForeignKeyLine(fk, stubTable)}`);
|
|
98
|
+
|
|
99
|
+
if (innerLines.length === 0 && preLines.length === 0 && postLines.length === 0) return '';
|
|
100
|
+
|
|
101
|
+
const parts: string[] = [];
|
|
102
|
+
if (preLines.length > 0) parts.push(preLines.join('\n'));
|
|
103
|
+
if (innerLines.length > 0) {
|
|
104
|
+
parts.push(` await db.schema.alter(${quoteStr(td.tableName)}, t => {\n${innerLines.join('\n')}\n });`);
|
|
105
|
+
}
|
|
106
|
+
if (postLines.length > 0) parts.push(postLines.join('\n'));
|
|
107
|
+
return parts.join('\n\n');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function renderTableBlock(table: TableSchema): string {
|
|
111
|
+
const lines: string[] = [];
|
|
112
|
+
|
|
113
|
+
for (const col of table.columns) {
|
|
114
|
+
lines.push(` ${renderColumnLine(col, table)}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Composite PK — single-column PK is emitted via .primary() on the column line
|
|
118
|
+
const pkCols = table.columns.filter(c => c.isPrimaryKey).map(c => c.name);
|
|
119
|
+
if (pkCols.length > 1) {
|
|
120
|
+
lines.push(` t.primary([${pkCols.map(quoteStr).join(', ')}]);`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const idx of table.indexes) {
|
|
124
|
+
lines.push(` ${renderIndexLine(idx, table)}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const fk of table.foreignKeys) {
|
|
128
|
+
lines.push(` ${renderForeignKeyLine(fk, table)}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return ` await db.schema.create(${quoteStr(table.name)}, t => {\n${lines.join('\n')}\n });`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Emit `.after('x')` / `.first()` based on entity-order position of an added column. */
|
|
135
|
+
function renderPositioning(col: ColumnSchema, entityColumns: ColumnSchema[], addedColumns: ColumnSchema[]): string {
|
|
136
|
+
const idx = entityColumns.findIndex(c => c.name === col.name);
|
|
137
|
+
if (idx < 0) return '';
|
|
138
|
+
const addedNames = new Set(addedColumns.map(c => c.name));
|
|
139
|
+
// Walk backwards to find the previous column that already exists in the DB (not in the same add batch)
|
|
140
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
141
|
+
const prev = entityColumns[i];
|
|
142
|
+
if (!addedNames.has(prev.name)) return `.after(${quoteStr(prev.name)})`;
|
|
143
|
+
}
|
|
144
|
+
return '.first()';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function renderColumnLine(col: ColumnSchema, table: TableSchema): string {
|
|
148
|
+
let line = `t.${pickBuilderMethod(col)}`;
|
|
149
|
+
|
|
150
|
+
// Modifier order chosen to read naturally
|
|
151
|
+
if (col.unsigned) line += '.unsigned()';
|
|
152
|
+
if (col.nullable) line += '.nullable()';
|
|
153
|
+
if (col.autoIncrement) line += '.autoIncrement()';
|
|
154
|
+
|
|
155
|
+
const pkCount = table.columns.filter(c => c.isPrimaryKey).length;
|
|
156
|
+
if (col.isPrimaryKey && pkCount === 1) line += '.primary()';
|
|
157
|
+
|
|
158
|
+
if (col.defaultExpression) {
|
|
159
|
+
line += `.defaultRaw(${quoteStr(col.defaultExpression)})`;
|
|
160
|
+
} else if (col.defaultValue !== undefined) {
|
|
161
|
+
line += `.default(${renderJsValue(col.defaultValue)})`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (col.onUpdateExpression) {
|
|
165
|
+
line += `.onUpdate(${quoteStr(col.onUpdateExpression)})`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return line + ';';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function pickBuilderMethod(col: ColumnSchema): string {
|
|
172
|
+
const n = quoteStr(col.name);
|
|
173
|
+
switch (col.type) {
|
|
174
|
+
case 'varchar':
|
|
175
|
+
return `string(${n}, ${col.size ?? 255})`;
|
|
176
|
+
case 'char':
|
|
177
|
+
// CHAR(36) is the canonical string-form UUID storage (dksf:uuid annotation)
|
|
178
|
+
if (col.size === 36) return `uuidString(${n})`;
|
|
179
|
+
return `char(${n}, ${col.size ?? 1})`;
|
|
180
|
+
case 'text':
|
|
181
|
+
return `text(${n})`;
|
|
182
|
+
case 'tinytext':
|
|
183
|
+
return `tinyText(${n})`;
|
|
184
|
+
case 'mediumtext':
|
|
185
|
+
return `mediumText(${n})`;
|
|
186
|
+
case 'longtext':
|
|
187
|
+
return `longText(${n})`;
|
|
188
|
+
case 'tinyint':
|
|
189
|
+
// tinyint(1) is the canonical MySQL boolean storage; map back to .boolean()
|
|
190
|
+
if (col.size === 1) return `boolean(${n})`;
|
|
191
|
+
return `tinyint(${n})`;
|
|
192
|
+
case 'smallint':
|
|
193
|
+
return `smallint(${n})`;
|
|
194
|
+
case 'int':
|
|
195
|
+
case 'integer':
|
|
196
|
+
return `integer(${n})`;
|
|
197
|
+
case 'bigint':
|
|
198
|
+
return `bigInteger(${n})`;
|
|
199
|
+
case 'boolean':
|
|
200
|
+
return `boolean(${n})`;
|
|
201
|
+
case 'float':
|
|
202
|
+
case 'real':
|
|
203
|
+
return `float(${n})`;
|
|
204
|
+
case 'double':
|
|
205
|
+
case 'double precision':
|
|
206
|
+
return `double(${n})`;
|
|
207
|
+
case 'decimal':
|
|
208
|
+
case 'numeric': {
|
|
209
|
+
const args = [n];
|
|
210
|
+
if (col.size !== undefined) {
|
|
211
|
+
args.push(String(col.size));
|
|
212
|
+
if (col.scale !== undefined) args.push(String(col.scale));
|
|
213
|
+
}
|
|
214
|
+
return `decimal(${args.join(', ')})`;
|
|
215
|
+
}
|
|
216
|
+
case 'date':
|
|
217
|
+
return `date(${n})`;
|
|
218
|
+
case 'datetime':
|
|
219
|
+
case 'timestamp':
|
|
220
|
+
return `dateTime(${n})`;
|
|
221
|
+
case 'timestamptz':
|
|
222
|
+
return `timestamptz(${n})`;
|
|
223
|
+
case 'binary':
|
|
224
|
+
// BINARY(16) is the canonical UUID storage on MySQL (Deepkit's UUIDv4 annotation)
|
|
225
|
+
if (col.size === 16) return `uuid(${n})`;
|
|
226
|
+
return `binary(${n}, ${col.size ?? 16})`;
|
|
227
|
+
case 'blob':
|
|
228
|
+
case 'bytea':
|
|
229
|
+
return `blob(${n})`;
|
|
230
|
+
case 'json':
|
|
231
|
+
return `json(${n})`;
|
|
232
|
+
case 'jsonb':
|
|
233
|
+
return `jsonb(${n})`;
|
|
234
|
+
case 'uuid':
|
|
235
|
+
return `uuid(${n})`;
|
|
236
|
+
case 'point':
|
|
237
|
+
return `point(${n})`;
|
|
238
|
+
case 'enum': {
|
|
239
|
+
const values = col.enumValues ?? [];
|
|
240
|
+
const valuesArg = `[${values.map(v => quoteStr(v)).join(', ')}]`;
|
|
241
|
+
const typeName = col.enumTypeName;
|
|
242
|
+
const args = typeName ? [n, valuesArg, quoteStr(typeName)] : [n, valuesArg];
|
|
243
|
+
return `enum(${args.join(', ')})`;
|
|
244
|
+
}
|
|
245
|
+
default:
|
|
246
|
+
throw new Error(`Unsupported column type for builder regeneration: '${col.type}' (column ${col.name})`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function renderIndexLine(idx: IndexSchema, table: TableSchema): string {
|
|
251
|
+
const colsArg = renderColumnsArg(idx.columns);
|
|
252
|
+
|
|
253
|
+
const suffix = idx.spatial ? 'spatial' : idx.unique ? 'unique' : 'index';
|
|
254
|
+
const expectedName = `${table.name}_${idx.columns.join('_')}_${suffix}`;
|
|
255
|
+
const nameArg = idx.name === expectedName ? '' : `, ${quoteStr(idx.name)}`;
|
|
256
|
+
|
|
257
|
+
if (idx.spatial) return `t.spatialIndex(${colsArg}${nameArg});`;
|
|
258
|
+
if (idx.unique) return `t.unique(${colsArg}${nameArg});`;
|
|
259
|
+
return `t.index(${colsArg}${nameArg});`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function renderForeignKeyLine(fk: ForeignKeySchema, table: TableSchema): string {
|
|
263
|
+
const colsArg = renderColumnsArg(fk.columns);
|
|
264
|
+
const expectedName = `${table.name}_${fk.columns.join('_')}_foreign`;
|
|
265
|
+
const nameArg = fk.name === expectedName ? '' : `, ${quoteStr(fk.name)}`;
|
|
266
|
+
|
|
267
|
+
let line = `t.foreign(${colsArg}${nameArg})`;
|
|
268
|
+
if (fk.referencedColumns.length === 1) {
|
|
269
|
+
line += `.references(${quoteStr(fk.referencedColumns[0])})`;
|
|
270
|
+
} else {
|
|
271
|
+
line += `.referencesAll([${fk.referencedColumns.map(quoteStr).join(', ')}])`;
|
|
272
|
+
}
|
|
273
|
+
line += `.on(${quoteStr(fk.referencedTable)})`;
|
|
274
|
+
if (fk.onDelete && fk.onDelete.toUpperCase() !== 'RESTRICT') line += `.onDelete(${quoteStr(fk.onDelete)})`;
|
|
275
|
+
if (fk.onUpdate && fk.onUpdate.toUpperCase() !== 'RESTRICT') line += `.onUpdate(${quoteStr(fk.onUpdate)})`;
|
|
276
|
+
return line + ';';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function renderColumnsArg(columns: string[]): string {
|
|
280
|
+
if (columns.length === 1) return quoteStr(columns[0]);
|
|
281
|
+
return `[${columns.map(quoteStr).join(', ')}]`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function quoteStr(s: string): string {
|
|
285
|
+
return `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function renderJsValue(value: unknown): string {
|
|
289
|
+
if (value === null) return 'null';
|
|
290
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
291
|
+
if (typeof value === 'number') return String(value);
|
|
292
|
+
if (typeof value === 'string') return quoteStr(value);
|
|
293
|
+
return JSON.stringify(value);
|
|
294
|
+
}
|
|
@@ -73,6 +73,7 @@ async function compareTable(entityTable: TableSchema, dbTable: TableSchema, dial
|
|
|
73
73
|
reorderedColumns: [],
|
|
74
74
|
addedIndexes: [],
|
|
75
75
|
removedIndexes: [],
|
|
76
|
+
renamedIndexes: [],
|
|
76
77
|
addedForeignKeys: [],
|
|
77
78
|
removedForeignKeys: [],
|
|
78
79
|
primaryKeyChanged: false,
|
|
@@ -154,9 +155,10 @@ async function compareTable(entityTable: TableSchema, dbTable: TableSchema, dial
|
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
// --- Indexes ---
|
|
157
|
-
const { added: addedIdx, removed: removedIdx } = compareIndexes(entityTable.indexes, dbTable.indexes);
|
|
158
|
+
const { added: addedIdx, removed: removedIdx, renamed: renamedIdx } = compareIndexes(entityTable.indexes, dbTable.indexes);
|
|
158
159
|
diff.addedIndexes = addedIdx;
|
|
159
160
|
diff.removedIndexes = removedIdx;
|
|
161
|
+
diff.renamedIndexes = renamedIdx;
|
|
160
162
|
|
|
161
163
|
// --- Foreign Keys ---
|
|
162
164
|
const { added: addedFK, removed: removedFK } = compareForeignKeys(entityTable.foreignKeys, dbTable.foreignKeys);
|
|
@@ -177,6 +179,7 @@ async function compareTable(entityTable: TableSchema, dbTable: TableSchema, dial
|
|
|
177
179
|
diff.reorderedColumns.length === 0 &&
|
|
178
180
|
diff.addedIndexes.length === 0 &&
|
|
179
181
|
diff.removedIndexes.length === 0 &&
|
|
182
|
+
diff.renamedIndexes.length === 0 &&
|
|
180
183
|
diff.addedForeignKeys.length === 0 &&
|
|
181
184
|
diff.removedForeignKeys.length === 0 &&
|
|
182
185
|
!diff.primaryKeyChanged &&
|
|
@@ -288,7 +291,16 @@ function defaultsMatch(a: ColumnSchema, b: ColumnSchema): boolean {
|
|
|
288
291
|
if (a.defaultValue === undefined && b.defaultValue === undefined) return true;
|
|
289
292
|
if (a.defaultValue === undefined || b.defaultValue === undefined) return false;
|
|
290
293
|
|
|
291
|
-
|
|
294
|
+
const av = String(a.defaultValue);
|
|
295
|
+
const bv = String(b.defaultValue);
|
|
296
|
+
if (av === bv) return true;
|
|
297
|
+
|
|
298
|
+
// Numeric defaults: '1' and '1.0'/'1.00' are equivalent (MySQL reports decimal defaults with scale).
|
|
299
|
+
const an = Number(av);
|
|
300
|
+
const bn = Number(bv);
|
|
301
|
+
if (av.trim() !== '' && bv.trim() !== '' && !Number.isNaN(an) && !Number.isNaN(bn)) return an === bn;
|
|
302
|
+
|
|
303
|
+
return false;
|
|
292
304
|
}
|
|
293
305
|
|
|
294
306
|
async function detectRenames(
|
|
@@ -356,7 +368,10 @@ function detectReorderingMySQL(entityTable: TableSchema, dbTable: TableSchema, d
|
|
|
356
368
|
return reorders;
|
|
357
369
|
}
|
|
358
370
|
|
|
359
|
-
function compareIndexes(
|
|
371
|
+
function compareIndexes(
|
|
372
|
+
entityIndexes: IndexSchema[],
|
|
373
|
+
dbIndexes: IndexSchema[]
|
|
374
|
+
): { added: IndexSchema[]; removed: IndexSchema[]; renamed: { from: string; to: string }[] } {
|
|
360
375
|
// Match by column set + uniqueness + spatial, not by name
|
|
361
376
|
const indexKey = (idx: IndexSchema) => `${idx.columns.join(',')}:${idx.unique}:${idx.spatial}`;
|
|
362
377
|
|
|
@@ -366,7 +381,22 @@ function compareIndexes(entityIndexes: IndexSchema[], dbIndexes: IndexSchema[]):
|
|
|
366
381
|
const added = entityIndexes.filter(i => !dbKeys.has(indexKey(i)));
|
|
367
382
|
const removed = dbIndexes.filter(i => !entityKeys.has(indexKey(i)));
|
|
368
383
|
|
|
369
|
-
|
|
384
|
+
// Name reconciliation: when an entity index sets an explicit name but the column-matched DB
|
|
385
|
+
// index carries a different name, rename it (instead of leaving the index permanently under a
|
|
386
|
+
// stale/auto name). Skip when the target name is still held by a DB index we're keeping — that
|
|
387
|
+
// would collide; the rename only fires once the conflicting index is among `removed`.
|
|
388
|
+
const removedNames = new Set(removed.map(i => i.name));
|
|
389
|
+
const keptDbNames = new Set(dbIndexes.filter(i => !removedNames.has(i.name)).map(i => i.name));
|
|
390
|
+
const renamed: { from: string; to: string }[] = [];
|
|
391
|
+
for (const e of entityIndexes) {
|
|
392
|
+
if (!e.explicitName) continue;
|
|
393
|
+
const dbMatch = dbKeys.get(indexKey(e));
|
|
394
|
+
if (!dbMatch || dbMatch.name === e.name) continue;
|
|
395
|
+
if (keptDbNames.has(e.name)) continue; // target name taken by an index we're keeping
|
|
396
|
+
renamed.push({ from: dbMatch.name, to: e.name });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return { added, removed, renamed };
|
|
370
400
|
}
|
|
371
401
|
|
|
372
402
|
function normalizeFkAction(action: string): string {
|