@fonderie/store 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fonderie, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @fonderie/store
2
+
3
+ The database brick: an `IStoreAdapter` interface, a PostgreSQL driver,
4
+ sequential migrations, and a tagged-template `sql` helper that makes
5
+ unparameterized queries impossible to write by accident.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install @fonderie/store
11
+ ```
12
+
13
+ ## Use
14
+
15
+ ```ts
16
+ import { sql, PGAdapter } from '@fonderie/store';
17
+
18
+ const { text, params } = sql`SELECT * FROM users WHERE id = ${userId}`;
19
+ const rows = await store.query(text, params);
20
+ ```
21
+
22
+ `MigrationRunner` applies each module's migrations in order —
23
+ every Fonderie brick ships its own schema and installs it through this
24
+ package.
25
+
26
+ ## Why this exists
27
+
28
+ You've shipped this plumbing before — auth, teams, billing, messaging —
29
+ and the next project will ask for it again. Fonderie packages it once:
30
+ plain TypeScript modules for
31
+ [`@fonderie/core`](https://github.com/fonderie-js/sdk/tree/main/packages/core),
32
+ PostgreSQL-backed, self-hosted, MIT. No external control plane, no
33
+ per-seat anything. Register the modules you need; skip the ones you don't.
34
+
35
+ **This package owns** how everything persists. The SQL boundary, the PostgreSQL adapter,
36
+ and the migration runner through which every brick installs its schema.
37
+
38
+ Browse the whole set at
39
+ [fonderie-js/sdk](https://github.com/fonderie-js/sdk) · follow
40
+ [@fonderiejs](https://x.com/fonderiejs)
41
+
42
+ ## License
43
+
44
+ MIT © Fonderie, Inc.
package/dist/index.cjs ADDED
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ InternalMigrationRunner: () => InternalMigrationRunner,
34
+ MigrationRunner: () => MigrationRunner,
35
+ PGAdapter: () => PGAdapter,
36
+ createMigrationsPath: () => createMigrationsPath,
37
+ sql: () => sql
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/sql.ts
42
+ function sql(strings, ...values) {
43
+ let text = "";
44
+ const params = [];
45
+ strings.forEach((str, i) => {
46
+ text += str;
47
+ if (i < values.length) {
48
+ params.push(values[i]);
49
+ text += `$${params.length}`;
50
+ }
51
+ });
52
+ return { text, params };
53
+ }
54
+
55
+ // src/migrations/runner.ts
56
+ var import_node_path = require("path");
57
+ var import_promises = require("fs/promises");
58
+ var MIGRATIONS_TABLE = "fonderie_migrations";
59
+ var RESERVED_PREFIX_RE = /\bfonderie_/i;
60
+ var MigrationRunner = class {
61
+ constructor(store, migrationsDir) {
62
+ this.store = store;
63
+ this.migrationsDir = migrationsDir;
64
+ }
65
+ store;
66
+ migrationsDir;
67
+ async run() {
68
+ await this.ensureTable();
69
+ const [applied, files] = await Promise.all([this.getApplied(), this.getFiles()]);
70
+ const pending = files.filter((f) => !applied.has(f));
71
+ if (pending.length === 0) {
72
+ console.log("[store] migrations: up to date");
73
+ return;
74
+ }
75
+ for (const file of pending) {
76
+ const sql2 = await (0, import_promises.readFile)((0, import_node_path.join)(this.migrationsDir, file), "utf8");
77
+ this.assertNoReservedPrefix(file, sql2);
78
+ await this.store.transaction(async (tx) => {
79
+ await tx.query(sql2);
80
+ await tx.query(`INSERT INTO ${MIGRATIONS_TABLE} (name, applied_at) VALUES ($1, now())`, [
81
+ file
82
+ ]);
83
+ });
84
+ console.log(`[store] migrations: applied ${file}`);
85
+ }
86
+ }
87
+ assertNoReservedPrefix(file, sql2) {
88
+ if (RESERVED_PREFIX_RE.test(sql2)) {
89
+ throw new Error(
90
+ `[store] migration "${file}" uses the reserved "fonderie_" prefix. Use InternalMigrationRunner for fonderie-owned migrations.`
91
+ );
92
+ }
93
+ }
94
+ async ensureTable() {
95
+ await this.store.query(`
96
+ CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
97
+ name TEXT PRIMARY KEY,
98
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
99
+ )
100
+ `);
101
+ }
102
+ async getApplied() {
103
+ const rows = await this.store.query(
104
+ `SELECT name FROM ${MIGRATIONS_TABLE} ORDER BY name`
105
+ );
106
+ return new Set(rows.map((r) => r.name));
107
+ }
108
+ async getFiles() {
109
+ const all = await (0, import_promises.readdir)(this.migrationsDir);
110
+ return all.filter((f) => f.endsWith(".sql")).sort();
111
+ }
112
+ };
113
+ var InternalMigrationRunner = class extends MigrationRunner {
114
+ assertNoReservedPrefix(_file, _sql) {
115
+ }
116
+ };
117
+
118
+ // src/migrations/path.ts
119
+ var import_node_url = require("url");
120
+ var import_node_path2 = require("path");
121
+ function createMigrationsPath(importMetaUrl) {
122
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl)), "sql");
123
+ }
124
+
125
+ // src/adapters/pg.ts
126
+ var import_pg = __toESM(require("pg"), 1);
127
+ var PGAdapter = class {
128
+ pool;
129
+ constructor(config) {
130
+ const options = typeof config === "string" ? { connectionString: config } : config;
131
+ this.pool = new import_pg.default.Pool(options);
132
+ this.pool.on("error", (err) => {
133
+ console.error("[store] idle client error", err.message);
134
+ });
135
+ }
136
+ async testConnection() {
137
+ try {
138
+ await this.pool.query("SELECT 1");
139
+ return true;
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+ async query(sql2, params) {
145
+ const result = await this.pool.query(sql2, params);
146
+ return result.rows;
147
+ }
148
+ async transaction(fn) {
149
+ const client = await this.pool.connect();
150
+ try {
151
+ await client.query("BEGIN");
152
+ const tx = {
153
+ query: async (sql2, params) => {
154
+ const result2 = await client.query(sql2, params);
155
+ return result2.rows;
156
+ },
157
+ transaction: (nested) => nested(tx)
158
+ };
159
+ const result = await fn(tx);
160
+ await client.query("COMMIT");
161
+ return result;
162
+ } catch (err) {
163
+ await client.query("ROLLBACK");
164
+ throw err;
165
+ } finally {
166
+ client.release();
167
+ }
168
+ }
169
+ async end() {
170
+ await this.pool.end();
171
+ }
172
+ };
173
+ // Annotate the CommonJS export names for ESM import in node:
174
+ 0 && (module.exports = {
175
+ InternalMigrationRunner,
176
+ MigrationRunner,
177
+ PGAdapter,
178
+ createMigrationsPath,
179
+ sql
180
+ });
181
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,29 @@
1
+ export { ISqlQuery, sql } from './sql.cjs';
2
+ import { IStoreAdapter, IPoolConfig } from './types.cjs';
3
+
4
+ declare class MigrationRunner {
5
+ private store;
6
+ private migrationsDir;
7
+ constructor(store: IStoreAdapter, migrationsDir: string);
8
+ run(): Promise<void>;
9
+ protected assertNoReservedPrefix(file: string, sql: string): void;
10
+ private ensureTable;
11
+ private getApplied;
12
+ private getFiles;
13
+ }
14
+ declare class InternalMigrationRunner extends MigrationRunner {
15
+ protected assertNoReservedPrefix(_file: string, _sql: string): void;
16
+ }
17
+
18
+ declare function createMigrationsPath(importMetaUrl: string): string;
19
+
20
+ declare class PGAdapter implements IStoreAdapter {
21
+ private pool;
22
+ constructor(config: IPoolConfig | string);
23
+ testConnection(): Promise<boolean>;
24
+ query<T = unknown>(sql: string, params?: unknown[]): Promise<T[]>;
25
+ transaction<T>(fn: (tx: IStoreAdapter) => Promise<T>): Promise<T>;
26
+ end(): Promise<void>;
27
+ }
28
+
29
+ export { IPoolConfig, IStoreAdapter, InternalMigrationRunner, MigrationRunner, PGAdapter, createMigrationsPath };
@@ -0,0 +1,29 @@
1
+ export { ISqlQuery, sql } from './sql.js';
2
+ import { IStoreAdapter, IPoolConfig } from './types.js';
3
+
4
+ declare class MigrationRunner {
5
+ private store;
6
+ private migrationsDir;
7
+ constructor(store: IStoreAdapter, migrationsDir: string);
8
+ run(): Promise<void>;
9
+ protected assertNoReservedPrefix(file: string, sql: string): void;
10
+ private ensureTable;
11
+ private getApplied;
12
+ private getFiles;
13
+ }
14
+ declare class InternalMigrationRunner extends MigrationRunner {
15
+ protected assertNoReservedPrefix(_file: string, _sql: string): void;
16
+ }
17
+
18
+ declare function createMigrationsPath(importMetaUrl: string): string;
19
+
20
+ declare class PGAdapter implements IStoreAdapter {
21
+ private pool;
22
+ constructor(config: IPoolConfig | string);
23
+ testConnection(): Promise<boolean>;
24
+ query<T = unknown>(sql: string, params?: unknown[]): Promise<T[]>;
25
+ transaction<T>(fn: (tx: IStoreAdapter) => Promise<T>): Promise<T>;
26
+ end(): Promise<void>;
27
+ }
28
+
29
+ export { IPoolConfig, IStoreAdapter, InternalMigrationRunner, MigrationRunner, PGAdapter, createMigrationsPath };
package/dist/index.js ADDED
@@ -0,0 +1,140 @@
1
+ // src/sql.ts
2
+ function sql(strings, ...values) {
3
+ let text = "";
4
+ const params = [];
5
+ strings.forEach((str, i) => {
6
+ text += str;
7
+ if (i < values.length) {
8
+ params.push(values[i]);
9
+ text += `$${params.length}`;
10
+ }
11
+ });
12
+ return { text, params };
13
+ }
14
+
15
+ // src/migrations/runner.ts
16
+ import { join } from "path";
17
+ import { readdir, readFile } from "fs/promises";
18
+ var MIGRATIONS_TABLE = "fonderie_migrations";
19
+ var RESERVED_PREFIX_RE = /\bfonderie_/i;
20
+ var MigrationRunner = class {
21
+ constructor(store, migrationsDir) {
22
+ this.store = store;
23
+ this.migrationsDir = migrationsDir;
24
+ }
25
+ store;
26
+ migrationsDir;
27
+ async run() {
28
+ await this.ensureTable();
29
+ const [applied, files] = await Promise.all([this.getApplied(), this.getFiles()]);
30
+ const pending = files.filter((f) => !applied.has(f));
31
+ if (pending.length === 0) {
32
+ console.log("[store] migrations: up to date");
33
+ return;
34
+ }
35
+ for (const file of pending) {
36
+ const sql2 = await readFile(join(this.migrationsDir, file), "utf8");
37
+ this.assertNoReservedPrefix(file, sql2);
38
+ await this.store.transaction(async (tx) => {
39
+ await tx.query(sql2);
40
+ await tx.query(`INSERT INTO ${MIGRATIONS_TABLE} (name, applied_at) VALUES ($1, now())`, [
41
+ file
42
+ ]);
43
+ });
44
+ console.log(`[store] migrations: applied ${file}`);
45
+ }
46
+ }
47
+ assertNoReservedPrefix(file, sql2) {
48
+ if (RESERVED_PREFIX_RE.test(sql2)) {
49
+ throw new Error(
50
+ `[store] migration "${file}" uses the reserved "fonderie_" prefix. Use InternalMigrationRunner for fonderie-owned migrations.`
51
+ );
52
+ }
53
+ }
54
+ async ensureTable() {
55
+ await this.store.query(`
56
+ CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
57
+ name TEXT PRIMARY KEY,
58
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
59
+ )
60
+ `);
61
+ }
62
+ async getApplied() {
63
+ const rows = await this.store.query(
64
+ `SELECT name FROM ${MIGRATIONS_TABLE} ORDER BY name`
65
+ );
66
+ return new Set(rows.map((r) => r.name));
67
+ }
68
+ async getFiles() {
69
+ const all = await readdir(this.migrationsDir);
70
+ return all.filter((f) => f.endsWith(".sql")).sort();
71
+ }
72
+ };
73
+ var InternalMigrationRunner = class extends MigrationRunner {
74
+ assertNoReservedPrefix(_file, _sql) {
75
+ }
76
+ };
77
+
78
+ // src/migrations/path.ts
79
+ import { fileURLToPath } from "url";
80
+ import { dirname, join as join2 } from "path";
81
+ function createMigrationsPath(importMetaUrl) {
82
+ return join2(dirname(fileURLToPath(importMetaUrl)), "sql");
83
+ }
84
+
85
+ // src/adapters/pg.ts
86
+ import pg from "pg";
87
+ var PGAdapter = class {
88
+ pool;
89
+ constructor(config) {
90
+ const options = typeof config === "string" ? { connectionString: config } : config;
91
+ this.pool = new pg.Pool(options);
92
+ this.pool.on("error", (err) => {
93
+ console.error("[store] idle client error", err.message);
94
+ });
95
+ }
96
+ async testConnection() {
97
+ try {
98
+ await this.pool.query("SELECT 1");
99
+ return true;
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+ async query(sql2, params) {
105
+ const result = await this.pool.query(sql2, params);
106
+ return result.rows;
107
+ }
108
+ async transaction(fn) {
109
+ const client = await this.pool.connect();
110
+ try {
111
+ await client.query("BEGIN");
112
+ const tx = {
113
+ query: async (sql2, params) => {
114
+ const result2 = await client.query(sql2, params);
115
+ return result2.rows;
116
+ },
117
+ transaction: (nested) => nested(tx)
118
+ };
119
+ const result = await fn(tx);
120
+ await client.query("COMMIT");
121
+ return result;
122
+ } catch (err) {
123
+ await client.query("ROLLBACK");
124
+ throw err;
125
+ } finally {
126
+ client.release();
127
+ }
128
+ }
129
+ async end() {
130
+ await this.pool.end();
131
+ }
132
+ };
133
+ export {
134
+ InternalMigrationRunner,
135
+ MigrationRunner,
136
+ PGAdapter,
137
+ createMigrationsPath,
138
+ sql
139
+ };
140
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":";AAYO,SAAS,IAAI,YAAkC,QAA8B;AACnF,MAAI,OAAO;AACX,QAAM,SAAoB,CAAC;AAE3B,UAAQ,QAAQ,CAAC,KAAK,MAAM;AAC3B,YAAQ;AACR,QAAI,IAAI,OAAO,QAAQ;AACtB,aAAO,KAAK,OAAO,CAAC,CAAC;AACrB,cAAQ,IAAI,OAAO,MAAM;AAAA,IAC1B;AAAA,EACD,CAAC;AAED,SAAO,EAAE,MAAM,OAAO;AACvB;;;ACzBA,SAAS,YAAY;AACrB,SAAS,SAAS,gBAAgB;AAIlC,IAAM,mBAAmB;AACzB,IAAM,qBAAqB;AAEpB,IAAM,kBAAN,MAAsB;AAAA,EAC5B,YACS,OACA,eACP;AAFO;AACA;AAAA,EACN;AAAA,EAFM;AAAA,EACA;AAAA,EAGT,MAAM,MAAqB;AAC1B,UAAM,KAAK,YAAY;AAEvB,UAAM,CAAC,SAAS,KAAK,IAAI,MAAM,QAAQ,IAAI,CAAC,KAAK,WAAW,GAAG,KAAK,SAAS,CAAC,CAAC;AAE/E,UAAM,UAAU,MAAM,OAAO,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC;AAEnD,QAAI,QAAQ,WAAW,GAAG;AACzB,cAAQ,IAAI,gCAAgC;AAC5C;AAAA,IACD;AAEA,eAAW,QAAQ,SAAS;AAC3B,YAAMA,OAAM,MAAM,SAAS,KAAK,KAAK,eAAe,IAAI,GAAG,MAAM;AAEjE,WAAK,uBAAuB,MAAMA,IAAG;AAErC,YAAM,KAAK,MAAM,YAAY,OAAO,OAAO;AAC1C,cAAM,GAAG,MAAMA,IAAG;AAClB,cAAM,GAAG,MAAM,eAAe,gBAAgB,0CAA0C;AAAA,UACvF;AAAA,QACD,CAAC;AAAA,MACF,CAAC;AAED,cAAQ,IAAI,+BAA+B,IAAI,EAAE;AAAA,IAClD;AAAA,EACD;AAAA,EAEU,uBAAuB,MAAcA,MAAmB;AACjE,QAAI,mBAAmB,KAAKA,IAAG,GAAG;AACjC,YAAM,IAAI;AAAA,QACT,sBAAsB,IAAI;AAAA,MAE3B;AAAA,IACD;AAAA,EACD;AAAA,EAEA,MAAc,cAA6B;AAC1C,UAAM,KAAK,MAAM,MAAM;AAAA,gCACO,gBAAgB;AAAA;AAAA;AAAA;AAAA,GAI7C;AAAA,EACF;AAAA,EAEA,MAAc,aAAmC;AAChD,UAAM,OAAO,MAAM,KAAK,MAAM;AAAA,MAC7B,oBAAoB,gBAAgB;AAAA,IACrC;AACA,WAAO,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;AAAA,EACvC;AAAA,EAEA,MAAc,WAA8B;AAC3C,UAAM,MAAM,MAAM,QAAQ,KAAK,aAAa;AAC5C,WAAO,IAAI,OAAO,CAAC,MAAM,EAAE,SAAS,MAAM,CAAC,EAAE,KAAK;AAAA,EACnD;AACD;AAGO,IAAM,0BAAN,cAAsC,gBAAgB;AAAA,EACzC,uBAAuB,OAAe,MAAoB;AAAA,EAAC;AAC/E;;;AC5EA,SAAS,qBAAqB;AAC9B,SAAS,SAAS,QAAAC,aAAY;AAQvB,SAAS,qBAAqB,eAA+B;AACnE,SAAOA,MAAK,QAAQ,cAAc,aAAa,CAAC,GAAG,KAAK;AACzD;;;ACXA,OAAO,QAAQ;AAGR,IAAM,YAAN,MAAyC;AAAA,EACvC;AAAA,EAER,YAAY,QAA8B;AACzC,UAAM,UAAU,OAAO,WAAW,WAAW,EAAE,kBAAkB,OAAO,IAAI;AAC5E,SAAK,OAAO,IAAI,GAAG,KAAK,OAAO;AAE/B,SAAK,KAAK,GAAG,SAAS,CAAC,QAAQ;AAC9B,cAAQ,MAAM,6BAA6B,IAAI,OAAO;AAAA,IACvD,CAAC;AAAA,EACF;AAAA,EAEA,MAAM,iBAAmC;AACxC,QAAI;AACH,YAAM,KAAK,KAAK,MAAM,UAAU;AAChC,aAAO;AAAA,IACR,QAAQ;AACP,aAAO;AAAA,IACR;AAAA,EACD;AAAA,EAEA,MAAM,MAAmBC,MAAa,QAAkC;AACvE,UAAM,SAAS,MAAM,KAAK,KAAK,MAAMA,MAAK,MAAM;AAChD,WAAO,OAAO;AAAA,EACf;AAAA,EAEA,MAAM,YAAe,IAAmD;AACvE,UAAM,SAAS,MAAM,KAAK,KAAK,QAAQ;AAEvC,QAAI;AACH,YAAM,OAAO,MAAM,OAAO;AAE1B,YAAM,KAAoB;AAAA,QACzB,OAAO,OAAoBA,MAAa,WAAuB;AAC9D,gBAAMC,UAAS,MAAM,OAAO,MAAMD,MAAK,MAAM;AAC7C,iBAAOC,QAAO;AAAA,QACf;AAAA,QACA,aAAa,CAAI,WAA8C,OAAO,EAAE;AAAA,MACzE;AAEA,YAAM,SAAS,MAAM,GAAG,EAAE;AAC1B,YAAM,OAAO,MAAM,QAAQ;AAC3B,aAAO;AAAA,IACR,SAAS,KAAK;AACb,YAAM,OAAO,MAAM,UAAU;AAC7B,YAAM;AAAA,IACP,UAAE;AACD,aAAO,QAAQ;AAAA,IAChB;AAAA,EACD;AAAA,EAEA,MAAM,MAAqB;AAC1B,UAAM,KAAK,KAAK,IAAI;AAAA,EACrB;AACD;","names":["sql","join","sql","result"]}
@@ -0,0 +1,75 @@
1
+ // src/migrations/runner.ts
2
+ import { join } from "path";
3
+ import { readdir, readFile } from "fs/promises";
4
+ var MIGRATIONS_TABLE = "fonderie_migrations";
5
+ var RESERVED_PREFIX_RE = /\bfonderie_/i;
6
+ var MigrationRunner = class {
7
+ constructor(store, migrationsDir) {
8
+ this.store = store;
9
+ this.migrationsDir = migrationsDir;
10
+ }
11
+ store;
12
+ migrationsDir;
13
+ async run() {
14
+ await this.ensureTable();
15
+ const [applied, files] = await Promise.all([this.getApplied(), this.getFiles()]);
16
+ const pending = files.filter((f) => !applied.has(f));
17
+ if (pending.length === 0) {
18
+ console.log("[store] migrations: up to date");
19
+ return;
20
+ }
21
+ for (const file of pending) {
22
+ const sql = await readFile(join(this.migrationsDir, file), "utf8");
23
+ this.assertNoReservedPrefix(file, sql);
24
+ await this.store.transaction(async (tx) => {
25
+ await tx.query(sql);
26
+ await tx.query(`INSERT INTO ${MIGRATIONS_TABLE} (name, applied_at) VALUES ($1, now())`, [
27
+ file
28
+ ]);
29
+ });
30
+ console.log(`[store] migrations: applied ${file}`);
31
+ }
32
+ }
33
+ assertNoReservedPrefix(file, sql) {
34
+ if (RESERVED_PREFIX_RE.test(sql)) {
35
+ throw new Error(
36
+ `[store] migration "${file}" uses the reserved "fonderie_" prefix. Use InternalMigrationRunner for fonderie-owned migrations.`
37
+ );
38
+ }
39
+ }
40
+ async ensureTable() {
41
+ await this.store.query(`
42
+ CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
43
+ name TEXT PRIMARY KEY,
44
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
45
+ )
46
+ `);
47
+ }
48
+ async getApplied() {
49
+ const rows = await this.store.query(
50
+ `SELECT name FROM ${MIGRATIONS_TABLE} ORDER BY name`
51
+ );
52
+ return new Set(rows.map((r) => r.name));
53
+ }
54
+ async getFiles() {
55
+ const all = await readdir(this.migrationsDir);
56
+ return all.filter((f) => f.endsWith(".sql")).sort();
57
+ }
58
+ };
59
+ var InternalMigrationRunner = class extends MigrationRunner {
60
+ assertNoReservedPrefix(_file, _sql) {
61
+ }
62
+ };
63
+
64
+ // src/migrations/path.ts
65
+ import { fileURLToPath } from "url";
66
+ import { dirname, join as join2 } from "path";
67
+ function createMigrationsPath(importMetaUrl) {
68
+ return join2(dirname(fileURLToPath(importMetaUrl)), "sql");
69
+ }
70
+ export {
71
+ InternalMigrationRunner,
72
+ MigrationRunner,
73
+ createMigrationsPath
74
+ };
75
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/migrations/runner.ts","../../src/migrations/path.ts"],"sourcesContent":["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"],"mappings":";AAAA,SAAS,YAAY;AACrB,SAAS,SAAS,gBAAgB;AAIlC,IAAM,mBAAmB;AACzB,IAAM,qBAAqB;AAEpB,IAAM,kBAAN,MAAsB;AAAA,EAC5B,YACS,OACA,eACP;AAFO;AACA;AAAA,EACN;AAAA,EAFM;AAAA,EACA;AAAA,EAGT,MAAM,MAAqB;AAC1B,UAAM,KAAK,YAAY;AAEvB,UAAM,CAAC,SAAS,KAAK,IAAI,MAAM,QAAQ,IAAI,CAAC,KAAK,WAAW,GAAG,KAAK,SAAS,CAAC,CAAC;AAE/E,UAAM,UAAU,MAAM,OAAO,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC;AAEnD,QAAI,QAAQ,WAAW,GAAG;AACzB,cAAQ,IAAI,gCAAgC;AAC5C;AAAA,IACD;AAEA,eAAW,QAAQ,SAAS;AAC3B,YAAM,MAAM,MAAM,SAAS,KAAK,KAAK,eAAe,IAAI,GAAG,MAAM;AAEjE,WAAK,uBAAuB,MAAM,GAAG;AAErC,YAAM,KAAK,MAAM,YAAY,OAAO,OAAO;AAC1C,cAAM,GAAG,MAAM,GAAG;AAClB,cAAM,GAAG,MAAM,eAAe,gBAAgB,0CAA0C;AAAA,UACvF;AAAA,QACD,CAAC;AAAA,MACF,CAAC;AAED,cAAQ,IAAI,+BAA+B,IAAI,EAAE;AAAA,IAClD;AAAA,EACD;AAAA,EAEU,uBAAuB,MAAc,KAAmB;AACjE,QAAI,mBAAmB,KAAK,GAAG,GAAG;AACjC,YAAM,IAAI;AAAA,QACT,sBAAsB,IAAI;AAAA,MAE3B;AAAA,IACD;AAAA,EACD;AAAA,EAEA,MAAc,cAA6B;AAC1C,UAAM,KAAK,MAAM,MAAM;AAAA,gCACO,gBAAgB;AAAA;AAAA;AAAA;AAAA,GAI7C;AAAA,EACF;AAAA,EAEA,MAAc,aAAmC;AAChD,UAAM,OAAO,MAAM,KAAK,MAAM;AAAA,MAC7B,oBAAoB,gBAAgB;AAAA,IACrC;AACA,WAAO,IAAI,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;AAAA,EACvC;AAAA,EAEA,MAAc,WAA8B;AAC3C,UAAM,MAAM,MAAM,QAAQ,KAAK,aAAa;AAC5C,WAAO,IAAI,OAAO,CAAC,MAAM,EAAE,SAAS,MAAM,CAAC,EAAE,KAAK;AAAA,EACnD;AACD;AAGO,IAAM,0BAAN,cAAsC,gBAAgB;AAAA,EACzC,uBAAuB,OAAe,MAAoB;AAAA,EAAC;AAC/E;;;AC5EA,SAAS,qBAAqB;AAC9B,SAAS,SAAS,QAAAA,aAAY;AAQvB,SAAS,qBAAqB,eAA+B;AACnE,SAAOA,MAAK,QAAQ,cAAc,aAAa,CAAC,GAAG,KAAK;AACzD;","names":["join"]}
package/dist/sql.cjs ADDED
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/sql.ts
21
+ var sql_exports = {};
22
+ __export(sql_exports, {
23
+ sql: () => sql
24
+ });
25
+ module.exports = __toCommonJS(sql_exports);
26
+ function sql(strings, ...values) {
27
+ let text = "";
28
+ const params = [];
29
+ strings.forEach((str, i) => {
30
+ text += str;
31
+ if (i < values.length) {
32
+ params.push(values[i]);
33
+ text += `$${params.length}`;
34
+ }
35
+ });
36
+ return { text, params };
37
+ }
38
+ // Annotate the CommonJS export names for ESM import in node:
39
+ 0 && (module.exports = {
40
+ sql
41
+ });
42
+ //# sourceMappingURL=sql.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sql.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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAYO,SAAS,IAAI,YAAkC,QAA8B;AACnF,MAAI,OAAO;AACX,QAAM,SAAoB,CAAC;AAE3B,UAAQ,QAAQ,CAAC,KAAK,MAAM;AAC3B,YAAQ;AACR,QAAI,IAAI,OAAO,QAAQ;AACtB,aAAO,KAAK,OAAO,CAAC,CAAC;AACrB,cAAQ,IAAI,OAAO,MAAM;AAAA,IAC1B;AAAA,EACD,CAAC;AAED,SAAO,EAAE,MAAM,OAAO;AACvB;","names":[]}
package/dist/sql.d.cts ADDED
@@ -0,0 +1,7 @@
1
+ interface ISqlQuery {
2
+ text: string;
3
+ params: unknown[];
4
+ }
5
+ declare function sql(strings: TemplateStringsArray, ...values: unknown[]): ISqlQuery;
6
+
7
+ export { type ISqlQuery, sql };
package/dist/sql.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ interface ISqlQuery {
2
+ text: string;
3
+ params: unknown[];
4
+ }
5
+ declare function sql(strings: TemplateStringsArray, ...values: unknown[]): ISqlQuery;
6
+
7
+ export { type ISqlQuery, sql };
package/dist/sql.js ADDED
@@ -0,0 +1,17 @@
1
+ // src/sql.ts
2
+ function sql(strings, ...values) {
3
+ let text = "";
4
+ const params = [];
5
+ strings.forEach((str, i) => {
6
+ text += str;
7
+ if (i < values.length) {
8
+ params.push(values[i]);
9
+ text += `$${params.length}`;
10
+ }
11
+ });
12
+ return { text, params };
13
+ }
14
+ export {
15
+ sql
16
+ };
17
+ //# sourceMappingURL=sql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sql.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"],"mappings":";AAYO,SAAS,IAAI,YAAkC,QAA8B;AACnF,MAAI,OAAO;AACX,QAAM,SAAoB,CAAC;AAE3B,UAAQ,QAAQ,CAAC,KAAK,MAAM;AAC3B,YAAQ;AACR,QAAI,IAAI,OAAO,QAAQ;AACtB,aAAO,KAAK,OAAO,CAAC,CAAC;AACrB,cAAQ,IAAI,OAAO,MAAM;AAAA,IAC1B;AAAA,EACD,CAAC;AAED,SAAO,EAAE,MAAM,OAAO;AACvB;","names":[]}
package/dist/types.cjs ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __copyProps = (to, from, except, desc) => {
7
+ if (from && typeof from === "object" || typeof from === "function") {
8
+ for (let key of __getOwnPropNames(from))
9
+ if (!__hasOwnProp.call(to, key) && key !== except)
10
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
11
+ }
12
+ return to;
13
+ };
14
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
15
+
16
+ // src/types.ts
17
+ var types_exports = {};
18
+ module.exports = __toCommonJS(types_exports);
19
+ //# sourceMappingURL=types.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["export interface IStoreAdapter {\n\tquery<T = unknown>(sql: string, params?: unknown[]): Promise<T[]>;\n\ttransaction<T>(fn: (tx: IStoreAdapter) => Promise<T>): Promise<T>;\n}\n\nexport interface IPoolConfig {\n\tconnectionString?: string;\n\thost?: string;\n\tport?: number;\n\tdatabase?: string;\n\tuser?: string;\n\tpassword?: string;\n\tmax?: number;\n\tidleTimeoutMillis?: number;\n\tconnectionTimeoutMillis?: number;\n\tssl?: boolean | { rejectUnauthorized: boolean };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
@@ -0,0 +1,20 @@
1
+ interface IStoreAdapter {
2
+ query<T = unknown>(sql: string, params?: unknown[]): Promise<T[]>;
3
+ transaction<T>(fn: (tx: IStoreAdapter) => Promise<T>): Promise<T>;
4
+ }
5
+ interface IPoolConfig {
6
+ connectionString?: string;
7
+ host?: string;
8
+ port?: number;
9
+ database?: string;
10
+ user?: string;
11
+ password?: string;
12
+ max?: number;
13
+ idleTimeoutMillis?: number;
14
+ connectionTimeoutMillis?: number;
15
+ ssl?: boolean | {
16
+ rejectUnauthorized: boolean;
17
+ };
18
+ }
19
+
20
+ export type { IPoolConfig, IStoreAdapter };
@@ -0,0 +1,20 @@
1
+ interface IStoreAdapter {
2
+ query<T = unknown>(sql: string, params?: unknown[]): Promise<T[]>;
3
+ transaction<T>(fn: (tx: IStoreAdapter) => Promise<T>): Promise<T>;
4
+ }
5
+ interface IPoolConfig {
6
+ connectionString?: string;
7
+ host?: string;
8
+ port?: number;
9
+ database?: string;
10
+ user?: string;
11
+ password?: string;
12
+ max?: number;
13
+ idleTimeoutMillis?: number;
14
+ connectionTimeoutMillis?: number;
15
+ ssl?: boolean | {
16
+ rejectUnauthorized: boolean;
17
+ };
18
+ }
19
+
20
+ export type { IPoolConfig, IStoreAdapter };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@fonderie/store",
3
+ "version": "0.1.0",
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
+ "keywords": [
6
+ "fonderie-js",
7
+ "postgres",
8
+ "postgresql",
9
+ "database",
10
+ "migrations",
11
+ "adapter",
12
+ "sql",
13
+ "saas",
14
+ "typescript"
15
+ ],
16
+ "license": "MIT",
17
+ "type": "module",
18
+ "engines": {
19
+ "node": ">=20"
20
+ },
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js",
25
+ "require": "./dist/index.cjs"
26
+ },
27
+ "./sql": {
28
+ "types": "./dist/sql.d.ts",
29
+ "import": "./dist/sql.js",
30
+ "require": "./dist/sql.cjs"
31
+ },
32
+ "./types": {
33
+ "types": "./dist/types.d.ts",
34
+ "import": "./dist/types.js",
35
+ "require": "./dist/types.cjs"
36
+ },
37
+ "./migrations": {
38
+ "types": "./dist/migrations/index.d.ts",
39
+ "import": "./dist/migrations/index.js"
40
+ }
41
+ },
42
+ "main": "./dist/index.cjs",
43
+ "module": "./dist/index.js",
44
+ "types": "./dist/index.d.ts",
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "dev": "tsup --watch",
48
+ "typecheck": "tsc --noEmit",
49
+ "test": "tsx --test src/__tests__/*.test.ts",
50
+ "lint": "biome lint src",
51
+ "format": "biome format --write src",
52
+ "check": "biome check --write src"
53
+ },
54
+ "dependencies": {
55
+ "pg": "^8.20.0"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^25.6.0",
59
+ "@types/pg": "^8.20.0",
60
+ "tsup": "^8.5.1",
61
+ "tsx": "^4.21.0",
62
+ "typescript": "^6.0.3"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public"
66
+ },
67
+ "files": [
68
+ "dist",
69
+ "LICENSE",
70
+ "README.md"
71
+ ],
72
+ "repository": {
73
+ "type": "git",
74
+ "url": "git+https://github.com/fonderie-js/sdk.git",
75
+ "directory": "packages/store"
76
+ },
77
+ "homepage": "https://github.com/fonderie-js/sdk/tree/main/packages/store#readme",
78
+ "bugs": {
79
+ "url": "https://github.com/fonderie-js/sdk/issues"
80
+ }
81
+ }