126 lines
3.5 KiB
TypeScript
126 lines
3.5 KiB
TypeScript
/**
|
|
* 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<EmDashPreviewDB> directly
|
|
* because Rpc.Result<T> resolves to `never` when the return type contains
|
|
* `unknown` (Record<string, unknown> in QueryResult.rows). This interface
|
|
* gives us clean typing without fighting the Rpc type system.
|
|
*/
|
|
export interface PreviewDBStub {
|
|
query(sql: string, params?: unknown[]): Promise<DOQueryResult>;
|
|
}
|
|
|
|
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<any>): DatabaseIntrospector {
|
|
return new D1Introspector(db);
|
|
}
|
|
}
|
|
|
|
class PreviewDODriver implements Driver {
|
|
readonly #config: PreviewDODialectConfig;
|
|
|
|
constructor(config: PreviewDODialectConfig) {
|
|
this.#config = config;
|
|
}
|
|
|
|
async init(): Promise<void> {}
|
|
|
|
async acquireConnection(): Promise<DatabaseConnection> {
|
|
return new PreviewDOConnection(this.#config.getStub());
|
|
}
|
|
|
|
async beginTransaction(): Promise<void> {
|
|
// No-op. Preview is read-only.
|
|
}
|
|
|
|
async commitTransaction(): Promise<void> {
|
|
// No-op.
|
|
}
|
|
|
|
async rollbackTransaction(): Promise<void> {
|
|
// No-op.
|
|
}
|
|
|
|
async releaseConnection(): Promise<void> {}
|
|
|
|
async destroy(): Promise<void> {}
|
|
}
|
|
|
|
class PreviewDOConnection implements DatabaseConnection {
|
|
readonly #stub: PreviewDBStub;
|
|
|
|
constructor(stub: PreviewDBStub) {
|
|
this.#stub = stub;
|
|
}
|
|
|
|
async executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> {
|
|
const sqlText = compiledQuery.sql;
|
|
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- CompiledQuery.parameters is ReadonlyArray<unknown>, 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<O>(): AsyncIterableIterator<QueryResult<O>> {
|
|
throw new Error("Preview DO dialect does not support streaming");
|
|
}
|
|
}
|