pgroll 0.0.8 → 0.0.10

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,204 @@
1
- #
2
-
3
1
  # pgroll
4
2
 
5
- `pgroll` is a thread-safe, 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
+ | `-s, --schema <schema>` | Schema for pgroll's internal migrations table (default: `public`). |
102
+ | `-V, --version` | Print the `pgroll` version. |
103
+ | `-h, --help` | Show help. |
104
+
105
+ ### Commands
106
+
107
+ **Apply all pending migrations:**
39
108
 
40
109
  ```bash
41
110
  npx pgroll up
42
111
  ```
43
112
 
44
- 2. Run Migrations Down:
113
+ **Roll back every applied migration:**
45
114
 
46
115
  ```bash
47
116
  npx pgroll down
48
117
  ```
49
118
 
50
- 3. Navigate to a Specific Version:
119
+ **Migrate to a specific version** (moves up or down as needed; `0` rolls everything back):
51
120
 
52
121
  ```bash
53
122
  npx pgroll go <version>
54
123
  ```
55
124
 
56
- 4. Create New Migration Files:
125
+ **Create a new migration pair** (writes `<timestamp>_<name>_up.sql` and `..._down.sql` with
126
+ placeholder contents for you to fill in):
57
127
 
58
128
  ```bash
59
- npx pgroll create <migration-name>
129
+ npx pgroll create <name>
130
+ ```
131
+
132
+ A typical workflow:
133
+
134
+ ```bash
135
+ npx pgroll create add_users_table # creates the up/down file pair
136
+ # …edit the generated *_up.sql / *_down.sql files…
137
+ npx pgroll up # apply it
138
+ ```
139
+
140
+ ## Programmatic API
141
+
142
+ `pgroll` can also be used as a library. Pass it a `postgres` client instance and drive migrations
143
+ directly:
144
+
145
+ ```ts
146
+ import postgres from 'postgres';
147
+ import { Migrator } from 'pgroll';
148
+
149
+ const sql = postgres('postgres://user:password@localhost:5432/mydb');
150
+ const migrator = new Migrator(sql, './migrations');
151
+
152
+ // Apply all pending migrations
153
+ await migrator.up();
154
+
155
+ console.log('Current version:', await migrator.getCurrentVersion());
156
+
157
+ // Migrate to a specific version, logging progress as it goes
158
+ await migrator.go(0, { eventHandler: info => console.log(info) });
159
+
160
+ await sql.end();
60
161
  ```
162
+
163
+ ### `new Migrator(dbClient, migrationsDir?, schema?)`
164
+
165
+ | Parameter | Type | Description |
166
+ | --------------- | ------------------- | -------------------------------------------------------------------- |
167
+ | `dbClient` | `Sql` (PostgresJS) | A `postgres` client instance. |
168
+ | `migrationsDir` | `string` (optional) | Directory of migration files. Defaults to `<cwd>/migrations`. |
169
+ | `schema` | `string` (optional) | Schema for pgroll's internal migrations table. Defaults to `public`. |
170
+
171
+ ### Methods
172
+
173
+ | Method | Description |
174
+ | --------------------- | ----------------------------------------------------------------------- |
175
+ | `up(opts?)` | Apply all pending up migrations. |
176
+ | `down(opts?)` | Roll back all applied migrations (to version `0`). |
177
+ | `go(version, opts?)` | Migrate forward or backward to `version` (`0` reverts everything). |
178
+ | `getCurrentVersion()` | Resolve to the highest applied version (`0` if none have been applied). |
179
+
180
+ `opts` is `{ eventHandler: (info: string) => void }` — an optional callback invoked with a
181
+ human-readable message as each migration is applied.
182
+
183
+ ## How it works
184
+
185
+ On the first run, `pgroll` creates a bookkeeping table in the configured schema (default
186
+ `public`, override with `-s, --schema`). All references to it are schema-qualified, so it is
187
+ unaffected by any `search_path` a migration sets:
188
+
189
+ ```sql
190
+ CREATE TABLE IF NOT EXISTS <schema>.migrations (
191
+ name varchar(500) PRIMARY KEY,
192
+ version smallint NOT NULL,
193
+ applied_at timestamp DEFAULT CURRENT_TIMESTAMP
194
+ );
195
+ ```
196
+
197
+ Each `up`/`down`/`go` run reserves a single connection, takes a session-level
198
+ `pg_advisory_lock`, and applies the relevant migration files within a transaction — recording or
199
+ removing the corresponding rows in `migrations` as it goes. The advisory lock serializes concurrent
200
+ runs across processes, and the surrounding transaction means a failed migration rolls back cleanly.
201
+
202
+ ## License
203
+
204
+ [ISC](./LICENSE)
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -1,28 +1,33 @@
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
- .option('-d, --migrationDir <filepath>', 'Specify migration directory(Default: ./migrations)')
13
+ .option('-d, --migrationDir <filepath>', 'Specify migration directory (Default: ./migrations)')
14
+ .option('-u, --url <url>', 'PostgreSQL connection URL (overrides PG* env vars)')
15
+ .option('-s, --schema <schema>', 'Specify schema (Default: public)')
13
16
  .hook('preAction', cmd => {
14
17
  const opts = cmd.opts();
15
- migrator = new Migrator(postgres({
18
+ const pgOptions = {
16
19
  onnotice: () => {
20
+ // do nothing
17
21
  }
18
- }), opts.migrationDir);
22
+ };
23
+ migrator = new Migrator(opts.url ? postgres(opts.url, pgOptions) : postgres(pgOptions), opts.migrationDir, opts.schema);
19
24
  });
