import { GoogleAdsApiError, GOOGLE_ADS_CUSTOMER_ID_SCHEMA, GOOGLE_ADS_MUTATE_RESPONSE_SCHEMA, GOOGLE_ADS_SEARCH_PAGE_SCHEMA, normalizeGoogleAdsCustomerId, } from "@automate.ax/integration-contracts/google-ads" import * as z from "zod" import { parseRetryAfter, retryableActionError, terminalActionError, } from "../../automation/actions" import type { ResolvedIntegrationAccount } from "../../automation/integrations" import { getAutomationRuntimeState } from "../../automation/runtime" export const GOOGLE_ADS_SCOPE = "https://www.googleapis.com/auth/adwords" export const GOOGLE_DATA_MANAGER_SCOPE = "https://www.googleapis.com/auth/datamanager" export const GOOGLE_ADS_CUSTOMER_CONTEXT_SHAPE = { customerId: GOOGLE_ADS_CUSTOMER_ID_SCHEMA, loginCustomerId: GOOGLE_ADS_CUSTOMER_ID_SCHEMA.optional(), } as const export const GOOGLE_ADS_RESOURCE_NAME_INPUT_SCHEMA = z .string() .regex(/^customers\/\d{10}\/[A-Za-z][A-Za-z0-9]*(?:\/[^/]+)+$/) /** * Executes one Google Ads request with platform retry classification. * * @param account - Resolved Google account identity. * @param path - Google Ads REST path after the API version. * @param options - Request options. * @param options.body - Optional JSON request body. * @param options.loginCustomerId - Optional manager account ID. * @param options.method - HTTP method override. * @param options.responseSchema - Schema for the response payload. */ export async function requestGoogleAds( account: ResolvedIntegrationAccount<"google">, path: string, options: { body?: z.input> loginCustomerId?: string method?: "GET" | "POST" responseSchema: TSchema }, ) { try { const transport = getAutomationRuntimeState()?.googleAdsRequest if (!transport || !account.accountId) { throw new Error("Google Ads requests require the platform runtime.") } const result = await transport({ accountId: account.accountId, body: options.body, loginCustomerId: options.loginCustomerId, method: options.method, path, }) if (!result.ok) throw new GoogleAdsApiError(result) return options.responseSchema.parse(result.payload) } catch (error) { if (!(error instanceof GoogleAdsApiError)) throw error if (error.status === 429 || error.providerStatus === "RESOURCE_EXHAUSTED") { throw retryableActionError(error, { retryAt: parseRetryAfter(error.retryAfter), }) } if (error.status < 500) throw terminalActionError(error) throw error } } /** * Executes one bounded Google Data Manager request for a Google Ads action. * * @param account - Resolved Google account identity. * @param path - Data Manager REST path after the API version. * @param options - Request options. * @param options.body - Optional JSON request body. * @param options.method - HTTP method override. * @param options.responseSchema - Schema for the response payload. */ export async function requestGoogleDataManager( account: ResolvedIntegrationAccount<"google">, path: string, options: { body?: z.input> method?: "GET" | "POST" responseSchema: TSchema }, ) { try { const transport = getAutomationRuntimeState()?.googleAdsRequest if (!transport || !account.accountId) { throw new Error( "Google Data Manager requests require the platform runtime.", ) } const result = await transport({ accountId: account.accountId, api: "dataManager", body: options.body, method: options.method, path, }) if (!result.ok) throw new GoogleAdsApiError(result) return options.responseSchema.parse(result.payload) } catch (error) { if (!(error instanceof GoogleAdsApiError)) throw error if (error.status === 429 || error.providerStatus === "RESOURCE_EXHAUSTED") { throw retryableActionError(error, { retryAt: parseRetryAfter(error.retryAfter), }) } if (error.status < 500) throw terminalActionError(error) throw error } } /** * Runs one fixed-size Google Ads search page. * * @param account - Resolved Google account identity. * @param input - Search request. * @param input.customerId - Target customer ID. * @param input.loginCustomerId - Optional manager account ID. * @param input.pageToken - Optional next-page token. * @param input.query - GAQL query. */ export function searchGoogleAds( account: ResolvedIntegrationAccount<"google">, input: { customerId: string loginCustomerId?: string pageToken?: string query: string }, ) { return requestGoogleAds( account, `customers/${normalizeGoogleAdsCustomerId(input.customerId)}/googleAds:search`, { body: { ...(input.pageToken && { pageToken: input.pageToken }), query: input.query, }, loginCustomerId: input.loginCustomerId, responseSchema: GOOGLE_ADS_SEARCH_PAGE_SCHEMA, }, ) } /** * Runs one single-operation Google Ads resource mutation. * * @param account - Resolved Google account identity. * @param input - Mutation request. * @param input.body - Google Ads mutation body. * @param input.collection - REST resource collection. * @param input.customerId - Target customer ID. * @param input.loginCustomerId - Optional manager account ID. */ export async function mutateGoogleAdsResource( account: ResolvedIntegrationAccount<"google">, input: { body: z.output> collection: string customerId: string loginCustomerId?: string }, ) { return ( ( await requestGoogleAds( account, `customers/${normalizeGoogleAdsCustomerId(input.customerId)}/${input.collection}:mutate`, { body: input.body, loginCustomerId: input.loginCustomerId, responseSchema: GOOGLE_ADS_MUTATE_RESPONSE_SCHEMA, }, ) ).results[0] ?? failMissingMutationResult() ) } /** * Builds a canonical resource name from either a numeric ID or full name. * * @param customerId - Target customer ID. * @param collection - REST resource collection. * @param reference - Numeric resource ID or full resource name. * @throws {TypeError} When the reference is neither a numeric ID nor a name. */ export function googleAdsResourceName( customerId: string, collection: string, reference: string, ) { if (reference.startsWith("customers/")) { return GOOGLE_ADS_RESOURCE_NAME_INPUT_SCHEMA.parse(reference) } if (!/^\d+(?:~\d+)?$/.test(reference)) { throw new TypeError( `Google Ads ${collection} reference must be a numeric ID or resource name.`, ) } return `customers/${normalizeGoogleAdsCustomerId(customerId)}/${collection}/${reference}` } /** @throws {Error} Always, because the mutation response omitted its result. */ function failMissingMutationResult(): never { throw new Error("Google Ads mutation returned no result.") }