f61c070428
- AskAIQuery model + migration (logs question/answer/citations per member) - AskAIService: RAG pipeline — retrieve via tenant-isolated SearchService (Meili), ground LLM in top-8 snippets with inline [n] citations, degrade gracefully when OPENROUTER_API_KEY absent or no context (snippet fallback) - Shared api/common/llm-client.ts (mirrors worker's OpenRouter client) - SearchModule now exports SearchService - Member endpoints: GET/POST /my/ask (history + ask) - /my/ask page + AskChat client component: question box, answer cards with sources - Ask AI nav item (top of member nav) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
754 lines
21 KiB
Plaintext
754 lines
21 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
// ============================================================================
|
|
// Organization layer (above Tenancy)
|
|
// ============================================================================
|
|
|
|
enum OrgAdminRole {
|
|
ORG_OWNER
|
|
ORG_ADMIN
|
|
}
|
|
|
|
model Organization {
|
|
id String @id @default(cuid())
|
|
slug String @unique
|
|
name String
|
|
settings Json @default("{}")
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
tenants Tenant[]
|
|
orgAdmins OrgAdmin[]
|
|
orgRules OrgRule[]
|
|
}
|
|
|
|
model OrgAdmin {
|
|
id String @id @default(cuid())
|
|
organizationId String
|
|
organization Organization @relation(fields: [organizationId], references: [id])
|
|
email String
|
|
passwordHash String
|
|
name String?
|
|
role OrgAdminRole @default(ORG_ADMIN)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([organizationId, email])
|
|
@@index([organizationId])
|
|
}
|
|
|
|
model OrgRule {
|
|
id String @id @default(cuid())
|
|
organizationId String
|
|
organization Organization @relation(fields: [organizationId], references: [id])
|
|
matchType RuleMatchType
|
|
matchValue String
|
|
action RuleAction
|
|
priority Int @default(0)
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([organizationId, matchType, matchValue])
|
|
@@index([organizationId, isActive])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tenancy
|
|
// ============================================================================
|
|
|
|
model Tenant {
|
|
id String @id @default(cuid())
|
|
slug String @unique
|
|
name String
|
|
isActive Boolean @default(true)
|
|
isForwardingPaused Boolean @default(false)
|
|
settings Json @default("{}")
|
|
organizationId String?
|
|
organization Organization? @relation(fields: [organizationId], references: [id])
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
admins Admin[]
|
|
tenantBots TenantBot[]
|
|
groups Group[]
|
|
messages Message[]
|
|
approvals Approval[]
|
|
syncRoutes SyncRoute[]
|
|
consentRecords ConsentRecord[]
|
|
memberOptOuts MemberOptOut[]
|
|
towerUsers TowerUser[]
|
|
auditEvents AuditEvent[]
|
|
rules TenantRule[]
|
|
groupAccesses GroupAccess[]
|
|
digestConfig DigestConfig?
|
|
digests Digest[]
|
|
events Event[]
|
|
threads Thread[]
|
|
sevaOpportunities SevaOpportunity[]
|
|
gamificationEvents GamificationEvent[]
|
|
circles Circle[]
|
|
askAIQueries AskAIQuery[]
|
|
}
|
|
|
|
enum AdminRole {
|
|
OWNER
|
|
ADMIN
|
|
VIEWER
|
|
}
|
|
|
|
model Admin {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
email String
|
|
passwordHash String
|
|
role AdminRole @default(ADMIN)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
claimedGroups Group[] @relation("claimer")
|
|
|
|
@@unique([tenantId, email])
|
|
@@index([tenantId])
|
|
}
|
|
|
|
model SuperAdmin {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
passwordHash String
|
|
name String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
enum ActorType {
|
|
ADMIN
|
|
SYSTEM
|
|
ADAPTER
|
|
MEMBER
|
|
}
|
|
|
|
model AuditEvent {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
actorType ActorType
|
|
actorId String?
|
|
action String
|
|
resourceType String
|
|
resourceId String
|
|
payload Json @default("{}")
|
|
traceId String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([tenantId, createdAt])
|
|
@@index([resourceType, resourceId])
|
|
}
|
|
|
|
// ============================================================================
|
|
// WhatsApp accounts (Phase 2B: bots only, tenant-less, accessed via TenantBot)
|
|
// ============================================================================
|
|
|
|
enum AccountStatus {
|
|
ACTIVE
|
|
DISCONNECTED
|
|
BANNED
|
|
PAIRING
|
|
}
|
|
|
|
model Account {
|
|
id String @id @default(cuid())
|
|
// tenantId REMOVED in Phase 2B — bots are global, access scoped via TenantBot
|
|
platform String
|
|
jid String
|
|
sessionPath String
|
|
displayName String?
|
|
status AccountStatus @default(ACTIVE)
|
|
qrCode String?
|
|
isBot Boolean @default(true)
|
|
pairingToken String? @unique
|
|
pairingExpiresAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
tenants TenantBot[]
|
|
groups Group[]
|
|
|
|
@@unique([platform, jid])
|
|
@@index([isBot])
|
|
@@index([status])
|
|
}
|
|
|
|
// Many-to-many: which tenants may claim groups from which bot.
|
|
// Phase 2B ships with implicit "all tenants" (UI auto-grants on first claim),
|
|
// but the table is wired so multi-bot + restricted sharing works in later phases.
|
|
model TenantBot {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
accountId String
|
|
account Account @relation(fields: [accountId], references: [id])
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([tenantId, accountId])
|
|
@@index([accountId])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Groups + claim lifecycle
|
|
// ============================================================================
|
|
|
|
enum GroupClaimStatus {
|
|
PENDING_CLAIM
|
|
CLAIMED
|
|
RELEASED
|
|
EXPIRED
|
|
}
|
|
|
|
model Group {
|
|
id String @id @default(cuid())
|
|
// tenantId nullable: null while PENDING_CLAIM/EXPIRED, set once CLAIMED
|
|
tenantId String?
|
|
tenant Tenant? @relation(fields: [tenantId], references: [id])
|
|
platform String
|
|
platformId String
|
|
name String
|
|
description String?
|
|
isActive Boolean @default(true)
|
|
accountId String?
|
|
account Account? @relation(fields: [accountId], references: [id])
|
|
claimStatus GroupClaimStatus @default(PENDING_CLAIM)
|
|
claimedAt DateTime?
|
|
claimedByAdminId String?
|
|
claimedByAdmin Admin? @relation("claimer", fields: [claimedByAdminId], references: [id])
|
|
claimExpiresAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
messages Message[]
|
|
syncRoutesFrom SyncRoute[] @relation("sourceGroup")
|
|
syncRoutesTo SyncRoute[] @relation("targetGroup")
|
|
consents ConsentRecord[]
|
|
claimTokens GroupClaimToken[]
|
|
groupAccesses GroupAccess[]
|
|
threads Thread[]
|
|
|
|
@@unique([platform, platformId])
|
|
@@index([accountId])
|
|
@@index([tenantId])
|
|
@@index([claimStatus])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Message ingest + approval
|
|
// ============================================================================
|
|
|
|
model Message {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
platform String
|
|
platformMsgId String
|
|
sourceGroupId String
|
|
sourceGroup Group @relation(fields: [sourceGroupId], references: [id])
|
|
senderJid String
|
|
senderName String?
|
|
senderTowerUserId String?
|
|
senderTowerUser TowerUser? @relation("senderTowerUser", fields: [senderTowerUserId], references: [id])
|
|
content String
|
|
mediaUrl String?
|
|
tags String[]
|
|
status MessageStatus @default(PENDING)
|
|
expiresAt DateTime?
|
|
quotedPlatformMsgId String?
|
|
threadId String?
|
|
thread Thread? @relation(fields: [threadId], references: [id])
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
approval Approval?
|
|
|
|
@@unique([platform, platformMsgId])
|
|
@@index([tenantId])
|
|
@@index([senderTowerUserId])
|
|
@@index([status, expiresAt])
|
|
@@index([threadId])
|
|
}
|
|
|
|
enum MessageStatus {
|
|
RAW
|
|
PENDING
|
|
APPROVED
|
|
REJECTED
|
|
DISTRIBUTED
|
|
ARCHIVED
|
|
DNC
|
|
}
|
|
|
|
model Thread {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
sourceGroupId String
|
|
sourceGroup Group @relation(fields: [sourceGroupId], references: [id])
|
|
lastActivityAt DateTime @default(now())
|
|
messageCount Int @default(0)
|
|
topic String?
|
|
createdAt DateTime @default(now())
|
|
|
|
messages Message[]
|
|
|
|
@@index([tenantId])
|
|
@@index([sourceGroupId, lastActivityAt])
|
|
}
|
|
|
|
model Approval {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
messageId String @unique
|
|
message Message @relation(fields: [messageId], references: [id])
|
|
adminId String
|
|
decision ApprovalDecision
|
|
notes String?
|
|
decidedAt DateTime @default(now())
|
|
|
|
@@index([tenantId])
|
|
}
|
|
|
|
enum ApprovalDecision {
|
|
APPROVED
|
|
REJECTED
|
|
}
|
|
|
|
model SyncRoute {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
sourceGroupId String
|
|
sourceGroup Group @relation("sourceGroup", fields: [sourceGroupId], references: [id])
|
|
targetGroupId String
|
|
targetGroup Group @relation("targetGroup", fields: [targetGroupId], references: [id])
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([sourceGroupId, targetGroupId])
|
|
@@index([tenantId])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Group claiming + sharing
|
|
// ============================================================================
|
|
|
|
model GroupClaimToken {
|
|
id String @id @default(cuid())
|
|
groupId String
|
|
group Group @relation(fields: [groupId], references: [id])
|
|
token String @unique
|
|
creatorJid String
|
|
expiresAt DateTime
|
|
consumedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([expiresAt])
|
|
}
|
|
|
|
model GroupAccess {
|
|
id String @id @default(cuid())
|
|
groupId String
|
|
group Group @relation(fields: [groupId], references: [id])
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
grantedBy String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([groupId, tenantId])
|
|
@@index([tenantId])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Member onboarding (Phase 2B)
|
|
// ============================================================================
|
|
|
|
enum ConsentScope {
|
|
INGEST
|
|
ARCHIVE
|
|
REPLICATE
|
|
DISPLAY
|
|
}
|
|
|
|
enum ConsentStatus {
|
|
GRANTED
|
|
REVOKED
|
|
}
|
|
|
|
enum MemberOptOutReason {
|
|
STOP_KEYWORD
|
|
SELF_PORTAL
|
|
ADMIN_ACTION
|
|
}
|
|
|
|
// Hashed identity: SHA-256 of E.164 phone number (pepper via JWT_SECRET).
|
|
model TowerUser {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
phoneHash String
|
|
jid String
|
|
displayName String?
|
|
avatar String?
|
|
hometown String?
|
|
currentLocation String?
|
|
interests String[]
|
|
language String @default("en")
|
|
digestPreference String @default("daily")
|
|
directoryVisible Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
consents ConsentRecord[]
|
|
optOuts MemberOptOut[]
|
|
sessions TowerSession[]
|
|
messages Message[] @relation("senderTowerUser")
|
|
rsvps EventRsvp[]
|
|
sevaEntries SevaEntry[]
|
|
gamificationEvents GamificationEvent[]
|
|
circleMemberships CircleMembership[]
|
|
askAIQueries AskAIQuery[]
|
|
|
|
@@unique([tenantId, phoneHash])
|
|
@@index([phoneHash])
|
|
@@index([tenantId])
|
|
@@index([jid])
|
|
}
|
|
|
|
model TowerSession {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
user TowerUser @relation(fields: [userId], references: [id])
|
|
tokenHash String @unique
|
|
expiresAt DateTime
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([userId])
|
|
@@index([expiresAt])
|
|
}
|
|
|
|
model ConsentRecord {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
groupId String
|
|
group Group @relation(fields: [groupId], references: [id])
|
|
userId String
|
|
user TowerUser @relation(fields: [userId], references: [id])
|
|
scopes ConsentScope[]
|
|
retentionDays Int @default(90)
|
|
policyVersion String
|
|
status ConsentStatus @default(GRANTED)
|
|
proofEventId String
|
|
effectiveAt DateTime @default(now())
|
|
revokedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([tenantId, groupId, userId])
|
|
@@index([status])
|
|
@@index([userId])
|
|
}
|
|
|
|
model MemberOptOut {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
userId String
|
|
user TowerUser @relation(fields: [userId], references: [id])
|
|
groupId String?
|
|
reason MemberOptOutReason
|
|
notes String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([tenantId, userId])
|
|
@@index([groupId])
|
|
@@index([userId])
|
|
}
|
|
|
|
model OtpChallenge {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
jid String
|
|
phoneHash String
|
|
code String
|
|
scopes ConsentScope[]
|
|
retentionDays Int @default(90)
|
|
policyVersion String
|
|
groupId String
|
|
expiresAt DateTime
|
|
consumedAt DateTime?
|
|
sentAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([tenantId, jid])
|
|
@@index([expiresAt])
|
|
@@index([sentAt])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tenant Rules Engine
|
|
// ============================================================================
|
|
|
|
enum RuleMatchType {
|
|
HASHTAG
|
|
PREFIX
|
|
REACTION_EMOJI
|
|
}
|
|
|
|
enum RuleAction {
|
|
FLAG
|
|
AUTO_APPROVE
|
|
SKIP
|
|
REJECT
|
|
}
|
|
|
|
model TenantRule {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
matchType RuleMatchType
|
|
matchValue String
|
|
action RuleAction
|
|
priority Int @default(0)
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([tenantId, matchType, matchValue])
|
|
@@index([tenantId, isActive])
|
|
@@index([tenantId, matchType])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Digest
|
|
// ============================================================================
|
|
|
|
model DigestConfig {
|
|
id String @id @default(cuid())
|
|
tenantId String @unique
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
targetGroupJid String
|
|
targetAccountId String
|
|
scheduleHour Int @default(20)
|
|
scheduleMinute Int @default(0)
|
|
isActive Boolean @default(true)
|
|
lastSentAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model Digest {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
content String
|
|
messageIds String[]
|
|
digestDate DateTime
|
|
sentAt DateTime @default(now())
|
|
|
|
@@unique([tenantId, digestDate])
|
|
@@index([tenantId])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Events + RSVP
|
|
// ============================================================================
|
|
|
|
enum RsvpStatus {
|
|
GOING
|
|
NOT_GOING
|
|
MAYBE
|
|
}
|
|
|
|
model Event {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
title String
|
|
description String?
|
|
location String?
|
|
startsAt DateTime
|
|
endsAt DateTime?
|
|
createdBy String
|
|
isPublished Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
rsvps EventRsvp[]
|
|
|
|
@@index([tenantId, startsAt])
|
|
@@index([tenantId, isPublished])
|
|
}
|
|
|
|
model EventRsvp {
|
|
id String @id @default(cuid())
|
|
eventId String
|
|
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
|
|
userId String
|
|
user TowerUser @relation(fields: [userId], references: [id])
|
|
status RsvpStatus
|
|
note String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([eventId, userId])
|
|
@@index([userId])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Seva (volunteering) + Gamification
|
|
// ============================================================================
|
|
|
|
enum SevaStatus {
|
|
OPEN
|
|
CLOSED
|
|
}
|
|
|
|
enum SevaEntryStatus {
|
|
SIGNED_UP
|
|
COMPLETED
|
|
CANCELLED
|
|
}
|
|
|
|
model SevaOpportunity {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
title String
|
|
description String?
|
|
location String?
|
|
slots Int?
|
|
startsAt DateTime?
|
|
pointsAward Int @default(20)
|
|
status SevaStatus @default(OPEN)
|
|
createdBy String
|
|
isPublished Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
entries SevaEntry[]
|
|
|
|
@@index([tenantId, status])
|
|
@@index([tenantId, isPublished])
|
|
}
|
|
|
|
model SevaEntry {
|
|
id String @id @default(cuid())
|
|
opportunityId String
|
|
opportunity SevaOpportunity @relation(fields: [opportunityId], references: [id], onDelete: Cascade)
|
|
userId String
|
|
user TowerUser @relation(fields: [userId], references: [id])
|
|
status SevaEntryStatus @default(SIGNED_UP)
|
|
hours Float?
|
|
note String?
|
|
completedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([opportunityId, userId])
|
|
@@index([userId])
|
|
}
|
|
|
|
// Multi-signal gamification — points come from many event types, not just seva.
|
|
enum GamificationEventType {
|
|
HELPFUL_ANSWER
|
|
SEVA_COMPLETED
|
|
EVENT_ATTENDANCE
|
|
FAQ_APPROVED
|
|
WELCOME
|
|
PROFILE_COMPLETED
|
|
BUSINESS_ADDED
|
|
}
|
|
|
|
model GamificationEvent {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
userId String
|
|
user TowerUser @relation(fields: [userId], references: [id])
|
|
type GamificationEventType
|
|
points Int
|
|
refType String?
|
|
refId String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([tenantId, userId])
|
|
@@index([userId, createdAt])
|
|
// One award per (user, type, ref) — prevents double-awarding the same source.
|
|
@@unique([userId, type, refId])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Circles (interest / affinity sub-groups within a chapter)
|
|
// ============================================================================
|
|
|
|
enum CircleRole {
|
|
MEMBER
|
|
LEAD
|
|
}
|
|
|
|
model Circle {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
name String
|
|
description String?
|
|
isPublic Boolean @default(true)
|
|
createdBy String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
memberships CircleMembership[]
|
|
|
|
@@unique([tenantId, name])
|
|
@@index([tenantId, isPublic])
|
|
}
|
|
|
|
model CircleMembership {
|
|
id String @id @default(cuid())
|
|
circleId String
|
|
circle Circle @relation(fields: [circleId], references: [id], onDelete: Cascade)
|
|
userId String
|
|
user TowerUser @relation(fields: [userId], references: [id])
|
|
role CircleRole @default(MEMBER)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([circleId, userId])
|
|
@@index([userId])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Ask AI (RAG over the message archive)
|
|
// ============================================================================
|
|
|
|
model AskAIQuery {
|
|
id String @id @default(cuid())
|
|
tenantId String
|
|
tenant Tenant @relation(fields: [tenantId], references: [id])
|
|
userId String
|
|
user TowerUser @relation(fields: [userId], references: [id])
|
|
question String
|
|
answer String
|
|
citations Json @default("[]")
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([tenantId, createdAt])
|
|
@@index([userId, createdAt])
|
|
}
|