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:
asepharyana
2026-07-25 11:35:16 +07:00
parent ecdce01387
commit e513cddd68
36 changed files with 4950 additions and 16 deletions
+197
View File
@@ -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