Replace inline queryAll calls in routes-meetings.ts with two new domain methods on db.knowledge: listMeetings (powers GET /api/companies/:companyId/meetings) and getMeeting (used by the PATCH meeting handler). The MeetingListRow shape mirrors the historical inline projection so HTTP responses stay byte-stable. Also adds listKnowledgeEntries here for use by routes-misc.
153 lines
5.5 KiB
TypeScript
153 lines
5.5 KiB
TypeScript
import { z } from "zod";
|
|
import { validateTaskCreation } from "../task-quality-gates";
|
|
import type { AgentApiServer } from "./server-core";
|
|
|
|
export function registerMeetingRoutes(server: AgentApiServer) {
|
|
const db = server.db;
|
|
const onDomainChanged = server.onDomainChanged;
|
|
const onWakeAgent = server.onWakeAgent;
|
|
|
|
server.addRoute("POST", "/api/companies/:companyId/meetings", (_req, res, params, token, body) => {
|
|
if (params.companyId !== token.companyId) {
|
|
server.json(res, 403, { error: "Company mismatch" });
|
|
return;
|
|
}
|
|
|
|
const meetingCreateSchema = z.object({
|
|
type: z.string().default("department_sync"),
|
|
title: z.string().default("Meeting"),
|
|
participantAgentIds: z.string().default("[]"),
|
|
scheduledAt: z.string().default(new Date().toISOString()),
|
|
durationMinutes: z.number().default(30),
|
|
agendaJson: z.string().default("[]"),
|
|
});
|
|
|
|
const parsed = meetingCreateSchema.safeParse(body);
|
|
if (!parsed.success) {
|
|
server.json(res, 400, { error: "Invalid meeting payload", details: parsed.error.issues });
|
|
return;
|
|
}
|
|
|
|
const input = parsed.data;
|
|
try {
|
|
const parsedParticipants = JSON.parse(input.participantAgentIds) as string[];
|
|
for (const agentId of parsedParticipants) {
|
|
if (typeof agentId !== "string" || !db.belongsToCompany("agents", agentId, token.companyId)) {
|
|
server.json(res, 400, { error: "Meeting participants must belong to the current company" });
|
|
return;
|
|
}
|
|
}
|
|
} catch {
|
|
server.json(res, 400, { error: "participantAgentIds must be a JSON array of agent ids" });
|
|
return;
|
|
}
|
|
const id = db.saveMeeting({
|
|
companyId: token.companyId,
|
|
type: input.type,
|
|
title: input.title,
|
|
organizerAgentId: token.agentId,
|
|
participantAgentIds: input.participantAgentIds,
|
|
scheduledAt: input.scheduledAt,
|
|
durationMinutes: input.durationMinutes,
|
|
agendaJson: input.agendaJson,
|
|
status: "scheduled",
|
|
});
|
|
onDomainChanged();
|
|
server.json(res, 201, { id });
|
|
});
|
|
|
|
server.addRoute("GET", "/api/companies/:companyId/meetings", (_req, res, params, token) => {
|
|
if (params.companyId !== token.companyId) {
|
|
server.json(res, 403, { error: "Company mismatch" });
|
|
return;
|
|
}
|
|
const meetings = db.listMeetings(token.companyId);
|
|
server.json(res, 200, meetings);
|
|
});
|
|
|
|
server.addRoute("PATCH", "/api/meetings/:meetingId", (_req, res, params, token, body) => {
|
|
const meetingId = params.meetingId;
|
|
if (!meetingId) {
|
|
server.json(res, 400, { error: "Missing meetingId" });
|
|
return;
|
|
}
|
|
const existing = db.getMeeting(meetingId, token.companyId);
|
|
if (!existing) {
|
|
server.json(res, 404, { error: "Meeting not found" });
|
|
return;
|
|
}
|
|
|
|
const meetingUpdateSchema = z.object({
|
|
type: z.string().optional(),
|
|
title: z.string().optional(),
|
|
notesJson: z.string().optional(),
|
|
decisionsJson: z.string().optional(),
|
|
actionItemsJson: z.string().optional(),
|
|
status: z.string().optional(),
|
|
scheduledAt: z.string().optional(),
|
|
});
|
|
|
|
const parsed = meetingUpdateSchema.safeParse(body);
|
|
if (!parsed.success) {
|
|
server.json(res, 400, { error: "Invalid meeting update payload", details: parsed.error.issues });
|
|
return;
|
|
}
|
|
|
|
const data = parsed.data;
|
|
let actionItems: Array<{ title?: string; assigneeAgentId?: string; description?: string }> = [];
|
|
if (data.actionItemsJson) {
|
|
try {
|
|
actionItems = JSON.parse(data.actionItemsJson) as Array<{ title?: string; assigneeAgentId?: string; description?: string }>;
|
|
} catch {
|
|
server.json(res, 400, { error: "actionItemsJson must be valid JSON" });
|
|
return;
|
|
}
|
|
for (const item of actionItems) {
|
|
if (item.assigneeAgentId && !db.belongsToCompany("agents", item.assigneeAgentId, token.companyId)) {
|
|
server.json(res, 400, { error: "Meeting action item assignees must belong to the current company" });
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
db.saveMeeting({
|
|
id: meetingId,
|
|
companyId: token.companyId,
|
|
type: data.type ?? "department_sync",
|
|
title: data.title ?? "Meeting",
|
|
notesJson: data.notesJson,
|
|
decisionsJson: data.decisionsJson,
|
|
actionItemsJson: data.actionItemsJson,
|
|
status: data.status,
|
|
scheduledAt: data.scheduledAt ?? new Date().toISOString(),
|
|
});
|
|
onDomainChanged();
|
|
// When action items are added, create tasks for them automatically
|
|
if (actionItems.length > 0) {
|
|
for (const item of actionItems) {
|
|
if (item.title && typeof item.title === "string") {
|
|
const meetingTaskValidation = validateTaskCreation(
|
|
{ title: item.title, description: item.description ?? `Action item from meeting: ${data.title ?? existing.title}` },
|
|
false,
|
|
);
|
|
if (!meetingTaskValidation.valid) {
|
|
// Log but skip invalid meeting action items rather than failing the whole request
|
|
continue;
|
|
}
|
|
db.saveTask({
|
|
companyId: token.companyId,
|
|
title: item.title,
|
|
description: item.description ?? `Action item from meeting: ${data.title ?? existing.title}`,
|
|
assigneeAgentId: item.assigneeAgentId ?? null,
|
|
status: item.assigneeAgentId ? "todo" : "backlog",
|
|
priority: "medium",
|
|
});
|
|
if (item.assigneeAgentId) {
|
|
onWakeAgent(item.assigneeAgentId, token.companyId, "assignment");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
server.json(res, 200, { updated: true });
|
|
});
|
|
}
|