b164ef945c
- MailInternalDto accepts attachments[]; deposit() stores each as a media part (image/video→MEDIA_REF, audio→VOICE_REF, else FILE_REF) - ingest now persists part sizeBytes; contract + DTO carry it - threads.getMessages returns mimeType + sizeBytes so mail readers can render inline images / file chips - test: internal mail stores an image attachment as a MEDIA_REF part Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
47 lines
1.7 KiB
TypeScript
47 lines
1.7 KiB
TypeScript
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
|
|
import type { IiosPlatformPorts } from '@insignia/iios-contracts';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { PLATFORM_PORTS } from '../platform/platform-ports';
|
|
import { decideOrThrow } from '../platform/fail-closed';
|
|
|
|
export interface ThreadMessage {
|
|
interactionId: string;
|
|
actorId: string | null;
|
|
kind: string;
|
|
occurredAt: Date;
|
|
parts: Array<{ kind: string; bodyText: string | null; contentRef: string | null; mimeType: string | null; sizeBytes: number | null }>;
|
|
}
|
|
|
|
@Injectable()
|
|
export class ThreadsService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
@Inject(PLATFORM_PORTS) private readonly ports: IiosPlatformPorts,
|
|
) {}
|
|
|
|
async getMessages(threadId: string): Promise<{ threadId: string; messages: ThreadMessage[] }> {
|
|
const thread = await this.prisma.iiosThread.findUnique({ where: { id: threadId } });
|
|
if (!thread) throw new NotFoundException('thread not found');
|
|
|
|
// Reads are policy-scoped too (fail-closed).
|
|
await decideOrThrow(this.ports, { action: 'iios.thread.read', threadId, scopeId: thread.scopeId });
|
|
|
|
const interactions = await this.prisma.iiosInteraction.findMany({
|
|
where: { threadId },
|
|
orderBy: { occurredAt: 'asc' },
|
|
include: { parts: { orderBy: { partIndex: 'asc' } } },
|
|
});
|
|
|
|
return {
|
|
threadId,
|
|
messages: interactions.map((i) => ({
|
|
interactionId: i.id,
|
|
actorId: i.actorId,
|
|
kind: i.kind,
|
|
occurredAt: i.occurredAt,
|
|
parts: i.parts.map((p) => ({ kind: p.kind, bodyText: p.bodyText, contentRef: p.contentRef, mimeType: p.mimeType, sizeBytes: p.sizeBytes != null ? Number(p.sizeBytes) : null })),
|
|
})),
|
|
};
|
|
}
|
|
}
|