111 lines
5.0 KiB
TypeScript
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 };
|
|
}
|
|
);
|
|
}
|