Skip to content

Commit 5925671

Browse files
authored
feat: allow organization edit (#3290)
1 parent 09398a6 commit 5925671

File tree

4 files changed

+348
-4
lines changed

4 files changed

+348
-4
lines changed

__tests__/schema/opportunity.ts

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3720,6 +3720,214 @@ describe('mutation editOpportunity', () => {
37203720
'Only opportunities in draft state can be edited',
37213721
);
37223722
});
3723+
3724+
it('should edit opportunity with organization data', async () => {
3725+
loggedUser = '1';
3726+
3727+
const MUTATION_WITH_ORG = /* GraphQL */ `
3728+
mutation EditOpportunityWithOrg(
3729+
$id: ID!
3730+
$payload: OpportunityEditInput!
3731+
) {
3732+
editOpportunity(id: $id, payload: $payload) {
3733+
id
3734+
organization {
3735+
id
3736+
website
3737+
description
3738+
perks
3739+
founded
3740+
location
3741+
category
3742+
size
3743+
stage
3744+
}
3745+
}
3746+
}
3747+
`;
3748+
3749+
const res = await client.mutate(MUTATION_WITH_ORG, {
3750+
variables: {
3751+
id: opportunitiesFixture[0].id,
3752+
payload: {
3753+
organization: {
3754+
website: 'https://updated.dev',
3755+
description: 'Updated description',
3756+
perks: ['Remote work', 'Flexible hours'],
3757+
founded: 2021,
3758+
location: 'Berlin, Germany',
3759+
category: 'Technology',
3760+
size: CompanySize.COMPANY_SIZE_51_200,
3761+
stage: CompanyStage.SERIES_B,
3762+
},
3763+
},
3764+
},
3765+
});
3766+
3767+
expect(res.errors).toBeFalsy();
3768+
expect(res.data.editOpportunity.organization).toMatchObject({
3769+
website: 'https://updated.dev',
3770+
description: 'Updated description',
3771+
perks: ['Remote work', 'Flexible hours'],
3772+
founded: 2021,
3773+
location: 'Berlin, Germany',
3774+
category: 'Technology',
3775+
size: CompanySize.COMPANY_SIZE_51_200,
3776+
stage: CompanyStage.SERIES_B,
3777+
});
3778+
3779+
// Verify the organization was updated in database
3780+
const organization = await con
3781+
.getRepository(Organization)
3782+
.findOneBy({ id: organizationsFixture[0].id });
3783+
3784+
expect(organization).toMatchObject({
3785+
website: 'https://updated.dev',
3786+
description: 'Updated description',
3787+
perks: ['Remote work', 'Flexible hours'],
3788+
founded: 2021,
3789+
location: 'Berlin, Germany',
3790+
category: 'Technology',
3791+
size: CompanySize.COMPANY_SIZE_51_200,
3792+
stage: CompanyStage.SERIES_B,
3793+
});
3794+
});
3795+
});
3796+
3797+
describe('mutation clearOrganizationImage', () => {
3798+
beforeEach(async () => {
3799+
await con.getRepository(OpportunityJob).update(
3800+
{
3801+
id: opportunitiesFixture[0].id,
3802+
},
3803+
{
3804+
state: OpportunityState.DRAFT,
3805+
},
3806+
);
3807+
});
3808+
3809+
const MUTATION = /* GraphQL */ `
3810+
mutation ClearOrganizationImage($id: ID!) {
3811+
clearOrganizationImage(id: $id) {
3812+
_
3813+
}
3814+
}
3815+
`;
3816+
3817+
it('should require authentication', async () => {
3818+
await testMutationErrorCode(
3819+
client,
3820+
{
3821+
mutation: MUTATION,
3822+
variables: {
3823+
id: opportunitiesFixture[0].id,
3824+
},
3825+
},
3826+
'UNAUTHENTICATED',
3827+
);
3828+
});
3829+
3830+
it('should throw error when user is not a recruiter for opportunity', async () => {
3831+
loggedUser = '2';
3832+
3833+
await testMutationErrorCode(
3834+
client,
3835+
{
3836+
mutation: MUTATION,
3837+
variables: {
3838+
id: opportunitiesFixture[0].id,
3839+
},
3840+
},
3841+
'FORBIDDEN',
3842+
'Access denied!',
3843+
);
3844+
});
3845+
3846+
it('should throw error when opportunity does not exist', async () => {
3847+
loggedUser = '1';
3848+
3849+
await testMutationErrorCode(
3850+
client,
3851+
{
3852+
mutation: MUTATION,
3853+
variables: {
3854+
id: '660e8400-e29b-41d4-a716-446655440999',
3855+
},
3856+
},
3857+
'FORBIDDEN',
3858+
);
3859+
});
3860+
3861+
it('should clear organization image', async () => {
3862+
loggedUser = '1';
3863+
3864+
// First set an image on the organization
3865+
await con
3866+
.getRepository(Organization)
3867+
.update(
3868+
{ id: organizationsFixture[0].id },
3869+
{ image: 'https://example.com/old-image.png' },
3870+
);
3871+
3872+
// Verify image is set
3873+
let organization = await con
3874+
.getRepository(Organization)
3875+
.findOneBy({ id: organizationsFixture[0].id });
3876+
expect(organization?.image).toBe('https://example.com/old-image.png');
3877+
3878+
// Clear the image
3879+
const res = await client.mutate(MUTATION, {
3880+
variables: {
3881+
id: opportunitiesFixture[0].id,
3882+
},
3883+
});
3884+
3885+
expect(res.errors).toBeFalsy();
3886+
expect(res.data.clearOrganizationImage).toEqual({ _: true });
3887+
3888+
// Verify image was cleared in database
3889+
organization = await con
3890+
.getRepository(Organization)
3891+
.findOneBy({ id: organizationsFixture[0].id });
3892+
expect(organization?.image).toBeNull();
3893+
});
3894+
3895+
it('should work with opportunity permissions not direct organization permissions', async () => {
3896+
loggedUser = '3';
3897+
3898+
// User 3 is not a recruiter for opportunity 0, but let's make them one
3899+
await saveFixtures(con, OpportunityUser, [
3900+
{
3901+
opportunityId: opportunitiesFixture[0].id,
3902+
userId: '3',
3903+
type: OpportunityUserType.Recruiter,
3904+
},
3905+
]);
3906+
3907+
// Set an image on the organization
3908+
await con
3909+
.getRepository(Organization)
3910+
.update(
3911+
{ id: organizationsFixture[0].id },
3912+
{ image: 'https://example.com/test-image.png' },
3913+
);
3914+
3915+
// Should be able to clear the image through opportunity permissions
3916+
const res = await client.mutate(MUTATION, {
3917+
variables: {
3918+
id: opportunitiesFixture[0].id,
3919+
},
3920+
});
3921+
3922+
expect(res.errors).toBeFalsy();
3923+
expect(res.data.clearOrganizationImage).toEqual({ _: true });
3924+
3925+
// Verify image was cleared
3926+
const organization = await con
3927+
.getRepository(Organization)
3928+
.findOneBy({ id: organizationsFixture[0].id });
3929+
expect(organization?.image).toBeNull();
3930+
});
37233931
});
37243932

