Skip to content

Commit 10c3211

Browse files
committed
refactor: implement Zod validation for opportunityMatches query
1 parent e783ec1 commit 10c3211

3 files changed

Lines changed: 29 additions & 19 deletions

File tree

__tests__/schema/opportunity.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,7 @@ describe('query opportunityMatches', () => {
11281128
it('should reject invalid status values', async () => {
11291129
loggedUser = '1';
11301130

1131-
// 'pending' is not an allowed status for this query - validation happens at GraphQL level
1131+
// 'pending' is not an allowed status for this query
11321132
await testQueryErrorCode(
11331133
client,
11341134
{
@@ -1139,7 +1139,7 @@ describe('query opportunityMatches', () => {
11391139
first: 10,
11401140
},
11411141
},
1142-
'GRAPHQL_VALIDATION_FAILED',
1142+
'ZOD_VALIDATION_ERROR',
11431143
);
11441144
});
11451145

src/common/schema/opportunities.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import z from 'zod';
33
import { organizationLinksSchema } from './organizations';
44
import { fileUploadSchema, urlParseSchema } from './common';
55
import { parseBigInt } from '../utils';
6+
import { OpportunityMatchStatus } from '../../entity/opportunities/types';
67

78
export const opportunityMatchDescriptionSchema = z.object({
89
reasoning: z.string(),
@@ -255,3 +256,16 @@ export const createSharedSlackChannelSchema = z.object({
255256
'Channel name can only contain lowercase letters, numbers, hyphens, and underscores',
256257
),
257258
});
259+
260+
export const opportunityMatchesQuerySchema = z.object({
261+
opportunityId: z.string(),
262+
status: z
263+
.enum([
264+
OpportunityMatchStatus.CandidateAccepted,
265+
OpportunityMatchStatus.RecruiterAccepted,
266+
OpportunityMatchStatus.RecruiterRejected,
267+
])
268+
.optional(),
269+
after: z.string().optional(),
270+
first: z.number().optional(),
271+
});

src/schema/opportunity.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import {
6565
opportunityUpdateStateSchema,
6666
createSharedSlackChannelSchema,
6767
parseOpportunitySchema,
68+
opportunityMatchesQuerySchema,
6869
} from '../common/schema/opportunities';
6970
import { OpportunityKeyword } from '../entity/OpportunityKeyword';
7071
import {
@@ -1070,31 +1071,26 @@ export const resolvers: IResolvers<unknown, BaseContext> = traceResolvers<
10701071
throw new NotFoundError('Not found!');
10711072
}
10721073

1073-
// Allowed statuses for this query
1074-
const allowedStatuses = [
1075-
OpportunityMatchStatus.CandidateAccepted,
1076-
OpportunityMatchStatus.RecruiterAccepted,
1077-
OpportunityMatchStatus.RecruiterRejected,
1078-
];
1079-
1080-
// Validate status if provided
1081-
if (args.status && !allowedStatuses.includes(args.status)) {
1082-
throw new ValidationError(
1083-
`Invalid status. Allowed values: ${allowedStatuses.join(', ')}`,
1084-
);
1085-
}
1074+
// Validate and parse args using Zod schema
1075+
const validatedArgs = opportunityMatchesQuerySchema.parse(args);
10861076

10871077
// First verify the user has access to this opportunity
10881078
await ensureOpportunityPermissions({
10891079
con: ctx.con.manager,
10901080
userId: ctx.userId,
1091-
opportunityId: args.opportunityId,
1081+
opportunityId: validatedArgs.opportunityId,
10921082
permission: OpportunityPermissions.UpdateState,
10931083
isTeamMember: ctx.isTeamMember,
10941084
});
10951085

10961086
// If status is provided, filter by that status; otherwise use all allowed statuses
1097-
const statusesToFilter = args.status ? [args.status] : allowedStatuses;
1087+
const statusesToFilter = validatedArgs.status
1088+
? [validatedArgs.status]
1089+
: [
1090+
OpportunityMatchStatus.CandidateAccepted,
1091+
OpportunityMatchStatus.RecruiterAccepted,
1092+
OpportunityMatchStatus.RecruiterRejected,
1093+
];
10981094

10991095
const [connection, totalCount] = await Promise.all([
11001096
queryPaginatedByDate<GQLOpportunityMatch, 'updatedAt', typeof args>(
@@ -1105,7 +1101,7 @@ export const resolvers: IResolvers<unknown, BaseContext> = traceResolvers<
11051101
{
11061102
queryBuilder: (builder) => {
11071103
builder.queryBuilder
1108-
.where({ opportunityId: args.opportunityId })
1104+
.where({ opportunityId: validatedArgs.opportunityId })
11091105
.andWhere(`${builder.alias}.status IN (:...statuses)`, {
11101106
statuses: statusesToFilter,
11111107
})
@@ -1130,7 +1126,7 @@ export const resolvers: IResolvers<unknown, BaseContext> = traceResolvers<
11301126
queryReadReplica(ctx.con, ({ queryRunner }) =>
11311127
queryRunner.manager.getRepository(OpportunityMatch).count({
11321128
where: {
1133-
opportunityId: args.opportunityId,
1129+
opportunityId: validatedArgs.opportunityId,
11341130
status: In(statusesToFilter),
11351131
},
11361132
}),

0 commit comments

Comments
 (0)