@philosophocat/postgres-migrations 0.1.2 → 0.1.4

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 CHANGED
@@ -65,6 +65,7 @@ const migrator = new Migrator({
65
65
  // schema, default 'public'
66
66
  // tableName, default 'migrations'
67
67
  // lockId, optional advisory lock id
68
+ // verbose, default true
68
69
  });
69
70
 
70
71
  const run = async () => {
package/dist/index.cjs CHANGED
@@ -91,10 +91,13 @@ var Repository = class {
91
91
  var Migrator = class {
92
92
  constructor(options) {
93
93
  this.options = options;
94
- this.repo = new Repository(options);
95
94
  }
96
95
  migrations = [];
97
- repo;
96
+ log = (message, level = "info") => {
97
+ if (this.options.verbose || level === "warn") {
98
+ console.log(message);
99
+ }
100
+ };
98
101
  scan = async (dir) => {
99
102
  const files = (0, import_node_fs.readdirSync)(dir).filter((f) => f.endsWith(".js") || f.endsWith(".ts")).sort();
100
103
  for (const file of files) {
@@ -103,7 +106,7 @@ var Migrator = class {
103
106
  const module2 = await import(filePath);
104
107
  const m = module2.migration || module2.default;
105
108
  if (!m) {
106
- console.warn(`Skipping ${file}: no migration export found`);
109
+ this.log(`Skipping ${file}: no migration export found`, "warn");
107
110
  continue;
108
111
  }
109
112
  if (this.migrations.find((migration) => migration.name === name)) {
@@ -118,57 +121,82 @@ var Migrator = class {
118
121
  };
119
122
  up = async () => {
120
123
  if (this.migrations.length === 0) {
121
- return console.log("Empty migrations list");
124
+ return this.log("Empty migrations list");
125
+ }
126
+ const connection = await this.options.sql.reserve();
127
+ try {
128
+ await this.upLocked(connection);
129
+ } finally {
130
+ connection.release();
122
131
  }
123
- const locked = await this.repo.tryLock();
132
+ };
133
+ upLocked = async (connection) => {
134
+ const repo = new Repository({ ...this.options, sql: connection }), locked = await repo.tryLock();
124
135
  if (!locked) {
125
- return console.log("Migrations are locked by another process");
136
+ return this.log("Migrations are locked by another process");
126
137
  }
127
138
  try {
128
- const applied = await this.repo.listApplied();
139
+ const applied = await repo.listApplied();
129
140
  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
- }
141
+ if (applied.has(m.name)) continue;
142
+ await this.applyMigration(m, repo, connection);
143
+ this.log(`Applied: ${m.name}`);
137
144
  }
138
- } catch (e) {
139
- console.error("Migration failed:", e);
140
- throw e;
141
145
  } finally {
142
- await this.repo.unlock();
146
+ await repo.unlock();
147
+ }
148
+ };
149
+ applyMigration = async (m, repo, connection) => {
150
+ await connection`BEGIN`;
151
+ try {
152
+ await m.up(connection);
153
+ await repo.markApplied(m.name, connection);
154
+ await connection`COMMIT`;
155
+ } catch (err) {
156
+ await connection`ROLLBACK`;
157
+ throw err;
143
158
  }
144
159
  };
145
160
  down = async (count = 1) => {
146
- const locked = await this.repo.tryLock();
161
+ const connection = await this.options.sql.reserve();
162
+ try {
163
+ await this.downLocked(connection, count);
164
+ } finally {
165
+ connection.release();
166
+ }
167
+ };
168
+ downLocked = async (connection, count) => {
169
+ const repo = new Repository({ ...this.options, sql: connection }), locked = await repo.tryLock();
147
170
  if (!locked) {
148
- return console.log("Migrations locked");
171
+ return this.log("Migrations locked");
149
172
  }
150
173
  try {
151
- const applied = await this.repo.listApplied();
152
- const toRollback = this.migrations.slice().reverse().filter((m) => applied.has(m.name));
174
+ const applied = await repo.listApplied(), toRollback = this.migrations.slice().reverse().filter((m) => applied.has(m.name));
175
+ if (toRollback.length === 0) {
176
+ return this.log("Nothing to rollback");
177
+ }
153
178
  let rolledBackCount = 0;
154
179
  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}`);
180
+ if (count > 0 && rolledBackCount >= count) break;
181
+ await this.revertMigration(m, repo, connection);
182
+ this.log(`Reverted: ${m.name}`);
163
183
  rolledBackCount++;
164
184
  }
165
185
  } finally {
166
- await this.repo.unlock();
186
+ await repo.unlock();
187
+ }
188
+ };
189
+ revertMigration = async (m, repo, connection) => {
190
+ await connection`BEGIN`;
191
+ try {
192
+ await m.down(connection);
193
+ await repo.unmarkApplied(m.name, connection);
194
+ await connection`COMMIT`;
195
+ } catch (err) {
196
+ await connection`ROLLBACK`;
197
+ throw err;
167
198
  }
168
199
  };
169
- async close() {
170
- await this.options.sql.end();
171
- }
172
200
  };
173
201
  // Annotate the CommonJS export names for ESM import in node:
174
202
  0 && (module.exports = {
@@ -1 +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":"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","names":["module"]}
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 type { Sql } from 'postgres';\nimport { Repository } from './repository';\nimport { Migration, MigratorOptions } from './types';\n\nexport class Migrator {\n\n private migrations: Migration[] = [];\n\n constructor(\n private readonly options: MigratorOptions\n ) {};\n\n private log = (\n message: string,\n level: 'info' | 'warn' = 'info'\n ) => {\n if (this.options.verbose || level === 'warn') {\n console.log(message);\n }\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 this.log(`Skipping ${file}: no migration export found`, 'warn');\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 this.log('Empty migrations list');\n }\n\n const connection = await this.options.sql.reserve();\n try {\n await this.upLocked(connection);\n } finally {\n connection.release();\n }\n };\n\n private upLocked = async (\n connection: Sql\n ) => {\n const\n repo = new Repository({ ...this.options, sql: connection }),\n locked = await repo.tryLock();\n\n if (!locked) {\n return this.log('Migrations are locked by another process');\n }\n\n try {\n const applied = await repo.listApplied();\n\n for (const m of this.migrations) {\n if (applied.has(m.name)) continue;\n await this.applyMigration(m, repo, connection);\n this.log(`Applied: ${m.name}`);\n }\n } finally {\n await repo.unlock();\n }\n }\n\n private applyMigration = async (\n m: Migration,\n repo: Repository,\n connection: Sql\n )=> {\n await connection`BEGIN`;\n try {\n await m.up(connection);\n await repo.markApplied(m.name, connection);\n await connection`COMMIT`;\n } catch (err) {\n await connection`ROLLBACK`;\n throw err;\n }\n }\n\n down = async (\n count = 1\n ) => {\n const connection = await this.options.sql.reserve();\n try {\n await this.downLocked(connection, count);\n } finally {\n connection.release();\n }\n }\n\n private downLocked = async (\n connection: Sql,\n count: number\n ) => {\n const\n repo = new Repository({ ...this.options, sql: connection }),\n locked = await repo.tryLock();\n\n if (!locked) {\n return this.log('Migrations locked');\n }\n\n try {\n const\n applied = await repo.listApplied(),\n toRollback = this.migrations\n .slice()\n .reverse()\n .filter(m => applied.has(m.name));\n\n if (toRollback.length === 0) {\n return this.log('Nothing to rollback');\n }\n\n let rolledBackCount = 0;\n for (const m of toRollback) {\n if (count > 0 && rolledBackCount >= count) break;\n\n await this.revertMigration(m, repo, connection);\n this.log(`Reverted: ${m.name}`);\n\n rolledBackCount++;\n }\n } finally {\n await repo.unlock();\n }\n }\n\n private revertMigration = async(\n m: Migration,\n repo: Repository,\n connection: Sql\n ) => {\n await connection`BEGIN`;\n try {\n await m.down(connection);\n await repo.unmarkApplied(m.name, connection);\n await connection`COMMIT`;\n } catch (err) {\n await connection`ROLLBACK`;\n throw err;\n }\n }\n}\n","import type { Sql } 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?: Sql\n ) {\n const sql = trx || this.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?: Sql\n ) {\n const sql = trx || this.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":["module"]}
package/dist/index.d.cts CHANGED
@@ -10,6 +10,7 @@ interface MigratorOptions {
10
10
  schema?: string;
11
11
  tableName?: string;
12
12
  lockId?: number;
13
+ verbose?: boolean;
13
14
  }
14
15
  interface MigrationRecord {
15
16
  id: number;
@@ -20,12 +21,15 @@ interface MigrationRecord {
20
21
  declare class Migrator {
21
22
  private readonly options;
22
23
  private migrations;
23
- private repo;
24
24
  constructor(options: MigratorOptions);
25
+ private log;
25
26
  scan: (dir: string) => Promise<void>;
26
27
  up: () => Promise<void>;
28
+ private upLocked;
29
+ private applyMigration;
27
30
  down: (count?: number) => Promise<void>;
28
- close(): Promise<void>;
31
+ private downLocked;
32
+ private revertMigration;
29
33
  }
30
34
 
31
35
  export { type Migration, type MigrationRecord, Migrator, type MigratorOptions };
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ interface MigratorOptions {
10
10
  schema?: string;
11
11
  tableName?: string;
12
12
  lockId?: number;
13
+ verbose?: boolean;
13
14
  }
14
15
  interface MigrationRecord {
15
16
  id: number;
@@ -20,12 +21,15 @@ interface MigrationRecord {
20
21
  declare class Migrator {
21
22
  private readonly options;
22
23
  private migrations;
23
- private repo;
24
24
  constructor(options: MigratorOptions);
25
+ private log;
25
26
  scan: (dir: string) => Promise<void>;
26
27
  up: () => Promise<void>;
28
+ private upLocked;
29
+ private applyMigration;
27
30
  down: (count?: number) => Promise<void>;
28
- close(): Promise<void>;
31
+ private downLocked;
32
+ private revertMigration;
29
33
  }
30
34
 
31
35
  export { type Migration, type MigrationRecord, Migrator, type MigratorOptions };
package/dist/index.js CHANGED
@@ -65,10 +65,13 @@ var Repository = class {
65
65
  var Migrator = class {
66
66
  constructor(options) {
67
67
  this.options = options;
68
- this.repo = new Repository(options);
69
68
  }
70
69
  migrations = [];
71
- repo;
70
+ log = (message, level = "info") => {
71
+ if (this.options.verbose || level === "warn") {
72
+ console.log(message);
73
+ }
74
+ };
72
75
  scan = async (dir) => {
73
76
  const files = readdirSync(dir).filter((f) => f.endsWith(".js") || f.endsWith(".ts")).sort();
74
77
  for (const file of files) {
@@ -77,7 +80,7 @@ var Migrator = class {
77
80
  const module = await import(filePath);
78
81
  const m = module.migration || module.default;
79
82
  if (!m) {
80
- console.warn(`Skipping ${file}: no migration export found`);
83
+ this.log(`Skipping ${file}: no migration export found`, "warn");
81
84
  continue;
82
85
  }
83
86
  if (this.migrations.find((migration) => migration.name === name)) {
@@ -92,57 +95,82 @@ var Migrator = class {
92
95
  };
93
96
  up = async () => {
94
97
  if (this.migrations.length === 0) {
95
- return console.log("Empty migrations list");
98
+ return this.log("Empty migrations list");
99
+ }
100
+ const connection = await this.options.sql.reserve();
101
+ try {
102
+ await this.upLocked(connection);
103
+ } finally {
104
+ connection.release();
96
105
  }
97
- const locked = await this.repo.tryLock();
106
+ };
107
+ upLocked = async (connection) => {
108
+ const repo = new Repository({ ...this.options, sql: connection }), locked = await repo.tryLock();
98
109
  if (!locked) {
99
- return console.log("Migrations are locked by another process");
110
+ return this.log("Migrations are locked by another process");
100
111
  }
101
112
  try {
102
- const applied = await this.repo.listApplied();
113
+ const applied = await repo.listApplied();
103
114
  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
- }
115
+ if (applied.has(m.name)) continue;
116
+ await this.applyMigration(m, repo, connection);
117
+ this.log(`Applied: ${m.name}`);
111
118
  }
112
- } catch (e) {
113
- console.error("Migration failed:", e);
114
- throw e;
115
119
  } finally {
116
- await this.repo.unlock();
120
+ await repo.unlock();
121
+ }
122
+ };
123
+ applyMigration = async (m, repo, connection) => {
124
+ await connection`BEGIN`;
125
+ try {
126
+ await m.up(connection);
127
+ await repo.markApplied(m.name, connection);
128
+ await connection`COMMIT`;
129
+ } catch (err) {
130
+ await connection`ROLLBACK`;
131
+ throw err;
117
132
  }
118
133
  };
119
134
  down = async (count = 1) => {
120
- const locked = await this.repo.tryLock();
135
+ const connection = await this.options.sql.reserve();
136
+ try {
137
+ await this.downLocked(connection, count);
138
+ } finally {
139
+ connection.release();
140
+ }
141
+ };
142
+ downLocked = async (connection, count) => {
143
+ const repo = new Repository({ ...this.options, sql: connection }), locked = await repo.tryLock();
121
144
  if (!locked) {
122
- return console.log("Migrations locked");
145
+ return this.log("Migrations locked");
123
146
  }
124
147
  try {
125
- const applied = await this.repo.listApplied();
126
- const toRollback = this.migrations.slice().reverse().filter((m) => applied.has(m.name));
148
+ const applied = await repo.listApplied(), toRollback = this.migrations.slice().reverse().filter((m) => applied.has(m.name));
149
+ if (toRollback.length === 0) {
150
+ return this.log("Nothing to rollback");
151
+ }
127
152
  let rolledBackCount = 0;
128
153
  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}`);
154
+ if (count > 0 && rolledBackCount >= count) break;
155
+ await this.revertMigration(m, repo, connection);
156
+ this.log(`Reverted: ${m.name}`);
137
157
  rolledBackCount++;
138
158
  }
139
159
  } finally {
140
- await this.repo.unlock();
160
+ await repo.unlock();
161
+ }
162
+ };
163
+ revertMigration = async (m, repo, connection) => {
164
+ await connection`BEGIN`;
165
+ try {
166
+ await m.down(connection);
167
+ await repo.unmarkApplied(m.name, connection);
168
+ await connection`COMMIT`;
169
+ } catch (err) {
170
+ await connection`ROLLBACK`;
171
+ throw err;
141
172
  }
142
173
  };
143
- async close() {
144
- await this.options.sql.end();
145
- }
146
174
  };
147
175
  export {
148
176
  Migrator
package/dist/index.js.map CHANGED
@@ -1 +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":[]}
1
+ {"version":3,"sources":["../src/migrator.ts","../src/repository.ts"],"sourcesContent":["import { readdirSync } from 'node:fs';\nimport { join, parse } from 'node:path';\nimport type { Sql } from 'postgres';\nimport { Repository } from './repository';\nimport { Migration, MigratorOptions } from './types';\n\nexport class Migrator {\n\n private migrations: Migration[] = [];\n\n constructor(\n private readonly options: MigratorOptions\n ) {};\n\n private log = (\n message: string,\n level: 'info' | 'warn' = 'info'\n ) => {\n if (this.options.verbose || level === 'warn') {\n console.log(message);\n }\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 this.log(`Skipping ${file}: no migration export found`, 'warn');\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 this.log('Empty migrations list');\n }\n\n const connection = await this.options.sql.reserve();\n try {\n await this.upLocked(connection);\n } finally {\n connection.release();\n }\n };\n\n private upLocked = async (\n connection: Sql\n ) => {\n const\n repo = new Repository({ ...this.options, sql: connection }),\n locked = await repo.tryLock();\n\n if (!locked) {\n return this.log('Migrations are locked by another process');\n }\n\n try {\n const applied = await repo.listApplied();\n\n for (const m of this.migrations) {\n if (applied.has(m.name)) continue;\n await this.applyMigration(m, repo, connection);\n this.log(`Applied: ${m.name}`);\n }\n } finally {\n await repo.unlock();\n }\n }\n\n private applyMigration = async (\n m: Migration,\n repo: Repository,\n connection: Sql\n )=> {\n await connection`BEGIN`;\n try {\n await m.up(connection);\n await repo.markApplied(m.name, connection);\n await connection`COMMIT`;\n } catch (err) {\n await connection`ROLLBACK`;\n throw err;\n }\n }\n\n down = async (\n count = 1\n ) => {\n const connection = await this.options.sql.reserve();\n try {\n await this.downLocked(connection, count);\n } finally {\n connection.release();\n }\n }\n\n private downLocked = async (\n connection: Sql,\n count: number\n ) => {\n const\n repo = new Repository({ ...this.options, sql: connection }),\n locked = await repo.tryLock();\n\n if (!locked) {\n return this.log('Migrations locked');\n }\n\n try {\n const\n applied = await repo.listApplied(),\n toRollback = this.migrations\n .slice()\n .reverse()\n .filter(m => applied.has(m.name));\n\n if (toRollback.length === 0) {\n return this.log('Nothing to rollback');\n }\n\n let rolledBackCount = 0;\n for (const m of toRollback) {\n if (count > 0 && rolledBackCount >= count) break;\n\n await this.revertMigration(m, repo, connection);\n this.log(`Reverted: ${m.name}`);\n\n rolledBackCount++;\n }\n } finally {\n await repo.unlock();\n }\n }\n\n private revertMigration = async(\n m: Migration,\n repo: Repository,\n connection: Sql\n ) => {\n await connection`BEGIN`;\n try {\n await m.down(connection);\n await repo.unmarkApplied(m.name, connection);\n await connection`COMMIT`;\n } catch (err) {\n await connection`ROLLBACK`;\n throw err;\n }\n }\n}\n","import type { Sql } 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?: Sql\n ) {\n const sql = trx || this.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?: Sql\n ) {\n const sql = trx || this.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 CHANGED
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "name": "@philosophocat/postgres-migrations",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Simple migrations for postgres.js",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
7
- "module": "./dist/index.mjs",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
9
  "exports": {
10
10
  ".": {
11
11
  "types": "./dist/index.d.ts",
12
- "import": "./dist/index.mjs",
13
- "require": "./dist/index.js"
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
14
  }
15
15
  },
16
16
  "files": [
17
17
  "dist"
18
18
  ],
19
19
  "scripts": {
20
- "build": "tsup",
20
+ "build": "tsup src/index.ts --format esm,cjs --dts",
21
21
  "dev": "tsup --watch",
22
22
  "lint": "tsc --noEmit",
23
23
  "test": "vitest run",