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>
This commit is contained in:
@@ -0,0 +1,62 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "IiosInboxItemKind" AS ENUM ('NEEDS_REPLY', 'NEEDS_REVIEW', 'NEEDS_APPROVAL', 'SUPPORT_UPDATE', 'MEETING_FOLLOWUP', 'DIGEST', 'SYSTEM_ALERT', 'CRM_OWNER_INTEREST');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "IiosInboxState" AS ENUM ('OPEN', 'SNOOZED', 'DONE', 'ARCHIVED', 'CANCELLED', 'STALE');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "IiosInboxItem" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"scopeId" TEXT NOT NULL,
|
||||||
|
"ownerActorId" TEXT NOT NULL,
|
||||||
|
"kind" "IiosInboxItemKind" NOT NULL,
|
||||||
|
"state" "IiosInboxState" NOT NULL DEFAULT 'OPEN',
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"summary" TEXT,
|
||||||
|
"priority" TEXT NOT NULL DEFAULT 'NORMAL',
|
||||||
|
"dueAt" TIMESTAMP(3),
|
||||||
|
"sourceInteractionId" TEXT,
|
||||||
|
"threadId" TEXT,
|
||||||
|
"traceId" TEXT,
|
||||||
|
"actionRef" JSONB,
|
||||||
|
"policyDecisionRef" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"metadata" JSONB,
|
||||||
|
|
||||||
|
CONSTRAINT "IiosInboxItem_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "IiosInboxItemStateHistory" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"inboxItemId" TEXT NOT NULL,
|
||||||
|
"fromState" "IiosInboxState",
|
||||||
|
"toState" "IiosInboxState" NOT NULL,
|
||||||
|
"actorId" TEXT,
|
||||||
|
"reasonCode" TEXT,
|
||||||
|
"at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "IiosInboxItemStateHistory_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "IiosInboxItem_ownerActorId_state_priority_idx" ON "IiosInboxItem"("ownerActorId", "state", "priority");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "IiosInboxItemStateHistory_inboxItemId_idx" ON "IiosInboxItemStateHistory"("inboxItemId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "IiosInboxItem" ADD CONSTRAINT "IiosInboxItem_scopeId_fkey" FOREIGN KEY ("scopeId") REFERENCES "IiosScope"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "IiosInboxItem" ADD CONSTRAINT "IiosInboxItem_ownerActorId_fkey" FOREIGN KEY ("ownerActorId") REFERENCES "IiosActorRef"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "IiosInboxItem" ADD CONSTRAINT "IiosInboxItem_sourceInteractionId_fkey" FOREIGN KEY ("sourceInteractionId") REFERENCES "IiosInteraction"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "IiosInboxItem" ADD CONSTRAINT "IiosInboxItem_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "IiosThread"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "IiosInboxItemStateHistory" ADD CONSTRAINT "IiosInboxItemStateHistory_inboxItemId_fkey" FOREIGN KEY ("inboxItemId") REFERENCES "IiosInboxItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -75,6 +75,26 @@ enum IiosReceiptKind {
|
|||||||
RATE_LIMITED
|
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 ────────────────────────────────────────────────
|
// ─── Kernel tables ────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Frozen six-vector scope. orgId + appId are REQUIRED (no global-by-null).
|
/// Frozen six-vector scope. orgId + appId are REQUIRED (no global-by-null).
|
||||||
@@ -93,6 +113,7 @@ model IiosScope {
|
|||||||
channels IiosChannel[]
|
channels IiosChannel[]
|
||||||
threads IiosThread[]
|
threads IiosThread[]
|
||||||
interactions IiosInteraction[]
|
interactions IiosInteraction[]
|
||||||
|
inboxItems IiosInboxItem[]
|
||||||
|
|
||||||
@@index([orgId, appId, tenantId])
|
@@index([orgId, appId, tenantId])
|
||||||
}
|
}
|
||||||
@@ -135,6 +156,7 @@ model IiosActorRef {
|
|||||||
interactions IiosInteraction[]
|
interactions IiosInteraction[]
|
||||||
receipts IiosMessageReceipt[]
|
receipts IiosMessageReceipt[]
|
||||||
unreadCounters IiosUnreadCounter[]
|
unreadCounters IiosUnreadCounter[]
|
||||||
|
inboxItemsOwned IiosInboxItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A configured channel surface (PORTAL in P1).
|
/// A configured channel surface (PORTAL in P1).
|
||||||
@@ -177,6 +199,7 @@ model IiosThread {
|
|||||||
participants IiosThreadParticipant[]
|
participants IiosThreadParticipant[]
|
||||||
interactions IiosInteraction[]
|
interactions IiosInteraction[]
|
||||||
unreadCounters IiosUnreadCounter[]
|
unreadCounters IiosUnreadCounter[]
|
||||||
|
inboxItems IiosInboxItem[]
|
||||||
|
|
||||||
@@index([scopeId, status])
|
@@index([scopeId, status])
|
||||||
}
|
}
|
||||||
@@ -221,6 +244,7 @@ model IiosInteraction {
|
|||||||
|
|
||||||
parts IiosMessagePart[]
|
parts IiosMessagePart[]
|
||||||
receipts IiosMessageReceipt[]
|
receipts IiosMessageReceipt[]
|
||||||
|
inboxItems IiosInboxItem[]
|
||||||
|
|
||||||
@@unique([scopeId, idempotencyKey])
|
@@unique([scopeId, idempotencyKey])
|
||||||
@@index([threadId, occurredAt])
|
@@index([threadId, occurredAt])
|
||||||
@@ -299,3 +323,49 @@ model IiosUnreadCounter {
|
|||||||
|
|
||||||
@@id([threadId, actorId])
|
@@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])
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { PrismaModule } from './prisma/prisma.module';
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
import { PlatformModule } from './platform/platform.module';
|
import { PlatformModule } from './platform/platform.module';
|
||||||
|
import { IdentityModule } from './identity/identity.module';
|
||||||
import { InteractionsModule } from './interactions/interactions.module';
|
import { InteractionsModule } from './interactions/interactions.module';
|
||||||
import { OutboxModule } from './outbox/outbox.module';
|
import { OutboxModule } from './outbox/outbox.module';
|
||||||
import { ThreadsModule } from './threads/threads.module';
|
import { ThreadsModule } from './threads/threads.module';
|
||||||
@@ -9,7 +10,7 @@ import { HealthController } from './health.controller';
|
|||||||
import { DevController } from './dev/dev.controller';
|
import { DevController } from './dev/dev.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, PlatformModule, InteractionsModule, OutboxModule, ThreadsModule, MessageModule],
|
imports: [PrismaModule, PlatformModule, IdentityModule, InteractionsModule, OutboxModule, ThreadsModule, MessageModule],
|
||||||
controllers: [HealthController, DevController],
|
controllers: [HealthController, DevController],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
|
/** The authenticated principal derived from the session port (token). */
|
||||||
|
export interface MessagePrincipal {
|
||||||
|
userId: string;
|
||||||
|
orgId: string;
|
||||||
|
appId: string;
|
||||||
|
tenantId?: string;
|
||||||
|
displayName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a principal to kernel rows (scope → source handle → actor) and
|
||||||
|
* manages thread participation. Shared by the message + inbox layers so neither
|
||||||
|
* re-implements identity resolution. Unknown handles stay UNVERIFIED (the MDM
|
||||||
|
* port resolution is applied at ingest; native users get a PORTAL_USER handle).
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ActorResolver {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async resolveScope(principal: MessagePrincipal) {
|
||||||
|
return (
|
||||||
|
(await this.prisma.iiosScope.findFirst({
|
||||||
|
where: { orgId: principal.orgId, appId: principal.appId, tenantId: principal.tenantId ?? null },
|
||||||
|
})) ??
|
||||||
|
(await this.prisma.iiosScope.create({
|
||||||
|
data: { orgId: principal.orgId, appId: principal.appId, tenantId: principal.tenantId },
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveActor(scopeId: string, principal: MessagePrincipal) {
|
||||||
|
const handle = await this.prisma.iiosSourceHandle.upsert({
|
||||||
|
where: { scopeId_kind_externalId: { scopeId, kind: 'PORTAL_USER', externalId: principal.userId } },
|
||||||
|
create: { scopeId, kind: 'PORTAL_USER', externalId: principal.userId, displayName: principal.displayName },
|
||||||
|
update: { lastSeenAt: new Date(), displayName: principal.displayName },
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
(await this.prisma.iiosActorRef.findFirst({ where: { sourceHandleId: handle.id } })) ??
|
||||||
|
(await this.prisma.iiosActorRef.create({
|
||||||
|
data: { kind: 'HUMAN', sourceHandleId: handle.id, displayName: principal.displayName },
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureParticipant(threadId: string, actorId: string): Promise<void> {
|
||||||
|
await this.prisma.iiosThreadParticipant.upsert({
|
||||||
|
where: { threadId_actorId: { threadId, actorId } },
|
||||||
|
create: { threadId, actorId },
|
||||||
|
update: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { ActorResolver } from './actor.resolver';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [ActorResolver],
|
||||||
|
exports: [ActorResolver],
|
||||||
|
})
|
||||||
|
export class IdentityModule {}
|
||||||
@@ -5,15 +5,9 @@ import { CloudEvent, IIOS_EVENTS, IiosPlatformPorts } from '@insignia/iios-contr
|
|||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { PLATFORM_PORTS } from '../platform/platform-ports';
|
import { PLATFORM_PORTS } from '../platform/platform-ports';
|
||||||
import { decideOrThrow } from '../platform/fail-closed';
|
import { decideOrThrow } from '../platform/fail-closed';
|
||||||
|
import { ActorResolver, type MessagePrincipal } from '../identity/actor.resolver';
|
||||||
|
|
||||||
/** The authenticated sender, derived from the session port (token). */
|
export type { MessagePrincipal };
|
||||||
export interface MessagePrincipal {
|
|
||||||
userId: string;
|
|
||||||
orgId: string;
|
|
||||||
appId: string;
|
|
||||||
tenantId?: string;
|
|
||||||
displayName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MessageDto {
|
export interface MessageDto {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -42,26 +36,27 @@ export class MessageService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
@Inject(PLATFORM_PORTS) private readonly ports: IiosPlatformPorts,
|
@Inject(PLATFORM_PORTS) private readonly ports: IiosPlatformPorts,
|
||||||
|
private readonly actors: ActorResolver,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Open an existing thread, or create one when no id is given. Joins as participant. */
|
/** Open an existing thread, or create one when no id is given. Joins as participant. */
|
||||||
async openThread(threadId: string | null, principal: MessagePrincipal): Promise<OpenThreadResult> {
|
async openThread(threadId: string | null, principal: MessagePrincipal): Promise<OpenThreadResult> {
|
||||||
if (!threadId) {
|
if (!threadId) {
|
||||||
await decideOrThrow(this.ports, { action: 'iios.thread.create', scope: principal });
|
await decideOrThrow(this.ports, { action: 'iios.thread.create', scope: principal });
|
||||||
const scope = await this.resolveScope(principal);
|
const scope = await this.actors.resolveScope(principal);
|
||||||
const actor = await this.resolveActor(scope.id, principal);
|
const actor = await this.actors.resolveActor(scope.id, principal);
|
||||||
const thread = await this.prisma.iiosThread.create({
|
const thread = await this.prisma.iiosThread.create({
|
||||||
data: { scopeId: scope.id, createdByActorId: actor.id },
|
data: { scopeId: scope.id, createdByActorId: actor.id },
|
||||||
});
|
});
|
||||||
await this.ensureParticipant(thread.id, actor.id);
|
await this.actors.ensureParticipant(thread.id, actor.id);
|
||||||
return { threadId: thread.id, status: thread.status, history: [] };
|
return { threadId: thread.id, status: thread.status, history: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const thread = await this.prisma.iiosThread.findUnique({ where: { id: threadId } });
|
const thread = await this.prisma.iiosThread.findUnique({ where: { id: threadId } });
|
||||||
if (!thread) throw new NotFoundException('thread not found');
|
if (!thread) throw new NotFoundException('thread not found');
|
||||||
await decideOrThrow(this.ports, { action: 'iios.thread.read', threadId, scopeId: thread.scopeId });
|
await decideOrThrow(this.ports, { action: 'iios.thread.read', threadId, scopeId: thread.scopeId });
|
||||||
const actor = await this.resolveActor(thread.scopeId, principal);
|
const actor = await this.actors.resolveActor(thread.scopeId, principal);
|
||||||
await this.ensureParticipant(threadId, actor.id);
|
await this.actors.ensureParticipant(threadId, actor.id);
|
||||||
return { threadId, status: thread.status, history: await this.history(threadId) };
|
return { threadId, status: thread.status, history: await this.history(threadId) };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,8 +72,8 @@ export class MessageService {
|
|||||||
|
|
||||||
await decideOrThrow(this.ports, { action: 'iios.message.send', threadId, scopeId: thread.scopeId });
|
await decideOrThrow(this.ports, { action: 'iios.message.send', threadId, scopeId: thread.scopeId });
|
||||||
|
|
||||||
const actor = await this.resolveActor(thread.scopeId, principal);
|
const actor = await this.actors.resolveActor(thread.scopeId, principal);
|
||||||
await this.ensureParticipant(threadId, actor.id);
|
await this.actors.ensureParticipant(threadId, actor.id);
|
||||||
|
|
||||||
// Idempotency: a repeat key returns the existing message (no re-increment).
|
// Idempotency: a repeat key returns the existing message (no re-increment).
|
||||||
const existing = await this.prisma.iiosInteraction.findUnique({
|
const existing = await this.prisma.iiosInteraction.findUnique({
|
||||||
@@ -168,7 +163,7 @@ export class MessageService {
|
|||||||
): Promise<{ interactionId: string; actorId: string }> {
|
): Promise<{ interactionId: string; actorId: string }> {
|
||||||
const thread = await this.prisma.iiosThread.findUnique({ where: { id: threadId } });
|
const thread = await this.prisma.iiosThread.findUnique({ where: { id: threadId } });
|
||||||
if (!thread) throw new NotFoundException('thread not found');
|
if (!thread) throw new NotFoundException('thread not found');
|
||||||
const actor = await this.resolveActor(thread.scopeId, principal);
|
const actor = await this.actors.resolveActor(thread.scopeId, principal);
|
||||||
|
|
||||||
await this.prisma.iiosMessageReceipt.upsert({
|
await this.prisma.iiosMessageReceipt.upsert({
|
||||||
where: { interactionId_actorId_receiptKind: { interactionId, actorId: actor.id, receiptKind: 'READ' } },
|
where: { interactionId_actorId_receiptKind: { interactionId, actorId: actor.id, receiptKind: 'READ' } },
|
||||||
@@ -190,7 +185,7 @@ export class MessageService {
|
|||||||
): Promise<{ interactionId: string; actorId: string }> {
|
): Promise<{ interactionId: string; actorId: string }> {
|
||||||
const thread = await this.prisma.iiosThread.findUnique({ where: { id: threadId } });
|
const thread = await this.prisma.iiosThread.findUnique({ where: { id: threadId } });
|
||||||
if (!thread) throw new NotFoundException('thread not found');
|
if (!thread) throw new NotFoundException('thread not found');
|
||||||
const actor = await this.resolveActor(thread.scopeId, principal);
|
const actor = await this.actors.resolveActor(thread.scopeId, principal);
|
||||||
await this.prisma.iiosMessageReceipt.upsert({
|
await this.prisma.iiosMessageReceipt.upsert({
|
||||||
where: { interactionId_actorId_receiptKind: { interactionId, actorId: actor.id, receiptKind: 'DELIVERED' } },
|
where: { interactionId_actorId_receiptKind: { interactionId, actorId: actor.id, receiptKind: 'DELIVERED' } },
|
||||||
create: { interactionId, actorId: actor.id, receiptKind: 'DELIVERED' },
|
create: { interactionId, actorId: actor.id, receiptKind: 'DELIVERED' },
|
||||||
@@ -219,39 +214,6 @@ export class MessageService {
|
|||||||
|
|
||||||
// ─── helpers ────────────────────────────────────────────────────
|
// ─── helpers ────────────────────────────────────────────────────
|
||||||
|
|
||||||
private async resolveScope(principal: MessagePrincipal) {
|
|
||||||
return (
|
|
||||||
(await this.prisma.iiosScope.findFirst({
|
|
||||||
where: { orgId: principal.orgId, appId: principal.appId, tenantId: principal.tenantId ?? null },
|
|
||||||
})) ??
|
|
||||||
(await this.prisma.iiosScope.create({
|
|
||||||
data: { orgId: principal.orgId, appId: principal.appId, tenantId: principal.tenantId },
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async resolveActor(scopeId: string, principal: MessagePrincipal) {
|
|
||||||
const handle = await this.prisma.iiosSourceHandle.upsert({
|
|
||||||
where: { scopeId_kind_externalId: { scopeId, kind: 'PORTAL_USER', externalId: principal.userId } },
|
|
||||||
create: { scopeId, kind: 'PORTAL_USER', externalId: principal.userId, displayName: principal.displayName },
|
|
||||||
update: { lastSeenAt: new Date(), displayName: principal.displayName },
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
(await this.prisma.iiosActorRef.findFirst({ where: { sourceHandleId: handle.id } })) ??
|
|
||||||
(await this.prisma.iiosActorRef.create({
|
|
||||||
data: { kind: 'HUMAN', sourceHandleId: handle.id, displayName: principal.displayName },
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ensureParticipant(threadId: string, actorId: string): Promise<void> {
|
|
||||||
await this.prisma.iiosThreadParticipant.upsert({
|
|
||||||
where: { threadId_actorId: { threadId, actorId } },
|
|
||||||
create: { threadId, actorId },
|
|
||||||
update: {},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private toDto(
|
private toDto(
|
||||||
interaction: { id: string; actorId: string | null; traceId: string | null; occurredAt: Date; parts: Array<{ kind: string; bodyText: string | null; contentRef: string | null }> },
|
interaction: { id: string; actorId: string | null; traceId: string | null; occurredAt: Date; parts: Array<{ kind: string; bodyText: string | null; contentRef: string | null }> },
|
||||||
threadId: string,
|
threadId: string,
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { makeFakePorts } from '@insignia/iios-testkit';
|
import { makeFakePorts } from '@insignia/iios-testkit';
|
||||||
import { MessageService, type MessagePrincipal } from './message.service';
|
import { MessageService, type MessagePrincipal } from './message.service';
|
||||||
|
import { ActorResolver } from '../identity/actor.resolver';
|
||||||
import type { PrismaService } from '../prisma/prisma.service';
|
import type { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
const url = process.env.DATABASE_URL ?? 'postgresql://iios:iios@localhost:5434/iios?schema=public';
|
const url = process.env.DATABASE_URL ?? 'postgresql://iios:iios@localhost:5434/iios?schema=public';
|
||||||
const prisma = new PrismaClient({ datasources: { db: { url } } });
|
const prisma = new PrismaClient({ datasources: { db: { url } } });
|
||||||
const asService = prisma as unknown as PrismaService;
|
const asService = prisma as unknown as PrismaService;
|
||||||
const svc = () => new MessageService(asService, makeFakePorts());
|
const svc = () => new MessageService(asService, makeFakePorts(), new ActorResolver(asService));
|
||||||
|
|
||||||
const alice: MessagePrincipal = { userId: 'alice', orgId: 'org_demo', appId: 'portal-demo', displayName: 'Alice' };
|
const alice: MessagePrincipal = { userId: 'alice', orgId: 'org_demo', appId: 'portal-demo', displayName: 'Alice' };
|
||||||
const bob: MessagePrincipal = { userId: 'bob', orgId: 'org_demo', appId: 'portal-demo', displayName: 'Bob' };
|
const bob: MessagePrincipal = { userId: 'bob', orgId: 'org_demo', appId: 'portal-demo', displayName: 'Bob' };
|
||||||
|
|||||||
Reference in New Issue
Block a user