20
25
  program
21
26
  .command('up')
22
27
  .description('Run all up migrations')
23
- .action(() => __awaiter(void 0, void 0, void 0, function* () {
28
+ .action(async () => {
24
29
  try {
25
- yield migrator.up({ eventHandler: console.log });
30
+ await migrator.up({ eventHandler: console.log });
26
31
  console.log('Migrations up completed successfully.');
27
32
  process.exit(0);
28
33
  }
@@ -30,13 +35,13 @@ program
30
35
  console.error('Error during migrations up:', error);
31
36
  process.exit(1);
32
37
  }
33
- }));
38
+ });
34
39
  program
35
40
  .command('down')
36
41
  .description('Run all down migrations')
37
- .action(() => __awaiter(void 0, void 0, void 0, function* () {
42
+ .action(async () => {
38
43
  try {
39
- yield migrator.down({ eventHandler: console.log });
44
+ await migrator.down({ eventHandler: console.log });
40
45
  console.log('Migrations down completed successfully.');
41
46
  process.exit(0);
42
47
  }
@@ -44,7 +49,7 @@ program
44
49
  console.error('Error during migrations down:', error);
45
50
  process.exit(1);
46
51
  }
47
- }));
52
+ });
48
53
  program
49
54
  .command('create')
50
55
  .description('Generate migration files in the migrations folder: one for applying changes (up), and one for reverting them (down).')
@@ -59,19 +64,19 @@ program
59
64
  .command('go')
60
65
  .description('Navigate to a specific version; version 0 performs a rollback, reverting all migrations.')
61
66
  .argument('<version>', 'version to migrate to')
62
- .action((version) => __awaiter(void 0, void 0, void 0, function* () {
63
- const parsedVersion = Number.parseInt(version, 10);
64
- if (Number.isNaN(parsedVersion)) {
67
+ .action(async (version) => {
68
+ const parsedVersion = Number(version);
69
+ if (!Number.isSafeInteger(parsedVersion) || parsedVersion < 0) {
65
70
  console.error('Invalid version number.');
66
71
  process.exit(1);
67
72
  }
68
73
  try {
69
- yield migrator.go(parsedVersion, { eventHandler: console.log });
74
+ await migrator.go(parsedVersion, { eventHandler: console.log });
70
75
  process.exit(0);
71
76
  }
72
77
  catch (error) {
73
78
  console.error('Error during migrations:', error);
74
79
  process.exit(1);
75
80
  }
76
- }));
81
+ });
77
82
  program.parse(process.argv);
@@ -1,10 +1,11 @@
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
  }
