import * as z from "zod" import { defineAction } from "../../../automation/actions" import { integrationScope as scope } from "../../../automation/integrations" import { apolloAccountOptions, getApolloApi, type ApolloApi } from "../lib/api" import { resolveApolloContact, resolveApolloReferences, } from "../lib/references" import { APOLLO_CONTACT_SCHEMA, APOLLO_PAGINATION_SCHEMA, APOLLO_PROVIDER_CONTACT_SCHEMA, APOLLO_PROVIDER_EMAIL_ACCOUNT_SCHEMA, APOLLO_PROVIDER_PAGINATION_SCHEMA, APOLLO_PROVIDER_SEQUENCE_SCHEMA, APOLLO_PROVIDER_USER_SCHEMA, APOLLO_REFERENCE_SCHEMA, APOLLO_SEQUENCE_SCHEMA, toApolloContact, toApolloPagination, toApolloSequence, toApolloUser, } from "../lib/schemas" const APOLLO_SEQUENCE_PAGE_RESPONSE_SCHEMA = z.looseObject({ emailer_campaigns: APOLLO_PROVIDER_SEQUENCE_SCHEMA.array(), pagination: APOLLO_PROVIDER_PAGINATION_SCHEMA, }) const APOLLO_EMAIL_SCHEDULE_SCHEMA = z.object({ /** Time at which Apollo created the schedule. */ createdAt: z.date().optional(), /** Whether this is the workspace's default sending schedule. */ default: z.boolean(), /** Stable Apollo sending-schedule identifier. */ id: z.string(), /** Human-readable sending-schedule name. */ name: z.string(), /** Weekly sending windows keyed by day name. */ schedule: z.record(z.string(), z.tuple([z.number(), z.number()]).array()), /** Whether Apollo skips sending on holidays. */ skipHolidays: z.boolean(), /** IANA time zone used by the schedule. */ timeZone: z.string(), /** Whether Apollo sends in each contact's time zone. */ useContactsTimeZone: z.boolean(), }) const APOLLO_PROVIDER_EMAIL_SCHEDULE_SCHEMA = z.looseObject({ created_at: z.iso.datetime({ offset: true }).nullish(), default: z.boolean().nullish(), id: z.string(), name: z.string(), schedule_hash: z .record(z.string(), z.tuple([z.number(), z.number()]).array()) .nullish(), skip_holidays: z.boolean().nullish(), time_zone: z.string().nullish(), use_contacts_time_zone: z.boolean().nullish(), }) const APOLLO_EMAIL_SCHEDULES_RESPONSE_SCHEMA = z.looseObject({ emailer_schedules: APOLLO_PROVIDER_EMAIL_SCHEDULE_SCHEMA.array(), }) const APOLLO_SEQUENCE_STEP_SCHEMA = z.object({ /** Stable Apollo sequence-step identifier. */ id: z.string(), /** Optional provider note. */ note: z.string().optional(), /** One-based position in the sequence. */ position: z.number().int().nonnegative().optional(), /** Stable Apollo sequence identifier. */ sequenceId: z.string(), /** Provider step type. */ type: z.string().optional(), /** Wait interval before the step. */ waitTime: z.number().optional(), /** Unit used for the wait interval. */ waitMode: z.string().optional(), }) const APOLLO_PROVIDER_SEQUENCE_STEP_SCHEMA = z.looseObject({ emailer_campaign_id: z.string(), id: z.string(), note: z.string().nullish(), position: z.number().int().nonnegative().nullish(), type: z.string().nullish(), wait_mode: z.string().nullish(), wait_time: z.number().nullish(), }) const APOLLO_SEQUENCE_TOUCH_SCHEMA = z.object({ /** Stable Apollo sequence-touch identifier. */ id: z.string(), /** Stable Apollo email template identifier. */ templateId: z.string().optional(), /** Stable Apollo sequence-step identifier. */ stepId: z.string(), /** Provider approval status. */ status: z.string().optional(), /** Provider touch type. */ type: z.string().optional(), }) const APOLLO_PROVIDER_SEQUENCE_TOUCH_SCHEMA = z.looseObject({ emailer_step_id: z.string(), emailer_template_id: z.string().nullish(), id: z.string(), status: z.string().nullish(), type: z.string().nullish(), }) const APOLLO_ADD_CONTACTS_RESPONSE_SCHEMA = z.looseObject({ contacts: APOLLO_PROVIDER_CONTACT_SCHEMA.array().prefault([]), emailer_campaign: APOLLO_PROVIDER_SEQUENCE_SCHEMA, emailer_steps: APOLLO_PROVIDER_SEQUENCE_STEP_SCHEMA.array().prefault([]), emailer_touches: APOLLO_PROVIDER_SEQUENCE_TOUCH_SCHEMA.array().prefault([]), skipped_contact_ids: z.record(z.string(), z.string()).prefault({}), }) const APOLLO_ADD_CONTACTS_RESULT_SCHEMA = z.object({ /** Contacts added to the sequence. */ contacts: APOLLO_CONTACT_SCHEMA.array(), /** Canonical Apollo email-account IDs used for sending. */ emailAccountIds: z.string().array(), /** Contacts Apollo skipped, keyed by canonical contact ID. */ skippedContacts: z.record(z.string(), z.string()), /** Sequence receiving the contacts. */ sequence: APOLLO_SEQUENCE_SCHEMA, /** Sequence steps returned by Apollo. */ steps: APOLLO_SEQUENCE_STEP_SCHEMA.array(), /** Sequence email touches returned by Apollo. */ touches: APOLLO_SEQUENCE_TOUCH_SCHEMA.array(), /** Canonical Apollo user ID attributed to the action. */ userId: z.string().optional(), }) const APOLLO_SEQUENCE_STATUS_MODE_SCHEMA = z.enum([ "mark_as_finished", "remove", "stop", ]) const APOLLO_PROVIDER_STATUS_JOB_SCHEMA = z.looseObject({ batch_size: z.number().int().nullable().optional(), entity_ids: z.string().array().prefault([]), id: z.string(), params: z .looseObject({ mode: APOLLO_SEQUENCE_STATUS_MODE_SCHEMA.optional(), sequence_ids: z.string().array().optional(), }) .optional(), progress: z.number().int().prefault(0), }) const APOLLO_SEQUENCE_STATUS_RESPONSE_SCHEMA = z.looseObject({ entity_progress_job: APOLLO_PROVIDER_STATUS_JOB_SCHEMA, }) const APOLLO_SEQUENCE_STATUS_RESULT_SCHEMA = z.object({ /** Provider batch size. */ batchSize: z.number().int().nullable().optional(), /** Canonical Apollo contact IDs being processed. */ contactIds: z.string().array(), /** Stable Apollo background-job identifier. */ jobId: z.string(), /** Requested sequence status operation. */ mode: APOLLO_SEQUENCE_STATUS_MODE_SCHEMA, /** Current provider progress value. */ progress: z.number().int(), /** Canonical Apollo sequence IDs being processed. */ sequenceIds: z.string().array(), }) const APOLLO_SEQUENCE_MUTATION_RESPONSE_SCHEMA = z.looseObject({ emailer_campaign: z.looseObject({ active: z.boolean().optional(), archived: z.boolean().optional(), deleted: z.boolean().optional(), id: z.string(), unique_scheduled: z.number().int().nonnegative().optional(), }), emailer_steps: APOLLO_PROVIDER_SEQUENCE_STEP_SCHEMA.array().optional(), }) const APOLLO_SEQUENCE_MUTATION_RESULT_SCHEMA = z.object({ /** Whether the sequence is active after the operation. */ active: z.boolean().optional(), /** Whether the sequence is archived after the operation. */ archived: z.boolean().optional(), /** Whether Apollo marked the sequence deleted. */ deleted: z.boolean().optional(), /** Canonical Apollo sequence ID. */ sequenceId: z.string(), /** Sequence steps returned by Apollo. */ steps: APOLLO_SEQUENCE_STEP_SCHEMA.array(), /** Number of contacts currently scheduled. */ uniqueScheduled: z.number().int().nonnegative().optional(), }) const APOLLO_SEQUENCE_EVENT_TYPE_SCHEMA = z.enum([ "completed", "enrolled", "failed", "paused", "removed", "replied", "resumed", ]) const APOLLO_PROVIDER_SEQUENCE_EVENT_SCHEMA = z.looseObject({ emailer_message_id: z.string().optional(), occurred_at: z.iso.datetime({ offset: true }), reason: z.string().optional(), sequence_id: z.string().optional(), sequence_name: z.string().optional(), step_position: z.number().int().nonnegative().optional(), type: APOLLO_SEQUENCE_EVENT_TYPE_SCHEMA, }) const APOLLO_SEQUENCE_EVENT_SCHEMA = z.object({ /** Stable Apollo email message ID for reply events. */ emailMessageId: z.string().optional(), /** Time at which the event occurred. */ occurredAt: z.date(), /** Provider-recorded reason for the event. */ reason: z.string().optional(), /** Canonical Apollo sequence ID associated with the event. */ sequenceId: z.string().optional(), /** Human-readable sequence name at event time. */ sequenceName: z.string().optional(), /** Sequence step position associated with the event. */ stepPosition: z.number().int().nonnegative().optional(), /** Sequence lifecycle event type. */ type: APOLLO_SEQUENCE_EVENT_TYPE_SCHEMA, }) const APOLLO_SEQUENCE_ACTIVITY_RESPONSE_SCHEMA = z.looseObject({ contact_id: z.string(), events: APOLLO_PROVIDER_SEQUENCE_EVENT_SCHEMA.array(), }) const APOLLO_SEQUENCE_ACTIVITY_SCHEMA = z.object({ /** Canonical Apollo contact ID. */ contactId: z.string(), /** Most recent sequence events, ordered newest first. */ events: APOLLO_SEQUENCE_EVENT_SCHEMA.array(), }) const APOLLO_USERS_RESPONSE_SCHEMA = z.looseObject({ pagination: APOLLO_PROVIDER_PAGINATION_SCHEMA, users: APOLLO_PROVIDER_USER_SCHEMA.array(), }) const APOLLO_EMAIL_ACCOUNTS_RESPONSE_SCHEMA = z.looseObject({ email_accounts: APOLLO_PROVIDER_EMAIL_ACCOUNT_SCHEMA.array(), }) /** Searches sequences in the connected Apollo workspace. */ export const searchApolloSequences = defineAction("Search Apollo sequences") .describe("Searches a page of Apollo sequences by a name substring.") .account("apollo", apolloAccountOptions("emailer_campaigns_search")) .input( z.object({ /** Name substring used by Apollo's sequence search. */ name: z.string().trim().min(1).optional(), /** One-based result page. */ page: z.number().int().min(1).prefault(1), /** Number of sequences to request per page. */ perPage: z.number().int().min(1).max(100).prefault(100), }), ) .output( z.object({ pagination: APOLLO_PAGINATION_SCHEMA, sequences: APOLLO_SEQUENCE_SCHEMA.array(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await requestSequencePage( getApolloApi(account), input.page, input.perPage, input.name, ) return { pagination: toApolloPagination(result.pagination), sequences: result.emailer_campaigns.map(toApolloSequence), } }) /** Lists Apollo sequence email-sending schedules. */ export const listApolloEmailSchedules = defineAction( "List Apollo email schedules", ) .describe("Lists every sending schedule available to Apollo sequences.") .account("apollo", apolloAccountOptions("emailer_schedules_list")) .output(z.object({ emailSchedules: APOLLO_EMAIL_SCHEDULE_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account }) => ({ emailSchedules: ( await getApolloApi(account).request("/emailer_schedules", { responseSchema: APOLLO_EMAIL_SCHEDULES_RESPONSE_SCHEMA, }) ).emailer_schedules.map(toApolloEmailSchedule), })) /** Adds contacts or named Apollo lists to a sequence. */ export const addContactsToApolloSequence = defineAction( "Add contacts to Apollo sequence", ) .describe( "Adds contacts to a sequence using contact references or list names.", ) .account( "apollo", apolloAccountOptions( scope.and( "emailer_campaigns_add_contact_ids", "emailer_campaigns_search", "email_accounts_list", "contact_read", "contacts_search", "users_list", ), ), ) .input( z .object({ /** Add contacts already waiting in Apollo's processing queue. */ addIfInQueue: z.boolean().optional(), /** Automatically resume paused contacts at this time. */ autoUnpauseAt: z.iso.datetime({ offset: true }).optional(), /** Add contacts even without ownership permission. */ contactsWithoutOwnershipPermission: z.boolean().optional(), /** Contact emails, names, IDs, or explicit ID references to add. */ contacts: APOLLO_REFERENCE_SCHEMA.array().min(1).optional(), /** Email-account emails or IDs; `{ id }` skips mailbox lookup. */ emailAccounts: APOLLO_REFERENCE_SCHEMA.array().min(1), /** Add contacts that are active in another sequence. */ includeActiveInOtherSequences: z.boolean().optional(), /** Add contacts that finished another sequence. */ includeFinishedInOtherSequences: z.boolean().optional(), /** Add contacts that have recently changed jobs. */ includeJobChanges: z.boolean().optional(), /** Add contacts without email addresses. */ includeMissingEmails: z.boolean().optional(), /** Add contacts from companies already present in this sequence. */ includeSameCompany: z.boolean().optional(), /** Add contacts with unverified email addresses. */ includeUnverifiedEmails: z.boolean().optional(), /** Apollo-native list names whose contacts should be added. */ listNames: z.string().trim().min(1).array().min(1).optional(), /** Exact sequence name or ID; `{ id }` skips sequence lookup. */ sequence: APOLLO_REFERENCE_SCHEMA, /** Specific sender address within the selected email account. */ sendFromEmailAddress: z.email().optional(), /** Skip Apollo's contact verification during enrollment. */ skipContactVerification: z.boolean().optional(), /** Initial sequence status for the contacts. */ status: z.enum(["active", "paused"]).optional(), /** Apollo user email, name, or ID attributed to the action. */ user: APOLLO_REFERENCE_SCHEMA.optional(), }) .superRefine((input, context) => { if (input.contacts === undefined && input.listNames === undefined) { context.addIssue({ code: "custom", message: "Provide contacts or listNames.", }) } if (input.autoUnpauseAt !== undefined && input.status !== "paused") { context.addIssue({ code: "custom", message: "autoUnpauseAt requires status to be paused.", path: ["autoUnpauseAt"], }) } }), ) .output(APOLLO_ADD_CONTACTS_RESULT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) const [sequenceId, emailAccountIds, contactIds, userId] = await Promise.all( [ resolveSequenceIds(api, [input.sequence]).then(([id]) => id!), resolveEmailAccountIds(api, input.emailAccounts), input.contacts === undefined ? Promise.resolve(undefined) : resolveContactIds(api, input.contacts), input.user === undefined ? Promise.resolve(undefined) : resolveUserId(api, input.user), ], ) const result = await api.request( `/emailer_campaigns/${encodeURIComponent(sequenceId)}/add_contact_ids`, { method: "POST", query: { add_if_in_queue: input.addIfInQueue, auto_unpause_at: input.autoUnpauseAt, contact_verification_skipped: input.skipContactVerification, "contact_ids[]": contactIds, contacts_without_ownership_permission: input.contactsWithoutOwnershipPermission, emailer_campaign_id: sequenceId, "label_names[]": input.listNames, send_email_from_email_account_id: emailAccountIds, send_email_from_email_address: input.sendFromEmailAddress, sequence_active_in_other_campaigns: input.includeActiveInOtherSequences, sequence_finished_in_other_campaigns: input.includeFinishedInOtherSequences, sequence_job_change: input.includeJobChanges, sequence_no_email: input.includeMissingEmails, sequence_same_company_in_same_campaign: input.includeSameCompany, sequence_unverified_email: input.includeUnverifiedEmails, status: input.status, user_id: userId, }, responseSchema: APOLLO_ADD_CONTACTS_RESPONSE_SCHEMA, }, ) return { contacts: result.contacts.map(toApolloContact), emailAccountIds, sequence: toApolloSequence(result.emailer_campaign), skippedContacts: result.skipped_contact_ids, steps: result.emailer_steps.map(toApolloSequenceStep), touches: result.emailer_touches.map(toApolloSequenceTouch), ...(userId && { userId }), } }) /** Updates contacts' state across one or more Apollo sequences. */ export const updateApolloContactSequenceStatus = defineAction( "Update Apollo contact sequence status", ) .describe("Finishes, removes, or stops contacts in Apollo sequences.") .account( "apollo", apolloAccountOptions( scope.and( "emailer_campaigns_remove_or_stop_contact_ids", "emailer_campaigns_search", "contact_read", "contacts_search", ), ), ) .input( z.object({ /** Contact emails, names, IDs, or explicit ID references to update. */ contacts: APOLLO_REFERENCE_SCHEMA.array().min(1), /** Status operation applied to the selected contacts. */ mode: APOLLO_SEQUENCE_STATUS_MODE_SCHEMA, /** Exact sequence names or IDs; `{ id }` skips lookup per item. */ sequences: APOLLO_REFERENCE_SCHEMA.array().min(1), }), ) .output(APOLLO_SEQUENCE_STATUS_RESULT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) const [sequenceIds, contactIds] = await Promise.all([ resolveSequenceIds(api, input.sequences), resolveContactIds(api, input.contacts), ]) const { entity_progress_job: job } = await api.request( "/emailer_campaigns/remove_or_stop_contact_ids", { method: "POST", query: { "contact_ids[]": contactIds, "emailer_campaign_ids[]": sequenceIds, mode: input.mode, }, responseSchema: APOLLO_SEQUENCE_STATUS_RESPONSE_SCHEMA, }, ) return { batchSize: job.batch_size, contactIds: job.entity_ids.length > 0 ? job.entity_ids : contactIds, jobId: job.id, mode: job.params?.mode ?? input.mode, progress: job.progress, sequenceIds: job.params?.sequence_ids ?? sequenceIds, } }) /** Activates an Apollo sequence selected by exact name or ID. */ export const activateApolloSequence = defineAction("Activate Apollo sequence") .describe("Activates an inactive Apollo sequence that has at least one step.") .account( "apollo", apolloAccountOptions( scope.and("emailer_campaigns_approve", "emailer_campaigns_search"), ), ) .input(sequenceReferenceInput()) .output(APOLLO_SEQUENCE_MUTATION_RESULT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateSequence(getApolloApi(account), input.sequence, "approve"), ) /** Deactivates an Apollo sequence selected by exact name or ID. */ export const deactivateApolloSequence = defineAction( "Deactivate Apollo sequence", ) .describe("Stops an active Apollo sequence and pauses its contacts.") .account( "apollo", apolloAccountOptions( scope.and("emailer_campaigns_abort", "emailer_campaigns_search"), ), ) .input(sequenceReferenceInput()) .output(APOLLO_SEQUENCE_MUTATION_RESULT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateSequence(getApolloApi(account), input.sequence, "abort"), ) /** Archives an Apollo sequence selected by exact name or ID. */ export const archiveApolloSequence = defineAction("Archive Apollo sequence") .describe("Archives a sequence and finishes its current contacts.") .account( "apollo", apolloAccountOptions( scope.and("emailer_campaigns_archive", "emailer_campaigns_search"), ), ) .input(sequenceReferenceInput()) .output(APOLLO_SEQUENCE_MUTATION_RESULT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => mutateSequence(getApolloApi(account), input.sequence, "archive"), ) /** Gets recent sequence lifecycle activity for one Apollo contact. */ export const getApolloContactSequenceActivity = defineAction( "Get Apollo contact sequence activity", ) .describe("Gets recent sequence events for one saved Apollo contact.") .account( "apollo", apolloAccountOptions( scope.and( "emailer_campaigns_activity_feed", "contact_read", "contacts_search", "emailer_campaigns_search", ), ), ) .input( z.object({ /** Contact email, name, ID, or explicit ID reference. */ contact: APOLLO_REFERENCE_SCHEMA, /** Maximum recent events returned. */ limit: z.number().int().min(1).max(50).prefault(50), /** Optional exact sequence name or ID; `{ id }` skips lookup. */ sequence: APOLLO_REFERENCE_SCHEMA.optional(), }), ) .output(APOLLO_SEQUENCE_ACTIVITY_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) const [contactId, sequenceId] = await Promise.all([ resolveApolloContact(api, input.contact), input.sequence ? resolveSequenceIds(api, [input.sequence]).then(([id]) => id) : Promise.resolve(undefined), ]) const result = await api.request("/emailer_campaigns/activity_feed", { body: { contact_id: contactId, per_page: input.limit, ...(sequenceId && { sequence_id: sequenceId }), }, responseSchema: APOLLO_SEQUENCE_ACTIVITY_RESPONSE_SCHEMA, }) return { contactId: result.contact_id, events: result.events.map(toApolloSequenceEvent), } }) /** Creates the repeated semantic sequence-reference input. */ function sequenceReferenceInput() { return z.object({ /** Exact sequence name or ID; `{ id }` skips sequence lookup. */ sequence: APOLLO_REFERENCE_SCHEMA, }) } /** * Requests one provider sequence-search page. * * @param api - Authenticated Apollo API client. * @param page - One-based page number. * @param perPage - Requested page size. * @param name - Optional provider name query. */ function requestSequencePage( api: ApolloApi, page: number, perPage: number, name?: string, ) { return api.request("/emailer_campaigns/search", { method: "POST", query: { page, per_page: perPage, q_name: name }, responseSchema: APOLLO_SEQUENCE_PAGE_RESPONSE_SCHEMA, }) } /** * Lists every Apollo sequence for deterministic reference resolution. * * @param api - Authenticated Apollo API client. */ async function listAllSequences(api: ApolloApi) { const sequences: z.output[] = [] let page = 1 let totalPages: number do { const result = await requestSequencePage(api, page, 100) sequences.push(...result.emailer_campaigns) totalPages = result.pagination.total_pages page += 1 } while (page <= totalPages) return sequences } /** * Resolves sequence references with at most one complete sequence listing. * * @param api - Authenticated Apollo API client. * @param references - Sequence names, IDs, or explicit ID references. */ async function resolveSequenceIds( api: ApolloApi, references: z.output[], ) { if (references.every((reference) => typeof reference !== "string")) { return references.map((reference) => typeof reference === "string" ? reference : reference.id, ) } const sequences = await listAllSequences(api) return references.map((reference) => resolveReference(reference, sequences, { labels: ({ name }) => [name], resource: "sequence", }), ) } /** * Resolves mailbox references with one email-account listing. * * @param api - Authenticated Apollo API client. * @param references - Mailbox emails, IDs, or explicit ID references. */ async function resolveEmailAccountIds( api: ApolloApi, references: z.output[], ) { if (references.every((reference) => typeof reference !== "string")) { return references.map((reference) => typeof reference === "string" ? reference : reference.id, ) } const result = await api.request("/email_accounts", { responseSchema: APOLLO_EMAIL_ACCOUNTS_RESPONSE_SCHEMA, }) return references.map((reference) => resolveReference(reference, result.email_accounts, { labels: (emailAccount) => [ emailAccount.email, ...(emailAccount.aliases ?? []), ], resource: "email account", }), ) } /** * Resolves contact references while preserving caller order. * * @param api - Authenticated Apollo API client. * @param references - Contact emails, names, IDs, or explicit ID references. */ function resolveContactIds( api: ApolloApi, references: z.output[], ) { return resolveApolloReferences(references, (reference) => resolveApolloContact(api, reference), ) } /** * Resolves a user reference across paginated workspace users. * * @param api - Authenticated Apollo API client. * @param reference - User email, name, ID, or explicit ID reference. */ async function resolveUserId( api: ApolloApi, reference: z.output, ) { if (typeof reference !== "string") return reference.id const users: z.output[] = [] let page = 1 let totalPages: number do { const result = await api.request("/users/search", { query: { page, per_page: 100 }, responseSchema: APOLLO_USERS_RESPONSE_SCHEMA, }) users.push(...result.users) totalPages = result.pagination.total_pages page += 1 } while (page <= totalPages) return resolveReference(reference, users, { labels: (user) => { const normalizedUser = toApolloUser(user) return [normalizedUser.email, normalizedUser.name] }, resource: "user", }) } /** * Resolves an Apollo reference by canonical ID, then exact normalized label. * * @param reference - Human-readable or canonical Apollo reference. * @param candidates - Provider resources eligible for selection. * @param options - Resource-specific matching behavior. * @param options.labels - Human labels accepted for one candidate. * @param options.resource - Resource name used in errors. * @throws When the reference has no match or multiple human-label matches. */ function resolveReference( reference: z.output, candidates: TCandidate[], options: { labels: (candidate: TCandidate) => (string | undefined)[] resource: string }, ) { if (typeof reference !== "string") return reference.id const idMatch = candidates.find(({ id }) => id === reference) if (idMatch) return idMatch.id const normalizedReference = reference.trim().toLowerCase() const labelMatches = candidates.filter((candidate) => options .labels(candidate) .some((label) => label?.trim().toLowerCase() === normalizedReference), ) if (labelMatches.length === 1) return labelMatches[0]!.id if (labelMatches.length > 1) { throw new Error( `Apollo ${options.resource} "${reference}" is ambiguous; matching IDs: ${labelMatches.map(({ id }) => id).join(", ")}.`, ) } throw new Error(`Apollo ${options.resource} "${reference}" was not found.`) } /** * Mutates an Apollo sequence's active or archived state. * * @param api - Authenticated Apollo API client. * @param reference - Sequence name, ID, or explicit ID reference. * @param operation - Provider sequence lifecycle operation. */ async function mutateSequence( api: ApolloApi, reference: z.output, operation: "abort" | "approve" | "archive", ) { const result = await api.request( `/emailer_campaigns/${encodeURIComponent((await resolveSequenceIds(api, [reference]))[0]!)}/${operation}`, { method: "POST", responseSchema: APOLLO_SEQUENCE_MUTATION_RESPONSE_SCHEMA, }, ) return { active: result.emailer_campaign.active, archived: result.emailer_campaign.archived, deleted: result.emailer_campaign.deleted, sequenceId: result.emailer_campaign.id, steps: (result.emailer_steps ?? []).map(toApolloSequenceStep), uniqueScheduled: result.emailer_campaign.unique_scheduled, } } /** * Normalizes one Apollo sending schedule. * * @param schedule - Provider sending schedule. */ function toApolloEmailSchedule( schedule: z.output, ) { return { createdAt: schedule.created_at ? new Date(schedule.created_at) : undefined, default: schedule.default ?? false, id: schedule.id, name: schedule.name, schedule: schedule.schedule_hash ?? {}, skipHolidays: schedule.skip_holidays ?? false, timeZone: schedule.time_zone ?? "UTC", useContactsTimeZone: schedule.use_contacts_time_zone ?? false, } } /** * Normalizes one Apollo sequence step. * * @param step - Provider sequence step. */ function toApolloSequenceStep( step: z.output, ) { return { id: step.id, note: step.note ?? undefined, position: step.position ?? undefined, sequenceId: step.emailer_campaign_id, type: step.type ?? undefined, waitMode: step.wait_mode ?? undefined, waitTime: step.wait_time ?? undefined, } } /** * Normalizes one Apollo sequence touch. * * @param touch - Provider sequence touch. */ function toApolloSequenceTouch( touch: z.output, ) { return { id: touch.id, status: touch.status ?? undefined, stepId: touch.emailer_step_id, templateId: touch.emailer_template_id ?? undefined, type: touch.type ?? undefined, } } /** * Normalizes one Apollo contact sequence event. * * @param event - Provider sequence event. */ function toApolloSequenceEvent( event: z.output, ) { return { emailMessageId: event.emailer_message_id, occurredAt: new Date(event.occurred_at), reason: event.reason, sequenceId: event.sequence_id, sequenceName: event.sequence_name, stepPosition: event.step_position, type: event.type, } }