Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
299 changes: 299 additions & 0 deletions __tests__/schema/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1774,3 +1774,302 @@ describe('mutation removeUserExperience', () => {
expect(res.errors).toBeFalsy();
});
});

describe('UserExperience image field', () => {
const USER_EXPERIENCE_IMAGE_QUERY = /* GraphQL */ `
query UserExperienceById($id: ID!) {
userExperienceById(id: $id) {
id
image
customDomain
company {
id
image
}
}
}
`;

it('should return company image when experience has companyId', async () => {
loggedUser = '1';

// exp-1 has companyId 'company-1' which has image 'https://daily.dev/logo.png'
const res = await client.query(USER_EXPERIENCE_IMAGE_QUERY, {
variables: { id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479' },
});

expect(res.errors).toBeFalsy();
expect(res.data.userExperienceById.company.image).toBe(
'https://daily.dev/logo.png',
);
expect(res.data.userExperienceById.image).toBe(
'https://daily.dev/logo.png',
);
});

it('should return customImage from flags when no companyId', async () => {
loggedUser = '1';

const experienceId = 'e5f6a7b8-9abc-4ef0-1234-567890123456';
await con.getRepository(UserExperience).save({
id: experienceId,
userId: '1',
companyId: null,
customCompanyName: 'Custom Company',
title: 'Developer',
startedAt: new Date('2023-01-01'),
type: UserExperienceType.Work,
flags: {
customDomain: 'https://custom.com',
customImage:
'https://www.google.com/s2/favicons?domain=custom.com&sz=128',
},
});

const res = await client.query(USER_EXPERIENCE_IMAGE_QUERY, {
variables: { id: experienceId },
});

expect(res.errors).toBeFalsy();
expect(res.data.userExperienceById.company).toBeNull();
expect(res.data.userExperienceById.customDomain).toBe('https://custom.com');
expect(res.data.userExperienceById.image).toBe(
'https://www.google.com/s2/favicons?domain=custom.com&sz=128',
);
});

it('should prioritize company image over customImage when both exist', async () => {
loggedUser = '1';

const experienceId = 'f6a7b8c9-abcd-4f01-2345-678901234567';
await con.getRepository(UserExperience).save({
id: experienceId,
userId: '1',
companyId: 'company-1',
title: 'Engineer',
startedAt: new Date('2023-01-01'),
type: UserExperienceType.Work,
flags: {
customDomain: 'https://other.com',
customImage:
'https://www.google.com/s2/favicons?domain=other.com&sz=128',
},
});

const res = await client.query(USER_EXPERIENCE_IMAGE_QUERY, {
variables: { id: experienceId },
});

expect(res.errors).toBeFalsy();
expect(res.data.userExperienceById.company.image).toBe(
'https://daily.dev/logo.png',
);
expect(res.data.userExperienceById.image).toBe(
'https://daily.dev/logo.png',
);
expect(res.data.userExperienceById.customDomain).toBe('https://other.com');
});

it('should return null image when neither companyId nor customImage exists', async () => {
loggedUser = '1';

const experienceId = 'a7b8c9d0-bcde-4012-3456-789012345678';
await con.getRepository(UserExperience).save({
id: experienceId,
userId: '1',
companyId: null,
customCompanyName: 'No Image Company',
title: 'Intern',
startedAt: new Date('2023-01-01'),
type: UserExperienceType.Work,
flags: {},
});

const res = await client.query(USER_EXPERIENCE_IMAGE_QUERY, {
variables: { id: experienceId },
});

expect(res.errors).toBeFalsy();
expect(res.data.userExperienceById.company).toBeNull();
expect(res.data.userExperienceById.image).toBeNull();
expect(res.data.userExperienceById.customDomain).toBeNull();
});

it('should still link to existing company when customDomain is provided', async () => {
loggedUser = '1';

const UPSERT_WORK_MUTATION = /* GraphQL */ `
mutation UpsertUserWorkExperience(
$input: UserExperienceWorkInput!
$id: ID
) {
upsertUserWorkExperience(input: $input, id: $id) {
id
image
customDomain
customCompanyName
company {
id
name
image
}
}
}
`;

const res = await client.mutate(UPSERT_WORK_MUTATION, {
variables: {
input: {
type: 'work',
title: 'Engineer',
startedAt: new Date('2023-01-01'),
customCompanyName: 'Daily.dev',
customDomain: 'https://mycustomdomain.com',
},
},
});

expect(res.errors).toBeFalsy();
expect(res.data.upsertUserWorkExperience.company).not.toBeNull();
expect(res.data.upsertUserWorkExperience.company.name).toBe('Daily.dev');
expect(res.data.upsertUserWorkExperience.customCompanyName).toBeNull();
expect(res.data.upsertUserWorkExperience.customDomain).toBe(
'mycustomdomain.com',
);
expect(res.data.upsertUserWorkExperience.image).toBe(
'https://daily.dev/logo.png',
);
});

it('should set removedEnrichment flag and prevent auto-linking on subsequent saves', async () => {
loggedUser = '1';

const experienceId = 'c9d0e1f2-def0-4234-5678-901234567890';
await con.getRepository(UserExperience).save({
id: experienceId,
userId: '1',
companyId: 'company-1',
title: 'Engineer',
startedAt: new Date('2023-01-01'),
type: UserExperienceType.Work,
flags: {},
});

const UPSERT_WORK_MUTATION = /* GraphQL */ `
mutation UpsertUserWorkExperience(
$input: UserExperienceWorkInput!
$id: ID
) {
upsertUserWorkExperience(input: $input, id: $id) {
id
company {
id
}
customCompanyName
}
}
`;

const res1 = await client.mutate(UPSERT_WORK_MUTATION, {
variables: {
id: experienceId,
input: {
type: 'work',
title: 'Engineer',
startedAt: new Date('2023-01-01'),
customCompanyName: 'Daily.dev',
},
},
});

expect(res1.errors).toBeFalsy();
expect(res1.data.upsertUserWorkExperience.company).toBeNull();
expect(res1.data.upsertUserWorkExperience.customCompanyName).toBe(
'Daily.dev',
);

const afterFirstSave = await con
.getRepository(UserExperience)
.findOne({ where: { id: experienceId } });
expect(afterFirstSave?.flags?.removedEnrichment).toBe(true);
expect(afterFirstSave?.companyId).toBeNull();

const res2 = await client.mutate(UPSERT_WORK_MUTATION, {
variables: {
id: experienceId,
input: {
type: 'work',
title: 'Senior Engineer',
startedAt: new Date('2023-01-01'),
customCompanyName: 'Daily.dev',
},
},
});

expect(res2.errors).toBeFalsy();
expect(res2.data.upsertUserWorkExperience.company).toBeNull();
expect(res2.data.upsertUserWorkExperience.customCompanyName).toBe(
'Daily.dev',
);

const afterSecondSave = await con
.getRepository(UserExperience)
.findOne({ where: { id: experienceId } });
expect(afterSecondSave?.companyId).toBeNull();
expect(afterSecondSave?.flags?.removedEnrichment).toBe(true);
});

it('should allow re-linking to company after removedEnrichment was set', async () => {
loggedUser = '1';

const experienceId = 'd0e1f2a3-ef01-5345-6789-012345678901';
await con.getRepository(UserExperience).save({
id: experienceId,
userId: '1',
companyId: null,
customCompanyName: 'Some Custom Company',
title: 'Developer',
startedAt: new Date('2023-01-01'),
type: UserExperienceType.Work,
flags: { removedEnrichment: true },
});

const UPSERT_WORK_MUTATION = /* GraphQL */ `
mutation UpsertUserWorkExperience(
$input: UserExperienceWorkInput!
$id: ID
) {
upsertUserWorkExperience(input: $input, id: $id) {
id
company {
id
name
}
customCompanyName
}
}
`;

const res = await client.mutate(UPSERT_WORK_MUTATION, {
variables: {
id: experienceId,
input: {
type: 'work',
title: 'Developer',
startedAt: new Date('2023-01-01'),
companyId: 'company-1',
},
},
});

expect(res.errors).toBeFalsy();
expect(res.data.upsertUserWorkExperience.company).not.toBeNull();
expect(res.data.upsertUserWorkExperience.company.id).toBe('company-1');
expect(res.data.upsertUserWorkExperience.customCompanyName).toBeNull();

const updated = await con
.getRepository(UserExperience)
.findOne({ where: { id: experienceId } });
expect(updated?.companyId).toBe('company-1');
});
});
2 changes: 1 addition & 1 deletion src/common/companyEnrichment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ async function validateDomain(
return null;
}

function getGoogleFaviconUrl(domain: string): string {
export function getGoogleFaviconUrl(domain: string): string {
return `${GOOGLE_FAVICON_URL}?domain=${encodeURIComponent(domain)}&sz=${FAVICON_SIZE}`;
}

Expand Down
12 changes: 11 additions & 1 deletion src/common/schema/profile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import z from 'zod';
import { UserExperienceType } from '../../entity/user/experiences/types';
import { paginationSchema, urlParseSchema } from './common';
import { domainOnly } from '../links';

const domainSchema = z.preprocess(
(val) => (val === '' ? null : val),
urlParseSchema.transform(domainOnly).nullish(),
);
Comment on lines +6 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can use z.url(); no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noo, we accept both full urls with protocols, just domains and null :)


export const userExperiencesSchema = z
.object({
Expand Down Expand Up @@ -34,7 +40,10 @@ export const userExperienceCertificationSchema = z
.extend(userExperienceInputBaseSchema.shape);

export const userExperienceEducationSchema = z
.object({ grade: z.string().nullish() })
.object({
grade: z.string().nullish(),
customDomain: domainSchema,
})
.extend(userExperienceInputBaseSchema.shape);

export const userExperienceProjectSchema = z
Expand All @@ -55,6 +64,7 @@ export const userExperienceWorkSchema = z
.max(50)
.optional()
.default([]),
customDomain: domainSchema,
})
.extend(userExperienceInputBaseSchema.shape);

Expand Down
3 changes: 3 additions & 0 deletions src/entity/user/experiences/UserExperience.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import type { UserExperienceSkill } from './UserExperienceSkill';

export type UserExperienceFlags = Partial<{
import: string;
customDomain: string;
customImage: string;
removedEnrichment: boolean;
}>;

@Entity()
Expand Down
7 changes: 7 additions & 0 deletions src/graphorm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1897,6 +1897,13 @@ const obj = new GraphORM({
customLocation: {
jsonType: true,
},
image: {
select: (_, alias) =>
`COALESCE((SELECT c.image FROM company c WHERE c.id = ${alias}."companyId"), ${alias}.flags->>'customImage')`,
},
customDomain: {
select: (_, alias) => `${alias}.flags->>'customDomain'`,
},
},
},
OpportunityMatchCandidatePreference: {
Expand Down
Loading
Loading