@murumets-ee/db 0.61.0 → 0.63.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/dist/extensions-CEyIHQA7.mjs +2 -0
- package/dist/extensions-CEyIHQA7.mjs.map +1 -0
- package/dist/index.d.mts +27 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +6 -6
- package/dist/index.mjs.map +1 -1
- package/dist/test-utils.d.mts.map +1 -1
- package/dist/test-utils.mjs +1 -1
- package/dist/test-utils.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/extensions-CyFQK8AR.mjs +0 -2
- package/dist/extensions-CyFQK8AR.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-utils.d.mts","names":[],"sources":["../src/test-utils.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"test-utils.d.mts","names":[],"sources":["../src/test-utils.ts"],"mappings":";;;UAMiB,WAAA;;AAAjB;;;;AAUY;AAGZ;;;EAHE,UAAU;AAAA;AAAA,UAGK,MAAA;EACf,EAAA,EAAI,kBAAA;EACJ,MAAA;EAGsB;EADtB,IAAA,GAAO,OAAA,EAAS,MAAA,mBAAyB,IAAA,GAAO,WAAA,KAAgB,OAAA;EAChE,OAAA,QAAe,OAAA;AAAA;;;;;;;;;;;AAAO;AAkBxB;;;;iBAAsB,YAAA,CAAA,GAAgB,OAAO,CAAC,MAAA;AA+J9C;AAAA,UAAiB,eAAA;EACf,MAAA;EACA,GAAG;AAAA;AAIL;AAAA,UAAiB,mBAAA,aAAgC,MAAA;EAC/C,IAAA,EAAM,GAAA;EACN,KAAA,CAAM,OAAA,GAAU,GAAA;EAChB,OAAA,CAAQ,KAAA,EAAO,MAAA,oBAA0B,OAAA,CAAQ,GAAA;EACjD,QAAA,CAAS,IAAA;IACP,KAAA,GAAQ,MAAA;IACR,OAAA,GAAU,eAAA;IACV,KAAA;IACA,MAAA;EAAA,IACE,OAAA,CAAQ,GAAA;EACZ,MAAA,CAAO,KAAA,EAAO,MAAA,oBAA0B,OAAA;EACxC,KAAA,CAAM,KAAA,GAAQ,MAAA,oBAA0B,OAAA;EACxC,MAAA,CAAO,MAAA,EAAQ,OAAA,CAAQ,GAAA,IAAO,OAAA,CAAQ,GAAA;EACtC,MAAA,CAAO,KAAA,EAAO,MAAA,mBAAyB,KAAA,EAAO,OAAA,CAAQ,GAAA,IAAO,OAAA,CAAQ,GAAA;EACrE,MAAA,CACE,MAAA,EAAQ,OAAA,CAAQ,GAAA,GAChB,IAAA;IAAQ,MAAA;IAA2B,GAAA,GAAM,OAAA,CAAQ,GAAA;EAAA,IAChD,OAAA,CAAQ,GAAA;EACX,MAAA,CAAO,KAAA,EAAO,MAAA,oBAA0B,OAAA,CAAQ,GAAA;EAChD,UAAA,CAAW,KAAA,EAAO,MAAA,oBAA0B,OAAA,CAAQ,GAAA;EACpD,QAAA,CACE,MAAA,EAAQ,OAAA,CAAQ,GAAA,GAChB,IAAA;IACE,MAAA;IACA,WAAA,EAAa,MAAA;IACb,GAAA,GAAM,OAAA,CAAQ,GAAA;EAAA,IAEf,OAAA;IAAU,QAAA;IAAgB,GAAA,EAAK,GAAA;EAAA;IAAU,QAAA;IAAiB,GAAA,EAAK,GAAA;EAAA;EAClE,SAAA,IAAa,IAAA;IACX,MAAA,EAAQ,MAAA;MAAiB,EAAA;MAA6C,MAAA;IAAA;IACtE,KAAA,GAAQ,MAAA;EAAA,MACJ,OAAA,CAAQ,KAAA,CAAM,MAAA;AAAA;;;;;;;;;;;;;;iBAgBN,yBAAA,aAAsC,MAAA,kBAAA,CACpD,OAAA;EAAW,OAAA,SAAgB,GAAA;EAAc,WAAA;EAA4B,OAAA,GAAU,GAAA;AAAA,IAC9E,mBAAA,CAAoB,GAAA"}
|
package/dist/test-utils.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{r as
|
|
1
|
+
import{o as e,r as t}from"./extensions-CEyIHQA7.mjs";import{drizzle as n}from"drizzle-orm/postgres-js";import r from"postgres";import{sql as i}from"drizzle-orm";async function a(){if(process.env.NODE_ENV===`production`)throw Error(`createTestDb() is not allowed when NODE_ENV=production — it issues CREATE/DROP SCHEMA and is intended for tests only`);let a=process.env.DATABASE_TEST_URL||process.env.DATABASE_URL;if(!a)throw Error(`DATABASE_TEST_URL (or DATABASE_URL) env var is required for integration tests`);let o=`test_${crypto.randomUUID().replaceAll(`-`,``).slice(0,12)}`,s=r(a,{max:1});try{await s.unsafe(`CREATE SCHEMA "${o}"`)}finally{await s.end()}let c=r(a,{max:3,idle_timeout:10,connection:{search_path:`"${o}",public`}}),u=e(n(c));return{db:u,schema:o,async push(e,n){if(n?.extensions&&n.extensions.length>0){let e=r(a,{max:1,connection:{search_path:`public`}});try{for(let r of n.extensions)await e.unsafe(t(r))}finally{await e.end()}}let{createRequire:i}=await import(`node:module`),{pushSchema:o}=i(import.meta.url)(`drizzle-kit/api`);await(await o(e,l(u))).apply()},async cleanup(){await u.execute(i.raw(`DROP SCHEMA IF EXISTS "${o}" CASCADE`)),await c.end()}}}function o(e,t){return`eq`in t?e===t.eq:`ne`in t?e!==t.ne:`in`in t?t.in.includes(e):`notIn`in t?!t.notIn.includes(e):`lt`in t?e<t.lt:`lte`in t?e<=t.lte:`gt`in t?e>t.gt:`gte`in t?e>=t.gte:`isNull`in t?e==null:`isNotNull`in t?e!=null:!1}function s(e,t){for(let[n,r]of Object.entries(t)){if(n===`$or`){if(!r.some(t=>s(e,t)))return!1;continue}if(n===`$and`){if(!r.every(t=>s(e,t)))return!1;continue}let t=e[n];if(r===null){if(t!=null)return!1;continue}if(typeof r==`object`&&r&&!(r instanceof Date)){if(!o(t,r))return!1;continue}if(t!==r)return!1}return!0}function c(e={}){let t=e.idField??`id`,n=1,r=e.idGenerator??(()=>`row-${n++}`),i={rows:e.initial?[...e.initial]:[]};return{get rows(){return i.rows},set rows(e){i.rows=e},reset(e){i.rows=e?[...e]:[],n=1},async findOne(e){return i.rows.find(t=>s(t,e))??null},async findMany(e){let t=e?.where?i.rows.filter(t=>s(t,e.where)):[...i.rows];e?.orderBy&&e.orderBy.length>0&&(t=[...t].sort((t,n)=>{for(let r of e.orderBy??[]){let e=t[r.column],i=n[r.column],a=r.dir===`desc`?-1:1;if(e instanceof Date&&i instanceof Date){let t=(e.getTime()-i.getTime())*a;if(t!==0)return t}else if(typeof e==`number`&&typeof i==`number`){let t=(e-i)*a;if(t!==0)return t}else if(typeof e==`string`&&typeof i==`string`){let t=e.localeCompare(i)*a;if(t!==0)return t}}return 0}));let n=e?.offset??0,r=e?.limit;return n>0&&(t=t.slice(n)),r!==void 0&&r>=0&&(t=t.slice(0,r)),t},async aggregate(e){let t=e?.where?i.rows.filter(t=>s(t,e.where)):i.rows,n={};for(let[r,i]of Object.entries(e.select))if(i.fn===`count`)n[r]=t.length;else if(i.fn===`max`&&i.column){let e=null;for(let n of t){let t=n[i.column];typeof t==`number`&&(e===null||t>e)&&(e=t)}n[r]=e}else if(i.fn===`min`&&i.column){let e=null;for(let n of t){let t=n[i.column];typeof t==`number`&&(e===null||t<e)&&(e=t)}n[r]=e}else if(i.fn===`sum`&&i.column){let e=0;for(let n of t){let t=n[i.column];typeof t==`number`&&(e+=t)}n[r]=e}return[n]},async exists(e){return i.rows.some(t=>s(t,e))},async count(e){return e?i.rows.filter(t=>s(t,e)).length:i.rows.length},async insert(e){let n={[t]:r(),...e};return i.rows.push(n),n},async update(e,t){let n=i.rows.filter(t=>s(t,e));if(n.length===0)return null;let r=n[0];return Object.assign(r,t),r},async upsert(e,n){let a=Array.isArray(n.target)?n.target:[n.target],o={};for(let t of a)o[t]=e[t];let c=i.rows.find(e=>s(e,o));if(c)return Object.assign(c,n.set??e),c;let l={[t]:r(),...e};return i.rows.push(l),l},async delete(e){let t=i.rows.findIndex(t=>s(t,e));if(t===-1)return null;let[n]=i.rows.splice(t,1);return n??null},async deleteMany(e){let t=i.rows.filter(t=>s(t,e));return i.rows=i.rows.filter(e=>!t.includes(e)),t},async tryClaim(e,n){let a=Array.isArray(n.target)?n.target:[n.target],o={};for(let t of a)o[t]=e[t];let c=i.rows.find(e=>s(e,o));if(!c){let n={[t]:r(),...e};return i.rows.push(n),{acquired:!0,row:n}}return s(c,n.replaceWhen)?(Object.assign(c,n.set??e),{acquired:!0,row:c}):{acquired:!1,row:c}}}}function l(e){return new Proxy(e,{get(e,t,n){return t===`execute`?async(...t)=>{let n=await e.execute(...t);return Array.isArray(n)&&!(`rows`in n)&&Object.defineProperty(n,"rows",{value:[...n],enumerable:!1}),n}:Reflect.get(e,t,n)}})}export{c as createInMemoryTableClient,a as createTestDb};
|
|
2
2
|
//# sourceMappingURL=test-utils.mjs.map
|
package/dist/test-utils.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-utils.mjs","names":["branches"],"sources":["../src/test-utils.ts"],"sourcesContent":["import { sql } from 'drizzle-orm'\nimport { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport postgres from 'postgres'\nimport { createExtensionStatement } from './extensions.js'\n\nexport interface PushOptions {\n /**\n * Postgres extensions to enable before pushing the schema — the test-side\n * mirror of a plugin's `server.extensions` (e.g. `['btree_gist']` for the\n * reservation GIST exclusion constraint). Installed into `public` (NOT the\n * per-suite schema) because an extension is per-database and installs into a\n * single schema: a per-suite install would be invisible to other parallel\n * suites' `search_path`. `public` is on every suite's search_path, so the\n * operator classes resolve everywhere. Idempotent (`IF NOT EXISTS`).\n */\n extensions?: readonly string[]\n}\n\nexport interface TestDb {\n db: PostgresJsDatabase\n schema: string\n /** Push Drizzle table definitions to the test schema (uses drizzle-kit/api) */\n push: (schemas: Record<string, unknown>, opts?: PushOptions) => Promise<void>\n cleanup: () => Promise<void>\n}\n\n/**\n * Create an isolated test database using a unique PostgreSQL schema.\n * Each test suite gets its own schema so tests can run in parallel safely.\n *\n * Requires `DATABASE_TEST_URL` env var pointing to a running PostgreSQL instance.\n * Falls back to `DATABASE_URL` if test URL is not set.\n *\n * Usage:\n * ```ts\n * const testDb = await createTestDb()\n * await testDb.push({ articles: articlesTable, entity_refs: entityRefsTable })\n * // ... run tests with testDb.db ...\n * await testDb.cleanup()\n * ```\n */\nexport async function createTestDb(): Promise<TestDb> {\n // Refuse to ever provision a scratch schema in production. `createTestDb`\n // issues `CREATE SCHEMA` and (via `cleanup()`) `DROP SCHEMA … CASCADE`,\n // which would be catastrophic if pointed at a real database. The whole\n // function is test-only, but the npm subpath export means an app could\n // import it by accident — fail loud instead of running.\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n 'createTestDb() is not allowed when NODE_ENV=production — it issues CREATE/DROP SCHEMA and is intended for tests only',\n )\n }\n\n const url = process.env.DATABASE_TEST_URL || process.env.DATABASE_URL\n if (!url) {\n throw new Error('DATABASE_TEST_URL (or DATABASE_URL) env var is required for integration tests')\n }\n\n const schemaName = `test_${crypto.randomUUID().replaceAll('-', '').slice(0, 12)}`\n\n // First connection: create the schema. We can't pass `search_path` as a\n // connection startup parameter for this client because the schema doesn't\n // exist yet — postgres rejects connections asking to enter a missing\n // schema with `invalid value for parameter \"search_path\"`.\n const setupConnection = postgres(url, { max: 1 })\n try {\n await setupConnection.unsafe(`CREATE SCHEMA \"${schemaName}\"`)\n } finally {\n // Always close the setup connection — even on CREATE SCHEMA failure\n // (rare with a UUID-derived name, but possible if the role lacks\n // permission). Without this, a thrown error would leak a postgres\n // connection until the process exits.\n await setupConnection.end()\n }\n\n // Working connection pool: every connection in the pool starts with\n // `search_path` pointing at the test schema. Without this, a second\n // pooled connection would fall back to the default search_path and miss\n // the test tables — which is exactly the bug that surfaced when concurrent\n // queries (e.g. tryClaim's race test) bypassed connection 1.\n const connection = postgres(url, {\n max: 3,\n idle_timeout: 10,\n connection: {\n search_path: `\"${schemaName}\",public`,\n },\n })\n\n const db = drizzle(connection)\n\n return {\n db,\n schema: schemaName,\n\n async push(schemas: Record<string, unknown>, opts?: PushOptions) {\n // Enable declared extensions into `public` FIRST (see PushOptions docs for\n // the per-database / parallel-suite reasoning). A dedicated single-use\n // connection pinned to `search_path=public` makes the install schema\n // deterministic regardless of the working pool's per-suite search_path.\n if (opts?.extensions && opts.extensions.length > 0) {\n const extConnection = postgres(url, { max: 1, connection: { search_path: 'public' } })\n try {\n for (const name of opts.extensions) {\n // createExtensionStatement validates the name + builds the canonical SQL.\n await extConnection.unsafe(createExtensionStatement(name))\n }\n } finally {\n await extConnection.end()\n }\n }\n\n // Use createRequire to load drizzle-kit/api via Node's CJS resolver.\n // drizzle-kit/api.mjs bundles CJS code with require('fs') — Vite's ESM\n // transform replaces require() with a polyfill that throws. Native CJS bypasses this.\n const { createRequire } = await import('node:module')\n const require = createRequire(import.meta.url)\n const { pushSchema } = require('drizzle-kit/api') as typeof import('drizzle-kit/api')\n\n // Use default schemaFilters ([\"public\"]). Our tables are unqualified (= public),\n // and SET search_path ensures DDL actually creates them in the test schema.\n const result = await pushSchema(schemas, withRowsShim(db))\n await result.apply()\n },\n\n async cleanup() {\n await db.execute(sql.raw(`DROP SCHEMA IF EXISTS \"${schemaName}\" CASCADE`))\n await connection.end()\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// In-memory TableClient shim — for unit tests that exercise consumer code\n// (LockService, ContentClient draft methods, etc.) without spinning up\n// Postgres. Implements the subset of TableClient that consumers actually\n// use; the matcher understands the same WhereClause shape (eq shorthand,\n// lt/gte/in/notIn operators, $or branches, jsonb-path keys are NOT\n// supported — use the real DB for those).\n//\n// Each consumer test previously hand-rolled its own ~100-LoC version of\n// this; collapsed into one place per audit-quality-content L#12.\n// ---------------------------------------------------------------------------\n\n/** Operators recognized by the in-memory matcher. Mirrors WhereClause. */\ntype MemOp =\n | { eq: unknown }\n | { ne: unknown }\n | { in: unknown[] }\n | { notIn: unknown[] }\n | { lt: unknown }\n | { lte: unknown }\n | { gt: unknown }\n | { gte: unknown }\n | { isNull: true }\n | { isNotNull: true }\n\nfunction matchesOp(rowVal: unknown, op: MemOp): boolean {\n if ('eq' in op) return rowVal === op.eq\n if ('ne' in op) return rowVal !== op.ne\n if ('in' in op) return op.in.includes(rowVal)\n if ('notIn' in op) return !op.notIn.includes(rowVal)\n if ('lt' in op) return (rowVal as Date | number) < (op.lt as Date | number)\n if ('lte' in op) return (rowVal as Date | number) <= (op.lte as Date | number)\n if ('gt' in op) return (rowVal as Date | number) > (op.gt as Date | number)\n if ('gte' in op) return (rowVal as Date | number) >= (op.gte as Date | number)\n if ('isNull' in op) return rowVal === null || rowVal === undefined\n if ('isNotNull' in op) return rowVal !== null && rowVal !== undefined\n return false\n}\n\n/** Recursive matcher: handles top-level keys, $or, value shorthand, and operator objects. */\nfunction matchesRow(row: Record<string, unknown>, where: Record<string, unknown>): boolean {\n for (const [key, val] of Object.entries(where)) {\n if (key === '$or') {\n const branches = val as Record<string, unknown>[]\n if (!branches.some((b) => matchesRow(row, b))) return false\n continue\n }\n if (key === '$and') {\n const branches = val as Record<string, unknown>[]\n if (!branches.every((b) => matchesRow(row, b))) return false\n continue\n }\n const rowVal = row[key]\n if (val === null) {\n if (rowVal !== null && rowVal !== undefined) return false\n continue\n }\n if (val !== null && typeof val === 'object' && !(val instanceof Date)) {\n if (!matchesOp(rowVal, val as MemOp)) return false\n continue\n }\n if (rowVal !== val) return false\n }\n return true\n}\n\n/** OrderBy spec mirrors TableClient's `OrderBySpec`. */\nexport interface InMemoryOrderBy {\n column: string\n dir?: 'asc' | 'desc'\n}\n\n/** Subset of TableClient methods used by tests. */\nexport interface InMemoryTableClient<Row extends Record<string, unknown>> {\n rows: Row[] // direct mutation for test seeding / inspection\n reset(initial?: Row[]): void\n findOne(where: Record<string, unknown>): Promise<Row | null>\n findMany(opts?: {\n where?: Record<string, unknown>\n orderBy?: InMemoryOrderBy[]\n limit?: number\n offset?: number\n }): Promise<Row[]>\n exists(where: Record<string, unknown>): Promise<boolean>\n count(where?: Record<string, unknown>): Promise<number>\n insert(values: Partial<Row>): Promise<Row>\n update(where: Record<string, unknown>, patch: Partial<Row>): Promise<Row | null>\n upsert(\n values: Partial<Row>,\n opts: { target: string[] | string; set?: Partial<Row> },\n ): Promise<Row>\n delete(where: Record<string, unknown>): Promise<Row | null>\n deleteMany(where: Record<string, unknown>): Promise<Row[]>\n tryClaim(\n values: Partial<Row>,\n opts: {\n target: string | string[]\n replaceWhen: Record<string, unknown>\n set?: Partial<Row>\n },\n ): Promise<{ acquired: true; row: Row } | { acquired: false; row: Row }>\n aggregate?: (opts: {\n select: Record<string, { fn: 'max' | 'min' | 'count' | 'sum' | 'avg'; column?: string }>\n where?: Record<string, unknown>\n }) => Promise<Array<Record<string, unknown>>>\n}\n\n/**\n * Build an in-memory TableClient stand-in for unit tests. Pass\n * `idGenerator` if your test wants deterministic synthetic IDs\n * (defaults to a monotonic `row-1`, `row-2`, ... counter).\n *\n * Example:\n * ```ts\n * const drafts = createInMemoryTableClient<DraftRow>({ idField: 'id' })\n * vi.mock('@murumets-ee/content/schema', () => ({\n * getContentTables: () => ({ drafts: { makeClient: () => drafts } })\n * }))\n * ```\n */\nexport function createInMemoryTableClient<Row extends Record<string, unknown>>(\n options: { idField?: keyof Row & string; idGenerator?: () => string; initial?: Row[] } = {},\n): InMemoryTableClient<Row> {\n const idField = (options.idField ?? 'id') as keyof Row & string\n let nextId = 1\n const idGen = options.idGenerator ?? (() => `row-${nextId++}`)\n\n const state: { rows: Row[] } = { rows: options.initial ? [...options.initial] : [] }\n\n const client: InMemoryTableClient<Row> = {\n get rows() {\n return state.rows\n },\n set rows(next: Row[]) {\n state.rows = next\n },\n\n reset(initial?: Row[]) {\n state.rows = initial ? [...initial] : []\n nextId = 1\n },\n\n async findOne(where) {\n return state.rows.find((r) => matchesRow(r, where)) ?? null\n },\n\n async findMany(opts) {\n let out = opts?.where\n ? state.rows.filter((r) => matchesRow(r, opts.where as Record<string, unknown>))\n : [...state.rows]\n if (opts?.orderBy && opts.orderBy.length > 0) {\n out = [...out].sort((a, b) => {\n for (const spec of opts.orderBy ?? []) {\n const av = a[spec.column]\n const bv = b[spec.column]\n const dir = spec.dir === 'desc' ? -1 : 1\n // Date and number comparisons via subtraction; string via localeCompare.\n if (av instanceof Date && bv instanceof Date) {\n const d = (av.getTime() - bv.getTime()) * dir\n if (d !== 0) return d\n } else if (typeof av === 'number' && typeof bv === 'number') {\n const d = (av - bv) * dir\n if (d !== 0) return d\n } else if (typeof av === 'string' && typeof bv === 'string') {\n const d = av.localeCompare(bv) * dir\n if (d !== 0) return d\n } else {\n // Mixed / nullable: fall through to next spec.\n }\n }\n return 0\n })\n }\n const offset = opts?.offset ?? 0\n const limit = opts?.limit\n if (offset > 0) out = out.slice(offset)\n if (limit !== undefined && limit >= 0) out = out.slice(0, limit)\n return out\n },\n\n async aggregate(opts) {\n const subset = opts?.where\n ? state.rows.filter((r) => matchesRow(r, opts.where as Record<string, unknown>))\n : state.rows\n const result: Record<string, unknown> = {}\n for (const [alias, spec] of Object.entries(opts.select)) {\n if (spec.fn === 'count') {\n result[alias] = subset.length\n } else if (spec.fn === 'max' && spec.column) {\n let max: number | null = null\n for (const r of subset) {\n const v = r[spec.column]\n if (typeof v === 'number') {\n if (max === null || v > max) max = v\n }\n }\n result[alias] = max\n } else if (spec.fn === 'min' && spec.column) {\n let min: number | null = null\n for (const r of subset) {\n const v = r[spec.column]\n if (typeof v === 'number') {\n if (min === null || v < min) min = v\n }\n }\n result[alias] = min\n } else if (spec.fn === 'sum' && spec.column) {\n let sum = 0\n for (const r of subset) {\n const v = r[spec.column]\n if (typeof v === 'number') sum += v\n }\n result[alias] = sum\n }\n }\n return [result]\n },\n\n async exists(where) {\n return state.rows.some((r) => matchesRow(r, where))\n },\n\n async count(where) {\n if (!where) return state.rows.length\n return state.rows.filter((r) => matchesRow(r, where)).length\n },\n\n async insert(values) {\n const row = { [idField]: idGen(), ...values } as Row\n state.rows.push(row)\n return row\n },\n\n async update(where, patch) {\n const matched = state.rows.filter((r) => matchesRow(r, where))\n if (matched.length === 0) return null\n // TableClient.update expects at-most-one match\n const target = matched[0] as Row\n Object.assign(target, patch)\n return target\n },\n\n async upsert(values, opts) {\n const targetKeys = Array.isArray(opts.target) ? opts.target : [opts.target]\n const targetWhere: Record<string, unknown> = {}\n for (const k of targetKeys) targetWhere[k] = (values as Record<string, unknown>)[k]\n const existing = state.rows.find((r) => matchesRow(r, targetWhere))\n if (existing) {\n Object.assign(existing, opts.set ?? values)\n return existing\n }\n const row = { [idField]: idGen(), ...values } as Row\n state.rows.push(row)\n return row\n },\n\n async delete(where) {\n const idx = state.rows.findIndex((r) => matchesRow(r, where))\n if (idx === -1) return null\n const [removed] = state.rows.splice(idx, 1)\n return (removed ?? null) as Row | null\n },\n\n async deleteMany(where) {\n const matched = state.rows.filter((r) => matchesRow(r, where))\n state.rows = state.rows.filter((r) => !matched.includes(r))\n return matched\n },\n\n async tryClaim(values, opts) {\n const targetKeys = Array.isArray(opts.target) ? opts.target : [opts.target]\n const targetWhere: Record<string, unknown> = {}\n for (const k of targetKeys) targetWhere[k] = (values as Record<string, unknown>)[k]\n const existing = state.rows.find((r) => matchesRow(r, targetWhere))\n\n if (!existing) {\n const row = { [idField]: idGen(), ...values } as Row\n state.rows.push(row)\n return { acquired: true, row }\n }\n\n if (matchesRow(existing, opts.replaceWhen)) {\n Object.assign(existing, opts.set ?? values)\n return { acquired: true, row: existing }\n }\n\n return { acquired: false, row: existing }\n },\n }\n\n return client\n}\n\n/**\n * Wrap a Drizzle handle so `.execute()` returns an array that *also* exposes\n * a `.rows` property. `drizzle-kit/api`'s `pushSchema` reads `res.rows`, but\n * postgres-js (the driver Drizzle is built on here) returns plain arrays\n * with no `.rows`. The shim is local to test-utils — production code never\n * needs it.\n */\nfunction withRowsShim(db: PostgresJsDatabase): PostgresJsDatabase {\n return new Proxy(db, {\n get(target, prop, receiver) {\n if (prop === 'execute') {\n return async (...args: unknown[]) => {\n const result = await (target.execute as (...a: unknown[]) => Promise<unknown[]>)(...args)\n if (Array.isArray(result) && !('rows' in result)) {\n Object.defineProperty(result, 'rows', { value: [...result], enumerable: false })\n }\n return result\n }\n }\n return Reflect.get(target, prop, receiver)\n },\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"test-utils.mjs","names":["branches"],"sources":["../src/test-utils.ts"],"sourcesContent":["import { sql } from 'drizzle-orm'\nimport { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport postgres from 'postgres'\nimport { createExtensionStatement } from './extensions.js'\nimport { trackTransactions } from './transaction-hooks.js'\n\nexport interface PushOptions {\n /**\n * Postgres extensions to enable before pushing the schema — the test-side\n * mirror of a plugin's `server.extensions` (e.g. `['btree_gist']` for the\n * reservation GIST exclusion constraint). Installed into `public` (NOT the\n * per-suite schema) because an extension is per-database and installs into a\n * single schema: a per-suite install would be invisible to other parallel\n * suites' `search_path`. `public` is on every suite's search_path, so the\n * operator classes resolve everywhere. Idempotent (`IF NOT EXISTS`).\n */\n extensions?: readonly string[]\n}\n\nexport interface TestDb {\n db: PostgresJsDatabase\n schema: string\n /** Push Drizzle table definitions to the test schema (uses drizzle-kit/api) */\n push: (schemas: Record<string, unknown>, opts?: PushOptions) => Promise<void>\n cleanup: () => Promise<void>\n}\n\n/**\n * Create an isolated test database using a unique PostgreSQL schema.\n * Each test suite gets its own schema so tests can run in parallel safely.\n *\n * Requires `DATABASE_TEST_URL` env var pointing to a running PostgreSQL instance.\n * Falls back to `DATABASE_URL` if test URL is not set.\n *\n * Usage:\n * ```ts\n * const testDb = await createTestDb()\n * await testDb.push({ articles: articlesTable, entity_refs: entityRefsTable })\n * // ... run tests with testDb.db ...\n * await testDb.cleanup()\n * ```\n */\nexport async function createTestDb(): Promise<TestDb> {\n // Refuse to ever provision a scratch schema in production. `createTestDb`\n // issues `CREATE SCHEMA` and (via `cleanup()`) `DROP SCHEMA … CASCADE`,\n // which would be catastrophic if pointed at a real database. The whole\n // function is test-only, but the npm subpath export means an app could\n // import it by accident — fail loud instead of running.\n if (process.env.NODE_ENV === 'production') {\n throw new Error(\n 'createTestDb() is not allowed when NODE_ENV=production — it issues CREATE/DROP SCHEMA and is intended for tests only',\n )\n }\n\n const url = process.env.DATABASE_TEST_URL || process.env.DATABASE_URL\n if (!url) {\n throw new Error('DATABASE_TEST_URL (or DATABASE_URL) env var is required for integration tests')\n }\n\n const schemaName = `test_${crypto.randomUUID().replaceAll('-', '').slice(0, 12)}`\n\n // First connection: create the schema. We can't pass `search_path` as a\n // connection startup parameter for this client because the schema doesn't\n // exist yet — postgres rejects connections asking to enter a missing\n // schema with `invalid value for parameter \"search_path\"`.\n const setupConnection = postgres(url, { max: 1 })\n try {\n await setupConnection.unsafe(`CREATE SCHEMA \"${schemaName}\"`)\n } finally {\n // Always close the setup connection — even on CREATE SCHEMA failure\n // (rare with a UUID-derived name, but possible if the role lacks\n // permission). Without this, a thrown error would leak a postgres\n // connection until the process exits.\n await setupConnection.end()\n }\n\n // Working connection pool: every connection in the pool starts with\n // `search_path` pointing at the test schema. Without this, a second\n // pooled connection would fall back to the default search_path and miss\n // the test tables — which is exactly the bug that surfaced when concurrent\n // queries (e.g. tryClaim's race test) bypassed connection 1.\n const connection = postgres(url, {\n max: 3,\n idle_timeout: 10,\n connection: {\n search_path: `\"${schemaName}\",public`,\n },\n })\n\n // Tracked, like `createDbClient`'s — a suite that opens a transaction must\n // get the on-commit behaviour production runs, not a bare drizzle client's.\n const db = trackTransactions(drizzle(connection))\n\n return {\n db,\n schema: schemaName,\n\n async push(schemas: Record<string, unknown>, opts?: PushOptions) {\n // Enable declared extensions into `public` FIRST (see PushOptions docs for\n // the per-database / parallel-suite reasoning). A dedicated single-use\n // connection pinned to `search_path=public` makes the install schema\n // deterministic regardless of the working pool's per-suite search_path.\n if (opts?.extensions && opts.extensions.length > 0) {\n const extConnection = postgres(url, { max: 1, connection: { search_path: 'public' } })\n try {\n for (const name of opts.extensions) {\n // createExtensionStatement validates the name + builds the canonical SQL.\n await extConnection.unsafe(createExtensionStatement(name))\n }\n } finally {\n await extConnection.end()\n }\n }\n\n // Use createRequire to load drizzle-kit/api via Node's CJS resolver.\n // drizzle-kit/api.mjs bundles CJS code with require('fs') — Vite's ESM\n // transform replaces require() with a polyfill that throws. Native CJS bypasses this.\n const { createRequire } = await import('node:module')\n const require = createRequire(import.meta.url)\n const { pushSchema } = require('drizzle-kit/api') as typeof import('drizzle-kit/api')\n\n // Use default schemaFilters ([\"public\"]). Our tables are unqualified (= public),\n // and SET search_path ensures DDL actually creates them in the test schema.\n const result = await pushSchema(schemas, withRowsShim(db))\n await result.apply()\n },\n\n async cleanup() {\n await db.execute(sql.raw(`DROP SCHEMA IF EXISTS \"${schemaName}\" CASCADE`))\n await connection.end()\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// In-memory TableClient shim — for unit tests that exercise consumer code\n// (LockService, ContentClient draft methods, etc.) without spinning up\n// Postgres. Implements the subset of TableClient that consumers actually\n// use; the matcher understands the same WhereClause shape (eq shorthand,\n// lt/gte/in/notIn operators, $or branches, jsonb-path keys are NOT\n// supported — use the real DB for those).\n//\n// Each consumer test previously hand-rolled its own ~100-LoC version of\n// this; collapsed into one place per audit-quality-content L#12.\n// ---------------------------------------------------------------------------\n\n/** Operators recognized by the in-memory matcher. Mirrors WhereClause. */\ntype MemOp =\n | { eq: unknown }\n | { ne: unknown }\n | { in: unknown[] }\n | { notIn: unknown[] }\n | { lt: unknown }\n | { lte: unknown }\n | { gt: unknown }\n | { gte: unknown }\n | { isNull: true }\n | { isNotNull: true }\n\nfunction matchesOp(rowVal: unknown, op: MemOp): boolean {\n if ('eq' in op) return rowVal === op.eq\n if ('ne' in op) return rowVal !== op.ne\n if ('in' in op) return op.in.includes(rowVal)\n if ('notIn' in op) return !op.notIn.includes(rowVal)\n if ('lt' in op) return (rowVal as Date | number) < (op.lt as Date | number)\n if ('lte' in op) return (rowVal as Date | number) <= (op.lte as Date | number)\n if ('gt' in op) return (rowVal as Date | number) > (op.gt as Date | number)\n if ('gte' in op) return (rowVal as Date | number) >= (op.gte as Date | number)\n if ('isNull' in op) return rowVal === null || rowVal === undefined\n if ('isNotNull' in op) return rowVal !== null && rowVal !== undefined\n return false\n}\n\n/** Recursive matcher: handles top-level keys, $or, value shorthand, and operator objects. */\nfunction matchesRow(row: Record<string, unknown>, where: Record<string, unknown>): boolean {\n for (const [key, val] of Object.entries(where)) {\n if (key === '$or') {\n const branches = val as Record<string, unknown>[]\n if (!branches.some((b) => matchesRow(row, b))) return false\n continue\n }\n if (key === '$and') {\n const branches = val as Record<string, unknown>[]\n if (!branches.every((b) => matchesRow(row, b))) return false\n continue\n }\n const rowVal = row[key]\n if (val === null) {\n if (rowVal !== null && rowVal !== undefined) return false\n continue\n }\n if (val !== null && typeof val === 'object' && !(val instanceof Date)) {\n if (!matchesOp(rowVal, val as MemOp)) return false\n continue\n }\n if (rowVal !== val) return false\n }\n return true\n}\n\n/** OrderBy spec mirrors TableClient's `OrderBySpec`. */\nexport interface InMemoryOrderBy {\n column: string\n dir?: 'asc' | 'desc'\n}\n\n/** Subset of TableClient methods used by tests. */\nexport interface InMemoryTableClient<Row extends Record<string, unknown>> {\n rows: Row[] // direct mutation for test seeding / inspection\n reset(initial?: Row[]): void\n findOne(where: Record<string, unknown>): Promise<Row | null>\n findMany(opts?: {\n where?: Record<string, unknown>\n orderBy?: InMemoryOrderBy[]\n limit?: number\n offset?: number\n }): Promise<Row[]>\n exists(where: Record<string, unknown>): Promise<boolean>\n count(where?: Record<string, unknown>): Promise<number>\n insert(values: Partial<Row>): Promise<Row>\n update(where: Record<string, unknown>, patch: Partial<Row>): Promise<Row | null>\n upsert(\n values: Partial<Row>,\n opts: { target: string[] | string; set?: Partial<Row> },\n ): Promise<Row>\n delete(where: Record<string, unknown>): Promise<Row | null>\n deleteMany(where: Record<string, unknown>): Promise<Row[]>\n tryClaim(\n values: Partial<Row>,\n opts: {\n target: string | string[]\n replaceWhen: Record<string, unknown>\n set?: Partial<Row>\n },\n ): Promise<{ acquired: true; row: Row } | { acquired: false; row: Row }>\n aggregate?: (opts: {\n select: Record<string, { fn: 'max' | 'min' | 'count' | 'sum' | 'avg'; column?: string }>\n where?: Record<string, unknown>\n }) => Promise<Array<Record<string, unknown>>>\n}\n\n/**\n * Build an in-memory TableClient stand-in for unit tests. Pass\n * `idGenerator` if your test wants deterministic synthetic IDs\n * (defaults to a monotonic `row-1`, `row-2`, ... counter).\n *\n * Example:\n * ```ts\n * const drafts = createInMemoryTableClient<DraftRow>({ idField: 'id' })\n * vi.mock('@murumets-ee/content/schema', () => ({\n * getContentTables: () => ({ drafts: { makeClient: () => drafts } })\n * }))\n * ```\n */\nexport function createInMemoryTableClient<Row extends Record<string, unknown>>(\n options: { idField?: keyof Row & string; idGenerator?: () => string; initial?: Row[] } = {},\n): InMemoryTableClient<Row> {\n const idField = (options.idField ?? 'id') as keyof Row & string\n let nextId = 1\n const idGen = options.idGenerator ?? (() => `row-${nextId++}`)\n\n const state: { rows: Row[] } = { rows: options.initial ? [...options.initial] : [] }\n\n const client: InMemoryTableClient<Row> = {\n get rows() {\n return state.rows\n },\n set rows(next: Row[]) {\n state.rows = next\n },\n\n reset(initial?: Row[]) {\n state.rows = initial ? [...initial] : []\n nextId = 1\n },\n\n async findOne(where) {\n return state.rows.find((r) => matchesRow(r, where)) ?? null\n },\n\n async findMany(opts) {\n let out = opts?.where\n ? state.rows.filter((r) => matchesRow(r, opts.where as Record<string, unknown>))\n : [...state.rows]\n if (opts?.orderBy && opts.orderBy.length > 0) {\n out = [...out].sort((a, b) => {\n for (const spec of opts.orderBy ?? []) {\n const av = a[spec.column]\n const bv = b[spec.column]\n const dir = spec.dir === 'desc' ? -1 : 1\n // Date and number comparisons via subtraction; string via localeCompare.\n if (av instanceof Date && bv instanceof Date) {\n const d = (av.getTime() - bv.getTime()) * dir\n if (d !== 0) return d\n } else if (typeof av === 'number' && typeof bv === 'number') {\n const d = (av - bv) * dir\n if (d !== 0) return d\n } else if (typeof av === 'string' && typeof bv === 'string') {\n const d = av.localeCompare(bv) * dir\n if (d !== 0) return d\n } else {\n // Mixed / nullable: fall through to next spec.\n }\n }\n return 0\n })\n }\n const offset = opts?.offset ?? 0\n const limit = opts?.limit\n if (offset > 0) out = out.slice(offset)\n if (limit !== undefined && limit >= 0) out = out.slice(0, limit)\n return out\n },\n\n async aggregate(opts) {\n const subset = opts?.where\n ? state.rows.filter((r) => matchesRow(r, opts.where as Record<string, unknown>))\n : state.rows\n const result: Record<string, unknown> = {}\n for (const [alias, spec] of Object.entries(opts.select)) {\n if (spec.fn === 'count') {\n result[alias] = subset.length\n } else if (spec.fn === 'max' && spec.column) {\n let max: number | null = null\n for (const r of subset) {\n const v = r[spec.column]\n if (typeof v === 'number') {\n if (max === null || v > max) max = v\n }\n }\n result[alias] = max\n } else if (spec.fn === 'min' && spec.column) {\n let min: number | null = null\n for (const r of subset) {\n const v = r[spec.column]\n if (typeof v === 'number') {\n if (min === null || v < min) min = v\n }\n }\n result[alias] = min\n } else if (spec.fn === 'sum' && spec.column) {\n let sum = 0\n for (const r of subset) {\n const v = r[spec.column]\n if (typeof v === 'number') sum += v\n }\n result[alias] = sum\n }\n }\n return [result]\n },\n\n async exists(where) {\n return state.rows.some((r) => matchesRow(r, where))\n },\n\n async count(where) {\n if (!where) return state.rows.length\n return state.rows.filter((r) => matchesRow(r, where)).length\n },\n\n async insert(values) {\n const row = { [idField]: idGen(), ...values } as Row\n state.rows.push(row)\n return row\n },\n\n async update(where, patch) {\n const matched = state.rows.filter((r) => matchesRow(r, where))\n if (matched.length === 0) return null\n // TableClient.update expects at-most-one match\n const target = matched[0] as Row\n Object.assign(target, patch)\n return target\n },\n\n async upsert(values, opts) {\n const targetKeys = Array.isArray(opts.target) ? opts.target : [opts.target]\n const targetWhere: Record<string, unknown> = {}\n for (const k of targetKeys) targetWhere[k] = (values as Record<string, unknown>)[k]\n const existing = state.rows.find((r) => matchesRow(r, targetWhere))\n if (existing) {\n Object.assign(existing, opts.set ?? values)\n return existing\n }\n const row = { [idField]: idGen(), ...values } as Row\n state.rows.push(row)\n return row\n },\n\n async delete(where) {\n const idx = state.rows.findIndex((r) => matchesRow(r, where))\n if (idx === -1) return null\n const [removed] = state.rows.splice(idx, 1)\n return (removed ?? null) as Row | null\n },\n\n async deleteMany(where) {\n const matched = state.rows.filter((r) => matchesRow(r, where))\n state.rows = state.rows.filter((r) => !matched.includes(r))\n return matched\n },\n\n async tryClaim(values, opts) {\n const targetKeys = Array.isArray(opts.target) ? opts.target : [opts.target]\n const targetWhere: Record<string, unknown> = {}\n for (const k of targetKeys) targetWhere[k] = (values as Record<string, unknown>)[k]\n const existing = state.rows.find((r) => matchesRow(r, targetWhere))\n\n if (!existing) {\n const row = { [idField]: idGen(), ...values } as Row\n state.rows.push(row)\n return { acquired: true, row }\n }\n\n if (matchesRow(existing, opts.replaceWhen)) {\n Object.assign(existing, opts.set ?? values)\n return { acquired: true, row: existing }\n }\n\n return { acquired: false, row: existing }\n },\n }\n\n return client\n}\n\n/**\n * Wrap a Drizzle handle so `.execute()` returns an array that *also* exposes\n * a `.rows` property. `drizzle-kit/api`'s `pushSchema` reads `res.rows`, but\n * postgres-js (the driver Drizzle is built on here) returns plain arrays\n * with no `.rows`. The shim is local to test-utils — production code never\n * needs it.\n */\nfunction withRowsShim(db: PostgresJsDatabase): PostgresJsDatabase {\n return new Proxy(db, {\n get(target, prop, receiver) {\n if (prop === 'execute') {\n return async (...args: unknown[]) => {\n const result = await (target.execute as (...a: unknown[]) => Promise<unknown[]>)(...args)\n if (Array.isArray(result) && !('rows' in result)) {\n Object.defineProperty(result, 'rows', { value: [...result], enumerable: false })\n }\n return result\n }\n }\n return Reflect.get(target, prop, receiver)\n },\n })\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const e=/^[a-z_][a-z0-9_]*$/;function t(t){if(!e.test(t))throw Error(`Invalid Postgres extension name: ${JSON.stringify(t)}. Must match ${e} (lowercase letters, digits, underscores; no quotes, whitespace, or punctuation).`)}function n(e){return t(e),`CREATE EXTENSION IF NOT EXISTS "${e}"`}export{t as n,n as r,e as t};
|
|
2
|
-
//# sourceMappingURL=extensions-CyFQK8AR.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"extensions-CyFQK8AR.mjs","names":[],"sources":["../src/extensions.ts"],"sourcesContent":["/**\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":"AA0BA,MAAa,EAAyB,qBAOtC,SAAgB,EAAyB,EAAoB,CAC3D,GAAI,CAAC,EAAuB,KAAK,CAAI,EACnC,MAAU,MACR,oCAAoC,KAAK,UAAU,CAAI,EAAE,eACzC,EAAuB,kFAEzC,CAEJ,CAUA,SAAgB,EAAyB,EAAsB,CAE7D,OADA,EAAyB,CAAI,EACtB,mCAAmC,EAAK,EACjD"}
|