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:
@@ -5,15 +5,9 @@ import { CloudEvent, IIOS_EVENTS, IiosPlatformPorts } from '@insignia/iios-contr
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { PLATFORM_PORTS } from '../platform/platform-ports';
|
||||
import { decideOrThrow } from '../platform/fail-closed';
|
||||
import { ActorResolver, type MessagePrincipal } from '../identity/actor.resolver';
|
||||
|
||||
/** The authenticated sender, derived from the session port (token). */
|
||||
export interface MessagePrincipal {
|
||||
userId: string;
|
||||
orgId: string;
|
||||
appId: string;
|
||||
tenantId?: string;
|
||||
displayName?: string;
|
||||
}
|
||||
export type { MessagePrincipal };
|
||||
|
||||
export interface MessageDto {
|
||||
id: string;
|
||||
@@ -42,26 +36,27 @@ export class MessageService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@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. */
|
||||
async openThread(threadId: string | null, principal: MessagePrincipal): Promise<OpenThreadResult> {
|
||||
if (!threadId) {
|
||||
await decideOrThrow(this.ports, { action: 'iios.thread.create', scope: principal });
|
||||
const scope = await this.resolveScope(principal);
|
||||
const actor = await this.resolveActor(scope.id, principal);
|
||||
const scope = await this.actors.resolveScope(principal);
|
||||
const actor = await this.actors.resolveActor(scope.id, principal);
|
||||
const thread = await this.prisma.iiosThread.create({
|
||||
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: [] };
|
||||
}
|
||||
|
||||
const thread = await this.prisma.iiosThread.findUnique({ where: { id: threadId } });
|
||||
if (!thread) throw new NotFoundException('thread not found');
|
||||
await decideOrThrow(this.ports, { action: 'iios.thread.read', threadId, scopeId: thread.scopeId });
|
||||
const actor = await this.resolveActor(thread.scopeId, principal);
|
||||
await this.ensureParticipant(threadId, actor.id);
|
||||
const actor = await this.actors.resolveActor(thread.scopeId, principal);
|
||||
await this.actors.ensureParticipant(threadId, actor.id);
|
||||
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 });
|
||||
|
||||
const actor = await this.resolveActor(thread.scopeId, principal);
|
||||
await this.ensureParticipant(threadId, actor.id);
|
||||
const actor = await this.actors.resolveActor(thread.scopeId, principal);
|
||||
await this.actors.ensureParticipant(threadId, actor.id);
|
||||
|
||||
// Idempotency: a repeat key returns the existing message (no re-increment).
|
||||
const existing = await this.prisma.iiosInteraction.findUnique({
|
||||
@@ -168,7 +163,7 @@ export class MessageService {
|
||||
): Promise<{ interactionId: string; actorId: string }> {
|
||||
const thread = await this.prisma.iiosThread.findUnique({ where: { id: threadId } });
|
||||
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({
|
||||
where: { interactionId_actorId_receiptKind: { interactionId, actorId: actor.id, receiptKind: 'READ' } },
|
||||
@@ -190,7 +185,7 @@ export class MessageService {
|
||||
): Promise<{ interactionId: string; actorId: string }> {
|
||||
const thread = await this.prisma.iiosThread.findUnique({ where: { id: threadId } });
|
||||
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({
|
||||
where: { interactionId_actorId_receiptKind: { interactionId, actorId: actor.id, receiptKind: 'DELIVERED' } },
|
||||
create: { interactionId, actorId: actor.id, receiptKind: 'DELIVERED' },
|
||||
@@ -219,39 +214,6 @@ export class MessageService {
|
||||
|
||||
// ─── 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(
|
||||
interaction: { id: string; actorId: string | null; traceId: string | null; occurredAt: Date; parts: Array<{ kind: string; bodyText: string | null; contentRef: string | null }> },
|
||||
threadId: string,
|
||||
|
||||
Reference in New Issue
Block a user