Files
gh-jezweb-claude-skills-ski…/templates/drizzle-queries.ts
2025-11-30 08:25:01 +08:00

544 lines
13 KiB
TypeScript

// Drizzle ORM Query Patterns for Neon Postgres
// Type-safe queries with full TypeScript support
import { db } from './db'; // Assuming you have db/index.ts set up
import { users, posts, comments } from './db/schema'; // Assuming you have db/schema.ts
import { eq, and, or, gt, lt, gte, lte, like, inArray, isNull, isNotNull, desc, asc } from 'drizzle-orm';
// ============================================================================
// SELECT QUERIES
// ============================================================================
// Simple select all
export async function getAllUsers() {
const allUsers = await db.select().from(users);
return allUsers;
}
// Select specific columns
export async function getUserEmails() {
const emails = await db
.select({
id: users.id,
email: users.email
})
.from(users);
return emails;
}
// Select with WHERE clause
export async function getUserById(id: number) {
const [user] = await db
.select()
.from(users)
.where(eq(users.id, id));
return user || null;
}
// Select with multiple conditions (AND)
export async function getActiveUserByEmail(email: string) {
const [user] = await db
.select()
.from(users)
.where(
and(
eq(users.email, email),
eq(users.active, true)
)
);
return user || null;
}
// Select with OR conditions
export async function searchUsers(searchTerm: string) {
const results = await db
.select()
.from(users)
.where(
or(
like(users.name, `%${searchTerm}%`),
like(users.email, `%${searchTerm}%`)
)
);
return results;
}
// Select with comparison operators
export async function getRecentPosts(daysAgo: number) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysAgo);
const recentPosts = await db
.select()
.from(posts)
.where(gte(posts.createdAt, cutoffDate))
.orderBy(desc(posts.createdAt));
return recentPosts;
}
// Select with IN clause
export async function getUsersByIds(ids: number[]) {
const selectedUsers = await db
.select()
.from(users)
.where(inArray(users.id, ids));
return selectedUsers;
}
// Select with NULL checks
export async function getUsersWithoutAvatar() {
const usersNoAvatar = await db
.select()
.from(users)
.where(isNull(users.avatarUrl));
return usersNoAvatar;
}
// ============================================================================
// JOINS
// ============================================================================
// Inner join
export async function getPostsWithAuthors() {
const postsWithAuthors = await db
.select({
postId: posts.id,
postTitle: posts.title,
postContent: posts.content,
authorId: users.id,
authorName: users.name,
authorEmail: users.email
})
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.orderBy(desc(posts.createdAt));
return postsWithAuthors;
}
// Left join (get all posts, even without authors)
export async function getAllPostsWithOptionalAuthors() {
const allPosts = await db
.select({
postId: posts.id,
postTitle: posts.title,
authorName: users.name // Will be null if no author
})
.from(posts)
.leftJoin(users, eq(posts.userId, users.id));
return allPosts;
}
// Multiple joins
export async function getPostsWithAuthorsAndComments() {
const data = await db
.select({
postId: posts.id,
postTitle: posts.title,
authorName: users.name,
commentCount: comments.id // Will need aggregation for actual count
})
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.leftJoin(comments, eq(comments.postId, posts.id));
return data;
}
// ============================================================================
// INSERT QUERIES
// ============================================================================
// Simple insert
export async function createUser(name: string, email: string) {
const [newUser] = await db
.insert(users)
.values({
name,
email
})
.returning();
return newUser;
}
// Insert multiple rows
export async function createMultipleUsers(userData: Array<{ name: string; email: string }>) {
const newUsers = await db
.insert(users)
.values(userData)
.returning();
return newUsers;
}
// Insert with default values
export async function createPost(userId: number, title: string, content: string) {
const [newPost] = await db
.insert(posts)
.values({
userId,
title,
content
// createdAt will use default NOW()
})
.returning();
return newPost;
}
// ============================================================================
// UPDATE QUERIES
// ============================================================================
// Simple update
export async function updateUserName(id: number, newName: string) {
const [updated] = await db
.update(users)
.set({ name: newName })
.where(eq(users.id, id))
.returning();
return updated || null;
}
// Update multiple fields
export async function updateUser(id: number, updates: { name?: string; email?: string; avatarUrl?: string }) {
const [updated] = await db
.update(users)
.set(updates)
.where(eq(users.id, id))
.returning();
return updated || null;
}
// Conditional update
export async function publishPost(postId: number, userId: number) {
// Only allow user to publish their own post
const [published] = await db
.update(posts)
.set({ published: true, publishedAt: new Date() })
.where(
and(
eq(posts.id, postId),
eq(posts.userId, userId)
)
)
.returning();
return published || null;
}
// ============================================================================
// DELETE QUERIES
// ============================================================================
// Simple delete
export async function deleteUser(id: number) {
const [deleted] = await db
.delete(users)
.where(eq(users.id, id))
.returning({ id: users.id });
return deleted ? true : false;
}
// Conditional delete
export async function deleteOldPosts(daysAgo: number) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysAgo);
const deleted = await db
.delete(posts)
.where(lt(posts.createdAt, cutoffDate))
.returning({ id: posts.id });
return deleted.length;
}
// Delete with complex conditions
export async function deleteUnpublishedDrafts(userId: number, daysOld: number) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
const deleted = await db
.delete(posts)
.where(
and(
eq(posts.userId, userId),
eq(posts.published, false),
lt(posts.createdAt, cutoffDate)
)
)
.returning({ id: posts.id });
return deleted;
}
// ============================================================================
// TRANSACTIONS
// ============================================================================
// Transaction example: Transfer credits between users
export async function transferCredits(fromUserId: number, toUserId: number, amount: number) {
const result = await db.transaction(async (tx) => {
// Get sender's balance
const [sender] = await tx
.select()
.from(users)
.where(eq(users.id, fromUserId))
.for('update'); // Lock row
if (!sender || sender.credits < amount) {
throw new Error('Insufficient credits');
}
// Deduct from sender
const [updatedSender] = await tx
.update(users)
.set({ credits: sender.credits - amount })
.where(eq(users.id, fromUserId))
.returning();
// Add to recipient
const [updatedRecipient] = await tx
.update(users)
.set({ credits: tx.sql`${users.credits} + ${amount}` })
.where(eq(users.id, toUserId))
.returning();
// Log transaction
await tx.insert(creditTransfers).values({
fromUserId,
toUserId,
amount
});
return { sender: updatedSender, recipient: updatedRecipient };
});
return result;
}
// Transaction with rollback
export async function createUserWithProfile(userData: {
name: string;
email: string;
bio?: string;
}) {
try {
const result = await db.transaction(async (tx) => {
// Create user
const [user] = await tx
.insert(users)
.values({
name: userData.name,
email: userData.email
})
.returning();
// Create profile
const [profile] = await tx
.insert(profiles)
.values({
userId: user.id,
bio: userData.bio || ''
})
.returning();
return { user, profile };
});
return result;
} catch (error) {
// Transaction automatically rolls back on error
console.error('Failed to create user with profile:', error);
throw error;
}
}
// ============================================================================
// AGGREGATIONS
// ============================================================================
// Count
export async function countUsers() {
const [{ count }] = await db
.select({ count: sql<number>`count(*)` })
.from(users);
return count;
}
// Group by
export async function getPostCountByUser() {
const counts = await db
.select({
userId: posts.userId,
userName: users.name,
postCount: sql<number>`count(${posts.id})`
})
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.groupBy(posts.userId, users.name);
return counts;
}
// Having clause
export async function getUsersWithMultiplePosts(minPosts: number) {
const users = await db
.select({
userId: posts.userId,
userName: users.name,
postCount: sql<number>`count(${posts.id})`
})
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.groupBy(posts.userId, users.name)
.having(sql`count(${posts.id}) >= ${minPosts}`);
return users;
}
// ============================================================================
// PAGINATION
// ============================================================================
export async function getPaginatedPosts(page: number = 1, pageSize: number = 20) {
const offset = (page - 1) * pageSize;
// Get total count
const [{ total }] = await db
.select({ total: sql<number>`count(*)` })
.from(posts);
// Get page data
const postsData = await db
.select()
.from(posts)
.orderBy(desc(posts.createdAt))
.limit(pageSize)
.offset(offset);
return {
posts: postsData,
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize),
hasNext: page * pageSize < total,
hasPrev: page > 1
}
};
}
// ============================================================================
// ADVANCED QUERIES
// ============================================================================
// Subquery
export async function getUsersWithNoPosts() {
const usersWithoutPosts = await db
.select()
.from(users)
.where(
sql`${users.id} NOT IN (SELECT DISTINCT user_id FROM posts)`
);
return usersWithoutPosts;
}
// Raw SQL with Drizzle (when needed)
export async function customQuery(userId: number) {
const result = await db.execute(
sql`
SELECT u.*, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
WHERE u.id = ${userId}
GROUP BY u.id
`
);
return result.rows[0];
}
// Prepared statement (performance optimization)
const getUserByEmailPrepared = db
.select()
.from(users)
.where(eq(users.email, sql.placeholder('email')))
.prepare('get_user_by_email');
export async function getUserByEmail(email: string) {
const [user] = await getUserByEmailPrepared.execute({ email });
return user || null;
}
// ============================================================================
// USAGE EXAMPLE: Cloudflare Worker with Drizzle
// ============================================================================
/*
// db/index.ts
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
import * as schema from './schema';
export function getDb(databaseUrl: string) {
const sql = neon(databaseUrl);
return drizzle(sql, { schema });
}
// src/index.ts
import { getDb } from './db';
import { users } from './db/schema';
import { eq } from 'drizzle-orm';
interface Env {
DATABASE_URL: string;
}
export default {
async fetch(request: Request, env: Env) {
const db = getDb(env.DATABASE_URL);
const url = new URL(request.url);
if (url.pathname === '/users' && request.method === 'GET') {
const allUsers = await db.select().from(users);
return Response.json(allUsers);
}
if (url.pathname.startsWith('/users/') && request.method === 'GET') {
const id = parseInt(url.pathname.split('/')[2]);
const [user] = await db.select().from(users).where(eq(users.id, id));
if (!user) {
return new Response('User not found', { status: 404 });
}
return Response.json(user);
}
if (url.pathname === '/users' && request.method === 'POST') {
const { name, email } = await request.json();
const [user] = await db.insert(users).values({ name, email }).returning();
return Response.json(user, { status: 201 });
}
return new Response('Not Found', { status: 404 });
}
};
*/