@google-cloud/nodejs-common 2.0.16-beta → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,10 +18,11 @@ and [Data Tasks Coordinator]. This library includes:
18
18
  - Google Ads click conversions upload
19
19
  - Google Ads customer match upload
20
20
  - Google Ads enhanced conversions upload
21
+ - Google Ads offline userdata job data upload
21
22
  - Google Ads conversions scheduled uploads based on Google Sheets
22
23
  - Measurement Protocol Google Analytics 4
23
24
 
24
- 1. Wrapper for some Google APIs for reporting, mainly
25
+ 2. Wrapper for some Google APIs for reporting, mainly
25
26
  for [Data Tasks Coordinator]:
26
27
 
27
28
  - Google Ads reporting
@@ -31,7 +32,7 @@ and [Data Tasks Coordinator]. This library includes:
31
32
  - YouTube Data API
32
33
  - Ads Data Hub querying
33
34
 
34
- 1. Utilities wrapper class for Google Cloud Products:
35
+ 3. Utilities wrapper class for Google Cloud Products:
35
36
 
36
37
  - **Firestore Access Object**: Firestore has two modes[[comparison]] which
37
38
  have different API. This class, with its two successors, offer a unified
@@ -66,7 +67,7 @@ and [Data Tasks Coordinator]. This library includes:
66
67
  an adapter to wrap a Node8 Cloud Functions into Node6 and Node8 compatible
67
68
  functions.~~ (This has been removed since v1.9.0)
68
69
 
69
- 1. A share library for [Bash] to facilitate installation tasks.
70
+ 4. A share library for [Bash] to facilitate installation tasks.
70
71
 
71
72
  [gmp and google ads connector]: https://github.com/GoogleCloudPlatform/cloud-for-marketing/tree/master/marketing-analytics/activation/gmp-googleads-connector
72
73
  [data tasks coordinator]: https://github.com/GoogleCloudPlatform/cloud-for-marketing/tree/master/marketing-analytics/activation/data-tasks-coordinator
package/bin/google_ads.sh CHANGED
@@ -15,7 +15,7 @@
15
15
  # limitations under the License.
16
16
 
17
17
  # Google Ads API version
18
- GOOGLE_ADS_API_VERSION=13
18
+ GOOGLE_ADS_API_VERSION=16
19
19
 
20
20
  #######################################
21
21
  # Verify whether the current OAuth token, CID and developer token can work.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@google-cloud/nodejs-common",
3
- "version": "2.0.16-beta",
3
+ "version": "2.1.0",
4
4
  "description": "A NodeJs common library for solutions based on Cloud Functions",
5
5
  "author": "Google Inc.",
6
6
  "license": "Apache-2.0",
