@storyshelf/db-sqlite 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { DDL, tableColumns } from "./ddl.mjs";
5
5
  import { createDrizzleAdapter, withManualTransactions } from "./drizzle-factory.mjs";
6
+ import { ensureDbDir } from "./ensure-db-dir.mjs";
6
7
  import { schema } from "./schema.mjs";
7
8
  import Database from "better-sqlite3";
8
9
  import { drizzle } from "drizzle-orm/better-sqlite3";
@@ -24,7 +25,7 @@ import { drizzle } from "drizzle-orm/better-sqlite3";
24
25
  function createBetterSqlite3Database(options) {
25
26
  if (!options.client && options.path === void 0) throw new Error("createBetterSqlite3Database: provide `path` or a preconfigured `client`");
26
27
  const owned = !options.client;
27
- const client = options.client ?? new Database(options.path, connectionOptions(options));
28
+ const client = options.client ?? openDatabase(options);
28
29
  if (owned) {
29
30
  client.exec("PRAGMA journal_mode = WAL");
30
31
  client.exec("PRAGMA busy_timeout = 5000");
@@ -110,6 +111,11 @@ function runMigrations(client) {
110
111
  execIgnore(client, "PRAGMA foreign_keys = ON");
111
112
  }
112
113
  }
114
+ /** Open the file-backed database, creating its directory first. */
115
+ function openDatabase(options) {
116
+ ensureDbDir(options.path ?? "");
117
+ return new Database(options.path, connectionOptions(options));
118
+ }
113
119
  //#endregion
114
120
  export { createBetterSqlite3Database };
115
121
 
@@ -1 +1 @@
1
- {"version":3,"file":"better-sqlite3.mjs","names":[],"sources":["../src/better-sqlite3.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\n/**\n * better-sqlite3 preset for the SQLite database adapter.\n *\n * Same schema, DDL, and Drizzle factory as the `node:sqlite` default;\n * only the driver differs (synchronous, native). Requires the optional\n * `better-sqlite3` peer (`nub add better-sqlite3`).\n */\nimport Database from \"better-sqlite3\";\nimport { drizzle } from \"drizzle-orm/better-sqlite3\";\nimport { DDL, tableColumns } from \"./ddl.ts\";\nimport { createDrizzleAdapter, withManualTransactions } from \"./drizzle-factory.ts\";\nimport { schema } from \"./schema/index.ts\";\n\ndeclare const __PKG_VERSION__: string | undefined;\n\ntype BetterSqlite3Client = InstanceType<typeof Database>;\n\n/**\n * Options for creating a better-sqlite3-backed DatabaseAdapter.\n *\n * When `client` is supplied, `path` is ignored and the caller owns the\n * lifecycle of the client (teardown is a no-op). This is the hook for\n * shared connections and hermetic tests.\n */\nexport interface BetterSqlite3DatabaseOptions {\n path?: string;\n readonly?: boolean;\n client?: BetterSqlite3Client;\n}\n\n/**\n * Create a better-sqlite3-backed DatabaseAdapter using Drizzle ORM.\n *\n * @param options - Either a filesystem `path` (use `\":memory:\"` for an\n * ephemeral database) or a preconfigured better-sqlite3 `client`.\n * @returns A DatabaseAdapter backed by the SQLite file (WAL mode).\n */\nexport function createBetterSqlite3Database(\n options: BetterSqlite3DatabaseOptions,\n): DatabaseAdapter {\n if (!options.client && options.path === undefined) {\n throw new Error(\"createBetterSqlite3Database: provide `path` or a preconfigured `client`\");\n }\n\n const owned = !options.client;\n const client = options.client ?? new Database(options.path, connectionOptions(options));\n if (owned) {\n client.exec(\"PRAGMA journal_mode = WAL\");\n client.exec(\"PRAGMA busy_timeout = 5000\");\n }\n const db = drizzle(client, { schema });\n\n const adapter = createDrizzleAdapter(db, {\n metadata: {\n name: \"better-sqlite3\",\n version: (globalThis as unknown as { __PKG_VERSION__?: string }).__PKG_VERSION__ ?? \"0.0.0\",\n description: \"better-sqlite3 database adapter (native + Drizzle)\",\n kind: \"better-sqlite3\",\n category: \"database\",\n },\n tables: schema,\n migrate: () => {\n runMigrations(client);\n },\n close: () => {\n if (owned) {\n client.close();\n }\n },\n ping: () => {\n client.prepare(\"SELECT 1\").get();\n },\n });\n\n return withManualTransactions(adapter, (sql) => {\n client.exec(sql);\n });\n}\n\nconst WEBHOOK_SECRET_DROP = \"ALTER TABLE webhooks DROP COLUMN secret\";\n\n/** better-sqlite3 rejects `undefined` option values — pass only defined keys. */\nfunction connectionOptions(options: BetterSqlite3DatabaseOptions): { readonly?: boolean } {\n return options.readonly === undefined ? {} : { readonly: options.readonly };\n}\n\nfunction execIgnore(client: BetterSqlite3Client, sql: string): void {\n try {\n client.exec(sql);\n } catch {\n // idempotent — column already exists or other no-op\n }\n}\n\nfunction migrateCommentsTable(client: BetterSqlite3Client): void {\n const row = client\n .prepare(\"SELECT sql FROM sqlite_master WHERE type='table' AND name='comments'\")\n .get() as { sql: string } | undefined;\n if (!row?.sql.includes(\"user_id TEXT NOT NULL REFERENCES users\")) {\n return;\n }\n client.exec(\"PRAGMA foreign_keys = OFF\");\n client.exec(`\n CREATE TABLE comments_new (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n snapshot_id TEXT REFERENCES snapshots(id) ON DELETE CASCADE,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n body TEXT NOT NULL,\n parent_id TEXT,\n resolved INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n )\n `);\n client.exec(\"INSERT INTO comments_new SELECT * FROM comments\");\n client.exec(\"DROP TABLE comments\");\n client.exec(\"ALTER TABLE comments_new RENAME TO comments\");\n client.exec(\"CREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id)\");\n client.exec(\"PRAGMA foreign_keys = ON\");\n}\n\n/** Add any DDL column missing from a volume created before it shipped. */\nfunction ensureColumns(client: BetterSqlite3Client, ddl: string): void {\n for (const [table, columns] of tableColumns(ddl)) {\n const rows = client.prepare(`PRAGMA table_info(${table})`).all() as { name: unknown }[];\n const existing = new Set(rows.map((row) => String(row.name)));\n for (const column of columns) {\n const [name] = column.split(/\\s+/u);\n if (name && !existing.has(name)) {\n execIgnore(client, `ALTER TABLE ${table} ADD COLUMN ${column}`);\n }\n }\n }\n}\n\nfunction runMigrations(client: BetterSqlite3Client): void {\n client.exec(\"PRAGMA foreign_keys = ON\");\n client.exec(DDL);\n ensureColumns(client, DDL);\n try {\n client.exec(WEBHOOK_SECRET_DROP);\n } catch {\n // already migrated — ignore\n }\n try {\n migrateCommentsTable(client);\n } catch {\n execIgnore(client, \"PRAGMA foreign_keys = ON\");\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,4BACd,SACiB;CACjB,IAAI,CAAC,QAAQ,UAAU,QAAQ,SAAS,KAAA,GACtC,MAAM,IAAI,MAAM,yEAAyE;CAG3F,MAAM,QAAQ,CAAC,QAAQ;CACvB,MAAM,SAAS,QAAQ,UAAU,IAAI,SAAS,QAAQ,MAAM,kBAAkB,OAAO,CAAC;CACtF,IAAI,OAAO;EACT,OAAO,KAAK,2BAA2B;EACvC,OAAO,KAAK,4BAA4B;CAC1C;CACA,MAAM,KAAK,QAAQ,QAAQ,EAAE,OAAO,CAAC;CAErC,MAAM,UAAU,qBAAqB,IAAI;EACvC,UAAU;GACR,MAAM;GACN,SAAU,WAAuD,mBAAmB;GACpF,aAAa;GACb,MAAM;GACN,UAAU;EACZ;EACA,QAAQ;EACR,eAAe;GACb,cAAc,MAAM;EACtB;EACA,aAAa;GACX,IAAI,OACF,OAAO,MAAM;EAEjB;EACA,YAAY;GACV,OAAO,QAAQ,UAAU,CAAC,CAAC,IAAI;EACjC;CACF,CAAC;CAED,OAAO,uBAAuB,UAAU,QAAQ;EAC9C,OAAO,KAAK,GAAG;CACjB,CAAC;AACH;AAEA,MAAM,sBAAsB;;AAG5B,SAAS,kBAAkB,SAA+D;CACxF,OAAO,QAAQ,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU,QAAQ,SAAS;AAC5E;AAEA,SAAS,WAAW,QAA6B,KAAmB;CAClE,IAAI;EACF,OAAO,KAAK,GAAG;CACjB,QAAQ,CAER;AACF;AAEA,SAAS,qBAAqB,QAAmC;CAI/D,IAAI,CAHQ,OACT,QAAQ,sEAAsE,CAAC,CAC/E,IACI,CAAC,EAAE,IAAI,SAAS,wCAAwC,GAC7D;CAEF,OAAO,KAAK,2BAA2B;CACvC,OAAO,KAAK;;;;;;;;;;;;;GAaX;CACD,OAAO,KAAK,iDAAiD;CAC7D,OAAO,KAAK,qBAAqB;CACjC,OAAO,KAAK,6CAA6C;CACzD,OAAO,KAAK,yEAAyE;CACrF,OAAO,KAAK,0BAA0B;AACxC;;AAGA,SAAS,cAAc,QAA6B,KAAmB;CACrE,KAAK,MAAM,CAAC,OAAO,YAAY,aAAa,GAAG,GAAG;EAChD,MAAM,OAAO,OAAO,QAAQ,qBAAqB,MAAM,EAAE,CAAC,CAAC,IAAI;EAC/D,MAAM,WAAW,IAAI,IAAI,KAAK,KAAK,QAAQ,OAAO,IAAI,IAAI,CAAC,CAAC;EAC5D,KAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,CAAC,QAAQ,OAAO,MAAM,MAAM;GAClC,IAAI,QAAQ,CAAC,SAAS,IAAI,IAAI,GAC5B,WAAW,QAAQ,eAAe,MAAM,cAAc,QAAQ;EAElE;CACF;AACF;AAEA,SAAS,cAAc,QAAmC;CACxD,OAAO,KAAK,0BAA0B;CACtC,OAAO,KAAK,GAAG;CACf,cAAc,QAAQ,GAAG;CACzB,IAAI;EACF,OAAO,KAAK,mBAAmB;CACjC,QAAQ,CAER;CACA,IAAI;EACF,qBAAqB,MAAM;CAC7B,QAAQ;EACN,WAAW,QAAQ,0BAA0B;CAC/C;AACF"}
1
+ {"version":3,"file":"better-sqlite3.mjs","names":[],"sources":["../src/better-sqlite3.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\n/**\n * better-sqlite3 preset for the SQLite database adapter.\n *\n * Same schema, DDL, and Drizzle factory as the `node:sqlite` default;\n * only the driver differs (synchronous, native). Requires the optional\n * `better-sqlite3` peer (`nub add better-sqlite3`).\n */\nimport Database from \"better-sqlite3\";\nimport { drizzle } from \"drizzle-orm/better-sqlite3\";\nimport { DDL, tableColumns } from \"./ddl.ts\";\nimport { createDrizzleAdapter, withManualTransactions } from \"./drizzle-factory.ts\";\nimport { ensureDbDir } from \"./ensure-db-dir.ts\";\nimport { schema } from \"./schema/index.ts\";\n\ndeclare const __PKG_VERSION__: string | undefined;\n\ntype BetterSqlite3Client = InstanceType<typeof Database>;\n\n/**\n * Options for creating a better-sqlite3-backed DatabaseAdapter.\n *\n * When `client` is supplied, `path` is ignored and the caller owns the\n * lifecycle of the client (teardown is a no-op). This is the hook for\n * shared connections and hermetic tests.\n */\nexport interface BetterSqlite3DatabaseOptions {\n path?: string;\n readonly?: boolean;\n client?: BetterSqlite3Client;\n}\n\n/**\n * Create a better-sqlite3-backed DatabaseAdapter using Drizzle ORM.\n *\n * @param options - Either a filesystem `path` (use `\":memory:\"` for an\n * ephemeral database) or a preconfigured better-sqlite3 `client`.\n * @returns A DatabaseAdapter backed by the SQLite file (WAL mode).\n */\nexport function createBetterSqlite3Database(\n options: BetterSqlite3DatabaseOptions,\n): DatabaseAdapter {\n if (!options.client && options.path === undefined) {\n throw new Error(\"createBetterSqlite3Database: provide `path` or a preconfigured `client`\");\n }\n\n const owned = !options.client;\n const client = options.client ?? openDatabase(options);\n if (owned) {\n client.exec(\"PRAGMA journal_mode = WAL\");\n client.exec(\"PRAGMA busy_timeout = 5000\");\n }\n const db = drizzle(client, { schema });\n\n const adapter = createDrizzleAdapter(db, {\n metadata: {\n name: \"better-sqlite3\",\n version: (globalThis as unknown as { __PKG_VERSION__?: string }).__PKG_VERSION__ ?? \"0.0.0\",\n description: \"better-sqlite3 database adapter (native + Drizzle)\",\n kind: \"better-sqlite3\",\n category: \"database\",\n },\n tables: schema,\n migrate: () => {\n runMigrations(client);\n },\n close: () => {\n if (owned) {\n client.close();\n }\n },\n ping: () => {\n client.prepare(\"SELECT 1\").get();\n },\n });\n\n return withManualTransactions(adapter, (sql) => {\n client.exec(sql);\n });\n}\n\nconst WEBHOOK_SECRET_DROP = \"ALTER TABLE webhooks DROP COLUMN secret\";\n\n/** better-sqlite3 rejects `undefined` option values — pass only defined keys. */\nfunction connectionOptions(options: BetterSqlite3DatabaseOptions): { readonly?: boolean } {\n return options.readonly === undefined ? {} : { readonly: options.readonly };\n}\n\nfunction execIgnore(client: BetterSqlite3Client, sql: string): void {\n try {\n client.exec(sql);\n } catch {\n // idempotent — column already exists or other no-op\n }\n}\n\nfunction migrateCommentsTable(client: BetterSqlite3Client): void {\n const row = client\n .prepare(\"SELECT sql FROM sqlite_master WHERE type='table' AND name='comments'\")\n .get() as { sql: string } | undefined;\n if (!row?.sql.includes(\"user_id TEXT NOT NULL REFERENCES users\")) {\n return;\n }\n client.exec(\"PRAGMA foreign_keys = OFF\");\n client.exec(`\n CREATE TABLE comments_new (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n snapshot_id TEXT REFERENCES snapshots(id) ON DELETE CASCADE,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n body TEXT NOT NULL,\n parent_id TEXT,\n resolved INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n )\n `);\n client.exec(\"INSERT INTO comments_new SELECT * FROM comments\");\n client.exec(\"DROP TABLE comments\");\n client.exec(\"ALTER TABLE comments_new RENAME TO comments\");\n client.exec(\"CREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id)\");\n client.exec(\"PRAGMA foreign_keys = ON\");\n}\n\n/** Add any DDL column missing from a volume created before it shipped. */\nfunction ensureColumns(client: BetterSqlite3Client, ddl: string): void {\n for (const [table, columns] of tableColumns(ddl)) {\n const rows = client.prepare(`PRAGMA table_info(${table})`).all() as { name: unknown }[];\n const existing = new Set(rows.map((row) => String(row.name)));\n for (const column of columns) {\n const [name] = column.split(/\\s+/u);\n if (name && !existing.has(name)) {\n execIgnore(client, `ALTER TABLE ${table} ADD COLUMN ${column}`);\n }\n }\n }\n}\n\nfunction runMigrations(client: BetterSqlite3Client): void {\n client.exec(\"PRAGMA foreign_keys = ON\");\n client.exec(DDL);\n ensureColumns(client, DDL);\n try {\n client.exec(WEBHOOK_SECRET_DROP);\n } catch {\n // already migrated — ignore\n }\n try {\n migrateCommentsTable(client);\n } catch {\n execIgnore(client, \"PRAGMA foreign_keys = ON\");\n }\n}\n\n/** Open the file-backed database, creating its directory first. */\nfunction openDatabase(options: BetterSqlite3DatabaseOptions): InstanceType<typeof Database> {\n ensureDbDir(options.path ?? \"\");\n return new Database(options.path, connectionOptions(options));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,4BACd,SACiB;CACjB,IAAI,CAAC,QAAQ,UAAU,QAAQ,SAAS,KAAA,GACtC,MAAM,IAAI,MAAM,yEAAyE;CAG3F,MAAM,QAAQ,CAAC,QAAQ;CACvB,MAAM,SAAS,QAAQ,UAAU,aAAa,OAAO;CACrD,IAAI,OAAO;EACT,OAAO,KAAK,2BAA2B;EACvC,OAAO,KAAK,4BAA4B;CAC1C;CACA,MAAM,KAAK,QAAQ,QAAQ,EAAE,OAAO,CAAC;CAErC,MAAM,UAAU,qBAAqB,IAAI;EACvC,UAAU;GACR,MAAM;GACN,SAAU,WAAuD,mBAAmB;GACpF,aAAa;GACb,MAAM;GACN,UAAU;EACZ;EACA,QAAQ;EACR,eAAe;GACb,cAAc,MAAM;EACtB;EACA,aAAa;GACX,IAAI,OACF,OAAO,MAAM;EAEjB;EACA,YAAY;GACV,OAAO,QAAQ,UAAU,CAAC,CAAC,IAAI;EACjC;CACF,CAAC;CAED,OAAO,uBAAuB,UAAU,QAAQ;EAC9C,OAAO,KAAK,GAAG;CACjB,CAAC;AACH;AAEA,MAAM,sBAAsB;;AAG5B,SAAS,kBAAkB,SAA+D;CACxF,OAAO,QAAQ,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU,QAAQ,SAAS;AAC5E;AAEA,SAAS,WAAW,QAA6B,KAAmB;CAClE,IAAI;EACF,OAAO,KAAK,GAAG;CACjB,QAAQ,CAER;AACF;AAEA,SAAS,qBAAqB,QAAmC;CAI/D,IAAI,CAHQ,OACT,QAAQ,sEAAsE,CAAC,CAC/E,IACI,CAAC,EAAE,IAAI,SAAS,wCAAwC,GAC7D;CAEF,OAAO,KAAK,2BAA2B;CACvC,OAAO,KAAK;;;;;;;;;;;;;GAaX;CACD,OAAO,KAAK,iDAAiD;CAC7D,OAAO,KAAK,qBAAqB;CACjC,OAAO,KAAK,6CAA6C;CACzD,OAAO,KAAK,yEAAyE;CACrF,OAAO,KAAK,0BAA0B;AACxC;;AAGA,SAAS,cAAc,QAA6B,KAAmB;CACrE,KAAK,MAAM,CAAC,OAAO,YAAY,aAAa,GAAG,GAAG;EAChD,MAAM,OAAO,OAAO,QAAQ,qBAAqB,MAAM,EAAE,CAAC,CAAC,IAAI;EAC/D,MAAM,WAAW,IAAI,IAAI,KAAK,KAAK,QAAQ,OAAO,IAAI,IAAI,CAAC,CAAC;EAC5D,KAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,CAAC,QAAQ,OAAO,MAAM,MAAM;GAClC,IAAI,QAAQ,CAAC,SAAS,IAAI,IAAI,GAC5B,WAAW,QAAQ,eAAe,MAAM,cAAc,QAAQ;EAElE;CACF;AACF;AAEA,SAAS,cAAc,QAAmC;CACxD,OAAO,KAAK,0BAA0B;CACtC,OAAO,KAAK,GAAG;CACf,cAAc,QAAQ,GAAG;CACzB,IAAI;EACF,OAAO,KAAK,mBAAmB;CACjC,QAAQ,CAER;CACA,IAAI;EACF,qBAAqB,MAAM;CAC7B,QAAQ;EACN,WAAW,QAAQ,0BAA0B;CAC/C;AACF;;AAGA,SAAS,aAAa,SAAsE;CAC1F,YAAY,QAAQ,QAAQ,EAAE;CAC9B,OAAO,IAAI,SAAS,QAAQ,MAAM,kBAAkB,OAAO,CAAC;AAC9D"}
@@ -3,6 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { DDL, tableColumns } from "./ddl.mjs";
5
5
  import { createDrizzleAdapter, withManualTransactions } from "./drizzle-factory.mjs";
6
+ import { ensureDbDir } from "./ensure-db-dir.mjs";
6
7
  import { schema } from "./schema.mjs";
7
8
  import { Database } from "bun:sqlite";
8
9
  import { drizzle } from "drizzle-orm/bun-sqlite";
@@ -24,7 +25,7 @@ import { drizzle } from "drizzle-orm/bun-sqlite";
24
25
  function createBunSqliteDatabase(options) {
25
26
  if (!options.client && options.path === void 0) throw new Error("createBunSqliteDatabase: provide `path` or a preconfigured `client`");
26
27
  const owned = !options.client;
27
- const client = options.client ?? new Database(options.path, connectionOptions(options));
28
+ const client = options.client ?? openDatabase(options);
28
29
  if (owned) {
29
30
  client.exec("PRAGMA journal_mode = WAL");
30
31
  client.exec("PRAGMA busy_timeout = 5000");
@@ -114,6 +115,11 @@ function runMigrations(client) {
114
115
  execIgnore(client, "PRAGMA foreign_keys = ON");
115
116
  }
116
117
  }
118
+ /** Open the file-backed database, creating its directory first. */
119
+ function openDatabase(options) {
120
+ ensureDbDir(options.path ?? "");
121
+ return new Database(options.path, connectionOptions(options));
122
+ }
117
123
  //#endregion
118
124
  export { createBunSqliteDatabase };
119
125
 
@@ -1 +1 @@
1
- {"version":3,"file":"bun-sqlite.mjs","names":[],"sources":["../src/bun-sqlite.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\n/**\n * Bun preset for the SQLite database adapter.\n *\n * Same schema, DDL, and Drizzle factory as the `node:sqlite` default;\n * only the driver differs (`bun:sqlite`, synchronous builtin). Bun-only:\n * importing this module outside Bun throws when resolving `bun:sqlite`.\n */\nimport { Database } from \"bun:sqlite\";\nimport { drizzle } from \"drizzle-orm/bun-sqlite\";\nimport { DDL, tableColumns } from \"./ddl.ts\";\nimport { createDrizzleAdapter, withManualTransactions } from \"./drizzle-factory.ts\";\nimport { schema } from \"./schema/index.ts\";\n\ndeclare const __PKG_VERSION__: string | undefined;\n\n/**\n * Options for creating a Bun-backed DatabaseAdapter.\n *\n * When `client` is supplied, `path` is ignored and the caller owns the\n * lifecycle of the client (teardown is a no-op). This is the hook for\n * shared connections and hermetic tests.\n */\nexport interface BunSqliteDatabaseOptions {\n path?: string;\n readonly?: boolean;\n create?: boolean;\n readwrite?: boolean;\n client?: Database;\n}\n\n/**\n * Create a Bun-backed DatabaseAdapter using Drizzle ORM.\n *\n * @param options - Either a filesystem `path` (use `\":memory:\"` for an\n * ephemeral database) or a preconfigured `bun:sqlite` `client`.\n * @returns A DatabaseAdapter backed by the SQLite file (WAL mode).\n */\nexport function createBunSqliteDatabase(options: BunSqliteDatabaseOptions): DatabaseAdapter {\n if (!options.client && options.path === undefined) {\n throw new Error(\"createBunSqliteDatabase: provide `path` or a preconfigured `client`\");\n }\n\n const owned = !options.client;\n const client = options.client ?? new Database(options.path, connectionOptions(options));\n if (owned) {\n client.exec(\"PRAGMA journal_mode = WAL\");\n client.exec(\"PRAGMA busy_timeout = 5000\");\n }\n const db = drizzle(client, { schema });\n\n const adapter = createDrizzleAdapter(db, {\n metadata: {\n name: \"Bun SQLite\",\n version: (globalThis as unknown as { __PKG_VERSION__?: string }).__PKG_VERSION__ ?? \"0.0.0\",\n description: \"Bun SQLite database adapter (bun:sqlite + Drizzle)\",\n kind: \"bun-sqlite\",\n category: \"database\",\n },\n tables: schema,\n migrate: () => {\n runMigrations(client);\n },\n close: () => {\n if (owned) {\n client.close();\n }\n },\n ping: () => {\n client.query(\"SELECT 1\").get();\n },\n });\n\n return withManualTransactions(adapter, (sql) => {\n client.exec(sql);\n });\n}\n\nconst WEBHOOK_SECRET_DROP = \"ALTER TABLE webhooks DROP COLUMN secret\";\n\n/** Bun ignores `undefined` options, but only defined keys are passed. */\nfunction connectionOptions(options: BunSqliteDatabaseOptions): {\n readonly?: boolean;\n create?: boolean;\n readwrite?: boolean;\n} {\n const result: { readonly?: boolean; create?: boolean; readwrite?: boolean } = {};\n if (options.readonly !== undefined) {\n result.readonly = options.readonly;\n }\n if (options.create !== undefined) {\n result.create = options.create;\n }\n if (options.readwrite !== undefined) {\n result.readwrite = options.readwrite;\n }\n return result;\n}\n\nfunction execIgnore(client: Database, sql: string): void {\n try {\n client.exec(sql);\n } catch {\n // idempotent — column already exists or other no-op\n }\n}\n\nfunction migrateCommentsTable(client: Database): void {\n const row = client\n .query(\"SELECT sql FROM sqlite_master WHERE type='table' AND name='comments'\")\n .get() as {\n sql: string;\n } | null;\n if (!row?.sql.includes(\"user_id TEXT NOT NULL REFERENCES users\")) {\n return;\n }\n client.exec(\"PRAGMA foreign_keys = OFF\");\n client.exec(`\n CREATE TABLE comments_new (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n snapshot_id TEXT REFERENCES snapshots(id) ON DELETE CASCADE,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n body TEXT NOT NULL,\n parent_id TEXT,\n resolved INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n )\n `);\n client.exec(\"INSERT INTO comments_new SELECT * FROM comments\");\n client.exec(\"DROP TABLE comments\");\n client.exec(\"ALTER TABLE comments_new RENAME TO comments\");\n client.exec(\"CREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id)\");\n client.exec(\"PRAGMA foreign_keys = ON\");\n}\n\n/** Add any DDL column missing from a volume created before it shipped. */\nfunction ensureColumns(client: Database, ddl: string): void {\n for (const [table, columns] of tableColumns(ddl)) {\n const rows = client.query(`PRAGMA table_info(${table})`).all() as { name: unknown }[];\n const existing = new Set(rows.map((row) => String(row.name)));\n for (const column of columns) {\n const [name] = column.split(/\\s+/u);\n if (name && !existing.has(name)) {\n execIgnore(client, `ALTER TABLE ${table} ADD COLUMN ${column}`);\n }\n }\n }\n}\n\nfunction runMigrations(client: Database): void {\n client.exec(\"PRAGMA foreign_keys = ON\");\n client.exec(DDL);\n ensureColumns(client, DDL);\n try {\n client.exec(WEBHOOK_SECRET_DROP);\n } catch {\n // already migrated — ignore\n }\n try {\n migrateCommentsTable(client);\n } catch {\n execIgnore(client, \"PRAGMA foreign_keys = ON\");\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,wBAAwB,SAAoD;CAC1F,IAAI,CAAC,QAAQ,UAAU,QAAQ,SAAS,KAAA,GACtC,MAAM,IAAI,MAAM,qEAAqE;CAGvF,MAAM,QAAQ,CAAC,QAAQ;CACvB,MAAM,SAAS,QAAQ,UAAU,IAAI,SAAS,QAAQ,MAAM,kBAAkB,OAAO,CAAC;CACtF,IAAI,OAAO;EACT,OAAO,KAAK,2BAA2B;EACvC,OAAO,KAAK,4BAA4B;CAC1C;CACA,MAAM,KAAK,QAAQ,QAAQ,EAAE,OAAO,CAAC;CAErC,MAAM,UAAU,qBAAqB,IAAI;EACvC,UAAU;GACR,MAAM;GACN,SAAU,WAAuD,mBAAmB;GACpF,aAAa;GACb,MAAM;GACN,UAAU;EACZ;EACA,QAAQ;EACR,eAAe;GACb,cAAc,MAAM;EACtB;EACA,aAAa;GACX,IAAI,OACF,OAAO,MAAM;EAEjB;EACA,YAAY;GACV,OAAO,MAAM,UAAU,CAAC,CAAC,IAAI;EAC/B;CACF,CAAC;CAED,OAAO,uBAAuB,UAAU,QAAQ;EAC9C,OAAO,KAAK,GAAG;CACjB,CAAC;AACH;AAEA,MAAM,sBAAsB;;AAG5B,SAAS,kBAAkB,SAIzB;CACA,MAAM,SAAwE,CAAC;CAC/E,IAAI,QAAQ,aAAa,KAAA,GACvB,OAAO,WAAW,QAAQ;CAE5B,IAAI,QAAQ,WAAW,KAAA,GACrB,OAAO,SAAS,QAAQ;CAE1B,IAAI,QAAQ,cAAc,KAAA,GACxB,OAAO,YAAY,QAAQ;CAE7B,OAAO;AACT;AAEA,SAAS,WAAW,QAAkB,KAAmB;CACvD,IAAI;EACF,OAAO,KAAK,GAAG;CACjB,QAAQ,CAER;AACF;AAEA,SAAS,qBAAqB,QAAwB;CAMpD,IAAI,CALQ,OACT,MAAM,sEAAsE,CAAC,CAC7E,IAGI,CAAC,EAAE,IAAI,SAAS,wCAAwC,GAC7D;CAEF,OAAO,KAAK,2BAA2B;CACvC,OAAO,KAAK;;;;;;;;;;;;;GAaX;CACD,OAAO,KAAK,iDAAiD;CAC7D,OAAO,KAAK,qBAAqB;CACjC,OAAO,KAAK,6CAA6C;CACzD,OAAO,KAAK,yEAAyE;CACrF,OAAO,KAAK,0BAA0B;AACxC;;AAGA,SAAS,cAAc,QAAkB,KAAmB;CAC1D,KAAK,MAAM,CAAC,OAAO,YAAY,aAAa,GAAG,GAAG;EAChD,MAAM,OAAO,OAAO,MAAM,qBAAqB,MAAM,EAAE,CAAC,CAAC,IAAI;EAC7D,MAAM,WAAW,IAAI,IAAI,KAAK,KAAK,QAAQ,OAAO,IAAI,IAAI,CAAC,CAAC;EAC5D,KAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,CAAC,QAAQ,OAAO,MAAM,MAAM;GAClC,IAAI,QAAQ,CAAC,SAAS,IAAI,IAAI,GAC5B,WAAW,QAAQ,eAAe,MAAM,cAAc,QAAQ;EAElE;CACF;AACF;AAEA,SAAS,cAAc,QAAwB;CAC7C,OAAO,KAAK,0BAA0B;CACtC,OAAO,KAAK,GAAG;CACf,cAAc,QAAQ,GAAG;CACzB,IAAI;EACF,OAAO,KAAK,mBAAmB;CACjC,QAAQ,CAER;CACA,IAAI;EACF,qBAAqB,MAAM;CAC7B,QAAQ;EACN,WAAW,QAAQ,0BAA0B;CAC/C;AACF"}
1
+ {"version":3,"file":"bun-sqlite.mjs","names":[],"sources":["../src/bun-sqlite.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\n/**\n * Bun preset for the SQLite database adapter.\n *\n * Same schema, DDL, and Drizzle factory as the `node:sqlite` default;\n * only the driver differs (`bun:sqlite`, synchronous builtin). Bun-only:\n * importing this module outside Bun throws when resolving `bun:sqlite`.\n */\nimport { Database } from \"bun:sqlite\";\nimport { drizzle } from \"drizzle-orm/bun-sqlite\";\nimport { DDL, tableColumns } from \"./ddl.ts\";\nimport { createDrizzleAdapter, withManualTransactions } from \"./drizzle-factory.ts\";\nimport { ensureDbDir } from \"./ensure-db-dir.ts\";\nimport { schema } from \"./schema/index.ts\";\n\ndeclare const __PKG_VERSION__: string | undefined;\n\n/**\n * Options for creating a Bun-backed DatabaseAdapter.\n *\n * When `client` is supplied, `path` is ignored and the caller owns the\n * lifecycle of the client (teardown is a no-op). This is the hook for\n * shared connections and hermetic tests.\n */\nexport interface BunSqliteDatabaseOptions {\n path?: string;\n readonly?: boolean;\n create?: boolean;\n readwrite?: boolean;\n client?: Database;\n}\n\n/**\n * Create a Bun-backed DatabaseAdapter using Drizzle ORM.\n *\n * @param options - Either a filesystem `path` (use `\":memory:\"` for an\n * ephemeral database) or a preconfigured `bun:sqlite` `client`.\n * @returns A DatabaseAdapter backed by the SQLite file (WAL mode).\n */\nexport function createBunSqliteDatabase(options: BunSqliteDatabaseOptions): DatabaseAdapter {\n if (!options.client && options.path === undefined) {\n throw new Error(\"createBunSqliteDatabase: provide `path` or a preconfigured `client`\");\n }\n\n const owned = !options.client;\n const client = options.client ?? openDatabase(options);\n if (owned) {\n client.exec(\"PRAGMA journal_mode = WAL\");\n client.exec(\"PRAGMA busy_timeout = 5000\");\n }\n const db = drizzle(client, { schema });\n\n const adapter = createDrizzleAdapter(db, {\n metadata: {\n name: \"Bun SQLite\",\n version: (globalThis as unknown as { __PKG_VERSION__?: string }).__PKG_VERSION__ ?? \"0.0.0\",\n description: \"Bun SQLite database adapter (bun:sqlite + Drizzle)\",\n kind: \"bun-sqlite\",\n category: \"database\",\n },\n tables: schema,\n migrate: () => {\n runMigrations(client);\n },\n close: () => {\n if (owned) {\n client.close();\n }\n },\n ping: () => {\n client.query(\"SELECT 1\").get();\n },\n });\n\n return withManualTransactions(adapter, (sql) => {\n client.exec(sql);\n });\n}\n\nconst WEBHOOK_SECRET_DROP = \"ALTER TABLE webhooks DROP COLUMN secret\";\n\n/** Bun ignores `undefined` options, but only defined keys are passed. */\nfunction connectionOptions(options: BunSqliteDatabaseOptions): {\n readonly?: boolean;\n create?: boolean;\n readwrite?: boolean;\n} {\n const result: { readonly?: boolean; create?: boolean; readwrite?: boolean } = {};\n if (options.readonly !== undefined) {\n result.readonly = options.readonly;\n }\n if (options.create !== undefined) {\n result.create = options.create;\n }\n if (options.readwrite !== undefined) {\n result.readwrite = options.readwrite;\n }\n return result;\n}\n\nfunction execIgnore(client: Database, sql: string): void {\n try {\n client.exec(sql);\n } catch {\n // idempotent — column already exists or other no-op\n }\n}\n\nfunction migrateCommentsTable(client: Database): void {\n const row = client\n .query(\"SELECT sql FROM sqlite_master WHERE type='table' AND name='comments'\")\n .get() as {\n sql: string;\n } | null;\n if (!row?.sql.includes(\"user_id TEXT NOT NULL REFERENCES users\")) {\n return;\n }\n client.exec(\"PRAGMA foreign_keys = OFF\");\n client.exec(`\n CREATE TABLE comments_new (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n snapshot_id TEXT REFERENCES snapshots(id) ON DELETE CASCADE,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n body TEXT NOT NULL,\n parent_id TEXT,\n resolved INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n )\n `);\n client.exec(\"INSERT INTO comments_new SELECT * FROM comments\");\n client.exec(\"DROP TABLE comments\");\n client.exec(\"ALTER TABLE comments_new RENAME TO comments\");\n client.exec(\"CREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id)\");\n client.exec(\"PRAGMA foreign_keys = ON\");\n}\n\n/** Add any DDL column missing from a volume created before it shipped. */\nfunction ensureColumns(client: Database, ddl: string): void {\n for (const [table, columns] of tableColumns(ddl)) {\n const rows = client.query(`PRAGMA table_info(${table})`).all() as { name: unknown }[];\n const existing = new Set(rows.map((row) => String(row.name)));\n for (const column of columns) {\n const [name] = column.split(/\\s+/u);\n if (name && !existing.has(name)) {\n execIgnore(client, `ALTER TABLE ${table} ADD COLUMN ${column}`);\n }\n }\n }\n}\n\nfunction runMigrations(client: Database): void {\n client.exec(\"PRAGMA foreign_keys = ON\");\n client.exec(DDL);\n ensureColumns(client, DDL);\n try {\n client.exec(WEBHOOK_SECRET_DROP);\n } catch {\n // already migrated — ignore\n }\n try {\n migrateCommentsTable(client);\n } catch {\n execIgnore(client, \"PRAGMA foreign_keys = ON\");\n }\n}\n\n/** Open the file-backed database, creating its directory first. */\nfunction openDatabase(options: BunSqliteDatabaseOptions): InstanceType<typeof Database> {\n ensureDbDir(options.path ?? \"\");\n return new Database(options.path, connectionOptions(options));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,wBAAwB,SAAoD;CAC1F,IAAI,CAAC,QAAQ,UAAU,QAAQ,SAAS,KAAA,GACtC,MAAM,IAAI,MAAM,qEAAqE;CAGvF,MAAM,QAAQ,CAAC,QAAQ;CACvB,MAAM,SAAS,QAAQ,UAAU,aAAa,OAAO;CACrD,IAAI,OAAO;EACT,OAAO,KAAK,2BAA2B;EACvC,OAAO,KAAK,4BAA4B;CAC1C;CACA,MAAM,KAAK,QAAQ,QAAQ,EAAE,OAAO,CAAC;CAErC,MAAM,UAAU,qBAAqB,IAAI;EACvC,UAAU;GACR,MAAM;GACN,SAAU,WAAuD,mBAAmB;GACpF,aAAa;GACb,MAAM;GACN,UAAU;EACZ;EACA,QAAQ;EACR,eAAe;GACb,cAAc,MAAM;EACtB;EACA,aAAa;GACX,IAAI,OACF,OAAO,MAAM;EAEjB;EACA,YAAY;GACV,OAAO,MAAM,UAAU,CAAC,CAAC,IAAI;EAC/B;CACF,CAAC;CAED,OAAO,uBAAuB,UAAU,QAAQ;EAC9C,OAAO,KAAK,GAAG;CACjB,CAAC;AACH;AAEA,MAAM,sBAAsB;;AAG5B,SAAS,kBAAkB,SAIzB;CACA,MAAM,SAAwE,CAAC;CAC/E,IAAI,QAAQ,aAAa,KAAA,GACvB,OAAO,WAAW,QAAQ;CAE5B,IAAI,QAAQ,WAAW,KAAA,GACrB,OAAO,SAAS,QAAQ;CAE1B,IAAI,QAAQ,cAAc,KAAA,GACxB,OAAO,YAAY,QAAQ;CAE7B,OAAO;AACT;AAEA,SAAS,WAAW,QAAkB,KAAmB;CACvD,IAAI;EACF,OAAO,KAAK,GAAG;CACjB,QAAQ,CAER;AACF;AAEA,SAAS,qBAAqB,QAAwB;CAMpD,IAAI,CALQ,OACT,MAAM,sEAAsE,CAAC,CAC7E,IAGI,CAAC,EAAE,IAAI,SAAS,wCAAwC,GAC7D;CAEF,OAAO,KAAK,2BAA2B;CACvC,OAAO,KAAK;;;;;;;;;;;;;GAaX;CACD,OAAO,KAAK,iDAAiD;CAC7D,OAAO,KAAK,qBAAqB;CACjC,OAAO,KAAK,6CAA6C;CACzD,OAAO,KAAK,yEAAyE;CACrF,OAAO,KAAK,0BAA0B;AACxC;;AAGA,SAAS,cAAc,QAAkB,KAAmB;CAC1D,KAAK,MAAM,CAAC,OAAO,YAAY,aAAa,GAAG,GAAG;EAChD,MAAM,OAAO,OAAO,MAAM,qBAAqB,MAAM,EAAE,CAAC,CAAC,IAAI;EAC7D,MAAM,WAAW,IAAI,IAAI,KAAK,KAAK,QAAQ,OAAO,IAAI,IAAI,CAAC,CAAC;EAC5D,KAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,CAAC,QAAQ,OAAO,MAAM,MAAM;GAClC,IAAI,QAAQ,CAAC,SAAS,IAAI,IAAI,GAC5B,WAAW,QAAQ,eAAe,MAAM,cAAc,QAAQ;EAElE;CACF;AACF;AAEA,SAAS,cAAc,QAAwB;CAC7C,OAAO,KAAK,0BAA0B;CACtC,OAAO,KAAK,GAAG;CACf,cAAc,QAAQ,GAAG;CACzB,IAAI;EACF,OAAO,KAAK,mBAAmB;CACjC,QAAQ,CAER;CACA,IAAI;EACF,qBAAqB,MAAM;CAC7B,QAAQ;EACN,WAAW,QAAQ,0BAA0B;CAC/C;AACF;;AAGA,SAAS,aAAa,SAAkE;CACtF,YAAY,QAAQ,QAAQ,EAAE;CAC9B,OAAO,IAAI,SAAS,QAAQ,MAAM,kBAAkB,OAAO,CAAC;AAC9D"}
@@ -0,0 +1,19 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { mkdirSync } from "node:fs";
5
+ import { dirname } from "node:path";
6
+ //#region src/ensure-db-dir.ts
7
+ /**
8
+ * Create the parent directory of a database file so a fresh checkout or an
9
+ * empty volume boots without a manual `mkdir` (SQLite creates the file but
10
+ * never its directory). In-memory databases and `file:` URIs are left alone.
11
+ */
12
+ function ensureDbDir(path) {
13
+ if (path === "" || path === ":memory:" || path.startsWith("file:")) return;
14
+ mkdirSync(dirname(path), { recursive: true });
15
+ }
16
+ //#endregion
17
+ export { ensureDbDir };
18
+
19
+ //# sourceMappingURL=ensure-db-dir.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ensure-db-dir.mjs","names":[],"sources":["../src/ensure-db-dir.ts"],"sourcesContent":["import { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\n/**\n * Create the parent directory of a database file so a fresh checkout or an\n * empty volume boots without a manual `mkdir` (SQLite creates the file but\n * never its directory). In-memory databases and `file:` URIs are left alone.\n */\nexport function ensureDbDir(path: string): void {\n if (path === \"\" || path === \":memory:\" || path.startsWith(\"file:\")) {\n return;\n }\n mkdirSync(dirname(path), { recursive: true });\n}\n"],"mappings":";;;;;;;;;;;AAQA,SAAgB,YAAY,MAAoB;CAC9C,IAAI,SAAS,MAAM,SAAS,cAAc,KAAK,WAAW,OAAO,GAC/D;CAEF,UAAU,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9C"}
package/dist/index.mjs CHANGED
@@ -3,6 +3,7 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { DDL, tableColumns } from "./ddl.mjs";
5
5
  import { createDrizzleAdapter } from "./drizzle-factory.mjs";
6
+ import { ensureDbDir } from "./ensure-db-dir.mjs";
6
7
  import { schema } from "./schema.mjs";
7
8
  import { drizzle } from "drizzle-orm/sqlite-proxy";
8
9
  import { DatabaseSync } from "node:sqlite";
@@ -14,6 +15,7 @@ import { DatabaseSync } from "node:sqlite";
14
15
  * @returns A DatabaseAdapter backed by the given SQLite file (WAL mode).
15
16
  */
16
17
  function createSqliteDatabase(path) {
18
+ ensureDbDir(path);
17
19
  const sqlite = new DatabaseSync(path, { returnArrays: true });
18
20
  sqlite.exec("PRAGMA journal_mode = WAL");
19
21
  sqlite.exec("PRAGMA busy_timeout = 5000");
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport { drizzle, type AsyncRemoteCallback } from \"drizzle-orm/sqlite-proxy\";\nimport { DatabaseSync, type SQLInputValue } from \"node:sqlite\";\nimport { DDL, tableColumns } from \"./ddl.ts\";\nimport { createDrizzleAdapter } from \"./drizzle-factory.ts\";\nimport { schema } from \"./schema/index.ts\";\n\ndeclare const __PKG_VERSION__: string | undefined;\n\n/**\n * Create a SQLite-backed DatabaseAdapter using node:sqlite and Drizzle ORM.\n *\n * @param path - Filesystem path to the SQLite database file.\n * @returns A DatabaseAdapter backed by the given SQLite file (WAL mode).\n */\nexport function createSqliteDatabase(path: string): DatabaseAdapter {\n const sqlite = new DatabaseSync(path, { returnArrays: true });\n sqlite.exec(\"PRAGMA journal_mode = WAL\");\n sqlite.exec(\"PRAGMA busy_timeout = 5000\");\n const callback = createCallback(sqlite);\n const db = drizzle(callback, { schema });\n\n return createDrizzleAdapter(db, {\n metadata: {\n name: \"SQLite\",\n version: (globalThis as unknown as { __PKG_VERSION__?: string }).__PKG_VERSION__ ?? \"0.0.0\",\n description: \"SQLite database adapter (node:sqlite + Drizzle)\",\n kind: \"sqlite\",\n category: \"database\",\n },\n tables: schema,\n migrate: () => {\n runMigrations(sqlite);\n },\n close: () => {\n sqlite.close();\n },\n ping: () => {\n sqlite.prepare(\"SELECT 1\").get();\n },\n });\n}\n\nconst WEBHOOK_SECRET_DROP = \"ALTER TABLE webhooks DROP COLUMN secret\";\n\n/**\n * Re-runs share `(project_id, git_sha)` by design (ULID build ids), so the\n * lookup index must not be unique. Existing databases created it UNIQUE —\n * drop and recreate it plain.\n */\nconst BUILDS_GITSHA_DEDUP = \"DROP INDEX IF EXISTS builds_project_gitsha_idx\";\nconst BUILDS_GITSHA_INDEX =\n \"CREATE INDEX IF NOT EXISTS builds_project_gitsha_idx ON builds (project_id, git_sha)\";\n\ntype ProxyMethod = \"run\" | \"all\" | \"values\" | \"get\";\n\n/**\n * Execute one Drizzle statement against DatabaseSync, returning proxy-shaped rows.\n * Drizzle builds the SQL and params; only the transport is hand-written.\n * `values` shares the `all` shape (array rows); the factory never emits it.\n * Note: for `get`, the proxy passes `result.rows` straight to the row mapper,\n * so `rows` is the single row (or undefined on a miss) — not a one-row array.\n */\nfunction executeStatement(\n sqlite: DatabaseSync,\n sql: string,\n params: unknown[],\n method: ProxyMethod,\n): { rows: unknown } {\n const stmt = sqlite.prepare(sql);\n // Single contained cast: Drizzle's params are SQLite scalars by construction.\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- tsc requires the narrowing; the rule misfires on the variadic overloads\n const args = params as SQLInputValue[];\n if (method === \"run\") {\n stmt.run(...args);\n return { rows: [] };\n }\n if (method === \"get\") {\n return { rows: stmt.get(...args) };\n }\n return { rows: stmt.all(...args) };\n}\n\nfunction createCallback(sqlite: DatabaseSync): AsyncRemoteCallback {\n const callback = (async (sql: string, params: unknown[], method: ProxyMethod) => {\n await Promise.resolve();\n return executeStatement(sqlite, sql, params, method);\n }) as AsyncRemoteCallback;\n return callback;\n}\n\nfunction execIgnore(sqlite: DatabaseSync, sql: string): void {\n try {\n sqlite.exec(sql);\n } catch {\n // idempotent — column already exists or other no-op\n }\n}\n\nfunction migrateCommentsTable(sqlite: DatabaseSync): void {\n const row = sqlite\n .prepare(\"SELECT sql FROM sqlite_master WHERE type='table' AND name='comments'\")\n .get() as { sql: string } | undefined;\n if (!row?.sql.includes(\"user_id TEXT NOT NULL REFERENCES users\")) {\n return;\n }\n sqlite.exec(\"PRAGMA foreign_keys = OFF\");\n sqlite.exec(`\n CREATE TABLE comments_new (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n snapshot_id TEXT REFERENCES snapshots(id) ON DELETE CASCADE,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n body TEXT NOT NULL,\n parent_id TEXT,\n resolved INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n )\n `);\n sqlite.exec(\"INSERT INTO comments_new SELECT * FROM comments\");\n sqlite.exec(\"DROP TABLE comments\");\n sqlite.exec(\"ALTER TABLE comments_new RENAME TO comments\");\n sqlite.exec(\"CREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id)\");\n sqlite.exec(\"PRAGMA foreign_keys = ON\");\n}\n\n/** Add any DDL column missing from a volume created before it shipped. */\nfunction ensureColumns(sqlite: DatabaseSync, ddl: string): void {\n for (const [table, columns] of tableColumns(ddl)) {\n const rows = sqlite.prepare(`PRAGMA table_info(${table})`).all() as unknown as unknown[][];\n const existing = new Set(rows.map((row) => String(row[1])));\n for (const column of columns) {\n const [name] = column.split(/\\s+/u);\n if (name && !existing.has(name)) {\n execIgnore(sqlite, `ALTER TABLE ${table} ADD COLUMN ${column}`);\n }\n }\n }\n}\n\nfunction runMigrations(sqlite: DatabaseSync): void {\n sqlite.exec(\"PRAGMA foreign_keys = ON\");\n sqlite.exec(DDL);\n ensureColumns(sqlite, DDL);\n try {\n sqlite.exec(WEBHOOK_SECRET_DROP);\n } catch {\n // already migrated — ignore\n }\n migrateBuildsGitShaIndex(sqlite);\n try {\n migrateCommentsTable(sqlite);\n } catch {\n execIgnore(sqlite, \"PRAGMA foreign_keys = ON\");\n }\n}\n\n/** Downgrade a legacy UNIQUE project/sha index to a plain lookup index. */\nfunction migrateBuildsGitShaIndex(sqlite: DatabaseSync): void {\n try {\n sqlite.exec(BUILDS_GITSHA_DEDUP);\n sqlite.exec(BUILDS_GITSHA_INDEX);\n } catch {\n // already migrated — ignore\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAgB,qBAAqB,MAA+B;CAClE,MAAM,SAAS,IAAI,aAAa,MAAM,EAAE,cAAc,KAAK,CAAC;CAC5D,OAAO,KAAK,2BAA2B;CACvC,OAAO,KAAK,4BAA4B;CACxC,MAAM,WAAW,eAAe,MAAM;CACtC,MAAM,KAAK,QAAQ,UAAU,EAAE,OAAO,CAAC;CAEvC,OAAO,qBAAqB,IAAI;EAC9B,UAAU;GACR,MAAM;GACN,SAAU,WAAuD,mBAAmB;GACpF,aAAa;GACb,MAAM;GACN,UAAU;EACZ;EACA,QAAQ;EACR,eAAe;GACb,cAAc,MAAM;EACtB;EACA,aAAa;GACX,OAAO,MAAM;EACf;EACA,YAAY;GACV,OAAO,QAAQ,UAAU,CAAC,CAAC,IAAI;EACjC;CACF,CAAC;AACH;AAEA,MAAM,sBAAsB;;;;;;AAO5B,MAAM,sBAAsB;AAC5B,MAAM,sBACJ;;;;;;;;AAWF,SAAS,iBACP,QACA,KACA,QACA,QACmB;CACnB,MAAM,OAAO,OAAO,QAAQ,GAAG;CAG/B,MAAM,OAAO;CACb,IAAI,WAAW,OAAO;EACpB,KAAK,IAAI,GAAG,IAAI;EAChB,OAAO,EAAE,MAAM,CAAC,EAAE;CACpB;CACA,IAAI,WAAW,OACb,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,EAAE;CAEnC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,EAAE;AACnC;AAEA,SAAS,eAAe,QAA2C;CACjE,MAAM,YAAY,OAAO,KAAa,QAAmB,WAAwB;EAC/E,MAAM,QAAQ,QAAQ;EACtB,OAAO,iBAAiB,QAAQ,KAAK,QAAQ,MAAM;CACrD;CACA,OAAO;AACT;AAEA,SAAS,WAAW,QAAsB,KAAmB;CAC3D,IAAI;EACF,OAAO,KAAK,GAAG;CACjB,QAAQ,CAER;AACF;AAEA,SAAS,qBAAqB,QAA4B;CAIxD,IAAI,CAHQ,OACT,QAAQ,sEAAsE,CAAC,CAC/E,IACI,CAAC,EAAE,IAAI,SAAS,wCAAwC,GAC7D;CAEF,OAAO,KAAK,2BAA2B;CACvC,OAAO,KAAK;;;;;;;;;;;;;GAaX;CACD,OAAO,KAAK,iDAAiD;CAC7D,OAAO,KAAK,qBAAqB;CACjC,OAAO,KAAK,6CAA6C;CACzD,OAAO,KAAK,yEAAyE;CACrF,OAAO,KAAK,0BAA0B;AACxC;;AAGA,SAAS,cAAc,QAAsB,KAAmB;CAC9D,KAAK,MAAM,CAAC,OAAO,YAAY,aAAa,GAAG,GAAG;EAChD,MAAM,OAAO,OAAO,QAAQ,qBAAqB,MAAM,EAAE,CAAC,CAAC,IAAI;EAC/D,MAAM,WAAW,IAAI,IAAI,KAAK,KAAK,QAAQ,OAAO,IAAI,EAAE,CAAC,CAAC;EAC1D,KAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,CAAC,QAAQ,OAAO,MAAM,MAAM;GAClC,IAAI,QAAQ,CAAC,SAAS,IAAI,IAAI,GAC5B,WAAW,QAAQ,eAAe,MAAM,cAAc,QAAQ;EAElE;CACF;AACF;AAEA,SAAS,cAAc,QAA4B;CACjD,OAAO,KAAK,0BAA0B;CACtC,OAAO,KAAK,GAAG;CACf,cAAc,QAAQ,GAAG;CACzB,IAAI;EACF,OAAO,KAAK,mBAAmB;CACjC,QAAQ,CAER;CACA,yBAAyB,MAAM;CAC/B,IAAI;EACF,qBAAqB,MAAM;CAC7B,QAAQ;EACN,WAAW,QAAQ,0BAA0B;CAC/C;AACF;;AAGA,SAAS,yBAAyB,QAA4B;CAC5D,IAAI;EACF,OAAO,KAAK,mBAAmB;EAC/B,OAAO,KAAK,mBAAmB;CACjC,QAAQ,CAER;AACF"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport { drizzle, type AsyncRemoteCallback } from \"drizzle-orm/sqlite-proxy\";\nimport { DatabaseSync, type SQLInputValue } from \"node:sqlite\";\nimport { DDL, tableColumns } from \"./ddl.ts\";\nimport { createDrizzleAdapter } from \"./drizzle-factory.ts\";\nimport { ensureDbDir } from \"./ensure-db-dir.ts\";\nimport { schema } from \"./schema/index.ts\";\n\ndeclare const __PKG_VERSION__: string | undefined;\n\n/**\n * Create a SQLite-backed DatabaseAdapter using node:sqlite and Drizzle ORM.\n *\n * @param path - Filesystem path to the SQLite database file.\n * @returns A DatabaseAdapter backed by the given SQLite file (WAL mode).\n */\nexport function createSqliteDatabase(path: string): DatabaseAdapter {\n ensureDbDir(path);\n const sqlite = new DatabaseSync(path, { returnArrays: true });\n sqlite.exec(\"PRAGMA journal_mode = WAL\");\n sqlite.exec(\"PRAGMA busy_timeout = 5000\");\n const callback = createCallback(sqlite);\n const db = drizzle(callback, { schema });\n\n return createDrizzleAdapter(db, {\n metadata: {\n name: \"SQLite\",\n version: (globalThis as unknown as { __PKG_VERSION__?: string }).__PKG_VERSION__ ?? \"0.0.0\",\n description: \"SQLite database adapter (node:sqlite + Drizzle)\",\n kind: \"sqlite\",\n category: \"database\",\n },\n tables: schema,\n migrate: () => {\n runMigrations(sqlite);\n },\n close: () => {\n sqlite.close();\n },\n ping: () => {\n sqlite.prepare(\"SELECT 1\").get();\n },\n });\n}\n\nconst WEBHOOK_SECRET_DROP = \"ALTER TABLE webhooks DROP COLUMN secret\";\n\n/**\n * Re-runs share `(project_id, git_sha)` by design (ULID build ids), so the\n * lookup index must not be unique. Existing databases created it UNIQUE —\n * drop and recreate it plain.\n */\nconst BUILDS_GITSHA_DEDUP = \"DROP INDEX IF EXISTS builds_project_gitsha_idx\";\nconst BUILDS_GITSHA_INDEX =\n \"CREATE INDEX IF NOT EXISTS builds_project_gitsha_idx ON builds (project_id, git_sha)\";\n\ntype ProxyMethod = \"run\" | \"all\" | \"values\" | \"get\";\n\n/**\n * Execute one Drizzle statement against DatabaseSync, returning proxy-shaped rows.\n * Drizzle builds the SQL and params; only the transport is hand-written.\n * `values` shares the `all` shape (array rows); the factory never emits it.\n * Note: for `get`, the proxy passes `result.rows` straight to the row mapper,\n * so `rows` is the single row (or undefined on a miss) — not a one-row array.\n */\nfunction executeStatement(\n sqlite: DatabaseSync,\n sql: string,\n params: unknown[],\n method: ProxyMethod,\n): { rows: unknown } {\n const stmt = sqlite.prepare(sql);\n // Single contained cast: Drizzle's params are SQLite scalars by construction.\n // oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- tsc requires the narrowing; the rule misfires on the variadic overloads\n const args = params as SQLInputValue[];\n if (method === \"run\") {\n stmt.run(...args);\n return { rows: [] };\n }\n if (method === \"get\") {\n return { rows: stmt.get(...args) };\n }\n return { rows: stmt.all(...args) };\n}\n\nfunction createCallback(sqlite: DatabaseSync): AsyncRemoteCallback {\n const callback = (async (sql: string, params: unknown[], method: ProxyMethod) => {\n await Promise.resolve();\n return executeStatement(sqlite, sql, params, method);\n }) as AsyncRemoteCallback;\n return callback;\n}\n\nfunction execIgnore(sqlite: DatabaseSync, sql: string): void {\n try {\n sqlite.exec(sql);\n } catch {\n // idempotent — column already exists or other no-op\n }\n}\n\nfunction migrateCommentsTable(sqlite: DatabaseSync): void {\n const row = sqlite\n .prepare(\"SELECT sql FROM sqlite_master WHERE type='table' AND name='comments'\")\n .get() as { sql: string } | undefined;\n if (!row?.sql.includes(\"user_id TEXT NOT NULL REFERENCES users\")) {\n return;\n }\n sqlite.exec(\"PRAGMA foreign_keys = OFF\");\n sqlite.exec(`\n CREATE TABLE comments_new (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n snapshot_id TEXT REFERENCES snapshots(id) ON DELETE CASCADE,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n body TEXT NOT NULL,\n parent_id TEXT,\n resolved INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n )\n `);\n sqlite.exec(\"INSERT INTO comments_new SELECT * FROM comments\");\n sqlite.exec(\"DROP TABLE comments\");\n sqlite.exec(\"ALTER TABLE comments_new RENAME TO comments\");\n sqlite.exec(\"CREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id)\");\n sqlite.exec(\"PRAGMA foreign_keys = ON\");\n}\n\n/** Add any DDL column missing from a volume created before it shipped. */\nfunction ensureColumns(sqlite: DatabaseSync, ddl: string): void {\n for (const [table, columns] of tableColumns(ddl)) {\n const rows = sqlite.prepare(`PRAGMA table_info(${table})`).all() as unknown as unknown[][];\n const existing = new Set(rows.map((row) => String(row[1])));\n for (const column of columns) {\n const [name] = column.split(/\\s+/u);\n if (name && !existing.has(name)) {\n execIgnore(sqlite, `ALTER TABLE ${table} ADD COLUMN ${column}`);\n }\n }\n }\n}\n\nfunction runMigrations(sqlite: DatabaseSync): void {\n sqlite.exec(\"PRAGMA foreign_keys = ON\");\n sqlite.exec(DDL);\n ensureColumns(sqlite, DDL);\n try {\n sqlite.exec(WEBHOOK_SECRET_DROP);\n } catch {\n // already migrated — ignore\n }\n migrateBuildsGitShaIndex(sqlite);\n try {\n migrateCommentsTable(sqlite);\n } catch {\n execIgnore(sqlite, \"PRAGMA foreign_keys = ON\");\n }\n}\n\n/** Downgrade a legacy UNIQUE project/sha index to a plain lookup index. */\nfunction migrateBuildsGitShaIndex(sqlite: DatabaseSync): void {\n try {\n sqlite.exec(BUILDS_GITSHA_DEDUP);\n sqlite.exec(BUILDS_GITSHA_INDEX);\n } catch {\n // already migrated — ignore\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,SAAgB,qBAAqB,MAA+B;CAClE,YAAY,IAAI;CAChB,MAAM,SAAS,IAAI,aAAa,MAAM,EAAE,cAAc,KAAK,CAAC;CAC5D,OAAO,KAAK,2BAA2B;CACvC,OAAO,KAAK,4BAA4B;CACxC,MAAM,WAAW,eAAe,MAAM;CACtC,MAAM,KAAK,QAAQ,UAAU,EAAE,OAAO,CAAC;CAEvC,OAAO,qBAAqB,IAAI;EAC9B,UAAU;GACR,MAAM;GACN,SAAU,WAAuD,mBAAmB;GACpF,aAAa;GACb,MAAM;GACN,UAAU;EACZ;EACA,QAAQ;EACR,eAAe;GACb,cAAc,MAAM;EACtB;EACA,aAAa;GACX,OAAO,MAAM;EACf;EACA,YAAY;GACV,OAAO,QAAQ,UAAU,CAAC,CAAC,IAAI;EACjC;CACF,CAAC;AACH;AAEA,MAAM,sBAAsB;;;;;;AAO5B,MAAM,sBAAsB;AAC5B,MAAM,sBACJ;;;;;;;;AAWF,SAAS,iBACP,QACA,KACA,QACA,QACmB;CACnB,MAAM,OAAO,OAAO,QAAQ,GAAG;CAG/B,MAAM,OAAO;CACb,IAAI,WAAW,OAAO;EACpB,KAAK,IAAI,GAAG,IAAI;EAChB,OAAO,EAAE,MAAM,CAAC,EAAE;CACpB;CACA,IAAI,WAAW,OACb,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,EAAE;CAEnC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,EAAE;AACnC;AAEA,SAAS,eAAe,QAA2C;CACjE,MAAM,YAAY,OAAO,KAAa,QAAmB,WAAwB;EAC/E,MAAM,QAAQ,QAAQ;EACtB,OAAO,iBAAiB,QAAQ,KAAK,QAAQ,MAAM;CACrD;CACA,OAAO;AACT;AAEA,SAAS,WAAW,QAAsB,KAAmB;CAC3D,IAAI;EACF,OAAO,KAAK,GAAG;CACjB,QAAQ,CAER;AACF;AAEA,SAAS,qBAAqB,QAA4B;CAIxD,IAAI,CAHQ,OACT,QAAQ,sEAAsE,CAAC,CAC/E,IACI,CAAC,EAAE,IAAI,SAAS,wCAAwC,GAC7D;CAEF,OAAO,KAAK,2BAA2B;CACvC,OAAO,KAAK;;;;;;;;;;;;;GAaX;CACD,OAAO,KAAK,iDAAiD;CAC7D,OAAO,KAAK,qBAAqB;CACjC,OAAO,KAAK,6CAA6C;CACzD,OAAO,KAAK,yEAAyE;CACrF,OAAO,KAAK,0BAA0B;AACxC;;AAGA,SAAS,cAAc,QAAsB,KAAmB;CAC9D,KAAK,MAAM,CAAC,OAAO,YAAY,aAAa,GAAG,GAAG;EAChD,MAAM,OAAO,OAAO,QAAQ,qBAAqB,MAAM,EAAE,CAAC,CAAC,IAAI;EAC/D,MAAM,WAAW,IAAI,IAAI,KAAK,KAAK,QAAQ,OAAO,IAAI,EAAE,CAAC,CAAC;EAC1D,KAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,CAAC,QAAQ,OAAO,MAAM,MAAM;GAClC,IAAI,QAAQ,CAAC,SAAS,IAAI,IAAI,GAC5B,WAAW,QAAQ,eAAe,MAAM,cAAc,QAAQ;EAElE;CACF;AACF;AAEA,SAAS,cAAc,QAA4B;CACjD,OAAO,KAAK,0BAA0B;CACtC,OAAO,KAAK,GAAG;CACf,cAAc,QAAQ,GAAG;CACzB,IAAI;EACF,OAAO,KAAK,mBAAmB;CACjC,QAAQ,CAER;CACA,yBAAyB,MAAM;CAC/B,IAAI;EACF,qBAAqB,MAAM;CAC7B,QAAQ;EACN,WAAW,QAAQ,0BAA0B;CAC/C;AACF;;AAGA,SAAS,yBAAyB,QAA4B;CAC5D,IAAI;EACF,OAAO,KAAK,mBAAmB;EAC/B,OAAO,KAAK,mBAAmB;CACjC,QAAQ,CAER;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storyshelf/db-sqlite",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "SQLite database adapter for StoryShelf (node:sqlite + Drizzle).",
5
5
  "keywords": [
6
6
  "database",
@@ -27,6 +27,9 @@
27
27
  "dist"
28
28
  ],
29
29
  "type": "module",
30
+ "engines": {
31
+ "node": ">=24"
32
+ },
30
33
  "sideEffects": false,
31
34
  "types": "./dist/index.d.mts",
32
35
  "exports": {
@@ -87,7 +90,7 @@
87
90
  "prepublishOnly": "nub run build"
88
91
  },
89
92
  "dependencies": {
90
- "@storyshelf/core": "0.6.1",
93
+ "@storyshelf/core": "0.6.2",
91
94
  "drizzle-orm": "^0.44.0"
92
95
  },
93
96
  "devDependencies": {