Skip to content

Commit 6b8caca

Browse files
authored
Merge pull request #295 from topcoder-platform/develop
Performance updates for API responses
2 parents a0540bc + e7e4744 commit 6b8caca

5 files changed

Lines changed: 235 additions & 137 deletions

File tree

src/api/review-summation/review-summation.service.spec.ts

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { UserRole } from 'src/shared/enums/userRole.enum';
88

99
describe('ReviewSummationService', () => {
1010
describe('searchSummation', () => {
11-
it('allows registered marathon submitters to view challenge summations with only safe progress metadata', async () => {
11+
it('allows registered marathon submitters to view challenge summations without metadata', async () => {
1212
const prismaMock = {
1313
reviewSummation: {
1414
findMany: jest.fn().mockResolvedValue([
@@ -127,27 +127,81 @@ describe('ReviewSummationService', () => {
127127
}),
128128
}),
129129
);
130+
const findManyArg = prismaMock.reviewSummation.findMany.mock.calls[0][0];
131+
expect(findManyArg.select).not.toHaveProperty('metadata');
130132
expect(result.data).toHaveLength(2);
131133
expect(result.data.map((summation) => summation.submitterId)).toEqual([
132134
111, 222,
133135
]);
134-
expect(result.data[0].metadata).toEqual({
136+
expect(result.data[0]).not.toHaveProperty('metadata');
137+
expect(result.data[1]).not.toHaveProperty('metadata');
138+
expect(JSON.stringify(result.data)).not.toContain('123456789');
139+
expect(JSON.stringify(result.data)).not.toContain('987654321');
140+
});
141+
142+
it('returns metadata to machine callers that explicitly request it', async () => {
143+
const metadata = {
135144
testProcess: 'provisional',
136-
testProgress: 0.5,
137-
testStatus: 'IN PROGRESS',
138-
testProgressDetails: {
139-
completedTests: 5,
140-
progress: 0.5,
141-
status: 'IN PROGRESS',
142-
totalTests: 10,
145+
testScores: [
146+
{
147+
score: 11,
148+
seed: 123456789,
149+
},
150+
],
151+
};
152+
const prismaMock = {
153+
reviewSummation: {
154+
findMany: jest.fn().mockResolvedValue([
155+
{
156+
id: 'summation-1',
157+
submissionId: 'submission-1',
158+
aggregateScore: -1,
159+
scorecardId: null,
160+
isPassing: false,
161+
isFinal: false,
162+
isProvisional: true,
163+
isExample: false,
164+
reviewedDate: null,
165+
createdAt: new Date('2026-05-01T00:00:00.000Z'),
166+
createdBy: null,
167+
updatedAt: null,
168+
updatedBy: null,
169+
metadata,
170+
},
171+
]),
172+
count: jest.fn().mockResolvedValue(1),
143173
},
144-
});
145-
expect(JSON.stringify(result.data[0].metadata)).not.toContain(
146-
'123456789',
174+
};
175+
const service = new ReviewSummationService(
176+
prismaMock as any,
177+
{} as any,
178+
{} as any,
179+
{ member: { findMany: jest.fn().mockResolvedValue([]) } } as any,
180+
{} as any,
147181
);
148-
expect(JSON.stringify(result.data[1].metadata)).not.toContain(
149-
'987654321',
182+
183+
const result = await service.searchSummation(
184+
{
185+
isMachine: true,
186+
roles: [],
187+
},
188+
{
189+
submissionId: 'submission-1',
190+
metadata: 'true',
191+
provisional: 'true',
192+
},
193+
{
194+
page: 1,
195+
perPage: 10,
196+
},
150197
);
198+
199+
const findManyArg = prismaMock.reviewSummation.findMany.mock.calls[0][0];
200+
expect(findManyArg.select).toHaveProperty('metadata', true);
201+
expect(findManyArg.select).not.toHaveProperty('submission');
202+
expect(result.data).toHaveLength(1);
203+
expect(result.data[0].metadata).toEqual(metadata);
204+
expect(result.data[0]).not.toHaveProperty('submission');
151205
});
152206
});
153207
});

src/api/review-summation/review-summation.service.ts

Lines changed: 98 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,47 @@ import { MemberPrismaService } from 'src/shared/modules/global/member-prisma.ser
2626
import { ResourceApiService } from 'src/shared/modules/global/resource.service';
2727
import { UserRole } from 'src/shared/enums/userRole.enum';
2828
import { Prisma } from '@prisma/client';
29-
import { buildSafeReviewSummationMetadata } from 'src/shared/utils/review-summation-metadata.util';
29+
30+
const REVIEW_SUMMATION_RESPONSE_SELECT = {
31+
id: true,
32+
submissionId: true,
33+
legacySubmissionId: true,
34+
aggregateScore: true,
35+
scorecardId: true,
36+
scorecardLegacyId: true,
37+
isPassing: true,
38+
isFinal: true,
39+
isProvisional: true,
40+
isExample: true,
41+
reviewedDate: true,
42+
createdAt: true,
43+
createdBy: true,
44+
updatedAt: true,
45+
updatedBy: true,
46+
} satisfies Prisma.reviewSummationSelect;
47+
48+
const REVIEW_SUMMATION_RESPONSE_WITH_METADATA_SELECT = {
49+
...REVIEW_SUMMATION_RESPONSE_SELECT,
50+
metadata: true,
51+
} satisfies Prisma.reviewSummationSelect;
52+
53+
const REVIEW_SUMMATION_WITH_SUBMITTER_SELECT = {
54+
...REVIEW_SUMMATION_RESPONSE_SELECT,
55+
submission: {
56+
select: {
57+
memberId: true,
58+
},
59+
},
60+
} satisfies Prisma.reviewSummationSelect;
61+
62+
const REVIEW_SUMMATION_WITH_SUBMITTER_AND_METADATA_SELECT = {
63+
...REVIEW_SUMMATION_RESPONSE_WITH_METADATA_SELECT,
64+
submission: {
65+
select: {
66+
memberId: true,
67+
},
68+
},
69+
} satisfies Prisma.reviewSummationSelect;
3070

3171
@Injectable()
3272
export class ReviewSummationService {
@@ -562,9 +602,10 @@ export class ReviewSummationService {
562602

563603
const data = await this.prisma.reviewSummation.create({
564604
data: createData,
605+
select: REVIEW_SUMMATION_RESPONSE_SELECT,
565606
});
566607
this.logger.log(`Review summation created with ID: ${data.id}`);
567-
return data as ReviewSummationResponseDto;
608+
return this.buildResponse(data);
568609
} catch (error) {
569610
// Re-throw NotFoundException and BadRequestException as-is
570611
if (
@@ -671,8 +712,6 @@ export class ReviewSummationService {
671712
: undefined;
672713
const challengeIdFilter =
673714
rawChallengeId && rawChallengeId.length ? rawChallengeId : undefined;
674-
const includeMetadata =
675-
(queryDto.metadata ?? '').toLowerCase() === 'true';
676715

677716
if (isSubmitterOnly) {
678717
const userId =
@@ -839,23 +878,23 @@ export class ReviewSummationService {
839878
};
840879

841880
const shouldEnrichSubmitterMetadata = Boolean(challengeIdFilter);
881+
const includeMetadata =
882+
(authUser?.isMachine ?? false) &&
883+
parseBooleanString(queryDto.metadata) === true;
884+
const summationSelect = shouldEnrichSubmitterMetadata
885+
? includeMetadata
886+
? REVIEW_SUMMATION_WITH_SUBMITTER_AND_METADATA_SELECT
887+
: REVIEW_SUMMATION_WITH_SUBMITTER_SELECT
888+
: includeMetadata
889+
? REVIEW_SUMMATION_RESPONSE_WITH_METADATA_SELECT
890+
: REVIEW_SUMMATION_RESPONSE_SELECT;
842891

843892
const summations = await this.prisma.reviewSummation.findMany({
844893
where: whereClause,
845894
skip,
846895
take: perPage,
847896
orderBy,
848-
...(shouldEnrichSubmitterMetadata
849-
? {
850-
include: {
851-
submission: {
852-
select: {
853-
memberId: true,
854-
},
855-
},
856-
},
857-
}
858-
: {}),
897+
select: summationSelect,
859898
});
860899

861900
const submitterInfoByMemberId = new Map<
@@ -929,11 +968,10 @@ export class ReviewSummationService {
929968
});
930969

931970
const data: ReviewSummationResponseDto[] = summations.map((summation) => {
932-
const { submission, metadata, ...rest } =
933-
summation as typeof summation & {
934-
submission?: { memberId: string | null };
935-
metadata?: Prisma.JsonValue | null;
936-
};
971+
const summationRecord = summation as typeof summation & {
972+
submission?: { memberId: string | null };
973+
};
974+
const { submission } = summationRecord;
937975

938976
let submitterId: number | null = null;
939977
let submitterHandle: string | null = null;
@@ -958,20 +996,17 @@ export class ReviewSummationService {
958996
}
959997
}
960998

961-
const base: ReviewSummationResponseDto = {
962-
...rest,
963-
submitterId,
964-
submitterHandle,
965-
submitterMaxRating,
966-
} as ReviewSummationResponseDto;
967-
968-
if (includeMetadata) {
969-
base.metadata = isSubmitterOnly
970-
? buildSafeReviewSummationMetadata(metadata)
971-
: (metadata ?? null);
972-
}
973-
974-
return base;
999+
return this.buildResponse(
1000+
summationRecord,
1001+
{
1002+
submitterId,
1003+
submitterHandle,
1004+
submitterMaxRating,
1005+
},
1006+
{
1007+
includeMetadata,
1008+
},
1009+
);
9751010
});
9761011

9771012
this.logger.log(
@@ -1081,9 +1116,10 @@ export class ReviewSummationService {
10811116
const data = await this.prisma.reviewSummation.update({
10821117
where: { id },
10831118
data: updateData,
1119+
select: REVIEW_SUMMATION_RESPONSE_SELECT,
10841120
});
10851121
this.logger.log(`Review summation updated successfully: ${id}`);
1086-
return data as ReviewSummationResponseDto;
1122+
return this.buildResponse(data);
10871123
} catch (error) {
10881124
// Re-throw NotFoundException and BadRequestException from checkSummation and validation as-is
10891125
if (
@@ -1165,13 +1201,14 @@ export class ReviewSummationService {
11651201
try {
11661202
const data = await this.prisma.reviewSummation.findUnique({
11671203
where: { id },
1204+
select: REVIEW_SUMMATION_RESPONSE_SELECT,
11681205
});
11691206
if (!data || !data.id) {
11701207
throw new NotFoundException(
11711208
`Review summation with ID ${id} not found. Please verify the summation ID is correct.`,
11721209
);
11731210
}
1174-
return data;
1211+
return this.buildResponse(data);
11751212
} catch (error) {
11761213
// Re-throw NotFoundException as-is
11771214
if (error instanceof NotFoundException) {
@@ -1189,4 +1226,29 @@ export class ReviewSummationService {
11891226
});
11901227
}
11911228
}
1229+
1230+
/**
1231+
* Builds a review summation response object.
1232+
* @param data Review summation row or row-like object to serialize.
1233+
* @param extras Optional computed submitter fields to append.
1234+
* @param options Response serialization options, including internal metadata access.
1235+
* @returns Review summation response DTO with internal relations removed.
1236+
* @throws This method does not throw.
1237+
* Used by all review summation response paths as a final guard against exposing per-seed metadata by default.
1238+
*/
1239+
private buildResponse(
1240+
data: Record<string, unknown>,
1241+
extras: Partial<ReviewSummationResponseDto> = {},
1242+
options: { includeMetadata?: boolean } = {},
1243+
): ReviewSummationResponseDto {
1244+
const response: Record<string, unknown> = {
1245+
...data,
1246+
...extras,
1247+
};
1248+
if (!options.includeMetadata) {
1249+
delete response.metadata;
1250+
}
1251+
delete response.submission;
1252+
return response as unknown as ReviewSummationResponseDto;
1253+
}
11921254
}

src/api/submission/submission.service.spec.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2246,7 +2246,7 @@ describe('SubmissionService', () => {
22462246
expect(submissionResult.url).toBeNull();
22472247
});
22482248

2249-
it('sanitizes review summation metadata for submitter-owned submissions', async () => {
2249+
it('omits review summation metadata for submitter-owned submissions', async () => {
22502250
const now = new Date('2026-05-01T12:00:00Z');
22512251
const submissions = [
22522252
{
@@ -2307,19 +2307,16 @@ describe('SubmissionService', () => {
23072307
{ page: 1, perPage: 10 } as any,
23082308
);
23092309

2310-
const metadata = result.data[0].reviewSummation?.[0].metadata;
2311-
expect(metadata).toEqual({
2312-
testProcess: 'system',
2313-
testProgress: 0.75,
2314-
testStatus: 'IN PROGRESS',
2315-
testProgressDetails: {
2316-
completedTests: 15,
2317-
progress: 0.75,
2318-
status: 'IN PROGRESS',
2319-
totalTests: 20,
2320-
},
2321-
});
2322-
expect(JSON.stringify(metadata)).not.toContain('987654321');
2310+
const findManyArg = prismaMock.submission.findMany.mock.calls[0][0];
2311+
expect(findManyArg.include.reviewSummation.select).not.toHaveProperty(
2312+
'metadata',
2313+
);
2314+
expect(result.data[0].reviewSummation?.[0]).not.toHaveProperty(
2315+
'metadata',
2316+
);
2317+
expect(JSON.stringify(result.data[0].reviewSummation)).not.toContain(
2318+
'987654321',
2319+
);
23232320
});
23242321
});
23252322

0 commit comments

Comments
 (0)