@@ -0,0 +1,116 @@
1
+ // Copyright 2024 Google Inc.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this fileAccessObject except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ /**
15
+ * @fileoverview Common functions for Google Ads and Search Ads API classes.
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const { Transform } = require('stream');
21
+ const {
22
+ extractObject,
23
+ changeObjectNamingFromLowerCamelToSnake,
24
+ getLogger,
25
+ } = require('../../components/utils.js');
26
+
27
+ /**
28
+ * Returns a integer format CID by removing dashes and spaces.
29
+ * @param {string} cid
30
+ * @return {string}
31
+ */
32
+ function getCleanCid(cid) {
33
+ return cid.toString().trim().replace(/-/g, '');
34
+ }
35
+
36
+ const START_TAG = '"results":';
37
+ const FIELD_MASK_TAG = '"fieldMask"';
38
+ const END_TAG = '"requestId"';
39
+
40
+ /**
41
+ * Generates a function that can convert a given JSON object to a JSON string
42
+ * with only specified fields(fieldMask), in specified naming convention.
43
+ * @param {string} fieldMask The 'fieldMask' string from response.
44
+ * @param {boolean=} snakeCase Whether or not output JSON in snake naming.
45
+ */
46
+ function generateProcessFn(fieldMask, snakeCase = false) {
47
+ const extractor = extractObject(fieldMask.split(','));
48
+ return (originalObject) => {
49
+ const extracted = extractor(originalObject);
50
+ const generatedObject = snakeCase
51
+ ? changeObjectNamingFromLowerCamelToSnake(extracted) : extracted;
52
+ return JSON.stringify(generatedObject);
53
+ };
54
+ };
55
+
56
+ /**
57
+ * A stream.Transform that can extract properties and convert naming of the
58
+ * reponse of Google/Search Ads report from REST interface.
59
+ */
60
+ class RestSearchStreamTransform extends Transform {
61
+
62
+ /**
63
+ * @constructor
64
+ * @param {boolean=} snakeCase Whether or not output JSON in snake naming.
65
+ */
66
+ constructor(snakeCase = false) {
67
+ super({ objectMode: true });
68
+ this.snakeCase = snakeCase;
69
+ this.chunks = [Buffer.from('')];
70
+ this.processFn; // The function to process a row of the report.
71
+ this.logger = getLogger('ADS.STREAM.T');
72
+ this.stopwatch = Date.now();
73
+ }
74
+
75
+ _transform(chunk, encoding, callback) {
76
+ const latest = Buffer.concat([this.chunks[this.chunks.length - 1], chunk]);
77
+ const endIndex = latest.indexOf(END_TAG);
78
+ if (endIndex > -1) {
79
+ this.chunks.push(chunk);
80
+ const rawString = Buffer.concat(this.chunks).toString();
81
+ const startIndex = rawString.indexOf(START_TAG) + START_TAG.length;
82
+ const maskIndex = rawString.lastIndexOf(FIELD_MASK_TAG);
83
+ if (!this.processFn) {
84
+ const fieldMask = rawString
85
+ .substring(maskIndex + FIELD_MASK_TAG.length, rawString.indexOf(END_TAG))
86
+ .split('"')[1];
87
+ this.logger.debug(`Got fieldMask: ${fieldMask}`);
88
+ this.processFn = generateProcessFn(fieldMask, this.snakeCase);
89
+ }
90
+ const resultsWithTailing = rawString.substring(startIndex, maskIndex);
91
+ const results = resultsWithTailing.substring(
92
+ 0, resultsWithTailing.lastIndexOf(','));
93
+ const rows = JSON.parse(results);
94
+ const data = rows.map(this.processFn).join('\n') + '\n';
95
+ // Clear cached chunks.
96
+ this.chunks = [latest.subarray(latest.indexOf(END_TAG) + END_TAG.length)];
97
+
98
+ this.logger.debug(`Got ${rows.length} rows. Process time:`,
99
+ Date.now() - this.stopwatch);
100
+ this.stopwatch = Date.now();
101
+ callback(null, data);
102
+ } else {
103
+ if (chunk.length < END_TAG.length) {// Update latest chunk for short chunk
104
+ this.chunks[this.chunks.length - 1] = latest;
105
+ } else {
106
+ this.chunks.push(chunk);
107
+ }
108
+ callback();
109
+ }
110
+ }
111
+ }
112
+
113
+ module.exports = {
114
+ getCleanCid,
115
+ RestSearchStreamTransform,
116
+ };
@@ -34,8 +34,8 @@ const API_SCOPES = Object.freeze([
34
34
  const API_VERSION = 'v3';
35
35
 
36
36
  /**
37
- * Display and Video 360 API v2 stub.
38
- * @see https://developers.google.com/display-video/api/reference/rest/v2
37
+ * Display and Video 360 API v3 stub.
38
+ * @see https://developers.google.com/display-video/api/reference/rest/v3
39
39
  * This is not the same to Reports Display & Video 360 API which is from Google
40
40
  * Bid Manager API.
41
41
  * @see https://developers.google.com/bid-manager/reference/rest
@@ -22,7 +22,7 @@
22
22
 
23
23
  const { Transform } = require('stream');
24
24
  const lodash = require('lodash');
25
-
25
+ const { request: gaxiosRequest } = require('gaxios');
26
26
  const {
27
27
  ConversionAdjustmentUploadServiceClient,
28
28
  ConversionUploadServiceClient,
@@ -33,6 +33,8 @@ const {
33
33
  UserListServiceClient,
34
34
  protos: { google: { ads: { googleads } } },
35
35
  } = require('google-ads-nodejs-client');
36
+
37
+ const API_VERSION = Object.keys(googleads)[0];
36
38
  const {
37
39
  common: {
38
40
  Consent,
@@ -88,7 +90,7 @@ const {
88
90
  UserListTypeEnum: { UserListType },
89
91
  CustomerMatchUploadKeyTypeEnum: { CustomerMatchUploadKeyType },
90
92
  },
91
- } = googleads.v16;
93
+ } = googleads[API_VERSION];
92
94
 
93
95
  const AuthClient = require('./auth_client.js');
94
96
  const {
@@ -99,10 +101,12 @@ const {
99
101
  changeObjectNamingFromSnakeToLowerCamel,
100
102
  changeObjectNamingFromLowerCamelToSnake,
101
103
  } = require('../components/utils.js');
104
+ const { getCleanCid, RestSearchStreamTransform }
105
+ = require('./base/ads_api_common.js');
102
106
 
103
107
  /** @type {!ReadonlyArray<string>} */
104
108
  const API_SCOPES = Object.freeze(['https://www.googleapis.com/auth/adwords']);
105
-
109
+ const API_ENDPOINT = 'https://googleads.googleapis.com';
106
110
  /**
107
111
  * List of properties that will be taken from the data file as elements of a
108
112
  * conversion or a conversion adjustment.
@@ -257,6 +261,9 @@ let ConversionConfig;
257
261
  * If audience listId is not present, 'listName' and 'uploadKeyType' need to
258
262
  * be there so they can be used to create a customer match user list.
259
263
  * operation must be one of 'create' or 'remove'.
264
+ * `customerMatchUserListMetadata` offers a request level metadata, including
265
+ * 'consent'. While the top level 'consent', if presents, it will serve as the
266
+ * fallback value for each UserIdentifier.
260
267
  * Should not include `userIdentifierSource` based on:
261
268
  * @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
262
269
  * @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataOperation
@@ -267,6 +274,7 @@ let ConversionConfig;
267
274
  * listId: (string|undefined),
268
275
  * listName: (string|undefined),
269
276
  * uploadKeyType: ('CONTACT_INFO'|'CRM_ID'|'MOBILE_ADVERTISING_ID'|undefined),
277
+ * customerMatchUserListMetadata: (undefined|CustomerMatchUserListMetadata),
270
278
  * operation: ('create'|'remove'),
271
279
  * consent: (!Consent),
272
280
  * }}
@@ -284,6 +292,9 @@ let CustomerMatchConfig;
284
292
  * create a customer match user list.
285
293
  * For job type 'CUSTOMER_MATCH_WITH_ATTRIBUTES', 'user_attribute' can be used
286
294
  * to store shared additional user attributes.
295
+ * For job type 'CUSTOMER_MATCH_*', `customerMatchUserListMetadata` offers a job
296
+ * level metadata, including 'consent'. While the top level 'consent',
297
+ * if presents, it will serve as the fallback value for each UserIdentifier.
287
298
  * For job type 'STORE_SALES_UPLOAD_FIRST_PARTY', `storeSalesMetadata` is
288
299
  * required to offer StoreSalesMetadata. Besides that, for the store sales data,
289
300
  * common data (e.g. `currencyCode`, `conversionAction`) in
@@ -298,6 +309,7 @@ let CustomerMatchConfig;
298
309
  * operation: ('create'|'remove'),
299
310
  * type: !OfflineUserDataJobType,
300
311
  * storeSalesMetadata: (undefined|StoreSalesMetadata),
312
+ * customerMatchUserListMetadata: (undefined|CustomerMatchUserListMetadata),
301
313
  * transactionAttribute: (undefined|TransactionAttribute),
302
314
  * userAttribute: (undefined|UserAttribute),
303
315
  * userIdentifierSource: (!UserIdentifierSource|undefined),
@@ -356,7 +368,7 @@ class GoogleAdsApi {
356
368
  async getReport(customerId, loginCustomerId, query) {
357
369
  const request = new SearchGoogleAdsRequest({
358
370
  query,
359
- customerId: this.getCleanCid_(customerId),
371
+ customerId: getCleanCid(customerId),
360
372
  });
361
373
  return this.getReport_(request, loginCustomerId, true);
362
374
  }
@@ -382,7 +394,7 @@ class GoogleAdsApi {
382
394
  const request = new SearchGoogleAdsRequest(
383
395
  Object.assign({
384
396
  query,
385
- customerId: this.getCleanCid_(customerId),
397
+ customerId: getCleanCid(customerId),
386
398
  pageSize: 10000,
387
399
  }, options)
388
400
  );
@@ -413,7 +425,6 @@ class GoogleAdsApi {
413
425
  return result;
414
426
  }
415
427
 
416
- //TODO: Test a big report see how the event 'data' is triggered
417
428
  /**
418
429
  * Gets stream report of a given Customer account. The stream will send
419
430
  * `SearchGoogleAdsResponse` objects with the event 'data'.
@@ -426,7 +437,7 @@ class GoogleAdsApi {
426
437
  const client = await this.getGoogleAdsServiceClient_();
427
438
  const request = new SearchGoogleAdsRequest({
428
439
  query,
429
- customerId: this.getCleanCid_(customerId),
440
+ customerId: getCleanCid(customerId),
430
441
  });
431
442
  const callOptions = this.getCallOptions_(loginCustomerId);
432
443
  const response = await client.searchStream(request, callOptions);
@@ -441,11 +452,11 @@ class GoogleAdsApi {
441
452
  * @param {string} customerId
442
453
  * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
443
454
  * @param {string} query A Google Ads Query string.
444
- * @param {boolean} outputSnake Output JSON objects in snake_case.
445
- * @return {!Promise<string>}
455
+ * @param {boolean} snakeCase Output JSON objects in snake_case.
456
+ * @return {!Promise<stream>}
446
457
  */
447
458
  async cleanedStreamReport(customerId, loginCustomerId, query,
448
- outputSnake = false) {
459
+ snakeCase = false) {
449
460
  const cleanReportStream = new Transform({
450
461
  writableObjectMode: true,
451
462
  transform(chunk, encoding, callback) {
@@ -454,7 +465,7 @@ class GoogleAdsApi {
454
465
  return path.split('.').map(changeNamingFromSnakeToLowerCamel).join('.');
455
466
  });
456
467
  const extractor = extractObject(camelPaths);
457
- const results = outputSnake
468
+ const results = snakeCase
458
469
  ? chunk.results.map(extractor).map(changeObjectNamingFromLowerCamelToSnake)
459
470
  : chunk.results.map(extractor);
460
471
  // Add a line break after each chunk to keep files in proper format.
@@ -467,25 +478,69 @@ class GoogleAdsApi {
467
478
  .pipe(cleanReportStream);
468
479
  }
469
480
 
481
+ /**
482
+ * Gets the report stream through REST interface.
483
+ * @param {string} customerId
484
+ * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
485
+ * @param {string} query A Google Ads Query string.
486
+ * @return {!Promise<stream>}
487
+ */
488
+ async restStreamReport(customerId, loginCustomerId, query) {
489
+ await this.authClient.prepareCredentials();
490
+ const headers = Object.assign(
491
+ await this.authClient.getDefaultAuth().getRequestHeaders(),
492
+ this.getGoogleAdsHeaders_(loginCustomerId)
493
+ );
494
+ const options = {
495
+ baseURL: `${API_ENDPOINT}/${API_VERSION}/`,
496
+ url: `customers/${getCleanCid(customerId)}/googleAds:searchStream`,
497
+ headers,
498
+ data: { query },
499
+ method: 'POST',
500
+ responseType: 'stream',
501
+ };
502
+ const response = await gaxiosRequest(options);
503
+ return response.data;
504
+ }
505
+
506
+ /**
507
+ * Gets the report stream through REST interface.
508
+ * Based on the `fieldMask` in the response to filter out
509
+ * selected fields of the report and returns an array of JSON format strings
510
+ * with the delimit of a line breaker.
511
+ * @param {string} customerId
512
+ * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
513
+ * @param {string} query A Google Ads Query string.
514
+ * @param {boolean} snakeCase Output JSON objects in snake_case.
515
+ * @return {!Promise<stream>}
516
+ */
517
+ async cleanedRestStreamReport(customerId, loginCustomerId, query,
518
+ snakeCase = false) {
519
+ const transform = new RestSearchStreamTransform(snakeCase);
520
+ const stream =
521
+ await this.restStreamReport(customerId, loginCustomerId, query);
522
+ return stream.on('error', (error) => transform.emit('error', error))
523
+ .pipe(transform);
524
+ }
525
+
470
526
  /**
471
527
  * Returns resources information from Google Ads API. see:
472
528
  * https://developers.google.com/google-ads/api/docs/concepts/field-service
473
529
  * Note, it looks like this function doesn't check the CID, just using
474
530
  * developer token and OAuth.
475
- * @param {string|number} loginCustomerId Login customer account ID.
476
531
  * @param {Array<string>} adFields Array of Ad fields.
477
532
  * @param {Array<string>} metadata Select fields, default values are:
478
533
  * name, data_type, is_repeated, type_url.
479
534
  * @return {!Promise<!Array<GoogleAdsField>>}
480
535
  */
481
- async searchMetaData(loginCustomerId, adFields, metadata = [
482
- 'name', 'data_type', 'is_repeated', 'type_url',]) {
536
+ async searchReportField(adFields,
537
+ metadata = ['name', 'data_type', 'is_repeated', 'type_url',]) {
483
538
  const client = await this.getGoogleAdsFieldServiceClient_();
484
539
  const selectClause = metadata.join(',');
485
540
  const fields = adFields.join('","');
486
541
  const query = `SELECT ${selectClause} WHERE name IN ("${fields}")`;
487
542
  const request = new SearchGoogleAdsFieldsRequest({ query });
488
- const callOptions = this.getCallOptions_(loginCustomerId);
543
+ const callOptions = this.getCallOptions_();
489
544
  const [results] = await client.searchGoogleAdsFields(request, callOptions);
490
545
  return results;
491
546
  }
@@ -569,8 +624,8 @@ class GoogleAdsApi {
569
624
  functionName, propertyForDebug) {
570
625
  /** @type {!ConversionConfig} */
571
626
  const adsConfig = this.getCamelConfig_(conversionConfig);
572
- adsConfig.customerId = this.getCleanCid_(customerId);
573
- adsConfig.loginCustomerId = this.getCleanCid_(loginCustomerId);
627
+ adsConfig.customerId = getCleanCid(customerId);
628
+ adsConfig.loginCustomerId = getCleanCid(loginCustomerId);
574
629
  /**
575
630
  * Sends a batch of hits to Google Ads API.
576
631
  * @param {!Array<string>} lines Data for single request. It should be
@@ -885,7 +940,7 @@ class GoogleAdsApi {
885
940
  SELECT user_list.id, user_list.resource_name
886
941
  FROM user_list
887
942
  WHERE user_list.name = '${listName}'
888
- AND customer.id = ${this.getCleanCid_(customerId)}
943
+ AND customer.id = ${getCleanCid(customerId)}
889
944
  AND user_list.type = CRM_BASED
890
945
  AND user_list.membership_status = OPEN
891
946
  AND user_list.crm_based_user_list.upload_key_type = ${uploadKeyType}
@@ -915,7 +970,7 @@ class GoogleAdsApi {
915
970
  crmBasedUserList: { uploadKeyType },
916
971
  });
917
972
  const request = new MutateUserListsRequest({
918
- customerId: this.getCleanCid_(customerId),
973
+ customerId: getCleanCid(customerId),
919
974
  operations: [{ create: userList }],
920
975
  validateOnly: this.debugMode, // when true makes no changes
921
976
  partialFailure: false, // Simplify error handling in creating userlist
@@ -1021,9 +1076,9 @@ class GoogleAdsApi {
1021
1076
  const operations = userDataList.map(
1022
1077
  (userData) => new UserDataOperation({ [operation]: new UserData(userData) })
1023
1078
  );
1024
- const metadata = this.buildCustomerMatchUserListMetadata_(customerId, listId);
1079
+ const metadata = this.buildCustomerMatchUserListMetadata_(config);
1025
1080
  const request = new UploadUserDataRequest({
1026
- customerId: this.getCleanCid_(customerId),
1081
+ customerId: getCleanCid(customerId),
1027
1082
  operations,
1028
1083
  customerMatchUserListMetadata: metadata,
1029
1084
  });
@@ -1035,16 +1090,20 @@ class GoogleAdsApi {
1035
1090
  /**
1036
1091
  * Creates CustomerMatchUserListMetadata.
1037
1092
  * @see https://developers.google.com/google-ads/api/reference/rpc/latest/CustomerMatchUserListMetadata
1038
- * @param {string} customerId part of the ResourceName to be mutated
1039
- * @param {string} userListId part of the ResourceName to be mutated
1093
+ * @param {{
1094
+ * customerId: string,
1095
+ * listId: string,
1096
+ * customerMatchUserListMetadata: undefined|!CustomerMatchUserListMetadata
1097
+ * }} config Configuration for CustomerMatchUserListMetadata
1040
1098
  * @return {!CustomerMatchUserListMetadata}
1041
1099
  * @private
1042
1100
  */
1043
- buildCustomerMatchUserListMetadata_(customerId, userListId) {
1044
- const resourceName = `customers/${customerId}/userLists/${userListId}`;
1045
- return new CustomerMatchUserListMetadata({
1101
+ buildCustomerMatchUserListMetadata_(config) {
1102
+ const { customerId, listId, customerMatchUserListMetadata } = config;
1103
+ const resourceName = `customers/${customerId}/userLists/${listId}`;
1104
+ return new CustomerMatchUserListMetadata(Object.assign({
1046
1105
  userList: resourceName,
1047
- });
1106
+ }, customerMatchUserListMetadata));
1048
1107
  }
1049
1108
 
1050
1109
  /**
@@ -1090,8 +1149,7 @@ class GoogleAdsApi {
1090
1149
  const jobData = { type };
1091
1150
  // https://developers.google.com/google-ads/api/rest/reference/rest/latest/OfflineUserDataJobs?hl=en#CustomerMatchUserListMetadata
1092
1151
  if (type.startsWith('CUSTOMER_MATCH')) {
1093
- const metadata = this.buildCustomerMatchUserListMetadata_(customerId,
1094
- listId);
1152
+ const metadata = this.buildCustomerMatchUserListMetadata_(config);
1095
1153
  jobData.customerMatchUserListMetadata = metadata;
1096
1154
  // https://developers.google.com/google-ads/api/rest/reference/rest/latest/OfflineUserDataJob?hl=en#StoreSalesMetadata
1097
1155
  } else if (type.startsWith('STORE_SALES')) {
@@ -1105,7 +1163,7 @@ class GoogleAdsApi {
1105
1163
  }
1106
1164
  const job = new OfflineUserDataJob(jobData);
1107
1165
  const request = new CreateOfflineUserDataJobRequest({
1108
- customerId: this.getCleanCid_(customerId),
1166
+ customerId: getCleanCid(customerId),
1109
1167
  job,
1110
1168
  validateOnly: this.debugMode, // when true makes no changes
1111
1169
  enableMatchRateRangePreview: true,
@@ -1258,16 +1316,6 @@ class GoogleAdsApi {
1258
1316
  }
1259
1317
  }
1260
1318
 
1261
- /**
1262
- * Returns a integer format CID by removing dashes.
1263
- * @param {string} cid
1264
- * @return {string}
1265
- * @private
1266
- */
1267
- getCleanCid_(cid) {
1268
- return cid.toString().replace(/-/g, '');
1269
- }
1270
-
1271
1319
  /**
1272
1320
  * Historically, we used a 3rd party library that adopted snake naming
1273
1321
  * convention as the protobuf files and API documents. However, the
@@ -1289,10 +1337,11 @@ class GoogleAdsApi {
1289
1337
  * @private
1290
1338
  */
1291
1339
  getGoogleAdsHeaders_(loginCustomerId) {
1292
- return {
1293
- "developer-token": this.developerToken,
1294
- "login-customer-id": loginCustomerId,
1295
- };
1340
+ const headers = { 'developer-token': this.developerToken };
1341
+ if (loginCustomerId) {
1342
+ headers['login-customer-id'] = getCleanCid(loginCustomerId);
1343
+ }
1344
+ return headers;
1296
1345
  }
1297
1346
 
1298
1347
  /**
@@ -1305,7 +1354,7 @@ class GoogleAdsApi {
1305
1354
  getCallOptions_(loginCustomerId) {
1306
1355
  return {
1307
1356
  otherArgs: {
1308
- headers: this.getGoogleAdsHeaders_(this.getCleanCid_(loginCustomerId)),
1357
+ headers: this.getGoogleAdsHeaders_(loginCustomerId),
1309
1358
  },
1310
1359
  };
1311
1360
  }
@@ -18,13 +18,17 @@
18
18
 
19
19
  'use strict';
20
20
 
21
+ const { request: gaxiosRequest } = require('gaxios');
21
22
  const { google } = require('googleapis');
22
23
  const AuthClient = require('./auth_client.js');
23
24
  const { getLogger } = require('../components/utils.js');
25
+ const { getCleanCid, RestSearchStreamTransform }
26
+ = require('./base/ads_api_common.js');
24
27
 
25
28
  const API_SCOPES = Object.freeze([
26
29
  'https://www.googleapis.com/auth/doubleclicksearch',
27
30
  ]);
31
+ const API_ENDPOINT = 'https://searchads360.googleapis.com';
28
32
  const API_VERSION = 'v0';
29
33
 
30
34
  /**
@@ -56,13 +60,15 @@ class SearchAds {
56
60
  * @private
57
61
  */
58
62
  async getApiClient_(loginCustomerId) {
59
- this.logger.debug(`Initialized Search Ads reporting instance for ${loginCustomerId}`);
60
- this.searchads360 = google.searchads360({
63
+ this.logger.debug(`Initialized SA reporting for ${loginCustomerId}`);
64
+ const options = {
61
65
  version: API_VERSION,
62
66
  auth: await this.getAuth_(),
63
- headers: { 'login-customer-id': loginCustomerId },
64
- });
65
- return this.searchads360;
67
+ };
68
+ if (loginCustomerId) {
69
+ options.headers = { 'login-customer-id': getCleanCid(loginCustomerId) };
70
+ }
71
+ return google.searchads360(options);
66
72
  }
67
73
 
68
74
  /**
@@ -77,61 +83,117 @@ class SearchAds {
77
83
  }
78
84
 
79
85
  /**
80
- * Returns all rows that match the search stream query.
81
- * The streamed content is not NDJSON format, but one JSON object with the
82
- * property `results`. The whole JSON string can be parsed to an object and
83
- * the `results` be extracted and converted to NDJSON lines. If the report
84
- * is too large to be handled in this way, a possible solution is to parse
85
- * the string directly to get the content of `results`.
86
- * @see https://developers.google.com/search-ads/reporting/api/reference/rest/v0/customers.searchAds360/searchStream
86
+ * Gets a report synchronously from a given Customer account.
87
+ * If there is a `nextPageToken` in the response, it means the report is not
88
+ * finished and there are more pages.
89
+ * @see https://developers.google.com/search-ads/reporting/api/reference/rpc/google.ads.searchads360.v0.services#searchads360service
87
90
  * @param {string} customerId
88
91
  * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
89
92
  * @param {string} query
90
- * @return {!ReadableStream}
93
+ * @param {object=} options Options for `SearchSearchAds360Request`.
94
+ * @see https://developers.google.com/search-ads/reporting/api/reference/rpc/google.ads.searchads360.v0.services#searchsearchads360request
95
+ * @return {!SearchAds360Field}
96
+ * @see https://developers.google.com/search-ads/reporting/api/reference/rpc/google.ads.searchads360.v0.services#searchsearchads360response
91
97
  */
92
- async streamReport(customerId, loginCustomerId, query) {
98
+ async getPaginatedReport(customerId, loginCustomerId, query, options = {}) {
93
99
  const searchads = await this.getApiClient_(loginCustomerId);
100
+ const requestBody = Object.assign({
101
+ query,
102
+ pageSize: 10000,
103
+ }, options);
94
104
  const response = await searchads.customers.searchAds360.search({
95
- customerId,
96
- requestBody: { query },
97
- }, { responseType: 'stream' });
105
+ customerId: getCleanCid(customerId),
106
+ requestBody,
107
+ });
98
108
  return response.data;
99
109
  }
100
110
 
101
111
  /**
102
- * Gets a report synchronously from a given Customer account.
103
- * This is for test as it does not handle page token. For product env, use
104
- * function `streamReport`.
105
- * @see https://developers.google.com/search-ads/reporting/api/reference/rest/v0/customers.searchAds360/search#request-body
112
+ * Gets a report stream from a Search Ads 360 reporting API.
113
+ * The streamed content is not NDJSON format, but an array of JSON objects
114
+ * with each element has a property `results`.
115
+ * `data` support `batchSize` to set how many rows in one result element.
106
116
  * @param {string} customerId
107
117
  * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
108
118
  * @param {string} query
109
- * @return {!SearchAds360Field}
110
- * @see https://developers.google.com/search-ads/reporting/api/reference/rest/v0/searchAds360Fields#SearchAds360Field
119
+ * @return {!Promise<stream>}
120
+ * @see https://developers.google.com/search-ads/reporting/api/reference/rest/search
111
121
  */
112
- async getReport(customerId, loginCustomerId, query) {
113
- const searchads = await this.getApiClient_(loginCustomerId);
114
- const response = await searchads.customers.searchAds360.search({
115
- customerId,
116
- requestBody: { query },
122
+ async restStreamReport(customerId, loginCustomerId, query) {
123
+ const auth = await this.getAuth_();
124
+ const headers = Object.assign(
125
+ await auth.getRequestHeaders(), {
126
+ 'login-customer-id': getCleanCid(loginCustomerId),
117
127
  });
118
- return response.data.results;
128
+ const options = {
129
+ baseURL: `${API_ENDPOINT}/${API_VERSION}/`,
130
+ url: `customers/${getCleanCid(customerId)}/searchAds360:searchStream`,
131
+ headers,
132
+ data: { query },
133
+ method: 'POST',
134
+ responseType: 'stream',
135
+ };
136
+ const response = await gaxiosRequest(options);
137
+ return response.data;
138
+ }
139
+
140
+ /**
141
+ * Gets the report stream through REST interface.
142
+ * Based on the `fieldMask` in the response to filter out
143
+ * selected fields of the report and returns an array of JSON format strings
144
+ * with the delimit of a line breaker.
145
+ * @param {string} customerId
146
+ * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
147
+ * @param {string} query A Google Ads Query string.
148
+ * @param {boolean=} snakeCase Output JSON objects in snake_case.
149
+ * @return {!Promise<stream>}
150
+ */
151
+ async cleanedRestStreamReport(customerId, loginCustomerId, query,
152
+ snakeCase = false) {
153
+ const transform = new RestSearchStreamTransform(snakeCase);
154
+ const stream =
155
+ await this.restStreamReport(customerId, loginCustomerId, query);
156
+ return stream.on('error', (error) => transform.emit('error', error))
157
+ .pipe(transform);
119
158
  }
120
159
 
121
160
  /**
122
161
  * Returns the requested field or resource (artifact) used by SearchAds360Service.
123
162
  * This service doesn't require `login-customer-id` HTTP header.
124
163
  * @see https://developers.google.com/search-ads/reporting/api/reference/rest/v0/searchAds360Fields/get
125
- * @param {string} resourceName
164
+ * @param {string} fieldName
126
165
  * @return {!SearchAds360Field}
127
166
  * @see https://developers.google.com/search-ads/reporting/api/reference/rest/v0/searchAds360Fields#SearchAds360Field
128
167
  */
129
- async getReportField(resourceName) {
168
+ async getReportField(fieldName) {
130
169
  const searchads = await this.getApiClient_();
131
- const response = await searchads.searchAds360Fields.get({ resourceName });
170
+ const resourceName = `searchAds360Fields/${fieldName}`;
171
+ const response =
172
+ await searchads.searchAds360Fields.get({ resourceName });
132
173
  return response.data;
133
174
  }
134
175
 
176
+ /**
177
+ * Returns resources information from Search Ads API.
178
+ * @see: https://developers.google.com/search-ads/reporting/api/reference/rest/v0/searchAds360Fields
179
+ * Note, it looks like this function doesn't check the CID, just using OAuth.
180
+ * @param {Array<string>} adFields Array of Ad fields.
181
+ * @param {Array<string>} metadata Select fields, default values are:
182
+ * name, data_type, is_repeated, type_url.
183
+ * @return {!Promise<!Array<GoogleAdsField>>}
184
+ * @see GoogleAdsApi.searchReportField
185
+ */
186
+ async searchReportField(adFields,
187
+ metadata = ['name', 'data_type', 'is_repeated', 'type_url',]) {
188
+ const searchads = await this.getApiClient_();
189
+ const selectClause = metadata.join(',');
190
+ const fields = adFields.join('","');
191
+ const query = `SELECT ${selectClause} WHERE name IN ("${fields}")`;
192
+ const response =
193
+ await searchads.searchAds360Fields.search({ query, pageSize: 10000 });
194
+ return response.data.results;
195
+ }
196
+
135
197
  /**
136
198
  * Returns all the custom columns associated with the customer in full detail.
137
199
  * @see https://developers.google.com/search-ads/reporting/api/reference/rest/v0/customers.customColumns/list
@@ -489,7 +489,7 @@ const extractObject = (paths) => {
489
489
  paths.forEach((path) => {
490
490
  const [value, owner, property] = path.split('.')
491
491
  .reduce(transcribe, [sourceObject, output, undefined]);
492
- if (value) {
492
+ if (typeof value !== 'undefined') {
493
493
  owner[property] = value;
494
494
  }
495
495
  });