Skip to content

Commit b845864

Browse files
committed
feat: option to edit recruiter info
1 parent 25b89c1 commit b845864

File tree

3 files changed

+161
-0
lines changed

3 files changed

+161
-0
lines changed

__tests__/schema/opportunity.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4198,6 +4198,118 @@ describe('mutation editOpportunity', () => {
41984198
stage: CompanyStage.SERIES_B,
41994199
});
42004200
});
4201+
4202+
it('should update recruiter title and bio', async () => {
4203+
loggedUser = '1'; // user 1 is recruiter for opportunitiesFixture[0]
4204+
4205+
const res = await client.mutate(MUTATION, {
4206+
variables: {
4207+
id: opportunitiesFixture[0].id,
4208+
payload: {
4209+
recruiter: {
4210+
userId: usersFixture[0].id,
4211+
title: 'Senior Tech Recruiter',
4212+
bio: 'Passionate about connecting great talent with amazing opportunities.',
4213+
},
4214+
},
4215+
},
4216+
});
4217+
4218+
expect(res.errors).toBeFalsy();
4219+
4220+
// Verify the user's title and bio were updated
4221+
const userAfter = await con
4222+
.getRepository(User)
4223+
.findOneBy({ id: usersFixture[0].id });
4224+
4225+
expect(userAfter?.title).toBe('Senior Tech Recruiter');
4226+
expect(userAfter?.bio).toBe(
4227+
'Passionate about connecting great talent with amazing opportunities.',
4228+
);
4229+
});
4230+
4231+
it('should fail to update recruiter when user is not a recruiter for the opportunity', async () => {
4232+
loggedUser = '1'; // user 1 is recruiter for opportunitiesFixture[0]
4233+
4234+
// user 2 is NOT a recruiter for opportunitiesFixture[0]
4235+
await testMutationErrorCode(
4236+
client,
4237+
{
4238+
mutation: MUTATION,
4239+
variables: {
4240+
id: opportunitiesFixture[0].id,
4241+
payload: {
4242+
recruiter: {
4243+
userId: usersFixture[1].id, // user 2
4244+
title: 'Unauthorized Recruiter',
4245+
bio: 'This should fail',
4246+
},
4247+
},
4248+
},
4249+
},
4250+
'FORBIDDEN',
4251+
'Access denied! Recruiter is not part of this opportunity',
4252+
);
4253+
});
4254+
4255+
it('should fail to update recruiter when recruiter does not exist in opportunity_user', async () => {
4256+
loggedUser = '1';
4257+
4258+
// user 3 exists but is not a recruiter for opportunitiesFixture[0]
4259+
await testMutationErrorCode(
4260+
client,
4261+
{
4262+
mutation: MUTATION,
4263+
variables: {
4264+
id: opportunitiesFixture[0].id,
4265+
payload: {
4266+
recruiter: {
4267+
userId: '3',
4268+
title: 'Non-existent Recruiter',
4269+
bio: 'This should fail',
4270+
},
4271+
},
4272+
},
4273+
},
4274+
'FORBIDDEN',
4275+
'Access denied! Recruiter is not part of this opportunity',
4276+
);
4277+
});
4278+
4279+
it('should only update title when bio is not provided', async () => {
4280+
loggedUser = '1';
4281+
4282+
// Set initial bio
4283+
await con.getRepository(User).update(
4284+
{ id: usersFixture[0].id },
4285+
{
4286+
title: 'Initial Title',
4287+
bio: 'Initial bio that should remain',
4288+
},
4289+
);
4290+
4291+
const res = await client.mutate(MUTATION, {
4292+
variables: {
4293+
id: opportunitiesFixture[0].id,
4294+
payload: {
4295+
recruiter: {
4296+
userId: usersFixture[0].id,
4297+
title: 'Updated Title Only',
4298+
},
4299+
},
4300+
},
4301+
});
4302+
4303+
expect(res.errors).toBeFalsy();
4304+
4305+
const userAfter = await con
4306+
.getRepository(User)
4307+
.findOneBy({ id: usersFixture[0].id });
4308+
4309+
expect(userAfter?.title).toBe('Updated Title Only');
4310+
// Bio should be set to undefined since it wasn't provided in the update
4311+
expect(userAfter?.bio).toBeUndefined();
4312+
});
42014313
});
42024314

