Skip to content

Commit 4202154

Browse files
authored
Merge pull request #135 from topcoder-platform/performance
Performance indices and also updates for handling TG Task Submissions of URL only
2 parents 0ec7859 + 0ed16ca commit 4202154

File tree

7 files changed

+180
-6
lines changed

7 files changed

+180
-6
lines changed

prisma/migrations/20250218000100_add_submission_is_file_submission/migration.sql renamed to prisma/migrations/20251031000100_add_submission_is_file_submission/migration.sql

File renamed without changes.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-- CreateIndex
2+
CREATE INDEX "appeal_response_appeal_resource_idx" ON "appealResponse"("appealId", "resourceId");
3+
4+
-- CreateIndex
5+
CREATE INDEX "review_resource_status_phase_idx" ON "review"("resourceId", "status", "phaseId");
6+
7+
-- Clean up orphaned reviewSummations before enforcing FK
8+
UPDATE "reviewSummation" rs
9+
SET "scorecardId" = NULL
10+
WHERE "scorecardId" IS NOT NULL
11+
AND NOT EXISTS (
12+
SELECT 1
13+
FROM "scorecard" sc
14+
WHERE sc."id" = rs."scorecardId"
15+
);
16+
17+
-- AddForeignKey
18+
ALTER TABLE "reviewSummation" ADD CONSTRAINT "reviewSummation_scorecardId_fkey" FOREIGN KEY ("scorecardId") REFERENCES "scorecard"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- CreateIndex
2+
CREATE INDEX "appeal_comment_resource_idx" ON "appeal"("reviewItemCommentId", "resourceId");
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-- Add composite indexes to improve My Reviews query performance
2+
3+
CREATE INDEX IF NOT EXISTS "review_resource_status_phase_idx"
4+
ON "reviews"."review"("resourceId", "status", "phaseId");
5+
6+
CREATE INDEX IF NOT EXISTS "appeal_response_appeal_resource_idx"
7+
ON "reviews"."appealResponse"("appealId", "resourceId");
8+
9+
CREATE INDEX IF NOT EXISTS "appeal_comment_resource_idx"
10+
ON "reviews"."appeal"("reviewItemCommentId", "resourceId");

prisma/schema.prisma

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ model review {
188188
@@index([status]) // Index for filtering by review status
189189
@@index([status, phaseId])
190190
@@index([resourceId, status])
191+
@@index([resourceId, status, phaseId], map: "review_resource_status_phase_idx") // Supports incomplete review lookups that also consider phase ordering
191192
@@unique([resourceId, submissionId, scorecardId])
192193
}
193194

@@ -284,6 +285,7 @@ model appeal {
284285
@@index([resourceId]) // Index for resource ID
285286
@@index([id]) // Index for direct ID lookups
286287
@@index([reviewItemCommentId]) // Index for joining with reviewItemComment table
288+
@@index([reviewItemCommentId, resourceId], map: "appeal_comment_resource_idx") // Supports filtered appeal lookups by comment and resource
287289
}
288290

