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:
2026-06-30 21:16:31 +05:30
parent c73acce0e8
commit 9cfd1ab1ab
12 changed files with 3743 additions and 10 deletions
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}
+36
View File
@@ -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"
+257
View File
@@ -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])
}
+9
View File
@@ -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 };
}
}
+18
View File
@@ -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();
}
}
+11
View File
@@ -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"]
}