Skip to content

Commit c43f988

Browse files
authored
Merge pull request #174 from kodustech/fix/get-prs-perfomance
adding server filters for prs
2 parents f89b4f7 + 2fe7616 commit c43f988

File tree

4 files changed

+300
-5
lines changed

4 files changed

+300
-5
lines changed

src/core/application/use-cases/platformIntegration/codeManagement/get-prs.use-case.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class GetPRsUseCase implements IUseCase {
1717
private readonly logger: PinoLoggerService,
1818
) {}
1919

20-
public async execute(params: { teamId: string }) {
20+
public async execute(params: { teamId: string; number?: number; title: string; url?: string }) {
2121
try {
2222
const { teamId } = params;
2323
const organizationId = this.request.user.organization.uuid;
@@ -36,6 +36,9 @@ export class GetPRsUseCase implements IUseCase {
3636
const defaultFilter = {
3737
startDate: thirtyDaysAgo,
3838
endDate: today,
39+
number: params.number,
40+
title: params.title,
41+
url: params.url,
3942
};
4043

4144
const pullRequests =

src/core/infrastructure/adapters/services/github/github.service.ts

Lines changed: 282 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -773,12 +773,17 @@ export class GithubService
773773
* @param params - The parameters for fetching pull requests, including organization and team data, repository filters, and pull request filters.
774774
* @param params.organizationAndTeamData - The organization and team data containing organizationId and teamId.
775775
* @param params.repository - Optional repository filter to fetch pull requests from a specific repository.
776-
* @param params.filters - Optional filters for pull requests, including startDate, endDate, state, author, and branch.
776+
* @param params.filters - Optional filters for pull requests, including startDate, endDate, state, author, branch, number, id, title, repository, and url.
777777
* @param params.filters.startDate - The start date for filtering pull requests.
778778
* @param params.filters.endDate - The end date for filtering pull requests.
779779
* @param params.filters.state - The state of the pull requests to filter (e.g., 'open', 'closed', 'all').
780780
* @param params.filters.author - The author of the pull requests to filter.
781781
* @param params.filters.branch - The branch from which to fetch pull requests.
782+
* @param params.filters.number - The pull request number to retrieve.
783+
* @param params.filters.id - The pull request id to filter by.
784+
* @param params.filters.title - The pull request title to filter by (contains match).
785+
* @param params.filters.repository - The repository name to filter by (contains match).
786+
* @param params.filters.url - The pull request URL to filter by (contains match).
782787
* @returns A promise that resolves to an array of PullRequest objects.
783788
*/
784789
async getPullRequests(params: {
@@ -793,6 +798,11 @@ export class GithubService
793798
state?: PullRequestState;
794799
author?: string;
795800
branch?: string;
801+
number?: number;
802+
id?: number;
803+
title?: string;
804+
repository?: string;
805+
url?: string;
796806
};
797807
}): Promise<PullRequest[]> {
798808
const { organizationAndTeamData, repository, filters = {} } = params;
@@ -851,11 +861,51 @@ export class GithubService
851861
}
852862

853863
reposToProcess = [foundRepo];
864+
} else if (filters.repository) {
865+
reposToProcess = allRepositories.filter((r) =>
866+
r.name.toLowerCase().includes(filters.repository!.toLowerCase())
867+
);
868+
869+
if (reposToProcess.length === 0) {
870+
this.logger.warn({
871+
message: `No repositories found matching filter: ${filters.repository}`,
872+
context: GithubService.name,
873+
metadata: params,
874+
});
875+
876+
return [];
877+
}
854878
}
855879

856880
const octokit = await this.instanceOctokit(organizationAndTeamData);
857881
const owner = await this.getCorrectOwner(githubAuthDetail, octokit);
858882

883+
// If URL filter is provided, try to extract PR info from URL for optimization
884+
if (filters.url) {
885+
const urlInfo = this.parseGithubUrl(filters.url);
886+
if (urlInfo?.owner && urlInfo?.repo && urlInfo?.prNumber) {
887+
// Direct fetch if URL contains complete PR info
888+
const specificRepo = reposToProcess.find(r =>
889+
r.name === urlInfo.repo ||
890+
r.name === `${urlInfo.owner}/${urlInfo.repo}`
891+
);
892+
893+
if (specificRepo) {
894+
const directResult = await this.getPullRequestsByRepo({
895+
octokit,
896+
owner,
897+
repo: specificRepo.name,
898+
filters: { ...filters, number: urlInfo.prNumber },
899+
});
900+
901+
const rawPullRequests = directResult.flat();
902+
return rawPullRequests.map((rawPr) =>
903+
this.transformPullRequest(rawPr, organizationAndTeamData),
904+
);
905+
}
906+
}
907+
}
908+
859909
const promises = reposToProcess.map((r) =>
860910
this.getPullRequestsByRepo({
861911
octokit,
@@ -898,14 +948,70 @@ export class GithubService
898948
state?: PullRequestState;
899949
author?: string;
900950
branch?: string;
951+
number?: number;
952+
id?: number;
953+
title?: string;
954+
url?: string;
901955
};
902956
}): Promise<
903957
| RestEndpointMethodTypes['pulls']['list']['response']['data']
904958
| RestEndpointMethodTypes['pulls']['get']['response']['data'][]
905959
> {
906960
const { octokit, owner, repo, filters = {} } = params;
907-
const { startDate, endDate, state, author, branch } = filters;
961+
const { startDate, endDate, state, author, branch, number, id, title, url } = filters;
962+
963+
// If PR number is provided, fetch it directly for this repo
964+
if (number) {
965+
try {
966+
const { data: pr } = await octokit.rest.pulls.get({
967+
owner,
968+
repo,
969+
pull_number: number,
970+
});
971+
972+
let isValid = true;
973+
974+
if (author) {
975+
isValid =
976+
isValid &&
977+
pr.user?.login.toLowerCase() === author.toLowerCase();
978+
}
979+
980+
if (typeof id === 'number') {
981+
isValid = isValid && pr.id === id;
982+
}
983+
984+
if (title) {
985+
isValid =
986+
isValid &&
987+
pr.title.toLowerCase().includes(title.toLowerCase());
988+
}
989+
990+
if (url) {
991+
isValid =
992+
isValid &&
993+
pr.html_url.toLowerCase().includes(url.toLowerCase());
994+
}
908995

996+
return isValid ? [pr] : [];
997+
} catch (error) {
998+
const status = (error as { status?: number })?.status;
999+
if (status === 404) return [];
1000+
return [];
1001+
}
1002+
}
1003+
1004+
// Use GitHub Search API for text-based filters (more efficient)
1005+
if (title || url) {
1006+
return this.searchPullRequestsByTitle({
1007+
octokit,
1008+
owner,
1009+
repo,
1010+
filters,
1011+
});
1012+
}
1013+
1014+
// Use native API filters when possible
9091015
const pullRequests = await octokit.paginate(octokit.rest.pulls.list, {
9101016
owner,
9111017
repo,
@@ -929,10 +1035,184 @@ export class GithubService
9291035
pr.user?.login.toLowerCase() === author.toLowerCase();
9301036
}
9311037

1038+
if (typeof id === 'number') {
1039+
isValid = isValid && pr.id === id;
1040+
}
1041+
1042+
if (url) {
1043+
isValid =
1044+
isValid &&
1045+
pr.html_url.toLowerCase().includes(url.toLowerCase());
1046+
}
1047+
9321048
return isValid;
9331049
});
9341050
}
9351051

1052+
private async searchPullRequestsByTitle(params: {
1053+
octokit: Octokit;
1054+
owner: string;
1055+
repo: string;
1056+
filters: {
1057+
startDate?: Date;
1058+
endDate?: Date;
1059+
state?: PullRequestState;
1060+
author?: string;
1061+
branch?: string;
1062+
title?: string;
1063+
id?: number;
1064+
url?: string;
1065+
};
1066+
}): Promise<RestEndpointMethodTypes['pulls']['list']['response']['data']> {
1067+
const { octokit, owner, repo, filters } = params;
1068+
const { startDate, endDate, state, author, branch, title, id, url } = filters;
1069+
1070+
let query = `is:pr repo:${owner}/${repo}`;
1071+
1072+
if (title) {
1073+
query += ` ${title} in:title`;
1074+
}
1075+
1076+
if (state && state !== PullRequestState.ALL) {
1077+
const githubState = this._prStateMapReverse.get(state);
1078+
if (githubState && githubState !== 'all') {
1079+
query += ` is:${githubState}`;
1080+
}
1081+
}
1082+
1083+
if (author) {
1084+
query += ` author:${author}`;
1085+
}
1086+
1087+
if (branch) {
1088+
query += ` base:${branch}`;
1089+
}
1090+
1091+
if (startDate) {
1092+
query += ` created:>=${startDate.toISOString().split('T')[0]}`;
1093+
}
1094+
1095+
if (endDate) {
1096+
query += ` created:<=${endDate.toISOString().split('T')[0]}`;
1097+
}
1098+
1099+
try {
1100+
const searchResults = await octokit.paginate(octokit.rest.search.issuesAndPullRequests, {
1101+
q: query,
1102+
sort: 'created',
1103+
order: 'desc',
1104+
per_page: 100,
1105+
});
1106+
1107+
const pullRequests = searchResults.filter((item) => item.pull_request);
1108+
1109+
const filteredBySearch = pullRequests.filter((pr) => {
1110+
let isValid = true;
1111+
1112+
if (typeof id === 'number') {
1113+
isValid = isValid && pr.id === id;
1114+
}
1115+
1116+
return isValid;
1117+
});
1118+
1119+
const prNumbers = filteredBySearch.map(pr => pr.number);
1120+
1121+
const detailedPRs = await Promise.all(
1122+
prNumbers.map(async (prNumber) => {
1123+
try {
1124+
const { data } = await octokit.rest.pulls.get({
1125+
owner,
1126+
repo,
1127+
pull_number: prNumber,
1128+
});
1129+
return data;
1130+
} catch (error) {
1131+
return null;
1132+
}
1133+
})
1134+
);
1135+
1136+
return detailedPRs.filter((pr) => pr !== null) as unknown as RestEndpointMethodTypes['pulls']['list']['response']['data'];
1137+
} catch (error) {
1138+
this.logger.warn({
1139+
message: 'GitHub Search API failed, falling back to list API',
1140+
context: GithubService.name,
1141+
error,
1142+
metadata: { query, repo: `${owner}/${repo}` },
1143+
});
1144+
1145+
const pullRequests = await octokit.paginate(octokit.rest.pulls.list, {
1146+
owner,
1147+
repo,
1148+
state: state
1149+
? this._prStateMapReverse.get(state)
1150+
: this._prStateMapReverse.get(PullRequestState.ALL),
1151+
base: branch,
1152+
sort: 'created',
1153+
direction: 'desc',
1154+
since: startDate?.toISOString(),
1155+
until: endDate?.toISOString(),
1156+
per_page: 100,
1157+
});
1158+
1159+
return pullRequests.filter((pr) => {
1160+
let isValid = true;
1161+
1162+
if (author) {
1163+
isValid =
1164+
isValid &&
1165+
pr.user?.login.toLowerCase() === author.toLowerCase();
1166+
}
1167+
1168+
if (typeof id === 'number') {
1169+
isValid = isValid && pr.id === id;
1170+
}
1171+
1172+
if (title) {
1173+
isValid =
1174+
isValid &&
1175+
pr.title.toLowerCase().includes(title.toLowerCase());
1176+
}
1177+
1178+
if (url) {
1179+
isValid =
1180+
isValid &&
1181+
pr.html_url.toLowerCase().includes(url.toLowerCase());
1182+
}
1183+
1184+
return isValid;
1185+
});
1186+
}
1187+
}
1188+
1189+
private parseGithubUrl(url: string): { owner: string; repo: string; prNumber: number } | null {
1190+
try {
1191+
// Parse GitHub PR URLs like:
1192+
// https://github.com/owner/repo/pull/123
1193+
// https://github.com/owner/repo/pulls/123
1194+
const urlObj = new URL(url);
1195+
const pathParts = urlObj.pathname.split('/').filter(part => part);
1196+
1197+
if (pathParts.length >= 4 &&
1198+
urlObj.hostname === 'github.com' &&
1199+
(pathParts[2] === 'pull' || pathParts[2] === 'pulls')) {
1200+
1201+
const owner = pathParts[0];
1202+
const repo = pathParts[1];
1203+
const prNumber = parseInt(pathParts[3], 10);
1204+
1205+
if (!isNaN(prNumber)) {
1206+
return { owner, repo, prNumber };
1207+
}
1208+
}
1209+
} catch (error) {
1210+
// Invalid URL, ignore
1211+
}
1212+
1213+
return null;
1214+
}
1215+
9361216
async getPullRequestAuthors(params: {
9371217
organizationAndTeamData: OrganizationAndTeamData;
9381218
}): Promise<PullRequestAuthor[]> {

src/core/infrastructure/adapters/services/platformIntegration/codeManagement.service.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,9 @@ export class CodeManagementService implements ICodeManagementService {
244244
state?: PullRequestState;
245245
author?: string;
246246
branch?: string;
247+
number?: number;
248+
title?: string;
249+
url?: string;
247250
};
248251
},
249252
type?: PlatformType,
@@ -957,6 +960,8 @@ export class CodeManagementService implements ICodeManagementService {
957960
filters?: {
958961
startDate: string;
959962
endDate: string;
963+
number?: number;
964+
branch?: string;
960965
};
961966
},
962967
type?: PlatformType,

src/core/infrastructure/http/controllers/platformIntegration/codeManagement.controller.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,15 @@ export class CodeManagementController {
122122
}
123123

124124
@Get('/get-prs')
125-
public async getPRs(@Query() query: { teamId: string }) {
126-
return await this.getPRsUseCase.execute({ teamId: query.teamId });
125+
public async getPRs(
126+
@Query() query: { teamId: string; number?: number; title: string; url?: string },
127+
) {
128+
return await this.getPRsUseCase.execute({
129+
teamId: query.teamId,
130+
number: query.number,
131+
title: query.title,
132+
url: query.url,
133+
});
127134
}
128135

129136
@Get('/get-code-review-started')

0 commit comments

Comments
 (0)