Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/modules/chat/services/ChatMessageTaskService.server.ts
T

111 lines
5.0 KiB
TypeScript

import type { OutboxPublisherTransaction } from "@hivegrid/db/outbox";
import type { AuditEventWriterClient } from "@hivegrid/db/audit-events.server";
import { CatalogError, foundationErrors, requirePermission } from "@hivegrid/foundation";
import { publishChatPersistentRealtimeWakeup } from "@hivegrid/chat/realtime-wakeup.server";
import { withWritableChatMessageTaskSource } from "@hivegrid/chat/repository.server";
import { tenantHasTasksModuleWithPermissionsTx } from "@hivegrid/chat/module-access.server";
import { writeChatSystemMessageWithTx } from "@hivegrid/chat/system-message.server";
import type { ChatRouteContext } from "../routes/chatAccess.server";
import { linkEntitiesWithTx } from "~/utils/integrations/entityLinkService.server";
import type { TasksRouteContext } from "~/utils/integrations/taskAccess.server";
import { auditTaskEvent, logTaskActivity } from "~/utils/integrations/taskAudit.server";
import { tasksDetailPath } from "~/utils/integrations/taskRoutePaths";
import { createIdempotentTaskFromSourceWithTx } from "~/utils/integrations/taskService.server";
export async function markChatMessageAsTask(input: {
readonly chatContext: ChatRouteContext;
readonly taskContext: TasksRouteContext;
readonly chatId: string;
readonly messageId: string;
readonly requestId: string;
readonly tenantSlug: string;
readonly fallbackTitle: string;
}): Promise<{ readonly taskId: string; readonly replayed: boolean }> {
if (input.chatContext.tenantId !== input.taskContext.tenantId || input.chatContext.userId !== input.taskContext.userId) {
throw new Error("Chat and task contexts must belong to the same tenant user");
}
const tenantSlug = input.tenantSlug.trim();
if (!tenantSlug) throw new CatalogError(foundationErrors.notFound);
return withWritableChatMessageTaskSource(
{
tenantId: input.chatContext.tenantId,
userId: input.chatContext.userId,
permissions: input.chatContext.permissions,
chatId: input.chatId,
messageId: input.messageId,
},
async (tx, source, currentPermissions) => {
if (!currentPermissions.includes("tasks.write")) {
throw new CatalogError(foundationErrors.notFound, { permission: "tasks.write" });
}
if (!(await tenantHasTasksModuleWithPermissionsTx(tx, input.chatContext.tenantId, currentPermissions))) {
throw new CatalogError(foundationErrors.notFound, { module: "tasks" });
}
requirePermission({ tenantId: input.taskContext.tenantId, userId: input.taskContext.userId, permissions: currentPermissions }, "tasks.write");
const currentTaskContext = { ...input.taskContext, permissions: currentPermissions };
const firstLine = source.body
.split(/\r?\n/u)
.map((line) => line.trim())
.find(Boolean);
const title = (firstLine || input.fallbackTitle).slice(0, 255);
const result = await createIdempotentTaskFromSourceWithTx(tx, {
tenantId: currentTaskContext.tenantId,
ctx: currentTaskContext,
title,
description: source.body.trim() || null,
linkedEntityType: source.projectId ? "project" : null,
linkedEntityId: source.projectId,
source: "chat",
sourceRef: `chat-message:${source.messageId}`,
});
await linkEntitiesWithTx(tx, {
tenantId: currentTaskContext.tenantId,
userId: currentTaskContext.userId,
sourceType: "chat_message",
sourceId: source.messageId,
targetType: "task",
targetId: result.task.id,
linkRole: "created-from",
});
if (!result.replayed) {
await auditTaskEvent(
{
ctx: currentTaskContext,
action: "task.created",
taskId: result.task.id,
metadata: { source: "chat", chatId: source.chatId, messageId: source.messageId },
},
{ client: tx as unknown as AuditEventWriterClient, strict: true }
);
await logTaskActivity(
{
ctx: currentTaskContext,
taskId: result.task.id,
activityType: "task_created",
summaryText: title,
linkedEntityType: result.task.linkedEntityType,
linkedEntityId: result.task.linkedEntityId,
},
{ client: tx, strict: true }
);
}
const taskHref = tasksDetailPath(tenantSlug, result.task.id);
const systemMessage = await writeChatSystemMessageWithTx(tx, {
tenantId: currentTaskContext.tenantId,
chatId: source.chatId,
systemEventKey: `task-created:${source.messageId}`,
requestId: `${input.requestId}:task-created`,
kind: "task_created",
metadata: { sourceMessageId: source.messageId, taskId: result.task.id, taskTitle: title, taskHref },
body: `${input.fallbackTitle}: ${title}`,
actorUserId: currentTaskContext.userId,
});
if (!systemMessage.replayed) {
await publishChatPersistentRealtimeWakeup(tx as unknown as OutboxPublisherTransaction, { event: systemMessage.event });
}
return { taskId: result.task.id, replayed: result.replayed };
}
);
}