diff --git a/sql/migrations/create.sql b/sql/migrations/create.sql index b96d00a..6fb75db 100644 --- a/sql/migrations/create.sql +++ b/sql/migrations/create.sql @@ -2,4 +2,12 @@ CREATE TABLE IF NOT EXISTS migrations ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text UNIQUE NOT NULL, applied_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP -) \ No newline at end of file +); + +CREATE TABLE IF NOT EXISTS migrations_lock ( + id integer PRIMARY KEY, + is_locked boolean NOT NULL DEFAULT false +); + +INSERT INTO migrations_lock (id, is_locked) VALUES (1, false) + ON CONFLICT DO NOTHING; diff --git a/sql/migrations/lock.sql b/sql/migrations/lock.sql new file mode 100644 index 0000000..9dfee5a --- /dev/null +++ b/sql/migrations/lock.sql @@ -0,0 +1,3 @@ +WITH rows as ( + UPDATE migrations_lock SET is_locked = true WHERE is_locked = false RETURNING 1 +) SELECT COUNT(*) FROM rows; diff --git a/sql/migrations/unlock.sql b/sql/migrations/unlock.sql new file mode 100644 index 0000000..aa26718 --- /dev/null +++ b/sql/migrations/unlock.sql @@ -0,0 +1 @@ +UPDATE migrations_lock SET is_locked = false WHERE is_locked = true; diff --git a/src/db/repos/migrations.ts b/src/db/repos/migrations.ts index 58e9c5f..7558573 100644 --- a/src/db/repos/migrations.ts +++ b/src/db/repos/migrations.ts @@ -19,6 +19,8 @@ import logger from "@kredens/logger"; import { DateTime } from "luxon"; import { IDatabase, IMain } from "pg-promise"; +export class LockError extends Error {} + export class MigrationRepository { private db: IDatabase; @@ -30,7 +32,19 @@ export class MigrationRepository { await this.db.none(sql.create); } + public async lock() { + const count = await this.db.one(sql.lock); + if (+count.count !== 1) { + throw new LockError("Failed to acquire migration lock"); + } + } + + public async unlock() { + return this.db.none(sql.unlock); + } + public async apply() { + await this.lock(); const applied = (await this.applied()).map(m => m.name); const toApply = sql.patches.filter( p => p.up.isSome() && !applied.find(o => o === p.name) @@ -45,6 +59,7 @@ export class MigrationRepository { }) .orLazy(() => Promise.resolve()); } + await this.unlock(); } public async applied(): Promise { diff --git a/src/db/sql/index.ts b/src/db/sql/index.ts index 7dcdd70..42a3f3e 100644 --- a/src/db/sql/index.ts +++ b/src/db/sql/index.ts @@ -24,6 +24,8 @@ const migrations = { applied: sql("migrations/applied.sql"), apply: sql("migrations/apply.sql"), create: sql("migrations/create.sql"), + lock: sql("migrations/lock.sql"), + unlock: sql("migrations/unlock.sql"), patches: subdirs(path.join("migrations", "patches")).map(patchName => ({ down: ifExists( path.join("migrations", "patches", patchName, "down.sql") diff --git a/tslint.json b/tslint.json index aacdda3..fb6c578 100644 --- a/tslint.json +++ b/tslint.json @@ -12,7 +12,8 @@ "@kredens" ], "no-implicit-dependencies": [true, ["@kredens"]], - "object-literal-sort-keys": [true, "match-declaration-order-only", "shorthand-first"] + "object-literal-sort-keys": [true, "match-declaration-order-only", "shorthand-first"], + "max-classes-per-file": false }, "rulesDirectory": [] }