Skip to content

Commit 1565e7b

Browse files
feat: custom domain for experience (#3439)
1 parent 41a202f commit 1565e7b

File tree

7 files changed

+410
-30
lines changed

7 files changed

+410
-30
lines changed

__tests__/schema/profile.ts

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1774,3 +1774,302 @@ describe('mutation removeUserExperience', () => {
17741774
expect(res.errors).toBeFalsy();
17751775
});
17761776
});
1777+
1778+
describe('UserExperience image field', () => {
1779+
const USER_EXPERIENCE_IMAGE_QUERY = /* GraphQL */ `
1780+
query UserExperienceById($id: ID!) {
1781+
userExperienceById(id: $id) {
1782+
id
1783+
image
1784+
customDomain
1785+
company {
1786+
id
1787+
image
1788+
}
1789+
}
1790+
}
1791+
`;
1792+
1793+
it('should return company image when experience has companyId', async () => {
1794+
loggedUser = '1';
1795+
1796+
// exp-1 has companyId 'company-1' which has image 'https://daily.dev/logo.png'
1797+
const res = await client.query(USER_EXPERIENCE_IMAGE_QUERY, {
1798+
variables: { id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479' },
1799+
});
1800+
1801+
expect(res.errors).toBeFalsy();
1802+
expect(res.data.userExperienceById.company.image).toBe(
1803+
'https://daily.dev/logo.png',
1804+
);
1805+
expect(res.data.userExperienceById.image).toBe(
1806+
'https://daily.dev/logo.png',
1807+
);
1808+
});
1809+
1810+
it('should return customImage from flags when no companyId', async () => {
1811+
loggedUser = '1';
1812+
1813+
const experienceId = 'e5f6a7b8-9abc-4ef0-1234-567890123456';
1814+
await con.getRepository(UserExperience).save({
1815+
id: experienceId,
1816+
userId: '1',
1817+
companyId: null,
1818+
customCompanyName: 'Custom Company',
1819+
title: 'Developer',
1820+
startedAt: new Date('2023-01-01'),
1821+
type: UserExperienceType.Work,
1822+
flags: {
1823+
customDomain: 'https://custom.com',
1824+
customImage:
1825+
'https://www.google.com/s2/favicons?domain=custom.com&sz=128',
1826+
},
1827+
});
1828+
1829+
const res = await client.query(USER_EXPERIENCE_IMAGE_QUERY, {
1830+
variables: { id: experienceId },
1831+
});
1832+
1833+
expect(res.errors).toBeFalsy();
1834+
expect(res.data.userExperienceById.company).toBeNull();
1835+
expect(res.data.userExperienceById.customDomain).toBe('https://custom.com');
1836+
expect(res.data.userExperienceById.image).toBe(
1837+
'https://www.google.com/s2/favicons?domain=custom.com&sz=128',
1838+
);
1839+
});
1840+
1841+
it('should prioritize company image over customImage when both exist', async () => {
1842+
loggedUser = '1';
1843+
1844+
const experienceId = 'f6a7b8c9-abcd-4f01-2345-678901234567';
1845+
await con.getRepository(UserExperience).save({
1846+
id: experienceId,
1847+
userId: '1',
1848+
companyId: 'company-1',
1849+
title: 'Engineer',
1850+
startedAt: new Date('2023-01-01'),
1851+
type: UserExperienceType.Work,
1852+
flags: {
1853+
customDomain: 'https://other.com',
1854+
customImage:
1855+
'https://www.google.com/s2/favicons?domain=other.com&sz=128',
1856+
},
1857+
});
1858+
1859+
const res = await client.query(USER_EXPERIENCE_IMAGE_QUERY, {
1860+
variables: { id: experienceId },
1861+
});
1862+
1863+
expect(res.errors).toBeFalsy();
1864+
expect(res.data.userExperienceById.company.image).toBe(
1865+
'https://daily.dev/logo.png',
1866+
);
1867+
expect(res.data.userExperienceById.image).toBe(
1868+
'https://daily.dev/logo.png',
1869+
);
1870+
expect(res.data.userExperienceById.customDomain).toBe('https://other.com');
1871+
});
1872+
1873+
it('should return null image when neither companyId nor customImage exists', async () => {
1874+
loggedUser = '1';
1875+
1876+
const experienceId = 'a7b8c9d0-bcde-4012-3456-789012345678';
1877+
await con.getRepository(UserExperience).save({
1878+
id: experienceId,
1879+
userId: '1',
1880+
companyId: null,
1881+
customCompanyName: 'No Image Company',
1882+
title: 'Intern',
1883+
startedAt: new Date('2023-01-01'),
1884+
type: UserExperienceType.Work,
1885+
flags: {},
1886+
});
1887+
1888+
const res = await client.query(USER_EXPERIENCE_IMAGE_QUERY, {
1889+
variables: { id: experienceId },
1890+
});
1891+
1892+
expect(res.errors).toBeFalsy();
1893+
expect(res.data.userExperienceById.company).toBeNull();
1894+
expect(res.data.userExperienceById.image).toBeNull();
1895+
expect(res.data.userExperienceById.customDomain).toBeNull();
1896+
});
1897+
1898+
it('should still link to existing company when customDomain is provided', async () => {
1899+
loggedUser = '1';
1900+
1901+
const UPSERT_WORK_MUTATION = /* GraphQL */ `
1902+
mutation UpsertUserWorkExperience(
1903+
$input: UserExperienceWorkInput!
1904+
$id: ID
1905+
) {
1906+
upsertUserWorkExperience(input: $input, id: $id) {
1907+
id
1908+
image
1909+
customDomain
1910+
customCompanyName
1911+
company {
1912+
id
1913+
name
1914+
image
1915+
}
1916+
}
1917+
}
1918+
`;
1919+
1920+
const res = await client.mutate(UPSERT_WORK_MUTATION, {
1921+
variables: {
1922+
input: {
1923+
type: 'work',
1924+
title: 'Engineer',
1925+
startedAt: new Date('2023-01-01'),
1926+
customCompanyName: 'Daily.dev',
1927+
customDomain: 'https://mycustomdomain.com',
1928+
},
1929+
},
1930+
});
1931+
1932+
expect(res.errors).toBeFalsy();
1933+
expect(res.data.upsertUserWorkExperience.company).not.toBeNull();
1934+
expect(res.data.upsertUserWorkExperience.company.name).toBe('Daily.dev');
1935+
expect(res.data.upsertUserWorkExperience.customCompanyName).toBeNull();
1936+
expect(res.data.upsertUserWorkExperience.customDomain).toBe(
1937+
'mycustomdomain.com',
1938+
);
1939+
expect(res.data.upsertUserWorkExperience.image).toBe(
1940+
'https://daily.dev/logo.png',
1941+
);
1942+
});
1943+
1944+
it('should set removedEnrichment flag and prevent auto-linking on subsequent saves', async () => {
1945+
loggedUser = '1';
1946+
1947+
const experienceId = 'c9d0e1f2-def0-4234-5678-901234567890';
1948+
await con.getRepository(UserExperience).save({
1949+
id: experienceId,
1950+
userId: '1',
1951+
companyId: 'company-1',
1952+
title: 'Engineer',
1953+
startedAt: new Date('2023-01-01'),
1954+
type: UserExperienceType.Work,
1955+
flags: {},
1956+
});
1957+
1958+
const UPSERT_WORK_MUTATION = /* GraphQL */ `
1959+
mutation UpsertUserWorkExperience(
1960+
$input: UserExperienceWorkInput!
1961+
$id: ID
1962+
) {
1963+
upsertUserWorkExperience(input: $input, id: $id) {
1964+
id
1965+
company {
1966+
id
1967+
}
1968+
customCompanyName
1969+
}
1970+
}
1971+
`;
1972+
1973+
const res1 = await client.mutate(UPSERT_WORK_MUTATION, {
1974+
variables: {
1975+
id: experienceId,
1976+
input: {
1977+
type: 'work',
1978+
title: 'Engineer',
1979+
startedAt: new Date('2023-01-01'),
1980+
customCompanyName: 'Daily.dev',
1981+
},
1982+
},
1983+
});
1984+
1985+
expect(res1.errors).toBeFalsy();
1986+
expect(res1.data.upsertUserWorkExperience.company).toBeNull();
1987+
expect(res1.data.upsertUserWorkExperience.customCompanyName).toBe(
1988+
'Daily.dev',
1989+
);
1990+
1991+
const afterFirstSave = await con
1992+
.getRepository(UserExperience)
1993+
.findOne({ where: { id: experienceId } });
1994+
expect(afterFirstSave?.flags?.removedEnrichment).toBe(true);
1995+
expect(afterFirstSave?.companyId).toBeNull();
1996+
1997+
const res2 = await client.mutate(UPSERT_WORK_MUTATION, {
1998+
variables: {
1999+
id: experienceId,
2000+
input: {
2001+
type: 'work',
2002+
title: 'Senior Engineer',
2003+
startedAt: new Date('2023-01-01'),
2004+
customCompanyName: 'Daily.dev',
2005+
},
2006+
},
2007+
});
2008+
2009+
expect(res2.errors).toBeFalsy();
2010+
expect(res2.data.upsertUserWorkExperience.company).toBeNull();
2011+
expect(res2.data.upsertUserWorkExperience.customCompanyName).toBe(
2012+
'Daily.dev',
2013+
);
2014+
2015+
const afterSecondSave = await con
2016+
.getRepository(UserExperience)
2017+
.findOne({ where: { id: experienceId } });
2018+
expect(afterSecondSave?.companyId).toBeNull();
2019+
expect(afterSecondSave?.flags?.removedEnrichment).toBe(true);
2020+
});
2021+
2022+
it('should allow re-linking to company after removedEnrichment was set', async () => {
2023+
loggedUser = '1';
2024+
2025+
const experienceId = 'd0e1f2a3-ef01-5345-6789-012345678901';
2026+
await con.getRepository(UserExperience).save({
2027+
id: experienceId,
2028+
userId: '1',
2029+
companyId: null,
2030+
customCompanyName: 'Some Custom Company',
2031+
title: 'Developer',
2032+
startedAt: new Date('2023-01-01'),
2033+
type: UserExperienceType.Work,
2034+
flags: { removedEnrichment: true },
2035+
});
2036+
2037+
const UPSERT_WORK_MUTATION = /* GraphQL */ `
2038+
mutation UpsertUserWorkExperience(
2039+
$input: UserExperienceWorkInput!
2040+
$id: ID
2041+
) {
2042+
upsertUserWorkExperience(input: $input, id: $id) {
2043+
id
2044+
company {
2045+
id
2046+
name
2047+
}
2048+
customCompanyName
2049+
}
2050+
}
2051+
`;
2052+
2053+
const res = await client.mutate(UPSERT_WORK_MUTATION, {
2054+
variables: {
2055+
id: experienceId,
2056+
input: {
2057+
type: 'work',
2058+
title: 'Developer',
2059+
startedAt: new Date('2023-01-01'),
2060+
companyId: 'company-1',
2061+
},
2062+
},
2063+
});
2064+
2065+
expect(res.errors).toBeFalsy();
2066+
expect(res.data.upsertUserWorkExperience.company).not.toBeNull();
2067+
expect(res.data.upsertUserWorkExperience.company.id).toBe('company-1');
2068+
expect(res.data.upsertUserWorkExperience.customCompanyName).toBeNull();
2069+
2070+
const updated = await con
2071+
.getRepository(UserExperience)
2072+
.findOne({ where: { id: experienceId } });
2073+
expect(updated?.companyId).toBe('company-1');
2074+
});
2075+
});

src/common/companyEnrichment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ async function validateDomain(
102102
return null;
103103
}
104104

105-
function getGoogleFaviconUrl(domain: string): string {
105+
export function getGoogleFaviconUrl(domain: string): string {
106106
return `${GOOGLE_FAVICON_URL}?domain=${encodeURIComponent(domain)}&sz=${FAVICON_SIZE}`;
107107
}
108108

src/common/schema/profile.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import z from 'zod';
22
import { UserExperienceType } from '../../entity/user/experiences/types';
33
import { paginationSchema, urlParseSchema } from './common';
4+
import { domainOnly } from '../links';
5+
6+
const domainSchema = z.preprocess(
7+
(val) => (val === '' ? null : val),
8+
urlParseSchema.transform(domainOnly).nullish(),
9+
);
410

511
export const userExperiencesSchema = z
612
.object({
@@ -34,7 +40,10 @@ export const userExperienceCertificationSchema = z
3440
.extend(userExperienceInputBaseSchema.shape);
3541

3642
export const userExperienceEducationSchema = z
37-
.object({ grade: z.string().nullish() })
43+
.object({
44+
grade: z.string().nullish(),
45+
customDomain: domainSchema,
46+
})
3847
.extend(userExperienceInputBaseSchema.shape);
3948

4049
export const userExperienceProjectSchema = z
@@ -55,6 +64,7 @@ export const userExperienceWorkSchema = z
5564
.max(50)
5665
.optional()
5766
.default([]),
67+
customDomain: domainSchema,
5868
})
5969
.extend(userExperienceInputBaseSchema.shape);
6070

src/entity/user/experiences/UserExperience.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import type { UserExperienceSkill } from './UserExperienceSkill';
1919

2020
export type UserExperienceFlags = Partial<{
2121
import: string;
22+
customDomain: string;
23+
customImage: string;
24+
removedEnrichment: boolean;
2225
}>;
2326

2427
@Entity()

src/graphorm/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1897,6 +1897,13 @@ const obj = new GraphORM({
18971897
customLocation: {
18981898
jsonType: true,
18991899
},
1900+
image: {
1901+
select: (_, alias) =>
1902+
`COALESCE((SELECT c.image FROM company c WHERE c.id = ${alias}."companyId"), ${alias}.flags->>'customImage')`,
1903+
},
1904+
customDomain: {
1905+
select: (_, alias) => `${alias}.flags->>'customDomain'`,
1906+
},
19001907
},
19011908
},
19021909
OpportunityMatchCandidatePreference: {

0 commit comments

Comments
 (0)