refactor(api): extract computation graph detail procedures
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import { z } from "zod";
|
||||
import { controllerProcedure } from "../trpc.js";
|
||||
|
||||
type GraphNodeLike = {
|
||||
id: string;
|
||||
domain: string;
|
||||
};
|
||||
|
||||
type GraphLinkLike = {
|
||||
source: string;
|
||||
target: string;
|
||||
};
|
||||
|
||||
type ResourceGraphSnapshot = {
|
||||
nodes: GraphNodeLike[];
|
||||
links: GraphLinkLike[];
|
||||
meta: {
|
||||
resourceEid: string;
|
||||
resourceName: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
type ProjectGraphSnapshot = {
|
||||
nodes: GraphNodeLike[];
|
||||
links: GraphLinkLike[];
|
||||
meta: {
|
||||
projectCode: string;
|
||||
projectName: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
function filterGraphData<
|
||||
TNode extends GraphNodeLike,
|
||||
TLink extends GraphLinkLike,
|
||||
>(input: {
|
||||
nodes: TNode[];
|
||||
links: TLink[];
|
||||
domain?: string;
|
||||
includeLinks?: boolean;
|
||||
}) {
|
||||
const requestedDomain = input.domain?.trim().toUpperCase();
|
||||
const nodes = requestedDomain
|
||||
? input.nodes.filter((node) => node.domain === requestedDomain)
|
||||
: input.nodes;
|
||||
const selectedNodeIds = new Set(nodes.map((node) => node.id));
|
||||
const links = input.includeLinks
|
||||
? input.links.filter((link) => selectedNodeIds.has(link.source) && selectedNodeIds.has(link.target))
|
||||
: [];
|
||||
|
||||
return {
|
||||
requestedDomain: requestedDomain ?? null,
|
||||
includedLinks: input.includeLinks ?? false,
|
||||
selectedNodeCount: nodes.length,
|
||||
selectedLinkCount: links.length,
|
||||
nodes,
|
||||
...(input.includeLinks ? { links } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function getAvailableDomains<TNode extends { domain: string }>(nodes: TNode[]) {
|
||||
return [...new Set(nodes.map((node) => node.domain))];
|
||||
}
|
||||
|
||||
function formatResourceGraphDetail(input: {
|
||||
resourceId: string;
|
||||
graph: ResourceGraphSnapshot;
|
||||
domain?: string;
|
||||
includeLinks?: boolean;
|
||||
}) {
|
||||
return {
|
||||
resource: {
|
||||
id: input.resourceId,
|
||||
eid: input.graph.meta.resourceEid,
|
||||
displayName: input.graph.meta.resourceName,
|
||||
},
|
||||
availableDomains: getAvailableDomains(input.graph.nodes),
|
||||
totalNodeCount: input.graph.nodes.length,
|
||||
totalLinkCount: input.graph.links.length,
|
||||
...filterGraphData({
|
||||
nodes: input.graph.nodes,
|
||||
links: input.graph.links,
|
||||
...(input.domain ? { domain: input.domain } : {}),
|
||||
...(input.includeLinks !== undefined ? { includeLinks: input.includeLinks } : {}),
|
||||
}),
|
||||
meta: input.graph.meta,
|
||||
};
|
||||
}
|
||||
|
||||
function formatProjectGraphDetail(input: {
|
||||
projectId: string;
|
||||
graph: ProjectGraphSnapshot;
|
||||
domain?: string;
|
||||
includeLinks?: boolean;
|
||||
}) {
|
||||
return {
|
||||
project: {
|
||||
id: input.projectId,
|
||||
shortCode: input.graph.meta.projectCode,
|
||||
name: input.graph.meta.projectName,
|
||||
},
|
||||
availableDomains: getAvailableDomains(input.graph.nodes),
|
||||
totalNodeCount: input.graph.nodes.length,
|
||||
totalLinkCount: input.graph.links.length,
|
||||
...filterGraphData({
|
||||
nodes: input.graph.nodes,
|
||||
links: input.graph.links,
|
||||
...(input.domain ? { domain: input.domain } : {}),
|
||||
...(input.includeLinks !== undefined ? { includeLinks: input.includeLinks } : {}),
|
||||
}),
|
||||
meta: input.graph.meta,
|
||||
};
|
||||
}
|
||||
|
||||
export function createComputationGraphDetailProcedures(input: {
|
||||
resourceGraphInputSchema: z.AnyZodObject;
|
||||
projectGraphInputSchema: z.AnyZodObject;
|
||||
readResourceGraphSnapshot: (
|
||||
ctx: any,
|
||||
input: any,
|
||||
) => Promise<ResourceGraphSnapshot>;
|
||||
readProjectGraphSnapshot: (
|
||||
ctx: any,
|
||||
input: any,
|
||||
) => Promise<ProjectGraphSnapshot>;
|
||||
}) {
|
||||
const resourceGraphDetailInputSchema = input.resourceGraphInputSchema.extend({
|
||||
domain: z.string().trim().min(1).optional(),
|
||||
includeLinks: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const projectGraphDetailInputSchema = input.projectGraphInputSchema.extend({
|
||||
domain: z.string().trim().min(1).optional(),
|
||||
includeLinks: z.boolean().optional(),
|
||||
});
|
||||
|
||||
return {
|
||||
getResourceDataDetail: controllerProcedure
|
||||
.input(resourceGraphDetailInputSchema)
|
||||
.query(async ({ ctx, input: procedureInput }) => {
|
||||
const graph = await input.readResourceGraphSnapshot(ctx, procedureInput);
|
||||
return formatResourceGraphDetail({
|
||||
resourceId: procedureInput.resourceId,
|
||||
graph,
|
||||
...(typeof procedureInput.domain === "string" ? { domain: procedureInput.domain } : {}),
|
||||
...(typeof procedureInput.includeLinks === "boolean"
|
||||
? { includeLinks: procedureInput.includeLinks }
|
||||
: {}),
|
||||
});
|
||||
}),
|
||||
|
||||
getProjectDataDetail: controllerProcedure
|
||||
.input(projectGraphDetailInputSchema)
|
||||
.query(async ({ ctx, input: procedureInput }) => {
|
||||
const graph = await input.readProjectGraphSnapshot(ctx, procedureInput);
|
||||
return formatProjectGraphDetail({
|
||||
projectId: procedureInput.projectId,
|
||||
graph,
|
||||
...(typeof procedureInput.domain === "string" ? { domain: procedureInput.domain } : {}),
|
||||
...(typeof procedureInput.includeLinks === "boolean"
|
||||
? { includeLinks: procedureInput.includeLinks }
|
||||
: {}),
|
||||
});
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
countEffectiveWorkingDays,
|
||||
loadResourceDailyAvailabilityContexts,
|
||||
} from "../lib/resource-capacity.js";
|
||||
import { createComputationGraphDetailProcedures } from "./computation-graph-detail.js";
|
||||
|
||||
// ─── Graph Types (mirrored from client for API response) ────────────────────
|
||||
|
||||
@@ -86,60 +87,26 @@ function sumAvailabilityHoursForDates(
|
||||
return dates.reduce((sum, date) => sum + getAvailabilityHoursForDate(availability, date), 0);
|
||||
}
|
||||
|
||||
function filterGraphData<
|
||||
TNode extends { id: string; domain: string },
|
||||
TLink extends { source: string; target: string },
|
||||
>(input: {
|
||||
nodes: TNode[];
|
||||
links: TLink[];
|
||||
domain?: string;
|
||||
includeLinks?: boolean;
|
||||
}) {
|
||||
const requestedDomain = input.domain?.trim().toUpperCase();
|
||||
const nodes = requestedDomain
|
||||
? input.nodes.filter((node) => node.domain === requestedDomain)
|
||||
: input.nodes;
|
||||
const selectedNodeIds = new Set(nodes.map((node) => node.id));
|
||||
const links = input.includeLinks
|
||||
? input.links.filter((link) => selectedNodeIds.has(link.source) && selectedNodeIds.has(link.target))
|
||||
: [];
|
||||
|
||||
return {
|
||||
requestedDomain: requestedDomain ?? null,
|
||||
includedLinks: input.includeLinks ?? false,
|
||||
selectedNodeCount: nodes.length,
|
||||
selectedLinkCount: links.length,
|
||||
nodes,
|
||||
...(input.includeLinks ? { links } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function getAvailableDomains(nodes: Array<{ domain: Domain }>): Domain[] {
|
||||
return [...new Set(nodes.map((node) => node.domain))];
|
||||
}
|
||||
|
||||
const resourceGraphInputSchema = z.object({
|
||||
resourceId: z.string(),
|
||||
month: z.string().regex(/^\d{4}-\d{2}$/),
|
||||
});
|
||||
|
||||
const resourceGraphDetailInputSchema = resourceGraphInputSchema.extend({
|
||||
domain: z.string().trim().min(1).optional(),
|
||||
includeLinks: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const projectGraphInputSchema = z.object({
|
||||
projectId: z.string(),
|
||||
});
|
||||
|
||||
const projectGraphDetailInputSchema = projectGraphInputSchema.extend({
|
||||
domain: z.string().trim().min(1).optional(),
|
||||
includeLinks: z.boolean().optional(),
|
||||
const computationGraphDetailProcedures = createComputationGraphDetailProcedures({
|
||||
resourceGraphInputSchema,
|
||||
projectGraphInputSchema,
|
||||
readResourceGraphSnapshot,
|
||||
readProjectGraphSnapshot,
|
||||
});
|
||||
|
||||
// ─── Router ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const computationGraphRouter = createTRPCRouter({
|
||||
...computationGraphDetailProcedures,
|
||||
/**
|
||||
* Resource View: SAH, Allocation, Rules, Chargeability, Budget
|
||||
* for a single resource in a single month.
|
||||
@@ -148,36 +115,12 @@ export const computationGraphRouter = createTRPCRouter({
|
||||
.input(resourceGraphInputSchema)
|
||||
.query(({ ctx, input }) => readResourceGraphSnapshot(ctx, input)),
|
||||
|
||||
getResourceDataDetail: controllerProcedure
|
||||
.input(resourceGraphDetailInputSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const graph = await readResourceGraphSnapshot(ctx, input);
|
||||
return formatResourceGraphDetail({
|
||||
resourceId: input.resourceId,
|
||||
graph,
|
||||
...(input.domain ? { domain: input.domain } : {}),
|
||||
...(input.includeLinks !== undefined ? { includeLinks: input.includeLinks } : {}),
|
||||
});
|
||||
}),
|
||||
|
||||
/**
|
||||
* Project View: Estimate, Commercial, Experience, Effort, Spread, Budget
|
||||
*/
|
||||
getProjectData: controllerProcedure
|
||||
.input(projectGraphInputSchema)
|
||||
.query(({ ctx, input }) => readProjectGraphSnapshot(ctx, input)),
|
||||
|
||||
getProjectDataDetail: controllerProcedure
|
||||
.input(projectGraphDetailInputSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const graph = await readProjectGraphSnapshot(ctx, input);
|
||||
return formatProjectGraphDetail({
|
||||
projectId: input.projectId,
|
||||
graph,
|
||||
...(input.domain ? { domain: input.domain } : {}),
|
||||
...(input.includeLinks !== undefined ? { includeLinks: input.includeLinks } : {}),
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
async function readResourceGraphSnapshot(
|
||||
@@ -1118,53 +1061,3 @@ async function readProjectGraphSnapshot(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function formatResourceGraphDetail(input: {
|
||||
resourceId: string;
|
||||
graph: Awaited<ReturnType<typeof readResourceGraphSnapshot>>;
|
||||
domain?: string;
|
||||
includeLinks?: boolean;
|
||||
}) {
|
||||
return {
|
||||
resource: {
|
||||
id: input.resourceId,
|
||||
eid: input.graph.meta.resourceEid,
|
||||
displayName: input.graph.meta.resourceName,
|
||||
},
|
||||
availableDomains: getAvailableDomains(input.graph.nodes),
|
||||
totalNodeCount: input.graph.nodes.length,
|
||||
totalLinkCount: input.graph.links.length,
|
||||
...filterGraphData({
|
||||
nodes: input.graph.nodes,
|
||||
links: input.graph.links,
|
||||
...(input.domain ? { domain: input.domain } : {}),
|
||||
...(input.includeLinks !== undefined ? { includeLinks: input.includeLinks } : {}),
|
||||
}),
|
||||
meta: input.graph.meta,
|
||||
};
|
||||
}
|
||||
|
||||
function formatProjectGraphDetail(input: {
|
||||
projectId: string;
|
||||
graph: Awaited<ReturnType<typeof readProjectGraphSnapshot>>;
|
||||
domain?: string;
|
||||
includeLinks?: boolean;
|
||||
}) {
|
||||
return {
|
||||
project: {
|
||||
id: input.projectId,
|
||||
shortCode: input.graph.meta.projectCode,
|
||||
name: input.graph.meta.projectName,
|
||||
},
|
||||
availableDomains: getAvailableDomains(input.graph.nodes),
|
||||
totalNodeCount: input.graph.nodes.length,
|
||||
totalLinkCount: input.graph.links.length,
|
||||
...filterGraphData({
|
||||
nodes: input.graph.nodes,
|
||||
links: input.graph.links,
|
||||
...(input.domain ? { domain: input.domain } : {}),
|
||||
...(input.includeLinks !== undefined ? { includeLinks: input.includeLinks } : {}),
|
||||
}),
|
||||
meta: input.graph.meta,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user