@autofleet/sequelize-utils 6.1.57 → 6.2.0-alpha.0

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
@@ -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,4 @@
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";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]
3
+ `),process.exit(1)}}export{};
4
+ //# sourceMappingURL=cli.js.map
@@ -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\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":";sJAKA,MAAM,EAAY,sCAIZ,EAAO,GAAgB,QAAQ,OAAO,MAAM,qBAAqB,EAAI,IAAI,CACzE,EAAQ,GAAgB,QAAQ,OAAO,MAAM,qBAAqB,EAAI,IAAI,CAC1E,EAAS,GAAgB,QAAQ,OAAO,MAAM,qBAAqB,EAAI,IAAI,CAMjF,SAAS,EAAU,EAAsB,CACvC,IAAMA,EAAe,EAAE,CACvB,IAAK,IAAI,EAAI,EAAG,EAAI,EAAK,OAAQ,IAC3B,EAAK,GAAG,WAAW,KAAK,EAAI,EAAI,EAAI,EAAK,QAAU,CAAC,EAAK,EAAI,GAAG,WAAW,KAAK,GAClF,EAAO,EAAK,GAAG,MAAM,EAAE,EAAI,EAAK,EAAI,GACpC,KAGJ,OAAO,EAKT,SAAS,EAAI,EAAY,EAAiB,EAA2C,CACnF,OAAO,QAAQ,IAAI,EAAK,IAAY,GAQtC,eAAe,EAAU,EAA2B,CAClD,IAAM,EAAU,OAAO,EAAI,EAAM,eAAgB,qBAAqB,CAAC,EAAI,WAErE,EAAS,IAAI,EAAG,OAAO,CAC3B,KAAM,EAAI,EAAM,cAAe,UAAU,EAAI,YAC7C,KAAM,OAAO,EAAI,EAAM,cAAe,UAAU,CAAC,EAAI,KACrD,SAAU,EAAI,EAAM,cAAe,UAAU,CAC7C,KAAM,EAAI,EAAM,kBAAmB,cAAc,CACjD,SAAU,EAAI,EAAM,kBAAmB,cAAc,CACtD,CAAC,CAEF,MAAM,EAAO,SAAS,CAGtB,MAAM,EAAO,MAAM,2BAA2B,EAAQ,GAAG,CAGzD,QAAQ,OAAO,SAAS,CAExB,IAAM,MAAgB,CACf,EAAO,MAAM,6BAA6B,EAAQ,GAAG,CACvD,SAAW,EAAO,KAAK,CAAC,CACxB,YAAc,QAAQ,KAAK,EAAE,CAAC,EAGnC,QAAQ,KAAK,UAAW,EAAQ,CAChC,QAAQ,KAAK,SAAU,EAAQ,CAKjC,eAAe,EAAe,EAA2B,CACvD,IAAM,EAAgB,OAAO,EAAI,EAAM,mBAAoB,4BAA4B,CAAC,EAAI,IAGtF,EAAgB,OAAO,QAAQ,EAAK,CAAC,SAAS,CAAC,EAAG,KAAO,CAAC,KAAK,IAAK,EAAE,CAAC,CAEvE,EAAS,EAAK,QAAQ,KAAK,GAAI,CAAC,WAAY,GAAG,EAAc,CAAE,CACnE,MAAO,CAAC,SAAU,UAAW,UAAW,MAAM,CAC9C,IAAK,QAAQ,IACb,SAAU,QAAQ,SACnB,CAAC,CAEF,MAAM,IAAI,SAAe,EAAS,IAAW,CAC3C,IAAM,EAAU,eAAiB,CAC/B,EAAO,MAAM,CACb,EAAW,MAAM,8CAA8C,EAAc,IAAI,CAAC,EACjF,EAAc,CAEjB,EAAO,KAAK,UAAY,GAAQ,CAC9B,aAAa,EAAQ,CACjB,IAAQ,SACV,GAAS,EAET,EAAO,MAAM,CACb,EAAW,MAAM,sBAAsB,KAAK,UAAU,EAAI,GAAG,CAAC,GAEhE,CAEF,EAAO,KAAK,QAAU,GAAQ,CAC5B,aAAa,EAAQ,CACrB,EAAO,EAAI,EACX,EACF,CAEF,EAAc,EAAW,OAAO,EAAO,IAAI,CAAC,CAC5C,EAAO,OAAO,CAEd,EAAI,gCAAgC,EAAO,IAAI,GAAG,CAKpD,SAAS,GAAyB,CAChC,GAAI,CAAC,EAAW,EAAU,CAAE,CAC1B,EAAK,0CAA0C,CAC/C,OAGF,IAAM,EAAM,OAAO,EAAa,EAAW,OAAO,CAAC,MAAM,CAAC,CAE1D,GAAI,CACF,QAAQ,KAAK,EAAK,UAAU,MACtB,CACN,EAAK,cAAc,EAAI,sCAAsC,CAG/D,EAAW,EAAU,CACrB,EAAI,0BAA0B,CAKhC,GAAI,QAAQ,KAAK,SAAS,WAAW,CAAE,CACrC,IAAM,EAAe,QAAQ,KAAK,QAAQ,WAAW,CAErD,EADa,EAAU,QAAQ,KAAK,MAAM,EAAe,EAAE,CAAC,CAC7C,CAAC,MAAO,GAAe,CACpC,EAAM,uBAAuB,EAAI,UAAU,CAC3C,QAAQ,KAAK,EAAE,EACf,KACG,CACL,GAAM,GAAI,EAAS,GAAG,GAAQ,QAAQ,KAChC,EAAO,EAAU,EAAK,CAE5B,OAAQ,EAAR,CACE,IAAK,kBACH,EAAe,EAAK,CAAC,MAAO,GAAe,CACzC,EAAM,EAAI,QAAQ,CAClB,QAAQ,KAAK,EAAE,EACf,CACF,MAEF,IAAK,oBACH,GAAkB,CAClB,MAEF,QACE,EAAM,oBAAoB,IAAU,CACpC,QAAQ,OAAO,MAAM;EAA2K,CAChM,QAAQ,KAAK,EAAE"}
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@autofleet/sequelize-utils",
3
- "version": "6.1.57",
3
+ "version": "6.2.0-alpha.0",
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.5.1",
54
- "@autofleet/network": "^1.12.5"
58
+ "@autofleet/network": "^1.11.2",
59
+ "@autofleet/logger": "^4.2.51"
55
60
  },
56
61
  "scripts": {
57
62
  "start": "ts-node src/index.ts",