@autofleet/sequelize-utils 6.1.57 → 6.2.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/dist/cli.js +5 -0
- package/dist/cli.js.map +1 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -84,6 +84,53 @@ const { registerModelEventHooks } = sequelizeUtilsInit(sequelize, logger);
|
|
|
84
84
|
registerModelEventHooks(modelEventsTableMapping, events);
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
## Migration locking CLI
|
|
88
|
+
|
|
89
|
+
Prevents multiple pods from running `sequelize db:migrate` simultaneously during deployment.
|
|
90
|
+
|
|
91
|
+
Each service's `init.sh` wraps the migrate command with `lock-migrations` / `unlock-migrations`. The first pod to run acquires a PostgreSQL advisory lock and runs migrations. Every other pod blocks on `lock-migrations` until the first pod calls `unlock-migrations`, at which point they proceed — but `sequelize db:migrate` is a no-op because `SequelizeMeta` already has all migrations recorded.
|
|
92
|
+
|
|
93
|
+
### Usage
|
|
94
|
+
|
|
95
|
+
Update `init.sh` in the service:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
#!/bin/bash
|
|
99
|
+
set -e
|
|
100
|
+
|
|
101
|
+
node_modules/.bin/sequelize db:create || echo 'Failed creating database, possibly since it already exists.'
|
|
102
|
+
|
|
103
|
+
npx sequelize-utils lock-migrations --db-name-env DB_NAME --db-username-env DB_USERNAME --db-password-env DB_PASSWORD
|
|
104
|
+
node --run migrate
|
|
105
|
+
npx sequelize-utils unlock-migrations
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The `--*-env` flags tell the CLI which environment variable to read for each connection parameter. This lets services use their own env var naming conventions without wrapper scripts.
|
|
109
|
+
|
|
110
|
+
### What happens with multiple pods
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
Pod 1 → lock-migrations (acquires pg advisory lock) → migrate (runs DDL) → unlock-migrations (releases lock)
|
|
114
|
+
Pod 2 → lock-migrations (blocks) ──────────────────────────────────────→ (unblocks) → migrate (no-op, already applied) → unlock-migrations
|
|
115
|
+
Pod 3 → lock-migrations (blocks) ──────────────────────────────────────→ (unblocks) → migrate (no-op, already applied) → unlock-migrations
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
If a pod crashes between `lock-migrations` and `unlock-migrations`, PostgreSQL automatically releases the advisory lock when the daemon connection closes — no manual cleanup needed.
|
|
119
|
+
|
|
120
|
+
### CLI flags
|
|
121
|
+
|
|
122
|
+
Each flag specifies the **name** of the environment variable to read, not the value itself.
|
|
123
|
+
|
|
124
|
+
| Flag | Default env var | Description |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| `--db-host-env` | `DB_HOST` | Database host (`localhost` if unset) |
|
|
127
|
+
| `--db-port-env` | `DB_PORT` | Database port (`5432` if unset) |
|
|
128
|
+
| `--db-name-env` | `DB_NAME` | Database name |
|
|
129
|
+
| `--db-username-env` | `DB_USERNAME` | Database user |
|
|
130
|
+
| `--db-password-env` | `DB_PASSWORD` | Database password |
|
|
131
|
+
| `--lock-key-env` | `MIGRATION_LOCK_KEY` | Advisory lock key — must match across all pods of the same service (default: `1234567890`) |
|
|
132
|
+
| `--lock-timeout-env` | `MIGRATION_LOCK_TIMEOUT_MS` | How long to wait for the lock before failing (default: `60000` ms) |
|
|
133
|
+
|
|
87
134
|
## Reference
|
|
88
135
|
|
|
89
136
|
Read the `@autofleet/events` v5 changes here: README: [link](https://github.com/Autofleet/autorepo/blob/60a175c70ebdba217612c2ee58be8f31881d6540/packages/events/README.md)
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{fork as e}from"node:child_process";import{existsSync as t,readFileSync as n,unlinkSync as r,writeFileSync as i}from"node:fs";import a from"pg";try{let e=n(`.env`,`utf8`).split(`
|
|
3
|
+
`);for(let t of e){let e=/^([^#=\s][^=]*)=(.*)$/.exec(t);if(e){let t=e[1].trim(),n=e[2].trim().replace(/^["']|["']$/g,``);t in process.env||(process.env[t]=n)}}}catch{}const o=`/tmp/__sequelize_migration_lock.pid`,s=e=>process.stdout.write(`[sequelize-utils] ${e}\n`),c=e=>process.stderr.write(`[sequelize-utils] ${e}\n`),l=e=>process.stderr.write(`[sequelize-utils] ${e}\n`);function u(e){let t={};for(let n=0;n<e.length;n++)e[n].startsWith(`--`)&&n+1<e.length&&!e[n+1].startsWith(`--`)&&(t[e[n].slice(2)]=e[n+1],n++);return t}function d(e,t,n){return process.env[e[t]??n]}async function f(e){let t=Number(d(e,`lock-key-env`,`MIGRATION_LOCK_KEY`))||1234567890,n=new a.Client({host:d(e,`db-host-env`,`DB_HOST`)??`localhost`,port:Number(d(e,`db-port-env`,`DB_PORT`))||5432,database:d(e,`db-name-env`,`DB_NAME`),user:d(e,`db-username-env`,`DB_USERNAME`),password:d(e,`db-password-env`,`DB_PASSWORD`)});await n.connect(),await n.query(`SELECT pg_advisory_lock(${t})`),process.send?.(`locked`);let r=()=>{n.query(`SELECT pg_advisory_unlock(${t})`).then(()=>n.end()).finally(()=>process.exit(0))};process.once(`SIGTERM`,r),process.once(`SIGINT`,r)}async function p(t){let n=Number(d(t,`lock-timeout-env`,`MIGRATION_LOCK_TIMEOUT_MS`))||6e4,r=Object.entries(t).flatMap(([e,t])=>[`--${e}`,t]),a=e(process.argv[1],[`--daemon`,...r],{stdio:[`ignore`,`inherit`,`inherit`,`ipc`],env:process.env,execArgv:process.execArgv});await new Promise((e,t)=>{let r=setTimeout(()=>{a.kill(),t(Error(`Timed out waiting for migration lock after ${n}ms`))},n);a.once(`message`,n=>{clearTimeout(r),n===`locked`?e():(a.kill(),t(Error(`Lock daemon error: ${JSON.stringify(n)}`)))}),a.once(`error`,e=>{clearTimeout(r),t(e)})}),i(o,String(a.pid)),a.unref(),s(`Migration lock acquired (pid=${a.pid})`)}function m(){if(!t(o)){c(`No lock file found — nothing to release`);return}let e=Number(n(o,`utf8`).trim());try{process.kill(e,`SIGTERM`)}catch{c(`Daemon pid=${e} not found (may have already exited)`)}r(o),s(`Migration lock released`)}if(process.argv.includes(`--daemon`)){let e=process.argv.indexOf(`--daemon`);f(u(process.argv.slice(e+1))).catch(e=>{l(`Lock daemon failed: ${e.message}`),process.exit(1)})}else{let[,,e,...t]=process.argv,n=u(t);switch(e){case`lock-migrations`:p(n).catch(e=>{l(e.message),process.exit(1)});break;case`unlock-migrations`:m();break;default:l(`Unknown command: ${e}`),process.stderr.write(`Usage: sequelize-utils <lock-migrations|unlock-migrations> [--db-host-env VAR] [--db-port-env VAR] [--db-name-env VAR] [--db-username-env VAR] [--db-password-env VAR]
|
|
4
|
+
`),process.exit(1)}}export{};
|
|
5
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","names":["result: Args"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { fork } from 'node:child_process';\nimport { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport pg from 'pg';\n\n// Auto-load .env from the current working directory (same behaviour as dotenv).\n// Only sets variables that are not already present in process.env.\ntry {\n const lines = readFileSync('.env', 'utf8').split('\\n');\n for (const line of lines) {\n const match = /^([^#=\\s][^=]*)=(.*)$/.exec(line);\n if (match) {\n const key = match[1].trim();\n const value = match[2].trim().replace(/^[\"']|[\"']$/g, '');\n if (!(key in process.env)) {\n process.env[key] = value;\n }\n }\n }\n} catch {\n // no .env file — that's fine\n}\n\nconst LOCK_FILE = '/tmp/__sequelize_migration_lock.pid';\nconst DEFAULT_LOCK_KEY = 1_234_567_890;\nconst DEFAULT_LOCK_TIMEOUT_MS = 60_000;\n\nconst log = (msg: string) => process.stdout.write(`[sequelize-utils] ${msg}\\n`);\nconst warn = (msg: string) => process.stderr.write(`[sequelize-utils] ${msg}\\n`);\nconst fatal = (msg: string) => process.stderr.write(`[sequelize-utils] ${msg}\\n`);\n\n// ─── Arg parsing ──────────────────────────────────────────────────────────────\n\ntype Args = Record<string, string>;\n\nfunction parseArgs(argv: string[]): Args {\n const result: Args = {};\n for (let i = 0; i < argv.length; i++) {\n if (argv[i].startsWith('--') && i + 1 < argv.length && !argv[i + 1].startsWith('--')) {\n result[argv[i].slice(2)] = argv[i + 1];\n i++;\n }\n }\n return result;\n}\n\n// Resolves the value of an env var whose name may be overridden by a CLI arg.\n// e.g. --db-name-env MY_SERVICE_DB_NAME → process.env['MY_SERVICE_DB_NAME']\nfunction env(args: Args, argName: string, defaultEnvVar: string): string | undefined {\n return process.env[args[argName] ?? defaultEnvVar];\n}\n\n// ─── Daemon ───────────────────────────────────────────────────────────────────\n// Spawned as a detached subprocess by `lock-migrations`.\n// Holds a PostgreSQL session-level advisory lock for as long as it lives.\n// When killed by `unlock-migrations`, the connection closes and pg auto-releases the lock.\n\nasync function runDaemon(args: Args): Promise<void> {\n const lockKey = Number(env(args, 'lock-key-env', 'MIGRATION_LOCK_KEY')) || DEFAULT_LOCK_KEY;\n\n const client = new pg.Client({\n host: env(args, 'db-host-env', 'DB_HOST') ?? 'localhost',\n port: Number(env(args, 'db-port-env', 'DB_PORT')) || 5432,\n database: env(args, 'db-name-env', 'DB_NAME'),\n user: env(args, 'db-username-env', 'DB_USERNAME'),\n password: env(args, 'db-password-env', 'DB_PASSWORD'),\n });\n\n await client.connect();\n\n // Blocking — waits until no other pod holds the lock\n await client.query(`SELECT pg_advisory_lock(${lockKey})`);\n\n // Signal parent that the lock is held\n process.send?.('locked');\n\n const release = () => {\n void client.query(`SELECT pg_advisory_unlock(${lockKey})`)\n .then(() => client.end())\n .finally(() => process.exit(0));\n };\n\n process.once('SIGTERM', release);\n process.once('SIGINT', release);\n}\n\n// ─── lock-migrations ──────────────────────────────────────────────────────────\n\nasync function lockMigrations(args: Args): Promise<void> {\n const lockTimeoutMs = Number(env(args, 'lock-timeout-env', 'MIGRATION_LOCK_TIMEOUT_MS')) || DEFAULT_LOCK_TIMEOUT_MS;\n\n // Forward all --*-env args to the daemon so it resolves the same env vars.\n const forwardedArgs = Object.entries(args).flatMap(([k, v]) => [`--${k}`, v]);\n\n const daemon = fork(process.argv[1], ['--daemon', ...forwardedArgs], {\n stdio: ['ignore', 'inherit', 'inherit', 'ipc'],\n env: process.env,\n execArgv: process.execArgv,\n });\n\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(() => {\n daemon.kill();\n reject(new Error(`Timed out waiting for migration lock after ${lockTimeoutMs}ms`));\n }, lockTimeoutMs);\n\n daemon.once('message', (msg) => {\n clearTimeout(timeout);\n if (msg === 'locked') {\n resolve();\n } else {\n daemon.kill();\n reject(new Error(`Lock daemon error: ${JSON.stringify(msg)}`));\n }\n });\n\n daemon.once('error', (err) => {\n clearTimeout(timeout);\n reject(err);\n });\n });\n\n writeFileSync(LOCK_FILE, String(daemon.pid));\n daemon.unref();\n\n log(`Migration lock acquired (pid=${daemon.pid})`);\n}\n\n// ─── unlock-migrations ────────────────────────────────────────────────────────\n\nfunction unlockMigrations(): void {\n if (!existsSync(LOCK_FILE)) {\n warn('No lock file found — nothing to release');\n return;\n }\n\n const pid = Number(readFileSync(LOCK_FILE, 'utf8').trim());\n\n try {\n process.kill(pid, 'SIGTERM');\n } catch {\n warn(`Daemon pid=${pid} not found (may have already exited)`);\n }\n\n unlinkSync(LOCK_FILE);\n log('Migration lock released');\n}\n\n// ─── Entry point ──────────────────────────────────────────────────────────────\n\nif (process.argv.includes('--daemon')) {\n const daemonArgIdx = process.argv.indexOf('--daemon');\n const args = parseArgs(process.argv.slice(daemonArgIdx + 1));\n runDaemon(args).catch((err: Error) => {\n fatal(`Lock daemon failed: ${err.message}`);\n process.exit(1);\n });\n} else {\n const [,, command, ...rest] = process.argv;\n const args = parseArgs(rest);\n\n switch (command) {\n case 'lock-migrations':\n lockMigrations(args).catch((err: Error) => {\n fatal(err.message);\n process.exit(1);\n });\n break;\n\n case 'unlock-migrations':\n unlockMigrations();\n break;\n\n default:\n fatal(`Unknown command: ${command}`);\n process.stderr.write('Usage: sequelize-utils <lock-migrations|unlock-migrations> [--db-host-env VAR] [--db-port-env VAR] [--db-name-env VAR] [--db-username-env VAR] [--db-password-env VAR]\\n');\n process.exit(1);\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autofleet/sequelize-utils",
|
|
3
|
-
"version": "6.1
|
|
3
|
+
"version": "6.2.0-alpha.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sequelize-utils": "./dist/cli.js"
|
|
8
|
+
},
|
|
6
9
|
"main": "dist/index.js",
|
|
7
10
|
"types": "dist/index.d.ts",
|
|
8
11
|
"exports": {
|
|
@@ -39,19 +42,21 @@
|
|
|
39
42
|
"peerDependencies": {
|
|
40
43
|
"@autofleet/events": ">=5",
|
|
41
44
|
"@autofleet/logger": ">=4",
|
|
45
|
+
"pg": ">=8",
|
|
42
46
|
"sequelize": ">=6"
|
|
43
47
|
},
|
|
44
48
|
"devDependencies": {
|
|
45
49
|
"@types/debug": "^4.1.6",
|
|
46
50
|
"@types/express": "^4.17.13",
|
|
47
51
|
"@types/node": "^22.16.5",
|
|
52
|
+
"@types/pg": "^8.20.0",
|
|
48
53
|
"axios": "^1.15.0",
|
|
49
54
|
"express": "^4.21.2",
|
|
50
55
|
"pg": "^8.16.3",
|
|
51
56
|
"sequelize": "^6.37.7",
|
|
52
57
|
"ts-node": "^10.9.2",
|
|
53
|
-
"@autofleet/logger": "^4.
|
|
54
|
-
"@autofleet/network": "^1.
|
|
58
|
+
"@autofleet/logger": "^4.2.51",
|
|
59
|
+
"@autofleet/network": "^1.11.2"
|
|
55
60
|
},
|
|
56
61
|
"scripts": {
|
|
57
62
|
"start": "ts-node src/index.ts",
|