Files
kandev/apps/web/lib/plugins/registry-normalization.ts

165 lines
5.1 KiB
TypeScript

import type {
ReviewItemSummary,
ReviewTaskAssociation,
ReviewTaskPipelineState,
ReviewTaskStatus,
} from "./types";
export function taskActionKey(pluginId: string, actionId: string): string {
return `${pluginId}:${actionId}`;
}
export function normalizeReviewItems(
providerId: string,
items: readonly ReviewItemSummary[],
): readonly ReviewItemSummary[] {
return items.flatMap((item) => {
if (
item.providerId !== providerId ||
!item.reviewKey ||
!item.title ||
!item.url ||
!item.connectionScope ||
!item.repositoryId ||
normalizeChangeRequestNumber(item.changeRequestNumber) === undefined ||
!item.state
) {
return [];
}
const statusBadge = item.statusBadge?.label
? {
label: item.statusBadge.label,
...(item.statusBadge.tone ? { tone: item.statusBadge.tone } : {}),
}
: undefined;
const taskStatus = normalizeReviewTaskStatus(item.taskStatus);
const changeRequestNumber = normalizeChangeRequestNumber(item.changeRequestNumber)!;
return [
{
providerId,
reviewKey: item.reviewKey,
title: item.title,
url: item.url,
connectionScope: item.connectionScope,
repositoryId: item.repositoryId,
changeRequestNumber,
state: item.state,
...(statusBadge ? { statusBadge } : {}),
...(taskStatus ? { taskStatus } : {}),
},
];
});
}
export function normalizeReviewAssociations(
providerId: string,
associations: readonly ReviewTaskAssociation[],
): readonly ReviewTaskAssociation[] {
const seen = new Set<string>();
return associations.flatMap((association) => {
if (!association.taskId || !association.reviewKey) return [];
const connectionScope = association.connectionScope?.trim();
const repositoryId = association.repositoryId?.trim();
const changeRequestNumber = normalizeChangeRequestNumber(association.changeRequestNumber);
const hasImmutableIdentity =
Boolean(connectionScope) && Boolean(repositoryId) && changeRequestNumber !== undefined;
if (!hasImmutableIdentity) return [];
const identity = `${connectionScope}\u0000${repositoryId}\u0000${String(changeRequestNumber)}`;
const key = `${association.taskId}\u0000${identity}`;
if (seen.has(key)) return [];
seen.add(key);
return [
{
providerId,
taskId: association.taskId,
reviewKey: association.reviewKey,
connectionScope: connectionScope!,
repositoryId: repositoryId!,
changeRequestNumber: changeRequestNumber!,
},
];
});
}
function normalizeChangeRequestNumber(value: string | number | undefined) {
if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
if (typeof value !== "string") return undefined;
return value.trim() || undefined;
}
const REVIEW_TASK_STATES = new Set<ReviewTaskStatus["state"]>([
"open",
"merged",
"closed",
"draft",
]);
const REVIEW_PIPELINE_STATES = new Set<ReviewTaskPipelineState>([
"success",
"failure",
"pending",
"neutral",
]);
function normalizeReviewTaskStatus(status: ReviewTaskStatus | undefined): ReviewTaskStatus | null {
if (
!status ||
(typeof status.number !== "string" && typeof status.number !== "number") ||
!REVIEW_TASK_STATES.has(status.state) ||
!REVIEW_PIPELINE_STATES.has(status.pipelineState) ||
!Array.isArray(status.checks)
) {
return null;
}
const checks = status.checks.flatMap((check) => {
if (!check.id || !check.label || !REVIEW_PIPELINE_STATES.has(check.state)) return [];
return [
{
id: check.id,
label: check.label,
state: check.state,
...(check.detail ? { detail: check.detail } : {}),
...(check.url ? { url: check.url } : {}),
},
];
});
const review = normalizeReviewTaskReview(status.review);
return {
number: status.number,
state: status.state,
pipelineState: status.pipelineState,
checks,
...(review ? { review } : {}),
...(typeof status.unresolvedComments === "number" && status.unresolvedComments >= 0
? { unresolvedComments: status.unresolvedComments }
: {}),
...(status.loading === true ? { loading: true } : {}),
...(status.error ? { error: status.error } : {}),
...(typeof status.updatedAt === "number" ? { updatedAt: status.updatedAt } : {}),
};
}
function normalizeReviewTaskReview(review: ReviewTaskStatus["review"]) {
if (
!review ||
!["approved", "changes_requested", "pending"].includes(review.state) ||
!Number.isFinite(review.approved) ||
review.approved < 0
) {
return null;
}
return {
state: review.state,
approved: review.approved,
...(Number.isFinite(review.required) && (review.required ?? -1) >= 0
? { required: review.required }
: {}),
...(Number.isFinite(review.requested) && (review.requested ?? -1) >= 0
? { requested: review.requested }
: {}),
};
}
export function pluginSlotOrderingId(pluginId: string, slot: string, ordinal: number): string {
return `plugin:${encodeURIComponent(pluginId)}:${encodeURIComponent(slot)}:${ordinal}`;
}