@rawdash/connector-google-ads 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,703 @@
1
+ // ../../connector-shared/dist/index.js
2
+ var HTTP_CLIENT_VERSION = "0.0.0";
3
+ var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
4
+ function connectorUserAgent(connectorId) {
5
+ return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
6
+ }
7
+
8
+ // src/google-ads.ts
9
+ import {
10
+ BaseConnector,
11
+ defineConfigFields,
12
+ defineConnectorDoc,
13
+ defineResources,
14
+ makeChunkedCursorGuard,
15
+ paginateChunked,
16
+ schemasFromResources,
17
+ selectActivePhases
18
+ } from "@rawdash/core";
19
+ import { z } from "zod";
20
+ var configFields = defineConfigFields(
21
+ z.object({
22
+ customerId: z.string().trim().regex(
23
+ /^\d{10}$/,
24
+ "customerId must be the 10-digit Google Ads ID, digits only (no dashes)"
25
+ ).meta({
26
+ label: "Customer ID",
27
+ description: "Google Ads customer ID for the account to sync, digits only (the dashed form 123-456-7890 with the dashes removed).",
28
+ placeholder: "1234567890"
29
+ }),
30
+ loginCustomerId: z.string().trim().regex(
31
+ /^\d{10}$/,
32
+ "loginCustomerId must be a 10-digit Google Ads ID, digits only"
33
+ ).optional().meta({
34
+ label: "Login Customer ID (MCC)",
35
+ description: "Manager (MCC) account ID, digits only. Set this when the OAuth credential authenticates against an MCC that owns the customer account.",
36
+ placeholder: "1234567890"
37
+ }),
38
+ clientId: z.string().min(1).meta({
39
+ label: "OAuth Client ID",
40
+ description: "OAuth 2.0 client ID from a Google Cloud project that has the Google Ads API enabled.",
41
+ placeholder: "\u2026apps.googleusercontent.com"
42
+ }),
43
+ clientSecret: z.object({ $secret: z.string() }).meta({
44
+ label: "OAuth Client Secret",
45
+ description: "OAuth 2.0 client secret paired with the client ID above.",
46
+ secret: true
47
+ }),
48
+ refreshToken: z.object({ $secret: z.string() }).meta({
49
+ label: "OAuth Refresh Token",
50
+ description: "Google OAuth 2.0 refresh token issued for the https://www.googleapis.com/auth/adwords scope.",
51
+ secret: true
52
+ }),
53
+ developerToken: z.object({ $secret: z.string() }).meta({
54
+ label: "Developer Token",
55
+ description: "Google Ads API developer token from the manager account that owns API access (Tools \u2192 API Center).",
56
+ secret: true
57
+ }),
58
+ lookbackDays: z.number().int().positive().optional().meta({
59
+ label: "Lookback days (full sync)",
60
+ description: "How many calendar days of metric history to fetch on a full sync. Defaults to 90.",
61
+ placeholder: "90"
62
+ }),
63
+ resources: z.array(
64
+ z.enum([
65
+ "campaigns",
66
+ "campaign_metrics",
67
+ "ad_group_metrics",
68
+ "keyword_metrics"
69
+ ])
70
+ ).nonempty().optional().meta({
71
+ label: "Resources",
72
+ description: "Which Google Ads resources to sync. Omit to sync everything; pin a subset to avoid pulling keyword-level metrics on a quota-limited token."
73
+ })
74
+ })
75
+ );
76
+ var doc = defineConnectorDoc({
77
+ displayName: "Google Ads",
78
+ category: "marketing",
79
+ brandColor: "#4285F4",
80
+ tagline: "Sync Google Ads campaigns plus daily campaign, ad-group, and keyword performance (impressions, clicks, cost, conversions) via GAQL.",
81
+ vendor: {
82
+ name: "Google Ads",
83
+ apiDocs: "https://developers.google.com/google-ads/api/docs/start",
84
+ website: "https://ads.google.com"
85
+ },
86
+ auth: {
87
+ summary: "OAuth 2.0 refresh token against an account with read access to the Google Ads customer, plus a developer token from the manager account that owns API access.",
88
+ setup: [
89
+ "Apply for Google Ads API access from your manager account (Tools \u2192 API Center). Copy the developer token - it lives on the manager, not the child account.",
90
+ "In Google Cloud Console, enable the Google Ads API on a project, create an OAuth 2.0 client ID, and complete the OAuth consent flow for the adwords scope to obtain a refresh token. The official walkthrough is at https://developers.google.com/google-ads/api/docs/oauth/overview.",
91
+ "Find the Google Ads customer ID at the top of the Ads UI (e.g. 123-456-7890) and store it without dashes (e.g. 1234567890).",
92
+ "If the OAuth credential authenticates against an MCC that owns the customer, set `loginCustomerId` to the MCC id (digits only). For a direct-access account, omit it.",
93
+ 'Store the client secret, refresh token, and developer token as secrets, then reference them as `clientSecret: secret("GADS_CLIENT_SECRET")`, `refreshToken: secret("GADS_REFRESH_TOKEN")`, and `developerToken: secret("GADS_DEVELOPER_TOKEN")`.'
94
+ ]
95
+ },
96
+ rateLimit: "Google Ads API basic-access tokens get a 15,000 operations / day quota per developer token; the connector treats 429 (RESOURCE_EXHAUSTED) as a transient error and the host backs off.",
97
+ limitations: [
98
+ "Cost values are stored in account currency units (cost_micros \xF7 1,000,000); the original micro-precision integer is also exposed in attributes.",
99
+ "Keyword metrics use the historical (per-day) quality score from `metrics.historical_quality_score`; criteria with no impressions on a day will report a null quality score.",
100
+ "Incremental syncs trail the last 3 days because Google Ads can attribute conversions to a click up to 3 days after the event.",
101
+ "Audience-, asset-, and recommendation-level reporting are out of scope; this connector covers campaign / ad-group / keyword performance only."
102
+ ]
103
+ });
104
+ var googleAdsCredentials = {
105
+ clientId: {
106
+ description: "Google OAuth 2.0 client ID (public, not a secret)",
107
+ auth: "required"
108
+ },
109
+ clientSecret: {
110
+ description: "Google OAuth 2.0 client secret",
111
+ auth: "required"
112
+ },
113
+ refreshToken: {
114
+ description: "Google OAuth 2.0 refresh token with the adwords scope",
115
+ auth: "required"
116
+ },
117
+ developerToken: {
118
+ description: "Google Ads API developer token",
119
+ auth: "required"
120
+ }
121
+ };
122
+ var PHASE_ORDER = [
123
+ "campaigns",
124
+ "campaign_metrics",
125
+ "ad_group_metrics",
126
+ "keyword_metrics"
127
+ ];
128
+ var isGoogleAdsSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
129
+ var API_VERSION = "v18";
130
+ var PAGE_SIZE = 1e4;
131
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
132
+ var DEFAULT_LOOKBACK_DAYS = 90;
133
+ var INCREMENTAL_LOOKBACK_DAYS = 3;
134
+ var MICROS_PER_UNIT = 1e6;
135
+ var TOKEN_URL = "https://oauth2.googleapis.com/token";
136
+ var ENTITY_TYPE_CAMPAIGN = "google_ads_campaign";
137
+ var METRIC_NAME = {
138
+ campaigns: ENTITY_TYPE_CAMPAIGN,
139
+ campaign_metrics: "google_ads_campaign_metrics",
140
+ ad_group_metrics: "google_ads_ad_group_metrics",
141
+ keyword_metrics: "google_ads_keyword_metrics"
142
+ };
143
+ var int64String = z.union([z.string().min(1), z.number()]);
144
+ var segmentsSchema = z.object({
145
+ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/)
146
+ });
147
+ var dateString = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
148
+ var campaignFieldsSchema = z.object({
149
+ id: int64String,
150
+ name: z.string().nullish(),
151
+ status: z.string().nullish(),
152
+ biddingStrategyType: z.string().nullish(),
153
+ startDate: dateString.nullish(),
154
+ endDate: dateString.nullish(),
155
+ resourceName: z.string().nullish()
156
+ });
157
+ var metricsSchema = z.object({
158
+ impressions: int64String.nullish(),
159
+ clicks: int64String.nullish(),
160
+ costMicros: int64String.nullish(),
161
+ conversions: z.number().nullish(),
162
+ conversionsValue: z.number().nullish(),
163
+ historicalQualityScore: int64String.nullish()
164
+ });
165
+ var campaignRowSchema = z.object({
166
+ campaign: campaignFieldsSchema
167
+ });
168
+ var campaignMetricRowSchema = z.object({
169
+ segments: segmentsSchema,
170
+ campaign: z.object({
171
+ id: int64String,
172
+ name: z.string().nullish(),
173
+ resourceName: z.string().nullish()
174
+ }),
175
+ metrics: metricsSchema
176
+ });
177
+ var adGroupMetricRowSchema = z.object({
178
+ segments: segmentsSchema,
179
+ campaign: z.object({ id: int64String }).nullish(),
180
+ adGroup: z.object({
181
+ id: int64String,
182
+ name: z.string().nullish(),
183
+ resourceName: z.string().nullish()
184
+ }),
185
+ metrics: metricsSchema
186
+ });
187
+ var keywordMetricRowSchema = z.object({
188
+ segments: segmentsSchema,
189
+ adGroup: z.object({ id: int64String }).nullish(),
190
+ adGroupCriterion: z.object({
191
+ criterionId: int64String,
192
+ keyword: z.object({
193
+ text: z.string().nullish(),
194
+ matchType: z.string().nullish()
195
+ }).nullish(),
196
+ resourceName: z.string().nullish()
197
+ }),
198
+ metrics: metricsSchema
199
+ });
200
+ var campaignsResponseSchema = z.array(campaignRowSchema);
201
+ var campaignMetricsResponseSchema = z.array(campaignMetricRowSchema);
202
+ var adGroupMetricsResponseSchema = z.array(adGroupMetricRowSchema);
203
+ var keywordMetricsResponseSchema = z.array(keywordMetricRowSchema);
204
+ var googleAdsResources = defineResources({
205
+ [ENTITY_TYPE_CAMPAIGN]: {
206
+ shape: "entity",
207
+ description: "Google Ads campaigns with id, name, status, bidding strategy type, and start / end dates.",
208
+ endpoint: "POST /v18/customers/{customerId}/googleAds:search",
209
+ fields: [
210
+ { name: "id", description: "Numeric Google Ads campaign id." },
211
+ { name: "name", description: "Campaign display name." },
212
+ {
213
+ name: "status",
214
+ description: "Campaign status (ENABLED, PAUSED, REMOVED, UNKNOWN, UNSPECIFIED)."
215
+ },
216
+ {
217
+ name: "biddingStrategyType",
218
+ description: "Bidding strategy in use (e.g. MAXIMIZE_CONVERSIONS, MANUAL_CPC)."
219
+ },
220
+ { name: "startDate", description: "Campaign start date (YYYY-MM-DD)." },
221
+ {
222
+ name: "endDate",
223
+ description: "Campaign end date (YYYY-MM-DD), if set."
224
+ }
225
+ ],
226
+ responses: {
227
+ oauth_token: z.object({
228
+ access_token: z.string().min(1),
229
+ expires_in: z.number().int().positive().optional()
230
+ }),
231
+ campaigns: campaignsResponseSchema
232
+ }
233
+ },
234
+ google_ads_campaign_metrics: {
235
+ shape: "metric",
236
+ description: "Daily campaign performance - impressions, clicks, cost, conversions, and conversion value per (date, campaignId).",
237
+ endpoint: "POST /v18/customers/{customerId}/googleAds:search",
238
+ unit: "cost",
239
+ granularity: "day",
240
+ dimensions: [
241
+ { name: "date", description: "Calendar day of the metric sample." },
242
+ { name: "campaignId", description: "Numeric Google Ads campaign id." },
243
+ {
244
+ name: "campaignName",
245
+ description: "Campaign display name at sync time."
246
+ },
247
+ { name: "impressions", description: "Ad impressions served on the day." },
248
+ { name: "clicks", description: "Clicks recorded on the day." },
249
+ {
250
+ name: "cost",
251
+ description: "Cost in account currency units (cost_micros \xF7 1,000,000)."
252
+ },
253
+ {
254
+ name: "costMicros",
255
+ description: "Raw cost in micros, as returned by the API."
256
+ },
257
+ {
258
+ name: "conversions",
259
+ description: "Counted conversions attributed to the day."
260
+ },
261
+ {
262
+ name: "conversionsValue",
263
+ description: "Total value of conversions for the day."
264
+ }
265
+ ],
266
+ notes: "Sample value is `cost` (account currency units). All other fields are mirrored in attributes for filtering and ratio metrics (CPA = cost / conversions, ROAS = conversionsValue / cost).",
267
+ responses: { campaign_metrics: campaignMetricsResponseSchema }
268
+ },
269
+ google_ads_ad_group_metrics: {
270
+ shape: "metric",
271
+ description: "Daily ad-group performance - impressions, clicks, cost, and conversions per (date, adGroupId).",
272
+ endpoint: "POST /v18/customers/{customerId}/googleAds:search",
273
+ unit: "cost",
274
+ granularity: "day",
275
+ dimensions: [
276
+ { name: "date", description: "Calendar day of the metric sample." },
277
+ { name: "adGroupId", description: "Numeric Google Ads ad-group id." },
278
+ {
279
+ name: "adGroupName",
280
+ description: "Ad-group display name at sync time."
281
+ },
282
+ { name: "campaignId", description: "Parent campaign id." },
283
+ { name: "impressions", description: "Ad impressions served on the day." },
284
+ { name: "clicks", description: "Clicks recorded on the day." },
285
+ { name: "cost", description: "Cost in account currency units." },
286
+ {
287
+ name: "costMicros",
288
+ description: "Raw cost in micros, as returned by the API."
289
+ },
290
+ {
291
+ name: "conversions",
292
+ description: "Counted conversions attributed to the day."
293
+ }
294
+ ],
295
+ responses: { ad_group_metrics: adGroupMetricsResponseSchema }
296
+ },
297
+ google_ads_keyword_metrics: {
298
+ shape: "metric",
299
+ description: "Daily keyword performance - impressions, clicks, cost, and historical quality score per (date, criterionId).",
300
+ endpoint: "POST /v18/customers/{customerId}/googleAds:search",
301
+ unit: "cost",
302
+ granularity: "day",
303
+ dimensions: [
304
+ { name: "date", description: "Calendar day of the metric sample." },
305
+ {
306
+ name: "criterionId",
307
+ description: "Numeric keyword (ad-group criterion) id."
308
+ },
309
+ { name: "keywordText", description: "Keyword text." },
310
+ {
311
+ name: "matchType",
312
+ description: "Match type (EXACT, PHRASE, BROAD, \u2026)."
313
+ },
314
+ { name: "adGroupId", description: "Parent ad-group id." },
315
+ { name: "impressions", description: "Ad impressions served on the day." },
316
+ { name: "clicks", description: "Clicks recorded on the day." },
317
+ { name: "cost", description: "Cost in account currency units." },
318
+ {
319
+ name: "costMicros",
320
+ description: "Raw cost in micros, as returned by the API."
321
+ },
322
+ {
323
+ name: "qualityScore",
324
+ description: "Historical quality score for the day (1-10), null when no impressions."
325
+ }
326
+ ],
327
+ notes: "Driven by `keyword_view`; the cost / impression columns roll up to the criterion-day pair.",
328
+ responses: { keyword_metrics: keywordMetricsResponseSchema }
329
+ }
330
+ });
331
+ function toDateString(date) {
332
+ const y = date.getUTCFullYear();
333
+ const m = String(date.getUTCMonth() + 1).padStart(2, "0");
334
+ const d = String(date.getUTCDate()).padStart(2, "0");
335
+ return `${y}-${m}-${d}`;
336
+ }
337
+ var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
338
+ function dateStringToMs(yyyyMmDd) {
339
+ const m = DATE_RE.exec(yyyyMmDd);
340
+ if (!m) {
341
+ return 0;
342
+ }
343
+ const ms = Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
344
+ return Number.isFinite(ms) ? ms : 0;
345
+ }
346
+ function getDateRange(options, lookbackDays, now = Date.now()) {
347
+ const endDate = toDateString(new Date(now));
348
+ if (options.mode === "latest") {
349
+ const startMs2 = now - (INCREMENTAL_LOOKBACK_DAYS - 1) * MS_PER_DAY;
350
+ return { startDate: toDateString(new Date(startMs2)), endDate };
351
+ }
352
+ if (options.since) {
353
+ const sinceMs = new Date(options.since).getTime();
354
+ if (Number.isFinite(sinceMs)) {
355
+ const days = Math.max(1, Math.ceil((now - sinceMs) / MS_PER_DAY));
356
+ const cappedDays = Math.min(days, lookbackDays);
357
+ const startMs2 = now - (cappedDays - 1) * MS_PER_DAY;
358
+ return { startDate: toDateString(new Date(startMs2)), endDate };
359
+ }
360
+ }
361
+ const startMs = now - (lookbackDays - 1) * MS_PER_DAY;
362
+ return { startDate: toDateString(new Date(startMs)), endDate };
363
+ }
364
+ function coerceInt(value) {
365
+ if (typeof value === "number") {
366
+ return Number.isFinite(value) ? value : 0;
367
+ }
368
+ if (typeof value === "string" && value !== "") {
369
+ const n = Number(value);
370
+ return Number.isFinite(n) ? n : 0;
371
+ }
372
+ return 0;
373
+ }
374
+ function coerceIntOrNull(value) {
375
+ if (value === null || value === void 0) {
376
+ return null;
377
+ }
378
+ const n = coerceInt(value);
379
+ return n;
380
+ }
381
+ function microsToUnits(micros) {
382
+ return coerceInt(micros) / MICROS_PER_UNIT;
383
+ }
384
+ function campaignsQuery() {
385
+ return [
386
+ "SELECT",
387
+ " campaign.id,",
388
+ " campaign.name,",
389
+ " campaign.status,",
390
+ " campaign.bidding_strategy_type,",
391
+ " campaign.start_date,",
392
+ " campaign.end_date",
393
+ "FROM campaign"
394
+ ].join(" ");
395
+ }
396
+ function campaignMetricsQuery(range) {
397
+ return [
398
+ "SELECT",
399
+ " segments.date,",
400
+ " campaign.id,",
401
+ " campaign.name,",
402
+ " metrics.impressions,",
403
+ " metrics.clicks,",
404
+ " metrics.cost_micros,",
405
+ " metrics.conversions,",
406
+ " metrics.conversions_value",
407
+ "FROM campaign",
408
+ `WHERE segments.date BETWEEN '${range.startDate}' AND '${range.endDate}'`
409
+ ].join(" ");
410
+ }
411
+ function adGroupMetricsQuery(range) {
412
+ return [
413
+ "SELECT",
414
+ " segments.date,",
415
+ " campaign.id,",
416
+ " ad_group.id,",
417
+ " ad_group.name,",
418
+ " metrics.impressions,",
419
+ " metrics.clicks,",
420
+ " metrics.cost_micros,",
421
+ " metrics.conversions",
422
+ "FROM ad_group",
423
+ `WHERE segments.date BETWEEN '${range.startDate}' AND '${range.endDate}'`
424
+ ].join(" ");
425
+ }
426
+ function keywordMetricsQuery(range) {
427
+ return [
428
+ "SELECT",
429
+ " segments.date,",
430
+ " ad_group.id,",
431
+ " ad_group_criterion.criterion_id,",
432
+ " ad_group_criterion.keyword.text,",
433
+ " ad_group_criterion.keyword.match_type,",
434
+ " metrics.impressions,",
435
+ " metrics.clicks,",
436
+ " metrics.cost_micros,",
437
+ " metrics.historical_quality_score",
438
+ "FROM keyword_view",
439
+ `WHERE segments.date BETWEEN '${range.startDate}' AND '${range.endDate}'`
440
+ ].join(" ");
441
+ }
442
+ function queryForPhase(phase, range) {
443
+ switch (phase) {
444
+ case "campaigns":
445
+ return campaignsQuery();
446
+ case "campaign_metrics":
447
+ return campaignMetricsQuery(range);
448
+ case "ad_group_metrics":
449
+ return adGroupMetricsQuery(range);
450
+ case "keyword_metrics":
451
+ return keywordMetricsQuery(range);
452
+ }
453
+ }
454
+ function campaignToEntity(row) {
455
+ const c = row.campaign;
456
+ const startMs = c.startDate ? dateStringToMs(c.startDate) : 0;
457
+ return {
458
+ type: ENTITY_TYPE_CAMPAIGN,
459
+ id: String(c.id),
460
+ attributes: {
461
+ name: c.name ?? null,
462
+ status: c.status ?? null,
463
+ biddingStrategyType: c.biddingStrategyType ?? null,
464
+ startDate: c.startDate ?? null,
465
+ endDate: c.endDate ?? null,
466
+ resourceName: c.resourceName ?? null
467
+ },
468
+ updated_at: startMs
469
+ };
470
+ }
471
+ function campaignMetricRowToSample(row) {
472
+ const m = row.metrics;
473
+ const cost = microsToUnits(m.costMicros);
474
+ return {
475
+ name: METRIC_NAME.campaign_metrics,
476
+ ts: dateStringToMs(row.segments.date),
477
+ value: cost,
478
+ attributes: {
479
+ date: row.segments.date,
480
+ campaignId: String(row.campaign.id),
481
+ campaignName: row.campaign.name ?? null,
482
+ impressions: coerceInt(m.impressions),
483
+ clicks: coerceInt(m.clicks),
484
+ cost,
485
+ costMicros: coerceInt(m.costMicros),
486
+ conversions: typeof m.conversions === "number" ? m.conversions : 0,
487
+ conversionsValue: typeof m.conversionsValue === "number" ? m.conversionsValue : 0
488
+ }
489
+ };
490
+ }
491
+ function adGroupMetricRowToSample(row) {
492
+ const m = row.metrics;
493
+ const cost = microsToUnits(m.costMicros);
494
+ return {
495
+ name: METRIC_NAME.ad_group_metrics,
496
+ ts: dateStringToMs(row.segments.date),
497
+ value: cost,
498
+ attributes: {
499
+ date: row.segments.date,
500
+ adGroupId: String(row.adGroup.id),
501
+ adGroupName: row.adGroup.name ?? null,
502
+ campaignId: row.campaign?.id != null ? String(row.campaign.id) : null,
503
+ impressions: coerceInt(m.impressions),
504
+ clicks: coerceInt(m.clicks),
505
+ cost,
506
+ costMicros: coerceInt(m.costMicros),
507
+ conversions: typeof m.conversions === "number" ? m.conversions : 0
508
+ }
509
+ };
510
+ }
511
+ function keywordMetricRowToSample(row) {
512
+ const m = row.metrics;
513
+ const cost = microsToUnits(m.costMicros);
514
+ return {
515
+ name: METRIC_NAME.keyword_metrics,
516
+ ts: dateStringToMs(row.segments.date),
517
+ value: cost,
518
+ attributes: {
519
+ date: row.segments.date,
520
+ criterionId: String(row.adGroupCriterion.criterionId),
521
+ keywordText: row.adGroupCriterion.keyword?.text ?? null,
522
+ matchType: row.adGroupCriterion.keyword?.matchType ?? null,
523
+ adGroupId: row.adGroup?.id != null ? String(row.adGroup.id) : null,
524
+ impressions: coerceInt(m.impressions),
525
+ clicks: coerceInt(m.clicks),
526
+ cost,
527
+ costMicros: coerceInt(m.costMicros),
528
+ qualityScore: coerceIntOrNull(m.historicalQualityScore)
529
+ }
530
+ };
531
+ }
532
+ var GoogleAdsConnector = class _GoogleAdsConnector extends BaseConnector {
533
+ static id = "google-ads";
534
+ static resources = googleAdsResources;
535
+ static schemas = schemasFromResources(googleAdsResources);
536
+ static create(input, ctx) {
537
+ const parsed = configFields.parse(input);
538
+ return new _GoogleAdsConnector(
539
+ {
540
+ customerId: parsed.customerId,
541
+ loginCustomerId: parsed.loginCustomerId,
542
+ lookbackDays: parsed.lookbackDays,
543
+ resources: parsed.resources
544
+ },
545
+ {
546
+ clientId: parsed.clientId,
547
+ clientSecret: parsed.clientSecret,
548
+ refreshToken: parsed.refreshToken,
549
+ developerToken: parsed.developerToken
550
+ },
551
+ ctx
552
+ );
553
+ }
554
+ id = "google-ads";
555
+ credentials = googleAdsCredentials;
556
+ cachedToken = null;
557
+ async fetchAccessToken(signal) {
558
+ const body = new URLSearchParams({
559
+ grant_type: "refresh_token",
560
+ client_id: this.creds.clientId,
561
+ client_secret: this.creds.clientSecret,
562
+ refresh_token: this.creds.refreshToken
563
+ }).toString();
564
+ const res = await this.post(TOKEN_URL, {
565
+ resource: "oauth_token",
566
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
567
+ body,
568
+ signal
569
+ });
570
+ const expiresIn = res.body.expires_in ?? 3600;
571
+ return {
572
+ token: res.body.access_token,
573
+ // Refresh 60s before expiry so a long-running phase doesn't 401 on the
574
+ // boundary.
575
+ expiresAt: Date.now() + (expiresIn - 60) * 1e3
576
+ };
577
+ }
578
+ async getAccessToken(signal) {
579
+ if (this.cachedToken && Date.now() < this.cachedToken.expiresAt) {
580
+ return this.cachedToken.token;
581
+ }
582
+ this.cachedToken = await this.fetchAccessToken(signal);
583
+ return this.cachedToken.token;
584
+ }
585
+ buildHeaders(accessToken) {
586
+ const headers = {
587
+ Authorization: `Bearer ${accessToken}`,
588
+ "Content-Type": "application/json",
589
+ "developer-token": this.creds.developerToken,
590
+ "User-Agent": connectorUserAgent("google-ads")
591
+ };
592
+ if (this.settings.loginCustomerId) {
593
+ headers["login-customer-id"] = this.settings.loginCustomerId;
594
+ }
595
+ return headers;
596
+ }
597
+ async searchPage(phase, range, pageToken, signal) {
598
+ const token = await this.getAccessToken(signal);
599
+ const url = `https://googleads.googleapis.com/${API_VERSION}/customers/${this.settings.customerId}/googleAds:search`;
600
+ const body = {
601
+ query: queryForPhase(phase, range),
602
+ pageSize: PAGE_SIZE
603
+ };
604
+ if (pageToken) {
605
+ body.pageToken = pageToken;
606
+ }
607
+ const res = await this.post(url, {
608
+ resource: phase,
609
+ headers: this.buildHeaders(token),
610
+ body: JSON.stringify(body),
611
+ signal
612
+ });
613
+ return {
614
+ items: res.body.results ?? [],
615
+ next: res.body.nextPageToken ?? null
616
+ };
617
+ }
618
+ async writePhase(phase, items, storage) {
619
+ switch (phase) {
620
+ case "campaigns": {
621
+ for (const row of items) {
622
+ await storage.entity(campaignToEntity(row));
623
+ }
624
+ return;
625
+ }
626
+ case "campaign_metrics": {
627
+ const samples = items.map(
628
+ campaignMetricRowToSample
629
+ );
630
+ await storage.metrics(samples, {
631
+ names: [METRIC_NAME.campaign_metrics]
632
+ });
633
+ return;
634
+ }
635
+ case "ad_group_metrics": {
636
+ const samples = items.map(
637
+ adGroupMetricRowToSample
638
+ );
639
+ await storage.metrics(samples, {
640
+ names: [METRIC_NAME.ad_group_metrics]
641
+ });
642
+ return;
643
+ }
644
+ case "keyword_metrics": {
645
+ const samples = items.map(
646
+ keywordMetricRowToSample
647
+ );
648
+ await storage.metrics(samples, {
649
+ names: [METRIC_NAME.keyword_metrics]
650
+ });
651
+ return;
652
+ }
653
+ }
654
+ }
655
+ async clearScopeOnFirstPage(phase, storage, isFull) {
656
+ if (phase === "campaigns") {
657
+ if (isFull) {
658
+ await storage.entities([], { types: [ENTITY_TYPE_CAMPAIGN] });
659
+ }
660
+ return;
661
+ }
662
+ await storage.metrics([], { names: [METRIC_NAME[phase]] });
663
+ }
664
+ async sync(options, storage, signal) {
665
+ const lookbackDays = this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;
666
+ const range = getDateRange(options, lookbackDays);
667
+ const isFull = options.mode === "full";
668
+ const phases = selectActivePhases(
669
+ (r) => r,
670
+ PHASE_ORDER,
671
+ this.settings.resources
672
+ );
673
+ const cursor = isGoogleAdsSyncCursor(options.cursor) ? options.cursor : void 0;
674
+ return paginateChunked({
675
+ phases,
676
+ cursor,
677
+ signal,
678
+ logger: this.logger,
679
+ fetchPage: (phase, page, sig) => this.searchPage(phase, range, page, sig),
680
+ writeBatch: async (phase, items, page) => {
681
+ if (page === null) {
682
+ await this.clearScopeOnFirstPage(phase, storage, isFull);
683
+ }
684
+ await this.writePhase(phase, items, storage);
685
+ }
686
+ });
687
+ }
688
+ };
689
+
690
+ // src/index.ts
691
+ var index_default = GoogleAdsConnector;
692
+ export {
693
+ GoogleAdsConnector,
694
+ adGroupMetricRowToSample,
695
+ campaignMetricRowToSample,
696
+ campaignToEntity,
697
+ configFields,
698
+ index_default as default,
699
+ doc,
700
+ getDateRange,
701
+ keywordMetricRowToSample
702
+ };
703
+ //# sourceMappingURL=index.js.map