@google-cloud/nodejs-common 0.9.5-alpha → 1.0.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
@@ -1,69 +1,78 @@
1
1
  # NodeJS Common Library
2
2
 
3
- <!--* freshness: { owner: 'lushu' reviewed: '2021-06-01' } *-->
4
-
5
- A NodeJs common library for other projects, e.g. [GMP and Google Ads
6
- Connector] and [Data Tasks Coordinator]. This library includes:
7
-
8
- 1. Authentication wrapper based on google auth library to support OAuth, JWT
9
- and ADC authentication;
10
-
11
- 1. Wrapper for some Google APIs for integration, mainly for [GMP and Google Ads
12
- Connector]:
13
-
14
- * Google Analytics data import
15
- * Google Analytics measurement protocol
16
- * Campaign Manager offline conversion upload
17
- * Search Ads 360 conversions upload
18
- * Google Ads click conversions upload
19
- * Google Ads customer match upload
20
- * Google Ads conversions scheduled uploads based on Google Sheets
21
- * Measurement Protocol Google Analytics 4
22
-
23
- 1. Wrapper for some Google APIs for reporting, mainly for [Data Tasks Coordinator]:
24
-
25
- * Google Ads reporting
26
- * Campaign Manager reporting
27
- * Search Ads 360 reporting
28
- * Display and Video 360 reporting
29
- * Ads Data Hub querying
30
-
31
- 1. Utilities wrapper class for Google Cloud Products:
32
-
33
- * **Firestore Access Object**: Firestore has two modes which are excluded
34
- to each other and can't be changed once selected in a Cloud
35
- Project[[2]]. This class, with its two successors offer a unified
36
- interface to operate data objects on either Firestore or Datastore.
37
-
38
- * **AutoMl Tables API**: Offers a unified entry to use this API based on
39
- Google Cloud client library combined with REST requests to service
40
- directly due to some functionalities missed in the client library.
41
-
42
- * **Pub/Sub Utilities**: Offers utilities functions to create topics and
43
- subscriptions for Pub/Sub, as well as the convenient way to publish a
44
- message.
45
-
46
- * **Storage Utilities**: Offers functions to manipulate the files on Cloud
47
- Storage. The main functions are:
48
-
49
- * Reading a given length (or slightly less) content without breaking a
50
- line;
51
- * Splitting a file into multiple files with the given length (or
52
- slightly less) without breaking a line;
53
- * Merging files into one file.
54
-
55
- * **Cloud Scheduler Adapter**: A wrapper to pause and resume Cloud
56
- Scheduler jobs.
57
-
58
- * **Cloud Functions Adapter**: Cloud Functions have different parameters
59
- in different environments, e.g. Node6 vs Node8[[1]]. This utility file
60
- offers an adapter to wrap a Node8 Cloud Functions into Node6 and Node8
61
- compatible functions.
62
-
3
+ <!--* freshness: { owner: 'lushu' reviewed: '2021-12-02' } *-->
4
+
5
+ A NodeJs common library for other projects, e.g. [GMP and Google Ads Connector]
6
+ and [Data Tasks Coordinator]. This library includes:
7
+
8
+ 1. Authentication wrapper based on google auth library to support OAuth, JWT and
9
+ ADC authentication;
10
+
11
+ 1. Wrapper for some Google APIs for integration, mainly
12
+ for [GMP and Google Ads Connector]:
13
+
14
+ * Google Analytics data import
15
+ * Google Analytics measurement protocol
16
+ * Campaign Manager offline conversion upload
17
+ * Search Ads 360 conversions upload
18
+ * Google Ads click conversions upload
19
+ * Google Ads customer match upload
20
+ * Google Ads conversions scheduled uploads based on Google Sheets
21
+ * Measurement Protocol Google Analytics 4
22
+
23
+ 1. Wrapper for some Google APIs for reporting, mainly
24
+ for [Data Tasks Coordinator]:
25
+
26
+ * Google Ads reporting
27
+ * Campaign Manager reporting
28
+ * Search Ads 360 reporting
29
+ * Display and Video 360 reporting
30
+ * YouTube Data API
31
+ * Ads Data Hub querying
32
+
33
+ 1. Utilities wrapper class for Google Cloud Products:
34
+
35
+ * **Firestore Access Object**: Firestore has two modes which are excluded to
36
+ each other and can't be changed once selected in a Cloud Project[[2]].
37
+ This class, with its two successors offer a unified interface to operate
38
+ data objects on either Firestore or Datastore.
39
+
40
+ * **AutoMl Tables API**: Offers a unified entry to use this API based on
41
+ Google Cloud client library combined with REST requests to service
42
+ directly due to some functionalities missed in the client library.
43
+
44
+ * **Vertex AI API**: Offers a unified entry to use this API based on Google
45
+ Cloud client library.
46
+
47
+ * **Pub/Sub Utilities**: Offers utilities functions to create topics and
48
+ subscriptions for Pub/Sub, as well as the convenient way to publish a
49
+ message.
50
+
51
+ * **Storage Utilities**: Offers functions to manipulate the files on Cloud
52
+ Storage. The main functions are:
53
+
54
+ * Reading a given length (or slightly less) content without breaking a
55
+ line;
56
+ * Splitting a file into multiple files with the given length (or
57
+ slightly less) without breaking a line;
58
+ * Merging files into one file.
59
+
60
+ * **Cloud Scheduler Adapter**: A wrapper to pause and resume Cloud Scheduler
61
+ jobs.
62
+
63
+ * **Cloud Functions Adapter**: Cloud Functions have different parameters in
64
+ different environments, e.g. Node6 vs Node8[[1]]. This utility file offers
65
+ an adapter to wrap a Node8 Cloud Functions into Node6 and Node8 compatible
66
+ functions.
67
+
63
68
  1. A share library for [Bash] to facilitate installation tasks.
