2025-03-28 10:40:09 -04:00
|
|
|
#!/usr/bin/env node
|
2025-04-07 15:12:12 -04:00
|
|
|
process.env.DB_URL = process.env.DB_URL || 'postgres://postgres:postgres@localhost:5432/immich';
|
2025-03-28 10:40:09 -04:00
|
|
|
|
2025-04-07 15:12:12 -04:00
|
|
|
import { Kysely } from 'kysely';
|
|
|
|
|
import { PostgresJSDialect } from 'kysely-postgres-js';
|
2025-03-28 10:40:09 -04:00
|
|
|
import { writeFileSync } from 'node:fs';
|
2025-04-07 15:12:12 -04:00
|
|
|
import { basename, dirname, extname, join } from 'node:path';
|
2025-03-28 10:40:09 -04:00
|
|
|
import postgres from 'postgres';
|
|
|
|
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
2025-04-07 15:12:12 -04:00
|
|
|
import { DatabaseRepository } from 'src/repositories/database.repository';
|
|
|
|
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
|
|
|
|
import 'src/schema';
|
|
|
|
|
import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools';
|
2025-03-28 10:40:09 -04:00
|
|
|
|
|
|
|
|
const main = async () => {
|
|
|
|
|
const command = process.argv[2];
|
2025-04-07 15:12:12 -04:00
|
|
|
const path = process.argv[3] || 'src/Migration';
|
2025-03-28 10:40:09 -04:00
|
|
|
|
|
|
|
|
switch (command) {
|
|
|
|
|
case 'debug': {
|
|
|
|
|
await debug();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-07 15:12:12 -04:00
|
|
|
case 'run': {
|
|
|
|
|
const only = process.argv[3] as 'kysely' | 'typeorm' | undefined;
|
|
|
|
|
await run(only);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-28 10:40:09 -04:00
|
|
|
case 'create': {
|
2025-04-07 15:12:12 -04:00
|
|
|
create(path, [], []);
|
2025-03-28 10:40:09 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'generate': {
|
2025-04-07 15:12:12 -04:00
|
|
|
await generate(path);
|
2025-03-28 10:40:09 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default: {
|
|
|
|
|
console.log(`Usage:
|
|
|
|
|
node dist/bin/migrations.js create <name>
|
|
|
|
|
node dist/bin/migrations.js generate <name>
|
2025-04-07 15:12:12 -04:00
|
|
|
node dist/bin/migrations.js run
|
2025-03-28 10:40:09 -04:00
|
|
|
`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-07 15:12:12 -04:00
|
|
|
const run = async (only?: 'kysely' | 'typeorm') => {
|
|
|
|
|
const configRepository = new ConfigRepository();
|
|
|
|
|
const { database } = configRepository.getEnv();
|
|
|
|
|
const logger = new LoggingRepository(undefined, configRepository);
|
|
|
|
|
const db = new Kysely<any>({
|
|
|
|
|
dialect: new PostgresJSDialect({ postgres: postgres(database.config.kysely) }),
|
|
|
|
|
log(event) {
|
|
|
|
|
if (event.level === 'error') {
|
|
|
|
|
console.error('Query failed :', {
|
|
|
|
|
durationMs: event.queryDurationMillis,
|
|
|
|
|
error: event.error,
|
|
|
|
|
sql: event.query.sql,
|
|
|
|
|
params: event.query.parameters,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const databaseRepository = new DatabaseRepository(db, logger, configRepository);
|
|
|
|
|
|
|
|
|
|
await databaseRepository.runMigrations({ only });
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-28 10:40:09 -04:00
|
|
|
const debug = async () => {
|
2025-04-07 15:12:12 -04:00
|
|
|
const { up } = await compare();
|
2025-03-28 10:40:09 -04:00
|
|
|
const upSql = '-- UP\n' + up.asSql({ comments: true }).join('\n');
|
2025-04-07 15:12:12 -04:00
|
|
|
// const downSql = '-- DOWN\n' + down.asSql({ comments: true }).join('\n');
|
|
|
|
|
writeFileSync('./migrations.sql', upSql + '\n\n');
|
2025-03-28 10:40:09 -04:00
|
|
|
console.log('Wrote migrations.sql');
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-07 15:12:12 -04:00
|
|
|
const generate = async (path: string) => {
|
2025-03-28 10:40:09 -04:00
|
|
|
const { up, down } = await compare();
|
|
|
|
|
if (up.items.length === 0) {
|
|
|
|
|
console.log('No changes detected');
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-04-07 15:12:12 -04:00
|
|
|
create(path, up.asSql(), down.asSql());
|
2025-03-28 10:40:09 -04:00
|
|
|
};
|
|
|
|
|
|
2025-04-07 15:12:12 -04:00
|
|
|
const create = (path: string, up: string[], down: string[]) => {
|
2025-03-29 09:26:24 -04:00
|
|
|
const timestamp = Date.now();
|
2025-04-07 15:12:12 -04:00
|
|
|
const name = basename(path, extname(path));
|
2025-03-29 09:26:24 -04:00
|
|
|
const filename = `${timestamp}-${name}.ts`;
|
2025-04-07 15:12:12 -04:00
|
|
|
const folder = dirname(path);
|
|
|
|
|
const fullPath = join(folder, filename);
|
2025-04-15 07:03:31 -04:00
|
|
|
writeFileSync(fullPath, asMigration('typeorm', { name, timestamp, up, down }));
|
2025-03-28 10:40:09 -04:00
|
|
|
console.log(`Wrote ${fullPath}`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const compare = async () => {
|
|
|
|
|
const configRepository = new ConfigRepository();
|
|
|
|
|
const { database } = configRepository.getEnv();
|
|
|
|
|
const db = postgres(database.config.kysely);
|
|
|
|
|
|
2025-04-07 15:12:12 -04:00
|
|
|
const source = schemaFromCode();
|
2025-03-28 10:40:09 -04:00
|
|
|
const target = await schemaFromDatabase(db, {});
|
|
|
|
|
|
2025-04-07 15:12:12 -04:00
|
|
|
const sourceParams = new Set(source.parameters.map(({ name }) => name));
|
|
|
|
|
target.parameters = target.parameters.filter(({ name }) => sourceParams.has(name));
|
2025-03-28 10:40:09 -04:00
|
|
|
|
2025-04-07 15:12:12 -04:00
|
|
|
const sourceTables = new Set(source.tables.map(({ name }) => name));
|
|
|
|
|
target.tables = target.tables.filter(({ name }) => sourceTables.has(name));
|
|
|
|
|
|
|
|
|
|
console.log(source.warnings.join('\n'));
|
2025-03-28 10:40:09 -04:00
|
|
|
|
2025-04-07 15:12:12 -04:00
|
|
|
const up = schemaDiff(source, target, {
|
|
|
|
|
tables: { ignoreExtra: true },
|
|
|
|
|
functions: { ignoreExtra: false },
|
|
|
|
|
});
|
|
|
|
|
const down = schemaDiff(target, source, {
|
|
|
|
|
tables: { ignoreExtra: false },
|
|
|
|
|
functions: { ignoreExtra: false },
|
|
|
|
|
});
|
2025-03-28 10:40:09 -04:00
|
|
|
|
|
|
|
|
return { up, down };
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-29 09:26:24 -04:00
|
|
|
type MigrationProps = {
|
|
|
|
|
name: string;
|
|
|
|
|
timestamp: number;
|
|
|
|
|
up: string[];
|
|
|
|
|
down: string[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const asMigration = (type: 'kysely' | 'typeorm', options: MigrationProps) =>
|
|
|
|
|
type === 'typeorm' ? asTypeOrmMigration(options) : asKyselyMigration(options);
|
2025-03-28 10:40:09 -04:00
|
|
|
|
2025-03-29 09:26:24 -04:00
|
|
|
const asTypeOrmMigration = ({ timestamp, name, up, down }: MigrationProps) => {
|
2025-03-28 10:40:09 -04:00
|
|
|
const upSql = up.map((sql) => ` await queryRunner.query(\`${sql}\`);`).join('\n');
|
|
|
|
|
const downSql = down.map((sql) => ` await queryRunner.query(\`${sql}\`);`).join('\n');
|
2025-03-29 09:26:24 -04:00
|
|
|
|
|
|
|
|
return `import { MigrationInterface, QueryRunner } from 'typeorm';
|
2025-03-28 10:40:09 -04:00
|
|
|
|
|
|
|
|
export class ${name}${timestamp} implements MigrationInterface {
|
|
|
|
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
|
|
|
${upSql}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
|
|
|
${downSql}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-29 09:26:24 -04:00
|
|
|
`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const asKyselyMigration = ({ up, down }: MigrationProps) => {
|
|
|
|
|
const upSql = up.map((sql) => ` await sql\`${sql}\`.execute(db);`).join('\n');
|
|
|
|
|
const downSql = down.map((sql) => ` await sql\`${sql}\`.execute(db);`).join('\n');
|
|
|
|
|
|
|
|
|
|
return `import { Kysely, sql } from 'kysely';
|
|
|
|
|
|
|
|
|
|
export async function up(db: Kysely<any>): Promise<void> {
|
|
|
|
|
${upSql}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function down(db: Kysely<any>): Promise<void> {
|
|
|
|
|
${downSql}
|
|
|
|
|
}
|
|
|
|
|
`;
|
2025-03-28 10:40:09 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
main()
|
|
|
|
|
.then(() => {
|
|
|
|
|
process.exit(0);
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
console.error(error);
|
|
|
|
|
console.log('Something went wrong');
|
|
|
|
|
process.exit(1);
|
|
|
|
|
});
|