@fonderie/store 0.2.0 → 0.2.2
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/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/sql.ts","../src/migrations/runner.ts","../src/migrations/path.ts","../src/adapters/pg.ts","../src/versioned.ts"],"sourcesContent":["// ── Public API ───────────────────────────────────────────────────\nexport { sql } from './sql';\nexport type { ISqlQuery } from './sql';\nexport type { IStoreAdapter, IPoolConfig } from './types';\nexport { MigrationRunner, InternalMigrationRunner, createMigrationsPath } from './migrations';\nexport { PGAdapter } from './adapters/pg';\n\n// Versioned-resource control-plane primitive — version index + optimistic\n// concurrency + advisory-locked writes + revisions + rollback + push-notify.\nexport { versionedWrite, versionedRollback, VersionConflictError } from './versioned';\nexport type { IVersionedResource } from './versioned';\n","export interface ISqlQuery {\n\ttext: string;\n\tparams: unknown[];\n}\n\n// Tagged template literal for safe parameterized queries.\n// Never concatenates user input — always uses $N placeholders.\n//\n// Usage:\n// const { text, params } = sql`SELECT * FROM users WHERE id = ${userId}`\n// const rows = await store.query<User>(text, params)\n\nexport function sql(strings: TemplateStringsArray, ...values: unknown[]): ISqlQuery {\n\tlet text = '';\n\tconst params: unknown[] = [];\n\n\tstrings.forEach((str, i) => {\n\t\ttext += str;\n\t\tif (i < values.length) {\n\t\t\tparams.push(values[i]);\n\t\t\ttext += `$${params.length}`;\n\t\t}\n\t});\n\n\treturn { text, params };\n}\n","import { join } from 'node:path';\nimport { readdir, readFile } from 'node:fs/promises';\n\nimport type { IStoreAdapter } from '../types';\n\nconst MIGRATIONS_TABLE = 'fonderie_migrations';\nconst RESERVED_PREFIX_RE = /\\bfonderie_/i;\n\nexport class MigrationRunner {\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate migrationsDir: string,\n\t) {}\n\n\tasync run(): Promise<void> {\n\t\tawait this.ensureTable();\n\n\t\tconst [applied, files] = await Promise.all([this.getApplied(), this.getFiles()]);\n\n\t\tconst pending = files.filter((f) => !applied.has(f));\n\n\t\tif (pending.length === 0) {\n\t\t\tconsole.log('[store] migrations: up to date');\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const file of pending) {\n\t\t\tconst sql = await readFile(join(this.migrationsDir, file), 'utf8');\n\n\t\t\tthis.assertNoReservedPrefix(file, sql);\n\n\t\t\tawait this.store.transaction(async (tx) => {\n\t\t\t\tawait tx.query(sql);\n\t\t\t\tawait tx.query(`INSERT INTO ${MIGRATIONS_TABLE} (name, applied_at) VALUES ($1, now())`, [\n\t\t\t\t\tfile,\n\t\t\t\t]);\n\t\t\t});\n\n\t\t\tconsole.log(`[store] migrations: applied ${file}`);\n\t\t}\n\t}\n\n\tprotected assertNoReservedPrefix(file: string, sql: string): void {\n\t\tif (RESERVED_PREFIX_RE.test(sql)) {\n\t\t\tthrow new Error(\n\t\t\t\t`[store] migration \"${file}\" uses the reserved \"fonderie_\" prefix. ` +\n\t\t\t\t`Use InternalMigrationRunner for fonderie-owned migrations.`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async ensureTable(): Promise<void> {\n\t\tawait this.store.query(`\n\t\t\tCREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (\n\t\t\t name TEXT PRIMARY KEY,\n\t\t\t applied_at TIMESTAMPTZ NOT NULL DEFAULT now()\n\t\t\t)\n\t\t`);\n\t}\n\n\tprivate async getApplied(): Promise<Set<string>> {\n\t\tconst rows = await this.store.query<{ name: string }>(\n\t\t\t`SELECT name FROM ${MIGRATIONS_TABLE} ORDER BY name`,\n\t\t);\n\t\treturn new Set(rows.map((r) => r.name));\n\t}\n\n\tprivate async getFiles(): Promise<string[]> {\n\t\tconst all = await readdir(this.migrationsDir);\n\t\treturn all.filter((f) => f.endsWith('.sql')).sort(); // lexicographic — timestamp prefix keeps order correct\n\t}\n}\n\n// For fonderie-internal use only. Skips the reserved-prefix guard.\nexport class InternalMigrationRunner extends MigrationRunner {\n\tprotected override assertNoReservedPrefix(_file: string, _sql: string): void {}\n}\n","import { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\n\n// Each package calls this with its own import.meta.url to resolve\n// the absolute path to its compiled migrations/sql/ directory.\n// Called from dist/migrations/index.js, so dirname is already dist/migrations/.\n//\n// Usage in any package's migrations/index.ts:\n// export const getMigrationsPath = () => createMigrationsPath(import.meta.url)\nexport function createMigrationsPath(importMetaUrl: string): string {\n\treturn join(dirname(fileURLToPath(importMetaUrl)), 'sql');\n}\n","import pg from 'pg';\nimport type { IStoreAdapter, IPoolConfig } from '../types';\n\nexport class PGAdapter implements IStoreAdapter {\n\tprivate pool: pg.Pool;\n\n\tconstructor(config: IPoolConfig | string) {\n\t\tconst options = typeof config === 'string' ? { connectionString: config } : config;\n\t\tthis.pool = new pg.Pool(options);\n\n\t\tthis.pool.on('error', (err) => {\n\t\t\tconsole.error('[store] idle client error', err.message);\n\t\t});\n\t}\n\n\tasync testConnection(): Promise<boolean> {\n\t\ttry {\n\t\t\tawait this.pool.query('SELECT 1');\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tasync query<T = unknown>(sql: string, params?: unknown[]): Promise<T[]> {\n\t\tconst result = await this.pool.query(sql, params);\n\t\treturn result.rows as T[];\n\t}\n\n\tasync transaction<T>(fn: (tx: IStoreAdapter) => Promise<T>): Promise<T> {\n\t\tconst client = await this.pool.connect();\n\n\t\ttry {\n\t\t\tawait client.query('BEGIN');\n\n\t\t\tconst tx: IStoreAdapter = {\n\t\t\t\tquery: async <U = unknown>(sql: string, params?: unknown[]) => {\n\t\t\t\t\tconst result = await client.query(sql, params);\n\t\t\t\t\treturn result.rows as U[];\n\t\t\t\t},\n\t\t\t\ttransaction: <V>(nested: (tx: IStoreAdapter) => Promise<V>) => nested(tx),\n\t\t\t};\n\n\t\t\tconst result = await fn(tx);\n\t\t\tawait client.query('COMMIT');\n\t\t\treturn result;\n\t\t} catch (err) {\n\t\t\tawait client.query('ROLLBACK');\n\t\t\tthrow err;\n\t\t} finally {\n\t\t\tclient.release();\n\t\t}\n\t}\n\n\tasync end(): Promise<void> {\n\t\tawait this.pool.end();\n\t}\n}\n","import type { IStoreAdapter } from './types';\n\n// The control-plane primitive: a version index + optimistic concurrency +\n// advisory-locked writes + append-only revisions + push-notify, reusable by any\n// resource identified by a (primary, scope) key pair. `@fonderie/config` (config\n// + secrets) and `@fonderie/courier` (email templates) run on it; a new resource\n// is a descriptor away. Lives in `store` because it's pure Postgres machinery\n// and every package already depends on store (no cycles).\n\n// Thrown when an optimistic-concurrency write loses the compare-and-swap: the\n// row's current version isn't the one the caller wrote against. Reject-and-retry.\nexport class VersionConflictError extends Error {\n\tconstructor(\n\t\tpublic readonly key: string,\n\t\tpublic readonly scope: string | null,\n\t\tpublic readonly currentVersion: number | null,\n\t\tpublic readonly expectedVersion: number,\n\t) {\n\t\tsuper(\n\t\t\t`\"${key}\" (${scope ?? 'base'}) is at version ${currentVersion ?? 'none'}, ` +\n\t\t\t\t`not ${expectedVersion} — reload and retry`,\n\t\t);\n\t\tthis.name = 'VersionConflictError';\n\t}\n}\n\n// A versioned resource's Postgres surface.\nexport interface IVersionedResource {\n\ttable: string; // main table, e.g. 'fonderie_config'\n\trevisions: string; // history table, e.g. 'fonderie_config_revisions'\n\tchannel: string; // LISTEN/NOTIFY channel, e.g. 'fonderie_config_changed'\n\t// The (primary, scope) key pair — config: ['key','environment'], courier:\n\t// ['type','locale']. `scope` is null-safe (a NULL locale is the base).\n\tkeyColumns: readonly [string, string];\n\t// Content columns — written AND snapshotted into revisions (config: ['value'];\n\t// courier: ['subject','html','text']).\n\tcontentColumns: readonly string[];\n\t// Extra main-table columns, set only when supplied, never revisioned (config:\n\t// ['description','active']; courier: ['active']).\n\tmetaColumns?: readonly string[];\n\t// SELECT/RETURNING column list shaping the caller's row type.\n\treturning: string;\n}\n\nconst lockSql = `SELECT pg_advisory_xact_lock(hashtext($1), hashtext($2))`;\n\nfunction keyMatch(r: IVersionedResource): string {\n\tconst [id, scope] = r.keyColumns;\n\treturn `${id} IS NOT DISTINCT FROM $1 AND ${scope} IS NOT DISTINCT FROM $2`;\n}\n\n// Write one versioned entry: advisory-lock the (key, scope) pair (serializes even\n// a create), enforce optimistic concurrency when `ifVersion` is given, bump the\n// version, append a revision (content columns only), and broadcast invalidation\n// on commit. `data` supplies every content column and any meta columns to set.\nexport async function versionedWrite<T>(\n\tr: IVersionedResource,\n\tstore: IStoreAdapter,\n\topts: {\n\t\tkey: string;\n\t\tscope: string | null;\n\t\tdata: Record<string, unknown>;\n\t\tifVersion?: number;\n\t\tactor: string | null;\n\t},\n): Promise<T> {\n\tconst [idCol, scopeCol] = r.keyColumns;\n\tconst content = r.contentColumns;\n\tconst meta = (r.metaColumns ?? []).filter((c) => c in opts.data);\n\tconst contentVals = content.map((c) => opts.data[c] ?? null);\n\tconst metaVals = meta.map((c) => opts.data[c] ?? null);\n\n\treturn store.transaction(async (tx) => {\n\t\tawait tx.query(lockSql, [opts.key, opts.scope ?? '']);\n\n\t\tconst [cur] = await tx.query<{ version: number }>(\n\t\t\t`SELECT version FROM ${r.table} WHERE ${keyMatch(r)} FOR UPDATE`,\n\t\t\t[opts.key, opts.scope],\n\t\t);\n\t\tconst currentVersion = cur?.version ?? null;\n\t\tif (opts.ifVersion !== undefined && currentVersion !== opts.ifVersion) {\n\t\t\tthrow new VersionConflictError(opts.key, opts.scope, currentVersion, opts.ifVersion);\n\t\t}\n\t\tconst version = (currentVersion ?? 0) + 1;\n\t\tconst writeVals = [opts.key, opts.scope, ...contentVals, ...metaVals, version, opts.actor];\n\n\t\tlet row: T | undefined;\n\t\tif (cur) {\n\t\t\tconst sets = [\n\t\t\t\t...content.map((c, i) => `${c} = $${3 + i}`),\n\t\t\t\t...meta.map((c, j) => `${c} = $${3 + content.length + j}`),\n\t\t\t\t`version = $${3 + content.length + meta.length}`,\n\t\t\t\t`updated_by = $${4 + content.length + meta.length}`,\n\t\t\t\t`updated_at = now()`,\n\t\t\t];\n\t\t\t[row] = await tx.query<T>(\n\t\t\t\t`UPDATE ${r.table} SET ${sets.join(', ')} WHERE ${keyMatch(r)} RETURNING ${r.returning}`,\n\t\t\t\twriteVals,\n\t\t\t);\n\t\t} else {\n\t\t\tconst cols = [idCol, scopeCol, ...content, ...meta, 'version', 'updated_by'];\n\t\t\tconst ph = cols.map((_, i) => `$${i + 1}`);\n\t\t\t[row] = await tx.query<T>(\n\t\t\t\t`INSERT INTO ${r.table} (${cols.join(', ')}) VALUES (${ph.join(', ')}) RETURNING ${r.returning}`,\n\t\t\t\twriteVals,\n\t\t\t);\n\t\t}\n\n\t\tconst revCols = [idCol, scopeCol, ...content, 'version', 'actor'];\n\t\tconst revPh = revCols.map((_, i) => `$${i + 1}`);\n\t\tawait tx.query(\n\t\t\t`INSERT INTO ${r.revisions} (${revCols.join(', ')}) VALUES (${revPh.join(', ')})`,\n\t\t\t[opts.key, opts.scope, ...contentVals, version, opts.actor],\n\t\t);\n\t\tawait tx.query(`SELECT pg_notify('${r.channel}', $1)`, [opts.scope ?? '']);\n\n\t\tif (!row) throw new Error(`Failed to write ${r.table} entry`);\n\t\treturn row;\n\t});\n}\n\n// Roll *forward* to a past revision's content as a new version (rollout undo —\n// history is never rewritten).\nexport async function versionedRollback<T>(\n\tr: IVersionedResource,\n\tstore: IStoreAdapter,\n\topts: { key: string; scope: string | null; toVersion: number; actor: string | null },\n): Promise<T> {\n\tconst [idCol, scopeCol] = r.keyColumns;\n\tconst content = r.contentColumns;\n\n\treturn store.transaction(async (tx) => {\n\t\tawait tx.query(lockSql, [opts.key, opts.scope ?? '']);\n\n\t\tconst [target] = await tx.query<Record<string, unknown>>(\n\t\t\t`SELECT ${content.join(', ')} FROM ${r.revisions} WHERE ${keyMatch(r)} AND version = $3`,\n\t\t\t[opts.key, opts.scope, opts.toVersion],\n\t\t);\n\t\tif (!target) {\n\t\t\tthrow new Error(`\"${opts.key}\" (${opts.scope ?? 'base'}) has no revision ${opts.toVersion}`);\n\t\t}\n\t\tconst [cur] = await tx.query<{ version: number }>(\n\t\t\t`SELECT version FROM ${r.table} WHERE ${keyMatch(r)} FOR UPDATE`,\n\t\t\t[opts.key, opts.scope],\n\t\t);\n\t\tif (!cur) throw new Error(`\"${opts.key}\" (${opts.scope ?? 'base'}) does not exist`);\n\t\tconst version = cur.version + 1;\n\t\tconst contentVals = content.map((c) => target[c] ?? null);\n\t\tconst writeVals = [opts.key, opts.scope, ...contentVals, version, opts.actor];\n\n\t\tconst sets = [\n\t\t\t...content.map((c, i) => `${c} = $${3 + i}`),\n\t\t\t`version = $${3 + content.length}`,\n\t\t\t`updated_by = $${4 + content.length}`,\n\t\t\t`updated_at = now()`,\n\t\t];\n\t\tconst [row] = await tx.query<T>(\n\t\t\t`UPDATE ${r.table} SET ${sets.join(', ')} WHERE ${keyMatch(r)} RETURNING ${r.returning}`,\n\t\t\twriteVals,\n\t\t);\n\n\t\tconst revCols = [idCol, scopeCol, ...content, 'version', 'actor'];\n\t\tconst revPh = revCols.map((_, i) => `$${i + 1}`);\n\t\tawait tx.query(\n\t\t\t`INSERT INTO ${r.revisions} (${revCols.join(', ')}) VALUES (${revPh.join(', ')})`,\n\t\t\twriteVals.slice(0, 2 + content.length).concat(version, opts.actor),\n\t\t);\n\t\tawait tx.query(`SELECT pg_notify('${r.channel}', $1)`, [opts.scope ?? '']);\n\n\t\tif (!row) throw new Error('rollback failed');\n\t\treturn row;\n\t});\n}\n"],"mappings":"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","names":["sql","import_node_path","pg","sql","result"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/sql.ts","../src/migrations/runner.ts","../src/migrations/path.ts","../src/adapters/pg.ts","../src/versioned.ts"],"sourcesContent":["// ── Public API ───────────────────────────────────────────────────\nexport { sql } from './sql';\nexport type { ISqlQuery } from './sql';\nexport type { IStoreAdapter, IPoolConfig } from './types';\nexport { MigrationRunner, InternalMigrationRunner, createMigrationsPath } from './migrations';\nexport { PGAdapter } from './adapters/pg';\n\n// Versioned-resource control-plane primitive — version index + optimistic\n// concurrency + advisory-locked writes + revisions + rollback + push-notify.\nexport { versionedWrite, versionedRollback, VersionConflictError } from './versioned';\nexport type { IVersionedResource } from './versioned';\n","export interface ISqlQuery {\n\ttext: string;\n\tparams: unknown[];\n}\n\n// Tagged template literal for safe parameterized queries.\n// Never concatenates user input — always uses $N placeholders.\n//\n// Usage:\n// const { text, params } = sql`SELECT * FROM users WHERE id = ${userId}`\n// const rows = await store.query<User>(text, params)\n\nexport function sql(strings: TemplateStringsArray, ...values: unknown[]): ISqlQuery {\n\tlet text = '';\n\tconst params: unknown[] = [];\n\n\tstrings.forEach((str, i) => {\n\t\ttext += str;\n\t\tif (i < values.length) {\n\t\t\tparams.push(values[i]);\n\t\t\ttext += `$${params.length}`;\n\t\t}\n\t});\n\n\treturn { text, params };\n}\n","import { join } from 'node:path';\nimport { readdir, readFile } from 'node:fs/promises';\n\nimport type { IStoreAdapter } from '../types';\n\nconst MIGRATIONS_TABLE = 'fonderie_migrations';\nconst RESERVED_PREFIX_RE = /\\bfonderie_/i;\n\nexport class MigrationRunner {\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate migrationsDir: string,\n\t) {}\n\n\tasync run(): Promise<void> {\n\t\tawait this.ensureTable();\n\n\t\tconst [applied, files] = await Promise.all([this.getApplied(), this.getFiles()]);\n\n\t\tconst pending = files.filter((f) => !applied.has(f));\n\n\t\tif (pending.length === 0) {\n\t\t\tconsole.log('[store] migrations: up to date');\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const file of pending) {\n\t\t\tconst sql = await readFile(join(this.migrationsDir, file), 'utf8');\n\n\t\t\tthis.assertNoReservedPrefix(file, sql);\n\n\t\t\tawait this.store.transaction(async (tx) => {\n\t\t\t\tawait tx.query(sql);\n\t\t\t\tawait tx.query(`INSERT INTO ${MIGRATIONS_TABLE} (name, applied_at) VALUES ($1, now())`, [\n\t\t\t\t\tfile,\n\t\t\t\t]);\n\t\t\t});\n\n\t\t\tconsole.log(`[store] migrations: applied ${file}`);\n\t\t}\n\t}\n\n\tprotected assertNoReservedPrefix(file: string, sql: string): void {\n\t\tif (RESERVED_PREFIX_RE.test(sql)) {\n\t\t\tthrow new Error(\n\t\t\t\t`[store] migration \"${file}\" uses the reserved \"fonderie_\" prefix. ` +\n\t\t\t\t`Use InternalMigrationRunner for fonderie-owned migrations.`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async ensureTable(): Promise<void> {\n\t\tawait this.store.query(`\n\t\t\tCREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (\n\t\t\t name TEXT PRIMARY KEY,\n\t\t\t applied_at TIMESTAMPTZ NOT NULL DEFAULT now()\n\t\t\t)\n\t\t`);\n\t}\n\n\tprivate async getApplied(): Promise<Set<string>> {\n\t\tconst rows = await this.store.query<{ name: string }>(\n\t\t\t`SELECT name FROM ${MIGRATIONS_TABLE} ORDER BY name`,\n\t\t);\n\t\treturn new Set(rows.map((r) => r.name));\n\t}\n\n\tprivate async getFiles(): Promise<string[]> {\n\t\tconst all = await readdir(this.migrationsDir);\n\t\treturn all.filter((f) => f.endsWith('.sql')).sort(); // lexicographic — timestamp prefix keeps order correct\n\t}\n}\n\n// For fonderie-internal use only. Skips the reserved-prefix guard.\nexport class InternalMigrationRunner extends MigrationRunner {\n\tprotected override assertNoReservedPrefix(_file: string, _sql: string): void {}\n}\n","import { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\n\n// Each package calls this with its own import.meta.url to resolve\n// the absolute path to its compiled migrations/sql/ directory.\n// Called from dist/migrations/index.js, so dirname is already dist/migrations/.\n//\n// Usage in any package's migrations/index.ts:\n// export const getMigrationsPath = () => createMigrationsPath(import.meta.url)\nexport function createMigrationsPath(importMetaUrl: string): string {\n\treturn join(dirname(fileURLToPath(importMetaUrl)), 'sql');\n}\n","import pg from 'pg';\nimport type { IStoreAdapter, IPoolConfig } from '../types';\n\nexport class PGAdapter implements IStoreAdapter {\n\tprivate pool: pg.Pool;\n\n\tconstructor(config: IPoolConfig | string) {\n\t\tconst options = typeof config === 'string' ? { connectionString: config } : config;\n\t\tthis.pool = new pg.Pool(options);\n\n\t\tthis.pool.on('error', (err) => {\n\t\t\tconsole.error('[store] idle client error', err.message);\n\t\t});\n\t}\n\n\tasync testConnection(): Promise<boolean> {\n\t\ttry {\n\t\t\tawait this.pool.query('SELECT 1');\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tasync query<T = unknown>(sql: string, params?: unknown[]): Promise<T[]> {\n\t\tconst result = await this.pool.query(sql, params);\n\t\treturn result.rows as T[];\n\t}\n\n\tasync transaction<T>(fn: (tx: IStoreAdapter) => Promise<T>): Promise<T> {\n\t\tconst client = await this.pool.connect();\n\n\t\ttry {\n\t\t\tawait client.query('BEGIN');\n\n\t\t\tconst tx: IStoreAdapter = {\n\t\t\t\tquery: async <U = unknown>(sql: string, params?: unknown[]) => {\n\t\t\t\t\tconst result = await client.query(sql, params);\n\t\t\t\t\treturn result.rows as U[];\n\t\t\t\t},\n\t\t\t\ttransaction: <V>(nested: (tx: IStoreAdapter) => Promise<V>) => nested(tx),\n\t\t\t};\n\n\t\t\tconst result = await fn(tx);\n\t\t\tawait client.query('COMMIT');\n\t\t\treturn result;\n\t\t} catch (err) {\n\t\t\tawait client.query('ROLLBACK');\n\t\t\tthrow err;\n\t\t} finally {\n\t\t\tclient.release();\n\t\t}\n\t}\n\n\tasync end(): Promise<void> {\n\t\tawait this.pool.end();\n\t}\n}\n","import type { IStoreAdapter } from './types';\n\n// The control-plane primitive: a version index + optimistic concurrency +\n// advisory-locked writes + append-only revisions + push-notify, reusable by any\n// resource identified by a (primary, scope) key pair. `@fonderie/config` (config\n// + secrets) and `@fonderie/courier` (email templates) run on it; a new resource\n// is a descriptor away. Lives in `store` because it's pure Postgres machinery\n// and every package already depends on store (no cycles).\n\n// Thrown when an optimistic-concurrency write loses the compare-and-swap: the\n// row's current version isn't the one the caller wrote against. Reject-and-retry.\nexport class VersionConflictError extends Error {\n\tconstructor(\n\t\tpublic readonly key: string,\n\t\tpublic readonly scope: string | null,\n\t\tpublic readonly currentVersion: number | null,\n\t\tpublic readonly expectedVersion: number,\n\t) {\n\t\tsuper(\n\t\t\t`\"${key}\" (${scope ?? 'base'}) is at version ${currentVersion ?? 'none'}, ` +\n\t\t\t\t`not ${expectedVersion} — reload and retry`,\n\t\t);\n\t\tthis.name = 'VersionConflictError';\n\t}\n}\n\n// A versioned resource's Postgres surface.\nexport interface IVersionedResource {\n\ttable: string; // main table, e.g. 'fonderie_config'\n\trevisions: string; // history table, e.g. 'fonderie_config_revisions'\n\tchannel: string; // LISTEN/NOTIFY channel, e.g. 'fonderie_config_changed'\n\t// The (primary, scope) key pair — config: ['key','environment'], courier:\n\t// ['type','locale']. `scope` is null-safe (a NULL locale is the base).\n\t// Intentionally a fixed 2-tuple: every resource is addressed by exactly one\n\t// primary key plus one optional scope. Composite 3+-part keys are out of\n\t// scope by design — model the extra dimension inside the primary key (e.g.\n\t// a compound string) or the scope rather than widening this contract.\n\tkeyColumns: readonly [string, string];\n\t// Content columns — written AND snapshotted into revisions (config: ['value'];\n\t// courier: ['subject','html','text']).\n\tcontentColumns: readonly string[];\n\t// Extra main-table columns, set only when supplied, never revisioned (config:\n\t// ['description','active']; courier: ['active']).\n\tmetaColumns?: readonly string[];\n\t// SELECT/RETURNING column list shaping the caller's row type.\n\treturning: string;\n}\n\nconst lockSql = `SELECT pg_advisory_xact_lock(hashtext($1), hashtext($2))`;\n\nfunction keyMatch(r: IVersionedResource): string {\n\tconst [id, scope] = r.keyColumns;\n\treturn `${id} IS NOT DISTINCT FROM $1 AND ${scope} IS NOT DISTINCT FROM $2`;\n}\n\n// Write one versioned entry: advisory-lock the (key, scope) pair (serializes even\n// a create), enforce optimistic concurrency when `ifVersion` is given, bump the\n// version, append a revision (content columns only), and broadcast invalidation\n// on commit. `data` supplies every content column and any meta columns to set.\nexport async function versionedWrite<T>(\n\tr: IVersionedResource,\n\tstore: IStoreAdapter,\n\topts: {\n\t\tkey: string;\n\t\tscope: string | null;\n\t\tdata: Record<string, unknown>;\n\t\tifVersion?: number;\n\t\tactor: string | null;\n\t},\n): Promise<T> {\n\tconst [idCol, scopeCol] = r.keyColumns;\n\tconst content = r.contentColumns;\n\tconst meta = (r.metaColumns ?? []).filter((c) => c in opts.data);\n\tconst contentVals = content.map((c) => opts.data[c] ?? null);\n\tconst metaVals = meta.map((c) => opts.data[c] ?? null);\n\n\treturn store.transaction(async (tx) => {\n\t\tawait tx.query(lockSql, [opts.key, opts.scope ?? '']);\n\n\t\tconst [cur] = await tx.query<{ version: number }>(\n\t\t\t`SELECT version FROM ${r.table} WHERE ${keyMatch(r)} FOR UPDATE`,\n\t\t\t[opts.key, opts.scope],\n\t\t);\n\t\tconst currentVersion = cur?.version ?? null;\n\t\tif (opts.ifVersion !== undefined && currentVersion !== opts.ifVersion) {\n\t\t\tthrow new VersionConflictError(opts.key, opts.scope, currentVersion, opts.ifVersion);\n\t\t}\n\t\tconst version = (currentVersion ?? 0) + 1;\n\t\tconst writeVals = [opts.key, opts.scope, ...contentVals, ...metaVals, version, opts.actor];\n\n\t\tlet row: T | undefined;\n\t\tif (cur) {\n\t\t\tconst sets = [\n\t\t\t\t...content.map((c, i) => `${c} = $${3 + i}`),\n\t\t\t\t...meta.map((c, j) => `${c} = $${3 + content.length + j}`),\n\t\t\t\t`version = $${3 + content.length + meta.length}`,\n\t\t\t\t`updated_by = $${4 + content.length + meta.length}`,\n\t\t\t\t`updated_at = now()`,\n\t\t\t];\n\t\t\t[row] = await tx.query<T>(\n\t\t\t\t`UPDATE ${r.table} SET ${sets.join(', ')} WHERE ${keyMatch(r)} RETURNING ${r.returning}`,\n\t\t\t\twriteVals,\n\t\t\t);\n\t\t} else {\n\t\t\tconst cols = [idCol, scopeCol, ...content, ...meta, 'version', 'updated_by'];\n\t\t\tconst ph = cols.map((_, i) => `$${i + 1}`);\n\t\t\t[row] = await tx.query<T>(\n\t\t\t\t`INSERT INTO ${r.table} (${cols.join(', ')}) VALUES (${ph.join(', ')}) RETURNING ${r.returning}`,\n\t\t\t\twriteVals,\n\t\t\t);\n\t\t}\n\n\t\tconst revCols = [idCol, scopeCol, ...content, 'version', 'actor'];\n\t\tconst revPh = revCols.map((_, i) => `$${i + 1}`);\n\t\tawait tx.query(\n\t\t\t`INSERT INTO ${r.revisions} (${revCols.join(', ')}) VALUES (${revPh.join(', ')})`,\n\t\t\t[opts.key, opts.scope, ...contentVals, version, opts.actor],\n\t\t);\n\t\tawait tx.query(`SELECT pg_notify('${r.channel}', $1)`, [opts.scope ?? '']);\n\n\t\tif (!row) throw new Error(`Failed to write ${r.table} entry`);\n\t\treturn row;\n\t});\n}\n\n// Roll *forward* to a past revision's content as a new version (rollout undo —\n// history is never rewritten).\nexport async function versionedRollback<T>(\n\tr: IVersionedResource,\n\tstore: IStoreAdapter,\n\topts: { key: string; scope: string | null; toVersion: number; actor: string | null },\n): Promise<T> {\n\tconst [idCol, scopeCol] = r.keyColumns;\n\tconst content = r.contentColumns;\n\n\treturn store.transaction(async (tx) => {\n\t\tawait tx.query(lockSql, [opts.key, opts.scope ?? '']);\n\n\t\tconst [target] = await tx.query<Record<string, unknown>>(\n\t\t\t`SELECT ${content.join(', ')} FROM ${r.revisions} WHERE ${keyMatch(r)} AND version = $3`,\n\t\t\t[opts.key, opts.scope, opts.toVersion],\n\t\t);\n\t\tif (!target) {\n\t\t\tthrow new Error(`\"${opts.key}\" (${opts.scope ?? 'base'}) has no revision ${opts.toVersion}`);\n\t\t}\n\t\tconst [cur] = await tx.query<{ version: number }>(\n\t\t\t`SELECT version FROM ${r.table} WHERE ${keyMatch(r)} FOR UPDATE`,\n\t\t\t[opts.key, opts.scope],\n\t\t);\n\t\tif (!cur) throw new Error(`\"${opts.key}\" (${opts.scope ?? 'base'}) does not exist`);\n\t\tconst version = cur.version + 1;\n\t\tconst contentVals = content.map((c) => target[c] ?? null);\n\t\tconst writeVals = [opts.key, opts.scope, ...contentVals, version, opts.actor];\n\n\t\tconst sets = [\n\t\t\t...content.map((c, i) => `${c} = $${3 + i}`),\n\t\t\t`version = $${3 + content.length}`,\n\t\t\t`updated_by = $${4 + content.length}`,\n\t\t\t`updated_at = now()`,\n\t\t];\n\t\tconst [row] = await tx.query<T>(\n\t\t\t`UPDATE ${r.table} SET ${sets.join(', ')} WHERE ${keyMatch(r)} RETURNING ${r.returning}`,\n\t\t\twriteVals,\n\t\t);\n\n\t\tconst revCols = [idCol, scopeCol, ...content, 'version', 'actor'];\n\t\tconst revPh = revCols.map((_, i) => `$${i + 1}`);\n\t\tawait tx.query(\n\t\t\t`INSERT INTO ${r.revisions} (${revCols.join(', ')}) VALUES (${revPh.join(', ')})`,\n\t\t\twriteVals.slice(0, 2 + content.length).concat(version, opts.actor),\n\t\t);\n\t\tawait tx.query(`SELECT pg_notify('${r.channel}', $1)`, [opts.scope ?? '']);\n\n\t\tif (!row) throw new Error('rollback failed');\n\t\treturn row;\n\t});\n}\n"],"mappings":"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","names":["sql","import_node_path","pg","sql","result"]}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sql.ts","../src/migrations/runner.ts","../src/migrations/path.ts","../src/adapters/pg.ts","../src/versioned.ts"],"sourcesContent":["export interface ISqlQuery {\n\ttext: string;\n\tparams: unknown[];\n}\n\n// Tagged template literal for safe parameterized queries.\n// Never concatenates user input — always uses $N placeholders.\n//\n// Usage:\n// const { text, params } = sql`SELECT * FROM users WHERE id = ${userId}`\n// const rows = await store.query<User>(text, params)\n\nexport function sql(strings: TemplateStringsArray, ...values: unknown[]): ISqlQuery {\n\tlet text = '';\n\tconst params: unknown[] = [];\n\n\tstrings.forEach((str, i) => {\n\t\ttext += str;\n\t\tif (i < values.length) {\n\t\t\tparams.push(values[i]);\n\t\t\ttext += `$${params.length}`;\n\t\t}\n\t});\n\n\treturn { text, params };\n}\n","import { join } from 'node:path';\nimport { readdir, readFile } from 'node:fs/promises';\n\nimport type { IStoreAdapter } from '../types';\n\nconst MIGRATIONS_TABLE = 'fonderie_migrations';\nconst RESERVED_PREFIX_RE = /\\bfonderie_/i;\n\nexport class MigrationRunner {\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate migrationsDir: string,\n\t) {}\n\n\tasync run(): Promise<void> {\n\t\tawait this.ensureTable();\n\n\t\tconst [applied, files] = await Promise.all([this.getApplied(), this.getFiles()]);\n\n\t\tconst pending = files.filter((f) => !applied.has(f));\n\n\t\tif (pending.length === 0) {\n\t\t\tconsole.log('[store] migrations: up to date');\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const file of pending) {\n\t\t\tconst sql = await readFile(join(this.migrationsDir, file), 'utf8');\n\n\t\t\tthis.assertNoReservedPrefix(file, sql);\n\n\t\t\tawait this.store.transaction(async (tx) => {\n\t\t\t\tawait tx.query(sql);\n\t\t\t\tawait tx.query(`INSERT INTO ${MIGRATIONS_TABLE} (name, applied_at) VALUES ($1, now())`, [\n\t\t\t\t\tfile,\n\t\t\t\t]);\n\t\t\t});\n\n\t\t\tconsole.log(`[store] migrations: applied ${file}`);\n\t\t}\n\t}\n\n\tprotected assertNoReservedPrefix(file: string, sql: string): void {\n\t\tif (RESERVED_PREFIX_RE.test(sql)) {\n\t\t\tthrow new Error(\n\t\t\t\t`[store] migration \"${file}\" uses the reserved \"fonderie_\" prefix. ` +\n\t\t\t\t`Use InternalMigrationRunner for fonderie-owned migrations.`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async ensureTable(): Promise<void> {\n\t\tawait this.store.query(`\n\t\t\tCREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (\n\t\t\t name TEXT PRIMARY KEY,\n\t\t\t applied_at TIMESTAMPTZ NOT NULL DEFAULT now()\n\t\t\t)\n\t\t`);\n\t}\n\n\tprivate async getApplied(): Promise<Set<string>> {\n\t\tconst rows = await this.store.query<{ name: string }>(\n\t\t\t`SELECT name FROM ${MIGRATIONS_TABLE} ORDER BY name`,\n\t\t);\n\t\treturn new Set(rows.map((r) => r.name));\n\t}\n\n\tprivate async getFiles(): Promise<string[]> {\n\t\tconst all = await readdir(this.migrationsDir);\n\t\treturn all.filter((f) => f.endsWith('.sql')).sort(); // lexicographic — timestamp prefix keeps order correct\n\t}\n}\n\n// For fonderie-internal use only. Skips the reserved-prefix guard.\nexport class InternalMigrationRunner extends MigrationRunner {\n\tprotected override assertNoReservedPrefix(_file: string, _sql: string): void {}\n}\n","import { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\n\n// Each package calls this with its own import.meta.url to resolve\n// the absolute path to its compiled migrations/sql/ directory.\n// Called from dist/migrations/index.js, so dirname is already dist/migrations/.\n//\n// Usage in any package's migrations/index.ts:\n// export const getMigrationsPath = () => createMigrationsPath(import.meta.url)\nexport function createMigrationsPath(importMetaUrl: string): string {\n\treturn join(dirname(fileURLToPath(importMetaUrl)), 'sql');\n}\n","import pg from 'pg';\nimport type { IStoreAdapter, IPoolConfig } from '../types';\n\nexport class PGAdapter implements IStoreAdapter {\n\tprivate pool: pg.Pool;\n\n\tconstructor(config: IPoolConfig | string) {\n\t\tconst options = typeof config === 'string' ? { connectionString: config } : config;\n\t\tthis.pool = new pg.Pool(options);\n\n\t\tthis.pool.on('error', (err) => {\n\t\t\tconsole.error('[store] idle client error', err.message);\n\t\t});\n\t}\n\n\tasync testConnection(): Promise<boolean> {\n\t\ttry {\n\t\t\tawait this.pool.query('SELECT 1');\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tasync query<T = unknown>(sql: string, params?: unknown[]): Promise<T[]> {\n\t\tconst result = await this.pool.query(sql, params);\n\t\treturn result.rows as T[];\n\t}\n\n\tasync transaction<T>(fn: (tx: IStoreAdapter) => Promise<T>): Promise<T> {\n\t\tconst client = await this.pool.connect();\n\n\t\ttry {\n\t\t\tawait client.query('BEGIN');\n\n\t\t\tconst tx: IStoreAdapter = {\n\t\t\t\tquery: async <U = unknown>(sql: string, params?: unknown[]) => {\n\t\t\t\t\tconst result = await client.query(sql, params);\n\t\t\t\t\treturn result.rows as U[];\n\t\t\t\t},\n\t\t\t\ttransaction: <V>(nested: (tx: IStoreAdapter) => Promise<V>) => nested(tx),\n\t\t\t};\n\n\t\t\tconst result = await fn(tx);\n\t\t\tawait client.query('COMMIT');\n\t\t\treturn result;\n\t\t} catch (err) {\n\t\t\tawait client.query('ROLLBACK');\n\t\t\tthrow err;\n\t\t} finally {\n\t\t\tclient.release();\n\t\t}\n\t}\n\n\tasync end(): Promise<void> {\n\t\tawait this.pool.end();\n\t}\n}\n","import type { IStoreAdapter } from './types';\n\n// The control-plane primitive: a version index + optimistic concurrency +\n// advisory-locked writes + append-only revisions + push-notify, reusable by any\n// resource identified by a (primary, scope) key pair. `@fonderie/config` (config\n// + secrets) and `@fonderie/courier` (email templates) run on it; a new resource\n// is a descriptor away. Lives in `store` because it's pure Postgres machinery\n// and every package already depends on store (no cycles).\n\n// Thrown when an optimistic-concurrency write loses the compare-and-swap: the\n// row's current version isn't the one the caller wrote against. Reject-and-retry.\nexport class VersionConflictError extends Error {\n\tconstructor(\n\t\tpublic readonly key: string,\n\t\tpublic readonly scope: string | null,\n\t\tpublic readonly currentVersion: number | null,\n\t\tpublic readonly expectedVersion: number,\n\t) {\n\t\tsuper(\n\t\t\t`\"${key}\" (${scope ?? 'base'}) is at version ${currentVersion ?? 'none'}, ` +\n\t\t\t\t`not ${expectedVersion} — reload and retry`,\n\t\t);\n\t\tthis.name = 'VersionConflictError';\n\t}\n}\n\n// A versioned resource's Postgres surface.\nexport interface IVersionedResource {\n\ttable: string; // main table, e.g. 'fonderie_config'\n\trevisions: string; // history table, e.g. 'fonderie_config_revisions'\n\tchannel: string; // LISTEN/NOTIFY channel, e.g. 'fonderie_config_changed'\n\t// The (primary, scope) key pair — config: ['key','environment'], courier:\n\t// ['type','locale']. `scope` is null-safe (a NULL locale is the base).\n\tkeyColumns: readonly [string, string];\n\t// Content columns — written AND snapshotted into revisions (config: ['value'];\n\t// courier: ['subject','html','text']).\n\tcontentColumns: readonly string[];\n\t// Extra main-table columns, set only when supplied, never revisioned (config:\n\t// ['description','active']; courier: ['active']).\n\tmetaColumns?: readonly string[];\n\t// SELECT/RETURNING column list shaping the caller's row type.\n\treturning: string;\n}\n\nconst lockSql = `SELECT pg_advisory_xact_lock(hashtext($1), hashtext($2))`;\n\nfunction keyMatch(r: IVersionedResource): string {\n\tconst [id, scope] = r.keyColumns;\n\treturn `${id} IS NOT DISTINCT FROM $1 AND ${scope} IS NOT DISTINCT FROM $2`;\n}\n\n// Write one versioned entry: advisory-lock the (key, scope) pair (serializes even\n// a create), enforce optimistic concurrency when `ifVersion` is given, bump the\n// version, append a revision (content columns only), and broadcast invalidation\n// on commit. `data` supplies every content column and any meta columns to set.\nexport async function versionedWrite<T>(\n\tr: IVersionedResource,\n\tstore: IStoreAdapter,\n\topts: {\n\t\tkey: string;\n\t\tscope: string | null;\n\t\tdata: Record<string, unknown>;\n\t\tifVersion?: number;\n\t\tactor: string | null;\n\t},\n): Promise<T> {\n\tconst [idCol, scopeCol] = r.keyColumns;\n\tconst content = r.contentColumns;\n\tconst meta = (r.metaColumns ?? []).filter((c) => c in opts.data);\n\tconst contentVals = content.map((c) => opts.data[c] ?? null);\n\tconst metaVals = meta.map((c) => opts.data[c] ?? null);\n\n\treturn store.transaction(async (tx) => {\n\t\tawait tx.query(lockSql, [opts.key, opts.scope ?? '']);\n\n\t\tconst [cur] = await tx.query<{ version: number }>(\n\t\t\t`SELECT version FROM ${r.table} WHERE ${keyMatch(r)} FOR UPDATE`,\n\t\t\t[opts.key, opts.scope],\n\t\t);\n\t\tconst currentVersion = cur?.version ?? null;\n\t\tif (opts.ifVersion !== undefined && currentVersion !== opts.ifVersion) {\n\t\t\tthrow new VersionConflictError(opts.key, opts.scope, currentVersion, opts.ifVersion);\n\t\t}\n\t\tconst version = (currentVersion ?? 0) + 1;\n\t\tconst writeVals = [opts.key, opts.scope, ...contentVals, ...metaVals, version, opts.actor];\n\n\t\tlet row: T | undefined;\n\t\tif (cur) {\n\t\t\tconst sets = [\n\t\t\t\t...content.map((c, i) => `${c} = $${3 + i}`),\n\t\t\t\t...meta.map((c, j) => `${c} = $${3 + content.length + j}`),\n\t\t\t\t`version = $${3 + content.length + meta.length}`,\n\t\t\t\t`updated_by = $${4 + content.length + meta.length}`,\n\t\t\t\t`updated_at = now()`,\n\t\t\t];\n\t\t\t[row] = await tx.query<T>(\n\t\t\t\t`UPDATE ${r.table} SET ${sets.join(', ')} WHERE ${keyMatch(r)} RETURNING ${r.returning}`,\n\t\t\t\twriteVals,\n\t\t\t);\n\t\t} else {\n\t\t\tconst cols = [idCol, scopeCol, ...content, ...meta, 'version', 'updated_by'];\n\t\t\tconst ph = cols.map((_, i) => `$${i + 1}`);\n\t\t\t[row] = await tx.query<T>(\n\t\t\t\t`INSERT INTO ${r.table} (${cols.join(', ')}) VALUES (${ph.join(', ')}) RETURNING ${r.returning}`,\n\t\t\t\twriteVals,\n\t\t\t);\n\t\t}\n\n\t\tconst revCols = [idCol, scopeCol, ...content, 'version', 'actor'];\n\t\tconst revPh = revCols.map((_, i) => `$${i + 1}`);\n\t\tawait tx.query(\n\t\t\t`INSERT INTO ${r.revisions} (${revCols.join(', ')}) VALUES (${revPh.join(', ')})`,\n\t\t\t[opts.key, opts.scope, ...contentVals, version, opts.actor],\n\t\t);\n\t\tawait tx.query(`SELECT pg_notify('${r.channel}', $1)`, [opts.scope ?? '']);\n\n\t\tif (!row) throw new Error(`Failed to write ${r.table} entry`);\n\t\treturn row;\n\t});\n}\n\n// Roll *forward* to a past revision's content as a new version (rollout undo —\n// history is never rewritten).\nexport async function versionedRollback<T>(\n\tr: IVersionedResource,\n\tstore: IStoreAdapter,\n\topts: { key: string; scope: string | null; toVersion: number; actor: string | null },\n): Promise<T> {\n\tconst [idCol, scopeCol] = r.keyColumns;\n\tconst content = r.contentColumns;\n\n\treturn store.transaction(async (tx) => {\n\t\tawait tx.query(lockSql, [opts.key, opts.scope ?? '']);\n\n\t\tconst [target] = await tx.query<Record<string, unknown>>(\n\t\t\t`SELECT ${content.join(', ')} FROM ${r.revisions} WHERE ${keyMatch(r)} AND version = $3`,\n\t\t\t[opts.key, opts.scope, opts.toVersion],\n\t\t);\n\t\tif (!target) {\n\t\t\tthrow new Error(`\"${opts.key}\" (${opts.scope ?? 'base'}) has no revision ${opts.toVersion}`);\n\t\t}\n\t\tconst [cur] = await tx.query<{ version: number }>(\n\t\t\t`SELECT version FROM ${r.table} WHERE ${keyMatch(r)} FOR UPDATE`,\n\t\t\t[opts.key, opts.scope],\n\t\t);\n\t\tif (!cur) throw new Error(`\"${opts.key}\" (${opts.scope ?? 'base'}) does not exist`);\n\t\tconst version = cur.version + 1;\n\t\tconst contentVals = content.map((c) => target[c] ?? null);\n\t\tconst writeVals = [opts.key, opts.scope, ...contentVals, version, opts.actor];\n\n\t\tconst sets = [\n\t\t\t...content.map((c, i) => `${c} = $${3 + i}`),\n\t\t\t`version = $${3 + content.length}`,\n\t\t\t`updated_by = $${4 + content.length}`,\n\t\t\t`updated_at = now()`,\n\t\t];\n\t\tconst [row] = await tx.query<T>(\n\t\t\t`UPDATE ${r.table} SET ${sets.join(', ')} WHERE ${keyMatch(r)} RETURNING ${r.returning}`,\n\t\t\twriteVals,\n\t\t);\n\n\t\tconst revCols = [idCol, scopeCol, ...content, 'version', 'actor'];\n\t\tconst revPh = revCols.map((_, i) => `$${i + 1}`);\n\t\tawait tx.query(\n\t\t\t`INSERT INTO ${r.revisions} (${revCols.join(', ')}) VALUES (${revPh.join(', ')})`,\n\t\t\twriteVals.slice(0, 2 + content.length).concat(version, opts.actor),\n\t\t);\n\t\tawait tx.query(`SELECT pg_notify('${r.channel}', $1)`, [opts.scope ?? '']);\n\n\t\tif (!row) throw new Error('rollback failed');\n\t\treturn row;\n\t});\n}\n"],"mappings":"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","names":["sql","join","sql","result"]}
|
|
1
|
+
{"version":3,"sources":["../src/sql.ts","../src/migrations/runner.ts","../src/migrations/path.ts","../src/adapters/pg.ts","../src/versioned.ts"],"sourcesContent":["export interface ISqlQuery {\n\ttext: string;\n\tparams: unknown[];\n}\n\n// Tagged template literal for safe parameterized queries.\n// Never concatenates user input — always uses $N placeholders.\n//\n// Usage:\n// const { text, params } = sql`SELECT * FROM users WHERE id = ${userId}`\n// const rows = await store.query<User>(text, params)\n\nexport function sql(strings: TemplateStringsArray, ...values: unknown[]): ISqlQuery {\n\tlet text = '';\n\tconst params: unknown[] = [];\n\n\tstrings.forEach((str, i) => {\n\t\ttext += str;\n\t\tif (i < values.length) {\n\t\t\tparams.push(values[i]);\n\t\t\ttext += `$${params.length}`;\n\t\t}\n\t});\n\n\treturn { text, params };\n}\n","import { join } from 'node:path';\nimport { readdir, readFile } from 'node:fs/promises';\n\nimport type { IStoreAdapter } from '../types';\n\nconst MIGRATIONS_TABLE = 'fonderie_migrations';\nconst RESERVED_PREFIX_RE = /\\bfonderie_/i;\n\nexport class MigrationRunner {\n\tconstructor(\n\t\tprivate store: IStoreAdapter,\n\t\tprivate migrationsDir: string,\n\t) {}\n\n\tasync run(): Promise<void> {\n\t\tawait this.ensureTable();\n\n\t\tconst [applied, files] = await Promise.all([this.getApplied(), this.getFiles()]);\n\n\t\tconst pending = files.filter((f) => !applied.has(f));\n\n\t\tif (pending.length === 0) {\n\t\t\tconsole.log('[store] migrations: up to date');\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const file of pending) {\n\t\t\tconst sql = await readFile(join(this.migrationsDir, file), 'utf8');\n\n\t\t\tthis.assertNoReservedPrefix(file, sql);\n\n\t\t\tawait this.store.transaction(async (tx) => {\n\t\t\t\tawait tx.query(sql);\n\t\t\t\tawait tx.query(`INSERT INTO ${MIGRATIONS_TABLE} (name, applied_at) VALUES ($1, now())`, [\n\t\t\t\t\tfile,\n\t\t\t\t]);\n\t\t\t});\n\n\t\t\tconsole.log(`[store] migrations: applied ${file}`);\n\t\t}\n\t}\n\n\tprotected assertNoReservedPrefix(file: string, sql: string): void {\n\t\tif (RESERVED_PREFIX_RE.test(sql)) {\n\t\t\tthrow new Error(\n\t\t\t\t`[store] migration \"${file}\" uses the reserved \"fonderie_\" prefix. ` +\n\t\t\t\t`Use InternalMigrationRunner for fonderie-owned migrations.`,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async ensureTable(): Promise<void> {\n\t\tawait this.store.query(`\n\t\t\tCREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (\n\t\t\t name TEXT PRIMARY KEY,\n\t\t\t applied_at TIMESTAMPTZ NOT NULL DEFAULT now()\n\t\t\t)\n\t\t`);\n\t}\n\n\tprivate async getApplied(): Promise<Set<string>> {\n\t\tconst rows = await this.store.query<{ name: string }>(\n\t\t\t`SELECT name FROM ${MIGRATIONS_TABLE} ORDER BY name`,\n\t\t);\n\t\treturn new Set(rows.map((r) => r.name));\n\t}\n\n\tprivate async getFiles(): Promise<string[]> {\n\t\tconst all = await readdir(this.migrationsDir);\n\t\treturn all.filter((f) => f.endsWith('.sql')).sort(); // lexicographic — timestamp prefix keeps order correct\n\t}\n}\n\n// For fonderie-internal use only. Skips the reserved-prefix guard.\nexport class InternalMigrationRunner extends MigrationRunner {\n\tprotected override assertNoReservedPrefix(_file: string, _sql: string): void {}\n}\n","import { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\n\n// Each package calls this with its own import.meta.url to resolve\n// the absolute path to its compiled migrations/sql/ directory.\n// Called from dist/migrations/index.js, so dirname is already dist/migrations/.\n//\n// Usage in any package's migrations/index.ts:\n// export const getMigrationsPath = () => createMigrationsPath(import.meta.url)\nexport function createMigrationsPath(importMetaUrl: string): string {\n\treturn join(dirname(fileURLToPath(importMetaUrl)), 'sql');\n}\n","import pg from 'pg';\nimport type { IStoreAdapter, IPoolConfig } from '../types';\n\nexport class PGAdapter implements IStoreAdapter {\n\tprivate pool: pg.Pool;\n\n\tconstructor(config: IPoolConfig | string) {\n\t\tconst options = typeof config === 'string' ? { connectionString: config } : config;\n\t\tthis.pool = new pg.Pool(options);\n\n\t\tthis.pool.on('error', (err) => {\n\t\t\tconsole.error('[store] idle client error', err.message);\n\t\t});\n\t}\n\n\tasync testConnection(): Promise<boolean> {\n\t\ttry {\n\t\t\tawait this.pool.query('SELECT 1');\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tasync query<T = unknown>(sql: string, params?: unknown[]): Promise<T[]> {\n\t\tconst result = await this.pool.query(sql, params);\n\t\treturn result.rows as T[];\n\t}\n\n\tasync transaction<T>(fn: (tx: IStoreAdapter) => Promise<T>): Promise<T> {\n\t\tconst client = await this.pool.connect();\n\n\t\ttry {\n\t\t\tawait client.query('BEGIN');\n\n\t\t\tconst tx: IStoreAdapter = {\n\t\t\t\tquery: async <U = unknown>(sql: string, params?: unknown[]) => {\n\t\t\t\t\tconst result = await client.query(sql, params);\n\t\t\t\t\treturn result.rows as U[];\n\t\t\t\t},\n\t\t\t\ttransaction: <V>(nested: (tx: IStoreAdapter) => Promise<V>) => nested(tx),\n\t\t\t};\n\n\t\t\tconst result = await fn(tx);\n\t\t\tawait client.query('COMMIT');\n\t\t\treturn result;\n\t\t} catch (err) {\n\t\t\tawait client.query('ROLLBACK');\n\t\t\tthrow err;\n\t\t} finally {\n\t\t\tclient.release();\n\t\t}\n\t}\n\n\tasync end(): Promise<void> {\n\t\tawait this.pool.end();\n\t}\n}\n","import type { IStoreAdapter } from './types';\n\n// The control-plane primitive: a version index + optimistic concurrency +\n// advisory-locked writes + append-only revisions + push-notify, reusable by any\n// resource identified by a (primary, scope) key pair. `@fonderie/config` (config\n// + secrets) and `@fonderie/courier` (email templates) run on it; a new resource\n// is a descriptor away. Lives in `store` because it's pure Postgres machinery\n// and every package already depends on store (no cycles).\n\n// Thrown when an optimistic-concurrency write loses the compare-and-swap: the\n// row's current version isn't the one the caller wrote against. Reject-and-retry.\nexport class VersionConflictError extends Error {\n\tconstructor(\n\t\tpublic readonly key: string,\n\t\tpublic readonly scope: string | null,\n\t\tpublic readonly currentVersion: number | null,\n\t\tpublic readonly expectedVersion: number,\n\t) {\n\t\tsuper(\n\t\t\t`\"${key}\" (${scope ?? 'base'}) is at version ${currentVersion ?? 'none'}, ` +\n\t\t\t\t`not ${expectedVersion} — reload and retry`,\n\t\t);\n\t\tthis.name = 'VersionConflictError';\n\t}\n}\n\n// A versioned resource's Postgres surface.\nexport interface IVersionedResource {\n\ttable: string; // main table, e.g. 'fonderie_config'\n\trevisions: string; // history table, e.g. 'fonderie_config_revisions'\n\tchannel: string; // LISTEN/NOTIFY channel, e.g. 'fonderie_config_changed'\n\t// The (primary, scope) key pair — config: ['key','environment'], courier:\n\t// ['type','locale']. `scope` is null-safe (a NULL locale is the base).\n\t// Intentionally a fixed 2-tuple: every resource is addressed by exactly one\n\t// primary key plus one optional scope. Composite 3+-part keys are out of\n\t// scope by design — model the extra dimension inside the primary key (e.g.\n\t// a compound string) or the scope rather than widening this contract.\n\tkeyColumns: readonly [string, string];\n\t// Content columns — written AND snapshotted into revisions (config: ['value'];\n\t// courier: ['subject','html','text']).\n\tcontentColumns: readonly string[];\n\t// Extra main-table columns, set only when supplied, never revisioned (config:\n\t// ['description','active']; courier: ['active']).\n\tmetaColumns?: readonly string[];\n\t// SELECT/RETURNING column list shaping the caller's row type.\n\treturning: string;\n}\n\nconst lockSql = `SELECT pg_advisory_xact_lock(hashtext($1), hashtext($2))`;\n\nfunction keyMatch(r: IVersionedResource): string {\n\tconst [id, scope] = r.keyColumns;\n\treturn `${id} IS NOT DISTINCT FROM $1 AND ${scope} IS NOT DISTINCT FROM $2`;\n}\n\n// Write one versioned entry: advisory-lock the (key, scope) pair (serializes even\n// a create), enforce optimistic concurrency when `ifVersion` is given, bump the\n// version, append a revision (content columns only), and broadcast invalidation\n// on commit. `data` supplies every content column and any meta columns to set.\nexport async function versionedWrite<T>(\n\tr: IVersionedResource,\n\tstore: IStoreAdapter,\n\topts: {\n\t\tkey: string;\n\t\tscope: string | null;\n\t\tdata: Record<string, unknown>;\n\t\tifVersion?: number;\n\t\tactor: string | null;\n\t},\n): Promise<T> {\n\tconst [idCol, scopeCol] = r.keyColumns;\n\tconst content = r.contentColumns;\n\tconst meta = (r.metaColumns ?? []).filter((c) => c in opts.data);\n\tconst contentVals = content.map((c) => opts.data[c] ?? null);\n\tconst metaVals = meta.map((c) => opts.data[c] ?? null);\n\n\treturn store.transaction(async (tx) => {\n\t\tawait tx.query(lockSql, [opts.key, opts.scope ?? '']);\n\n\t\tconst [cur] = await tx.query<{ version: number }>(\n\t\t\t`SELECT version FROM ${r.table} WHERE ${keyMatch(r)} FOR UPDATE`,\n\t\t\t[opts.key, opts.scope],\n\t\t);\n\t\tconst currentVersion = cur?.version ?? null;\n\t\tif (opts.ifVersion !== undefined && currentVersion !== opts.ifVersion) {\n\t\t\tthrow new VersionConflictError(opts.key, opts.scope, currentVersion, opts.ifVersion);\n\t\t}\n\t\tconst version = (currentVersion ?? 0) + 1;\n\t\tconst writeVals = [opts.key, opts.scope, ...contentVals, ...metaVals, version, opts.actor];\n\n\t\tlet row: T | undefined;\n\t\tif (cur) {\n\t\t\tconst sets = [\n\t\t\t\t...content.map((c, i) => `${c} = $${3 + i}`),\n\t\t\t\t...meta.map((c, j) => `${c} = $${3 + content.length + j}`),\n\t\t\t\t`version = $${3 + content.length + meta.length}`,\n\t\t\t\t`updated_by = $${4 + content.length + meta.length}`,\n\t\t\t\t`updated_at = now()`,\n\t\t\t];\n\t\t\t[row] = await tx.query<T>(\n\t\t\t\t`UPDATE ${r.table} SET ${sets.join(', ')} WHERE ${keyMatch(r)} RETURNING ${r.returning}`,\n\t\t\t\twriteVals,\n\t\t\t);\n\t\t} else {\n\t\t\tconst cols = [idCol, scopeCol, ...content, ...meta, 'version', 'updated_by'];\n\t\t\tconst ph = cols.map((_, i) => `$${i + 1}`);\n\t\t\t[row] = await tx.query<T>(\n\t\t\t\t`INSERT INTO ${r.table} (${cols.join(', ')}) VALUES (${ph.join(', ')}) RETURNING ${r.returning}`,\n\t\t\t\twriteVals,\n\t\t\t);\n\t\t}\n\n\t\tconst revCols = [idCol, scopeCol, ...content, 'version', 'actor'];\n\t\tconst revPh = revCols.map((_, i) => `$${i + 1}`);\n\t\tawait tx.query(\n\t\t\t`INSERT INTO ${r.revisions} (${revCols.join(', ')}) VALUES (${revPh.join(', ')})`,\n\t\t\t[opts.key, opts.scope, ...contentVals, version, opts.actor],\n\t\t);\n\t\tawait tx.query(`SELECT pg_notify('${r.channel}', $1)`, [opts.scope ?? '']);\n\n\t\tif (!row) throw new Error(`Failed to write ${r.table} entry`);\n\t\treturn row;\n\t});\n}\n\n// Roll *forward* to a past revision's content as a new version (rollout undo —\n// history is never rewritten).\nexport async function versionedRollback<T>(\n\tr: IVersionedResource,\n\tstore: IStoreAdapter,\n\topts: { key: string; scope: string | null; toVersion: number; actor: string | null },\n): Promise<T> {\n\tconst [idCol, scopeCol] = r.keyColumns;\n\tconst content = r.contentColumns;\n\n\treturn store.transaction(async (tx) => {\n\t\tawait tx.query(lockSql, [opts.key, opts.scope ?? '']);\n\n\t\tconst [target] = await tx.query<Record<string, unknown>>(\n\t\t\t`SELECT ${content.join(', ')} FROM ${r.revisions} WHERE ${keyMatch(r)} AND version = $3`,\n\t\t\t[opts.key, opts.scope, opts.toVersion],\n\t\t);\n\t\tif (!target) {\n\t\t\tthrow new Error(`\"${opts.key}\" (${opts.scope ?? 'base'}) has no revision ${opts.toVersion}`);\n\t\t}\n\t\tconst [cur] = await tx.query<{ version: number }>(\n\t\t\t`SELECT version FROM ${r.table} WHERE ${keyMatch(r)} FOR UPDATE`,\n\t\t\t[opts.key, opts.scope],\n\t\t);\n\t\tif (!cur) throw new Error(`\"${opts.key}\" (${opts.scope ?? 'base'}) does not exist`);\n\t\tconst version = cur.version + 1;\n\t\tconst contentVals = content.map((c) => target[c] ?? null);\n\t\tconst writeVals = [opts.key, opts.scope, ...contentVals, version, opts.actor];\n\n\t\tconst sets = [\n\t\t\t...content.map((c, i) => `${c} = $${3 + i}`),\n\t\t\t`version = $${3 + content.length}`,\n\t\t\t`updated_by = $${4 + content.length}`,\n\t\t\t`updated_at = now()`,\n\t\t];\n\t\tconst [row] = await tx.query<T>(\n\t\t\t`UPDATE ${r.table} SET ${sets.join(', ')} WHERE ${keyMatch(r)} RETURNING ${r.returning}`,\n\t\t\twriteVals,\n\t\t);\n\n\t\tconst revCols = [idCol, scopeCol, ...content, 'version', 'actor'];\n\t\tconst revPh = revCols.map((_, i) => `$${i + 1}`);\n\t\tawait tx.query(\n\t\t\t`INSERT INTO ${r.revisions} (${revCols.join(', ')}) VALUES (${revPh.join(', ')})`,\n\t\t\twriteVals.slice(0, 2 + content.length).concat(version, opts.actor),\n\t\t);\n\t\tawait tx.query(`SELECT pg_notify('${r.channel}', $1)`, [opts.scope ?? '']);\n\n\t\tif (!row) throw new Error('rollback failed');\n\t\treturn row;\n\t});\n}\n"],"mappings":"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","names":["sql","join","sql","result"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonderie/store",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Database abstraction layer — IStoreAdapter interface, PostgreSQL driver, sequential migration runner, and SQL tagged-template helpers. The only package every other module depends on.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fonderie-js",
|
|
@@ -72,11 +72,11 @@
|
|
|
72
72
|
],
|
|
73
73
|
"repository": {
|
|
74
74
|
"type": "git",
|
|
75
|
-
"url": "git+https://github.com/fonderiejs/
|
|
75
|
+
"url": "git+https://github.com/fonderiejs/fonderie.git",
|
|
76
76
|
"directory": "packages/store"
|
|
77
77
|
},
|
|
78
|
-
"homepage": "https://github.com/fonderiejs/
|
|
78
|
+
"homepage": "https://github.com/fonderiejs/fonderie/tree/main/packages/store#readme",
|
|
79
79
|
"bugs": {
|
|
80
|
-
"url": "https://github.com/fonderiejs/
|
|
80
|
+
"url": "https://github.com/fonderiejs/fonderie/issues"
|
|
81
81
|
}
|
|
82
82
|
}
|