Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:24:43 +08:00
commit 0961c5806a
21 changed files with 3552 additions and 0 deletions

333
templates/basic-queries.ts Normal file
View File

@@ -0,0 +1,333 @@
/**
* Basic CRUD Queries with Drizzle ORM and D1
*
* This file demonstrates all basic database operations:
* - Create (INSERT)
* - Read (SELECT)
* - Update (UPDATE)
* - Delete (DELETE)
*/
import { drizzle } from 'drizzle-orm/d1';
import { users, posts, type NewUser, type NewPost } from './schema';
import { eq, and, or, gt, lt, gte, lte, like, notLike, isNull, isNotNull, inArray } from 'drizzle-orm';
// Assuming db is initialized
// const db = drizzle(env.DB);
/**
* CREATE Operations (INSERT)
*/
// Insert single user
export async function createUser(db: ReturnType<typeof drizzle>, email: string, name: string) {
const newUser: NewUser = {
email,
name,
};
// Insert and return the created user
const [user] = await db.insert(users).values(newUser).returning();
return user;
}
// Insert multiple users
export async function createUsers(db: ReturnType<typeof drizzle>, usersData: NewUser[]) {
const createdUsers = await db.insert(users).values(usersData).returning();
return createdUsers;
}
// Insert post
export async function createPost(
db: ReturnType<typeof drizzle>,
data: { title: string; slug: string; content: string; authorId: number }
) {
const newPost: NewPost = {
...data,
published: false, // Default to unpublished
};
const [post] = await db.insert(posts).values(newPost).returning();
return post;
}
/**
* READ Operations (SELECT)
*/
// Get all users
export async function getAllUsers(db: ReturnType<typeof drizzle>) {
return await db.select().from(users).all();
}
// Get user by ID
export async function getUserById(db: ReturnType<typeof drizzle>, id: number) {
return await db.select().from(users).where(eq(users.id, id)).get();
}
// Get user by email
export async function getUserByEmail(db: ReturnType<typeof drizzle>, email: string) {
return await db.select().from(users).where(eq(users.email, email)).get();
}
// Get multiple users by IDs
export async function getUsersByIds(db: ReturnType<typeof drizzle>, ids: number[]) {
return await db.select().from(users).where(inArray(users.id, ids)).all();
}
// Get users with conditions
export async function searchUsers(db: ReturnType<typeof drizzle>, searchTerm: string) {
return await db
.select()
.from(users)
.where(
or(
like(users.email, `%${searchTerm}%`),
like(users.name, `%${searchTerm}%`)
)
)
.all();
}
// Get recent posts (ordered)
export async function getRecentPosts(db: ReturnType<typeof drizzle>, limit = 10) {
return await db
.select()
.from(posts)
.where(eq(posts.published, true))
.orderBy(posts.createdAt) // or desc(posts.createdAt) for descending
.limit(limit)
.all();
}
// Get posts with pagination
export async function getPosts(db: ReturnType<typeof drizzle>, page = 1, pageSize = 10) {
const offset = (page - 1) * pageSize;
return await db
.select()
.from(posts)
.where(eq(posts.published, true))
.limit(pageSize)
.offset(offset)
.all();
}
// Get posts by author
export async function getPostsByAuthor(db: ReturnType<typeof drizzle>, authorId: number) {
return await db
.select()
.from(posts)
.where(eq(posts.authorId, authorId))
.all();
}
// Complex WHERE conditions
export async function getPublishedPostsAfterDate(
db: ReturnType<typeof drizzle>,
date: Date
) {
return await db
.select()
.from(posts)
.where(
and(
eq(posts.published, true),
gte(posts.createdAt, date)
)
)
.all();
}
// Select specific columns
export async function getUserEmails(db: ReturnType<typeof drizzle>) {
return await db
.select({
email: users.email,
name: users.name,
})
.from(users)
.all();
}
// Count queries
export async function countUsers(db: ReturnType<typeof drizzle>) {
const result = await db
.select({ count: sql<number>`count(*)` })
.from(users)
.get();
return result?.count ?? 0;
}
/**
* UPDATE Operations
*/
// Update user by ID
export async function updateUser(
db: ReturnType<typeof drizzle>,
id: number,
data: { name?: string; bio?: string }
) {
const [updated] = await db
.update(users)
.set({
...data,
updatedAt: new Date(),
})
.where(eq(users.id, id))
.returning();
return updated;
}
// Update post
export async function updatePost(
db: ReturnType<typeof drizzle>,
id: number,
data: { title?: string; content?: string; published?: boolean }
) {
const [updated] = await db
.update(posts)
.set({
...data,
updatedAt: new Date(),
})
.where(eq(posts.id, id))
.returning();
return updated;
}
// Publish post
export async function publishPost(db: ReturnType<typeof drizzle>, id: number) {
const [updated] = await db
.update(posts)
.set({
published: true,
updatedAt: new Date(),
})
.where(eq(posts.id, id))
.returning();
return updated;
}
// Update with conditions
export async function unpublishOldPosts(db: ReturnType<typeof drizzle>, cutoffDate: Date) {
return await db
.update(posts)
.set({
published: false,
updatedAt: new Date(),
})
.where(
and(
eq(posts.published, true),
lt(posts.createdAt, cutoffDate)
)
)
.returning();
}
/**
* DELETE Operations
*/
// Delete user by ID
export async function deleteUser(db: ReturnType<typeof drizzle>, id: number) {
const [deleted] = await db
.delete(users)
.where(eq(users.id, id))
.returning();
return deleted;
}
// Delete post by ID
export async function deletePost(db: ReturnType<typeof drizzle>, id: number) {
const [deleted] = await db
.delete(posts)
.where(eq(posts.id, id))
.returning();
return deleted;
}
// Delete multiple posts
export async function deletePostsByAuthor(db: ReturnType<typeof drizzle>, authorId: number) {
return await db
.delete(posts)
.where(eq(posts.authorId, authorId))
.returning();
}
// Delete with conditions
export async function deleteUnpublishedPostsOlderThan(
db: ReturnType<typeof drizzle>,
cutoffDate: Date
) {
return await db
.delete(posts)
.where(
and(
eq(posts.published, false),
lt(posts.createdAt, cutoffDate)
)
)
.returning();
}
/**
* Operator Reference:
*
* Comparison:
* - eq(column, value) - Equal (=)
* - ne(column, value) - Not equal (!=)
* - gt(column, value) - Greater than (>)
* - gte(column, value) - Greater than or equal (>=)
* - lt(column, value) - Less than (<)
* - lte(column, value) - Less than or equal (<=)
*
* Logical:
* - and(...conditions) - AND
* - or(...conditions) - OR
* - not(condition) - NOT
*
* Pattern Matching:
* - like(column, pattern) - LIKE
* - notLike(column, pattern) - NOT LIKE
*
* NULL:
* - isNull(column) - IS NULL
* - isNotNull(column) - IS NOT NULL
*
* Arrays:
* - inArray(column, values) - IN (...)
* - notInArray(column, values) - NOT IN (...)
*
* Between:
* - between(column, min, max) - BETWEEN
* - notBetween(column, min, max) - NOT BETWEEN
*/
/**
* Method Reference:
*
* Execution:
* - .all() - Returns all results as array
* - .get() - Returns first result or undefined
* - .run() - Executes query, returns metadata (for INSERT/UPDATE/DELETE without RETURNING)
* - .returning() - Returns affected rows (works with INSERT/UPDATE/DELETE)
*
* Modifiers:
* - .where(condition) - Filter results
* - .orderBy(column) - Sort results (ascending)
* - .orderBy(desc(column)) - Sort results (descending)
* - .limit(n) - Limit results
* - .offset(n) - Skip n results
*/

