import * as z from "zod" import { defineAction } from "../../../automation/actions" import { integrationScope as scope } from "../../../automation/integrations" import { apolloAccountOptions, getApolloApi } from "../lib/api" import { resolveApolloAccount, resolveApolloAccountStage, resolveApolloList, resolveApolloReferences, resolveApolloUser, } from "../lib/references" import { APOLLO_ACCOUNT_SCHEMA, APOLLO_PAGINATION_SCHEMA, APOLLO_PROVIDER_ACCOUNT_SCHEMA, APOLLO_PROVIDER_STAGE_SCHEMA, APOLLO_REFERENCE_SCHEMA, APOLLO_STAGE_SCHEMA, toApolloAccount, toApolloPagination, toApolloStage, } from "../lib/schemas" const CUSTOM_FIELDS_SCHEMA = z.record(z.string(), z.string()) const ACCOUNT_RESPONSE_SCHEMA = z.looseObject({ account: APOLLO_PROVIDER_ACCOUNT_SCHEMA, }) const ACCOUNTS_RESPONSE_SCHEMA = z.looseObject({ accounts: APOLLO_PROVIDER_ACCOUNT_SCHEMA.array().prefault([]), }) const ACCOUNT_MUTATION_RESULT_PROVIDER_SCHEMA = z.looseObject({ account_stage_id: z.string().nullish(), crm_owner_id: z.string().nullish(), id: z.string(), owner_id: z.string().nullish(), }) const ACCOUNT_MUTATION_RESULTS_RESPONSE_SCHEMA = z.looseObject({ accounts: ACCOUNT_MUTATION_RESULT_PROVIDER_SCHEMA.array().prefault([]), }) const ACCOUNT_MUTATION_RESULT_SCHEMA = z.object({ /** Account stage after the mutation, when Apollo returns it. */ accountStageId: z.string().optional(), /** CRM owner after the mutation, when Apollo returns it. */ crmOwnerId: z.string().optional(), /** Stable Apollo account ID. */ id: z.string(), /** Apollo owner after the mutation, when Apollo returns it. */ ownerId: z.string().optional(), }) const ACCOUNT_SEARCH_RESPONSE_SCHEMA = ACCOUNTS_RESPONSE_SCHEMA.extend({ pagination: z.looseObject({ page: z.number().int(), per_page: z.number().int(), total_entries: z.number().int(), total_pages: z.number().int(), }), }) const ACCOUNT_PAGE_SCHEMA = z.object({ /** Accounts returned on this page. */ accounts: APOLLO_ACCOUNT_SCHEMA.array(), /** Pagination metadata for requesting another page. */ pagination: APOLLO_PAGINATION_SCHEMA, }) const ACCOUNT_STAGES_RESPONSE_SCHEMA = z.looseObject({ account_stages: APOLLO_PROVIDER_STAGE_SCHEMA.array().prefault([]), }) const ACCOUNT_MUTATION_SCHEMA = z.object({ /** Account stage ID, exact name, or explicit ID reference. */ stage: APOLLO_REFERENCE_SCHEMA.optional(), /** Custom field values keyed by Apollo field ID. */ customFields: CUSTOM_FIELDS_SCHEMA.optional(), /** Primary company domain without `www`. */ domain: z.string().trim().min(1).optional(), /** Human-readable account name. */ name: z.string().trim().min(1).optional(), /** Owner ID, exact email/name, or explicit ID reference. */ owner: APOLLO_REFERENCE_SCHEMA.optional(), /** Main account phone number. */ phone: z.string().trim().min(1).optional(), /** Human-readable company address or location. */ rawAddress: z.string().trim().min(1).optional(), }) const CREATE_ACCOUNT_INPUT_SCHEMA = ACCOUNT_MUTATION_SCHEMA.refine( ({ domain, name }) => domain !== undefined || name !== undefined, "Provide an account name or domain.", ) const UPDATE_ACCOUNT_INPUT_SCHEMA = ACCOUNT_MUTATION_SCHEMA.extend({ /** Account ID, exact name/domain, or explicit ID reference. */ target: APOLLO_REFERENCE_SCHEMA, }).refine(hasAccountMutation, "Provide at least one account field to update.") const BULK_ACCOUNT_FIELDS_SCHEMA = z.object({ /** Account stage ID, exact name, or explicit ID reference. */ stage: APOLLO_REFERENCE_SCHEMA.optional(), /** Custom field values keyed by Apollo field ID. */ customFields: CUSTOM_FIELDS_SCHEMA.optional(), /** Primary company domain. */ domain: z.string().trim().min(1).optional(), /** Facebook company-page URL. */ facebookUrl: z.url().optional(), /** HubSpot company ID used for deduplication. */ hubspotId: z.string().min(1).optional(), /** LinkedIn company-page URL. */ linkedinUrl: z.url().optional(), /** Human-readable account name. */ name: z.string().trim().min(1).optional(), /** Apollo organization ID. */ organizationId: z.string().min(1).optional(), /** Owner ID, exact email/name, or explicit ID reference. */ owner: APOLLO_REFERENCE_SCHEMA.optional(), /** Parent account ID, exact name/domain, or explicit ID reference. */ parentAccount: APOLLO_REFERENCE_SCHEMA.optional(), /** Main account phone number. */ phone: z.string().trim().min(1).optional(), /** Human-readable company address. */ rawAddress: z.string().trim().min(1).optional(), /** Salesforce account ID used for deduplication. */ salesforceId: z.string().min(1).optional(), /** X/Twitter company profile URL. */ twitterUrl: z.url().optional(), }) const BULK_CREATE_ACCOUNT_SCHEMA = BULK_ACCOUNT_FIELDS_SCHEMA.refine( ({ domain, name }) => domain !== undefined || name !== undefined, "Provide an account name or domain.", ) const BULK_UPDATE_ACCOUNT_SCHEMA = z .object({ /** Account ID, exact name/domain, or explicit ID reference. */ account: APOLLO_REFERENCE_SCHEMA, /** Account stage ID, exact name, or explicit ID reference. */ stage: APOLLO_REFERENCE_SCHEMA.optional(), /** Custom field values keyed by Apollo field ID. */ customFields: CUSTOM_FIELDS_SCHEMA.optional(), /** New account name. */ name: z.string().trim().min(1).optional(), /** Owner ID, exact email/name, or explicit ID reference. */ owner: APOLLO_REFERENCE_SCHEMA.optional(), }) .refine( hasBulkAccountMutation, "Provide at least one account field to update.", ) const BULK_CREATE_RESPONSE_SCHEMA = z.looseObject({ created_accounts: APOLLO_PROVIDER_ACCOUNT_SCHEMA.array().prefault([]), existing_accounts: APOLLO_PROVIDER_ACCOUNT_SCHEMA.array().prefault([]), }) const BULK_CREATE_OUTPUT_SCHEMA = z.object({ /** Newly created accounts. */ createdAccounts: APOLLO_ACCOUNT_SCHEMA.array(), /** Existing accounts matched by deduplication. */ existingAccounts: APOLLO_ACCOUNT_SCHEMA.array(), }) const SORT_FIELD_SCHEMA = z.enum([ "account_created_at", "account_last_activity_date", "account_updated_at", ]) /** Searches accounts saved in the connected Apollo workspace. */ export const searchApolloAccounts = defineAction("Search Apollo accounts") .describe( "Searches saved accounts with semantic filters and explicit pagination.", ) .account( "apollo", apolloAccountOptions( scope.and("accounts_search", "account_stages_list", "tags_list"), ), ) .input( z.object({ /** Apollo list IDs, exact names, or explicit ID references. */ lists: APOLLO_REFERENCE_SCHEMA.array().optional(), /** One-indexed result page. */ page: z.number().int().min(1).prefault(1), /** Maximum accounts returned per page. */ perPage: z.number().int().min(1).max(100).prefault(100), /** Account-name keywords. */ query: z.string().trim().min(1).optional(), /** Sort oldest-to-newest instead of newest-to-oldest. */ sortAscending: z.boolean().prefault(false), /** Account field used for sorting. */ sortBy: SORT_FIELD_SCHEMA.optional(), /** Account stage IDs, exact names, or explicit ID references. */ stages: APOLLO_REFERENCE_SCHEMA.array().optional(), }), ) .output(ACCOUNT_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) const [stageIds, listIds] = await Promise.all([ input.stages ? resolveApolloReferences(input.stages, (reference) => resolveApolloAccountStage(api, reference), ) : undefined, input.lists ? resolveApolloReferences(input.lists, (reference) => resolveApolloList(api, reference, "accounts"), ) : undefined, ]) const result = await api.request("accounts/search", { body: { account_label_ids: listIds, account_stage_ids: stageIds, page: input.page, per_page: input.perPage, q_organization_name: input.query, sort_ascending: input.sortAscending, sort_by_field: input.sortBy, }, responseSchema: ACCOUNT_SEARCH_RESPONSE_SCHEMA, }) return { accounts: result.accounts.map(toApolloAccount), pagination: toApolloPagination(result.pagination), } }) /** Gets one saved Apollo account by ID, exact name, or domain. */ export const getApolloAccount = defineAction("Get Apollo account") .describe("Gets a saved account using a semantic reference.") .account( "apollo", apolloAccountOptions(scope.and("account_read", "accounts_search")), ) .input( z.object({ /** Account ID, exact name/domain, or explicit ID reference. */ target: APOLLO_REFERENCE_SCHEMA, }), ) .output(APOLLO_ACCOUNT_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) // Names the canonical ID used to build the provider request path. const accountId = await resolveApolloAccount(api, input.target) // Keeps the response envelope readable while normalizing its account. const result = await api.request( `accounts/${encodeURIComponent(accountId)}`, { responseSchema: ACCOUNT_RESPONSE_SCHEMA, }, ) return toApolloAccount(result.account) }) /** Creates a saved Apollo account. */ export const createApolloAccount = defineAction("Create Apollo account") .describe("Creates an account with optional owner, stage, and custom fields.") .account( "apollo", apolloAccountOptions( scope.and("account_write", "account_stages_list", "users_list"), ), ) .input(CREATE_ACCOUNT_INPUT_SCHEMA) .output(APOLLO_ACCOUNT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) // Keeps the response envelope readable while normalizing its account. const result = await api.request("accounts", { body: await toAccountMutationBody(api, input), responseSchema: ACCOUNT_RESPONSE_SCHEMA, }) return toApolloAccount(result.account) }) /** Updates selected fields on a saved Apollo account. */ export const updateApolloAccount = defineAction("Update Apollo account") .describe("Updates account fields using a semantic account reference.") .account( "apollo", apolloAccountOptions( scope.and( "account_update", "account_read", "accounts_search", "account_stages_list", "users_list", ), ), ) .input(UPDATE_ACCOUNT_INPUT_SCHEMA) .output(APOLLO_ACCOUNT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) // Names the canonical ID used to build the provider request path. const accountId = await resolveApolloAccount(api, input.target) // Keeps the response envelope readable while normalizing its account. const result = await api.request( `accounts/${encodeURIComponent(accountId)}`, { body: await toAccountMutationBody(api, input), method: "PATCH", responseSchema: ACCOUNT_RESPONSE_SCHEMA, }, ) return toApolloAccount(result.account) }) /** Creates up to 100 saved Apollo accounts. */ export const bulkCreateApolloAccounts = defineAction( "Bulk create Apollo accounts", ) .describe( "Creates up to 100 accounts and reports deduplication matches separately.", ) .account( "apollo", apolloAccountOptions( scope.and( "account_bulk_create", "account_read", "accounts_search", "account_stages_list", "users_list", ), ), ) .input( z.object({ /** Accounts to create. */ accounts: BULK_CREATE_ACCOUNT_SCHEMA.array().min(1).max(100), /** Apollo list names added to every new account. */ listNames: z.string().trim().min(1).array().optional(), /** Deduplicate by domain, organization ID, and account name. */ runDedupe: z.boolean().prefault(true), }), ) .output(BULK_CREATE_OUTPUT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) // Resolves per-account references before submitting the bounded batch. const accounts = await Promise.all( input.accounts.map(async (value) => ({ ...toBulkAccountFields(value), account_stage_id: value.stage ? await resolveApolloAccountStage(api, value.stage) : undefined, owner_id: value.owner ? await resolveApolloUser(api, value.owner) : undefined, parent_account_id: value.parentAccount ? await resolveApolloAccount(api, value.parentAccount) : undefined, })), ) const result = await api.request("accounts/bulk_create", { body: { accounts, append_label_names: input.listNames, run_dedupe: input.runDedupe, }, responseSchema: BULK_CREATE_RESPONSE_SCHEMA, }) return { createdAccounts: result.created_accounts.map(toApolloAccount), existingAccounts: result.existing_accounts.map(toApolloAccount), } }) /** Updates up to 100 saved Apollo accounts synchronously. */ export const bulkUpdateApolloAccounts = defineAction( "Bulk update Apollo accounts", ) .describe( "Updates up to 100 accounts with individual owner, stage, or field changes.", ) .account( "apollo", apolloAccountOptions( scope.and( "account_stages_update", "account_read", "accounts_search", "account_stages_list", "users_list", ), ), ) .input( z.object({ updates: BULK_UPDATE_ACCOUNT_SCHEMA.array().min(1).max(100) }), ) .output(ACCOUNT_MUTATION_RESULT_SCHEMA.array()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) // Resolves every semantic reference before submitting the bounded batch. const accountAttributes = await Promise.all( input.updates.map(async (update) => ({ account_stage_id: update.stage ? await resolveApolloAccountStage(api, update.stage) : undefined, id: await resolveApolloAccount(api, update.account), name: update.name, owner_id: update.owner ? await resolveApolloUser(api, update.owner) : undefined, typed_custom_fields: update.customFields, })), ) // Keeps the response envelope readable while normalizing its accounts. const result = await api.request("accounts/bulk_update", { body: { account_attributes: accountAttributes }, responseSchema: ACCOUNT_MUTATION_RESULTS_RESPONSE_SCHEMA, }) return result.accounts.map(toAccountMutationResult) }) /** Assigns one owner to one or more Apollo accounts. */ export const setApolloAccountOwner = defineAction("Set Apollo account owner") .describe("Assigns an owner by ID, exact email, or exact name.") .account( "apollo", apolloAccountOptions( scope.and( "account_owners_update", "account_read", "accounts_search", "users_list", ), ), ) .input( z.object({ /** Account IDs, exact names/domains, or explicit ID references. */ accounts: APOLLO_REFERENCE_SCHEMA.array().min(1), /** Owner ID, exact email/name, or explicit ID reference. */ owner: APOLLO_REFERENCE_SCHEMA, }), ) .output(ACCOUNT_MUTATION_RESULT_SCHEMA.array()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) const [accountIds, ownerId] = await Promise.all([ resolveApolloReferences(input.accounts, (reference) => resolveApolloAccount(api, reference), ), resolveApolloUser(api, input.owner), ]) // Keeps the response envelope readable while normalizing its accounts. const result = await api.request("accounts/update_owners", { method: "POST", query: { "account_ids[]": accountIds, owner_id: ownerId }, responseSchema: ACCOUNT_MUTATION_RESULTS_RESPONSE_SCHEMA, }) return result.accounts.map(toAccountMutationResult) }) /** Lists account stages configured in the connected Apollo workspace. */ export const listApolloAccountStages = defineAction( "List Apollo account stages", ) .describe("Lists account stages for semantic stage selection.") .account("apollo", apolloAccountOptions("account_stages_list")) .input(z.object({})) .output(APOLLO_STAGE_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account }) => { // Keeps the response envelope readable while normalizing its stages. const result = await getApolloApi(account).request("account_stages", { responseSchema: ACCOUNT_STAGES_RESPONSE_SCHEMA, }) return result.account_stages.map(toApolloStage) }) /** * Converts shared account mutation fields to Apollo's provider shape. * * @param api - Authenticated Apollo client. * @param input - Parsed account mutation fields. */ async function toAccountMutationBody( api: ReturnType, input: z.output, ) { return { account_stage_id: input.stage ? await resolveApolloAccountStage(api, input.stage) : undefined, domain: input.domain, name: input.name, owner_id: input.owner ? await resolveApolloUser(api, input.owner) : undefined, phone: input.phone, raw_address: input.rawAddress, typed_custom_fields: input.customFields, } } /** * Converts bulk account creation fields to Apollo's provider shape. * * @param input - Parsed bulk account fields. */ function toBulkAccountFields( input: z.output, ) { return { domain: input.domain, facebook_url: input.facebookUrl, hubspot_id: input.hubspotId, linkedin_url: input.linkedinUrl, name: input.name, organization_id: input.organizationId, phone: input.phone, raw_address: input.rawAddress, salesforce_id: input.salesforceId, twitter_url: input.twitterUrl, typed_custom_fields: input.customFields, } } /** * Normalizes Apollo's sparse account-mutation receipt. * * @param value - Sparse provider account mutation result. */ function toAccountMutationResult( value: z.output, ) { return { accountStageId: value.account_stage_id ?? undefined, crmOwnerId: value.crm_owner_id ?? undefined, id: value.id, ownerId: value.owner_id ?? undefined, } } /** * Returns whether any mutable account field was provided. * * @param input - Parsed account update input. */ function hasAccountMutation( input: z.input & { target: unknown }, ) { return Object.entries(input).some( ([name, value]) => name !== "target" && value !== undefined, ) } /** * Returns whether a bulk account update includes a mutable field. * * @param input - Parsed per-account bulk update. * @param input.account - Account reference. * @param input.customFields - Custom field changes. * @param input.name - Account name change. * @param input.owner - Owner reference change. * @param input.stage - Stage reference change. */ function hasBulkAccountMutation(input: { account: unknown customFields?: unknown name?: unknown owner?: unknown stage?: unknown }) { return Object.entries(input).some( ([name, value]) => name !== "account" && value !== undefined, ) }