Skip to content

Commit c8f1ee3

Browse files
authored
feat: opportunity preview Gondul (#3323)
1 parent 29c67c0 commit c8f1ee3

4 files changed

Lines changed: 487 additions & 1 deletion

File tree

src/common/users.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,39 @@ export const getUserReadingTags = (
361361
);
362362
};
363363

364+
export const getUserTopReadingTags = (
365+
con: DataSource,
366+
{
367+
userId,
368+
limit = 5,
369+
readLimit = 100,
370+
}: { userId: string; limit?: number; readLimit?: number },
371+
): Promise<Array<{ tag: string; count: number }>> => {
372+
return con.query(
373+
`--sql
374+
WITH recent_reads AS (
375+
SELECT v."postId"
376+
FROM "view" v
377+
WHERE v."userId" = $1
378+
AND v.hidden = false
379+
ORDER BY v.timestamp DESC
380+
LIMIT $3
381+
)
382+
SELECT
383+
pk.keyword AS tag,
384+
COUNT(*) AS count
385+
FROM recent_reads rr
386+
JOIN post_keyword pk ON rr."postId" = pk."postId"
387+
WHERE pk.status = 'allow'
388+
AND pk.keyword != 'general-programming'
389+
GROUP BY pk.keyword
390+
ORDER BY COUNT(*) DESC
391+
LIMIT $2;
392+
`,
393+
[userId, limit, readLimit],
394+
);
395+
};
396+
364397
export const getUserReadingRank = async (
365398
con: DataSource,
366399
userId: string,

src/entity/opportunities/Opportunity.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import type { QuestionFeedback } from '../questions/QuestionFeedback';
2222

2323
export type OpportunityFlags = Partial<{
2424
anonUserId: string | null;
25+
preview: {
26+
userIds: string[];
27+
totalCount: number;
28+
};
2529
}>;
2630

2731
@Entity()

src/graphorm/index.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
type PostFlagsPublic,
2222
type Campaign,
2323
type OrganizationLink,
24+
SourceType,
2425
} from '../entity';
2526
import {
2627
OrganizationMemberRole,
@@ -36,6 +37,7 @@ import {
3637
domainOnly,
3738
getSmartTitle,
3839
getTranslationRecord,
40+
getUserTopReadingTags,
3941
transformDate,
4042
} from '../common';
4143
import { GQLComment } from '../schema/comments';
@@ -64,6 +66,7 @@ import { OpportunityUserType } from '../entity/opportunities/types';
6466
import { OrganizationLinkType } from '../common/schema/organizations';
6567
import type { GCSBlob } from '../common/schema/userCandidate';
6668
import { QuestionType } from '../entity/questions/types';
69+
import { snotraClient } from '../integrations/snotra';
6770

6871
const existsByUserAndPost =
6972
(entity: string, build?: (queryBuilder: QueryBuilder) => QueryBuilder) =>
@@ -1715,6 +1718,182 @@ const obj = new GraphORM({
17151718
},
17161719
},
17171720
},
1721+
OpportunityPreviewCompany: {
1722+
from: 'UserExperience',
1723+
requiredColumns: ['userId', 'verified', 'type', 'startedAt'],
1724+
additionalQuery: (_, alias, qb) =>
1725+
qb.leftJoin('company', 'c', `c.id = ${alias}."companyId"`),
1726+
fields: {
1727+
name: {
1728+
rawSelect: true,
1729+
select: (_, alias) => `COALESCE(c.name, ${alias}."customCompanyName")`,
1730+
},
1731+
favicon: {
1732+
rawSelect: true,
1733+
select: () => 'NULL',
1734+
},
1735+
},
1736+
},
1737+
OpportunityPreviewUser: {
1738+
from: 'User',
1739+
requiredColumns: ['id'],
1740+
fields: {
1741+
profileImage: {
1742+
select: 'image',
1743+
},
1744+
anonId: {
1745+
select: () => 'NULL',
1746+
transform: (_, ctx, parent) => {
1747+
const user = parent as User;
1748+
if (!user.id) return null;
1749+
1750+
// Deterministic hash from userId
1751+
let hash = 0;
1752+
for (let i = 0; i < user.id.length; i++) {
1753+
hash = (hash << 5) - hash + user.id.charCodeAt(i);
1754+
hash = hash & hash;
1755+
}
1756+
const totalCount =
1757+
(ctx as Context & { previewTotalCount?: number })
1758+
.previewTotalCount || 1000;
1759+
const anonNumber = (Math.abs(hash) % totalCount) + 1;
1760+
return `anon #${anonNumber}`;
1761+
},
1762+
},
1763+
description: {
1764+
select: () => 'NULL',
1765+
transform: async (_, ctx, parent) => {
1766+
const user = parent as User;
1767+
try {
1768+
const profile = await snotraClient.getProfile({
1769+
user_id: user.id,
1770+
});
1771+
return profile?.profile_text || null;
1772+
} catch (error) {
1773+
return null;
1774+
}
1775+
},
1776+
},
1777+
openToWork: {
1778+
select: (_, alias, qb) =>
1779+
qb
1780+
.select('ucp.status')
1781+
.from('user_candidate_preference', 'ucp')
1782+
.where(`ucp."userId" = ${alias}.id`)
1783+
.limit(1),
1784+
transform: (status: number | null): boolean => status === 1,
1785+
},
1786+
seniority: {
1787+
select: 'experienceLevel',
1788+
},
1789+
company: {
1790+
relation: {
1791+
isMany: false,
1792+
customRelation: (_, parentAlias, childAlias, qb): QueryBuilder =>
1793+
qb
1794+
.where(`${childAlias}."userId" = ${parentAlias}.id`)
1795+
.andWhere(`${childAlias}.verified = true`)
1796+
.andWhere(`${childAlias}.type = '1'`)
1797+
.orderBy(`${childAlias}."startedAt"`, 'DESC')
1798+
.limit(1),
1799+
},
1800+
},
1801+
location: {
1802+
select: (_, alias) => `
1803+
COALESCE(
1804+
(
1805+
SELECT jsonb_build_object(
1806+
'city', location->0->>'city',
1807+
'subdivision', location->0->>'subdivision',
1808+
'country', location->0->>'country'
1809+
)
1810+
FROM user_candidate_preference
1811+
WHERE "userId" = ${alias}.id
1812+
LIMIT 1
1813+
),
1814+
${alias}.flags
1815+
)
1816+
`,
1817+
transform: (data: Record<string, unknown>): string | null => {
1818+
if (!data) return null;
1819+
1820+
if (data.city || data.subdivision || data.country) {
1821+
return [data.city, data.subdivision, data.country]
1822+
.filter(Boolean)
1823+
.join(', ');
1824+
}
1825+
1826+
return null;
1827+
},
1828+
},
1829+
1830+
lastActivity: {
1831+
select: () => 'NULL',
1832+
transform: async (_, ctx, parent) => {
1833+
const user = parent as User;
1834+
if (!user.id) {
1835+
return null;
1836+
}
1837+
return await ctx.dataLoader.userLastActive.load({
1838+
userId: user.id,
1839+
});
1840+
},
1841+
},
1842+
topTags: {
1843+
select: () => 'NULL',
1844+
transform: async (_, ctx, parent) => {
1845+
const user = parent as User;
1846+
if (!user.id) {
1847+
return null;
1848+
}
1849+
try {
1850+
const tags = await getUserTopReadingTags(ctx.con, {
1851+
userId: user.id,
1852+
limit: 5,
1853+
readLimit: 100,
1854+
});
1855+
return tags && tags.length > 0 ? tags.map((t) => t.tag) : null;
1856+
} catch (error) {
1857+
return null;
1858+
}
1859+
},
1860+
},
1861+
recentlyRead: {
1862+
select: (_, alias, qb) =>
1863+
qb.select(`
1864+
ARRAY(
1865+
SELECT jsonb_build_object(
1866+
'tag', utr."keywordValue",
1867+
'issuedAt', utr."issuedAt"
1868+
)
1869+
FROM user_top_reader utr
1870+
WHERE utr."userId" = ${alias}.id
1871+
ORDER BY utr."issuedAt" DESC
1872+
LIMIT 3
1873+
)
1874+
`),
1875+
transform: (
1876+
badges: Array<{ tag: string; issuedAt: string }> | null,
1877+
): Array<{ tag: string; issuedAt: string }> | null => {
1878+
return badges && badges.length > 0 ? badges : null;
1879+
},
1880+
},
1881+
activeSquads: {
1882+
select: (_, alias) => `
1883+
ARRAY(
1884+
SELECT sm."sourceId"
1885+
FROM source_member sm
1886+
INNER JOIN source s ON s.id = sm."sourceId"
1887+
WHERE sm."userId" = ${alias}.id
1888+
AND s.type = '${SourceType.Squad}'
1889+
AND s.active = true
1890+
ORDER BY sm."createdAt" DESC
1891+
LIMIT 5
1892+
)
1893+
`,
1894+
},
1895+
},
1896+
},
17181897
});
17191898

17201899
export default obj;

0 commit comments

Comments
 (0)