import * as z from "zod" import { ApolloApiError, type ApolloApi } from "./api" import { APOLLO_PROVIDER_ACCOUNT_SCHEMA, APOLLO_PROVIDER_CONTACT_SCHEMA, APOLLO_PROVIDER_LIST_SCHEMA, APOLLO_PROVIDER_STAGE_SCHEMA, APOLLO_PROVIDER_TASK_SCHEMA, APOLLO_PROVIDER_USER_SCHEMA, APOLLO_REFERENCE_SCHEMA, } from "./schemas" const CONTACT_RESPONSE_SCHEMA = z.looseObject({ contact: APOLLO_PROVIDER_CONTACT_SCHEMA, }) const CONTACT_SEARCH_RESPONSE_SCHEMA = z.looseObject({ contacts: APOLLO_PROVIDER_CONTACT_SCHEMA.array().prefault([]), pagination: z.looseObject({ page: z.number().int(), total_pages: z.number().int(), }), }) const ACCOUNT_RESPONSE_SCHEMA = z.looseObject({ account: APOLLO_PROVIDER_ACCOUNT_SCHEMA, }) const ACCOUNT_SEARCH_RESPONSE_SCHEMA = z.looseObject({ accounts: APOLLO_PROVIDER_ACCOUNT_SCHEMA.array().prefault([]), pagination: z.looseObject({ page: z.number().int(), total_pages: z.number().int(), }), }) const CONTACT_STAGES_RESPONSE_SCHEMA = z.looseObject({ contact_stages: APOLLO_PROVIDER_STAGE_SCHEMA.array().prefault([]), }) const ACCOUNT_STAGES_RESPONSE_SCHEMA = z.looseObject({ account_stages: APOLLO_PROVIDER_STAGE_SCHEMA.array().prefault([]), }) const USERS_RESPONSE_SCHEMA = z.looseObject({ pagination: z.looseObject({ page: z.number().int(), total_pages: z.number().int(), }), users: APOLLO_PROVIDER_USER_SCHEMA.array().prefault([]), }) const LISTS_RESPONSE_SCHEMA = APOLLO_PROVIDER_LIST_SCHEMA.array() const TASKS_RESPONSE_SCHEMA = z.looseObject({ pagination: z.looseObject({ page: z.number().int(), total_pages: z.number().int(), }), tasks: APOLLO_PROVIDER_TASK_SCHEMA.array().prefault([]), }) type ApolloReference = z.output type ReferenceCandidate = { domain?: string | null email?: string | null id: string name?: string | null title?: string | null } /** * Resolves an Apollo contact by ID, exact email, or exact name. * * @param api - Authenticated Apollo client. * @param reference - Semantic or explicit contact reference. */ export async function resolveApolloContact( api: ApolloApi, reference: ApolloReference, ) { if (typeof reference !== "string") return reference.id const direct = await tryDirectReference( api, `contacts/${encodeURIComponent(reference)}`, CONTACT_RESPONSE_SCHEMA, ) if (direct) return direct.contact.id return pickExactReference( "contact", reference, await collectPages(async (page) => api.request("contacts/search", { body: { page, per_page: 100, q_keywords: reference }, responseSchema: CONTACT_SEARCH_RESPONSE_SCHEMA, }), ), ) } /** * Resolves an Apollo account by ID, exact domain, or exact name. * * @param api - Authenticated Apollo client. * @param reference - Semantic or explicit account reference. */ export async function resolveApolloAccount( api: ApolloApi, reference: ApolloReference, ) { if (typeof reference !== "string") return reference.id const direct = await tryDirectReference( api, `accounts/${encodeURIComponent(reference)}`, ACCOUNT_RESPONSE_SCHEMA, ) if (direct) return direct.account.id const nameCandidates = await collectPages(async (page) => api.request("accounts/search", { body: { page, per_page: 100, q_organization_name: reference }, responseSchema: ACCOUNT_SEARCH_RESPONSE_SCHEMA, }), ) if (getExactReferenceMatches(reference, nameCandidates).length > 0) { return pickExactReference("account", reference, nameCandidates) } return pickExactReference( "account", reference, await collectPages(async (page) => api.request("accounts/search", { body: { page, per_page: 100 }, responseSchema: ACCOUNT_SEARCH_RESPONSE_SCHEMA, }), ), ) } /** * Resolves an Apollo contact stage by ID or exact name. * * @param api - Authenticated Apollo client. * @param reference - Semantic or explicit contact-stage reference. */ export async function resolveApolloContactStage( api: ApolloApi, reference: ApolloReference, ) { if (typeof reference !== "string") return reference.id const { contact_stages: stages } = await api.request("contact_stages", { responseSchema: CONTACT_STAGES_RESPONSE_SCHEMA, }) return pickExactReference("contact stage", reference, stages) } /** * Resolves an Apollo account stage by ID or exact name. * * @param api - Authenticated Apollo client. * @param reference - Semantic or explicit account-stage reference. */ export async function resolveApolloAccountStage( api: ApolloApi, reference: ApolloReference, ) { if (typeof reference !== "string") return reference.id const { account_stages: stages } = await api.request("account_stages", { responseSchema: ACCOUNT_STAGES_RESPONSE_SCHEMA, }) return pickExactReference("account stage", reference, stages) } /** * Resolves an Apollo workspace user by ID, exact email, or exact name. * * @param api - Authenticated Apollo client. * @param reference - Semantic or explicit user reference. */ export async function resolveApolloUser( api: ApolloApi, reference: ApolloReference, ) { if (typeof reference !== "string") return reference.id return pickExactReference( "user", reference, await collectPages(async (page) => api.request("users/search", { query: { page, per_page: 100 }, responseSchema: USERS_RESPONSE_SCHEMA, }), ), ) } /** * Resolves an Apollo list by ID or exact name. * * @param api - Authenticated Apollo client. * @param reference - Semantic or explicit list reference. * @param modality - Resource type the list must contain. */ export async function resolveApolloList( api: ApolloApi, reference: ApolloReference, modality: "accounts" | "contacts", ) { if (typeof reference !== "string") return reference.id return pickExactReference( "list", reference, ( await api.request("labels", { responseSchema: LISTS_RESPONSE_SCHEMA }) ).filter((list) => list.modality === modality), ) } /** * Resolves an Apollo task by ID or exact title. * * @param api - Authenticated Apollo client. * @param reference - Semantic or explicit task reference. */ export async function resolveApolloTask( api: ApolloApi, reference: ApolloReference, ) { if (typeof reference !== "string") return reference.id const direct = await tryDirectReference( api, `tasks/${encodeURIComponent(reference)}`, z.looseObject({ task: APOLLO_PROVIDER_TASK_SCHEMA }), ) if (direct) return direct.task.id return pickExactReference( "task", reference, await collectPages(async (page) => api.request("tasks/search", { method: "POST", query: { page, per_page: 100 }, responseSchema: TASKS_RESPONSE_SCHEMA, }), ), ) } /** * Resolves multiple references while preserving input order. * * @param references - References to resolve. * @param resolve - Resource-specific reference resolver. */ export function resolveApolloReferences( references: readonly ApolloReference[], resolve: (reference: ApolloReference) => Promise, ) { return Promise.all(references.map(resolve)) } /** * Attempts direct ID lookup before semantic matching. * * @param api - Authenticated Apollo client. * @param path - Direct provider resource path. * @param responseSchema - Successful response schema. */ async function tryDirectReference( api: ApolloApi, path: string, responseSchema: TSchema, ) { try { return await api.request(path, { responseSchema }) } catch (cause) { if ( cause instanceof ApolloApiError && [400, 403, 404, 422].includes(cause.status) ) { return undefined } throw cause } } /** * Collects all zero-credit lookup pages for one exact reference. * * @param getPage - Fetches one provider result page. */ async function collectPages( getPage: (page: number) => Promise<{ pagination?: { page: number; total_pages: number } [key: string]: unknown }>, ) { const candidates: T[] = [] let page = 1 let totalPages = 1 do { const result = await getPage(page) const collection = Object.values(result).find( (value): value is T[] => Array.isArray(value) && value.every(isReferenceCandidate), ) if (collection) candidates.push(...collection) totalPages = result.pagination?.total_pages ?? page page += 1 } while (page <= totalPages) return candidates } /** * Selects one exact human match after checking case-sensitive IDs. * * @param resource - Human-readable provider resource name. * @param reference - Reference supplied by the automation author. * @param candidates - Candidate provider resources. * @throws When no unique exact match exists. */ function pickExactReference( resource: string, reference: string, candidates: readonly ReferenceCandidate[], ) { const idMatch = candidates.find(({ id }) => id === reference) if (idMatch) return idMatch.id const matches = getExactReferenceMatches(reference, candidates) if (matches.length === 1) return matches[0]!.id if (matches.length > 1) { throw new Error( `Apollo ${resource} "${reference}" is ambiguous; matching IDs: ${matches.map(({ id }) => id).join(", ")}.`, ) } throw new Error(`Apollo ${resource} "${reference}" was not found.`) } /** * Finds case-insensitive exact human identifiers for one reference. * * @param reference - Reference supplied by the automation author. * @param candidates - Candidate provider resources. */ function getExactReferenceMatches( reference: string, candidates: readonly ReferenceCandidate[], ) { const normalized = reference.trim().toLocaleLowerCase() return candidates.filter(({ domain, email, name, title }) => [domain, email, name, title].some( (value) => value?.trim().toLocaleLowerCase() === normalized, ), ) } /** * Narrows unknown provider collections before reference matching. * * @param value - Unknown provider value. */ function isReferenceCandidate(value: unknown): value is ReferenceCandidate { return ( typeof value === "object" && value !== null && "id" in value && typeof value.id === "string" ) }