Type-safe database access using Kysely ORM with SQLite on Cloudflare Durable Objects, including migration support and schema inference from migration definitions.
Creates a Kysely database instance connected to a Durable Object.
/**
* Creates a Kysely database instance connected to a Durable Object
* @param durableObjectBinding - Durable Object namespace binding
* @param name - Database name (default: "main")
* @returns Kysely instance with type-safe query builder
*/
function createDb<DatabaseType>(
durableObjectBinding: DurableObjectNamespace,
name?: string
): Kysely<DatabaseType>;Usage Example:
import { createDb } from 'rwsdk/db';
interface Database {
users: {
id: number;
name: string;
email: string;
createdAt: string;
};
posts: {
id: number;
userId: number;
title: string;
content: string;
};
}
// In a route handler or server function
async function getUsers(env: any) {
const db = createDb<Database>(env.DB_DO);
const users = await db
.selectFrom('users')
.selectAll()
.execute();
return users;
}Kysely's SQL template tag for raw SQL queries.
/**
* SQL template tag for raw SQL queries (re-exported from kysely)
*/
const sql: SqlTag;Usage Example:
import { createDb, sql } from 'rwsdk/db';
const db = createDb<Database>(env.DB_DO);
// Raw SQL query
const result = await sql`SELECT * FROM users WHERE age > ${18}`.execute(db);
// SQL in query builder
const users = await db
.selectFrom('users')
.select('name')
.where(sql`age > 18`)
.execute();Base class for Durable Objects that include a Kysely-backed SQLite database with automatic migrations.
/**
* Base class for Durable Objects with Kysely database
*/
class SqliteDurableObject<T> {
/**
* Creates a new SQLite Durable Object
* @param ctx - Durable Object state
* @param env - Environment bindings
* @param migrations - Migration definitions
* @param migrationTableName - Migration tracking table name (default: "migrations")
* @param plugins - Optional Kysely plugins
*/
constructor(
ctx: DurableObjectState,
env: any,
migrations: Record<string, Migration>,
migrationTableName?: string,
plugins?: KyselyPlugin[]
);
/**
* Initializes the database and runs pending migrations
*/
initialize(): Promise<void>;
/**
* RPC method for executing queries from external clients
* @param compiledQuery - Compiled Kysely query
* @returns Query result
*/
kyselyExecuteQuery<R>(
compiledQuery: CompiledQuery
): Promise<QueryResult<R>>;
/**
* Kysely instance for database operations
*/
kysely: Kysely<T>;
/**
* Migration definitions
*/
migrations: Record<string, Migration>;
}Usage Example:
import { SqliteDurableObject, sql } from 'rwsdk/db';
// Define migrations
const migrations = {
'001_initial': {
async up(db) {
await db.schema
.createTable('users')
.addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('email', 'text', (col) => col.notNull().unique())
.addColumn('created_at', 'text', (col) =>
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
)
.execute();
},
async down(db) {
await db.schema.dropTable('users').execute();
},
},
'002_add_posts': {
async up(db) {
await db.schema
.createTable('posts')
.addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
.addColumn('user_id', 'integer', (col) =>
col.notNull().references('users.id').onDelete('cascade')
)
.addColumn('title', 'text', (col) => col.notNull())
.addColumn('content', 'text')
.execute();
},
async down(db) {
await db.schema.dropTable('posts').execute();
},
},
};
// Create Durable Object class
export class MyDatabase extends SqliteDurableObject<Database> {
constructor(ctx: DurableObjectState, env: any) {
super(ctx, env, migrations);
}
async fetch(request: Request) {
// Ensure database is initialized
await this.initialize();
// Use kysely for queries
const users = await this.kysely
.selectFrom('users')
.selectAll()
.execute();
return Response.json({ users });
}
}Functions for managing database migrations.
/**
* Creates a Kysely migrator
* @param db - Kysely database instance
* @param migrations - Migration definitions
* @param migrationTableName - Migration tracking table name
* @returns Kysely Migrator instance
*/
function createMigrator(
db: Kysely<any>,
migrations: Record<string, Migration>,
migrationTableName?: string
): Migrator;
/**
* In-memory migration provider
*/
class InMemoryMigrationProvider implements MigrationProvider {
constructor(migrations: Record<string, Migration>);
}Utilities for inferring database schema types from migration definitions.
/**
* Migration interface
*/
interface Migration<TUpReturn = void> {
up(db: Kysely<any>): Promise<TUpReturn>;
down?(db: Kysely<any>): Promise<void>;
}
/**
* Builder for migrations with type inference
*/
type MigrationBuilder = {
up(fn: (db: Kysely<any>) => Promise<void>): MigrationBuilder;
down(fn: (db: Kysely<any>) => Promise<void>): MigrationBuilder;
};
/**
* Record of migration definitions
*/
type Migrations = Record<string, Migration>;
/**
* Infers database schema type from migrations
*/
type Database<TMigrations extends Migrations> = InferDatabaseFromMigrations<TMigrations>;
/**
* Builder with schema inference
*/
type InferenceBuilder = {
createTable(name: string): TableBuilder;
alterTable(name: string): TableBuilder;
};import { SqliteDurableObject, sql } from 'rwsdk/db';
import { createDb } from 'rwsdk/db';
// Define database schema
interface Database {
users: {
id: number;
name: string;
email: string;
password_hash: string;
created_at: string;
};
posts: {
id: number;
user_id: number;
title: string;
content: string;
published: number;
created_at: string;
};
}
// Define migrations
const migrations = {
'001_initial_schema': {
async up(db) {
await db.schema
.createTable('users')
.addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
.addColumn('name', 'text', (col) => col.notNull())
.addColumn('email', 'text', (col) => col.notNull().unique())
.addColumn('password_hash', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) =>
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
)
.execute();
await db.schema
.createTable('posts')
.addColumn('id', 'integer', (col) => col.primaryKey().autoIncrement())
.addColumn('user_id', 'integer', (col) =>
col.notNull().references('users.id').onDelete('cascade')
)
.addColumn('title', 'text', (col) => col.notNull())
.addColumn('content', 'text')
.addColumn('published', 'integer', (col) => col.notNull().defaultTo(0))
.addColumn('created_at', 'text', (col) =>
col.notNull().defaultTo(sql`CURRENT_TIMESTAMP`)
)
.execute();
await db.schema
.createIndex('posts_user_id_idx')
.on('posts')
.column('user_id')
.execute();
},
},
};
// Durable Object implementation
export class AppDatabase extends SqliteDurableObject<Database> {
constructor(ctx: DurableObjectState, env: any) {
super(ctx, env, migrations);
}
async fetch(request: Request) {
await this.initialize();
const url = new URL(request.url);
if (url.pathname === '/users' && request.method === 'GET') {
const users = await this.kysely
.selectFrom('users')
.select(['id', 'name', 'email', 'created_at'])
.execute();
return Response.json({ users });
}
if (url.pathname === '/posts' && request.method === 'GET') {
const posts = await this.kysely
.selectFrom('posts')
.innerJoin('users', 'users.id', 'posts.user_id')
.select([
'posts.id',
'posts.title',
'posts.content',
'posts.published',
'users.name as author_name',
])
.where('posts.published', '=', 1)
.orderBy('posts.created_at', 'desc')
.execute();
return Response.json({ posts });
}
return new Response('Not Found', { status: 404 });
}
}
// Using the database from worker
import { createDb } from 'rwsdk/db';
async function getPosts(env: any) {
const db = createDb<Database>(env.APP_DB);
const posts = await db
.selectFrom('posts')
.innerJoin('users', 'users.id', 'posts.user_id')
.select([
'posts.id',
'posts.title',
'posts.content',
'users.name as author',
])
.where('posts.published', '=', 1)
.execute();
return posts;
}001_initial_schema, 002_add_users)down migrations for rollback capabilityup migrations without proper backup