109
templates/client.ts Normal file
View File

@@ -0,0 +1,109 @@
/**
* Drizzle Client for Cloudflare D1
*
* This file shows how to initialize the Drizzle client with D1 in a Cloudflare Worker.
*/
import { drizzle } from 'drizzle-orm/d1';
import * as schema from './schema';
/**
* Environment Interface
*
* Define your Worker's environment bindings
*/
export interface Env {
// D1 database binding (name must match wrangler.jsonc)
DB: D1Database;
// Add other bindings as needed
// KV: KVNamespace;
// R2: R2Bucket;
}
/**
* Create Drizzle Client
*
* Initialize Drizzle with your D1 database binding
*
* @param db - D1Database instance from env.DB
* @returns Drizzle database client
*/
export function createDrizzleClient(db: D1Database) {
// Option 1: Without schema (for basic queries)
// return drizzle(db);
// Option 2: With schema (enables relational queries)
return drizzle(db, { schema });
}
/**
* Usage in Worker:
*
* export default {
* async fetch(request: Request, env: Env): Promise<Response> {
* const db = createDrizzleClient(env.DB);
*
* // Now you can use db for queries
* const users = await db.select().from(schema.users).all();
*
* return Response.json(users);
* },
* };
*/
/**
* Type-Safe Client
*
* For better TypeScript inference, you can create a typed client
*/
export type DrizzleD1 = ReturnType<typeof createDrizzleClient>;
/**
* Usage with typed client:
*
* async function getUsers(db: DrizzleD1) {
* return await db.select().from(schema.users).all();
* }
*/
/**
* Relational Queries
*
* When you pass schema to drizzle(), you get access to db.query API
* for type-safe relational queries:
*
* const db = drizzle(env.DB, { schema });
*
* // Get user with all their posts
* const user = await db.query.users.findFirst({
* where: eq(schema.users.id, 1),
* with: {
* posts: true,
* },
* });
*/
/**
* IMPORTANT: D1 Binding Name
*
* The binding name "DB" must match exactly between:
*
* 1. wrangler.jsonc:
* {
* "d1_databases": [
* {
* "binding": "DB", // ← Must match
* ...
* }
* ]
* }
*
* 2. Env interface:
* export interface Env {
* DB: D1Database; // ← Must match
* }
*
* 3. Worker code:
* const db = drizzle(env.DB); // ← Must match
*/

View File