37253933
describe('mutation recommendOpportunityScreeningQuestions', () => {
@@ -3955,6 +4163,10 @@ describe('mutation updateOpportunityState', () => {
39554163
'content.responsibilities',
39564164
'content.requirements',
39574165
'questions',
4166+
'organization.links.0.socialType',
4167+
'organization.links.1.socialType',
4168+
'organization.links.2.title',
4169+
'organization.links.3.socialType',
39584170
]);
39594171
},
39604172
);

src/common/schema/opportunities.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { OpportunityState } from '@dailydotdev/schema';
22
import z from 'zod';
3+
import { organizationLinksSchema } from './organizations';
34

45
export const opportunityMatchDescriptionSchema = z.object({
56
reasoning: z.string(),
@@ -143,6 +144,17 @@ export const opportunityEditSchema = z
143144
)
144145
.min(1)
145146
.max(3),
147+
organization: z.object({
148+
website: z.string().max(500).nullable().optional(),
149+
description: z.string().max(2000).nullable().optional(),
150+
perks: z.array(z.string().max(240)).max(50).nullable().optional(),
151+
founded: z.number().int().min(1800).max(2100).nullable().optional(),
152+
location: z.string().max(500).nullable().optional(),
153+
category: z.string().max(240).nullable().optional(),
154+
size: z.number().int().nullable().optional(),
155+
stage: z.number().int().nullable().optional(),
156+
links: z.array(organizationLinksSchema).max(50).optional(),
157+
}),
146158
})
147159
.partial();
148160

src/entity/Organization.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class Organization {
3737
name: string;
3838

3939
@Column({ type: 'text', nullable: true })
40-
image: string;
40+
image: string | null;
4141

4242
@Column({ type: 'smallint', default: 1 })
4343
seats: number;

0 commit comments

Comments
 (0)