@open-mercato/ai-assistant 0.6.4-develop.3921.1.8a42ddf4c8 → 0.6.4-develop.3929.1.fcf7afece2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/[userId]/route.js +4 -0
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/[userId]/route.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js +9 -1
- package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js.map +2 -2
- package/dist/modules/ai_assistant/lib/conversation-storage.js +2 -0
- package/dist/modules/ai_assistant/lib/conversation-storage.js.map +2 -2
- package/package.json +6 -6
- package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/[userId]/route.ts +4 -0
- package/src/modules/ai_assistant/data/repositories/AiChatConversationRepository.ts +9 -1
- package/src/modules/ai_assistant/data/repositories/__tests__/AiChatConversationRepository.test.ts +41 -2
- package/src/modules/ai_assistant/lib/conversation-storage.ts +2 -0
|
@@ -5,6 +5,7 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
|
5
5
|
import { hasRequiredFeatures } from "../../../../../../lib/auth.js";
|
|
6
6
|
import {
|
|
7
7
|
AiChatConversationAccessError,
|
|
8
|
+
AiChatParticipantNotFoundError,
|
|
8
9
|
createConversationStorage
|
|
9
10
|
} from "../../../../../../lib/conversation-storage.js";
|
|
10
11
|
import { emitAiAssistantEvent } from "../../../../../../events.js";
|
|
@@ -110,6 +111,9 @@ async function DELETE(req, context) {
|
|
|
110
111
|
}
|
|
111
112
|
return new NextResponse(null, { status: 204 });
|
|
112
113
|
} catch (err) {
|
|
114
|
+
if (err instanceof AiChatParticipantNotFoundError) {
|
|
115
|
+
return jsonError(404, err.message || "Participant not found or already revoked.", "participant_not_found");
|
|
116
|
+
}
|
|
113
117
|
if (err instanceof AiChatConversationAccessError) {
|
|
114
118
|
return jsonError(403, err.message || "Access denied.", "forbidden");
|
|
115
119
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../../src/modules/ai_assistant/api/ai/conversations/%5BconversationId%5D/participants/%5BuserId%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { hasRequiredFeatures } from '../../../../../../lib/auth'\nimport {\n AiChatConversationAccessError,\n createConversationStorage,\n} from '../../../../../../lib/conversation-storage'\nimport { emitAiAssistantEvent } from '../../../../../../events'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\nconst SHARE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.share'\n\nconst participantParamsSchema = z.object({\n conversationId: z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters'),\n userId: z.string().uuid('userId must be a valid UUID'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Revoke a conversation participant',\n methods: {\n DELETE: {\n operationId: 'aiAssistantRevokeConversationParticipant',\n summary: 'Revoke a participant from a conversation (soft-delete).',\n description:\n 'Soft-deletes the participant row. If no active non-owner participants remain, ' +\n 'the conversation visibility is reset to \"private\". ' +\n 'Only the conversation owner or a manager may revoke participants.',\n responses: [\n {\n status: 204,\n description: 'Participant revoked.',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid path parameters.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks required features or is not the owner.' },\n { status: 404, description: 'Conversation not found.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n DELETE: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ conversationId: string; userId: string }>\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nexport async function DELETE(req: NextRequest, context: RouteContext): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return jsonError(401, 'Unauthorized', 'unauthenticated')\n const rawParams = await context.params\n const parseResult = participantParamsSchema.safeParse(rawParams)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid path parameters.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n if (!auth.tenantId) return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n const canShare = hasRequiredFeatures(\n [SHARE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n )\n if (!canShare) {\n return jsonError(\n 403,\n `Caller lacks required feature \"${SHARE_CONVERSATIONS_FEATURE}\".`,\n 'forbidden',\n )\n }\n\n try {\n const repo = createConversationStorage(container)\n await repo.revokeParticipant(\n parseResult.data.conversationId,\n parseResult.data.userId,\n {\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n canManageConversations: hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n ),\n },\n )\n try {\n await emitAiAssistantEvent(\n 'ai_assistant.conversation.unshared',\n {\n conversationId: parseResult.data.conversationId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n ownerUserId: auth.sub,\n participantUserId: parseResult.data.userId,\n },\n { persistent: false },\n )\n } catch {\n // non-fatal\n }\n return new NextResponse(null, { status: 204 })\n } catch (err) {\n if (err instanceof AiChatConversationAccessError) {\n return jsonError(403, err.message || 'Access denied.', 'forbidden')\n }\n return jsonError(500, 'Internal server error.', 'internal_error')\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { hasRequiredFeatures } from '../../../../../../lib/auth'\nimport {\n AiChatConversationAccessError,\n AiChatParticipantNotFoundError,\n createConversationStorage,\n} from '../../../../../../lib/conversation-storage'\nimport { emitAiAssistantEvent } from '../../../../../../events'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\nconst SHARE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.share'\n\nconst participantParamsSchema = z.object({\n conversationId: z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters'),\n userId: z.string().uuid('userId must be a valid UUID'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Revoke a conversation participant',\n methods: {\n DELETE: {\n operationId: 'aiAssistantRevokeConversationParticipant',\n summary: 'Revoke a participant from a conversation (soft-delete).',\n description:\n 'Soft-deletes the participant row. If no active non-owner participants remain, ' +\n 'the conversation visibility is reset to \"private\". ' +\n 'Only the conversation owner or a manager may revoke participants.',\n responses: [\n {\n status: 204,\n description: 'Participant revoked.',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid path parameters.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks required features or is not the owner.' },\n { status: 404, description: 'Conversation not found.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n DELETE: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ conversationId: string; userId: string }>\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nexport async function DELETE(req: NextRequest, context: RouteContext): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return jsonError(401, 'Unauthorized', 'unauthenticated')\n const rawParams = await context.params\n const parseResult = participantParamsSchema.safeParse(rawParams)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid path parameters.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n if (!auth.tenantId) return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n const canShare = hasRequiredFeatures(\n [SHARE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n )\n if (!canShare) {\n return jsonError(\n 403,\n `Caller lacks required feature \"${SHARE_CONVERSATIONS_FEATURE}\".`,\n 'forbidden',\n )\n }\n\n try {\n const repo = createConversationStorage(container)\n await repo.revokeParticipant(\n parseResult.data.conversationId,\n parseResult.data.userId,\n {\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n canManageConversations: hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n ),\n },\n )\n try {\n await emitAiAssistantEvent(\n 'ai_assistant.conversation.unshared',\n {\n conversationId: parseResult.data.conversationId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n ownerUserId: auth.sub,\n participantUserId: parseResult.data.userId,\n },\n { persistent: false },\n )\n } catch {\n // non-fatal\n }\n return new NextResponse(null, { status: 204 })\n } catch (err) {\n if (err instanceof AiChatParticipantNotFoundError) {\n return jsonError(404, err.message || 'Participant not found or already revoked.', 'participant_not_found')\n }\n if (err instanceof AiChatConversationAccessError) {\n return jsonError(403, err.message || 'Access denied.', 'forbidden')\n }\n return jsonError(500, 'Internal server error.', 'internal_error')\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -24,6 +24,12 @@ class AiChatConversationDuplicateParticipantError extends Error {
|
|
|
24
24
|
this.name = "AiChatConversationDuplicateParticipantError";
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
class AiChatParticipantNotFoundError extends Error {
|
|
28
|
+
constructor(message = "Participant not found or already revoked.") {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "AiChatParticipantNotFoundError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
27
33
|
class AiChatConversationOrgNotFoundError extends Error {
|
|
28
34
|
constructor(message = "Organization does not exist or is inactive for this tenant.") {
|
|
29
35
|
super(message);
|
|
@@ -542,7 +548,7 @@ class AiChatConversationRepository {
|
|
|
542
548
|
AiChatConversationParticipant,
|
|
543
549
|
participantFilter
|
|
544
550
|
);
|
|
545
|
-
if (!participant)
|
|
551
|
+
if (!participant) throw new AiChatParticipantNotFoundError();
|
|
546
552
|
participant.deletedAt = /* @__PURE__ */ new Date();
|
|
547
553
|
const remainingCount = await tx.count(AiChatConversationParticipant, {
|
|
548
554
|
tenantId: ctx.tenantId,
|
|
@@ -562,6 +568,7 @@ class AiChatConversationRepository {
|
|
|
562
568
|
tenantId,
|
|
563
569
|
conversationId,
|
|
564
570
|
deletedAt: null,
|
|
571
|
+
role: { $ne: "owner" },
|
|
565
572
|
...organizationId ? { organizationId } : {}
|
|
566
573
|
});
|
|
567
574
|
}
|
|
@@ -665,6 +672,7 @@ export {
|
|
|
665
672
|
AiChatConversationDuplicateParticipantError,
|
|
666
673
|
AiChatConversationOrgNotFoundError,
|
|
667
674
|
AiChatConversationRepository,
|
|
675
|
+
AiChatParticipantNotFoundError,
|
|
668
676
|
AiChatConversationRepository_default as default
|
|
669
677
|
};
|
|
670
678
|
//# sourceMappingURL=AiChatConversationRepository.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/ai_assistant/data/repositories/AiChatConversationRepository.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport {\n findOneWithDecryption,\n findWithDecryption,\n} from '@open-mercato/shared/lib/encryption/find'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport {\n AiChatConversation,\n AiChatConversationParticipant,\n AiChatMessage,\n} from '../entities'\nimport type {\n AiChatMessageAppendInput,\n AiChatPageContextInput,\n} from '../validators'\n\n/**\n * Persistent store for AI chat conversations, participants, and messages.\n *\n * Owner-first MVP per spec\n * `2026-05-05-ai-chat-server-side-conversation-storage`. Every read/write\n * goes through `findOneWithDecryption` / `findWithDecryption` so the repo\n * stays consistent with the rest of the module and is GDPR-encryption-ready\n * without a second refactor when `content` / `ui_parts` columns are\n * eventually flagged.\n *\n * Tenant + organization scope is required on every method. View-only callers\n * are owner-scoped. Callers with `ai_assistant.conversations.manage` may\n * list/read/update/delete any conversation in the same tenant/org, but never\n * outside that boundary. The participant row is written transactionally\n * alongside conversation create/import.\n *\n */\n\nexport interface AiChatConversationContext {\n tenantId: string\n organizationId?: string | null\n userId: string\n canManageConversations?: boolean\n}\n\nexport interface AiChatConversationCreateOrGetInput {\n conversationId?: string | null\n agentId: string\n title?: string | null\n pageContext?: AiChatPageContextInput | null\n /** Marks the conversation as imported from local storage (sets `importedFromLocalAt`). */\n importedFromLocal?: boolean\n /** Optional explicit `now` for deterministic tests. */\n now?: Date\n}\n\nexport interface AiChatConversationListOptions {\n agentId?: string | null\n status?: 'open' | 'closed' | null\n limit?: number\n cursor?: string | null\n}\n\nexport interface AiChatConversationUpdateInput {\n title?: string | null\n status?: 'open' | 'closed'\n pageContext?: AiChatPageContextInput | null\n /** Optional explicit `now` for deterministic tests. */\n now?: Date\n}\n\nexport interface AiChatTranscriptOptions {\n limit?: number\n /** ISO timestamp string; rows strictly older than this are returned. */\n before?: string | null\n}\n\nexport interface AiChatTranscriptResult {\n conversation: AiChatConversation\n messages: AiChatMessage[]\n nextCursor: string | null\n}\n\nexport interface AiChatMessageAppendOptions {\n /** Override the message timestamp (used to thread server-injected stream-completion turns). */\n createdAt?: Date\n /** Override `createdByUserId` (defaults to the calling context user). */\n createdByUserId?: string | null\n}\n\nexport interface AiChatConversationImportResult {\n conversation: AiChatConversation\n importedMessageCount: number\n skippedMessageCount: number\n}\n\nconst DEFAULT_LIST_LIMIT = 50\nconst MAX_LIST_LIMIT = 100\nconst DEFAULT_TRANSCRIPT_LIMIT = 100\nconst MAX_TRANSCRIPT_LIMIT = 200\n\nexport class AiChatConversationAccessError extends Error {\n override readonly name = 'AiChatConversationAccessError'\n constructor(message: string = 'Conversation is not accessible to the caller.') {\n super(message)\n }\n}\n\nexport class AiChatConversationDuplicateParticipantError extends Error {\n override readonly name = 'AiChatConversationDuplicateParticipantError'\n constructor(message: string = 'User is already an active participant in this conversation.') {\n super(message)\n }\n}\n\nexport class AiChatConversationOrgNotFoundError extends Error {\n override readonly name = 'AiChatConversationOrgNotFoundError'\n constructor(message: string = 'Organization does not exist or is inactive for this tenant.') {\n super(message)\n }\n}\n\nexport class AiChatConversationRepository {\n constructor(private readonly em: EntityManager) {}\n\n /**\n * Idempotent create. If a non-deleted conversation already exists for the\n * caller in this tenant/org with the same `conversationId`, returns the\n * existing row. The owner-participant row is created in the same\n * transaction; a partial failure leaves no orphan conversation.\n */\n async createOrGet(\n input: AiChatConversationCreateOrGetInput,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversation> {\n assertContext(ctx, 'createOrGet')\n if (!input?.agentId) {\n throw new Error('AiChatConversationRepository.createOrGet requires agentId')\n }\n const now = input.now ?? new Date()\n const conversationId = (input.conversationId ?? '').trim() || generateConversationId()\n\n return this.em.transactional(async (tx) => {\n const existing = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (existing) {\n if (existing.ownerUserId !== ctx.userId) {\n throw new AiChatConversationAccessError()\n }\n return existing\n }\n await assertOrganizationExists(tx as unknown as EntityManager, ctx)\n const conversation = tx.create(AiChatConversation, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n agentId: input.agentId,\n ownerUserId: ctx.userId,\n title: normalizeTitle(input.title),\n status: 'open',\n visibility: 'private',\n pageContext: input.pageContext ?? null,\n lastMessageAt: null,\n importedFromLocalAt: input.importedFromLocal ? now : null,\n createdAt: now,\n updatedAt: now,\n deletedAt: null,\n } as unknown as AiChatConversation)\n const participant = tx.create(AiChatConversationParticipant, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n userId: ctx.userId,\n role: 'owner',\n lastReadAt: null,\n createdAt: now,\n updatedAt: now,\n } as unknown as AiChatConversationParticipant)\n await tx.persist(conversation).persist(participant).flush()\n return conversation\n })\n }\n\n /** Fetch within tenant/org. View-only callers see only their own conversations. */\n async getById(\n conversationId: string,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversation | null> {\n assertContext(ctx, 'getById')\n if (!conversationId) return null\n const row = await findOneAccessibleConversation(this.em, conversationId, ctx)\n if (!row) return null\n const isParticipant =\n !canManageConversations(ctx) && row.ownerUserId !== ctx.userId\n ? await this.loadParticipantFlag(\n this.em,\n ctx.tenantId!,\n ctx.organizationId,\n row.conversationId,\n ctx.userId!,\n )\n : false\n if (!canAccessConversation(row, ctx, isParticipant)) return null\n return row\n }\n\n /** Owner-scoped list unless the caller has tenant/org manage access. Participants also see shared conversations. */\n async list(\n ctx: AiChatConversationContext,\n options: AiChatConversationListOptions = {},\n ): Promise<{ items: AiChatConversation[]; nextCursor: string | null }> {\n assertContext(ctx, 'list')\n const limit = clampLimit(options.limit, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT)\n const where: Record<string, unknown> = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n deletedAt: null,\n }\n if (!canManageConversations(ctx)) {\n const participantFilter: FilterQuery<AiChatConversationParticipant> = {\n tenantId: ctx.tenantId,\n userId: ctx.userId,\n deletedAt: null,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n const participantRows = await findWithDecryption<AiChatConversationParticipant>(\n this.em,\n AiChatConversationParticipant,\n participantFilter,\n { fields: ['conversationId'] as any },\n { tenantId: ctx.tenantId ?? null, organizationId: ctx.organizationId ?? null },\n )\n const participantConvIds = participantRows.map((p) => p.conversationId)\n if (participantConvIds.length > 0) {\n where.$or = [\n { ownerUserId: ctx.userId },\n { conversationId: { $in: participantConvIds } },\n ]\n } else {\n where.ownerUserId = ctx.userId\n }\n }\n if (options.agentId) where.agentId = options.agentId\n if (options.status) where.status = options.status\n if (options.cursor) {\n const cursorDate = parseIso(options.cursor)\n if (cursorDate) {\n where.lastMessageAt = { $lt: cursorDate }\n }\n }\n const rows = await findWithDecryption<AiChatConversation>(\n this.em,\n AiChatConversation,\n where as any,\n {\n orderBy: [{ lastMessageAt: 'desc' }, { createdAt: 'desc' }] as any,\n limit: limit + 1,\n },\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n let nextCursor: string | null = null\n if (rows.length > limit) {\n const lastIncluded = rows[limit - 1]\n const cursorValue = lastIncluded.lastMessageAt ?? lastIncluded.createdAt\n nextCursor = cursorValue ? cursorValue.toISOString() : null\n }\n return { items: rows.slice(0, limit), nextCursor }\n }\n\n /** Update within tenant/org. View-only callers can update only their own conversations. */\n async update(\n conversationId: string,\n patch: AiChatConversationUpdateInput,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversation> {\n assertContext(ctx, 'update')\n if (!conversationId) {\n throw new Error('AiChatConversationRepository.update requires conversationId')\n }\n return this.em.transactional(async (tx) => {\n const existing = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!existing) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (!canAccessConversation(existing, ctx)) {\n throw new AiChatConversationAccessError()\n }\n const now = patch.now ?? new Date()\n if (Object.prototype.hasOwnProperty.call(patch, 'title')) {\n existing.title = normalizeTitle(patch.title)\n }\n if (patch.status) existing.status = patch.status\n if (Object.prototype.hasOwnProperty.call(patch, 'pageContext')) {\n existing.pageContext = patch.pageContext ?? null\n }\n existing.updatedAt = now\n await tx.persist(existing).flush()\n return existing\n })\n }\n\n /** Soft-delete the conversation and all its messages in one transaction. */\n async softDelete(\n conversationId: string,\n ctx: AiChatConversationContext,\n now: Date = new Date(),\n ): Promise<void> {\n assertContext(ctx, 'softDelete')\n if (!conversationId) {\n throw new Error('AiChatConversationRepository.softDelete requires conversationId')\n }\n await this.em.transactional(async (tx) => {\n const existing = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!existing) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (!canAccessConversation(existing, ctx)) {\n throw new AiChatConversationAccessError()\n }\n existing.deletedAt = now\n existing.status = 'closed'\n existing.updatedAt = now\n await tx.persist(existing).flush()\n\n const messages = await findWithDecryption<AiChatMessage>(\n tx as unknown as EntityManager,\n AiChatMessage,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n for (const msg of messages) {\n msg.deletedAt = now\n msg.updatedAt = now\n tx.persist(msg)\n }\n if (messages.length > 0) await tx.flush()\n })\n }\n\n /**\n * Owner-only transcript hydration. Internally fetched DESC so the `before`\n * cursor naturally advances toward older messages, then reversed so the\n * response contract (`messages` array ordered ascending by `createdAt`)\n * stays stable for callers. `nextCursor` points to the OLDEST message in\n * the returned page \u2014 the next call with `before=<cursor>` fetches the\n * next-older window.\n */\n async getTranscript(\n conversationId: string,\n ctx: AiChatConversationContext,\n options: AiChatTranscriptOptions = {},\n ): Promise<AiChatTranscriptResult | null> {\n assertContext(ctx, 'getTranscript')\n if (!conversationId) return null\n const conversation = await this.getById(conversationId, ctx)\n if (!conversation) return null\n const limit = clampLimit(options.limit, DEFAULT_TRANSCRIPT_LIMIT, MAX_TRANSCRIPT_LIMIT)\n const where: Record<string, unknown> = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n deletedAt: null,\n }\n if (options.before) {\n const beforeDate = parseIso(options.before)\n if (beforeDate) {\n where.createdAt = { $lt: beforeDate }\n }\n }\n const rows = await findWithDecryption<AiChatMessage>(\n this.em,\n AiChatMessage,\n where as any,\n {\n orderBy: { createdAt: 'desc' } as any,\n limit: limit + 1,\n },\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n let nextCursor: string | null = null\n let pageDesc: AiChatMessage[]\n if (rows.length > limit) {\n pageDesc = rows.slice(0, limit)\n const oldestIncluded = pageDesc[pageDesc.length - 1]\n nextCursor = oldestIncluded?.createdAt ? oldestIncluded.createdAt.toISOString() : null\n } else {\n pageDesc = rows\n }\n const messages = [...pageDesc].reverse()\n return { conversation, messages, nextCursor }\n }\n\n /**\n * Append a single message to an owner-accessible conversation. Honors\n * `clientMessageId` idempotency: if a non-deleted message with the same\n * client id already exists, returns it untouched.\n */\n async appendMessage(\n conversationId: string,\n input: AiChatMessageAppendInput,\n ctx: AiChatConversationContext,\n options: AiChatMessageAppendOptions = {},\n ): Promise<AiChatMessage> {\n assertContext(ctx, 'appendMessage')\n if (!conversationId) {\n throw new Error('AiChatConversationRepository.appendMessage requires conversationId')\n }\n return this.em.transactional(async (tx) => {\n const conversation = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!conversation) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (conversation.ownerUserId !== ctx.userId) {\n throw new AiChatConversationAccessError()\n }\n const now = options.createdAt ?? new Date()\n if (input.clientMessageId) {\n const existing = await findOneWithDecryption<AiChatMessage>(\n tx as unknown as EntityManager,\n AiChatMessage,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n clientMessageId: input.clientMessageId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (existing) return existing\n }\n const message = tx.create(AiChatMessage, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n clientMessageId: input.clientMessageId ?? null,\n role: input.role,\n content: input.content,\n uiParts: normalizeArray(input.uiParts),\n attachmentIds: normalizeArray(input.attachmentIds),\n filesMetadata: normalizeArray(input.files),\n model: input.model ?? null,\n metadata: input.metadata ?? null,\n createdByUserId:\n options.createdByUserId === undefined\n ? input.role === 'user'\n ? ctx.userId\n : null\n : options.createdByUserId,\n createdAt: now,\n updatedAt: now,\n deletedAt: null,\n } as unknown as AiChatMessage)\n conversation.lastMessageAt = now\n conversation.updatedAt = now\n await tx.persist(message).persist(conversation).flush()\n return message\n })\n }\n\n /**\n * Lazy migration entrypoint: create-or-get the conversation and append the\n * provided messages with `clientMessageId` dedupe. Designed to be safe to\n * call repeatedly \u2014 repeated imports of the same payload return the same\n * counts of imported/skipped rows.\n */\n async importLocalConversation(\n input: {\n conversation: AiChatConversationCreateOrGetInput & {\n status?: 'open' | 'closed'\n }\n messages: AiChatMessageAppendInput[]\n },\n ctx: AiChatConversationContext,\n now: Date = new Date(),\n ): Promise<AiChatConversationImportResult> {\n assertContext(ctx, 'importLocalConversation')\n const conversation = await this.createOrGet(\n { ...input.conversation, importedFromLocal: true, now },\n ctx,\n )\n if (input.conversation.status && conversation.status !== input.conversation.status) {\n await this.update(\n conversation.conversationId,\n { status: input.conversation.status, now },\n ctx,\n )\n }\n let imported = 0\n let skipped = 0\n for (const message of input.messages) {\n if (!message.clientMessageId) {\n // Without an idempotency key the import has no safe way to dedupe.\n skipped += 1\n continue\n }\n const before = await findOneWithDecryption<AiChatMessage>(\n this.em,\n AiChatMessage,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId: conversation.conversationId,\n clientMessageId: message.clientMessageId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (before) {\n skipped += 1\n continue\n }\n await this.appendMessage(\n conversation.conversationId,\n message,\n ctx,\n { createdAt: now },\n )\n imported += 1\n }\n return {\n conversation,\n importedMessageCount: imported,\n skippedMessageCount: skipped,\n }\n }\n\n async listParticipants(\n conversationId: string,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversationParticipant[]> {\n assertContext(ctx, 'listParticipants')\n const conv = await findOneAccessibleConversation(this.em, conversationId, ctx)\n if (!conv) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (conv.ownerUserId !== ctx.userId && !canManageConversations(ctx)) {\n throw new AiChatConversationAccessError(\n 'Only the conversation owner or a manager can list participants.',\n )\n }\n const filter: FilterQuery<AiChatConversationParticipant> = {\n tenantId: ctx.tenantId,\n conversationId,\n deletedAt: null,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n return findWithDecryption<AiChatConversationParticipant>(\n this.em,\n AiChatConversationParticipant,\n filter,\n { orderBy: { createdAt: 'asc' } as any },\n { tenantId: ctx.tenantId ?? null, organizationId: ctx.organizationId ?? null },\n )\n }\n\n async addParticipant(\n conversationId: string,\n userId: string,\n role: 'viewer',\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversationParticipant> {\n assertContext(ctx, 'addParticipant')\n return this.em.transactional(async (tx) => {\n const conv = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!conv) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (conv.ownerUserId !== ctx.userId) {\n throw new AiChatConversationAccessError(\n 'Only the conversation owner can add participants.',\n )\n }\n const existingFilter: FilterQuery<AiChatConversationParticipant> = {\n tenantId: ctx.tenantId,\n conversationId,\n userId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n const existing = await findOneWithDecryption<AiChatConversationParticipant>(\n tx as unknown as EntityManager,\n AiChatConversationParticipant,\n existingFilter,\n )\n if (existing) {\n if (existing.deletedAt === null) {\n throw new AiChatConversationDuplicateParticipantError()\n }\n existing.deletedAt = null\n existing.role = role\n await tx.persist(existing).flush()\n if (conv.visibility === 'private') {\n conv.visibility = 'shared'\n await tx.persist(conv).flush()\n }\n return existing\n }\n const participant = tx.create(AiChatConversationParticipant, {\n tenantId: ctx.tenantId!,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n userId,\n role,\n } as unknown as AiChatConversationParticipant)\n if (conv.visibility === 'private') {\n conv.visibility = 'shared'\n }\n await tx.persist(participant).persist(conv).flush()\n return participant\n })\n }\n\n async revokeParticipant(\n conversationId: string,\n targetUserId: string,\n ctx: AiChatConversationContext,\n ): Promise<void> {\n assertContext(ctx, 'revokeParticipant')\n await this.em.transactional(async (tx) => {\n const conv = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!conv) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (conv.ownerUserId !== ctx.userId) {\n throw new AiChatConversationAccessError(\n 'Only the conversation owner can revoke participants.',\n )\n }\n if (targetUserId === conv.ownerUserId) {\n throw new AiChatConversationAccessError('Cannot revoke the conversation owner.')\n }\n const participantFilter: FilterQuery<AiChatConversationParticipant> = {\n tenantId: ctx.tenantId,\n conversationId,\n userId: targetUserId,\n deletedAt: null,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n const participant = await findOneWithDecryption<AiChatConversationParticipant>(\n tx as unknown as EntityManager,\n AiChatConversationParticipant,\n participantFilter,\n )\n if (!participant) return\n participant.deletedAt = new Date()\n const remainingCount = await tx.count(AiChatConversationParticipant, {\n tenantId: ctx.tenantId,\n conversationId,\n deletedAt: null,\n role: { $ne: 'owner' },\n } as FilterQuery<AiChatConversationParticipant>)\n if (remainingCount <= 1) {\n conv.visibility = 'private'\n await tx.persist(conv)\n }\n await tx.persist(participant).flush()\n })\n }\n\n async getParticipantCount(\n tenantId: string,\n organizationId: string | null | undefined,\n conversationId: string,\n ): Promise<number> {\n return this.em.count(AiChatConversationParticipant, {\n tenantId,\n conversationId,\n deletedAt: null,\n ...(organizationId ? { organizationId } : {}),\n } as FilterQuery<AiChatConversationParticipant>)\n }\n\n private async loadParticipantFlag(\n em: EntityManager,\n tenantId: string,\n organizationId: string | null | undefined,\n conversationId: string,\n userId: string,\n ): Promise<boolean> {\n const row = await findOneWithDecryption<AiChatConversationParticipant>(\n em,\n AiChatConversationParticipant,\n {\n tenantId,\n conversationId,\n userId,\n deletedAt: null,\n ...(organizationId ? { organizationId } : {}),\n } as FilterQuery<AiChatConversationParticipant>,\n )\n return row !== null\n }\n}\n\nfunction assertContext(ctx: AiChatConversationContext | undefined, method: string): void {\n if (!ctx?.tenantId) {\n throw new Error(`AiChatConversationRepository.${method} requires tenantId`)\n }\n if (!ctx?.userId) {\n throw new Error(`AiChatConversationRepository.${method} requires userId`)\n }\n}\n\nfunction canManageConversations(ctx: AiChatConversationContext): boolean {\n return ctx.canManageConversations === true\n}\n\nfunction canAccessConversation(\n row: AiChatConversation,\n ctx: AiChatConversationContext,\n isParticipant = false,\n): boolean {\n return canManageConversations(ctx) || row.ownerUserId === ctx.userId || isParticipant\n}\n\nasync function assertOrganizationExists(\n em: EntityManager,\n ctx: AiChatConversationContext,\n): Promise<void> {\n if (!ctx.organizationId) return\n const org = await findOneWithDecryption<Organization>(\n em,\n Organization,\n {\n id: ctx.organizationId,\n tenant: ctx.tenantId,\n deletedAt: null,\n isActive: true,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (!org) {\n throw new AiChatConversationOrgNotFoundError(\n `Organization \"${ctx.organizationId}\" does not exist or is inactive in tenant \"${ctx.tenantId}\".`,\n )\n }\n}\n\nasync function findOneAccessibleConversation(\n em: EntityManager,\n conversationId: string,\n ctx: AiChatConversationContext,\n): Promise<AiChatConversation | null> {\n const row = await findOneWithDecryption<AiChatConversation>(\n em,\n AiChatConversation,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n return row ?? null\n}\n\nfunction normalizeTitle(title: string | null | undefined): string | null {\n if (title === undefined) return null\n if (title === null) return null\n const trimmed = title.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction normalizeArray<T>(value: T[] | null | undefined): T[] | null {\n if (!Array.isArray(value) || value.length === 0) return null\n return value\n}\n\nfunction clampLimit(value: number | undefined | null, fallback: number, max: number): number {\n if (typeof value !== 'number' || !Number.isFinite(value)) return fallback\n return Math.max(1, Math.min(Math.floor(value), max))\n}\n\nfunction parseIso(value: string): Date | null {\n if (!value) return null\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? null : date\n}\n\nfunction generateConversationId(): string {\n // Prefer the runtime crypto generator when present; fall back to a non-cryptographic\n // string for environments without `crypto.randomUUID()` (older Node / test mocks).\n const cryptoMod: { randomUUID?: () => string } | undefined =\n typeof globalThis === 'object' ? (globalThis as any).crypto : undefined\n if (cryptoMod?.randomUUID) return cryptoMod.randomUUID()\n return `chat_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`\n}\n\nexport default AiChatConversationRepository\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport {\n findOneWithDecryption,\n findWithDecryption,\n} from '@open-mercato/shared/lib/encryption/find'\nimport { Organization } from '@open-mercato/core/modules/directory/data/entities'\nimport {\n AiChatConversation,\n AiChatConversationParticipant,\n AiChatMessage,\n} from '../entities'\nimport type {\n AiChatMessageAppendInput,\n AiChatPageContextInput,\n} from '../validators'\n\n/**\n * Persistent store for AI chat conversations, participants, and messages.\n *\n * Owner-first MVP per spec\n * `2026-05-05-ai-chat-server-side-conversation-storage`. Every read/write\n * goes through `findOneWithDecryption` / `findWithDecryption` so the repo\n * stays consistent with the rest of the module and is GDPR-encryption-ready\n * without a second refactor when `content` / `ui_parts` columns are\n * eventually flagged.\n *\n * Tenant + organization scope is required on every method. View-only callers\n * are owner-scoped. Callers with `ai_assistant.conversations.manage` may\n * list/read/update/delete any conversation in the same tenant/org, but never\n * outside that boundary. The participant row is written transactionally\n * alongside conversation create/import.\n *\n */\n\nexport interface AiChatConversationContext {\n tenantId: string\n organizationId?: string | null\n userId: string\n canManageConversations?: boolean\n}\n\nexport interface AiChatConversationCreateOrGetInput {\n conversationId?: string | null\n agentId: string\n title?: string | null\n pageContext?: AiChatPageContextInput | null\n /** Marks the conversation as imported from local storage (sets `importedFromLocalAt`). */\n importedFromLocal?: boolean\n /** Optional explicit `now` for deterministic tests. */\n now?: Date\n}\n\nexport interface AiChatConversationListOptions {\n agentId?: string | null\n status?: 'open' | 'closed' | null\n limit?: number\n cursor?: string | null\n}\n\nexport interface AiChatConversationUpdateInput {\n title?: string | null\n status?: 'open' | 'closed'\n pageContext?: AiChatPageContextInput | null\n /** Optional explicit `now` for deterministic tests. */\n now?: Date\n}\n\nexport interface AiChatTranscriptOptions {\n limit?: number\n /** ISO timestamp string; rows strictly older than this are returned. */\n before?: string | null\n}\n\nexport interface AiChatTranscriptResult {\n conversation: AiChatConversation\n messages: AiChatMessage[]\n nextCursor: string | null\n}\n\nexport interface AiChatMessageAppendOptions {\n /** Override the message timestamp (used to thread server-injected stream-completion turns). */\n createdAt?: Date\n /** Override `createdByUserId` (defaults to the calling context user). */\n createdByUserId?: string | null\n}\n\nexport interface AiChatConversationImportResult {\n conversation: AiChatConversation\n importedMessageCount: number\n skippedMessageCount: number\n}\n\nconst DEFAULT_LIST_LIMIT = 50\nconst MAX_LIST_LIMIT = 100\nconst DEFAULT_TRANSCRIPT_LIMIT = 100\nconst MAX_TRANSCRIPT_LIMIT = 200\n\nexport class AiChatConversationAccessError extends Error {\n override readonly name = 'AiChatConversationAccessError'\n constructor(message: string = 'Conversation is not accessible to the caller.') {\n super(message)\n }\n}\n\nexport class AiChatConversationDuplicateParticipantError extends Error {\n override readonly name = 'AiChatConversationDuplicateParticipantError'\n constructor(message: string = 'User is already an active participant in this conversation.') {\n super(message)\n }\n}\n\nexport class AiChatParticipantNotFoundError extends Error {\n override readonly name = 'AiChatParticipantNotFoundError'\n constructor(message: string = 'Participant not found or already revoked.') {\n super(message)\n }\n}\n\nexport class AiChatConversationOrgNotFoundError extends Error {\n override readonly name = 'AiChatConversationOrgNotFoundError'\n constructor(message: string = 'Organization does not exist or is inactive for this tenant.') {\n super(message)\n }\n}\n\nexport class AiChatConversationRepository {\n constructor(private readonly em: EntityManager) {}\n\n /**\n * Idempotent create. If a non-deleted conversation already exists for the\n * caller in this tenant/org with the same `conversationId`, returns the\n * existing row. The owner-participant row is created in the same\n * transaction; a partial failure leaves no orphan conversation.\n */\n async createOrGet(\n input: AiChatConversationCreateOrGetInput,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversation> {\n assertContext(ctx, 'createOrGet')\n if (!input?.agentId) {\n throw new Error('AiChatConversationRepository.createOrGet requires agentId')\n }\n const now = input.now ?? new Date()\n const conversationId = (input.conversationId ?? '').trim() || generateConversationId()\n\n return this.em.transactional(async (tx) => {\n const existing = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (existing) {\n if (existing.ownerUserId !== ctx.userId) {\n throw new AiChatConversationAccessError()\n }\n return existing\n }\n await assertOrganizationExists(tx as unknown as EntityManager, ctx)\n const conversation = tx.create(AiChatConversation, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n agentId: input.agentId,\n ownerUserId: ctx.userId,\n title: normalizeTitle(input.title),\n status: 'open',\n visibility: 'private',\n pageContext: input.pageContext ?? null,\n lastMessageAt: null,\n importedFromLocalAt: input.importedFromLocal ? now : null,\n createdAt: now,\n updatedAt: now,\n deletedAt: null,\n } as unknown as AiChatConversation)\n const participant = tx.create(AiChatConversationParticipant, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n userId: ctx.userId,\n role: 'owner',\n lastReadAt: null,\n createdAt: now,\n updatedAt: now,\n } as unknown as AiChatConversationParticipant)\n await tx.persist(conversation).persist(participant).flush()\n return conversation\n })\n }\n\n /** Fetch within tenant/org. View-only callers see only their own conversations. */\n async getById(\n conversationId: string,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversation | null> {\n assertContext(ctx, 'getById')\n if (!conversationId) return null\n const row = await findOneAccessibleConversation(this.em, conversationId, ctx)\n if (!row) return null\n const isParticipant =\n !canManageConversations(ctx) && row.ownerUserId !== ctx.userId\n ? await this.loadParticipantFlag(\n this.em,\n ctx.tenantId!,\n ctx.organizationId,\n row.conversationId,\n ctx.userId!,\n )\n : false\n if (!canAccessConversation(row, ctx, isParticipant)) return null\n return row\n }\n\n /** Owner-scoped list unless the caller has tenant/org manage access. Participants also see shared conversations. */\n async list(\n ctx: AiChatConversationContext,\n options: AiChatConversationListOptions = {},\n ): Promise<{ items: AiChatConversation[]; nextCursor: string | null }> {\n assertContext(ctx, 'list')\n const limit = clampLimit(options.limit, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT)\n const where: Record<string, unknown> = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n deletedAt: null,\n }\n if (!canManageConversations(ctx)) {\n const participantFilter: FilterQuery<AiChatConversationParticipant> = {\n tenantId: ctx.tenantId,\n userId: ctx.userId,\n deletedAt: null,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n const participantRows = await findWithDecryption<AiChatConversationParticipant>(\n this.em,\n AiChatConversationParticipant,\n participantFilter,\n { fields: ['conversationId'] as any },\n { tenantId: ctx.tenantId ?? null, organizationId: ctx.organizationId ?? null },\n )\n const participantConvIds = participantRows.map((p) => p.conversationId)\n if (participantConvIds.length > 0) {\n where.$or = [\n { ownerUserId: ctx.userId },\n { conversationId: { $in: participantConvIds } },\n ]\n } else {\n where.ownerUserId = ctx.userId\n }\n }\n if (options.agentId) where.agentId = options.agentId\n if (options.status) where.status = options.status\n if (options.cursor) {\n const cursorDate = parseIso(options.cursor)\n if (cursorDate) {\n where.lastMessageAt = { $lt: cursorDate }\n }\n }\n const rows = await findWithDecryption<AiChatConversation>(\n this.em,\n AiChatConversation,\n where as any,\n {\n orderBy: [{ lastMessageAt: 'desc' }, { createdAt: 'desc' }] as any,\n limit: limit + 1,\n },\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n let nextCursor: string | null = null\n if (rows.length > limit) {\n const lastIncluded = rows[limit - 1]\n const cursorValue = lastIncluded.lastMessageAt ?? lastIncluded.createdAt\n nextCursor = cursorValue ? cursorValue.toISOString() : null\n }\n return { items: rows.slice(0, limit), nextCursor }\n }\n\n /** Update within tenant/org. View-only callers can update only their own conversations. */\n async update(\n conversationId: string,\n patch: AiChatConversationUpdateInput,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversation> {\n assertContext(ctx, 'update')\n if (!conversationId) {\n throw new Error('AiChatConversationRepository.update requires conversationId')\n }\n return this.em.transactional(async (tx) => {\n const existing = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!existing) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (!canAccessConversation(existing, ctx)) {\n throw new AiChatConversationAccessError()\n }\n const now = patch.now ?? new Date()\n if (Object.prototype.hasOwnProperty.call(patch, 'title')) {\n existing.title = normalizeTitle(patch.title)\n }\n if (patch.status) existing.status = patch.status\n if (Object.prototype.hasOwnProperty.call(patch, 'pageContext')) {\n existing.pageContext = patch.pageContext ?? null\n }\n existing.updatedAt = now\n await tx.persist(existing).flush()\n return existing\n })\n }\n\n /** Soft-delete the conversation and all its messages in one transaction. */\n async softDelete(\n conversationId: string,\n ctx: AiChatConversationContext,\n now: Date = new Date(),\n ): Promise<void> {\n assertContext(ctx, 'softDelete')\n if (!conversationId) {\n throw new Error('AiChatConversationRepository.softDelete requires conversationId')\n }\n await this.em.transactional(async (tx) => {\n const existing = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!existing) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (!canAccessConversation(existing, ctx)) {\n throw new AiChatConversationAccessError()\n }\n existing.deletedAt = now\n existing.status = 'closed'\n existing.updatedAt = now\n await tx.persist(existing).flush()\n\n const messages = await findWithDecryption<AiChatMessage>(\n tx as unknown as EntityManager,\n AiChatMessage,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n for (const msg of messages) {\n msg.deletedAt = now\n msg.updatedAt = now\n tx.persist(msg)\n }\n if (messages.length > 0) await tx.flush()\n })\n }\n\n /**\n * Owner-only transcript hydration. Internally fetched DESC so the `before`\n * cursor naturally advances toward older messages, then reversed so the\n * response contract (`messages` array ordered ascending by `createdAt`)\n * stays stable for callers. `nextCursor` points to the OLDEST message in\n * the returned page \u2014 the next call with `before=<cursor>` fetches the\n * next-older window.\n */\n async getTranscript(\n conversationId: string,\n ctx: AiChatConversationContext,\n options: AiChatTranscriptOptions = {},\n ): Promise<AiChatTranscriptResult | null> {\n assertContext(ctx, 'getTranscript')\n if (!conversationId) return null\n const conversation = await this.getById(conversationId, ctx)\n if (!conversation) return null\n const limit = clampLimit(options.limit, DEFAULT_TRANSCRIPT_LIMIT, MAX_TRANSCRIPT_LIMIT)\n const where: Record<string, unknown> = {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n deletedAt: null,\n }\n if (options.before) {\n const beforeDate = parseIso(options.before)\n if (beforeDate) {\n where.createdAt = { $lt: beforeDate }\n }\n }\n const rows = await findWithDecryption<AiChatMessage>(\n this.em,\n AiChatMessage,\n where as any,\n {\n orderBy: { createdAt: 'desc' } as any,\n limit: limit + 1,\n },\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n let nextCursor: string | null = null\n let pageDesc: AiChatMessage[]\n if (rows.length > limit) {\n pageDesc = rows.slice(0, limit)\n const oldestIncluded = pageDesc[pageDesc.length - 1]\n nextCursor = oldestIncluded?.createdAt ? oldestIncluded.createdAt.toISOString() : null\n } else {\n pageDesc = rows\n }\n const messages = [...pageDesc].reverse()\n return { conversation, messages, nextCursor }\n }\n\n /**\n * Append a single message to an owner-accessible conversation. Honors\n * `clientMessageId` idempotency: if a non-deleted message with the same\n * client id already exists, returns it untouched.\n */\n async appendMessage(\n conversationId: string,\n input: AiChatMessageAppendInput,\n ctx: AiChatConversationContext,\n options: AiChatMessageAppendOptions = {},\n ): Promise<AiChatMessage> {\n assertContext(ctx, 'appendMessage')\n if (!conversationId) {\n throw new Error('AiChatConversationRepository.appendMessage requires conversationId')\n }\n return this.em.transactional(async (tx) => {\n const conversation = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!conversation) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (conversation.ownerUserId !== ctx.userId) {\n throw new AiChatConversationAccessError()\n }\n const now = options.createdAt ?? new Date()\n if (input.clientMessageId) {\n const existing = await findOneWithDecryption<AiChatMessage>(\n tx as unknown as EntityManager,\n AiChatMessage,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n clientMessageId: input.clientMessageId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (existing) return existing\n }\n const message = tx.create(AiChatMessage, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n clientMessageId: input.clientMessageId ?? null,\n role: input.role,\n content: input.content,\n uiParts: normalizeArray(input.uiParts),\n attachmentIds: normalizeArray(input.attachmentIds),\n filesMetadata: normalizeArray(input.files),\n model: input.model ?? null,\n metadata: input.metadata ?? null,\n createdByUserId:\n options.createdByUserId === undefined\n ? input.role === 'user'\n ? ctx.userId\n : null\n : options.createdByUserId,\n createdAt: now,\n updatedAt: now,\n deletedAt: null,\n } as unknown as AiChatMessage)\n conversation.lastMessageAt = now\n conversation.updatedAt = now\n await tx.persist(message).persist(conversation).flush()\n return message\n })\n }\n\n /**\n * Lazy migration entrypoint: create-or-get the conversation and append the\n * provided messages with `clientMessageId` dedupe. Designed to be safe to\n * call repeatedly \u2014 repeated imports of the same payload return the same\n * counts of imported/skipped rows.\n */\n async importLocalConversation(\n input: {\n conversation: AiChatConversationCreateOrGetInput & {\n status?: 'open' | 'closed'\n }\n messages: AiChatMessageAppendInput[]\n },\n ctx: AiChatConversationContext,\n now: Date = new Date(),\n ): Promise<AiChatConversationImportResult> {\n assertContext(ctx, 'importLocalConversation')\n const conversation = await this.createOrGet(\n { ...input.conversation, importedFromLocal: true, now },\n ctx,\n )\n if (input.conversation.status && conversation.status !== input.conversation.status) {\n await this.update(\n conversation.conversationId,\n { status: input.conversation.status, now },\n ctx,\n )\n }\n let imported = 0\n let skipped = 0\n for (const message of input.messages) {\n if (!message.clientMessageId) {\n // Without an idempotency key the import has no safe way to dedupe.\n skipped += 1\n continue\n }\n const before = await findOneWithDecryption<AiChatMessage>(\n this.em,\n AiChatMessage,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId: conversation.conversationId,\n clientMessageId: message.clientMessageId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (before) {\n skipped += 1\n continue\n }\n await this.appendMessage(\n conversation.conversationId,\n message,\n ctx,\n { createdAt: now },\n )\n imported += 1\n }\n return {\n conversation,\n importedMessageCount: imported,\n skippedMessageCount: skipped,\n }\n }\n\n async listParticipants(\n conversationId: string,\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversationParticipant[]> {\n assertContext(ctx, 'listParticipants')\n const conv = await findOneAccessibleConversation(this.em, conversationId, ctx)\n if (!conv) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (conv.ownerUserId !== ctx.userId && !canManageConversations(ctx)) {\n throw new AiChatConversationAccessError(\n 'Only the conversation owner or a manager can list participants.',\n )\n }\n const filter: FilterQuery<AiChatConversationParticipant> = {\n tenantId: ctx.tenantId,\n conversationId,\n deletedAt: null,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n return findWithDecryption<AiChatConversationParticipant>(\n this.em,\n AiChatConversationParticipant,\n filter,\n { orderBy: { createdAt: 'asc' } as any },\n { tenantId: ctx.tenantId ?? null, organizationId: ctx.organizationId ?? null },\n )\n }\n\n async addParticipant(\n conversationId: string,\n userId: string,\n role: 'viewer',\n ctx: AiChatConversationContext,\n ): Promise<AiChatConversationParticipant> {\n assertContext(ctx, 'addParticipant')\n return this.em.transactional(async (tx) => {\n const conv = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!conv) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (conv.ownerUserId !== ctx.userId) {\n throw new AiChatConversationAccessError(\n 'Only the conversation owner can add participants.',\n )\n }\n const existingFilter: FilterQuery<AiChatConversationParticipant> = {\n tenantId: ctx.tenantId,\n conversationId,\n userId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n const existing = await findOneWithDecryption<AiChatConversationParticipant>(\n tx as unknown as EntityManager,\n AiChatConversationParticipant,\n existingFilter,\n )\n if (existing) {\n if (existing.deletedAt === null) {\n throw new AiChatConversationDuplicateParticipantError()\n }\n existing.deletedAt = null\n existing.role = role\n await tx.persist(existing).flush()\n if (conv.visibility === 'private') {\n conv.visibility = 'shared'\n await tx.persist(conv).flush()\n }\n return existing\n }\n const participant = tx.create(AiChatConversationParticipant, {\n tenantId: ctx.tenantId!,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n userId,\n role,\n } as unknown as AiChatConversationParticipant)\n if (conv.visibility === 'private') {\n conv.visibility = 'shared'\n }\n await tx.persist(participant).persist(conv).flush()\n return participant\n })\n }\n\n async revokeParticipant(\n conversationId: string,\n targetUserId: string,\n ctx: AiChatConversationContext,\n ): Promise<void> {\n assertContext(ctx, 'revokeParticipant')\n await this.em.transactional(async (tx) => {\n const conv = await findOneAccessibleConversation(\n tx as unknown as EntityManager,\n conversationId,\n ctx,\n )\n if (!conv) {\n throw new AiChatConversationAccessError(\n `Conversation \"${conversationId}\" was not found for the caller.`,\n )\n }\n if (conv.ownerUserId !== ctx.userId) {\n throw new AiChatConversationAccessError(\n 'Only the conversation owner can revoke participants.',\n )\n }\n if (targetUserId === conv.ownerUserId) {\n throw new AiChatConversationAccessError('Cannot revoke the conversation owner.')\n }\n const participantFilter: FilterQuery<AiChatConversationParticipant> = {\n tenantId: ctx.tenantId,\n conversationId,\n userId: targetUserId,\n deletedAt: null,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n const participant = await findOneWithDecryption<AiChatConversationParticipant>(\n tx as unknown as EntityManager,\n AiChatConversationParticipant,\n participantFilter,\n )\n if (!participant) throw new AiChatParticipantNotFoundError()\n participant.deletedAt = new Date()\n const remainingCount = await tx.count(AiChatConversationParticipant, {\n tenantId: ctx.tenantId,\n conversationId,\n deletedAt: null,\n role: { $ne: 'owner' },\n } as FilterQuery<AiChatConversationParticipant>)\n if (remainingCount <= 1) {\n conv.visibility = 'private'\n await tx.persist(conv)\n }\n await tx.persist(participant).flush()\n })\n }\n\n async getParticipantCount(\n tenantId: string,\n organizationId: string | null | undefined,\n conversationId: string,\n ): Promise<number> {\n return this.em.count(AiChatConversationParticipant, {\n tenantId,\n conversationId,\n deletedAt: null,\n role: { $ne: 'owner' },\n ...(organizationId ? { organizationId } : {}),\n } as FilterQuery<AiChatConversationParticipant>)\n }\n\n private async loadParticipantFlag(\n em: EntityManager,\n tenantId: string,\n organizationId: string | null | undefined,\n conversationId: string,\n userId: string,\n ): Promise<boolean> {\n const row = await findOneWithDecryption<AiChatConversationParticipant>(\n em,\n AiChatConversationParticipant,\n {\n tenantId,\n conversationId,\n userId,\n deletedAt: null,\n ...(organizationId ? { organizationId } : {}),\n } as FilterQuery<AiChatConversationParticipant>,\n )\n return row !== null\n }\n}\n\nfunction assertContext(ctx: AiChatConversationContext | undefined, method: string): void {\n if (!ctx?.tenantId) {\n throw new Error(`AiChatConversationRepository.${method} requires tenantId`)\n }\n if (!ctx?.userId) {\n throw new Error(`AiChatConversationRepository.${method} requires userId`)\n }\n}\n\nfunction canManageConversations(ctx: AiChatConversationContext): boolean {\n return ctx.canManageConversations === true\n}\n\nfunction canAccessConversation(\n row: AiChatConversation,\n ctx: AiChatConversationContext,\n isParticipant = false,\n): boolean {\n return canManageConversations(ctx) || row.ownerUserId === ctx.userId || isParticipant\n}\n\nasync function assertOrganizationExists(\n em: EntityManager,\n ctx: AiChatConversationContext,\n): Promise<void> {\n if (!ctx.organizationId) return\n const org = await findOneWithDecryption<Organization>(\n em,\n Organization,\n {\n id: ctx.organizationId,\n tenant: ctx.tenantId,\n deletedAt: null,\n isActive: true,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (!org) {\n throw new AiChatConversationOrgNotFoundError(\n `Organization \"${ctx.organizationId}\" does not exist or is inactive in tenant \"${ctx.tenantId}\".`,\n )\n }\n}\n\nasync function findOneAccessibleConversation(\n em: EntityManager,\n conversationId: string,\n ctx: AiChatConversationContext,\n): Promise<AiChatConversation | null> {\n const row = await findOneWithDecryption<AiChatConversation>(\n em,\n AiChatConversation,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n conversationId,\n deletedAt: null,\n } as any,\n {},\n {\n tenantId: ctx.tenantId ?? null,\n organizationId: ctx.organizationId ?? null,\n },\n )\n return row ?? null\n}\n\nfunction normalizeTitle(title: string | null | undefined): string | null {\n if (title === undefined) return null\n if (title === null) return null\n const trimmed = title.trim()\n return trimmed.length > 0 ? trimmed : null\n}\n\nfunction normalizeArray<T>(value: T[] | null | undefined): T[] | null {\n if (!Array.isArray(value) || value.length === 0) return null\n return value\n}\n\nfunction clampLimit(value: number | undefined | null, fallback: number, max: number): number {\n if (typeof value !== 'number' || !Number.isFinite(value)) return fallback\n return Math.max(1, Math.min(Math.floor(value), max))\n}\n\nfunction parseIso(value: string): Date | null {\n if (!value) return null\n const date = new Date(value)\n return Number.isNaN(date.getTime()) ? null : date\n}\n\nfunction generateConversationId(): string {\n // Prefer the runtime crypto generator when present; fall back to a non-cryptographic\n // string for environments without `crypto.randomUUID()` (older Node / test mocks).\n const cryptoMod: { randomUUID?: () => string } | undefined =\n typeof globalThis === 'object' ? (globalThis as any).crypto : undefined\n if (cryptoMod?.randomUUID) return cryptoMod.randomUUID()\n return `chat_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`\n}\n\nexport default AiChatConversationRepository\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
AiChatConversationAccessError,
|
|
3
3
|
AiChatConversationDuplicateParticipantError,
|
|
4
4
|
AiChatConversationOrgNotFoundError,
|
|
5
|
+
AiChatParticipantNotFoundError,
|
|
5
6
|
AiChatConversationRepository
|
|
6
7
|
} from "../data/repositories/AiChatConversationRepository.js";
|
|
7
8
|
function createConversationStorage(container) {
|
|
@@ -43,6 +44,7 @@ export {
|
|
|
43
44
|
AiChatConversationAccessError,
|
|
44
45
|
AiChatConversationDuplicateParticipantError,
|
|
45
46
|
AiChatConversationOrgNotFoundError,
|
|
47
|
+
AiChatParticipantNotFoundError,
|
|
46
48
|
createConversationStorage,
|
|
47
49
|
serializeAiChatConversation,
|
|
48
50
|
serializeAiChatMessage
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/conversation-storage.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport {\n AiChatConversation,\n AiChatMessage,\n} from '../data/entities'\nimport {\n AiChatConversationAccessError,\n AiChatConversationDuplicateParticipantError,\n AiChatConversationOrgNotFoundError,\n AiChatConversationRepository,\n type AiChatConversationContext,\n} from '../data/repositories/AiChatConversationRepository'\n\n/**\n * Thin service-layer wrapper that resolves the entity manager from the\n * Awilix container and exposes a typed API on top of\n * `AiChatConversationRepository`. The REST routes for the conversation APIs\n * call into this surface; the future chat dispatcher write path will reuse\n * the same helpers so persistence stays consistent across entry points.\n *\n * Spec: `2026-05-05-ai-chat-server-side-conversation-storage` \u00A7\"Commands\".\n *\n * Re-exports the access error so route handlers can map it to a 404 without\n * importing the repository directly.\n */\nexport {\n AiChatConversationAccessError,\n AiChatConversationDuplicateParticipantError,\n AiChatConversationOrgNotFoundError,\n}\nexport type { AiChatConversationContext }\n\nexport function createConversationStorage(\n container: AwilixContainer,\n): AiChatConversationRepository {\n const em = container.resolve<EntityManager>('em')\n return new AiChatConversationRepository(em)\n}\n\nexport interface SerializedAiChatConversation {\n conversationId: string\n agentId: string\n title: string | null\n status: 'open' | 'closed'\n visibility: 'private' | 'shared' | 'organization'\n pageContext: Record<string, unknown> | null\n createdAt: string\n updatedAt: string\n lastMessageAt: string | null\n importedFromLocalAt: string | null\n participantCount: number\n isOwner: boolean | null\n}\n\nexport interface AiChatConversationSerializeEnrich {\n callerUserId?: string | null\n participantCount?: number\n}\n\nexport interface SerializedAiChatMessage {\n id: string\n clientMessageId: string | null\n role: 'user' | 'assistant' | 'system'\n content: string\n uiParts: unknown[]\n attachmentIds: string[]\n files: Array<Record<string, unknown>>\n model: string | null\n metadata: Record<string, unknown> | null\n createdAt: string\n senderUserId: string | null\n}\n\nexport function serializeAiChatConversation(\n row: AiChatConversation,\n enrich: AiChatConversationSerializeEnrich = {},\n): SerializedAiChatConversation {\n return {\n conversationId: row.conversationId,\n agentId: row.agentId,\n title: row.title ?? null,\n status: row.status,\n visibility: row.visibility,\n pageContext: row.pageContext ?? null,\n createdAt: row.createdAt.toISOString(),\n updatedAt: row.updatedAt.toISOString(),\n lastMessageAt: row.lastMessageAt ? row.lastMessageAt.toISOString() : null,\n importedFromLocalAt: row.importedFromLocalAt\n ? row.importedFromLocalAt.toISOString()\n : null,\n participantCount: enrich.participantCount ?? 0,\n isOwner:\n enrich.callerUserId != null ? row.ownerUserId === enrich.callerUserId : null,\n }\n}\n\nexport function serializeAiChatMessage(row: AiChatMessage): SerializedAiChatMessage {\n return {\n id: row.id,\n clientMessageId: row.clientMessageId ?? null,\n role: row.role,\n content: row.content,\n uiParts: Array.isArray(row.uiParts) ? row.uiParts : [],\n attachmentIds: Array.isArray(row.attachmentIds) ? row.attachmentIds : [],\n files: Array.isArray(row.filesMetadata) ? row.filesMetadata : [],\n model: row.model ?? null,\n metadata: row.metadata ?? null,\n createdAt: row.createdAt.toISOString(),\n senderUserId: row.createdByUserId ?? null,\n }\n}\n"],
|
|
5
|
-
"mappings": "AAMA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport {\n AiChatConversation,\n AiChatMessage,\n} from '../data/entities'\nimport {\n AiChatConversationAccessError,\n AiChatConversationDuplicateParticipantError,\n AiChatConversationOrgNotFoundError,\n AiChatParticipantNotFoundError,\n AiChatConversationRepository,\n type AiChatConversationContext,\n} from '../data/repositories/AiChatConversationRepository'\n\n/**\n * Thin service-layer wrapper that resolves the entity manager from the\n * Awilix container and exposes a typed API on top of\n * `AiChatConversationRepository`. The REST routes for the conversation APIs\n * call into this surface; the future chat dispatcher write path will reuse\n * the same helpers so persistence stays consistent across entry points.\n *\n * Spec: `2026-05-05-ai-chat-server-side-conversation-storage` \u00A7\"Commands\".\n *\n * Re-exports the access error so route handlers can map it to a 404 without\n * importing the repository directly.\n */\nexport {\n AiChatConversationAccessError,\n AiChatConversationDuplicateParticipantError,\n AiChatConversationOrgNotFoundError,\n AiChatParticipantNotFoundError,\n}\nexport type { AiChatConversationContext }\n\nexport function createConversationStorage(\n container: AwilixContainer,\n): AiChatConversationRepository {\n const em = container.resolve<EntityManager>('em')\n return new AiChatConversationRepository(em)\n}\n\nexport interface SerializedAiChatConversation {\n conversationId: string\n agentId: string\n title: string | null\n status: 'open' | 'closed'\n visibility: 'private' | 'shared' | 'organization'\n pageContext: Record<string, unknown> | null\n createdAt: string\n updatedAt: string\n lastMessageAt: string | null\n importedFromLocalAt: string | null\n participantCount: number\n isOwner: boolean | null\n}\n\nexport interface AiChatConversationSerializeEnrich {\n callerUserId?: string | null\n participantCount?: number\n}\n\nexport interface SerializedAiChatMessage {\n id: string\n clientMessageId: string | null\n role: 'user' | 'assistant' | 'system'\n content: string\n uiParts: unknown[]\n attachmentIds: string[]\n files: Array<Record<string, unknown>>\n model: string | null\n metadata: Record<string, unknown> | null\n createdAt: string\n senderUserId: string | null\n}\n\nexport function serializeAiChatConversation(\n row: AiChatConversation,\n enrich: AiChatConversationSerializeEnrich = {},\n): SerializedAiChatConversation {\n return {\n conversationId: row.conversationId,\n agentId: row.agentId,\n title: row.title ?? null,\n status: row.status,\n visibility: row.visibility,\n pageContext: row.pageContext ?? null,\n createdAt: row.createdAt.toISOString(),\n updatedAt: row.updatedAt.toISOString(),\n lastMessageAt: row.lastMessageAt ? row.lastMessageAt.toISOString() : null,\n importedFromLocalAt: row.importedFromLocalAt\n ? row.importedFromLocalAt.toISOString()\n : null,\n participantCount: enrich.participantCount ?? 0,\n isOwner:\n enrich.callerUserId != null ? row.ownerUserId === enrich.callerUserId : null,\n }\n}\n\nexport function serializeAiChatMessage(row: AiChatMessage): SerializedAiChatMessage {\n return {\n id: row.id,\n clientMessageId: row.clientMessageId ?? null,\n role: row.role,\n content: row.content,\n uiParts: Array.isArray(row.uiParts) ? row.uiParts : [],\n attachmentIds: Array.isArray(row.attachmentIds) ? row.attachmentIds : [],\n files: Array.isArray(row.filesMetadata) ? row.filesMetadata : [],\n model: row.model ?? null,\n metadata: row.metadata ?? null,\n createdAt: row.createdAt.toISOString(),\n senderUserId: row.createdByUserId ?? null,\n }\n}\n"],
|
|
5
|
+
"mappings": "AAMA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAsBA,SAAS,0BACd,WAC8B;AAC9B,QAAM,KAAK,UAAU,QAAuB,IAAI;AAChD,SAAO,IAAI,6BAA6B,EAAE;AAC5C;AAoCO,SAAS,4BACd,KACA,SAA4C,CAAC,GACf;AAC9B,SAAO;AAAA,IACL,gBAAgB,IAAI;AAAA,IACpB,SAAS,IAAI;AAAA,IACb,OAAO,IAAI,SAAS;AAAA,IACpB,QAAQ,IAAI;AAAA,IACZ,YAAY,IAAI;AAAA,IAChB,aAAa,IAAI,eAAe;AAAA,IAChC,WAAW,IAAI,UAAU,YAAY;AAAA,IACrC,WAAW,IAAI,UAAU,YAAY;AAAA,IACrC,eAAe,IAAI,gBAAgB,IAAI,cAAc,YAAY,IAAI;AAAA,IACrE,qBAAqB,IAAI,sBACrB,IAAI,oBAAoB,YAAY,IACpC;AAAA,IACJ,kBAAkB,OAAO,oBAAoB;AAAA,IAC7C,SACE,OAAO,gBAAgB,OAAO,IAAI,gBAAgB,OAAO,eAAe;AAAA,EAC5E;AACF;AAEO,SAAS,uBAAuB,KAA6C;AAClF,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,iBAAiB,IAAI,mBAAmB;AAAA,IACxC,MAAM,IAAI;AAAA,IACV,SAAS,IAAI;AAAA,IACb,SAAS,MAAM,QAAQ,IAAI,OAAO,IAAI,IAAI,UAAU,CAAC;AAAA,IACrD,eAAe,MAAM,QAAQ,IAAI,aAAa,IAAI,IAAI,gBAAgB,CAAC;AAAA,IACvE,OAAO,MAAM,QAAQ,IAAI,aAAa,IAAI,IAAI,gBAAgB,CAAC;AAAA,IAC/D,OAAO,IAAI,SAAS;AAAA,IACpB,UAAU,IAAI,YAAY;AAAA,IAC1B,WAAW,IAAI,UAAU,YAAY;AAAA,IACrC,cAAc,IAAI,mBAAmB;AAAA,EACvC;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ai-assistant",
|
|
3
|
-
"version": "0.6.4-develop.
|
|
3
|
+
"version": "0.6.4-develop.3929.1.fcf7afece2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -98,16 +98,16 @@
|
|
|
98
98
|
"zod-to-json-schema": "^3.25.2"
|
|
99
99
|
},
|
|
100
100
|
"peerDependencies": {
|
|
101
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
102
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
101
|
+
"@open-mercato/shared": "0.6.4-develop.3929.1.fcf7afece2",
|
|
102
|
+
"@open-mercato/ui": "0.6.4-develop.3929.1.fcf7afece2",
|
|
103
103
|
"react": "^19.0.0",
|
|
104
104
|
"react-dom": "^19.0.0",
|
|
105
105
|
"zod": ">=3.23.0"
|
|
106
106
|
},
|
|
107
107
|
"devDependencies": {
|
|
108
|
-
"@open-mercato/cli": "0.6.4-develop.
|
|
109
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
110
|
-
"@open-mercato/ui": "0.6.4-develop.
|
|
108
|
+
"@open-mercato/cli": "0.6.4-develop.3929.1.fcf7afece2",
|
|
109
|
+
"@open-mercato/shared": "0.6.4-develop.3929.1.fcf7afece2",
|
|
110
|
+
"@open-mercato/ui": "0.6.4-develop.3929.1.fcf7afece2",
|
|
111
111
|
"@types/react": "^19.2.15",
|
|
112
112
|
"@types/react-dom": "^19.2.3",
|
|
113
113
|
"react": "19.2.6",
|
|
@@ -7,6 +7,7 @@ import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacS
|
|
|
7
7
|
import { hasRequiredFeatures } from '../../../../../../lib/auth'
|
|
8
8
|
import {
|
|
9
9
|
AiChatConversationAccessError,
|
|
10
|
+
AiChatParticipantNotFoundError,
|
|
10
11
|
createConversationStorage,
|
|
11
12
|
} from '../../../../../../lib/conversation-storage'
|
|
12
13
|
import { emitAiAssistantEvent } from '../../../../../../events'
|
|
@@ -137,6 +138,9 @@ export async function DELETE(req: NextRequest, context: RouteContext): Promise<R
|
|
|
137
138
|
}
|
|
138
139
|
return new NextResponse(null, { status: 204 })
|
|
139
140
|
} catch (err) {
|
|
141
|
+
if (err instanceof AiChatParticipantNotFoundError) {
|
|
142
|
+
return jsonError(404, err.message || 'Participant not found or already revoked.', 'participant_not_found')
|
|
143
|
+
}
|
|
140
144
|
if (err instanceof AiChatConversationAccessError) {
|
|
141
145
|
return jsonError(403, err.message || 'Access denied.', 'forbidden')
|
|
142
146
|
}
|
|
@@ -109,6 +109,13 @@ export class AiChatConversationDuplicateParticipantError extends Error {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
export class AiChatParticipantNotFoundError extends Error {
|
|
113
|
+
override readonly name = 'AiChatParticipantNotFoundError'
|
|
114
|
+
constructor(message: string = 'Participant not found or already revoked.') {
|
|
115
|
+
super(message)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
112
119
|
export class AiChatConversationOrgNotFoundError extends Error {
|
|
113
120
|
override readonly name = 'AiChatConversationOrgNotFoundError'
|
|
114
121
|
constructor(message: string = 'Organization does not exist or is inactive for this tenant.') {
|
|
@@ -695,7 +702,7 @@ export class AiChatConversationRepository {
|
|
|
695
702
|
AiChatConversationParticipant,
|
|
696
703
|
participantFilter,
|
|
697
704
|
)
|
|
698
|
-
if (!participant)
|
|
705
|
+
if (!participant) throw new AiChatParticipantNotFoundError()
|
|
699
706
|
participant.deletedAt = new Date()
|
|
700
707
|
const remainingCount = await tx.count(AiChatConversationParticipant, {
|
|
701
708
|
tenantId: ctx.tenantId,
|
|
@@ -720,6 +727,7 @@ export class AiChatConversationRepository {
|
|
|
720
727
|
tenantId,
|
|
721
728
|
conversationId,
|
|
722
729
|
deletedAt: null,
|
|
730
|
+
role: { $ne: 'owner' },
|
|
723
731
|
...(organizationId ? { organizationId } : {}),
|
|
724
732
|
} as FilterQuery<AiChatConversationParticipant>)
|
|
725
733
|
}
|
package/src/modules/ai_assistant/data/repositories/__tests__/AiChatConversationRepository.test.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
AiChatConversationAccessError,
|
|
9
9
|
AiChatConversationDuplicateParticipantError,
|
|
10
10
|
AiChatConversationOrgNotFoundError,
|
|
11
|
+
AiChatParticipantNotFoundError,
|
|
11
12
|
AiChatConversationRepository,
|
|
12
13
|
} from '../AiChatConversationRepository'
|
|
13
14
|
|
|
@@ -83,6 +84,10 @@ function matchesWhere(row: Record<string, any>, where: any): boolean {
|
|
|
83
84
|
if (!inList.includes(actual)) return false
|
|
84
85
|
continue
|
|
85
86
|
}
|
|
87
|
+
if (expected && typeof expected === 'object' && '$ne' in expected) {
|
|
88
|
+
if (actual === expected.$ne) return false
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
86
91
|
if (expected === null) {
|
|
87
92
|
if (actual !== null && actual !== undefined) return false
|
|
88
93
|
continue
|
|
@@ -832,7 +837,7 @@ describe('AiChatConversationRepository', () => {
|
|
|
832
837
|
expect(row.organizationId).toBeNull()
|
|
833
838
|
})
|
|
834
839
|
|
|
835
|
-
it('getParticipantCount
|
|
840
|
+
it('getParticipantCount excludes the owner and counts only non-owner active participants', async () => {
|
|
836
841
|
const em = mockEm()
|
|
837
842
|
const repo = new AiChatConversationRepository(em)
|
|
838
843
|
const ownerCtx = { tenantId: tenantAlpha, organizationId: null, userId: 'u-owner' }
|
|
@@ -842,6 +847,40 @@ describe('AiChatConversationRepository', () => {
|
|
|
842
847
|
await repo.revokeParticipant('c-count', 'u-v1', ownerCtx)
|
|
843
848
|
|
|
844
849
|
const total = await repo.getParticipantCount(tenantAlpha, null, 'c-count')
|
|
845
|
-
expect(total).toBe(
|
|
850
|
+
expect(total).toBe(1)
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
it('getParticipantCount returns 0 for a private (owner-only) conversation', async () => {
|
|
854
|
+
const em = mockEm()
|
|
855
|
+
const repo = new AiChatConversationRepository(em)
|
|
856
|
+
const ownerCtx = { tenantId: tenantAlpha, organizationId: null, userId: 'u-owner' }
|
|
857
|
+
await repo.createOrGet({ conversationId: 'c-private', agentId: 'a' }, ownerCtx)
|
|
858
|
+
|
|
859
|
+
const count = await repo.getParticipantCount(tenantAlpha, null, 'c-private')
|
|
860
|
+
expect(count).toBe(0)
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
it('revokeParticipant throws AiChatParticipantNotFoundError for a non-existent userId', async () => {
|
|
864
|
+
const em = mockEm()
|
|
865
|
+
const repo = new AiChatConversationRepository(em)
|
|
866
|
+
const ownerCtx = { tenantId: tenantAlpha, organizationId: null, userId: 'u-owner' }
|
|
867
|
+
await repo.createOrGet({ conversationId: 'c-revoke-nf', agentId: 'a' }, ownerCtx)
|
|
868
|
+
|
|
869
|
+
await expect(
|
|
870
|
+
repo.revokeParticipant('c-revoke-nf', 'u-nonexistent', ownerCtx),
|
|
871
|
+
).rejects.toBeInstanceOf(AiChatParticipantNotFoundError)
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
it('revokeParticipant throws AiChatParticipantNotFoundError when revoking an already-revoked participant', async () => {
|
|
875
|
+
const em = mockEm()
|
|
876
|
+
const repo = new AiChatConversationRepository(em)
|
|
877
|
+
const ownerCtx = { tenantId: tenantAlpha, organizationId: null, userId: 'u-owner' }
|
|
878
|
+
await repo.createOrGet({ conversationId: 'c-double-revoke', agentId: 'a' }, ownerCtx)
|
|
879
|
+
await repo.addParticipant('c-double-revoke', 'u-viewer', 'viewer', ownerCtx)
|
|
880
|
+
await repo.revokeParticipant('c-double-revoke', 'u-viewer', ownerCtx)
|
|
881
|
+
|
|
882
|
+
await expect(
|
|
883
|
+
repo.revokeParticipant('c-double-revoke', 'u-viewer', ownerCtx),
|
|
884
|
+
).rejects.toBeInstanceOf(AiChatParticipantNotFoundError)
|
|
846
885
|
})
|
|
847
886
|
})
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
AiChatConversationAccessError,
|
|
9
9
|
AiChatConversationDuplicateParticipantError,
|
|
10
10
|
AiChatConversationOrgNotFoundError,
|
|
11
|
+
AiChatParticipantNotFoundError,
|
|
11
12
|
AiChatConversationRepository,
|
|
12
13
|
type AiChatConversationContext,
|
|
13
14
|
} from '../data/repositories/AiChatConversationRepository'
|
|
@@ -28,6 +29,7 @@ export {
|
|
|
28
29
|
AiChatConversationAccessError,
|
|
29
30
|
AiChatConversationDuplicateParticipantError,
|
|
30
31
|
AiChatConversationOrgNotFoundError,
|
|
32
|
+
AiChatParticipantNotFoundError,
|
|
31
33
|
}
|
|
32
34
|
export type { AiChatConversationContext }
|
|
33
35
|
|