Skip to content

Commit

Permalink
feat: sync code review request
Browse files Browse the repository at this point in the history
  • Loading branch information
waltergalvao committed Dec 17, 2024
1 parent 0c0e0e2 commit b7f9e4c
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- CreateTable
CREATE TABLE "CodeReviewRequest" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
"pullRequestId" INTEGER NOT NULL,
"reviewerId" INTEGER NOT NULL,
"workspaceId" INTEGER NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "CodeReviewRequest_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "CodeReviewRequest_workspaceId_idx" ON "CodeReviewRequest"("workspaceId");

-- CreateIndex
CREATE INDEX "CodeReviewRequest_reviewerId_idx" ON "CodeReviewRequest"("reviewerId");

-- CreateIndex
CREATE INDEX "CodeReviewRequest_pullRequestId_idx" ON "CodeReviewRequest"("pullRequestId");

-- CreateIndex
CREATE UNIQUE INDEX "CodeReviewRequest_pullRequestId_reviewerId_key" ON "CodeReviewRequest"("pullRequestId", "reviewerId");

-- RLS
ALTER TABLE "CodeReviewRequest" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "CodeReviewRequest" FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON "CodeReviewRequest" USING ("workspaceId" = current_setting('app.current_workspace_id', TRUE)::int);
CREATE POLICY bypass_rls_policy ON "CodeReviewRequest" USING (current_setting('app.bypass_rls', TRUE)::text = 'on');
40 changes: 33 additions & 7 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,11 @@ model GitProfile {
userId Int?
// A single user might have multiple git profiles, with multiple personal workspaces.
workspace Workspace?
pullRequests PullRequest[]
teamMemberships TeamMember[]
codeReviews CodeReview[]
workspace Workspace?
pullRequests PullRequest[]
teamMemberships TeamMember[]
codeReviews CodeReview[]
CodeReviewRequest CodeReviewRequest[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
Expand Down Expand Up @@ -154,6 +155,7 @@ model Workspace {
teams Team[]
Automation Automation[]
CodeReview CodeReview[]
CodeReviewRequest CodeReviewRequest[]
PullRequestTracking PullRequestTracking[]
PullRequest PullRequest[]
TeamMember TeamMember[]
Expand Down Expand Up @@ -294,9 +296,10 @@ model PullRequest {
repository Repository @relation(fields: [repositoryId], references: [id], onDelete: Cascade)
repositoryId Int
author GitProfile @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId Int
codeReviews CodeReview[]
author GitProfile @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId Int
codeReviews CodeReview[]
codeReviewRequests CodeReviewRequest[]
tracking PullRequestTracking?
Expand Down Expand Up @@ -368,6 +371,29 @@ model CodeReview {
@@index([pullRequestId])
}

model CodeReviewRequest {
id Int @id @default(autoincrement())
createdAt DateTime
deletedAt DateTime?
pullRequest PullRequest @relation(fields: [pullRequestId], references: [id], onDelete: Cascade)
pullRequestId Int
reviewer GitProfile @relation(fields: [reviewerId], references: [id], onDelete: Cascade)
reviewerId Int
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
workspaceId Int
updatedAt DateTime @default(now()) @updatedAt
@@unique([pullRequestId, reviewerId])
@@index([workspaceId])
@@index([reviewerId])
@@index([pullRequestId])
}

model Team {
id Int @id @default(autoincrement())
name String
Expand Down
135 changes: 130 additions & 5 deletions apps/api/src/app/github/services/github-code-review.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ interface ReviewData {
createdAt: string;
}

interface ReviewRequestData {
createdAt: Date;
deletedAt: Date | null;
author: Author;
}

export const syncCodeReviews = async (
gitInstallationId: number,
pullRequestId: string
Expand Down Expand Up @@ -84,11 +90,10 @@ export const syncCodeReviews = async (
return;
}

const { reviews, firstReviewerRequestedAt } = await fetchPullRequestReviews(
gitInstallationId,
pullRequestId
);
const { reviews, firstReviewerRequestedAt, reviewRequests } =
await fetchPullRequestReviews(gitInstallationId, pullRequestId);

await upsertCodeReviewRequests(pullRequest, reviewRequests);
await upsertCodeReviews(pullRequest, reviews);
await updatePullRequestTracking(
pullRequest,
Expand All @@ -102,6 +107,7 @@ const fetchPullRequestReviews = async (
pullRequestId: string
): Promise<{
reviews: ReviewData[];
reviewRequests: ReviewRequestData[];
firstReviewerRequestedAt: string | null;
}> => {
const fireGraphQLRequest = getInstallationGraphQLOctoKit(installationId);
Expand All @@ -110,12 +116,34 @@ const fetchPullRequestReviews = async (
let cursor: string | null = null;
const isFirstRequest = cursor === null;
let firstReviewerRequestedAt = null;
let reviewRequests: ReviewRequestData[] = [];

const timelineItemsFragment = `timelineItems(itemTypes: [REVIEW_REQUESTED_EVENT], last: ${GITHUB_MAX_PAGE_LIMIT}) {
const timelineItemsFragment = `timelineItems(itemTypes: [REVIEW_REQUESTED_EVENT, REVIEW_REQUEST_REMOVED_EVENT], last: ${GITHUB_MAX_PAGE_LIMIT}) {
nodes {
__typename
... on ReviewRequestedEvent {
createdAt
requestedReviewer {
__typename
... on User {
id
login
name
avatarUrl
}
}
}
... on ReviewRequestRemovedEvent {
createdAt
requestedReviewer {
__typename
... on User {
id
login
name
avatarUrl
}
}
}
}
}`;
Expand Down Expand Up @@ -177,6 +205,12 @@ const fetchPullRequestReviews = async (
if (isFirstRequest) {
firstReviewerRequestedAt =
response.node.timelineItems.nodes[0]?.createdAt || null;

reviewRequests = getReviewRequests(
response.node.timelineItems.nodes.filter(
(node) => node.requestedReviewer.__typename === "User"
)
);
}

// Aggregate reviews by author
Expand Down Expand Up @@ -216,6 +250,7 @@ const fetchPullRequestReviews = async (

return {
reviews: Object.values(reviews),
reviewRequests,
firstReviewerRequestedAt,
};
};
Expand All @@ -238,6 +273,96 @@ const getCodeReviewState = (state: string) => {
return CodeReviewState.COMMENTED;
};

const getReviewRequests = (nodes: any[]): ReviewRequestData[] => {
const requestedReviewers = new Map<string, ReviewRequestData>();

// Sort by createdAt
nodes.sort(
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
);

nodes.forEach((node) => {
if (
node.__typename === "ReviewRequestedEvent" &&
node.requestedReviewer.__typename === "User"
) {
const reviewerId = node.requestedReviewer.id;

requestedReviewers.set(reviewerId, {
createdAt: new Date(node.createdAt),
deletedAt: null,
author: {
id: reviewerId,
login: node.requestedReviewer.login,
name: node.requestedReviewer.name,
avatarUrl: node.requestedReviewer.avatarUrl,
},
});
}

if (
node.__typename === "ReviewRequestRemovedEvent" &&
node.requestedReviewer.__typename === "User"
) {
const reviewerId = node.requestedReviewer.id;

if (requestedReviewers.has(reviewerId)) {
const reviewRequest = requestedReviewers.get(reviewerId);

if (reviewRequest) {
reviewRequest.deletedAt = new Date(node.createdAt);
reviewRequest.author = {
id: reviewerId,
login: node.requestedReviewer.login,
name: node.requestedReviewer.name,
avatarUrl: node.requestedReviewer.avatarUrl,
};
}
}
}
});

return Array.from(requestedReviewers.values());
};

const upsertCodeReviewRequests = async (
pullRequest: PullRequest,
reviewRequests: ReviewRequestData[]
) => {
logger.debug("upsertCodeReviewRequests", { pullRequest, reviewRequests });

return parallel(10, reviewRequests, async (reviewRequest) => {
if (!reviewRequest.author?.id) {
logger.info("syncCodeReviews: Skipping unknown author", {
reviewRequest,
});

return;
}

const gitProfile = await upsertGitProfile(reviewRequest.author);

const data = {
workspaceId: pullRequest.workspaceId,
reviewerId: gitProfile.id,
pullRequestId: pullRequest.id,
createdAt: new Date(),
deletedAt: reviewRequest.deletedAt,
};

return await getPrisma(pullRequest.workspaceId).codeReviewRequest.upsert({
where: {
pullRequestId_reviewerId: {
reviewerId: gitProfile.id,
pullRequestId: pullRequest.id,
},
},
create: data,
update: data,
});
});
};

const upsertCodeReviews = async (
pullRequest: PullRequest,
reviews: ReviewData[]
Expand Down

0 comments on commit b7f9e4c

Please sign in to comment.