6
6
  export interface IMigrator {
7
7
  migrationsDir: string;
8
+ schema: string;
8
9
  up: (opts?: Option) => Promise<void>;
9
10
  down: (opts?: Option) => Promise<void>;
10
11
  go: (version: number, opts?: Option) => Promise<void>;
@@ -13,7 +14,8 @@ export interface IMigrator {
13
14
  export declare class Migrator implements IMigrator {
14
15
  private readonly dbClient;
15
16
  readonly migrationsDir: string;
16
- constructor(dbClient: Sql, migrationsDir?: string);
17
+ readonly schema: string;
18
+ constructor(dbClient: Sql, migrationsDir?: string, schema?: string);
17
19
  ensureMigrationTable(tx: ReservedSql): Promise<void>;
18
20
  up(): Promise<void>;
19
21
  down(): Promise<void>;
@@ -0,0 +1,138 @@
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
+ schema;
8
+ constructor(dbClient, migrationsDir = '', schema = 'public') {
9
+ this.dbClient = dbClient;
10
+ this.migrationsDir = migrationsDir || `${process.cwd()}/migrations`;
11
+ this.schema = schema;
12
+ }
13
+ async ensureMigrationTable(tx) {
14
+ await tx `CREATE SCHEMA IF NOT EXISTS ${tx(this.schema)}`;
15
+ await tx `CREATE TABLE IF NOT EXISTS ${tx(this.schema)}.migrations(
16
+ name varchar(500) PRIMARY KEY,
17
+ version smallint NOT NULL,
18
+ applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);`;
19
+ }
20
+ async up() {
21
+ return this.migrate('up');
22
+ }
23
+ async down() {
24
+ return this.migrate('down');
25
+ }
26
+ async go(version, opts) {
27
+ const tx = await this.dbClient.reserve();
28
+ try {
29
+ await this.acquireLock(tx);
30
+ await this.begin(tx);
31
+ await this.ensureMigrationTable(tx);
32
+ const currentVersion = await this.getCurrentVersionWithTx(tx);
33
+ if (currentVersion === version) {
34
+ opts?.eventHandler(`Already at version ${version}`);
35
+ await this.commit(tx);
36
+ return;
37
+ }
38
+ const direction = version > currentVersion ? 'up' : 'down';
39
+ const fileNames = getMigrationFiles(this.migrationsDir, direction);
40
+ if (direction === 'up') {
41
+ const fileVersion = Math.min(fileNames.length, version);
42
+ for (let i = currentVersion; i < fileVersion; i++) {
43
+ const file = fileNames[i] ?? '';
44
+ await tx.file(path.join(this.migrationsDir, file)).execute();
45
+ await tx `INSERT INTO ${tx(this.schema)}.migrations(name, version) VALUES (${file}, ${i} + 1)`;
46
+ opts?.eventHandler(`Successfully migrated: ${file}`);
47
+ }
48
+ if (version > fileNames.length) {
49
+ opts?.eventHandler(`Currently at latest version: ${currentVersion}`);
50
+ }
51
+ }
52
+ else {
53
+ // get index of the current file
54
+ const start = fileNames.length - currentVersion;
55
+ // current index + number of file to down
56
+ const end = start + (currentVersion - version);
57
+ for (let i = start; i < end; i++) {
58
+ const file = fileNames[i] ?? '';
59
+ await tx.file(path.join(this.migrationsDir, file)).execute();
60
+ await tx `DELETE FROM ${tx(this.schema)}.migrations WHERE version = ${fileNames.length - i}`;
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 tx.file(path.join(this.migrationsDir, fileName)).execute();
88
+ await tx `INSERT INTO ${tx(this.schema)}.migrations(name, version) VALUES (${fileName}, ${id} + 1)`;
89
+ opts?.eventHandler(`Successfully migrated: ${fileName}`);
90
+ }
91
+ }
92
+ }
93
+ else {
94
+ // calculate start to know where the down migration starts
95
+ const start = fileNames.length - currentVersion;
96
+ for (let i = start; i < fileNames.length; i++) {
97
+ const file = fileNames[i] ?? '';
98
+ await tx.file(path.join(this.migrationsDir, file)).execute();
99
+ await tx `DELETE FROM ${tx(this.schema)}.migrations WHERE version = ${fileNames.length - i}`;
100
+ opts?.eventHandler(`Successfully migrated: ${file}`);
101
+ }
102
+ }
103
+ await this.commit(tx);
104
+ }
105
+ catch (error) {
106
+ await this.rollback(tx);
107
+ throw error;
108
+ }
109
+ finally {
110
+ await this.releaseLock(tx);
111
+ tx.release();
112
+ }
113
+ }
114
+ async getCurrentVersion() {
115
+ const result = await this
116
+ .dbClient `SELECT version FROM ${this.dbClient(this.schema)}.migrations ORDER BY version DESC LIMIT 1`;
117
+ return result.length > 0 ? result[0]?.['version'] : 0;
118
+ }
119
+ async getCurrentVersionWithTx(tx) {
120
+ const result = await tx `SELECT version FROM ${tx(this.schema)}.migrations ORDER BY version DESC LIMIT 1`;
121
+ return result.length > 0 ? result[0]?.['version'] : 0;
122
+ }
123
+ async acquireLock(tx) {
124
+ await tx `SELECT pg_advisory_lock(21421431414441411)`;
125
+ }
126
+ async releaseLock(tx) {
127
+ await tx `SELECT pg_advisory_unlock(21421431414441411)`;
128
+ }
129
+ async begin(tx) {
130
+ await tx `BEGIN`;
131
+ }
132
+ async commit(tx) {
133
+ await tx `COMMIT`;
134
+ }
135
+ async rollback(tx) {
136
+ await tx `ROLLBACK`;
137
+ }
138
+ }
@@ -1,10 +1,8 @@
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') {
4
+ const files = fs.readdirSync(dir).filter(file => file.endsWith(`_${direction}.sql`));
5
+ if (direction === 'up') {
8
6
  return files.toSorted((a, b) => a.localeCompare(b));
9
7
  }
10
8
  return files.toSorted((a, b) => b.localeCompare(a));
@@ -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.8",
3
+ "version": "0.0.10",
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
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",
@@ -26,48 +31,43 @@
26
31
  ],
