chore: add "pnpm run migrations:revert" command (#23869)

This commit is contained in:
idubnori
2025-11-14 01:12:59 +09:00
committed by GitHub
parent 4a6c50cd81
commit 2cefbf8ca3
5 changed files with 116 additions and 13 deletions

View File

@@ -26,6 +26,7 @@
"migrations:generate": "node ./dist/bin/migrations.js generate",
"migrations:create": "node ./dist/bin/migrations.js create",
"migrations:run": "node ./dist/bin/migrations.js run",
"migrations:revert": "node ./dist/bin/migrations.js revert",
"schema:drop": "node ./dist/bin/migrations.js query 'DROP schema public cascade; CREATE schema public;'",
"schema:reset": "npm run schema:drop && npm run migrations:run",
"sync:open-api": "node ./dist/bin/sync-open-api.js",

View File

@@ -2,7 +2,7 @@
process.env.DB_URL = process.env.DB_URL || 'postgres://postgres:postgres@localhost:5432/immich';
import { Kysely, sql } from 'kysely';
import { mkdirSync, writeFileSync } from 'node:fs';
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
import { basename, dirname, extname, join } from 'node:path';
import postgres from 'postgres';
import { ConfigRepository } from 'src/repositories/config.repository';
@@ -27,6 +27,11 @@ const main = async () => {
return;
}
case 'revert': {
await revert();
return;
}
case 'query': {
const query = process.argv[3];
await runQuery(query);
@@ -48,6 +53,7 @@ const main = async () => {
node dist/bin/migrations.js create <name>
node dist/bin/migrations.js generate <name>
node dist/bin/migrations.js run
node dist/bin/migrations.js revert
`);
}
}
@@ -74,6 +80,25 @@ const runMigrations = async () => {
await db.destroy();
};
const revert = async () => {
const configRepository = new ConfigRepository();
const logger = LoggingRepository.create();
const db = getDatabaseClient();
const databaseRepository = new DatabaseRepository(db, logger, configRepository);
try {
const migrationName = await databaseRepository.revertLastMigration();
if (!migrationName) {
console.log('No migrations to revert');
return;
}
markMigrationAsReverted(migrationName);
} finally {
await db.destroy();
}
};
const debug = async () => {
const { up } = await compare();
const upSql = '-- UP\n' + up.asSql({ comments: true }).join('\n');
@@ -148,6 +173,37 @@ ${downSql}
`;
};
const markMigrationAsReverted = (migrationName: string) => {
// eslint-disable-next-line unicorn/prefer-module
const distRoot = join(__dirname, '..');
const projectRoot = join(distRoot, '..');
const sourceFolder = join(projectRoot, 'src', 'schema', 'migrations');
const distFolder = join(distRoot, 'schema', 'migrations');
const sourcePath = join(sourceFolder, `${migrationName}.ts`);
const revertedFolder = join(sourceFolder, 'reverted');
const revertedPath = join(revertedFolder, `${migrationName}.ts`);
if (existsSync(revertedPath)) {
console.log(`Migration ${migrationName} is already marked as reverted`);
} else if (existsSync(sourcePath)) {
mkdirSync(revertedFolder, { recursive: true });
renameSync(sourcePath, revertedPath);
console.log(`Moved ${sourcePath} to ${revertedPath}`);
} else {
console.warn(`Source migration file not found for ${migrationName}`);
}
const distBase = join(distFolder, migrationName);
for (const extension of ['.js', '.js.map', '.d.ts']) {
const filePath = `${distBase}${extension}`;
if (existsSync(filePath)) {
rmSync(filePath, { force: true });
console.log(`Removed ${filePath}`);
}
}
};
main()
.then(() => {
process.exit(0);

View File

@@ -360,18 +360,7 @@ export class DatabaseRepository {
async runMigrations(): Promise<void> {
this.logger.debug('Running migrations');
const migrator = new Migrator({
db: this.db,
migrationLockTableName: 'kysely_migrations_lock',
allowUnorderedMigrations: this.configRepository.isDev(),
migrationTableName: 'kysely_migrations',
provider: new FileMigrationProvider({
fs: { readdir },
path: { join },
// eslint-disable-next-line unicorn/prefer-module
migrationFolder: join(__dirname, '..', 'schema/migrations'),
}),
});
const migrator = this.createMigrator();
const { error, results } = await migrator.migrateToLatest();
@@ -477,4 +466,50 @@ export class DatabaseRepository {
private async releaseLock(lock: DatabaseLock, connection: Kysely<DB>): Promise<void> {
await sql`SELECT pg_advisory_unlock(${lock})`.execute(connection);
}
async revertLastMigration(): Promise<string | undefined> {
this.logger.debug('Reverting last migration');
const migrator = this.createMigrator();
const { error, results } = await migrator.migrateDown();
for (const result of results ?? []) {
if (result.status === 'Success') {
this.logger.log(`Reverted migration "${result.migrationName}"`);
}
if (result.status === 'Error') {
this.logger.warn(`Failed to revert migration "${result.migrationName}"`);
}
}
if (error) {
this.logger.error(`Failed to revert migrations: ${error}`);
throw error;
}
const reverted = results?.find((result) => result.direction === 'Down' && result.status === 'Success');
if (!reverted) {
this.logger.debug('No migrations to revert');
return undefined;
}
this.logger.debug('Finished reverting migration');
return reverted.migrationName;
}
private createMigrator(): Migrator {
return new Migrator({
db: this.db,
migrationLockTableName: 'kysely_migrations_lock',
allowUnorderedMigrations: this.configRepository.isDev(),
migrationTableName: 'kysely_migrations',
provider: new FileMigrationProvider({
fs: { readdir },
path: { join },
// eslint-disable-next-line unicorn/prefer-module
migrationFolder: join(__dirname, '..', 'schema/migrations'),
}),
});
}
}

View File

@@ -19,6 +19,7 @@ export const newDatabaseRepositoryMock = (): Mocked<RepositoryInterface<Database
deleteAllSearchEmbeddings: vitest.fn(),
prewarm: vitest.fn(),
runMigrations: vitest.fn(),
revertLastMigration: vitest.fn(),
withLock: vitest.fn().mockImplementation((_, function_: <R>() => Promise<R>) => function_()),
tryLock: vitest.fn(),
isBusy: vitest.fn(),