@@ -0,0 +1,367 @@
/**
* Complete Cloudflare Worker with Drizzle ORM and D1
*
* This template demonstrates a full-featured Worker using:
* - Hono for routing
* - Drizzle ORM for database queries
* - D1 for serverless SQLite
* - TypeScript for type safety
*/
import { Hono } from 'hono';
import { drizzle } from 'drizzle-orm/d1';
import { cors } from 'hono/cors';
import { prettyJSON } from 'hono/pretty-json';
import { eq } from 'drizzle-orm';
import * as schema from './db/schema';
import { users, posts, comments } from './db/schema';
/**
* Environment Interface
*
* Define all Cloudflare bindings
*/
export interface Env {
DB: D1Database; // D1 database binding
// Add other bindings as needed:
// KV: KVNamespace;
// R2: R2Bucket;
// AI: Ai;
}
/**
* Initialize Hono App
*/
const app = new Hono<{ Bindings: Env }>();
/**
* Middleware
*/
// CORS
app.use('/*', cors());
// Pretty JSON responses
app.use('/*', prettyJSON());
// Add database to context
app.use('*', async (c, next) => {
// Initialize Drizzle client with schema for relational queries
c.set('db', drizzle(c.env.DB, { schema }));
await next();
});
/**
* Health Check
*/
app.get('/', (c) => {
return c.json({
message: 'Cloudflare Worker with Drizzle ORM + D1',
status: 'ok',
timestamp: new Date().toISOString(),
});
});
/**
* Users Routes
*/
// Get all users
app.get('/api/users', async (c) => {
const db = c.get('db');
const allUsers = await db.select().from(users).all();
return c.json(allUsers);
});
// Get user by ID
app.get('/api/users/:id', async (c) => {
const db = c.get('db');
const id = parseInt(c.req.param('id'));
if (isNaN(id)) {
return c.json({ error: 'Invalid user ID' }, 400);
}
const user = await db.select().from(users).where(eq(users.id, id)).get();
if (!user) {
return c.json({ error: 'User not found' }, 404);
}
return c.json(user);
});
// Get user with posts (relational query)
app.get('/api/users/:id/posts', async (c) => {
const db = c.get('db');
const id = parseInt(c.req.param('id'));
if (isNaN(id)) {
return c.json({ error: 'Invalid user ID' }, 400);
}
const userWithPosts = await db.query.users.findFirst({
where: eq(users.id, id),
with: {
posts: {
orderBy: (posts, { desc }) => [desc(posts.createdAt)],
},
},
});
if (!userWithPosts) {
return c.json({ error: 'User not found' }, 404);
}
return c.json(userWithPosts);
});
// Create user
app.post('/api/users', async (c) => {
const db = c.get('db');
try {
const body = await c.req.json();
// Validate input
if (!body.email || !body.name) {
return c.json({ error: 'Email and name are required' }, 400);
}
// Check if user exists
const existing = await db
.select()
.from(users)
.where(eq(users.email, body.email))
.get();
if (existing) {
return c.json({ error: 'User with this email already exists' }, 409);
}
// Create user
const [newUser] = await db
.insert(users)
.values({
email: body.email,
name: body.name,
bio: body.bio,
})
.returning();
return c.json(newUser, 201);
} catch (error) {
console.error('Error creating user:', error);
return c.json({ error: 'Failed to create user' }, 500);
}
});
// Update user
app.put('/api/users/:id', async (c) => {
const db = c.get('db');
const id = parseInt(c.req.param('id'));
if (isNaN(id)) {
return c.json({ error: 'Invalid user ID' }, 400);
}
try {
const body = await c.req.json();
const [updated] = await db
.update(users)
.set({
name: body.name,
bio: body.bio,
updatedAt: new Date(),
})
.where(eq(users.id, id))
.returning();
if (!updated) {
return c.json({ error: 'User not found' }, 404);
}
return c.json(updated);
} catch (error) {
console.error('Error updating user:', error);
return c.json({ error: 'Failed to update user' }, 500);
}
});
// Delete user
app.delete('/api/users/:id', async (c) => {
const db = c.get('db');
const id = parseInt(c.req.param('id'));
if (isNaN(id)) {
return c.json({ error: 'Invalid user ID' }, 400);
}
try {
const [deleted] = await db
.delete(users)
.where(eq(users.id, id))
.returning();
if (!deleted) {
return c.json({ error: 'User not found' }, 404);
}
return c.json({ message: 'User deleted successfully', user: deleted });
} catch (error) {
console.error('Error deleting user:', error);
return c.json({ error: 'Failed to delete user' }, 500);
}
});
/**
* Posts Routes
*/
// Get all published posts
app.get('/api/posts', async (c) => {
const db = c.get('db');
const publishedPosts = await db.query.posts.findMany({
where: eq(posts.published, true),
with: {
author: {
columns: {
id: true,
name: true,
email: true,
},
},
},
orderBy: (posts, { desc }) => [desc(posts.createdAt)],
});
return c.json(publishedPosts);
});
// Get post by ID (with author and comments)
app.get('/api/posts/:id', async (c) => {
const db = c.get('db');
const id = parseInt(c.req.param('id'));
if (isNaN(id)) {
return c.json({ error: 'Invalid post ID' }, 400);
}
const post = await db.query.posts.findFirst({
where: eq(posts.id, id),
with: {
author: true,
comments: {
with: {
author: true,
},
orderBy: (comments, { desc }) => [desc(comments.createdAt)],
},
},
});
if (!post) {
return c.json({ error: 'Post not found' }, 404);
}
return c.json(post);
});
// Create post
app.post('/api/posts', async (c) => {
const db = c.get('db');
try {
const body = await c.req.json();
// Validate input
if (!body.title || !body.slug || !body.content || !body.authorId) {
return c.json(
{ error: 'Title, slug, content, and authorId are required' },
400
);
}
// Check if author exists
const author = await db
.select()
.from(users)
.where(eq(users.id, body.authorId))
.get();
if (!author) {
return c.json({ error: 'Author not found' }, 404);
}
// Create post
const [newPost] = await db
.insert(posts)
.values({
title: body.title,
slug: body.slug,
content: body.content,
authorId: body.authorId,
published: body.published ?? false,
})
.returning();
return c.json(newPost, 201);
} catch (error) {
console.error('Error creating post:', error);
return c.json({ error: 'Failed to create post' }, 500);
}
});
/**
* Error Handling
*/
app.onError((err, c) => {
console.error('Unhandled error:', err);
return c.json({ error: 'Internal server error' }, 500);
});
/**
* 404 Handler
*/
app.notFound((c) => {
return c.json({ error: 'Not found' }, 404);
});
/**
* Export Worker
*/
export default app;
/**
* Usage:
*
* 1. Deploy: npx wrangler deploy
* 2. Test: curl https://your-worker.workers.dev/
*
* API Endpoints:
* - GET /api/users - Get all users
* - GET /api/users/:id - Get user by ID
* - GET /api/users/:id/posts - Get user with posts
* - POST /api/users - Create user
* - PUT /api/users/:id - Update user
* - DELETE /api/users/:id - Delete user
* - GET /api/posts - Get all published posts
* - GET /api/posts/:id - Get post with author and comments
* - POST /api/posts - Create post
*/
/**
* Type-Safe Context
*
* For better TypeScript support, you can extend Hono's context
*/
declare module 'hono' {
interface ContextVariableMap {
db: ReturnType<typeof drizzle>;
}
}

View File

