Files
iios/packages/iios-service/prisma/schema.prisma
T
maaz519 7b174e24fa feat(service): P3.1 inbox tables + extract ActorResolver
IiosInboxItem + IiosInboxItemStateHistory (migration inbox-init). Extracted
resolveScope/resolveActor/ensureParticipant into a shared ActorResolver
(IdentityModule) so message + inbox layers share identity resolution; MessageService
delegates (P2 tests unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 02:41:50 +05:30

372 lines
11 KiB
Plaintext

// IIOS Interaction Kernel — P1 schema (native portal only).
// Lifted from the Bottom-Up Evolution Atlas §10 kernel DDL. ONLY kernel tables:
// no support / inbox / route / AI tables (those are earned in later phases).
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ─── Enums (one value per line) ───────────────────────────────────
enum IiosActorKind {
HUMAN
AI
SERVICE
BOT
UNKNOWN
}
enum IiosHandleKind {
PORTAL_USER
EMAIL
PHONE
WHATSAPP
SLACK
MATTERMOST
TELEGRAM
EXTERNAL_VISITOR
}
enum IiosInteractionKind {
MESSAGE
EMAIL
SYSTEM_NOTICE
INBOX_WORK
SUPPORT_CASE
MEETING_REQUEST
DIGEST
SUMMARY
NOTIFICATION
}
enum IiosMessagePartKind {
TEXT
HTML
MARKDOWN
MEDIA_REF
FILE_REF
VOICE_REF
LOCATION
STRUCTURED_JSON
}
enum IiosDeliveryState {
RECEIVED
NORMALIZED
POLICY_HELD
STORED
DELIVERED
READ
FAILED
DELETED_LOCAL
REDACTED
}
enum IiosReceiptKind {
DELIVERED
READ
ACKED
FAILED
SUPPRESSED
RATE_LIMITED
}
enum IiosInboxItemKind {
NEEDS_REPLY
NEEDS_REVIEW
NEEDS_APPROVAL
SUPPORT_UPDATE
MEETING_FOLLOWUP
DIGEST
SYSTEM_ALERT
CRM_OWNER_INTEREST
}
enum IiosInboxState {
OPEN
SNOOZED
DONE
ARCHIVED
CANCELLED
STALE
}
// ─── Kernel tables ────────────────────────────────────────────────
/// Frozen six-vector scope. orgId + appId are REQUIRED (no global-by-null).
model IiosScope {
id String @id @default(cuid())
orgId String
appId String
buId String?
tenantId String?
workspaceId String?
projectId String?
userScopeId String?
createdAt DateTime @default(now())
sourceHandles IiosSourceHandle[]
channels IiosChannel[]
threads IiosThread[]
interactions IiosInteraction[]
inboxItems IiosInboxItem[]
@@index([orgId, appId, tenantId])
}
/// A channel-specific handle (phone/email/portal user). NOT identity — stays
/// UNVERIFIED until MDM resolves it; no silent merge.
model IiosSourceHandle {
id String @id @default(cuid())
scopeId String
scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
kind IiosHandleKind
externalId String
displayName String?
canonicalEntityId String? // set only when MDM resolves; null = unresolved
confidence Float @default(0)
verificationState String @default("UNVERIFIED")
firstSeenAt DateTime @default(now())
lastSeenAt DateTime @default(now())
metadata Json?
actors IiosActorRef[]
@@unique([scopeId, kind, externalId])
@@index([canonicalEntityId])
}
/// The local actor (human / AI / service / unknown) behind an interaction.
model IiosActorRef {
id String @id @default(cuid())
kind IiosActorKind
sourceHandleId String?
sourceHandle IiosSourceHandle? @relation(fields: [sourceHandleId], references: [id])
platformPrincipalRef String?
canonicalEntityId String?
displayName String?
createdAt DateTime @default(now())
threadsCreated IiosThread[] @relation("ThreadCreatedBy")
participations IiosThreadParticipant[]
interactions IiosInteraction[]
receipts IiosMessageReceipt[]
unreadCounters IiosUnreadCounter[]
inboxItemsOwned IiosInboxItem[]
}
/// A configured channel surface (PORTAL in P1).
model IiosChannel {
id String @id @default(cuid())
scopeId String
scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
channelType String
channelName String
direction String @default("BOTH")
capabilityContract Json?
externalRef String?
enabled Boolean @default(true)
createdAt DateTime @default(now())
threads IiosThread[]
interactions IiosInteraction[]
@@index([scopeId, channelType])
}
/// A conversation/thread on a channel.
model IiosThread {
id String @id @default(cuid())
scopeId String
scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
channelId String?
channel IiosChannel? @relation(fields: [channelId], references: [id])
threadKind String @default("CONVERSATION")
subject String?
externalThreadRef String?
status String @default("OPEN")
visibilityClass String @default("PRIVATE")
createdByActorId String?
createdByActor IiosActorRef? @relation("ThreadCreatedBy", fields: [createdByActorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
metadata Json?
participants IiosThreadParticipant[]
interactions IiosInteraction[]
unreadCounters IiosUnreadCounter[]
inboxItems IiosInboxItem[]
@@index([scopeId, status])
}
model IiosThreadParticipant {
threadId String
thread IiosThread @relation(fields: [threadId], references: [id], onDelete: Cascade)
actorId String
actor IiosActorRef @relation(fields: [actorId], references: [id])
participantRole String @default("MEMBER")
joinedAt DateTime @default(now())
leftAt DateTime?
@@id([threadId, actorId])
}
/// The semantic envelope. Idempotent per (scope, idempotencyKey).
model IiosInteraction {
id String @id @default(cuid())
scopeId String
scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
kind IiosInteractionKind
threadId String?
thread IiosThread? @relation(fields: [threadId], references: [id])
channelId String?
channel IiosChannel? @relation(fields: [channelId], references: [id])
actorId String?
actor IiosActorRef? @relation(fields: [actorId], references: [id])
parentInteractionId String?
parent IiosInteraction? @relation("InteractionParent", fields: [parentInteractionId], references: [id])
children IiosInteraction[] @relation("InteractionParent")
externalId String?
idempotencyKey String
occurredAt DateTime @default(now())
receivedAt DateTime @default(now())
status IiosDeliveryState @default(RECEIVED)
policyDecisionRef String?
consentReceiptRef String?
crreBundleRef String?
traceId String?
metadata Json?
parts IiosMessagePart[]
receipts IiosMessageReceipt[]
inboxItems IiosInboxItem[]
@@unique([scopeId, idempotencyKey])
@@index([threadId, occurredAt])
}
model IiosMessagePart {
id String @id @default(cuid())
interactionId String
interaction IiosInteraction @relation(fields: [interactionId], references: [id], onDelete: Cascade)
partIndex Int
kind IiosMessagePartKind
bodyText String?
contentRef String?
mimeType String?
sizeBytes BigInt?
checksumSha256 String?
metadata Json?
@@unique([interactionId, partIndex])
}
/// Transactional outbox — written in the same tx as the business row.
model IiosOutboxEvent {
eventId String @id @default(cuid())
aggregateType String
aggregateId String
eventType String
cloudEvent Json
partitionKey String
status String @default("PENDING")
attempts Int @default(0)
nextAttemptAt DateTime @default(now())
createdAt DateTime @default(now())
publishedAt DateTime?
@@index([status, nextAttemptAt, createdAt])
}
/// Idempotent-consumer ledger for the outbox relay / downstream consumers.
model IiosProcessedEvent {
consumerName String
eventId String
processedAt DateTime @default(now())
resultHash String?
@@id([consumerName, eventId])
}
// ─── P2: messaging — receipts & unread ────────────────────────────
/// Delivery/read receipt for a message interaction, per actor.
model IiosMessageReceipt {
id String @id @default(cuid())
interactionId String
interaction IiosInteraction @relation(fields: [interactionId], references: [id], onDelete: Cascade)
actorId String?
actor IiosActorRef? @relation(fields: [actorId], references: [id])
receiptKind IiosReceiptKind
occurredAt DateTime @default(now())
providerReceiptRef String?
metadata Json?
@@unique([interactionId, actorId, receiptKind])
@@index([interactionId])
}
/// Per-(thread, actor) unread count + last-read marker.
model IiosUnreadCounter {
threadId String
thread IiosThread @relation(fields: [threadId], references: [id], onDelete: Cascade)
actorId String
actor IiosActorRef @relation(fields: [actorId], references: [id])
unreadCount Int @default(0)
lastReadInteractionId String?
updatedAt DateTime @updatedAt
@@id([threadId, actorId])
}
// ─── P3: Inbox (work/awareness surface) ───────────────────────────
/// A durable action/awareness item owned by one actor. Fed by the event
/// projector (deterministic, collapse-per-thread). NOT a thin unread projection.
model IiosInboxItem {
id String @id @default(cuid())
scopeId String
scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
ownerActorId String
ownerActor IiosActorRef @relation(fields: [ownerActorId], references: [id])
kind IiosInboxItemKind
state IiosInboxState @default(OPEN)
title String
summary String?
priority String @default("NORMAL")
dueAt DateTime?
sourceInteractionId String?
sourceInteraction IiosInteraction? @relation(fields: [sourceInteractionId], references: [id])
threadId String?
thread IiosThread? @relation(fields: [threadId], references: [id])
traceId String?
actionRef Json?
policyDecisionRef String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
metadata Json?
stateHistory IiosInboxItemStateHistory[]
@@index([ownerActorId, state, priority])
}
/// Audit trail of inbox-item state transitions.
model IiosInboxItemStateHistory {
id String @id @default(cuid())
inboxItemId String
inboxItem IiosInboxItem @relation(fields: [inboxItemId], references: [id], onDelete: Cascade)
fromState IiosInboxState?
toState IiosInboxState
actorId String?
reasonCode String?
at DateTime @default(now())
@@index([inboxItemId])
}