Skip to content

Commit 3a0a134

Browse files
committed
feat: parse cv to profile
1 parent 49a6574 commit 3a0a134

File tree

7 files changed

+181
-63
lines changed

7 files changed

+181
-63
lines changed

.infra/common.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,10 @@ export const workers: Worker[] = [
437437
topic: 'api.v1.recruiter-rejected-candidate-match',
438438
subscription: 'api.recruiter-rejected-candidate-match-email',
439439
},
440+
{
441+
topic: 'api.v1.candidate-preference-updated',
442+
subscription: 'api.parse-cv-profile',
443+
},
440444
];
441445

442446
export const personalizedDigestWorkers: Worker[] = [

bin/importProfileFromJSON.ts

Lines changed: 5 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,7 @@ import { z } from 'zod';
55
import createOrGetConnection from '../src/db';
66
import { type DataSource } from 'typeorm';
77
import { readFile } from 'node:fs/promises';
8-
import { userExperienceInputBaseSchema } from '../src/common/schema/profile';
9-
import { UserExperienceType } from '../src/entity/user/experiences/types';
10-
import {
11-
importUserExperienceWork,
12-
importUserExperienceEducation,
13-
importUserExperienceCertification,
14-
importUserExperienceProject,
15-
} from '../src/common/profile/import';
8+
import { importUserExperienceFromJSON } from '../src/common/profile/import';
169

1710
/**
1811
* Import profile from JSON to user by id
@@ -47,55 +40,10 @@ const main = async () => {
4740

4841
const dataJSON = JSON.parse(await readFile(params.path, 'utf-8'));
4942

50-
const data = z
51-
.array(
52-
userExperienceInputBaseSchema
53-
.pick({
54-
type: true,
55-
})
56-
.loose(),
57-
)
58-
.parse(dataJSON);
59-
60-
await con.transaction(async (entityManager) => {
61-
for (const item of data) {
62-
switch (item.type) {
63-
case UserExperienceType.Work:
64-
await importUserExperienceWork({
65-
data: item,
66-
con: entityManager,
67-
userId: params.userId,
68-
});
69-
70-
break;
71-
case UserExperienceType.Education:
72-
await importUserExperienceEducation({
73-
data: item,
74-
con: entityManager,
75-
userId: params.userId,
76-
});
77-
78-
break;
79-
case UserExperienceType.Certification:
80-
await importUserExperienceCertification({
81-
data: item,
82-
con: entityManager,
83-
userId: params.userId,
84-
});
85-
86-
break;
87-
case UserExperienceType.Project:
88-
await importUserExperienceProject({
89-
data: item,
90-
con: entityManager,
91-
userId: params.userId,
92-
});
93-
94-
break;
95-
default:
96-
throw new Error(`Unsupported experience type: ${item.type}`);
97-
}
98-
}
43+
await importUserExperienceFromJSON({
44+
con: con.manager,
45+
dataJson: dataJSON,
46+
userId: params.userId,
9947
});
10048
} catch (error) {
10149
console.error(error instanceof z.ZodError ? z.prettifyError(error) : error);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"@connectrpc/connect-fastify": "^1.6.1",
3737
"@connectrpc/connect-node": "^1.6.1",
3838
"@dailydotdev/graphql-redis-subscriptions": "^2.4.3",
39-
"@dailydotdev/schema": "0.2.50",
39+
"@dailydotdev/schema": "0.2.51",
4040
"@dailydotdev/ts-ioredis-pool": "^1.0.2",
4141
"@fastify/cookie": "^11.0.2",
4242
"@fastify/cors": "^11.1.0",

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/common/profile/import.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type EntityManager } from 'typeorm';
22
import {
33
userExperienceCertificationImportSchema,
44
userExperienceEducationImportSchema,
5+
userExperienceInputBaseSchema,
56
userExperienceProjectImportSchema,
67
userExperienceWorkImportSchema,
78
} from '../../../src/common/schema/profile';
@@ -15,6 +16,8 @@ import { DatasetLocation } from '../../../src/entity/dataset/DatasetLocation';
1516
import { UserExperienceEducation } from '../../../src/entity/user/experiences/UserExperienceEducation';
1617
import { UserExperienceCertification } from '../../../src/entity/user/experiences/UserExperienceCertification';
1718
import { UserExperienceProject } from '../../../src/entity/user/experiences/UserExperienceProject';
19+
import z from 'zod';
20+
import { UserExperienceType } from '../../entity/user/experiences/types';
1821

1922
const resolveUserCompanyPart = async ({
2023
name,
@@ -306,3 +309,68 @@ export const importUserExperienceProject = async ({
306309
experienceId,
307310
};
308311
};
312+
313+
export const importUserExperienceFromJSON = async ({
314+
con,
315+
dataJson,
316+
userId,
317+
}: {
318+
con: EntityManager;
319+
dataJson: unknown;
320+
userId: string;
321+
}) => {
322+
if (!userId) {
323+
throw new Error('userId is required');
324+
}
325+
326+
const data = z
327+
.array(
328+
userExperienceInputBaseSchema
329+
.pick({
330+
type: true,
331+
})
332+
.loose(),
333+
)
334+
.parse(dataJson);
335+
336+
await con.transaction(async (entityManager) => {
337+
for (const item of data) {
338+
switch (item.type) {
339+
case UserExperienceType.Work:
340+
await importUserExperienceWork({
341+
data: item,
342+
con: entityManager,
343+
userId,
344+
});
345+
346+
break;
347+
case UserExperienceType.Education:
348+
await importUserExperienceEducation({
349+
data: item,
350+
con: entityManager,
351+
userId,
352+
});
353+
354+
break;
355+
case UserExperienceType.Certification:
356+
await importUserExperienceCertification({
357+
data: item,
358+
con: entityManager,
359+
userId,
360+
});
361+
362+
break;
363+
case UserExperienceType.Project:
364+
await importUserExperienceProject({
365+
data: item,
366+
con: entityManager,
367+
userId,
368+
});
369+
370+
break;
371+
default:
372+
throw new Error(`Unsupported experience type: ${item.type}`);
373+
}
374+
}
375+
});
376+
};

src/entity/user/User.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export type UserFlags = Partial<{
4444
lng: number | null | undefined;
4545
};
4646
subdivision: string | null;
47+
lastCVParseAt: Date;
4748
}>;
4849

4950
export type UserFlagsPublic = Pick<UserFlags, 'showPlusGift'>;
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import {
2+
BrokkrParseRequest,
3+
CandidatePreferenceUpdated,
4+
} from '@dailydotdev/schema';
5+
import type { TypedWorker } from '../worker';
6+
import { User } from '../../entity/user/User';
7+
import { getBrokkrClient } from '../../common/brokkr';
8+
import { updateFlagsStatement } from '../../common/utils';
9+
import { importUserExperienceFromJSON } from '../../common/profile/import';
10+
11+
export const parseCVProfileWorker: TypedWorker<'api.v1.candidate-preference-updated'> =
12+
{
13+
subscription: 'api.parse-cv-profile',
14+
parseMessage: ({ data }) => CandidatePreferenceUpdated.fromBinary(data),
15+
handler: async ({ data }, con) => {
16+
const { userId, cv } = data.payload || {};
17+
18+
if (!cv?.blob || !cv?.bucket) {
19+
return;
20+
}
21+
22+
if (!cv?.lastModified) {
23+
return;
24+
}
25+
26+
if (!userId) {
27+
return;
28+
}
29+
30+
const user: Pick<User, 'flags'> | null = await con
31+
.getRepository(User)
32+
.findOne({
33+
select: ['flags'],
34+
where: {
35+
id: userId,
36+
},
37+
});
38+
39+
if (!user) {
40+
return;
41+
}
42+
43+
const lastModifiedCVDate = new Date(cv.lastModified);
44+
45+
if (Number.isNaN(lastModifiedCVDate.getTime())) {
46+
return;
47+
}
48+
49+
const lastProfileParseDate = user.flags.lastCVParseAt || new Date(0);
50+
51+
if (lastModifiedCVDate <= lastProfileParseDate) {
52+
return;
53+
}
54+
55+
const brokkrClient = getBrokkrClient();
56+
57+
try {
58+
await con.getRepository(User).update(
59+
{ id: userId },
60+
{
61+
flags: updateFlagsStatement<User>({
62+
lastCVParseAt: new Date(),
63+
}),
64+
},
65+
);
66+
67+
const result = await brokkrClient.garmr.execute(() => {
68+
return brokkrClient.instance.parseCV(
69+
new BrokkrParseRequest({
70+
bucketName: cv.bucket,
71+
blobName: cv.blob,
72+
}),
73+
);
74+
});
75+
76+
const dataJson = JSON.parse(result.parsedCv);
77+
78+
await importUserExperienceFromJSON({
79+
con: con.manager,
80+
dataJson,
81+
userId,
82+
});
83+
} catch (error) {
84+
// revert to previous date on error
85+
await con.getRepository(User).update(
86+
{ id: userId },
87+
{
88+
flags: updateFlagsStatement<User>({
89+
lastCVParseAt: user.flags.lastCVParseAt,
90+
}),
91+
},
92+
);
93+
94+
throw error;
95+
}
96+
},
97+
};

0 commit comments

Comments
 (0)