@@ -0,0 +1,84 @@
import { defineConfig } from 'drizzle-kit';
/**
* Drizzle Kit Configuration for Cloudflare D1
*
* This configuration uses the D1 HTTP driver to connect to your Cloudflare D1
* database for running migrations, introspection, and Drizzle Studio.
*
* IMPORTANT: Never commit credentials to version control!
* Use environment variables for all sensitive data.
*/
export default defineConfig({
// Schema location (can be a single file or directory)
schema: './src/db/schema.ts',
// Output directory for generated migrations
// This should match the migrations_dir in wrangler.jsonc
out: './migrations',
// Database dialect (D1 is SQLite-based)
dialect: 'sqlite',
// Driver for connecting to D1 via HTTP API
driver: 'd1-http',
// Cloudflare credentials (from environment variables)
dbCredentials: {
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
token: process.env.CLOUDFLARE_D1_TOKEN!,
},
// Enable verbose output for debugging
verbose: true,
// Enable strict mode (recommended)
strict: true,
});
/**
* How to get credentials:
*
* 1. CLOUDFLARE_ACCOUNT_ID
* - Go to Cloudflare Dashboard
* - Click on your account
* - Account ID is shown in the right sidebar
*
* 2. CLOUDFLARE_DATABASE_ID
* - Run: wrangler d1 list
* - Find your database and copy the Database ID
* - Or create a new database: wrangler d1 create my-database
*
* 3. CLOUDFLARE_D1_TOKEN
* - Go to Cloudflare Dashboard → My Profile → API Tokens
* - Click "Create Token"
* - Use template "Edit Cloudflare Workers" or create custom token
* - Make sure it has D1 permissions
*/
/**
* Create a .env file in your project root:
*
* CLOUDFLARE_ACCOUNT_ID=your-account-id-here
* CLOUDFLARE_DATABASE_ID=your-database-id-here
* CLOUDFLARE_D1_TOKEN=your-api-token-here
*
* Never commit .env to Git! Add it to .gitignore.
*/
/**
* Usage:
*
* # Generate migration from schema changes
* npx drizzle-kit generate
*
* # Push schema directly to database (dev only, not recommended for prod)
* npx drizzle-kit push
*
* # Open Drizzle Studio to browse your database
* npx drizzle-kit studio
*
* # Introspect existing database
* npx drizzle-kit introspect
*/

View File

