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

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.
@@ -18,20 +18,24 @@
18
18
 
19
19
  'use strict';
20
20
 
21
+ const { request: gaxiosRequest } = require('gaxios');
21
22
  const { google } = require('googleapis');
22
- const AuthClient = require('./auth_client.js');
23
+ const { GoogleApiClient } = require('./base/google_api_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
  /**
31
35
  * Search Ads 360 Reporting API stub.
32
36
  * See: https://developers.google.com/search-ads/reporting/api/reference/release-notes
33
37
  */
34
- class SearchAds {
38
+ class SearchAds extends GoogleApiClient {
35
39
 
36
40
  /**
37
41
  * @constructor
@@ -39,10 +43,21 @@ class SearchAds {
39
43
  * variables.
40
44
  */
41
45
  constructor(env = process.env) {
42
- this.authClient = new AuthClient(API_SCOPES, env);
46
+ super(env);
47
+ this.googleApi = 'searchads360';
43
48
  this.logger = getLogger('API.SA');
44
49
  }
45
50
 
51
+ /** @override */
52
+ getScope() {
53
+ return API_SCOPES;
54
+ }
55
+
56
+ /** @override */
57
+ getVersion() {
58
+ return API_VERSION;
59
+ }
60
+
46
61
  /**
47
62
  * Prepares the Search Ads 360 Reporting API instance.
48
63
  * OAuth 2.0 application credentials is required for calling this API.
@@ -55,83 +70,130 @@ class SearchAds {
55
70
  * @return {!google.searchads360}
56
71
  * @private
57
72
  */
58
- async getApiClient_(loginCustomerId) {
59
- this.logger.debug(`Initialized Search Ads reporting instance for ${loginCustomerId}`);
60
- this.searchads360 = google.searchads360({
61
- version: API_VERSION,
62
- auth: await this.getAuth_(),
63
- headers: { 'login-customer-id': loginCustomerId },
64
- });
65
- return this.searchads360;
73
+ async getApiClient(loginCustomerId) {
74
+ this.logger.debug(`Initialized SA reporting for ${loginCustomerId}`);
75
+ const options = {
76
+ version: this.getVersion(),
77
+ auth: await this.getAuth(),
78
+ };
79
+ if (loginCustomerId) {
80
+ options.headers = { 'login-customer-id': getCleanCid(loginCustomerId) };
81
+ }
82
+ return google.searchads360(options);
66
83
  }
67
84
 
68
85
  /**
69
- * Gets the auth object.
70
- * @return {!Promise<{!OAuth2Client|!JWT|!Compute}>}
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
90
+ * @param {string} customerId
91
+ * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
92
+ * @param {string} query
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
71
97
  */
72
- async getAuth_() {
73
- if (this.auth) return this.auth;
74
- await this.authClient.prepareCredentials();
75
- this.auth = this.authClient.getDefaultAuth();
76
- return this.auth;
98
+ async getPaginatedReport(customerId, loginCustomerId, query, options = {}) {
99
+ const searchads = await this.getApiClient(loginCustomerId);
100
+ const requestBody = Object.assign({
101
+ query,
102
+ pageSize: 10000,
103
+ }, options);
104
+ const response = await searchads.customers.searchAds360.search({
105
+ customerId: getCleanCid(customerId),
106
+ requestBody,
107
+ });
108
+ return response.data;
77
109
  }
78
110
 
79
111
  /**
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
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.
87
116
  * @param {string} customerId
88
117
  * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
89
118
  * @param {string} query
90
- * @return {!ReadableStream}
119
+ * @return {!Promise<stream>}
120
+ * @see https://developers.google.com/search-ads/reporting/api/reference/rest/search
91
121
  */
92
- async streamReport(customerId, loginCustomerId, query) {
93
- const searchads = await this.getApiClient_(loginCustomerId);
94
- const response = await searchads.customers.searchAds360.search({
95
- customerId,
96
- requestBody: { query },
97
- }, { responseType: 'stream' });
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),
127
+ });
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);
98
137
  return response.data;
99
138
  }
100
139
 
101
140
  /**
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
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.
106
145
  * @param {string} customerId
107
146
  * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
108
- * @param {string} query
109
- * @return {!SearchAds360Field}
110
- * @see https://developers.google.com/search-ads/reporting/api/reference/rest/v0/searchAds360Fields#SearchAds360Field
147
+ * @param {string} query A Google Ads Query string.
148
+ * @param {boolean=} snakeCase Output JSON objects in snake_case.
149
+ * @return {!Promise<stream>}
111
150
  */
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 },
117
- });
118
- return response.data.results;
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) {
130
- const searchads = await this.getApiClient_();
131
- const response = await searchads.searchAds360Fields.get({ resourceName });
168
+ async getReportField(fieldName) {
169
+ const searchads = await this.getApiClient();
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
@@ -141,7 +203,7 @@ class SearchAds {
141
203
  * @see https://developers.google.com/search-ads/reporting/api/reference/rest/v0/customers.customColumns#CustomColumn
142
204
  */
143
205
  async listCustomColumns(customerId, loginCustomerId) {
144
- const searchads = await this.getApiClient_(loginCustomerId);
206
+ const searchads = await this.getApiClient(loginCustomerId);
145
207
  const response = await searchads.customers.customColumns.list({ customerId });
146
208
  return response.data.customColumns;
147
209
  }
@@ -157,7 +219,7 @@ class SearchAds {
157
219
  */
158
220
  async getCustomColumn(columnId, customerId, loginCustomerId) {
159
221
  const resourceName = `customers/${customerId}/customColumns/${columnId}`;
160
- const searchads = await this.getApiClient_(loginCustomerId);
222
+ const searchads = await this.getApiClient(loginCustomerId);
161
223
  const response = await searchads.customers.customColumns.get({ resourceName });
162
224
  return response.data;
163
225
  }
@@ -19,6 +19,7 @@
19
19
  'use strict';
20
20
 
21
21
  const {google} = require('googleapis');
22
+ const { GoogleApiClient } = require('./base/google_api_client.js');
22
23
  const {Params$Resource$Spreadsheets$Get} = google.sheets;
23
24
  const AuthClient = require('./auth_client.js');
24
25
  const {getLogger, BatchResult} = require('../components/utils.js');
@@ -63,7 +64,7 @@ let DimensionRange;
63
64
  /**
64
65
  * Google Spreadsheets API v4 stub.
65
66
  */
66
- class Spreadsheets {
67
+ class Spreadsheets extends GoogleApiClient {
67
68
  /**
68
69
  * Init Spreadsheets API client.
69
70
  * @param {string} spreadsheetId
@@ -71,29 +72,21 @@ class Spreadsheets {
71
72
  * variables.
72
73
  */
73
74
  constructor(spreadsheetId, env = process.env) {
75
+ super(env);
76
+ this.googleApi = 'sheets';
74
77
  /** @const {string} */
75
78
  this.spreadsheetId = spreadsheetId;
76
- this.authClient = new AuthClient(API_SCOPES, env);
77
- /**
78
- * Logger object from 'log4js' package where this type is not exported.
79
- */
80
79
  this.logger = getLogger('API.GS');
81
80
  }
82
81
 
83
- /**
84
- * Prepares the Google Sheets instance.
85
- * @return {!google.sheets}
86
- * @private
87
- */
88
- async getApiClient_() {
89
- if (this.sheets) return this.sheets;
90
- await this.authClient.prepareCredentials();
91
- this.logger.debug(`Initialized ${this.constructor.name} instance.`);
92
- this.sheets = google.sheets({
93
- version: API_VERSION,
94
- auth: this.authClient.getDefaultAuth(),
95
- });
96
- return this.sheets;
82
+ /** @override */
83
+ getScope() {
84
+ return API_SCOPES;
85
+ }
86
+
87
+ /** @override */
88
+ getVersion() {
89
+ return API_VERSION;
97
90
  }
98
91
 
99
92
  /**
@@ -108,7 +101,7 @@ class Spreadsheets {
108
101
  spreadsheetId: this.spreadsheetId,
109
102
  ranges: sheetName,
110
103
  };
111
- const sheets = await this.getApiClient_();
104
+ const sheets = await this.getApiClient();
112
105
  const response = await sheets.spreadsheets.get(request);
113
106
  const sheet = response.data.sheets[0];
114
107
  this.logger.debug(`Get sheet[${sheetName}]: `, sheet);
@@ -127,7 +120,7 @@ class Spreadsheets {
127
120
  range: sheetName,
128
121
  };
129
122
  try {
130
- const sheets = await this.getApiClient_();
123
+ const sheets = await this.getApiClient();
131
124
  const response = await sheets.spreadsheets.values.clear(request);
132
125
  const data = response.data;
133
126
  this.logger.debug(`Clear sheet[${sheetName}}]: `, data);
@@ -180,7 +173,7 @@ class Spreadsheets {
180
173
  ranges: sheetName,
181
174
  };
182
175
  try {
183
- const sheets = await this.getApiClient_();
176
+ const sheets = await this.getApiClient();
184
177
  const response = await sheets.spreadsheets.get(request);
185
178
  const sheet = response.data.sheets[0];
186
179
  const sheetId = sheet.properties.sheetId;
@@ -232,7 +225,7 @@ class Spreadsheets {
232
225
  numberOfLines: data.trim().split('\n').length,
233
226
  };
234
227
  try {
235
- const sheets = await this.getApiClient_();
228
+ const sheets = await this.getApiClient();
236
229
  const response = await sheets.spreadsheets.batchUpdate(request);
237
230
  const data = response.data;
238
231
  this.logger.debug(`Batch[${batchId}] uploaded: `, data);
@@ -19,6 +19,7 @@
19
19
  'use strict';
20
20
 
21
21
  const {google} = require('googleapis');
22
+ const { GoogleApiClient } = require('./base/google_api_client.js');
22
23
  const {
23
24
  Schema$Channel,
24
25
  Schema$Video,
@@ -26,8 +27,7 @@ const {
26
27
  Schema$Playlist,
27
28
  Schema$Search,
28
29
  } = google.youtube;
29
- const AuthClient = require('./auth_client.js');
30
- const {getLogger} = require('../components/utils.js');
30
+ const { getLogger } = require('../components/utils.js');
31
31
 
32
32
  const API_SCOPES = Object.freeze([
33
33
  'https://www.googleapis.com/auth/youtube.force-ssl'
@@ -159,34 +159,26 @@ let ListSearchConfig;
159
159
  * Search list type definition, see:
160
160
  * https://developers.google.com/youtube/v3/docs/search/list
161
161
  */
162
- class YouTube {
162
+ class YouTube extends GoogleApiClient {
163
163
  /**
164
164
  * @constructor
165
165
  * @param {!Object<string,string>=} env The environment object to hold env
166
166
  * variables.
167
167
  */
168
168
  constructor(env = process.env) {
169
- this.authClient = new AuthClient(API_SCOPES, env);
170
- /**
171
- * Logger object from 'log4js' package where this type is not exported.
172
- */
169
+ super(env);
170
+ this.googleApi = 'youtube';
173
171
  this.logger = getLogger('API.YT');
174
172
  }
175
173
 
176
- /**
177
- * Prepares the Google YouTube instance.
178
- * @return {!google.youtube}
179
- * @private
180
- */
181
- async getApiClient_() {
182
- if (this.youtube) return this.youtube;
183
- await this.authClient.prepareCredentials();
184
- this.logger.debug(`Initialized ${this.constructor.name} instance.`);
185
- this.youtube = google.youtube({
186
- version: API_VERSION,
187
- auth: this.authClient.getDefaultAuth(),
188
- });
189
- return this.youtube;
174
+ /** @override */
175
+ getScope() {
176
+ return API_SCOPES;
177
+ }
178
+
179
+ /** @override */
180
+ getVersion() {
181
+ return API_VERSION;
190
182
  }
191
183
 
192
184
  /**
@@ -200,7 +192,7 @@ class YouTube {
200
192
  const channelListRequest = Object.assign({}, config);
201
193
  channelListRequest.part = channelListRequest.part.join(',')
202
194
  try {
203
- const youtube = await this.getApiClient_();
195
+ const youtube = await this.getApiClient();
204
196
  const response = await youtube.channels.list(channelListRequest);
205
197
  this.logger.debug('Response: ', response);
206
198
  return response.data.items;
@@ -223,7 +215,7 @@ class YouTube {
223
215
  const videoListRequest = Object.assign({}, config);
224
216
  videoListRequest.part = videoListRequest.part.join(',')
225
217
  try {
226
- const youtube = await this.getApiClient_();
218
+ const youtube = await this.getApiClient();
227
219
  const response = await youtube.videos.list(videoListRequest);
228
220
  this.logger.debug('Response: ', response);
229
221
  return response.data.items;
@@ -247,7 +239,7 @@ class YouTube {
247
239
  const commentThreadsRequest = Object.assign({}, config);
248
240
  commentThreadsRequest.part = commentThreadsRequest.part.join(',')
249
241
  try {
250
- const youtube = await this.getApiClient_();
242
+ const youtube = await this.getApiClient();
251
243
  const response = await youtube.commentThreads.list(commentThreadsRequest);
252
244
  this.logger.debug('Response: ', response.data);
253
245
  return response.data.items;
@@ -282,7 +274,7 @@ class YouTube {
282
274
  }
283
275
 
284
276
  try {
285
- const youtube = await this.getApiClient_();
277
+ const youtube = await this.getApiClient();
286
278
  const response = await youtube.playlists.list(playlistsRequest);
287
279
  this.logger.debug('Response: ', response.data);
288
280
  if (response.data.nextPageToken) {
@@ -324,7 +316,7 @@ class YouTube {
324
316
  }
325
317
 
326
318
  try {
327
- const youtube = await this.getApiClient_();
319
+ const youtube = await this.getApiClient();
328
320
  const response = await youtube.search.list(searchRequest);
329
321
  this.logger.debug('Response: ', response.data);
330
322
  if (response.data.nextPageToken) {
@@ -75,7 +75,7 @@ class DatastoreModeAccess {
75
75
  */
76
76
  getKey(id) {
77
77
  const keyPath = [this.kind];
78
- if (id) keyPath.push(isNaN(id) ? id : parseInt(id));
78
+ if (id) keyPath.push(isNaN(id) ? id : this.datastore.int(id));
79
79
  return this.datastore.key({
80
80
  namespace: this.namespace,
81
81
  path: keyPath,
@@ -116,10 +116,11 @@ class DatastoreModeAccess {
116
116
  const key = this.getKey(id);
117
117
  const apiResponse =
118
118
  await this.datastore.save({ key, data, excludeLargeProperties: true });
119
- // Default key in Datastore is a number in response like following.
119
+ // Default key in Datastore is an int as string in response. It could be
120
+ // larger than JavaScript max safe integer, so keep it as string here.
120
121
  // With a given id, the key in response is null.
121
122
  const updatedId = id !== undefined ? id
122
- : +apiResponse[0].mutationResults[0].key.path[0].id;
123
+ : apiResponse[0].mutationResults[0].key.path[0].id;
123
124
  this.logger.debug(`Result of saving ${updatedId}@${this.kind}: `,
124
125
  JSON.stringify(apiResponse));
125
126
  // Datastore has a delay to write entity. This method only returns id
@@ -487,9 +487,9 @@ const extractObject = (paths) => {
487
487
  return (sourceObject) => {
488
488
  const output = {};
489
489
  paths.forEach((path) => {
490
- const [value, owner, property] = path.split('.')
490
+ const [value, owner, property] = path.trim().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
  });
@@ -509,7 +509,7 @@ const getObjectByPath = (obj, paths) => {
509
509
  paths.split('.').filter((key) => !!key).forEach((key) => {
510
510
  instance = instance[key];
511
511
  if (!instance) {
512
- console.error('Fail to get function containter', paths);
512
+ console.error('Fail to get element from path:', paths);
513
513
  return instance;
514
514
  }
515
515
  });
@@ -591,7 +591,24 @@ const changeObjectNamingFromLowerCamelToSnake = (obj) => {
591
591
  } else {
592
592
  return obj;
593
593
  }
594
- }
594
+ };
595
+
596
+ /**
597
+ * Generates a function that can convert a given JSON object to a JSON string
598
+ * with only specified fields(fieldMask), in specified naming convention.
599
+ * @param {string} fieldMask The 'fieldMask' string from response.
600
+ * @param {boolean=} snakeCase Whether or not output JSON in snake naming.
601
+ */
602
+ const getFilterAndStringifyFn = (fieldMask, snakeCase = false) => {
603
+ const extractor = extractObject(
604
+ Array.isArray(fieldMask) ? fieldMask : fieldMask.split(','));
605
+ return (originalObject) => {
606
+ const extracted = extractor(originalObject);
607
+ const generatedObject = snakeCase
608
+ ? changeObjectNamingFromLowerCamelToSnake(extracted) : extracted;
609
+ return JSON.stringify(generatedObject);
610
+ };
611
+ };
595
612
 
596
613
  /**
597
614
  * Returns the response data for a HTTP request. It will retry the specific
@@ -619,7 +636,7 @@ const requestWithRetry = async (options, logger = console, retryTimes = 3) => {
619
636
  logger.error(`Request ${JSON.stringify(options)}`, error);
620
637
  }
621
638
  } while (processedTimes <= retryTimes)
622
- }
639
+ };
623
640
 
624
641
  // noinspection JSUnusedAssignment
625
642
  module.exports = {
@@ -641,5 +658,6 @@ module.exports = {
641
658
  changeNamingFromLowerCamelToSnake,
642
659
  changeObjectNamingFromSnakeToLowerCamel,
643
660
  changeObjectNamingFromLowerCamelToSnake,
661
+ getFilterAndStringifyFn,
644
662
  requestWithRetry,
645
663
  };