refactor(api-server): move tasks SQL into db.tasks/db.documents domain methods
Replace inline queryAll calls in routes-tasks.ts with two new domain methods: tasks.listTasksByStatuses (used by GET /api/companies/:id/issues) and documents.listImportantDocsForReview (used by the deliverable-review gate). Routes now hold no SQL or column-mapping logic; the TaskListByStatusesRow shape preserves the existing HTTP response fields including requiresUserReview as a boolean.
This commit is contained in:
1 parent
555d8e59eb
commit
7f097e3319
5 files changed
+85
-24
No files matched your search
@@ -6,17 +6,6 @@ import { inferTaskType } from "../connector-matching";
|
||||
import { validateTaskCreation } from "../task-quality-gates";
|
||||
import { requireParam, type AgentApiServer } from "./server-core";
|
||||
|
||||
type TaskListRow = {
|
||||
id: string; companyId: string; projectId: string | null; goalId: string | null;
|
||||
parentId: string | null; title: string; description: string;
|
||||
assigneeAgentId: string | null; workspaceId: string | null; status: string;
|
||||
priority: string; activeRunId: string | null; sessionJson: string | null;
|
||||
sessionDisplayId: string | null; costUsd: number | null;
|
||||
requiresUserReview: number; startedAt: string | null;
|
||||
completedAt: string | null; cancelledAt: string | null;
|
||||
createdAt: string; updatedAt: string;
|
||||
};
|
||||
|
||||
const taskUpdateSchema = z.object({
|
||||
status: z.enum(["backlog", "todo", "in_progress", "in_review", "done", "blocked", "cancelled"]).optional(),
|
||||
comment: z.string().min(1).max(4000).optional(),
|
||||
@@ -51,11 +40,7 @@ export function registerTaskRoutes(server: AgentApiServer) {
|
||||
const tasks = db.listAgentTasks(assigneeAgentId, companyId, statuses);
|
||||
server.json(res, 200, tasks);
|
||||
} else {
|
||||
const placeholders = statuses.map(() => "?").join(", ");
|
||||
const tasks = db.queryAll<TaskListRow>(
|
||||
`select id, company_id as companyId, project_id as projectId, goal_id as goalId, parent_id as parentId, title, description, assignee_agent_id as assigneeAgentId, workspace_id as workspaceId, status, priority, active_run_id as activeRunId, session_json as sessionJson, session_display_id as sessionDisplayId, cost_usd as costUsd, requires_user_review as requiresUserReview, started_at as startedAt, completed_at as completedAt, cancelled_at as cancelledAt, created_at as createdAt, updated_at as updatedAt from tasks where company_id = ? and status in (${placeholders}) order by created_at desc`,
|
||||
companyId, ...statuses,
|
||||
).map((t) => ({ ...t, requiresUserReview: Boolean(t.requiresUserReview), startedAt: t.startedAt ?? null, completedAt: t.completedAt ?? null, cancelledAt: t.cancelledAt ?? null }));
|
||||
const tasks = db.listTasksByStatuses(companyId, statuses);
|
||||
server.json(res, 200, tasks);
|
||||
}
|
||||
});
|
||||
@@ -139,14 +124,13 @@ export function registerTaskRoutes(server: AgentApiServer) {
|
||||
// Deliverable review gate: intercept agent trying to advance a flagged task to "done".
|
||||
// This MUST succeed — silently skipping review would let unreviewed work advance.
|
||||
if (update.status === "done" && task.requiresUserReview) {
|
||||
const importantTypes = IMPORTANT_DOC_TYPES;
|
||||
const placeholders = importantTypes.map(() => "?").join(", ");
|
||||
const docs = db.queryAll<{ id: string; type: string; title: string }>(
|
||||
`select id, type, title from documents where company_id = ? and type in (${placeholders})
|
||||
and (author_agent_id = ? or created_at >= (select created_at from tasks where id = ?))
|
||||
order by created_at desc limit 10`,
|
||||
task.companyId, ...importantTypes, task.assigneeAgentId ?? "", task.id,
|
||||
);
|
||||
const docs = db.listImportantDocsForReview({
|
||||
companyId: task.companyId,
|
||||
types: IMPORTANT_DOC_TYPES,
|
||||
authorAgentId: task.assigneeAgentId ?? null,
|
||||
taskId: task.id,
|
||||
limit: 10,
|
||||
});
|
||||
const reviewerAgent = db.getAgent(token.agentId);
|
||||
const docTitles = docs.map((d) => d.title).join(", ");
|
||||
db.requestApproval({
|
||||
|
||||
@@ -242,6 +242,7 @@ export class AppDatabaseBase {
|
||||
releaseTaskLock(taskId: string, runId: string) { return this.tasks.releaseTaskLock(taskId, runId); }
|
||||
renewTaskLease(taskId: string, runId: string) { return this.tasks.renewTaskLease(taskId, runId); }
|
||||
listAgentTasks(agentId: string, companyId: string, statuses: string[]) { return this.tasks.listAgentTasks(agentId, companyId, statuses); }
|
||||
listTasksByStatuses(companyId: string, statuses: string[]) { return this.tasks.listTasksByStatuses(companyId, statuses); }
|
||||
findReusableHeartbeatTask(agentId: string, companyId: string) { return this.tasks.findReusableHeartbeatTask(agentId, companyId); }
|
||||
acquireHeartbeatTask(input: { agentId: string; companyId: string; runId: string; workspaceId: string | null; title: string; description: string; priority: string }) { return this.tasks.acquireHeartbeatTask(input); }
|
||||
cleanupDuplicateHeartbeatTasks(companyId: string) { return this.tasks.cleanupDuplicateHeartbeatTasks(companyId); }
|
||||
|
||||
@@ -82,6 +82,7 @@ export class AppDatabase extends AppDatabaseBase {
|
||||
// ─── Documents & Work Products ─────────────────────────────────────────────
|
||||
|
||||
saveDocument(input: { id?: string | null; companyId: string; type: string; title: string; content?: string; authorAgentId?: string | null; reviewerAgentId?: string | null; projectId?: string | null; goalId?: string | null; parentDocId?: string | null; version?: number; status?: string; tagsJson?: string; sourceRunId?: string | null }) { return this.documentsMod.saveDocument(input); }
|
||||
listImportantDocsForReview(input: { companyId: string; types: readonly string[]; authorAgentId: string | null; taskId: string; limit?: number }) { return this.documentsMod.listImportantDocsForReview(input); }
|
||||
updateDocumentStatus(id: string, companyId: string, status: string) { this.documentsMod.updateDocumentStatus(id, companyId, status); }
|
||||
deleteDocument(id: string, companyId: string) { this.documentsMod.deleteDocument(id, companyId); }
|
||||
createDocumentRevision(input: { documentId: string; companyId: string; body: string; changeSummary?: string | null; createdByAgentId?: string | null; createdByUserId?: string | null }) { return this.documentsMod.createDocumentRevision(input); }
|
||||
|
||||
@@ -68,6 +68,33 @@ export function buildDocumentMethods(
|
||||
return row?.latest_revision_id ?? null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns up to `limit` documents of the given types for a company that
|
||||
* are either authored by `authorAgentId` or were created at/after the
|
||||
* task's createdAt. Used by the deliverable-review gate when a task is
|
||||
* advanced to "done".
|
||||
*/
|
||||
listImportantDocsForReview(input: {
|
||||
companyId: string;
|
||||
types: readonly string[];
|
||||
authorAgentId: string | null;
|
||||
taskId: string;
|
||||
limit?: number;
|
||||
}): Array<{ id: string; type: string; title: string }> {
|
||||
const limit = input.limit ?? 10;
|
||||
const placeholders = input.types.map(() => "?").join(", ");
|
||||
return readAll<{ id: string; type: string; title: string }>(
|
||||
`select id, type, title from documents where company_id = ? and type in (${placeholders})
|
||||
and (author_agent_id = ? or created_at >= (select created_at from tasks where id = ?))
|
||||
order by created_at desc limit ?`,
|
||||
input.companyId,
|
||||
...input.types,
|
||||
input.authorAgentId ?? "",
|
||||
input.taskId,
|
||||
limit,
|
||||
);
|
||||
},
|
||||
|
||||
listDocumentRevisions(documentId: string, companyId: string) {
|
||||
return readAll<{
|
||||
id: string; company_id: string; document_id: string; revision_number: number;
|
||||
|
||||
@@ -10,6 +10,36 @@ import {
|
||||
taskPriorityOrderSql,
|
||||
} from "./helpers";
|
||||
|
||||
/**
|
||||
* Row shape returned by listTasksByStatuses — used by the
|
||||
* GET /api/companies/:companyId/issues route. Mirrors the historical
|
||||
* inline-SQL projection (no taskType column) so the HTTP response shape
|
||||
* stays stable for agent CLI clients.
|
||||
*/
|
||||
export type TaskListByStatusesRow = {
|
||||
id: string;
|
||||
companyId: string;
|
||||
projectId: string | null;
|
||||
goalId: string | null;
|
||||
parentId: string | null;
|
||||
title: string;
|
||||
description: string;
|
||||
assigneeAgentId: string | null;
|
||||
workspaceId: string | null;
|
||||
status: string;
|
||||
priority: string;
|
||||
activeRunId: string | null;
|
||||
sessionJson: string | null;
|
||||
sessionDisplayId: string | null;
|
||||
costUsd: number | null;
|
||||
requiresUserReview: boolean;
|
||||
startedAt: string | null;
|
||||
completedAt: string | null;
|
||||
cancelledAt: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export function buildTaskCrudMethods(
|
||||
getDb: () => DatabaseSync,
|
||||
readAll: ReadAll,
|
||||
@@ -160,6 +190,24 @@ export function buildTaskCrudMethods(
|
||||
);
|
||||
},
|
||||
|
||||
listTasksByStatuses(
|
||||
companyId: string,
|
||||
statuses: string[],
|
||||
): TaskListByStatusesRow[] {
|
||||
const placeholders = statuses.map(() => "?").join(", ");
|
||||
return readAll<TaskListByStatusesRow & { requiresUserReview: number | boolean }>(
|
||||
`select id, company_id as companyId, project_id as projectId, goal_id as goalId, parent_id as parentId, title, description, assignee_agent_id as assigneeAgentId, workspace_id as workspaceId, status, priority, active_run_id as activeRunId, session_json as sessionJson, session_display_id as sessionDisplayId, cost_usd as costUsd, requires_user_review as requiresUserReview, started_at as startedAt, completed_at as completedAt, cancelled_at as cancelledAt, created_at as createdAt, updated_at as updatedAt from tasks where company_id = ? and status in (${placeholders}) order by created_at desc`,
|
||||
companyId,
|
||||
...statuses,
|
||||
).map((t) => ({
|
||||
...t,
|
||||
requiresUserReview: Boolean(t.requiresUserReview),
|
||||
startedAt: t.startedAt ?? null,
|
||||
completedAt: t.completedAt ?? null,
|
||||
cancelledAt: t.cancelledAt ?? null,
|
||||
})) as TaskListByStatusesRow[];
|
||||
},
|
||||
|
||||
countActiveTasksForCompany(companyId: string): number {
|
||||
const row = getDb()
|
||||
.prepare(
|
||||
|
||||
Reference in new issue
Block a user