Files
2026-05-22 15:50:33 -06:00

167 lines
5.6 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("CLOUDRON_POSTGRESQL_URL")
}
model User {
id String @id @default(cuid())
email String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
draft RuleDraft?
rules PublishedRule[]
/// At most one pending verified email change (CR-103).
emailChangeToken EmailChangeToken?
/// Rules this user was invited to as a stakeholder (after accepting invite).
ruleStakeholders RuleStakeholder[] @relation("RuleStakeholderUser")
/// Stakeholder rows where this user sent the invite.
stakeholderInvitesSent RuleStakeholder[] @relation("RuleStakeholderInvitedBy")
}
/// Pending email change: user must open verify link sent to `newEmail` (CR-103).
/// Separate from `MagicLinkToken` so sign-in and email-change flows cannot be confused.
model EmailChangeToken {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
newEmail String
tokenHash String @unique
expiresAt DateTime
createdAt DateTime @default(now())
@@index([newEmail])
}
model Session {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
tokenHash String @unique
expiresAt DateTime
createdAt DateTime @default(now())
@@index([userId])
}
model MagicLinkToken {
id String @id @default(cuid())
email String
tokenHash String @unique
expiresAt DateTime
nextPath String?
/// Optional create-flow draft captured at magic-link request (save-progress).
draftPayload Json?
createdAt DateTime @default(now())
@@index([email])
}
model RuleDraft {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
payload Json
updatedAt DateTime @updatedAt
}
model PublishedRule {
id String @id @default(cuid())
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
title String
summary String?
document Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
stakeholders RuleStakeholder[]
@@index([userId])
}
/// Invite + access for a published rule: email invite at publish; userId set after magic-link-style accept.
model RuleStakeholder {
id String @id @default(cuid())
ruleId String
rule PublishedRule @relation(fields: [ruleId], references: [id], onDelete: Cascade)
/// Normalized lowercase email (invite target).
email String
/// Publisher at invite time; null if that account was removed.
invitedByUserId String?
invitedBy User? @relation("RuleStakeholderInvitedBy", fields: [invitedByUserId], references: [id], onDelete: SetNull)
/// Set when the invitee completes the verify link (same account as `email`).
userId String?
user User? @relation("RuleStakeholderUser", fields: [userId], references: [id], onDelete: SetNull)
/// One-time invite token (hashed); null after accept or revoke path (consume on verify).
inviteTokenHash String? @unique
inviteExpiresAt DateTime?
invitedAt DateTime @default(now())
acceptedAt DateTime?
@@unique([ruleId, email])
@@index([userId])
@@index([email])
}
model RuleTemplate {
id String @id @default(cuid())
slug String @unique
title String
category String?
description String?
body Json
sortOrder Int @default(0)
featured Boolean @default(false)
}
/// Recommendation matrix (CR-88).
/// JSON in `data/create/customRule/<section>.json` is canonical; this table is
/// rebuilt from those files at `prisma db seed` time so the API can join.
/// See `docs/guides/template-recommendation-matrix.md` §7.
model MethodFacet {
id String @id @default(cuid())
/// One of "communication" | "membership" | "decisionApproaches" | "conflictManagement".
section String
/// Matches the `id` of an entry in `messages/en/create/customRule/<section>.json#/methods`.
slug String
/// One of "size" | "orgType" | "scale" | "maturity".
group String
/// Canonical facet value id, e.g. "workersCoop", "earlyStage".
value String
/// `true` iff the JSON marks this method as matching the facet (`✓` cell).
matches Boolean
/// Optional per-cell weight; reserved for a future weighted-rank pass (v1 ignores).
weight Float?
@@unique([section, slug, group, value])
@@index([section])
@@index([group, value, matches])
}
/// Template-level recommendation matrix (Template Composition, cols GY). Canonical
/// JSON in `data/templates/templateFacet.json`; rebuilt at seed like
/// `MethodFacet`. One row per `(templateSlug, group, value)` where the matrix
/// marks a fit (✓). `GET /api/templates?facet.*` joins these rows to user facets.
/// See `docs/guides/template-recommendation-matrix.md` (parallel to `MethodFacet` §7).
model TemplateFacet {
id String @id @default(cuid())
/// `RuleTemplate.slug` (e.g. `consensus`, `do-ocracy`).
templateSlug String
/// `size` | `orgType` | `scale` | `maturity` — same as `MethodFacet.group`.
group String
/// Canonical facet value id, e.g. `workersCoop`, `local`.
value String
/// `true` iff the JSON marks a fit; seed only writes `true` rows.
matches Boolean
@@unique([templateSlug, group, value])
@@index([templateSlug])
@@index([group, value, matches])
}