Initial commit
This commit is contained in:
333
templates/basic-queries.ts
Normal file
333
templates/basic-queries.ts
Normal 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
109
templates/client.ts
Normal 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
|
||||
*/
|
||||
367
templates/cloudflare-worker-integration.ts
Normal file
367
templates/cloudflare-worker-integration.ts
Normal 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>;
|
||||
}
|
||||
}
|
||||
84
templates/drizzle.config.ts
Normal file
84
templates/drizzle.config.ts
Normal 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
|
||||
*/
|
||||
68
templates/migrations/0001_example.sql
Normal file
68
templates/migrations/0001_example.sql
Normal 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
40
templates/package.json
Normal 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"
|
||||
}
|
||||
305
templates/prepared-statements.ts
Normal file
305
templates/prepared-statements.ts
Normal 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>;
|
||||
};
|
||||
277
templates/relations-queries.ts
Normal file
277
templates/relations-queries.ts
Normal 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
237
templates/schema.ts
Normal 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
257
templates/transactions.ts
Normal 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';
|
||||
Reference in New Issue
Block a user