diff --git a/services/apps/premium/members_enrichment_worker/src/factory.ts b/services/apps/premium/members_enrichment_worker/src/factory.ts index 6ea8143162..754df359cf 100644 --- a/services/apps/premium/members_enrichment_worker/src/factory.ts +++ b/services/apps/premium/members_enrichment_worker/src/factory.ts @@ -3,6 +3,7 @@ import { Logger } from '@crowd/logging' import { MemberEnrichmentSource } from '@crowd/types' import EnrichmentServiceClearbit from './sources/clearbit/service' +import EnrichmentServiceCrustdata from './sources/crustdata/service' import EnrichmentServiceProgAILinkedinScraper from './sources/progai-linkedin-scraper/service' import EnrichmentServiceProgAI from './sources/progai/service' import EnrichmentServiceSerpApi from './sources/serp/service' @@ -27,6 +28,8 @@ export class EnrichmentSourceServiceFactory { return new EnrichmentServiceSerpApi(log) case MemberEnrichmentSource.PROGAI_LINKEDIN_SCRAPER: return new EnrichmentServiceProgAILinkedinScraper(log) + case MemberEnrichmentSource.CRUSTDATA: + return new EnrichmentServiceCrustdata(log) default: throw new Error(`Enrichment service for ${source} is not found!`) } diff --git a/services/apps/premium/members_enrichment_worker/src/main.ts b/services/apps/premium/members_enrichment_worker/src/main.ts index e255b09aa0..7603252158 100644 --- a/services/apps/premium/members_enrichment_worker/src/main.ts +++ b/services/apps/premium/members_enrichment_worker/src/main.ts @@ -12,6 +12,8 @@ const config: Config = { 'CROWD_ENRICHMENT_CLEARBIT_API_KEY', 'CROWD_ENRICHMENT_SERP_API_URL', 'CROWD_ENRICHMENT_SERP_API_KEY', + 'CROWD_ENRICHMENT_CRUSTDATA_URL', + 'CROWD_ENRICHMENT_CRUSTDATA_API_KEY', ], producer: { enabled: false, diff --git a/services/apps/premium/members_enrichment_worker/src/sources/crustdata/service.ts b/services/apps/premium/members_enrichment_worker/src/sources/crustdata/service.ts new file mode 100644 index 0000000000..6e2e20808e --- /dev/null +++ b/services/apps/premium/members_enrichment_worker/src/sources/crustdata/service.ts @@ -0,0 +1,343 @@ +import axios from 'axios' + +import { isEmail } from '@crowd/common' +import { Logger, LoggerBase } from '@crowd/logging' +import { + IMemberEnrichmentCache, + IMemberIdentity, + MemberAttributeName, + MemberEnrichmentSource, + MemberIdentityType, + OrganizationIdentityType, + OrganizationSource, + PlatformType, +} from '@crowd/types' + +import { findMemberEnrichmentCacheForAllSources } from '../../activities/enrichment' +import { EnrichmentSourceServiceFactory } from '../../factory' +import { + IEnrichmentService, + IEnrichmentSourceInput, + IMemberEnrichmentAttributeSettings, + IMemberEnrichmentData, + IMemberEnrichmentDataNormalized, +} from '../../types' +import { normalizeAttributes, normalizeSocialIdentity } from '../../utils/common' + +import { + IMemberEnrichmentCrustdataAPIErrorResponse, + IMemberEnrichmentCrustdataAPIResponse, + IMemberEnrichmentCrustdataRemainingCredits, + IMemberEnrichmentDataCrustdata, +} from './types' + +export default class EnrichmentServiceCrustdata extends LoggerBase implements IEnrichmentService { + public source: MemberEnrichmentSource = MemberEnrichmentSource.CRUSTDATA + public platform = `enrichment-${this.source}` + + public alsoFindInputsInSourceCaches: MemberEnrichmentSource[] = [ + MemberEnrichmentSource.PROGAI, + MemberEnrichmentSource.CLEARBIT, + MemberEnrichmentSource.SERP, + ] + + public enrichMembersWithActivityMoreThan = 1000 + + public enrichableBySql = `("activitySummary".total_count > ${this.enrichMembersWithActivityMoreThan}) AND mi.verified AND mi.type = 'username' and mi.platform = 'linkedin'` + + public cacheObsoleteAfterSeconds = 60 * 60 * 24 * 90 + + public attributeSettings: IMemberEnrichmentAttributeSettings = { + [MemberAttributeName.AVATAR_URL]: { + fields: ['profile_picture_url'], + }, + [MemberAttributeName.JOB_TITLE]: { + fields: ['title'], + }, + [MemberAttributeName.BIO]: { + fields: ['summary', 'headline'], + }, + [MemberAttributeName.SKILLS]: { + fields: ['skills'], + transform: (skills: string) => skills.split(',').sort(), + }, + [MemberAttributeName.LANGUAGES]: { + fields: ['languages'], + transform: (languages: string[]) => languages.sort(), + }, + [MemberAttributeName.SCHOOLS]: { + fields: ['all_schools'], + transform: (schools: string[]) => schools.sort(), + }, + } + + constructor(public readonly log: Logger) { + super(log) + } + + async isEnrichableBySource(input: IEnrichmentSourceInput): Promise { + const caches = await findMemberEnrichmentCacheForAllSources(input.memberId) + + let hasEnrichableLinkedinInCache = false + for (const cache of caches) { + if (this.alsoFindInputsInSourceCaches.includes(cache.source)) { + const service = EnrichmentSourceServiceFactory.getEnrichmentSourceService( + cache.source, + this.log, + ) + const normalized = service.normalize(cache.data) as IMemberEnrichmentDataNormalized + if (normalized.identities.some((i) => i.platform === PlatformType.LINKEDIN)) { + hasEnrichableLinkedinInCache = true + break + } + } + } + + return ( + input.activityCount > this.enrichMembersWithActivityMoreThan && + (hasEnrichableLinkedinInCache || + (input.linkedin && input.linkedin.value && input.linkedin.verified)) + ) + } + + async hasRemainingCredits(): Promise { + try { + const config = { + method: 'get', + url: `${process.env['CROWD_ENRICHMENT_CRUSTDATA_URL']}/user/credits`, + headers: { + Authorization: `Token ${process.env['CROWD_ENRICHMENT_CRUSTDATA_API_KEY']}`, + }, + } + + const response: IMemberEnrichmentCrustdataRemainingCredits = (await axios(config)).data + + // realtime linkedin enrichment costs 5 credits + return response.credits > 5 + } catch (error) { + this.log.error('Error while checking Crustdata account usage', error) + return false + } + } + + async getData(input: IEnrichmentSourceInput): Promise { + const profiles: IMemberEnrichmentDataCrustdata[] = [] + const caches = await findMemberEnrichmentCacheForAllSources(input.memberId) + + const consumableIdentities = await this.findDistinctScrapableLinkedinIdentities(input, caches) + + for (const identity of consumableIdentities) { + const data = await this.getDataUsingLinkedinHandle(identity.value) + if (data) { + profiles.push({ + ...data, + metadata: { + repeatedTimesInDifferentSources: identity.repeatedTimesInDifferentSources, + isFromVerifiedSource: identity.isFromVerifiedSource, + }, + }) + } + } + + return profiles.length > 0 ? profiles : null + } + + private async getDataUsingLinkedinHandle( + handle: string, + ): Promise { + const url = `${process.env['CROWD_ENRICHMENT_CRUSTDATA_URL']}/screener/person/enrich` + const config = { + method: 'get', + url, + params: { + linkedin_profile_url: `https://linkedin.com/in/${handle}`, + enrich_realtime: true, + }, + headers: { + Authorization: `Token ${process.env['CROWD_ENRICHMENT_CRUSTDATA_API_KEY']}`, + }, + } + + const response: IMemberEnrichmentCrustdataAPIResponse[] = (await axios(config)).data + + if (response.length === 0 || this.isErrorResponse(response[0])) { + return null + } + + return response[0] + } + + private isErrorResponse( + response: IMemberEnrichmentCrustdataAPIResponse, + ): response is IMemberEnrichmentCrustdataAPIErrorResponse { + return (response as IMemberEnrichmentCrustdataAPIErrorResponse).error !== undefined + } + + private async findDistinctScrapableLinkedinIdentities( + input: IEnrichmentSourceInput, + caches: IMemberEnrichmentCache[], + ): Promise< + (IMemberIdentity & { repeatedTimesInDifferentSources: number; isFromVerifiedSource: boolean })[] + > { + const consumableIdentities: (IMemberIdentity & { + repeatedTimesInDifferentSources: number + isFromVerifiedSource: boolean + })[] = [] + const linkedinUrlHashmap = new Map() + + for (const cache of caches) { + if (this.alsoFindInputsInSourceCaches.includes(cache.source)) { + const service = EnrichmentSourceServiceFactory.getEnrichmentSourceService( + cache.source, + this.log, + ) + const normalized = service.normalize(cache.data) as IMemberEnrichmentDataNormalized + if (normalized.identities.some((i) => i.platform === PlatformType.LINKEDIN)) { + const identity = normalized.identities.find((i) => i.platform === PlatformType.LINKEDIN) + if (!linkedinUrlHashmap.get(identity.value)) { + consumableIdentities.push({ + ...identity, + repeatedTimesInDifferentSources: 1, + isFromVerifiedSource: false, + }) + linkedinUrlHashmap.set(identity.value, 1) + } else { + const repeatedTimesInDifferentSources = linkedinUrlHashmap.get(identity.value) + 1 + linkedinUrlHashmap.set(identity.value, repeatedTimesInDifferentSources) + consumableIdentities.find( + (i) => i.value === identity.value, + ).repeatedTimesInDifferentSources = repeatedTimesInDifferentSources + } + } + } + } + + if (input.linkedin && input.linkedin.value && input.linkedin.verified) { + if (!linkedinUrlHashmap.get(input.linkedin.value)) { + consumableIdentities.push({ + ...input.linkedin, + repeatedTimesInDifferentSources: 1, + isFromVerifiedSource: true, + }) + } else { + const repeatedTimesInDifferentSources = linkedinUrlHashmap.get(input.linkedin.value) + 1 + const identityFound = consumableIdentities.find((i) => i.value === input.linkedin.value) + + identityFound.repeatedTimesInDifferentSources = repeatedTimesInDifferentSources + identityFound.isFromVerifiedSource = true + } + } + return consumableIdentities + } + + normalize(profiles: IMemberEnrichmentDataCrustdata[]): IMemberEnrichmentDataNormalized[] { + const normalizedProfiles: IMemberEnrichmentDataNormalized[] = [] + + for (const profile of profiles) { + const profileNormalized = this.normalizeOneResult(profile) + normalizedProfiles.push({ ...profileNormalized, metadata: profile.metadata }) + } + + return normalizedProfiles.length > 0 ? normalizedProfiles : null + } + + private normalizeOneResult( + data: IMemberEnrichmentDataCrustdata, + ): IMemberEnrichmentDataNormalized { + let normalized: IMemberEnrichmentDataNormalized = { + identities: [], + attributes: {}, + memberOrganizations: [], + reach: {}, + } + + normalized = this.normalizeIdentities(data, normalized) + normalized = normalizeAttributes(data, normalized, this.attributeSettings, this.platform) + normalized = this.normalizeEmployment(data, normalized) + + if (data.num_of_connections) { + normalized.reach[this.platform] = data.num_of_connections + } + + return normalized + } + + private normalizeIdentities( + data: IMemberEnrichmentDataCrustdata, + normalized: IMemberEnrichmentDataNormalized, + ): IMemberEnrichmentDataNormalized { + if (!normalized.identities) { + normalized.identities = [] + } + + if (!normalized.attributes) { + normalized.attributes = {} + } + + if (data.name) { + normalized.displayName = data.name + } + + if (data.email) { + for (const email of data.email.split(',').filter(isEmail)) { + normalized.identities.push({ + type: MemberIdentityType.EMAIL, + platform: this.platform, + value: email.trim(), + verified: false, + }) + } + } + + if (data.twitter_handle) { + normalized = normalizeSocialIdentity( + { + handle: data.twitter_handle, + platform: PlatformType.TWITTER, + }, + MemberIdentityType.USERNAME, + true, + normalized, + ) + } + + return normalized + } + + private normalizeEmployment( + data: IMemberEnrichmentDataCrustdata, + normalized: IMemberEnrichmentDataNormalized, + ): IMemberEnrichmentDataNormalized { + if (!normalized.memberOrganizations) { + normalized.memberOrganizations = [] + } + + const employmentInformation = (data.past_employers || []).concat(data.current_employers || []) + if (employmentInformation.length > 0) { + for (const workExperience of employmentInformation) { + const identities = [] + + if (workExperience.employer_linkedin_id) { + identities.push({ + platform: PlatformType.LINKEDIN, + value: `company:${workExperience.employer_linkedin_id}`, + type: OrganizationIdentityType.USERNAME, + verified: true, + }) + } + + normalized.memberOrganizations.push({ + name: workExperience.employer_name, + source: OrganizationSource.ENRICHMENT_CRUSTDATA, + identities, + title: workExperience.employee_title, + startDate: workExperience.start_date, + endDate: workExperience.end_date, + organizationDescription: workExperience.employer_linkedin_description, + }) + } + } + + return normalized + } +} diff --git a/services/apps/premium/members_enrichment_worker/src/sources/crustdata/types.ts b/services/apps/premium/members_enrichment_worker/src/sources/crustdata/types.ts new file mode 100644 index 0000000000..6fe2266261 --- /dev/null +++ b/services/apps/premium/members_enrichment_worker/src/sources/crustdata/types.ts @@ -0,0 +1,53 @@ +import { IMemberEnrichmentLinkedinScraperMetadata } from '../../types' + +export interface IMemberEnrichmentCrustdataEmployer { + employer_name: string + employer_linkedin_id: string + employer_linkedin_description?: string + employer_company_id: number[] + employee_position_id?: number + employee_title: string + employee_description?: string + employee_location: string + start_date: string + end_date: string +} + +export interface IMemberEnrichmentDataCrustdata { + linkedin_profile_url: string + linkedin_flagship_url: string + name: string + email: string + title: string + last_updated: string + headline: string + summary: string + num_of_connections: number + skills: string + profile_picture_url: string + twitter_handle: string + languages: string[] + all_employers: string[] + past_employers: IMemberEnrichmentCrustdataEmployer[] + current_employers: IMemberEnrichmentCrustdataEmployer[] + all_employers_company_id: number[] + all_titles: string[] + all_schools: string[] + all_degrees: string[] + metadata: IMemberEnrichmentLinkedinScraperMetadata +} + +export type IMemberEnrichmentCrustdataAPIResponse = + | IMemberEnrichmentDataCrustdata + | IMemberEnrichmentCrustdataAPIErrorResponse + +export interface IMemberEnrichmentCrustdataAPIErrorResponse { + error: string + linkedin_profile_url: string + last_tried_linkedin_enrichment_date: string + did_last_linkedin_enrichment_succeed: boolean +} + +export interface IMemberEnrichmentCrustdataRemainingCredits { + credits: number +} diff --git a/services/apps/premium/members_enrichment_worker/src/sources/progai-linkedin-scraper/service.ts b/services/apps/premium/members_enrichment_worker/src/sources/progai-linkedin-scraper/service.ts index 29f95d4187..f5c4d45f3a 100644 --- a/services/apps/premium/members_enrichment_worker/src/sources/progai-linkedin-scraper/service.ts +++ b/services/apps/premium/members_enrichment_worker/src/sources/progai-linkedin-scraper/service.ts @@ -57,8 +57,6 @@ export default class EnrichmentServiceProgAILinkedinScraper hasEnrichableLinkedinInCache = true break } - - break } } @@ -72,7 +70,57 @@ export default class EnrichmentServiceProgAILinkedinScraper return true } - private async findConsumableLinkedinIdentities( + async getData( + input: IEnrichmentSourceInput, + ): Promise { + const profiles: IMemberEnrichmentDataProgAILinkedinScraper[] = [] + const caches = await findMemberEnrichmentCacheForAllSources(input.memberId) + + const consumableIdentities = await this.findDistinctScrapableLinkedinIdentities(input, caches) + + for (const identity of consumableIdentities) { + const data = await this.getDataUsingLinkedinHandle(identity.value) + if (data) { + const existingProgaiCache = caches.find((c) => c.source === MemberEnrichmentSource.PROGAI) + // we don't want to reinforce the cache with the same data, only save to cache + // if a new profile is returned from progai + if ((existingProgaiCache?.data as IMemberEnrichmentDataProgAI)?.id == data.id) { + continue + } + profiles.push({ + ...data, + metadata: { + repeatedTimesInDifferentSources: identity.repeatedTimesInDifferentSources, + isFromVerifiedSource: identity.isFromVerifiedSource, + }, + }) + } + } + + return profiles.length > 0 ? profiles : null + } + + private async getDataUsingLinkedinHandle( + handle: string, + ): Promise { + const url = `${process.env['CROWD_ENRICHMENT_PROGAI_URL']}/get_profile` + const config = { + method: 'get', + url, + params: { + linkedin_url: `https://linkedin.com/in/${handle}`, + with_emails: true, + api_key: process.env['CROWD_ENRICHMENT_PROGAI_API_KEY'], + }, + headers: {}, + } + + const response: IMemberEnrichmentDataProgAIResponse = (await axios(config)).data + + return response?.profile || null + } + + private async findDistinctScrapableLinkedinIdentities( input: IEnrichmentSourceInput, caches: IMemberEnrichmentCache[], ): Promise< @@ -130,58 +178,6 @@ export default class EnrichmentServiceProgAILinkedinScraper return consumableIdentities } - async getData( - input: IEnrichmentSourceInput, - ): Promise { - const profiles: IMemberEnrichmentDataProgAILinkedinScraper[] = [] - const caches = await findMemberEnrichmentCacheForAllSources(input.memberId) - - const consumableIdentities = await this.findConsumableLinkedinIdentities(input, caches) - - for (const identity of consumableIdentities) { - const data = await this.getDataUsingLinkedinHandle(identity.value) - if (data) { - const existingProgaiCache = caches.find((c) => c.source === MemberEnrichmentSource.PROGAI) - // we don't want to reinforce the cache with the same data, only save to cache - // if a new profile is returned from progai - if ( - existingProgaiCache && - existingProgaiCache.data && - (existingProgaiCache.data as IMemberEnrichmentDataProgAI).id == data.id - ) { - continue - } - profiles.push({ - ...data, - metadata: { - repeatedTimesInDifferentSources: identity.repeatedTimesInDifferentSources, - isFromVerifiedSource: identity.isFromVerifiedSource, - }, - }) - } - } - - return profiles.length > 0 ? profiles : null - } - - private async getDataUsingLinkedinHandle(handle: string): Promise { - const url = `${process.env['CROWD_ENRICHMENT_PROGAI_URL']}/get_profile` - const config = { - method: 'get', - url, - params: { - linkedin_url: `https://linkedin.com/in/${handle}`, - with_emails: true, - api_key: process.env['CROWD_ENRICHMENT_PROGAI_API_KEY'], - }, - headers: {}, - } - - const response: IMemberEnrichmentDataProgAIResponse = (await axios(config)).data - - return response?.profile || null - } - normalize( profiles: IMemberEnrichmentDataProgAILinkedinScraper[], ): IMemberEnrichmentDataNormalized[] { diff --git a/services/apps/premium/members_enrichment_worker/src/sources/progai-linkedin-scraper/types.ts b/services/apps/premium/members_enrichment_worker/src/sources/progai-linkedin-scraper/types.ts index af7bda13c7..3b36f81906 100644 --- a/services/apps/premium/members_enrichment_worker/src/sources/progai-linkedin-scraper/types.ts +++ b/services/apps/premium/members_enrichment_worker/src/sources/progai-linkedin-scraper/types.ts @@ -1,8 +1,6 @@ +import { IMemberEnrichmentLinkedinScraperMetadata } from '../../types' import { IMemberEnrichmentDataProgAI } from '../progai/types' export interface IMemberEnrichmentDataProgAILinkedinScraper extends IMemberEnrichmentDataProgAI { - metadata: { - repeatedTimesInDifferentSources: number - isFromVerifiedSource: boolean - } + metadata: IMemberEnrichmentLinkedinScraperMetadata } diff --git a/services/apps/premium/members_enrichment_worker/src/types.ts b/services/apps/premium/members_enrichment_worker/src/types.ts index 4229dea19e..fe706eacd2 100644 --- a/services/apps/premium/members_enrichment_worker/src/types.ts +++ b/services/apps/premium/members_enrichment_worker/src/types.ts @@ -2,6 +2,7 @@ import { IAttributes, IMemberContribution, IMemberIdentity, + IMemberReach, IOrganizationIdentity, MemberAttributeName, MemberEnrichmentSource, @@ -10,6 +11,7 @@ import { } from '@crowd/types' import { IMemberEnrichmentDataClearbit } from './sources/clearbit/types' +import { IMemberEnrichmentDataCrustdata } from './sources/crustdata/types' import { IMemberEnrichmentDataProgAILinkedinScraper } from './sources/progai-linkedin-scraper/types' import { IMemberEnrichmentDataProgAI } from './sources/progai/types' import { IMemberEnrichmentDataSerp } from './sources/serp/types' @@ -30,6 +32,8 @@ export type IMemberEnrichmentData = | IMemberEnrichmentDataClearbit | IMemberEnrichmentDataSerp | IMemberEnrichmentDataProgAILinkedinScraper[] + | IMemberEnrichmentDataCrustdata + | IMemberEnrichmentDataCrustdata[] export interface IEnrichmentService { source: MemberEnrichmentSource @@ -57,24 +61,33 @@ export interface IEnrichmentService { ): IMemberEnrichmentDataNormalized | IMemberEnrichmentDataNormalized[] } +export type IMemberEnrichmentMetadataNormalized = IMemberEnrichmentLinkedinScraperMetadata + export interface IMemberEnrichmentDataNormalized { identities?: IMemberIdentity[] contributions?: IMemberContribution[] attributes?: IAttributes + reach?: IMemberReach memberOrganizations?: IMemberEnrichmentDataNormalizedOrganization[] displayName?: string - metadata?: Record + metadata?: IMemberEnrichmentMetadataNormalized } export interface IMemberEnrichmentDataNormalizedOrganization { name: string identities?: IOrganizationIdentity[] title?: string + organizationDescription?: string startDate?: string endDate?: string source: OrganizationSource } +export interface IMemberEnrichmentLinkedinScraperMetadata { + repeatedTimesInDifferentSources: number + isFromVerifiedSource: boolean +} + export interface IGetMembersForEnrichmentArgs { afterCursor: { activityCount: number; memberId: string } | null } diff --git a/services/apps/premium/members_enrichment_worker/src/workflows/getMembersToEnrich.ts b/services/apps/premium/members_enrichment_worker/src/workflows/getMembersToEnrich.ts index 5b4a639e65..6439666ac5 100644 --- a/services/apps/premium/members_enrichment_worker/src/workflows/getMembersToEnrich.ts +++ b/services/apps/premium/members_enrichment_worker/src/workflows/getMembersToEnrich.ts @@ -18,13 +18,14 @@ const { getEnrichableMembers } = proxyActivities({ }) export async function getMembersToEnrich(args: IGetMembersForEnrichmentArgs): Promise { - const MEMBER_ENRICHMENT_PER_RUN = 100 + const MEMBER_ENRICHMENT_PER_RUN = 500 const afterCursor = args?.afterCursor || null const sources = [ MemberEnrichmentSource.PROGAI, MemberEnrichmentSource.CLEARBIT, MemberEnrichmentSource.SERP, MemberEnrichmentSource.PROGAI_LINKEDIN_SCRAPER, + MemberEnrichmentSource.CRUSTDATA, ] const members = await getEnrichableMembers(MEMBER_ENRICHMENT_PER_RUN, sources, afterCursor) diff --git a/services/libs/types/src/enums/enrichment.ts b/services/libs/types/src/enums/enrichment.ts index b3c939d67d..7e46da08e7 100644 --- a/services/libs/types/src/enums/enrichment.ts +++ b/services/libs/types/src/enums/enrichment.ts @@ -3,4 +3,5 @@ export enum MemberEnrichmentSource { CLEARBIT = 'clearbit', SERP = 'serp', PROGAI_LINKEDIN_SCRAPER = 'progai-linkedin-scraper', + CRUSTDATA = 'crustdata', } diff --git a/services/libs/types/src/enums/members.ts b/services/libs/types/src/enums/members.ts index 84e2708bd2..47ec335bb7 100644 --- a/services/libs/types/src/enums/members.ts +++ b/services/libs/types/src/enums/members.ts @@ -36,6 +36,7 @@ export enum MemberAttributeName { COUNTRY = 'country', YEARS_OF_EXPERIENCE = 'yearsOfExperience', EDUCATION = 'education', + SCHOOLS = 'schools', AWARDS = 'awards', CERTIFICATIONS = 'certifications', WORK_EXPERIENCES = 'workExperiences', @@ -155,6 +156,10 @@ export const MemberAttributes = { name: MemberAttributeName.EDUCATION, label: 'Education', }, + [MemberAttributeName.SCHOOLS]: { + name: MemberAttributeName.SCHOOLS, + label: 'Schools', + }, [MemberAttributeName.AWARDS]: { name: MemberAttributeName.AWARDS, label: 'Awards', diff --git a/services/libs/types/src/enums/organizations.ts b/services/libs/types/src/enums/organizations.ts index 6261cd4b64..34dfd708cd 100644 --- a/services/libs/types/src/enums/organizations.ts +++ b/services/libs/types/src/enums/organizations.ts @@ -9,6 +9,7 @@ export enum OrganizationSource { EMAIL_DOMAIN = 'email-domain', ENRICHMENT_PROGAI = 'enrichment-progai', ENRICHMENT_CLEARBIT = 'enrichment-clearbit', + ENRICHMENT_CRUSTDATA = 'enrichment-crustdata', HUBSPOT = 'hubspot', GITHUB = 'github', UI = 'ui',