{"_id":"@atelia/pg-change-context","_rev":"5-5593d35875150a48d967f54b8cfe5bce","name":"@atelia/pg-change-context","dist-tags":{"latest":"0.1.1"},"versions":{"0.0.1":{"name":"@atelia/pg-change-context","version":"0.0.1","keywords":["postgresql","cdc","change-data-capture","logical-decoding","audit","prisma"],"license":"MIT","_id":"@atelia/pg-change-context@0.0.1","maintainers":[{"name":"sriram-nyshadham","email":"sriram@ateliahealth.com"}],"homepage":"https://github.com/ateliahealth/bemi-io/tree/main/context#readme","bugs":{"url":"https://github.com/ateliahealth/bemi-io/issues"},"dist":{"shasum":"ce5c42c0fd5b086ef78326cf1b779b36f580d4d3","tarball":"https://registry.npmjs.org/@atelia/pg-change-context/-/pg-change-context-0.0.1.tgz","fileCount":6,"integrity":"sha512-FJaEwz71J42NlqOAQl93rzevksxb3CbP2Suo9623rV8yUv88MqldeMI6AdCOOJMgQmDUM1JGgPtv/20pxrm1/A==","signatures":[{"sig":"MEUCIQDxDSlOz/YfVM2QV4LbDeo9OKdvo3j+t4IjQzK6zfALvQIgZ47yljDoF9Gk61JyrzAFiuA9BWeQolKI79B8XUPqzRE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":14987},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"4f3a5d15d8a5dd8ae1bfe9ecfb26a02e3e5b2c5f","scripts":{"lint":"oxlint src","test":"vitest run","build":"tsc","format":"prettier --write --ignore-path ../.prettierignore src","typecheck":"tsc --noEmit -p tsconfig.spec.json","test:watch":"vitest","format:check":"prettier --check --ignore-path ../.prettierignore src"},"_npmUser":{"name":"sriram-nyshadham","email":"sriram@ateliahealth.com"},"deprecated":"bootstrap release, use 0.1.0","repository":{"url":"git+https://github.com/ateliahealth/bemi-io.git","type":"git","directory":"context"},"_npmVersion":"11.6.2","description":"Emit application context alongside PostgreSQL data changes for logical-decoding CDC","directories":{},"_nodeVersion":"24.11.1","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.10","typescript":"catalog:","@types/node":"catalog:"},"_npmOperationalInternal":{"tmp":"tmp/pg-change-context_0.0.1_1786311028189_0.5302919946032754","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@atelia/pg-change-context","version":"0.1.0","keywords":["postgresql","cdc","change-data-capture","logical-decoding","audit","prisma"],"license":"MIT","_id":"@atelia/pg-change-context@0.1.0","maintainers":[{"name":"sriram-nyshadham","email":"sriram@ateliahealth.com"}],"homepage":"https://github.com/ateliahealth/bemi-io/tree/main/context#readme","bugs":{"url":"https://github.com/ateliahealth/bemi-io/issues"},"dist":{"shasum":"9672f4f10a5b9f749cb9e8ad852774c38344eb2d","tarball":"https://registry.npmjs.org/@atelia/pg-change-context/-/pg-change-context-0.1.0.tgz","fileCount":6,"integrity":"sha512-ypjrIvWDVWtHlgauvbbsXDCF2cHp0np/Sc2Q8vfJMbimooy1ZA0S1t/mwGNnBt7OXAKlbF5yy9IZlQmi68eDuQ==","signatures":[{"sig":"MEQCIA8a6ZUM2H5L31SbeMGeZ1ygWrGo/y2Z5DFiFyN8ROe4AiAY8ojSQKwWL+fIHnPdMt4DgScawIiJkJ8+mnfWf5ITHQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@atelia%2fpg-change-context@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":14987},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"b0f6e0a21cb430565c7d38bbcd3677dacf90bd85","scripts":{"lint":"oxlint src","test":"vitest run","build":"tsc","format":"prettier --write --ignore-path ../.prettierignore src","typecheck":"tsc --noEmit -p tsconfig.spec.json","test:watch":"vitest","format:check":"prettier --check --ignore-path ../.prettierignore src"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:d3771b85-35ec-4135-ad4f-5c93ad461740"}},"repository":{"url":"git+https://github.com/ateliahealth/bemi-io.git","type":"git","directory":"context"},"_npmVersion":"11.16.0","description":"Emit application context alongside PostgreSQL data changes for logical-decoding CDC","directories":{},"_nodeVersion":"24.18.0","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.10","typescript":"catalog:","@types/node":"catalog:"},"_npmOperationalInternal":{"tmp":"tmp/pg-change-context_0.1.0_1786311658547_0.7073567809207464","host":"s3://npm-registry-packages-npm-production"},"deprecated":"throws for Prisma consumers; use 0.1.1"},"0.1.1":{"name":"@atelia/pg-change-context","version":"0.1.1","keywords":["postgresql","cdc","change-data-capture","logical-decoding","audit","prisma"],"license":"MIT","_id":"@atelia/pg-change-context@0.1.1","maintainers":[{"name":"sriram-nyshadham","email":"sriram@ateliahealth.com"}],"homepage":"https://github.com/ateliahealth/bemi-io/tree/main/context#readme","bugs":{"url":"https://github.com/ateliahealth/bemi-io/issues"},"dist":{"shasum":"f5787315871d9844868c0bac595439af6afef090","tarball":"https://registry.npmjs.org/@atelia/pg-change-context/-/pg-change-context-0.1.1.tgz","fileCount":6,"integrity":"sha512-NGARAff+U/SL5cMakZcSgGS6f9cL5TsYwktQc9eey9dJMRqz1sjjHgJHe3FS1GD5zjqZEjjhADsE/L8NpXVuhA==","signatures":[{"sig":"MEUCIQC91CIp8fQ42OLwcPFF2XSN2E54GX7FE6ATMpxZCYeoDQIgWvLwClxqrHEjrHTdEoH80u2U19pK5wRxiXct9SgPwGQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@atelia%2fpg-change-context@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":15283},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"gitHead":"4ceca498c12861fb1a7bbe94fac4b4be9cced267","scripts":{"lint":"oxlint src","test":"vitest run","build":"tsc","format":"prettier --write --ignore-path ../.prettierignore src","typecheck":"tsc --noEmit -p tsconfig.spec.json","test:watch":"vitest","format:check":"prettier --check --ignore-path ../.prettierignore src"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:d3771b85-35ec-4135-ad4f-5c93ad461740"}},"repository":{"url":"git+https://github.com/ateliahealth/bemi-io.git","type":"git","directory":"context"},"_npmVersion":"11.16.0","description":"Emit application context alongside PostgreSQL data changes for logical-decoding CDC","directories":{},"_nodeVersion":"24.18.0","publishConfig":{"access":"public","provenance":true},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.10","typescript":"catalog:","@types/node":"catalog:"},"_npmOperationalInternal":{"tmp":"tmp/pg-change-context_0.1.1_1786315545715_0.6111492409263364","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-08-09T21:30:28.016Z","modified":"2026-08-09T22:46:53.318Z","0.0.1":"2026-08-09T21:30:28.337Z","0.1.0":"2026-08-09T21:40:58.691Z","0.1.1":"2026-08-09T22:45:45.855Z"},"bugs":{"url":"https://github.com/ateliahealth/bemi-io/issues"},"license":"MIT","homepage":"https://github.com/ateliahealth/bemi-io/tree/main/context#readme","keywords":["postgresql","cdc","change-data-capture","logical-decoding","audit","prisma"],"repository":{"url":"git+https://github.com/ateliahealth/bemi-io.git","type":"git","directory":"context"},"description":"Emit application context alongside PostgreSQL data changes for logical-decoding CDC","maintainers":[{"name":"sriram-nyshadham","email":"sriram@ateliahealth.com"}],"readme":"# @atelia/pg-change-context\n\nEmits application context alongside PostgreSQL data changes, for consumers that\nread it out of the write-ahead log via logical decoding.\n\nMIT licensed. Independent of the rest of this repository, which is SSPL-1.0 —\nsee [Licence](#licence).\n\n## What it does\n\nOne thing: issues `pg_logical_emit_message(true, '_bemi', <json>)` on a client\nyou give it.\n\n```ts\nimport { emitChangeContext } from '@atelia/pg-change-context'\n\nawait prisma.$transaction(async (tx) => {\n  await emitChangeContext(tx, { tenantId, userId })\n  await tx.thing.update({ ... })\n})\n```\n\nThe message is transactional, so it becomes visible only if the transaction\ncommits, and a rollback discards it exactly as it discards the writes. A CDC\nconsumer pairs it to the changes by transaction id.\n\n## The one thing to get right\n\n**Emit on the same client the writes go to.** Inside an interactive transaction\nthat is the transaction client, not the top-level one.\n\nEmitting on the top-level client sends the message over a different connection,\nin a different transaction. It then has a different transaction id, so it pairs\nwith nothing, and it does not roll back with the writes. Both failures are\nsilent: changes are recorded correctly but with no context, which is\nindistinguishable from a code path that never set any.\n\n## What this package deliberately does not do\n\n**It does not decide whether to open a transaction.**\n\nA lone write with no enclosing transaction is the awkward case. Prisma\nautocommits it, so emitting and then writing produces two transactions, two\ntransaction ids, and no pairing. Making them atomic means opening a transaction\n— but doing that when the caller _already_ has one is what breaks the caller's\ntransaction.\n\nSo the rule is: wrap when there is no caller transaction, never wrap when there\nis. This package cannot evaluate that rule, because only the application's\ntransaction manager knows whether one is open. Guessing produces exactly the\ndefect this package exists to avoid.\n\nOwn that decision where transactions are already managed — a CLS-scoped\ntransaction host, a unit-of-work, or an explicit `$transaction` at the call\nsite — and call `emitChangeContext` with the client that manager hands you.\n\nIf you do wrap a previously-autocommitting write, keep the transaction to\nexactly the emit and the write. Nothing else belongs inside it — no reads, no\nservice calls, no outbound requests. A write that used to hold a pooled\nconnection for a single statement now holds one for a transaction, on what is\nusually the hottest path in the system, and anything added inside later\nextends that hold. Worth asserting the statement count where the wrapping is\nimplemented, so a third statement fails a test rather than quietly changing\nconnection behaviour under load.\n\n## Why the context is an argument\n\nThe context is passed in, rather than read from an ambient store the library\nowns. That is deliberate.\n\nAn emitter that reads from async-local storage inherits whatever is in the\nstore, and a store populated with `enterWith` can persist into later work on\nthe same execution context. The failure that produces is not a missing\ncontext — it is one request's tenant and user attached to another request's\nchanges. On any system where attribution matters, a change credited to the\nwrong tenant is worse than a change credited to nobody: the first is wrong and\nlooks right, the second is merely incomplete and obvious.\n\nTaking the value as an argument makes that class of bug unreachable here. If\nthe calling application keeps context in a request scope, it resolves it there\nand passes the result — the scoping stays where the request lifecycle is\nunderstood, and this package cannot outlive it.\n\n## Behaviour\n\n| Input                       | Result                              |\n| --------------------------- | ----------------------------------- |\n| Empty context `{}`          | Returns without emitting            |\n| Context over the byte limit | Throws — never truncated or skipped |\n| Non-serialisable value      | Throws                              |\n| Not a plain object          | Throws                              |\n| Executor rejects            | Propagates                          |\n\nIt returns `void`. Every failure throws, so there is no result to check and no\nway to ignore one — a call that returns normally either emitted or had nothing\nto emit. A boolean would have to be checked to mean anything, and an unchecked\nreturn that quietly meant \"did nothing\" is the shape of bug this package exists\nto remove.\n\nNothing here fails quietly. A context that did not reach the log means changes\nwill be attributed to nobody, and only the caller can decide whether that\nshould fail the write.\n\nThe limit defaults to 8 KiB and is per-call configurable. It is generous\nrelative to real payloads; exceeding it usually means something unintended is\nbeing attached.\n\n## Releasing\n\nTagged in its own namespace, separate from the `bemi-v*` tags that select what\nthe deployment pipeline builds.\n\n```sh\n# bump \"version\" in context/package.json, commit, then:\ngit tag pg-change-context-v0.2.0\ngit push origin pg-change-context-v0.2.0\n```\n\n`.github/workflows/publish-context.yml` runs the full repository gate before\npublishing, and refuses if the tag disagrees with the manifest version or if\nthe tarball is missing its licence or build output.\n\n## Licence\n\nMIT — see `LICENSE` in this directory.\n\nThe rest of this repository is SSPL-1.0. This package is separate and imports\nnothing from it, so that applications can link it without their own licensing\nbeing affected. `scripts/licence-boundary.sh` enforces that separation as a\nbuild failure rather than a convention, because the import that would break it\nlooks like a harmless cleanup.\n","readmeFilename":"README.md"}