@murumets-ee/db 0.0.0-preview-20261014110621

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/LICENSE ADDED
@@ -0,0 +1,94 @@
1
+ Elastic License 2.0 (ELv2)
2
+
3
+ URL: https://www.elastic.co/licensing/elastic-license
4
+
5
+ ## Acceptance
6
+
7
+ By using the software, you agree to all of the terms and conditions below.
8
+
9
+ ## Copyright License
10
+
11
+ The licensor grants you a non-exclusive, royalty-free, worldwide,
12
+ non-sublicensable, non-transferable license to use, copy, distribute, make
13
+ available, and prepare derivative works of the software, in each case subject
14
+ to the limitations and conditions below.
15
+
16
+ ## Limitations
17
+
18
+ You may not provide the software to third parties as a hosted or managed
19
+ service, where the service provides users with access to any substantial set
20
+ of the features or functionality of the software.
21
+
22
+ You may not move, change, disable, or circumvent the license key functionality
23
+ in the software, and you may not remove or obscure any functionality in the
24
+ software that is protected by the license key.
25
+
26
+ You may not alter, remove, or obscure any licensing, copyright, or other
27
+ notices of the licensor in the software. Any use of the licensor's trademarks
28
+ is subject to applicable law.
29
+
30
+ ## Patents
31
+
32
+ The licensor grants you a license, under any patent claims the licensor can
33
+ license, or becomes able to license, to make, have made, use, sell, offer for
34
+ sale, import and have imported the software, in each case subject to the
35
+ limitations and conditions in this license. This license does not cover any
36
+ patent claims that you cause to be infringed by modifications or additions to
37
+ the software. If you or your company make any written claim that the software
38
+ infringes or contributes to infringement of any patent, your patent license
39
+ for the software granted under these terms ends immediately. If your company
40
+ makes such a claim, your patent license ends immediately for work on behalf
41
+ of your company.
42
+
43
+ ## Notices
44
+
45
+ You must ensure that anyone who gets a copy of any part of the software from
46
+ you also gets a copy of these terms.
47
+
48
+ If you modify the software, you must include in any modified copies of the
49
+ software prominent notices stating that you have modified the software.
50
+
51
+ ## No Other Rights
52
+
53
+ These terms do not imply any licenses other than those expressly granted in
54
+ these terms.
55
+
56
+ ## Termination
57
+
58
+ If you use the software in violation of these terms, such use is not licensed,
59
+ and your licenses will automatically terminate. If the licensor provides you
60
+ with a notice of your violation, and you cease all violation of this license
61
+ no later than 30 days after you receive that notice, your licenses will be
62
+ reinstated retroactively. However, if you violate these terms after such
63
+ reinstatement, any additional violation of these terms will cause your
64
+ licenses to terminate automatically and permanently.
65
+
66
+ ## No Liability
67
+
68
+ As far as the law allows, the software comes as is, without any warranty or
69
+ condition, and the licensor will not be liable to you for any damages arising
70
+ out of these terms or the use or nature of the software, under any kind of
71
+ legal claim.
72
+
73
+ ## Definitions
74
+
75
+ The **licensor** is the entity offering these terms, and the **software** is
76
+ the software the licensor makes available under these terms, including any
77
+ portion of it.
78
+
79
+ **you** refers to the individual or entity agreeing to these terms.
80
+
81
+ **your company** is any legal entity, sole proprietorship, or other kind of
82
+ organization that you work for, plus all organizations that have control over,
83
+ are under the control of, or are under common control with that organization.
84
+ **control** means ownership of substantially all the assets of an entity, or
85
+ the power to direct the management and policies of an entity (for example, by
86
+ voting right, contract, or otherwise). Control can be direct or indirect.
87
+
88
+ **your licenses** are all the licenses granted to you for the software under
89
+ these terms.
90
+
91
+ **use** means anything you do with the software requiring one of your
92
+ licenses.
93
+
94
+ **trademark** means trademarks, service marks, and similar rights.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @murumets-ee/db
2
+
3
+ Database utilities — Drizzle ORM setup, migration runner, schema registry, and test helpers.
4
+
5
+ Part of [Lumi CMS](https://github.com/murumets-ee/lumi-cms) — a modular, type-safe CMS toolkit for Node.js.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @murumets-ee/db
11
+ ```
12
+
13
+ ## License
14
+
15
+ [Elastic License 2.0 (ELv2)](../../LICENSE)
@@ -0,0 +1,2 @@
1
+ const e=Symbol.for(`@murumets-ee/db:transaction-hooks`),t=Symbol.for(`@murumets-ee/db:transaction-hooks:tracked`);function n(){let t=globalThis,n=t[e];return n||(n={handles:new WeakMap},t[e]=n),n}function r(e){if(e[t])return e;let n=e.transaction.bind(e);return e.transaction=(e,t)=>o(e=>n(e,t),e),Object.defineProperty(e,t,{value:!0}),e}function i(e,t){if(e==null)return!1;let r=n().handles.get(e),i=r?.stack[r.stack.length-1];return i?(i.callbacks.push(t),!0):!1}function a(e){return e!=null&&n().handles.has(e)}async function o(e,t){let r={stack:[{callbacks:[]}]},i=await e(async e=>{s(e,r);try{return await t(e)}finally{n().handles.delete(e)}}),a=[...r.stack[0]?.callbacks??[]];for(let e of a)await c(e);return i}function s(e,t){n().handles.set(e,t);let r=e.transaction.bind(e);e.transaction=async e=>{let i=t.stack[t.stack.length-1];if(!i)throw Error(`transaction-hooks: a savepoint opened on a transaction with no open frame`);let a={callbacks:[]},o=await r(async r=>{t.stack.push(a),s(r,t);try{return await e(r)}finally{n().handles.delete(r);let e=t.stack.lastIndexOf(a);e!==-1&&t.stack.splice(e,1)}});return i.callbacks.push(...a.callbacks),o}}async function c(e){try{await e()}catch(e){let t=e instanceof Error?e.stack:void 0;process.emitWarning(`An on-commit callback threw after its transaction committed. The write stands, and the remaining callbacks still ran: `+(e instanceof Error?e.message:String(e)),{code:`TOOLKIT_ON_COMMIT_CALLBACK_FAILED`,...t!==void 0&&{detail:t}})}}const l=/^[a-z_][a-z0-9_]*$/;function u(e){if(!l.test(e))throw Error(`Invalid Postgres extension name: ${JSON.stringify(e)}. Must match ${l} (lowercase letters, digits, underscores; no quotes, whitespace, or punctuation).`)}function d(e){return u(e),`CREATE EXTENSION IF NOT EXISTS "${e}"`}export{i as a,a as i,u as n,r as o,d as r,l as t};
2
+ //# sourceMappingURL=extensions-CEyIHQA7.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extensions-CEyIHQA7.mjs","names":["marked"],"sources":["../src/transaction-hooks.ts","../src/extensions.ts"],"sourcesContent":["/**\n * Per-transaction on-commit callbacks — Django's `transaction.on_commit`, for\n * drizzle's postgres-js driver (rent-go-live PR13, F023).\n *\n * ## Why this exists\n *\n * Some work is only correct once a write is TRUE: telling another process its\n * cache is stale, maintaining an index that other connections read. Inside a\n * transaction the CALLER owns, the code making the write cannot know whether it\n * will commit — and drizzle exposes no after-commit hook (postgres-js offers\n * `begin` and `savepoint`, nothing that runs after COMMIT). Firing immediately\n * announces writes that a later statement in the same transaction may still\n * roll back, which is exactly what F023 found `AdminClient` doing.\n *\n * ## Semantics — Django's, deliberately\n *\n * - A callback registered on a tracked transaction runs after that transaction\n * COMMITS and before `db.transaction()` resolves, in registration order, each\n * awaited — so a caller's next statement cannot race its own side effects.\n * - A rollback discards every callback the transaction registered, and \"a\n * rollback\" is whatever postgres-js decides, not only a throw out of the body:\n * `tx.rollback()`, a COMMIT that fails (a deferred constraint is checked\n * there), and a body that CAUGHT a failed statement and returned normally —\n * postgres-js records every statement's error on its transaction and rolls it\n * back once the body returns.\n * - A savepoint (`tx.transaction(...)`) is a frame of its own. Its callbacks join\n * the enclosing frame only once its savepoint scope has settled without a\n * ROLLBACK TO — never merely because its body returned, since the same\n * caught-statement rule can still roll it back after that. (postgres-js never\n * sends RELEASE SAVEPOINT; a completed savepoint simply stays part of its\n * parent until COMMIT.) Rolled back, its callbacks are discarded and the\n * enclosing frame's are kept.\n * - Registration binds to the innermost OPEN frame, whichever handle it arrives\n * through. One connection runs every statement, so a write made through the\n * outer `tx` while a savepoint is open is inside that savepoint and rolls back\n * with it — its callback must too.\n * - A callback that throws cannot fail the transaction: it has already\n * committed, and reporting a committed write as failed invites a retry of work\n * that is done. It is reported through `process.emitWarning` (this package is\n * logger-free, as `table/registry.ts` notes) and the remaining callbacks still\n * run. Callers that own a logger should catch and log themselves; this is the\n * last resort, not the reporting channel.\n * - `onCommit` on anything that is not an OPEN tracked transaction returns\n * `false` and does nothing. Django runs the callback immediately there;\n * returning instead leaves \"no transaction\" to the caller, whose immediate\n * path may rightly want errors to propagate.\n *\n * ## What running later means for a caller\n *\n * - A callback runs in the async context of the `db.transaction()` CALL, not of\n * the statement that registered it. A body that switches `AsyncLocalStorage`\n * context inside the transaction (elevating with `runWithContextAsync`, say)\n * must not expect its callbacks to inherit the switch.\n * - A connection lost DURING COMMIT rejects `db.transaction()` and discards the\n * callbacks, although the COMMIT may have reached the server. Django has the\n * same gap; it is the one case where \"discarded\" can mean \"not run for a write\n * that happened\".\n * - A transaction holds its callbacks, and whatever they close over, until it\n * commits: one closure per registration. That is the work the caller would\n * have done inline, moved later — it adds no fan-out of its own.\n *\n * ## Where tracking is installed\n *\n * `createDbClient` — so every `app.db.readWrite` built by `createApp`, and the\n * CLI's — and `createTestDb`, so integration suites run the production shape. A\n * client built with a bare `drizzle(...)` is untracked and `onCommit` refuses\n * its transactions.\n *\n * ## Why the registry lives on `globalThis`\n *\n * Tracking is installed through one entry (`createDbClient` in `.`,\n * `createTestDb` in `./test-utils`) and read through another (`onCommit`,\n * imported by `@murumets-ee/entity`). Our own build emits this module once, into\n * a chunk both entries share — but a consumer's bundler is free to duplicate it,\n * and Turbopack's dev module duplication is a failure mode this repo has already\n * shipped. A module-local registry would then exist twice, and a transaction\n * tracked by one copy would be invisible to `onCommit` from the other: silently,\n * with every callback refused and every seam falling back to firing in-tx. Same\n * `Symbol.for` slot shape as `@murumets-ee/queue`'s enqueue-on-commit slot and\n * `@murumets-ee/entity`'s write-observer slot.\n *\n * ## Not supported\n *\n * Concurrent savepoints on one transaction (`Promise.all` over\n * `tx.transaction(...)`). They share a connection, their SAVEPOINT and ROLLBACK TO\n * statements interleave, and a frame stack cannot know which callback belongs to\n * which; drizzle gives that shape no defined meaning either. Nor a savepoint the\n * body does not await: its callbacks merge whenever its promise settles, and if\n * that is after the transaction committed, they never run.\n */\n\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\n\n/** Work to run once the transaction it was registered on has committed. */\nexport type CommitCallback = () => void | Promise<void>\n\ntype DatabaseTransaction = PostgresJsDatabase['transaction']\n/** The handle drizzle passes to a transaction body — outer or savepoint. */\ntype TransactionHandle = Parameters<Parameters<DatabaseTransaction>[0]>[0]\ntype TransactionConfig = Parameters<DatabaseTransaction>[1]\n\n/** Callbacks registered while one transaction or savepoint was the innermost open one. */\ninterface Frame {\n callbacks: CommitCallback[]\n}\n\n/**\n * One outermost transaction. `stack[0]` is the transaction's own frame; each\n * open savepoint pushes one above it.\n */\ninterface Root {\n stack: Frame[]\n}\n\ninterface HookState {\n /** Every OPEN handle (outer or savepoint) → the root transaction it belongs to. */\n handles: WeakMap<object, Root>\n}\n\nconst STATE_KEY = Symbol.for('@murumets-ee/db:transaction-hooks')\nconst TRACKED_KEY = Symbol.for('@murumets-ee/db:transaction-hooks:tracked')\n\nfunction hookState(): HookState {\n const slot = globalThis as { [STATE_KEY]?: HookState }\n let state = slot[STATE_KEY]\n if (!state) {\n state = { handles: new WeakMap() }\n slot[STATE_KEY] = state\n }\n return state\n}\n\n/**\n * Install on-commit tracking on a read-write client, in place, and return it.\n *\n * Idempotent: a client that is already tracked is returned untouched, so a\n * callback never runs twice because two layers each installed tracking.\n */\nexport function trackTransactions<TDb extends PostgresJsDatabase>(db: TDb): TDb {\n const marked = db as TDb & { [TRACKED_KEY]?: true }\n if (marked[TRACKED_KEY]) return db\n\n const begin = db.transaction.bind(db)\n db.transaction = <T>(\n run: (tx: TransactionHandle) => Promise<T>,\n config?: TransactionConfig,\n ): Promise<T> => runOutermost((body) => begin(body, config), run)\n\n Object.defineProperty(db, TRACKED_KEY, { value: true })\n return db\n}\n\n/**\n * Register `callback` to run after the transaction `executor` belongs to\n * commits. Returns `true` when it was registered, and `false` — having done\n * nothing — when `executor` is not an open tracked transaction, in which case\n * the caller decides whether to run the work now.\n */\nexport function onCommit(executor: object | null | undefined, callback: CommitCallback): boolean {\n if (executor === null || executor === undefined) return false\n const root = hookState().handles.get(executor)\n const frame = root?.stack[root.stack.length - 1]\n if (!frame) return false\n frame.callbacks.push(callback)\n return true\n}\n\n/** Whether `executor` is a transaction handle that is open and tracked right now. */\nexport function isTrackedTransaction(executor: object | null | undefined): boolean {\n return executor !== null && executor !== undefined && hookState().handles.has(executor)\n}\n\nasync function runOutermost<T>(\n begin: (body: (tx: TransactionHandle) => Promise<T>) => Promise<T>,\n run: (tx: TransactionHandle) => Promise<T>,\n): Promise<T> {\n const root: Root = { stack: [{ callbacks: [] }] }\n\n // Any rollback — a throw, `tx.rollback()`, a caught statement error, a failed\n // COMMIT — rejects here, before a callback is read, and the root is dropped\n // with everything registered on it.\n const result = await begin(async (tx) => {\n adopt(tx, root)\n try {\n return await run(tx)\n } finally {\n // No registrations once the body is done: it can no longer write.\n hookState().handles.delete(tx)\n }\n })\n\n // COMMITTED. A snapshot, so a straggler merging in while these run — an\n // unawaited savepoint settling late — is not picked up mid-iteration.\n const callbacks = [...(root.stack[0]?.callbacks ?? [])]\n for (const callback of callbacks) {\n await runCallback(callback)\n }\n return result\n}\n\n/** Track an open handle, and make its savepoints keep frames of their own. */\nfunction adopt(tx: TransactionHandle, root: Root): void {\n hookState().handles.set(tx, root)\n\n const savepoint = tx.transaction.bind(tx)\n tx.transaction = async <T>(run: (sp: TransactionHandle) => Promise<T>): Promise<T> => {\n const enclosing = root.stack[root.stack.length - 1]\n if (!enclosing) {\n throw new Error('transaction-hooks: a savepoint opened on a transaction with no open frame')\n }\n const frame: Frame = { callbacks: [] }\n\n // Rejects when postgres-js rolls the savepoint back — including for a failed\n // statement the body caught, which it only acts on AFTER the body returns —\n // and then `frame` is simply dropped.\n const value = await savepoint(async (sp) => {\n root.stack.push(frame)\n adopt(sp, root)\n try {\n return await run(sp)\n } finally {\n hookState().handles.delete(sp)\n const index = root.stack.lastIndexOf(frame)\n if (index !== -1) root.stack.splice(index, 1)\n }\n })\n\n // Settled without ROLLBACK TO — only now do these callbacks belong to the\n // enclosing frame.\n enclosing.callbacks.push(...frame.callbacks)\n return value\n }\n}\n\nasync function runCallback(callback: CommitCallback): Promise<void> {\n try {\n await callback()\n } catch (error) {\n const stack = error instanceof Error ? error.stack : undefined\n process.emitWarning(\n 'An on-commit callback threw after its transaction committed. The write stands, ' +\n 'and the remaining callbacks still ran: ' +\n (error instanceof Error ? error.message : String(error)),\n {\n code: 'TOOLKIT_ON_COMMIT_CALLBACK_FAILED',\n ...(stack !== undefined && { detail: stack }),\n },\n )\n }\n}\n","/**\n * Postgres extension helpers — the single source of truth for how the toolkit\n * enables a Postgres extension.\n *\n * Declarative source: plugins declare `server.extensions: ['btree_gist']`\n * (see `@murumets-ee/core`'s `PluginServer`). Consumers — `lumi migrate`\n * (emits into the generated migration) and `@murumets-ee/db`'s `createTestDb`\n * (enables before `pushSchema`) — both route through the helpers here so the\n * validation rule and the emitted SQL are identical everywhere (the migrate\n * dedup-scan relies on the canonical statement being byte-for-byte stable).\n *\n * Why validate the name: `CREATE EXTENSION` cannot parameterize an identifier,\n * so the extension name is interpolated as a literal. Names come from plugin\n * code (not request input), but per the \"validate at the boundary\" rule we\n * still whitelist the identifier shape so a typo or a hostile plugin can't\n * inject SQL through the one place a raw identifier reaches a DDL string.\n */\n\n/**\n * Valid Postgres extension identifier: a lowercase unquoted identifier —\n * leading letter/underscore, then letters/digits/underscores. Covers every\n * real extension name (`btree_gist`, `pg_trgm`, `uuid-ossp` is the lone\n * hyphenated outlier and is intentionally rejected here; declare it quoted via\n * a custom migration if ever needed). Anchored, so no embedded whitespace,\n * quotes, semicolons, or comment markers can slip through.\n */\nexport const EXTENSION_NAME_PATTERN = /^[a-z_][a-z0-9_]*$/\n\n/**\n * Throw if `name` is not a safe, unquoted Postgres extension identifier.\n * Kept separate from {@link createExtensionStatement} so callers that only\n * want to validate (e.g. a config linter) don't have to build SQL.\n */\nexport function assertValidExtensionName(name: string): void {\n if (!EXTENSION_NAME_PATTERN.test(name)) {\n throw new Error(\n `Invalid Postgres extension name: ${JSON.stringify(name)}. ` +\n `Must match ${EXTENSION_NAME_PATTERN} (lowercase letters, digits, underscores; ` +\n 'no quotes, whitespace, or punctuation).',\n )\n }\n}\n\n/**\n * The canonical, idempotent statement that enables one extension.\n * Validates first, then double-quotes the (already-safe) identifier.\n *\n * This exact string is what `lumi migrate` writes into migration files AND\n * what it scans prior migrations for to avoid re-emitting — do not change the\n * shape without updating the dedup scan in `packages/cli/src/commands/migrate.ts`.\n */\nexport function createExtensionStatement(name: string): string {\n assertValidExtensionName(name)\n return `CREATE EXTENSION IF NOT EXISTS \"${name}\"`\n}\n"],"mappings":"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"}