wcz-layout
Version:
93 lines (78 loc) • 3.85 kB
Markdown
---
name: db-schema
description: "Use when changing PostgreSQL tables, columns, constraints, indexes, enums, foreign keys, relations, migrations, or Drizzle-derived Zod schemas in a wcz-layout app."
metadata:
type: convention
library: wcz-layout
---
- One table per file, named after the table (e.g. `bookTable` lives in `book.ts`). Never put multiple tables in one file, even when they are related — a parent and its children each get their own file.
- Use `uuid` for application-table primary keys and `snakeCase` for generating tables.
- For `uuid` columns, do not generate random values in the schema. The client generates the id with `uuidv7()` from `wcz-layout/utils` when creating the record.
- Always add `withTimezone: true` to timestamp columns.
- Always declare `onDelete` explicitly on every foreign key — never rely on the default. Pick the behavior from the business rule: `"cascade"` when the child is meaningless without its parent, `"restrict"` when the parent must not be deleted while children exist, `"set null"` for optional references.
- Index every foreign key column. Postgres does not create one for you.
- Define relations in a central `relations.ts` file.
- Declare enums with `pgEnum` in the table's own file, export them, and reference `enumValues` everywhere else — never re-declare the values.
- Generate the full client record schema with `createSelectSchema` from `drizzle-orm/zod`. Use `createInsertSchema`; override fields with stricter rules (trim, min/max) where needed.
- The second argument of `createSelectSchema` refines **existing columns only**. Adding a key
that is not a column is a type error. Attach related data with `.extend()` after it.
- Do not auto-generate migrations unless the user explicitly asks. When asked, run `npm run db:generate`; migrations apply automatically at app startup while `DATABASE_AUTO_MIGRATE=true`.
```ts
// src/server/db/schemas/library.ts
import { snakeCase, text, uuid } from "drizzle-orm/pg-core";
export const libraryTable = snakeCase.table("libraries", {
id: uuid().primaryKey(),
name: text().notNull(),
});
// src/server/db/schemas/book.ts
import { index, pgEnum, snakeCase, text, timestamp, uuid } from "drizzle-orm/pg-core";
import { libraryTable } from "./library";
export const bookStatusEnum = pgEnum("book_status", ["draft", "published"]);
export const bookTable = snakeCase.table(
"books",
{
id: uuid().primaryKey(),
title: text().notNull(),
status: bookStatusEnum().notNull().default("draft"),
libraryId: uuid()
.notNull()
.references(() => libraryTable.id, { onDelete: "cascade" }),
createdAt: timestamp({ withTimezone: true }).defaultNow().notNull(),
},
(table) => [index().on(table.libraryId)],
);
// src/server/db/schemas/relations.ts
import { defineRelations } from "drizzle-orm";
import { bookTable } from "./book";
import { libraryTable } from "./library";
export const relations = defineRelations(
{ libraryTable, bookTable },
({ one, many, libraryTable: library, bookTable: book }) => ({
libraryTable: {
books: many.bookTable(),
},
bookTable: {
library: one.libraryTable({ from: book.libraryId, to: library.id }),
},
}),
);
// src/lib/schemas/<table>.ts
import { createSelectSchema } from "drizzle-orm/zod";
import { t } from "wcz-layout/utils";
import type { z } from "zod";
import { bookTable } from "~/server/db/schemas/book";
import { PageSchema } from "./page";
export const BookSchema = createSelectSchema(bookTable, {
title: (schema) =>
schema
.trim()
.min(1, t("Validation.Required"))
.max(255, t("Validation.MaxLength", { length: 255 })),
}).extend({
pages: PageSchema.array().min(1, t("Validation.Required")),
});
export type Book = z.infer<typeof BookSchema>;
```