@storyshelf/db-sqlite 0.9.0-next-48e06c3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/better-sqlite3.mjs +3 -1
- package/dist/better-sqlite3.mjs.map +1 -1
- package/dist/bun-sqlite.mjs +3 -1
- package/dist/bun-sqlite.mjs.map +1 -1
- package/dist/capture-job-backfill.mjs +19 -0
- package/dist/capture-job-backfill.mjs.map +1 -0
- package/dist/d1.mjs +3 -1
- package/dist/d1.mjs.map +1 -1
- package/dist/ddl.d.mts +1 -1
- package/dist/ddl.mjs +1 -0
- package/dist/ddl.mjs.map +1 -1
- package/dist/index.mjs +3 -1
- package/dist/index.mjs.map +1 -1
- package/dist/schema/build.d.mts +20 -0
- package/dist/schema/build.mjs +1 -0
- package/dist/schema/build.mjs.map +1 -1
- package/dist/schema/capture-job.d.mts +2 -2
- package/dist/turso.mjs +3 -1
- package/dist/turso.mjs.map +1 -1
- package/package.json +2 -2
package/dist/better-sqlite3.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
+
import { CAPTURE_JOB_BACKFILL } from "./capture-job-backfill.mjs";
|
|
4
5
|
import { DDL, IDENTITY_STATEMENTS, tableColumns } from "./ddl.mjs";
|
|
5
6
|
import { createDrizzleAdapter, withManualTransactions } from "./drizzle-factory.mjs";
|
|
6
7
|
import { ensureDbDir } from "./ensure-db-dir.mjs";
|
|
@@ -34,7 +35,7 @@ function createBetterSqlite3Database(options) {
|
|
|
34
35
|
const adapter = createDrizzleAdapter(db, {
|
|
35
36
|
metadata: {
|
|
36
37
|
name: "better-sqlite3",
|
|
37
|
-
version: "0.9.0
|
|
38
|
+
version: "0.9.0",
|
|
38
39
|
description: "better-sqlite3 database adapter (native + Drizzle)",
|
|
39
40
|
kind: "better-sqlite3",
|
|
40
41
|
category: "database"
|
|
@@ -107,6 +108,7 @@ function runMigrations(client) {
|
|
|
107
108
|
client.exec(DDL);
|
|
108
109
|
ensureColumns(client, DDL);
|
|
109
110
|
applyIdentityStatements(client);
|
|
111
|
+
client.exec(CAPTURE_JOB_BACKFILL);
|
|
110
112
|
try {
|
|
111
113
|
client.exec(WEBHOOK_SECRET_DROP);
|
|
112
114
|
} catch {}
|
|
@@ -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, IDENTITY_STATEMENTS, 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: typeof __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\n/** Swap in the browser-aware unique indexes (after `ensureColumns` added the column). */\nfunction applyIdentityStatements(client: BetterSqlite3Client): void {\n for (const statement of IDENTITY_STATEMENTS) {\n execIgnore(client, statement);\n }\n}\n\nfunction runMigrations(client: BetterSqlite3Client): void {\n client.exec(\"PRAGMA foreign_keys = ON\");\n client.exec(DDL);\n ensureColumns(client, DDL);\n applyIdentityStatements(client);\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":"
|
|
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 { CAPTURE_JOB_BACKFILL } from \"./capture-job-backfill.ts\";\nimport { DDL, IDENTITY_STATEMENTS, 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: typeof __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\n/** Swap in the browser-aware unique indexes (after `ensureColumns` added the column). */\nfunction applyIdentityStatements(client: BetterSqlite3Client): void {\n for (const statement of IDENTITY_STATEMENTS) {\n execIgnore(client, statement);\n }\n}\n\nfunction runMigrations(client: BetterSqlite3Client): void {\n client.exec(\"PRAGMA foreign_keys = ON\");\n client.exec(DDL);\n ensureColumns(client, DDL);\n applyIdentityStatements(client);\n client.exec(CAPTURE_JOB_BACKFILL); // not ignored: see its doc\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":";;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,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,SAAA;GACA,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;;AAGA,SAAS,wBAAwB,QAAmC;CAClE,KAAK,MAAM,aAAa,qBACtB,WAAW,QAAQ,SAAS;AAEhC;AAEA,SAAS,cAAc,QAAmC;CACxD,OAAO,KAAK,0BAA0B;CACtC,OAAO,KAAK,GAAG;CACf,cAAc,QAAQ,GAAG;CACzB,wBAAwB,MAAM;CAC9B,OAAO,KAAK,oBAAoB;CAChC,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"}
|
package/dist/bun-sqlite.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
+
import { CAPTURE_JOB_BACKFILL } from "./capture-job-backfill.mjs";
|
|
4
5
|
import { DDL, IDENTITY_STATEMENTS, tableColumns } from "./ddl.mjs";
|
|
5
6
|
import { createDrizzleAdapter, withManualTransactions } from "./drizzle-factory.mjs";
|
|
6
7
|
import { ensureDbDir } from "./ensure-db-dir.mjs";
|
|
@@ -34,7 +35,7 @@ function createBunSqliteDatabase(options) {
|
|
|
34
35
|
const adapter = createDrizzleAdapter(db, {
|
|
35
36
|
metadata: {
|
|
36
37
|
name: "Bun SQLite",
|
|
37
|
-
version: "0.9.0
|
|
38
|
+
version: "0.9.0",
|
|
38
39
|
description: "Bun SQLite database adapter (bun:sqlite + Drizzle)",
|
|
39
40
|
kind: "bun-sqlite",
|
|
40
41
|
category: "database"
|
|
@@ -111,6 +112,7 @@ function runMigrations(client) {
|
|
|
111
112
|
client.exec(DDL);
|
|
112
113
|
ensureColumns(client, DDL);
|
|
113
114
|
applyIdentityStatements(client);
|
|
115
|
+
client.exec(CAPTURE_JOB_BACKFILL);
|
|
114
116
|
try {
|
|
115
117
|
client.exec(WEBHOOK_SECRET_DROP);
|
|
116
118
|
} catch {}
|
package/dist/bun-sqlite.mjs.map
CHANGED
|
@@ -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, IDENTITY_STATEMENTS, 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: typeof __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\n/** Swap in the browser-aware unique indexes (after `ensureColumns` added the column). */\nfunction applyIdentityStatements(client: Database): void {\n for (const statement of IDENTITY_STATEMENTS) {\n execIgnore(client, statement);\n }\n}\n\nfunction runMigrations(client: Database): void {\n client.exec(\"PRAGMA foreign_keys = ON\");\n client.exec(DDL);\n ensureColumns(client, DDL);\n applyIdentityStatements(client);\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":"
|
|
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 { CAPTURE_JOB_BACKFILL } from \"./capture-job-backfill.ts\";\nimport { DDL, IDENTITY_STATEMENTS, 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: typeof __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\n/** Swap in the browser-aware unique indexes (after `ensureColumns` added the column). */\nfunction applyIdentityStatements(client: Database): void {\n for (const statement of IDENTITY_STATEMENTS) {\n execIgnore(client, statement);\n }\n}\n\nfunction runMigrations(client: Database): void {\n client.exec(\"PRAGMA foreign_keys = ON\");\n client.exec(DDL);\n ensureColumns(client, DDL);\n applyIdentityStatements(client);\n client.exec(CAPTURE_JOB_BACKFILL); // not ignored: see its doc\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":";;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,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,SAAA;GACA,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;;AAGA,SAAS,wBAAwB,QAAwB;CACvD,KAAK,MAAM,aAAa,qBACtB,WAAW,QAAQ,SAAS;AAEhC;AAEA,SAAS,cAAc,QAAwB;CAC7C,OAAO,KAAK,0BAA0B;CACtC,OAAO,KAAK,GAAG;CACf,cAAc,QAAQ,GAAG;CACzB,wBAAwB,MAAM;CAC9B,OAAO,KAAK,oBAAoB;CAChC,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
|
+
// src/capture-job-backfill.ts
|
|
5
|
+
/**
|
|
6
|
+
* A run in flight when `builds.capture_job_id` was added owns the build it was started for. Run by
|
|
7
|
+
* every SQLite migration after the columns exist and, unlike the `IDENTITY_STATEMENTS` of `ddl.ts`,
|
|
8
|
+
* never ignored: a failure leaves in-flight builds unowned, so setup must stop. Internal: it is not
|
|
9
|
+
* part of the published `@storyshelf/db-sqlite/ddl` surface.
|
|
10
|
+
*
|
|
11
|
+
* A build gets an owner only when exactly one eligible job (unfinished, non-cancelled, not a shard)
|
|
12
|
+
* belongs to it. With several, "newest" cannot be told apart reliably (each instance writes
|
|
13
|
+
* `created_at` from its own clock, and ULIDs made in one millisecond are not ordered), so the build
|
|
14
|
+
* stays unowned and the exhausted-job hook never fails it.
|
|
15
|
+
*/
|
|
16
|
+
const CAPTURE_JOB_BACKFILL = "UPDATE builds SET capture_job_id = (SELECT j.job_id FROM capture_jobs j WHERE j.entry = 'job' AND j.kind = 'capture' AND j.shard_id IS NULL AND j.cancel_requested_at IS NULL AND j.status IN ('queued', 'running') AND j.build_id = builds.id LIMIT 1) WHERE capture_job_id IS NULL AND status IN ('pending', 'capturing') AND (SELECT COUNT(*) FROM capture_jobs j WHERE j.entry = 'job' AND j.kind = 'capture' AND j.shard_id IS NULL AND j.cancel_requested_at IS NULL AND j.status IN ('queued', 'running') AND j.build_id = builds.id) = 1";
|
|
17
|
+
export { CAPTURE_JOB_BACKFILL };
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=capture-job-backfill.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture-job-backfill.mjs","names":[],"sources":["../src/capture-job-backfill.ts"],"sourcesContent":["/**\n * A run in flight when `builds.capture_job_id` was added owns the build it was started for. Run by\n * every SQLite migration after the columns exist and, unlike the `IDENTITY_STATEMENTS` of `ddl.ts`,\n * never ignored: a failure leaves in-flight builds unowned, so setup must stop. Internal: it is not\n * part of the published `@storyshelf/db-sqlite/ddl` surface.\n *\n * A build gets an owner only when exactly one eligible job (unfinished, non-cancelled, not a shard)\n * belongs to it. With several, \"newest\" cannot be told apart reliably (each instance writes\n * `created_at` from its own clock, and ULIDs made in one millisecond are not ordered), so the build\n * stays unowned and the exhausted-job hook never fails it.\n */\nexport const CAPTURE_JOB_BACKFILL =\n \"UPDATE builds SET capture_job_id = (SELECT j.job_id FROM capture_jobs j WHERE j.entry = 'job' AND j.kind = 'capture' AND j.shard_id IS NULL AND j.cancel_requested_at IS NULL AND j.status IN ('queued', 'running') AND j.build_id = builds.id LIMIT 1) WHERE capture_job_id IS NULL AND status IN ('pending', 'capturing') AND (SELECT COUNT(*) FROM capture_jobs j WHERE j.entry = 'job' AND j.kind = 'capture' AND j.shard_id IS NULL AND j.cancel_requested_at IS NULL AND j.status IN ('queued', 'running') AND j.build_id = builds.id) = 1\";\n"],"mappings":";;;;;;;;;;;;;;;AAWA,MAAa,uBACX"}
|
package/dist/d1.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
+
import { CAPTURE_JOB_BACKFILL } from "./capture-job-backfill.mjs";
|
|
4
5
|
import { DDL, IDENTITY_STATEMENTS, tableColumns } from "./ddl.mjs";
|
|
5
6
|
import { createDrizzleAdapter } from "./drizzle-factory.mjs";
|
|
6
7
|
import { schema } from "./schema.mjs";
|
|
@@ -19,7 +20,7 @@ function createD1Database(options) {
|
|
|
19
20
|
return createDrizzleAdapter(db, {
|
|
20
21
|
metadata: {
|
|
21
22
|
name: "Cloudflare D1",
|
|
22
|
-
version: "0.9.0
|
|
23
|
+
version: "0.9.0",
|
|
23
24
|
description: "Cloudflare D1 database adapter (D1 binding + Drizzle)",
|
|
24
25
|
kind: "d1",
|
|
25
26
|
category: "database",
|
|
@@ -40,6 +41,7 @@ async function runMigrations(client) {
|
|
|
40
41
|
for (const statement of splitStatements(DDL)) await execIgnore(client, statement);
|
|
41
42
|
await ensureColumns(client, DDL);
|
|
42
43
|
for (const statement of IDENTITY_STATEMENTS) await execIgnore(client, statement);
|
|
44
|
+
await client.exec(CAPTURE_JOB_BACKFILL);
|
|
43
45
|
await execIgnore(client, WEBHOOK_SECRET_DROP);
|
|
44
46
|
await migrateCommentsTable(client);
|
|
45
47
|
}
|
package/dist/d1.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"d1.mjs","names":[],"sources":["../src/d1.ts"],"sourcesContent":["/**\n * Cloudflare D1 preset for the SQLite database adapter.\n *\n * Same schema, DDL, and Drizzle factory as the `node:sqlite` default;\n * only the driver differs (D1 HTTP binding, async). No peer dependency:\n * the `D1Database` binding is provided by the Workers runtime and its\n * minimal surface is declared locally (no `@cloudflare/workers-types`\n * needed).\n */\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport { drizzle } from \"drizzle-orm/d1\";\nimport { DDL, IDENTITY_STATEMENTS, 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/** Minimal prepared-statement surface of a D1 database binding. */\nexport interface D1PreparedStatement {\n bind(...params: unknown[]): D1PreparedStatement;\n all(): Promise<{ results: unknown[] }>;\n run(): Promise<unknown>;\n first(): Promise<unknown>;\n raw(): Promise<unknown[]>;\n}\n\n/** Minimal surface of a Cloudflare D1 database binding. */\nexport interface D1Database {\n prepare(query: string): D1PreparedStatement;\n batch(statements: D1PreparedStatement[]): Promise<unknown[]>;\n exec(query: string): Promise<unknown>;\n}\n\n/**\n * Options for creating a D1-backed DatabaseAdapter. Client-only: the\n * binding comes from the Workers runtime (`env.DB`), so the caller always\n * owns its lifecycle (teardown is a no-op).\n */\nexport interface D1DatabaseOptions {\n client: D1Database;\n}\n\n/**\n * Create a Cloudflare-D1-backed DatabaseAdapter using Drizzle ORM.\n *\n * @param options - The D1 database binding (`env.DB`).\n * @returns A DatabaseAdapter backed by the D1 database.\n */\nexport function createD1Database(options: D1DatabaseOptions): DatabaseAdapter {\n if (!options.client) {\n throw new Error(\"createD1Database: provide a D1 `client` binding (env.DB)\");\n }\n const { client } = options;\n // oxlint-disable-next-line typescript/no-unsafe-argument -- structural D1 binding vs platform type\n const db = drizzle(client as never, { schema });\n\n return createDrizzleAdapter(db, {\n metadata: {\n name: \"Cloudflare D1\",\n version: typeof __PKG_VERSION__ === \"string\" ? __PKG_VERSION__ : \"0.0.0\",\n description: \"Cloudflare D1 database adapter (D1 binding + Drizzle)\",\n kind: \"d1\",\n category: \"database\",\n // A Workers-only binding: no long-running process to consume capture jobs (ADR 0006, decision 3).\n supportsTesting: false,\n },\n tables: schema,\n migrate: async () => {\n await runMigrations(client);\n },\n close: () => {\n // no-op: the Workers runtime owns the binding\n },\n ping: async () => {\n await client.prepare(\"SELECT 1\").bind().first();\n },\n });\n}\n\nconst WEBHOOK_SECRET_DROP = \"ALTER TABLE webhooks DROP COLUMN secret\";\n\nasync function runMigrations(client: D1Database): Promise<void> {\n // Sequential: DDL order matters (tables before indexes) and D1 has no\n // multi-statement exec.\n for (const statement of splitStatements(DDL)) {\n // oxlint-disable-next-line no-await-in-loop -- ordered DDL, see above\n await execIgnore(client, statement);\n }\n await ensureColumns(client, DDL);\n for (const statement of IDENTITY_STATEMENTS) {\n // oxlint-disable-next-line no-await-in-loop -- ordered: drop old indexes, then create new\n await execIgnore(client, statement);\n }\n await execIgnore(client, WEBHOOK_SECRET_DROP);\n await migrateCommentsTable(client);\n}\n\n/** Split a multi-statement DDL script into single statements for `exec`. */\nfunction splitStatements(ddl: string): string[] {\n return ddl\n .split(/;\\n/u)\n .map((statement) => statement.trim())\n .filter((statement) => statement !== \"\");\n}\n\nasync function execIgnore(client: D1Database, sql: string): Promise<void> {\n try {\n await client.exec(sql);\n } catch {\n // idempotent — already migrated\n }\n}\n\n/** Add any DDL column missing from a database created before it shipped. */\nasync function ensureColumns(client: D1Database, ddl: string): Promise<void> {\n // Sequential: one PRAGMA round-trip per table keeps the per-table round-trips ordered.\n for (const [table, columns] of tableColumns(ddl)) {\n // oxlint-disable-next-line no-await-in-loop -- ordered per-table inspection\n const existing = await inspectColumns(client, table);\n for (const column of columns) {\n const [name] = column.split(/\\s+/u);\n if (name && (existing === undefined || !existing.has(name))) {\n // oxlint-disable-next-line no-await-in-loop -- ordered column backfill\n await execIgnore(client, `ALTER TABLE ${table} ADD COLUMN ${column}`);\n }\n }\n }\n}\n\n/**\n * Read a table's columns via `PRAGMA table_info`. Returns `undefined` when\n * the binding cannot serve PRAGMA (callers then add columns blindly).\n */\nasync function inspectColumns(client: D1Database, table: string): Promise<Set<string> | undefined> {\n try {\n const result = await client.prepare(`PRAGMA table_info(${table})`).bind().all();\n const rows = result.results as { name: unknown }[];\n return new Set(rows.map((row) => String(row.name)));\n } catch {\n return undefined;\n }\n}\n\nasync function migrateCommentsTable(client: D1Database): Promise<void> {\n try {\n const row = (await client\n .prepare(\"SELECT sql FROM sqlite_master WHERE type='table' AND name='comments'\")\n .bind()\n .first()) as { sql: string } | null;\n if (!row?.sql.includes(\"user_id TEXT NOT NULL REFERENCES users\")) {\n return;\n }\n await recreateCommentsTable(client);\n } catch {\n await execIgnore(client, \"PRAGMA foreign_keys = ON\");\n }\n}\n\nasync function recreateCommentsTable(client: D1Database): Promise<void> {\n await client.exec(\"PRAGMA foreign_keys = OFF\");\n await 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 await client.exec(\"INSERT INTO comments_new SELECT * FROM comments\");\n await client.exec(\"DROP TABLE comments\");\n await client.exec(\"ALTER TABLE comments_new RENAME TO comments\");\n await client.exec(\"CREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id)\");\n await client.exec(\"PRAGMA foreign_keys = ON\");\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"d1.mjs","names":[],"sources":["../src/d1.ts"],"sourcesContent":["/**\n * Cloudflare D1 preset for the SQLite database adapter.\n *\n * Same schema, DDL, and Drizzle factory as the `node:sqlite` default;\n * only the driver differs (D1 HTTP binding, async). No peer dependency:\n * the `D1Database` binding is provided by the Workers runtime and its\n * minimal surface is declared locally (no `@cloudflare/workers-types`\n * needed).\n */\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport { drizzle } from \"drizzle-orm/d1\";\nimport { CAPTURE_JOB_BACKFILL } from \"./capture-job-backfill.ts\";\nimport { DDL, IDENTITY_STATEMENTS, 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/** Minimal prepared-statement surface of a D1 database binding. */\nexport interface D1PreparedStatement {\n bind(...params: unknown[]): D1PreparedStatement;\n all(): Promise<{ results: unknown[] }>;\n run(): Promise<unknown>;\n first(): Promise<unknown>;\n raw(): Promise<unknown[]>;\n}\n\n/** Minimal surface of a Cloudflare D1 database binding. */\nexport interface D1Database {\n prepare(query: string): D1PreparedStatement;\n batch(statements: D1PreparedStatement[]): Promise<unknown[]>;\n exec(query: string): Promise<unknown>;\n}\n\n/**\n * Options for creating a D1-backed DatabaseAdapter. Client-only: the\n * binding comes from the Workers runtime (`env.DB`), so the caller always\n * owns its lifecycle (teardown is a no-op).\n */\nexport interface D1DatabaseOptions {\n client: D1Database;\n}\n\n/**\n * Create a Cloudflare-D1-backed DatabaseAdapter using Drizzle ORM.\n *\n * @param options - The D1 database binding (`env.DB`).\n * @returns A DatabaseAdapter backed by the D1 database.\n */\nexport function createD1Database(options: D1DatabaseOptions): DatabaseAdapter {\n if (!options.client) {\n throw new Error(\"createD1Database: provide a D1 `client` binding (env.DB)\");\n }\n const { client } = options;\n // oxlint-disable-next-line typescript/no-unsafe-argument -- structural D1 binding vs platform type\n const db = drizzle(client as never, { schema });\n\n return createDrizzleAdapter(db, {\n metadata: {\n name: \"Cloudflare D1\",\n version: typeof __PKG_VERSION__ === \"string\" ? __PKG_VERSION__ : \"0.0.0\",\n description: \"Cloudflare D1 database adapter (D1 binding + Drizzle)\",\n kind: \"d1\",\n category: \"database\",\n // A Workers-only binding: no long-running process to consume capture jobs (ADR 0006, decision 3).\n supportsTesting: false,\n },\n tables: schema,\n migrate: async () => {\n await runMigrations(client);\n },\n close: () => {\n // no-op: the Workers runtime owns the binding\n },\n ping: async () => {\n await client.prepare(\"SELECT 1\").bind().first();\n },\n });\n}\n\nconst WEBHOOK_SECRET_DROP = \"ALTER TABLE webhooks DROP COLUMN secret\";\n\nasync function runMigrations(client: D1Database): Promise<void> {\n // Sequential: DDL order matters (tables before indexes) and D1 has no\n // multi-statement exec.\n for (const statement of splitStatements(DDL)) {\n // oxlint-disable-next-line no-await-in-loop -- ordered DDL, see above\n await execIgnore(client, statement);\n }\n await ensureColumns(client, DDL);\n for (const statement of IDENTITY_STATEMENTS) {\n // oxlint-disable-next-line no-await-in-loop -- ordered: drop old indexes, then create new\n await execIgnore(client, statement);\n }\n await client.exec(CAPTURE_JOB_BACKFILL); // not ignored: see its doc\n await execIgnore(client, WEBHOOK_SECRET_DROP);\n await migrateCommentsTable(client);\n}\n\n/** Split a multi-statement DDL script into single statements for `exec`. */\nfunction splitStatements(ddl: string): string[] {\n return ddl\n .split(/;\\n/u)\n .map((statement) => statement.trim())\n .filter((statement) => statement !== \"\");\n}\n\nasync function execIgnore(client: D1Database, sql: string): Promise<void> {\n try {\n await client.exec(sql);\n } catch {\n // idempotent — already migrated\n }\n}\n\n/** Add any DDL column missing from a database created before it shipped. */\nasync function ensureColumns(client: D1Database, ddl: string): Promise<void> {\n // Sequential: one PRAGMA round-trip per table keeps the per-table round-trips ordered.\n for (const [table, columns] of tableColumns(ddl)) {\n // oxlint-disable-next-line no-await-in-loop -- ordered per-table inspection\n const existing = await inspectColumns(client, table);\n for (const column of columns) {\n const [name] = column.split(/\\s+/u);\n if (name && (existing === undefined || !existing.has(name))) {\n // oxlint-disable-next-line no-await-in-loop -- ordered column backfill\n await execIgnore(client, `ALTER TABLE ${table} ADD COLUMN ${column}`);\n }\n }\n }\n}\n\n/**\n * Read a table's columns via `PRAGMA table_info`. Returns `undefined` when\n * the binding cannot serve PRAGMA (callers then add columns blindly).\n */\nasync function inspectColumns(client: D1Database, table: string): Promise<Set<string> | undefined> {\n try {\n const result = await client.prepare(`PRAGMA table_info(${table})`).bind().all();\n const rows = result.results as { name: unknown }[];\n return new Set(rows.map((row) => String(row.name)));\n } catch {\n return undefined;\n }\n}\n\nasync function migrateCommentsTable(client: D1Database): Promise<void> {\n try {\n const row = (await client\n .prepare(\"SELECT sql FROM sqlite_master WHERE type='table' AND name='comments'\")\n .bind()\n .first()) as { sql: string } | null;\n if (!row?.sql.includes(\"user_id TEXT NOT NULL REFERENCES users\")) {\n return;\n }\n await recreateCommentsTable(client);\n } catch {\n await execIgnore(client, \"PRAGMA foreign_keys = ON\");\n }\n}\n\nasync function recreateCommentsTable(client: D1Database): Promise<void> {\n await client.exec(\"PRAGMA foreign_keys = OFF\");\n await 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 await client.exec(\"INSERT INTO comments_new SELECT * FROM comments\");\n await client.exec(\"DROP TABLE comments\");\n await client.exec(\"ALTER TABLE comments_new RENAME TO comments\");\n await client.exec(\"CREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id)\");\n await client.exec(\"PRAGMA foreign_keys = ON\");\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiDA,SAAgB,iBAAiB,SAA6C;CAC5E,IAAI,CAAC,QAAQ,QACX,MAAM,IAAI,MAAM,0DAA0D;CAE5E,MAAM,EAAE,WAAW;CAEnB,MAAM,KAAK,QAAQ,QAAiB,EAAE,OAAO,CAAC;CAE9C,OAAO,qBAAqB,IAAI;EAC9B,UAAU;GACR,MAAM;GACN,SAAA;GACA,aAAa;GACb,MAAM;GACN,UAAU;GAEV,iBAAiB;EACnB;EACA,QAAQ;EACR,SAAS,YAAY;GACnB,MAAM,cAAc,MAAM;EAC5B;EACA,aAAa,CAEb;EACA,MAAM,YAAY;GAChB,MAAM,OAAO,QAAQ,UAAU,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM;EAChD;CACF,CAAC;AACH;AAEA,MAAM,sBAAsB;AAE5B,eAAe,cAAc,QAAmC;CAG9D,KAAK,MAAM,aAAa,gBAAgB,GAAG,GAEzC,MAAM,WAAW,QAAQ,SAAS;CAEpC,MAAM,cAAc,QAAQ,GAAG;CAC/B,KAAK,MAAM,aAAa,qBAEtB,MAAM,WAAW,QAAQ,SAAS;CAEpC,MAAM,OAAO,KAAK,oBAAoB;CACtC,MAAM,WAAW,QAAQ,mBAAmB;CAC5C,MAAM,qBAAqB,MAAM;AACnC;;AAGA,SAAS,gBAAgB,KAAuB;CAC9C,OAAO,IACJ,MAAM,MAAM,CAAC,CACb,KAAK,cAAc,UAAU,KAAK,CAAC,CAAC,CACpC,QAAQ,cAAc,cAAc,EAAE;AAC3C;AAEA,eAAe,WAAW,QAAoB,KAA4B;CACxE,IAAI;EACF,MAAM,OAAO,KAAK,GAAG;CACvB,QAAQ,CAER;AACF;;AAGA,eAAe,cAAc,QAAoB,KAA4B;CAE3E,KAAK,MAAM,CAAC,OAAO,YAAY,aAAa,GAAG,GAAG;EAEhD,MAAM,WAAW,MAAM,eAAe,QAAQ,KAAK;EACnD,KAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,CAAC,QAAQ,OAAO,MAAM,MAAM;GAClC,IAAI,SAAS,aAAa,KAAA,KAAa,CAAC,SAAS,IAAI,IAAI,IAEvD,MAAM,WAAW,QAAQ,eAAe,MAAM,cAAc,QAAQ;EAExE;CACF;AACF;;;;;AAMA,eAAe,eAAe,QAAoB,OAAiD;CACjG,IAAI;EAEF,MAAM,QAAO,MADQ,OAAO,QAAQ,qBAAqB,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAA,CAC1D;EACpB,OAAO,IAAI,IAAI,KAAK,KAAK,QAAQ,OAAO,IAAI,IAAI,CAAC,CAAC;CACpD,QAAQ;EACN;CACF;AACF;AAEA,eAAe,qBAAqB,QAAmC;CACrE,IAAI;EAKF,IAAI,EAAC,MAJc,OAChB,QAAQ,sEAAsE,CAAC,CAC/E,KAAK,CAAC,CACN,MAAM,EAAA,EACC,IAAI,SAAS,wCAAwC,GAC7D;EAEF,MAAM,sBAAsB,MAAM;CACpC,QAAQ;EACN,MAAM,WAAW,QAAQ,0BAA0B;CACrD;AACF;AAEA,eAAe,sBAAsB,QAAmC;CACtE,MAAM,OAAO,KAAK,2BAA2B;CAC7C,MAAM,OAAO,KAAK;;;;;;;;;;;;;GAajB;CACD,MAAM,OAAO,KAAK,iDAAiD;CACnE,MAAM,OAAO,KAAK,qBAAqB;CACvC,MAAM,OAAO,KAAK,6CAA6C;CAC/D,MAAM,OAAO,KAAK,yEAAyE;CAC3F,MAAM,OAAO,KAAK,0BAA0B;AAC9C"}
|
package/dist/ddl.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
3
|
/** Canonical SQLite DDL for all StoryShelf tables and indexes. */
|
|
4
|
-
declare const DDL = "\nCREATE TABLE IF NOT EXISTS projects (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n slug TEXT NOT NULL UNIQUE,\n git_repository TEXT,\n git_default_branch TEXT NOT NULL DEFAULT 'main',\n pixel_threshold REAL NOT NULL DEFAULT 0.1,\n max_diff_ratio REAL NOT NULL DEFAULT 0.01,\n public_branch_regex TEXT,\n storybook_meta TEXT,\n execute_play INTEGER NOT NULL DEFAULT 0,\n play_timeout_ms INTEGER NOT NULL DEFAULT 10000,\n run_a11y INTEGER NOT NULL DEFAULT 1,\n run_tests INTEGER NOT NULL DEFAULT 0,\n detect_unstable INTEGER NOT NULL DEFAULT 1,\n browser TEXT NOT NULL DEFAULT 'chromium',\n viewports TEXT,\n browsers TEXT,\n modes_allowlist TEXT,\n automigrate INTEGER NOT NULL DEFAULT 0,\n ai_profile TEXT,\n review_enabled INTEGER NOT NULL DEFAULT 1,\n review_approvals INTEGER NOT NULL DEFAULT 1,\n storybook_url_ttl_seconds INTEGER,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS builds (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n git_sha TEXT NOT NULL,\n git_branch TEXT NOT NULL,\n is_default INTEGER NOT NULL DEFAULT 0,\n author_email TEXT,\n author_name TEXT,\n message TEXT,\n public INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL DEFAULT 'pending',\n retention_state TEXT NOT NULL DEFAULT 'available',\n snapshot_count INTEGER NOT NULL DEFAULT 0,\n changed_count INTEGER NOT NULL DEFAULT 0,\n approved_count INTEGER NOT NULL DEFAULT 0,\n rejected_count INTEGER NOT NULL DEFAULT 0,\n inherited_count INTEGER NOT NULL DEFAULT 0,\n affected_only INTEGER NOT NULL DEFAULT 1,\n baseline_sha TEXT,\n changed_files TEXT,\n affected_import_paths TEXT,\n test_results TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS builds_project_gitsha_idx ON builds (project_id, git_sha);\nCREATE INDEX IF NOT EXISTS builds_git_branch_idx ON builds (git_branch);\nCREATE TABLE IF NOT EXISTS build_references (\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n owner_type TEXT NOT NULL CHECK (owner_type IN ('baseline', 'investigation')),\n owner_id TEXT NOT NULL,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE RESTRICT,\n PRIMARY KEY (project_id, owner_type, owner_id, build_id)\n);\nCREATE INDEX IF NOT EXISTS build_references_build_idx ON build_references (build_id);\nCREATE TABLE IF NOT EXISTS build_reference_generations (\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n owner_type TEXT NOT NULL CHECK (owner_type IN ('baseline', 'investigation')),\n owner_id TEXT NOT NULL,\n generation INTEGER NOT NULL CHECK (generation >= 0),\n PRIMARY KEY (project_id, owner_type, owner_id)\n);\nCREATE TABLE IF NOT EXISTS capture_attempts (\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 attempt_no INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'queued',\n error TEXT,\n req_id TEXT,\n story_count INTEGER NOT NULL DEFAULT 0,\n failed_count INTEGER NOT NULL DEFAULT 0,\n queued_at TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS capture_attempts_build_no_idx ON capture_attempts (build_id, attempt_no);\nCREATE INDEX IF NOT EXISTS capture_attempts_build_id_idx ON capture_attempts (build_id);\nCREATE TABLE IF NOT EXISTS capture_shards (\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 kind TEXT NOT NULL,\n ordinal INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n attempt_id TEXT,\n payload TEXT,\n result TEXT,\n error TEXT,\n started_at TEXT,\n finished_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS capture_shards_build_kind_ordinal_idx ON capture_shards (build_id, kind, ordinal);\nCREATE INDEX IF NOT EXISTS capture_shards_build_id_idx ON capture_shards (build_id);\nCREATE TABLE IF NOT EXISTS capture_jobs (\n id TEXT PRIMARY KEY,\n entry TEXT NOT NULL,\n job_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n build_id TEXT,\n shard_id TEXT,\n attempt INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT 3,\n status TEXT NOT NULL,\n visible_at TEXT,\n owner TEXT,\n payload TEXT,\n cancel_requested_at TEXT,\n error TEXT,\n started_at TEXT,\n finished_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS capture_jobs_poll_idx ON capture_jobs (kind, visible_at, status);\nCREATE INDEX IF NOT EXISTS capture_jobs_job_id_idx ON capture_jobs (job_id);\nCREATE INDEX IF NOT EXISTS capture_jobs_build_id_idx ON capture_jobs (build_id);\nCREATE INDEX IF NOT EXISTS capture_jobs_entry_created_idx ON capture_jobs (entry, created_at);\nCREATE TABLE IF NOT EXISTS site_settings (\n id TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_by TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS build_components (\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 import_path TEXT NOT NULL,\n title TEXT NOT NULL,\n story_count INTEGER NOT NULL DEFAULT 0,\n hash TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS build_components_build_id_idx ON build_components (build_id);\nCREATE INDEX IF NOT EXISTS build_components_project_import_path_idx ON build_components (project_id, import_path);\nCREATE TABLE IF NOT EXISTS capture_logs (\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 attempt_id TEXT NOT NULL REFERENCES capture_attempts(id) ON DELETE CASCADE,\n seq INTEGER NOT NULL,\n level TEXT NOT NULL DEFAULT 'info',\n message TEXT NOT NULL,\n fields TEXT,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS capture_logs_attempt_seq_idx ON capture_logs (attempt_id, seq);\nCREATE TABLE IF NOT EXISTS snapshots (\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 story_id TEXT NOT NULL,\n story_name TEXT NOT NULL,\n story_title TEXT NOT NULL,\n story_import_path TEXT,\n design_url TEXT,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n viewport_width INTEGER NOT NULL DEFAULT 1280,\n viewport_height INTEGER NOT NULL DEFAULT 720,\n screenshot_path TEXT NOT NULL,\n diff_path TEXT,\n diff_pixels INTEGER,\n diff_ratio REAL,\n diff_passed INTEGER,\n status TEXT NOT NULL DEFAULT 'pending',\n reviewed_by TEXT,\n reviewed_at TEXT,\n infra_hash TEXT,\n env_change TEXT,\n inherited INTEGER NOT NULL DEFAULT 0,\n baseline_id TEXT,\n baseline_version TEXT,\n source_build_id TEXT,\n comparison_image_path TEXT,\n comparison_metadata TEXT,\n mode TEXT,\n browser TEXT NOT NULL DEFAULT 'chromium',\n duration_ms INTEGER,\n a11y_violations TEXT,\n unstable INTEGER,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS snapshots_build_id_idx ON snapshots (build_id);\nCREATE TABLE IF NOT EXISTS baselines (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n story_id TEXT NOT NULL,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n branch TEXT NOT NULL,\n snapshot_id TEXT,\n source_build_id TEXT,\n screenshot_path TEXT NOT NULL,\n infra_hash TEXT,\n fingerprint TEXT,\n mode TEXT,\n browser TEXT NOT NULL DEFAULT 'chromium',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS baselines_project_story_idx ON baselines (project_id, story_id);\nCREATE TABLE IF NOT EXISTS comments (\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 story_id TEXT\n);\nCREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id);\nCREATE TABLE IF NOT EXISTS build_reviews (\n id TEXT PRIMARY KEY,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n verdict TEXT NOT NULL,\n body TEXT,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS build_reviews_build_id_idx ON build_reviews (build_id);\nCREATE TABLE IF NOT EXISTS label_types (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n key TEXT NOT NULL,\n name TEXT NOT NULL,\n link_template TEXT,\n color TEXT,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS label_types_project_key_idx ON label_types (project_id, key);\nCREATE TABLE IF NOT EXISTS build_labels (\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 type_key TEXT NOT NULL,\n value TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS build_labels_build_type_value_idx ON build_labels (build_id, type_key, value);\nCREATE TABLE IF NOT EXISTS project_links (\n id TEXT PRIMARY KEY,\n downstream_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n upstream_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n package_name TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_links_downstream_package_idx ON project_links (downstream_id, package_name);\nCREATE TABLE IF NOT EXISTS design_links (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n import_path TEXT,\n label TEXT,\n url TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS design_links_project_id_idx ON design_links (project_id);\nCREATE TABLE IF NOT EXISTS design_credentials (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n token_encrypted TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'unknown',\n checked_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS design_credentials_project_provider_idx ON design_credentials (project_id, provider);\nCREATE TABLE IF NOT EXISTS design_nodes (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n viewer_id TEXT NOT NULL DEFAULT '',\n file_key TEXT NOT NULL,\n node_id TEXT NOT NULL DEFAULT '',\n name TEXT,\n version TEXT,\n last_modified TEXT,\n state TEXT NOT NULL DEFAULT 'ok',\n render_path TEXT,\n render_version TEXT,\n notified_version TEXT,\n fetched_at TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS design_nodes_project_file_node_idx ON design_nodes (project_id, provider, viewer_id, file_key, node_id);\nCREATE TABLE IF NOT EXISTS build_package_usage (\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 story_import_path TEXT NOT NULL,\n package_name TEXT NOT NULL,\n module_path TEXT NOT NULL,\n version TEXT,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS build_package_usage_build_id_idx ON build_package_usage (build_id);\nCREATE TABLE IF NOT EXISTS tokens (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n hash TEXT NOT NULL UNIQUE,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n last_used_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS webhooks (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n url TEXT NOT NULL,\n secret_encrypted TEXT NOT NULL,\n events TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS notification_channels (\n id TEXT PRIMARY KEY,\n project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n config TEXT NOT NULL,\n secret_encrypted TEXT,\n events TEXT,\n enabled INTEGER NOT NULL DEFAULT 1,\n last_error TEXT,\n last_error_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS notification_channels_project_id_idx ON notification_channels (project_id);\nCREATE TABLE IF NOT EXISTS notification_subscriptions (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n events TEXT,\n via TEXT,\n enabled INTEGER NOT NULL DEFAULT 1,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS notification_subscriptions_project_user_idx ON notification_subscriptions (project_id, user_id);\nCREATE TABLE IF NOT EXISTS insights (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT REFERENCES builds(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n window_key TEXT,\n input_hash TEXT NOT NULL,\n status TEXT NOT NULL,\n profile TEXT NOT NULL,\n model TEXT NOT NULL,\n prompt_version TEXT NOT NULL,\n verdict TEXT,\n summary TEXT,\n result TEXT,\n error_code TEXT,\n created_at TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT\n);\nCREATE INDEX IF NOT EXISTS insights_project_build_idx ON insights (project_id, build_id);\nCREATE INDEX IF NOT EXISTS insights_created_at_idx ON insights (created_at);\nCREATE UNIQUE INDEX IF NOT EXISTS insights_triage_unique_idx ON insights (project_id, build_id, kind, input_hash);\nCREATE UNIQUE INDEX IF NOT EXISTS insights_health_unique_idx ON insights (project_id, kind, window_key);\nCREATE TABLE IF NOT EXISTS ai_usage (\n id TEXT PRIMARY KEY,\n insight_id TEXT,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n profile TEXT NOT NULL,\n task TEXT NOT NULL,\n model TEXT NOT NULL,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n estimated INTEGER NOT NULL DEFAULT 0,\n images INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS ai_usage_created_at_idx ON ai_usage (created_at);\nCREATE INDEX IF NOT EXISTS ai_usage_project_created_idx ON ai_usage (project_id, created_at);\nCREATE TABLE IF NOT EXISTS ai_budget_alerts (\n id TEXT PRIMARY KEY,\n day TEXT NOT NULL,\n threshold INTEGER NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY,\n email TEXT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n avatar_url TEXT,\n role TEXT NOT NULL DEFAULT 'member',\n last_login_at TEXT,\n created_at TEXT NOT NULL,\n password_hash TEXT,\n display_name_override TEXT,\n auth_provider TEXT NOT NULL DEFAULT 'oidc',\n disabled INTEGER NOT NULL DEFAULT 0\n);\nCREATE TABLE IF NOT EXISTS user_connections (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n service TEXT NOT NULL,\n token_encrypted TEXT NOT NULL,\n refresh_token_encrypted TEXT,\n expires_at TEXT,\n status TEXT NOT NULL DEFAULT 'ok',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS user_connections_user_service_idx ON user_connections (user_id, service);\nCREATE TABLE IF NOT EXISTS user_invite_tokens (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n token_hash TEXT NOT NULL UNIQUE,\n expires_at TEXT NOT NULL,\n used_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS user_invite_tokens_user_id_idx ON user_invite_tokens (user_id);\nCREATE TABLE IF NOT EXISTS project_members (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n role TEXT NOT NULL DEFAULT 'viewer',\n source TEXT NOT NULL DEFAULT 'manual',\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_members_project_user_idx ON project_members (project_id, user_id);\nCREATE TABLE IF NOT EXISTS project_group_mappings (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n group_name TEXT NOT NULL,\n role TEXT NOT NULL DEFAULT 'viewer',\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_group_mappings_project_group_idx ON project_group_mappings (project_id, group_name);\nCREATE TABLE IF NOT EXISTS content_refs (\n hash TEXT PRIMARY KEY,\n ref_count INTEGER NOT NULL DEFAULT 1,\n last_seen_at TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS user (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n email TEXT NOT NULL UNIQUE,\n email_verified INTEGER NOT NULL DEFAULT 0,\n image TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS session (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,\n token TEXT NOT NULL UNIQUE,\n expires_at TEXT NOT NULL,\n ip_address TEXT,\n user_agent TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS session_token_idx ON session (token);\nCREATE INDEX IF NOT EXISTS session_user_id_idx ON session (user_id);\nCREATE TABLE IF NOT EXISTS account (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,\n account_id TEXT NOT NULL,\n provider_id TEXT NOT NULL,\n access_token TEXT,\n refresh_token TEXT,\n id_token TEXT,\n access_token_expires_at TEXT,\n refresh_token_expires_at TEXT,\n scope TEXT,\n expires_at TEXT,\n password TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS account_user_id_idx ON account (user_id);\nCREATE TABLE IF NOT EXISTS verification (\n id TEXT PRIMARY KEY,\n identifier TEXT NOT NULL,\n value TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS verification_identifier_idx ON verification (identifier);\nCREATE TABLE IF NOT EXISTS passkey (\n id TEXT PRIMARY KEY,\n name TEXT,\n public_key TEXT NOT NULL,\n user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,\n credential_id TEXT NOT NULL,\n counter INTEGER NOT NULL,\n device_type TEXT NOT NULL,\n backed_up INTEGER NOT NULL DEFAULT 0,\n transports TEXT,\n aaguid TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT\n);\nCREATE INDEX IF NOT EXISTS passkey_user_id_idx ON passkey (user_id);\nCREATE INDEX IF NOT EXISTS passkey_credential_id_idx ON passkey (credential_id);\nCREATE TABLE IF NOT EXISTS ssoProvider (\n id TEXT PRIMARY KEY,\n issuer TEXT NOT NULL,\n oidc_config TEXT,\n saml_config TEXT,\n user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,\n provider_id TEXT NOT NULL UNIQUE,\n organization_id TEXT,\n domain TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS ssoprovider_domain_idx ON ssoProvider (domain);\n";
|
|
4
|
+
declare const DDL = "\nCREATE TABLE IF NOT EXISTS projects (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n slug TEXT NOT NULL UNIQUE,\n git_repository TEXT,\n git_default_branch TEXT NOT NULL DEFAULT 'main',\n pixel_threshold REAL NOT NULL DEFAULT 0.1,\n max_diff_ratio REAL NOT NULL DEFAULT 0.01,\n public_branch_regex TEXT,\n storybook_meta TEXT,\n execute_play INTEGER NOT NULL DEFAULT 0,\n play_timeout_ms INTEGER NOT NULL DEFAULT 10000,\n run_a11y INTEGER NOT NULL DEFAULT 1,\n run_tests INTEGER NOT NULL DEFAULT 0,\n detect_unstable INTEGER NOT NULL DEFAULT 1,\n browser TEXT NOT NULL DEFAULT 'chromium',\n viewports TEXT,\n browsers TEXT,\n modes_allowlist TEXT,\n automigrate INTEGER NOT NULL DEFAULT 0,\n ai_profile TEXT,\n review_enabled INTEGER NOT NULL DEFAULT 1,\n review_approvals INTEGER NOT NULL DEFAULT 1,\n storybook_url_ttl_seconds INTEGER,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS builds (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n git_sha TEXT NOT NULL,\n git_branch TEXT NOT NULL,\n is_default INTEGER NOT NULL DEFAULT 0,\n author_email TEXT,\n author_name TEXT,\n message TEXT,\n public INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL DEFAULT 'pending',\n retention_state TEXT NOT NULL DEFAULT 'available',\n snapshot_count INTEGER NOT NULL DEFAULT 0,\n changed_count INTEGER NOT NULL DEFAULT 0,\n approved_count INTEGER NOT NULL DEFAULT 0,\n rejected_count INTEGER NOT NULL DEFAULT 0,\n inherited_count INTEGER NOT NULL DEFAULT 0,\n affected_only INTEGER NOT NULL DEFAULT 1,\n baseline_sha TEXT,\n changed_files TEXT,\n affected_import_paths TEXT,\n test_results TEXT,\n capture_job_id TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS builds_project_gitsha_idx ON builds (project_id, git_sha);\nCREATE INDEX IF NOT EXISTS builds_git_branch_idx ON builds (git_branch);\nCREATE TABLE IF NOT EXISTS build_references (\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n owner_type TEXT NOT NULL CHECK (owner_type IN ('baseline', 'investigation')),\n owner_id TEXT NOT NULL,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE RESTRICT,\n PRIMARY KEY (project_id, owner_type, owner_id, build_id)\n);\nCREATE INDEX IF NOT EXISTS build_references_build_idx ON build_references (build_id);\nCREATE TABLE IF NOT EXISTS build_reference_generations (\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n owner_type TEXT NOT NULL CHECK (owner_type IN ('baseline', 'investigation')),\n owner_id TEXT NOT NULL,\n generation INTEGER NOT NULL CHECK (generation >= 0),\n PRIMARY KEY (project_id, owner_type, owner_id)\n);\nCREATE TABLE IF NOT EXISTS capture_attempts (\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 attempt_no INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'queued',\n error TEXT,\n req_id TEXT,\n story_count INTEGER NOT NULL DEFAULT 0,\n failed_count INTEGER NOT NULL DEFAULT 0,\n queued_at TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS capture_attempts_build_no_idx ON capture_attempts (build_id, attempt_no);\nCREATE INDEX IF NOT EXISTS capture_attempts_build_id_idx ON capture_attempts (build_id);\nCREATE TABLE IF NOT EXISTS capture_shards (\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 kind TEXT NOT NULL,\n ordinal INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n attempt_id TEXT,\n payload TEXT,\n result TEXT,\n error TEXT,\n started_at TEXT,\n finished_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS capture_shards_build_kind_ordinal_idx ON capture_shards (build_id, kind, ordinal);\nCREATE INDEX IF NOT EXISTS capture_shards_build_id_idx ON capture_shards (build_id);\nCREATE TABLE IF NOT EXISTS capture_jobs (\n id TEXT PRIMARY KEY,\n entry TEXT NOT NULL,\n job_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n build_id TEXT,\n shard_id TEXT,\n attempt INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT 3,\n status TEXT NOT NULL,\n visible_at TEXT,\n owner TEXT,\n payload TEXT,\n cancel_requested_at TEXT,\n error TEXT,\n started_at TEXT,\n finished_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS capture_jobs_poll_idx ON capture_jobs (kind, visible_at, status);\nCREATE INDEX IF NOT EXISTS capture_jobs_job_id_idx ON capture_jobs (job_id);\nCREATE INDEX IF NOT EXISTS capture_jobs_build_id_idx ON capture_jobs (build_id);\nCREATE INDEX IF NOT EXISTS capture_jobs_entry_created_idx ON capture_jobs (entry, created_at);\nCREATE TABLE IF NOT EXISTS site_settings (\n id TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_by TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS build_components (\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 import_path TEXT NOT NULL,\n title TEXT NOT NULL,\n story_count INTEGER NOT NULL DEFAULT 0,\n hash TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS build_components_build_id_idx ON build_components (build_id);\nCREATE INDEX IF NOT EXISTS build_components_project_import_path_idx ON build_components (project_id, import_path);\nCREATE TABLE IF NOT EXISTS capture_logs (\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 attempt_id TEXT NOT NULL REFERENCES capture_attempts(id) ON DELETE CASCADE,\n seq INTEGER NOT NULL,\n level TEXT NOT NULL DEFAULT 'info',\n message TEXT NOT NULL,\n fields TEXT,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS capture_logs_attempt_seq_idx ON capture_logs (attempt_id, seq);\nCREATE TABLE IF NOT EXISTS snapshots (\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 story_id TEXT NOT NULL,\n story_name TEXT NOT NULL,\n story_title TEXT NOT NULL,\n story_import_path TEXT,\n design_url TEXT,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n viewport_width INTEGER NOT NULL DEFAULT 1280,\n viewport_height INTEGER NOT NULL DEFAULT 720,\n screenshot_path TEXT NOT NULL,\n diff_path TEXT,\n diff_pixels INTEGER,\n diff_ratio REAL,\n diff_passed INTEGER,\n status TEXT NOT NULL DEFAULT 'pending',\n reviewed_by TEXT,\n reviewed_at TEXT,\n infra_hash TEXT,\n env_change TEXT,\n inherited INTEGER NOT NULL DEFAULT 0,\n baseline_id TEXT,\n baseline_version TEXT,\n source_build_id TEXT,\n comparison_image_path TEXT,\n comparison_metadata TEXT,\n mode TEXT,\n browser TEXT NOT NULL DEFAULT 'chromium',\n duration_ms INTEGER,\n a11y_violations TEXT,\n unstable INTEGER,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS snapshots_build_id_idx ON snapshots (build_id);\nCREATE TABLE IF NOT EXISTS baselines (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n story_id TEXT NOT NULL,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n branch TEXT NOT NULL,\n snapshot_id TEXT,\n source_build_id TEXT,\n screenshot_path TEXT NOT NULL,\n infra_hash TEXT,\n fingerprint TEXT,\n mode TEXT,\n browser TEXT NOT NULL DEFAULT 'chromium',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS baselines_project_story_idx ON baselines (project_id, story_id);\nCREATE TABLE IF NOT EXISTS comments (\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 story_id TEXT\n);\nCREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id);\nCREATE TABLE IF NOT EXISTS build_reviews (\n id TEXT PRIMARY KEY,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n verdict TEXT NOT NULL,\n body TEXT,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS build_reviews_build_id_idx ON build_reviews (build_id);\nCREATE TABLE IF NOT EXISTS label_types (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n key TEXT NOT NULL,\n name TEXT NOT NULL,\n link_template TEXT,\n color TEXT,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS label_types_project_key_idx ON label_types (project_id, key);\nCREATE TABLE IF NOT EXISTS build_labels (\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 type_key TEXT NOT NULL,\n value TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS build_labels_build_type_value_idx ON build_labels (build_id, type_key, value);\nCREATE TABLE IF NOT EXISTS project_links (\n id TEXT PRIMARY KEY,\n downstream_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n upstream_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n package_name TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_links_downstream_package_idx ON project_links (downstream_id, package_name);\nCREATE TABLE IF NOT EXISTS design_links (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n import_path TEXT,\n label TEXT,\n url TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS design_links_project_id_idx ON design_links (project_id);\nCREATE TABLE IF NOT EXISTS design_credentials (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n token_encrypted TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'unknown',\n checked_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS design_credentials_project_provider_idx ON design_credentials (project_id, provider);\nCREATE TABLE IF NOT EXISTS design_nodes (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n viewer_id TEXT NOT NULL DEFAULT '',\n file_key TEXT NOT NULL,\n node_id TEXT NOT NULL DEFAULT '',\n name TEXT,\n version TEXT,\n last_modified TEXT,\n state TEXT NOT NULL DEFAULT 'ok',\n render_path TEXT,\n render_version TEXT,\n notified_version TEXT,\n fetched_at TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS design_nodes_project_file_node_idx ON design_nodes (project_id, provider, viewer_id, file_key, node_id);\nCREATE TABLE IF NOT EXISTS build_package_usage (\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 story_import_path TEXT NOT NULL,\n package_name TEXT NOT NULL,\n module_path TEXT NOT NULL,\n version TEXT,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS build_package_usage_build_id_idx ON build_package_usage (build_id);\nCREATE TABLE IF NOT EXISTS tokens (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n hash TEXT NOT NULL UNIQUE,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n last_used_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS webhooks (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n url TEXT NOT NULL,\n secret_encrypted TEXT NOT NULL,\n events TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS notification_channels (\n id TEXT PRIMARY KEY,\n project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n config TEXT NOT NULL,\n secret_encrypted TEXT,\n events TEXT,\n enabled INTEGER NOT NULL DEFAULT 1,\n last_error TEXT,\n last_error_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS notification_channels_project_id_idx ON notification_channels (project_id);\nCREATE TABLE IF NOT EXISTS notification_subscriptions (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n events TEXT,\n via TEXT,\n enabled INTEGER NOT NULL DEFAULT 1,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS notification_subscriptions_project_user_idx ON notification_subscriptions (project_id, user_id);\nCREATE TABLE IF NOT EXISTS insights (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT REFERENCES builds(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n window_key TEXT,\n input_hash TEXT NOT NULL,\n status TEXT NOT NULL,\n profile TEXT NOT NULL,\n model TEXT NOT NULL,\n prompt_version TEXT NOT NULL,\n verdict TEXT,\n summary TEXT,\n result TEXT,\n error_code TEXT,\n created_at TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT\n);\nCREATE INDEX IF NOT EXISTS insights_project_build_idx ON insights (project_id, build_id);\nCREATE INDEX IF NOT EXISTS insights_created_at_idx ON insights (created_at);\nCREATE UNIQUE INDEX IF NOT EXISTS insights_triage_unique_idx ON insights (project_id, build_id, kind, input_hash);\nCREATE UNIQUE INDEX IF NOT EXISTS insights_health_unique_idx ON insights (project_id, kind, window_key);\nCREATE TABLE IF NOT EXISTS ai_usage (\n id TEXT PRIMARY KEY,\n insight_id TEXT,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n profile TEXT NOT NULL,\n task TEXT NOT NULL,\n model TEXT NOT NULL,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n estimated INTEGER NOT NULL DEFAULT 0,\n images INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS ai_usage_created_at_idx ON ai_usage (created_at);\nCREATE INDEX IF NOT EXISTS ai_usage_project_created_idx ON ai_usage (project_id, created_at);\nCREATE TABLE IF NOT EXISTS ai_budget_alerts (\n id TEXT PRIMARY KEY,\n day TEXT NOT NULL,\n threshold INTEGER NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY,\n email TEXT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n avatar_url TEXT,\n role TEXT NOT NULL DEFAULT 'member',\n last_login_at TEXT,\n created_at TEXT NOT NULL,\n password_hash TEXT,\n display_name_override TEXT,\n auth_provider TEXT NOT NULL DEFAULT 'oidc',\n disabled INTEGER NOT NULL DEFAULT 0\n);\nCREATE TABLE IF NOT EXISTS user_connections (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n service TEXT NOT NULL,\n token_encrypted TEXT NOT NULL,\n refresh_token_encrypted TEXT,\n expires_at TEXT,\n status TEXT NOT NULL DEFAULT 'ok',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS user_connections_user_service_idx ON user_connections (user_id, service);\nCREATE TABLE IF NOT EXISTS user_invite_tokens (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n token_hash TEXT NOT NULL UNIQUE,\n expires_at TEXT NOT NULL,\n used_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS user_invite_tokens_user_id_idx ON user_invite_tokens (user_id);\nCREATE TABLE IF NOT EXISTS project_members (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n role TEXT NOT NULL DEFAULT 'viewer',\n source TEXT NOT NULL DEFAULT 'manual',\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_members_project_user_idx ON project_members (project_id, user_id);\nCREATE TABLE IF NOT EXISTS project_group_mappings (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n group_name TEXT NOT NULL,\n role TEXT NOT NULL DEFAULT 'viewer',\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_group_mappings_project_group_idx ON project_group_mappings (project_id, group_name);\nCREATE TABLE IF NOT EXISTS content_refs (\n hash TEXT PRIMARY KEY,\n ref_count INTEGER NOT NULL DEFAULT 1,\n last_seen_at TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS user (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n email TEXT NOT NULL UNIQUE,\n email_verified INTEGER NOT NULL DEFAULT 0,\n image TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS session (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,\n token TEXT NOT NULL UNIQUE,\n expires_at TEXT NOT NULL,\n ip_address TEXT,\n user_agent TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS session_token_idx ON session (token);\nCREATE INDEX IF NOT EXISTS session_user_id_idx ON session (user_id);\nCREATE TABLE IF NOT EXISTS account (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,\n account_id TEXT NOT NULL,\n provider_id TEXT NOT NULL,\n access_token TEXT,\n refresh_token TEXT,\n id_token TEXT,\n access_token_expires_at TEXT,\n refresh_token_expires_at TEXT,\n scope TEXT,\n expires_at TEXT,\n password TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS account_user_id_idx ON account (user_id);\nCREATE TABLE IF NOT EXISTS verification (\n id TEXT PRIMARY KEY,\n identifier TEXT NOT NULL,\n value TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS verification_identifier_idx ON verification (identifier);\nCREATE TABLE IF NOT EXISTS passkey (\n id TEXT PRIMARY KEY,\n name TEXT,\n public_key TEXT NOT NULL,\n user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,\n credential_id TEXT NOT NULL,\n counter INTEGER NOT NULL,\n device_type TEXT NOT NULL,\n backed_up INTEGER NOT NULL DEFAULT 0,\n transports TEXT,\n aaguid TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT\n);\nCREATE INDEX IF NOT EXISTS passkey_user_id_idx ON passkey (user_id);\nCREATE INDEX IF NOT EXISTS passkey_credential_id_idx ON passkey (credential_id);\nCREATE TABLE IF NOT EXISTS ssoProvider (\n id TEXT PRIMARY KEY,\n issuer TEXT NOT NULL,\n oidc_config TEXT,\n saml_config TEXT,\n user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,\n provider_id TEXT NOT NULL UNIQUE,\n organization_id TEXT,\n domain TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS ssoprovider_domain_idx ON ssoProvider (domain);\n";
|
|
5
5
|
/**
|
|
6
6
|
* Indexes that must be created after `ensureColumns` (they reference columns
|
|
7
7
|
* older volumes only gain then, or can fail on legacy duplicates). Runs the
|
package/dist/ddl.mjs
CHANGED
package/dist/ddl.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ddl.mjs","names":[],"sources":["../src/ddl.ts"],"sourcesContent":["/** Canonical SQLite DDL for all StoryShelf tables and indexes. */\nexport const DDL = `\nCREATE TABLE IF NOT EXISTS projects (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n slug TEXT NOT NULL UNIQUE,\n git_repository TEXT,\n git_default_branch TEXT NOT NULL DEFAULT 'main',\n pixel_threshold REAL NOT NULL DEFAULT 0.1,\n max_diff_ratio REAL NOT NULL DEFAULT 0.01,\n public_branch_regex TEXT,\n storybook_meta TEXT,\n execute_play INTEGER NOT NULL DEFAULT 0,\n play_timeout_ms INTEGER NOT NULL DEFAULT 10000,\n run_a11y INTEGER NOT NULL DEFAULT 1,\n run_tests INTEGER NOT NULL DEFAULT 0,\n detect_unstable INTEGER NOT NULL DEFAULT 1,\n browser TEXT NOT NULL DEFAULT 'chromium',\n viewports TEXT,\n browsers TEXT,\n modes_allowlist TEXT,\n automigrate INTEGER NOT NULL DEFAULT 0,\n ai_profile TEXT,\n review_enabled INTEGER NOT NULL DEFAULT 1,\n review_approvals INTEGER NOT NULL DEFAULT 1,\n storybook_url_ttl_seconds INTEGER,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS builds (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n git_sha TEXT NOT NULL,\n git_branch TEXT NOT NULL,\n is_default INTEGER NOT NULL DEFAULT 0,\n author_email TEXT,\n author_name TEXT,\n message TEXT,\n public INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL DEFAULT 'pending',\n retention_state TEXT NOT NULL DEFAULT 'available',\n snapshot_count INTEGER NOT NULL DEFAULT 0,\n changed_count INTEGER NOT NULL DEFAULT 0,\n approved_count INTEGER NOT NULL DEFAULT 0,\n rejected_count INTEGER NOT NULL DEFAULT 0,\n inherited_count INTEGER NOT NULL DEFAULT 0,\n affected_only INTEGER NOT NULL DEFAULT 1,\n baseline_sha TEXT,\n changed_files TEXT,\n affected_import_paths TEXT,\n test_results TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS builds_project_gitsha_idx ON builds (project_id, git_sha);\nCREATE INDEX IF NOT EXISTS builds_git_branch_idx ON builds (git_branch);\nCREATE TABLE IF NOT EXISTS build_references (\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n owner_type TEXT NOT NULL CHECK (owner_type IN ('baseline', 'investigation')),\n owner_id TEXT NOT NULL,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE RESTRICT,\n PRIMARY KEY (project_id, owner_type, owner_id, build_id)\n);\nCREATE INDEX IF NOT EXISTS build_references_build_idx ON build_references (build_id);\nCREATE TABLE IF NOT EXISTS build_reference_generations (\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n owner_type TEXT NOT NULL CHECK (owner_type IN ('baseline', 'investigation')),\n owner_id TEXT NOT NULL,\n generation INTEGER NOT NULL CHECK (generation >= 0),\n PRIMARY KEY (project_id, owner_type, owner_id)\n);\nCREATE TABLE IF NOT EXISTS capture_attempts (\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 attempt_no INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'queued',\n error TEXT,\n req_id TEXT,\n story_count INTEGER NOT NULL DEFAULT 0,\n failed_count INTEGER NOT NULL DEFAULT 0,\n queued_at TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS capture_attempts_build_no_idx ON capture_attempts (build_id, attempt_no);\nCREATE INDEX IF NOT EXISTS capture_attempts_build_id_idx ON capture_attempts (build_id);\nCREATE TABLE IF NOT EXISTS capture_shards (\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 kind TEXT NOT NULL,\n ordinal INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n attempt_id TEXT,\n payload TEXT,\n result TEXT,\n error TEXT,\n started_at TEXT,\n finished_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS capture_shards_build_kind_ordinal_idx ON capture_shards (build_id, kind, ordinal);\nCREATE INDEX IF NOT EXISTS capture_shards_build_id_idx ON capture_shards (build_id);\nCREATE TABLE IF NOT EXISTS capture_jobs (\n id TEXT PRIMARY KEY,\n entry TEXT NOT NULL,\n job_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n build_id TEXT,\n shard_id TEXT,\n attempt INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT 3,\n status TEXT NOT NULL,\n visible_at TEXT,\n owner TEXT,\n payload TEXT,\n cancel_requested_at TEXT,\n error TEXT,\n started_at TEXT,\n finished_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS capture_jobs_poll_idx ON capture_jobs (kind, visible_at, status);\nCREATE INDEX IF NOT EXISTS capture_jobs_job_id_idx ON capture_jobs (job_id);\nCREATE INDEX IF NOT EXISTS capture_jobs_build_id_idx ON capture_jobs (build_id);\nCREATE INDEX IF NOT EXISTS capture_jobs_entry_created_idx ON capture_jobs (entry, created_at);\nCREATE TABLE IF NOT EXISTS site_settings (\n id TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_by TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS build_components (\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 import_path TEXT NOT NULL,\n title TEXT NOT NULL,\n story_count INTEGER NOT NULL DEFAULT 0,\n hash TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS build_components_build_id_idx ON build_components (build_id);\nCREATE INDEX IF NOT EXISTS build_components_project_import_path_idx ON build_components (project_id, import_path);\nCREATE TABLE IF NOT EXISTS capture_logs (\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 attempt_id TEXT NOT NULL REFERENCES capture_attempts(id) ON DELETE CASCADE,\n seq INTEGER NOT NULL,\n level TEXT NOT NULL DEFAULT 'info',\n message TEXT NOT NULL,\n fields TEXT,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS capture_logs_attempt_seq_idx ON capture_logs (attempt_id, seq);\nCREATE TABLE IF NOT EXISTS snapshots (\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 story_id TEXT NOT NULL,\n story_name TEXT NOT NULL,\n story_title TEXT NOT NULL,\n story_import_path TEXT,\n design_url TEXT,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n viewport_width INTEGER NOT NULL DEFAULT 1280,\n viewport_height INTEGER NOT NULL DEFAULT 720,\n screenshot_path TEXT NOT NULL,\n diff_path TEXT,\n diff_pixels INTEGER,\n diff_ratio REAL,\n diff_passed INTEGER,\n status TEXT NOT NULL DEFAULT 'pending',\n reviewed_by TEXT,\n reviewed_at TEXT,\n infra_hash TEXT,\n env_change TEXT,\n inherited INTEGER NOT NULL DEFAULT 0,\n baseline_id TEXT,\n baseline_version TEXT,\n source_build_id TEXT,\n comparison_image_path TEXT,\n comparison_metadata TEXT,\n mode TEXT,\n browser TEXT NOT NULL DEFAULT 'chromium',\n duration_ms INTEGER,\n a11y_violations TEXT,\n unstable INTEGER,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS snapshots_build_id_idx ON snapshots (build_id);\nCREATE TABLE IF NOT EXISTS baselines (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n story_id TEXT NOT NULL,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n branch TEXT NOT NULL,\n snapshot_id TEXT,\n source_build_id TEXT,\n screenshot_path TEXT NOT NULL,\n infra_hash TEXT,\n fingerprint TEXT,\n mode TEXT,\n browser TEXT NOT NULL DEFAULT 'chromium',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS baselines_project_story_idx ON baselines (project_id, story_id);\nCREATE TABLE IF NOT EXISTS comments (\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 story_id TEXT\n);\nCREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id);\nCREATE TABLE IF NOT EXISTS build_reviews (\n id TEXT PRIMARY KEY,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n verdict TEXT NOT NULL,\n body TEXT,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS build_reviews_build_id_idx ON build_reviews (build_id);\nCREATE TABLE IF NOT EXISTS label_types (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n key TEXT NOT NULL,\n name TEXT NOT NULL,\n link_template TEXT,\n color TEXT,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS label_types_project_key_idx ON label_types (project_id, key);\nCREATE TABLE IF NOT EXISTS build_labels (\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 type_key TEXT NOT NULL,\n value TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS build_labels_build_type_value_idx ON build_labels (build_id, type_key, value);\nCREATE TABLE IF NOT EXISTS project_links (\n id TEXT PRIMARY KEY,\n downstream_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n upstream_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n package_name TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_links_downstream_package_idx ON project_links (downstream_id, package_name);\nCREATE TABLE IF NOT EXISTS design_links (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n import_path TEXT,\n label TEXT,\n url TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS design_links_project_id_idx ON design_links (project_id);\nCREATE TABLE IF NOT EXISTS design_credentials (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n token_encrypted TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'unknown',\n checked_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS design_credentials_project_provider_idx ON design_credentials (project_id, provider);\nCREATE TABLE IF NOT EXISTS design_nodes (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n viewer_id TEXT NOT NULL DEFAULT '',\n file_key TEXT NOT NULL,\n node_id TEXT NOT NULL DEFAULT '',\n name TEXT,\n version TEXT,\n last_modified TEXT,\n state TEXT NOT NULL DEFAULT 'ok',\n render_path TEXT,\n render_version TEXT,\n notified_version TEXT,\n fetched_at TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS design_nodes_project_file_node_idx ON design_nodes (project_id, provider, viewer_id, file_key, node_id);\nCREATE TABLE IF NOT EXISTS build_package_usage (\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 story_import_path TEXT NOT NULL,\n package_name TEXT NOT NULL,\n module_path TEXT NOT NULL,\n version TEXT,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS build_package_usage_build_id_idx ON build_package_usage (build_id);\nCREATE TABLE IF NOT EXISTS tokens (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n hash TEXT NOT NULL UNIQUE,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n last_used_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS webhooks (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n url TEXT NOT NULL,\n secret_encrypted TEXT NOT NULL,\n events TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS notification_channels (\n id TEXT PRIMARY KEY,\n project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n config TEXT NOT NULL,\n secret_encrypted TEXT,\n events TEXT,\n enabled INTEGER NOT NULL DEFAULT 1,\n last_error TEXT,\n last_error_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS notification_channels_project_id_idx ON notification_channels (project_id);\nCREATE TABLE IF NOT EXISTS notification_subscriptions (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n events TEXT,\n via TEXT,\n enabled INTEGER NOT NULL DEFAULT 1,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS notification_subscriptions_project_user_idx ON notification_subscriptions (project_id, user_id);\nCREATE TABLE IF NOT EXISTS insights (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT REFERENCES builds(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n window_key TEXT,\n input_hash TEXT NOT NULL,\n status TEXT NOT NULL,\n profile TEXT NOT NULL,\n model TEXT NOT NULL,\n prompt_version TEXT NOT NULL,\n verdict TEXT,\n summary TEXT,\n result TEXT,\n error_code TEXT,\n created_at TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT\n);\nCREATE INDEX IF NOT EXISTS insights_project_build_idx ON insights (project_id, build_id);\nCREATE INDEX IF NOT EXISTS insights_created_at_idx ON insights (created_at);\nCREATE UNIQUE INDEX IF NOT EXISTS insights_triage_unique_idx ON insights (project_id, build_id, kind, input_hash);\nCREATE UNIQUE INDEX IF NOT EXISTS insights_health_unique_idx ON insights (project_id, kind, window_key);\nCREATE TABLE IF NOT EXISTS ai_usage (\n id TEXT PRIMARY KEY,\n insight_id TEXT,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n profile TEXT NOT NULL,\n task TEXT NOT NULL,\n model TEXT NOT NULL,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n estimated INTEGER NOT NULL DEFAULT 0,\n images INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS ai_usage_created_at_idx ON ai_usage (created_at);\nCREATE INDEX IF NOT EXISTS ai_usage_project_created_idx ON ai_usage (project_id, created_at);\nCREATE TABLE IF NOT EXISTS ai_budget_alerts (\n id TEXT PRIMARY KEY,\n day TEXT NOT NULL,\n threshold INTEGER NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY,\n email TEXT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n avatar_url TEXT,\n role TEXT NOT NULL DEFAULT 'member',\n last_login_at TEXT,\n created_at TEXT NOT NULL,\n password_hash TEXT,\n display_name_override TEXT,\n auth_provider TEXT NOT NULL DEFAULT 'oidc',\n disabled INTEGER NOT NULL DEFAULT 0\n);\nCREATE TABLE IF NOT EXISTS user_connections (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n service TEXT NOT NULL,\n token_encrypted TEXT NOT NULL,\n refresh_token_encrypted TEXT,\n expires_at TEXT,\n status TEXT NOT NULL DEFAULT 'ok',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS user_connections_user_service_idx ON user_connections (user_id, service);\nCREATE TABLE IF NOT EXISTS user_invite_tokens (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n token_hash TEXT NOT NULL UNIQUE,\n expires_at TEXT NOT NULL,\n used_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS user_invite_tokens_user_id_idx ON user_invite_tokens (user_id);\nCREATE TABLE IF NOT EXISTS project_members (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n role TEXT NOT NULL DEFAULT 'viewer',\n source TEXT NOT NULL DEFAULT 'manual',\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_members_project_user_idx ON project_members (project_id, user_id);\nCREATE TABLE IF NOT EXISTS project_group_mappings (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n group_name TEXT NOT NULL,\n role TEXT NOT NULL DEFAULT 'viewer',\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_group_mappings_project_group_idx ON project_group_mappings (project_id, group_name);\nCREATE TABLE IF NOT EXISTS content_refs (\n hash TEXT PRIMARY KEY,\n ref_count INTEGER NOT NULL DEFAULT 1,\n last_seen_at TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS user (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n email TEXT NOT NULL UNIQUE,\n email_verified INTEGER NOT NULL DEFAULT 0,\n image TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS session (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,\n token TEXT NOT NULL UNIQUE,\n expires_at TEXT NOT NULL,\n ip_address TEXT,\n user_agent TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS session_token_idx ON session (token);\nCREATE INDEX IF NOT EXISTS session_user_id_idx ON session (user_id);\nCREATE TABLE IF NOT EXISTS account (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,\n account_id TEXT NOT NULL,\n provider_id TEXT NOT NULL,\n access_token TEXT,\n refresh_token TEXT,\n id_token TEXT,\n access_token_expires_at TEXT,\n refresh_token_expires_at TEXT,\n scope TEXT,\n expires_at TEXT,\n password TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS account_user_id_idx ON account (user_id);\nCREATE TABLE IF NOT EXISTS verification (\n id TEXT PRIMARY KEY,\n identifier TEXT NOT NULL,\n value TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS verification_identifier_idx ON verification (identifier);\nCREATE TABLE IF NOT EXISTS passkey (\n id TEXT PRIMARY KEY,\n name TEXT,\n public_key TEXT NOT NULL,\n user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,\n credential_id TEXT NOT NULL,\n counter INTEGER NOT NULL,\n device_type TEXT NOT NULL,\n backed_up INTEGER NOT NULL DEFAULT 0,\n transports TEXT,\n aaguid TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT\n);\nCREATE INDEX IF NOT EXISTS passkey_user_id_idx ON passkey (user_id);\nCREATE INDEX IF NOT EXISTS passkey_credential_id_idx ON passkey (credential_id);\nCREATE TABLE IF NOT EXISTS ssoProvider (\n id TEXT PRIMARY KEY,\n issuer TEXT NOT NULL,\n oidc_config TEXT,\n saml_config TEXT,\n user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,\n provider_id TEXT NOT NULL UNIQUE,\n organization_id TEXT,\n domain TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS ssoprovider_domain_idx ON ssoProvider (domain);\n`;\n\n/**\n * Indexes that must be created after `ensureColumns` (they reference columns\n * older volumes only gain then, or can fail on legacy duplicates). Runs the\n * browser-aware identity swap — rows default to `chromium` — plus the story-file\n * lookup index and the one-mapping-per-file unique index. Idempotent; a step\n * that fails (e.g. duplicate mappings from before the unique index) is skipped, not fatal.\n */\nexport const IDENTITY_STATEMENTS: string[] = [\n \"DROP INDEX IF EXISTS snapshots_build_story_viewport_idx\",\n \"DROP INDEX IF EXISTS baselines_project_story_viewport_branch_idx\",\n \"CREATE UNIQUE INDEX IF NOT EXISTS snapshots_build_story_mode_browser_idx ON snapshots (build_id, story_id, viewport_name, browser)\",\n \"CREATE UNIQUE INDEX IF NOT EXISTS baselines_project_story_mode_browser_branch_idx ON baselines (project_id, story_id, viewport_name, browser, branch)\",\n \"CREATE INDEX IF NOT EXISTS snapshots_project_import_path_idx ON snapshots (project_id, story_import_path)\",\n \"CREATE UNIQUE INDEX IF NOT EXISTS design_links_project_import_path_idx ON design_links (project_id, import_path)\",\n // Pre-adapter Figma tables, replaced by design_nodes / design_credentials (no data is carried over).\n \"DROP TABLE IF EXISTS figma_nodes\",\n \"DROP TABLE IF EXISTS design_tokens\",\n];\n\nconst CONSTRAINT_PREFIX = /^(?:PRIMARY|FOREIGN|UNIQUE|CHECK|CONSTRAINT)\\s/iu;\nconst TABLE_BLOCK = /CREATE TABLE IF NOT EXISTS\\s+(?<table>\\w+)\\s*\\((?<body>[\\s\\S]*?)\\n\\);/gu;\n\n/**\n * Parse the `CREATE TABLE` statements in a DDL string into a map of table name\n * → column definition fragments (e.g. `name TEXT NOT NULL`). Table-level\n * constraints are skipped; every StoryShelf column is declared inline.\n */\nexport function tableColumns(ddl: string): Map<string, string[]> {\n const tables = new Map<string, string[]>();\n for (const match of ddl.matchAll(TABLE_BLOCK)) {\n const columns = (match.groups?.[\"body\"] ?? \"\")\n .split(\"\\n\")\n .map((line) => line.trim().replace(/,$/u, \"\"))\n .filter((line) => line !== \"\" && !CONSTRAINT_PREFIX.test(line));\n const table = match.groups?.[\"table\"];\n if (table) {\n tables.set(table, columns);\n }\n }\n return tables;\n}\n"],"mappings":";;;;;AACA,MAAa,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+hBnB,MAAa,sBAAgC;CAC3C;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;AACF;AAEA,MAAM,oBAAoB;AAC1B,MAAM,cAAc;;;;;;AAOpB,SAAgB,aAAa,KAAoC;CAC/D,MAAM,yBAAS,IAAI,IAAsB;CACzC,KAAK,MAAM,SAAS,IAAI,SAAS,WAAW,GAAG;EAC7C,MAAM,WAAW,MAAM,SAAS,WAAW,GAAA,CACxC,MAAM,IAAI,CAAC,CACX,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAC,CAC7C,QAAQ,SAAS,SAAS,MAAM,CAAC,kBAAkB,KAAK,IAAI,CAAC;EAChE,MAAM,QAAQ,MAAM,SAAS;EAC7B,IAAI,OACF,OAAO,IAAI,OAAO,OAAO;CAE7B;CACA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"ddl.mjs","names":[],"sources":["../src/ddl.ts"],"sourcesContent":["/** Canonical SQLite DDL for all StoryShelf tables and indexes. */\nexport const DDL = `\nCREATE TABLE IF NOT EXISTS projects (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n slug TEXT NOT NULL UNIQUE,\n git_repository TEXT,\n git_default_branch TEXT NOT NULL DEFAULT 'main',\n pixel_threshold REAL NOT NULL DEFAULT 0.1,\n max_diff_ratio REAL NOT NULL DEFAULT 0.01,\n public_branch_regex TEXT,\n storybook_meta TEXT,\n execute_play INTEGER NOT NULL DEFAULT 0,\n play_timeout_ms INTEGER NOT NULL DEFAULT 10000,\n run_a11y INTEGER NOT NULL DEFAULT 1,\n run_tests INTEGER NOT NULL DEFAULT 0,\n detect_unstable INTEGER NOT NULL DEFAULT 1,\n browser TEXT NOT NULL DEFAULT 'chromium',\n viewports TEXT,\n browsers TEXT,\n modes_allowlist TEXT,\n automigrate INTEGER NOT NULL DEFAULT 0,\n ai_profile TEXT,\n review_enabled INTEGER NOT NULL DEFAULT 1,\n review_approvals INTEGER NOT NULL DEFAULT 1,\n storybook_url_ttl_seconds INTEGER,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS builds (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n git_sha TEXT NOT NULL,\n git_branch TEXT NOT NULL,\n is_default INTEGER NOT NULL DEFAULT 0,\n author_email TEXT,\n author_name TEXT,\n message TEXT,\n public INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL DEFAULT 'pending',\n retention_state TEXT NOT NULL DEFAULT 'available',\n snapshot_count INTEGER NOT NULL DEFAULT 0,\n changed_count INTEGER NOT NULL DEFAULT 0,\n approved_count INTEGER NOT NULL DEFAULT 0,\n rejected_count INTEGER NOT NULL DEFAULT 0,\n inherited_count INTEGER NOT NULL DEFAULT 0,\n affected_only INTEGER NOT NULL DEFAULT 1,\n baseline_sha TEXT,\n changed_files TEXT,\n affected_import_paths TEXT,\n test_results TEXT,\n capture_job_id TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS builds_project_gitsha_idx ON builds (project_id, git_sha);\nCREATE INDEX IF NOT EXISTS builds_git_branch_idx ON builds (git_branch);\nCREATE TABLE IF NOT EXISTS build_references (\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n owner_type TEXT NOT NULL CHECK (owner_type IN ('baseline', 'investigation')),\n owner_id TEXT NOT NULL,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE RESTRICT,\n PRIMARY KEY (project_id, owner_type, owner_id, build_id)\n);\nCREATE INDEX IF NOT EXISTS build_references_build_idx ON build_references (build_id);\nCREATE TABLE IF NOT EXISTS build_reference_generations (\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n owner_type TEXT NOT NULL CHECK (owner_type IN ('baseline', 'investigation')),\n owner_id TEXT NOT NULL,\n generation INTEGER NOT NULL CHECK (generation >= 0),\n PRIMARY KEY (project_id, owner_type, owner_id)\n);\nCREATE TABLE IF NOT EXISTS capture_attempts (\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 attempt_no INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'queued',\n error TEXT,\n req_id TEXT,\n story_count INTEGER NOT NULL DEFAULT 0,\n failed_count INTEGER NOT NULL DEFAULT 0,\n queued_at TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS capture_attempts_build_no_idx ON capture_attempts (build_id, attempt_no);\nCREATE INDEX IF NOT EXISTS capture_attempts_build_id_idx ON capture_attempts (build_id);\nCREATE TABLE IF NOT EXISTS capture_shards (\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 kind TEXT NOT NULL,\n ordinal INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n attempt_id TEXT,\n payload TEXT,\n result TEXT,\n error TEXT,\n started_at TEXT,\n finished_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS capture_shards_build_kind_ordinal_idx ON capture_shards (build_id, kind, ordinal);\nCREATE INDEX IF NOT EXISTS capture_shards_build_id_idx ON capture_shards (build_id);\nCREATE TABLE IF NOT EXISTS capture_jobs (\n id TEXT PRIMARY KEY,\n entry TEXT NOT NULL,\n job_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n build_id TEXT,\n shard_id TEXT,\n attempt INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT 3,\n status TEXT NOT NULL,\n visible_at TEXT,\n owner TEXT,\n payload TEXT,\n cancel_requested_at TEXT,\n error TEXT,\n started_at TEXT,\n finished_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS capture_jobs_poll_idx ON capture_jobs (kind, visible_at, status);\nCREATE INDEX IF NOT EXISTS capture_jobs_job_id_idx ON capture_jobs (job_id);\nCREATE INDEX IF NOT EXISTS capture_jobs_build_id_idx ON capture_jobs (build_id);\nCREATE INDEX IF NOT EXISTS capture_jobs_entry_created_idx ON capture_jobs (entry, created_at);\nCREATE TABLE IF NOT EXISTS site_settings (\n id TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_by TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS build_components (\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 import_path TEXT NOT NULL,\n title TEXT NOT NULL,\n story_count INTEGER NOT NULL DEFAULT 0,\n hash TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS build_components_build_id_idx ON build_components (build_id);\nCREATE INDEX IF NOT EXISTS build_components_project_import_path_idx ON build_components (project_id, import_path);\nCREATE TABLE IF NOT EXISTS capture_logs (\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 attempt_id TEXT NOT NULL REFERENCES capture_attempts(id) ON DELETE CASCADE,\n seq INTEGER NOT NULL,\n level TEXT NOT NULL DEFAULT 'info',\n message TEXT NOT NULL,\n fields TEXT,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS capture_logs_attempt_seq_idx ON capture_logs (attempt_id, seq);\nCREATE TABLE IF NOT EXISTS snapshots (\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 story_id TEXT NOT NULL,\n story_name TEXT NOT NULL,\n story_title TEXT NOT NULL,\n story_import_path TEXT,\n design_url TEXT,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n viewport_width INTEGER NOT NULL DEFAULT 1280,\n viewport_height INTEGER NOT NULL DEFAULT 720,\n screenshot_path TEXT NOT NULL,\n diff_path TEXT,\n diff_pixels INTEGER,\n diff_ratio REAL,\n diff_passed INTEGER,\n status TEXT NOT NULL DEFAULT 'pending',\n reviewed_by TEXT,\n reviewed_at TEXT,\n infra_hash TEXT,\n env_change TEXT,\n inherited INTEGER NOT NULL DEFAULT 0,\n baseline_id TEXT,\n baseline_version TEXT,\n source_build_id TEXT,\n comparison_image_path TEXT,\n comparison_metadata TEXT,\n mode TEXT,\n browser TEXT NOT NULL DEFAULT 'chromium',\n duration_ms INTEGER,\n a11y_violations TEXT,\n unstable INTEGER,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS snapshots_build_id_idx ON snapshots (build_id);\nCREATE TABLE IF NOT EXISTS baselines (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n story_id TEXT NOT NULL,\n viewport_name TEXT NOT NULL DEFAULT 'desktop',\n branch TEXT NOT NULL,\n snapshot_id TEXT,\n source_build_id TEXT,\n screenshot_path TEXT NOT NULL,\n infra_hash TEXT,\n fingerprint TEXT,\n mode TEXT,\n browser TEXT NOT NULL DEFAULT 'chromium',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS baselines_project_story_idx ON baselines (project_id, story_id);\nCREATE TABLE IF NOT EXISTS comments (\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 story_id TEXT\n);\nCREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id);\nCREATE TABLE IF NOT EXISTS build_reviews (\n id TEXT PRIMARY KEY,\n build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n verdict TEXT NOT NULL,\n body TEXT,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS build_reviews_build_id_idx ON build_reviews (build_id);\nCREATE TABLE IF NOT EXISTS label_types (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n key TEXT NOT NULL,\n name TEXT NOT NULL,\n link_template TEXT,\n color TEXT,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS label_types_project_key_idx ON label_types (project_id, key);\nCREATE TABLE IF NOT EXISTS build_labels (\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 type_key TEXT NOT NULL,\n value TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS build_labels_build_type_value_idx ON build_labels (build_id, type_key, value);\nCREATE TABLE IF NOT EXISTS project_links (\n id TEXT PRIMARY KEY,\n downstream_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n upstream_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n package_name TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_links_downstream_package_idx ON project_links (downstream_id, package_name);\nCREATE TABLE IF NOT EXISTS design_links (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n import_path TEXT,\n label TEXT,\n url TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS design_links_project_id_idx ON design_links (project_id);\nCREATE TABLE IF NOT EXISTS design_credentials (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n token_encrypted TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'unknown',\n checked_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS design_credentials_project_provider_idx ON design_credentials (project_id, provider);\nCREATE TABLE IF NOT EXISTS design_nodes (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n viewer_id TEXT NOT NULL DEFAULT '',\n file_key TEXT NOT NULL,\n node_id TEXT NOT NULL DEFAULT '',\n name TEXT,\n version TEXT,\n last_modified TEXT,\n state TEXT NOT NULL DEFAULT 'ok',\n render_path TEXT,\n render_version TEXT,\n notified_version TEXT,\n fetched_at TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS design_nodes_project_file_node_idx ON design_nodes (project_id, provider, viewer_id, file_key, node_id);\nCREATE TABLE IF NOT EXISTS build_package_usage (\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 story_import_path TEXT NOT NULL,\n package_name TEXT NOT NULL,\n module_path TEXT NOT NULL,\n version TEXT,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS build_package_usage_build_id_idx ON build_package_usage (build_id);\nCREATE TABLE IF NOT EXISTS tokens (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n hash TEXT NOT NULL UNIQUE,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n last_used_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS webhooks (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n url TEXT NOT NULL,\n secret_encrypted TEXT NOT NULL,\n events TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS notification_channels (\n id TEXT PRIMARY KEY,\n project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,\n provider TEXT NOT NULL,\n config TEXT NOT NULL,\n secret_encrypted TEXT,\n events TEXT,\n enabled INTEGER NOT NULL DEFAULT 1,\n last_error TEXT,\n last_error_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS notification_channels_project_id_idx ON notification_channels (project_id);\nCREATE TABLE IF NOT EXISTS notification_subscriptions (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n events TEXT,\n via TEXT,\n enabled INTEGER NOT NULL DEFAULT 1,\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS notification_subscriptions_project_user_idx ON notification_subscriptions (project_id, user_id);\nCREATE TABLE IF NOT EXISTS insights (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n build_id TEXT REFERENCES builds(id) ON DELETE CASCADE,\n kind TEXT NOT NULL,\n window_key TEXT,\n input_hash TEXT NOT NULL,\n status TEXT NOT NULL,\n profile TEXT NOT NULL,\n model TEXT NOT NULL,\n prompt_version TEXT NOT NULL,\n verdict TEXT,\n summary TEXT,\n result TEXT,\n error_code TEXT,\n created_at TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT\n);\nCREATE INDEX IF NOT EXISTS insights_project_build_idx ON insights (project_id, build_id);\nCREATE INDEX IF NOT EXISTS insights_created_at_idx ON insights (created_at);\nCREATE UNIQUE INDEX IF NOT EXISTS insights_triage_unique_idx ON insights (project_id, build_id, kind, input_hash);\nCREATE UNIQUE INDEX IF NOT EXISTS insights_health_unique_idx ON insights (project_id, kind, window_key);\nCREATE TABLE IF NOT EXISTS ai_usage (\n id TEXT PRIMARY KEY,\n insight_id TEXT,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n profile TEXT NOT NULL,\n task TEXT NOT NULL,\n model TEXT NOT NULL,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n estimated INTEGER NOT NULL DEFAULT 0,\n images INTEGER NOT NULL DEFAULT 0,\n status TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS ai_usage_created_at_idx ON ai_usage (created_at);\nCREATE INDEX IF NOT EXISTS ai_usage_project_created_idx ON ai_usage (project_id, created_at);\nCREATE TABLE IF NOT EXISTS ai_budget_alerts (\n id TEXT PRIMARY KEY,\n day TEXT NOT NULL,\n threshold INTEGER NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY,\n email TEXT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n avatar_url TEXT,\n role TEXT NOT NULL DEFAULT 'member',\n last_login_at TEXT,\n created_at TEXT NOT NULL,\n password_hash TEXT,\n display_name_override TEXT,\n auth_provider TEXT NOT NULL DEFAULT 'oidc',\n disabled INTEGER NOT NULL DEFAULT 0\n);\nCREATE TABLE IF NOT EXISTS user_connections (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n service TEXT NOT NULL,\n token_encrypted TEXT NOT NULL,\n refresh_token_encrypted TEXT,\n expires_at TEXT,\n status TEXT NOT NULL DEFAULT 'ok',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS user_connections_user_service_idx ON user_connections (user_id, service);\nCREATE TABLE IF NOT EXISTS user_invite_tokens (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n token_hash TEXT NOT NULL UNIQUE,\n expires_at TEXT NOT NULL,\n used_at TEXT,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS user_invite_tokens_user_id_idx ON user_invite_tokens (user_id);\nCREATE TABLE IF NOT EXISTS project_members (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n role TEXT NOT NULL DEFAULT 'viewer',\n source TEXT NOT NULL DEFAULT 'manual',\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_members_project_user_idx ON project_members (project_id, user_id);\nCREATE TABLE IF NOT EXISTS project_group_mappings (\n id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n group_name TEXT NOT NULL,\n role TEXT NOT NULL DEFAULT 'viewer',\n created_at TEXT NOT NULL\n);\nCREATE UNIQUE INDEX IF NOT EXISTS project_group_mappings_project_group_idx ON project_group_mappings (project_id, group_name);\nCREATE TABLE IF NOT EXISTS content_refs (\n hash TEXT PRIMARY KEY,\n ref_count INTEGER NOT NULL DEFAULT 1,\n last_seen_at TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS user (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n email TEXT NOT NULL UNIQUE,\n email_verified INTEGER NOT NULL DEFAULT 0,\n image TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS session (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,\n token TEXT NOT NULL UNIQUE,\n expires_at TEXT NOT NULL,\n ip_address TEXT,\n user_agent TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS session_token_idx ON session (token);\nCREATE INDEX IF NOT EXISTS session_user_id_idx ON session (user_id);\nCREATE TABLE IF NOT EXISTS account (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,\n account_id TEXT NOT NULL,\n provider_id TEXT NOT NULL,\n access_token TEXT,\n refresh_token TEXT,\n id_token TEXT,\n access_token_expires_at TEXT,\n refresh_token_expires_at TEXT,\n scope TEXT,\n expires_at TEXT,\n password TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS account_user_id_idx ON account (user_id);\nCREATE TABLE IF NOT EXISTS verification (\n id TEXT PRIMARY KEY,\n identifier TEXT NOT NULL,\n value TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS verification_identifier_idx ON verification (identifier);\nCREATE TABLE IF NOT EXISTS passkey (\n id TEXT PRIMARY KEY,\n name TEXT,\n public_key TEXT NOT NULL,\n user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,\n credential_id TEXT NOT NULL,\n counter INTEGER NOT NULL,\n device_type TEXT NOT NULL,\n backed_up INTEGER NOT NULL DEFAULT 0,\n transports TEXT,\n aaguid TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT\n);\nCREATE INDEX IF NOT EXISTS passkey_user_id_idx ON passkey (user_id);\nCREATE INDEX IF NOT EXISTS passkey_credential_id_idx ON passkey (credential_id);\nCREATE TABLE IF NOT EXISTS ssoProvider (\n id TEXT PRIMARY KEY,\n issuer TEXT NOT NULL,\n oidc_config TEXT,\n saml_config TEXT,\n user_id TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,\n provider_id TEXT NOT NULL UNIQUE,\n organization_id TEXT,\n domain TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS ssoprovider_domain_idx ON ssoProvider (domain);\n`;\n\n/**\n * Indexes that must be created after `ensureColumns` (they reference columns\n * older volumes only gain then, or can fail on legacy duplicates). Runs the\n * browser-aware identity swap — rows default to `chromium` — plus the story-file\n * lookup index and the one-mapping-per-file unique index. Idempotent; a step\n * that fails (e.g. duplicate mappings from before the unique index) is skipped, not fatal.\n */\nexport const IDENTITY_STATEMENTS: string[] = [\n \"DROP INDEX IF EXISTS snapshots_build_story_viewport_idx\",\n \"DROP INDEX IF EXISTS baselines_project_story_viewport_branch_idx\",\n \"CREATE UNIQUE INDEX IF NOT EXISTS snapshots_build_story_mode_browser_idx ON snapshots (build_id, story_id, viewport_name, browser)\",\n \"CREATE UNIQUE INDEX IF NOT EXISTS baselines_project_story_mode_browser_branch_idx ON baselines (project_id, story_id, viewport_name, browser, branch)\",\n \"CREATE INDEX IF NOT EXISTS snapshots_project_import_path_idx ON snapshots (project_id, story_import_path)\",\n \"CREATE UNIQUE INDEX IF NOT EXISTS design_links_project_import_path_idx ON design_links (project_id, import_path)\",\n // Pre-adapter Figma tables, replaced by design_nodes / design_credentials (no data is carried over).\n \"DROP TABLE IF EXISTS figma_nodes\",\n \"DROP TABLE IF EXISTS design_tokens\",\n];\n\nconst CONSTRAINT_PREFIX = /^(?:PRIMARY|FOREIGN|UNIQUE|CHECK|CONSTRAINT)\\s/iu;\nconst TABLE_BLOCK = /CREATE TABLE IF NOT EXISTS\\s+(?<table>\\w+)\\s*\\((?<body>[\\s\\S]*?)\\n\\);/gu;\n\n/**\n * Parse the `CREATE TABLE` statements in a DDL string into a map of table name\n * → column definition fragments (e.g. `name TEXT NOT NULL`). Table-level\n * constraints are skipped; every StoryShelf column is declared inline.\n */\nexport function tableColumns(ddl: string): Map<string, string[]> {\n const tables = new Map<string, string[]>();\n for (const match of ddl.matchAll(TABLE_BLOCK)) {\n const columns = (match.groups?.[\"body\"] ?? \"\")\n .split(\"\\n\")\n .map((line) => line.trim().replace(/,$/u, \"\"))\n .filter((line) => line !== \"\" && !CONSTRAINT_PREFIX.test(line));\n const table = match.groups?.[\"table\"];\n if (table) {\n tables.set(table, columns);\n }\n }\n return tables;\n}\n"],"mappings":";;;;;AACA,MAAa,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgiBnB,MAAa,sBAAgC;CAC3C;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;AACF;AAEA,MAAM,oBAAoB;AAC1B,MAAM,cAAc;;;;;;AAOpB,SAAgB,aAAa,KAAoC;CAC/D,MAAM,yBAAS,IAAI,IAAsB;CACzC,KAAK,MAAM,SAAS,IAAI,SAAS,WAAW,GAAG;EAC7C,MAAM,WAAW,MAAM,SAAS,WAAW,GAAA,CACxC,MAAM,IAAI,CAAC,CACX,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC,QAAQ,OAAO,EAAE,CAAC,CAAC,CAC7C,QAAQ,SAAS,SAAS,MAAM,CAAC,kBAAkB,KAAK,IAAI,CAAC;EAChE,MAAM,QAAQ,MAAM,SAAS;EAC7B,IAAI,OACF,OAAO,IAAI,OAAO,OAAO;CAE7B;CACA,OAAO;AACT"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
+
import { CAPTURE_JOB_BACKFILL } from "./capture-job-backfill.mjs";
|
|
4
5
|
import { DDL, IDENTITY_STATEMENTS, tableColumns } from "./ddl.mjs";
|
|
5
6
|
import { createDrizzleAdapter } from "./drizzle-factory.mjs";
|
|
6
7
|
import { ensureDbDir } from "./ensure-db-dir.mjs";
|
|
@@ -24,7 +25,7 @@ function createSqliteDatabase(path) {
|
|
|
24
25
|
return createDrizzleAdapter(db, {
|
|
25
26
|
metadata: {
|
|
26
27
|
name: "SQLite",
|
|
27
|
-
version: "0.9.0
|
|
28
|
+
version: "0.9.0",
|
|
28
29
|
description: "SQLite database adapter (node:sqlite + Drizzle)",
|
|
29
30
|
kind: "sqlite",
|
|
30
31
|
category: "database"
|
|
@@ -115,6 +116,7 @@ function ensureColumns(sqlite, ddl) {
|
|
|
115
116
|
/** Swap in the browser-aware unique indexes (after `ensureColumns` added the column). */
|
|
116
117
|
function applyIdentityStatements(sqlite) {
|
|
117
118
|
for (const statement of IDENTITY_STATEMENTS) execIgnore(sqlite, statement);
|
|
119
|
+
sqlite.exec(CAPTURE_JOB_BACKFILL);
|
|
118
120
|
}
|
|
119
121
|
function runMigrations(sqlite) {
|
|
120
122
|
sqlite.exec("PRAGMA foreign_keys = ON");
|
package/dist/index.mjs.map
CHANGED
|
@@ -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, IDENTITY_STATEMENTS, 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: typeof __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\nexport function 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\n/** Swap in the browser-aware unique indexes (after `ensureColumns` added the column). */\nfunction applyIdentityStatements(sqlite: DatabaseSync): void {\n for (const statement of IDENTITY_STATEMENTS) {\n execIgnore(sqlite, statement);\n }\n}\n\nfunction runMigrations(sqlite: DatabaseSync): void {\n sqlite.exec(\"PRAGMA foreign_keys = ON\");\n sqlite.exec(DDL);\n ensureColumns(sqlite, DDL);\n applyIdentityStatements(sqlite);\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":"
|
|
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 { CAPTURE_JOB_BACKFILL } from \"./capture-job-backfill.ts\";\nimport { DDL, IDENTITY_STATEMENTS, 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: typeof __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\nexport function 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\n/** Swap in the browser-aware unique indexes (after `ensureColumns` added the column). */\nfunction applyIdentityStatements(sqlite: DatabaseSync): void {\n for (const statement of IDENTITY_STATEMENTS) {\n execIgnore(sqlite, statement);\n }\n sqlite.exec(CAPTURE_JOB_BACKFILL); // not ignored: see its doc\n}\n\nfunction runMigrations(sqlite: DatabaseSync): void {\n sqlite.exec(\"PRAGMA foreign_keys = ON\");\n sqlite.exec(DDL);\n ensureColumns(sqlite, DDL);\n applyIdentityStatements(sqlite);\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":";;;;;;;;;;;;;;;;;AAiBA,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,SAAA;GACA,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,SAAgB,eAAe,QAA2C;CACxE,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;;AAGA,SAAS,wBAAwB,QAA4B;CAC3D,KAAK,MAAM,aAAa,qBACtB,WAAW,QAAQ,SAAS;CAE9B,OAAO,KAAK,oBAAoB;AAClC;AAEA,SAAS,cAAc,QAA4B;CACjD,OAAO,KAAK,0BAA0B;CACtC,OAAO,KAAK,GAAG;CACf,cAAc,QAAQ,GAAG;CACzB,wBAAwB,MAAM;CAC9B,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/dist/schema/build.d.mts
CHANGED
|
@@ -390,6 +390,25 @@ declare const builds: import("drizzle-orm/sqlite-core/table.js").SQLiteTableWith
|
|
|
390
390
|
}, {}, {
|
|
391
391
|
length: number | undefined;
|
|
392
392
|
}>;
|
|
393
|
+
captureJobId: import("drizzle-orm/sqlite-core/index.js").SQLiteColumn<{
|
|
394
|
+
name: "capture_job_id";
|
|
395
|
+
tableName: "builds";
|
|
396
|
+
dataType: "string";
|
|
397
|
+
columnType: "SQLiteText";
|
|
398
|
+
data: string;
|
|
399
|
+
driverParam: string;
|
|
400
|
+
notNull: false;
|
|
401
|
+
hasDefault: false;
|
|
402
|
+
isPrimaryKey: false;
|
|
403
|
+
isAutoincrement: false;
|
|
404
|
+
hasRuntimeDefault: false;
|
|
405
|
+
enumValues: [string, ...string[]];
|
|
406
|
+
baseColumn: never;
|
|
407
|
+
identity: undefined;
|
|
408
|
+
generated: undefined;
|
|
409
|
+
}, {}, {
|
|
410
|
+
length: number | undefined;
|
|
411
|
+
}>;
|
|
393
412
|
createdAt: import("drizzle-orm/sqlite-core/index.js").SQLiteColumn<{
|
|
394
413
|
name: "created_at";
|
|
395
414
|
tableName: "builds";
|
|
@@ -454,6 +473,7 @@ interface Build {
|
|
|
454
473
|
changedFiles: string | null;
|
|
455
474
|
affectedImportPaths: string | null;
|
|
456
475
|
testResults: string | null;
|
|
476
|
+
captureJobId: string | null;
|
|
457
477
|
createdAt: string;
|
|
458
478
|
updatedAt: string;
|
|
459
479
|
}
|
package/dist/schema/build.mjs
CHANGED
|
@@ -28,6 +28,7 @@ const builds = sqliteTable("builds", {
|
|
|
28
28
|
changedFiles: text("changed_files"),
|
|
29
29
|
affectedImportPaths: text("affected_import_paths"),
|
|
30
30
|
testResults: text("test_results"),
|
|
31
|
+
captureJobId: text("capture_job_id"),
|
|
31
32
|
createdAt: text("created_at").notNull(),
|
|
32
33
|
updatedAt: text("updated_at").notNull()
|
|
33
34
|
}, (t) => [index("builds_project_gitsha_idx").on(t.projectId, t.gitSha), index("builds_git_branch_idx").on(t.gitBranch)]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.mjs","names":[],"sources":["../../src/schema/build.ts"],"sourcesContent":["import type { BuildStatus } from \"@storyshelf/core/types\";\nimport { index, integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\nimport { projects } from \"./project.ts\";\n\n/** Narrow `builds` table definition. */\nexport const builds = sqliteTable(\n \"builds\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n gitSha: text(\"git_sha\").notNull(),\n gitBranch: text(\"git_branch\").notNull(),\n isDefault: integer(\"is_default\", { mode: \"boolean\" }).notNull().default(false),\n authorEmail: text(\"author_email\"),\n authorName: text(\"author_name\"),\n message: text(\"message\"),\n public: integer(\"public\", { mode: \"boolean\" }).notNull().default(false),\n status: text(\"status\").$type<BuildStatus>().notNull().default(\"pending\"),\n retentionState: text(\"retention_state\").notNull().default(\"available\"),\n snapshotCount: integer(\"snapshot_count\").notNull().default(0),\n changedCount: integer(\"changed_count\").notNull().default(0),\n approvedCount: integer(\"approved_count\").notNull().default(0),\n rejectedCount: integer(\"rejected_count\").notNull().default(0),\n /** Snapshots inherited from the baseline without rendering (affected capture). */\n inheritedCount: integer(\"inherited_count\").notNull().default(0),\n affectedOnly: integer(\"affected_only\", { mode: \"boolean\" }).notNull().default(true),\n baselineSha: text(\"baseline_sha\"),\n changedFiles: text(\"changed_files\"),\n affectedImportPaths: text(\"affected_import_paths\"),\n testResults: text(\"test_results\"),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n },\n (t) => [\n index(\"builds_project_gitsha_idx\").on(t.projectId, t.gitSha),\n index(\"builds_git_branch_idx\").on(t.gitBranch),\n ],\n);\n\n/** A build row. */\nexport interface Build {\n id: string;\n projectId: string;\n gitSha: string;\n gitBranch: string;\n isDefault: boolean;\n authorEmail: string | null;\n authorName: string | null;\n message: string | null;\n public: boolean;\n status: BuildStatus;\n retentionState: string;\n snapshotCount: number;\n changedCount: number;\n approvedCount: number;\n rejectedCount: number;\n inheritedCount: number;\n affectedOnly: boolean;\n baselineSha: string | null;\n changedFiles: string | null;\n affectedImportPaths: string | null;\n testResults: string | null;\n createdAt: string;\n updatedAt: string;\n}\n"],"mappings":";;;;;;;AAKA,MAAa,SAAS,YACpB,UACA;CACE,IAAI,KAAK,IAAI,CAAC,CAAC,WAAW;CAC1B,WAAW,KAAK,YAAY,CAAC,CAC1B,QAAQ,CAAC,CACT,iBAAiB,SAAS,IAAI,EAAE,UAAU,UAAU,CAAC;CACxD,QAAQ,KAAK,SAAS,CAAC,CAAC,QAAQ;CAChC,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ;CACtC,WAAW,QAAQ,cAAc,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,KAAK;CAC7E,aAAa,KAAK,cAAc;CAChC,YAAY,KAAK,aAAa;CAC9B,SAAS,KAAK,SAAS;CACvB,QAAQ,QAAQ,UAAU,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,KAAK;CACtE,QAAQ,KAAK,QAAQ,CAAC,CAAC,MAAmB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,SAAS;CACvE,gBAAgB,KAAK,iBAAiB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,WAAW;CACrE,eAAe,QAAQ,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CAC5D,cAAc,QAAQ,eAAe,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CAC1D,eAAe,QAAQ,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CAC5D,eAAe,QAAQ,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;;CAE5D,gBAAgB,QAAQ,iBAAiB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CAC9D,cAAc,QAAQ,iBAAiB,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,IAAI;CAClF,aAAa,KAAK,cAAc;CAChC,cAAc,KAAK,eAAe;CAClC,qBAAqB,KAAK,uBAAuB;CACjD,aAAa,KAAK,cAAc;CAChC,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ;CACtC,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ;AACxC,IACC,MAAM,CACL,MAAM,2BAA2B,CAAC,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,GAC3D,MAAM,uBAAuB,CAAC,CAAC,GAAG,EAAE,SAAS,CAC/C,CACF"}
|
|
1
|
+
{"version":3,"file":"build.mjs","names":[],"sources":["../../src/schema/build.ts"],"sourcesContent":["import type { BuildStatus } from \"@storyshelf/core/types\";\nimport { index, integer, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\nimport { projects } from \"./project.ts\";\n\n/** Narrow `builds` table definition. */\nexport const builds = sqliteTable(\n \"builds\",\n {\n id: text(\"id\").primaryKey(),\n projectId: text(\"project_id\")\n .notNull()\n .references(() => projects.id, { onDelete: \"cascade\" }),\n gitSha: text(\"git_sha\").notNull(),\n gitBranch: text(\"git_branch\").notNull(),\n isDefault: integer(\"is_default\", { mode: \"boolean\" }).notNull().default(false),\n authorEmail: text(\"author_email\"),\n authorName: text(\"author_name\"),\n message: text(\"message\"),\n public: integer(\"public\", { mode: \"boolean\" }).notNull().default(false),\n status: text(\"status\").$type<BuildStatus>().notNull().default(\"pending\"),\n retentionState: text(\"retention_state\").notNull().default(\"available\"),\n snapshotCount: integer(\"snapshot_count\").notNull().default(0),\n changedCount: integer(\"changed_count\").notNull().default(0),\n approvedCount: integer(\"approved_count\").notNull().default(0),\n rejectedCount: integer(\"rejected_count\").notNull().default(0),\n /** Snapshots inherited from the baseline without rendering (affected capture). */\n inheritedCount: integer(\"inherited_count\").notNull().default(0),\n affectedOnly: integer(\"affected_only\", { mode: \"boolean\" }).notNull().default(true),\n baselineSha: text(\"baseline_sha\"),\n changedFiles: text(\"changed_files\"),\n affectedImportPaths: text(\"affected_import_paths\"),\n testResults: text(\"test_results\"),\n captureJobId: text(\"capture_job_id\"),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n },\n (t) => [\n index(\"builds_project_gitsha_idx\").on(t.projectId, t.gitSha),\n index(\"builds_git_branch_idx\").on(t.gitBranch),\n ],\n);\n\n/** A build row. */\nexport interface Build {\n id: string;\n projectId: string;\n gitSha: string;\n gitBranch: string;\n isDefault: boolean;\n authorEmail: string | null;\n authorName: string | null;\n message: string | null;\n public: boolean;\n status: BuildStatus;\n retentionState: string;\n snapshotCount: number;\n changedCount: number;\n approvedCount: number;\n rejectedCount: number;\n inheritedCount: number;\n affectedOnly: boolean;\n baselineSha: string | null;\n changedFiles: string | null;\n affectedImportPaths: string | null;\n testResults: string | null;\n captureJobId: string | null;\n createdAt: string;\n updatedAt: string;\n}\n"],"mappings":";;;;;;;AAKA,MAAa,SAAS,YACpB,UACA;CACE,IAAI,KAAK,IAAI,CAAC,CAAC,WAAW;CAC1B,WAAW,KAAK,YAAY,CAAC,CAC1B,QAAQ,CAAC,CACT,iBAAiB,SAAS,IAAI,EAAE,UAAU,UAAU,CAAC;CACxD,QAAQ,KAAK,SAAS,CAAC,CAAC,QAAQ;CAChC,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ;CACtC,WAAW,QAAQ,cAAc,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,KAAK;CAC7E,aAAa,KAAK,cAAc;CAChC,YAAY,KAAK,aAAa;CAC9B,SAAS,KAAK,SAAS;CACvB,QAAQ,QAAQ,UAAU,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,KAAK;CACtE,QAAQ,KAAK,QAAQ,CAAC,CAAC,MAAmB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,SAAS;CACvE,gBAAgB,KAAK,iBAAiB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,WAAW;CACrE,eAAe,QAAQ,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CAC5D,cAAc,QAAQ,eAAe,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CAC1D,eAAe,QAAQ,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CAC5D,eAAe,QAAQ,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;;CAE5D,gBAAgB,QAAQ,iBAAiB,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC;CAC9D,cAAc,QAAQ,iBAAiB,EAAE,MAAM,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,QAAQ,IAAI;CAClF,aAAa,KAAK,cAAc;CAChC,cAAc,KAAK,eAAe;CAClC,qBAAqB,KAAK,uBAAuB;CACjD,aAAa,KAAK,cAAc;CAChC,cAAc,KAAK,gBAAgB;CACnC,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ;CACtC,WAAW,KAAK,YAAY,CAAC,CAAC,QAAQ;AACxC,IACC,MAAM,CACL,MAAM,2BAA2B,CAAC,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,GAC3D,MAAM,uBAAuB,CAAC,CAAC,GAAG,EAAE,SAAS,CAC/C,CACF"}
|
|
@@ -164,7 +164,7 @@ declare const captureJobs: import("drizzle-orm/sqlite-core").SQLiteTableWithColu
|
|
|
164
164
|
tableName: "capture_jobs";
|
|
165
165
|
dataType: "string";
|
|
166
166
|
columnType: "SQLiteText";
|
|
167
|
-
data: "cancelled" | "done" | "expired" | "failed" | "queued" | "running";
|
|
167
|
+
data: "cancelled" | "done" | "exhausting" | "expired" | "failed" | "queued" | "running";
|
|
168
168
|
driverParam: string;
|
|
169
169
|
notNull: true;
|
|
170
170
|
hasDefault: false;
|
|
@@ -177,7 +177,7 @@ declare const captureJobs: import("drizzle-orm/sqlite-core").SQLiteTableWithColu
|
|
|
177
177
|
generated: undefined;
|
|
178
178
|
}, {}, {
|
|
179
179
|
length: number | undefined;
|
|
180
|
-
$type: "cancelled" | "done" | "expired" | "failed" | "queued" | "running";
|
|
180
|
+
$type: "cancelled" | "done" | "exhausting" | "expired" | "failed" | "queued" | "running";
|
|
181
181
|
}>;
|
|
182
182
|
visibleAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
183
183
|
name: "visible_at";
|
package/dist/turso.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
+
import { CAPTURE_JOB_BACKFILL } from "./capture-job-backfill.mjs";
|
|
4
5
|
import { DDL, IDENTITY_STATEMENTS, tableColumns } from "./ddl.mjs";
|
|
5
6
|
import { createDrizzleAdapter } from "./drizzle-factory.mjs";
|
|
6
7
|
import { schema } from "./schema.mjs";
|
|
@@ -29,7 +30,7 @@ function createTursoDatabase(options) {
|
|
|
29
30
|
return createDrizzleAdapter(db, {
|
|
30
31
|
metadata: {
|
|
31
32
|
name: "Turso",
|
|
32
|
-
version: "0.9.0
|
|
33
|
+
version: "0.9.0",
|
|
33
34
|
description: "Turso/libSQL database adapter",
|
|
34
35
|
kind: "turso",
|
|
35
36
|
category: "database"
|
|
@@ -85,6 +86,7 @@ async function runMigrations(client) {
|
|
|
85
86
|
await client.executeMultiple(DDL);
|
|
86
87
|
await ensureColumns(client, DDL);
|
|
87
88
|
for (const statement of IDENTITY_STATEMENTS) await execIgnore(client, statement);
|
|
89
|
+
await client.execute(CAPTURE_JOB_BACKFILL);
|
|
88
90
|
await execIgnore(client, WEBHOOK_SECRET_DROP);
|
|
89
91
|
await migrateCommentsTable(client);
|
|
90
92
|
}
|
package/dist/turso.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turso.mjs","names":[],"sources":["../src/turso.ts"],"sourcesContent":["import type { Client } from \"@libsql/client\";\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport * as drizzleLibsqlCore from \"drizzle-orm/libsql/driver-core\";\n/**\n * Turso/libSQL preset for the SQLite database adapter.\n *\n * Same schema, DDL, and Drizzle factory as the `node:sqlite` default;\n * only the transport differs (`@libsql/client`: `file:`, `libsql://`,\n * `wss://`, or embedded replica via `syncUrl`). Requires the optional\n * `@libsql/client` peer (`nub add @libsql/client`).\n */\nimport { createRequire } from \"node:module\";\nimport { DDL, IDENTITY_STATEMENTS, tableColumns } from \"./ddl.ts\";\nimport { createDrizzleAdapter } from \"./drizzle-factory.ts\";\nimport { schema } from \"./schema/index.ts\";\n\ndeclare const __PKG_VERSION__: string | undefined;\n\ntype LibsqlClient = Client;\ntype CreateClient = typeof import(\"@libsql/client\").createClient;\ntype Construct = (client: LibsqlClient, config: { schema: typeof schema }) => unknown;\n\n// `driver-core` exports `construct` at runtime but omits it from its typings.\nconst construct = (drizzleLibsqlCore as unknown as { construct: Construct }).construct;\n\n/**\n * Options for creating a Turso-backed DatabaseAdapter.\n *\n * When `client` is supplied, all other connection options are ignored and\n * the caller owns the lifecycle of the client (teardown is a no-op). This\n * is the hook for embedded replicas with custom sync loops and hermetic\n * tests.\n */\nexport interface TursoDatabaseOptions {\n url?: string;\n authToken?: string;\n syncUrl?: string;\n syncInterval?: number;\n client?: LibsqlClient;\n}\n\n/**\n * Create a Turso/libSQL-backed DatabaseAdapter using Drizzle ORM.\n *\n * @param options - Either a connection `url` (plus `authToken`/`syncUrl`/\n * `syncInterval`) or a preconfigured libSQL `client`.\n * @returns A DatabaseAdapter backed by the Turso database.\n */\nexport function createTursoDatabase(options: TursoDatabaseOptions): DatabaseAdapter {\n const { client, owned } = resolveClient(options);\n const db = construct(client, { schema });\n\n return createDrizzleAdapter(db, {\n metadata: {\n name: \"Turso\",\n version: typeof __PKG_VERSION__ === \"string\" ? __PKG_VERSION__ : \"0.0.0\",\n description: \"Turso/libSQL database adapter\",\n kind: \"turso\",\n category: \"database\",\n },\n tables: schema,\n migrate: async () => {\n await runMigrations(client);\n },\n close: () => {\n if (owned) {\n client.close();\n }\n },\n ping: async () => {\n await client.execute(\"SELECT 1\");\n },\n });\n}\n\nconst SUPPORTED_URL = /^(?<proto>libsql|https?|wss?):\\/\\/|^file:|^:memory:/u;\n\nfunction isSupportedUrl(url: string): boolean {\n return SUPPORTED_URL.test(url);\n}\n\n/** Resolve the libSQL client, distinguishing caller-owned from owned. */\nfunction resolveClient(options: TursoDatabaseOptions): { client: LibsqlClient; owned: boolean } {\n if (options.client) {\n return { client: options.client, owned: false };\n }\n const url = options.url?.trim();\n if (!url) {\n throw new Error(\n \"createTursoDatabase: provide `url` (or `syncUrl` with a `file:` url) or a preconfigured `client`\",\n );\n }\n if (!isSupportedUrl(url)) {\n throw new Error(\n `createTursoDatabase: unsupported url \"${url}\" (expected libsql://, https://, wss://, file:, or :memory:)`,\n );\n }\n return {\n client: createNodeClient({\n url,\n authToken: options.authToken,\n syncUrl: options.syncUrl,\n syncInterval: options.syncInterval,\n }),\n owned: true,\n };\n}\n\n/**\n * Load the default Node client only when a URL is used. Importing\n * `@libsql/client` at module load pulls in its native addon, which makes a\n * process that only injects a `sqlite3` client (tests, embedded replicas)\n * crash on teardown under some Node versions.\n */\nlet nodeCreateClient: CreateClient | undefined;\n\nfunction createNodeClient(config: Parameters<CreateClient>[0]): Client {\n nodeCreateClient ??= (\n createRequire(import.meta.url)(\"@libsql/client\") as { createClient: CreateClient }\n ).createClient;\n return nodeCreateClient(config);\n}\n\nconst WEBHOOK_SECRET_DROP = \"ALTER TABLE webhooks DROP COLUMN secret\";\n\nasync function runMigrations(client: LibsqlClient): Promise<void> {\n await client.executeMultiple(DDL);\n await ensureColumns(client, DDL);\n for (const statement of IDENTITY_STATEMENTS) {\n // oxlint-disable-next-line no-await-in-loop -- ordered: drop old indexes, then create new\n await execIgnore(client, statement);\n }\n await execIgnore(client, WEBHOOK_SECRET_DROP);\n await migrateCommentsTable(client);\n}\n\n/** Add any DDL column missing from a volume created before it shipped. */\nasync function ensureColumns(client: LibsqlClient, ddl: string): Promise<void> {\n const inspected = await Promise.all(\n [...tableColumns(ddl)].map(async ([table, columns]) => {\n const result = await client.execute(`PRAGMA table_info(${table})`);\n const rows = result.rows as unknown as { name: unknown }[];\n const existing = new Set(rows.map((row) => String(row.name)));\n return { table, columns, existing };\n }),\n );\n const statements = inspected.flatMap(({ table, columns, existing }) =>\n columns\n .filter((column) => isMissing(column, existing))\n .map((column) => [table, column] as const),\n );\n await Promise.all(\n statements.map(async ([table, column]) => {\n await execIgnore(client, `ALTER TABLE ${table} ADD COLUMN ${column}`);\n }),\n );\n}\n\nfunction isMissing(column: string, existing: Set<string>): boolean {\n const [name] = column.split(/\\s+/u);\n return name !== undefined && !existing.has(name);\n}\n\nasync function execIgnore(client: LibsqlClient, sql: string): Promise<void> {\n try {\n await client.execute(sql);\n } catch {\n // idempotent — already migrated\n }\n}\n\nasync function migrateCommentsTable(client: LibsqlClient): Promise<void> {\n try {\n const result = await client.execute(\n \"SELECT sql FROM sqlite_master WHERE type='table' AND name='comments'\",\n );\n const sql = (result.rows[0] as unknown as { sql: string } | undefined)?.sql;\n if (!sql?.includes(\"user_id TEXT NOT NULL REFERENCES users\")) {\n return;\n }\n await recreateCommentsTable(client);\n } catch {\n await execIgnore(client, \"PRAGMA foreign_keys = ON\");\n }\n}\n\nasync function recreateCommentsTable(client: LibsqlClient): Promise<void> {\n await client.execute(\"PRAGMA foreign_keys = OFF\");\n await client.execute(`\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 await client.execute(\"INSERT INTO comments_new SELECT * FROM comments\");\n await client.execute(\"DROP TABLE comments\");\n await client.execute(\"ALTER TABLE comments_new RENAME TO comments\");\n await client.execute(\"CREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id)\");\n await client.execute(\"PRAGMA foreign_keys = ON\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAuBA,MAAM,YAAa,kBAA0D;;;;;;;;AAyB7E,SAAgB,oBAAoB,SAAgD;CAClF,MAAM,EAAE,QAAQ,UAAU,cAAc,OAAO;CAC/C,MAAM,KAAK,UAAU,QAAQ,EAAE,OAAO,CAAC;CAEvC,OAAO,qBAAqB,IAAI;EAC9B,UAAU;GACR,MAAM;GACN,SAAA;GACA,aAAa;GACb,MAAM;GACN,UAAU;EACZ;EACA,QAAQ;EACR,SAAS,YAAY;GACnB,MAAM,cAAc,MAAM;EAC5B;EACA,aAAa;GACX,IAAI,OACF,OAAO,MAAM;EAEjB;EACA,MAAM,YAAY;GAChB,MAAM,OAAO,QAAQ,UAAU;EACjC;CACF,CAAC;AACH;AAEA,MAAM,gBAAgB;AAEtB,SAAS,eAAe,KAAsB;CAC5C,OAAO,cAAc,KAAK,GAAG;AAC/B;;AAGA,SAAS,cAAc,SAAyE;CAC9F,IAAI,QAAQ,QACV,OAAO;EAAE,QAAQ,QAAQ;EAAQ,OAAO;CAAM;CAEhD,MAAM,MAAM,QAAQ,KAAK,KAAK;CAC9B,IAAI,CAAC,KACH,MAAM,IAAI,MACR,kGACF;CAEF,IAAI,CAAC,eAAe,GAAG,GACrB,MAAM,IAAI,MACR,yCAAyC,IAAI,6DAC/C;CAEF,OAAO;EACL,QAAQ,iBAAiB;GACvB;GACA,WAAW,QAAQ;GACnB,SAAS,QAAQ;GACjB,cAAc,QAAQ;EACxB,CAAC;EACD,OAAO;CACT;AACF;;;;;;;AAQA,IAAI;AAEJ,SAAS,iBAAiB,QAA6C;CACrE,qBACE,cAAc,YAAY,GAAG,CAAC,CAAC,gBAAgB,CAAC,CAChD;CACF,OAAO,iBAAiB,MAAM;AAChC;AAEA,MAAM,sBAAsB;AAE5B,eAAe,cAAc,QAAqC;CAChE,MAAM,OAAO,gBAAgB,GAAG;CAChC,MAAM,cAAc,QAAQ,GAAG;CAC/B,KAAK,MAAM,aAAa,qBAEtB,MAAM,WAAW,QAAQ,SAAS;CAEpC,MAAM,WAAW,QAAQ,mBAAmB;CAC5C,MAAM,qBAAqB,MAAM;AACnC;;AAGA,eAAe,cAAc,QAAsB,KAA4B;CAS7E,MAAM,cAAa,MARK,QAAQ,IAC9B,CAAC,GAAG,aAAa,GAAG,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,OAAO,aAAa;EAErD,MAAM,QAAO,MADQ,OAAO,QAAQ,qBAAqB,MAAM,EAAE,EAAA,CAC7C;EAEpB,OAAO;GAAE;GAAO;GAAS,UAAA,IADJ,IAAI,KAAK,KAAK,QAAQ,OAAO,IAAI,IAAI,CAAC,CAC3B;EAAE;CACpC,CAAC,CACH,EAAA,CAC6B,SAAS,EAAE,OAAO,SAAS,eACtD,QACG,QAAQ,WAAW,UAAU,QAAQ,QAAQ,CAAC,CAAC,CAC/C,KAAK,WAAW,CAAC,OAAO,MAAM,CAAU,CAC7C;CACA,MAAM,QAAQ,IACZ,WAAW,IAAI,OAAO,CAAC,OAAO,YAAY;EACxC,MAAM,WAAW,QAAQ,eAAe,MAAM,cAAc,QAAQ;CACtE,CAAC,CACH;AACF;AAEA,SAAS,UAAU,QAAgB,UAAgC;CACjE,MAAM,CAAC,QAAQ,OAAO,MAAM,MAAM;CAClC,OAAO,SAAS,KAAA,KAAa,CAAC,SAAS,IAAI,IAAI;AACjD;AAEA,eAAe,WAAW,QAAsB,KAA4B;CAC1E,IAAI;EACF,MAAM,OAAO,QAAQ,GAAG;CAC1B,QAAQ,CAER;AACF;AAEA,eAAe,qBAAqB,QAAqC;CACvE,IAAI;EAKF,IAAI,GADS,MAHQ,OAAO,QAC1B,sEACF,EAAA,CACoB,KAAK,EAAE,EAA6C,IAAA,EAC9D,SAAS,wCAAwC,GACzD;EAEF,MAAM,sBAAsB,MAAM;CACpC,QAAQ;EACN,MAAM,WAAW,QAAQ,0BAA0B;CACrD;AACF;AAEA,eAAe,sBAAsB,QAAqC;CACxE,MAAM,OAAO,QAAQ,2BAA2B;CAChD,MAAM,OAAO,QAAQ;;;;;;;;;;;;;GAapB;CACD,MAAM,OAAO,QAAQ,iDAAiD;CACtE,MAAM,OAAO,QAAQ,qBAAqB;CAC1C,MAAM,OAAO,QAAQ,6CAA6C;CAClE,MAAM,OAAO,QAAQ,yEAAyE;CAC9F,MAAM,OAAO,QAAQ,0BAA0B;AACjD"}
|
|
1
|
+
{"version":3,"file":"turso.mjs","names":[],"sources":["../src/turso.ts"],"sourcesContent":["import type { Client } from \"@libsql/client\";\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport * as drizzleLibsqlCore from \"drizzle-orm/libsql/driver-core\";\n/**\n * Turso/libSQL preset for the SQLite database adapter.\n *\n * Same schema, DDL, and Drizzle factory as the `node:sqlite` default;\n * only the transport differs (`@libsql/client`: `file:`, `libsql://`,\n * `wss://`, or embedded replica via `syncUrl`). Requires the optional\n * `@libsql/client` peer (`nub add @libsql/client`).\n */\nimport { createRequire } from \"node:module\";\nimport { CAPTURE_JOB_BACKFILL } from \"./capture-job-backfill.ts\";\nimport { DDL, IDENTITY_STATEMENTS, tableColumns } from \"./ddl.ts\";\nimport { createDrizzleAdapter } from \"./drizzle-factory.ts\";\nimport { schema } from \"./schema/index.ts\";\n\ndeclare const __PKG_VERSION__: string | undefined;\n\ntype LibsqlClient = Client;\ntype CreateClient = typeof import(\"@libsql/client\").createClient;\ntype Construct = (client: LibsqlClient, config: { schema: typeof schema }) => unknown;\n\n// `driver-core` exports `construct` at runtime but omits it from its typings.\nconst construct = (drizzleLibsqlCore as unknown as { construct: Construct }).construct;\n\n/**\n * Options for creating a Turso-backed DatabaseAdapter.\n *\n * When `client` is supplied, all other connection options are ignored and\n * the caller owns the lifecycle of the client (teardown is a no-op). This\n * is the hook for embedded replicas with custom sync loops and hermetic\n * tests.\n */\nexport interface TursoDatabaseOptions {\n url?: string;\n authToken?: string;\n syncUrl?: string;\n syncInterval?: number;\n client?: LibsqlClient;\n}\n\n/**\n * Create a Turso/libSQL-backed DatabaseAdapter using Drizzle ORM.\n *\n * @param options - Either a connection `url` (plus `authToken`/`syncUrl`/\n * `syncInterval`) or a preconfigured libSQL `client`.\n * @returns A DatabaseAdapter backed by the Turso database.\n */\nexport function createTursoDatabase(options: TursoDatabaseOptions): DatabaseAdapter {\n const { client, owned } = resolveClient(options);\n const db = construct(client, { schema });\n\n return createDrizzleAdapter(db, {\n metadata: {\n name: \"Turso\",\n version: typeof __PKG_VERSION__ === \"string\" ? __PKG_VERSION__ : \"0.0.0\",\n description: \"Turso/libSQL database adapter\",\n kind: \"turso\",\n category: \"database\",\n },\n tables: schema,\n migrate: async () => {\n await runMigrations(client);\n },\n close: () => {\n if (owned) {\n client.close();\n }\n },\n ping: async () => {\n await client.execute(\"SELECT 1\");\n },\n });\n}\n\nconst SUPPORTED_URL = /^(?<proto>libsql|https?|wss?):\\/\\/|^file:|^:memory:/u;\n\nfunction isSupportedUrl(url: string): boolean {\n return SUPPORTED_URL.test(url);\n}\n\n/** Resolve the libSQL client, distinguishing caller-owned from owned. */\nfunction resolveClient(options: TursoDatabaseOptions): { client: LibsqlClient; owned: boolean } {\n if (options.client) {\n return { client: options.client, owned: false };\n }\n const url = options.url?.trim();\n if (!url) {\n throw new Error(\n \"createTursoDatabase: provide `url` (or `syncUrl` with a `file:` url) or a preconfigured `client`\",\n );\n }\n if (!isSupportedUrl(url)) {\n throw new Error(\n `createTursoDatabase: unsupported url \"${url}\" (expected libsql://, https://, wss://, file:, or :memory:)`,\n );\n }\n return {\n client: createNodeClient({\n url,\n authToken: options.authToken,\n syncUrl: options.syncUrl,\n syncInterval: options.syncInterval,\n }),\n owned: true,\n };\n}\n\n/**\n * Load the default Node client only when a URL is used. Importing\n * `@libsql/client` at module load pulls in its native addon, which makes a\n * process that only injects a `sqlite3` client (tests, embedded replicas)\n * crash on teardown under some Node versions.\n */\nlet nodeCreateClient: CreateClient | undefined;\n\nfunction createNodeClient(config: Parameters<CreateClient>[0]): Client {\n nodeCreateClient ??= (\n createRequire(import.meta.url)(\"@libsql/client\") as { createClient: CreateClient }\n ).createClient;\n return nodeCreateClient(config);\n}\n\nconst WEBHOOK_SECRET_DROP = \"ALTER TABLE webhooks DROP COLUMN secret\";\n\nasync function runMigrations(client: LibsqlClient): Promise<void> {\n await client.executeMultiple(DDL);\n await ensureColumns(client, DDL);\n for (const statement of IDENTITY_STATEMENTS) {\n // oxlint-disable-next-line no-await-in-loop -- ordered: drop old indexes, then create new\n await execIgnore(client, statement);\n }\n await client.execute(CAPTURE_JOB_BACKFILL); // not ignored: see its doc\n await execIgnore(client, WEBHOOK_SECRET_DROP);\n await migrateCommentsTable(client);\n}\n\n/** Add any DDL column missing from a volume created before it shipped. */\nasync function ensureColumns(client: LibsqlClient, ddl: string): Promise<void> {\n const inspected = await Promise.all(\n [...tableColumns(ddl)].map(async ([table, columns]) => {\n const result = await client.execute(`PRAGMA table_info(${table})`);\n const rows = result.rows as unknown as { name: unknown }[];\n const existing = new Set(rows.map((row) => String(row.name)));\n return { table, columns, existing };\n }),\n );\n const statements = inspected.flatMap(({ table, columns, existing }) =>\n columns\n .filter((column) => isMissing(column, existing))\n .map((column) => [table, column] as const),\n );\n await Promise.all(\n statements.map(async ([table, column]) => {\n await execIgnore(client, `ALTER TABLE ${table} ADD COLUMN ${column}`);\n }),\n );\n}\n\nfunction isMissing(column: string, existing: Set<string>): boolean {\n const [name] = column.split(/\\s+/u);\n return name !== undefined && !existing.has(name);\n}\n\nasync function execIgnore(client: LibsqlClient, sql: string): Promise<void> {\n try {\n await client.execute(sql);\n } catch {\n // idempotent — already migrated\n }\n}\n\nasync function migrateCommentsTable(client: LibsqlClient): Promise<void> {\n try {\n const result = await client.execute(\n \"SELECT sql FROM sqlite_master WHERE type='table' AND name='comments'\",\n );\n const sql = (result.rows[0] as unknown as { sql: string } | undefined)?.sql;\n if (!sql?.includes(\"user_id TEXT NOT NULL REFERENCES users\")) {\n return;\n }\n await recreateCommentsTable(client);\n } catch {\n await execIgnore(client, \"PRAGMA foreign_keys = ON\");\n }\n}\n\nasync function recreateCommentsTable(client: LibsqlClient): Promise<void> {\n await client.execute(\"PRAGMA foreign_keys = OFF\");\n await client.execute(`\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 await client.execute(\"INSERT INTO comments_new SELECT * FROM comments\");\n await client.execute(\"DROP TABLE comments\");\n await client.execute(\"ALTER TABLE comments_new RENAME TO comments\");\n await client.execute(\"CREATE INDEX IF NOT EXISTS comments_build_id_idx ON comments (build_id)\");\n await client.execute(\"PRAGMA foreign_keys = ON\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAwBA,MAAM,YAAa,kBAA0D;;;;;;;;AAyB7E,SAAgB,oBAAoB,SAAgD;CAClF,MAAM,EAAE,QAAQ,UAAU,cAAc,OAAO;CAC/C,MAAM,KAAK,UAAU,QAAQ,EAAE,OAAO,CAAC;CAEvC,OAAO,qBAAqB,IAAI;EAC9B,UAAU;GACR,MAAM;GACN,SAAA;GACA,aAAa;GACb,MAAM;GACN,UAAU;EACZ;EACA,QAAQ;EACR,SAAS,YAAY;GACnB,MAAM,cAAc,MAAM;EAC5B;EACA,aAAa;GACX,IAAI,OACF,OAAO,MAAM;EAEjB;EACA,MAAM,YAAY;GAChB,MAAM,OAAO,QAAQ,UAAU;EACjC;CACF,CAAC;AACH;AAEA,MAAM,gBAAgB;AAEtB,SAAS,eAAe,KAAsB;CAC5C,OAAO,cAAc,KAAK,GAAG;AAC/B;;AAGA,SAAS,cAAc,SAAyE;CAC9F,IAAI,QAAQ,QACV,OAAO;EAAE,QAAQ,QAAQ;EAAQ,OAAO;CAAM;CAEhD,MAAM,MAAM,QAAQ,KAAK,KAAK;CAC9B,IAAI,CAAC,KACH,MAAM,IAAI,MACR,kGACF;CAEF,IAAI,CAAC,eAAe,GAAG,GACrB,MAAM,IAAI,MACR,yCAAyC,IAAI,6DAC/C;CAEF,OAAO;EACL,QAAQ,iBAAiB;GACvB;GACA,WAAW,QAAQ;GACnB,SAAS,QAAQ;GACjB,cAAc,QAAQ;EACxB,CAAC;EACD,OAAO;CACT;AACF;;;;;;;AAQA,IAAI;AAEJ,SAAS,iBAAiB,QAA6C;CACrE,qBACE,cAAc,YAAY,GAAG,CAAC,CAAC,gBAAgB,CAAC,CAChD;CACF,OAAO,iBAAiB,MAAM;AAChC;AAEA,MAAM,sBAAsB;AAE5B,eAAe,cAAc,QAAqC;CAChE,MAAM,OAAO,gBAAgB,GAAG;CAChC,MAAM,cAAc,QAAQ,GAAG;CAC/B,KAAK,MAAM,aAAa,qBAEtB,MAAM,WAAW,QAAQ,SAAS;CAEpC,MAAM,OAAO,QAAQ,oBAAoB;CACzC,MAAM,WAAW,QAAQ,mBAAmB;CAC5C,MAAM,qBAAqB,MAAM;AACnC;;AAGA,eAAe,cAAc,QAAsB,KAA4B;CAS7E,MAAM,cAAa,MARK,QAAQ,IAC9B,CAAC,GAAG,aAAa,GAAG,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,OAAO,aAAa;EAErD,MAAM,QAAO,MADQ,OAAO,QAAQ,qBAAqB,MAAM,EAAE,EAAA,CAC7C;EAEpB,OAAO;GAAE;GAAO;GAAS,UAAA,IADJ,IAAI,KAAK,KAAK,QAAQ,OAAO,IAAI,IAAI,CAAC,CAC3B;EAAE;CACpC,CAAC,CACH,EAAA,CAC6B,SAAS,EAAE,OAAO,SAAS,eACtD,QACG,QAAQ,WAAW,UAAU,QAAQ,QAAQ,CAAC,CAAC,CAC/C,KAAK,WAAW,CAAC,OAAO,MAAM,CAAU,CAC7C;CACA,MAAM,QAAQ,IACZ,WAAW,IAAI,OAAO,CAAC,OAAO,YAAY;EACxC,MAAM,WAAW,QAAQ,eAAe,MAAM,cAAc,QAAQ;CACtE,CAAC,CACH;AACF;AAEA,SAAS,UAAU,QAAgB,UAAgC;CACjE,MAAM,CAAC,QAAQ,OAAO,MAAM,MAAM;CAClC,OAAO,SAAS,KAAA,KAAa,CAAC,SAAS,IAAI,IAAI;AACjD;AAEA,eAAe,WAAW,QAAsB,KAA4B;CAC1E,IAAI;EACF,MAAM,OAAO,QAAQ,GAAG;CAC1B,QAAQ,CAER;AACF;AAEA,eAAe,qBAAqB,QAAqC;CACvE,IAAI;EAKF,IAAI,GADS,MAHQ,OAAO,QAC1B,sEACF,EAAA,CACoB,KAAK,EAAE,EAA6C,IAAA,EAC9D,SAAS,wCAAwC,GACzD;EAEF,MAAM,sBAAsB,MAAM;CACpC,QAAQ;EACN,MAAM,WAAW,QAAQ,0BAA0B;CACrD;AACF;AAEA,eAAe,sBAAsB,QAAqC;CACxE,MAAM,OAAO,QAAQ,2BAA2B;CAChD,MAAM,OAAO,QAAQ;;;;;;;;;;;;;GAapB;CACD,MAAM,OAAO,QAAQ,iDAAiD;CACtE,MAAM,OAAO,QAAQ,qBAAqB;CAC1C,MAAM,OAAO,QAAQ,6CAA6C;CAClE,MAAM,OAAO,QAAQ,yEAAyE;CAC9F,MAAM,OAAO,QAAQ,0BAA0B;AACjD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storyshelf/db-sqlite",
|
|
3
|
-
"version": "0.9.0
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "SQLite database adapter for StoryShelf (node:sqlite + Drizzle).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"database",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"prepublishOnly": "nub run build"
|
|
88
88
|
},
|
|
89
89
|
"dependencies": {
|
|
90
|
-
"@storyshelf/core": "0.9.0
|
|
90
|
+
"@storyshelf/core": "0.9.0",
|
|
91
91
|
"drizzle-orm": "^0.45.4"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|