import { GOOGLE_ADS_AD_GROUP_AD_SCHEMA, GOOGLE_ADS_AD_GROUP_SCHEMA, GOOGLE_ADS_ASSET_SCHEMA, GOOGLE_ADS_AUDIENCE_SCHEMA, GOOGLE_ADS_CAMPAIGN_BUDGET_SCHEMA, GOOGLE_ADS_CAMPAIGN_SCHEMA, GOOGLE_ADS_CONVERSION_ACTION_SCHEMA, GOOGLE_ADS_CUSTOMER_CLIENT_SCHEMA, GOOGLE_ADS_CUSTOMER_SCHEMA, GOOGLE_ADS_MUTATE_RESULT_SCHEMA, GOOGLE_ADS_SEARCH_PAGE_SCHEMA, GOOGLE_ADS_USER_LIST_SCHEMA, normalizeGoogleAdsCustomerId, } from "@automate.ax/integration-contracts/google-ads" import type { Encodable } from "@automate.ax/codec" import type { JsonValue } from "type-fest" import * as z from "zod" import { defineAction } from "../../automation/actions" import { GOOGLE_ADS_CUSTOMER_CONTEXT_SHAPE, GOOGLE_ADS_RESOURCE_NAME_INPUT_SCHEMA, GOOGLE_ADS_SCOPE, GOOGLE_DATA_MANAGER_SCOPE, googleAdsResourceName, mutateGoogleAdsResource, requestGoogleAds, requestGoogleDataManager, searchGoogleAds, } from "./lib" const PAGE_INPUT_SHAPE = { ...GOOGLE_ADS_CUSTOMER_CONTEXT_SHAPE, pageToken: z.string().min(1).optional(), } as const const STATUS_SCHEMA = z.enum(["ENABLED", "PAUSED"]) const AMOUNT_MICROS_SCHEMA = z.string().regex(/^\d+$/) const CAMPAIGN_BIDDING_STRATEGY_SCHEMA = z.discriminatedUnion("type", [ z.object({ enhancedCpcEnabled: z.boolean().optional(), type: z.literal("manualCpc"), }), z.object({ targetCpaMicros: AMOUNT_MICROS_SCHEMA.optional(), type: z.literal("maximizeConversions"), }), z.object({ targetRoas: z.number().positive().optional(), type: z.literal("maximizeConversionValue"), }), z.object({ cpcBidCeilingMicros: AMOUNT_MICROS_SCHEMA.optional(), type: z.literal("targetSpend"), }), z.object({ targetRoas: z.number().min(0.01).max(1_000), type: z.literal("targetRoas"), }), z.object({ cpcBidCeilingMicros: AMOUNT_MICROS_SCHEMA, location: z.enum([ "ABSOLUTE_TOP_OF_PAGE", "ANYWHERE_ON_PAGE", "TOP_OF_PAGE", ]), locationFractionMicros: AMOUNT_MICROS_SCHEMA, type: z.literal("targetImpressionShare"), }), z.object({ cpcBidCeilingMicros: AMOUNT_MICROS_SCHEMA.optional(), enhancedCpcEnabled: z.boolean().optional(), type: z.literal("percentCpc"), }), ]) const CAMPAIGN_CREATE_SHAPE = { advertisingChannelSubType: z.string().min(1).optional(), advertisingChannelType: z.enum([ "DEMAND_GEN", "DISPLAY", "HOTEL", "PERFORMANCE_MAX", "SEARCH", "SHOPPING", "VIDEO", ]), biddingStrategy: CAMPAIGN_BIDDING_STRATEGY_SCHEMA, campaignBudget: GOOGLE_ADS_RESOURCE_NAME_INPUT_SCHEMA, containsEuPoliticalAdvertising: z.enum([ "CONTAINS_EU_POLITICAL_ADVERTISING", "DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING", ]), endDateTime: z .string() .regex(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/) .optional(), hotelSetting: z.object({ hotelCenterId: AMOUNT_MICROS_SCHEMA }).optional(), name: z.string().trim().min(1), shoppingSetting: z .object({ campaignPriority: z.number().int().min(0).max(2), enableLocal: z.boolean().optional(), feedLabel: z .string() .regex(/^[A-Z0-9_-]{1,20}$/) .optional(), merchantId: AMOUNT_MICROS_SCHEMA, }) .optional(), startDateTime: z .string() .regex(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/) .optional(), status: STATUS_SCHEMA.optional(), } as const const CAMPAIGN_UPDATE_SHAPE = { campaignBudget: GOOGLE_ADS_RESOURCE_NAME_INPUT_SCHEMA.optional(), endDateTime: z .string() .regex(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/) .optional(), name: z.string().trim().min(1).optional(), startDateTime: z .string() .regex(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/) .optional(), status: STATUS_SCHEMA.optional(), } as const const CAMPAIGN_BUDGET_CREATE_SHAPE = { amountMicros: AMOUNT_MICROS_SCHEMA.optional(), deliveryMethod: z.enum(["ACCELERATED", "STANDARD"]).optional(), explicitlyShared: z.boolean().optional(), name: z.string().trim().min(1), period: z.enum(["CUSTOM_PERIOD", "DAILY"]).optional(), totalAmountMicros: AMOUNT_MICROS_SCHEMA.optional(), } as const const CAMPAIGN_BUDGET_UPDATE_SHAPE = { amountMicros: AMOUNT_MICROS_SCHEMA.optional(), deliveryMethod: z.enum(["ACCELERATED", "STANDARD"]).optional(), explicitlyShared: z.boolean().optional(), name: z.string().trim().min(1).optional(), totalAmountMicros: AMOUNT_MICROS_SCHEMA.optional(), } as const const AD_GROUP_CREATE_SHAPE = { campaign: GOOGLE_ADS_RESOURCE_NAME_INPUT_SCHEMA, cpcBidMicros: AMOUNT_MICROS_SCHEMA.optional(), name: z.string().trim().min(1), status: STATUS_SCHEMA.optional(), targetCpaMicros: AMOUNT_MICROS_SCHEMA.optional(), type: z .enum([ "DISPLAY_STANDARD", "HOTEL_ADS", "SEARCH_DYNAMIC_ADS", "SEARCH_STANDARD", "SHOPPING_PRODUCT_ADS", "SHOPPING_SMART_ADS", "VIDEO_BUMPER", "VIDEO_EFFICIENT_REACH", "VIDEO_NON_SKIPPABLE_IN_STREAM", "VIDEO_RESPONSIVE", "PROMOTED_HOTEL_ADS", "SHOPPING_COMPARISON_LISTING_ADS", "SMART_CAMPAIGN_ADS", "TRAVEL_ADS", "YOUTUBE_AUDIO", ]) .optional(), } as const const AD_GROUP_UPDATE_SHAPE = { cpcBidMicros: AMOUNT_MICROS_SCHEMA.optional(), name: z.string().trim().min(1).optional(), status: STATUS_SCHEMA.optional(), targetCpaMicros: AMOUNT_MICROS_SCHEMA.optional(), } as const const CONVERSION_ACTION_TYPE_SCHEMA = z.enum([ "AD_CALL", "CLICK_TO_CALL", "GOOGLE_PLAY_DOWNLOAD", "GOOGLE_PLAY_IN_APP_PURCHASE", "UPLOAD_CALLS", "UPLOAD_CLICKS", "WEBPAGE", "WEBSITE_CALL", ]) const CONVERSION_VALUE_SETTINGS_SCHEMA = z.object({ alwaysUseDefaultValue: z.boolean().optional(), defaultCurrencyCode: z.string().length(3).optional(), defaultValue: z.number().nonnegative().optional(), }) const CONVERSION_ACTION_CREATE_SHAPE = { category: z.string().min(1).optional(), clickThroughLookbackWindowDays: z .number() .int() .min(1) .max(60) .transform(String) .optional(), countingType: z.enum(["MANY_PER_CLICK", "ONE_PER_CLICK"]).optional(), name: z.string().trim().min(1), primaryForGoal: z.boolean().optional(), status: z.enum(["ENABLED", "HIDDEN"]).optional(), type: CONVERSION_ACTION_TYPE_SCHEMA, valueSettings: CONVERSION_VALUE_SETTINGS_SCHEMA.optional(), } as const const CONVERSION_ACTION_UPDATE_SHAPE = { category: z.string().min(1).optional(), clickThroughLookbackWindowDays: CONVERSION_ACTION_CREATE_SHAPE.clickThroughLookbackWindowDays, countingType: z.enum(["MANY_PER_CLICK", "ONE_PER_CLICK"]).optional(), name: z.string().trim().min(1).optional(), primaryForGoal: z.boolean().optional(), status: z.enum(["ENABLED", "HIDDEN", "REMOVED"]).optional(), valueSettings: CONVERSION_VALUE_SETTINGS_SCHEMA.extend({ alwaysUseDefaultValue: z.literal(true).optional(), }).optional(), } as const const AUDIENCE_DIMENSION_SCHEMA = z.union([ z.object({ audienceSegments: z.object({ segments: z .object({ userList: z.object({ userList: z.string().min(1) }).optional(), }) .array() .min(1), }), }), z.object({ age: z.object({ ageRanges: z .object({ maxAge: z .union([ z.literal(24), z.literal(34), z.literal(44), z.literal(54), z.literal(64), ]) .optional(), minAge: z.union([ z.literal(18), z.literal(25), z.literal(35), z.literal(45), z.literal(55), z.literal(65), ]), }) .array() .min(1), includeUndetermined: z.boolean().optional(), }), }), z.object({ gender: z.object({ genders: z.string().array().min(1) }) }), z.object({ householdIncome: z.object({ incomeRanges: z .enum([ "INCOME_RANGE_0_50", "INCOME_RANGE_50_60", "INCOME_RANGE_60_70", "INCOME_RANGE_70_80", "INCOME_RANGE_80_90", "INCOME_RANGE_90_UP", "INCOME_RANGE_UNDETERMINED", ]) .array() .min(1), includeUndetermined: z.boolean().optional(), }), }), z.object({ parentalStatus: z.object({ parentalStatuses: z.string().array().min(1) }), }), ]) const AUDIENCE_CREATE_SHAPE = { description: z.string().optional(), dimensions: AUDIENCE_DIMENSION_SCHEMA.array().min(1), name: z.string().trim().min(1), } as const const AUDIENCE_UPDATE_SHAPE = { description: z.string().optional(), dimensions: AUDIENCE_DIMENSION_SCHEMA.array().min(1).optional(), name: z.string().trim().min(1).optional(), } as const const USER_LIST_CREATE_SHAPE = { description: z.string().optional(), membershipLifeSpan: z .number() .int() .min(0) .max(540) .transform(String) .optional(), membershipStatus: z.enum(["CLOSED", "OPEN"]).optional(), name: z.string().trim().min(1), } as const const USER_LIST_UPDATE_SHAPE = { description: z.string().optional(), membershipLifeSpan: USER_LIST_CREATE_SHAPE.membershipLifeSpan, membershipStatus: z.enum(["CLOSED", "OPEN"]).optional(), name: z.string().trim().min(1).optional(), } as const const USER_IDENTIFIER_SCHEMA = z.discriminatedUnion("type", [ z.object({ hashedEmail: z.string().length(64), type: z.literal("hashedEmail"), }), z.object({ hashedPhoneNumber: z.string().length(64), type: z.literal("hashedPhoneNumber"), }), z.object({ mobileId: z.string().min(1), type: z.literal("mobileId") }), z.object({ thirdPartyUserId: z.string().min(1), type: z.literal("thirdPartyUserId"), }), ]) const DATA_MANAGER_RESOURCE_ID_SCHEMA = (collection: string) => z .string() .regex( new RegExp(`^(?:customers/\\d{10}/${collection}/)?\\d+$`), `Enter a numeric ID or canonical ${collection} resource name.`, ) const DATA_MANAGER_CONVERSION_TIME_SCHEMA = z .string() .regex( /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/, "Enter a conversion time with a UTC offset.", ) /** Lists customer accounts directly accessible to the connected Google user. */ export const listAccessibleGoogleAdsCustomers = defineAction( "List accessible Google Ads customers", ) .describe( "Lists customer resource names directly accessible by OAuth identity.", ) .account("google", GOOGLE_ADS_SCOPE) .input(z.object({})) .output(z.object({ resourceNames: z.string().array() })) .retry({ replaySafety: "safe" }) .handler(({ account }) => requestGoogleAds(account, "customers:listAccessibleCustomers", { responseSchema: z.object({ resourceNames: z.string().array() }), }), ) /** Lists one manager account's visible customer hierarchy. */ export const listGoogleAdsCustomerClients = defineGoogleAdsListAction({ description: "Lists customer accounts below a manager account.", fields: [ "customer_client.client_customer", "customer_client.currency_code", "customer_client.descriptive_name", "customer_client.hidden", "customer_client.id", "customer_client.level", "customer_client.manager", "customer_client.resource_name", "customer_client.test_account", "customer_client.time_zone", ], name: "List Google Ads customer clients", resourceField: "customerClient", resourceName: "customer_client", schema: GOOGLE_ADS_CUSTOMER_CLIENT_SCHEMA, }) /** Gets metadata for one Google Ads customer. */ export const getGoogleAdsCustomer = defineAction("Get Google Ads customer") .describe("Gets identity, locale, manager, and test-account metadata.") .account("google", GOOGLE_ADS_SCOPE) .input(z.object(GOOGLE_ADS_CUSTOMER_CONTEXT_SHAPE)) .output(GOOGLE_ADS_CUSTOMER_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { return GOOGLE_ADS_CUSTOMER_SCHEMA.parse( ( await searchGoogleAds(account, { ...input, query: "SELECT customer.resource_name, customer.id, customer.descriptive_name, customer.currency_code, customer.time_zone, customer.manager, customer.test_account, customer.auto_tagging_enabled FROM customer LIMIT 1", }) ).results[0]?.customer, ) }) export const listGoogleAdsCampaigns = defineGoogleAdsListAction({ description: "Lists campaigns with status, channel, dates, and budget.", fields: [ "campaign.resource_name", "campaign.id", "campaign.name", "campaign.status", "campaign.serving_status", "campaign.advertising_channel_type", "campaign.advertising_channel_sub_type", "campaign.campaign_budget", "campaign.start_date_time", "campaign.end_date_time", ], name: "List Google Ads campaigns", resourceField: "campaign", resourceName: "campaign", schema: GOOGLE_ADS_CAMPAIGN_SCHEMA, }) export const createGoogleAdsCampaign = defineGoogleAdsCreateAction({ collection: "campaigns", description: "Creates a campaign with a budget and channel type.", name: "Create Google Ads campaign", shape: CAMPAIGN_CREATE_SHAPE, transformResource: (resource) => { const { biddingStrategy, ...campaign } = resource const { type, ...settings } = CAMPAIGN_BIDDING_STRATEGY_SCHEMA.parse(biddingStrategy) return { ...campaign, [type]: settings } }, validateResource: (resource) => { const campaign = z.object(CAMPAIGN_CREATE_SHAPE).parse(resource) if (campaign.advertisingChannelType === "HOTEL") { return ( campaign.hotelSetting !== undefined && campaign.shoppingSetting === undefined && campaign.biddingStrategy.type === "percentCpc" ) } if (campaign.advertisingChannelType === "SHOPPING") { return ( campaign.shoppingSetting !== undefined && campaign.hotelSetting === undefined && ["manualCpc", "targetRoas", "targetSpend"].includes( campaign.biddingStrategy.type, ) ) } return ( campaign.hotelSetting === undefined && campaign.shoppingSetting === undefined && campaign.biddingStrategy.type !== "percentCpc" ) }, validationMessage: "Shopping campaigns require shoppingSetting; Hotel campaigns require hotelSetting and percentCpc bidding.", }) export const updateGoogleAdsCampaign = defineGoogleAdsUpdateAction({ collection: "campaigns", description: "Updates selected campaign fields.", name: "Update Google Ads campaign", shape: CAMPAIGN_UPDATE_SHAPE, }) export const removeGoogleAdsCampaign = defineGoogleAdsRemoveAction({ collection: "campaigns", description: "Removes a campaign while preserving historical reporting.", name: "Remove Google Ads campaign", }) export const listGoogleAdsCampaignBudgets = defineGoogleAdsListAction({ description: "Lists campaign budgets and delivery settings.", fields: [ "campaign_budget.resource_name", "campaign_budget.id", "campaign_budget.name", "campaign_budget.status", "campaign_budget.amount_micros", "campaign_budget.total_amount_micros", "campaign_budget.delivery_method", "campaign_budget.explicitly_shared", ], name: "List Google Ads campaign budgets", resourceField: "campaignBudget", resourceName: "campaign_budget", schema: GOOGLE_ADS_CAMPAIGN_BUDGET_SCHEMA, }) export const createGoogleAdsCampaignBudget = defineGoogleAdsCreateAction({ collection: "campaignBudgets", description: "Creates a daily or total campaign budget.", name: "Create Google Ads campaign budget", shape: CAMPAIGN_BUDGET_CREATE_SHAPE, validateResource: (resource) => { const hasTotalAmount = resource.totalAmountMicros !== undefined if ((resource.amountMicros !== undefined) === hasTotalAmount) return false return hasTotalAmount ? resource.period === "CUSTOM_PERIOD" && resource.explicitlyShared === false : resource.period === undefined || resource.period === "DAILY" }, validationMessage: "Provide either amountMicros for a daily budget or totalAmountMicros with period CUSTOM_PERIOD and explicitlyShared false.", }) export const updateGoogleAdsCampaignBudget = defineGoogleAdsUpdateAction({ collection: "campaignBudgets", description: "Updates selected campaign budget fields.", name: "Update Google Ads campaign budget", shape: CAMPAIGN_BUDGET_UPDATE_SHAPE, validateResource: (resource) => resource.amountMicros === undefined || resource.totalAmountMicros === undefined, validationMessage: "Update amountMicros or totalAmountMicros, not both.", }) export const removeGoogleAdsCampaignBudget = defineGoogleAdsRemoveAction({ collection: "campaignBudgets", description: "Removes an unused campaign budget.", name: "Remove Google Ads campaign budget", }) export const listGoogleAdsAdGroups = defineGoogleAdsListAction({ description: "Lists ad groups with bids, status, type, and campaign.", fields: [ "ad_group.resource_name", "ad_group.id", "ad_group.name", "ad_group.status", "ad_group.type", "ad_group.campaign", "ad_group.cpc_bid_micros", "ad_group.target_cpa_micros", ], name: "List Google Ads ad groups", resourceField: "adGroup", resourceName: "ad_group", schema: GOOGLE_ADS_AD_GROUP_SCHEMA, }) export const createGoogleAdsAdGroup = defineGoogleAdsCreateAction({ collection: "adGroups", description: "Creates an ad group inside a campaign.", name: "Create Google Ads ad group", shape: AD_GROUP_CREATE_SHAPE, }) export const updateGoogleAdsAdGroup = defineGoogleAdsUpdateAction({ collection: "adGroups", description: "Updates selected ad-group fields.", name: "Update Google Ads ad group", shape: AD_GROUP_UPDATE_SHAPE, }) export const removeGoogleAdsAdGroup = defineGoogleAdsRemoveAction({ collection: "adGroups", description: "Removes an ad group and stops its ads.", name: "Remove Google Ads ad group", }) export const listGoogleAdsAdGroupAds = defineGoogleAdsListAction({ description: "Lists ad-group ads and responsive search creative.", fields: [ "ad_group_ad.resource_name", "ad_group_ad.status", "ad_group_ad.ad_group", "ad_group_ad.ad.resource_name", "ad_group_ad.ad.id", "ad_group_ad.ad.name", "ad_group_ad.ad.type", "ad_group_ad.ad.final_urls", "ad_group_ad.ad.responsive_search_ad.headlines", "ad_group_ad.ad.responsive_search_ad.descriptions", "ad_group_ad.ad.responsive_search_ad.path1", "ad_group_ad.ad.responsive_search_ad.path2", ], name: "List Google Ads ad-group ads", resourceField: "adGroupAd", resourceName: "ad_group_ad", schema: GOOGLE_ADS_AD_GROUP_AD_SCHEMA, }) export const createGoogleAdsResponsiveSearchAd = defineAction( "Create Google Ads responsive search ad", ) .describe("Creates a responsive search ad in one ad group.") .account("google", GOOGLE_ADS_SCOPE) .input( z .object({ ...GOOGLE_ADS_CUSTOMER_CONTEXT_SHAPE, adGroup: z.string().min(1), descriptions: z.string().trim().min(1).max(90).array().min(2).max(4), finalUrls: z.url().array().min(1), headlines: z.string().trim().min(1).max(30).array().min(3).max(15), path1: z.string().max(15).optional(), path2: z.string().max(15).optional(), status: STATUS_SCHEMA.optional(), }) .refine(({ path1, path2 }) => !path2 || path1 !== undefined, { message: "path1 is required when path2 is set.", path: ["path2"], }), ) .output(GOOGLE_ADS_MUTATE_RESULT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => { const { adGroup, customerId, descriptions, finalUrls, headlines, loginCustomerId, path1, path2, status, } = input return mutateGoogleAdsResource(account, { body: z.json().parse({ operations: [ { create: { ad: { finalUrls, responsiveSearchAd: { descriptions: descriptions.map((text) => ({ text })), headlines: headlines.map((text) => ({ text })), ...(path1 && { path1 }), ...(path2 && { path2 }), }, }, adGroup: googleAdsResourceName(customerId, "adGroups", adGroup), ...(status && { status }), }, }, ], }), collection: "adGroupAds", customerId, loginCustomerId, }) }) export const updateGoogleAdsAdGroupAdStatus = defineGoogleAdsUpdateAction({ collection: "adGroupAds", description: "Enables or pauses an ad-group ad.", name: "Update Google Ads ad-group ad status", shape: { status: STATUS_SCHEMA }, }) export const removeGoogleAdsAdGroupAd = defineGoogleAdsRemoveAction({ collection: "adGroupAds", description: "Removes an ad-group ad.", name: "Remove Google Ads ad-group ad", }) export const listGoogleAdsAssets = defineGoogleAdsListAction({ description: "Lists reusable account assets.", fields: ["asset.resource_name", "asset.id", "asset.name", "asset.type"], name: "List Google Ads assets", resourceField: "asset", resourceName: "asset", schema: GOOGLE_ADS_ASSET_SCHEMA, }) const ASSET_INPUT_SCHEMA = z .discriminatedUnion("type", [ z.object({ text: z.string().min(1), type: z.literal("text") }), z.object({ data: z.string().min(1), type: z.literal("image") }), z.object({ type: z.literal("youtubeVideo"), youtubeVideoId: z.string().min(1), }), z.object({ calloutText: z.string().min(1).max(25), type: z.literal("callout"), }), z.object({ description1: z.string().max(35).optional(), description2: z.string().max(35).optional(), linkText: z.string().min(1).max(25), type: z.literal("sitelink"), }), z.object({ countryCode: z.string().length(2), phoneNumber: z.string().min(1), type: z.literal("call"), }), z.object({ header: z.string().min(1).max(25), type: z.literal("structuredSnippet"), values: z.string().min(1).max(25).array().min(3).max(10), }), ]) .refine( (asset) => asset.type !== "sitelink" || (asset.description1 === undefined) === (asset.description2 === undefined), { message: "Sitelink descriptions must be set together.", path: ["description2"], }, ) export const createGoogleAdsAsset = defineAction("Create Google Ads asset") .describe( "Creates a text, image, video, callout, sitelink, call, or structured-snippet asset.", ) .account("google", GOOGLE_ADS_SCOPE) .input( z.object({ ...GOOGLE_ADS_CUSTOMER_CONTEXT_SHAPE, asset: ASSET_INPUT_SCHEMA, name: z.string().trim().min(1).optional(), }), ) .output(GOOGLE_ADS_MUTATE_RESULT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => { const { asset, customerId, loginCustomerId, name } = input return mutateGoogleAdsResource(account, { body: z.json().parse({ operations: [ { create: { ...(name && { name }), ...toGoogleAdsAsset(asset) } }, ], }), collection: "assets", customerId, loginCustomerId, }) }) export const updateGoogleAdsAssetName = defineGoogleAdsUpdateAction({ collection: "assets", description: "Updates an asset's optional name.", name: "Update Google Ads asset name", shape: { name: z.string().trim().min(1) }, }) export const linkGoogleAdsCampaignAsset = defineAssetLinkAction({ collection: "campaignAssets", ownerCollection: "campaigns", ownerKey: "campaign", name: "Link Google Ads campaign asset", }) export const unlinkGoogleAdsCampaignAsset = defineAssetUnlinkAction({ collection: "campaignAssets", name: "Unlink Google Ads campaign asset", }) export const updateGoogleAdsCampaignAsset = defineGoogleAdsUpdateAction({ collection: "campaignAssets", description: "Updates a campaign asset link's status.", name: "Update Google Ads campaign asset", shape: { status: STATUS_SCHEMA }, }) export const linkGoogleAdsAdGroupAsset = defineAssetLinkAction({ collection: "adGroupAssets", ownerCollection: "adGroups", ownerKey: "adGroup", name: "Link Google Ads ad-group asset", }) export const unlinkGoogleAdsAdGroupAsset = defineAssetUnlinkAction({ collection: "adGroupAssets", name: "Unlink Google Ads ad-group asset", }) export const updateGoogleAdsAdGroupAsset = defineGoogleAdsUpdateAction({ collection: "adGroupAssets", description: "Updates an ad-group asset link's status.", name: "Update Google Ads ad-group asset", shape: { status: STATUS_SCHEMA }, }) export const listGoogleAdsConversionActions = defineGoogleAdsListAction({ description: "Lists conversion actions and attribution settings.", fields: [ "conversion_action.resource_name", "conversion_action.id", "conversion_action.name", "conversion_action.status", "conversion_action.type", "conversion_action.category", "conversion_action.counting_type", "conversion_action.primary_for_goal", "conversion_action.click_through_lookback_window_days", "conversion_action.value_settings.always_use_default_value", "conversion_action.value_settings.default_currency_code", "conversion_action.value_settings.default_value", ], name: "List Google Ads conversion actions", resourceField: "conversionAction", resourceName: "conversion_action", schema: GOOGLE_ADS_CONVERSION_ACTION_SCHEMA, }) export const createGoogleAdsConversionAction = defineGoogleAdsCreateAction({ collection: "conversionActions", description: "Creates a conversion action.", name: "Create Google Ads conversion action", shape: CONVERSION_ACTION_CREATE_SHAPE, validateResource: (resource) => { const conversion = z.object(CONVERSION_ACTION_CREATE_SHAPE).parse(resource) const isCallConversion = conversion.type === "AD_CALL" || conversion.type === "WEBSITE_CALL" return ( (isCallConversion || conversion.clickThroughLookbackWindowDays === undefined || Number(conversion.clickThroughLookbackWindowDays) <= 30) && (!isCallConversion || conversion.valueSettings?.alwaysUseDefaultValue !== false) ) }, validationMessage: "Non-call look-back windows cannot exceed 30 days, and call conversions must always use the default value.", }) export const updateGoogleAdsConversionAction = defineGoogleAdsUpdateAction({ collection: "conversionActions", description: "Updates selected conversion-action fields.", name: "Update Google Ads conversion action", shape: { ...CONVERSION_ACTION_UPDATE_SHAPE, clickThroughLookbackWindowDays: z .number() .int() .min(1) .max(30) .transform(String) .optional(), }, }) export const removeGoogleAdsConversionAction = defineGoogleAdsRemoveAction({ collection: "conversionActions", description: "Removes a conversion action.", name: "Remove Google Ads conversion action", }) const CLICK_CONVERSION_SCHEMA = z .object({ conversionAction: DATA_MANAGER_RESOURCE_ID_SCHEMA("conversionActions"), conversionDateTime: DATA_MANAGER_CONVERSION_TIME_SCHEMA, conversionValue: z.number().optional(), currencyCode: z.string().length(3).optional(), eventSource: z .enum(["APP", "IN_STORE", "MESSAGE", "OTHER", "PHONE", "WEB"]) .prefault("WEB"), gbraid: z.string().optional(), gclid: z.string().optional(), orderId: z.string().optional(), wbraid: z.string().optional(), }) .refine( (value) => [value.gclid, value.gbraid, value.wbraid].filter(Boolean).length === 1, "Provide exactly one click identifier.", ) export const uploadGoogleAdsClickConversions = defineAction( "Upload Google Ads click conversions", ) .describe("Uploads offline click conversions through Google Data Manager.") .account("google", GOOGLE_DATA_MANAGER_SCOPE) .input( z.object({ ...GOOGLE_ADS_CUSTOMER_CONTEXT_SHAPE, items: CLICK_CONVERSION_SCHEMA.array().min(1).max(2000), validateOnly: z.boolean().optional(), }), ) .output(z.object({ requestId: z.string() })) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => { const destinationReferences = new Map() for (const item of input.items) { const conversionActionId = getGoogleAdsResourceId(item.conversionAction) if (!destinationReferences.has(conversionActionId)) { destinationReferences.set( conversionActionId, `conversion_${destinationReferences.size + 1}`, ) } } return requestGoogleDataManager(account, "events:ingest", { body: { destinations: [...destinationReferences].map( ([productDestinationId, reference]) => ({ ...(input.loginCustomerId && { loginAccount: { accountId: input.loginCustomerId, accountType: "GOOGLE_ADS", }, }), operatingAccount: { accountId: input.customerId, accountType: "GOOGLE_ADS", }, productDestinationId, reference, }), ), events: input.items.map((item) => ({ adIdentifiers: { ...(item.gbraid && { gbraid: item.gbraid }), ...(item.gclid && { gclid: item.gclid }), ...(item.wbraid && { wbraid: item.wbraid }), }, ...(item.conversionValue !== undefined && { conversionValue: item.conversionValue, }), ...(item.currencyCode && { currency: item.currencyCode }), destinationReferences: [ destinationReferences.get( getGoogleAdsResourceId(item.conversionAction), )!, ], eventSource: item.eventSource, eventTimestamp: item.conversionDateTime.replace(" ", "T"), ...(item.orderId && { transactionId: item.orderId }), })), encoding: "HEX", ...(input.validateOnly !== undefined && { validateOnly: input.validateOnly, }), }, responseSchema: z.object({ requestId: z.string() }), }) }) export const uploadGoogleAdsCallConversions = defineConversionUploadAction({ collection: "uploadCallConversions", itemKey: "conversions", itemSchema: z.object({ callerId: z.string().min(1), callStartDateTime: z.string().min(1), conversionAction: z.string().min(1), conversionDateTime: z.string().min(1), conversionValue: z.number().optional(), currencyCode: z.string().length(3).optional(), }), name: "Upload Google Ads call conversions", }) export const uploadGoogleAdsConversionAdjustments = defineConversionUploadAction({ collection: "uploadConversionAdjustments", itemKey: "conversionAdjustments", itemSchema: z.discriminatedUnion("adjustmentType", [ z.object({ adjustmentDateTime: z.string().min(1), adjustmentType: z.literal("RETRACTION"), conversionAction: z.string().min(1), orderId: z.string().min(1), }), z.object({ adjustmentDateTime: z.string().min(1), adjustmentType: z.literal("RESTATEMENT"), conversionAction: z.string().min(1), orderId: z.string().min(1), restatementValue: z.object({ adjustedValue: z.number(), currencyCode: z.string().length(3), }), }), ]), name: "Upload Google Ads conversion adjustments", }) export const listGoogleAdsAudiences = defineGoogleAdsListAction({ description: "Lists reusable audience definitions.", fields: [ "audience.resource_name", "audience.id", "audience.name", "audience.description", "audience.status", "audience.dimensions", ], name: "List Google Ads audiences", resourceField: "audience", resourceName: "audience", schema: GOOGLE_ADS_AUDIENCE_SCHEMA, }) export const createGoogleAdsAudience = defineGoogleAdsCreateAction({ collection: "audiences", description: "Creates a reusable audience definition.", name: "Create Google Ads audience", shape: AUDIENCE_CREATE_SHAPE, }) export const updateGoogleAdsAudience = defineGoogleAdsUpdateAction({ collection: "audiences", description: "Updates selected audience fields.", name: "Update Google Ads audience", shape: AUDIENCE_UPDATE_SHAPE, }) export const listGoogleAdsUserLists = defineGoogleAdsListAction({ description: "Lists remarketing and Customer Match user lists.", fields: [ "user_list.resource_name", "user_list.id", "user_list.name", "user_list.description", "user_list.membership_status", "user_list.membership_life_span", "user_list.size_for_display", "user_list.size_for_search", "user_list.type", ], name: "List Google Ads user lists", resourceField: "userList", resourceName: "user_list", schema: GOOGLE_ADS_USER_LIST_SCHEMA, }) export const createGoogleAdsUserList = defineGoogleAdsCreateAction({ collection: "userLists", description: "Creates a Customer Match user list.", name: "Create Google Ads user list", shape: USER_LIST_CREATE_SHAPE, staticFields: { crmBasedUserList: { uploadKeyType: "CONTACT_INFO" } }, }) export const uploadGoogleAdsUserListMembers = defineAction( "Upload Google Ads user-list members", ) .describe("Adds or removes Customer Match identifiers through Data Manager.") .account("google", GOOGLE_DATA_MANAGER_SCOPE) .input( z .object({ ...GOOGLE_ADS_CUSTOMER_CONTEXT_SHAPE, consent: z .object({ adPersonalization: z.enum(["DENIED", "GRANTED", "UNSPECIFIED"]), adUserData: z.enum(["DENIED", "GRANTED", "UNSPECIFIED"]), }) .optional(), customerMatchTermsAccepted: z.literal(true).optional(), identifiers: USER_IDENTIFIER_SCHEMA.array().min(1).max(10_000), operation: z.enum(["create", "remove"]), userList: DATA_MANAGER_RESOURCE_ID_SCHEMA("userLists"), validateOnly: z.boolean().optional(), }) .refine( (input) => input.operation === "remove" || input.customerMatchTermsAccepted, "Accept the Customer Match terms to add audience members.", ) .refine( (input) => new Set(input.identifiers.map(getGoogleDataManagerIdentifierType)) .size === 1, "Send contact information, mobile IDs, or user IDs in separate requests.", ), ) .output( z.object({ fieldWarnings: z.record(z.string(), z.json()).array().optional(), requestId: z.string(), }), ) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => requestGoogleDataManager( account, input.operation === "create" ? "audienceMembers:ingest" : "audienceMembers:remove", { body: z.json().parse({ audienceMembers: input.identifiers.map( toGoogleDataManagerAudienceMember, ), ...(input.operation === "create" && input.consent && { consent: { adPersonalization: toGoogleDataManagerConsent( input.consent.adPersonalization, ), adUserData: toGoogleDataManagerConsent( input.consent.adUserData, ), }, }), destinations: [ { ...(input.loginCustomerId && { loginAccount: { accountId: input.loginCustomerId, accountType: "GOOGLE_ADS", }, }), operatingAccount: { accountId: input.customerId, accountType: "GOOGLE_ADS", }, productDestinationId: getGoogleAdsResourceId(input.userList), }, ], encoding: "HEX", ...(input.operation === "create" && { termsOfService: { customerMatchTermsOfServiceStatus: "ACCEPTED" }, }), ...(input.validateOnly !== undefined && { validateOnly: input.validateOnly, }), }), responseSchema: z.object({ fieldWarnings: z.record(z.string(), z.json()).array().optional(), requestId: z.string(), }), }, ), ) export const updateGoogleAdsUserList = defineGoogleAdsUpdateAction({ collection: "userLists", description: "Updates selected user-list fields.", name: "Update Google Ads user list", shape: USER_LIST_UPDATE_SHAPE, }) export const removeGoogleAdsUserList = defineGoogleAdsRemoveAction({ collection: "userLists", description: "Removes a user list.", name: "Remove Google Ads user list", }) /** Runs one GAQL report page with the provider's fixed 10,000-row page size. */ export const queryGoogleAdsReport = defineAction("Query Google Ads report") .describe("Runs one validated Google Ads Query Language report page.") .account("google", GOOGLE_ADS_SCOPE) .input(z.object({ ...PAGE_INPUT_SHAPE, query: z.string().trim().min(1) })) .output(GOOGLE_ADS_SEARCH_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(({ account, input }) => searchGoogleAds(account, input)) export const getGoogleAdsCampaignPerformance = definePerformanceReportAction({ dimensions: [ "campaign.resource_name", "campaign.id", "campaign.name", "campaign.status", ], name: "Get Google Ads campaign performance", resourceName: "campaign", }) export const getGoogleAdsAdGroupPerformance = definePerformanceReportAction({ dimensions: [ "campaign.resource_name", "campaign.name", "ad_group.resource_name", "ad_group.id", "ad_group.name", "ad_group.status", ], name: "Get Google Ads ad-group performance", resourceName: "ad_group", }) export const getGoogleAdsAdPerformance = definePerformanceReportAction({ dimensions: [ "campaign.resource_name", "campaign.name", "ad_group.resource_name", "ad_group.name", "ad_group_ad.resource_name", "ad_group_ad.status", "ad_group_ad.ad.id", "ad_group_ad.ad.type", ], name: "Get Google Ads ad performance", resourceName: "ad_group_ad", }) export const getGoogleAdsConversionPerformance = definePerformanceReportAction({ dimensions: [ "campaign.resource_name", "campaign.name", "segments.conversion_action", "segments.conversion_action_name", "segments.conversion_action_category", ], metrics: ["metrics.conversions", "metrics.conversions_value"], name: "Get Google Ads conversion performance", resourceName: "campaign", }) /** * Defines one paginated Google Ads list action. * * @param options - Action definition options. * @param options.description - Public action description. * @param options.fields - GAQL fields to select. * @param options.name - Public action name. * @param options.resourceField - Result property containing each resource. * @param options.resourceName - GAQL resource name. * @param options.schema - Output resource schema. */ function defineGoogleAdsListAction< TOutput extends Encodable, TSchema extends z.ZodType, >(options: { description: string fields: string[] name: string resourceField: string resourceName: string schema: TSchema }) { return defineAction(options.name) .describe(options.description) .account("google", GOOGLE_ADS_SCOPE) .input(z.object(PAGE_INPUT_SHAPE)) .output( z.object({ items: options.schema.array(), nextPageToken: z.string().optional(), totalResultsCount: z.string().optional(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const page = await searchGoogleAds(account, { ...input, query: `SELECT ${options.fields.join(", ")} FROM ${options.resourceName}`, }) return { items: page.results.map((row) => z.encode( options.schema, options.schema.parse(row[options.resourceField]), ), ), nextPageToken: page.nextPageToken, totalResultsCount: page.totalResultsCount, } }) } /** * Defines one Google Ads resource creation action. * * @param options - Action definition options. * @param options.collection - REST mutation collection. * @param options.description - Public action description. * @param options.name - Public action name. * @param options.shape - Resource input shape. * @param options.staticFields - Fields added to every created resource. * @param options.transformResource - Provider-specific resource transformer. * @param options.validateResource - Optional cross-field validator. * @param options.validationMessage - Cross-field validation error. */ function defineGoogleAdsCreateAction< const TShape extends z.ZodRawShape, >(options: { collection: string description: string name: string shape: TShape staticFields?: Record transformResource?: ( resource: Record, ) => Record validateResource?: (resource: Record) => boolean validationMessage?: string }) { return defineAction(options.name) .describe(options.description) .account("google", GOOGLE_ADS_SCOPE) .input( z .object({ ...GOOGLE_ADS_CUSTOMER_CONTEXT_SHAPE, ...options.shape }) .refine((input) => { if (!options.validateResource) return true return options.validateResource( Object.fromEntries( Object.entries( z.record(z.string(), z.json()).parse(input), ).filter( ([key]) => key !== "customerId" && key !== "loginCustomerId", ), ), ) }, options.validationMessage), ) .output(GOOGLE_ADS_MUTATE_RESULT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => { const parsed = z.record(z.string(), z.json()).parse(input) const resource = Object.fromEntries( Object.entries(parsed).filter( ([key]) => key !== "customerId" && key !== "loginCustomerId", ), ) return mutateGoogleAdsResource(account, { body: z.json().parse({ operations: [ { create: { ...(options.transformResource ? options.transformResource(resource) : resource), ...options.staticFields, }, }, ], }), collection: options.collection, customerId: z.string().parse(parsed.customerId), loginCustomerId: z.string().optional().parse(parsed.loginCustomerId), }) }) } /** * Defines one masked Google Ads resource update action. * * @param options - Action definition options. * @param options.collection - REST mutation collection. * @param options.description - Public action description. * @param options.name - Public action name. * @param options.shape - Mutable resource input shape. * @param options.validateResource - Optional cross-field validator. * @param options.validationMessage - Cross-field validation error. */ function defineGoogleAdsUpdateAction< const TShape extends z.ZodRawShape, >(options: { collection: string description: string name: string shape: TShape validateResource?: (resource: Record) => boolean validationMessage?: string }) { const fieldNames = Object.keys(options.shape) return defineAction(options.name) .describe(options.description) .account("google", GOOGLE_ADS_SCOPE) .input( z .object({ ...GOOGLE_ADS_CUSTOMER_CONTEXT_SHAPE, resourceName: GOOGLE_ADS_RESOURCE_NAME_INPUT_SCHEMA, ...options.shape, }) .refine((input) => { const parsed = z.record(z.string(), z.json()).parse(input) return fieldNames.some((field) => parsed[field] !== undefined) }, "Provide at least one field to update.") .refine((input) => { if (!options.validateResource) return true return options.validateResource( Object.fromEntries( Object.entries( z.record(z.string(), z.json()).parse(input), ).filter( ([key]) => key !== "customerId" && key !== "loginCustomerId" && key !== "resourceName", ), ), ) }, options.validationMessage), ) .output(GOOGLE_ADS_MUTATE_RESULT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => { const parsed = z.record(z.string(), z.json()).parse(input) const resourceFields = Object.fromEntries( Object.entries(parsed).filter( ([key]) => key !== "customerId" && key !== "loginCustomerId" && key !== "resourceName", ), ) return mutateGoogleAdsResource(account, { body: { operations: [ { update: { resourceName: z.string().parse(parsed.resourceName), ...resourceFields, }, updateMask: fieldNames .filter((field) => resourceFields[field] !== undefined) .join(","), }, ], }, collection: options.collection, customerId: z.string().parse(parsed.customerId), loginCustomerId: z.string().optional().parse(parsed.loginCustomerId), }) }) } /** * Defines one Google Ads resource removal action. * * @param options - Action definition options. * @param options.collection - REST mutation collection. * @param options.description - Public action description. * @param options.name - Public action name. */ function defineGoogleAdsRemoveAction(options: { collection: string description: string name: string }) { return defineAction(options.name) .describe(options.description) .account("google", GOOGLE_ADS_SCOPE) .input( z.object({ ...GOOGLE_ADS_CUSTOMER_CONTEXT_SHAPE, resourceName: GOOGLE_ADS_RESOURCE_NAME_INPUT_SCHEMA, }), ) .output(GOOGLE_ADS_MUTATE_RESULT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => mutateGoogleAdsResource(account, { body: { operations: [{ remove: input.resourceName }] }, collection: options.collection, customerId: input.customerId, loginCustomerId: input.loginCustomerId, }), ) } /** * Defines an action that links an asset to its owner. * * @param options - Action definition options. * @param options.collection - REST mutation collection. * @param options.name - Public action name. * @param options.ownerCollection - Owner resource collection. * @param options.ownerKey - Owner property in the link resource. */ function defineAssetLinkAction(options: { collection: string name: string ownerCollection: string ownerKey: "adGroup" | "campaign" }) { return defineAction(options.name) .describe( `Links an asset to a Google Ads ${options.ownerKey === "campaign" ? "campaign" : "ad group"}.`, ) .account("google", GOOGLE_ADS_SCOPE) .input( z.object({ ...GOOGLE_ADS_CUSTOMER_CONTEXT_SHAPE, asset: z.string().min(1), fieldType: z.string().min(1), owner: z.string().min(1), }), ) .output(GOOGLE_ADS_MUTATE_RESULT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => mutateGoogleAdsResource(account, { body: { operations: [ { create: { asset: googleAdsResourceName( input.customerId, "assets", input.asset, ), fieldType: input.fieldType, [options.ownerKey]: googleAdsResourceName( input.customerId, options.ownerCollection, input.owner, ), }, }, ], }, collection: options.collection, customerId: input.customerId, loginCustomerId: input.loginCustomerId, }), ) } /** * Defines an action that removes an asset link. * * @param options - Action definition options. * @param options.collection - REST mutation collection. * @param options.name - Public action name. */ function defineAssetUnlinkAction(options: { collection: string name: string }) { return defineAction(options.name) .describe("Removes an asset link without removing the underlying asset.") .account("google", GOOGLE_ADS_SCOPE) .input( z.object({ ...GOOGLE_ADS_CUSTOMER_CONTEXT_SHAPE, resourceName: GOOGLE_ADS_RESOURCE_NAME_INPUT_SCHEMA, }), ) .output(GOOGLE_ADS_MUTATE_RESULT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => mutateGoogleAdsResource(account, { body: { operations: [{ remove: input.resourceName }] }, collection: options.collection, customerId: input.customerId, loginCustomerId: input.loginCustomerId, }), ) } /** * Defines one offline conversion upload action. * * @param options - Action definition options. * @param options.collection - Customer-level upload method. * @param options.itemKey - Request property containing conversion records. * @param options.itemSchema - Conversion record schema. * @param options.name - Public action name. */ function defineConversionUploadAction< TOutput extends JsonValue, TSchema extends z.ZodType, >(options: { collection: string itemKey: "conversionAdjustments" | "conversions" itemSchema: TSchema name: string }) { const responseSchema = z .object({ jobId: z.string().optional(), partialFailureError: z.record(z.string(), z.json()).optional(), results: z.record(z.string(), z.json()).array().prefault([]), }) .catchall(z.json()) return defineAction(options.name) .describe("Uploads conversion records and returns per-record results.") .account("google", GOOGLE_ADS_SCOPE) .input( z.object({ ...GOOGLE_ADS_CUSTOMER_CONTEXT_SHAPE, items: options.itemSchema.array().min(1).max(2000), partialFailure: z.literal(true).prefault(true), validateOnly: z.boolean().optional(), }), ) .output(responseSchema) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => { return requestGoogleAds( account, `customers/${normalizeGoogleAdsCustomerId(input.customerId)}:${options.collection}`, { body: z.json().parse({ [options.itemKey]: z.json().array().parse(input.items), partialFailure: input.partialFailure, ...(input.validateOnly !== undefined && { validateOnly: input.validateOnly, }), }), loginCustomerId: input.loginCustomerId, responseSchema, }, ) }) } /** * Defines one common Google Ads performance report action. * * @param options - Report definition options. * @param options.dimensions - GAQL dimensions to select. * @param options.metrics - GAQL metrics to select instead of the common set. * @param options.name - Public action name. * @param options.resourceName - GAQL resource used by the report. */ function definePerformanceReportAction(options: { dimensions: string[] metrics?: string[] name: string resourceName: string }) { const metrics = options.metrics ?? [ "metrics.clicks", "metrics.conversions", "metrics.conversions_value", "metrics.cost_micros", "metrics.impressions", "metrics.interactions", ] return defineAction(options.name) .describe("Returns common performance metrics for a date range.") .account("google", GOOGLE_ADS_SCOPE) .input( z.object({ ...PAGE_INPUT_SHAPE, dateFrom: z.iso.date(), dateTo: z.iso.date(), }), ) .output(GOOGLE_ADS_SEARCH_PAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(({ account, input }) => searchGoogleAds(account, { ...input, query: `SELECT ${[...options.dimensions, ...metrics].join(", ")} FROM ${options.resourceName} WHERE segments.date BETWEEN '${input.dateFrom}' AND '${input.dateTo}'`, }), ) } /** * Converts the public asset union to the matching Google Ads resource. * * @param asset - Public asset input. */ function toGoogleAdsAsset(asset: z.output) { switch (asset.type) { case "text": return { textAsset: { text: asset.text } } case "image": return { imageAsset: { data: asset.data } } case "youtubeVideo": return { youtubeVideoAsset: { youtubeVideoId: asset.youtubeVideoId } } case "callout": return { calloutAsset: { calloutText: asset.calloutText } } case "sitelink": return { sitelinkAsset: { ...(asset.description1 && { description1: asset.description1 }), ...(asset.description2 && { description2: asset.description2 }), linkText: asset.linkText, }, } case "call": return { callAsset: { countryCode: asset.countryCode, phoneNumber: asset.phoneNumber, }, } case "structuredSnippet": return { structuredSnippetAsset: { header: asset.header, values: asset.values }, } } } /** * Converts the public identifier union to an offline-user-data identifier. * * @param identifier - Public user identifier input. */ function toGoogleDataManagerAudienceMember( identifier: z.output, ) { switch (identifier.type) { case "hashedEmail": return { compositeData: { userData: { userIdentifiers: [{ emailAddress: identifier.hashedEmail }], }, }, } case "hashedPhoneNumber": return { compositeData: { userData: { userIdentifiers: [{ phoneNumber: identifier.hashedPhoneNumber }], }, }, } case "mobileId": return { mobileData: { mobileIds: [identifier.mobileId] } } case "thirdPartyUserId": return { userIdData: { userId: identifier.thirdPartyUserId } } } } /** Returns the Data Manager member shape used by an identifier. */ /** @param identifier - Public Google Ads audience identifier. */ function getGoogleDataManagerIdentifierType( identifier: z.output, ) { if ( identifier.type === "hashedEmail" || identifier.type === "hashedPhoneNumber" ) { return "contact" } return identifier.type } /** Maps Google Ads consent values to Data Manager consent values. */ /** @param value - Public consent value. */ function toGoogleDataManagerConsent( value: "DENIED" | "GRANTED" | "UNSPECIFIED", ) { switch (value) { case "DENIED": return "CONSENT_DENIED" case "GRANTED": return "CONSENT_GRANTED" case "UNSPECIFIED": return "CONSENT_STATUS_UNSPECIFIED" } } /** Extracts the numeric provider ID from a validated ID or resource name. */ /** @param resource - Numeric ID or canonical Google Ads resource name. */ function getGoogleAdsResourceId(resource: string) { return resource.slice(resource.lastIndexOf("/") + 1) }