diff --git a/src/resolvers/issue-resolver.ts b/src/resolvers/issue-resolver.ts index 07d6e8c..462bbd8 100644 --- a/src/resolvers/issue-resolver.ts +++ b/src/resolvers/issue-resolver.ts @@ -6,8 +6,15 @@ import { type TeamEstimateContext, } from "./team-resolver.js"; -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; +interface IssueTeamRelationProjection { + id?: unknown; + key?: unknown; +} + +interface IssueEstimateProjection { + id?: unknown; + teamId?: unknown; + team?: unknown; } function isPromiseLike(value: unknown): value is PromiseLike { @@ -22,17 +29,38 @@ async function resolveRelationValue(value: unknown): Promise { return isPromiseLike(value) ? await value : value; } +function readIssueEstimateProjection( + value: unknown, +): IssueEstimateProjection | undefined { + if (typeof value !== "object" || value === null) { + return undefined; + } + + return value as IssueEstimateProjection; +} + +function readIssueTeamRelationProjection( + value: unknown, +): IssueTeamRelationProjection | undefined { + if (typeof value !== "object" || value === null) { + return undefined; + } + + return value as IssueTeamRelationProjection; +} + function getTeamLookupFromRelation(team: unknown): string | undefined { - if (!isRecord(team)) return undefined; + const relation = readIssueTeamRelationProjection(team); + if (!relation) return undefined; - if (typeof team.id === "string") return team.id; - if (typeof team.key === "string") return team.key; + if (typeof relation.id === "string") return relation.id; + if (typeof relation.key === "string") return relation.key; return undefined; } async function getIssueTeamLookup( - node: Record, + node: IssueEstimateProjection, ): Promise { if (typeof node.teamId === "string") return node.teamId; @@ -104,8 +132,8 @@ export async function resolveIssueEstimateContext( throw notFoundError("Issue", issueIdOrIdentifier); } - const issueNode = issues.nodes[0]; - if (!isRecord(issueNode) || typeof issueNode.id !== "string") { + const issueNode = readIssueEstimateProjection(issues.nodes[0]); + if (!issueNode || typeof issueNode.id !== "string") { throw new Error( `Issue "${issueIdOrIdentifier}" is missing required team context`, ); diff --git a/src/resolvers/team-resolver.ts b/src/resolvers/team-resolver.ts index f24adc8..e23af5b 100644 --- a/src/resolvers/team-resolver.ts +++ b/src/resolvers/team-resolver.ts @@ -27,8 +27,36 @@ type TeamEstimateNode = { issueEstimationAllowZero: boolean; }; -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; +interface TeamEstimateProjection { + id: unknown; + key: unknown; + name: unknown; + issueEstimationType: unknown; + issueEstimationExtended: unknown; + issueEstimationAllowZero: unknown; +} + +function readTeamEstimateProjection( + value: unknown, +): TeamEstimateProjection | undefined { + if (typeof value !== "object" || value === null) { + return undefined; + } + + if ( + !( + "id" in value && + "key" in value && + "name" in value && + "issueEstimationType" in value && + "issueEstimationExtended" in value && + "issueEstimationAllowZero" in value + ) + ) { + return undefined; + } + + return value as TeamEstimateProjection; } function isTeamEstimationType(value: unknown): value is TeamEstimationType { @@ -45,18 +73,19 @@ function toTeamEstimateNode( node: unknown, keyOrNameOrId: string, ): TeamEstimateNode { - if (!isRecord(node)) { + const projection = readTeamEstimateProjection(node); + if (!projection) { throw new Error( `Team "${keyOrNameOrId}" is missing required estimation context`, ); } - const id = node.id; - const key = node.key; - const name = node.name; - const issueEstimationType = node.issueEstimationType; - const issueEstimationExtended = node.issueEstimationExtended; - const issueEstimationAllowZero = node.issueEstimationAllowZero; + const id = projection.id; + const key = projection.key; + const name = projection.name; + const issueEstimationType = projection.issueEstimationType; + const issueEstimationExtended = projection.issueEstimationExtended; + const issueEstimationAllowZero = projection.issueEstimationAllowZero; if ( typeof id !== "string" || diff --git a/tests/unit/resolvers/issue-resolver.test.ts b/tests/unit/resolvers/issue-resolver.test.ts index a7342fd..aad8230 100644 --- a/tests/unit/resolvers/issue-resolver.test.ts +++ b/tests/unit/resolvers/issue-resolver.test.ts @@ -7,7 +7,7 @@ import { } from "../../../src/resolvers/issue-resolver.js"; type IssueNode = { - id: string; + id?: string; teamId?: string; team?: | { @@ -232,4 +232,12 @@ describe("resolveIssueEstimateContext", () => { 'Issue "ENG-42" is missing required team context', ); }); + + it("preserves team-context error when issue projection is missing id", async () => { + const client = mockSdkClient([{ teamId }]); + + await expect(resolveIssueEstimateContext(client, "ENG-42")).rejects.toThrow( + 'Issue "ENG-42" is missing required team context', + ); + }); }); diff --git a/tests/unit/resolvers/team-resolver.test.ts b/tests/unit/resolvers/team-resolver.test.ts index f5ef4d6..2617a8b 100644 --- a/tests/unit/resolvers/team-resolver.test.ts +++ b/tests/unit/resolvers/team-resolver.test.ts @@ -174,4 +174,22 @@ describe("resolveTeamEstimateContext", () => { 'Team "NOPE" not found', ); }); + + it("preserves estimation-context error when required projection fields are missing", async () => { + const client = mockSdkClient({ + nodes: [ + { + id: "uuid-3", + key: "ENG", + name: "Engineering", + issueEstimationType: "linear", + issueEstimationExtended: false, + }, + ], + }); + + await expect(resolveTeamEstimateContext(client, "ENG")).rejects.toThrow( + 'Team "ENG" is missing required estimation context', + ); + }); });