feat(hub-guide): expand plugin with 26 best-practice skills, hooks, and references
Transform hub-guide from a single-skill Hub monorepo guide into a comprehensive programming best-practice plugin covering all situations. Skills (26): - Core: engineering-principles, clean-code, clean-architecture, design-patterns, testing, error-handling, security, api-design, git-workflow, documentation, logging-observability, performance - Languages: typescript, python, rust, go - Frameworks: react-frontend, elysiajs, hono-backend, drizzle-database, nextjs - Infrastructure: docker, ci-cd, monitoring - Monorepo: monorepo, hub-guide (existing) Hooks: - SessionStart: auto-detect project type and activate relevant skills - PreToolUse (Write|Edit): inject language-specific rules per file type Reference files for deep dives: - clean-architecture/references/solid.md (SOLID + component principles) - design-patterns/references/catalog.md (full GoF catalog with examples) - testing/references/mocks.md (test double taxonomy) Restructure plugin to modern skills/ directory format.
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
---
|
||||
name: drizzle-database
|
||||
description: Drizzle ORM best practices — schema design, queries, migrations, relations, and performance. Use when designing database schemas, writing Drizzle queries, managing migrations, or whenever the user mentions "Drizzle," "Drizzle ORM," "drizzle-orm," "drizzle-kit," "schema," "migration," "PostgreSQL," "SQLite," or "database design."
|
||||
---
|
||||
|
||||
# Drizzle ORM Best Practices
|
||||
|
||||
## Schema Design
|
||||
|
||||
```typescript
|
||||
import { pgTable, serial, text, timestamp, boolean, integer, jsonb } from 'drizzle-orm/pg-core';
|
||||
import { relations } from 'drizzle-orm';
|
||||
|
||||
// Tables with explicit foreign keys
|
||||
export const users = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
email: text('email').notNull().unique(),
|
||||
name: text('name'),
|
||||
role: text('role', { enum: ['admin', 'user'] }).default('user').notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const orders = pgTable('orders', {
|
||||
id: serial('id').primaryKey(),
|
||||
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
status: text('status', { enum: ['pending', 'paid', 'shipped'] }).default('pending').notNull(),
|
||||
total: integer('total').notNull(), // in cents
|
||||
metadata: jsonb('metadata'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Relations
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
orders: many(orders),
|
||||
}));
|
||||
|
||||
export const ordersRelations = relations(orders, ({ one }) => ({
|
||||
user: one(users, { fields: [orders.userId], references: [users.id] }),
|
||||
}));
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- `serial` for auto-increment PKs, `uuid` for distributed/public IDs.
|
||||
- `timestamp` with `defaultNow()` for created/updated.
|
||||
- JSONB for flexible metadata (Postgres). Text JSON for SQLite.
|
||||
- Enums as `text` with `enum` constraint (not Postgres `CREATE TYPE` — easier migrations).
|
||||
|
||||
## Queries
|
||||
|
||||
### Basic CRUD
|
||||
```typescript
|
||||
import { eq, and, or, like, gte, lte, asc, desc, sql, inArray } from 'drizzle-orm';
|
||||
|
||||
// Create
|
||||
const [user] = await db.insert(users).values({ email: 'a@b.com' }).returning();
|
||||
|
||||
// Read
|
||||
const allUsers = await db.select().from(users);
|
||||
const user = await db.select().from(users).where(eq(users.id, id)).limit(1);
|
||||
const admins = await db.select().from(users).where(eq(users.role, 'admin')).orderBy(desc(users.createdAt));
|
||||
|
||||
// Update
|
||||
const [updated] = await db.update(users).set({ name }).where(eq(users.id, id)).returning();
|
||||
|
||||
// Delete
|
||||
await db.delete(users).where(eq(users.id, id));
|
||||
```
|
||||
|
||||
### Joins
|
||||
```typescript
|
||||
// One-to-many
|
||||
const result = await db.select()
|
||||
.from(users)
|
||||
.leftJoin(orders, eq(users.id, orders.userId))
|
||||
.where(eq(users.id, id));
|
||||
|
||||
// With relations (prepared — uses multiple queries or JOINs internally)
|
||||
const userWithOrders = await db.query.users.findFirst({
|
||||
where: eq(users.id, id),
|
||||
with: { orders: { limit: 5 } },
|
||||
});
|
||||
```
|
||||
|
||||
### Aggregations
|
||||
```typescript
|
||||
import { count, sum, avg, min, max, sql } from 'drizzle-orm';
|
||||
|
||||
const stats = await db.select({
|
||||
total: count(),
|
||||
totalRevenue: sum(orders.total),
|
||||
avgOrderValue: avg(orders.total),
|
||||
byStatus: sql`${orders.status}::text`,
|
||||
}).from(orders)
|
||||
.groupBy(orders.status);
|
||||
```
|
||||
|
||||
## Migrations (drizzle-kit)
|
||||
|
||||
```jsonc
|
||||
// drizzle.config.ts
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
dialect: 'postgresql',
|
||||
schema: './src/db/schema/*.ts',
|
||||
out: './src/db/migrations',
|
||||
dbCredentials: { url: process.env.DATABASE_URL! },
|
||||
});
|
||||
```
|
||||
|
||||
```bash
|
||||
# Commands
|
||||
bunx drizzle-kit generate # Generate migration from schema changes
|
||||
bunx drizzle-kit migrate # Apply migrations to database
|
||||
bunx drizzle-kit push # Push schema (dev only — no migration files)
|
||||
bunx drizzle-kit studio # Drizzle Studio (GUI for DB inspection)
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Generate migrations, then apply. Use `drizzle-kit push` only in dev.
|
||||
- Code review migration files before applying to production.
|
||||
- Write custom SQL for complex migrations (backfills, data transformations).
|
||||
- Never edit generated migration files manually (unless you know what you're doing).
|
||||
|
||||
## Performance
|
||||
|
||||
### Indexes
|
||||
```typescript
|
||||
import { index, uniqueIndex } from 'drizzle-orm/pg-core';
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
email: text('email').notNull().unique(),
|
||||
// ...
|
||||
}, (table) => ({
|
||||
emailIdx: uniqueIndex('users_email_idx').on(table.email),
|
||||
roleIdx: index('users_role_idx').on(table.role),
|
||||
// Composite index for common queries
|
||||
createdRoleIdx: index('users_created_role_idx').on(table.createdAt, table.role),
|
||||
}));
|
||||
```
|
||||
|
||||
### N+1 Prevention
|
||||
```typescript
|
||||
// ❌ N+1 — one query per order item
|
||||
for (const order of orders) {
|
||||
const items = await db.select().from(orderItems).where(eq(orderItems.orderId, order.id));
|
||||
}
|
||||
|
||||
// ✅ Eager with `IN`
|
||||
const orderIds = orders.map(o => o.id);
|
||||
const allItems = await db.select().from(orderItems).where(inArray(orderItems.orderId, orderIds));
|
||||
```
|
||||
|
||||
### Prepared Statements
|
||||
```typescript
|
||||
const findUserByEmail = db.select().from(users).where(eq(users.email, sql.placeholder('email'))).prepare();
|
||||
|
||||
// Reuse
|
||||
const user1 = await findUserByEmail.execute({ email: 'a@b.com' });
|
||||
const user2 = await findUserByEmail.execute({ email: 'c@d.com' });
|
||||
```
|
||||
|
||||
## Repository Pattern
|
||||
|
||||
```typescript
|
||||
// Always accessed through repository — never direct db calls from routes
|
||||
export class UserRepository {
|
||||
constructor(private db: DB) {}
|
||||
|
||||
async findByEmail(email: string): Promise<User | null> {
|
||||
const result = await this.db.select().from(users).where(eq(users.email, email)).limit(1);
|
||||
return result[0] ?? null;
|
||||
}
|
||||
|
||||
async create(input: CreateUserInput): Promise<User> {
|
||||
const [user] = await this.db.insert(users).values(input).returning();
|
||||
return user;
|
||||
}
|
||||
|
||||
async update(id: number, data: Partial<User>): Promise<User | null> {
|
||||
const [user] = await this.db.update(users).set({ ...data, updatedAt: new Date() }).where(eq(users.id, id)).returning();
|
||||
return user ?? null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- ❌ Raw SQL strings instead of Drizzle query builder when Drizzle provides it
|
||||
- ❌ `select *` in production — name specific columns
|
||||
- ❌ No repository layer — Drizzle queries in route handlers
|
||||
- ❌ Missing indexes on foreign keys and filtered columns
|
||||
- ❌ `await` in loops for sequential queries — use `Promise.all` or `IN` queries
|
||||
- ❌ Editing generated migration files
|
||||
- ❌ Using `serial` for user-facing IDs — use `uuid` for public IDs
|
||||
Reference in New Issue
Block a user