27
32
  "scripts": {
28
33
  "preinstall": "npx only-allow pnpm",
29
- "dev": "tsx ./src/cli.ts",
30
- "build-cjs": "tsc -p tsconfig.cjs.json",
31
- "build-esm": "tsc -p tsconfig.esm.json",
32
- "build": "rm -rf dist && pnpm build-cjs && pnpm build-esm",
33
- "start": "node ./dist/index.js",
34
+ "dev": "node --watch ./src/cli.ts",
35
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
36
+ "start": "node ./dist/src/cli.js",
34
37
  "format": "prettier --write .",
35
38
  "format:check": "prettier --check .",
36
39
  "lint": "eslint . --fix",
37
40
  "lint:check": "eslint .",
38
- "test": "jest",
39
- "pp": "pnpm build && node update-version && npm publish --access=public"
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"
40
45
  },
41
46
  "packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316",
42
47
  "engines": {
43
- "node": ">=v24.13.0"
48
+ "node": ">=v24.16.0"
44
49
  },
45
50
  "license": "ISC",
46
51
  "devDependencies": {
47
- "@eslint/compat": "^2.0.1",
48
- "@eslint/eslintrc": "^3.3.3",
49
- "@eslint/js": "^9.39.2",
50
- "@types/jest": "^30.0.0",
51
- "@types/node": "^25.0.9",
52
- "@typescript-eslint/eslint-plugin": "^8.53.1",
53
- "eslint": "^9.39.2",
52
+ "@eslint/js": "^10.0.1",
53
+ "@types/node": "^26.0.1",
54
+ "@vitest/coverage-v8": "^4.1.9",
55
+ "@vitest/eslint-plugin": "^1.6.20",
56
+ "eslint": "^10.5.0",
54
57
  "eslint-config-prettier": "^10.1.8",
55
- "eslint-import-resolver-typescript": "^4.4.4",
56
- "eslint-plugin-import": "^2.32.0",
57
- "eslint-plugin-jest": "^29.12.1",
58
- "eslint-plugin-promise": "^7.2.1",
59
- "eslint-plugin-sonarjs": "^3.0.5",
60
- "eslint-plugin-unicorn": "^62.0.0",
61
- "jest": "^30.2.0",
62
- "prettier": "^3.8.0",
63
- "ts-jest": "^29.4.6",
64
- "tsx": "^4.21.0",
65
- "typescript": "^5.9.3",
66
- "typescript-eslint": "^8.53.1"
58
+ "eslint-import-resolver-typescript": "^4.4.5",
59
+ "eslint-plugin-import-x": "^4.17.0",
60
+ "eslint-plugin-promise": "^7.3.0",
61
+ "eslint-plugin-sonarjs": "^4.1.0",
62
+ "eslint-plugin-unicorn": "^69.0.0",
63
+ "globals": "^17.7.0",
64
+ "prettier": "^3.8.4",
65
+ "typescript": "^6.0.3",
66
+ "typescript-eslint": "^8.62.0",
67
+ "vitest": "^4.1.9"
67
68
  },
68
69
  "dependencies": {
69
- "commander": "^14.0.2",
70
- "postgres": "^3.4.8",
71
- "tslib": "^2.8.1"
70
+ "commander": "^15.0.0",
71
+ "postgres": "^3.4.9"
72
72
  }
73
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.toSorted((a, b) => a.localeCompare(b));
13
- }
14
- return files.toSorted((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