@fonderie/store 0.1.2 → 0.2.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/brain/signatures.md +27 -0
- package/dist/index.cjs +122 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +118 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/brain/signatures.md
CHANGED
|
@@ -47,4 +47,31 @@ new PGAdapter(config: string | IPoolConfig): PGAdapter
|
|
|
47
47
|
.query<T = unknown>(sql: string, params?: unknown[] | undefined): Promise<T[]>
|
|
48
48
|
.transaction<T>(fn: (tx: IStoreAdapter) => Promise<T>): Promise<T>
|
|
49
49
|
.end(): Promise<void>
|
|
50
|
+
|
|
51
|
+
function versionedWrite<T>(r: IVersionedResource, store: IStoreAdapter, opts: { key: string; scope: string | null; data: Record<string, unknown>; ifVersion?: number; actor: string | null; }): Promise<...>
|
|
52
|
+
|
|
53
|
+
function versionedRollback<T>(r: IVersionedResource, store: IStoreAdapter, opts: { key: string; scope: string | null; toVersion: number; actor: string | null; }): Promise<T>
|
|
54
|
+
|
|
55
|
+
new VersionConflictError(key: string, scope: string | null, currentVersion: number | null, expectedVersion: number): VersionConflictError
|
|
56
|
+
.key: string
|
|
57
|
+
.scope: string | null
|
|
58
|
+
.currentVersion: number | null
|
|
59
|
+
.expectedVersion: number
|
|
60
|
+
.name: string
|
|
61
|
+
.message: string
|
|
62
|
+
.stack: string
|
|
63
|
+
.cause: unknown
|
|
64
|
+
|
|
65
|
+
interface IVersionedResource {
|
|
66
|
+
table: string;
|
|
67
|
+
revisions: string;
|
|
68
|
+
channel: string;
|
|
69
|
+
keyColumns: readonly [
|
|
70
|
+
string,
|
|
71
|
+
string
|
|
72
|
+
];
|
|
73
|
+
contentColumns: readonly string[];
|
|
74
|
+
metaColumns?: readonly string[];
|
|
75
|
+
returning: string;
|
|
76
|
+
}
|
|
50
77
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -33,8 +33,11 @@ __export(index_exports, {
|
|
|
33
33
|
InternalMigrationRunner: () => InternalMigrationRunner,
|
|
34
34
|
MigrationRunner: () => MigrationRunner,
|
|
35
35
|
PGAdapter: () => PGAdapter,
|
|
36
|
+
VersionConflictError: () => VersionConflictError,
|
|
36
37
|
createMigrationsPath: () => createMigrationsPath,
|
|
37
|
-
sql: () => sql
|
|
38
|
+
sql: () => sql,
|
|
39
|
+
versionedRollback: () => versionedRollback,
|
|
40
|
+
versionedWrite: () => versionedWrite
|
|
38
41
|
});
|
|
39
42
|
module.exports = __toCommonJS(index_exports);
|
|
40
43
|
|
|
@@ -170,12 +173,129 @@ var PGAdapter = class {
|
|
|
170
173
|
await this.pool.end();
|
|
171
174
|
}
|
|
172
175
|
};
|
|
176
|
+
|
|
177
|
+
// src/versioned.ts
|
|
178
|
+
var VersionConflictError = class extends Error {
|
|
179
|
+
constructor(key, scope, currentVersion, expectedVersion) {
|
|
180
|
+
super(
|
|
181
|
+
`"${key}" (${scope ?? "base"}) is at version ${currentVersion ?? "none"}, not ${expectedVersion} \u2014 reload and retry`
|
|
182
|
+
);
|
|
183
|
+
this.key = key;
|
|
184
|
+
this.scope = scope;
|
|
185
|
+
this.currentVersion = currentVersion;
|
|
186
|
+
this.expectedVersion = expectedVersion;
|
|
187
|
+
this.name = "VersionConflictError";
|
|
188
|
+
}
|
|
189
|
+
key;
|
|
190
|
+
scope;
|
|
191
|
+
currentVersion;
|
|
192
|
+
expectedVersion;
|
|
193
|
+
};
|
|
194
|
+
var lockSql = `SELECT pg_advisory_xact_lock(hashtext($1), hashtext($2))`;
|
|
195
|
+
function keyMatch(r) {
|
|
196
|
+
const [id, scope] = r.keyColumns;
|
|
197
|
+
return `${id} IS NOT DISTINCT FROM $1 AND ${scope} IS NOT DISTINCT FROM $2`;
|
|
198
|
+
}
|
|
199
|
+
async function versionedWrite(r, store, opts) {
|
|
200
|
+
const [idCol, scopeCol] = r.keyColumns;
|
|
201
|
+
const content = r.contentColumns;
|
|
202
|
+
const meta = (r.metaColumns ?? []).filter((c) => c in opts.data);
|
|
203
|
+
const contentVals = content.map((c) => opts.data[c] ?? null);
|
|
204
|
+
const metaVals = meta.map((c) => opts.data[c] ?? null);
|
|
205
|
+
return store.transaction(async (tx) => {
|
|
206
|
+
await tx.query(lockSql, [opts.key, opts.scope ?? ""]);
|
|
207
|
+
const [cur] = await tx.query(
|
|
208
|
+
`SELECT version FROM ${r.table} WHERE ${keyMatch(r)} FOR UPDATE`,
|
|
209
|
+
[opts.key, opts.scope]
|
|
210
|
+
);
|
|
211
|
+
const currentVersion = cur?.version ?? null;
|
|
212
|
+
if (opts.ifVersion !== void 0 && currentVersion !== opts.ifVersion) {
|
|
213
|
+
throw new VersionConflictError(opts.key, opts.scope, currentVersion, opts.ifVersion);
|
|
214
|
+
}
|
|
215
|
+
const version = (currentVersion ?? 0) + 1;
|
|
216
|
+
const writeVals = [opts.key, opts.scope, ...contentVals, ...metaVals, version, opts.actor];
|
|
217
|
+
let row;
|
|
218
|
+
if (cur) {
|
|
219
|
+
const sets = [
|
|
220
|
+
...content.map((c, i) => `${c} = $${3 + i}`),
|
|
221
|
+
...meta.map((c, j) => `${c} = $${3 + content.length + j}`),
|
|
222
|
+
`version = $${3 + content.length + meta.length}`,
|
|
223
|
+
`updated_by = $${4 + content.length + meta.length}`,
|
|
224
|
+
`updated_at = now()`
|
|
225
|
+
];
|
|
226
|
+
[row] = await tx.query(
|
|
227
|
+
`UPDATE ${r.table} SET ${sets.join(", ")} WHERE ${keyMatch(r)} RETURNING ${r.returning}`,
|
|
228
|
+
writeVals
|
|
229
|
+
);
|
|
230
|
+
} else {
|
|
231
|
+
const cols = [idCol, scopeCol, ...content, ...meta, "version", "updated_by"];
|
|
232
|
+
const ph = cols.map((_, i) => `$${i + 1}`);
|
|
233
|
+
[row] = await tx.query(
|
|
234
|
+
`INSERT INTO ${r.table} (${cols.join(", ")}) VALUES (${ph.join(", ")}) RETURNING ${r.returning}`,
|
|
235
|
+
writeVals
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const revCols = [idCol, scopeCol, ...content, "version", "actor"];
|
|
239
|
+
const revPh = revCols.map((_, i) => `$${i + 1}`);
|
|
240
|
+
await tx.query(
|
|
241
|
+
`INSERT INTO ${r.revisions} (${revCols.join(", ")}) VALUES (${revPh.join(", ")})`,
|
|
242
|
+
[opts.key, opts.scope, ...contentVals, version, opts.actor]
|
|
243
|
+
);
|
|
244
|
+
await tx.query(`SELECT pg_notify('${r.channel}', $1)`, [opts.scope ?? ""]);
|
|
245
|
+
if (!row) throw new Error(`Failed to write ${r.table} entry`);
|
|
246
|
+
return row;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
async function versionedRollback(r, store, opts) {
|
|
250
|
+
const [idCol, scopeCol] = r.keyColumns;
|
|
251
|
+
const content = r.contentColumns;
|
|
252
|
+
return store.transaction(async (tx) => {
|
|
253
|
+
await tx.query(lockSql, [opts.key, opts.scope ?? ""]);
|
|
254
|
+
const [target] = await tx.query(
|
|
255
|
+
`SELECT ${content.join(", ")} FROM ${r.revisions} WHERE ${keyMatch(r)} AND version = $3`,
|
|
256
|
+
[opts.key, opts.scope, opts.toVersion]
|
|
257
|
+
);
|
|
258
|
+
if (!target) {
|
|
259
|
+
throw new Error(`"${opts.key}" (${opts.scope ?? "base"}) has no revision ${opts.toVersion}`);
|
|
260
|
+
}
|
|
261
|
+
const [cur] = await tx.query(
|
|
262
|
+
`SELECT version FROM ${r.table} WHERE ${keyMatch(r)} FOR UPDATE`,
|
|
263
|
+
[opts.key, opts.scope]
|
|
264
|
+
);
|
|
265
|
+
if (!cur) throw new Error(`"${opts.key}" (${opts.scope ?? "base"}) does not exist`);
|
|
266
|
+
const version = cur.version + 1;
|
|
267
|
+
const contentVals = content.map((c) => target[c] ?? null);
|
|
268
|
+
const writeVals = [opts.key, opts.scope, ...contentVals, version, opts.actor];
|
|
269
|
+
const sets = [
|
|
270
|
+
...content.map((c, i) => `${c} = $${3 + i}`),
|
|
271
|
+
`version = $${3 + content.length}`,
|
|
272
|
+
`updated_by = $${4 + content.length}`,
|
|
273
|
+
`updated_at = now()`
|
|
274
|
+
];
|
|
275
|
+
const [row] = await tx.query(
|
|
276
|
+
`UPDATE ${r.table} SET ${sets.join(", ")} WHERE ${keyMatch(r)} RETURNING ${r.returning}`,
|
|
277
|
+
writeVals
|
|
278
|
+
);
|
|
279
|
+
const revCols = [idCol, scopeCol, ...content, "version", "actor"];
|
|
280
|
+
const revPh = revCols.map((_, i) => `$${i + 1}`);
|
|
281
|
+
await tx.query(
|
|
282
|
+
`INSERT INTO ${r.revisions} (${revCols.join(", ")}) VALUES (${revPh.join(", ")})`,
|
|
283
|
+
writeVals.slice(0, 2 + content.length).concat(version, opts.actor)
|
|
284
|
+
);
|
|
285
|
+
await tx.query(`SELECT pg_notify('${r.channel}', $1)`, [opts.scope ?? ""]);
|
|
286
|
+
if (!row) throw new Error("rollback failed");
|
|
287
|
+
return row;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
173
290
|
// Annotate the CommonJS export names for ESM import in node:
|
|
174
291
|
0 && (module.exports = {
|
|
175
292
|
InternalMigrationRunner,
|
|
176
293
|
MigrationRunner,
|
|
177
294
|
PGAdapter,
|
|
295
|
+
VersionConflictError,
|
|
178
296
|
createMigrationsPath,
|
|
179
|
-
sql
|
|
297
|
+
sql,
|
|
298
|
+
versionedRollback,
|
|
299
|
+
versionedWrite
|
|
180
300
|
});
|
|
181
301
|
//# sourceMappingURL=index.cjs.map
|
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"],"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","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"],"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\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.d.cts
CHANGED
|
@@ -26,4 +26,34 @@ declare class PGAdapter implements IStoreAdapter {
|
|
|
26
26
|
end(): Promise<void>;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
declare class VersionConflictError extends Error {
|
|
30
|
+
readonly key: string;
|
|
31
|
+
readonly scope: string | null;
|
|
32
|
+
readonly currentVersion: number | null;
|
|
33
|
+
readonly expectedVersion: number;
|
|
34
|
+
constructor(key: string, scope: string | null, currentVersion: number | null, expectedVersion: number);
|
|
35
|
+
}
|
|
36
|
+
interface IVersionedResource {
|
|
37
|
+
table: string;
|
|
38
|
+
revisions: string;
|
|
39
|
+
channel: string;
|
|
40
|
+
keyColumns: readonly [string, string];
|
|
41
|
+
contentColumns: readonly string[];
|
|
42
|
+
metaColumns?: readonly string[];
|
|
43
|
+
returning: string;
|
|
44
|
+
}
|
|
45
|
+
declare function versionedWrite<T>(r: IVersionedResource, store: IStoreAdapter, opts: {
|
|
46
|
+
key: string;
|
|
47
|
+
scope: string | null;
|
|
48
|
+
data: Record<string, unknown>;
|
|
49
|
+
ifVersion?: number;
|
|
50
|
+
actor: string | null;
|
|
51
|
+
}): Promise<T>;
|
|
52
|
+
declare function versionedRollback<T>(r: IVersionedResource, store: IStoreAdapter, opts: {
|
|
53
|
+
key: string;
|
|
54
|
+
scope: string | null;
|
|
55
|
+
toVersion: number;
|
|
56
|
+
actor: string | null;
|
|
57
|
+
}): Promise<T>;
|
|
58
|
+
|
|
59
|
+
export { IPoolConfig, IStoreAdapter, type IVersionedResource, InternalMigrationRunner, MigrationRunner, PGAdapter, VersionConflictError, createMigrationsPath, versionedRollback, versionedWrite };
|
package/dist/index.d.ts
CHANGED
|
@@ -26,4 +26,34 @@ declare class PGAdapter implements IStoreAdapter {
|
|
|
26
26
|
end(): Promise<void>;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
declare class VersionConflictError extends Error {
|
|
30
|
+
readonly key: string;
|
|
31
|
+
readonly scope: string | null;
|
|
32
|
+
readonly currentVersion: number | null;
|
|
33
|
+
readonly expectedVersion: number;
|
|
34
|
+
constructor(key: string, scope: string | null, currentVersion: number | null, expectedVersion: number);
|
|
35
|
+
}
|
|
36
|
+
interface IVersionedResource {
|
|
37
|
+
table: string;
|
|
38
|
+
revisions: string;
|
|
39
|
+
channel: string;
|
|
40
|
+
keyColumns: readonly [string, string];
|
|
41
|
+
contentColumns: readonly string[];
|
|
42
|
+
metaColumns?: readonly string[];
|
|
43
|
+
returning: string;
|
|
44
|
+
}
|
|
45
|
+
declare function versionedWrite<T>(r: IVersionedResource, store: IStoreAdapter, opts: {
|
|
46
|
+
key: string;
|
|
47
|
+
scope: string | null;
|
|
48
|
+
data: Record<string, unknown>;
|
|
49
|
+
ifVersion?: number;
|
|
50
|
+
actor: string | null;
|
|
51
|
+
}): Promise<T>;
|
|
52
|
+
declare function versionedRollback<T>(r: IVersionedResource, store: IStoreAdapter, opts: {
|
|
53
|
+
key: string;
|
|
54
|
+
scope: string | null;
|
|
55
|
+
toVersion: number;
|
|
56
|
+
actor: string | null;
|
|
57
|
+
}): Promise<T>;
|
|
58
|
+
|
|
59
|
+
export { IPoolConfig, IStoreAdapter, type IVersionedResource, InternalMigrationRunner, MigrationRunner, PGAdapter, VersionConflictError, createMigrationsPath, versionedRollback, versionedWrite };
|
package/dist/index.js
CHANGED
|
@@ -130,11 +130,128 @@ var PGAdapter = class {
|
|
|
130
130
|
await this.pool.end();
|
|
131
131
|
}
|
|
132
132
|
};
|
|
133
|
+
|
|
134
|
+
// src/versioned.ts
|
|
135
|
+
var VersionConflictError = class extends Error {
|
|
136
|
+
constructor(key, scope, currentVersion, expectedVersion) {
|
|
137
|
+
super(
|
|
138
|
+
`"${key}" (${scope ?? "base"}) is at version ${currentVersion ?? "none"}, not ${expectedVersion} \u2014 reload and retry`
|
|
139
|
+
);
|
|
140
|
+
this.key = key;
|
|
141
|
+
this.scope = scope;
|
|
142
|
+
this.currentVersion = currentVersion;
|
|
143
|
+
this.expectedVersion = expectedVersion;
|
|
144
|
+
this.name = "VersionConflictError";
|
|
145
|
+
}
|
|
146
|
+
key;
|
|
147
|
+
scope;
|
|
148
|
+
currentVersion;
|
|
149
|
+
expectedVersion;
|
|
150
|
+
};
|
|
151
|
+
var lockSql = `SELECT pg_advisory_xact_lock(hashtext($1), hashtext($2))`;
|
|
152
|
+
function keyMatch(r) {
|
|
153
|
+
const [id, scope] = r.keyColumns;
|
|
154
|
+
return `${id} IS NOT DISTINCT FROM $1 AND ${scope} IS NOT DISTINCT FROM $2`;
|
|
155
|
+
}
|
|
156
|
+
async function versionedWrite(r, store, opts) {
|
|
157
|
+
const [idCol, scopeCol] = r.keyColumns;
|
|
158
|
+
const content = r.contentColumns;
|
|
159
|
+
const meta = (r.metaColumns ?? []).filter((c) => c in opts.data);
|
|
160
|
+
const contentVals = content.map((c) => opts.data[c] ?? null);
|
|
161
|
+
const metaVals = meta.map((c) => opts.data[c] ?? null);
|
|
162
|
+
return store.transaction(async (tx) => {
|
|
163
|
+
await tx.query(lockSql, [opts.key, opts.scope ?? ""]);
|
|
164
|
+
const [cur] = await tx.query(
|
|
165
|
+
`SELECT version FROM ${r.table} WHERE ${keyMatch(r)} FOR UPDATE`,
|
|
166
|
+
[opts.key, opts.scope]
|
|
167
|
+
);
|
|
168
|
+
const currentVersion = cur?.version ?? null;
|
|
169
|
+
if (opts.ifVersion !== void 0 && currentVersion !== opts.ifVersion) {
|
|
170
|
+
throw new VersionConflictError(opts.key, opts.scope, currentVersion, opts.ifVersion);
|
|
171
|
+
}
|
|
172
|
+
const version = (currentVersion ?? 0) + 1;
|
|
173
|
+
const writeVals = [opts.key, opts.scope, ...contentVals, ...metaVals, version, opts.actor];
|
|
174
|
+
let row;
|
|
175
|
+
if (cur) {
|
|
176
|
+
const sets = [
|
|
177
|
+
...content.map((c, i) => `${c} = $${3 + i}`),
|
|
178
|
+
...meta.map((c, j) => `${c} = $${3 + content.length + j}`),
|
|
179
|
+
`version = $${3 + content.length + meta.length}`,
|
|
180
|
+
`updated_by = $${4 + content.length + meta.length}`,
|
|
181
|
+
`updated_at = now()`
|
|
182
|
+
];
|
|
183
|
+
[row] = await tx.query(
|
|
184
|
+
`UPDATE ${r.table} SET ${sets.join(", ")} WHERE ${keyMatch(r)} RETURNING ${r.returning}`,
|
|
185
|
+
writeVals
|
|
186
|
+
);
|
|
187
|
+
} else {
|
|
188
|
+
const cols = [idCol, scopeCol, ...content, ...meta, "version", "updated_by"];
|
|
189
|
+
const ph = cols.map((_, i) => `$${i + 1}`);
|
|
190
|
+
[row] = await tx.query(
|
|
191
|
+
`INSERT INTO ${r.table} (${cols.join(", ")}) VALUES (${ph.join(", ")}) RETURNING ${r.returning}`,
|
|
192
|
+
writeVals
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const revCols = [idCol, scopeCol, ...content, "version", "actor"];
|
|
196
|
+
const revPh = revCols.map((_, i) => `$${i + 1}`);
|
|
197
|
+
await tx.query(
|
|
198
|
+
`INSERT INTO ${r.revisions} (${revCols.join(", ")}) VALUES (${revPh.join(", ")})`,
|
|
199
|
+
[opts.key, opts.scope, ...contentVals, version, opts.actor]
|
|
200
|
+
);
|
|
201
|
+
await tx.query(`SELECT pg_notify('${r.channel}', $1)`, [opts.scope ?? ""]);
|
|
202
|
+
if (!row) throw new Error(`Failed to write ${r.table} entry`);
|
|
203
|
+
return row;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
async function versionedRollback(r, store, opts) {
|
|
207
|
+
const [idCol, scopeCol] = r.keyColumns;
|
|
208
|
+
const content = r.contentColumns;
|
|
209
|
+
return store.transaction(async (tx) => {
|
|
210
|
+
await tx.query(lockSql, [opts.key, opts.scope ?? ""]);
|
|
211
|
+
const [target] = await tx.query(
|
|
212
|
+
`SELECT ${content.join(", ")} FROM ${r.revisions} WHERE ${keyMatch(r)} AND version = $3`,
|
|
213
|
+
[opts.key, opts.scope, opts.toVersion]
|
|
214
|
+
);
|
|
215
|
+
if (!target) {
|
|
216
|
+
throw new Error(`"${opts.key}" (${opts.scope ?? "base"}) has no revision ${opts.toVersion}`);
|
|
217
|
+
}
|
|
218
|
+
const [cur] = await tx.query(
|
|
219
|
+
`SELECT version FROM ${r.table} WHERE ${keyMatch(r)} FOR UPDATE`,
|
|
220
|
+
[opts.key, opts.scope]
|
|
221
|
+
);
|
|
222
|
+
if (!cur) throw new Error(`"${opts.key}" (${opts.scope ?? "base"}) does not exist`);
|
|
223
|
+
const version = cur.version + 1;
|
|
224
|
+
const contentVals = content.map((c) => target[c] ?? null);
|
|
225
|
+
const writeVals = [opts.key, opts.scope, ...contentVals, version, opts.actor];
|
|
226
|
+
const sets = [
|
|
227
|
+
...content.map((c, i) => `${c} = $${3 + i}`),
|
|
228
|
+
`version = $${3 + content.length}`,
|
|
229
|
+
`updated_by = $${4 + content.length}`,
|
|
230
|
+
`updated_at = now()`
|
|
231
|
+
];
|
|
232
|
+
const [row] = await tx.query(
|
|
233
|
+
`UPDATE ${r.table} SET ${sets.join(", ")} WHERE ${keyMatch(r)} RETURNING ${r.returning}`,
|
|
234
|
+
writeVals
|
|
235
|
+
);
|
|
236
|
+
const revCols = [idCol, scopeCol, ...content, "version", "actor"];
|
|
237
|
+
const revPh = revCols.map((_, i) => `$${i + 1}`);
|
|
238
|
+
await tx.query(
|
|
239
|
+
`INSERT INTO ${r.revisions} (${revCols.join(", ")}) VALUES (${revPh.join(", ")})`,
|
|
240
|
+
writeVals.slice(0, 2 + content.length).concat(version, opts.actor)
|
|
241
|
+
);
|
|
242
|
+
await tx.query(`SELECT pg_notify('${r.channel}', $1)`, [opts.scope ?? ""]);
|
|
243
|
+
if (!row) throw new Error("rollback failed");
|
|
244
|
+
return row;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
133
247
|
export {
|
|
134
248
|
InternalMigrationRunner,
|
|
135
249
|
MigrationRunner,
|
|
136
250
|
PGAdapter,
|
|
251
|
+
VersionConflictError,
|
|
137
252
|
createMigrationsPath,
|
|
138
|
-
sql
|
|
253
|
+
sql,
|
|
254
|
+
versionedRollback,
|
|
255
|
+
versionedWrite
|
|
139
256
|
};
|
|
140
257
|
//# sourceMappingURL=index.js.map
|
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"],"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"],"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\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.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|