feat(service): NestJS kernel skeleton + 10-table kernel schema (P1.1+1.2)
Reuses support-service bootstrap/health/prisma patterns. Kernel-only Prisma
schema (scope/source_handle/actor/channel/thread/participant/interaction/
message_part/outbox/processed_event) with orgId/appId NOT NULL and
@@unique([scopeId, idempotencyKey]). Health verified: {status:ok,db:true}.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
// ─── 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[]
|
||||
|
||||
@@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[]
|
||||
}
|
||||
|
||||
/// 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[]
|
||||
|
||||
@@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[]
|
||||
|
||||
@@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])
|
||||
}
|
||||
Reference in New Issue
Block a user