import { encodableSchema, type Encodable } from "@automate.ax/codec" import { CALENDLY_PAGINATION_SCHEMA, CalendlyApiError, getCalendlyApi as getCalendlyContractApi, } from "@automate.ax/integration-contracts/calendly" import * as z from "zod" import { retryableActionError, terminalActionError, } from "../../../automation/actions" import type { ResolvedIntegrationAccount } from "../../../automation/integrations" export const CALENDLY_ACCOUNT = "calendly" /** * Builds a Calendly OAuth account requirement for one scope. * * @param requiredScope - Calendly OAuth scope. */ export function calendlyAccountRequirement(requiredScope: string) { return { connections: [{ connectionMethodId: "oauth", requiredScope }], } as const } export const CALENDLY_PAGE_INPUT_SCHEMA = z.object({ count: z.number().int().min(1).max(100).prefault(20), pageToken: z.string().optional(), }) export const CALENDLY_PAGE_INFO_SCHEMA = z.object({ nextPageToken: z.string().nullable(), }) /** * Wraps the Calendly client with action retry classification. * * @param account - Resolved Calendly account. */ export function getCalendlyApi( account: ResolvedIntegrationAccount<"calendly">, ): ReturnType { const api = getCalendlyContractApi(account) const request = api.request.bind(api) api.request = async (path, options) => { try { return await request(path, options) } catch (error) { if (!(error instanceof CalendlyApiError)) throw error if (error.status === 429) { throw retryableActionError(error, { retryAt: error.rateLimitReset === undefined ? undefined : new Date(Date.now() + error.rateLimitReset * 1_000), }) } throw error.status < 500 ? terminalActionError(error) : error } } return api } /** * Omits undefined fields before encoding a Calendly request body. * * @param value - Action input object. */ export function compactCalendlyInput( value: Record, ): Encodable { return encodableSchema.parse( Object.fromEntries( Object.entries(value).filter(([, item]) => item !== undefined), ), ) } /** * Omits undefined Calendly query parameters. * * @param value - Query parameter object. */ export function compactCalendlyQuery( value: Record, ) { const result: Record = {} for (const [name, item] of Object.entries(value)) { if (item !== undefined) result[name] = item } return result } /** * Validates a Calendly time window against its endpoint limit. * * @param value - Requested time window. * @param value.endTime - Exclusive window end. * @param value.startTime - Inclusive window start. * @param context - Zod refinement context. * @param maximumDays - Provider maximum window length. */ export function validateCalendlyDateRange( value: { endTime: string; startTime: string }, context: z.RefinementCtx, maximumDays: number, ) { const milliseconds = Date.parse(value.endTime) - Date.parse(value.startTime) if (milliseconds <= 0) { context.addIssue({ code: "custom", message: "endTime must be after startTime.", path: ["endTime"], }) } else if (milliseconds > maximumDays * 24 * 60 * 60 * 1_000) { context.addIssue({ code: "custom", message: `The time range cannot exceed ${maximumDays} days.`, path: ["endTime"], }) } } /** * Builds a Calendly resource response schema. * * @param schema - Resource schema. */ export function resourceResponse(schema: TSchema) { return z.object({ resource: schema }) } /** * Builds a Calendly collection response schema. * * @param schema - Collection item schema. */ export function collectionResponse(schema: TSchema) { return z.object({ collection: schema.array(), pagination: CALENDLY_PAGINATION_SCHEMA, }) } /** * Normalizes Calendly collection pagination for public actions. * * @param response - Calendly collection response. * @param response.collection - Provider collection. * @param response.pagination - Provider pagination metadata. */ export function toPage(response: { collection: T[] pagination: z.output }) { return { items: response.collection, pageInfo: { nextPageToken: response.pagination.nextPageToken }, } }