UNPKG

cogsbox-shape

Version:

A TypeScript library for creating type-safe database schemas with Zod validation, SQL type definitions, and automatic client/server transformations. Unifies client, server, and database types through a single schema definition, with built-in support for r

125 lines (124 loc) 4.3 kB
import { Kysely, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler, } from "kysely"; export class D1Dialect { #database; constructor(database) { this.#database = database; } createDriver() { return new D1Driver(this.#database); } createQueryCompiler() { return new SqliteQueryCompiler(); } createAdapter() { return new SqliteAdapter(); } createIntrospector(db) { return new SqliteIntrospector(db); } } export function createD1Db(database) { const db = new Kysely({ dialect: new D1Dialect(database), }); Object.defineProperty(db, "batch", { configurable: false, enumerable: false, value: (queries) => executeD1Batch(database, queries), writable: false, }); return db; } export async function executeD1Batch(database, queries) { if (queries.length === 0) return []; const compiledQueries = queries.map((query) => query.compile()); const statements = compiledQueries.map((query) => prepareD1Statement(database, query)); const results = await database.batch(statements); if (results.length !== compiledQueries.length) { throw new Error(`D1 batch returned ${results.length} results for ${compiledQueries.length} queries.`); } for (let index = 0; index < results.length; index++) { assertD1Success(results[index], compiledQueries[index].sql); } return results; } class D1Driver { #connection; constructor(database) { this.#connection = new D1Connection(database); } async init(_options) { // Cloudflare D1 bindings are already initialized by the Worker runtime. } async acquireConnection(_options) { return this.#connection; } async beginTransaction(_connection, _settings) { throw new Error("Cloudflare D1 does not support interactive transactions. Use db.batch([...queries]) for an atomic D1 transaction."); } async commitTransaction(_connection) { throw new Error("Cloudflare D1 transactions must be committed through db.batch([...queries])."); } async rollbackTransaction(_connection) { throw new Error("Cloudflare D1 transactions are rolled back automatically when db.batch([...queries]) fails."); } async releaseConnection(_connection, _options) { // D1 exposes a stateless binding instead of pooled connections. } async destroy(_options) { // Nothing to close for a Worker binding. } } class D1Connection { #database; constructor(database) { this.#database = database; } async executeQuery(compiledQuery) { const statement = this.#prepare(compiledQuery); const result = shouldReturnRows(compiledQuery.sql) ? await statement.all() : await statement.run(); assertD1Success(result, compiledQuery.sql); return { insertId: result.meta?.last_row_id != null ? BigInt(result.meta.last_row_id) : undefined, numAffectedRows: affectedRows(result), rows: (result.results ?? []), }; } async *streamQuery(compiledQuery, _chunkSize) { const result = await this.executeQuery(compiledQuery); for (const row of result.rows) { yield { rows: [row] }; } } #prepare(compiledQuery) { return prepareD1Statement(this.#database, compiledQuery); } } function prepareD1Statement(database, compiledQuery) { const statement = database.prepare(compiledQuery.sql); return compiledQuery.parameters.length > 0 ? statement.bind(...compiledQuery.parameters) : statement; } function shouldReturnRows(sql) { const trimmed = sql.trim().toLowerCase(); return (trimmed.startsWith("select") || trimmed.startsWith("with") || trimmed.startsWith("pragma") || trimmed.startsWith("explain") || /\breturning\b/i.test(sql)); } function affectedRows(result) { const count = result.meta?.changes ?? result.meta?.rows_written; return count != null ? BigInt(count) : undefined; } function assertD1Success(result, sql) { if (!result.success) { throw new Error(result.error ?? `D1 query failed: ${sql}`); } }