pgroll 0.0.7 → 0.0.9

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
@@ -1,60 +1,200 @@
1
- #
2
-
3
1
  # pgroll
4
2
 
5
- `pgroll` is a lightweight and flexible database migration tool for PostgreSQL
3
+ [![npm version](https://img.shields.io/npm/v/pgroll.svg)](https://www.npmjs.com/package/pgroll)
4
+ [![license](https://img.shields.io/npm/l/pgroll.svg)](./LICENSE)
5
+ [![node](https://img.shields.io/node/v/pgroll.svg)](https://nodejs.org)
6
6
 
7
- PostgreSQL clients currently supporting:
7
+ A thread-safe, lightweight, and flexible database migration tool for **PostgreSQL**.
8
8
 
9
- - [x] PostgresJS
10
- - [ ] pg
11
- - [ ] ...
9
+ `pgroll` runs your plain-SQL migrations through the [`postgres`](https://github.com/porsager/postgres)
10
+ (PostgresJS) client. Migrations are applied inside a transaction and guarded by a session-level
11
+ advisory lock, so it is safe to run concurrently from multiple processes — only one migration runs
12
+ at a time.
12
13
 
13
- `postgresjs` client. It offers simple commands to manage your database schema changes with `up`, `down`, `create`, and `go` features.
14
+ > **Supported PostgreSQL clients**
15
+ >
16
+ > - [x] PostgresJS
17
+ > - [ ] node-postgres (`pg`)
14
18
 
15
19
  ## Features
16
20
 
17
- - **up**: Apply all pending migrations.
18
- - **down**: Rollback the last applied migration.
19
- - **create**: Create new migration files.
20
- - **go**: Migrate the database schema to a specific version.
21
+ - **up** Apply all pending migrations.
22
+ - **down** Roll back **all** applied migrations (down to version `0`).
23
+ - **go** Migrate forward or backward to a specific version (`go 0` reverts everything).
24
+ - **create** Generate a matching up/down migration file pair.
25
+
26
+ ## Requirements
27
+
28
+ - **Node.js ≥ 24.16.0** (the package ships as ESM)
29
+ - A reachable **PostgreSQL** database
21
30
 
22
31
  ## Installation
23
32
 
24
- You can install `pgroll` via npm:
33
+ Install globally to use the CLI anywhere:
34
+
35
+ ```bash
36
+ npm install -g pgroll
37
+ ```
38
+
39
+ …or add it to a project and run it with `npx`:
25
40
 
26
41
  ```bash
27
42
  npm install pgroll
28
43
  ```
29
44
 
30
- ## Usage
45
+ ## Configuration
31
46
 
32
- ### Command Line Interface (CLI)
47
+ ### Connecting to PostgreSQL
33
48
 
34
- `pgroll` provides a CLI to manage your database migrations. Below are the available commands:
49
+ The CLI connects using a connection URL or the standard libpq environment variables (read by the
50
+ underlying `postgres` client).
35
51
 
36
- #### Running the CLI
52
+ **Option 1 — connection URL** via the `-u, --url` flag:
37
53
 
38
- 1. Run Migrations Up:
54
+ ```bash
55
+ npx pgroll --url "postgres://user:password@localhost:5432/mydb" up
56
+ ```
57
+
58
+ **Option 2 — environment variables:**
59
+
60
+ | Variable | Description | Default |
61
+ | ------------ | ------------- | ------------ |
62
+ | `PGHOST` | Server host | `localhost` |
63
+ | `PGPORT` | Server port | `5432` |
64
+ | `PGDATABASE` | Database name | — |
65
+ | `PGUSER` | User name | OS user name |
66
+ | `PGPASSWORD` | Password | — |
67
+
68
+ ```bash
69
+ PGHOST=localhost PGDATABASE=mydb PGUSER=me PGPASSWORD=secret npx pgroll up
70
+ ```
71
+
72
+ When `--url` is provided, it takes precedence over the `PG*` variables.
73
+
74
+ ### Migration files
75
+
76
+ Migrations live in a directory (default `./migrations`, override with `-d, --migrationDir <path>`).
77
+ Each migration is a **pair** of plain `.sql` files distinguished by suffix:
78
+
79
+ ```
80
+ 20240619121610402_init_up.sql # applied on "up"
81
+ 20240619121610402_init_down.sql # applied on "down"
82
+ ```
83
+
84
+ - The leading number is a timestamp generated by `create`; it determines order.
85
+ - `up` migrations are applied in **ascending** filename order; `down` migrations in **descending**
86
+ order, so rollbacks unwind in the reverse of how they were applied.
87
+ - A migration's _version_ is its position in that ordered list (the first up migration is version `1`).
88
+
89
+ ## CLI
90
+
91
+ ```text
92
+ pgroll [global options] <command>
93
+ ```
94
+
95
+ ### Global options
96
+
97
+ | Option | Description |
98
+ | --------------------------- | --------------------------------------------------------------- |
99
+ | `-d, --migrationDir <path>` | Directory holding the migration files (default `./migrations`). |
100
+ | `-u, --url <url>` | PostgreSQL connection URL (overrides `PG*` env vars). |
101
+ | `-V, --version` | Print the `pgroll` version. |
102
+ | `-h, --help` | Show help. |
103
+
104
+ ### Commands
105
+
106
+ **Apply all pending migrations:**
39
107
 
40
108
  ```bash
41
109
  npx pgroll up
42
110
  ```
43
111
 
44
- 2. Run Migrations Down:
112
+ **Roll back every applied migration:**
45
113
 
46
114
  ```bash
47
115
  npx pgroll down
48
116
  ```
49
117
 
50
- 3. Navigate to a Specific Version:
118
+ **Migrate to a specific version** (moves up or down as needed; `0` rolls everything back):
51
119
 
52
120
  ```bash
53
121
  npx pgroll go <version>
54
122
  ```
55
123
 
56
- 4. Create New Migration Files:
124
+ **Create a new migration pair** (writes `<timestamp>_<name>_up.sql` and `..._down.sql` with
125
+ placeholder contents for you to fill in):
57
126
 
58
127
  ```bash
59
- npx roll create <migration-name>
128
+ npx pgroll create <name>
129
+ ```
130
+
131
+ A typical workflow:
132
+
133
+ ```bash
134
+ npx pgroll create add_users_table # creates the up/down file pair
135
+ # …edit the generated *_up.sql / *_down.sql files…
136
+ npx pgroll up # apply it
137
+ ```
138
+
139
+ ## Programmatic API
140
+
141
+ `pgroll` can also be used as a library. Pass it a `postgres` client instance and drive migrations
142
+ directly:
143
+
144
+ ```ts
145
+ import postgres from 'postgres';
146
+ import { Migrator } from 'pgroll';
147
+
148
+ const sql = postgres('postgres://user:password@localhost:5432/mydb');
149
+ const migrator = new Migrator(sql, './migrations');
150
+
151
+ // Apply all pending migrations
152
+ await migrator.up();
153
+
154
+ console.log('Current version:', await migrator.getCurrentVersion());
155
+
156
+ // Migrate to a specific version, logging progress as it goes
157
+ await migrator.go(0, { eventHandler: info => console.log(info) });
158
+
159
+ await sql.end();
60
160
  ```
161
+
162
+ ### `new Migrator(dbClient, migrationsDir?)`
163
+
164
+ | Parameter | Type | Description |
165
+ | --------------- | ------------------- | ------------------------------------------------------------- |
166
+ | `dbClient` | `Sql` (PostgresJS) | A `postgres` client instance. |
167
+ | `migrationsDir` | `string` (optional) | Directory of migration files. Defaults to `<cwd>/migrations`. |
168
+
169
+ ### Methods
170
+
171
+ | Method | Description |
172
+ | --------------------- | ----------------------------------------------------------------------- |
173
+ | `up(opts?)` | Apply all pending up migrations. |
174
+ | `down(opts?)` | Roll back all applied migrations (to version `0`). |
175
+ | `go(version, opts?)` | Migrate forward or backward to `version` (`0` reverts everything). |
176
+ | `getCurrentVersion()` | Resolve to the highest applied version (`0` if none have been applied). |
177
+
178
+ `opts` is `{ eventHandler: (info: string) => void }` — an optional callback invoked with a
179
+ human-readable message as each migration is applied.
180
+
181
+ ## How it works
182
+
183
+ On the first run, `pgroll` creates a bookkeeping table:
184
+
185
+ ```sql
186
+ CREATE TABLE IF NOT EXISTS migrations (
187
+ name varchar(500) PRIMARY KEY,
188
+ version smallint NOT NULL,
189
+ applied_at timestamp DEFAULT CURRENT_TIMESTAMP
190
+ );
191
+ ```
192
+
193
+ Each `up`/`down`/`go` run reserves a single connection, takes a session-level
194
+ `pg_advisory_lock`, and applies the relevant migration files within a transaction — recording or
195
+ removing the corresponding rows in `migrations` as it goes. The advisory lock serializes concurrent
196
+ runs across processes, and the surrounding transaction means a failed migration rolls back cleanly.
197
+
198
+ ## License
199
+
200
+ [ISC](./LICENSE)
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -1,28 +1,32 @@
1
- import { __awaiter } from "tslib";
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
2
3
  import * as process from 'node:process';
3
4
  import { Command } from 'commander';
4
5
  import postgres from 'postgres';
5
- import { createFile } from './utils';
6
- import { Migrator } from './index';
6
+ import { Migrator } from "./index.js";
7
+ import { createFile } from "./utils.js";
7
8
  const program = new Command();
8
9
  let migrator;
9
10
  program
10
- .version('0.0.1')
11
+ .version('0.0.9')
11
12
  .description('Database migration tool')
12
13
  .option('-d, --migrationDir <filepath>', 'Specify migration directory(Default: ./migrations)')
14
+ .option('-u, --url <url>', 'PostgreSQL connection URL (overrides PG* env vars)')
13
15
  .hook('preAction', cmd => {
14
16
  const opts = cmd.opts();
15
- migrator = new Migrator(postgres({
17
+ const pgOptions = {
16
18
  onnotice: () => {
19
+ // do nothing
17
20
  }
18
- }), opts.migrationDir);
21
+ };
22
+ migrator = new Migrator(opts.url ? postgres(opts.url, pgOptions) : postgres(pgOptions), opts.migrationDir);
19
23
  });
20
24
  program
21
25
  .command('up')
22
26
  .description('Run all up migrations')
23
- .action(() => __awaiter(void 0, void 0, void 0, function* () {
27
+ .action(async () => {
24
28
  try {
25
- yield migrator.up({ eventHandler: console.log });
29
+ await migrator.up({ eventHandler: console.log });
26
30
  console.log('Migrations up completed successfully.');
27
31
  process.exit(0);
28
32
  }
@@ -30,13 +34,13 @@ program
30
34
  console.error('Error during migrations up:', error);
31
35
  process.exit(1);
32
36
  }
33
- }));
37
+ });
34
38
  program
35
39
  .command('down')
36
40
  .description('Run all down migrations')
37
- .action(() => __awaiter(void 0, void 0, void 0, function* () {
41
+ .action(async () => {
38
42
  try {
39
- yield migrator.down({ eventHandler: console.log });
43
+ await migrator.down({ eventHandler: console.log });
40
44
  console.log('Migrations down completed successfully.');
41
45
  process.exit(0);
42
46
  }
@@ -44,7 +48,7 @@ program
44
48
  console.error('Error during migrations down:', error);
45
49
  process.exit(1);
46
50
  }
47
- }));
51
+ });
48
52
  program
49
53
  .command('create')
50
54
  .description('Generate migration files in the migrations folder: one for applying changes (up), and one for reverting them (down).')
@@ -59,19 +63,19 @@ program
59
63
  .command('go')
60
64
  .description('Navigate to a specific version; version 0 performs a rollback, reverting all migrations.')
61
65
  .argument('<version>', 'version to migrate to')
62
- .action((version) => __awaiter(void 0, void 0, void 0, function* () {
66
+ .action(async (version) => {
63
67
  const parsedVersion = Number.parseInt(version, 10);
64
68
  if (Number.isNaN(parsedVersion)) {
65
69
  console.error('Invalid version number.');
66
70
  process.exit(1);
67
71
  }
68
72
  try {
69
- yield migrator.go(parsedVersion, { eventHandler: console.log });
73
+ await migrator.go(parsedVersion, { eventHandler: console.log });
70
74
  process.exit(0);
71
75
  }
72
76
  catch (error) {
73
77
  console.error('Error during migrations:', error);
74
78
  process.exit(1);
75
79
  }
76
- }));
80
+ });
77
81
  program.parse(process.argv);
@@ -1,5 +1,5 @@
1
- import { ReservedSql, Sql } from 'postgres';
2
- import { Direction } from './utils';
1
+ import { type ReservedSql, type Sql } from 'postgres';
2
+ import { type Direction } from './utils.ts';
3
3
  interface Option {
4
4
  eventHandler: (info: string) => void;
5
5
  }
@@ -0,0 +1,141 @@
1
+ import path from 'node:path';
2
+ import {} from 'postgres';
3
+ import { getMigrationFiles } from "./utils.js";
4
+ export class Migrator {
5
+ dbClient;
6
+ migrationsDir;
7
+ constructor(dbClient, migrationsDir = '') {
8
+ this.dbClient = dbClient;
9
+ this.migrationsDir = migrationsDir || `${process.cwd()}/migrations`;
10
+ }
11
+ async ensureMigrationTable(tx) {
12
+ await tx `CREATE TABLE IF NOT EXISTS migrations(
13
+ name varchar(500) PRIMARY KEY,
14
+ version smallint NOT NULL,
15
+ applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);`;
16
+ }
17
+ async up() {
18
+ return this.migrate('up');
19
+ }
20
+ async down() {
21
+ return this.migrate('down');
22
+ }
23
+ async go(version, opts) {
24
+ const tx = await this.dbClient.reserve();
25
+ try {
26
+ await this.acquireLock(tx);
27
+ await this.begin(tx);
28
+ await this.ensureMigrationTable(tx);
29
+ const currentVersion = await this.getCurrentVersionWithTx(tx);
30
+ if (currentVersion === version) {
31
+ opts?.eventHandler(`Already at version ${version}`);
32
+ return;
33
+ }
34
+ const direction = version > currentVersion ? 'up' : 'down';
35
+ const fileNames = getMigrationFiles(this.migrationsDir, direction);
36
+ if (direction === 'up') {
37
+ const fileVersion = Math.min(fileNames.length, version);
38
+ for (let i = currentVersion; i < fileVersion; i++) {
39
+ const file = fileNames[i] ?? '';
40
+ await Promise.all([
41
+ tx.file(path.join(this.migrationsDir, file)).execute(),
42
+ tx `INSERT INTO migrations(name, version) VALUES (${file}, ${i} + 1)`
43
+ ]);
44
+ opts?.eventHandler(`Successfully migrated: ${file}`);
45
+ }
46
+ if (version > fileNames.length) {
47
+ opts?.eventHandler(`Currently at latest version: ${currentVersion}`);
48
+ }
49
+ }
50
+ else {
51
+ // get index of the current file
52
+ const start = fileNames.length - currentVersion;
53
+ // current index + number of file to down
54
+ const end = start + (currentVersion - version);
55
+ for (let i = start; i < end; i++) {
56
+ const file = fileNames[i] ?? '';
57
+ await Promise.all([
58
+ tx.file(path.join(this.migrationsDir, file)).execute(),
59
+ tx `DELETE FROM migrations WHERE version = ${fileNames.length - i}`
60
+ ]);
61
+ opts?.eventHandler(`Successfully migrated: ${file}`);
62
+ }
63
+ }
64
+ await this.commit(tx);
65
+ }
66
+ catch (error) {
67
+ await this.rollback(tx);
68
+ throw error;
69
+ }
70
+ finally {
71
+ await this.releaseLock(tx);
72
+ tx.release();
73
+ }
74
+ }
75
+ async migrate(direction, opts) {
76
+ const tx = await this.dbClient.reserve();
77
+ try {
78
+ await this.acquireLock(tx);
79
+ await this.begin(tx);
80
+ await this.ensureMigrationTable(tx);
81
+ const currentVersion = await this.getCurrentVersionWithTx(tx);
82
+ const fileNames = getMigrationFiles(this.migrationsDir, direction);
83
+ if (direction === 'up') {
84
+ for (const fileName of fileNames) {
85
+ const id = fileNames.indexOf(fileName);
86
+ if (id >= currentVersion) {
87
+ await Promise.all([
88
+ tx.file(path.join(this.migrationsDir, fileName)).execute(),
89
+ tx `INSERT INTO migrations(name, version) VALUES (${fileName}, ${id} + 1)`
90
+ ]);
91
+ opts?.eventHandler(`Successfully migrated: ${fileName}`);
92
+ }
93
+ }
94
+ }
95
+ else {
96
+ // calculate start to know where the down migration starts
97
+ const start = fileNames.length - currentVersion;
98
+ for (let i = start; i < fileNames.length; i++) {
99
+ const file = fileNames[i] ?? '';
100
+ await Promise.all([
101
+ tx.file(path.join(this.migrationsDir, file)).execute(),
102
+ tx `DELETE FROM migrations WHERE version = ${fileNames.length - i}`
103
+ ]);
104
+ opts?.eventHandler(`Successfully migrated: ${file}`);
105
+ }
106
+ }
107
+ await this.commit(tx);
108
+ }
109
+ catch (error) {
110
+ await this.rollback(tx);
111
+ throw error;
112
+ }
113
+ finally {
114
+ await this.releaseLock(tx);
115
+ tx.release();
116
+ }
117
+ }
118
+ async getCurrentVersion() {
119
+ const result = await this.dbClient `SELECT version FROM migrations ORDER BY version DESC LIMIT 1`;
120
+ return result.length > 0 ? result[0]?.['version'] : 0;
121
+ }
122
+ async getCurrentVersionWithTx(tx) {
123
+ const result = await tx `SELECT version FROM migrations ORDER BY version DESC LIMIT 1`;
124
+ return result.length > 0 ? result[0]?.['version'] : 0;
125
+ }
126
+ async acquireLock(tx) {
127
+ await tx `SELECT pg_advisory_lock(21421431414441411)`;
128
+ }
129
+ async releaseLock(tx) {
130
+ await tx `SELECT pg_advisory_unlock(21421431414441411)`;
131
+ }
132
+ async begin(tx) {
133
+ await tx `BEGIN`;
134
+ }
135
+ async commit(tx) {
136
+ await tx `COMMIT`;
137
+ }
138
+ async rollback(tx) {
139
+ await tx `ROLLBACK`;
140
+ }
141
+ }
@@ -1,13 +1,11 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  export const getMigrationFiles = (dir, direction) => {
4
- const files = fs
5
- .readdirSync(dir)
6
- .filter(file => file.endsWith(`_${direction}.sql`));
7
- if (direction == 'up') {
8
- return files.sort();
4
+ const files = fs.readdirSync(dir).filter(file => file.endsWith(`_${direction}.sql`));
5
+ if (direction === 'up') {
6
+ return files.toSorted((a, b) => a.localeCompare(b));
9
7
  }
10
- return files.sort((a, b) => b.localeCompare(a));
8
+ return files.toSorted((a, b) => b.localeCompare(a));
11
9
  };
12
10
  export const createFolderIfNotExists = (filePath) => {
13
11
  if (!fs.existsSync(filePath)) {
@@ -18,10 +16,12 @@ export const createFile = (migrationDir, name) => {
18
16
  createFolderIfNotExists(migrationDir);
19
17
  const timestamp = new Date().toISOString().replaceAll(/[.:TZ-]/g, '');
20
18
  const ressult = [];
19
+ // make up file
21
20
  let fileName = `${timestamp}_${name}_up.sql`;
22
21
  let filePath = path.join(migrationDir, fileName);
23
22
  fs.writeFileSync(filePath, '-- up SQL here');
24
23
  ressult.push(filePath);
24
+ // make down file
25
25
  fileName = `${timestamp}_${name}_down.sql`;
26
26
  filePath = path.join(migrationDir, fileName);
27
27
  fs.writeFileSync(filePath, '-- down SQL here');
package/package.json CHANGED
@@ -1,20 +1,25 @@
1
1
  {
2
2
  "name": "pgroll",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Postgres migration tool",
5
- "main": "dist/cjs/src/index.js",
6
- "module": "dist/esm/src/index.js",
7
- "types": "dist/cjs/src/index.d.ts",
5
+ "type": "module",
6
+ "main": "dist/src/index.js",
7
+ "types": "dist/src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/src/index.d.ts",
11
+ "import": "./dist/src/index.js"
12
+ }
13
+ },
8
14
  "repository": {
9
15
  "type": "git",
10
- "url": "https://github.com/tnht95/pgroll"
16
+ "url": "git+https://github.com/tnht95/pgroll.git"
11
17
  },
12
18
  "files": [
13
- "dist/cjs/src",
14
- "dist/esm/src"
19
+ "dist/src"
15
20
  ],
16
21
  "bin": {
17
- "pgroll": "./dist/cjs/src/cli.js"
22
+ "pgroll": "dist/src/cli.js"
18
23
  },
19
24
  "keywords": [
20
25
  "postgres",
@@ -25,50 +30,44 @@
25
30
  "typescript"
26
31
  ],
27
32
  "scripts": {
28
- "dev": "tsx ./src/cli.ts",
29
- "build-cjs": "tsc -p tsconfig.cjs.json",
30
- "build-esm": "tsc -p tsconfig.esm.json",
31
- "build": "rm -rf dist && pnpm build-cjs && pnpm build-esm",
32
- "start": "node ./dist/index.js",
33
- "fmt": "prettier --write .",
34
- "fmtc": "prettier --check .",
33
+ "preinstall": "npx only-allow pnpm",
34
+ "dev": "node --watch ./src/cli.ts",
35
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
36
+ "start": "node ./dist/src/cli.js",
37
+ "format": "prettier --write .",
38
+ "format:check": "prettier --check .",
35
39
  "lint": "eslint . --fix",
36
- "lintc": "eslint .",
37
- "test": "jest",
38
- "pp": "pnpm build && node update-version && npm publish --access=public"
40
+ "lint:check": "eslint .",
41
+ "test": "vitest run",
42
+ "test:watch": "vitest",
43
+ "test:coverage": "vitest run --coverage",
44
+ "pp": "pnpm build && node update-version.ts && npm publish --access=public"
39
45
  },
40
- "packageManager": "pnpm@9.4.0",
46
+ "packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316",
41
47
  "engines": {
42
- "node": ">=20.14.0",
43
- "npm": "please-use-pnpm",
44
- "yarn": "please-use-pnpm",
45
- "pnpm": ">=9.4.0"
48
+ "node": ">=v24.16.0"
46
49
  },
47
50
  "license": "ISC",
48
51
  "devDependencies": {
49
- "@eslint/compat": "^1.1.0",
50
- "@eslint/eslintrc": "^3.1.0",
51
- "@eslint/js": "^9.5.0",
52
- "@types/jest": "^29.5.12",
53
- "@types/node": "^20.14.8",
54
- "@typescript-eslint/eslint-plugin": "^7.14.1",
55
- "eslint": "^9.5.0",
56
- "eslint-config-prettier": "^9.1.0",
57
- "eslint-import-resolver-typescript": "^3.6.1",
58
- "eslint-plugin-import": "^2.29.1",
59
- "eslint-plugin-jest": "^28.6.0",
60
- "eslint-plugin-promise": "^6.2.0",
61
- "eslint-plugin-sonarjs": "^1.0.3",
62
- "eslint-plugin-unicorn": "^54.0.0",
63
- "jest": "^29.7.0",
64
- "prettier": "^3.3.2",
65
- "ts-jest": "^29.1.5",
66
- "tsx": "^4.15.7",
67
- "typescript": "^5.5.2"
52
+ "@eslint/js": "^10.0.1",
53
+ "@types/node": "^25.9.1",
54
+ "@vitest/coverage-v8": "^4.1.7",
55
+ "@vitest/eslint-plugin": "^1.6.18",
56
+ "eslint": "^10.4.0",
57
+ "eslint-config-prettier": "^10.1.8",
58
+ "eslint-import-resolver-typescript": "^4.4.4",
59
+ "eslint-plugin-import-x": "^4.16.2",
60
+ "eslint-plugin-promise": "^7.3.0",
61
+ "eslint-plugin-sonarjs": "^4.0.3",
62
+ "eslint-plugin-unicorn": "^64.0.0",
63
+ "globals": "^17.6.0",
64
+ "prettier": "^3.8.3",
65
+ "typescript": "^6.0.3",
66
+ "typescript-eslint": "^8.60.0",
67
+ "vitest": "^4.1.7"
68
68
  },
69
69
  "dependencies": {
70
- "commander": "^12.1.0",
71
- "postgres": "^3.4.4",
72
- "tslib": "^2.6.3"
70
+ "commander": "^14.0.3",
71
+ "postgres": "^3.4.9"
73
72
  }
74
73
  }
@@ -1 +0,0 @@
1
- export {};
@@ -1,79 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const tslib_1 = require("tslib");
4
- const process = tslib_1.__importStar(require("node:process"));
5
- const commander_1 = require("commander");
6
- const postgres_1 = tslib_1.__importDefault(require("postgres"));
7
- const utils_1 = require("./utils");
8
- const index_1 = require("./index");
9
- const program = new commander_1.Command();
10
- let migrator;
11
- program
12
- .version('0.0.1')
13
- .description('Database migration tool')
14
- .option('-d, --migrationDir <filepath>', 'Specify migration directory(Default: ./migrations)')
15
- .hook('preAction', cmd => {
16
- const opts = cmd.opts();
17
- migrator = new index_1.Migrator((0, postgres_1.default)({
18
- onnotice: () => {
19
- }
20
- }), opts.migrationDir);
21
- });
22
- program
23
- .command('up')
24
- .description('Run all up migrations')
25
- .action(() => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
26
- try {
27
- yield migrator.up({ eventHandler: console.log });
28
- console.log('Migrations up completed successfully.');
29
- process.exit(0);
30
- }
31
- catch (error) {
32
- console.error('Error during migrations up:', error);
33
- process.exit(1);
34
- }
35
- }));
36
- program
37
- .command('down')
38
- .description('Run all down migrations')
39
- .action(() => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
40
- try {
41
- yield migrator.down({ eventHandler: console.log });
42
- console.log('Migrations down completed successfully.');
43
- process.exit(0);
44
- }
45
- catch (error) {
46
- console.error('Error during migrations down:', error);
47
- process.exit(1);
48
- }
49
- }));
50
- program
51
- .command('create')
52
- .description('Generate migration files in the migrations folder: one for applying changes (up), and one for reverting them (down).')
53
- .argument('<filename>', 'file name to be created')
54
- .action((fileName) => {
55
- const result = (0, utils_1.createFile)(migrator.migrationsDir, fileName);
56
- for (const f of result)
57
- console.log(`Successfully created migration files: ${f}`);
58
- process.exit(0);
59
- });
60
- program
61
- .command('go')
62
- .description('Navigate to a specific version; version 0 performs a rollback, reverting all migrations.')
63
- .argument('<version>', 'version to migrate to')
64
- .action((version) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
65
- const parsedVersion = Number.parseInt(version, 10);
66
- if (Number.isNaN(parsedVersion)) {
67
- console.error('Invalid version number.');
68
- process.exit(1);
69
- }
70
- try {
71
- yield migrator.go(parsedVersion, { eventHandler: console.log });
72
- process.exit(0);
73
- }
74
- catch (error) {
75
- console.error('Error during migrations:', error);
76
- process.exit(1);
77
- }
78
- }));
79
- program.parse(process.argv);
@@ -1,30 +0,0 @@
1
- import { ReservedSql, Sql } from 'postgres';
2
- import { Direction } from './utils';
3
- interface Option {
4
- eventHandler: (info: string) => void;
5
- }
6
- export interface IMigrator {
7
- migrationsDir: string;
8
- up: (opts?: Option) => Promise<void>;
9
- down: (opts?: Option) => Promise<void>;
10
- go: (version: number, opts?: Option) => Promise<void>;
11
- getCurrentVersion: () => Promise<number>;
12
- }
13
- export declare class Migrator implements IMigrator {
14
- private readonly dbClient;
15
- readonly migrationsDir: string;
16
- constructor(dbClient: Sql, migrationsDir?: string);
17
- ensureMigrationTable(tx: ReservedSql): Promise<void>;
18
- up(): Promise<void>;
19
- down(): Promise<void>;
20
- go(version: number, opts?: Option): Promise<void>;
21
- migrate(direction: Direction, opts?: Option): Promise<void>;
22
- getCurrentVersion(): Promise<number>;
23
- getCurrentVersionWithTx(tx: ReservedSql): Promise<number>;
24
- acquireLock(tx: ReservedSql): Promise<void>;
25
- releaseLock(tx: ReservedSql): Promise<void>;
26
- begin(tx: ReservedSql): Promise<void>;
27
- commit(tx: ReservedSql): Promise<void>;
28
- rollback(tx: ReservedSql): Promise<void>;
29
- }
30
- export {};
@@ -1,181 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Migrator = void 0;
4
- const tslib_1 = require("tslib");
5
- const node_path_1 = tslib_1.__importDefault(require("node:path"));
6
- const utils_1 = require("./utils");
7
- class Migrator {
8
- constructor(dbClient, migrationsDir = '') {
9
- Object.defineProperty(this, "dbClient", {
10
- enumerable: true,
11
- configurable: true,
12
- writable: true,
13
- value: void 0
14
- });
15
- Object.defineProperty(this, "migrationsDir", {
16
- enumerable: true,
17
- configurable: true,
18
- writable: true,
19
- value: void 0
20
- });
21
- this.dbClient = dbClient;
22
- this.migrationsDir = migrationsDir || `${process.cwd()}/migrations`;
23
- }
24
- ensureMigrationTable(tx) {
25
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
26
- yield tx `CREATE TABLE IF NOT EXISTS migrations(
27
- name varchar(500) PRIMARY KEY,
28
- version smallint NOT NULL,
29
- applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);`;
30
- });
31
- }
32
- up() {
33
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
34
- return this.migrate('up');
35
- });
36
- }
37
- down() {
38
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
39
- return this.migrate('down');
40
- });
41
- }
42
- go(version, opts) {
43
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
44
- var _a, _b;
45
- const tx = yield this.dbClient.reserve();
46
- try {
47
- yield this.acquireLock(tx);
48
- yield this.begin(tx);
49
- yield this.ensureMigrationTable(tx);
50
- const currentVersion = yield this.getCurrentVersionWithTx(tx);
51
- if (currentVersion === version) {
52
- opts === null || opts === void 0 ? void 0 : opts.eventHandler(`Already at version ${version}`);
53
- return;
54
- }
55
- const direction = version > currentVersion ? 'up' : 'down';
56
- const fileNames = (0, utils_1.getMigrationFiles)(this.migrationsDir, direction);
57
- if (direction === 'up') {
58
- const fileVersion = Math.min(fileNames.length, version);
59
- for (let i = currentVersion; i < fileVersion; i++) {
60
- const file = (_a = fileNames[i]) !== null && _a !== void 0 ? _a : '';
61
- yield Promise.all([
62
- tx.file(node_path_1.default.join(this.migrationsDir, file)).execute(),
63
- tx `INSERT INTO migrations(name, version) VALUES (${file}, ${i} + 1)`
64
- ]);
65
- opts === null || opts === void 0 ? void 0 : opts.eventHandler(`Successfully migrated: ${file}`);
66
- }
67
- if (version > fileNames.length) {
68
- opts === null || opts === void 0 ? void 0 : opts.eventHandler(`Currently at latest version: ${currentVersion}`);
69
- }
70
- }
71
- else {
72
- const start = fileNames.length - currentVersion;
73
- const end = start + (currentVersion - version);
74
- for (let i = start; i < end; i++) {
75
- const file = (_b = fileNames[i]) !== null && _b !== void 0 ? _b : '';
76
- yield Promise.all([
77
- tx.file(node_path_1.default.join(this.migrationsDir, file)).execute(),
78
- tx `DELETE FROM migrations WHERE version = ${fileNames.length - i}`
79
- ]);
80
- opts === null || opts === void 0 ? void 0 : opts.eventHandler(`Successfully migrated: ${file}`);
81
- }
82
- }
83
- yield this.commit(tx);
84
- }
85
- catch (error) {
86
- yield this.rollback(tx);
87
- throw error;
88
- }
89
- finally {
90
- yield this.releaseLock(tx);
91
- tx.release();
92
- }
93
- });
94
- }
95
- migrate(direction, opts) {
96
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
97
- var _a;
98
- const tx = yield this.dbClient.reserve();
99
- try {
100
- yield this.acquireLock(tx);
101
- yield this.begin(tx);
102
- yield this.ensureMigrationTable(tx);
103
- const currentVersion = yield this.getCurrentVersionWithTx(tx);
104
- const fileNames = (0, utils_1.getMigrationFiles)(this.migrationsDir, direction);
105
- if (direction === 'up') {
106
- for (const fileName of fileNames) {
107
- const id = fileNames.indexOf(fileName);
108
- if (id >= currentVersion) {
109
- yield Promise.all([
110
- tx.file(node_path_1.default.join(this.migrationsDir, fileName)).execute(),
111
- tx `INSERT INTO migrations(name, version) VALUES (${fileName}, ${id} + 1)`
112
- ]);
113
- opts === null || opts === void 0 ? void 0 : opts.eventHandler(`Successfully migrated: ${fileName}`);
114
- }
115
- }
116
- }
117
- else {
118
- const start = fileNames.length - currentVersion;
119
- for (let i = start; i < fileNames.length; i++) {
120
- const file = (_a = fileNames[i]) !== null && _a !== void 0 ? _a : '';
121
- yield Promise.all([
122
- tx.file(node_path_1.default.join(this.migrationsDir, file)).execute(),
123
- tx `DELETE FROM migrations WHERE version = ${fileNames.length - i}`
124
- ]);
125
- opts === null || opts === void 0 ? void 0 : opts.eventHandler(`Successfully migrated: ${file}`);
126
- }
127
- }
128
- yield this.commit(tx);
129
- }
130
- catch (error) {
131
- yield this.rollback(tx);
132
- throw error;
133
- }
134
- finally {
135
- yield this.releaseLock(tx);
136
- tx.release();
137
- }
138
- });
139
- }
140
- getCurrentVersion() {
141
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
142
- var _a;
143
- const result = yield this
144
- .dbClient `SELECT version FROM migrations ORDER BY version DESC LIMIT 1`;
145
- return result.length > 0 ? (_a = result[0]) === null || _a === void 0 ? void 0 : _a['version'] : 0;
146
- });
147
- }
148
- getCurrentVersionWithTx(tx) {
149
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
150
- var _a;
151
- const result = yield tx `SELECT version FROM migrations ORDER BY version DESC LIMIT 1`;
152
- return result.length > 0 ? (_a = result[0]) === null || _a === void 0 ? void 0 : _a['version'] : 0;
153
- });
154
- }
155
- acquireLock(tx) {
156
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
157
- yield tx `SELECT pg_advisory_lock(21421431414441411)`;
158
- });
159
- }
160
- releaseLock(tx) {
161
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
162
- yield tx `SELECT pg_advisory_unlock(21421431414441411)`;
163
- });
164
- }
165
- begin(tx) {
166
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
167
- yield tx `BEGIN`;
168
- });
169
- }
170
- commit(tx) {
171
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
172
- yield tx `COMMIT`;
173
- });
174
- }
175
- rollback(tx) {
176
- return tslib_1.__awaiter(this, void 0, void 0, function* () {
177
- yield tx `ROLLBACK`;
178
- });
179
- }
180
- }
181
- exports.Migrator = Migrator;
@@ -1,37 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createFile = exports.createFolderIfNotExists = exports.getMigrationFiles = void 0;
4
- const tslib_1 = require("tslib");
5
- const node_fs_1 = tslib_1.__importDefault(require("node:fs"));
6
- const node_path_1 = tslib_1.__importDefault(require("node:path"));
7
- const getMigrationFiles = (dir, direction) => {
8
- const files = node_fs_1.default
9
- .readdirSync(dir)
10
- .filter(file => file.endsWith(`_${direction}.sql`));
11
- if (direction == 'up') {
12
- return files.sort();
13
- }
14
- return files.sort((a, b) => b.localeCompare(a));
15
- };
16
- exports.getMigrationFiles = getMigrationFiles;
17
- const createFolderIfNotExists = (filePath) => {
18
- if (!node_fs_1.default.existsSync(filePath)) {
19
- node_fs_1.default.mkdirSync(filePath, { recursive: true });
20
- }
21
- };
22
- exports.createFolderIfNotExists = createFolderIfNotExists;
23
- const createFile = (migrationDir, name) => {
24
- (0, exports.createFolderIfNotExists)(migrationDir);
25
- const timestamp = new Date().toISOString().replaceAll(/[.:TZ-]/g, '');
26
- const ressult = [];
27
- let fileName = `${timestamp}_${name}_up.sql`;
28
- let filePath = node_path_1.default.join(migrationDir, fileName);
29
- node_fs_1.default.writeFileSync(filePath, '-- up SQL here');
30
- ressult.push(filePath);
31
- fileName = `${timestamp}_${name}_down.sql`;
32
- filePath = node_path_1.default.join(migrationDir, fileName);
33
- node_fs_1.default.writeFileSync(filePath, '-- down SQL here');
34
- ressult.push(filePath);
35
- return ressult;
36
- };
37
- exports.createFile = createFile;
@@ -1 +0,0 @@
1
- export {};
@@ -1,177 +0,0 @@
1
- import { __awaiter } from "tslib";
2
- import path from 'node:path';
3
- import { getMigrationFiles } from './utils';
4
- export class Migrator {
5
- constructor(dbClient, migrationsDir = '') {
6
- Object.defineProperty(this, "dbClient", {
7
- enumerable: true,
8
- configurable: true,
9
- writable: true,
10
- value: void 0
11
- });
12
- Object.defineProperty(this, "migrationsDir", {
13
- enumerable: true,
14
- configurable: true,
15
- writable: true,
16
- value: void 0
17
- });
18
- this.dbClient = dbClient;
19
- this.migrationsDir = migrationsDir || `${process.cwd()}/migrations`;
20
- }
21
- ensureMigrationTable(tx) {
22
- return __awaiter(this, void 0, void 0, function* () {
23
- yield tx `CREATE TABLE IF NOT EXISTS migrations(
24
- name varchar(500) PRIMARY KEY,
25
- version smallint NOT NULL,
26
- applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);`;
27
- });
28
- }
29
- up() {
30
- return __awaiter(this, void 0, void 0, function* () {
31
- return this.migrate('up');
32
- });
33
- }
34
- down() {
35
- return __awaiter(this, void 0, void 0, function* () {
36
- return this.migrate('down');
37
- });
38
- }
39
- go(version, opts) {
40
- return __awaiter(this, void 0, void 0, function* () {
41
- var _a, _b;
42
- const tx = yield this.dbClient.reserve();
43
- try {
44
- yield this.acquireLock(tx);
45
- yield this.begin(tx);
46
- yield this.ensureMigrationTable(tx);
47
- const currentVersion = yield this.getCurrentVersionWithTx(tx);
48
- if (currentVersion === version) {
49
- opts === null || opts === void 0 ? void 0 : opts.eventHandler(`Already at version ${version}`);
50
- return;
51
- }
52
- const direction = version > currentVersion ? 'up' : 'down';
53
- const fileNames = getMigrationFiles(this.migrationsDir, direction);
54
- if (direction === 'up') {
55
- const fileVersion = Math.min(fileNames.length, version);
56
- for (let i = currentVersion; i < fileVersion; i++) {
57
- const file = (_a = fileNames[i]) !== null && _a !== void 0 ? _a : '';
58
- yield Promise.all([
59
- tx.file(path.join(this.migrationsDir, file)).execute(),
60
- tx `INSERT INTO migrations(name, version) VALUES (${file}, ${i} + 1)`
61
- ]);
62
- opts === null || opts === void 0 ? void 0 : opts.eventHandler(`Successfully migrated: ${file}`);
63
- }
64
- if (version > fileNames.length) {
65
- opts === null || opts === void 0 ? void 0 : opts.eventHandler(`Currently at latest version: ${currentVersion}`);
66
- }
67
- }
68
- else {
69
- const start = fileNames.length - currentVersion;
70
- const end = start + (currentVersion - version);
71
- for (let i = start; i < end; i++) {
72
- const file = (_b = fileNames[i]) !== null && _b !== void 0 ? _b : '';
73
- yield Promise.all([
74
- tx.file(path.join(this.migrationsDir, file)).execute(),
75
- tx `DELETE FROM migrations WHERE version = ${fileNames.length - i}`
76
- ]);
77
- opts === null || opts === void 0 ? void 0 : opts.eventHandler(`Successfully migrated: ${file}`);
78
- }
79
- }
80
- yield this.commit(tx);
81
- }
82
- catch (error) {
83
- yield this.rollback(tx);
84
- throw error;
85
- }
86
- finally {
87
- yield this.releaseLock(tx);
88
- tx.release();
89
- }
90
- });
91
- }
92
- migrate(direction, opts) {
93
- return __awaiter(this, void 0, void 0, function* () {
94
- var _a;
95
- const tx = yield this.dbClient.reserve();
96
- try {
97
- yield this.acquireLock(tx);
98
- yield this.begin(tx);
99
- yield this.ensureMigrationTable(tx);
100
- const currentVersion = yield this.getCurrentVersionWithTx(tx);
101
- const fileNames = getMigrationFiles(this.migrationsDir, direction);
102
- if (direction === 'up') {
103
- for (const fileName of fileNames) {
104
- const id = fileNames.indexOf(fileName);
105
- if (id >= currentVersion) {
106
- yield Promise.all([
107
- tx.file(path.join(this.migrationsDir, fileName)).execute(),
108
- tx `INSERT INTO migrations(name, version) VALUES (${fileName}, ${id} + 1)`
109
- ]);
110
- opts === null || opts === void 0 ? void 0 : opts.eventHandler(`Successfully migrated: ${fileName}`);
111
- }
112
- }
113
- }
114
- else {
115
- const start = fileNames.length - currentVersion;
116
- for (let i = start; i < fileNames.length; i++) {
117
- const file = (_a = fileNames[i]) !== null && _a !== void 0 ? _a : '';
118
- yield Promise.all([
119
- tx.file(path.join(this.migrationsDir, file)).execute(),
120
- tx `DELETE FROM migrations WHERE version = ${fileNames.length - i}`
121
- ]);
122
- opts === null || opts === void 0 ? void 0 : opts.eventHandler(`Successfully migrated: ${file}`);
123
- }
124
- }
125
- yield this.commit(tx);
126
- }
127
- catch (error) {
128
- yield this.rollback(tx);
129
- throw error;
130
- }
131
- finally {
132
- yield this.releaseLock(tx);
133
- tx.release();
134
- }
135
- });
136
- }
137
- getCurrentVersion() {
138
- return __awaiter(this, void 0, void 0, function* () {
139
- var _a;
140
- const result = yield this
141
- .dbClient `SELECT version FROM migrations ORDER BY version DESC LIMIT 1`;
142
- return result.length > 0 ? (_a = result[0]) === null || _a === void 0 ? void 0 : _a['version'] : 0;
143
- });
144
- }
145
- getCurrentVersionWithTx(tx) {
146
- return __awaiter(this, void 0, void 0, function* () {
147
- var _a;
148
- const result = yield tx `SELECT version FROM migrations ORDER BY version DESC LIMIT 1`;
149
- return result.length > 0 ? (_a = result[0]) === null || _a === void 0 ? void 0 : _a['version'] : 0;
150
- });
151
- }
152
- acquireLock(tx) {
153
- return __awaiter(this, void 0, void 0, function* () {
154
- yield tx `SELECT pg_advisory_lock(21421431414441411)`;
155
- });
156
- }
157
- releaseLock(tx) {
158
- return __awaiter(this, void 0, void 0, function* () {
159
- yield tx `SELECT pg_advisory_unlock(21421431414441411)`;
160
- });
161
- }
162
- begin(tx) {
163
- return __awaiter(this, void 0, void 0, function* () {
164
- yield tx `BEGIN`;
165
- });
166
- }
167
- commit(tx) {
168
- return __awaiter(this, void 0, void 0, function* () {
169
- yield tx `COMMIT`;
170
- });
171
- }
172
- rollback(tx) {
173
- return __awaiter(this, void 0, void 0, function* () {
174
- yield tx `ROLLBACK`;
175
- });
176
- }
177
- }
@@ -1,4 +0,0 @@
1
- export type Direction = 'up' | 'down';
2
- export declare const getMigrationFiles: (dir: string, direction: Direction) => string[];
3
- export declare const createFolderIfNotExists: (filePath: string) => void;
4
- export declare const createFile: (migrationDir: string, name: string) => string[];
File without changes