75 lines
3.4 KiB
Text
75 lines
3.4 KiB
Text
|
|
---
|
||
|
|
description: Prisma schema and database access conventions
|
||
|
|
globs: prisma/**,src/app/api/**/*.ts,src/lib/**/*.ts,scripts/**/*.ts
|
||
|
|
---
|
||
|
|
|
||
|
|
# Prisma Conventions
|
||
|
|
|
||
|
|
## Client import
|
||
|
|
|
||
|
|
Always import the singleton:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
import { prisma } from "@/lib/db";
|
||
|
|
```
|
||
|
|
|
||
|
|
`src/lib/db.ts` exposes a lazy `Proxy` over `PrismaClient` — the underlying client (and Postgres pool) is only constructed on first property access. Never `new PrismaClient()` outside of `src/lib/db.ts`. Don't call `prisma` from top-level module code; the lazy proxy depends on `DATABASE_URL` being set at access time, not import time.
|
||
|
|
|
||
|
|
## Generated client path
|
||
|
|
|
||
|
|
The Prisma client is generated to `src/generated/prisma/` (custom `output` in `prisma/schema.prisma`), not `@prisma/client`. To import generated types or enums:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
import { Prisma, PrismaClient } from "@/generated/prisma/client";
|
||
|
|
```
|
||
|
|
|
||
|
|
`src/generated/` is a no-go zone — regenerate via `npx prisma generate` instead of editing.
|
||
|
|
|
||
|
|
## Schema patterns (what this repo actually does)
|
||
|
|
|
||
|
|
- **IDs:** `String @id @default(cuid())`.
|
||
|
|
- **Timestamps:** `createdAt DateTime @default(now())` + `updatedAt DateTime @updatedAt`.
|
||
|
|
- **Multi-tenancy:** every business model has `organizationId String` + `@@index([organizationId])`. Auth tables (`User`, `Account`, `Session`) and the `Organization` itself are the exceptions.
|
||
|
|
- **JSON columns:** `Json?` is used for `ResponseCard.fieldData`, `ResponseCard.rawOcrResponse`, `Integration.config`, etc. Treat the shape as untrusted on read.
|
||
|
|
- **Indexes:** `@@index` on FKs and the columns we sort/filter by. Add an index when you add a `where:` filter.
|
||
|
|
- **No `@@map`:** Prisma defaults to PascalCase tables here; do not add `@@map(...)` to existing models.
|
||
|
|
|
||
|
|
## Multi-tenant scoping (mandatory)
|
||
|
|
|
||
|
|
Every read or write against a business model MUST scope by `organizationId`:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// Read
|
||
|
|
const items = await prisma.responseCard.findMany({
|
||
|
|
where: { organizationId: session.user.orgId, /* ... */ },
|
||
|
|
});
|
||
|
|
|
||
|
|
// Create
|
||
|
|
await prisma.responseCard.create({
|
||
|
|
data: { organizationId: session.user.orgId, /* ... */ },
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
Cross-org leakage is a security bug. Returning a `404` (not `403`) on accidental cross-org IDs is the convention so we don't reveal existence.
|
||
|
|
|
||
|
|
## After schema changes
|
||
|
|
|
||
|
|
```bash
|
||
|
|
npx prisma generate # regenerate client at src/generated/prisma/
|
||
|
|
npx prisma migrate dev --name descriptive_name # create + apply a migration locally
|
||
|
|
npm run schema:map # refresh docs/SCHEMA_MAP.md (see prisma-schema-map.mdc)
|
||
|
|
```
|
||
|
|
|
||
|
|
For prototype-stage edits (no DB structure change you intend to keep), `npm run db:push` is also available.
|
||
|
|
|
||
|
|
## Query best practices
|
||
|
|
|
||
|
|
- `findUnique` for ID / unique lookups; `findFirst` when filtering by org.
|
||
|
|
- Use `select` when you only need a few fields and the model has heavy relations.
|
||
|
|
- Wrap multi-step writes in `prisma.$transaction([...])` or `prisma.$transaction(async (tx) => ...)`.
|
||
|
|
- Background jobs go through `ProcessingJob` — see `src/lib/ocr.ts` for the canonical pattern.
|
||
|
|
|
||
|
|
## Denormalized fields
|
||
|
|
|
||
|
|
`ResponseCard.name` is a denormalized display string derived from `firstName + lastName`. Three places set it: OCR pipeline (`src/lib/ocr.ts`), the public survey submit handler (`src/app/api/survey/submit/route.ts`), and the PUT route (`src/app/api/cards/[id]/route.ts`, which recomputes on any first/last change). When you write a new path that mutates `firstName` or `lastName`, recompute `name` too — or call into the PUT path so it does it for you.
|