diff --git a/packages/core/src/modules/employment/employment.types.ts b/packages/core/src/modules/employment/employment.types.ts index b89873293..9e0df8363 100644 --- a/packages/core/src/modules/employment/employment.types.ts +++ b/packages/core/src/modules/employment/employment.types.ts @@ -10,7 +10,7 @@ import { Student, } from '@oyster/types'; -import { parseLinkedInCompanySlug } from '@/modules/employment/use-cases/fetch-company-from-linkedin'; +import { parseLinkedInCompanySlug } from '@/modules/employment/employment.utils'; import { ListSearchParams } from '@/shared/types'; // Enums diff --git a/packages/core/src/modules/employment/employment.utils.ts b/packages/core/src/modules/employment/employment.utils.ts new file mode 100644 index 000000000..d6584f14a --- /dev/null +++ b/packages/core/src/modules/employment/employment.utils.ts @@ -0,0 +1,25 @@ +/** + * Normalizes user input into a LinkedIn company slug. + * + * Accepts either a full LinkedIn company URL or a bare slug, so forms can be + * lenient about what they accept. + * + * This lives apart from the use cases that fetch from LinkedIn because it is + * also needed by `employment.types.ts`, which is bundled for the browser. Any + * import of a use case from there pulls Redis and the Sentry Node SDK into the + * client build. + * + * @example parseLinkedInCompanySlug('https://www.linkedin.com/company/google') // 'google' + * @example parseLinkedInCompanySlug('@google') // 'google' + */ +export function parseLinkedInCompanySlug(input: string) { + const trimmed = input.trim(); + + const match = trimmed.match(/linkedin\.com\/company\/([^/?#]+)/i); + + if (match?.[1]) { + return match[1]; + } + + return trimmed.replace(/^@/, ''); +} diff --git a/packages/core/src/modules/employment/use-cases/fetch-company-from-linkedin.ts b/packages/core/src/modules/employment/use-cases/fetch-company-from-linkedin.ts index ac476a28d..555380ed3 100644 --- a/packages/core/src/modules/employment/use-cases/fetch-company-from-linkedin.ts +++ b/packages/core/src/modules/employment/use-cases/fetch-company-from-linkedin.ts @@ -2,29 +2,46 @@ import { z } from 'zod'; import { withCache } from '@/infrastructure/redis'; import { runActor } from '@/modules/apify'; +import { parseLinkedInCompanySlug } from '@/modules/employment/employment.utils'; import { ColorStackError } from '@/shared/errors'; -const LinkedInCompany = z.object({ +const LinkedInLogo = z.object({ + height: z.number().optional(), + url: z.string().url(), + width: z.number().optional(), +}); + +const LinkedInCompanyResponse = z.object({ description: z.string().nullish(), id: z.string(), logo: z.string().url().optional(), + logos: LinkedInLogo.array().optional(), name: z.string(), universalName: z.string(), website: z.string().url().nullish(), }); -export type LinkedInCompany = z.infer; - -export function parseLinkedInCompanySlug(input: string) { - const trimmed = input.trim(); +const LinkedInCompany = LinkedInCompanyResponse.transform((company) => { + return { + description: company.description, + id: company.id, + logo: company.logo ?? getBestLogoUrl(company.logos), + name: company.name, + universalName: company.universalName, + website: company.website, + }; +}); - const match = trimmed.match(/linkedin\.com\/company\/([^/?#]+)/i); +export type LinkedInCompany = z.infer; - if (match?.[1]) { - return match[1]; +function getBestLogoUrl(logos: z.infer[] | undefined) { + if (!logos?.length) { + return undefined; } - return trimmed.replace(/^@/, ''); + return logos.reduce((best, current) => { + return (current.width ?? 0) > (best.width ?? 0) ? current : best; + }).url; } export async function fetchCompanyFromLinkedIn(