@rawdash/connector-google-ads 0.28.0 → 0.28.2
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/README.md +4 -4
- package/dist/index.d.ts +8 -8
- package/dist/index.js +42 -26
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ OAuth 2.0 refresh token against an account with read access to the Google Ads cu
|
|
|
39
39
|
## Resources
|
|
40
40
|
|
|
41
41
|
- **`google_ads_campaign`** _(entity)_ - Google Ads campaigns with id, name, status, bidding strategy type, and start / end dates.
|
|
42
|
-
- Endpoint: `POST /
|
|
42
|
+
- Endpoint: `POST /v24/customers/{customerId}/googleAds:search`
|
|
43
43
|
- `id`: Numeric Google Ads campaign id.
|
|
44
44
|
- `name`: Campaign display name.
|
|
45
45
|
- `status`: Campaign status (ENABLED, PAUSED, REMOVED, UNKNOWN, UNSPECIFIED).
|
|
@@ -47,18 +47,18 @@ OAuth 2.0 refresh token against an account with read access to the Google Ads cu
|
|
|
47
47
|
- `startDate`: Campaign start date (YYYY-MM-DD).
|
|
48
48
|
- `endDate`: Campaign end date (YYYY-MM-DD), if set.
|
|
49
49
|
- **`google_ads_campaign_metrics`** _(metric)_ - Daily campaign performance - impressions, clicks, cost, conversions, and conversion value per (date, campaignId).
|
|
50
|
-
- Endpoint: `POST /
|
|
50
|
+
- Endpoint: `POST /v24/customers/{customerId}/googleAds:search`
|
|
51
51
|
- Unit: USD
|
|
52
52
|
- Granularity: day
|
|
53
53
|
- Dimensions: `date`, `campaignId`, `campaignName`, `impressions`, `clicks`, `cost`, `costMicros`, `conversions`, `conversionsValue`
|
|
54
54
|
- 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).
|
|
55
55
|
- **`google_ads_ad_group_metrics`** _(metric)_ - Daily ad-group performance - impressions, clicks, cost, and conversions per (date, adGroupId).
|
|
56
|
-
- Endpoint: `POST /
|
|
56
|
+
- Endpoint: `POST /v24/customers/{customerId}/googleAds:search`
|
|
57
57
|
- Unit: USD
|
|
58
58
|
- Granularity: day
|
|
59
59
|
- Dimensions: `date`, `adGroupId`, `adGroupName`, `campaignId`, `impressions`, `clicks`, `cost`, `costMicros`, `conversions`
|
|
60
60
|
- **`google_ads_keyword_metrics`** _(metric)_ - Daily keyword performance - impressions, clicks, cost, and historical quality score per (date, criterionId).
|
|
61
|
-
- Endpoint: `POST /
|
|
61
|
+
- Endpoint: `POST /v24/customers/{customerId}/googleAds:search`
|
|
62
62
|
- Unit: USD
|
|
63
63
|
- Granularity: day
|
|
64
64
|
- Dimensions: `date`, `criterionId`, `keywordText`, `matchType`, `adGroupId`, `impressions`, `clicks`, `cost`, `costMicros`, `qualityScore`
|
package/dist/index.d.ts
CHANGED
|
@@ -138,7 +138,7 @@ declare const googleAdsResources: {
|
|
|
138
138
|
readonly values: ["ENABLED", "PAUSED", "REMOVED"];
|
|
139
139
|
}];
|
|
140
140
|
readonly description: "Google Ads campaigns with id, name, status, bidding strategy type, and start / end dates.";
|
|
141
|
-
readonly endpoint: "POST /
|
|
141
|
+
readonly endpoint: "POST /v24/customers/{customerId}/googleAds:search";
|
|
142
142
|
readonly fields: [{
|
|
143
143
|
readonly name: "id";
|
|
144
144
|
readonly description: "Numeric Google Ads campaign id.";
|
|
@@ -179,7 +179,7 @@ declare const googleAdsResources: {
|
|
|
179
179
|
readonly google_ads_campaign_metrics: {
|
|
180
180
|
readonly shape: "metric";
|
|
181
181
|
readonly description: "Daily campaign performance - impressions, clicks, cost, conversions, and conversion value per (date, campaignId).";
|
|
182
|
-
readonly endpoint: "POST /
|
|
182
|
+
readonly endpoint: "POST /v24/customers/{customerId}/googleAds:search";
|
|
183
183
|
readonly unit: "USD";
|
|
184
184
|
readonly granularity: "day";
|
|
185
185
|
readonly dimensions: [{
|
|
@@ -235,7 +235,7 @@ declare const googleAdsResources: {
|
|
|
235
235
|
readonly google_ads_ad_group_metrics: {
|
|
236
236
|
readonly shape: "metric";
|
|
237
237
|
readonly description: "Daily ad-group performance - impressions, clicks, cost, and conversions per (date, adGroupId).";
|
|
238
|
-
readonly endpoint: "POST /
|
|
238
|
+
readonly endpoint: "POST /v24/customers/{customerId}/googleAds:search";
|
|
239
239
|
readonly unit: "USD";
|
|
240
240
|
readonly granularity: "day";
|
|
241
241
|
readonly dimensions: [{
|
|
@@ -293,7 +293,7 @@ declare const googleAdsResources: {
|
|
|
293
293
|
readonly google_ads_keyword_metrics: {
|
|
294
294
|
readonly shape: "metric";
|
|
295
295
|
readonly description: "Daily keyword performance - impressions, clicks, cost, and historical quality score per (date, criterionId).";
|
|
296
|
-
readonly endpoint: "POST /
|
|
296
|
+
readonly endpoint: "POST /v24/customers/{customerId}/googleAds:search";
|
|
297
297
|
readonly unit: "USD";
|
|
298
298
|
readonly granularity: "day";
|
|
299
299
|
readonly dimensions: [{
|
|
@@ -397,7 +397,7 @@ declare class GoogleAdsConnector extends BaseConnector<GoogleAdsSettings, Google
|
|
|
397
397
|
readonly values: ["ENABLED", "PAUSED", "REMOVED"];
|
|
398
398
|
}];
|
|
399
399
|
readonly description: "Google Ads campaigns with id, name, status, bidding strategy type, and start / end dates.";
|
|
400
|
-
readonly endpoint: "POST /
|
|
400
|
+
readonly endpoint: "POST /v24/customers/{customerId}/googleAds:search";
|
|
401
401
|
readonly fields: [{
|
|
402
402
|
readonly name: "id";
|
|
403
403
|
readonly description: "Numeric Google Ads campaign id.";
|
|
@@ -438,7 +438,7 @@ declare class GoogleAdsConnector extends BaseConnector<GoogleAdsSettings, Google
|
|
|
438
438
|
readonly google_ads_campaign_metrics: {
|
|
439
439
|
readonly shape: "metric";
|
|
440
440
|
readonly description: "Daily campaign performance - impressions, clicks, cost, conversions, and conversion value per (date, campaignId).";
|
|
441
|
-
readonly endpoint: "POST /
|
|
441
|
+
readonly endpoint: "POST /v24/customers/{customerId}/googleAds:search";
|
|
442
442
|
readonly unit: "USD";
|
|
443
443
|
readonly granularity: "day";
|
|
444
444
|
readonly dimensions: [{
|
|
@@ -494,7 +494,7 @@ declare class GoogleAdsConnector extends BaseConnector<GoogleAdsSettings, Google
|
|
|
494
494
|
readonly google_ads_ad_group_metrics: {
|
|
495
495
|
readonly shape: "metric";
|
|
496
496
|
readonly description: "Daily ad-group performance - impressions, clicks, cost, and conversions per (date, adGroupId).";
|
|
497
|
-
readonly endpoint: "POST /
|
|
497
|
+
readonly endpoint: "POST /v24/customers/{customerId}/googleAds:search";
|
|
498
498
|
readonly unit: "USD";
|
|
499
499
|
readonly granularity: "day";
|
|
500
500
|
readonly dimensions: [{
|
|
@@ -552,7 +552,7 @@ declare class GoogleAdsConnector extends BaseConnector<GoogleAdsSettings, Google
|
|
|
552
552
|
readonly google_ads_keyword_metrics: {
|
|
553
553
|
readonly shape: "metric";
|
|
554
554
|
readonly description: "Daily keyword performance - impressions, clicks, cost, and historical quality score per (date, criterionId).";
|
|
555
|
-
readonly endpoint: "POST /
|
|
555
|
+
readonly endpoint: "POST /v24/customers/{customerId}/googleAds:search";
|
|
556
556
|
readonly unit: "USD";
|
|
557
557
|
readonly granularity: "day";
|
|
558
558
|
readonly dimensions: [{
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,14 @@ async function signRS256JWT(payload, privateKeyPem) {
|
|
|
44
44
|
return `${signingInput}.${base64urlFromBytes(new Uint8Array(signature))}`;
|
|
45
45
|
}
|
|
46
46
|
function parseServiceAccountJson(value) {
|
|
47
|
+
if (typeof value === "object" && value !== null) {
|
|
48
|
+
return serviceAccountKeySchema.parse(value);
|
|
49
|
+
}
|
|
50
|
+
if (typeof value !== "string") {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`serviceAccountJson must be a JSON object, raw JSON string, or base64-encoded JSON, but received ${typeof value}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
47
55
|
const trimmed = value.trim();
|
|
48
56
|
if (trimmed.startsWith("{")) {
|
|
49
57
|
return serviceAccountKeySchema.parse(JSON.parse(trimmed));
|
|
@@ -292,7 +300,7 @@ var PHASE_ORDER = [
|
|
|
292
300
|
"keyword_metrics"
|
|
293
301
|
];
|
|
294
302
|
var isGoogleAdsSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
|
|
295
|
-
var API_VERSION = "
|
|
303
|
+
var API_VERSION = "v24";
|
|
296
304
|
var PAGE_SIZE = 1e4;
|
|
297
305
|
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
298
306
|
var DEFAULT_LOOKBACK_DAYS = 90;
|
|
@@ -377,7 +385,7 @@ var googleAdsResources = defineResources({
|
|
|
377
385
|
}
|
|
378
386
|
],
|
|
379
387
|
description: "Google Ads campaigns with id, name, status, bidding strategy type, and start / end dates.",
|
|
380
|
-
endpoint: "POST /
|
|
388
|
+
endpoint: "POST /v24/customers/{customerId}/googleAds:search",
|
|
381
389
|
fields: [
|
|
382
390
|
{ name: "id", description: "Numeric Google Ads campaign id." },
|
|
383
391
|
{ name: "name", description: "Campaign display name." },
|
|
@@ -406,7 +414,7 @@ var googleAdsResources = defineResources({
|
|
|
406
414
|
google_ads_campaign_metrics: {
|
|
407
415
|
shape: "metric",
|
|
408
416
|
description: "Daily campaign performance - impressions, clicks, cost, conversions, and conversion value per (date, campaignId).",
|
|
409
|
-
endpoint: "POST /
|
|
417
|
+
endpoint: "POST /v24/customers/{customerId}/googleAds:search",
|
|
410
418
|
unit: "USD",
|
|
411
419
|
granularity: "day",
|
|
412
420
|
dimensions: [
|
|
@@ -441,7 +449,7 @@ var googleAdsResources = defineResources({
|
|
|
441
449
|
google_ads_ad_group_metrics: {
|
|
442
450
|
shape: "metric",
|
|
443
451
|
description: "Daily ad-group performance - impressions, clicks, cost, and conversions per (date, adGroupId).",
|
|
444
|
-
endpoint: "POST /
|
|
452
|
+
endpoint: "POST /v24/customers/{customerId}/googleAds:search",
|
|
445
453
|
unit: "USD",
|
|
446
454
|
granularity: "day",
|
|
447
455
|
dimensions: [
|
|
@@ -469,7 +477,7 @@ var googleAdsResources = defineResources({
|
|
|
469
477
|
google_ads_keyword_metrics: {
|
|
470
478
|
shape: "metric",
|
|
471
479
|
description: "Daily keyword performance - impressions, clicks, cost, and historical quality score per (date, criterionId).",
|
|
472
|
-
endpoint: "POST /
|
|
480
|
+
endpoint: "POST /v24/customers/{customerId}/googleAds:search",
|
|
473
481
|
unit: "USD",
|
|
474
482
|
granularity: "day",
|
|
475
483
|
dimensions: [
|
|
@@ -515,6 +523,14 @@ function dateStringToMs(yyyyMmDd) {
|
|
|
515
523
|
const ms = Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
516
524
|
return Number.isFinite(ms) ? ms : 0;
|
|
517
525
|
}
|
|
526
|
+
function dateRangeToReplaceWindow(range) {
|
|
527
|
+
const start = dateStringToMs(range.startDate);
|
|
528
|
+
const end = dateStringToMs(range.endDate) + MS_PER_DAY - 1;
|
|
529
|
+
if (start > end) {
|
|
530
|
+
return void 0;
|
|
531
|
+
}
|
|
532
|
+
return { start, end };
|
|
533
|
+
}
|
|
518
534
|
function getDateRange(options, lookbackDays, now = Date.now()) {
|
|
519
535
|
const endDate = toDateString(new Date(now));
|
|
520
536
|
if (options.mode === "latest") {
|
|
@@ -809,46 +825,41 @@ var GoogleAdsConnector = class _GoogleAdsConnector extends BaseConnector {
|
|
|
809
825
|
return;
|
|
810
826
|
}
|
|
811
827
|
case "campaign_metrics": {
|
|
812
|
-
const
|
|
813
|
-
campaignMetricRowToSample
|
|
814
|
-
|
|
815
|
-
await storage.metrics(samples, {
|
|
816
|
-
names: [METRIC_NAME.campaign_metrics]
|
|
817
|
-
});
|
|
828
|
+
for (const row of items) {
|
|
829
|
+
await storage.metric(campaignMetricRowToSample(row));
|
|
830
|
+
}
|
|
818
831
|
return;
|
|
819
832
|
}
|
|
820
833
|
case "ad_group_metrics": {
|
|
821
|
-
const
|
|
822
|
-
adGroupMetricRowToSample
|
|
823
|
-
|
|
824
|
-
await storage.metrics(samples, {
|
|
825
|
-
names: [METRIC_NAME.ad_group_metrics]
|
|
826
|
-
});
|
|
834
|
+
for (const row of items) {
|
|
835
|
+
await storage.metric(adGroupMetricRowToSample(row));
|
|
836
|
+
}
|
|
827
837
|
return;
|
|
828
838
|
}
|
|
829
839
|
case "keyword_metrics": {
|
|
830
|
-
const
|
|
831
|
-
keywordMetricRowToSample
|
|
832
|
-
|
|
833
|
-
await storage.metrics(samples, {
|
|
834
|
-
names: [METRIC_NAME.keyword_metrics]
|
|
835
|
-
});
|
|
840
|
+
for (const row of items) {
|
|
841
|
+
await storage.metric(keywordMetricRowToSample(row));
|
|
842
|
+
}
|
|
836
843
|
return;
|
|
837
844
|
}
|
|
838
845
|
}
|
|
839
846
|
}
|
|
840
|
-
async clearScopeOnFirstPage(phase, storage, isFull) {
|
|
847
|
+
async clearScopeOnFirstPage(phase, storage, isFull, replaceWindow) {
|
|
841
848
|
if (phase === "campaigns") {
|
|
842
849
|
if (isFull) {
|
|
843
850
|
await storage.entities([], { types: [ENTITY_TYPE_CAMPAIGN] });
|
|
844
851
|
}
|
|
845
852
|
return;
|
|
846
853
|
}
|
|
847
|
-
await storage.metrics([], {
|
|
854
|
+
await storage.metrics([], {
|
|
855
|
+
names: [METRIC_NAME[phase]],
|
|
856
|
+
...replaceWindow ? { replaceWindow } : {}
|
|
857
|
+
});
|
|
848
858
|
}
|
|
849
859
|
async sync(options, storage, signal) {
|
|
850
860
|
const lookbackDays = this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;
|
|
851
861
|
const range = getDateRange(options, lookbackDays);
|
|
862
|
+
const replaceWindow = dateRangeToReplaceWindow(range);
|
|
852
863
|
const isFull = options.mode === "full";
|
|
853
864
|
const phases = selectActivePhases(
|
|
854
865
|
(r) => r,
|
|
@@ -865,7 +876,12 @@ var GoogleAdsConnector = class _GoogleAdsConnector extends BaseConnector {
|
|
|
865
876
|
fetchPage: (phase, page, sig) => this.searchPage(phase, range, page, campaignSpec, sig),
|
|
866
877
|
writeBatch: async (phase, items, page) => {
|
|
867
878
|
if (page === null) {
|
|
868
|
-
await this.clearScopeOnFirstPage(
|
|
879
|
+
await this.clearScopeOnFirstPage(
|
|
880
|
+
phase,
|
|
881
|
+
storage,
|
|
882
|
+
isFull,
|
|
883
|
+
replaceWindow
|
|
884
|
+
);
|
|
869
885
|
}
|
|
870
886
|
await this.writePhase(phase, items, storage);
|
|
871
887
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../gcp-shared/src/auth.ts","../../gcp-shared/src/config.ts","../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/map-concurrent.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../../gcp-shared/src/bigquery.ts","../../gcp-shared/src/access-token.ts","../../gcp-shared/src/dates.ts","../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/map-concurrent.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/google-ads.ts","../src/index.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport interface ServiceAccountKey {\n client_email: string;\n private_key: string;\n token_uri?: string;\n project_id?: string;\n}\n\nconst serviceAccountKeySchema = z.object({\n client_email: z.string().min(1),\n private_key: z.string().min(1),\n token_uri: z.string().url().optional(),\n project_id: z.string().optional(),\n});\n\nexport interface TokenResponse {\n access_token: string;\n expires_in?: number;\n}\n\nexport const tokenResponseSchema = z.object({\n access_token: z.string().min(1),\n expires_in: z.number().int().positive().optional(),\n});\n\nfunction base64urlFromBytes(bytes: Uint8Array): string {\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n}\n\nfunction base64urlFromString(str: string): string {\n return base64urlFromBytes(new TextEncoder().encode(str));\n}\n\nasync function signRS256JWT(\n payload: Record<string, unknown>,\n privateKeyPem: string,\n): Promise<string> {\n const header = { alg: 'RS256', typ: 'JWT' };\n const headerB64 = base64urlFromString(JSON.stringify(header));\n const payloadB64 = base64urlFromString(JSON.stringify(payload));\n const signingInput = `${headerB64}.${payloadB64}`;\n\n const pemContent = privateKeyPem\n .replace(/-----BEGIN PRIVATE KEY-----/g, '')\n .replace(/-----END PRIVATE KEY-----/g, '')\n .replace(/\\s/g, '');\n const der = Uint8Array.from(atob(pemContent), (c) => c.charCodeAt(0));\n\n const key = await globalThis.crypto.subtle.importKey(\n 'pkcs8',\n der,\n { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n\n const signature = await globalThis.crypto.subtle.sign(\n 'RSASSA-PKCS1-v1_5',\n key,\n new TextEncoder().encode(signingInput),\n );\n\n return `${signingInput}.${base64urlFromBytes(new Uint8Array(signature))}`;\n}\n\nexport function parseServiceAccountJson(value: string): ServiceAccountKey {\n const trimmed = value.trim();\n if (trimmed.startsWith('{')) {\n return serviceAccountKeySchema.parse(JSON.parse(trimmed));\n }\n const binary = atob(trimmed);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n const decoded = new TextDecoder().decode(bytes);\n return serviceAccountKeySchema.parse(JSON.parse(decoded));\n}\n\nexport async function buildServiceAccountJwt(\n serviceAccountJson: string,\n scope: string,\n): Promise<{ url: string; body: string }> {\n const sa = parseServiceAccountJson(serviceAccountJson);\n const now = Math.floor(Date.now() / 1000);\n const jwt = await signRS256JWT(\n {\n iss: sa.client_email,\n scope,\n aud: sa.token_uri ?? 'https://oauth2.googleapis.com/token',\n exp: now + 3600,\n iat: now,\n },\n sa.private_key,\n );\n\n const body = new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion: jwt,\n }).toString();\n\n return {\n url: sa.token_uri ?? 'https://oauth2.googleapis.com/token',\n body,\n };\n}\n\nexport interface RefreshTokenCredentials {\n refreshToken: string;\n clientId: string;\n clientSecret: string;\n}\n\nexport function buildRefreshTokenGrant(credentials: RefreshTokenCredentials): {\n url: string;\n body: string;\n} {\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: credentials.refreshToken,\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n }).toString();\n\n return { url: 'https://oauth2.googleapis.com/token', body };\n}\n","import { z } from 'zod';\n\nexport const gcpAuthConfigShape = {\n serviceAccountJson: z.object({ $secret: z.string().trim().min(1) }).meta({\n label: 'Service Account JSON',\n description:\n 'Contents of the JSON key file for a Google service account with the role required by this connector. Create one at Google Cloud -> IAM & Admin -> Service Accounts and store the JSON as a secret.',\n secret: true,\n }),\n} as const;\n\nexport interface GcpAuthConfig {\n serviceAccountJson: { $secret: string };\n}\n","import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import { parseEpoch } from '@rawdash/connector-shared';\nimport { z } from 'zod';\n\nexport const BQ_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;\nexport const BQ_DATASET_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nexport const BQ_API_BASE = 'https://bigquery.googleapis.com/bigquery/v2';\nexport const BQ_READONLY_SCOPE =\n 'https://www.googleapis.com/auth/bigquery.readonly';\nexport const BQ_PAGE_SIZE = 10_000;\nexport const BQ_QUERY_TIMEOUT_MS = 30_000;\n\nexport const bqQueryResponseSchema = z.object({\n jobComplete: z.boolean().optional(),\n schema: z\n .object({\n fields: z.array(z.object({ name: z.string(), type: z.string() })),\n })\n .optional(),\n rows: z\n .array(\n z.object({\n f: z.array(z.object({ v: z.string().nullable().optional() })),\n }),\n )\n .optional(),\n pageToken: z.string().optional(),\n jobReference: z\n .object({\n projectId: z.string(),\n jobId: z.string(),\n location: z.string().optional(),\n })\n .optional(),\n});\n\nexport type BqQueryResponse = z.infer<typeof bqQueryResponseSchema>;\nexport type BqJobReference = NonNullable<BqQueryResponse['jobReference']>;\n\nexport type BqPageRequest =\n | { method: 'POST'; url: string; body: string }\n | { method: 'GET'; url: string };\n\nexport interface BqPageLogger {\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n}\n\nexport function buildBigQueryPageRequest(opts: {\n projectId: string;\n sql: string;\n pageToken: string | undefined;\n jobReference: BqJobReference | undefined;\n location?: string;\n pageSize?: number;\n timeoutMs?: number;\n}): BqPageRequest {\n const pageSize = opts.pageSize ?? BQ_PAGE_SIZE;\n const timeoutMs = opts.timeoutMs ?? BQ_QUERY_TIMEOUT_MS;\n\n if (opts.pageToken === undefined) {\n const url = `${BQ_API_BASE}/projects/${encodeURIComponent(\n opts.projectId,\n )}/queries`;\n const body: Record<string, unknown> = {\n query: opts.sql,\n useLegacySql: false,\n maxResults: pageSize,\n timeoutMs,\n };\n if (opts.location !== undefined) {\n body['location'] = opts.location;\n }\n return { method: 'POST', url, body: JSON.stringify(body) };\n }\n\n if (opts.jobReference === undefined) {\n throw new Error(\n 'cannot fetch the next page of BigQuery results without a jobReference',\n );\n }\n\n const params = new URLSearchParams({\n pageToken: opts.pageToken,\n maxResults: String(pageSize),\n timeoutMs: String(timeoutMs),\n });\n const location = opts.jobReference.location ?? opts.location;\n if (location !== undefined) {\n params.set('location', location);\n }\n const url = `${BQ_API_BASE}/projects/${encodeURIComponent(\n opts.jobReference.projectId,\n )}/queries/${encodeURIComponent(opts.jobReference.jobId)}?${params.toString()}`;\n return { method: 'GET', url };\n}\n\nexport async function collectBigQueryPages<T>(opts: {\n projectId: string;\n sql: string;\n resource: string;\n fetchPage: (\n request: BqPageRequest,\n signal: AbortSignal | undefined,\n ) => Promise<BqQueryResponse>;\n mapRows: (response: BqQueryResponse) => T[];\n jobIncompleteMessage: string;\n location?: string;\n pageSize?: number;\n signal?: AbortSignal;\n logger?: BqPageLogger;\n}): Promise<{ rows: T[]; aborted: boolean }> {\n const rows: T[] = [];\n let pageToken: string | undefined;\n let jobReference: BqJobReference | undefined;\n let page = 0;\n const phaseStart = Date.now();\n\n do {\n if (opts.signal?.aborted) {\n return { rows, aborted: true };\n }\n const request = buildBigQueryPageRequest({\n projectId: opts.projectId,\n sql: opts.sql,\n pageToken,\n jobReference,\n location: opts.location,\n pageSize: opts.pageSize,\n });\n let response: BqQueryResponse;\n try {\n response = await opts.fetchPage(request, opts.signal);\n } catch (err) {\n opts.logger?.warn('fetch page failed', {\n resource: opts.resource,\n page: page + 1,\n error: err instanceof Error ? err.message : String(err),\n });\n throw err;\n }\n if (response.jobComplete === false) {\n throw new Error(opts.jobIncompleteMessage);\n }\n if (response.jobReference !== undefined) {\n jobReference = response.jobReference;\n }\n const pageRows = opts.mapRows(response);\n rows.push(...pageRows);\n pageToken =\n typeof response.pageToken === 'string' && response.pageToken.length > 0\n ? response.pageToken\n : undefined;\n page += 1;\n opts.logger?.info('fetched page', {\n resource: opts.resource,\n page,\n items: pageRows.length,\n next: pageToken ?? null,\n });\n } while (pageToken !== undefined);\n\n opts.logger?.info('resource done', {\n resource: opts.resource,\n pages: page,\n items: rows.length,\n duration_ms: Date.now() - phaseStart,\n });\n return { rows, aborted: false };\n}\n\nexport function indexBqFields(\n response: BqQueryResponse,\n): Record<string, number> {\n const fieldIndex: Record<string, number> = {};\n (response.schema?.fields ?? []).forEach((field, idx) => {\n fieldIndex[field.name] = idx;\n });\n return fieldIndex;\n}\n\nexport function readBqCell(\n cells: ReadonlyArray<{ v?: string | null }>,\n fieldIndex: Record<string, number>,\n name: string,\n): string | null {\n const idx = fieldIndex[name];\n if (idx === undefined) {\n return null;\n }\n const raw = cells[idx]?.v;\n if (raw === undefined || raw === null) {\n return null;\n }\n return raw;\n}\n\nexport function parseBqDateOrEpoch(value: string): number | null {\n const dateMatch = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value);\n if (dateMatch) {\n return Date.UTC(\n Number(dateMatch[1]),\n Number(dateMatch[2]) - 1,\n Number(dateMatch[3]),\n );\n }\n return parseEpoch(value, 'iso');\n}\n","import { AuthError } from '@rawdash/connector-shared';\n\nimport {\n type RefreshTokenCredentials,\n buildRefreshTokenGrant,\n buildServiceAccountJwt,\n} from './auth';\n\ninterface GcpTokenResponse {\n access_token: string;\n expires_in?: number;\n}\n\nexport type GcpTokenPoster = (\n url: string,\n opts: {\n resource: string;\n headers: Record<string, string>;\n body: string;\n signal?: AbortSignal;\n },\n) => Promise<{ body: GcpTokenResponse }>;\n\nexport class GcpAccessTokenProvider {\n private cached: { token: string; expiresAt: number } | null = null;\n\n constructor(\n private readonly opts: {\n connectorId: string;\n scope: string;\n getServiceAccountJson: () => string | undefined;\n getRefreshTokenCredentials?: () => RefreshTokenCredentials | undefined;\n post: GcpTokenPoster;\n },\n ) {}\n\n private async resolveGrant(): Promise<{ url: string; body: string }> {\n const serviceAccountJson = this.opts.getServiceAccountJson();\n if (serviceAccountJson) {\n return buildServiceAccountJwt(serviceAccountJson, this.opts.scope);\n }\n const refreshTokenCredentials = this.opts.getRefreshTokenCredentials?.();\n if (refreshTokenCredentials) {\n return buildRefreshTokenGrant(refreshTokenCredentials);\n }\n throw new AuthError(\n `${this.opts.connectorId}: missing serviceAccountJson or refresh-token credentials`,\n );\n }\n\n async getToken(signal?: AbortSignal): Promise<string> {\n if (this.cached && Date.now() < this.cached.expiresAt) {\n return this.cached.token;\n }\n const { url, body } = await this.resolveGrant();\n const res = await this.opts.post(url, {\n resource: 'oauth_token',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body,\n signal,\n });\n const expiresIn = res.body.expires_in ?? 3600;\n this.cached = {\n token: res.body.access_token,\n expiresAt: Date.now() + (expiresIn - 60) * 1000,\n };\n return this.cached.token;\n }\n}\n","export const MS_PER_DAY = 86_400_000;\n\nfunction pad2(n: number): string {\n return String(n).padStart(2, '0');\n}\n\nexport function toDateStr(ms: number): string {\n const d = new Date(ms);\n return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;\n}\n\nexport function startOfUtcDay(ms: number): number {\n return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;\n}\n","import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import { GcpAccessTokenProvider } from '@rawdash/connector-gcp-shared';\nimport { connectorUserAgent } from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type FetchSpec,\n type FilterClause,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n makeChunkedCursorGuard,\n paginateChunked,\n schemasFromResources,\n selectActivePhases,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\nexport const configFields = defineConfigFields(\n z.object({\n customerId: z\n .string()\n .trim()\n .regex(\n /^\\d{10}$/,\n 'customerId must be the 10-digit Google Ads ID, digits only (no dashes)',\n )\n .meta({\n label: 'Customer ID',\n description:\n 'Google Ads customer ID for the account to sync, digits only (the dashed form 123-456-7890 with the dashes removed).',\n placeholder: '1234567890',\n }),\n loginCustomerId: z\n .string()\n .trim()\n .regex(\n /^\\d{10}$/,\n 'loginCustomerId must be a 10-digit Google Ads ID, digits only',\n )\n .optional()\n .meta({\n label: 'Login Customer ID (MCC)',\n description:\n 'Manager (MCC) account ID, digits only. Set this when the OAuth credential authenticates against an MCC that owns the customer account.',\n placeholder: '1234567890',\n }),\n clientId: z.string().min(1).meta({\n label: 'OAuth Client ID',\n description:\n 'OAuth 2.0 client ID from a Google Cloud project that has the Google Ads API enabled.',\n placeholder: '…apps.googleusercontent.com',\n }),\n clientSecret: z.object({ $secret: z.string() }).meta({\n label: 'OAuth Client Secret',\n description: 'OAuth 2.0 client secret paired with the client ID above.',\n secret: true,\n }),\n refreshToken: z.object({ $secret: z.string() }).meta({\n label: 'OAuth Refresh Token',\n description:\n 'Google OAuth 2.0 refresh token issued for the https://www.googleapis.com/auth/adwords scope.',\n secret: true,\n }),\n developerToken: z.object({ $secret: z.string() }).meta({\n label: 'Developer Token',\n description:\n 'Google Ads API developer token from the manager account that owns API access (Tools → API Center).',\n secret: true,\n }),\n lookbackDays: z.number().int().positive().optional().meta({\n label: 'Lookback days (full sync)',\n description:\n 'How many calendar days of metric history to fetch on a full sync. Defaults to 90.',\n placeholder: '90',\n }),\n resources: z\n .array(\n z.enum([\n 'campaigns',\n 'campaign_metrics',\n 'ad_group_metrics',\n 'keyword_metrics',\n ]),\n )\n .nonempty()\n .optional()\n .meta({\n label: 'Resources',\n description:\n 'Which Google Ads resources to sync. Omit to sync everything; pin a subset to avoid pulling keyword-level metrics on a quota-limited token.',\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Google Ads',\n category: 'marketing',\n brandColor: '#4285F4',\n tagline:\n 'Sync Google Ads campaigns plus daily campaign, ad-group, and keyword performance (impressions, clicks, cost, conversions) via GAQL.',\n vendor: {\n name: 'Google Ads',\n domain: 'ads.google.com',\n apiDocs: 'https://developers.google.com/google-ads/api/docs/start',\n website: 'https://ads.google.com',\n },\n auth: {\n summary:\n '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.',\n setup: [\n 'Apply for Google Ads API access from your manager account (Tools → API Center). Copy the developer token - it lives on the manager, not the child account.',\n '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.',\n '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).',\n '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.',\n '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\")`.',\n ],\n },\n rateLimit:\n '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.',\n limitations: [\n 'Cost values are stored in account currency units (cost_micros ÷ 1,000,000); the original micro-precision integer is also exposed in attributes.',\n '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.',\n 'Incremental syncs trail the last 3 days because Google Ads can attribute conversions to a click up to 3 days after the event.',\n 'Audience-, asset-, and recommendation-level reporting are out of scope; this connector covers campaign / ad-group / keyword performance only.',\n ],\n});\n\nexport interface GoogleAdsSettings {\n customerId: string;\n loginCustomerId?: string;\n lookbackDays?: number;\n resources?: readonly GoogleAdsResource[];\n}\n\nconst googleAdsCredentials = {\n clientId: {\n description: 'Google OAuth 2.0 client ID (public, not a secret)',\n auth: 'required' as const,\n },\n clientSecret: {\n description: 'Google OAuth 2.0 client secret',\n auth: 'required' as const,\n },\n refreshToken: {\n description: 'Google OAuth 2.0 refresh token with the adwords scope',\n auth: 'required' as const,\n },\n developerToken: {\n description: 'Google Ads API developer token',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype GoogleAdsCredentials = typeof googleAdsCredentials;\n\nconst PHASE_ORDER = [\n 'campaigns',\n 'campaign_metrics',\n 'ad_group_metrics',\n 'keyword_metrics',\n] as const;\n\ntype GoogleAdsPhase = (typeof PHASE_ORDER)[number];\n\nexport type GoogleAdsResource = GoogleAdsPhase;\n\nconst isGoogleAdsSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);\n\nconst API_VERSION = 'v18';\nconst PAGE_SIZE = 10_000;\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\nconst DEFAULT_LOOKBACK_DAYS = 90;\nconst INCREMENTAL_LOOKBACK_DAYS = 3;\nconst MICROS_PER_UNIT = 1_000_000;\n\nconst ENTITY_TYPE_CAMPAIGN = 'google_ads_campaign';\nconst METRIC_NAME: Record<GoogleAdsPhase, string> = {\n campaigns: ENTITY_TYPE_CAMPAIGN,\n campaign_metrics: 'google_ads_campaign_metrics',\n ad_group_metrics: 'google_ads_ad_group_metrics',\n keyword_metrics: 'google_ads_keyword_metrics',\n};\n\nconst int64String = z.union([z.string().min(1), z.number()]);\n\nconst segmentsSchema = z.object({\n date: z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/),\n});\n\nconst dateString = z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/);\n\nconst campaignFieldsSchema = z.object({\n id: int64String,\n name: z.string().nullish(),\n status: z.string().nullish(),\n biddingStrategyType: z.string().nullish(),\n startDate: dateString.nullish(),\n endDate: dateString.nullish(),\n resourceName: z.string().nullish(),\n});\n\nconst metricsSchema = z.object({\n impressions: int64String.nullish(),\n clicks: int64String.nullish(),\n costMicros: int64String.nullish(),\n conversions: z.number().nullish(),\n conversionsValue: z.number().nullish(),\n historicalQualityScore: int64String.nullish(),\n});\n\nconst campaignRowSchema = z.object({\n campaign: campaignFieldsSchema,\n});\n\nconst campaignMetricRowSchema = z.object({\n segments: segmentsSchema,\n campaign: z.object({\n id: int64String,\n name: z.string().nullish(),\n resourceName: z.string().nullish(),\n }),\n metrics: metricsSchema,\n});\n\nconst adGroupMetricRowSchema = z.object({\n segments: segmentsSchema,\n campaign: z.object({ id: int64String }).nullish(),\n adGroup: z.object({\n id: int64String,\n name: z.string().nullish(),\n resourceName: z.string().nullish(),\n }),\n metrics: metricsSchema,\n});\n\nconst keywordMetricRowSchema = z.object({\n segments: segmentsSchema,\n adGroup: z.object({ id: int64String }).nullish(),\n adGroupCriterion: z.object({\n criterionId: int64String,\n keyword: z\n .object({\n text: z.string().nullish(),\n matchType: z.string().nullish(),\n })\n .nullish(),\n resourceName: z.string().nullish(),\n }),\n metrics: metricsSchema,\n});\n\nconst campaignsResponseSchema = z.array(campaignRowSchema);\nconst campaignMetricsResponseSchema = z.array(campaignMetricRowSchema);\nconst adGroupMetricsResponseSchema = z.array(adGroupMetricRowSchema);\nconst keywordMetricsResponseSchema = z.array(keywordMetricRowSchema);\n\ntype CampaignRow = z.infer<typeof campaignRowSchema>;\ntype CampaignMetricRow = z.infer<typeof campaignMetricRowSchema>;\ntype AdGroupMetricRow = z.infer<typeof adGroupMetricRowSchema>;\ntype KeywordMetricRow = z.infer<typeof keywordMetricRowSchema>;\n\nexport const googleAdsResources = defineResources({\n [ENTITY_TYPE_CAMPAIGN]: {\n shape: 'entity',\n filterable: [\n {\n field: 'status',\n ops: ['eq'],\n values: ['ENABLED', 'PAUSED', 'REMOVED'],\n },\n ],\n description:\n 'Google Ads campaigns with id, name, status, bidding strategy type, and start / end dates.',\n endpoint: 'POST /v18/customers/{customerId}/googleAds:search',\n fields: [\n { name: 'id', description: 'Numeric Google Ads campaign id.' },\n { name: 'name', description: 'Campaign display name.' },\n {\n name: 'status',\n description:\n 'Campaign status (ENABLED, PAUSED, REMOVED, UNKNOWN, UNSPECIFIED).',\n },\n {\n name: 'biddingStrategyType',\n description:\n 'Bidding strategy in use (e.g. MAXIMIZE_CONVERSIONS, MANUAL_CPC).',\n },\n { name: 'startDate', description: 'Campaign start date (YYYY-MM-DD).' },\n {\n name: 'endDate',\n description: 'Campaign end date (YYYY-MM-DD), if set.',\n },\n ],\n responses: {\n oauth_token: z.object({\n access_token: z.string().min(1),\n expires_in: z.number().int().positive().optional(),\n }),\n campaigns: campaignsResponseSchema,\n },\n },\n google_ads_campaign_metrics: {\n shape: 'metric',\n description:\n 'Daily campaign performance - impressions, clicks, cost, conversions, and conversion value per (date, campaignId).',\n endpoint: 'POST /v18/customers/{customerId}/googleAds:search',\n unit: 'USD',\n granularity: 'day',\n dimensions: [\n { name: 'date', description: 'Calendar day of the metric sample.' },\n { name: 'campaignId', description: 'Numeric Google Ads campaign id.' },\n {\n name: 'campaignName',\n description: 'Campaign display name at sync time.',\n },\n { name: 'impressions', description: 'Ad impressions served on the day.' },\n { name: 'clicks', description: 'Clicks recorded on the day.' },\n {\n name: 'cost',\n description:\n 'Cost in account currency units (cost_micros ÷ 1,000,000).',\n },\n {\n name: 'costMicros',\n description: 'Raw cost in micros, as returned by the API.',\n },\n {\n name: 'conversions',\n description: 'Counted conversions attributed to the day.',\n },\n {\n name: 'conversionsValue',\n description: 'Total value of conversions for the day.',\n },\n ],\n notes:\n '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).',\n responses: { campaign_metrics: campaignMetricsResponseSchema },\n },\n google_ads_ad_group_metrics: {\n shape: 'metric',\n description:\n 'Daily ad-group performance - impressions, clicks, cost, and conversions per (date, adGroupId).',\n endpoint: 'POST /v18/customers/{customerId}/googleAds:search',\n unit: 'USD',\n granularity: 'day',\n dimensions: [\n { name: 'date', description: 'Calendar day of the metric sample.' },\n { name: 'adGroupId', description: 'Numeric Google Ads ad-group id.' },\n {\n name: 'adGroupName',\n description: 'Ad-group display name at sync time.',\n },\n { name: 'campaignId', description: 'Parent campaign id.' },\n { name: 'impressions', description: 'Ad impressions served on the day.' },\n { name: 'clicks', description: 'Clicks recorded on the day.' },\n { name: 'cost', description: 'Cost in account currency units.' },\n {\n name: 'costMicros',\n description: 'Raw cost in micros, as returned by the API.',\n },\n {\n name: 'conversions',\n description: 'Counted conversions attributed to the day.',\n },\n ],\n responses: { ad_group_metrics: adGroupMetricsResponseSchema },\n },\n google_ads_keyword_metrics: {\n shape: 'metric',\n description:\n 'Daily keyword performance - impressions, clicks, cost, and historical quality score per (date, criterionId).',\n endpoint: 'POST /v18/customers/{customerId}/googleAds:search',\n unit: 'USD',\n granularity: 'day',\n dimensions: [\n { name: 'date', description: 'Calendar day of the metric sample.' },\n {\n name: 'criterionId',\n description: 'Numeric keyword (ad-group criterion) id.',\n },\n { name: 'keywordText', description: 'Keyword text.' },\n {\n name: 'matchType',\n description: 'Match type (EXACT, PHRASE, BROAD, …).',\n },\n { name: 'adGroupId', description: 'Parent ad-group id.' },\n { name: 'impressions', description: 'Ad impressions served on the day.' },\n { name: 'clicks', description: 'Clicks recorded on the day.' },\n { name: 'cost', description: 'Cost in account currency units.' },\n {\n name: 'costMicros',\n description: 'Raw cost in micros, as returned by the API.',\n },\n {\n name: 'qualityScore',\n description:\n 'Historical quality score for the day (1-10), null when no impressions.',\n },\n ],\n notes:\n 'Driven by `keyword_view`; the cost / impression columns roll up to the criterion-day pair.',\n responses: { keyword_metrics: keywordMetricsResponseSchema },\n },\n});\n\nfunction toDateString(date: Date): string {\n const y = date.getUTCFullYear();\n const m = String(date.getUTCMonth() + 1).padStart(2, '0');\n const d = String(date.getUTCDate()).padStart(2, '0');\n return `${y}-${m}-${d}`;\n}\n\nconst DATE_RE = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\nfunction dateStringToMs(yyyyMmDd: string): number {\n const m = DATE_RE.exec(yyyyMmDd);\n if (!m) {\n return 0;\n }\n const ms = Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));\n return Number.isFinite(ms) ? ms : 0;\n}\n\ninterface DateRange {\n startDate: string;\n endDate: string;\n}\n\nexport function getDateRange(\n options: SyncOptions,\n lookbackDays: number,\n now: number = Date.now(),\n): DateRange {\n const endDate = toDateString(new Date(now));\n if (options.mode === 'latest') {\n const startMs = now - (INCREMENTAL_LOOKBACK_DAYS - 1) * MS_PER_DAY;\n return { startDate: toDateString(new Date(startMs)), endDate };\n }\n if (options.since) {\n const sinceMs = new Date(options.since).getTime();\n if (Number.isFinite(sinceMs)) {\n const days = Math.max(1, Math.ceil((now - sinceMs) / MS_PER_DAY));\n const cappedDays = Math.min(days, lookbackDays);\n const startMs = now - (cappedDays - 1) * MS_PER_DAY;\n return { startDate: toDateString(new Date(startMs)), endDate };\n }\n }\n const startMs = now - (lookbackDays - 1) * MS_PER_DAY;\n return { startDate: toDateString(new Date(startMs)), endDate };\n}\n\nfunction coerceInt(value: unknown): number {\n if (typeof value === 'number') {\n return Number.isFinite(value) ? value : 0;\n }\n if (typeof value === 'string' && value !== '') {\n const n = Number(value);\n return Number.isFinite(n) ? n : 0;\n }\n return 0;\n}\n\nfunction coerceIntOrNull(value: unknown): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n const n = coerceInt(value);\n return n;\n}\n\nfunction microsToUnits(micros: unknown): number {\n return coerceInt(micros) / MICROS_PER_UNIT;\n}\n\nconst CAMPAIGN_STATUS_VALUES = new Set(['ENABLED', 'PAUSED', 'REMOVED']);\n\nfunction singleSpec(specs: FetchSpec[] | undefined): FetchSpec | undefined {\n return specs && specs.length === 1 ? specs[0] : undefined;\n}\n\nfunction gaqlStringLiteral(value: string): string {\n return `'${value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\")}'`;\n}\n\nfunction pushableEq(\n filter: FilterClause[] | undefined,\n field: string,\n): string | undefined {\n if (!filter) {\n return undefined;\n }\n for (const clause of filter) {\n if (!('field' in clause) || clause.field !== field || clause.op !== 'eq') {\n continue;\n }\n if (typeof clause.value === 'string') {\n return clause.value;\n }\n }\n return undefined;\n}\n\nfunction campaignsQuery(spec?: FetchSpec): string {\n const parts = [\n 'SELECT',\n ' campaign.id,',\n ' campaign.name,',\n ' campaign.status,',\n ' campaign.bidding_strategy_type,',\n ' campaign.start_date,',\n ' campaign.end_date',\n 'FROM campaign',\n ];\n const status = pushableEq(spec?.filter, 'status');\n if (status && CAMPAIGN_STATUS_VALUES.has(status)) {\n parts.push(`WHERE campaign.status = ${gaqlStringLiteral(status)}`);\n }\n return parts.join(' ');\n}\n\nfunction campaignMetricsQuery(range: DateRange): string {\n return [\n 'SELECT',\n ' segments.date,',\n ' campaign.id,',\n ' campaign.name,',\n ' metrics.impressions,',\n ' metrics.clicks,',\n ' metrics.cost_micros,',\n ' metrics.conversions,',\n ' metrics.conversions_value',\n 'FROM campaign',\n `WHERE segments.date BETWEEN '${range.startDate}' AND '${range.endDate}'`,\n ].join(' ');\n}\n\nfunction adGroupMetricsQuery(range: DateRange): string {\n return [\n 'SELECT',\n ' segments.date,',\n ' campaign.id,',\n ' ad_group.id,',\n ' ad_group.name,',\n ' metrics.impressions,',\n ' metrics.clicks,',\n ' metrics.cost_micros,',\n ' metrics.conversions',\n 'FROM ad_group',\n `WHERE segments.date BETWEEN '${range.startDate}' AND '${range.endDate}'`,\n ].join(' ');\n}\n\nfunction keywordMetricsQuery(range: DateRange): string {\n return [\n 'SELECT',\n ' segments.date,',\n ' ad_group.id,',\n ' ad_group_criterion.criterion_id,',\n ' ad_group_criterion.keyword.text,',\n ' ad_group_criterion.keyword.match_type,',\n ' metrics.impressions,',\n ' metrics.clicks,',\n ' metrics.cost_micros,',\n ' metrics.historical_quality_score',\n 'FROM keyword_view',\n `WHERE segments.date BETWEEN '${range.startDate}' AND '${range.endDate}'`,\n ].join(' ');\n}\n\nfunction queryForPhase(\n phase: GoogleAdsPhase,\n range: DateRange,\n campaignSpec?: FetchSpec,\n): string {\n switch (phase) {\n case 'campaigns':\n return campaignsQuery(campaignSpec);\n case 'campaign_metrics':\n return campaignMetricsQuery(range);\n case 'ad_group_metrics':\n return adGroupMetricsQuery(range);\n case 'keyword_metrics':\n return keywordMetricsQuery(range);\n }\n}\n\nexport function campaignToEntity(row: CampaignRow): {\n type: string;\n id: string;\n attributes: Record<string, string | number | null>;\n updated_at: number;\n} {\n const c = row.campaign;\n const startMs = c.startDate ? dateStringToMs(c.startDate) : 0;\n return {\n type: ENTITY_TYPE_CAMPAIGN,\n id: String(c.id),\n attributes: {\n name: c.name ?? null,\n status: c.status ?? null,\n biddingStrategyType: c.biddingStrategyType ?? null,\n startDate: c.startDate ?? null,\n endDate: c.endDate ?? null,\n resourceName: c.resourceName ?? null,\n },\n updated_at: startMs,\n };\n}\n\nexport function campaignMetricRowToSample(row: CampaignMetricRow): {\n name: string;\n ts: number;\n value: number;\n attributes: Record<string, string | number | null>;\n} {\n const m = row.metrics;\n const cost = microsToUnits(m.costMicros);\n return {\n name: METRIC_NAME.campaign_metrics,\n ts: dateStringToMs(row.segments.date),\n value: cost,\n attributes: {\n date: row.segments.date,\n campaignId: String(row.campaign.id),\n campaignName: row.campaign.name ?? null,\n impressions: coerceInt(m.impressions),\n clicks: coerceInt(m.clicks),\n cost,\n costMicros: coerceInt(m.costMicros),\n conversions: typeof m.conversions === 'number' ? m.conversions : 0,\n conversionsValue:\n typeof m.conversionsValue === 'number' ? m.conversionsValue : 0,\n },\n };\n}\n\nexport function adGroupMetricRowToSample(row: AdGroupMetricRow): {\n name: string;\n ts: number;\n value: number;\n attributes: Record<string, string | number | null>;\n} {\n const m = row.metrics;\n const cost = microsToUnits(m.costMicros);\n return {\n name: METRIC_NAME.ad_group_metrics,\n ts: dateStringToMs(row.segments.date),\n value: cost,\n attributes: {\n date: row.segments.date,\n adGroupId: String(row.adGroup.id),\n adGroupName: row.adGroup.name ?? null,\n campaignId: row.campaign?.id != null ? String(row.campaign.id) : null,\n impressions: coerceInt(m.impressions),\n clicks: coerceInt(m.clicks),\n cost,\n costMicros: coerceInt(m.costMicros),\n conversions: typeof m.conversions === 'number' ? m.conversions : 0,\n },\n };\n}\n\nexport function keywordMetricRowToSample(row: KeywordMetricRow): {\n name: string;\n ts: number;\n value: number;\n attributes: Record<string, string | number | null>;\n} {\n const m = row.metrics;\n const cost = microsToUnits(m.costMicros);\n return {\n name: METRIC_NAME.keyword_metrics,\n ts: dateStringToMs(row.segments.date),\n value: cost,\n attributes: {\n date: row.segments.date,\n criterionId: String(row.adGroupCriterion.criterionId),\n keywordText: row.adGroupCriterion.keyword?.text ?? null,\n matchType: row.adGroupCriterion.keyword?.matchType ?? null,\n adGroupId: row.adGroup?.id != null ? String(row.adGroup.id) : null,\n impressions: coerceInt(m.impressions),\n clicks: coerceInt(m.clicks),\n cost,\n costMicros: coerceInt(m.costMicros),\n qualityScore: coerceIntOrNull(m.historicalQualityScore),\n },\n };\n}\n\ninterface SearchResponse<TRow> {\n results?: TRow[];\n nextPageToken?: string;\n}\n\nexport const id = 'google-ads';\n\nexport class GoogleAdsConnector extends BaseConnector<\n GoogleAdsSettings,\n GoogleAdsCredentials\n> {\n static readonly id = id;\n\n static readonly resources = googleAdsResources;\n\n static readonly schemas = schemasFromResources(googleAdsResources);\n\n static create(input: unknown, ctx?: ConnectorContext): GoogleAdsConnector {\n const parsed = configFields.parse(input);\n return new GoogleAdsConnector(\n {\n customerId: parsed.customerId,\n loginCustomerId: parsed.loginCustomerId,\n lookbackDays: parsed.lookbackDays,\n resources: parsed.resources,\n },\n {\n clientId: parsed.clientId,\n clientSecret: parsed.clientSecret,\n refreshToken: parsed.refreshToken,\n developerToken: parsed.developerToken,\n },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = googleAdsCredentials;\n\n private tokenProvider?: GcpAccessTokenProvider;\n\n private getAccessToken(signal?: AbortSignal): Promise<string> {\n this.tokenProvider ??= new GcpAccessTokenProvider({\n connectorId: this.id,\n scope: 'https://www.googleapis.com/auth/adwords',\n getServiceAccountJson: () => undefined,\n getRefreshTokenCredentials: () => ({\n refreshToken: this.creds.refreshToken,\n clientId: this.creds.clientId,\n clientSecret: this.creds.clientSecret,\n }),\n post: (url, opts) =>\n this.post<{ access_token: string; expires_in?: number }>(url, opts),\n });\n return this.tokenProvider.getToken(signal);\n }\n\n private buildHeaders(accessToken: string): Record<string, string> {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n 'developer-token': this.creds.developerToken,\n 'User-Agent': connectorUserAgent('google-ads'),\n };\n if (this.settings.loginCustomerId) {\n headers['login-customer-id'] = this.settings.loginCustomerId;\n }\n return headers;\n }\n\n private async searchPage<TRow>(\n phase: GoogleAdsPhase,\n range: DateRange,\n pageToken: string | null,\n campaignSpec: FetchSpec | undefined,\n signal?: AbortSignal,\n ): Promise<{ items: TRow[]; next: string | null }> {\n const token = await this.getAccessToken(signal);\n const url = `https://googleads.googleapis.com/${API_VERSION}/customers/${this.settings.customerId}/googleAds:search`;\n const body: Record<string, unknown> = {\n query: queryForPhase(phase, range, campaignSpec),\n pageSize: PAGE_SIZE,\n };\n if (pageToken) {\n body.pageToken = pageToken;\n }\n const res = await this.post<SearchResponse<TRow>>(url, {\n resource: phase,\n headers: this.buildHeaders(token),\n body: JSON.stringify(body),\n signal,\n });\n return {\n items: res.body.results ?? [],\n next: res.body.nextPageToken ?? null,\n };\n }\n\n private async writePhase(\n phase: GoogleAdsPhase,\n items: unknown[],\n storage: StorageHandle,\n ): Promise<void> {\n switch (phase) {\n case 'campaigns': {\n for (const row of items as CampaignRow[]) {\n await storage.entity(campaignToEntity(row));\n }\n return;\n }\n case 'campaign_metrics': {\n const samples = (items as CampaignMetricRow[]).map(\n campaignMetricRowToSample,\n );\n await storage.metrics(samples, {\n names: [METRIC_NAME.campaign_metrics],\n });\n return;\n }\n case 'ad_group_metrics': {\n const samples = (items as AdGroupMetricRow[]).map(\n adGroupMetricRowToSample,\n );\n await storage.metrics(samples, {\n names: [METRIC_NAME.ad_group_metrics],\n });\n return;\n }\n case 'keyword_metrics': {\n const samples = (items as KeywordMetricRow[]).map(\n keywordMetricRowToSample,\n );\n await storage.metrics(samples, {\n names: [METRIC_NAME.keyword_metrics],\n });\n return;\n }\n }\n }\n\n private async clearScopeOnFirstPage(\n phase: GoogleAdsPhase,\n storage: StorageHandle,\n isFull: boolean,\n ): Promise<void> {\n if (phase === 'campaigns') {\n if (isFull) {\n await storage.entities([], { types: [ENTITY_TYPE_CAMPAIGN] });\n }\n return;\n }\n await storage.metrics([], { names: [METRIC_NAME[phase]] });\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const lookbackDays = this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;\n const range = getDateRange(options, lookbackDays);\n const isFull = options.mode === 'full';\n\n const phases = selectActivePhases<GoogleAdsResource, GoogleAdsPhase>(\n (r) => r,\n PHASE_ORDER,\n this.settings.resources,\n );\n\n const cursor = isGoogleAdsSyncCursor(options.cursor)\n ? options.cursor\n : undefined;\n\n const campaignSpec = singleSpec(options.fetchSpecs?.[ENTITY_TYPE_CAMPAIGN]);\n\n return paginateChunked<GoogleAdsPhase, string>({\n phases,\n cursor,\n signal,\n logger: this.logger,\n fetchPage: (phase, page, sig) =>\n this.searchPage<unknown>(phase, range, page, campaignSpec, sig),\n writeBatch: async (phase, items, page) => {\n if (page === null) {\n await this.clearScopeOnFirstPage(phase, storage, isFull);\n }\n await this.writePhase(phase, items, storage);\n },\n });\n }\n}\n","import { GoogleAdsConnector } from './google-ads';\n\nexport {\n adGroupMetricRowToSample,\n campaignMetricRowToSample,\n campaignToEntity,\n configFields,\n doc,\n getDateRange,\n GoogleAdsConnector,\n googleAdsResources as resources,\n id,\n keywordMetricRowToSample,\n} from './google-ads';\nexport type { GoogleAdsResource, GoogleAdsSettings } from './google-ads';\nexport default GoogleAdsConnector;\n"],"mappings":"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","names":["z","z","HTTP_CLIENT_VERSION","DEFAULT_USER_AGENT","z","startMs"]}
|
|
1
|
+
{"version":3,"sources":["../../gcp-shared/src/auth.ts","../../gcp-shared/src/config.ts","../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/map-concurrent.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../../gcp-shared/src/bigquery.ts","../../gcp-shared/src/access-token.ts","../../gcp-shared/src/dates.ts","../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/map-concurrent.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/google-ads.ts","../src/index.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport interface ServiceAccountKey {\n client_email: string;\n private_key: string;\n token_uri?: string;\n project_id?: string;\n}\n\nconst serviceAccountKeySchema = z.object({\n client_email: z.string().min(1),\n private_key: z.string().min(1),\n token_uri: z.string().url().optional(),\n project_id: z.string().optional(),\n});\n\nexport type ServiceAccountInput = string | Record<string, unknown>;\n\nexport interface TokenResponse {\n access_token: string;\n expires_in?: number;\n}\n\nexport const tokenResponseSchema = z.object({\n access_token: z.string().min(1),\n expires_in: z.number().int().positive().optional(),\n});\n\nfunction base64urlFromBytes(bytes: Uint8Array): string {\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n}\n\nfunction base64urlFromString(str: string): string {\n return base64urlFromBytes(new TextEncoder().encode(str));\n}\n\nasync function signRS256JWT(\n payload: Record<string, unknown>,\n privateKeyPem: string,\n): Promise<string> {\n const header = { alg: 'RS256', typ: 'JWT' };\n const headerB64 = base64urlFromString(JSON.stringify(header));\n const payloadB64 = base64urlFromString(JSON.stringify(payload));\n const signingInput = `${headerB64}.${payloadB64}`;\n\n const pemContent = privateKeyPem\n .replace(/-----BEGIN PRIVATE KEY-----/g, '')\n .replace(/-----END PRIVATE KEY-----/g, '')\n .replace(/\\s/g, '');\n const der = Uint8Array.from(atob(pemContent), (c) => c.charCodeAt(0));\n\n const key = await globalThis.crypto.subtle.importKey(\n 'pkcs8',\n der,\n { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n\n const signature = await globalThis.crypto.subtle.sign(\n 'RSASSA-PKCS1-v1_5',\n key,\n new TextEncoder().encode(signingInput),\n );\n\n return `${signingInput}.${base64urlFromBytes(new Uint8Array(signature))}`;\n}\n\nexport function parseServiceAccountJson(value: unknown): ServiceAccountKey {\n if (typeof value === 'object' && value !== null) {\n return serviceAccountKeySchema.parse(value);\n }\n if (typeof value !== 'string') {\n throw new Error(\n `serviceAccountJson must be a JSON object, raw JSON string, or base64-encoded JSON, but received ${typeof value}`,\n );\n }\n const trimmed = value.trim();\n if (trimmed.startsWith('{')) {\n return serviceAccountKeySchema.parse(JSON.parse(trimmed));\n }\n const binary = atob(trimmed);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n const decoded = new TextDecoder().decode(bytes);\n return serviceAccountKeySchema.parse(JSON.parse(decoded));\n}\n\nexport async function buildServiceAccountJwt(\n serviceAccountJson: ServiceAccountInput,\n scope: string,\n): Promise<{ url: string; body: string }> {\n const sa = parseServiceAccountJson(serviceAccountJson);\n const now = Math.floor(Date.now() / 1000);\n const jwt = await signRS256JWT(\n {\n iss: sa.client_email,\n scope,\n aud: sa.token_uri ?? 'https://oauth2.googleapis.com/token',\n exp: now + 3600,\n iat: now,\n },\n sa.private_key,\n );\n\n const body = new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion: jwt,\n }).toString();\n\n return {\n url: sa.token_uri ?? 'https://oauth2.googleapis.com/token',\n body,\n };\n}\n\nexport interface RefreshTokenCredentials {\n refreshToken: string;\n clientId: string;\n clientSecret: string;\n}\n\nexport function buildRefreshTokenGrant(credentials: RefreshTokenCredentials): {\n url: string;\n body: string;\n} {\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: credentials.refreshToken,\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n }).toString();\n\n return { url: 'https://oauth2.googleapis.com/token', body };\n}\n","import { z } from 'zod';\n\nexport const gcpAuthConfigShape = {\n serviceAccountJson: z.object({ $secret: z.string().trim().min(1) }).meta({\n label: 'Service Account JSON',\n description:\n 'Contents of the JSON key file for a Google service account with the role required by this connector. Create one at Google Cloud -> IAM & Admin -> Service Accounts and store the JSON as a secret.',\n secret: true,\n }),\n} as const;\n\nexport interface GcpAuthConfig {\n serviceAccountJson: { $secret: string };\n}\n","import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(\n res: Response,\n parseJson: boolean,\n binary: boolean,\n): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n if (binary) {\n return new Uint8Array(await res.arrayBuffer());\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n const binary = req.binary ?? false;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson, binary);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import { parseEpoch } from '@rawdash/connector-shared';\nimport { z } from 'zod';\n\nexport const BQ_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;\nexport const BQ_DATASET_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nexport const BQ_API_BASE = 'https://bigquery.googleapis.com/bigquery/v2';\nexport const BQ_READONLY_SCOPE =\n 'https://www.googleapis.com/auth/bigquery.readonly';\nexport const BQ_PAGE_SIZE = 10_000;\nexport const BQ_QUERY_TIMEOUT_MS = 30_000;\n\nexport const bqQueryResponseSchema = z.object({\n jobComplete: z.boolean().optional(),\n schema: z\n .object({\n fields: z.array(z.object({ name: z.string(), type: z.string() })),\n })\n .optional(),\n rows: z\n .array(\n z.object({\n f: z.array(z.object({ v: z.string().nullable().optional() })),\n }),\n )\n .optional(),\n pageToken: z.string().optional(),\n jobReference: z\n .object({\n projectId: z.string(),\n jobId: z.string(),\n location: z.string().optional(),\n })\n .optional(),\n});\n\nexport type BqQueryResponse = z.infer<typeof bqQueryResponseSchema>;\nexport type BqJobReference = NonNullable<BqQueryResponse['jobReference']>;\n\nexport type BqPageRequest =\n | { method: 'POST'; url: string; body: string }\n | { method: 'GET'; url: string };\n\nexport interface BqPageLogger {\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n}\n\nexport function buildBigQueryPageRequest(opts: {\n projectId: string;\n sql: string;\n pageToken: string | undefined;\n jobReference: BqJobReference | undefined;\n location?: string;\n pageSize?: number;\n timeoutMs?: number;\n}): BqPageRequest {\n const pageSize = opts.pageSize ?? BQ_PAGE_SIZE;\n const timeoutMs = opts.timeoutMs ?? BQ_QUERY_TIMEOUT_MS;\n\n if (opts.pageToken === undefined) {\n const url = `${BQ_API_BASE}/projects/${encodeURIComponent(\n opts.projectId,\n )}/queries`;\n const body: Record<string, unknown> = {\n query: opts.sql,\n useLegacySql: false,\n maxResults: pageSize,\n timeoutMs,\n };\n if (opts.location !== undefined) {\n body['location'] = opts.location;\n }\n return { method: 'POST', url, body: JSON.stringify(body) };\n }\n\n if (opts.jobReference === undefined) {\n throw new Error(\n 'cannot fetch the next page of BigQuery results without a jobReference',\n );\n }\n\n const params = new URLSearchParams({\n pageToken: opts.pageToken,\n maxResults: String(pageSize),\n timeoutMs: String(timeoutMs),\n });\n const location = opts.jobReference.location ?? opts.location;\n if (location !== undefined) {\n params.set('location', location);\n }\n const url = `${BQ_API_BASE}/projects/${encodeURIComponent(\n opts.jobReference.projectId,\n )}/queries/${encodeURIComponent(opts.jobReference.jobId)}?${params.toString()}`;\n return { method: 'GET', url };\n}\n\nexport async function collectBigQueryPages<T>(opts: {\n projectId: string;\n sql: string;\n resource: string;\n fetchPage: (\n request: BqPageRequest,\n signal: AbortSignal | undefined,\n ) => Promise<BqQueryResponse>;\n mapRows: (response: BqQueryResponse) => T[];\n jobIncompleteMessage: string;\n location?: string;\n pageSize?: number;\n signal?: AbortSignal;\n logger?: BqPageLogger;\n}): Promise<{ rows: T[]; aborted: boolean }> {\n const rows: T[] = [];\n let pageToken: string | undefined;\n let jobReference: BqJobReference | undefined;\n let page = 0;\n const phaseStart = Date.now();\n\n do {\n if (opts.signal?.aborted) {\n return { rows, aborted: true };\n }\n const request = buildBigQueryPageRequest({\n projectId: opts.projectId,\n sql: opts.sql,\n pageToken,\n jobReference,\n location: opts.location,\n pageSize: opts.pageSize,\n });\n let response: BqQueryResponse;\n try {\n response = await opts.fetchPage(request, opts.signal);\n } catch (err) {\n opts.logger?.warn('fetch page failed', {\n resource: opts.resource,\n page: page + 1,\n error: err instanceof Error ? err.message : String(err),\n });\n throw err;\n }\n if (response.jobComplete === false) {\n throw new Error(opts.jobIncompleteMessage);\n }\n if (response.jobReference !== undefined) {\n jobReference = response.jobReference;\n }\n const pageRows = opts.mapRows(response);\n rows.push(...pageRows);\n pageToken =\n typeof response.pageToken === 'string' && response.pageToken.length > 0\n ? response.pageToken\n : undefined;\n page += 1;\n opts.logger?.info('fetched page', {\n resource: opts.resource,\n page,\n items: pageRows.length,\n next: pageToken ?? null,\n });\n } while (pageToken !== undefined);\n\n opts.logger?.info('resource done', {\n resource: opts.resource,\n pages: page,\n items: rows.length,\n duration_ms: Date.now() - phaseStart,\n });\n return { rows, aborted: false };\n}\n\nexport function indexBqFields(\n response: BqQueryResponse,\n): Record<string, number> {\n const fieldIndex: Record<string, number> = {};\n (response.schema?.fields ?? []).forEach((field, idx) => {\n fieldIndex[field.name] = idx;\n });\n return fieldIndex;\n}\n\nexport function readBqCell(\n cells: ReadonlyArray<{ v?: string | null }>,\n fieldIndex: Record<string, number>,\n name: string,\n): string | null {\n const idx = fieldIndex[name];\n if (idx === undefined) {\n return null;\n }\n const raw = cells[idx]?.v;\n if (raw === undefined || raw === null) {\n return null;\n }\n return raw;\n}\n\nexport function parseBqDateOrEpoch(value: string): number | null {\n const dateMatch = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value);\n if (dateMatch) {\n return Date.UTC(\n Number(dateMatch[1]),\n Number(dateMatch[2]) - 1,\n Number(dateMatch[3]),\n );\n }\n return parseEpoch(value, 'iso');\n}\n","import { AuthError } from '@rawdash/connector-shared';\n\nimport {\n type RefreshTokenCredentials,\n type ServiceAccountInput,\n buildRefreshTokenGrant,\n buildServiceAccountJwt,\n} from './auth';\n\ninterface GcpTokenResponse {\n access_token: string;\n expires_in?: number;\n}\n\nexport type GcpTokenPoster = (\n url: string,\n opts: {\n resource: string;\n headers: Record<string, string>;\n body: string;\n signal?: AbortSignal;\n },\n) => Promise<{ body: GcpTokenResponse }>;\n\nexport class GcpAccessTokenProvider {\n private cached: { token: string; expiresAt: number } | null = null;\n\n constructor(\n private readonly opts: {\n connectorId: string;\n scope: string;\n getServiceAccountJson: () => ServiceAccountInput | undefined;\n getRefreshTokenCredentials?: () => RefreshTokenCredentials | undefined;\n post: GcpTokenPoster;\n },\n ) {}\n\n private async resolveGrant(): Promise<{ url: string; body: string }> {\n const serviceAccountJson = this.opts.getServiceAccountJson();\n if (serviceAccountJson) {\n return buildServiceAccountJwt(serviceAccountJson, this.opts.scope);\n }\n const refreshTokenCredentials = this.opts.getRefreshTokenCredentials?.();\n if (refreshTokenCredentials) {\n return buildRefreshTokenGrant(refreshTokenCredentials);\n }\n throw new AuthError(\n `${this.opts.connectorId}: missing serviceAccountJson or refresh-token credentials`,\n );\n }\n\n async getToken(signal?: AbortSignal): Promise<string> {\n if (this.cached && Date.now() < this.cached.expiresAt) {\n return this.cached.token;\n }\n const { url, body } = await this.resolveGrant();\n const res = await this.opts.post(url, {\n resource: 'oauth_token',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body,\n signal,\n });\n const expiresIn = res.body.expires_in ?? 3600;\n this.cached = {\n token: res.body.access_token,\n expiresAt: Date.now() + (expiresIn - 60) * 1000,\n };\n return this.cached.token;\n }\n}\n","export const MS_PER_DAY = 86_400_000;\n\nfunction pad2(n: number): string {\n return String(n).padStart(2, '0');\n}\n\nexport function toDateStr(ms: number): string {\n const d = new Date(ms);\n return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;\n}\n\nexport function startOfUtcDay(ms: number): number {\n return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;\n}\n","import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(\n res: Response,\n parseJson: boolean,\n binary: boolean,\n): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n if (binary) {\n return new Uint8Array(await res.arrayBuffer());\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n const binary = req.binary ?? false;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson, binary);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import { GcpAccessTokenProvider } from '@rawdash/connector-gcp-shared';\nimport { connectorUserAgent } from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type FetchSpec,\n type FilterClause,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n makeChunkedCursorGuard,\n paginateChunked,\n schemasFromResources,\n selectActivePhases,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\nexport const configFields = defineConfigFields(\n z.object({\n customerId: z\n .string()\n .trim()\n .regex(\n /^\\d{10}$/,\n 'customerId must be the 10-digit Google Ads ID, digits only (no dashes)',\n )\n .meta({\n label: 'Customer ID',\n description:\n 'Google Ads customer ID for the account to sync, digits only (the dashed form 123-456-7890 with the dashes removed).',\n placeholder: '1234567890',\n }),\n loginCustomerId: z\n .string()\n .trim()\n .regex(\n /^\\d{10}$/,\n 'loginCustomerId must be a 10-digit Google Ads ID, digits only',\n )\n .optional()\n .meta({\n label: 'Login Customer ID (MCC)',\n description:\n 'Manager (MCC) account ID, digits only. Set this when the OAuth credential authenticates against an MCC that owns the customer account.',\n placeholder: '1234567890',\n }),\n clientId: z.string().min(1).meta({\n label: 'OAuth Client ID',\n description:\n 'OAuth 2.0 client ID from a Google Cloud project that has the Google Ads API enabled.',\n placeholder: '…apps.googleusercontent.com',\n }),\n clientSecret: z.object({ $secret: z.string() }).meta({\n label: 'OAuth Client Secret',\n description: 'OAuth 2.0 client secret paired with the client ID above.',\n secret: true,\n }),\n refreshToken: z.object({ $secret: z.string() }).meta({\n label: 'OAuth Refresh Token',\n description:\n 'Google OAuth 2.0 refresh token issued for the https://www.googleapis.com/auth/adwords scope.',\n secret: true,\n }),\n developerToken: z.object({ $secret: z.string() }).meta({\n label: 'Developer Token',\n description:\n 'Google Ads API developer token from the manager account that owns API access (Tools → API Center).',\n secret: true,\n }),\n lookbackDays: z.number().int().positive().optional().meta({\n label: 'Lookback days (full sync)',\n description:\n 'How many calendar days of metric history to fetch on a full sync. Defaults to 90.',\n placeholder: '90',\n }),\n resources: z\n .array(\n z.enum([\n 'campaigns',\n 'campaign_metrics',\n 'ad_group_metrics',\n 'keyword_metrics',\n ]),\n )\n .nonempty()\n .optional()\n .meta({\n label: 'Resources',\n description:\n 'Which Google Ads resources to sync. Omit to sync everything; pin a subset to avoid pulling keyword-level metrics on a quota-limited token.',\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Google Ads',\n category: 'marketing',\n brandColor: '#4285F4',\n tagline:\n 'Sync Google Ads campaigns plus daily campaign, ad-group, and keyword performance (impressions, clicks, cost, conversions) via GAQL.',\n vendor: {\n name: 'Google Ads',\n domain: 'ads.google.com',\n apiDocs: 'https://developers.google.com/google-ads/api/docs/start',\n website: 'https://ads.google.com',\n },\n auth: {\n summary:\n '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.',\n setup: [\n 'Apply for Google Ads API access from your manager account (Tools → API Center). Copy the developer token - it lives on the manager, not the child account.',\n '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.',\n '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).',\n '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.',\n '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\")`.',\n ],\n },\n rateLimit:\n '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.',\n limitations: [\n 'Cost values are stored in account currency units (cost_micros ÷ 1,000,000); the original micro-precision integer is also exposed in attributes.',\n '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.',\n 'Incremental syncs trail the last 3 days because Google Ads can attribute conversions to a click up to 3 days after the event.',\n 'Audience-, asset-, and recommendation-level reporting are out of scope; this connector covers campaign / ad-group / keyword performance only.',\n ],\n});\n\nexport interface GoogleAdsSettings {\n customerId: string;\n loginCustomerId?: string;\n lookbackDays?: number;\n resources?: readonly GoogleAdsResource[];\n}\n\nconst googleAdsCredentials = {\n clientId: {\n description: 'Google OAuth 2.0 client ID (public, not a secret)',\n auth: 'required' as const,\n },\n clientSecret: {\n description: 'Google OAuth 2.0 client secret',\n auth: 'required' as const,\n },\n refreshToken: {\n description: 'Google OAuth 2.0 refresh token with the adwords scope',\n auth: 'required' as const,\n },\n developerToken: {\n description: 'Google Ads API developer token',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype GoogleAdsCredentials = typeof googleAdsCredentials;\n\nconst PHASE_ORDER = [\n 'campaigns',\n 'campaign_metrics',\n 'ad_group_metrics',\n 'keyword_metrics',\n] as const;\n\ntype GoogleAdsPhase = (typeof PHASE_ORDER)[number];\n\nexport type GoogleAdsResource = GoogleAdsPhase;\n\nconst isGoogleAdsSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);\n\nconst API_VERSION = 'v24';\nconst PAGE_SIZE = 10_000;\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\nconst DEFAULT_LOOKBACK_DAYS = 90;\nconst INCREMENTAL_LOOKBACK_DAYS = 3;\nconst MICROS_PER_UNIT = 1_000_000;\n\nconst ENTITY_TYPE_CAMPAIGN = 'google_ads_campaign';\nconst METRIC_NAME: Record<GoogleAdsPhase, string> = {\n campaigns: ENTITY_TYPE_CAMPAIGN,\n campaign_metrics: 'google_ads_campaign_metrics',\n ad_group_metrics: 'google_ads_ad_group_metrics',\n keyword_metrics: 'google_ads_keyword_metrics',\n};\n\nconst int64String = z.union([z.string().min(1), z.number()]);\n\nconst segmentsSchema = z.object({\n date: z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/),\n});\n\nconst dateString = z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/);\n\nconst campaignFieldsSchema = z.object({\n id: int64String,\n name: z.string().nullish(),\n status: z.string().nullish(),\n biddingStrategyType: z.string().nullish(),\n startDate: dateString.nullish(),\n endDate: dateString.nullish(),\n resourceName: z.string().nullish(),\n});\n\nconst metricsSchema = z.object({\n impressions: int64String.nullish(),\n clicks: int64String.nullish(),\n costMicros: int64String.nullish(),\n conversions: z.number().nullish(),\n conversionsValue: z.number().nullish(),\n historicalQualityScore: int64String.nullish(),\n});\n\nconst campaignRowSchema = z.object({\n campaign: campaignFieldsSchema,\n});\n\nconst campaignMetricRowSchema = z.object({\n segments: segmentsSchema,\n campaign: z.object({\n id: int64String,\n name: z.string().nullish(),\n resourceName: z.string().nullish(),\n }),\n metrics: metricsSchema,\n});\n\nconst adGroupMetricRowSchema = z.object({\n segments: segmentsSchema,\n campaign: z.object({ id: int64String }).nullish(),\n adGroup: z.object({\n id: int64String,\n name: z.string().nullish(),\n resourceName: z.string().nullish(),\n }),\n metrics: metricsSchema,\n});\n\nconst keywordMetricRowSchema = z.object({\n segments: segmentsSchema,\n adGroup: z.object({ id: int64String }).nullish(),\n adGroupCriterion: z.object({\n criterionId: int64String,\n keyword: z\n .object({\n text: z.string().nullish(),\n matchType: z.string().nullish(),\n })\n .nullish(),\n resourceName: z.string().nullish(),\n }),\n metrics: metricsSchema,\n});\n\nconst campaignsResponseSchema = z.array(campaignRowSchema);\nconst campaignMetricsResponseSchema = z.array(campaignMetricRowSchema);\nconst adGroupMetricsResponseSchema = z.array(adGroupMetricRowSchema);\nconst keywordMetricsResponseSchema = z.array(keywordMetricRowSchema);\n\ntype CampaignRow = z.infer<typeof campaignRowSchema>;\ntype CampaignMetricRow = z.infer<typeof campaignMetricRowSchema>;\ntype AdGroupMetricRow = z.infer<typeof adGroupMetricRowSchema>;\ntype KeywordMetricRow = z.infer<typeof keywordMetricRowSchema>;\n\nexport const googleAdsResources = defineResources({\n [ENTITY_TYPE_CAMPAIGN]: {\n shape: 'entity',\n filterable: [\n {\n field: 'status',\n ops: ['eq'],\n values: ['ENABLED', 'PAUSED', 'REMOVED'],\n },\n ],\n description:\n 'Google Ads campaigns with id, name, status, bidding strategy type, and start / end dates.',\n endpoint: 'POST /v24/customers/{customerId}/googleAds:search',\n fields: [\n { name: 'id', description: 'Numeric Google Ads campaign id.' },\n { name: 'name', description: 'Campaign display name.' },\n {\n name: 'status',\n description:\n 'Campaign status (ENABLED, PAUSED, REMOVED, UNKNOWN, UNSPECIFIED).',\n },\n {\n name: 'biddingStrategyType',\n description:\n 'Bidding strategy in use (e.g. MAXIMIZE_CONVERSIONS, MANUAL_CPC).',\n },\n { name: 'startDate', description: 'Campaign start date (YYYY-MM-DD).' },\n {\n name: 'endDate',\n description: 'Campaign end date (YYYY-MM-DD), if set.',\n },\n ],\n responses: {\n oauth_token: z.object({\n access_token: z.string().min(1),\n expires_in: z.number().int().positive().optional(),\n }),\n campaigns: campaignsResponseSchema,\n },\n },\n google_ads_campaign_metrics: {\n shape: 'metric',\n description:\n 'Daily campaign performance - impressions, clicks, cost, conversions, and conversion value per (date, campaignId).',\n endpoint: 'POST /v24/customers/{customerId}/googleAds:search',\n unit: 'USD',\n granularity: 'day',\n dimensions: [\n { name: 'date', description: 'Calendar day of the metric sample.' },\n { name: 'campaignId', description: 'Numeric Google Ads campaign id.' },\n {\n name: 'campaignName',\n description: 'Campaign display name at sync time.',\n },\n { name: 'impressions', description: 'Ad impressions served on the day.' },\n { name: 'clicks', description: 'Clicks recorded on the day.' },\n {\n name: 'cost',\n description:\n 'Cost in account currency units (cost_micros ÷ 1,000,000).',\n },\n {\n name: 'costMicros',\n description: 'Raw cost in micros, as returned by the API.',\n },\n {\n name: 'conversions',\n description: 'Counted conversions attributed to the day.',\n },\n {\n name: 'conversionsValue',\n description: 'Total value of conversions for the day.',\n },\n ],\n notes:\n '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).',\n responses: { campaign_metrics: campaignMetricsResponseSchema },\n },\n google_ads_ad_group_metrics: {\n shape: 'metric',\n description:\n 'Daily ad-group performance - impressions, clicks, cost, and conversions per (date, adGroupId).',\n endpoint: 'POST /v24/customers/{customerId}/googleAds:search',\n unit: 'USD',\n granularity: 'day',\n dimensions: [\n { name: 'date', description: 'Calendar day of the metric sample.' },\n { name: 'adGroupId', description: 'Numeric Google Ads ad-group id.' },\n {\n name: 'adGroupName',\n description: 'Ad-group display name at sync time.',\n },\n { name: 'campaignId', description: 'Parent campaign id.' },\n { name: 'impressions', description: 'Ad impressions served on the day.' },\n { name: 'clicks', description: 'Clicks recorded on the day.' },\n { name: 'cost', description: 'Cost in account currency units.' },\n {\n name: 'costMicros',\n description: 'Raw cost in micros, as returned by the API.',\n },\n {\n name: 'conversions',\n description: 'Counted conversions attributed to the day.',\n },\n ],\n responses: { ad_group_metrics: adGroupMetricsResponseSchema },\n },\n google_ads_keyword_metrics: {\n shape: 'metric',\n description:\n 'Daily keyword performance - impressions, clicks, cost, and historical quality score per (date, criterionId).',\n endpoint: 'POST /v24/customers/{customerId}/googleAds:search',\n unit: 'USD',\n granularity: 'day',\n dimensions: [\n { name: 'date', description: 'Calendar day of the metric sample.' },\n {\n name: 'criterionId',\n description: 'Numeric keyword (ad-group criterion) id.',\n },\n { name: 'keywordText', description: 'Keyword text.' },\n {\n name: 'matchType',\n description: 'Match type (EXACT, PHRASE, BROAD, …).',\n },\n { name: 'adGroupId', description: 'Parent ad-group id.' },\n { name: 'impressions', description: 'Ad impressions served on the day.' },\n { name: 'clicks', description: 'Clicks recorded on the day.' },\n { name: 'cost', description: 'Cost in account currency units.' },\n {\n name: 'costMicros',\n description: 'Raw cost in micros, as returned by the API.',\n },\n {\n name: 'qualityScore',\n description:\n 'Historical quality score for the day (1-10), null when no impressions.',\n },\n ],\n notes:\n 'Driven by `keyword_view`; the cost / impression columns roll up to the criterion-day pair.',\n responses: { keyword_metrics: keywordMetricsResponseSchema },\n },\n});\n\nfunction toDateString(date: Date): string {\n const y = date.getUTCFullYear();\n const m = String(date.getUTCMonth() + 1).padStart(2, '0');\n const d = String(date.getUTCDate()).padStart(2, '0');\n return `${y}-${m}-${d}`;\n}\n\nconst DATE_RE = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\nfunction dateStringToMs(yyyyMmDd: string): number {\n const m = DATE_RE.exec(yyyyMmDd);\n if (!m) {\n return 0;\n }\n const ms = Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));\n return Number.isFinite(ms) ? ms : 0;\n}\n\ninterface DateRange {\n startDate: string;\n endDate: string;\n}\n\nfunction dateRangeToReplaceWindow(\n range: DateRange,\n): { start: number; end: number } | undefined {\n const start = dateStringToMs(range.startDate);\n const end = dateStringToMs(range.endDate) + MS_PER_DAY - 1;\n if (start > end) {\n return undefined;\n }\n return { start, end };\n}\n\nexport function getDateRange(\n options: SyncOptions,\n lookbackDays: number,\n now: number = Date.now(),\n): DateRange {\n const endDate = toDateString(new Date(now));\n if (options.mode === 'latest') {\n const startMs = now - (INCREMENTAL_LOOKBACK_DAYS - 1) * MS_PER_DAY;\n return { startDate: toDateString(new Date(startMs)), endDate };\n }\n if (options.since) {\n const sinceMs = new Date(options.since).getTime();\n if (Number.isFinite(sinceMs)) {\n const days = Math.max(1, Math.ceil((now - sinceMs) / MS_PER_DAY));\n const cappedDays = Math.min(days, lookbackDays);\n const startMs = now - (cappedDays - 1) * MS_PER_DAY;\n return { startDate: toDateString(new Date(startMs)), endDate };\n }\n }\n const startMs = now - (lookbackDays - 1) * MS_PER_DAY;\n return { startDate: toDateString(new Date(startMs)), endDate };\n}\n\nfunction coerceInt(value: unknown): number {\n if (typeof value === 'number') {\n return Number.isFinite(value) ? value : 0;\n }\n if (typeof value === 'string' && value !== '') {\n const n = Number(value);\n return Number.isFinite(n) ? n : 0;\n }\n return 0;\n}\n\nfunction coerceIntOrNull(value: unknown): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n const n = coerceInt(value);\n return n;\n}\n\nfunction microsToUnits(micros: unknown): number {\n return coerceInt(micros) / MICROS_PER_UNIT;\n}\n\nconst CAMPAIGN_STATUS_VALUES = new Set(['ENABLED', 'PAUSED', 'REMOVED']);\n\nfunction singleSpec(specs: FetchSpec[] | undefined): FetchSpec | undefined {\n return specs && specs.length === 1 ? specs[0] : undefined;\n}\n\nfunction gaqlStringLiteral(value: string): string {\n return `'${value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\")}'`;\n}\n\nfunction pushableEq(\n filter: FilterClause[] | undefined,\n field: string,\n): string | undefined {\n if (!filter) {\n return undefined;\n }\n for (const clause of filter) {\n if (!('field' in clause) || clause.field !== field || clause.op !== 'eq') {\n continue;\n }\n if (typeof clause.value === 'string') {\n return clause.value;\n }\n }\n return undefined;\n}\n\nfunction campaignsQuery(spec?: FetchSpec): string {\n const parts = [\n 'SELECT',\n ' campaign.id,',\n ' campaign.name,',\n ' campaign.status,',\n ' campaign.bidding_strategy_type,',\n ' campaign.start_date,',\n ' campaign.end_date',\n 'FROM campaign',\n ];\n const status = pushableEq(spec?.filter, 'status');\n if (status && CAMPAIGN_STATUS_VALUES.has(status)) {\n parts.push(`WHERE campaign.status = ${gaqlStringLiteral(status)}`);\n }\n return parts.join(' ');\n}\n\nfunction campaignMetricsQuery(range: DateRange): string {\n return [\n 'SELECT',\n ' segments.date,',\n ' campaign.id,',\n ' campaign.name,',\n ' metrics.impressions,',\n ' metrics.clicks,',\n ' metrics.cost_micros,',\n ' metrics.conversions,',\n ' metrics.conversions_value',\n 'FROM campaign',\n `WHERE segments.date BETWEEN '${range.startDate}' AND '${range.endDate}'`,\n ].join(' ');\n}\n\nfunction adGroupMetricsQuery(range: DateRange): string {\n return [\n 'SELECT',\n ' segments.date,',\n ' campaign.id,',\n ' ad_group.id,',\n ' ad_group.name,',\n ' metrics.impressions,',\n ' metrics.clicks,',\n ' metrics.cost_micros,',\n ' metrics.conversions',\n 'FROM ad_group',\n `WHERE segments.date BETWEEN '${range.startDate}' AND '${range.endDate}'`,\n ].join(' ');\n}\n\nfunction keywordMetricsQuery(range: DateRange): string {\n return [\n 'SELECT',\n ' segments.date,',\n ' ad_group.id,',\n ' ad_group_criterion.criterion_id,',\n ' ad_group_criterion.keyword.text,',\n ' ad_group_criterion.keyword.match_type,',\n ' metrics.impressions,',\n ' metrics.clicks,',\n ' metrics.cost_micros,',\n ' metrics.historical_quality_score',\n 'FROM keyword_view',\n `WHERE segments.date BETWEEN '${range.startDate}' AND '${range.endDate}'`,\n ].join(' ');\n}\n\nfunction queryForPhase(\n phase: GoogleAdsPhase,\n range: DateRange,\n campaignSpec?: FetchSpec,\n): string {\n switch (phase) {\n case 'campaigns':\n return campaignsQuery(campaignSpec);\n case 'campaign_metrics':\n return campaignMetricsQuery(range);\n case 'ad_group_metrics':\n return adGroupMetricsQuery(range);\n case 'keyword_metrics':\n return keywordMetricsQuery(range);\n }\n}\n\nexport function campaignToEntity(row: CampaignRow): {\n type: string;\n id: string;\n attributes: Record<string, string | number | null>;\n updated_at: number;\n} {\n const c = row.campaign;\n const startMs = c.startDate ? dateStringToMs(c.startDate) : 0;\n return {\n type: ENTITY_TYPE_CAMPAIGN,\n id: String(c.id),\n attributes: {\n name: c.name ?? null,\n status: c.status ?? null,\n biddingStrategyType: c.biddingStrategyType ?? null,\n startDate: c.startDate ?? null,\n endDate: c.endDate ?? null,\n resourceName: c.resourceName ?? null,\n },\n updated_at: startMs,\n };\n}\n\nexport function campaignMetricRowToSample(row: CampaignMetricRow): {\n name: string;\n ts: number;\n value: number;\n attributes: Record<string, string | number | null>;\n} {\n const m = row.metrics;\n const cost = microsToUnits(m.costMicros);\n return {\n name: METRIC_NAME.campaign_metrics,\n ts: dateStringToMs(row.segments.date),\n value: cost,\n attributes: {\n date: row.segments.date,\n campaignId: String(row.campaign.id),\n campaignName: row.campaign.name ?? null,\n impressions: coerceInt(m.impressions),\n clicks: coerceInt(m.clicks),\n cost,\n costMicros: coerceInt(m.costMicros),\n conversions: typeof m.conversions === 'number' ? m.conversions : 0,\n conversionsValue:\n typeof m.conversionsValue === 'number' ? m.conversionsValue : 0,\n },\n };\n}\n\nexport function adGroupMetricRowToSample(row: AdGroupMetricRow): {\n name: string;\n ts: number;\n value: number;\n attributes: Record<string, string | number | null>;\n} {\n const m = row.metrics;\n const cost = microsToUnits(m.costMicros);\n return {\n name: METRIC_NAME.ad_group_metrics,\n ts: dateStringToMs(row.segments.date),\n value: cost,\n attributes: {\n date: row.segments.date,\n adGroupId: String(row.adGroup.id),\n adGroupName: row.adGroup.name ?? null,\n campaignId: row.campaign?.id != null ? String(row.campaign.id) : null,\n impressions: coerceInt(m.impressions),\n clicks: coerceInt(m.clicks),\n cost,\n costMicros: coerceInt(m.costMicros),\n conversions: typeof m.conversions === 'number' ? m.conversions : 0,\n },\n };\n}\n\nexport function keywordMetricRowToSample(row: KeywordMetricRow): {\n name: string;\n ts: number;\n value: number;\n attributes: Record<string, string | number | null>;\n} {\n const m = row.metrics;\n const cost = microsToUnits(m.costMicros);\n return {\n name: METRIC_NAME.keyword_metrics,\n ts: dateStringToMs(row.segments.date),\n value: cost,\n attributes: {\n date: row.segments.date,\n criterionId: String(row.adGroupCriterion.criterionId),\n keywordText: row.adGroupCriterion.keyword?.text ?? null,\n matchType: row.adGroupCriterion.keyword?.matchType ?? null,\n adGroupId: row.adGroup?.id != null ? String(row.adGroup.id) : null,\n impressions: coerceInt(m.impressions),\n clicks: coerceInt(m.clicks),\n cost,\n costMicros: coerceInt(m.costMicros),\n qualityScore: coerceIntOrNull(m.historicalQualityScore),\n },\n };\n}\n\ninterface SearchResponse<TRow> {\n results?: TRow[];\n nextPageToken?: string;\n}\n\nexport const id = 'google-ads';\n\nexport class GoogleAdsConnector extends BaseConnector<\n GoogleAdsSettings,\n GoogleAdsCredentials\n> {\n static readonly id = id;\n\n static readonly resources = googleAdsResources;\n\n static readonly schemas = schemasFromResources(googleAdsResources);\n\n static create(input: unknown, ctx?: ConnectorContext): GoogleAdsConnector {\n const parsed = configFields.parse(input);\n return new GoogleAdsConnector(\n {\n customerId: parsed.customerId,\n loginCustomerId: parsed.loginCustomerId,\n lookbackDays: parsed.lookbackDays,\n resources: parsed.resources,\n },\n {\n clientId: parsed.clientId,\n clientSecret: parsed.clientSecret,\n refreshToken: parsed.refreshToken,\n developerToken: parsed.developerToken,\n },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = googleAdsCredentials;\n\n private tokenProvider?: GcpAccessTokenProvider;\n\n private getAccessToken(signal?: AbortSignal): Promise<string> {\n this.tokenProvider ??= new GcpAccessTokenProvider({\n connectorId: this.id,\n scope: 'https://www.googleapis.com/auth/adwords',\n getServiceAccountJson: () => undefined,\n getRefreshTokenCredentials: () => ({\n refreshToken: this.creds.refreshToken,\n clientId: this.creds.clientId,\n clientSecret: this.creds.clientSecret,\n }),\n post: (url, opts) =>\n this.post<{ access_token: string; expires_in?: number }>(url, opts),\n });\n return this.tokenProvider.getToken(signal);\n }\n\n private buildHeaders(accessToken: string): Record<string, string> {\n const headers: Record<string, string> = {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n 'developer-token': this.creds.developerToken,\n 'User-Agent': connectorUserAgent('google-ads'),\n };\n if (this.settings.loginCustomerId) {\n headers['login-customer-id'] = this.settings.loginCustomerId;\n }\n return headers;\n }\n\n private async searchPage<TRow>(\n phase: GoogleAdsPhase,\n range: DateRange,\n pageToken: string | null,\n campaignSpec: FetchSpec | undefined,\n signal?: AbortSignal,\n ): Promise<{ items: TRow[]; next: string | null }> {\n const token = await this.getAccessToken(signal);\n const url = `https://googleads.googleapis.com/${API_VERSION}/customers/${this.settings.customerId}/googleAds:search`;\n const body: Record<string, unknown> = {\n query: queryForPhase(phase, range, campaignSpec),\n pageSize: PAGE_SIZE,\n };\n if (pageToken) {\n body.pageToken = pageToken;\n }\n const res = await this.post<SearchResponse<TRow>>(url, {\n resource: phase,\n headers: this.buildHeaders(token),\n body: JSON.stringify(body),\n signal,\n });\n return {\n items: res.body.results ?? [],\n next: res.body.nextPageToken ?? null,\n };\n }\n\n private async writePhase(\n phase: GoogleAdsPhase,\n items: unknown[],\n storage: StorageHandle,\n ): Promise<void> {\n switch (phase) {\n case 'campaigns': {\n for (const row of items as CampaignRow[]) {\n await storage.entity(campaignToEntity(row));\n }\n return;\n }\n case 'campaign_metrics': {\n for (const row of items as CampaignMetricRow[]) {\n await storage.metric(campaignMetricRowToSample(row));\n }\n return;\n }\n case 'ad_group_metrics': {\n for (const row of items as AdGroupMetricRow[]) {\n await storage.metric(adGroupMetricRowToSample(row));\n }\n return;\n }\n case 'keyword_metrics': {\n for (const row of items as KeywordMetricRow[]) {\n await storage.metric(keywordMetricRowToSample(row));\n }\n return;\n }\n }\n }\n\n private async clearScopeOnFirstPage(\n phase: GoogleAdsPhase,\n storage: StorageHandle,\n isFull: boolean,\n replaceWindow: { start: number; end: number } | undefined,\n ): Promise<void> {\n if (phase === 'campaigns') {\n if (isFull) {\n await storage.entities([], { types: [ENTITY_TYPE_CAMPAIGN] });\n }\n return;\n }\n await storage.metrics([], {\n names: [METRIC_NAME[phase]],\n ...(replaceWindow ? { replaceWindow } : {}),\n });\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const lookbackDays = this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;\n const range = getDateRange(options, lookbackDays);\n const replaceWindow = dateRangeToReplaceWindow(range);\n const isFull = options.mode === 'full';\n\n const phases = selectActivePhases<GoogleAdsResource, GoogleAdsPhase>(\n (r) => r,\n PHASE_ORDER,\n this.settings.resources,\n );\n\n const cursor = isGoogleAdsSyncCursor(options.cursor)\n ? options.cursor\n : undefined;\n\n const campaignSpec = singleSpec(options.fetchSpecs?.[ENTITY_TYPE_CAMPAIGN]);\n\n return paginateChunked<GoogleAdsPhase, string>({\n phases,\n cursor,\n signal,\n logger: this.logger,\n fetchPage: (phase, page, sig) =>\n this.searchPage<unknown>(phase, range, page, campaignSpec, sig),\n writeBatch: async (phase, items, page) => {\n if (page === null) {\n await this.clearScopeOnFirstPage(\n phase,\n storage,\n isFull,\n replaceWindow,\n );\n }\n await this.writePhase(phase, items, storage);\n },\n });\n }\n}\n","import { GoogleAdsConnector } from './google-ads';\n\nexport {\n adGroupMetricRowToSample,\n campaignMetricRowToSample,\n campaignToEntity,\n configFields,\n doc,\n getDateRange,\n GoogleAdsConnector,\n googleAdsResources as resources,\n id,\n keywordMetricRowToSample,\n} from './google-ads';\nexport type { GoogleAdsResource, GoogleAdsSettings } from './google-ads';\nexport default GoogleAdsConnector;\n"],"mappings":"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","names":["z","z","HTTP_CLIENT_VERSION","DEFAULT_USER_AGENT","z","startMs"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rawdash/connector-google-ads",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.2",
|
|
4
4
|
"description": "Rawdash connector for Google Ads — syncs campaigns plus daily campaign / ad-group / keyword performance metrics into the six-shape storage model",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"zod": "^4.4.3",
|
|
27
|
-
"@rawdash/core": "0.28.
|
|
27
|
+
"@rawdash/core": "0.28.2"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"fast-check": "^4.8.0",
|