64
69
 
65
70
  [GMP and Google Ads Connector]:https://github.com/GoogleCloudPlatform/cloud-for-marketing/tree/master/marketing-analytics/activation/gmp-googleads-connector
71
+
66
72
  [Data Tasks Coordinator]:https://github.com/GoogleCloudPlatform/cloud-for-marketing/tree/master/marketing-analytics/activation/data-tasks-coordinator
73
+
67
74
  [1]:https://cloud.google.com/functions/docs/writing/background#functions-writing-background-hello-pubsub-node8-10
75
+
68
76
  [2]:https://cloud.google.com/datastore/docs/concepts/overview#comparison_with_traditional_databases
77
+
69
78
  [Bash]:https://www.gnu.org/software/bash/
@@ -15,7 +15,7 @@
15
15
  # limitations under the License.
16
16
 
17
17
  # Cloud Functions Runtime Environment.
18
- CF_RUNTIME="${CF_RUNTIME:=nodejs10}"
18
+ CF_RUNTIME="${CF_RUNTIME:=nodejs14}"
19
19
 
20
20
  # Counter for steps.
21
21
  STEP=0
@@ -743,8 +743,8 @@ select_dataset_location() {
743
743
  "ASIA_PACIFIC"
744
744
  )
745
745
  local MULTI_REGIONAL=(
746
- "Data centers within member states of the European Union (EU)"
747
- "Data centers in the United States (US)"
746
+ "Data centers within member states of the European Union (eu)"
747
+ "Data centers in the United States (us)"
748
748
  )
