171 lines
5.4 KiB
Plaintext
171 lines
5.4 KiB
Plaintext
// Prisma schema for the organizer app.
|
|
// Learn more: https://pris.ly/d/prisma-schema
|
|
|
|
generator client {
|
|
provider = "prisma-client"
|
|
output = "../lib/generated/prisma"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
enum Role {
|
|
ADMIN
|
|
USER
|
|
PENDING
|
|
}
|
|
|
|
// How the site handles new sign-ups. See SiteSettings.signupMode.
|
|
enum SignupMode {
|
|
// Anyone can sign up and is immediately a USER. Today's behavior.
|
|
OPEN
|
|
// Anyone can sign up, but starts PENDING until an admin approves them
|
|
// (moves them to USER or ADMIN) on the Admin page.
|
|
APPROVED
|
|
// Anyone can sign up, but starts PENDING until they confirm their email.
|
|
// Not implemented yet -- the Admin page exposes this option greyed out.
|
|
CONFIRMED
|
|
// Sign-ups are turned off entirely: no form, no working sign-up action.
|
|
CLOSED
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
passwordHash String
|
|
name String?
|
|
// The very first person to sign up becomes ADMIN regardless of
|
|
// signupMode (the site needs at least one admin to bootstrap). Everyone
|
|
// after that gets USER (signupMode OPEN) or PENDING (APPROVED/CONFIRMED),
|
|
// per SiteSettings.signupMode.
|
|
role Role @default(USER)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
categories Category[]
|
|
projects Project[] @relation("ProjectOwner")
|
|
}
|
|
|
|
/**
|
|
* An isolated board: its own Categories/Groups/To-Dos, invisible from Home
|
|
* and from every other Project. Sole-owner for now -- `ownerId` is both
|
|
* "who created it" and (for now) the only person with access. When
|
|
* Projects become shared spaces, add a `ProjectMember` join table
|
|
* (projectId, userId, role) and extend the access checks in
|
|
* lib/access.ts to consult it too; `ownerId` stays as the implicit
|
|
* full-access member and doesn't need to change.
|
|
*/
|
|
model Project {
|
|
id String @id @default(cuid())
|
|
title String
|
|
ownerId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
owner User @relation("ProjectOwner", fields: [ownerId], references: [id], onDelete: Cascade)
|
|
categories Category[]
|
|
|
|
@@index([ownerId, createdAt])
|
|
}
|
|
|
|
// Single-row table of site-wide settings. Always has exactly one row, at
|
|
// the fixed id below -- read with `findFirst` (falling back to defaults
|
|
// when the row doesn't exist yet) and written with `upsert`.
|
|
model SiteSettings {
|
|
id String @id @default("singleton")
|
|
signupMode SignupMode @default(OPEN)
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
/**
|
|
* Single-row config for an OpenAI-compatible AI provider (e.g. OpenWebUI)
|
|
* the admin has wired up. Same singleton pattern as SiteSettings -- see
|
|
* lib/ai-settings.ts, which is also the *only* place `apiKey` should ever
|
|
* be read in full; every other consumer gets the masked view.
|
|
*/
|
|
model AiSettings {
|
|
id String @id @default("singleton")
|
|
apiUrl String?
|
|
// Stored as-is, not hashed -- it has to be sent to the provider
|
|
// verbatim on every request, unlike a password. Never sent back to the
|
|
// client in full.
|
|
apiKey String?
|
|
model String?
|
|
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model Category {
|
|
id String @id @default(cuid())
|
|
name String
|
|
order Int
|
|
// Exactly one of these is set: userId for a Home category (personal,
|
|
// outside any project), projectId for a category inside a Project.
|
|
// Enforced by a DB check constraint (see migration) as well as by every
|
|
// access path going through lib/access.ts rather than querying userId
|
|
// or projectId directly.
|
|
userId String?
|
|
projectId String?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
groups Group[]
|
|
|
|
@@index([userId, order])
|
|
@@index([projectId, order])
|
|
}
|
|
|
|
model Group {
|
|
id String @id @default(cuid())
|
|
// Short display title, enforced at <=20 chars in app-level validation too.
|
|
title String @db.VarChar(20)
|
|
// Palette key (see lib/colors.ts) rather than a raw hex value, so the
|
|
// curated palette can be retinted centrally without a data migration.
|
|
color String
|
|
// Position within its Category lane.
|
|
order Int
|
|
// Single evolving markdown document for the group. Not a separate model:
|
|
// it's a 1:1, always-exists field with no independent lifecycle.
|
|
noteContent String @default("") @db.Text
|
|
// Set once a group is archived; archived groups are kept (not deleted)
|
|
// but excluded from the Home board query. Null = active.
|
|
archivedAt DateTime?
|
|
|
|
categoryId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
|
|
todos Todo[]
|
|
|
|
@@index([categoryId, order])
|
|
}
|
|
|
|
model Todo {
|
|
id String @id @default(cuid())
|
|
title String @db.VarChar(20)
|
|
details String? @db.Text
|
|
completed Boolean @default(false)
|
|
// Set when `completed` flips to true, cleared back to null when it
|
|
// flips to false. Distinct from `updatedAt`, which bumps on *any* field
|
|
// change (a title edit, a reorder, ...), not just completion.
|
|
completedAt DateTime?
|
|
// Position within its Group's to-do list.
|
|
order Int
|
|
|
|
groupId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([groupId, order])
|
|
}
|