/** * Kysely dialect for Durable Object preview databases * * Proxies all queries to an EmDashPreviewDB DO instance via RPC. * Preview mode is read-only — no transaction support needed. */ import type { CompiledQuery, DatabaseConnection, DatabaseIntrospector, Dialect, Driver, Kysely, QueryResult, } from "kysely"; import { SqliteAdapter, SqliteQueryCompiler } from "kysely"; import { D1Introspector } from "./d1-introspector.js"; import type { QueryResult as DOQueryResult } from "./do-class.js"; /** * Minimal interface for the DO stub's RPC methods. * * We define this instead of using DurableObjectStub directly * because Rpc.Result resolves to `never` when the return type contains * `unknown` (Record in QueryResult.rows). This interface * gives us clean typing without fighting the Rpc type system. */ export interface PreviewDBStub { query(sql: string, params?: unknown[]): Promise; } export interface PreviewDODialectConfig { /** * Factory that returns a fresh DO stub on each call. * * DO stubs are bound to the request context that created them. * Since the Kysely instance may be cached across requests, we can't * hold a single stub — each connection must get a fresh one via * namespace.get(id), which is cheap (no RPC, just a local ref). */ getStub: () => PreviewDBStub; } export class PreviewDODialect implements Dialect { readonly #config: PreviewDODialectConfig; constructor(config: PreviewDODialectConfig) { this.#config = config; } createAdapter(): SqliteAdapter { return new SqliteAdapter(); } createDriver(): Driver { return new PreviewDODriver(this.#config); } createQueryCompiler(): SqliteQueryCompiler { return new SqliteQueryCompiler(); } createIntrospector(db: Kysely): DatabaseIntrospector { return new D1Introspector(db); } } class PreviewDODriver implements Driver { readonly #config: PreviewDODialectConfig; constructor(config: PreviewDODialectConfig) { this.#config = config; } async init(): Promise {} async acquireConnection(): Promise { return new PreviewDOConnection(this.#config.getStub()); } async beginTransaction(): Promise { // No-op. Preview is read-only. } async commitTransaction(): Promise { // No-op. } async rollbackTransaction(): Promise { // No-op. } async releaseConnection(): Promise {} async destroy(): Promise {} } class PreviewDOConnection implements DatabaseConnection { readonly #stub: PreviewDBStub; constructor(stub: PreviewDBStub) { this.#stub = stub; } async executeQuery(compiledQuery: CompiledQuery): Promise> { const sqlText = compiledQuery.sql; // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- CompiledQuery.parameters is ReadonlyArray, stub expects unknown[] const params = compiledQuery.parameters as unknown[]; const result = await this.#stub.query(sqlText, params); return { // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Kysely generic O is the caller's row type; we trust the DB returned matching rows rows: result.rows as O[], numAffectedRows: result.changes !== undefined ? BigInt(result.changes) : undefined, }; } // eslint-disable-next-line require-yield -- interface requires AsyncIterableIterator but DO doesn't support streaming async *streamQuery(): AsyncIterableIterator> { throw new Error("Preview DO dialect does not support streaming"); } }