chore: add "pnpm run migrations:revert" command (#23869)
parent
4a6c50cd81
commit
2cefbf8ca3
|
|
@ -12,3 +12,13 @@ pnpm run migrations:generate <migration-name>
|
||||||
3. Move the migration file to folder `./server/src/schema/migrations` in your code editor.
|
3. Move the migration file to folder `./server/src/schema/migrations` in your code editor.
|
||||||
|
|
||||||
The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately.
|
The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately.
|
||||||
|
|
||||||
|
## Reverting a Migration
|
||||||
|
|
||||||
|
If you need to undo the most recently applied migration—for example, when developing or testing on schema changes—run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run migrations:revert
|
||||||
|
```
|
||||||
|
|
||||||
|
This command rolls back the latest migration and brings the database schema back to its previous state.
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
"migrations:generate": "node ./dist/bin/migrations.js generate",
|
"migrations:generate": "node ./dist/bin/migrations.js generate",
|
||||||
"migrations:create": "node ./dist/bin/migrations.js create",
|
"migrations:create": "node ./dist/bin/migrations.js create",
|
||||||
"migrations:run": "node ./dist/bin/migrations.js run",
|
"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:drop": "node ./dist/bin/migrations.js query 'DROP schema public cascade; CREATE schema public;'",
|
||||||
"schema:reset": "npm run schema:drop && npm run migrations:run",
|
"schema:reset": "npm run schema:drop && npm run migrations:run",
|
||||||
"sync:open-api": "node ./dist/bin/sync-open-api.js",
|
"sync:open-api": "node ./dist/bin/sync-open-api.js",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
process.env.DB_URL = process.env.DB_URL || 'postgres://postgres:postgres@localhost:5432/immich';
|
process.env.DB_URL = process.env.DB_URL || 'postgres://postgres:postgres@localhost:5432/immich';
|
||||||
|
|
||||||
import { Kysely, sql } from 'kysely';
|
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 { basename, dirname, extname, join } from 'node:path';
|
||||||
import postgres from 'postgres';
|
import postgres from 'postgres';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
|
|
@ -27,6 +27,11 @@ const main = async () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'revert': {
|
||||||
|
await revert();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
case 'query': {
|
case 'query': {
|
||||||
const query = process.argv[3];
|
const query = process.argv[3];
|
||||||
await runQuery(query);
|
await runQuery(query);
|
||||||
|
|
@ -48,6 +53,7 @@ const main = async () => {
|
||||||
node dist/bin/migrations.js create <name>
|
node dist/bin/migrations.js create <name>
|
||||||
node dist/bin/migrations.js generate <name>
|
node dist/bin/migrations.js generate <name>
|
||||||
node dist/bin/migrations.js run
|
node dist/bin/migrations.js run
|
||||||
|
node dist/bin/migrations.js revert
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,6 +80,25 @@ const runMigrations = async () => {
|
||||||
await db.destroy();
|
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 debug = async () => {
|
||||||
const { up } = await compare();
|
const { up } = await compare();
|
||||||
const upSql = '-- UP\n' + up.asSql({ comments: true }).join('\n');
|
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()
|
main()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|
|
||||||
|
|
@ -360,18 +360,7 @@ export class DatabaseRepository {
|
||||||
async runMigrations(): Promise<void> {
|
async runMigrations(): Promise<void> {
|
||||||
this.logger.debug('Running migrations');
|
this.logger.debug('Running migrations');
|
||||||
|
|
||||||
const migrator = new Migrator({
|
const migrator = this.createMigrator();
|
||||||
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 { error, results } = await migrator.migrateToLatest();
|
const { error, results } = await migrator.migrateToLatest();
|
||||||
|
|
||||||
|
|
@ -477,4 +466,50 @@ export class DatabaseRepository {
|
||||||
private async releaseLock(lock: DatabaseLock, connection: Kysely<DB>): Promise<void> {
|
private async releaseLock(lock: DatabaseLock, connection: Kysely<DB>): Promise<void> {
|
||||||
await sql`SELECT pg_advisory_unlock(${lock})`.execute(connection);
|
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'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ export const newDatabaseRepositoryMock = (): Mocked<RepositoryInterface<Database
|
||||||
deleteAllSearchEmbeddings: vitest.fn(),
|
deleteAllSearchEmbeddings: vitest.fn(),
|
||||||
prewarm: vitest.fn(),
|
prewarm: vitest.fn(),
|
||||||
runMigrations: vitest.fn(),
|
runMigrations: vitest.fn(),
|
||||||
|
revertLastMigration: vitest.fn(),
|
||||||
withLock: vitest.fn().mockImplementation((_, function_: <R>() => Promise<R>) => function_()),
|
withLock: vitest.fn().mockImplementation((_, function_: <R>() => Promise<R>) => function_()),
|
||||||
tryLock: vitest.fn(),
|
tryLock: vitest.fn(),
|
||||||
isBusy: vitest.fn(),
|
isBusy: vitest.fn(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue