@philosophocat/postgres-migrations 0.1.1

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/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # Postgres Simple Migrations
2
+
3
+ A tiny migration tool for Node.js/Bun build for [`postgres.js`](https://github.com/porsager/postgres).
4
+
5
+ It keeps things simple: no CLI magic, just a typed class you can use in your own scripts.
6
+
7
+ ## Features
8
+
9
+ - **Zero Heavy Dependencies**: Only depends on `postgres` (peer dependency).
10
+ - **Advisory Locks**: Prevents race conditions when multiple instances try to migrate simultaneously.
11
+ - **Atomic**: Migrations are applied inside transactions. If a migration fails, the DB state rolls back.
12
+ - **File-based Scanning**: Automatically loads and sorts migrations from a directory.
13
+
14
+ ## Installation
15
+
16
+ ### npm
17
+ ```bash
18
+ npm install @philosophocat/postgres-migrations
19
+ ```
20
+
21
+ ### bun
22
+ ```bash
23
+ bun add @philosophocat/postgres-migrations
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ Create a file in your migrations folder (e.g., migrations/001_init.ts). The file name determines the execution order.
29
+
30
+ ```typescript
31
+ // migrations/001_init.ts
32
+ import { Sql } from 'postgres';
33
+
34
+ // The export name must be 'migration' or 'default'
35
+ export const migration = {
36
+ name: 'users', // optional
37
+ async up(sql: Sql) {
38
+ await sql`
39
+ CREATE TABLE users (
40
+ id SERIAL PRIMARY KEY,
41
+ name TEXT NOT NULL,
42
+ created_at TIMESTAMP DEFAULT NOW()
43
+ );
44
+ `;
45
+ },
46
+ async down(sql: Sql) {
47
+ await sql`DROP TABLE users`;
48
+ },
49
+ };
50
+ ```
51
+
52
+ And run your migrations
53
+
54
+ ```typescript
55
+ import { resolve } from 'node:path';
56
+ import postgres from 'postgres';
57
+ import { Migrator } from '@philosophocat/postgres-migrations';
58
+
59
+ // 1. Setup postgres client
60
+ const sql = postgres(process.env.DATABASE_URL!);
61
+
62
+ // 2. Initialize Migrator
63
+ const migrator = new Migrator({
64
+ sql,
65
+ // schema, default 'public'
66
+ // tableName, default 'migrations'
67
+ // lockId, optional advisory lock id
68
+ });
69
+
70
+ const run = async () => {
71
+ try {
72
+ // 3. Scan directory for .ts/.js files
73
+ await migrator.scan(resolve(__dirname, '../migrations'));
74
+ await migrator.up();
75
+ } catch (e) {
76
+ console.error(e);
77
+ process.exit(1);
78
+ } finally {
79
+ await sql.end();
80
+ }
81
+ };
82
+
83
+ run();
84
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Migrator: () => Migrator
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/migrator.ts
28
+ var import_node_fs = require("fs");
29
+ var import_node_path = require("path");
30
+
31
+ // src/repository.ts
32
+ var Repository = class {
33
+ schema;
34
+ tableName;
35
+ sql;
36
+ lockID;
37
+ constructor({
38
+ sql,
39
+ schema = "public",
40
+ tableName = "migrations",
41
+ lockId = 2128506
42
+ }) {
43
+ this.sql = sql;
44
+ this.schema = schema;
45
+ this.tableName = tableName;
46
+ this.lockID = lockId;
47
+ }
48
+ async ensureTable() {
49
+ await this.sql`CREATE SCHEMA IF NOT EXISTS ${this.sql(this.schema)}`;
50
+ await this.sql`
51
+ CREATE TABLE IF NOT EXISTS ${this.sql(this.schema)}.${this.sql(this.tableName)} (
52
+ id SERIAL PRIMARY KEY,
53
+ name VARCHAR(255) NOT NULL UNIQUE,
54
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
55
+ )
56
+ `;
57
+ }
58
+ async tryLock() {
59
+ const [result] = await this.sql`
60
+ SELECT pg_try_advisory_lock(${this.lockID}) as locked
61
+ `;
62
+ return !!result?.locked;
63
+ }
64
+ async unlock() {
65
+ await this.sql`
66
+ SELECT pg_advisory_unlock(${this.lockID})
67
+ `;
68
+ }
69
+ async listApplied() {
70
+ await this.ensureTable();
71
+ const rows = await this.sql`
72
+ SELECT name FROM ${this.sql(this.schema)}.${this.sql(this.tableName)}
73
+ `;
74
+ return new Set(rows.map((r) => r.name));
75
+ }
76
+ async markApplied(name, trx) {
77
+ const sql = trx || this.sql;
78
+ await sql`
79
+ INSERT INTO ${this.sql(this.schema)}.${this.sql(this.tableName)} (name) VALUES (${name})
80
+ `;
81
+ }
82
+ async unmarkApplied(name, trx) {
83
+ const sql = trx || this.sql;
84
+ await sql`
85
+ DELETE FROM ${this.sql(this.schema)}.${this.sql(this.tableName)} WHERE name = ${name}
86
+ `;
87
+ }
88
+ };
89
+
90
+ // src/migrator.ts
91
+ var Migrator = class {
92
+ constructor(options) {
93
+ this.options = options;
94
+ this.repo = new Repository(options);
95
+ }
96
+ migrations = [];
97
+ repo;
98
+ scan = async (dir) => {
99
+ const files = (0, import_node_fs.readdirSync)(dir).filter((f) => f.endsWith(".js") || f.endsWith(".ts")).sort();
100
+ for (const file of files) {
101
+ const filePath = (0, import_node_path.join)(dir, file);
102
+ const name = (0, import_node_path.parse)(file).name;
103
+ const module2 = await import(filePath);
104
+ const m = module2.migration || module2.default;
105
+ if (!m) {
106
+ console.warn(`Skipping ${file}: no migration export found`);
107
+ continue;
108
+ }
109
+ if (this.migrations.find((migration) => migration.name === name)) {
110
+ throw new Error(`Duplicated migration name: ${name}`);
111
+ }
112
+ this.migrations.push({
113
+ up: m.up,
114
+ down: m.down,
115
+ name: m.name || name
116
+ });
117
+ }
118
+ };
119
+ up = async () => {
120
+ if (this.migrations.length === 0) {
121
+ return console.log("Empty migrations list");
122
+ }
123
+ const locked = await this.repo.tryLock();
124
+ if (!locked) {
125
+ return console.log("Migrations are locked by another process");
126
+ }
127
+ try {
128
+ const applied = await this.repo.listApplied();
129
+ for (const m of this.migrations) {
130
+ if (!applied.has(m.name)) {
131
+ console.log(`Applying: ${m.name}`);
132
+ await this.options.sql.begin(async (trx) => {
133
+ await m.up(trx);
134
+ await this.repo.markApplied(m.name, trx);
135
+ });
136
+ }
137
+ }
138
+ } catch (e) {
139
+ console.error("Migration failed:", e);
140
+ throw e;
141
+ } finally {
142
+ await this.repo.unlock();
143
+ }
144
+ };
145
+ down = async (count = 1) => {
146
+ const locked = await this.repo.tryLock();
147
+ if (!locked) {
148
+ return console.log("Migrations locked");
149
+ }
150
+ try {
151
+ const applied = await this.repo.listApplied();
152
+ const toRollback = this.migrations.slice().reverse().filter((m) => applied.has(m.name));
153
+ let rolledBackCount = 0;
154
+ for (const m of toRollback) {
155
+ if (rolledBackCount >= count && count !== -1) {
156
+ break;
157
+ }
158
+ await this.options.sql.begin(async (trx) => {
159
+ await m.down(trx);
160
+ await this.repo.unmarkApplied(m.name, trx);
161
+ });
162
+ console.log(`Reverted: ${m.name}`);
163
+ rolledBackCount++;
164
+ }
165
+ } finally {
166
+ await this.repo.unlock();
167
+ }
168
+ };
169
+ async close() {
170
+ await this.options.sql.end();
171
+ }
172
+ };
173
+ // Annotate the CommonJS export names for ESM import in node:
174
+ 0 && (module.exports = {
175
+ Migrator
176
+ });
177
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/migrator.ts","../src/repository.ts"],"sourcesContent":["export * from './types';\nexport * from './migrator';\n","import { readdirSync } from 'node:fs';\nimport { join, parse } from 'node:path';\nimport { Repository } from './repository';\nimport { Migration, MigratorOptions } from './types';\n\nexport class Migrator {\n\n private migrations: Migration[] = [];\n\n private repo: Repository;\n\n constructor(\n private readonly options: MigratorOptions\n ) {\n this.repo = new Repository(options);\n }\n\n public scan = async (\n dir: string\n ) => {\n const files = readdirSync(dir)\n .filter(f => f.endsWith('.js') || f.endsWith('.ts'))\n .sort();\n\n for (const file of files) {\n const filePath = join(dir, file);\n const name = parse(file).name;\n const module = await import(filePath);\n const m = module.migration || module.default;\n\n if (!m) {\n console.warn(`Skipping ${file}: no migration export found`);\n continue;\n }\n\n if (this.migrations.find(migration => migration.name === name)) {\n throw new Error(`Duplicated migration name: ${name}`);\n }\n\n this.migrations.push({\n up: m.up,\n down: m.down,\n name: m.name || name,\n });\n }\n }\n\n up = async () => {\n if (this.migrations.length === 0) {\n return console.log('Empty migrations list')\n }\n\n const locked = await this.repo.tryLock();\n if (!locked) {\n return console.log('Migrations are locked by another process');\n }\n\n try {\n const applied = await this.repo.listApplied();\n\n for (const m of this.migrations) {\n if (!applied.has(m.name)) {\n console.log(`Applying: ${ m.name }`);\n\n await this.options.sql.begin(async (trx) => {\n await m.up(trx);\n await this.repo.markApplied(m.name, trx);\n });\n }\n }\n } catch (e) {\n console.error('Migration failed:', e);\n throw e;\n } finally {\n await this.repo.unlock();\n }\n };\n\n down = async (\n count = 1\n ) => {\n const locked = await this.repo.tryLock();\n if (!locked) {\n return console.log('Migrations locked');\n }\n\n try {\n const applied = await this.repo.listApplied();\n const toRollback = this.migrations\n .slice()\n .reverse()\n .filter(m => applied.has(m.name));\n\n let rolledBackCount = 0;\n for (const m of toRollback) {\n if (rolledBackCount >= count && count !== -1) {\n break;\n }\n\n await this.options.sql.begin(async (trx) => {\n await m.down(trx);\n await this.repo.unmarkApplied(m.name, trx);\n });\n console.log(`Reverted: ${m.name}`);\n rolledBackCount++;\n }\n } finally {\n await this.repo.unlock();\n }\n }\n\n async close() {\n await this.options.sql.end();\n };\n}\n","import type { Sql, TransactionSql } from 'postgres';\nimport { MigratorOptions } from './types';\n\nexport class Repository {\n private readonly schema: string;\n private readonly tableName: string;\n private readonly sql: Sql;\n private readonly lockID: number;\n\n constructor({\n sql,\n schema = 'public',\n tableName = 'migrations',\n lockId = 2128506,\n }: MigratorOptions) {\n this.sql = sql;\n this.schema = schema;\n this.tableName = tableName;\n this.lockID = lockId;\n }\n\n async ensureTable() {\n await this.sql`CREATE SCHEMA IF NOT EXISTS ${this.sql(this.schema)}`;\n await this.sql`\n CREATE TABLE IF NOT EXISTS ${ this.sql(this.schema) }.${ this.sql(this.tableName) } (\n id SERIAL PRIMARY KEY,\n name VARCHAR(255) NOT NULL UNIQUE,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n `;\n };\n\n async tryLock(): Promise<boolean> {\n const [result] = await this.sql`\n SELECT pg_try_advisory_lock(${ this.lockID }) as locked\n `;\n return !!result?.locked;\n };\n\n async unlock(): Promise<void> {\n await this.sql`\n SELECT pg_advisory_unlock(${ this.lockID })\n `;\n };\n\n async listApplied(): Promise<Set<string>> {\n await this.ensureTable();\n\n const rows = await this.sql<{ name: string }[]>`\n SELECT name FROM ${ this.sql(this.schema) }.${ this.sql(this.tableName) }\n `;\n\n return new Set(rows.map(r => r.name));\n };\n\n async markApplied(\n name: string,\n trx?: TransactionSql\n ) {\n const sql = (trx || this.sql) as Sql;\n await sql`\n INSERT INTO ${ this.sql(this.schema) }.${ this.sql(this.tableName) } (name) VALUES (${name})\n `;\n }\n\n async unmarkApplied(\n name: string,\n trx?: TransactionSql\n ) {\n const sql = (trx || this.sql) as Sql;\n await sql`\n DELETE FROM ${ this.sql(this.schema) }.${ this.sql(this.tableName) } WHERE name = ${name}\n `;\n }\n}"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,qBAA4B;AAC5B,uBAA4B;;;ACErB,IAAM,aAAN,MAAiB;AAAA,EACH;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY;AAAA,IACR;AAAA,IACA,SAAS;AAAA,IACT,YAAY;AAAA,IACZ,SAAS;AAAA,EACb,GAAoB;AAChB,SAAK,MAAM;AACX,SAAK,SAAS;AACd,SAAK,YAAY;AACjB,SAAK,SAAS;AAAA,EAClB;AAAA,EAEA,MAAM,cAAc;AAChB,UAAM,KAAK,kCAAkC,KAAK,IAAI,KAAK,MAAM,CAAC;AAClE,UAAM,KAAK;AAAA,yCACuB,KAAK,IAAI,KAAK,MAAM,CAAE,IAAK,KAAK,IAAI,KAAK,SAAS,CAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM1F;AAAA,EAEA,MAAM,UAA4B;AAC9B,UAAM,CAAC,MAAM,IAAI,MAAM,KAAK;AAAA,0CACO,KAAK,MAAO;AAAA;AAE/C,WAAO,CAAC,CAAC,QAAQ;AAAA,EACrB;AAAA,EAEA,MAAM,SAAwB;AAC1B,UAAM,KAAK;AAAA,wCACsB,KAAK,MAAO;AAAA;AAAA,EAEjD;AAAA,EAEA,MAAM,cAAoC;AACtC,UAAM,KAAK,YAAY;AAEvB,UAAM,OAAO,MAAM,KAAK;AAAA,+BACA,KAAK,IAAI,KAAK,MAAM,CAAE,IAAK,KAAK,IAAI,KAAK,SAAS,CAAE;AAAA;AAG5E,WAAO,IAAI,IAAI,KAAK,IAAI,OAAK,EAAE,IAAI,CAAC;AAAA,EACxC;AAAA,EAEA,MAAM,YACF,MACA,KACF;AACE,UAAM,MAAO,OAAO,KAAK;AACzB,UAAM;AAAA,0BACa,KAAK,IAAI,KAAK,MAAM,CAAE,IAAK,KAAK,IAAI,KAAK,SAAS,CAAE,mBAAmB,IAAI;AAAA;AAAA,EAElG;AAAA,EAEA,MAAM,cACF,MACA,KACF;AACE,UAAM,MAAO,OAAO,KAAK;AACzB,UAAM;AAAA,0BACa,KAAK,IAAI,KAAK,MAAM,CAAE,IAAK,KAAK,IAAI,KAAK,SAAS,CAAE,iBAAiB,IAAI;AAAA;AAAA,EAEhG;AACJ;;;ADrEO,IAAM,WAAN,MAAe;AAAA,EAMlB,YACqB,SACnB;AADmB;AAEjB,SAAK,OAAO,IAAI,WAAW,OAAO;AAAA,EACtC;AAAA,EARQ,aAA0B,CAAC;AAAA,EAE3B;AAAA,EAQD,OAAO,OACV,QACC;AACD,UAAM,YAAQ,4BAAY,GAAG,EACxB,OAAO,OAAK,EAAE,SAAS,KAAK,KAAK,EAAE,SAAS,KAAK,CAAC,EAClD,KAAK;AAEV,eAAW,QAAQ,OAAO;AACtB,YAAM,eAAW,uBAAK,KAAK,IAAI;AAC/B,YAAM,WAAO,wBAAM,IAAI,EAAE;AACzB,YAAMA,UAAS,MAAM,OAAO;AAC5B,YAAM,IAAIA,QAAO,aAAaA,QAAO;AAErC,UAAI,CAAC,GAAG;AACJ,gBAAQ,KAAK,YAAY,IAAI,6BAA6B;AAC1D;AAAA,MACJ;AAEA,UAAI,KAAK,WAAW,KAAK,eAAa,UAAU,SAAS,IAAI,GAAG;AAC5D,cAAM,IAAI,MAAM,8BAA8B,IAAI,EAAE;AAAA,MACxD;AAEA,WAAK,WAAW,KAAK;AAAA,QACjB,IAAI,EAAE;AAAA,QACN,MAAM,EAAE;AAAA,QACR,MAAM,EAAE,QAAQ;AAAA,MACpB,CAAC;AAAA,IACL;AAAA,EACJ;AAAA,EAEA,KAAK,YAAY;AACb,QAAI,KAAK,WAAW,WAAW,GAAG;AAC9B,aAAO,QAAQ,IAAI,uBAAuB;AAAA,IAC9C;AAEA,UAAM,SAAS,MAAM,KAAK,KAAK,QAAQ;AACvC,QAAI,CAAC,QAAQ;AACT,aAAO,QAAQ,IAAI,0CAA0C;AAAA,IACjE;AAEA,QAAI;AACA,YAAM,UAAU,MAAM,KAAK,KAAK,YAAY;AAE5C,iBAAW,KAAK,KAAK,YAAY;AAC7B,YAAI,CAAC,QAAQ,IAAI,EAAE,IAAI,GAAG;AACtB,kBAAQ,IAAI,aAAc,EAAE,IAAK,EAAE;AAEnC,gBAAM,KAAK,QAAQ,IAAI,MAAM,OAAO,QAAQ;AACxC,kBAAM,EAAE,GAAG,GAAG;AACd,kBAAM,KAAK,KAAK,YAAY,EAAE,MAAM,GAAG;AAAA,UAC3C,CAAC;AAAA,QACL;AAAA,MACJ;AAAA,IACJ,SAAS,GAAG;AACR,cAAQ,MAAM,qBAAqB,CAAC;AACpC,YAAM;AAAA,IACV,UAAE;AACE,YAAM,KAAK,KAAK,OAAO;AAAA,IAC3B;AAAA,EACJ;AAAA,EAEA,OAAO,OACH,QAAQ,MACN;AACF,UAAM,SAAS,MAAM,KAAK,KAAK,QAAQ;AACvC,QAAI,CAAC,QAAQ;AACT,aAAO,QAAQ,IAAI,mBAAmB;AAAA,IAC1C;AAEA,QAAI;AACA,YAAM,UAAU,MAAM,KAAK,KAAK,YAAY;AAC5C,YAAM,aAAa,KAAK,WACnB,MAAM,EACN,QAAQ,EACR,OAAO,OAAK,QAAQ,IAAI,EAAE,IAAI,CAAC;AAEpC,UAAI,kBAAkB;AACtB,iBAAW,KAAK,YAAY;AACxB,YAAI,mBAAmB,SAAS,UAAU,IAAI;AAC1C;AAAA,QACJ;AAEA,cAAM,KAAK,QAAQ,IAAI,MAAM,OAAO,QAAQ;AACxC,gBAAM,EAAE,KAAK,GAAG;AAChB,gBAAM,KAAK,KAAK,cAAc,EAAE,MAAM,GAAG;AAAA,QAC7C,CAAC;AACD,gBAAQ,IAAI,aAAa,EAAE,IAAI,EAAE;AACjC;AAAA,MACJ;AAAA,IACJ,UAAE;AACE,YAAM,KAAK,KAAK,OAAO;AAAA,IAC3B;AAAA,EACJ;AAAA,EAEA,MAAM,QAAQ;AACV,UAAM,KAAK,QAAQ,IAAI,IAAI;AAAA,EAC/B;AACJ;","names":["module"]}
@@ -0,0 +1,31 @@
1
+ import { Sql, TransactionSql } from 'postgres';
2
+
3
+ interface Migration {
4
+ name: string;
5
+ up(sql: Sql | TransactionSql): Promise<void>;
6
+ down(sql: Sql | TransactionSql): Promise<void>;
7
+ }
8
+ interface MigratorOptions {
9
+ sql: Sql;
10
+ schema?: string;
11
+ tableName?: string;
12
+ lockId?: number;
13
+ }
14
+ interface MigrationRecord {
15
+ id: number;
16
+ name: string;
17
+ run_on: Date;
18
+ }
19
+
20
+ declare class Migrator {
21
+ private readonly options;
22
+ private migrations;
23
+ private repo;
24
+ constructor(options: MigratorOptions);
25
+ scan: (dir: string) => Promise<void>;
26
+ up: () => Promise<void>;
27
+ down: (count?: number) => Promise<void>;
28
+ close(): Promise<void>;
29
+ }
30
+
31
+ export { type Migration, type MigrationRecord, Migrator, type MigratorOptions };
@@ -0,0 +1,31 @@
1
+ import { Sql, TransactionSql } from 'postgres';
2
+
3
+ interface Migration {
4
+ name: string;
5
+ up(sql: Sql | TransactionSql): Promise<void>;
6
+ down(sql: Sql | TransactionSql): Promise<void>;
7
+ }
8
+ interface MigratorOptions {
9
+ sql: Sql;
10
+ schema?: string;
11
+ tableName?: string;
12
+ lockId?: number;
13
+ }
14
+ interface MigrationRecord {
15
+ id: number;
16
+ name: string;
17
+ run_on: Date;
18
+ }
19
+
20
+ declare class Migrator {
21
+ private readonly options;
22
+ private migrations;
23
+ private repo;
24
+ constructor(options: MigratorOptions);
25
+ scan: (dir: string) => Promise<void>;
26
+ up: () => Promise<void>;
27
+ down: (count?: number) => Promise<void>;
28
+ close(): Promise<void>;
29
+ }
30
+
31
+ export { type Migration, type MigrationRecord, Migrator, type MigratorOptions };
package/dist/index.js ADDED
@@ -0,0 +1,150 @@
1
+ // src/migrator.ts
2
+ import { readdirSync } from "fs";
3
+ import { join, parse } from "path";
4
+
5
+ // src/repository.ts
6
+ var Repository = class {
7
+ schema;
8
+ tableName;
9
+ sql;
10
+ lockID;
11
+ constructor({
12
+ sql,
13
+ schema = "public",
14
+ tableName = "migrations",
15
+ lockId = 2128506
16
+ }) {
17
+ this.sql = sql;
18
+ this.schema = schema;
19
+ this.tableName = tableName;
20
+ this.lockID = lockId;
21
+ }
22
+ async ensureTable() {
23
+ await this.sql`CREATE SCHEMA IF NOT EXISTS ${this.sql(this.schema)}`;
24
+ await this.sql`
25
+ CREATE TABLE IF NOT EXISTS ${this.sql(this.schema)}.${this.sql(this.tableName)} (
26
+ id SERIAL PRIMARY KEY,
27
+ name VARCHAR(255) NOT NULL UNIQUE,
28
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
29
+ )
30
+ `;
31
+ }
32
+ async tryLock() {
33
+ const [result] = await this.sql`
34
+ SELECT pg_try_advisory_lock(${this.lockID}) as locked
35
+ `;
36
+ return !!result?.locked;
37
+ }
38
+ async unlock() {
39
+ await this.sql`
40
+ SELECT pg_advisory_unlock(${this.lockID})
41
+ `;
42
+ }
43
+ async listApplied() {
44
+ await this.ensureTable();
45
+ const rows = await this.sql`
46
+ SELECT name FROM ${this.sql(this.schema)}.${this.sql(this.tableName)}
47
+ `;
48
+ return new Set(rows.map((r) => r.name));
49
+ }
50
+ async markApplied(name, trx) {
51
+ const sql = trx || this.sql;
52
+ await sql`
53
+ INSERT INTO ${this.sql(this.schema)}.${this.sql(this.tableName)} (name) VALUES (${name})
54
+ `;
55
+ }
56
+ async unmarkApplied(name, trx) {
57
+ const sql = trx || this.sql;
58
+ await sql`
59
+ DELETE FROM ${this.sql(this.schema)}.${this.sql(this.tableName)} WHERE name = ${name}
60
+ `;
61
+ }
62
+ };
63
+
64
+ // src/migrator.ts
65
+ var Migrator = class {
66
+ constructor(options) {
67
+ this.options = options;
68
+ this.repo = new Repository(options);
69
+ }
70
+ migrations = [];
71
+ repo;
72
+ scan = async (dir) => {
73
+ const files = readdirSync(dir).filter((f) => f.endsWith(".js") || f.endsWith(".ts")).sort();
74
+ for (const file of files) {
75
+ const filePath = join(dir, file);
76
+ const name = parse(file).name;
77
+ const module = await import(filePath);
78
+ const m = module.migration || module.default;
79
+ if (!m) {
80
+ console.warn(`Skipping ${file}: no migration export found`);
81
+ continue;
82
+ }
83
+ if (this.migrations.find((migration) => migration.name === name)) {
84
+ throw new Error(`Duplicated migration name: ${name}`);
85
+ }
86
+ this.migrations.push({
87
+ up: m.up,
88
+ down: m.down,
89
+ name: m.name || name
90
+ });
91
+ }
92
+ };
93
+ up = async () => {
94
+ if (this.migrations.length === 0) {
95
+ return console.log("Empty migrations list");
96
+ }
97
+ const locked = await this.repo.tryLock();
98
+ if (!locked) {
99
+ return console.log("Migrations are locked by another process");
100
+ }
101
+ try {
102
+ const applied = await this.repo.listApplied();
103
+ for (const m of this.migrations) {
104
+ if (!applied.has(m.name)) {
105
+ console.log(`Applying: ${m.name}`);
106
+ await this.options.sql.begin(async (trx) => {
107
+ await m.up(trx);
108
+ await this.repo.markApplied(m.name, trx);
109
+ });
110
+ }
111
+ }
112
+ } catch (e) {
113
+ console.error("Migration failed:", e);
114
+ throw e;
115
+ } finally {
116
+ await this.repo.unlock();
117
+ }
118
+ };
119
+ down = async (count = 1) => {
120
+ const locked = await this.repo.tryLock();
121
+ if (!locked) {
122
+ return console.log("Migrations locked");
123
+ }
124
+ try {
125
+ const applied = await this.repo.listApplied();
126
+ const toRollback = this.migrations.slice().reverse().filter((m) => applied.has(m.name));
127
+ let rolledBackCount = 0;
128
+ for (const m of toRollback) {
129
+ if (rolledBackCount >= count && count !== -1) {
130
+ break;
131
+ }
132
+ await this.options.sql.begin(async (trx) => {
133
+ await m.down(trx);
134
+ await this.repo.unmarkApplied(m.name, trx);
135
+ });
136
+ console.log(`Reverted: ${m.name}`);
137
+ rolledBackCount++;
138
+ }
139
+ } finally {
140
+ await this.repo.unlock();
141
+ }
142
+ };
143
+ async close() {
144
+ await this.options.sql.end();
145
+ }
146
+ };
147
+ export {
148
+ Migrator
149
+ };
150
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/migrator.ts","../src/repository.ts"],"sourcesContent":["import { readdirSync } from 'node:fs';\nimport { join, parse } from 'node:path';\nimport { Repository } from './repository';\nimport { Migration, MigratorOptions } from './types';\n\nexport class Migrator {\n\n private migrations: Migration[] = [];\n\n private repo: Repository;\n\n constructor(\n private readonly options: MigratorOptions\n ) {\n this.repo = new Repository(options);\n }\n\n public scan = async (\n dir: string\n ) => {\n const files = readdirSync(dir)\n .filter(f => f.endsWith('.js') || f.endsWith('.ts'))\n .sort();\n\n for (const file of files) {\n const filePath = join(dir, file);\n const name = parse(file).name;\n const module = await import(filePath);\n const m = module.migration || module.default;\n\n if (!m) {\n console.warn(`Skipping ${file}: no migration export found`);\n continue;\n }\n\n if (this.migrations.find(migration => migration.name === name)) {\n throw new Error(`Duplicated migration name: ${name}`);\n }\n\n this.migrations.push({\n up: m.up,\n down: m.down,\n name: m.name || name,\n });\n }\n }\n\n up = async () => {\n if (this.migrations.length === 0) {\n return console.log('Empty migrations list')\n }\n\n const locked = await this.repo.tryLock();\n if (!locked) {\n return console.log('Migrations are locked by another process');\n }\n\n try {\n const applied = await this.repo.listApplied();\n\n for (const m of this.migrations) {\n if (!applied.has(m.name)) {\n console.log(`Applying: ${ m.name }`);\n\n await this.options.sql.begin(async (trx) => {\n await m.up(trx);\n await this.repo.markApplied(m.name, trx);\n });\n }\n }\n } catch (e) {\n console.error('Migration failed:', e);\n throw e;\n } finally {\n await this.repo.unlock();\n }\n };\n\n down = async (\n count = 1\n ) => {\n const locked = await this.repo.tryLock();\n if (!locked) {\n return console.log('Migrations locked');\n }\n\n try {\n const applied = await this.repo.listApplied();\n const toRollback = this.migrations\n .slice()\n .reverse()\n .filter(m => applied.has(m.name));\n\n let rolledBackCount = 0;\n for (const m of toRollback) {\n if (rolledBackCount >= count && count !== -1) {\n break;\n }\n\n await this.options.sql.begin(async (trx) => {\n await m.down(trx);\n await this.repo.unmarkApplied(m.name, trx);\n });\n console.log(`Reverted: ${m.name}`);\n rolledBackCount++;\n }\n } finally {\n await this.repo.unlock();\n }\n }\n\n async close() {\n await this.options.sql.end();\n };\n}\n","import type { Sql, TransactionSql } from 'postgres';\nimport { MigratorOptions } from './types';\n\nexport class Repository {\n private readonly schema: string;\n private readonly tableName: string;\n private readonly sql: Sql;\n private readonly lockID: number;\n\n constructor({\n sql,\n schema = 'public',\n tableName = 'migrations',\n lockId = 2128506,\n }: MigratorOptions) {\n this.sql = sql;\n this.schema = schema;\n this.tableName = tableName;\n this.lockID = lockId;\n }\n\n async ensureTable() {\n await this.sql`CREATE SCHEMA IF NOT EXISTS ${this.sql(this.schema)}`;\n await this.sql`\n CREATE TABLE IF NOT EXISTS ${ this.sql(this.schema) }.${ this.sql(this.tableName) } (\n id SERIAL PRIMARY KEY,\n name VARCHAR(255) NOT NULL UNIQUE,\n created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()\n )\n `;\n };\n\n async tryLock(): Promise<boolean> {\n const [result] = await this.sql`\n SELECT pg_try_advisory_lock(${ this.lockID }) as locked\n `;\n return !!result?.locked;\n };\n\n async unlock(): Promise<void> {\n await this.sql`\n SELECT pg_advisory_unlock(${ this.lockID })\n `;\n };\n\n async listApplied(): Promise<Set<string>> {\n await this.ensureTable();\n\n const rows = await this.sql<{ name: string }[]>`\n SELECT name FROM ${ this.sql(this.schema) }.${ this.sql(this.tableName) }\n `;\n\n return new Set(rows.map(r => r.name));\n };\n\n async markApplied(\n name: string,\n trx?: TransactionSql\n ) {\n const sql = (trx || this.sql) as Sql;\n await sql`\n INSERT INTO ${ this.sql(this.schema) }.${ this.sql(this.tableName) } (name) VALUES (${name})\n `;\n }\n\n async unmarkApplied(\n name: string,\n trx?: TransactionSql\n ) {\n const sql = (trx || this.sql) as Sql;\n await sql`\n DELETE FROM ${ this.sql(this.schema) }.${ this.sql(this.tableName) } WHERE name = ${name}\n `;\n }\n}"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@philosophocat/postgres-migrations",
3
+ "version": "0.1.1",
4
+ "description": "Simple migrations for postgres.js",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "dev": "tsup --watch",
22
+ "lint": "tsc --noEmit",
23
+ "test": "vitest run",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "postgres",
28
+ "migrations",
29
+ "typescript"
30
+ ],
31
+ "peerDependencies": {
32
+ "postgres": "^3.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.0.0",
36
+ "postgres": "^3.4.8",
37
+ "tsup": "^8.0.0",
38
+ "typescript": "^5.0.0",
39
+ "vitest": "^4.0.17"
40
+ },
41
+ "sideEffects": false,
42
+ "author": "Viacheslav Sergeev <philosophocat@gmail.com>",
43
+ "license": "ISC",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/твое-имя/репозиторий.git"
47
+ }
48
+ }