289291
model appealResponse {
@@ -303,6 +305,7 @@ model appealResponse {
303305
@@index([id]) // Index for direct ID lookups
304306
@@index([appealId]) // Index for joining with appeal table
305307
@@index([resourceId]) // Index for filtering by resource (responder)
308+
@@index([appealId, resourceId], map: "appeal_response_appeal_resource_idx") // Supports lookups for pending appeal responses by appeal and resource
306309
}
307310

308311
model challengeResult {

src/api/my-review/myReview.service.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ export class MyReviewService {
157157
}
158158

159159
const baseJoins: Prisma.Sql[] = [];
160+
const countJoins: Prisma.Sql[] = [];
161+
const countExtras: Prisma.Sql[] = [];
162+
const rowExtras: Prisma.Sql[] = [];
160163

161164
if (!adminUser) {
162165
if (!normalizedUserId) {
@@ -175,7 +178,17 @@ export class MyReviewService {
175178
`,
176179
);
177180

178-
whereFragments.push(Prisma.sql`r."challengeId" IS NOT NULL`);
181+
rowExtras.push(Prisma.sql`r."challengeId" IS NOT NULL`);
182+
countExtras.push(
183+
Prisma.sql`
184+
EXISTS (
185+
SELECT 1
186+
FROM resources."Resource" r
187+
WHERE r."challengeId" = c.id
188+
AND r."memberId" = ${normalizedUserId}
189+
)
190+
`,
191+
);
179192
} else {
180193
baseJoins.push(
181194
Prisma.sql`
@@ -193,6 +206,11 @@ export class MyReviewService {
193206
LEFT JOIN challenges."ChallengeType" ct ON ct.id = c."typeId"
194207
`,
195208
);
209+
countJoins.push(
210+
Prisma.sql`
211+
LEFT JOIN challenges."ChallengeType" ct ON ct.id = c."typeId"
212+
`,
213+
);
196214

197215
const metricJoins: Prisma.Sql[] = [
198216
Prisma.sql`
@@ -318,7 +336,10 @@ export class MyReviewService {
318336
[...baseJoins, ...metricJoins],
319337
Prisma.sql``,
320338
);
321-
const countJoinClause = joinSqlFragments(baseJoins, Prisma.sql``);
339+
const countJoinClause = joinSqlFragments(countJoins, Prisma.sql``);
340+
341+
const rowWhereFragments = [...whereFragments, ...rowExtras];
342+
const countWhereFragments = [...whereFragments, ...countExtras];
322343

323344
if (challengeTypeId) {
324345
whereFragments.push(Prisma.sql`c."typeId" = ${challengeTypeId}`);
@@ -340,7 +361,14 @@ export class MyReviewService {
340361
);
341362
}
342363

343-
const whereClause = joinSqlFragments(whereFragments, Prisma.sql` AND `);
364+
const rowWhereClause = joinSqlFragments(
365+
rowWhereFragments,
366+
Prisma.sql` AND `,
367+
);
368+
const countWhereClause = joinSqlFragments(
369+
countWhereFragments,
370+
Prisma.sql` AND `,
371+
);
344372

345373
const phaseEndExpression = Prisma.sql`
346374
COALESCE(cp."actualEndDate", cp."scheduledEndDate")
@@ -416,10 +444,10 @@ export class MyReviewService {
416444
const orderClause = joinSqlFragments(orderFragments, Prisma.sql`, `);
417445

418446
const countQuery = Prisma.sql`
419-
SELECT COUNT(DISTINCT c.id) AS "total"
447+
SELECT COUNT(*) AS "total"
420448
FROM challenges."Challenge" c
421449
${countJoinClause}
422-
WHERE ${whereClause}
450+
WHERE ${countWhereClause}
423451
`;
424452

425453
const countQueryDetails = countQuery.inspect();
@@ -470,7 +498,7 @@ export class MyReviewService {
470498
c.status AS "status"
471499
FROM challenges."Challenge" c
472500
${joinClause}
473-
WHERE ${whereClause}
501+
WHERE ${rowWhereClause}
474502
ORDER BY ${orderClause}
475503
LIMIT ${perPage}
476504
OFFSET ${offset}

src/api/submission/submission.service.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,23 @@ type SubmissionMinimal = {
6060
url: string | null;
6161
};
6262

63+
interface TopgearSubmissionEventPayload {
64+
submissionId: string;
65+
challengeId: string;
66+
submissionUrl: string;
67+
memberHandle: string;
68+
memberId: string;
69+
submittedDate: string;
70+
}
71+
72+
type TopgearSubmissionRecord = {
73+
id: string;
74+
challengeId: string | null;
75+
memberId: string | null;
76+
url: string | null;
77+
createdAt: Date;
78+
};
79+
6380
type SubmissionBusPayloadSource = Prisma.submissionGetPayload<{
6481
select: {
6582
id: true;
@@ -1399,6 +1416,95 @@ export class SubmissionService {
13991416
);
14001417
}
14011418

1419+
private async publishTopgearSubmissionEventIfEligible(
1420+
submission: TopgearSubmissionRecord,
1421+
): Promise<void> {
1422+
if (!submission.challengeId) {
1423+
this.logger.log(
1424+
`Submission ${submission.id} missing challengeId. Skipping Topgear event publish.`,
1425+
);
1426+
return;
1427+
}
1428+
1429+
const challenge = await this.challengeApiService.getChallengeDetail(
1430+
submission.challengeId,
1431+
);
1432+
1433+
if (!this.isTopgearTaskChallenge(challenge?.type)) {
1434+
this.logger.log(
1435+
`Challenge ${submission.challengeId} is not Topgear Task. Skipping immediate Topgear event for submission ${submission.id}.`,
1436+
);
1437+
return;
1438+
}
1439+
1440+
if (!submission.url) {
1441+
throw new InternalServerErrorException({
1442+
message:
1443+
'Updated submission does not contain a URL required for Topgear event payload.',
1444+
code: 'TOPGEAR_SUBMISSION_URL_MISSING',
1445+
details: { submissionId: submission.id },
1446+
});
1447+
}
1448+
1449+
if (!submission.memberId) {
1450+
throw new InternalServerErrorException({
1451+
message:
1452+
'Submission is missing memberId. Cannot publish Topgear event.',
1453+
code: 'TOPGEAR_SUBMISSION_MEMBER_MISSING',
1454+
details: { submissionId: submission.id },
1455+
});
1456+
}
1457+
1458+
const memberHandle = await this.lookupMemberHandle(
1459+
submission.challengeId,
1460+
submission.memberId,
1461+
);
1462+
1463+
if (!memberHandle) {
1464+
throw new InternalServerErrorException({
1465+
message: 'Unable to locate member handle for Topgear event payload.',
1466+
code: 'TOPGEAR_MEMBER_HANDLE_MISSING',
1467+
details: {
1468+
submissionId: submission.id,
1469+
challengeId: submission.challengeId,
1470+
memberId: submission.memberId,
1471+
},
1472+
});
1473+
}
1474+
1475+
const payload: TopgearSubmissionEventPayload = {
1476+
submissionId: submission.id,
1477+
challengeId: submission.challengeId,
1478+
submissionUrl: submission.url,
1479+
memberHandle,
1480+
memberId: submission.memberId,
1481+
submittedDate: submission.createdAt.toISOString(),
1482+
};
1483+
1484+
await this.eventBusService.publish('topgear.submission.received', payload);
1485+
this.logger.log(
1486+
`Published topgear.submission.received event for submission ${submission.id} immediately after creation.`,
1487+
);
1488+
}
1489+
1490+
private isTopgearTaskChallenge(typeName?: string): boolean {
1491+
return (typeName ?? '').trim().toLowerCase() === 'topgear task';
1492+
}
1493+
1494+
private async lookupMemberHandle(
1495+
challengeId: string,
1496+
memberId: string,
1497+
): Promise<string | null> {
1498+
const resource = await this.resourcePrisma.resource.findFirst({
1499+
where: {
1500+
challengeId,
1501+
memberId,
1502+
},
1503+
});
1504+
1505+
return resource?.memberHandle ?? null;
1506+
}
1507+
14021508
async createSubmission(
14031509
authUser: JwtUser,
14041510
body: SubmissionRequestDto,
@@ -1588,6 +1694,13 @@ export class SubmissionService {
15881694
this.logger.log(
15891695
`Skipping AV scan event for submission ${data.id} because it is not a file-based submission.`,
15901696
);
1697+
await this.publishTopgearSubmissionEventIfEligible({
1698+
id: data.id,
1699+
challengeId: data.challengeId,
1700+
memberId: data.memberId,
1701+
url: data.url,
1702+
createdAt: data.createdAt,
1703+
});
15911704
}
15921705
// Increment challenge submission counters if challengeId present
15931706
if (body.challengeId) {

0 commit comments

Comments
 (0)