add claude skills for database, development, and features
Build and Push Image / docker (push) Successful in 7m6s
Build and Push Image / docker (push) Successful in 7m6s
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
---
|
||||
name: buzz-database
|
||||
description: Drizzle ORM workflows for the Buzz PostgreSQL database
|
||||
metadata:
|
||||
author: dmgnr
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Buzz — Database Workflows (Drizzle ORM)
|
||||
|
||||
This skill covers working with the PostgreSQL database via Drizzle ORM.
|
||||
|
||||
## Schema Overview
|
||||
|
||||
All tables are defined in `lib/db/schema.ts`. The database uses **three named schemas** + the public schema:
|
||||
|
||||
### Public schema
|
||||
|
||||
| Table | Purpose |
|
||||
|---|---|
|
||||
| `cdn` | File storage (bytea), used for images, cards, slips |
|
||||
| `auditLog` | Admin action audit trail |
|
||||
| `characters` | Genshin characters (linked to versions, CDN images) |
|
||||
| `versions` | Game version tracking (self-referencing FK for `from`) |
|
||||
| `settings` | Global app settings (singleton row, PK is boolean `true`) |
|
||||
| `guides` | Character build guide links |
|
||||
| `user`, `session`, `account`, `verification` | Better-auth tables |
|
||||
|
||||
### `artifact` schema
|
||||
|
||||
| Table | Purpose |
|
||||
|---|---|
|
||||
| `submissions` | Artifact review queue submissions |
|
||||
| `cards` | Generated character cards for submissions |
|
||||
| `settings` | Artifact-specific settings (locked, limit) |
|
||||
|
||||
### `endgame` schema
|
||||
|
||||
| Table | Purpose |
|
||||
|---|---|
|
||||
| `submissions` | Rubgram (endgame) queue submissions |
|
||||
| `sarchive` | Archived completed submissions |
|
||||
| `expired` | Expired unpaid submissions |
|
||||
| `slips` | SlipOK payment slip images and data |
|
||||
| `settings` | Rubgram-specific settings |
|
||||
| `discord` | Discord user mapping |
|
||||
| `types` | Service types with pricing |
|
||||
|
||||
### `tierlist` schema
|
||||
|
||||
| Table | Purpose |
|
||||
|---|---|
|
||||
| `types` | Tierlist categories (Spiral Abyss, Stygian Onslaught) |
|
||||
| `tiers` | Tier definitions (S, A, B, etc.) |
|
||||
| `columns` | Column layout definitions |
|
||||
| `badges` | Badge icons for tiers |
|
||||
| `versions` | Tierlist versions per type |
|
||||
| `states` | Character placements per version (JSONB badges) |
|
||||
|
||||
### Key schema patterns
|
||||
|
||||
**UUIDv7 primary keys:**
|
||||
```ts
|
||||
import { uuidv7 } from "uuidv7";
|
||||
|
||||
id: uuid("id").primaryKey().$defaultFn(uuidv7),
|
||||
```
|
||||
|
||||
**Named schemas with `pgSchema`:**
|
||||
```ts
|
||||
import { pgSchema } from "drizzle-orm/pg-core";
|
||||
|
||||
const artifact = pgSchema("artifact");
|
||||
export const submissions = artifact.table("submissions", { ... });
|
||||
```
|
||||
|
||||
**Custom `bytea` column** (for binary data like images):
|
||||
```ts
|
||||
import { bytea } from "$/db/custom";
|
||||
// used in: cdn, cards, slips tables
|
||||
```
|
||||
|
||||
**Generated columns:**
|
||||
```ts
|
||||
paid: boolean().generatedAlwaysAs(
|
||||
sql`(price <= (0)::numeric) OR (slip IS NOT NULL)`
|
||||
),
|
||||
```
|
||||
|
||||
**Serial sequences** for queue numbering per schema:
|
||||
```ts
|
||||
queue: integer().notNull().default(sql`(nextval('artifact.submissions_queue_seq'::regclass))`),
|
||||
```
|
||||
|
||||
**Enums:**
|
||||
```ts
|
||||
export const characterElement = pgEnum("character_element", [
|
||||
"anemo", "geo", "dendro", "hydro", "pyro", "cryo", "electro",
|
||||
]);
|
||||
```
|
||||
|
||||
**Foreign keys with cascade:**
|
||||
```ts
|
||||
char: uuid("char").notNull().references(() => characters.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
```
|
||||
|
||||
## Migration Workflow
|
||||
|
||||
### Important: database boundary
|
||||
|
||||
Schema changes must be pushed to the database **before** the new code deploys. Otherwise the running app will crash on startup when it queries tables or columns that don't exist yet.
|
||||
|
||||
| Environment | Command | Timing |
|
||||
|---|---|---|
|
||||
| **Dev** | `bun ds dr push` | Run inside app container (via `bun ds`) against dev DB |
|
||||
| **Production** | `bun dr push` | Run locally with production env vars against prod DB. **Must run before `git push`.** |
|
||||
|
||||
### Full migration workflow
|
||||
|
||||
1. **Edit schema** in `lib/db/schema.ts`
|
||||
2. **Generate migration files** (optional, for tracking):
|
||||
```bash
|
||||
bun dr generate
|
||||
```
|
||||
3. **Push to dev** to verify:
|
||||
```bash
|
||||
bun ds dr push
|
||||
```
|
||||
4. **If using migrate instead of push** for production:
|
||||
```bash
|
||||
bun dr migrate
|
||||
```
|
||||
5. **Push to production DB** (with production credentials):
|
||||
```bash
|
||||
bun dr push
|
||||
```
|
||||
6. **Deploy code** via `git push` (Gitea Actions handles the rest)
|
||||
|
||||
When using `bun dr push`, Drizzle automatically detects differences between the schema and the database, and applies the necessary ALTER statements. For production, always verify the SQL Drizzle generates first.
|
||||
|
||||
### Drizzle Studio
|
||||
|
||||
Accessible at `http://localhost:4983` when dev containers are running. The `drizzle` service in `dev/compose.yml` provides this:
|
||||
|
||||
```bash
|
||||
bun ds bun dr studio --host $(hostname)
|
||||
```
|
||||
|
||||
## Common Schema Operations
|
||||
|
||||
### Adding a column
|
||||
|
||||
```ts
|
||||
// In the table definition, add:
|
||||
newColumn: varchar("new_column", { length: 255 }).default(""),
|
||||
```
|
||||
|
||||
Generate + push.
|
||||
|
||||
### Adding a new table
|
||||
|
||||
```ts
|
||||
export const myTable = pgSchema("myschema").table("my_table", {
|
||||
id: uuid("id").primaryKey().$defaultFn(uuidv7),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
Export and use in queries. Generate + push.
|
||||
|
||||
### Adding a new enum
|
||||
|
||||
```ts
|
||||
export const myEnum = pgEnum("my_enum", ["value1", "value2"]);
|
||||
|
||||
export const myTable = pgSchema("myschema").table("my_table", {
|
||||
status: myEnum("status").default("value1"),
|
||||
});
|
||||
```
|
||||
|
||||
### Adding foreign key
|
||||
|
||||
```ts
|
||||
refId: uuid("ref_id")
|
||||
.notNull()
|
||||
.references(() => otherTable.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
```
|
||||
|
||||
## Query patterns
|
||||
|
||||
### Basic select
|
||||
```ts
|
||||
import { db } from "$/db";
|
||||
import { submissions } from "$/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
const items = await db.select().from(submissions).where(eq(submissions.checked, false));
|
||||
```
|
||||
|
||||
### Select with join
|
||||
```ts
|
||||
const result = await db
|
||||
.select({
|
||||
name: characters.name,
|
||||
queue: submissions.queue,
|
||||
})
|
||||
.from(submissions)
|
||||
.innerJoin(characters, eq(submissions.char, characters.id))
|
||||
.orderBy(submissions.queue);
|
||||
```
|
||||
|
||||
### Insert
|
||||
```ts
|
||||
const [inserted] = await db
|
||||
.insert(submissions)
|
||||
.values({ name: "Test", uid: "123456789", char: charId })
|
||||
.returning({ id: submissions.id, queue: submissions.queue });
|
||||
```
|
||||
|
||||
### Update
|
||||
```ts
|
||||
await db
|
||||
.update(submissions)
|
||||
.set({ checked: true })
|
||||
.where(eq(submissions.id, id));
|
||||
```
|
||||
|
||||
### Delete with cascade check (CDN)
|
||||
```ts
|
||||
// Use checkCdnRefs() from $api before deleting CDN files
|
||||
```
|
||||
|
||||
### Raw SQL
|
||||
```ts
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
await db.execute(sql`ALTER SEQUENCE artifact.submissions_queue_seq RESTART WITH 1`);
|
||||
```
|
||||
|
||||
### Transactions
|
||||
```ts
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(...);
|
||||
await tx.update(...);
|
||||
});
|
||||
```
|
||||
|
||||
### Counting
|
||||
```ts
|
||||
const count = await db.$count(submissions);
|
||||
```
|
||||
Reference in New Issue
Block a user