Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ export async function saveCompanyIfNecessary(
async () => {
return runActor({
actorId: 'harvestapi~linkedin-company',
body: { companies: [companyNameOrLinkedInId] },
body: {
companies: [
`https://www.linkedin.com/company/${companyNameOrLinkedInId}`,
],
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Company lookup broken when using company names

High Severity

The saveCompanyIfNecessary function accepts both company names and LinkedIn IDs (as indicated by the parameter name and database query), but the new URL construction assumes the input is always a LinkedIn ID or slug. When called from offers.ts with human-readable company names like "Google" or "The Walt Disney Company", the constructed URL https://www.linkedin.com/company/${companyNameOrLinkedInId} will likely fail since company display names rarely match their LinkedIn universal names exactly. Additionally, company names with spaces aren't URL-encoded, creating malformed URLs. This breaks the offer creation flow for any company not already in the database.

Fix in Cursor Fix in Web

});
}
);
Expand Down
37 changes: 18 additions & 19 deletions packages/core/src/modules/linkedin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,19 +324,18 @@ const LinkedInLocation = z.object({
});

const LinkedInProfile = z.object({
element: z.object({
education: z.array(LinkedInEducation),
experience: z.array(LinkedInExperience),
headline: z.string().nullish(),
location: LinkedInLocation,
openToWork: z.boolean().nullish(),
photo: z.string().url().nullish(),
}),
education: z.array(LinkedInEducation),
error: z.undefined(),
experience: z.array(LinkedInExperience),
headline: z.string().nullish(),
location: LinkedInLocation,
openToWork: z.boolean().nullish(),
photo: z.string().url().nullish(),
originalQuery: z.object({ url: z.string() }),
});

const LinkedInFailure = z.object({
element: z.null(),
error: z.array(z.any()),
originalQuery: z.object({ url: z.string() }),
});

Expand All @@ -345,11 +344,11 @@ const LinkedInResult = z.union([LinkedInProfile, LinkedInFailure]);
// Types

type LinkedInEducation = NonNullable<
z.infer<typeof LinkedInProfile>['element']['education'][number]
z.infer<typeof LinkedInProfile>['education'][number]
>;

type LinkedInExperience = NonNullable<
z.infer<typeof LinkedInProfile>['element']['experience'][number]
z.infer<typeof LinkedInProfile>['experience'][number]
>;

type LinkedInProfile = z.infer<typeof LinkedInProfile>;
Expand Down Expand Up @@ -430,7 +429,7 @@ export async function syncLinkedInProfiles(

// This is the case where there are multiple members with the same
// LinkedIn URL, something that should be fixed.
if (!profile.element || !member) {
if (profile.error || !member) {
await finishSync(memberId);

console.log(`Profile not found for ${memberId}, moving on.`);
Expand Down Expand Up @@ -746,7 +745,7 @@ async function scrapeProfiles(profilesToScrape: string[]) {
await db.transaction().execute(async (trx) => {
await Promise.all(
newResults.map(async (result) => {
if (result.element) {
if (!result.error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cache key unchanged despite schema change causing parse failures

High Severity

The cache key version remains v2 despite the schema change from element-wrapped structure to flat structure. Cached data from before this change has the old format (e.g., { element: { education: [...] } }), but LinkedInResult.parse(value) at line 652 now expects the new flat format. This causes Zod parse errors when processing members with old cached profiles. Since there's no try-catch around the parse and it's inside Promise.all, the entire batch fails. The cache key version needs to be incremented (e.g., to v3) to invalidate stale cached data.

Additional Locations (1)

Fix in Cursor Fix in Web

return successfulResults.push(result);
}

Expand Down Expand Up @@ -803,7 +802,7 @@ async function processProfile({
}
});

const checkEducationPromises = profile.element.education.map(
const checkEducationPromises = profile.education.map(
async (education) => {
if (!education) {
return;
Expand All @@ -826,7 +825,7 @@ async function processProfile({
}
);

const checkExperiencesPromises = profile.element.experience.map(
const checkExperiencesPromises = profile.experience.map(
async (experience) => {
if (!experience) {
return;
Expand Down Expand Up @@ -896,7 +895,7 @@ type CheckMemberInput = {

async function checkMember({ member, profile, trx }: CheckMemberInput) {
const updatedLocation = await run(async () => {
const locationFromLinkedIn = profile.element.location.parsed?.text;
const locationFromLinkedIn = profile.location.parsed?.text;

if (!locationFromLinkedIn) {
return null;
Expand All @@ -914,18 +913,18 @@ async function checkMember({ member, profile, trx }: CheckMemberInput) {
return getMostRelevantLocation(locationFromLinkedIn, 'geocode');
});

if (!!profile.element.photo && !profile.element.openToWork) {
if (!!profile.photo && !profile.openToWork) {
await uploadProfilePicture({
memberId: member.id,
pictureUrl: profile.element.photo,
pictureUrl: profile.photo,
});
}

return trx
.updateTable('students')
.set({
...(!member.headline && {
headline: profile.element.headline,
headline: profile.headline,
}),
...(!!updatedLocation && {
currentLocation: updatedLocation.formattedAddress,
Expand Down