Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 36 additions & 8 deletions src/resolvers/issue-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ import {
type TeamEstimateContext,
} from "./team-resolver.js";

function isRecord(value: unknown): value is Record<string, unknown> {
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<unknown> {
Expand All @@ -22,17 +29,38 @@ async function resolveRelationValue(value: unknown): Promise<unknown> {
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<string, unknown>,
node: IssueEstimateProjection,
): Promise<string | undefined> {
if (typeof node.teamId === "string") return node.teamId;

Expand Down Expand Up @@ -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`,
);
Expand Down
47 changes: 38 additions & 9 deletions src/resolvers/team-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,36 @@ type TeamEstimateNode = {
issueEstimationAllowZero: boolean;
};

function isRecord(value: unknown): value is Record<string, unknown> {
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 {
Expand All @@ -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" ||
Expand Down
10 changes: 9 additions & 1 deletion tests/unit/resolvers/issue-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "../../../src/resolvers/issue-resolver.js";

type IssueNode = {
id: string;
id?: string;
teamId?: string;
team?:
| {
Expand Down Expand Up @@ -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',
);
});
});
18 changes: 18 additions & 0 deletions tests/unit/resolvers/team-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
);
});
});