42034315
describe('mutation clearOrganizationImage', () => {

src/common/schema/opportunities.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@ export const opportunityEditSchema = z
155155
stage: z.number().int().nullable().optional(),
156156
links: z.array(organizationLinksSchema).max(50).optional(),
157157
}),
158+
recruiter: z.object({
159+
userId: z.string(),
160+
title: z.string().max(240).optional(),
161+
bio: z.string().max(2000).optional(),
162+
}),
158163
})
159164
.partial();
160165

src/schema/opportunity.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ import {
3030
opportunityFeedbackAnswersSchema,
3131
} from '../common/schema/opportunityMatch';
3232
import { OpportunityJob } from '../entity/opportunities/OpportunityJob';
33+
import { OpportunityUserRecruiter } from '../entity/opportunities/user/OpportunityUserRecruiter';
3334
import { ForbiddenError } from 'apollo-server-errors';
3435
import { ConflictError, NotFoundError } from '../errors';
3536
import { UserCandidateKeyword } from '../entity/user/UserCandidateKeyword';
37+
import { User } from '../entity/user/User';
3638
import { EMPLOYMENT_AGREEMENT_BUCKET_NAME } from '../config';
3739
import {
3840
deleteEmploymentAgreementByUserId,
@@ -420,6 +422,12 @@ export const typeDefs = /* GraphQL */ `
420422
links: [OrganizationLinkInput!]
421423
}
422424
425+
input RecruiterInput {
426+
userId: ID!
427+
title: String
428+
bio: String
429+
}
430+
423431
input OpportunityEditInput {
424432
title: String
425433
tldr: String
@@ -429,6 +437,7 @@ export const typeDefs = /* GraphQL */ `
429437
content: OpportunityContentInput
430438
questions: [OpportunityScreeningQuestionInput!]
431439
organization: OrganizationEditInput
440+
recruiter: RecruiterInput
432441
}
433442
434443
extend type Mutation {
@@ -1256,6 +1265,7 @@ export const resolvers: IResolvers<unknown, BaseContext> = traceResolvers<
12561265
content,
12571266
questions,
12581267
organization,
1268+
recruiter,
12591269
...opportunityUpdate
12601270
} = opportunity;
12611271

@@ -1369,6 +1379,40 @@ export const resolvers: IResolvers<unknown, BaseContext> = traceResolvers<
13691379
{ conflictPaths: ['id'] },
13701380
);
13711381
}
1382+
1383+
if (recruiter) {
1384+
// Check if the recruiter is part of the recruiters for this opportunity
1385+
const existingRecruiter = await entityManager
1386+
.getRepository(OpportunityUserRecruiter)
1387+
.findOne({
1388+
where: {
1389+
opportunityId: id,
1390+
userId: recruiter.userId,
1391+
type: OpportunityUserType.Recruiter,
1392+
},
1393+
});
1394+
1395+
if (!existingRecruiter) {
1396+
ctx.log.error(
1397+
{ opportunityId: id, userId: recruiter.userId },
1398+
'Recruiter is not part of this opportunity',
1399+
);
1400+
throw new ForbiddenError(
1401+
'Access denied! Recruiter is not part of this opportunity',
1402+
);
1403+
}
1404+
1405+
// Update the recruiter's title and bio on the User entity
1406+
await entityManager.getRepository(User).update(
1407+
{
1408+
id: recruiter.userId,
1409+
},
1410+
{
1411+
title: recruiter.title,
1412+
bio: recruiter.bio,
1413+
},
1414+
);
1415+
}
13721416
});
13731417

13741418
return graphorm.queryOneOrFail<GQLOpportunity>(ctx, info, (builder) => {

0 commit comments

Comments
 (0)