749
749
  local AMERICAS=(
750
750
  "${NORTH_AMERICA[@]}"
@@ -1199,21 +1199,17 @@ create_or_update_sink() {
1199
1199
  local existingFilter
1200
1200
  existingFilter=$(gcloud logging sinks list --filter="name:${sinkName}" \
1201
1201
  --format="value(filter)")
1202
- if [[ "${existingFilter}" != "${logFilter}" ]]; then
1203
- local action
1204
- if [[ -z "${existingFilter}" ]]; then
1205
- action="create"
1206
- printf '%s\n' " Logging Export [${sinkName}] doesn't exist. Creating..."
1207
- else
1208
- action="update"
1209
- printf '%s\n' " Logging Export [${sinkName}] exists with a different \
1210
- filter. Updating..."
1211
- fi
1212
- gcloud -q logging sinks ${action} "${sinkName}" "${sinkDestAndFlags[@]}" \
1213
- --log-filter="${logFilter}"
1202
+ local action
1203
+ if [[ -z "${existingFilter}" ]]; then
1204
+ action="create"
1205
+ printf '%s\n' " Logging Export [${sinkName}] doesn't exist. Creating..."
1214
1206
  else
1215
- printf '%s\n' " Logging Export [${sinkName}] exists. Continue..."
1207
+ action="update"
1208
+ printf '%s\n' " Logging Export [${sinkName}] exists with a different \
1209
+ filter. Updating..."
1216
1210
  fi
1211
+ gcloud -q logging sinks ${action} "${sinkName}" "${sinkDestAndFlags[@]}" \
1212
+ --log-filter="${logFilter}"
1217
1213
  if [[ $? -gt 0 ]];then
1218
1214
  printf '%s\n' "Failed to create or update Logs router sink."
1219
1215
  return 1
@@ -1833,9 +1829,8 @@ get_cloud_functions_service_account() {
1833
1829
  # None.
1834
1830
  #######################################
1835
1831
  check_firestore_existence() {
1836
- local firestore_status
1837
- firestore_status=$(gcloud firestore operations list 2>&1)
1838
- while [[ ${firestore_status} =~ .*NOT_FOUND.* ]]; do
1832
+ gcloud firestore indexes fields list >/dev/null 2>&1
1833
+ while [[ $? -gt 0 ]]; do
1839
1834
  cat <<EOF
1840
1835
  Cannot find Firestore or Datastore in current project. Please visit \
1841
1836
  https://console.cloud.google.com/firestore?project=${GCP_PROJECT} to create a \
@@ -1846,7 +1841,7 @@ EOF
1846
1841
  local any
1847
1842
  read -n1 -s any
1848
1843
  printf '\n'
1849
- firestore_status=$(gcloud firestore operations list 2>&1)
1844
+ gcloud firestore indexes fields list >/dev/null 2>&1
1850
1845
  done
1851
1846
  }
1852
1847
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@google-cloud/nodejs-common",
3
- "version": "0.9.5-alpha",
3
+ "version": "1.0.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",
@@ -16,21 +16,22 @@
16
16
  },
17
17
  "homepage": "https://github.com/GoogleCloudPlatform/cloud-for-marketing/blob/master/marketing-analytics/activation/common-libs/nodejs-common/README.md",
18
18
  "dependencies": {
19
- "@google-cloud/aiplatform": "^1.10.1",
20
- "@google-cloud/automl": "^2.4.2",
21
- "@google-cloud/bigquery": "^5.6.0",
22
- "@google-cloud/datastore": "^6.4.0",
23
- "@google-cloud/firestore": "^4.12.2",
24
- "@google-cloud/logging-winston": "^4.0.5",
25
- "@google-cloud/pubsub": "^2.12.0",
26
- "@google-cloud/storage": "^5.8.5",
27
- "gaxios": "^4.3.0",
28
- "google-ads-api": "^8.1.0",
29
- "google-ads-node": "^6.1.3",
30
- "google-auth-library": "^7.1.0",
31
- "googleapis": "^74.2.0",
32
- "soap": "^0.38.0",
33
- "winston": "^3.3.3"
19
+ "@google-cloud/aiplatform": "^1.13.0",
20
+ "@google-cloud/automl": "^2.5.1",
21
+ "@google-cloud/bigquery": "^5.9.2",
22
+ "@google-cloud/datastore": "^6.6.2",
23
+ "@google-cloud/firestore": "^4.15.1",
24
+ "@google-cloud/logging-winston": "^4.1.1",
25
+ "@google-cloud/pubsub": "^2.18.2",
26
+ "@google-cloud/storage": "^5.16.0",
27
+ "gaxios": "^4.3.2",
28
+ "google-ads-api": "^9.0.0",
29
+ "google-ads-node": "^7.0.0",
30
+ "google-auth-library": "^7.10.2",
31
+ "googleapis": "^91.0.0",
32
+ "soap": "^0.43.0",
33
+ "winston": "^3.3.3",
34
+ "lodash": "^4.17.21"
34
35
  },
35
36
  "devDependencies": {
36
37
  "jasmine": "^3.5.0"
@@ -267,7 +267,7 @@ class DoubleClickSearch {
267
267
  batchResult.failedLines = [];
268
268
  batchResult.groupedFailed = {};
269
269
  const errors = new Set();
270
- const messageReg = /.*Details: \[(.*) index=\d* conversionId=.*/;
270
+ const messageReg = /.*Details: \[(.*) index=\d+ conversionId=.*/;
271
271
  const indexReg = /.*index=(\d*) .*/;
272
272
  errorMessages.forEach((message) => {
273
273
  const errorMessage = messageReg.exec(message);
@@ -41,25 +41,77 @@ const {
41
41
  },
42
42
  } = googleAdsLib;
43
43
  const {GoogleAdsApi} = require('google-ads-api');
44
+ const lodash = require('lodash');
45
+
44
46
  const AuthClient = require('./auth_client.js');
45
47
  const {getLogger, BatchResult,} = require('../components/utils.js');
46
48
 
47
49
  /** @type {!ReadonlyArray<string>} */
48
50
  const API_SCOPES = Object.freeze(['https://www.googleapis.com/auth/adwords',]);
49
51
 
52
+ /**
53
+ * List of properties that will be taken from the data file as elements of a
54
+ * conversion.
55
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/ClickConversion
56
+ * @type {Array<string>}
57
+ */
58
+ const PICKED_PROPERTIES = [
59
+ 'external_attribution_data',
60
+ 'cart_data',
61
+ 'user_identifiers',
62
+ 'gclid',
63
+ 'conversion_action',
64
+ 'conversion_date_time',
65
+ 'conversion_value',
66
+ 'currency_code',
67
+ 'order_id',
68
+ ];
69
+
70
+ /**
71
+ * Kinds of UserIdentifier.
72
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
73
+ * @type {Array<string>}
74
+ */
75
+ const IDENTIFIERS = [
76
+ 'hashed_email',
77
+ 'hashed_phone_number',
78
+ 'mobile_id',
79
+ 'third_party_user_id',
80
+ 'address_info',
81
+ ];
82
+
50
83
  /**
51
84
  * Configuration for uploading click conversions for Google Ads, includes:
52
85
  * gclid, conversion_action, conversion_date_time, conversion_value,
53
- * currency_code, order_id, external_attribution_data
86
+ * currency_code, order_id, external_attribution_data, etc.
87
+ * @see PICKED_PROPERTIES
88
+ *
89
+ * Other properties that will be used to build the conversions but not picked by
90
+ * the value directly including:
91
+ * 1. 'user_identifier_source', source of the user identifier. If there is user
92
+ * identifiers information in the conversion, this property should be set as
93
+ * 'FIRST_PARTY'.
94
+ * @see IDENTIFIERS
95
+ * @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier?hl=en
96
+ * 2. 'custom_variable_tags', the tags of conversion custom variables. To upload
97
+ * custom variables, 'conversion_custom_variable_id' is required rather than the
98
+ * 'tag'. So the invoker is expected to use the function
99
+ * 'getConversionCustomVariableId' to get the ids and pass in as a
100
+ * map(customVariables) of <tag, id> pairs before uploading conversions.
101
+ *
54
102
  * @see https://developers.google.com/google-ads/api/reference/rpc/latest/ClickConversion
55
103
  * @typedef {{
104
+ * external_attribution_data: (GoogleAdsApi.ExternalAttributionData|undefined),
105
+ * cart_data: (object|undefined),
56
106
  * gclid: string,
57
107
  * conversion_action: string,
58
108
  * conversion_date_time: string,
59
109
  * conversion_value: number,
60
110
  * currency_code:(string|undefined),
61
111
  * order_id: (string|undefined),
62
- * external_attribution_data: (GoogleAdsApi.ExternalAttributionData|undefined),
112
+ * user_identifier_source:(UserIdentifierSource|undefined),
113
+ * custom_variable_tags:(!Array<string>|undefined),
114
+ * customVariables:(!object<string,string>|undefined),
63
115
  * }}
64
116
  */
65
117
  let ClickConversionConfig;
@@ -219,10 +271,8 @@ class GoogleAds {
219
271
  */
220
272
  return async (lines, batchId) => {
221
273
  /** @type {!Array<ClickConversionConfig>} */
222
- const conversions = lines.map((line) => {
223
- const record = JSON.parse(line);
224
- return Object.assign({}, adsConfig, record);
225
- });
274
+ const conversions = lines.map(
275
+ (line) => buildClickConversionFromLine(line, adsConfig, customerId));
226
276
  /** @const {BatchResult} */
227
277
  const batchResult = {
228
278
  result: true,
@@ -346,7 +396,6 @@ class GoogleAds {
346
396
  * failure.
347
397
  * groupedFailed - a hashmap of failed the lines. The key is the reason, the
348
398
  * value is the array of failed lines due to this reason.
349
- * TODO: Confirm how to surface and handle groupedFailed.
350
399
  * @param {!BatchResult} batchResult
351
400
  * @param {!Array<!GoogleAdsFailure>} failures
352
401
  * @param {!Array<string>} lines The original input data.
@@ -404,6 +453,26 @@ class GoogleAds {
404
453
  return customer.conversionUploads.uploadClickConversions(request);
405
454
  }
406
455
 
456
+ /**
457
+ * Returns the id of Conversion Custom Variable with the given tag.
458
+ * @param {string} tag Custom Variable tag.
459
+ * @param {string} customerId
460
+ * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
461
+ * @return {Promise<number|undefined>} Returns undefined if can't find tag.
462
+ */
463
+ async getConversionCustomVariableId(tag, customerId, loginCustomerId) {
464
+ const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
465
+ const customVariables = await customer.query(`
466
+ SELECT conversion_custom_variable.id,
467
+ conversion_custom_variable.tag
468
+ FROM conversion_custom_variable
469
+ WHERE conversion_custom_variable.tag = "${tag}" LIMIT 1
470
+ `);
471
+ if (customVariables.length > 0) {
472
+ return customVariables[0].conversion_custom_variable.id;
473
+ }
474
+ }
475
+
407
476
  /**
408
477
  * Returns the function to send out a request to Google Ads API with
409
478
  * user ids for Customer Match upload
@@ -535,6 +604,43 @@ class GoogleAds {
535
604
 
536
605
  }
537
606
 
607
+ /**
608
+ * Returns a conversion object based the given config and line data.
609
+ * @param {string} line A JSON string of a conversion data.
610
+ * @param {ClickConversionConfig} config Default click conversion params
611
+ * @param {string} customerId
612
+ * @return {object} A conversion
613
+ */
614
+ const buildClickConversionFromLine = (line, config, customerId) => {
615
+ const {customVariables, user_identifier_source} = config;
616
+ const record = JSON.parse(line);
617
+ const conversion = lodash.merge(lodash.pick(config, PICKED_PROPERTIES),
618
+ lodash.pick(record, PICKED_PROPERTIES));
619
+ if (customVariables) {
620
+ const tags = Object.keys(customVariables);
621
+ conversion.custom_variables = tags.map((tag) => {
622
+ return {
623
+ conversion_custom_variable:
624
+ `customers/${customerId}/conversionCustomVariables/${customVariables[tag]}`,
625
+ value: record[tag],
626
+ };
627
+ });
628
+ }
629
+ const user_identifiers = [];
630
+ IDENTIFIERS.forEach((identifier) => {
631
+ if (record[identifier]) {
632
+ user_identifiers.push({
633
+ user_identifier_source,
634
+ [identifier]: record[identifier],
635
+ });
636
+ }
637
+ });
638
+ if (user_identifiers.length > 0) {
639
+ conversion.user_identifiers = user_identifiers;
640
+ }
641
+ return conversion;
642
+ }
643
+
538
644
  module.exports = {
539
645
  ClickConversionConfig,
540
646
  CustomerMatchRecord,
@@ -542,4 +648,5 @@ module.exports = {
542
648
  GoogleAds,
543
649
  ReportQueryConfig,
544
650
  GoogleAdsField,
651
+ buildClickConversionFromLine,
545
652
  };
@@ -20,6 +20,7 @@
20
20
  'use strict';
21
21
 
22
22
  const {request} = require('gaxios');
23
+ const lodash = require('lodash');
23
24
  const {
24
25
  getLogger,
25
26
  SendSingleBatch,
@@ -97,7 +98,7 @@ class MeasurementProtocolGA4 {
97
98
  this.logger.warn(
98
99
  "Only one line data expected. Will only send the first line.");
99
100
  }
100
- const hit = Object.assign({}, config.requestBody, JSON.parse(line));
101
+ const hit = lodash.merge({}, config.requestBody, JSON.parse(line));
101
102
 
102
103
  const requestOptions = {
103
104
  method: 'POST',
@@ -19,7 +19,13 @@
19
19
  'use strict';
20
20
 
21
21
  const {google} = require('googleapis');
22
- const {Schema$Channel, Schema$Video, Schema$CommentThread} = google.youtube;
22
+ const {
23
+ Schema$Channel,
24
+ Schema$Video,
25
+ Schema$CommentThread,
26
+ Schema$Playlist,
27
+ Schema$Search,
28
+ } = google.youtube;
23
29
  const AuthClient = require('./auth_client.js');
24
30
  const {getLogger} = require('../components/utils.js');
25
31
 
@@ -83,6 +89,62 @@ let ListVideosConfig;
83
89
  */
84
90
  let ListCommentThreadsConfig;
85
91
 
92
+ /**
93
+ * Configuration for listing youtube play list.
94
+ * @see https://developers.google.com/youtube/v3/docs/Playlists/list
95
+ * @typedef {{
96
+ * part: Array<string>,
97
+ * channelId: (string|undefined),
98
+ * id: (string|undefined),
99
+ * mine: (boolean|undefined),
100
+ * hl: (string|undefined),
101
+ * maxResults: (unsigned integer|undefined),
102
+ * onBehalfOfContentOwner: (string|undefined),
103
+ * onBehalfOfContentOwnerChannel: (string|undefined),
104
+ * pageToken: (string|undefined)
105
+ * }}
106
+ */
107
+ let ListPlaylistConfig;
108
+
109
+ /**
110
+ * Configuration for listing youtube search results.
111
+ * @see https://developers.google.com/youtube/v3/docs/search/list
112
+ * @typedef {{
113
+ * part: Array<string>,
114
+ * forContentOwner: (boolean|undefined),
115
+ * forDeveloper: (boolean|undefined),
116
+ * forMine: (boolean|undefined),
117
+ * relatedToVideoId: (string|undefined),
118
+ * channelId: (string|undefined),
119
+ * channelType: (string|undefined),
120
+ * eventType: (string|undefined),
121
+ * location: (string|undefined),
122
+ * locationRadius: (string|undefined),
123
+ * maxResults: (unsigned integer|undefined),
124
+ * onBehalfOfContentOwner: (string|undefined),
125
+ * order: (string|undefined),
126
+ * pageToken: (string|undefined),
127
+ * publishedAfter: (datetime|undefined),
128
+ * publishedBefore: (datetime|undefined),
129
+ * q: (string|undefined),
130
+ * regionCode: (string|undefined),
131
+ * relevanceLanguage: (string|undefined),
132
+ * safeSearch: (string|undefined),
133
+ * topicId: (string|undefined),
134
+ * type: (string|undefined),
135
+ * videoCaption: (string|undefined),
136
+ * videoCategoryId: (string|undefined),
137
+ * videoDefinition: (string|undefined),
138
+ * videoDimension: (string|undefined),
139
+ * videoDuration: (string|undefined),
140
+ * videoEmbeddable: (string|undefined),
141
+ * videoLicense: (string|undefined),
142
+ * videoSyndicated: (string|undefined),
143
+ * videoType: (string|undefined)
144
+ * }}
145
+ */
146
+ let ListSearchConfig;
147
+
86
148
  /**
87
149
  * Youtube API v3 stub.
88
150
  * See: https://developers.google.com/youtube/v3/docs
@@ -90,10 +152,21 @@ let ListCommentThreadsConfig;
90
152
  * https://developers.google.com/youtube/v3/docs/channels/list
91
153
  * Video list type definition, see:
92
154
  * https://developers.google.com/youtube/v3/docs/videos/list
155
+ * CommentThread list type definition, see:
156
+ * https://developers.google.com/youtube/v3/docs/commentThreads/list
157
+ * Playlist list type definition, see:
158
+ * https://developers.google.com/youtube/v3/docs/playlists/list
159
+ * Search list type definition, see:
160
+ * https://developers.google.com/youtube/v3/docs/search/list
93
161
  */
94
162
  class YouTube {
95
- constructor() {
96
- const authClient = new AuthClient(API_SCOPES);
163
+ /**
164
+ * @constructor
165
+ * @param {!Object<string,string>=} env The environment object to hold env
166
+ * variables.
167
+ */
168
+ constructor(env = process.env) {
169
+ const authClient = new AuthClient(API_SCOPES, env);
97
170
  this.auth = authClient.getDefaultAuth();
98
171
  /** @const {!google.youtube} */
99
172
  this.instance = google.youtube({
@@ -179,6 +252,96 @@ class YouTube {
179
252
  throw new Error(errorMsg);
180
253
  }
181
254
  }
255
+
256
+ /**
257
+ * Returns a collection of playlists that match the API request parameters.
258
+ * @see https://developers.google.com/youtube/v3/docs/playlists/list
259
+ * @param {!ListPlaylistConfig} config List playlist configuration.
260
+ * @param {number} resultLimit The limit of the number of results.
261
+ * @param {string} pageToken Token to identify a specific page in the result.
262
+ * @return {!Promise<Array<Schema$Playlist>>}
263
+ */
264
+ async listPlaylists(config, resultLimit = 1000, pageToken = null) {
265
+ if (resultLimit <= 0) return [];
266
+
267
+ const playlistsRequest = Object.assign({
268
+ auth: this.auth,
269
+ }, config, {
270
+ pageToken
271
+ });
272
+
273
+ if (Array.isArray(playlistsRequest.part)) {
274
+ playlistsRequest.part = playlistsRequest.part.join(',');
275
+ }
276
+
277
+ try {
278
+ const response = await this.instance.playlists.list(
279
+ playlistsRequest);
280
+ this.logger.debug('Response: ', response.data);
281
+ if (response.data.nextPageToken) {
282
+ this.logger.debug(
283
+ 'Call youtube playlist:list API agian with Token: ',
284
+ response.data.nextPageToken);
285
+ const nextResponse = await this.listPlaylists(
286
+ config,
287
+ resultLimit - response.data.items.length,
288
+ response.data.nextPageToken);
289
+ return response.data.items.concat(nextResponse);
290
+ }
291
+ return response.data.items;
292
+ } catch (error) {
293
+ const errorMsg =
294
+ `Fail to list playlists: ${JSON.stringify(playlistsRequest)}`;
295
+ this.logger.error('YouTube list playlists failed.', error.message);
296
+ this.logger.debug('Errors in response:', error);
297
+ throw new Error(errorMsg);
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Returns a collection of search results that match the query parameters
303
+ * specified in the API request.
304
+ * @see https://developers.google.com/youtube/v3/docs/search/list
305
+ * @param {!ListSearchConfig} config List search result configuration.
306
+ * @param {number} resultLimit The limit of the number of results.
307
+ * @param {string} pageToken Token to identify a specific page in the result.
308
+ * @return {!Promise<Array<Schema$Search>>}
309
+ */
310
+ async listSearchResults(config, resultLimit = 1000, pageToken = null) {
311
+ if (resultLimit <= 0) return [];
312
+
313
+ const searchRequest = Object.assign({
314
+ auth: this.auth,
315
+ }, config, {
316
+ pageToken
317
+ });
318
+
319
+ if (Array.isArray(searchRequest.part)) {
320
+ searchRequest.part = searchRequest.part.join(',');
321
+ }
322
+
323
+ try {
324
+ const response = await this.instance.search.list(searchRequest);
325
+ this.logger.debug('Response: ', response.data);
326
+ if (response.data.nextPageToken) {
327
+ this.logger.debug(
328
+ 'Call youtube search:list API agian with Token: ',
329
+ response.data.nextPageToken);
330
+ const nextResponse = await this.listSearchResults(
331
+ config,
332
+ resultLimit - response.data.items.length,
333
+ response.data.nextPageToken);
334
+ return response.data.items.concat(nextResponse);
335
+ }
336
+ return response.data.items;
337
+ } catch (error) {
338
+ const errorMsg =
339
+ `Fail to list search results: ${JSON.stringify(searchRequest)}`;
340
+ this.logger.error('YouTube list search results failed.', error.message);
341
+ this.logger.debug('Errors in response:', error);
342
+ throw new Error(errorMsg);
343
+ }
344
+ }
182
345
  }
183
346
 
184
347
  module.exports = {
@@ -186,6 +349,8 @@ module.exports = {
186
349
  ListChannelsConfig,
187
350
  ListVideosConfig,
188
351
  ListCommentThreadsConfig,
352
+ ListPlaylistConfig,
353
+ ListSearchConfig,
189
354
  API_VERSION,
190
355
  API_SCOPES,
191
356
  };
@@ -26,8 +26,11 @@ const {CloudPlatformApis} = require('../apis/cloud_platform_apis.js');
26
26
  /**
27
27
  * The result of a batch of data sent to target API. The batch here means the
28
28
  * data that will be sent out in one single request.
29
+ * Some APIs allows partial failure: it will take those correct data and
30
+ * response with reasons for those failed ones. 'groupedFailed' uses error
31
+ * message as the key, and tthe array of related failed lines(records) as value.
29
32
  * Some APIs upload whole file. In this case, there will be not 'numberOfLines'
30
- * or 'failedLines'.
33
+ * or 'failedLines', etc.
31
34
  * @typedef {{
32
35
  * result: boolean,
33
36
  * numberOfLines: (number|undefined),
@@ -177,6 +180,27 @@ const splitArray = (records, splitSize) => {
177
180
  return results;
178
181
  };
179
182
 
183
+ /**
184
+ * Merges an object of 'groupedFailed into the object 'mergedResult'
185
+ * @param {!BatchResult} mergedResult
186
+ * @param {!BatchResult} groupedFailed
187
+ * @private
188
+ */
189
+ const mergeGroupedFailed_ = (mergedResult, groupedFailed) => {
190
+ if (groupedFailed) {
191
+ const mergedKeys = Object.keys(mergedResult.groupedFailed);
192
+ mergedKeys.forEach((key) => {
193
+ mergedResult.groupedFailed[key] =
194
+ mergedResult.groupedFailed[key].concat(groupedFailed[key]);
195
+ });
196
+ Object.keys(groupedFailed)
197
+ .filter((key) => mergedKeys.indexOf(key) < 0)
198
+ .forEach((key) => {
199
+ mergedResult.groupedFailed[key] = groupedFailed[key];
200
+ });
201
+ }
202
+ }
203
+
180
204
  /**
181
205
  * Merges an array of API results (BatchResult) in to a single one.
182
206
  *
@@ -198,8 +222,8 @@ const mergeBatchResults = (batchResults, batchPrefix) => {
198
222
  const {
199
223
  result,
200
224
  numberOfLines,
201
- failedLines,
202
- errors,
225
+ failedLines = [],
226
+ errors = [],
203
227
  groupedFailed,
204
228
  } = batchResult;
205
229
  if (logger.isDebugEnabled()) {
@@ -213,22 +237,13 @@ const mergeBatchResults = (batchResults, batchPrefix) => {
213
237
  }
214
238
  mergedResult.result = mergedResult.result && result;
215
239
  mergedResult.numberOfLines += numberOfLines;
216
- mergedResult.failedLines = mergedResult.failedLines.concat(
217
- failedLines || []);
218
- mergedResult.errors = mergedResult.errors.concat(errors || []);
219
- if (groupedFailed) {
220
- const mergedKeys = Object.keys(mergedResult.groupedFailed);
221
- mergedKeys.forEach((key) => {
222
- mergedResult.groupedFailed[key] =
223
- mergedResult.groupedFailed[key].concat(groupedFailed[key]);
224
- });
225
- Object.keys(groupedFailed)
226
- .filter((key) => mergedKeys.indexOf(key) < 0)
227
- .forEach((key) => {
228
- mergedResult.groupedFailed[key] = groupedFailed[key];
229
- })
230
- }
231
-
240
+ mergedResult.failedLines = mergedResult.failedLines.concat(failedLines);
241
+ errors.forEach((error) => {
242
+ if (mergedResult.errors.indexOf(error) === -1) {
243
+ mergedResult.errors.push(error);
244
+ }
245
+ });
246
+ mergeGroupedFailed_(mergedResult, groupedFailed);
232
247
  });
233
248
  return mergedResult;
234
249
  };