@@ -0,0 +1,68 @@
-- Migration: Initial Schema
-- Generated by Drizzle Kit
-- This is an example migration file showing the structure
-- Actual migrations should be generated with: drizzle-kit generate
-- Create users table
CREATE TABLE `users` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`email` text NOT NULL,
`name` text NOT NULL,
`bio` text,
`created_at` integer NOT NULL,
`updated_at` integer
);
-- Create unique index on email
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);
-- Create index on email for faster lookups
CREATE INDEX `users_email_idx` ON `users` (`email`);
-- Create index on created_at for sorting
CREATE INDEX `users_created_at_idx` ON `users` (`created_at`);
-- Create posts table
CREATE TABLE `posts` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`title` text NOT NULL,
`slug` text NOT NULL,
`content` text NOT NULL,
`published` integer DEFAULT false NOT NULL,
`author_id` integer NOT NULL,
`created_at` integer NOT NULL,
`updated_at` integer,
FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON DELETE cascade
);
-- Create unique index on slug
CREATE UNIQUE INDEX `posts_slug_unique` ON `posts` (`slug`);
-- Create index on slug for URL lookups
CREATE INDEX `posts_slug_idx` ON `posts` (`slug`);
-- Create index on author_id for user's posts
CREATE INDEX `posts_author_idx` ON `posts` (`author_id`);
-- Create composite index on published + created_at
CREATE INDEX `posts_published_created_idx` ON `posts` (`published`, `created_at`);
-- Create comments table
CREATE TABLE `comments` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`content` text NOT NULL,
`post_id` integer NOT NULL,
`author_id` integer NOT NULL,
`created_at` integer NOT NULL,
FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`) ON DELETE cascade,
FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON DELETE cascade
);
-- Create index on post_id for post's comments
CREATE INDEX `comments_post_idx` ON `comments` (`post_id`);
-- Create index on author_id for user's comments
CREATE INDEX `comments_author_idx` ON `comments` (`author_id`);
-- Optimize database
PRAGMA optimize;

40
templates/package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "drizzle-d1-worker",
"version": "1.0.0",
"description": "Cloudflare Worker with Drizzle ORM and D1",
"main": "src/index.ts",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"db:generate": "drizzle-kit generate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
"db:migrate:local": "wrangler d1 migrations apply my-database --local",
"db:migrate:remote": "wrangler d1 migrations apply my-database --remote",
"db:create": "wrangler d1 create my-database",
"db:list": "wrangler d1 list",
"db:info": "wrangler d1 info my-database",
"db:delete": "wrangler d1 delete my-database"
},
"dependencies": {
"drizzle-orm": "^0.44.7",
"hono": "^4.6.11"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20251014.0",
"drizzle-kit": "^0.31.5",
"typescript": "^5.7.3",
"wrangler": "^4.43.0"
},
"keywords": [
"cloudflare",
"workers",
"drizzle",
"d1",
"orm",
"database",
"typescript"
],
"author": "",
"license": "MIT"
}

View File

@@ -0,0 +1,305 @@
/**
* Prepared Statements with Drizzle ORM and D1
*
* Prepared statements allow you to define queries once and execute them
* multiple times with different parameters for better performance.
*
* IMPORTANT: D1 doesn't cache prepared statements between requests like
* traditional SQLite. They're still useful for code reusability and type safety.
*/
import { drizzle } from 'drizzle-orm/d1';
import { users, posts, comments } from './schema';
import { eq, and, gte, sql } from 'drizzle-orm';
/**
* Basic Prepared Statements
*/
// Get user by ID (prepared)
export function prepareGetUserById(db: ReturnType<typeof drizzle>) {
return db
.select()
.from(users)
.where(eq(users.id, sql.placeholder('id')))
.prepare();
}
// Usage:
// const getUserById = prepareGetUserById(db);
// const user1 = await getUserById.get({ id: 1 });
// const user2 = await getUserById.get({ id: 2 });
// Get user by email (prepared)
export function prepareGetUserByEmail(db: ReturnType<typeof drizzle>) {
return db
.select()
.from(users)
.where(eq(users.email, sql.placeholder('email')))
.prepare();
}
// Get posts by author (prepared)
export function prepareGetPostsByAuthor(db: ReturnType<typeof drizzle>) {
return db
.select()
.from(posts)
.where(eq(posts.authorId, sql.placeholder('authorId')))
.prepare();
}
/**
* Prepared Statements with Multiple Parameters
*/
// Get published posts after a date
export function prepareGetPublishedPostsAfterDate(db: ReturnType<typeof drizzle>) {
return db
.select()
.from(posts)
.where(
and(
eq(posts.published, sql.placeholder('published')),
gte(posts.createdAt, sql.placeholder('afterDate'))
)
)
.prepare();
}
// Usage:
// const getPublishedPosts = prepareGetPublishedPostsAfterDate(db);
// const recentPosts = await getPublishedPosts.all({
// published: true,
// afterDate: new Date('2024-01-01'),
// });
// Search users by partial email/name match
export function prepareSearchUsers(db: ReturnType<typeof drizzle>) {
return db
.select()
.from(users)
.where(
sql`${users.email} LIKE ${sql.placeholder('searchTerm')} OR ${users.name} LIKE ${sql.placeholder('searchTerm')}`
)
.prepare();
}
// Usage:
// const searchUsers = prepareSearchUsers(db);
// const results = await searchUsers.all({ searchTerm: '%john%' });
/**
* Prepared Statements for INSERT
*/
// Insert user (prepared)
export function prepareInsertUser(db: ReturnType<typeof drizzle>) {
return db
.insert(users)
.values({
email: sql.placeholder('email'),
name: sql.placeholder('name'),
bio: sql.placeholder('bio'),
})
.returning()
.prepare();
}
// Usage:
// const insertUser = prepareInsertUser(db);
// const [newUser] = await insertUser.get({
// email: 'test@example.com',
// name: 'Test User',
// bio: null,
// });
// Insert post (prepared)
export function prepareInsertPost(db: ReturnType<typeof drizzle>) {
return db
.insert(posts)
.values({
title: sql.placeholder('title'),
slug: sql.placeholder('slug'),
content: sql.placeholder('content'),
authorId: sql.placeholder('authorId'),
published: sql.placeholder('published'),
})
.returning()
.prepare();
}
/**
* Prepared Statements for UPDATE
*/
// Update user name (prepared)
export function prepareUpdateUserName(db: ReturnType<typeof drizzle>) {
return db
.update(users)
.set({
name: sql.placeholder('name'),
updatedAt: sql.placeholder('updatedAt'),
})
.where(eq(users.id, sql.placeholder('id')))
.returning()
.prepare();
}
// Usage:
// const updateUserName = prepareUpdateUserName(db);
// const [updated] = await updateUserName.get({
// id: 1,
// name: 'New Name',
// updatedAt: new Date(),
// });
// Publish post (prepared)
export function preparePublishPost(db: ReturnType<typeof drizzle>) {
return db
.update(posts)
.set({
published: true,
updatedAt: sql.placeholder('updatedAt'),
})
.where(eq(posts.id, sql.placeholder('id')))
.returning()
.prepare();
}
/**
* Prepared Statements for DELETE
*/
// Delete user (prepared)
export function prepareDeleteUser(db: ReturnType<typeof drizzle>) {
return db
.delete(users)
.where(eq(users.id, sql.placeholder('id')))
.returning()
.prepare();
}
// Delete posts by author (prepared)
export function prepareDeletePostsByAuthor(db: ReturnType<typeof drizzle>) {
return db
.delete(posts)
.where(eq(posts.authorId, sql.placeholder('authorId')))
.returning()
.prepare();
}
/**
* Best Practices
*/
// Create a class to encapsulate all prepared statements
export class PreparedQueries {
private db: ReturnType<typeof drizzle>;
// Prepared statements
private getUserByIdStmt;
private getUserByEmailStmt;
private insertUserStmt;
private updateUserNameStmt;
private deleteUserStmt;
constructor(db: ReturnType<typeof drizzle>) {
this.db = db;
// Initialize all prepared statements once
this.getUserByIdStmt = prepareGetUserById(db);
this.getUserByEmailStmt = prepareGetUserByEmail(db);
this.insertUserStmt = prepareInsertUser(db);
this.updateUserNameStmt = prepareUpdateUserName(db);
this.deleteUserStmt = prepareDeleteUser(db);
}
// Convenient methods that use prepared statements
async getUserById(id: number) {
return await this.getUserByIdStmt.get({ id });
}
async getUserByEmail(email: string) {
return await this.getUserByEmailStmt.get({ email });
}
async insertUser(data: { email: string; name: string; bio?: string | null }) {
const [user] = await this.insertUserStmt.get({
email: data.email,
name: data.name,
bio: data.bio ?? null,
});
return user;
}
async updateUserName(id: number, name: string) {
const [user] = await this.updateUserNameStmt.get({
id,
name,
updatedAt: new Date(),
});
return user;
}
async deleteUser(id: number) {
const [user] = await this.deleteUserStmt.get({ id });
return user;
}
}
// Usage:
// const queries = new PreparedQueries(db);
// const user = await queries.getUserById(1);
/**
* Performance Considerations for D1
*
* Unlike traditional SQLite:
* - D1 doesn't cache prepared statements between requests
* - Each request starts fresh
* - Prepared statements are still useful for:
* 1. Code reusability
* 2. Type safety
* 3. Preventing SQL injection
* 4. Cleaner code organization
*
* But don't expect:
* - Performance improvements from statement caching
* - Faster execution on repeated calls
* - Shared state between requests
*/
/**
* When to Use Prepared Statements:
*
* ✅ Good for:
* - Queries you'll execute multiple times in the same request
* - Complex queries with dynamic parameters
* - Code organization and reusability
* - Type-safe parameter passing
*
* ❌ Not necessary for:
* - One-off queries
* - Simple CRUD operations
* - Static queries without parameters
*/
/**
* Execution Methods:
*
* - .all() - Returns all results as array
* - .get() - Returns first result or undefined
* - .run() - Executes query, returns metadata only
*/
/**
* TypeScript Types
*/
import type { InferSelectModel } from 'drizzle-orm';
export type PreparedQuery<T> = {
all: (params: T) => Promise<any[]>;
get: (params: T) => Promise<any | undefined>;
run: (params: T) => Promise<any>;
};

View File

@@ -0,0 +1,277 @@
/**
* Relational Queries with Drizzle ORM
*
* This file demonstrates how to query relations between tables using:
* 1. Drizzle's relational query API (db.query)
* 2. Manual JOINs
*/
import { drizzle } from 'drizzle-orm/d1';
import { users, posts, comments, usersRelations, postsRelations, commentsRelations } from './schema';
import { eq, desc, sql } from 'drizzle-orm';
/**
* IMPORTANT: To use relational queries (db.query), you must pass the schema to drizzle()
*
* const db = drizzle(env.DB, {
* schema: { users, posts, comments, usersRelations, postsRelations, commentsRelations }
* });
*/
/**
* Relational Query API (db.query)
*
* This is the recommended way to query relations in Drizzle
*/
// Get user with all their posts
export async function getUserWithPosts(db: ReturnType<typeof drizzle>, userId: number) {
return await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
posts: true,
},
});
}
// Get user with posts and comments
export async function getUserWithPostsAndComments(db: ReturnType<typeof drizzle>, userId: number) {
return await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
posts: true,
comments: true,
},
});
}
// Get post with author
export async function getPostWithAuthor(db: ReturnType<typeof drizzle>, postId: number) {
return await db.query.posts.findFirst({
where: eq(posts.id, postId),
with: {
author: true,
},
});
}
// Get post with author and comments
export async function getPostWithAuthorAndComments(db: ReturnType<typeof drizzle>, postId: number) {
return await db.query.posts.findFirst({
where: eq(posts.id, postId),
with: {
author: true,
comments: {
with: {
author: true, // Nested: get comment author too
},
},
},
});
}
// Get all published posts with authors
export async function getPublishedPostsWithAuthors(db: ReturnType<typeof drizzle>, limit = 10) {
return await db.query.posts.findMany({
where: eq(posts.published, true),
with: {
author: {
columns: {
id: true,
name: true,
email: true,
// Exclude bio and timestamps
},
},
},
orderBy: [desc(posts.createdAt)],
limit,
});
}
// Get user with filtered posts (only published)
export async function getUserWithPublishedPosts(db: ReturnType<typeof drizzle>, userId: number) {
return await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
posts: {
where: eq(posts.published, true),
orderBy: [desc(posts.createdAt)],
},
},
});
}
// Get post with recent comments
export async function getPostWithRecentComments(
db: ReturnType<typeof drizzle>,
postId: number,
commentLimit = 10
) {
return await db.query.posts.findFirst({
where: eq(posts.id, postId),
with: {
author: true,
comments: {
with: {
author: true,
},
orderBy: [desc(comments.createdAt)],
limit: commentLimit,
},
},
});
}
/**
* Manual JOINs
*
* For more complex queries, you can use manual JOINs
*/
// Left join: Get all users with their post counts
export async function getUsersWithPostCounts(db: ReturnType<typeof drizzle>) {
return await db
.select({
user: users,
postCount: sql<number>`count(${posts.id})`,
})
.from(users)
.leftJoin(posts, eq(posts.authorId, users.id))
.groupBy(users.id)
.all();
}
// Inner join: Get users who have posts
export async function getUsersWithPosts(db: ReturnType<typeof drizzle>) {
return await db
.select({
user: users,
post: posts,
})
.from(users)
.innerJoin(posts, eq(posts.authorId, users.id))
.all();
}
// Multiple joins: Get comments with post and author info
export async function getCommentsWithDetails(db: ReturnType<typeof drizzle>) {
return await db
.select({
comment: comments,
post: {
id: posts.id,
title: posts.title,
slug: posts.slug,
},
author: {
id: users.id,
name: users.name,
email: users.email,
},
})
.from(comments)
.innerJoin(posts, eq(comments.postId, posts.id))
.innerJoin(users, eq(comments.authorId, users.id))
.all();
}
// Complex join with aggregation
export async function getPostsWithCommentCounts(db: ReturnType<typeof drizzle>) {
return await db
.select({
post: posts,
author: users,
commentCount: sql<number>`count(${comments.id})`,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.leftJoin(comments, eq(comments.postId, posts.id))
.groupBy(posts.id, users.id)
.all();
}
/**
* Subqueries
*/
// Get users with more than 5 posts
export async function getActiveAuthors(db: ReturnType<typeof drizzle>) {
const postCounts = db
.select({
authorId: posts.authorId,
count: sql<number>`count(*)`.as('count'),
})
.from(posts)
.groupBy(posts.authorId)
.as('post_counts');
return await db
.select({
user: users,
postCount: postCounts.count,
})
.from(users)
.innerJoin(postCounts, eq(users.id, postCounts.authorId))
.where(sql`${postCounts.count} > 5`)
.all();
}
/**
* Aggregations with Relations
*/
// Get post statistics
export async function getPostStatistics(db: ReturnType<typeof drizzle>, postId: number) {
const [stats] = await db
.select({
post: posts,
commentCount: sql<number>`count(DISTINCT ${comments.id})`,
})
.from(posts)
.leftJoin(comments, eq(comments.postId, posts.id))
.where(eq(posts.id, postId))
.groupBy(posts.id)
.all();
return stats;
}
/**
* Tips for Relational Queries:
*
* 1. Use db.query for simple relations (cleaner syntax, type-safe)
* 2. Use manual JOINs for complex queries with aggregations
* 3. Use `with` to load nested relations
* 4. Use `columns` to select specific fields
* 5. Apply `where`, `orderBy`, `limit` within relations
* 6. Remember: Must pass schema to drizzle() for db.query to work
*/
/**
* Performance Tips:
*
* 1. Be selective with relations (only load what you need)
* 2. Use `columns` to exclude unnecessary fields
* 3. Apply limits to prevent loading too much data
* 4. Consider pagination for large datasets
* 5. Use indexes on foreign keys (already done in schema.ts)
*/
/**
* Common Patterns:
*
* 1. One-to-Many: User has many Posts
* - Use: db.query.users.findFirst({ with: { posts: true } })
*
* 2. Many-to-One: Post belongs to User
* - Use: db.query.posts.findFirst({ with: { author: true } })
*
* 3. Nested Relations: Post with Author and Comments (with their Authors)
* - Use: db.query.posts.findFirst({
* with: {
* author: true,
* comments: { with: { author: true } }
* }
* })
*/

237
templates/schema.ts Normal file
View File

@@ -0,0 +1,237 @@
/**
* Database Schema for Drizzle ORM with Cloudflare D1
*
* This file defines the database schema including tables, columns, constraints,
* and relations. Drizzle uses this to generate TypeScript types and SQL migrations.
*
* Example: Blog database with users, posts, and comments
*/
import { sqliteTable, text, integer, index } from 'drizzle-orm/sqlite-core';
import { relations } from 'drizzle-orm';
/**
* Users Table
*
* Stores user accounts with email authentication
*/
export const users = sqliteTable(
'users',
{
// Primary key with auto-increment
id: integer('id').primaryKey({ autoIncrement: true }),
// Email (required, unique)
email: text('email').notNull().unique(),
// Name (required)
name: text('name').notNull(),
// Bio (optional, longer text)
bio: text('bio'),
// Created timestamp (integer in Unix milliseconds)
// Use integer with mode: 'timestamp' for dates in D1/SQLite
createdAt: integer('created_at', { mode: 'timestamp' })
.$defaultFn(() => new Date()),
// Updated timestamp (optional)
updatedAt: integer('updated_at', { mode: 'timestamp' }),
},
(table) => {
return {
// Index on email for fast lookups (already unique, but helps with queries)
emailIdx: index('users_email_idx').on(table.email),
// Index on createdAt for sorting
createdAtIdx: index('users_created_at_idx').on(table.createdAt),
};
}
);
/**
* Posts Table
*
* Stores blog posts written by users
*/
export const posts = sqliteTable(
'posts',
{
id: integer('id').primaryKey({ autoIncrement: true }),
// Title (required)
title: text('title').notNull(),
// Slug for URLs (unique)
slug: text('slug').notNull().unique(),
// Content (required)
content: text('content').notNull(),
// Published status (default to false)
published: integer('published', { mode: 'boolean' }).notNull().default(false),
// Foreign key to users table
// onDelete: 'cascade' means deleting a user deletes their posts
authorId: integer('author_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
// Timestamps
createdAt: integer('created_at', { mode: 'timestamp' })
.$defaultFn(() => new Date()),
updatedAt: integer('updated_at', { mode: 'timestamp' }),
},
(table) => {
return {
// Index on slug for URL lookups
slugIdx: index('posts_slug_idx').on(table.slug),
// Index on authorId for user's posts
authorIdx: index('posts_author_idx').on(table.authorId),
// Index on published + createdAt for listing published posts
publishedCreatedIdx: index('posts_published_created_idx').on(
table.published,
table.createdAt
),
};
}
);
/**
* Comments Table
*
* Stores comments on posts
*/
export const comments = sqliteTable(
'comments',
{
id: integer('id').primaryKey({ autoIncrement: true }),
// Comment content
content: text('content').notNull(),
// Foreign key to posts (cascade delete)
postId: integer('post_id')
.notNull()
.references(() => posts.id, { onDelete: 'cascade' }),
// Foreign key to users (cascade delete)
authorId: integer('author_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
// Timestamps
createdAt: integer('created_at', { mode: 'timestamp' })
.$defaultFn(() => new Date()),
},
(table) => {
return {
// Index on postId for post's comments
postIdx: index('comments_post_idx').on(table.postId),
// Index on authorId for user's comments
authorIdx: index('comments_author_idx').on(table.authorId),
};
}
);
/**
* Relations
*
* Define relationships between tables for type-safe joins
* These are used by Drizzle's relational query API
*/
// User has many posts
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
comments: many(comments),
}));
// Post belongs to one user, has many comments
export const postsRelations = relations(posts, ({ one, many }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
comments: many(comments),
}));
// Comment belongs to one post and one user
export const commentsRelations = relations(comments, ({ one }) => ({
post: one(posts, {
fields: [comments.postId],
references: [posts.id],
}),
author: one(users, {
fields: [comments.authorId],
references: [users.id],
}),
}));
/**
* TypeScript Types
*
* Infer types from schema for use in your application
*/
import { InferSelectModel, InferInsertModel } from 'drizzle-orm';
// Select types (for reading from database)
export type User = InferSelectModel<typeof users>;
export type Post = InferSelectModel<typeof posts>;
export type Comment = InferSelectModel<typeof comments>;
// Insert types (for writing to database, optional fields allowed)
export type NewUser = InferInsertModel<typeof users>;
export type NewPost = InferInsertModel<typeof posts>;
export type NewComment = InferInsertModel<typeof comments>;
/**
* Usage Examples:
*
* const user: User = await db.select().from(users).where(eq(users.id, 1)).get();
*
* const newUser: NewUser = {
* email: 'test@example.com',
* name: 'Test User',
* // createdAt is optional (has default)
* };
*
* await db.insert(users).values(newUser);
*/
/**
* Column Types Reference:
*
* Text:
* - text('column_name') - Variable length text
* - text('column_name', { length: 255 }) - Max length (not enforced by SQLite)
*
* Integer:
* - integer('column_name') - Integer number
* - integer('column_name', { mode: 'number' }) - JavaScript number (default)
* - integer('column_name', { mode: 'boolean' }) - Boolean (0 = false, 1 = true)
* - integer('column_name', { mode: 'timestamp' }) - JavaScript Date object
* - integer('column_name', { mode: 'timestamp_ms' }) - Milliseconds timestamp
*
* Real:
* - real('column_name') - Floating point number
*
* Blob:
* - blob('column_name') - Binary data
* - blob('column_name', { mode: 'buffer' }) - Node.js Buffer
* - blob('column_name', { mode: 'json' }) - JSON stored as blob
*
* Modifiers:
* - .notNull() - NOT NULL constraint
* - .unique() - UNIQUE constraint
* - .default(value) - DEFAULT value
* - .$defaultFn(() => value) - Dynamic default (function)
* - .primaryKey() - PRIMARY KEY
* - .primaryKey({ autoIncrement: true }) - AUTO INCREMENT
* - .references(() => table.column) - FOREIGN KEY
* - .references(() => table.column, { onDelete: 'cascade' }) - CASCADE DELETE
*/

257
templates/transactions.ts Normal file
View File

@@ -0,0 +1,257 @@
/**
* Transactions with Drizzle ORM and D1 Batch API
*
* IMPORTANT: D1 does not support traditional SQL transactions (BEGIN/COMMIT/ROLLBACK).
* Instead, use D1's batch API to execute multiple statements atomically.
*
* Issue: https://github.com/drizzle-team/drizzle-orm/issues/4212
*/
import { drizzle } from 'drizzle-orm/d1';
import { users, posts, comments } from './schema';
/**
* ❌ DON'T: Use Drizzle's transaction API
*
* This will fail with D1_ERROR: Cannot use BEGIN TRANSACTION
*/
export async function DONT_useTraditionalTransaction(db: ReturnType<typeof drizzle>) {
try {
await db.transaction(async (tx) => {
await tx.insert(users).values({ email: 'test@example.com', name: 'Test' });
await tx.insert(posts).values({ title: 'Post', slug: 'post', content: 'Content', authorId: 1 });
});
} catch (error) {
console.error('Transaction failed:', error);
// Error: D1_ERROR: Cannot use BEGIN TRANSACTION
}
}
/**
* ✅ DO: Use D1 Batch API
*
* Execute multiple statements in a single batch
*/
// Basic batch insert
export async function batchInsertUsers(
db: ReturnType<typeof drizzle>,
usersData: { email: string; name: string }[]
) {
const statements = usersData.map((user) =>
db.insert(users).values(user).returning()
);
// All statements execute atomically
const results = await db.batch(statements);
return results;
}
// Batch insert with related records
export async function createUserWithPosts(
db: ReturnType<typeof drizzle>,
userData: { email: string; name: string },
postsData: { title: string; slug: string; content: string }[]
) {
try {
// First insert user
const [user] = await db.insert(users).values(userData).returning();
// Then batch insert posts
const postStatements = postsData.map((post) =>
db.insert(posts).values({ ...post, authorId: user.id }).returning()
);
const postResults = await db.batch(postStatements);
return {
user,
posts: postResults.flat(),
};
} catch (error) {
console.error('Batch operation failed:', error);
// Manual cleanup if needed
// await db.delete(users).where(eq(users.email, userData.email));
throw error;
}
}
// Batch with mixed operations (insert, update, delete)
export async function batchMixedOperations(db: ReturnType<typeof drizzle>) {
const results = await db.batch([
// Insert new user
db.insert(users).values({ email: 'new@example.com', name: 'New User' }).returning(),
// Update existing post
db.update(posts).set({ published: true }).where(eq(posts.id, 1)).returning(),
// Delete old comments
db.delete(comments).where(lt(comments.createdAt, new Date('2024-01-01'))).returning(),
]);
const [newUsers, updatedPosts, deletedComments] = results;
return {
newUsers,
updatedPosts,
deletedComments,
};
}
/**
* Error Handling with Batch API
*
* If any statement in a batch fails, the entire batch fails.
* However, D1 doesn't provide automatic rollback like traditional transactions.
*/
// Batch with error handling
export async function batchWithErrorHandling(
db: ReturnType<typeof drizzle>,
usersData: { email: string; name: string }[]
) {
const statements = usersData.map((user) =>
db.insert(users).values(user).returning()
);
try {
const results = await db.batch(statements);
console.log('All operations succeeded');
return { success: true, results };
} catch (error) {
console.error('Batch failed:', error);
// Implement manual cleanup logic if needed
// For example, delete any partially created records
return { success: false, error };
}
}
// Batch with validation before execution
export async function safeBatchInsert(
db: ReturnType<typeof drizzle>,
usersData: { email: string; name: string }[]
) {
// Validate all data before batching
for (const user of usersData) {
if (!user.email || !user.name) {
throw new Error('Invalid user data');
}
// Check for duplicates
const existing = await db
.select()
.from(users)
.where(eq(users.email, user.email))
.get();
if (existing) {
throw new Error(`User with email ${user.email} already exists`);
}
}
// All validation passed, execute batch
const statements = usersData.map((user) =>
db.insert(users).values(user).returning()
);
return await db.batch(statements);
}
/**
* Batch Query Patterns
*/
// Batch read operations
export async function batchReadOperations(db: ReturnType<typeof drizzle>, userIds: number[]) {
const queries = userIds.map((id) =>
db.select().from(users).where(eq(users.id, id))
);
return await db.batch(queries);
}
// Batch with dependent operations
export async function createBlogPost(
db: ReturnType<typeof drizzle>,
postData: { title: string; slug: string; content: string; authorId: number },
tagsData: string[]
) {
// Insert post first
const [post] = await db.insert(posts).values(postData).returning();
// Then batch insert tags (if you had a tags table)
// This is a two-step process because we need the post.id
return post;
}
/**
* Performance Optimization with Batch
*/
// Batch insert for large datasets
export async function bulkInsertUsers(
db: ReturnType<typeof drizzle>,
usersData: { email: string; name: string }[]
) {
const BATCH_SIZE = 100; // Process in chunks
const results = [];
for (let i = 0; i < usersData.length; i += BATCH_SIZE) {
const chunk = usersData.slice(i, i + BATCH_SIZE);
const statements = chunk.map((user) =>
db.insert(users).values(user).returning()
);
const chunkResults = await db.batch(statements);
results.push(...chunkResults);
}
return results.flat();
}
/**
* Important Notes:
*
* 1. D1 Batch API vs Traditional Transactions:
* - Batch API: Executes multiple statements in one round-trip
* - Traditional transactions: Support ROLLBACK on error (not available in D1)
*
* 2. Error Handling:
* - If batch fails, manually clean up any partially created records
* - Use try-catch for error handling
* - Validate data before executing batch
*
* 3. Atomicity:
* - All statements in a batch execute together
* - If one fails, the entire batch fails
* - No partial success (all or nothing)
*
* 4. Performance:
* - Batching reduces round-trips to the database
* - Process large datasets in chunks (100-1000 records)
* - Consider rate limits when batching
*
* 5. Supported Operations:
* - INSERT
* - UPDATE
* - DELETE
* - SELECT
* - Mixed operations in a single batch
*/
/**
* Workaround for Complex Transactions:
*
* If you need more complex transaction logic with rollback:
* 1. Use application-level transaction management
* 2. Implement compensating transactions
* 3. Use idempotency keys to prevent duplicate operations
* 4. Consider using a different database if ACID is critical
*/
import { eq, lt } from 'drizzle-orm';