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
JavaScript
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}`);
}
}