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,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@insignia/iios-service",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"start": "nest start",
|
||||
"dev": "nest start --watch",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"prisma:studio": "prisma studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@insignia/iios-contracts": "workspace:*",
|
||||
"@nestjs/common": "^11.1.27",
|
||||
"@nestjs/core": "^11.1.27",
|
||||
"@nestjs/platform-express": "^11.1.27",
|
||||
"@prisma/client": "^6.2.1",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.15.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@insignia/iios-testkit": "workspace:*",
|
||||
"@nestjs/cli": "^11.0.23",
|
||||
"@nestjs/schematics": "^11.1.0",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^26.0.1",
|
||||
"prisma": "^6.2.1",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IiosActorKind" AS ENUM ('HUMAN', 'AI', 'SERVICE', 'BOT', 'UNKNOWN');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IiosHandleKind" AS ENUM ('PORTAL_USER', 'EMAIL', 'PHONE', 'WHATSAPP', 'SLACK', 'MATTERMOST', 'TELEGRAM', 'EXTERNAL_VISITOR');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IiosInteractionKind" AS ENUM ('MESSAGE', 'EMAIL', 'SYSTEM_NOTICE', 'INBOX_WORK', 'SUPPORT_CASE', 'MEETING_REQUEST', 'DIGEST', 'SUMMARY', 'NOTIFICATION');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IiosMessagePartKind" AS ENUM ('TEXT', 'HTML', 'MARKDOWN', 'MEDIA_REF', 'FILE_REF', 'VOICE_REF', 'LOCATION', 'STRUCTURED_JSON');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "IiosDeliveryState" AS ENUM ('RECEIVED', 'NORMALIZED', 'POLICY_HELD', 'STORED', 'DELIVERED', 'READ', 'FAILED', 'DELETED_LOCAL', 'REDACTED');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IiosScope" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orgId" TEXT NOT NULL,
|
||||
"appId" TEXT NOT NULL,
|
||||
"buId" TEXT,
|
||||
"tenantId" TEXT,
|
||||
"workspaceId" TEXT,
|
||||
"projectId" TEXT,
|
||||
"userScopeId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "IiosScope_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IiosSourceHandle" (
|
||||
"id" TEXT NOT NULL,
|
||||
"scopeId" TEXT NOT NULL,
|
||||
"kind" "IiosHandleKind" NOT NULL,
|
||||
"externalId" TEXT NOT NULL,
|
||||
"displayName" TEXT,
|
||||
"canonicalEntityId" TEXT,
|
||||
"confidence" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
"verificationState" TEXT NOT NULL DEFAULT 'UNVERIFIED',
|
||||
"firstSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"metadata" JSONB,
|
||||
|
||||
CONSTRAINT "IiosSourceHandle_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IiosActorRef" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kind" "IiosActorKind" NOT NULL,
|
||||
"sourceHandleId" TEXT,
|
||||
"platformPrincipalRef" TEXT,
|
||||
"canonicalEntityId" TEXT,
|
||||
"displayName" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "IiosActorRef_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IiosChannel" (
|
||||
"id" TEXT NOT NULL,
|
||||
"scopeId" TEXT NOT NULL,
|
||||
"channelType" TEXT NOT NULL,
|
||||
"channelName" TEXT NOT NULL,
|
||||
"direction" TEXT NOT NULL DEFAULT 'BOTH',
|
||||
"capabilityContract" JSONB,
|
||||
"externalRef" TEXT,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "IiosChannel_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IiosThread" (
|
||||
"id" TEXT NOT NULL,
|
||||
"scopeId" TEXT NOT NULL,
|
||||
"channelId" TEXT,
|
||||
"threadKind" TEXT NOT NULL DEFAULT 'CONVERSATION',
|
||||
"subject" TEXT,
|
||||
"externalThreadRef" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'OPEN',
|
||||
"visibilityClass" TEXT NOT NULL DEFAULT 'PRIVATE',
|
||||
"createdByActorId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"metadata" JSONB,
|
||||
|
||||
CONSTRAINT "IiosThread_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IiosThreadParticipant" (
|
||||
"threadId" TEXT NOT NULL,
|
||||
"actorId" TEXT NOT NULL,
|
||||
"participantRole" TEXT NOT NULL DEFAULT 'MEMBER',
|
||||
"joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"leftAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "IiosThreadParticipant_pkey" PRIMARY KEY ("threadId","actorId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IiosInteraction" (
|
||||
"id" TEXT NOT NULL,
|
||||
"scopeId" TEXT NOT NULL,
|
||||
"kind" "IiosInteractionKind" NOT NULL,
|
||||
"threadId" TEXT,
|
||||
"channelId" TEXT,
|
||||
"actorId" TEXT,
|
||||
"parentInteractionId" TEXT,
|
||||
"externalId" TEXT,
|
||||
"idempotencyKey" TEXT NOT NULL,
|
||||
"occurredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"receivedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"status" "IiosDeliveryState" NOT NULL DEFAULT 'RECEIVED',
|
||||
"policyDecisionRef" TEXT,
|
||||
"consentReceiptRef" TEXT,
|
||||
"crreBundleRef" TEXT,
|
||||
"traceId" TEXT,
|
||||
"metadata" JSONB,
|
||||
|
||||
CONSTRAINT "IiosInteraction_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IiosMessagePart" (
|
||||
"id" TEXT NOT NULL,
|
||||
"interactionId" TEXT NOT NULL,
|
||||
"partIndex" INTEGER NOT NULL,
|
||||
"kind" "IiosMessagePartKind" NOT NULL,
|
||||
"bodyText" TEXT,
|
||||
"contentRef" TEXT,
|
||||
"mimeType" TEXT,
|
||||
"sizeBytes" BIGINT,
|
||||
"checksumSha256" TEXT,
|
||||
"metadata" JSONB,
|
||||
|
||||
CONSTRAINT "IiosMessagePart_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IiosOutboxEvent" (
|
||||
"eventId" TEXT NOT NULL,
|
||||
"aggregateType" TEXT NOT NULL,
|
||||
"aggregateId" TEXT NOT NULL,
|
||||
"eventType" TEXT NOT NULL,
|
||||
"cloudEvent" JSONB NOT NULL,
|
||||
"partitionKey" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'PENDING',
|
||||
"attempts" INTEGER NOT NULL DEFAULT 0,
|
||||
"nextAttemptAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"publishedAt" TIMESTAMP(3),
|
||||
|
||||
CONSTRAINT "IiosOutboxEvent_pkey" PRIMARY KEY ("eventId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "IiosProcessedEvent" (
|
||||
"consumerName" TEXT NOT NULL,
|
||||
"eventId" TEXT NOT NULL,
|
||||
"processedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"resultHash" TEXT,
|
||||
|
||||
CONSTRAINT "IiosProcessedEvent_pkey" PRIMARY KEY ("consumerName","eventId")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IiosScope_orgId_appId_tenantId_idx" ON "IiosScope"("orgId", "appId", "tenantId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IiosSourceHandle_canonicalEntityId_idx" ON "IiosSourceHandle"("canonicalEntityId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "IiosSourceHandle_scopeId_kind_externalId_key" ON "IiosSourceHandle"("scopeId", "kind", "externalId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IiosChannel_scopeId_channelType_idx" ON "IiosChannel"("scopeId", "channelType");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IiosThread_scopeId_status_idx" ON "IiosThread"("scopeId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IiosInteraction_threadId_occurredAt_idx" ON "IiosInteraction"("threadId", "occurredAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "IiosInteraction_scopeId_idempotencyKey_key" ON "IiosInteraction"("scopeId", "idempotencyKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "IiosMessagePart_interactionId_partIndex_key" ON "IiosMessagePart"("interactionId", "partIndex");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "IiosOutboxEvent_status_nextAttemptAt_createdAt_idx" ON "IiosOutboxEvent"("status", "nextAttemptAt", "createdAt");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosSourceHandle" ADD CONSTRAINT "IiosSourceHandle_scopeId_fkey" FOREIGN KEY ("scopeId") REFERENCES "IiosScope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosActorRef" ADD CONSTRAINT "IiosActorRef_sourceHandleId_fkey" FOREIGN KEY ("sourceHandleId") REFERENCES "IiosSourceHandle"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosChannel" ADD CONSTRAINT "IiosChannel_scopeId_fkey" FOREIGN KEY ("scopeId") REFERENCES "IiosScope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosThread" ADD CONSTRAINT "IiosThread_scopeId_fkey" FOREIGN KEY ("scopeId") REFERENCES "IiosScope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosThread" ADD CONSTRAINT "IiosThread_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "IiosChannel"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosThread" ADD CONSTRAINT "IiosThread_createdByActorId_fkey" FOREIGN KEY ("createdByActorId") REFERENCES "IiosActorRef"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosThreadParticipant" ADD CONSTRAINT "IiosThreadParticipant_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "IiosThread"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosThreadParticipant" ADD CONSTRAINT "IiosThreadParticipant_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "IiosActorRef"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosInteraction" ADD CONSTRAINT "IiosInteraction_scopeId_fkey" FOREIGN KEY ("scopeId") REFERENCES "IiosScope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosInteraction" ADD CONSTRAINT "IiosInteraction_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "IiosThread"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosInteraction" ADD CONSTRAINT "IiosInteraction_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "IiosChannel"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosInteraction" ADD CONSTRAINT "IiosInteraction_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "IiosActorRef"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosInteraction" ADD CONSTRAINT "IiosInteraction_parentInteractionId_fkey" FOREIGN KEY ("parentInteractionId") REFERENCES "IiosInteraction"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "IiosMessagePart" ADD CONSTRAINT "IiosMessagePart_interactionId_fkey" FOREIGN KEY ("interactionId") REFERENCES "IiosInteraction"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -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])
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { HealthController } from './health.controller';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [HealthController],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma/prisma.service';
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
@Get()
|
||||
async check(): Promise<{ status: string; db: boolean }> {
|
||||
let db = false;
|
||||
try {
|
||||
await this.prisma.$queryRaw`SELECT 1`;
|
||||
db = true;
|
||||
} catch {
|
||||
db = false;
|
||||
}
|
||||
return { status: 'ok', db };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'reflect-metadata';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap(): Promise<void> {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }),
|
||||
);
|
||||
app.enableCors({ origin: true, credentials: true });
|
||||
const port = Number(process.env.PORT ?? 3200);
|
||||
await app.listen(port);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`iios-service listening on :${port}`);
|
||||
}
|
||||
|
||||
void bootstrap();
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [PrismaService],
|
||||
exports: [PrismaService],
|
||||
})
|
||||
export class PrismaModule {}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||
async onModuleInit(): Promise<void> {
|
||||
await this.$connect();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": false,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist", "src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user