UNPKG

wcz-layout

Version:

93 lines (78 loc) 3.85 kB
--- 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 --- # Database schema patterns ## Rules - 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`. ## Examples ```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>; ```