UNPKG

rwsdk

Version:

Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime

130 lines (129 loc) 6.56 kB
// NOTE(justinvdm, 9 Jun 2026): This file copies Kysely's SqliteIntrospector. // Before trying to simplify it, read this comment. // // Problem: Cloudflare's DO SQLite adds internal tables (_cf_KV, _cf_METADATA) // when storage.put/setAlarm is used. Kysely's SqliteIntrospector discovers // these via sqlite_master, then runs PRAGMA table_info on each. Cloudflare's // authorizer rejects PRAGMA on _cf_* tables with SQLITE_AUTH, breaking // migrations and rendering the DO unusable. // // Why we copied instead of composed: // - Kysely's SqliteIntrospector uses JS private fields (#db, #tablesQuery, // #getTableMetadata). Private fields are truly private — subclasses cannot // override them, and wrappers cannot intercept internal calls. // - We considered a lighter approach: query sqlite_master with Kysely's // builder, then run PRAGMA table_info per-table. This is lighter but // requires inlining the table name into raw SQL. Even with escaping, // any injection risk is unacceptable. The CTE approach below uses // `pragma_table_info(tl.name)` where `tl.name` is a column reference, // not an inlined value — zero injection surface. // - We also considered Kysely plugins (AST rewriting, SQL string rewriting) // but these are fragile: they depend on Kysely's internal AST shape and // SQL formatting, both of which can change between releases. // // This is a copy of Kysely's SqliteIntrospector (MIT licensed) with one // change: `.where('name', 'not like', '_cf_%')` to exclude Cloudflare tables. // // See: https://github.com/redwoodjs/sdk/issues/1219 var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _DOSqliteIntrospector_instances, _DOSqliteIntrospector_db, _DOSqliteIntrospector_tablesQuery, _DOSqliteIntrospector_getTableMetadata; import { DEFAULT_MIGRATION_LOCK_TABLE, DEFAULT_MIGRATION_TABLE, sql, } from "kysely"; export class DOSqliteIntrospector { constructor(db) { _DOSqliteIntrospector_instances.add(this); _DOSqliteIntrospector_db.set(this, void 0); __classPrivateFieldSet(this, _DOSqliteIntrospector_db, db, "f"); } async getSchemas() { // Sqlite doesn't support schemas. return []; } async getTables(options = { withInternalKyselyTables: false }) { return await __classPrivateFieldGet(this, _DOSqliteIntrospector_instances, "m", _DOSqliteIntrospector_getTableMetadata).call(this, options); } async getMetadata(options) { return { tables: await this.getTables(options), }; } } _DOSqliteIntrospector_db = new WeakMap(), _DOSqliteIntrospector_instances = new WeakSet(), _DOSqliteIntrospector_tablesQuery = function _DOSqliteIntrospector_tablesQuery(qb, options) { let tablesQuery = qb .selectFrom("sqlite_master") .where("type", "in", ["table", "view"]) .where("name", "not like", "sqlite_%") // context(justinvdm, 9 Jun 2026): Exclude Cloudflare internal tables. // These are added by the DO runtime and cannot be introspected. .where("name", "not like", "_cf_%") .select(["name", "sql", "type"]) .orderBy("name"); if (!options.withInternalKyselyTables) { tablesQuery = tablesQuery .where("name", "!=", DEFAULT_MIGRATION_TABLE) .where("name", "!=", DEFAULT_MIGRATION_LOCK_TABLE); } return tablesQuery; }, _DOSqliteIntrospector_getTableMetadata = async function _DOSqliteIntrospector_getTableMetadata(options) { const tablesResult = await __classPrivateFieldGet(this, _DOSqliteIntrospector_instances, "m", _DOSqliteIntrospector_tablesQuery).call(this, __classPrivateFieldGet(this, _DOSqliteIntrospector_db, "f"), options).execute(); const tableMetadata = await __classPrivateFieldGet(this, _DOSqliteIntrospector_db, "f") .with("table_list", (qb) => __classPrivateFieldGet(this, _DOSqliteIntrospector_instances, "m", _DOSqliteIntrospector_tablesQuery).call(this, qb, options)) .selectFrom([ "table_list as tl", sql `pragma_table_info(tl.name)`.as("p"), ]) .select([ "tl.name as table", "p.cid", "p.name", "p.type", "p.notnull", "p.dflt_value", "p.pk", ]) .orderBy("tl.name") .orderBy("p.cid") .execute(); const columnsByTable = {}; for (const row of tableMetadata) { columnsByTable[row.table] ??= []; columnsByTable[row.table].push(row); } return tablesResult.map(({ name, sql: tableSql, type }) => { let autoIncrementCol = tableSql ?.split(/[\(\),]/) ?.find((it) => it.toLowerCase().includes("autoincrement")) ?.trimStart() ?.split(/\s+/)?.[0] ?.replace(/["`]/g, ""); const columns = columnsByTable[name] ?? []; if (!autoIncrementCol) { const pkCols = columns.filter((r) => r.pk > 0); if (pkCols.length === 1 && pkCols[0].type.toLowerCase() === "integer") { autoIncrementCol = pkCols[0].name; } } return { name: name, isView: type === "view", columns: columns.map((col) => ({ name: col.name, dataType: col.type, isNullable: !col.notnull, isAutoIncrementing: col.name === autoIncrementCol, hasDefaultValue: col.dflt_value != null, comment: undefined, })), }; }); };