refactor(api-server): move work-graph SQL into db domain methods
Replace six inline queryAll calls in routes-work-graph.ts with domain methods. db.entityCrud gains listGoals, getGoalForUpdate, listProjects, getProjectForUpdate. db.knowledge gains listSprints. db.company gains getDashboardSnapshot, which returns the raw agent/task/activity rows the dashboard handler aggregates. The route-level row shapes are preserved in the new GoalListRow / ProjectListRow / SprintListRow types so HTTP responses remain identical.
This commit is contained in:
1 parent
2be1bd1612
commit
efb9fdb8cc
6 files changed
+162
-60
No files matched your search
@@ -3,32 +3,6 @@ import { goalInputSchema, projectInputSchema } from "@shared/contracts";
|
||||
import { canManageGoal, canManageProject } from "../agent-permissions";
|
||||
import { requireParam, type AgentApiServer } from "./server-core";
|
||||
|
||||
type ProjectRow = {
|
||||
id: string; companyId: string; goalId: string | null; name: string;
|
||||
description: string; leadAgentId: string | null; status: string;
|
||||
targetDate: string | null; createdAt: string; updatedAt: string;
|
||||
};
|
||||
|
||||
type GoalRow = {
|
||||
id: string; companyId: string; title: string; description: string;
|
||||
parentId: string | null; ownerAgentId: string | null; status: string;
|
||||
createdAt: string; updatedAt: string;
|
||||
};
|
||||
|
||||
type SprintRow = {
|
||||
id: string; companyId: string; name: string; goal: string;
|
||||
startDate: string; endDate: string; status: string;
|
||||
retrospectiveNotes: string | null; velocityPoints: number;
|
||||
completedPoints: number; createdAt: string; updatedAt: string;
|
||||
};
|
||||
|
||||
type AgentStatusRow = { id: string; status: string };
|
||||
type TaskStatusRow = { id: string; status: string };
|
||||
type ActivityRow = {
|
||||
id: string; companyId: string; actor: string; action: string;
|
||||
entityType: string; entityId: string; detail: string | null; createdAt: string;
|
||||
};
|
||||
|
||||
export function registerWorkGraphRoutes(server: AgentApiServer) {
|
||||
const db = server.db;
|
||||
const onDomainChanged = server.onDomainChanged;
|
||||
@@ -104,11 +78,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const projectRows = db.queryAll<ProjectRow>(
|
||||
"select id, company_id as companyId, goal_id as goalId, name, description, lead_agent_id as leadAgentId, status, target_date as targetDate, created_at as createdAt, updated_at as updatedAt from projects where id = ? and company_id = ? limit 1",
|
||||
projectId, token.companyId,
|
||||
);
|
||||
const existing = projectRows[0] ?? null;
|
||||
const existing = db.getProjectForUpdate(projectId, token.companyId);
|
||||
if (!existing) {
|
||||
server.json(res, 404, { error: "Project not found" });
|
||||
return;
|
||||
@@ -195,11 +165,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const goalRows = db.queryAll<GoalRow>(
|
||||
"select id, company_id as companyId, title, description, parent_id as parentId, owner_agent_id as ownerAgentId, status, created_at as createdAt, updated_at as updatedAt from goals where id = ? and company_id = ? limit 1",
|
||||
goalId, token.companyId,
|
||||
);
|
||||
const existing = goalRows[0] ?? null;
|
||||
const existing = db.getGoalForUpdate(goalId, token.companyId);
|
||||
if (!existing) {
|
||||
server.json(res, 404, { error: "Goal not found" });
|
||||
return;
|
||||
@@ -231,10 +197,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) {
|
||||
server.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
const goals = db.queryAll<GoalRow>(
|
||||
"select id, company_id as companyId, title, description, parent_id as parentId, owner_agent_id as ownerAgentId, status, created_at as createdAt, updated_at as updatedAt from goals where company_id = ? order by created_at asc",
|
||||
params.companyId,
|
||||
);
|
||||
const goals = db.listGoals(params.companyId);
|
||||
server.json(res, 200, goals);
|
||||
});
|
||||
|
||||
@@ -243,10 +206,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) {
|
||||
server.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
const projects = db.queryAll<ProjectRow>(
|
||||
"select id, company_id as companyId, goal_id as goalId, name, description, lead_agent_id as leadAgentId, status, target_date as targetDate, created_at as createdAt, updated_at as updatedAt from projects where company_id = ? order by created_at desc",
|
||||
params.companyId,
|
||||
);
|
||||
const projects = db.listProjects(params.companyId);
|
||||
server.json(res, 200, projects);
|
||||
});
|
||||
|
||||
@@ -256,10 +216,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) {
|
||||
server.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
const sprints = db.queryAll<SprintRow>(
|
||||
"select id, company_id as companyId, name, goal, start_date as startDate, end_date as endDate, status, retrospective_notes as retrospectiveNotes, velocity_points as velocityPoints, completed_points as completedPoints, created_at as createdAt, updated_at as updatedAt from sprints where company_id = ? order by start_date desc",
|
||||
companyId,
|
||||
);
|
||||
const sprints = db.listSprints(companyId);
|
||||
server.json(res, 200, sprints);
|
||||
});
|
||||
|
||||
@@ -295,18 +252,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) {
|
||||
server.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
const agents = db.queryAll<AgentStatusRow>(
|
||||
"select id, status from agents where company_id = ?",
|
||||
companyId,
|
||||
);
|
||||
const tasks = db.queryAll<TaskStatusRow>(
|
||||
"select id, status from tasks where company_id = ?",
|
||||
companyId,
|
||||
);
|
||||
const recentActivity = db.queryAll<ActivityRow>(
|
||||
"select id, company_id as companyId, actor, action, entity_type as entityType, entity_id as entityId, detail, created_at as createdAt from activity where company_id = ? order by created_at desc limit 20",
|
||||
companyId,
|
||||
);
|
||||
const { agents, tasks, recentActivity } = db.getDashboardSnapshot(companyId);
|
||||
|
||||
const tasksByStatus: Record<string, number> = {};
|
||||
for (const t of tasks) {
|
||||
|
||||
@@ -190,6 +190,7 @@ export class AppDatabaseBase {
|
||||
getInbox(companyId: string) { return this.company.getInbox(companyId); }
|
||||
getCompanyMetrics(companyId: string, days: number = 30) { return this.company.getCompanyMetrics(companyId, days); }
|
||||
getStandupReport(companyId: string) { return this.company.getStandupReport(companyId); }
|
||||
getDashboardSnapshot(companyId: string) { return this.company.getDashboardSnapshot(companyId); }
|
||||
|
||||
// ─── Connectors ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -65,7 +65,11 @@ export class AppDatabase extends AppDatabaseBase {
|
||||
// ─── Entity CRUD ───────────────────────────────────────────────────────────
|
||||
|
||||
saveGoal(input: { id?: string | null; companyId: string; title: string; description: string; parentId?: string | null; ownerAgentId?: string | null; status: string }) { return this.entityCrud.saveGoal(input); }
|
||||
listGoals(companyId: string) { return this.entityCrud.listGoals(companyId); }
|
||||
getGoalForUpdate(goalId: string, companyId: string) { return this.entityCrud.getGoalForUpdate(goalId, companyId); }
|
||||
saveProject(input: { id?: string | null; companyId: string; goalId?: string | null; name: string; description: string; leadAgentId?: string | null; targetDate?: string | null; status: string }) { return this.entityCrud.saveProject(input); }
|
||||
listProjects(companyId: string) { return this.entityCrud.listProjects(companyId); }
|
||||
getProjectForUpdate(projectId: string, companyId: string) { return this.entityCrud.getProjectForUpdate(projectId, companyId); }
|
||||
saveSecret(input: { id?: string | null; companyId: string; name: string; description: string; value: string }) { return this.entityCrud.saveSecret(input, this.vault); }
|
||||
resolveSecretEnv(bindingText: string, companyId: string) { return this.entityCrud.resolveSecretEnv(bindingText, companyId, this.vault, listInvalidEnvBindingKeys, parseEnvBindingText); }
|
||||
deleteGoal(id: string, companyId: string) { this.entityCrud.deleteGoal(id, companyId); }
|
||||
@@ -118,6 +122,7 @@ export class AppDatabase extends AppDatabaseBase {
|
||||
listKnowledgeEntries(companyId: string) { return this.knowledgeMod.listKnowledgeEntries(companyId); }
|
||||
saveSprint(input: { id?: string | null; companyId: string; name: string; goal?: string; startDate: string; endDate: string; status?: string; retrospectiveNotes?: string; velocityPoints?: number; completedPoints?: number }) { return this.knowledgeMod.saveSprint(input); }
|
||||
deleteSprint(id: string, companyId: string) { this.knowledgeMod.deleteSprint(id, companyId); }
|
||||
listSprints(companyId: string) { return this.knowledgeMod.listSprints(companyId); }
|
||||
|
||||
// ─── Automation ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -21,6 +21,44 @@ export function buildCompanySettingsMethods(
|
||||
},
|
||||
) {
|
||||
return {
|
||||
/**
|
||||
* Returns the raw inputs for the GET /api/companies/:id/dashboard
|
||||
* endpoint: agent statuses, task statuses, and the last 20 activity rows.
|
||||
* Route code aggregates counts by status on top of this raw data.
|
||||
*/
|
||||
getDashboardSnapshot(companyId: string): {
|
||||
agents: Array<{ id: string; status: string }>;
|
||||
tasks: Array<{ id: string; status: string }>;
|
||||
recentActivity: Array<{
|
||||
id: string;
|
||||
companyId: string;
|
||||
actor: string;
|
||||
action: string;
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
detail: string | null;
|
||||
createdAt: string;
|
||||
}>;
|
||||
} {
|
||||
return {
|
||||
agents: readAll<{ id: string; status: string }>(
|
||||
"select id, status from agents where company_id = ?",
|
||||
companyId,
|
||||
),
|
||||
tasks: readAll<{ id: string; status: string }>(
|
||||
"select id, status from tasks where company_id = ?",
|
||||
companyId,
|
||||
),
|
||||
recentActivity: readAll<{
|
||||
id: string; companyId: string; actor: string; action: string;
|
||||
entityType: string; entityId: string; detail: string | null; createdAt: string;
|
||||
}>(
|
||||
"select id, company_id as companyId, actor, action, entity_type as entityType, entity_id as entityId, detail, created_at as createdAt from activity where company_id = ? order by created_at desc limit 20",
|
||||
companyId,
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
getCompanyMetrics(companyId: string, days: number = 30): CompanyMetrics {
|
||||
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
||||
const db = getDb();
|
||||
|
||||
@@ -3,6 +3,40 @@ import type { AgentRecord, ConnectorId, TaskRecord } from "@shared/types";
|
||||
import type { ReadAll, Transaction } from "./helpers";
|
||||
import { ensureId, normalizeOptionalId, nowIso } from "./helpers";
|
||||
|
||||
/**
|
||||
* Goal row shape returned by listGoals / getGoalForUpdate. Mirrors the
|
||||
* historical inline SQL projection in routes-work-graph.ts so HTTP
|
||||
* responses stay byte-stable.
|
||||
*/
|
||||
export type GoalListRow = {
|
||||
id: string;
|
||||
companyId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
parentId: string | null;
|
||||
ownerAgentId: string | null;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Project row shape returned by listProjects / getProjectForUpdate.
|
||||
* Mirrors the historical inline SQL projection in routes-work-graph.ts.
|
||||
*/
|
||||
export type ProjectListRow = {
|
||||
id: string;
|
||||
companyId: string;
|
||||
goalId: string | null;
|
||||
name: string;
|
||||
description: string;
|
||||
leadAgentId: string | null;
|
||||
status: string;
|
||||
targetDate: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export function buildEntityCrudMethods(
|
||||
getDb: () => DatabaseSync,
|
||||
readAll: ReadAll,
|
||||
@@ -37,6 +71,53 @@ export function buildEntityCrudMethods(
|
||||
return row;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns all goals for a company. Used by GET /api/companies/:id/goals.
|
||||
*/
|
||||
listGoals(companyId: string): GoalListRow[] {
|
||||
return readAll<GoalListRow>(
|
||||
"select id, company_id as companyId, title, description, parent_id as parentId, owner_agent_id as ownerAgentId, status, created_at as createdAt, updated_at as updatedAt from goals where company_id = ? order by created_at asc",
|
||||
companyId,
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the goal lookup row used by PATCH /api/goals/:goalId.
|
||||
* Returns null when the goal is missing or belongs to another company.
|
||||
*/
|
||||
getGoalForUpdate(goalId: string, companyId: string): GoalListRow | null {
|
||||
const row = getDb()
|
||||
.prepare(
|
||||
"select id, company_id as companyId, title, description, parent_id as parentId, owner_agent_id as ownerAgentId, status, created_at as createdAt, updated_at as updatedAt from goals where id = ? and company_id = ? limit 1",
|
||||
)
|
||||
.get(goalId, companyId) as GoalListRow | undefined;
|
||||
return row ?? null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns all projects for a company. Used by
|
||||
* GET /api/companies/:id/projects.
|
||||
*/
|
||||
listProjects(companyId: string): ProjectListRow[] {
|
||||
return readAll<ProjectListRow>(
|
||||
"select id, company_id as companyId, goal_id as goalId, name, description, lead_agent_id as leadAgentId, status, target_date as targetDate, created_at as createdAt, updated_at as updatedAt from projects where company_id = ? order by created_at desc",
|
||||
companyId,
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the project lookup row used by PATCH /api/projects/:projectId.
|
||||
* Returns null when the project is missing or belongs to another company.
|
||||
*/
|
||||
getProjectForUpdate(projectId: string, companyId: string): ProjectListRow | null {
|
||||
const row = getDb()
|
||||
.prepare(
|
||||
"select id, company_id as companyId, goal_id as goalId, name, description, lead_agent_id as leadAgentId, status, target_date as targetDate, created_at as createdAt, updated_at as updatedAt from projects where id = ? and company_id = ? limit 1",
|
||||
)
|
||||
.get(projectId, companyId) as ProjectListRow | undefined;
|
||||
return row ?? null;
|
||||
},
|
||||
|
||||
saveGoal(input: { id?: string | null; companyId: string; title: string; description: string; parentId?: string | null; ownerAgentId?: string | null; status: string }) {
|
||||
const id = ensureId(input.id ?? null);
|
||||
const ts = nowIso();
|
||||
|
||||
@@ -28,6 +28,26 @@ export type MeetingListRow = {
|
||||
const MEETING_LIST_COLUMNS =
|
||||
"select id, company_id as companyId, type, title, organizer_agent_id as organizerAgentId, participant_agent_ids as participantAgentIds, scheduled_at as scheduledAt, duration_minutes as durationMinutes, agenda_json as agendaJson, notes_json as notesJson, decisions_json as decisionsJson, action_items_json as actionItemsJson, status, created_at as createdAt, updated_at as updatedAt from meetings";
|
||||
|
||||
/**
|
||||
* Sprint row shape returned by listSprints — used by
|
||||
* GET /api/companies/:companyId/sprints. Mirrors the historical inline
|
||||
* SQL projection.
|
||||
*/
|
||||
export type SprintListRow = {
|
||||
id: string;
|
||||
companyId: string;
|
||||
name: string;
|
||||
goal: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
status: string;
|
||||
retrospectiveNotes: string | null;
|
||||
velocityPoints: number;
|
||||
completedPoints: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Knowledge entry row shape returned by listKnowledgeEntries — used by
|
||||
* GET /api/companies/:companyId/knowledge. Mirrors the historical inline
|
||||
@@ -151,5 +171,16 @@ export function buildKnowledgeMethods(
|
||||
deleteSprint(id: string, companyId: string) {
|
||||
getDb().prepare("delete from sprints where id = ? and company_id = ?").run(id, companyId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns all sprints for a company, ordered by start_date desc.
|
||||
* Used by GET /api/companies/:companyId/sprints.
|
||||
*/
|
||||
listSprints(companyId: string): SprintListRow[] {
|
||||
return readAll<SprintListRow>(
|
||||
"select id, company_id as companyId, name, goal, start_date as startDate, end_date as endDate, status, retrospective_notes as retrospectiveNotes, velocity_points as velocityPoints, completed_points as completedPoints, created_at as createdAt, updated_at as updatedAt from sprints where company_id = ? order by start_date desc",
|
||||
companyId,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user