@autofleet/sequelize-utils 6.2.0-alpha.2 → 6.2.1-beta-416ae768.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,53 +84,6 @@ 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
-
134
87
  ## Reference
135
88
 
136
89
  Read the `@autofleet/events` v5 changes here: README: [link](https://github.com/Autofleet/autorepo/blob/60a175c70ebdba217612c2ee58be8f31881d6540/packages/events/README.md)
package/dist/index.cjs CHANGED
@@ -1,2 +1,56 @@
1
- var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`@autofleet/logger`);c=s(c);let l=require(`sequelize`),u=require(`debug`);u=s(u);const d=(0,c.default)();var f=d;const p=async(e,t)=>{t.transaction?t.transaction.afterCommit(()=>e()):await e()};var m=p;const h=e=>e instanceof Date||Object.prototype.toString.call(e)===`[object Date]`,g=e=>{let t={};for(let[n,r]of Object.entries(e))t[n]=h(r)?r.getTime()/1e3:r;return t},_=(e,t,n,r)=>{let i=r??f;if(!n){i.warn(`Events object must be provided to addModelEventHooks`);return}let a=(e,r=!1)=>{try{let a=e.get();r&&Object.assign(a,{deletedAt:new Date});let o=t[e.constructor.name]?.tableName,s=g(a);if(!o)return;n.sendObject(o,`1`,s,Object.keys(e.rawAttributes)).catch(e=>{i.error(`sendObject error`,{tableName:o,eventVersion:`1`,e})})}catch(e){i.error(`dimTables error`,{e})}};e.addHook(`afterSave`,(e,t)=>m(()=>a(e),t)),e.addHook(`afterDestroy`,(e,t)=>m(()=>a(e,!0),t))};var v=_;const y=(0,u.default)(`sequelize-utils`),b=async(e,t,n=2,r={})=>{if(typeof t!=`function`)throw Error(`funcToRun must be a function`);if(typeof n!=`number`)throw Error(`if defined, retriesCount must be a number`);if(n<0)throw Error(`retriesCount must be a positive number`);try{return await e.transaction(r,async e=>await t(e))}catch(i){if((i instanceof l.DatabaseError||i?.constructor?.name===`DatabaseError`)&&--n>=0)return y(`error inside transactionWithRetry - will retry times ${n}`,i),b(e,t,n,r);throw n===-1&&y(`error inside transactionWithRetry - will stop retry`,i),i}},x=`rollback has been called on this transaction`,S=`Transaction cancelled due to request cancellation`,C=(e,t,n,r)=>{let i=!1;if(t.socket.destroyed)throw y(S),Error(S);return e.transaction(async e=>{let a=async()=>{y(S),!i&&(i=!0,await e.rollback())},o=async()=>{if(!n.writableFinished){if(y(S),i)return;i=!0,await e.rollback()}};t.socket.once(`close`,a),n.once(`close`,o);let s=()=>{t.socket.removeListener(`close`,a),n.removeListener(`close`,o)};try{let t=await r(e);return s(),t}catch(e){throw s(),e.message.includes(`rollback has been called on this transaction`)?Error(S):e}})};var w=(e,t)=>{let n=(t,n,r)=>b(e,t,n,r);return{httpBasedTransaction:(t,n,r)=>C(e,t,n,r),transactionWithRetry:n,registerModelEventHooks:(n,r)=>v(e,n,r,t),runAfterTransactionCommits:m,useOrCreateTransaction:(e,t)=>e?t(e):n(t,0)}};module.exports=w;
1
+ Object.defineProperty(exports,`__esModule`,{value:!0});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`@autofleet/logger`);c=s(c);let l=require(`sequelize`),u=require(`debug`);u=s(u);let d=require(`@autofleet/outbreak`);const f=(0,c.default)();var p=f;const m=async(e,t)=>{t.transaction?t.transaction.afterCommit(()=>e()):await e()};var h=m;const g=e=>e instanceof Date||Object.prototype.toString.call(e)===`[object Date]`,_=e=>{let t={};for(let[n,r]of Object.entries(e))t[n]=g(r)?r.getTime()/1e3:r;return t},v=(e,t,n,r)=>{let i=r??p;if(!n){i.warn(`Events object must be provided to addModelEventHooks`);return}let a=(e,r=!1)=>{try{let a=e.get();r&&Object.assign(a,{deletedAt:new Date});let o=t[e.constructor.name]?.tableName,s=_(a);if(!o)return;n.sendObject(o,`1`,s,Object.keys(e.rawAttributes)).catch(e=>{i.error(`sendObject error`,{tableName:o,eventVersion:`1`,e})})}catch(e){i.error(`dimTables error`,{e})}};e.addHook(`afterSave`,(e,t)=>h(()=>a(e),t)),e.addHook(`afterDestroy`,(e,t)=>h(()=>a(e,!0),t))};var ee=v;const y=(0,u.default)(`sequelize-utils`),b=async(e,t,n=2,r={})=>{if(typeof t!=`function`)throw Error(`funcToRun must be a function`);if(typeof n!=`number`)throw Error(`if defined, retriesCount must be a number`);if(n<0)throw Error(`retriesCount must be a positive number`);try{return await e.transaction(r,async e=>await t(e))}catch(i){if((i instanceof l.DatabaseError||i?.constructor?.name===`DatabaseError`)&&--n>=0)return y(`error inside transactionWithRetry - will retry times ${n}`,i),b(e,t,n,r);throw n===-1&&y(`error inside transactionWithRetry - will stop retry`,i),i}},te=`rollback has been called on this transaction`,x=`Transaction cancelled due to request cancellation`,S=(e,t,n,r)=>{let i=!1;if(t.socket.destroyed)throw y(x),Error(x);return e.transaction(async e=>{let a=async()=>{y(x),!i&&(i=!0,await e.rollback())},o=async()=>{if(!n.writableFinished){if(y(x),i)return;i=!0,await e.rollback()}};t.socket.once(`close`,a),n.once(`close`,o);let s=()=>{t.socket.removeListener(`close`,a),n.removeListener(`close`,o)};try{let t=await r(e);return s(),t}catch(e){throw s(),e.message.includes(`rollback has been called on this transaction`)?Error(x):e}})},C=`x-af-context-paths`,w=`mat_path.paths`,T=`mat_path.path_roots`,E=`mat_path.mode`,D=`mat_path_role`,ne=`mat_path_init`,O=`_mat_path_policy`,re=`USE_MATERIALIZED_PATH`,k={SCOPE:`scope`,INHERIT:`inherit`,BOTH:`both`},A=/^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/,j=new Set([k.SCOPE,k.INHERIT,k.BOTH]),M=e=>{for(let t of e)if(!A.test(t))throw Error(`Invalid materialized path: "${t}". Path must consist of alphanumeric/underscore segments separated by dots.`)},N=e=>{if(!j.has(e))throw Error(`Invalid auth mode: "${e}". Must be '${k.SCOPE}', '${k.INHERIT}', or '${k.BOTH}'.`)},P=e=>`{${e.join(`,`)}}`,F=e=>{let t=new Set;for(let n of e){let e=n.split(`.`)[0];t.add(e)}return Array.from(t)},I=()=>{let e=(0,d.getCurrentContext)()?.context?.get(C);if(!(typeof e!=`string`||!e?.length))return e.split(`,`)},L=async(e,t,n={})=>{let{mode:r=k.SCOPE,transaction:i}=n,a=I();if(!a)return t(i);M(a),N(r);let o=P(a),s=P(F(a)),c=async t=>{await e.query(`
2
+ SELECT
3
+ set_config('role', '${D}', true),
4
+ set_config('${w}', $1, true),
5
+ set_config('${T}', $2, true),
6
+ set_config('${E}', $3, true);
7
+ `,{bind:[o,s,r],transaction:t})};return i?(await c(i),t(i)):e.transaction(async e=>(await c(e),t(e)))},R=e=>{let t=e.getTableName();return typeof t==`string`?{tableName:t,schema:`public`}:{tableName:t.tableName,schema:t.schema??`public`}},z=`current_setting('${w}')::ltree[]`,B=e=>e?`AND ${e} = ANY(current_setting('${T}')::text[])`:``,V=(e,t)=>t===!1?`ALTER TABLE ${e} DISABLE ROW LEVEL SECURITY;`:`ALTER TABLE ${e} ENABLE ROW LEVEL SECURITY;`,H=e=>{let{model:t,identityColumns:n,rlsEnabled:r,clientPathRootColumn:i=``}=e,{tableName:a,schema:o}=R(t),s=`${a}${O}`,c=`${o}.${a}`,l=B(i);return`
8
+ ${V(c,r)}
9
+
10
+ DO $$
11
+ BEGIN
12
+ IF NOT EXISTS (
13
+ SELECT 1 FROM pg_policies
14
+ WHERE schemaname = '${o}'
15
+ AND tablename = '${a}'
16
+ AND policyname = '${s}'
17
+ ) THEN
18
+ CREATE POLICY ${s} ON ${c}
19
+ FOR ALL
20
+ TO ${D}
21
+ USING (
22
+ (${n.map(({column:e,entityType:t})=>`${e} = ANY(
23
+ ARRAY(
24
+ SELECT DISTINCT entity_id
25
+ FROM ${o}.context_scope_path
26
+ WHERE (
27
+ CASE current_setting('${E}')
28
+ WHEN '${k.SCOPE}' THEN path <@ ANY(${z})
29
+ WHEN '${k.INHERIT}' THEN path @> ANY(${z})
30
+ WHEN '${k.BOTH}' THEN (path <@ ANY(${z}) OR path @> ANY(${z}))
31
+ END
32
+ )
33
+ AND entity_type = '${t}'
34
+ AND deleted_at IS NULL
35
+ )
36
+ )`).join(`
37
+ OR `)})
38
+ ${l}
39
+ );
40
+ END IF;
41
+ END $$;
42
+ `},U=()=>`
43
+ DO $$
44
+ BEGIN
45
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${D}') THEN
46
+ CREATE ROLE ${D} NOLOGIN;
47
+ END IF;
48
+ END
49
+ $$;`,W=()=>`GRANT ${D} TO CURRENT_USER;`,G=(e,t=`public`)=>`GRANT SELECT, INSERT, UPDATE, DELETE ON ${t}.${e} TO ${D};`,K=(e=`public`)=>`GRANT SELECT ON ${e}.context_scope_path TO ${D};`,q=()=>`SELECT pg_try_advisory_xact_lock(hashtext('mat_path_init')) AS successfully_locked;`,J=()=>`CREATE EXTENSION IF NOT EXISTS ltree;`,Y=()=>`EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'ltree') AS "ltreeExists"`,X=()=>`EXISTS(SELECT 1 FROM pg_roles WHERE rolname = '${D}') AS "roleExists"`,Z=({policyName:e,schema:t,tableName:n})=>`EXISTS(
50
+ SELECT 1 FROM pg_policies
51
+ WHERE policyname = '${e}'
52
+ AND schemaname = '${t}'
53
+ AND tablename = '${n}'
54
+ ) AS "${e}"`,Q=({schema:e,tableName:t})=>`has_table_privilege('${D}', '${e}.${t}', 'SELECT,INSERT,UPDATE,DELETE') AS "grant_${e}.${t}"`,$=()=>`has_table_privilege('${D}', 'public.context_scope_path', 'SELECT') AS "grant_public.context_scope_path"`,ie=e=>{let t=Y(),n=X(),r=$(),i=e.map(Z),a=e.map(Q);return`SELECT ${[t,n,r,...i,...a].join(`,
55
+ `)}`};var ae=class{#e;#t;#n;#r=!1;#i=!1;constructor(e){this.#e=e.sequelize,this.#t=e.models,this.#n=e.logger,this.enabled=process.env.USE_MATERIALIZED_PATH===`true`}#a(){let e=this.#t.map(({model:e})=>{let{tableName:t,schema:n}=R(e);return G(t,n)}),t=this.#t.map(e=>H(e));return[J(),U(),W(),K(),...e,...t].join(``)}async bootstrap(){if(!this.enabled){this.#n.debug(`Materialized Path: disabled, skipping bootstrap`);return}if(!this.#r)try{await this.#e.transaction(async e=>{let[{successfully_locked:t}]=await this.#e.query(q(),{transaction:e,type:l.QueryTypes.SELECT});if(!t){this.#n.debug(`Materialized Path: another instance is bootstrapping, skipping`);return}await this.#e.query(this.#a(),{transaction:e}),this.#n.debug(`Materialized Path: bootstrap complete`)}),this.#r=!0}catch(e){throw this.#n.error(`Materialized Path: bootstrap failed`,{error:e}),e}}async withPaths(e,t={}){return this.enabled?L(this.#e,e,t):e(t.transaction)}async isReady(){if(!this.enabled||this.#i)return!0;if(!this.#r)return!1;try{let e=this.#t.map(({model:e})=>{let{tableName:t,schema:n}=R(e);return{policyName:`${t}${O}`,schema:n,tableName:t}}),[t]=await this.#e.query(ie(e),{type:l.QueryTypes.SELECT}),n=e.filter(({policyName:e})=>!t[e]).map(({policyName:e})=>e),r=e.filter(({schema:e,tableName:n})=>!t[`grant_${e}.${n}`]).map(({schema:e,tableName:t})=>`${e}.${t}`),i=t[`grant_public.context_scope_path`];return!t.ltreeExists||!t.roleExists||n.length||r.length||!i?(this.#n.warn(`Materialized Path: readiness check failed`,{ltreeExists:t.ltreeExists,roleExists:t.roleExists,contextScopePathGranted:i,missingPolicies:n,missingGrants:r}),!1):(this.#i=!0,!0)}catch(e){return this.#n.error(`Materialized Path: readiness check error`,{error:e}),!1}}},oe=(e,t)=>{let n=(t,n,r)=>b(e,t,n,r);return{httpBasedTransaction:(t,n,r)=>S(e,t,n,r),transactionWithRetry:n,registerModelEventHooks:(n,r)=>ee(e,n,r,t),runAfterTransactionCommits:h,useOrCreateTransaction:(e,t)=>e?t(e):n(t,0)}};exports.CONTEXT_PATH_HEADER=C,exports.MAT_PATH_MODE=k,exports.MatPathManager=ae,exports.default=oe;
2
56
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["defaultExport: LoggerInstanceManager","newObj: Record<string, unknown>","packageLogger","runAfterTransactionCommits","debugLog: debug.Debugger","sequelize","DatabaseError","sequelize","transactionWithRetry","_transactionWithRetry","sequelize","httpBasedTransaction","_httpBasedTransaction","addModelEventHooks"],"sources":["../src/logger.ts","../src/runAfterTransactionCommits.ts","../src/model-event-hooks.ts","../src/common.ts","../src/transaction-with-retry.ts","../src/http-based-transaction.ts","../src/index.ts"],"sourcesContent":["import Logger, { type LoggerInstanceManager } from '@autofleet/logger';\n\nconst defaultExport: LoggerInstanceManager = Logger();\nexport default defaultExport;\n","import type { Transactionable } from 'sequelize';\n\nconst runAfterTransactionCommits = async (cb: () => void | Promise<void>, options: Transactionable): Promise<void> => {\n if (options.transaction) {\n options.transaction.afterCommit(() => cb());\n } else {\n await cb();\n }\n};\nexport default runAfterTransactionCommits;\n","import type { Model, Sequelize } from 'sequelize';\nimport type Events from '@autofleet/events';\nimport type { LoggerInstanceManager } from '@autofleet/logger';\nimport packageLogger from './logger';\nimport runAfterTransactionCommits from './runAfterTransactionCommits';\n\nexport type ModelMapping = Record<string, { tableName: string; }>;\n\nconst isDate = (input: unknown): input is Date => input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';\nconst formatDatesInObject = (obj: object) => {\n const newObj: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n const valueToUse = isDate(value) ? value.getTime() / 1000 : value;\n newObj[key] = valueToUse;\n }\n return newObj;\n};\n\nconst addModelEventHooks = (sequelize: Sequelize, modelTableMapping: ModelMapping, events: Events, logger?: LoggerInstanceManager): void => {\n const localLogger = logger ?? packageLogger;\n if (!events) {\n localLogger.warn('Events object must be provided to addModelEventHooks');\n return;\n }\n const updateEventToDimTable = (object: Model, isDelete = false) => {\n try {\n const objectToSend = object.get();\n if (isDelete) {\n Object.assign(objectToSend, { deletedAt: new Date() });\n }\n const tableName = modelTableMapping[object.constructor.name]?.tableName;\n const eventVersion = '1';\n const objectToSendFormatted = formatDatesInObject(objectToSend);\n if (!tableName) {\n return;\n }\n events.sendObject(\n tableName,\n eventVersion,\n objectToSendFormatted,\n // @ts-expect-error the rawAttributes is typed as static, while it actually is on the instance level.\n Object.keys(object.rawAttributes),\n ).catch((e) => {\n localLogger.error('sendObject error', { tableName, eventVersion, e });\n });\n } catch (e) {\n localLogger.error('dimTables error', { e });\n }\n };\n\n sequelize.addHook('afterSave', (savedObject, options) => runAfterTransactionCommits(() => updateEventToDimTable(savedObject), options));\n sequelize.addHook('afterDestroy', (savedObject, options) => runAfterTransactionCommits(() => updateEventToDimTable(savedObject, true), options));\n};\n\nexport default addModelEventHooks;\n","import debug from 'debug';\n\nexport const debugLog: debug.Debugger = debug('sequelize-utils');\n","import {\n DatabaseError, type Sequelize, type Transaction, type TransactionOptions,\n} from 'sequelize';\nimport { debugLog } from './common';\n\nexport const transactionWithRetry = async <T>(sequelize: Sequelize, funcToRun: (transaction: Transaction) => Promise<T>, retriesCount = 2, options: TransactionOptions = {}): Promise<T> => {\n if (typeof funcToRun !== 'function') {\n throw new Error('funcToRun must be a function');\n }\n if (typeof retriesCount !== 'number') {\n throw new Error('if defined, retriesCount must be a number');\n }\n if (retriesCount < 0) {\n throw new Error('retriesCount must be a positive number');\n }\n try {\n const transValue = await sequelize.transaction(options, async (transaction) => {\n const funcValue = await funcToRun(transaction);\n return funcValue;\n });\n return transValue;\n } catch (e) {\n if ((e instanceof DatabaseError || e?.constructor?.name === 'DatabaseError') && --retriesCount >= 0) {\n debugLog(`error inside transactionWithRetry - will retry times ${retriesCount}`, e);\n return transactionWithRetry(sequelize, funcToRun, retriesCount, options);\n }\n if (retriesCount === -1) {\n debugLog('error inside transactionWithRetry - will stop retry', e);\n }\n throw e;\n }\n};\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { Sequelize, Transaction } from 'sequelize';\nimport { debugLog } from './common';\n\nconst rollbackErrorText = 'rollback has been called on this transaction';\nconst abortErrorText = 'Transaction cancelled due to request cancellation';\n\nexport const httpBasedTransaction = <T>(sequelize: Sequelize, req: IncomingMessage, res: ServerResponse, cb: (transaction: Transaction) => Promise<T>): Promise<T> => {\n let aborted = false;\n if (req.socket.destroyed) {\n debugLog(abortErrorText);\n throw new Error(abortErrorText);\n }\n return sequelize.transaction(async (transaction: Transaction) => {\n // https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/test/parallel/test-http-aborted.js#L9\n\n // 2023-04-13: Node.js 16.0.0\n // Added also the `res.writableFinished` check, because it seems that the socket is not destroyed when the request is aborted, but the response is not finished.\n // https://github.com/Jimbly/http-proxy-node16/commit/ba0c414cd03799e357c5d867c11dea06a9c34ec8#diff-b2d1e3b7c5f3b424a0af7971c582c822fd25a5ea279040ef6dc93fc4b2cf619dR151\n\n const rollback = async () => {\n debugLog(abortErrorText);\n if (aborted) return;\n aborted = true;\n await transaction.rollback();\n };\n const resRollback = async () => {\n const didNotFinishWrite = !res.writableFinished;\n if (didNotFinishWrite) {\n debugLog(abortErrorText);\n if (aborted) return;\n aborted = true;\n await transaction.rollback();\n }\n };\n req.socket.once('close', rollback);\n res.once('close', resRollback);\n\n const removeListeners = () => {\n req.socket.removeListener('close', rollback);\n res.removeListener('close', resRollback);\n };\n\n try {\n const response = await cb(transaction);\n removeListeners();\n return response;\n } catch (e) {\n removeListeners();\n if (e.message.includes(rollbackErrorText)) {\n throw new Error(abortErrorText);\n }\n throw e;\n }\n });\n};\n","import type {\n Sequelize,\n Transaction,\n TransactionOptions,\n} from 'sequelize';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { LoggerInstanceManager } from '@autofleet/logger';\nimport type Events from '@autofleet/events';\nimport addModelEventHooks, { type ModelMapping } from './model-event-hooks';\nimport { transactionWithRetry as _transactionWithRetry } from './transaction-with-retry';\nimport { httpBasedTransaction as _httpBasedTransaction } from './http-based-transaction';\nimport runAfterTransactionCommits from './runAfterTransactionCommits';\n\ntype ArgsWithoutSequelize<T> = T extends (arg1: Sequelize, ...args: infer U) => unknown ? U : never;\n\nexport default (sequelize: Sequelize, logger?: LoggerInstanceManager): {\n transactionWithRetry: <T>(...args: ArgsWithoutSequelize<typeof _transactionWithRetry<T>>) => Promise<T>;\n httpBasedTransaction: <T>(...args: ArgsWithoutSequelize<typeof _httpBasedTransaction<T>>) => Promise<T>;\n registerModelEventHooks: (modelTableMapping: ModelMapping, events: Events) => void;\n runAfterTransactionCommits: typeof runAfterTransactionCommits;\n useOrCreateTransaction: <T>(transaction: Transaction | null | undefined, cb: (t: Transaction) => Promise<T>) => Promise<T>;\n} => {\n const transactionWithRetry = <T>(\n funcToRun: (transaction: Transaction) => Promise<T>,\n retriesCount?: number,\n options?: TransactionOptions,\n ): Promise<T> => _transactionWithRetry(sequelize, funcToRun, retriesCount, options);\n const httpBasedTransaction = <T>(req: IncomingMessage, res: ServerResponse, cb: (transaction: Transaction) => Promise<T>): Promise<T> => _httpBasedTransaction(sequelize, req, res, cb);\n const registerModelEventHooks = (modelTableMapping: ModelMapping, events: Events) => addModelEventHooks(sequelize, modelTableMapping, events, logger);\n const useOrCreateTransaction = <T>(transaction: Transaction | null | undefined, cb: (t: Transaction) => Promise<T>) => {\n if (transaction) {\n return cb(transaction);\n }\n return transactionWithRetry(cb, 0);\n };\n\n return {\n httpBasedTransaction,\n transactionWithRetry,\n registerModelEventHooks,\n runAfterTransactionCommits,\n useOrCreateTransaction,\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["defaultExport: LoggerInstanceManager","newObj: Record<string, unknown>","packageLogger","runAfterTransactionCommits","debugLog: debug.Debugger","sequelize","DatabaseError","sequelize","VALID_MODES: ReadonlySet<AuthMode>","sequelize","#sequelize","#modelConfigs","#logger","#bootstrapped","QueryTypes","#buildBootstrapSQL","#isReady","transactionWithRetry","_transactionWithRetry","sequelize","httpBasedTransaction","_httpBasedTransaction","addModelEventHooks"],"sources":["../src/logger.ts","../src/runAfterTransactionCommits.ts","../src/model-event-hooks.ts","../src/common.ts","../src/transaction-with-retry.ts","../src/http-based-transaction.ts","../src/materialized-path/constants.ts","../src/materialized-path/with-paths.ts","../src/materialized-path/utils/utils.ts","../src/materialized-path/sql-generators/generate-policy.ts","../src/materialized-path/sql-generators/generate-role.ts","../src/materialized-path/sql-generators/utils.ts","../src/materialized-path/sql-generators/generate-ready-check.ts","../src/materialized-path/mat-path-manager.ts","../src/index.ts"],"sourcesContent":["import Logger, { type LoggerInstanceManager } from '@autofleet/logger';\n\nconst defaultExport: LoggerInstanceManager = Logger();\nexport default defaultExport;\n","import type { Transactionable } from 'sequelize';\n\nconst runAfterTransactionCommits = async (cb: () => void | Promise<void>, options: Transactionable): Promise<void> => {\n if (options.transaction) {\n options.transaction.afterCommit(() => cb());\n } else {\n await cb();\n }\n};\nexport default runAfterTransactionCommits;\n","import type { Model, Sequelize } from 'sequelize';\nimport type Events from '@autofleet/events';\nimport type { LoggerInstanceManager } from '@autofleet/logger';\nimport packageLogger from './logger';\nimport runAfterTransactionCommits from './runAfterTransactionCommits';\n\nexport type ModelMapping = Record<string, { tableName: string; }>;\n\nconst isDate = (input: unknown): input is Date => input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';\nconst formatDatesInObject = (obj: object) => {\n const newObj: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n const valueToUse = isDate(value) ? value.getTime() / 1000 : value;\n newObj[key] = valueToUse;\n }\n return newObj;\n};\n\nconst addModelEventHooks = (sequelize: Sequelize, modelTableMapping: ModelMapping, events: Events, logger?: LoggerInstanceManager): void => {\n const localLogger = logger ?? packageLogger;\n if (!events) {\n localLogger.warn('Events object must be provided to addModelEventHooks');\n return;\n }\n const updateEventToDimTable = (object: Model, isDelete = false) => {\n try {\n const objectToSend = object.get();\n if (isDelete) {\n Object.assign(objectToSend, { deletedAt: new Date() });\n }\n const tableName = modelTableMapping[object.constructor.name]?.tableName;\n const eventVersion = '1';\n const objectToSendFormatted = formatDatesInObject(objectToSend);\n if (!tableName) {\n return;\n }\n events.sendObject(\n tableName,\n eventVersion,\n objectToSendFormatted,\n // @ts-expect-error the rawAttributes is typed as static, while it actually is on the instance level.\n Object.keys(object.rawAttributes),\n ).catch((e) => {\n localLogger.error('sendObject error', { tableName, eventVersion, e });\n });\n } catch (e) {\n localLogger.error('dimTables error', { e });\n }\n };\n\n sequelize.addHook('afterSave', (savedObject, options) => runAfterTransactionCommits(() => updateEventToDimTable(savedObject), options));\n sequelize.addHook('afterDestroy', (savedObject, options) => runAfterTransactionCommits(() => updateEventToDimTable(savedObject, true), options));\n};\n\nexport default addModelEventHooks;\n","import debug from 'debug';\n\nexport const debugLog: debug.Debugger = debug('sequelize-utils');\n","import {\n DatabaseError, type Sequelize, type Transaction, type TransactionOptions,\n} from 'sequelize';\nimport { debugLog } from './common';\n\nexport const transactionWithRetry = async <T>(sequelize: Sequelize, funcToRun: (transaction: Transaction) => Promise<T>, retriesCount = 2, options: TransactionOptions = {}): Promise<T> => {\n if (typeof funcToRun !== 'function') {\n throw new Error('funcToRun must be a function');\n }\n if (typeof retriesCount !== 'number') {\n throw new Error('if defined, retriesCount must be a number');\n }\n if (retriesCount < 0) {\n throw new Error('retriesCount must be a positive number');\n }\n try {\n const transValue = await sequelize.transaction(options, async (transaction) => {\n const funcValue = await funcToRun(transaction);\n return funcValue;\n });\n return transValue;\n } catch (e) {\n if ((e instanceof DatabaseError || e?.constructor?.name === 'DatabaseError') && --retriesCount >= 0) {\n debugLog(`error inside transactionWithRetry - will retry times ${retriesCount}`, e);\n return transactionWithRetry(sequelize, funcToRun, retriesCount, options);\n }\n if (retriesCount === -1) {\n debugLog('error inside transactionWithRetry - will stop retry', e);\n }\n throw e;\n }\n};\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { Sequelize, Transaction } from 'sequelize';\nimport { debugLog } from './common';\n\nconst rollbackErrorText = 'rollback has been called on this transaction';\nconst abortErrorText = 'Transaction cancelled due to request cancellation';\n\nexport const httpBasedTransaction = <T>(sequelize: Sequelize, req: IncomingMessage, res: ServerResponse, cb: (transaction: Transaction) => Promise<T>): Promise<T> => {\n let aborted = false;\n if (req.socket.destroyed) {\n debugLog(abortErrorText);\n throw new Error(abortErrorText);\n }\n return sequelize.transaction(async (transaction: Transaction) => {\n // https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/test/parallel/test-http-aborted.js#L9\n\n // 2023-04-13: Node.js 16.0.0\n // Added also the `res.writableFinished` check, because it seems that the socket is not destroyed when the request is aborted, but the response is not finished.\n // https://github.com/Jimbly/http-proxy-node16/commit/ba0c414cd03799e357c5d867c11dea06a9c34ec8#diff-b2d1e3b7c5f3b424a0af7971c582c822fd25a5ea279040ef6dc93fc4b2cf619dR151\n\n const rollback = async () => {\n debugLog(abortErrorText);\n if (aborted) return;\n aborted = true;\n await transaction.rollback();\n };\n const resRollback = async () => {\n const didNotFinishWrite = !res.writableFinished;\n if (didNotFinishWrite) {\n debugLog(abortErrorText);\n if (aborted) return;\n aborted = true;\n await transaction.rollback();\n }\n };\n req.socket.once('close', rollback);\n res.once('close', resRollback);\n\n const removeListeners = () => {\n req.socket.removeListener('close', rollback);\n res.removeListener('close', resRollback);\n };\n\n try {\n const response = await cb(transaction);\n removeListeners();\n return response;\n } catch (e) {\n removeListeners();\n if (e.message.includes(rollbackErrorText)) {\n throw new Error(abortErrorText);\n }\n throw e;\n }\n });\n};\n","export const CONTEXT_PATHS_HEADER = 'x-af-context-paths';\n\nexport const PATHS_SETTING = 'mat_path.paths';\nexport const PATH_ROOTS_SETTING = 'mat_path.path_roots';\nexport const MODE_SETTING = 'mat_path.mode';\nexport const ROLE = 'mat_path_role';\nexport const ADVISORY_LOCK_KEY = 'mat_path_init';\nexport const POLICY_NAME_SUFFIX = '_mat_path_policy';\n\nexport const FEATURE_FLAG_ENV = 'USE_MATERIALIZED_PATH';\nexport const MODE = {\n SCOPE: 'scope',\n INHERIT: 'inherit',\n BOTH: 'both',\n} as const;\n","import type { Sequelize, Transaction } from 'sequelize';\nimport type { AuthMode, SequelizeQueryCallback, WithPathsOptions } from './types.js';\nimport { CONTEXT_PATHS_HEADER, MODE, MODE_SETTING, PATHS_SETTING, PATH_ROOTS_SETTING, ROLE } from './constants.js';\nimport { getCurrentContext } from '@autofleet/outbreak';\n\n/** Validates the provided string is a valid ltree label, consisting of alphanumeric/underscore segments separated by dots. */\nconst VALID_PATH_REGEX = /^[A-Za-z0-9_]+(\\.[A-Za-z0-9_]+)*$/;\nconst VALID_MODES: ReadonlySet<AuthMode> = new Set([MODE.SCOPE, MODE.INHERIT, MODE.BOTH]);\n\nconst validatePaths = (paths: string[]): void => {\n for (const path of paths) {\n if (!VALID_PATH_REGEX.test(path)) {\n throw new Error(\n `Invalid materialized path: \"${path}\". `\n + 'Path must consist of alphanumeric/underscore segments separated by dots.',\n );\n }\n }\n};\n\nexport const validateMode = (mode: AuthMode): void => {\n if (!VALID_MODES.has(mode)) {\n throw new Error(`Invalid auth mode: \"${mode}\". Must be '${MODE.SCOPE}', '${MODE.INHERIT}', or '${MODE.BOTH}'.`);\n }\n};\n\nconst toArrayLiteral = (items: string[]): string => `{${items.join(',')}}`;\n\n/** Extracts the unique root segments from the provided paths. For example, given ['AF.1', 'AF.2', 'BF.1'], it will return ['AF', 'BF']. */\nconst getPathRoots = (paths: string[]): string[] => {\n const roots = new Set<string>();\n\n for (const path of paths) {\n const root = path.split('.')[0];\n roots.add(root);\n }\n\n return Array.from(roots);\n};\n\nconst getContextPath = (): string[] | undefined => {\n const trace = getCurrentContext();\n const value = trace?.context?.get(CONTEXT_PATHS_HEADER);\n if (typeof value !== 'string' || !value?.length) return undefined;\n return value.split(',');\n};\n\n/**\n * Sets transaction-local config variables needed for RLS policy enforcement, then runs the callback.\n * The callback should directly perform a Sequelize query (e.g. `findAll`, `create`, `update`) using the provided transaction.\n * If no transaction is provided, creates one. Otherwise reuses the existing transaction and issues `RESET ROLE` on completion.\n */\nexport const withPaths = async <T>(\n sequelize: Sequelize,\n callback: SequelizeQueryCallback<T>,\n options: WithPathsOptions = {},\n): Promise<T> => {\n const { mode = MODE.SCOPE, transaction: existingTransaction } = options;\n const paths = getContextPath();\n\n if (!paths) return callback(existingTransaction);\n\n validatePaths(paths);\n validateMode(mode);\n\n const pathArrayLiteral = toArrayLiteral(paths);\n const roots = getPathRoots(paths);\n const rootsArrayLiteral = toArrayLiteral(roots);\n\n const setLocalConfig = async (transaction: Transaction): Promise<void> => {\n await sequelize.query(\n `\n SELECT\n set_config('role', '${ROLE}', true),\n set_config('${PATHS_SETTING}', $1, true),\n set_config('${PATH_ROOTS_SETTING}', $2, true),\n set_config('${MODE_SETTING}', $3, true);\n `,\n { bind: [pathArrayLiteral, rootsArrayLiteral, mode], transaction },\n );\n };\n\n if (!existingTransaction) {\n return sequelize.transaction(async (transaction) => {\n await setLocalConfig(transaction);\n return callback(transaction);\n });\n }\n\n await setLocalConfig(existingTransaction);\n return callback(existingTransaction);\n};\n","import type { Model, ModelStatic } from 'sequelize';\nimport type { ResolvedTableInfo } from '../types';\nimport { getCurrentContext } from '@autofleet/outbreak';\nimport { CONTEXT_PATHS_HEADER } from '../constants';\n\nexport const resolveTableInfo = (model: ModelStatic<Model>): ResolvedTableInfo => {\n const tableInfo = model.getTableName();\n if (typeof tableInfo === 'string') return { tableName: tableInfo, schema: 'public' };\n return { tableName: tableInfo.tableName, schema: tableInfo.schema ?? 'public' };\n};\n\nexport const getContextPath = (): string | undefined => {\n const trace = getCurrentContext();\n const value = trace?.context?.get(CONTEXT_PATHS_HEADER);\n if (typeof value !== 'string' || !value?.length) return undefined;\n return value;\n};\n\nexport const setContextPath = (path: string): void => {\n const trace = getCurrentContext();\n trace?.context?.set(CONTEXT_PATHS_HEADER, path);\n};\n","import type { ModelPolicyConfig } from '../types';\nimport { resolveTableInfo } from '../utils/utils';\nimport { MODE, PATHS_SETTING, PATH_ROOTS_SETTING, MODE_SETTING, ROLE, POLICY_NAME_SUFFIX } from '../constants';\n\nconst PATHS_SETTING_FILTER = `current_setting('${PATHS_SETTING}')::ltree[]`;\n\n/**\n * Generates a `clientPathRootColumn` filter if the model config specifies a column for it.\n */\nconst generateClientPathRootFilter = (clientPathRootColumn: string): string => {\n if (!clientPathRootColumn) return '';\n\n return `AND ${clientPathRootColumn} = ANY(current_setting('${PATH_ROOTS_SETTING}')::text[])`;\n};\n\n/**\n * Generates the SQL to enable or disable RLS on the target table based on the `rlsEnabled` flag in the model config.\n * If `rlsEnabled` is `true` or `undefined`, RLS is enabled. If `false`, RLS is disabled.\n */\nconst generateRLSControl = (qualifiedTable: string, rlsEnabled?: boolean): string => {\n if (rlsEnabled === false) return `ALTER TABLE ${qualifiedTable} DISABLE ROW LEVEL SECURITY;`;\n\n return `ALTER TABLE ${qualifiedTable} ENABLE ROW LEVEL SECURITY;`;\n};\n\n/**\n * Generates the RLS policy SQL for a single table.\n *\n * The policy uses CASE current_setting('mat_path.mode') inside the InitPlan\n * subqueries to support per-query {@link MODE} switching.\n */\nexport const generatePolicySQL = (config: ModelPolicyConfig): string => {\n const { model, identityColumns, rlsEnabled, clientPathRootColumn = '' } = config;\n const { tableName, schema } = resolveTableInfo(model);\n const policyName = `${tableName}${POLICY_NAME_SUFFIX}`;\n\n const qualifiedTable = `${schema}.${tableName}`;\n const clientPathRootFilter = generateClientPathRootFilter(clientPathRootColumn);\n const rlsControlSQL = generateRLSControl(qualifiedTable, rlsEnabled);\n\n const identityChecks = identityColumns\n .map(({ column, entityType }) => `${column} = ANY(\n ARRAY(\n SELECT DISTINCT entity_id\n FROM ${schema}.context_scope_path\n WHERE (\n CASE current_setting('${MODE_SETTING}')\n WHEN '${MODE.SCOPE}' THEN path <@ ANY(${PATHS_SETTING_FILTER})\n WHEN '${MODE.INHERIT}' THEN path @> ANY(${PATHS_SETTING_FILTER})\n WHEN '${MODE.BOTH}' THEN (path <@ ANY(${PATHS_SETTING_FILTER}) OR path @> ANY(${PATHS_SETTING_FILTER}))\n END\n )\n AND entity_type = '${entityType}'\n AND deleted_at IS NULL\n )\n )`)\n .join('\\n OR ');\n\n return `\n ${rlsControlSQL}\n\n DO $$\n BEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_policies\n WHERE schemaname = '${schema}'\n AND tablename = '${tableName}'\n AND policyname = '${policyName}'\n ) THEN\n CREATE POLICY ${policyName} ON ${qualifiedTable}\n FOR ALL\n TO ${ROLE}\n USING (\n (${identityChecks})\n ${clientPathRootFilter}\n );\n END IF;\n END $$;\n `;\n};\n","import { ROLE } from '../constants';\n\nexport const generateCreateRoleSQL = (): string => `\n DO $$\n BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${ROLE}') THEN\n CREATE ROLE ${ROLE} NOLOGIN;\n END IF;\n END\n $$;`;\n\nexport const generateGrantRoleSQL = (): string => `GRANT ${ROLE} TO CURRENT_USER;`;\n\nexport const generateGrantTableSQL = (\n tableName: string,\n schema = 'public',\n): string => `GRANT SELECT, INSERT, UPDATE, DELETE ON ${schema}.${tableName} TO ${ROLE};`;\n\nexport const generateGrantContextScopePathSQL = (\n schema = 'public',\n): string => `GRANT SELECT ON ${schema}.context_scope_path TO ${ROLE};`;\n","import { ADVISORY_LOCK_KEY } from '../constants';\n\nexport const generateTryAdvisoryLockSQL = (): string => `SELECT pg_try_advisory_xact_lock(hashtext('${ADVISORY_LOCK_KEY}')) AS successfully_locked;`;\n\nexport const generateCreateLtreeExtensionSQL = (): string => `CREATE EXTENSION IF NOT EXISTS ltree;`;\nexport const generateLtreeExtensionEnabledSQL = (): string => `SELECT extname FROM pg_extension WHERE extname = 'ltree';`;\n","import { ROLE } from '../constants';\n\ninterface TablePolicyInfo {\n policyName: string;\n schema: string;\n tableName: string;\n}\n\nconst generateLtreeExtensionCheck = (): string =>\n `EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'ltree') AS \"ltreeExists\"`;\n\nconst generateRoleExistenceCheck = (): string =>\n `EXISTS(SELECT 1 FROM pg_roles WHERE rolname = '${ROLE}') AS \"roleExists\"`;\n\nconst generatePolicyExistenceCheck = ({ policyName, schema, tableName }: TablePolicyInfo): string =>\n `EXISTS(\n SELECT 1 FROM pg_policies\n WHERE policyname = '${policyName}'\n AND schemaname = '${schema}'\n AND tablename = '${tableName}'\n ) AS \"${policyName}\"`;\n\nconst generateGrantCheck = ({ schema, tableName }: TablePolicyInfo): string =>\n `has_table_privilege('${ROLE}', '${schema}.${tableName}', 'SELECT,INSERT,UPDATE,DELETE') AS \"grant_${schema}.${tableName}\"`;\n\nconst generateContextScopePathGrantCheck = (): string =>\n `has_table_privilege('${ROLE}', 'public.context_scope_path', 'SELECT') AS \"grant_public.context_scope_path\"`;\n\nexport const generateReadinessCheckSQL = (expectedPolicies: TablePolicyInfo[]): string => {\n const ltreeCheck = generateLtreeExtensionCheck();\n\n const roleCheck = generateRoleExistenceCheck();\n\n const contextScopePathCheck = generateContextScopePathGrantCheck();\n\n const policyChecks = expectedPolicies.map(generatePolicyExistenceCheck);\n\n const grantChecks = expectedPolicies.map(generateGrantCheck);\n\n return `SELECT ${[ltreeCheck, roleCheck, contextScopePathCheck, ...policyChecks, ...grantChecks].join(',\\n')}`;\n};\n","import { QueryTypes, type Sequelize } from 'sequelize';\nimport type { LoggerInstanceManager } from '@autofleet/logger';\nimport type { MatPathOptions, ModelPolicyConfig, SequelizeQueryCallback, WithPathsOptions } from './types';\nimport { withPaths } from './with-paths';\nimport { generatePolicySQL } from './sql-generators/generate-policy';\nimport {\n generateCreateRoleSQL,\n generateGrantRoleSQL,\n generateGrantTableSQL,\n generateGrantContextScopePathSQL,\n} from './sql-generators/generate-role';\nimport { FEATURE_FLAG_ENV, POLICY_NAME_SUFFIX } from './constants';\nimport { generateTryAdvisoryLockSQL, generateCreateLtreeExtensionSQL } from './sql-generators/utils';\nimport { resolveTableInfo } from './utils/utils';\nimport { generateReadinessCheckSQL } from './sql-generators/generate-ready-check';\n\nexport class MatPathManager {\n readonly #sequelize: Sequelize;\n readonly #modelConfigs: ModelPolicyConfig[];\n readonly #logger: LoggerInstanceManager;\n readonly enabled: boolean;\n #bootstrapped = false;\n #isReady = false;\n\n constructor(options: MatPathOptions) {\n this.#sequelize = options.sequelize;\n this.#modelConfigs = options.models;\n this.#logger = options.logger;\n this.enabled = process.env[FEATURE_FLAG_ENV] === 'true';\n }\n\n #buildBootstrapSQL(): string {\n const grantTableSQL = this.#modelConfigs.map(({ model }) => {\n const { tableName, schema } = resolveTableInfo(model);\n return generateGrantTableSQL(tableName, schema);\n });\n\n const policySQL = this.#modelConfigs.map(modelConfig => generatePolicySQL(modelConfig));\n\n return [\n generateCreateLtreeExtensionSQL(),\n generateCreateRoleSQL(),\n generateGrantRoleSQL(),\n generateGrantContextScopePathSQL(),\n ...grantTableSQL,\n ...policySQL,\n ].join('');\n }\n\n /** Idempotent and concurrency-safe function that sets up the database for Materialized Path usage via RLS enforcement (role, grants, policies). */\n async bootstrap(): Promise<void> {\n if (!this.enabled) {\n this.#logger.debug('Materialized Path: disabled, skipping bootstrap');\n return;\n }\n if (this.#bootstrapped) return;\n\n try {\n await this.#sequelize.transaction(async (transaction) => {\n const [{ successfully_locked: successfullyLocked }] = await this.#sequelize.query<{ successfully_locked: boolean; }>(\n generateTryAdvisoryLockSQL(),\n { transaction, type: QueryTypes.SELECT },\n );\n\n if (!successfullyLocked) {\n this.#logger.debug('Materialized Path: another instance is bootstrapping, skipping');\n return;\n }\n\n await this.#sequelize.query(this.#buildBootstrapSQL(), { transaction });\n this.#logger.debug('Materialized Path: bootstrap complete');\n });\n\n this.#bootstrapped = true;\n } catch (error) {\n this.#logger.error('Materialized Path: bootstrap failed', { error });\n throw error;\n }\n }\n\n /**\n * Wraps a callback with the necessary local config setup to enforce RLS policies based on the provided paths and mode.\n * The callback should directly perform a Sequelize query (e.g. `findAll`, `create`, `update`) using the provided transaction.\n * When disabled, runs the callback without RLS.\n */\n async withPaths<T>(\n callback: SequelizeQueryCallback<T>,\n options: WithPathsOptions = {},\n ): Promise<T> {\n if (!this.enabled) return callback(options.transaction);\n\n return withPaths(this.#sequelize, callback, options);\n }\n\n /**\n * Check if the materialized path infrastructure is ready.\n * Verifies that the ltree extension, role, table grants, and RLS policies were all created during bootstrap.\n * Returns true immediately when disabled or after a previous successful check (cached).\n *\n * @example\n * const ready = await matPath.isReady();\n */\n async isReady(): Promise<boolean> {\n if (!this.enabled || this.#isReady) return true;\n if (!this.#bootstrapped) return false;\n\n try {\n const expectedPolicies = this.#modelConfigs.map(({ model }) => {\n const { tableName, schema } = resolveTableInfo(model);\n return { policyName: `${tableName}${POLICY_NAME_SUFFIX}`, schema, tableName };\n });\n\n const [result] = await this.#sequelize.query<Record<string, boolean>>(\n generateReadinessCheckSQL(expectedPolicies),\n { type: QueryTypes.SELECT },\n );\n\n const missingPolicies = expectedPolicies\n .filter(({ policyName }) => !result[policyName])\n .map(({ policyName }) => policyName);\n\n const missingGrants = expectedPolicies\n .filter(({ schema, tableName }) => !result[`grant_${schema}.${tableName}`])\n .map(({ schema, tableName }) => `${schema}.${tableName}`);\n\n const contextScopePathGranted = result['grant_public.context_scope_path'];\n\n if (!result.ltreeExists || !result.roleExists || missingPolicies.length || missingGrants.length || !contextScopePathGranted) {\n this.#logger.warn('Materialized Path: readiness check failed', {\n ltreeExists: result.ltreeExists,\n roleExists: result.roleExists,\n contextScopePathGranted,\n missingPolicies,\n missingGrants,\n });\n return false;\n }\n\n this.#isReady = true;\n return true;\n } catch (error) {\n this.#logger.error('Materialized Path: readiness check error', { error });\n return false;\n }\n }\n}\n","import type {\n Sequelize,\n Transaction,\n TransactionOptions,\n} from 'sequelize';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { LoggerInstanceManager } from '@autofleet/logger';\nimport type Events from '@autofleet/events';\nimport addModelEventHooks, { type ModelMapping } from './model-event-hooks';\nimport { transactionWithRetry as _transactionWithRetry } from './transaction-with-retry';\nimport { httpBasedTransaction as _httpBasedTransaction } from './http-based-transaction';\nimport runAfterTransactionCommits from './runAfterTransactionCommits';\n\ntype ArgsWithoutSequelize<T> = T extends (arg1: Sequelize, ...args: infer U) => unknown ? U : never;\n\nexport default (sequelize: Sequelize, logger?: LoggerInstanceManager): {\n transactionWithRetry: <T>(...args: ArgsWithoutSequelize<typeof _transactionWithRetry<T>>) => Promise<T>;\n httpBasedTransaction: <T>(...args: ArgsWithoutSequelize<typeof _httpBasedTransaction<T>>) => Promise<T>;\n registerModelEventHooks: (modelTableMapping: ModelMapping, events: Events) => void;\n runAfterTransactionCommits: typeof runAfterTransactionCommits;\n useOrCreateTransaction: <T>(transaction: Transaction | null | undefined, cb: (t: Transaction) => Promise<T>) => Promise<T>;\n} => {\n const transactionWithRetry = <T>(\n funcToRun: (transaction: Transaction) => Promise<T>,\n retriesCount?: number,\n options?: TransactionOptions,\n ): Promise<T> => _transactionWithRetry(sequelize, funcToRun, retriesCount, options);\n const httpBasedTransaction = <T>(req: IncomingMessage, res: ServerResponse, cb: (transaction: Transaction) => Promise<T>): Promise<T> => _httpBasedTransaction(sequelize, req, res, cb);\n const registerModelEventHooks = (modelTableMapping: ModelMapping, events: Events) => addModelEventHooks(sequelize, modelTableMapping, events, logger);\n const useOrCreateTransaction = <T>(transaction: Transaction | null | undefined, cb: (t: Transaction) => Promise<T>) => {\n if (transaction) {\n return cb(transaction);\n }\n return transactionWithRetry(cb, 0);\n };\n\n return {\n httpBasedTransaction,\n transactionWithRetry,\n registerModelEventHooks,\n runAfterTransactionCommits,\n useOrCreateTransaction,\n };\n};\n\nexport * from './materialized-path';\n"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -1,7 +1,8 @@
1
- import { Sequelize, Transaction, TransactionOptions, Transactionable } from "sequelize";
1
+ import { Model, ModelStatic, Sequelize, Transaction, TransactionOptions, Transactionable } from "sequelize";
2
2
  import { LoggerInstanceManager } from "@autofleet/logger";
3
3
  import Events from "@autofleet/events";
4
4
  import { IncomingMessage, ServerResponse } from "node:http";
5
+ import { ValueOf } from "@autofleet/common-types/lib/utils";
5
6
 
6
7
  //#region src/model-event-hooks.d.ts
7
8
  type ModelMapping = Record<string, {
@@ -17,6 +18,73 @@ declare const httpBasedTransaction: <T>(sequelize: Sequelize, req: IncomingMessa
17
18
  //#region src/runAfterTransactionCommits.d.ts
18
19
  declare const runAfterTransactionCommits: (cb: () => void | Promise<void>, options: Transactionable) => Promise<void>;
19
20
  //#endregion
21
+ //#region src/materialized-path/constants.d.ts
22
+ declare const CONTEXT_PATHS_HEADER = "x-af-context-paths";
23
+ declare const MODE: {
24
+ readonly SCOPE: "scope";
25
+ readonly INHERIT: "inherit";
26
+ readonly BOTH: "both";
27
+ };
28
+ //#endregion
29
+ //#region src/materialized-path/types.d.ts
30
+ type AuthMode = ValueOf<typeof MODE>;
31
+ interface IdentityColumnConfig {
32
+ /** Column name on the target table (e.g., 'business_model_id'). */
33
+ column: string;
34
+ /** Matching entity_type value in context_scope_path (e.g., 'business_model'). */
35
+ entityType: string;
36
+ }
37
+ interface ModelPolicyConfig {
38
+ /** The Sequelize model for the table to apply RLS policies to. */
39
+ model: ModelStatic<Model>;
40
+ /** Mapping from context_scope_path entity types to columns on this model. */
41
+ identityColumns: IdentityColumnConfig[];
42
+ /** Whether row-level security (RLS) is enabled for this model. */
43
+ rlsEnabled?: boolean;
44
+ /** Optional column storing the root path segment for planner optimization. */
45
+ clientPathRootColumn?: string;
46
+ }
47
+ interface MatPathOptions {
48
+ /** The Sequelize instance. */
49
+ sequelize: Sequelize;
50
+ /** Configurations for each model that should have RLS policies. */
51
+ models: ModelPolicyConfig[];
52
+ /** Logger instance. */
53
+ logger: LoggerInstanceManager;
54
+ }
55
+ /** A callback that performs a Sequelize query using the provided transaction. */
56
+ type SequelizeQueryCallback<T> = (transaction?: Transaction) => Promise<T>;
57
+ interface WithPathsOptions {
58
+ /** Query mode. Defaults to {@link MODE.SCOPE}. */
59
+ mode?: AuthMode;
60
+ /** Existing transaction to reuse. If not provided, a new one is created. */
61
+ transaction?: Transaction;
62
+ }
63
+ //#endregion
64
+ //#region src/materialized-path/mat-path-manager.d.ts
65
+ declare class MatPathManager {
66
+ #private;
67
+ readonly enabled: boolean;
68
+ constructor(options: MatPathOptions);
69
+ /** Idempotent and concurrency-safe function that sets up the database for Materialized Path usage via RLS enforcement (role, grants, policies). */
70
+ bootstrap(): Promise<void>;
71
+ /**
72
+ * Wraps a callback with the necessary local config setup to enforce RLS policies based on the provided paths and mode.
73
+ * The callback should directly perform a Sequelize query (e.g. `findAll`, `create`, `update`) using the provided transaction.
74
+ * When disabled, runs the callback without RLS.
75
+ */
76
+ withPaths<T>(callback: SequelizeQueryCallback<T>, options?: WithPathsOptions): Promise<T>;
77
+ /**
78
+ * Check if the materialized path infrastructure is ready.
79
+ * Verifies that the ltree extension, role, table grants, and RLS policies were all created during bootstrap.
80
+ * Returns true immediately when disabled or after a previous successful check (cached).
81
+ *
82
+ * @example
83
+ * const ready = await matPath.isReady();
84
+ */
85
+ isReady(): Promise<boolean>;
86
+ }
87
+ //#endregion
20
88
  //#region src/index.d.ts
21
89
  type ArgsWithoutSequelize<T> = T extends ((arg1: Sequelize, ...args: infer U) => unknown) ? U : never;
22
90
  declare const _default: (sequelize: Sequelize, logger?: LoggerInstanceManager) => {
@@ -26,5 +94,6 @@ declare const _default: (sequelize: Sequelize, logger?: LoggerInstanceManager) =
26
94
  runAfterTransactionCommits: typeof runAfterTransactionCommits;
27
95
  useOrCreateTransaction: <T>(transaction: Transaction | null | undefined, cb: (t: Transaction) => Promise<T>) => Promise<T>;
28
96
  };
29
- export = _default;
97
+ //#endregion
98
+ export { type AuthMode, CONTEXT_PATHS_HEADER as CONTEXT_PATH_HEADER, type IdentityColumnConfig, MODE as MAT_PATH_MODE, MatPathManager, type MatPathOptions, type ModelPolicyConfig as TablePolicyConfig, type WithPathsOptions, _default as default };
30
99
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { LoggerInstanceManager } from "@autofleet/logger";
2
- import { Sequelize, Transaction, TransactionOptions, Transactionable } from "sequelize";
2
+ import { Model, ModelStatic, Sequelize, Transaction, TransactionOptions, Transactionable } from "sequelize";
3
3
  import Events from "@autofleet/events";
4
4
  import { IncomingMessage, ServerResponse } from "node:http";
5
+ import { ValueOf } from "@autofleet/common-types/lib/utils";
5
6
 
6
7
  //#region src/model-event-hooks.d.ts
7
8
  type ModelMapping = Record<string, {
@@ -17,6 +18,73 @@ declare const httpBasedTransaction: <T>(sequelize: Sequelize, req: IncomingMessa
17
18
  //#region src/runAfterTransactionCommits.d.ts
18
19
  declare const runAfterTransactionCommits: (cb: () => void | Promise<void>, options: Transactionable) => Promise<void>;
19
20
  //#endregion
21
+ //#region src/materialized-path/constants.d.ts
22
+ declare const CONTEXT_PATHS_HEADER = "x-af-context-paths";
23
+ declare const MODE: {
24
+ readonly SCOPE: "scope";
25
+ readonly INHERIT: "inherit";
26
+ readonly BOTH: "both";
27
+ };
28
+ //#endregion
29
+ //#region src/materialized-path/types.d.ts
30
+ type AuthMode = ValueOf<typeof MODE>;
31
+ interface IdentityColumnConfig {
32
+ /** Column name on the target table (e.g., 'business_model_id'). */
33
+ column: string;
34
+ /** Matching entity_type value in context_scope_path (e.g., 'business_model'). */
35
+ entityType: string;
36
+ }
37
+ interface ModelPolicyConfig {
38
+ /** The Sequelize model for the table to apply RLS policies to. */
39
+ model: ModelStatic<Model>;
40
+ /** Mapping from context_scope_path entity types to columns on this model. */
41
+ identityColumns: IdentityColumnConfig[];
42
+ /** Whether row-level security (RLS) is enabled for this model. */
43
+ rlsEnabled?: boolean;
44
+ /** Optional column storing the root path segment for planner optimization. */
45
+ clientPathRootColumn?: string;
46
+ }
47
+ interface MatPathOptions {
48
+ /** The Sequelize instance. */
49
+ sequelize: Sequelize;
50
+ /** Configurations for each model that should have RLS policies. */
51
+ models: ModelPolicyConfig[];
52
+ /** Logger instance. */
53
+ logger: LoggerInstanceManager;
54
+ }
55
+ /** A callback that performs a Sequelize query using the provided transaction. */
56
+ type SequelizeQueryCallback<T> = (transaction?: Transaction) => Promise<T>;
57
+ interface WithPathsOptions {
58
+ /** Query mode. Defaults to {@link MODE.SCOPE}. */
59
+ mode?: AuthMode;
60
+ /** Existing transaction to reuse. If not provided, a new one is created. */
61
+ transaction?: Transaction;
62
+ }
63
+ //#endregion
64
+ //#region src/materialized-path/mat-path-manager.d.ts
65
+ declare class MatPathManager {
66
+ #private;
67
+ readonly enabled: boolean;
68
+ constructor(options: MatPathOptions);
69
+ /** Idempotent and concurrency-safe function that sets up the database for Materialized Path usage via RLS enforcement (role, grants, policies). */
70
+ bootstrap(): Promise<void>;
71
+ /**
72
+ * Wraps a callback with the necessary local config setup to enforce RLS policies based on the provided paths and mode.
73
+ * The callback should directly perform a Sequelize query (e.g. `findAll`, `create`, `update`) using the provided transaction.
74
+ * When disabled, runs the callback without RLS.
75
+ */
76
+ withPaths<T>(callback: SequelizeQueryCallback<T>, options?: WithPathsOptions): Promise<T>;
77
+ /**
78
+ * Check if the materialized path infrastructure is ready.
79
+ * Verifies that the ltree extension, role, table grants, and RLS policies were all created during bootstrap.
80
+ * Returns true immediately when disabled or after a previous successful check (cached).
81
+ *
82
+ * @example
83
+ * const ready = await matPath.isReady();
84
+ */
85
+ isReady(): Promise<boolean>;
86
+ }
87
+ //#endregion
20
88
  //#region src/index.d.ts
21
89
  type ArgsWithoutSequelize<T> = T extends ((arg1: Sequelize, ...args: infer U) => unknown) ? U : never;
22
90
  declare const _default: (sequelize: Sequelize, logger?: LoggerInstanceManager) => {
@@ -27,5 +95,5 @@ declare const _default: (sequelize: Sequelize, logger?: LoggerInstanceManager) =
27
95
  useOrCreateTransaction: <T>(transaction: Transaction | null | undefined, cb: (t: Transaction) => Promise<T>) => Promise<T>;
28
96
  };
29
97
  //#endregion
30
- export { _default as default };
98
+ export { type AuthMode, CONTEXT_PATHS_HEADER as CONTEXT_PATH_HEADER, type IdentityColumnConfig, MODE as MAT_PATH_MODE, MatPathManager, type MatPathOptions, type ModelPolicyConfig as TablePolicyConfig, type WithPathsOptions, _default as default };
31
99
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,2 +1,56 @@
1
- import e from"@autofleet/logger";import{DatabaseError as t}from"sequelize";import n from"debug";var r=e(),i=async(e,t)=>{t.transaction?t.transaction.afterCommit(()=>e()):await e()};const a=e=>e instanceof Date||Object.prototype.toString.call(e)===`[object Date]`,o=e=>{let t={};for(let[n,r]of Object.entries(e))t[n]=a(r)?r.getTime()/1e3:r;return t};var s=(e,t,n,a)=>{let s=a??r;if(!n){s.warn(`Events object must be provided to addModelEventHooks`);return}let c=(e,r=!1)=>{try{let i=e.get();r&&Object.assign(i,{deletedAt:new Date});let a=t[e.constructor.name]?.tableName,c=o(i);if(!a)return;n.sendObject(a,`1`,c,Object.keys(e.rawAttributes)).catch(e=>{s.error(`sendObject error`,{tableName:a,eventVersion:`1`,e})})}catch(e){s.error(`dimTables error`,{e})}};e.addHook(`afterSave`,(e,t)=>i(()=>c(e),t)),e.addHook(`afterDestroy`,(e,t)=>i(()=>c(e,!0),t))};const c=n(`sequelize-utils`),l=async(e,n,r=2,i={})=>{if(typeof n!=`function`)throw Error(`funcToRun must be a function`);if(typeof r!=`number`)throw Error(`if defined, retriesCount must be a number`);if(r<0)throw Error(`retriesCount must be a positive number`);try{return await e.transaction(i,async e=>await n(e))}catch(a){if((a instanceof t||a?.constructor?.name===`DatabaseError`)&&--r>=0)return c(`error inside transactionWithRetry - will retry times ${r}`,a),l(e,n,r,i);throw r===-1&&c(`error inside transactionWithRetry - will stop retry`,a),a}},u=`Transaction cancelled due to request cancellation`,d=(e,t,n,r)=>{let i=!1;if(t.socket.destroyed)throw c(u),Error(u);return e.transaction(async e=>{let a=async()=>{c(u),!i&&(i=!0,await e.rollback())},o=async()=>{if(!n.writableFinished){if(c(u),i)return;i=!0,await e.rollback()}};t.socket.once(`close`,a),n.once(`close`,o);let s=()=>{t.socket.removeListener(`close`,a),n.removeListener(`close`,o)};try{let t=await r(e);return s(),t}catch(e){throw s(),e.message.includes(`rollback has been called on this transaction`)?Error(u):e}})};var f=(e,t)=>{let n=(t,n,r)=>l(e,t,n,r);return{httpBasedTransaction:(t,n,r)=>d(e,t,n,r),transactionWithRetry:n,registerModelEventHooks:(n,r)=>s(e,n,r,t),runAfterTransactionCommits:i,useOrCreateTransaction:(e,t)=>e?t(e):n(t,0)}};export{f as default};
1
+ import e from"@autofleet/logger";import{DatabaseError as t,QueryTypes as n}from"sequelize";import r from"debug";import{getCurrentContext as i}from"@autofleet/outbreak";var a=e(),o=async(e,t)=>{t.transaction?t.transaction.afterCommit(()=>e()):await e()};const s=e=>e instanceof Date||Object.prototype.toString.call(e)===`[object Date]`,c=e=>{let t={};for(let[n,r]of Object.entries(e))t[n]=s(r)?r.getTime()/1e3:r;return t};var l=(e,t,n,r)=>{let i=r??a;if(!n){i.warn(`Events object must be provided to addModelEventHooks`);return}let s=(e,r=!1)=>{try{let a=e.get();r&&Object.assign(a,{deletedAt:new Date});let o=t[e.constructor.name]?.tableName,s=c(a);if(!o)return;n.sendObject(o,`1`,s,Object.keys(e.rawAttributes)).catch(e=>{i.error(`sendObject error`,{tableName:o,eventVersion:`1`,e})})}catch(e){i.error(`dimTables error`,{e})}};e.addHook(`afterSave`,(e,t)=>o(()=>s(e),t)),e.addHook(`afterDestroy`,(e,t)=>o(()=>s(e,!0),t))};const u=r(`sequelize-utils`),d=async(e,n,r=2,i={})=>{if(typeof n!=`function`)throw Error(`funcToRun must be a function`);if(typeof r!=`number`)throw Error(`if defined, retriesCount must be a number`);if(r<0)throw Error(`retriesCount must be a positive number`);try{return await e.transaction(i,async e=>await n(e))}catch(a){if((a instanceof t||a?.constructor?.name===`DatabaseError`)&&--r>=0)return u(`error inside transactionWithRetry - will retry times ${r}`,a),d(e,n,r,i);throw r===-1&&u(`error inside transactionWithRetry - will stop retry`,a),a}},f=`Transaction cancelled due to request cancellation`,p=(e,t,n,r)=>{let i=!1;if(t.socket.destroyed)throw u(f),Error(f);return e.transaction(async e=>{let a=async()=>{u(f),!i&&(i=!0,await e.rollback())},o=async()=>{if(!n.writableFinished){if(u(f),i)return;i=!0,await e.rollback()}};t.socket.once(`close`,a),n.once(`close`,o);let s=()=>{t.socket.removeListener(`close`,a),n.removeListener(`close`,o)};try{let t=await r(e);return s(),t}catch(e){throw s(),e.message.includes(`rollback has been called on this transaction`)?Error(f):e}})},m=`x-af-context-paths`,h=`mat_path.paths`,g=`mat_path.path_roots`,_=`mat_path.mode`,v=`mat_path_role`,y=`_mat_path_policy`,b={SCOPE:`scope`,INHERIT:`inherit`,BOTH:`both`},x=/^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/,S=new Set([b.SCOPE,b.INHERIT,b.BOTH]),C=e=>{for(let t of e)if(!x.test(t))throw Error(`Invalid materialized path: "${t}". Path must consist of alphanumeric/underscore segments separated by dots.`)},w=e=>{if(!S.has(e))throw Error(`Invalid auth mode: "${e}". Must be '${b.SCOPE}', '${b.INHERIT}', or '${b.BOTH}'.`)},T=e=>`{${e.join(`,`)}}`,E=e=>{let t=new Set;for(let n of e){let e=n.split(`.`)[0];t.add(e)}return Array.from(t)},D=()=>{let e=i()?.context?.get(m);if(!(typeof e!=`string`||!e?.length))return e.split(`,`)},O=async(e,t,n={})=>{let{mode:r=b.SCOPE,transaction:i}=n,a=D();if(!a)return t(i);C(a),w(r);let o=T(a),s=T(E(a)),c=async t=>{await e.query(`
2
+ SELECT
3
+ set_config('role', '${v}', true),
4
+ set_config('${h}', $1, true),
5
+ set_config('${g}', $2, true),
6
+ set_config('${_}', $3, true);
7
+ `,{bind:[o,s,r],transaction:t})};return i?(await c(i),t(i)):e.transaction(async e=>(await c(e),t(e)))},k=e=>{let t=e.getTableName();return typeof t==`string`?{tableName:t,schema:`public`}:{tableName:t.tableName,schema:t.schema??`public`}},A=`current_setting('${h}')::ltree[]`,j=e=>e?`AND ${e} = ANY(current_setting('${g}')::text[])`:``,M=(e,t)=>t===!1?`ALTER TABLE ${e} DISABLE ROW LEVEL SECURITY;`:`ALTER TABLE ${e} ENABLE ROW LEVEL SECURITY;`,N=e=>{let{model:t,identityColumns:n,rlsEnabled:r,clientPathRootColumn:i=``}=e,{tableName:a,schema:o}=k(t),s=`${a}${y}`,c=`${o}.${a}`,l=j(i);return`
8
+ ${M(c,r)}
9
+
10
+ DO $$
11
+ BEGIN
12
+ IF NOT EXISTS (
13
+ SELECT 1 FROM pg_policies
14
+ WHERE schemaname = '${o}'
15
+ AND tablename = '${a}'
16
+ AND policyname = '${s}'
17
+ ) THEN
18
+ CREATE POLICY ${s} ON ${c}
19
+ FOR ALL
20
+ TO ${v}
21
+ USING (
22
+ (${n.map(({column:e,entityType:t})=>`${e} = ANY(
23
+ ARRAY(
24
+ SELECT DISTINCT entity_id
25
+ FROM ${o}.context_scope_path
26
+ WHERE (
27
+ CASE current_setting('${_}')
28
+ WHEN '${b.SCOPE}' THEN path <@ ANY(${A})
29
+ WHEN '${b.INHERIT}' THEN path @> ANY(${A})
30
+ WHEN '${b.BOTH}' THEN (path <@ ANY(${A}) OR path @> ANY(${A}))
31
+ END
32
+ )
33
+ AND entity_type = '${t}'
34
+ AND deleted_at IS NULL
35
+ )
36
+ )`).join(`
37
+ OR `)})
38
+ ${l}
39
+ );
40
+ END IF;
41
+ END $$;
42
+ `},P=()=>`
43
+ DO $$
44
+ BEGIN
45
+ IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${v}') THEN
46
+ CREATE ROLE ${v} NOLOGIN;
47
+ END IF;
48
+ END
49
+ $$;`,F=()=>`GRANT ${v} TO CURRENT_USER;`,I=(e,t=`public`)=>`GRANT SELECT, INSERT, UPDATE, DELETE ON ${t}.${e} TO ${v};`,L=(e=`public`)=>`GRANT SELECT ON ${e}.context_scope_path TO ${v};`,R=()=>`SELECT pg_try_advisory_xact_lock(hashtext('mat_path_init')) AS successfully_locked;`,z=()=>`CREATE EXTENSION IF NOT EXISTS ltree;`,B=()=>`EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'ltree') AS "ltreeExists"`,V=()=>`EXISTS(SELECT 1 FROM pg_roles WHERE rolname = '${v}') AS "roleExists"`,H=({policyName:e,schema:t,tableName:n})=>`EXISTS(
50
+ SELECT 1 FROM pg_policies
51
+ WHERE policyname = '${e}'
52
+ AND schemaname = '${t}'
53
+ AND tablename = '${n}'
54
+ ) AS "${e}"`,U=({schema:e,tableName:t})=>`has_table_privilege('${v}', '${e}.${t}', 'SELECT,INSERT,UPDATE,DELETE') AS "grant_${e}.${t}"`,W=()=>`has_table_privilege('${v}', 'public.context_scope_path', 'SELECT') AS "grant_public.context_scope_path"`,G=e=>{let t=B(),n=V(),r=W(),i=e.map(H),a=e.map(U);return`SELECT ${[t,n,r,...i,...a].join(`,
55
+ `)}`};var K=class{#e;#t;#n;#r=!1;#i=!1;constructor(e){this.#e=e.sequelize,this.#t=e.models,this.#n=e.logger,this.enabled=process.env.USE_MATERIALIZED_PATH===`true`}#a(){let e=this.#t.map(({model:e})=>{let{tableName:t,schema:n}=k(e);return I(t,n)}),t=this.#t.map(e=>N(e));return[z(),P(),F(),L(),...e,...t].join(``)}async bootstrap(){if(!this.enabled){this.#n.debug(`Materialized Path: disabled, skipping bootstrap`);return}if(!this.#r)try{await this.#e.transaction(async e=>{let[{successfully_locked:t}]=await this.#e.query(R(),{transaction:e,type:n.SELECT});if(!t){this.#n.debug(`Materialized Path: another instance is bootstrapping, skipping`);return}await this.#e.query(this.#a(),{transaction:e}),this.#n.debug(`Materialized Path: bootstrap complete`)}),this.#r=!0}catch(e){throw this.#n.error(`Materialized Path: bootstrap failed`,{error:e}),e}}async withPaths(e,t={}){return this.enabled?O(this.#e,e,t):e(t.transaction)}async isReady(){if(!this.enabled||this.#i)return!0;if(!this.#r)return!1;try{let e=this.#t.map(({model:e})=>{let{tableName:t,schema:n}=k(e);return{policyName:`${t}${y}`,schema:n,tableName:t}}),[t]=await this.#e.query(G(e),{type:n.SELECT}),r=e.filter(({policyName:e})=>!t[e]).map(({policyName:e})=>e),i=e.filter(({schema:e,tableName:n})=>!t[`grant_${e}.${n}`]).map(({schema:e,tableName:t})=>`${e}.${t}`),a=t[`grant_public.context_scope_path`];return!t.ltreeExists||!t.roleExists||r.length||i.length||!a?(this.#n.warn(`Materialized Path: readiness check failed`,{ltreeExists:t.ltreeExists,roleExists:t.roleExists,contextScopePathGranted:a,missingPolicies:r,missingGrants:i}),!1):(this.#i=!0,!0)}catch(e){return this.#n.error(`Materialized Path: readiness check error`,{error:e}),!1}}},q=(e,t)=>{let n=(t,n,r)=>d(e,t,n,r);return{httpBasedTransaction:(t,n,r)=>p(e,t,n,r),transactionWithRetry:n,registerModelEventHooks:(n,r)=>l(e,n,r,t),runAfterTransactionCommits:o,useOrCreateTransaction:(e,t)=>e?t(e):n(t,0)}};export{m as CONTEXT_PATH_HEADER,b as MAT_PATH_MODE,K as MatPathManager,q as default};
2
56
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["defaultExport: LoggerInstanceManager","newObj: Record<string, unknown>","packageLogger","runAfterTransactionCommits","debugLog: debug.Debugger","transactionWithRetry","_transactionWithRetry","httpBasedTransaction","_httpBasedTransaction","addModelEventHooks"],"sources":["../src/logger.ts","../src/runAfterTransactionCommits.ts","../src/model-event-hooks.ts","../src/common.ts","../src/transaction-with-retry.ts","../src/http-based-transaction.ts","../src/index.ts"],"sourcesContent":["import Logger, { type LoggerInstanceManager } from '@autofleet/logger';\n\nconst defaultExport: LoggerInstanceManager = Logger();\nexport default defaultExport;\n","import type { Transactionable } from 'sequelize';\n\nconst runAfterTransactionCommits = async (cb: () => void | Promise<void>, options: Transactionable): Promise<void> => {\n if (options.transaction) {\n options.transaction.afterCommit(() => cb());\n } else {\n await cb();\n }\n};\nexport default runAfterTransactionCommits;\n","import type { Model, Sequelize } from 'sequelize';\nimport type Events from '@autofleet/events';\nimport type { LoggerInstanceManager } from '@autofleet/logger';\nimport packageLogger from './logger';\nimport runAfterTransactionCommits from './runAfterTransactionCommits';\n\nexport type ModelMapping = Record<string, { tableName: string; }>;\n\nconst isDate = (input: unknown): input is Date => input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';\nconst formatDatesInObject = (obj: object) => {\n const newObj: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n const valueToUse = isDate(value) ? value.getTime() / 1000 : value;\n newObj[key] = valueToUse;\n }\n return newObj;\n};\n\nconst addModelEventHooks = (sequelize: Sequelize, modelTableMapping: ModelMapping, events: Events, logger?: LoggerInstanceManager): void => {\n const localLogger = logger ?? packageLogger;\n if (!events) {\n localLogger.warn('Events object must be provided to addModelEventHooks');\n return;\n }\n const updateEventToDimTable = (object: Model, isDelete = false) => {\n try {\n const objectToSend = object.get();\n if (isDelete) {\n Object.assign(objectToSend, { deletedAt: new Date() });\n }\n const tableName = modelTableMapping[object.constructor.name]?.tableName;\n const eventVersion = '1';\n const objectToSendFormatted = formatDatesInObject(objectToSend);\n if (!tableName) {\n return;\n }\n events.sendObject(\n tableName,\n eventVersion,\n objectToSendFormatted,\n // @ts-expect-error the rawAttributes is typed as static, while it actually is on the instance level.\n Object.keys(object.rawAttributes),\n ).catch((e) => {\n localLogger.error('sendObject error', { tableName, eventVersion, e });\n });\n } catch (e) {\n localLogger.error('dimTables error', { e });\n }\n };\n\n sequelize.addHook('afterSave', (savedObject, options) => runAfterTransactionCommits(() => updateEventToDimTable(savedObject), options));\n sequelize.addHook('afterDestroy', (savedObject, options) => runAfterTransactionCommits(() => updateEventToDimTable(savedObject, true), options));\n};\n\nexport default addModelEventHooks;\n","import debug from 'debug';\n\nexport const debugLog: debug.Debugger = debug('sequelize-utils');\n","import {\n DatabaseError, type Sequelize, type Transaction, type TransactionOptions,\n} from 'sequelize';\nimport { debugLog } from './common';\n\nexport const transactionWithRetry = async <T>(sequelize: Sequelize, funcToRun: (transaction: Transaction) => Promise<T>, retriesCount = 2, options: TransactionOptions = {}): Promise<T> => {\n if (typeof funcToRun !== 'function') {\n throw new Error('funcToRun must be a function');\n }\n if (typeof retriesCount !== 'number') {\n throw new Error('if defined, retriesCount must be a number');\n }\n if (retriesCount < 0) {\n throw new Error('retriesCount must be a positive number');\n }\n try {\n const transValue = await sequelize.transaction(options, async (transaction) => {\n const funcValue = await funcToRun(transaction);\n return funcValue;\n });\n return transValue;\n } catch (e) {\n if ((e instanceof DatabaseError || e?.constructor?.name === 'DatabaseError') && --retriesCount >= 0) {\n debugLog(`error inside transactionWithRetry - will retry times ${retriesCount}`, e);\n return transactionWithRetry(sequelize, funcToRun, retriesCount, options);\n }\n if (retriesCount === -1) {\n debugLog('error inside transactionWithRetry - will stop retry', e);\n }\n throw e;\n }\n};\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { Sequelize, Transaction } from 'sequelize';\nimport { debugLog } from './common';\n\nconst rollbackErrorText = 'rollback has been called on this transaction';\nconst abortErrorText = 'Transaction cancelled due to request cancellation';\n\nexport const httpBasedTransaction = <T>(sequelize: Sequelize, req: IncomingMessage, res: ServerResponse, cb: (transaction: Transaction) => Promise<T>): Promise<T> => {\n let aborted = false;\n if (req.socket.destroyed) {\n debugLog(abortErrorText);\n throw new Error(abortErrorText);\n }\n return sequelize.transaction(async (transaction: Transaction) => {\n // https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/test/parallel/test-http-aborted.js#L9\n\n // 2023-04-13: Node.js 16.0.0\n // Added also the `res.writableFinished` check, because it seems that the socket is not destroyed when the request is aborted, but the response is not finished.\n // https://github.com/Jimbly/http-proxy-node16/commit/ba0c414cd03799e357c5d867c11dea06a9c34ec8#diff-b2d1e3b7c5f3b424a0af7971c582c822fd25a5ea279040ef6dc93fc4b2cf619dR151\n\n const rollback = async () => {\n debugLog(abortErrorText);\n if (aborted) return;\n aborted = true;\n await transaction.rollback();\n };\n const resRollback = async () => {\n const didNotFinishWrite = !res.writableFinished;\n if (didNotFinishWrite) {\n debugLog(abortErrorText);\n if (aborted) return;\n aborted = true;\n await transaction.rollback();\n }\n };\n req.socket.once('close', rollback);\n res.once('close', resRollback);\n\n const removeListeners = () => {\n req.socket.removeListener('close', rollback);\n res.removeListener('close', resRollback);\n };\n\n try {\n const response = await cb(transaction);\n removeListeners();\n return response;\n } catch (e) {\n removeListeners();\n if (e.message.includes(rollbackErrorText)) {\n throw new Error(abortErrorText);\n }\n throw e;\n }\n });\n};\n","import type {\n Sequelize,\n Transaction,\n TransactionOptions,\n} from 'sequelize';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { LoggerInstanceManager } from '@autofleet/logger';\nimport type Events from '@autofleet/events';\nimport addModelEventHooks, { type ModelMapping } from './model-event-hooks';\nimport { transactionWithRetry as _transactionWithRetry } from './transaction-with-retry';\nimport { httpBasedTransaction as _httpBasedTransaction } from './http-based-transaction';\nimport runAfterTransactionCommits from './runAfterTransactionCommits';\n\ntype ArgsWithoutSequelize<T> = T extends (arg1: Sequelize, ...args: infer U) => unknown ? U : never;\n\nexport default (sequelize: Sequelize, logger?: LoggerInstanceManager): {\n transactionWithRetry: <T>(...args: ArgsWithoutSequelize<typeof _transactionWithRetry<T>>) => Promise<T>;\n httpBasedTransaction: <T>(...args: ArgsWithoutSequelize<typeof _httpBasedTransaction<T>>) => Promise<T>;\n registerModelEventHooks: (modelTableMapping: ModelMapping, events: Events) => void;\n runAfterTransactionCommits: typeof runAfterTransactionCommits;\n useOrCreateTransaction: <T>(transaction: Transaction | null | undefined, cb: (t: Transaction) => Promise<T>) => Promise<T>;\n} => {\n const transactionWithRetry = <T>(\n funcToRun: (transaction: Transaction) => Promise<T>,\n retriesCount?: number,\n options?: TransactionOptions,\n ): Promise<T> => _transactionWithRetry(sequelize, funcToRun, retriesCount, options);\n const httpBasedTransaction = <T>(req: IncomingMessage, res: ServerResponse, cb: (transaction: Transaction) => Promise<T>): Promise<T> => _httpBasedTransaction(sequelize, req, res, cb);\n const registerModelEventHooks = (modelTableMapping: ModelMapping, events: Events) => addModelEventHooks(sequelize, modelTableMapping, events, logger);\n const useOrCreateTransaction = <T>(transaction: Transaction | null | undefined, cb: (t: Transaction) => Promise<T>) => {\n if (transaction) {\n return cb(transaction);\n }\n return transactionWithRetry(cb, 0);\n };\n\n return {\n httpBasedTransaction,\n transactionWithRetry,\n registerModelEventHooks,\n runAfterTransactionCommits,\n useOrCreateTransaction,\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["defaultExport: LoggerInstanceManager","newObj: Record<string, unknown>","packageLogger","runAfterTransactionCommits","debugLog: debug.Debugger","VALID_MODES: ReadonlySet<AuthMode>","#sequelize","#modelConfigs","#logger","#bootstrapped","#buildBootstrapSQL","#isReady","transactionWithRetry","_transactionWithRetry","httpBasedTransaction","_httpBasedTransaction","addModelEventHooks"],"sources":["../src/logger.ts","../src/runAfterTransactionCommits.ts","../src/model-event-hooks.ts","../src/common.ts","../src/transaction-with-retry.ts","../src/http-based-transaction.ts","../src/materialized-path/constants.ts","../src/materialized-path/with-paths.ts","../src/materialized-path/utils/utils.ts","../src/materialized-path/sql-generators/generate-policy.ts","../src/materialized-path/sql-generators/generate-role.ts","../src/materialized-path/sql-generators/utils.ts","../src/materialized-path/sql-generators/generate-ready-check.ts","../src/materialized-path/mat-path-manager.ts","../src/index.ts"],"sourcesContent":["import Logger, { type LoggerInstanceManager } from '@autofleet/logger';\n\nconst defaultExport: LoggerInstanceManager = Logger();\nexport default defaultExport;\n","import type { Transactionable } from 'sequelize';\n\nconst runAfterTransactionCommits = async (cb: () => void | Promise<void>, options: Transactionable): Promise<void> => {\n if (options.transaction) {\n options.transaction.afterCommit(() => cb());\n } else {\n await cb();\n }\n};\nexport default runAfterTransactionCommits;\n","import type { Model, Sequelize } from 'sequelize';\nimport type Events from '@autofleet/events';\nimport type { LoggerInstanceManager } from '@autofleet/logger';\nimport packageLogger from './logger';\nimport runAfterTransactionCommits from './runAfterTransactionCommits';\n\nexport type ModelMapping = Record<string, { tableName: string; }>;\n\nconst isDate = (input: unknown): input is Date => input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';\nconst formatDatesInObject = (obj: object) => {\n const newObj: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n const valueToUse = isDate(value) ? value.getTime() / 1000 : value;\n newObj[key] = valueToUse;\n }\n return newObj;\n};\n\nconst addModelEventHooks = (sequelize: Sequelize, modelTableMapping: ModelMapping, events: Events, logger?: LoggerInstanceManager): void => {\n const localLogger = logger ?? packageLogger;\n if (!events) {\n localLogger.warn('Events object must be provided to addModelEventHooks');\n return;\n }\n const updateEventToDimTable = (object: Model, isDelete = false) => {\n try {\n const objectToSend = object.get();\n if (isDelete) {\n Object.assign(objectToSend, { deletedAt: new Date() });\n }\n const tableName = modelTableMapping[object.constructor.name]?.tableName;\n const eventVersion = '1';\n const objectToSendFormatted = formatDatesInObject(objectToSend);\n if (!tableName) {\n return;\n }\n events.sendObject(\n tableName,\n eventVersion,\n objectToSendFormatted,\n // @ts-expect-error the rawAttributes is typed as static, while it actually is on the instance level.\n Object.keys(object.rawAttributes),\n ).catch((e) => {\n localLogger.error('sendObject error', { tableName, eventVersion, e });\n });\n } catch (e) {\n localLogger.error('dimTables error', { e });\n }\n };\n\n sequelize.addHook('afterSave', (savedObject, options) => runAfterTransactionCommits(() => updateEventToDimTable(savedObject), options));\n sequelize.addHook('afterDestroy', (savedObject, options) => runAfterTransactionCommits(() => updateEventToDimTable(savedObject, true), options));\n};\n\nexport default addModelEventHooks;\n","import debug from 'debug';\n\nexport const debugLog: debug.Debugger = debug('sequelize-utils');\n","import {\n DatabaseError, type Sequelize, type Transaction, type TransactionOptions,\n} from 'sequelize';\nimport { debugLog } from './common';\n\nexport const transactionWithRetry = async <T>(sequelize: Sequelize, funcToRun: (transaction: Transaction) => Promise<T>, retriesCount = 2, options: TransactionOptions = {}): Promise<T> => {\n if (typeof funcToRun !== 'function') {\n throw new Error('funcToRun must be a function');\n }\n if (typeof retriesCount !== 'number') {\n throw new Error('if defined, retriesCount must be a number');\n }\n if (retriesCount < 0) {\n throw new Error('retriesCount must be a positive number');\n }\n try {\n const transValue = await sequelize.transaction(options, async (transaction) => {\n const funcValue = await funcToRun(transaction);\n return funcValue;\n });\n return transValue;\n } catch (e) {\n if ((e instanceof DatabaseError || e?.constructor?.name === 'DatabaseError') && --retriesCount >= 0) {\n debugLog(`error inside transactionWithRetry - will retry times ${retriesCount}`, e);\n return transactionWithRetry(sequelize, funcToRun, retriesCount, options);\n }\n if (retriesCount === -1) {\n debugLog('error inside transactionWithRetry - will stop retry', e);\n }\n throw e;\n }\n};\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { Sequelize, Transaction } from 'sequelize';\nimport { debugLog } from './common';\n\nconst rollbackErrorText = 'rollback has been called on this transaction';\nconst abortErrorText = 'Transaction cancelled due to request cancellation';\n\nexport const httpBasedTransaction = <T>(sequelize: Sequelize, req: IncomingMessage, res: ServerResponse, cb: (transaction: Transaction) => Promise<T>): Promise<T> => {\n let aborted = false;\n if (req.socket.destroyed) {\n debugLog(abortErrorText);\n throw new Error(abortErrorText);\n }\n return sequelize.transaction(async (transaction: Transaction) => {\n // https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/test/parallel/test-http-aborted.js#L9\n\n // 2023-04-13: Node.js 16.0.0\n // Added also the `res.writableFinished` check, because it seems that the socket is not destroyed when the request is aborted, but the response is not finished.\n // https://github.com/Jimbly/http-proxy-node16/commit/ba0c414cd03799e357c5d867c11dea06a9c34ec8#diff-b2d1e3b7c5f3b424a0af7971c582c822fd25a5ea279040ef6dc93fc4b2cf619dR151\n\n const rollback = async () => {\n debugLog(abortErrorText);\n if (aborted) return;\n aborted = true;\n await transaction.rollback();\n };\n const resRollback = async () => {\n const didNotFinishWrite = !res.writableFinished;\n if (didNotFinishWrite) {\n debugLog(abortErrorText);\n if (aborted) return;\n aborted = true;\n await transaction.rollback();\n }\n };\n req.socket.once('close', rollback);\n res.once('close', resRollback);\n\n const removeListeners = () => {\n req.socket.removeListener('close', rollback);\n res.removeListener('close', resRollback);\n };\n\n try {\n const response = await cb(transaction);\n removeListeners();\n return response;\n } catch (e) {\n removeListeners();\n if (e.message.includes(rollbackErrorText)) {\n throw new Error(abortErrorText);\n }\n throw e;\n }\n });\n};\n","export const CONTEXT_PATHS_HEADER = 'x-af-context-paths';\n\nexport const PATHS_SETTING = 'mat_path.paths';\nexport const PATH_ROOTS_SETTING = 'mat_path.path_roots';\nexport const MODE_SETTING = 'mat_path.mode';\nexport const ROLE = 'mat_path_role';\nexport const ADVISORY_LOCK_KEY = 'mat_path_init';\nexport const POLICY_NAME_SUFFIX = '_mat_path_policy';\n\nexport const FEATURE_FLAG_ENV = 'USE_MATERIALIZED_PATH';\nexport const MODE = {\n SCOPE: 'scope',\n INHERIT: 'inherit',\n BOTH: 'both',\n} as const;\n","import type { Sequelize, Transaction } from 'sequelize';\nimport type { AuthMode, SequelizeQueryCallback, WithPathsOptions } from './types.js';\nimport { CONTEXT_PATHS_HEADER, MODE, MODE_SETTING, PATHS_SETTING, PATH_ROOTS_SETTING, ROLE } from './constants.js';\nimport { getCurrentContext } from '@autofleet/outbreak';\n\n/** Validates the provided string is a valid ltree label, consisting of alphanumeric/underscore segments separated by dots. */\nconst VALID_PATH_REGEX = /^[A-Za-z0-9_]+(\\.[A-Za-z0-9_]+)*$/;\nconst VALID_MODES: ReadonlySet<AuthMode> = new Set([MODE.SCOPE, MODE.INHERIT, MODE.BOTH]);\n\nconst validatePaths = (paths: string[]): void => {\n for (const path of paths) {\n if (!VALID_PATH_REGEX.test(path)) {\n throw new Error(\n `Invalid materialized path: \"${path}\". `\n + 'Path must consist of alphanumeric/underscore segments separated by dots.',\n );\n }\n }\n};\n\nexport const validateMode = (mode: AuthMode): void => {\n if (!VALID_MODES.has(mode)) {\n throw new Error(`Invalid auth mode: \"${mode}\". Must be '${MODE.SCOPE}', '${MODE.INHERIT}', or '${MODE.BOTH}'.`);\n }\n};\n\nconst toArrayLiteral = (items: string[]): string => `{${items.join(',')}}`;\n\n/** Extracts the unique root segments from the provided paths. For example, given ['AF.1', 'AF.2', 'BF.1'], it will return ['AF', 'BF']. */\nconst getPathRoots = (paths: string[]): string[] => {\n const roots = new Set<string>();\n\n for (const path of paths) {\n const root = path.split('.')[0];\n roots.add(root);\n }\n\n return Array.from(roots);\n};\n\nconst getContextPath = (): string[] | undefined => {\n const trace = getCurrentContext();\n const value = trace?.context?.get(CONTEXT_PATHS_HEADER);\n if (typeof value !== 'string' || !value?.length) return undefined;\n return value.split(',');\n};\n\n/**\n * Sets transaction-local config variables needed for RLS policy enforcement, then runs the callback.\n * The callback should directly perform a Sequelize query (e.g. `findAll`, `create`, `update`) using the provided transaction.\n * If no transaction is provided, creates one. Otherwise reuses the existing transaction and issues `RESET ROLE` on completion.\n */\nexport const withPaths = async <T>(\n sequelize: Sequelize,\n callback: SequelizeQueryCallback<T>,\n options: WithPathsOptions = {},\n): Promise<T> => {\n const { mode = MODE.SCOPE, transaction: existingTransaction } = options;\n const paths = getContextPath();\n\n if (!paths) return callback(existingTransaction);\n\n validatePaths(paths);\n validateMode(mode);\n\n const pathArrayLiteral = toArrayLiteral(paths);\n const roots = getPathRoots(paths);\n const rootsArrayLiteral = toArrayLiteral(roots);\n\n const setLocalConfig = async (transaction: Transaction): Promise<void> => {\n await sequelize.query(\n `\n SELECT\n set_config('role', '${ROLE}', true),\n set_config('${PATHS_SETTING}', $1, true),\n set_config('${PATH_ROOTS_SETTING}', $2, true),\n set_config('${MODE_SETTING}', $3, true);\n `,\n { bind: [pathArrayLiteral, rootsArrayLiteral, mode], transaction },\n );\n };\n\n if (!existingTransaction) {\n return sequelize.transaction(async (transaction) => {\n await setLocalConfig(transaction);\n return callback(transaction);\n });\n }\n\n await setLocalConfig(existingTransaction);\n return callback(existingTransaction);\n};\n","import type { Model, ModelStatic } from 'sequelize';\nimport type { ResolvedTableInfo } from '../types';\nimport { getCurrentContext } from '@autofleet/outbreak';\nimport { CONTEXT_PATHS_HEADER } from '../constants';\n\nexport const resolveTableInfo = (model: ModelStatic<Model>): ResolvedTableInfo => {\n const tableInfo = model.getTableName();\n if (typeof tableInfo === 'string') return { tableName: tableInfo, schema: 'public' };\n return { tableName: tableInfo.tableName, schema: tableInfo.schema ?? 'public' };\n};\n\nexport const getContextPath = (): string | undefined => {\n const trace = getCurrentContext();\n const value = trace?.context?.get(CONTEXT_PATHS_HEADER);\n if (typeof value !== 'string' || !value?.length) return undefined;\n return value;\n};\n\nexport const setContextPath = (path: string): void => {\n const trace = getCurrentContext();\n trace?.context?.set(CONTEXT_PATHS_HEADER, path);\n};\n","import type { ModelPolicyConfig } from '../types';\nimport { resolveTableInfo } from '../utils/utils';\nimport { MODE, PATHS_SETTING, PATH_ROOTS_SETTING, MODE_SETTING, ROLE, POLICY_NAME_SUFFIX } from '../constants';\n\nconst PATHS_SETTING_FILTER = `current_setting('${PATHS_SETTING}')::ltree[]`;\n\n/**\n * Generates a `clientPathRootColumn` filter if the model config specifies a column for it.\n */\nconst generateClientPathRootFilter = (clientPathRootColumn: string): string => {\n if (!clientPathRootColumn) return '';\n\n return `AND ${clientPathRootColumn} = ANY(current_setting('${PATH_ROOTS_SETTING}')::text[])`;\n};\n\n/**\n * Generates the SQL to enable or disable RLS on the target table based on the `rlsEnabled` flag in the model config.\n * If `rlsEnabled` is `true` or `undefined`, RLS is enabled. If `false`, RLS is disabled.\n */\nconst generateRLSControl = (qualifiedTable: string, rlsEnabled?: boolean): string => {\n if (rlsEnabled === false) return `ALTER TABLE ${qualifiedTable} DISABLE ROW LEVEL SECURITY;`;\n\n return `ALTER TABLE ${qualifiedTable} ENABLE ROW LEVEL SECURITY;`;\n};\n\n/**\n * Generates the RLS policy SQL for a single table.\n *\n * The policy uses CASE current_setting('mat_path.mode') inside the InitPlan\n * subqueries to support per-query {@link MODE} switching.\n */\nexport const generatePolicySQL = (config: ModelPolicyConfig): string => {\n const { model, identityColumns, rlsEnabled, clientPathRootColumn = '' } = config;\n const { tableName, schema } = resolveTableInfo(model);\n const policyName = `${tableName}${POLICY_NAME_SUFFIX}`;\n\n const qualifiedTable = `${schema}.${tableName}`;\n const clientPathRootFilter = generateClientPathRootFilter(clientPathRootColumn);\n const rlsControlSQL = generateRLSControl(qualifiedTable, rlsEnabled);\n\n const identityChecks = identityColumns\n .map(({ column, entityType }) => `${column} = ANY(\n ARRAY(\n SELECT DISTINCT entity_id\n FROM ${schema}.context_scope_path\n WHERE (\n CASE current_setting('${MODE_SETTING}')\n WHEN '${MODE.SCOPE}' THEN path <@ ANY(${PATHS_SETTING_FILTER})\n WHEN '${MODE.INHERIT}' THEN path @> ANY(${PATHS_SETTING_FILTER})\n WHEN '${MODE.BOTH}' THEN (path <@ ANY(${PATHS_SETTING_FILTER}) OR path @> ANY(${PATHS_SETTING_FILTER}))\n END\n )\n AND entity_type = '${entityType}'\n AND deleted_at IS NULL\n )\n )`)\n .join('\\n OR ');\n\n return `\n ${rlsControlSQL}\n\n DO $$\n BEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_policies\n WHERE schemaname = '${schema}'\n AND tablename = '${tableName}'\n AND policyname = '${policyName}'\n ) THEN\n CREATE POLICY ${policyName} ON ${qualifiedTable}\n FOR ALL\n TO ${ROLE}\n USING (\n (${identityChecks})\n ${clientPathRootFilter}\n );\n END IF;\n END $$;\n `;\n};\n","import { ROLE } from '../constants';\n\nexport const generateCreateRoleSQL = (): string => `\n DO $$\n BEGIN\n IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${ROLE}') THEN\n CREATE ROLE ${ROLE} NOLOGIN;\n END IF;\n END\n $$;`;\n\nexport const generateGrantRoleSQL = (): string => `GRANT ${ROLE} TO CURRENT_USER;`;\n\nexport const generateGrantTableSQL = (\n tableName: string,\n schema = 'public',\n): string => `GRANT SELECT, INSERT, UPDATE, DELETE ON ${schema}.${tableName} TO ${ROLE};`;\n\nexport const generateGrantContextScopePathSQL = (\n schema = 'public',\n): string => `GRANT SELECT ON ${schema}.context_scope_path TO ${ROLE};`;\n","import { ADVISORY_LOCK_KEY } from '../constants';\n\nexport const generateTryAdvisoryLockSQL = (): string => `SELECT pg_try_advisory_xact_lock(hashtext('${ADVISORY_LOCK_KEY}')) AS successfully_locked;`;\n\nexport const generateCreateLtreeExtensionSQL = (): string => `CREATE EXTENSION IF NOT EXISTS ltree;`;\nexport const generateLtreeExtensionEnabledSQL = (): string => `SELECT extname FROM pg_extension WHERE extname = 'ltree';`;\n","import { ROLE } from '../constants';\n\ninterface TablePolicyInfo {\n policyName: string;\n schema: string;\n tableName: string;\n}\n\nconst generateLtreeExtensionCheck = (): string =>\n `EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'ltree') AS \"ltreeExists\"`;\n\nconst generateRoleExistenceCheck = (): string =>\n `EXISTS(SELECT 1 FROM pg_roles WHERE rolname = '${ROLE}') AS \"roleExists\"`;\n\nconst generatePolicyExistenceCheck = ({ policyName, schema, tableName }: TablePolicyInfo): string =>\n `EXISTS(\n SELECT 1 FROM pg_policies\n WHERE policyname = '${policyName}'\n AND schemaname = '${schema}'\n AND tablename = '${tableName}'\n ) AS \"${policyName}\"`;\n\nconst generateGrantCheck = ({ schema, tableName }: TablePolicyInfo): string =>\n `has_table_privilege('${ROLE}', '${schema}.${tableName}', 'SELECT,INSERT,UPDATE,DELETE') AS \"grant_${schema}.${tableName}\"`;\n\nconst generateContextScopePathGrantCheck = (): string =>\n `has_table_privilege('${ROLE}', 'public.context_scope_path', 'SELECT') AS \"grant_public.context_scope_path\"`;\n\nexport const generateReadinessCheckSQL = (expectedPolicies: TablePolicyInfo[]): string => {\n const ltreeCheck = generateLtreeExtensionCheck();\n\n const roleCheck = generateRoleExistenceCheck();\n\n const contextScopePathCheck = generateContextScopePathGrantCheck();\n\n const policyChecks = expectedPolicies.map(generatePolicyExistenceCheck);\n\n const grantChecks = expectedPolicies.map(generateGrantCheck);\n\n return `SELECT ${[ltreeCheck, roleCheck, contextScopePathCheck, ...policyChecks, ...grantChecks].join(',\\n')}`;\n};\n","import { QueryTypes, type Sequelize } from 'sequelize';\nimport type { LoggerInstanceManager } from '@autofleet/logger';\nimport type { MatPathOptions, ModelPolicyConfig, SequelizeQueryCallback, WithPathsOptions } from './types';\nimport { withPaths } from './with-paths';\nimport { generatePolicySQL } from './sql-generators/generate-policy';\nimport {\n generateCreateRoleSQL,\n generateGrantRoleSQL,\n generateGrantTableSQL,\n generateGrantContextScopePathSQL,\n} from './sql-generators/generate-role';\nimport { FEATURE_FLAG_ENV, POLICY_NAME_SUFFIX } from './constants';\nimport { generateTryAdvisoryLockSQL, generateCreateLtreeExtensionSQL } from './sql-generators/utils';\nimport { resolveTableInfo } from './utils/utils';\nimport { generateReadinessCheckSQL } from './sql-generators/generate-ready-check';\n\nexport class MatPathManager {\n readonly #sequelize: Sequelize;\n readonly #modelConfigs: ModelPolicyConfig[];\n readonly #logger: LoggerInstanceManager;\n readonly enabled: boolean;\n #bootstrapped = false;\n #isReady = false;\n\n constructor(options: MatPathOptions) {\n this.#sequelize = options.sequelize;\n this.#modelConfigs = options.models;\n this.#logger = options.logger;\n this.enabled = process.env[FEATURE_FLAG_ENV] === 'true';\n }\n\n #buildBootstrapSQL(): string {\n const grantTableSQL = this.#modelConfigs.map(({ model }) => {\n const { tableName, schema } = resolveTableInfo(model);\n return generateGrantTableSQL(tableName, schema);\n });\n\n const policySQL = this.#modelConfigs.map(modelConfig => generatePolicySQL(modelConfig));\n\n return [\n generateCreateLtreeExtensionSQL(),\n generateCreateRoleSQL(),\n generateGrantRoleSQL(),\n generateGrantContextScopePathSQL(),\n ...grantTableSQL,\n ...policySQL,\n ].join('');\n }\n\n /** Idempotent and concurrency-safe function that sets up the database for Materialized Path usage via RLS enforcement (role, grants, policies). */\n async bootstrap(): Promise<void> {\n if (!this.enabled) {\n this.#logger.debug('Materialized Path: disabled, skipping bootstrap');\n return;\n }\n if (this.#bootstrapped) return;\n\n try {\n await this.#sequelize.transaction(async (transaction) => {\n const [{ successfully_locked: successfullyLocked }] = await this.#sequelize.query<{ successfully_locked: boolean; }>(\n generateTryAdvisoryLockSQL(),\n { transaction, type: QueryTypes.SELECT },\n );\n\n if (!successfullyLocked) {\n this.#logger.debug('Materialized Path: another instance is bootstrapping, skipping');\n return;\n }\n\n await this.#sequelize.query(this.#buildBootstrapSQL(), { transaction });\n this.#logger.debug('Materialized Path: bootstrap complete');\n });\n\n this.#bootstrapped = true;\n } catch (error) {\n this.#logger.error('Materialized Path: bootstrap failed', { error });\n throw error;\n }\n }\n\n /**\n * Wraps a callback with the necessary local config setup to enforce RLS policies based on the provided paths and mode.\n * The callback should directly perform a Sequelize query (e.g. `findAll`, `create`, `update`) using the provided transaction.\n * When disabled, runs the callback without RLS.\n */\n async withPaths<T>(\n callback: SequelizeQueryCallback<T>,\n options: WithPathsOptions = {},\n ): Promise<T> {\n if (!this.enabled) return callback(options.transaction);\n\n return withPaths(this.#sequelize, callback, options);\n }\n\n /**\n * Check if the materialized path infrastructure is ready.\n * Verifies that the ltree extension, role, table grants, and RLS policies were all created during bootstrap.\n * Returns true immediately when disabled or after a previous successful check (cached).\n *\n * @example\n * const ready = await matPath.isReady();\n */\n async isReady(): Promise<boolean> {\n if (!this.enabled || this.#isReady) return true;\n if (!this.#bootstrapped) return false;\n\n try {\n const expectedPolicies = this.#modelConfigs.map(({ model }) => {\n const { tableName, schema } = resolveTableInfo(model);\n return { policyName: `${tableName}${POLICY_NAME_SUFFIX}`, schema, tableName };\n });\n\n const [result] = await this.#sequelize.query<Record<string, boolean>>(\n generateReadinessCheckSQL(expectedPolicies),\n { type: QueryTypes.SELECT },\n );\n\n const missingPolicies = expectedPolicies\n .filter(({ policyName }) => !result[policyName])\n .map(({ policyName }) => policyName);\n\n const missingGrants = expectedPolicies\n .filter(({ schema, tableName }) => !result[`grant_${schema}.${tableName}`])\n .map(({ schema, tableName }) => `${schema}.${tableName}`);\n\n const contextScopePathGranted = result['grant_public.context_scope_path'];\n\n if (!result.ltreeExists || !result.roleExists || missingPolicies.length || missingGrants.length || !contextScopePathGranted) {\n this.#logger.warn('Materialized Path: readiness check failed', {\n ltreeExists: result.ltreeExists,\n roleExists: result.roleExists,\n contextScopePathGranted,\n missingPolicies,\n missingGrants,\n });\n return false;\n }\n\n this.#isReady = true;\n return true;\n } catch (error) {\n this.#logger.error('Materialized Path: readiness check error', { error });\n return false;\n }\n }\n}\n","import type {\n Sequelize,\n Transaction,\n TransactionOptions,\n} from 'sequelize';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { LoggerInstanceManager } from '@autofleet/logger';\nimport type Events from '@autofleet/events';\nimport addModelEventHooks, { type ModelMapping } from './model-event-hooks';\nimport { transactionWithRetry as _transactionWithRetry } from './transaction-with-retry';\nimport { httpBasedTransaction as _httpBasedTransaction } from './http-based-transaction';\nimport runAfterTransactionCommits from './runAfterTransactionCommits';\n\ntype ArgsWithoutSequelize<T> = T extends (arg1: Sequelize, ...args: infer U) => unknown ? U : never;\n\nexport default (sequelize: Sequelize, logger?: LoggerInstanceManager): {\n transactionWithRetry: <T>(...args: ArgsWithoutSequelize<typeof _transactionWithRetry<T>>) => Promise<T>;\n httpBasedTransaction: <T>(...args: ArgsWithoutSequelize<typeof _httpBasedTransaction<T>>) => Promise<T>;\n registerModelEventHooks: (modelTableMapping: ModelMapping, events: Events) => void;\n runAfterTransactionCommits: typeof runAfterTransactionCommits;\n useOrCreateTransaction: <T>(transaction: Transaction | null | undefined, cb: (t: Transaction) => Promise<T>) => Promise<T>;\n} => {\n const transactionWithRetry = <T>(\n funcToRun: (transaction: Transaction) => Promise<T>,\n retriesCount?: number,\n options?: TransactionOptions,\n ): Promise<T> => _transactionWithRetry(sequelize, funcToRun, retriesCount, options);\n const httpBasedTransaction = <T>(req: IncomingMessage, res: ServerResponse, cb: (transaction: Transaction) => Promise<T>): Promise<T> => _httpBasedTransaction(sequelize, req, res, cb);\n const registerModelEventHooks = (modelTableMapping: ModelMapping, events: Events) => addModelEventHooks(sequelize, modelTableMapping, events, logger);\n const useOrCreateTransaction = <T>(transaction: Transaction | null | undefined, cb: (t: Transaction) => Promise<T>) => {\n if (transaction) {\n return cb(transaction);\n }\n return transactionWithRetry(cb, 0);\n };\n\n return {\n httpBasedTransaction,\n transactionWithRetry,\n registerModelEventHooks,\n runAfterTransactionCommits,\n useOrCreateTransaction,\n };\n};\n\nexport * from './materialized-path';\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,11 +1,8 @@
1
1
  {
2
2
  "name": "@autofleet/sequelize-utils",
3
- "version": "6.2.0-alpha.2",
3
+ "version": "6.2.1-beta-416ae768.0",
4
4
  "description": "",
5
5
  "type": "module",
6
- "bin": {
7
- "sequelize-utils": "./dist/cli.js"
8
- },
9
6
  "main": "dist/index.js",
10
7
  "types": "dist/index.d.ts",
11
8
  "exports": {
@@ -37,26 +34,32 @@
37
34
  "node": ">=18.0.0"
38
35
  },
39
36
  "dependencies": {
40
- "debug": "^4.4.0"
37
+ "debug": "^4.3.2"
41
38
  },
42
39
  "peerDependencies": {
43
40
  "@autofleet/events": ">=5",
44
41
  "@autofleet/logger": ">=4",
45
- "pg": ">=8",
42
+ "@autofleet/outbreak": ">=2",
46
43
  "sequelize": ">=6"
47
44
  },
45
+ "peerDependenciesMeta": {
46
+ "@autofleet/outbreak": {
47
+ "optional": true
48
+ }
49
+ },
48
50
  "devDependencies": {
51
+ "@autofleet/common-types": "4.140.2",
49
52
  "@types/debug": "^4.1.6",
50
53
  "@types/express": "^4.17.13",
51
54
  "@types/node": "^22.16.5",
52
- "@types/pg": "^8.20.0",
53
- "axios": "^1.15.0",
55
+ "axios": "^0.30.3",
54
56
  "express": "^4.21.2",
55
57
  "pg": "^8.16.3",
56
58
  "sequelize": "^6.37.7",
57
59
  "ts-node": "^10.9.2",
58
- "@autofleet/logger": "^4.2.51",
59
- "@autofleet/network": "^1.11.2"
60
+ "@autofleet/logger": "^4.2.45",
61
+ "@autofleet/outbreak": "^2.6.0",
62
+ "@autofleet/network": "^1.9.14"
60
63
  },
61
64
  "scripts": {
62
65
  "start": "ts-node src/index.ts",
package/dist/cli.js DELETED
@@ -1,5 +0,0 @@
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.disconnect(),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 DELETED
@@ -1 +0,0 @@
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.disconnect(); // close IPC channel so the parent process can exit\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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"}