diff --git a/services/apps/premium/members_enrichment_worker/src/activities.ts b/services/apps/premium/members_enrichment_worker/src/activities.ts index 1d9ae661c8..cd22e736ea 100644 --- a/services/apps/premium/members_enrichment_worker/src/activities.ts +++ b/services/apps/premium/members_enrichment_worker/src/activities.ts @@ -8,7 +8,7 @@ import { touchMemberEnrichmentCacheUpdatedAt, updateMemberEnrichmentCache, } from './activities/enrichment' -import { getMembers } from './activities/getMembers' +import { getEnrichableMembers } from './activities/getMembers' import { refreshToken } from './activities/lf-auth0/authenticateLFAuth0' import { getIdentitiesExistInOtherMembers, @@ -29,7 +29,7 @@ import { } from './activities/syncEnrichedData' export { - getMembers, + getEnrichableMembers, getEnrichmentData, normalizeEnrichmentData, findMemberEnrichmentCache, diff --git a/services/apps/premium/members_enrichment_worker/src/activities/getMembers.ts b/services/apps/premium/members_enrichment_worker/src/activities/getMembers.ts index ded3cbacac..6b99f3c42e 100644 --- a/services/apps/premium/members_enrichment_worker/src/activities/getMembers.ts +++ b/services/apps/premium/members_enrichment_worker/src/activities/getMembers.ts @@ -1,21 +1,25 @@ import { fetchMembersForEnrichment } from '@crowd/data-access-layer/src/old/apps/premium/members_enrichment_worker' -import { IMember, IMemberEnrichmentSourceQueryInput, MemberEnrichmentSource } from '@crowd/types' +import { + IEnrichableMember, + IMemberEnrichmentSourceQueryInput, + MemberEnrichmentSource, +} from '@crowd/types' import { EnrichmentSourceServiceFactory } from '../factory' import { svc } from '../main' -export async function getMembers( +export async function getEnrichableMembers( limit: number, sources: MemberEnrichmentSource[], afterId: string, -): Promise { - let rows: IMember[] = [] +): Promise { + let rows: IEnrichableMember[] = [] const sourceInputs: IMemberEnrichmentSourceQueryInput[] = sources.map((s) => { const srv = EnrichmentSourceServiceFactory.getEnrichmentSourceService(s, svc.log) return { source: s, cacheObsoleteAfterSeconds: srv.cacheObsoleteAfterSeconds, - enrichableBy: srv.enrichableBy, + enrichableBySql: srv.enrichableBySql, } }) const db = svc.postgres.reader diff --git a/services/apps/premium/members_enrichment_worker/src/factory.ts b/services/apps/premium/members_enrichment_worker/src/factory.ts index 68557029b2..fe5054efe8 100644 --- a/services/apps/premium/members_enrichment_worker/src/factory.ts +++ b/services/apps/premium/members_enrichment_worker/src/factory.ts @@ -4,6 +4,7 @@ import { MemberEnrichmentSource } from '@crowd/types' import EnrichmentServiceClearbit from './sources/clearbit/service' import EnrichmentServiceProgAI from './sources/progai/service' +import EnrichmentServiceSerpApi from './sources/serp/service' import { IEnrichmentService } from './types' import { ALSO_USE_EMAIL_IDENTITIES_FOR_ENRICHMENT, ENRICH_EMAIL_IDENTITIES } from './utils/config' @@ -21,6 +22,8 @@ export class EnrichmentSourceServiceFactory { ) case MemberEnrichmentSource.CLEARBIT: return new EnrichmentServiceClearbit(log) + case MemberEnrichmentSource.SERP: + return new EnrichmentServiceSerpApi(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 c4d5b92c3d..e255b09aa0 100644 --- a/services/apps/premium/members_enrichment_worker/src/main.ts +++ b/services/apps/premium/members_enrichment_worker/src/main.ts @@ -10,6 +10,8 @@ const config: Config = { 'CROWD_ENRICHMENT_PROGAI_API_KEY', 'CROWD_ENRICHMENT_CLEARBIT_URL', 'CROWD_ENRICHMENT_CLEARBIT_API_KEY', + 'CROWD_ENRICHMENT_SERP_API_URL', + 'CROWD_ENRICHMENT_SERP_API_KEY', ], producer: { enabled: false, diff --git a/services/apps/premium/members_enrichment_worker/src/sources/clearbit/service.ts b/services/apps/premium/members_enrichment_worker/src/sources/clearbit/service.ts index b90ec662ae..4264f8a4ca 100644 --- a/services/apps/premium/members_enrichment_worker/src/sources/clearbit/service.ts +++ b/services/apps/premium/members_enrichment_worker/src/sources/clearbit/service.ts @@ -2,7 +2,6 @@ import axios from 'axios' import { Logger, LoggerBase } from '@crowd/logging' import { - IMemberEnrichmentSourceEnrichableBy, MemberAttributeName, MemberEnrichmentSource, MemberIdentityType, @@ -28,11 +27,7 @@ import { export default class EnrichmentServiceClearbit extends LoggerBase implements IEnrichmentService { public source: MemberEnrichmentSource = MemberEnrichmentSource.CLEARBIT public platform = `enrichment-${this.source}` - public enrichableBy: IMemberEnrichmentSourceEnrichableBy[] = [ - { - type: MemberIdentityType.EMAIL, - }, - ] + public enrichableBySql = `mi.type = 'email' and mi.verified` // bust cache after 120 days public cacheObsoleteAfterSeconds = 60 * 60 * 24 * 120 @@ -60,7 +55,7 @@ export default class EnrichmentServiceClearbit extends LoggerBase implements IEn } isEnrichableBySource(input: IEnrichmentSourceInput): boolean { - return !!input.email?.value + return !!input.email?.value && input.email?.verified } async getData(input: IEnrichmentSourceInput): Promise { diff --git a/services/apps/premium/members_enrichment_worker/src/sources/progai/service.ts b/services/apps/premium/members_enrichment_worker/src/sources/progai/service.ts index 42b78cd54f..0ac91d758f 100644 --- a/services/apps/premium/members_enrichment_worker/src/sources/progai/service.ts +++ b/services/apps/premium/members_enrichment_worker/src/sources/progai/service.ts @@ -3,7 +3,6 @@ import lodash from 'lodash' import { Logger, LoggerBase } from '@crowd/logging' import { - IMemberEnrichmentSourceEnrichableBy, MemberAttributeName, MemberEnrichmentSource, MemberIdentityType, @@ -33,15 +32,8 @@ import { export default class EnrichmentServiceProgAI extends LoggerBase implements IEnrichmentService { public source: MemberEnrichmentSource = MemberEnrichmentSource.PROGAI public platform = `enrichment-${this.source}` - public enrichableBy: IMemberEnrichmentSourceEnrichableBy[] = [ - { - type: MemberIdentityType.USERNAME, - platform: PlatformType.GITHUB, - }, - { - type: MemberIdentityType.EMAIL, - }, - ] + + enrichableBySql = `mi.verified and ((mi.type = 'username' AND mi.platform = 'github') OR (mi.type = 'email'))` // bust cache after 90 days public cacheObsoleteAfterSeconds = 60 * 60 * 24 * 90 diff --git a/services/apps/premium/members_enrichment_worker/src/sources/serp/service.ts b/services/apps/premium/members_enrichment_worker/src/sources/serp/service.ts new file mode 100644 index 0000000000..d0628a45be --- /dev/null +++ b/services/apps/premium/members_enrichment_worker/src/sources/serp/service.ts @@ -0,0 +1,133 @@ +import axios from 'axios' + +import { Logger, LoggerBase } from '@crowd/logging' +import { MemberEnrichmentSource, MemberIdentityType, PlatformType } from '@crowd/types' + +import { + IEnrichmentService, + IEnrichmentSourceInput, + IMemberEnrichmentDataNormalized, +} from '../../types' + +import { IMemberEnrichmentDataSerp, IMemberEnrichmentSerpApiResponse } from './types' + +export default class EnrichmentServiceSerpApi extends LoggerBase implements IEnrichmentService { + public source: MemberEnrichmentSource = MemberEnrichmentSource.SERP + public platform = `enrichment-${this.source}` + public enrichMembersWithActivityMoreThan = 10 + + public enrichableBySql = ` + ("activitySummary".total_count > ${this.enrichMembersWithActivityMoreThan}) AND + (members."displayName" like '% %') AND + (members.attributes->'location'->>'default' is not null and members.attributes->'location'->>'default' <> '') AND + ((members.attributes->'websiteUrl'->>'default' is not null and members.attributes->'websiteUrl'->>'default' <> '') OR + (mi.verified AND mi.type = 'username' and mi.platform = 'github') OR + (mi.verified AND mi.type = 'email') + )` + + // bust cache after 120 days + public cacheObsoleteAfterSeconds = 60 * 60 * 24 * 120 + + constructor(public readonly log: Logger) { + super(log) + } + + isEnrichableBySource(input: IEnrichmentSourceInput): boolean { + const displayNameSplit = input.displayName?.split(' ') + return ( + displayNameSplit?.length > 1 && + !!input.location && + ((!!input.email && input.email.verified) || + (!!input.github && input.github.verified) || + !!input.website) + ) + } + + async getData(input: IEnrichmentSourceInput): Promise { + let enriched: IMemberEnrichmentDataSerp = null + + if (input.displayName && input.location && input.website) { + enriched = await this.querySerpApi(input.displayName, input.location, input.website) + } + + if (!enriched && input.displayName && input.location && input.github && input.github.value) { + enriched = await this.querySerpApi(input.displayName, input.location, input.github.value) + } + + if (!enriched && input.displayName && input.location && input.email && input.email.value) { + enriched = await this.querySerpApi(input.displayName, input.location, input.email.value) + } + return enriched + } + + private async querySerpApi( + displayName: string, + location: string, + identifier: string, + ): Promise { + const config = { + method: 'get', + url: process.env['CROWD_ENRICHMENT_SERP_API_URL'], + params: { + api_key: process.env['CROWD_ENRICHMENT_SERP_API_KEY'], + q: `"${displayName}" ${location} "${identifier}" site:linkedin.com/in`, + num: 3, + engine: 'google', + }, + } + + const response: IMemberEnrichmentSerpApiResponse = (await axios(config)).data + + if (response.search_information.total_results > 0) { + if ( + response.organic_results.length > 0 && + response.organic_results[0].link && + !response.search_information.spelling_fix && + !response.search_information.spelling_fix_type + ) { + return { + linkedinUrl: response.organic_results[0].link, + } + } + } + + return null + } + + normalize(data: IMemberEnrichmentDataSerp): IMemberEnrichmentDataNormalized { + const normalized: IMemberEnrichmentDataNormalized = { + identities: [ + { + platform: PlatformType.LINKEDIN, + type: MemberIdentityType.USERNAME, + verified: false, + value: this.normalizeLinkedUrl(data.linkedinUrl), + }, + ], + } + return normalized + } + + private normalizeLinkedUrl(url: string): string { + try { + const parsedUrl = new URL(url) + + if (parsedUrl.hostname.endsWith('linkedin.com')) { + parsedUrl.hostname = 'linkedin.com' + parsedUrl.search = '' + + let path = parsedUrl.pathname + if (path.endsWith('/')) { + path = path.slice(0, -1) + } + + return parsedUrl.origin + path + } + + return url + } catch (error) { + this.log.error(`Error while normalizing linkedin url: ${url}`, error) + throw error + } + } +} diff --git a/services/apps/premium/members_enrichment_worker/src/sources/serp/tests.ts b/services/apps/premium/members_enrichment_worker/src/sources/serp/tests.ts new file mode 100644 index 0000000000..f3654aef84 --- /dev/null +++ b/services/apps/premium/members_enrichment_worker/src/sources/serp/tests.ts @@ -0,0 +1,69 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import axios from 'axios' + +// import { writeFileSync } from 'fs' +// import { Parser } from 'json2csv' +import { timeout } from '@crowd/common' + +const testSerpApi = async () => { + const members = [] as any[] + + for (const mem of members) { + const url = `https://serpapi.com/search.json` + const config = { + method: 'get', + url, + params: { + api_key: process.env['CROWD_SERP_API_KEY'], + q: `"${mem.displayName}" ${mem.location} "${mem.website}" site:linkedin.com/in`, + num: 3, + engine: 'google', + }, + } + + const response = (await axios(config)).data + + if (response.search_information.total_results > 0) { + if ( + response.organic_results.length > 0 && + response.organic_results[0].link && + !response.search_information.spelling_fix && + !response.search_information.spelling_fix_type + ) { + console.log(`Found LinkedIn for ${mem.displayName}: ${response.organic_results[0].link}`) + console.log(response.search_information) + mem.linkedinFromSerp = response.organic_results[0].link + } else { + console.log(`No LinkedIn found for ${mem.displayName}`) + } + } else { + console.log(`No LinkedIn found for ${mem.displayName}`) + } + + await timeout(1000) + } + + try { + // const fields = [ + // 'id', + // 'displayName', + // 'location', + // 'profileUrl', + // 'website', + // 'linkedinFromClearbit', + // 'linkedinFromProgai', + // 'linkedinFromSerp', + // ] + // const json2csvParser = new Parser({ fields }) + // const csv = json2csvParser.parse(members) + // writeFileSync('output.csv', csv) + // console.log('CSV file has been successfully written.') + } catch (err) { + console.error('Error writing CSV file:', err) + } +} + +setImmediate(async () => { + await testSerpApi() + process.exit(0) +}) diff --git a/services/apps/premium/members_enrichment_worker/src/sources/serp/types.ts b/services/apps/premium/members_enrichment_worker/src/sources/serp/types.ts new file mode 100644 index 0000000000..87484aca0e --- /dev/null +++ b/services/apps/premium/members_enrichment_worker/src/sources/serp/types.ts @@ -0,0 +1,37 @@ +export interface IMemberEnrichmentDataSerp { + linkedinUrl: string +} + +export interface IMemberEnrichmentSerpApiResponse { + organic_results: IMemberEnrichmentSerpApiResponseOrganicResult[] + search_information: IMemberEnrichmentSerpApiResponseSearchInformation +} + +export interface IMemberEnrichmentSerpApiResponseSearchInformation { + query_displayed: string + total_results: number + time_taken_displayed: number + organic_results_state: string + spelling_fix?: string + spelling_fix_type?: string +} + +export interface IMemberEnrichmentSerpApiResponseOrganicResult { + position: number + title: string + link: string + redirect_link: string + displayed_link: string + favicon: string + snippet: string + snippet_highlighted_words: string[] + sitelinks: { + inline: IMemberEnrichmentSerpApiResponseOrganicResultSitelinkInline[] + } + source: string +} + +export interface IMemberEnrichmentSerpApiResponseOrganicResultSitelinkInline { + title: string + link: string +} diff --git a/services/apps/premium/members_enrichment_worker/src/types.ts b/services/apps/premium/members_enrichment_worker/src/types.ts index 1cdd70fcb8..1b60fca527 100644 --- a/services/apps/premium/members_enrichment_worker/src/types.ts +++ b/services/apps/premium/members_enrichment_worker/src/types.ts @@ -1,7 +1,6 @@ import { IAttributes, IMemberContribution, - IMemberEnrichmentSourceEnrichableBy, IMemberIdentity, IOrganizationIdentity, MemberAttributeName, @@ -12,14 +11,21 @@ import { import { IMemberEnrichmentDataClearbit } from './sources/clearbit/types' import { IMemberEnrichmentDataProgAI } from './sources/progai/types' +import { IMemberEnrichmentDataSerp } from './sources/serp/types' export interface IEnrichmentSourceInput { github?: IMemberIdentity linkedin?: IMemberIdentity email?: IMemberIdentity + website?: string + location?: string + displayName?: string } -export type IMemberEnrichmentData = IMemberEnrichmentDataProgAI | IMemberEnrichmentDataClearbit +export type IMemberEnrichmentData = + | IMemberEnrichmentDataProgAI + | IMemberEnrichmentDataClearbit + | IMemberEnrichmentDataSerp export interface IEnrichmentService { source: MemberEnrichmentSource @@ -30,8 +36,11 @@ export interface IEnrichmentService { // can the source enrich using this input isEnrichableBySource(input: IEnrichmentSourceInput): boolean - // what kind of identities can this source use as input - enrichableBy: IMemberEnrichmentSourceEnrichableBy[] + // SQL filter to get enrichable members for a source + // members table is available as "members" alias + // memberIdentities table is available as "mi" alias + // activity count is available in "activitySummary" alias, "activitySummary".total_count field + enrichableBySql: string // should either return the data or null if it's a miss getData(input: IEnrichmentSourceInput): Promise diff --git a/services/apps/premium/members_enrichment_worker/src/workflows/enrichMember.ts b/services/apps/premium/members_enrichment_worker/src/workflows/enrichMember.ts index 207991cd3b..d756d7e0c6 100644 --- a/services/apps/premium/members_enrichment_worker/src/workflows/enrichMember.ts +++ b/services/apps/premium/members_enrichment_worker/src/workflows/enrichMember.ts @@ -1,6 +1,11 @@ import { proxyActivities } from '@temporalio/workflow' -import { IMember, MemberEnrichmentSource, MemberIdentityType, PlatformType } from '@crowd/types' +import { + IEnrichableMember, + MemberEnrichmentSource, + MemberIdentityType, + PlatformType, +} from '@crowd/types' import * as activities from '../activities' import { IEnrichmentSourceInput } from '../types' @@ -15,7 +20,7 @@ const { isCacheObsolete, normalizeEnrichmentData, } = proxyActivities({ - startToCloseTimeout: '10 seconds', + startToCloseTimeout: '20 seconds', retry: { initialInterval: '5s', backoffCoefficient: 2.0, @@ -25,7 +30,7 @@ const { }) export async function enrichMember( - input: IMember, + input: IEnrichableMember, sources: MemberEnrichmentSource[], ): Promise { let changeInEnrichmentSourceData = false @@ -50,6 +55,9 @@ export async function enrichMember( i.platform === PlatformType.LINKEDIN && i.type === MemberIdentityType.USERNAME, ), + displayName: input.displayName || undefined, + website: input.website || undefined, + location: input.location || undefined, } const data = await getEnrichmentData(source, enrichmentInput) 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 99527c128a..72898e92e8 100644 --- a/services/apps/premium/members_enrichment_worker/src/workflows/getMembersToEnrich.ts +++ b/services/apps/premium/members_enrichment_worker/src/workflows/getMembersToEnrich.ts @@ -13,18 +13,20 @@ import { IGetMembersForEnrichmentArgs } from '../types' import { enrichMember } from './enrichMember' -// Configure timeouts and retry policies to retrieve members to enrich from the -// database. -const { getMembers } = proxyActivities({ +const { getEnrichableMembers } = proxyActivities({ startToCloseTimeout: '10 seconds', }) export async function getMembersToEnrich(args: IGetMembersForEnrichmentArgs): Promise { const MEMBER_ENRICHMENT_PER_RUN = 100 const afterId = args?.afterId || null - const sources = [MemberEnrichmentSource.PROGAI, MemberEnrichmentSource.CLEARBIT] + const sources = [ + MemberEnrichmentSource.PROGAI, + MemberEnrichmentSource.CLEARBIT, + MemberEnrichmentSource.SERP, + ] - const members = await getMembers(MEMBER_ENRICHMENT_PER_RUN, sources, afterId) + const members = await getEnrichableMembers(MEMBER_ENRICHMENT_PER_RUN, sources, afterId) if (members.length === 0) { return diff --git a/services/libs/data-access-layer/src/old/apps/premium/members_enrichment_worker/index.ts b/services/libs/data-access-layer/src/old/apps/premium/members_enrichment_worker/index.ts index 314dfe89ac..82590bdbb7 100644 --- a/services/libs/data-access-layer/src/old/apps/premium/members_enrichment_worker/index.ts +++ b/services/libs/data-access-layer/src/old/apps/premium/members_enrichment_worker/index.ts @@ -2,9 +2,8 @@ import { generateUUIDv4, redactNullByte } from '@crowd/common' import { DbConnOrTx, DbStore, DbTransaction } from '@crowd/database' import { IAttributes, - IMember, + IEnrichableMember, IMemberEnrichmentCache, - IMemberEnrichmentSourceEnrichableBy, IMemberEnrichmentSourceQueryInput, IMemberIdentity, IOrganizationIdentity, @@ -18,12 +17,11 @@ export async function fetchMembersForEnrichment( limit: number, sourceInputs: IMemberEnrichmentSourceQueryInput[], afterId: string, -): Promise { +): Promise { const idFilter = afterId ? ' and members.id < $2 ' : '' const sourceInnerQueryItems = [] - const enrichableByHashMap = {} - const distinctEnrichableByConditions: IMemberEnrichmentSourceEnrichableBy[] = [] + const enrichableBySqlConditions = [] sourceInputs.forEach((input) => { sourceInnerQueryItems.push( @@ -36,40 +34,35 @@ export async function fetchMembersForEnrichment( )`, ) - input.enrichableBy.forEach((enrichableBy) => { - const key = `${enrichableBy.type}-${enrichableBy.platform}` - if (!enrichableByHashMap[key]) { - enrichableByHashMap[key] = true - distinctEnrichableByConditions.push(enrichableBy) - } - }) + enrichableBySqlConditions.push(`(${input.enrichableBySql})`) }) - const identityFilterArray = [] + let enrichableBySqlJoined = '' - distinctEnrichableByConditions.forEach((enrichableBy) => { - if (!enrichableBy.platform && enrichableBy.type === MemberIdentityType.USERNAME) { - throw new Error( - 'Platform must be provided for username identity types. Searching in all platforms for the username type is not allowed.', - ) - } else if (!enrichableBy.platform && enrichableBy.type === MemberIdentityType.EMAIL) { - identityFilterArray.push(`(mi.type = '${MemberIdentityType.EMAIL}')`) - } else { - identityFilterArray.push( - `(mi.platform = '${enrichableBy.platform}') AND (mi.type = '${enrichableBy.type}')`, - ) - } - }) + if (enrichableBySqlConditions.length > 0) { + enrichableBySqlJoined = `(${enrichableBySqlConditions.join(' OR ')}) ` + } return db.connection().query( - `SELECT + ` + WITH "activitySummary" AS ( + SELECT + msa."memberId", + sum(msa."activityCount") AS total_count + FROM "memberSegmentsAgg" msa + WHERE msa."segmentId" IN ( + SELECT id + FROM segments + WHERE "grandparentId" IS NOT NULL AND "parentId" IS NOT NULL + ) + group by msa."memberId" + ) + SELECT members."id", - members."displayName", - members."attributes", - members."contributions", - members."score", - members."reach", members."tenantId", + members."displayName", + members.attributes->'location'->>'default' as location, + members.attributes->'websiteUrl'->>'default' as website, json_agg(json_build_object( 'platform', mi.platform, 'value', mi.value, @@ -79,8 +72,9 @@ export async function fetchMembersForEnrichment( FROM members INNER JOIN tenants ON tenants.id = members."tenantId" INNER JOIN "memberIdentities" mi ON mi."memberId" = members.id + INNER JOIN "activitySummary" on "activitySummary"."memberId" = members.id WHERE - ( mi.verified AND (${identityFilterArray.join(' OR ')}) ) + ${enrichableBySqlJoined} AND tenants."deletedAt" IS NULL AND members."deletedAt" IS NULL AND (${sourceInnerQueryItems.join(' OR ')}) diff --git a/services/libs/types/src/enums/enrichment.ts b/services/libs/types/src/enums/enrichment.ts index fb6243e464..6c9ecc7d28 100644 --- a/services/libs/types/src/enums/enrichment.ts +++ b/services/libs/types/src/enums/enrichment.ts @@ -1,4 +1,5 @@ export enum MemberEnrichmentSource { PROGAI = 'progai', CLEARBIT = 'clearbit', + SERP = 'serp', } diff --git a/services/libs/types/src/premium/enrichment.ts b/services/libs/types/src/premium/enrichment.ts index 5da12a44a5..504c3cb6fb 100644 --- a/services/libs/types/src/premium/enrichment.ts +++ b/services/libs/types/src/premium/enrichment.ts @@ -1,4 +1,5 @@ -import { MemberEnrichmentSource, MemberIdentityType, PlatformType } from '../enums' +import { MemberEnrichmentSource } from '../enums' +import { IMemberIdentity } from '../members' export interface IMemberEnrichmentCache { createdAt: string @@ -8,13 +9,17 @@ export interface IMemberEnrichmentCache { source: MemberEnrichmentSource } -export interface IMemberEnrichmentSourceEnrichableBy { - platform?: PlatformType - type: MemberIdentityType -} - export interface IMemberEnrichmentSourceQueryInput { source: MemberEnrichmentSource cacheObsoleteAfterSeconds: number - enrichableBy: IMemberEnrichmentSourceEnrichableBy[] + enrichableBySql: string +} + +export interface IEnrichableMember { + id: string + tenantId: string + displayName: string + location: string + website: string + identities: IMemberIdentity[] }