@google-cloud/nodejs-common 1.4.0 → 1.5.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/index.js CHANGED
@@ -24,6 +24,7 @@ exports.automl = require('./src/components/automl.js');
24
24
  exports.firestore = require('./src/components/firestore/index.js');
25
25
  exports.pubsub = require('./src/components/pubsub.js');
26
26
  exports.scheduler = require('./src/components/scheduler.js');
27
+ exports.secretmanager = require('./src/components/secret_manager.js');
27
28
  exports.storage = require('./src/components/storage.js');
28
29
  exports.utils = require('./src/components/utils.js');
29
30
  exports.vertexai = require('./src/components/vertex_ai.js');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@google-cloud/nodejs-common",
3
- "version": "1.4.0",
3
+ "version": "1.5.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",
@@ -25,6 +25,7 @@
25
25
  "@google-cloud/pubsub": "^3.2.0",
26
26
  "@google-cloud/storage": "^6.5.2",
27
27
  "@google-cloud/scheduler": "^3.0.4",
28
+ "@google-cloud/secret-manager": "^4.1.3",
28
29
  "gaxios": "^5.0.2",
29
30
  "google-ads-api": "^11.1.0",
30
31
  "google-ads-node": "^9.1.0",
@@ -41,12 +41,22 @@ class AdsDataHub {
41
41
  * variables.
42
42
  */
43
43
  constructor(options, customerId = undefined, env = process.env) {
44
- const authClient = new AuthClient(API_SCOPES, env);
45
- this.auth = authClient.getDefaultAuth();
44
+ this.authClient = new AuthClient(API_SCOPES, env);
46
45
  /** @const{GaxiosOptions} */ this.options = options || {};
47
46
  /** @const{string|undefined=} */ this.customerId = customerId;
48
47
  }
49
48
 
49
+ /**
50
+ * Gets the auth object.
51
+ * @return {!Promise<{!OAuth2Client|!JWT|!Compute}>}
52
+ */
53
+ async getAuth_() {
54
+ if (this.auth) return this.auth;
55
+ await this.authClient.prepareCredentials();
56
+ this.auth = this.authClient.getDefaultAuth();
57
+ return this.auth;
58
+ }
59
+
50
60
  /**
51
61
  * Query name has the form
52
62
  * 'customers/[customerId]/analysisQueries/[resource_id]'. For better
@@ -82,7 +92,8 @@ class AdsDataHub {
82
92
  * @private
83
93
  */
84
94
  async sendRequestAndReturnResponse_(path, method = 'GET', data = undefined) {
85
- const headers = await this.auth.getRequestHeaders();
95
+ const auth = await this.getAuth_();
96
+ const headers = await auth.getRequestHeaders();
86
97
  const url = this.getRequestBaseUrl_() + path;
87
98
  const response = await request(/** @type {GaxiosOptions} */
88
99
  Object.assign({}, this.options, {
@@ -62,15 +62,24 @@ class Analytics {
62
62
  * variables.
63
63
  */
64
64
  constructor(env = process.env) {
65
- const authClient = new AuthClient(API_SCOPES, env);
66
- const auth = authClient.getDefaultAuth();
67
- /** @type {!google.analytics} */
68
- this.instance = google.analytics({
65
+ this.authClient = new AuthClient(API_SCOPES, env);
66
+ this.logger = getLogger('API.GA');
67
+ }
68
+
69
+ /**
70
+ * Prepares the Google Analytics instace.
71
+ * @return {!google.analytics}
72
+ * @private
73
+ */
74
+ async getApiClient_() {
75
+ if (this.analytics) return this.analytics;
76
+ await this.authClient.prepareCredentials();
77
+ this.logger.debug(`Initialized ${this.constructor.name} instance.`);
78
+ this.analytics = google.analytics({
69
79
  version: API_VERSION,
70
- auth,
80
+ auth: this.authClient.getDefaultAuth(),
71
81
  });
72
- this.logger = getLogger('API.GA');
73
- this.logger.debug(`Init ${this.constructor.name} with Debug Mode.`);
82
+ return this.analytics;
74
83
  }
75
84
 
76
85
  /**
@@ -92,7 +101,9 @@ class Analytics {
92
101
  }
93
102
  },
94
103
  config);
95
- const response = await this.instance.management.uploads.uploadData(
104
+
105
+ const analytics = await this.getApiClient_();
106
+ const response = await analytics.management.uploads.uploadData(
96
107
  uploadConfig);
97
108
  this.logger.debug('Configuration: ', config);
98
109
  this.logger.debug('Upload Data: ', data);
@@ -140,7 +151,8 @@ class Analytics {
140
151
  * @return {!Promise<!Schema$Upload>} Updated data import Job status.
141
152
  */
142
153
  async checkJobStatus(jobConfig) {
143
- const {data: job} = await this.instance.management.uploads.get(jobConfig);
154
+ const analytics = await this.getApiClient_();
155
+ const { data: job } = await analytics.management.uploads.get(jobConfig);
144
156
  if (job.status !== 'PENDING') return job;
145
157
  this.logger.debug(
146
158
  `GA Data Import Job[${jobConfig.uploadId}] is not finished.`);
@@ -157,7 +169,8 @@ class Analytics {
157
169
  * @return {!Promise<!Array<string>>}
158
170
  */
159
171
  async listAccounts() {
160
- const response = await this.instance.management.accounts.list();
172
+ const analytics = await this.getApiClient_();
173
+ const response = await analytics.management.accounts.list();
161
174
  return response.data.items.map(
162
175
  (account) => `Account id: ${account.name}[${account.id}]`
163
176
  );
@@ -169,7 +182,8 @@ class Analytics {
169
182
  * @return {!Promise<!Array<Object>>}
170
183
  */
171
184
  async listUploads(config) {
172
- const response = await this.instance.management.uploads.list(config);
185
+ const analytics = await this.getApiClient_();
186
+ const response = await analytics.management.uploads.list(config);
173
187
  return response.data.items;
174
188
  }
175
189
 
@@ -189,7 +203,8 @@ class Analytics {
189
203
  const request = Object.assign({}, config, {
190
204
  resource: {customDataImportUids},
191
205
  });
192
- await this.instance.management.uploads.deleteUploadData(request);
206
+ const analytics = await this.getApiClient_();
207
+ await analytics.management.uploads.deleteUploadData(request);
193
208
  this.logger.debug('Delete uploads: ', customDataImportUids);
194
209
  }
195
210
 
@@ -22,7 +22,12 @@
22
22
  const fs = require('fs');
23
23
  const path = require('path');
24
24
  const {GoogleAuth, OAuth2Client, JWT, Compute} = require('google-auth-library');
25
- /** Environment variable name for OAuth2 key file location. */
25
+ const { SecretManager } = require('../components/secret_manager.js');
26
+ const { getLogger } = require('../components/utils.js');
27
+
28
+ /** Environment variable name for Secret Manager secret name. */
29
+ const DEFAULT_ENV_SECRET = 'SECRET_NAME';
30
+ /** Environment variable name for OAuth2 token file location. */
26
31
  const DEFAULT_ENV_OAUTH = 'OAUTH2_TOKEN_JSON';
27
32
  /** Environment variable name for Service account key file location. */
28
33
  const DEFAULT_ENV_KEYFILE = 'API_SERVICE_ACCOUNT';
@@ -37,15 +42,25 @@ const DEFAULT_ENV_KEYFILE = 'API_SERVICE_ACCOUNT';
37
42
  *
38
43
  * There are two use cases for this authentication helper class:
39
44
  * 1. The user only has OAuth access due to some reasons, so ADC can't be used;
40
- * 2. ADC doesn't work for some external APIs. User-managed service account is
41
- * the only option. We have to manually initiate the Authentication object by
42
- * specifying the key files.
45
+ * 2. User-managed service account is requried for external APIs for some
46
+ * specific considerations, e.g. security. In this case, a file based key file
47
+ * can be used to generate a JWT auth client.
43
48
  *
44
- * To solve these challenges, this class tries to probe the OAuth key file,
45
- * then service account key file based on the environment variables. It will
49
+ * To solve these challenges, this class tries to probe the settings from
50
+ * enviroment variables, starts from the name of secret (Secret Manager), OAuth
51
+ * token file (deprecated), then service account key file (deprecated). It will
46
52
  * fallback to ADC if those probing failed.
53
+ * Note, Secret Manager is the recommanded way to store tokens because it is a
54
+ * secure and convenient central storage system to manage access across Google
55
+ * Cloud.
56
+ *
57
+ * The recommended environment variable is:
58
+ * SECRET_NAME: the name of secret. The secret can be a oauth token file or a
59
+ * service account key file. This env var is used to offer a global auth for a
60
+ * solution. If different auths are required, the value of passed `env` can be
61
+ * set
47
62
  *
48
- * The expected environment variables are:
63
+ * Alternative environment variable but not recommended for prod environment:
49
64
  * OAUTH2_TOKEN_JSON : the oauth token key files, refresh token and proper API
50
65
  * scopes are expected here.
51
66
  * API_SERVICE_ACCOUNT : the service account key file. The email in the key file
@@ -55,35 +70,81 @@ class AuthClient {
55
70
  /**
56
71
  * Create a new instance with given API scopes.
57
72
  * @param {string|!Array<string>|!ReadonlyArray<string>} scopes
73
+ * @param {!Object<string,string>=} overwrittenEnv The key-value pairs to
74
+ * over write env variables.
75
+ */
76
+ constructor(scopes, overwrittenEnv = {}) {
77
+ this.logger = getLogger('AUTH');
78
+ this.scopes = scopes;
79
+ this.env = Object.assign({}, process.env, overwrittenEnv);
80
+ }
81
+
82
+ /**
83
+ * Prepares the `oauthToken` object and/or `serviceAccountKey` based on the
84
+ * settings in enviroment object.
85
+ * A secret name is preferred to offer the token of the OAuth or key of a
86
+ * service account.
87
+ * To be compatible, this function also checks the env for oauth token file
88
+ * and service account key file if there is no secret name was set in the env.
89
+ */
90
+ async prepareCredentials() {
91
+ if (this.env[DEFAULT_ENV_SECRET]) {
92
+ const secretmanager = new SecretManager({
93
+ projectId: this.env.GCP_PROJECT,
94
+ });
95
+ const secret = await secretmanager.access(this.env[DEFAULT_ENV_SECRET]);
96
+ if (secret) {
97
+ const secretObj = JSON.parse(secret);
98
+ if (secretObj.token) {
99
+ this.oauthToken = secretObj;
100
+ } else {
101
+ this.serviceAccountKey = secretObj;
102
+ }
103
+ this.logger.info(`Get secret from SM ${this.env[DEFAULT_ENV_SECRET]}.`);
104
+ return;
105
+ }
106
+ this.logger.warn(`Cannot find SM ${this.env[DEFAULT_ENV_SECRET]}.`);
107
+ }
108
+ // To be compatible with previous solution.
109
+ const oauthTokenFile = this.getContentFromEnvVar(DEFAULT_ENV_OAUTH);
110
+ if (oauthTokenFile) {
111
+ this.oauthToken = JSON.parse(oauthTokenFile);
112
+ }
113
+ const serviceAccountKeyFile =
114
+ this.getContentFromEnvVar(DEFAULT_ENV_KEYFILE);
115
+ if (serviceAccountKeyFile) {
116
+ this.serviceAccountKey = JSON.parse(serviceAccountKeyFile);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Factory method to offer a prepared AuthClient intance in an async way.
122
+ * @param {string|!Array<string>|!ReadonlyArray<string>} scopes
58
123
  * @param {!Object<string,string>=} env The environment object to hold env
59
124
  * variables.
125
+ * @return {!Promise<!AuthClient>}
60
126
  */
61
- constructor(scopes, env = process.env) {
62
- this.scopes = scopes;
63
- this.oauthTokenFile = getFullPathForEnv(DEFAULT_ENV_OAUTH, env);
64
- this.serviceAccountKeyFile = getFullPathForEnv(DEFAULT_ENV_KEYFILE, env);
127
+ static async build(scopes, env) {
128
+ const instance = new AuthClient(scopes, env);
129
+ await instance.prepareCredentials();
130
+ return instance;
65
131
  }
66
132
 
67
133
  /**
68
134
  * Generates an authentication client of OAuth, JWT or ADC based on the
69
- * environment settings. The authentication method is determined by
70
- * environment variables at runtime. The priorities for different
71
- * authentications are:
72
- * 1. OAuth, return an OAuth client if there is a OAuth key file available.
73
- * 2. JWT, return JWT client if a user managed service account key file is
74
- * available.
135
+ * environment settings. The authentication method is determined by the type
136
+ * of available credentials:
137
+ * 1. OAuth, return an OAuth token if available.
138
+ * 2. JWT, return JWT client if a service account key is available.
75
139
  * 3. ADC if none of these files exists.
76
140
  * @return {!OAuth2Client|!JWT|!Compute}
77
141
  */
78
142
  getDefaultAuth() {
79
- if (typeof this.oauthTokenFile !== 'undefined') {
80
- console.log(`Auth mode OAUTH: ${this.oauthTokenFile}`);
143
+ if (typeof this.oauthToken !== 'undefined') {
81
144
  return this.getOAuth2Client();
82
- } else if (typeof this.serviceAccountKeyFile !== 'undefined') {
83
- console.log(`Auth mode JWT: ${this.serviceAccountKeyFile}`);
145
+ } else if (typeof this.serviceAccountKey !== 'undefined') {
84
146
  return this.getServiceAccount();
85
147
  } else {
86
- console.log(`Auth mode ADC`);
87
148
  return this.getApplicationDefaultCredentials();
88
149
  }
89
150
  }
@@ -100,33 +161,36 @@ class AuthClient {
100
161
  * @return {!Compute|!JWT}
101
162
  */
102
163
  getApplicationDefaultCredentials() {
164
+ this.logger.info(`Mode ADC`);
103
165
  return new GoogleAuth({scopes: this.scopes});
104
166
  }
105
167
 
106
168
  /**
107
169
  * Returns an OAuth2 client based on the given key file.
108
- * @param {string=} keyFile Full path for the OAuth key file.
109
- * `client_id`, `client_secret`, `tokens` are expected in that JSON file.
110
170
  * @return {!OAuth2Client}
111
171
  */
112
- getOAuth2Client(keyFile = this.oauthTokenFile) {
113
- this.ensureFileExisting_(keyFile, 'OAUTH token');
114
- const key = JSON.parse(fs.readFileSync(keyFile).toString());
115
- console.log(`Get OAuth token with client Id: ${key.client_id}`);
116
- const oAuth2Client = new OAuth2Client(key.client_id, key.client_secret);
117
- oAuth2Client.setCredentials({refresh_token: key.token.refresh_token});
172
+ getOAuth2Client() {
173
+ this.ensureCredentialExists_(this.oauthToken, 'OAuth token');
174
+ const { client_id, client_secret, token } = this.oauthToken;
175
+ const oAuth2Client = new OAuth2Client(client_id, client_secret);
176
+ oAuth2Client.setCredentials({ refresh_token: token.refresh_token });
118
177
  return oAuth2Client;
119
178
  }
120
179
 
121
180
  /**
122
181
  * Returns a JWT client based on the given service account key file.
123
- * @param {string=} keyFile Full path for the service account key file.
124
182
  * @return {!JWT}
125
183
  */
126
- getServiceAccount(keyFile = this.serviceAccountKeyFile) {
127
- this.ensureFileExisting_(keyFile, 'Service Account key');
128
- console.log(`Get Service Account's key file: ${keyFile}`);
129
- return new JWT({keyFile, scopes: this.scopes,});
184
+ getServiceAccount() {
185
+ this.ensureCredentialExists_(this.serviceAccountKey, 'Service Account key');
186
+ this.logger.info(`Mode JWT`);
187
+ const { private_key_id, private_key, client_email } = this.serviceAccountKey;
188
+ return new JWT({
189
+ email: client_email,
190
+ key: private_key,
191
+ keyId: private_key_id,
192
+ scopes: this.scopes,
193
+ });
130
194
  }
131
195
 
132
196
  /**
@@ -134,58 +198,57 @@ class AuthClient {
134
198
  * OAuth2 based on the given key file.
135
199
  * Some API library (google-ads-api) doesn't support google-auth-library
136
200
  * directly and needs plain keys.
137
- * @param {string=} keyFile Full path for the OAuth key file.
138
- * `client_id`, `client_secret`, `tokens` are expected in that JSON file.
139
201
  * @return {{
140
202
  * clientId:string,
141
203
  * clientSecret:string,
142
204
  * refreshToken:string,
143
205
  * }}
144
206
  */
145
- getOAuth2Token(keyFile = this.oauthTokenFile) {
146
- this.ensureFileExisting_(keyFile, 'OAuth token');
147
- const key = JSON.parse(fs.readFileSync(keyFile).toString());
207
+ getOAuth2Token() {
208
+ this.ensureCredentialExists_(this.oauthToken, 'OAuth token');
209
+ this.logger.info(`Mode OAUTH`);
210
+ const { client_id, client_secret, token } = this.oauthToken;
148
211
  return {
149
- clientId: key.client_id,
150
- clientSecret: key.client_secret,
151
- refreshToken: key.token.refresh_token,
212
+ clientId: client_id,
213
+ clientSecret: client_secret,
214
+ refreshToken: token.refresh_token,
152
215
  };
153
216
  }
154
217
 
155
218
  /**
156
219
  * Some APIs only support one authorization type, so we have to designate a
157
- * specific auth method. If the related key/token file is not available, the
158
- * auth will fail. The function throws errors with more meaningful message.
159
- * @param {string} file Target file path
220
+ * specific auth method rather than the 'getDefaultAuth'. If the related
221
+ * key/token is not available, the auth should fail. The function throws
222
+ * errors with a meaningful message.
223
+ * @param {object} credential - Credential object.
160
224
  * @param {string} type Key type name, 'OAuth token' or 'Service Account key'
161
225
  * @private
162
226
  */
163
- ensureFileExisting_(file, type) {
164
- if (!file) throw new Error(`Required ${type} file doesn't exist.`);
227
+ ensureCredentialExists_(credential, type) {
228
+ if (!credential) throw new Error(`Required ${type} does not exist.`);
165
229
  }
166
- }
167
230
 
168
- /**
169
- * Returns the full path of a existent file whose path (relative or
170
- * absolute) is the value of the given environment variable.
171
- * @param {string} envName The name of environment variable for the file path.
172
- * @param {!Object<string,string>=} env The environment object to hold env
173
- * variables.
174
- * @return {?string} Full path of the file what set as an environment variable.
175
- */
176
- function getFullPathForEnv(envName, env) {
177
- const envValue = env[envName];
178
- if (typeof envValue === 'undefined') {
179
- console.log(`Env[${envName}] doesn't have a value.`);
180
- } else {
181
- const fullPath = envValue.startsWith('/') ?
182
- envValue :
183
- path.join(__dirname, envValue);
184
- if (fs.existsSync(fullPath)) {
185
- console.log(`Find file '${fullPath}' set in env as [${envName}].`);
186
- return fullPath;
187
- } else {
188
- console.error(`Can't find '${fullPath}' which set in env[${envName}].`);
231
+ /**
232
+ * Returns the content of a file whose path is set as an env variable.
233
+ * The path can be relative or absolute. The function will append current path
234
+ * before the relative path.
235
+ * @param {string} varName The name of environment variable for the file path.
236
+ * @return {?string} Content of the file what set as an environment variable.
237
+ */
238
+ getContentFromEnvVar(varName) {
239
+ const value = this.env[varName];
240
+ if (value) {
241
+ const fullPath = value.startsWith('/')
242
+ ? value
243
+ : path.join(__dirname, value);
244
+ if (fs.existsSync(fullPath)) {
245
+ this.logger.info(`Find file '${fullPath}' as [${varName}] in env.`);
246
+ return fs.readFileSync(fullPath).toString();
247
+ } else {
248
+ this.logger.error(
249
+ `Cannot find '${fullPath}' which is as env[${varName}].`
250
+ );
251
+ }
189
252
  }
190
253
  }
191
254
  }
@@ -34,11 +34,21 @@ const API_VERSION = 'v1';
34
34
  class CloudPlatformApis {
35
35
  constructor(env = process.env) {
36
36
  /** @const {!AuthClient} */
37
- const authClient = new AuthClient(API_SCOPES, env);
38
- this.auth = authClient.getDefaultAuth();
37
+ this.authClient = new AuthClient(API_SCOPES, env);
39
38
  this.projectId = env['GCP_PROJECT'];
40
39
  }
41
40
 
41
+ /**
42
+ * Gets the auth object.
43
+ * @return {!Promise<{!OAuth2Client|!JWT|!Compute}>}
44
+ */
45
+ async getAuth_() {
46
+ if (this.auth) return this.auth;
47
+ await this.authClient.prepareCredentials();
48
+ this.auth = this.authClient.getDefaultAuth();
49
+ return this.auth;
50
+ }
51
+
42
52
  /**
43
53
  * Gets the GCP project Id. In Cloud Functions, it *should* be passed in
44
54
  * through environment variable during the deployment. But if it doesn't exist
@@ -67,7 +77,7 @@ class CloudPlatformApis {
67
77
  async testIamPermissions(permissions) {
68
78
  const resourceManager = google.cloudresourcemanager({
69
79
  version: API_VERSION,
70
- auth: this.auth,
80
+ auth: await this.getAuth_(),
71
81
  });
72
82
  const projectId = await this.getProjectId_();
73
83
  const request = {
@@ -34,7 +34,7 @@ const API_SCOPES = Object.freeze([
34
34
  'https://www.googleapis.com/auth/dfareporting',
35
35
  'https://www.googleapis.com/auth/dfatrafficking',
36
36
  ]);
37
- const API_VERSION = 'v3.5';
37
+ const API_VERSION = 'v4';
38
38
 
39
39
  /**
40
40
  * Configuration for preparing conversions for Campaign Manager, includes:
@@ -65,7 +65,7 @@ let InsertConversionsConfig;
65
65
  /**
66
66
  * List of properties that will be take from the data file as elements of a
67
67
  * conversion.
68
- * See https://developers.google.com/doubleclick-advertisers/rest/v3.5/Conversion
68
+ * See https://developers.google.com/doubleclick-advertisers/rest/v4/Conversion
69
69
  * @type {Array<string>}
70
70
  */
71
71
  const PICKED_PROPERTIES = [
@@ -87,14 +87,34 @@ class DfaReporting {
87
87
  * variables.
88
88
  */
89
89
  constructor(env = process.env) {
90
- const authClient = new AuthClient(API_SCOPES, env);
91
- this.auth = authClient.getDefaultAuth();
92
- /** @const {!google.dfareporting} */
93
- this.instance = google.dfareporting({
90
+ this.authClient = new AuthClient(API_SCOPES, env);
91
+ this.logger = getLogger('API.CM');
92
+ }
93
+
94
+ /**
95
+ * Prepares the Google DfaReport API instance.
96
+ * @return {!google.dfareporting}
97
+ * @private
98
+ */
99
+ async getApiClient_() {
100
+ if (this.dfareporting) return this.dfareporting;
101
+ this.logger.debug(`Initialized ${this.constructor.name} instance.`);
102
+ this.dfareporting = google.dfareporting({
94
103
  version: API_VERSION,
95
- auth: this.auth,
104
+ auth: await this.getAuth_(),
96
105
  });
97
- this.logger = getLogger('API.CM');
106
+ return this.dfareporting;
107
+ }
108
+
109
+ /**
110
+ * Gets the auth object.
111
+ * @return {!Promise<{!OAuth2Client|!JWT|!Compute}>}
112
+ */
113
+ async getAuth_() {
114
+ if (this.auth) return this.auth;
115
+ await this.authClient.prepareCredentials();
116
+ this.auth = this.authClient.getDefaultAuth();
117
+ return this.auth;
98
118
  }
99
119
 
100
120
  /**
@@ -105,13 +125,14 @@ class DfaReporting {
105
125
  * @return {!Promise<string>}
106
126
  */
107
127
  async getProfileId(accountId) {
108
- const {data: {items}} = await this.instance.userProfiles.list();
128
+ const dfareporting = await this.getApiClient_();
129
+ const { data: { items } } = await dfareporting.userProfiles.list();
109
130
  const profiles = items.filter(
110
131
  (profile) => profile.accountId === accountId
111
132
  );
112
133
  if (profiles.length === 0) {
113
- throw new Error(`Fail to find profile of current user for CM account ${
114
- accountId}`);
134
+ throw new Error(
135
+ `Failed to find profile of current user for CM account ${accountId}`);
115
136
  } else {
116
137
  const {profileId, userName, accountId, accountName,} = profiles[0];
117
138
  this.logger.debug(`Find UserProfile: ${profileId}[${userName}] for`
@@ -166,7 +187,8 @@ class DfaReporting {
166
187
  numberOfLines: lines.length,
167
188
  };
168
189
  try {
169
- const response = await this.instance.conversions.batchinsert({
190
+ const dfareporting = await this.getApiClient_();
191
+ const response = await dfareporting.conversions.batchinsert({
170
192
  profileId: config.profileId,
171
193
  requestBody: requestBody,
172
194
  });
@@ -194,9 +216,9 @@ class DfaReporting {
194
216
  * ConversionStatus object. This function extras failed lines and error
195
217
  * messages based on the 'errors'.
196
218
  * For 'ConversionStatus', see:
197
- * https://developers.google.com/doubleclick-advertisers/rest/v3.5/ConversionStatus
219
+ * https://developers.google.com/doubleclick-advertisers/rest/v4/ConversionStatus
198
220
  * For 'ConversionError', see:
199
- * https://developers.google.com/doubleclick-advertisers/rest/v3.5/ConversionStatus#ConversionError
221
+ * https://developers.google.com/doubleclick-advertisers/rest/v4/ConversionStatus#ConversionError
200
222
  * @param {!BatchResult} batchResult
201
223
  * @param {!Array<!Schema$ConversionStatus>} statuses
202
224
  * @param {!Array<string>} lines The original input data.
@@ -232,7 +254,8 @@ class DfaReporting {
232
254
  * @return {!Promise<!Array<string>>}
233
255
  */
234
256
  async listUserProfiles() {
235
- const {data: {items}} = await this.instance.userProfiles.list();
257
+ const dfareporting = await this.getApiClient_();
258
+ const { data: { items } } = await dfareporting.userProfiles.list();
236
259
  return items.map(({profileId, userName, accountId, accountName}) => {
237
260
  return `Profile: ${profileId}[${userName}] `
238
261
  + `Account: ${accountId}[${accountName}]`;
@@ -261,7 +284,7 @@ class DfaReporting {
261
284
  * Runs a report and return the file Id. As an asynchronized process, the
262
285
  * returned file Id will be a placeholder until the status changes to
263
286
  * 'REPORT_AVAILABLE' in the response of `getFile`.
264
- * @see https://developers.google.com/doubleclick-advertisers/rest/v3.5/reports/run
287
+ * @see https://developers.google.com/doubleclick-advertisers/rest/v4/reports/run
265
288
  *
266
289
  * @param {{
267
290
  * accountId:(string|undefined),
@@ -272,7 +295,8 @@ class DfaReporting {
272
295
  */
273
296
  async runReport(config) {
274
297
  const profileId = await this.getProfileForOperation_(config);
275
- const response = await this.instance.reports.run({
298
+ const dfareporting = await this.getApiClient_();
299
+ const response = await dfareporting.reports.run({
276
300
  profileId,
277
301
  reportId: config.reportId,
278
302
  synchronous: false,
@@ -282,9 +306,9 @@ class DfaReporting {
282
306
 
283
307
  /**
284
308
  * Returns file url from a report. If the report status is 'REPORT_AVAILABLE',
285
- * then return the apiUrl from the response; if the status is 'PROCESSING',
286
- * returns undefined; otherwise throws an error.
287
- * @see https://developers.google.com/doubleclick-advertisers/rest/v3.5/reports/get
309
+ * then return the apiUrl from the response; if the status is 'PROCESSING' or
310
+ * 'QUEUED', returns undefined as it is unfinished; otherwise throws an error.
311
+ * @see https://developers.google.com/doubleclick-advertisers/rest/v4/reports/get
288
312
  *
289
313
  * @param {{
290
314
  * accountId:(string|undefined),
@@ -296,13 +320,14 @@ class DfaReporting {
296
320
  */
297
321
  async getReportFileUrl(config) {
298
322
  const profileId = await this.getProfileForOperation_(config);
299
- const response = await this.instance.reports.files.get({
323
+ const dfareporting = await this.getApiClient_();
324
+ const response = await dfareporting.reports.files.get({
300
325
  profileId,
301
326
  reportId: config.reportId,
302
327
  fileId: config.fileId,
303
328
  });
304
- const {data} = response;
305
- if (data.status === 'PROCESSING') return;
329
+ const { data } = response;
330
+ if (data.status === 'PROCESSING' || data.status === 'QUEUED') return;
306
331
  if (data.status === 'REPORT_AVAILABLE') return data.urls.apiUrl;
307
332
  throw new Error(`Unsupported report status: ${data.status}`);
308
333
  }
@@ -314,7 +339,8 @@ class DfaReporting {
314
339
  * @return {!Promise<string>}
315
340
  */
316
341
  async downloadReportFile(url) {
317
- const headers = await this.auth.getRequestHeaders();
342
+ const auth = await this.getAuth_();
343
+ const headers = await auth.getRequestHeaders();
318
344
  const response = await request({
319
345
  method: 'GET',
320
346
  headers,
@@ -21,6 +21,7 @@
21
21
 
22
22
  const {google} = require('googleapis');
23
23
  const AuthClient = require('./auth_client.js');
24
+ const { getLogger } = require('../components/utils.js');
24
25
 
25
26
  const API_SCOPES = Object.freeze([
26
27
  'https://www.googleapis.com/auth/doubleclickbidmanager',
@@ -62,13 +63,24 @@ class DoubleClickBidManager {
62
63
  * variables.
63
64
  */
64
65
  constructor(env = process.env) {
65
- const authClient = new AuthClient(API_SCOPES, env);
66
- const auth = authClient.getDefaultAuth();
67
- /** @const {!google.doubleclickbidmanager} */
68
- this.instance = google.doubleclickbidmanager({
66
+ this.authClient = new AuthClient(API_SCOPES, env);
67
+ this.logger = getLogger('API.DV3');
68
+ }
69
+
70
+ /**
71
+ * Prepares the Google DBM instance.
72
+ * @return {!google.doubleclickbidmanager}
73
+ * @private
74
+ */
75
+ async getApiClient_() {
76
+ if (this.doubleclickbidmanager) return this.doubleclickbidmanager;
77
+ await this.authClient.prepareCredentials();
78
+ this.logger.debug(`Initialized ${this.constructor.name} instance.`);
79
+ this.doubleclickbidmanager = google.doubleclickbidmanager({
69
80
  version: API_VERSION,
70
- auth,
81
+ auth: this.authClient.getDefaultAuth(),
71
82
  });
83
+ return this.doubleclickbidmanager;
72
84
  }
73
85
 
74
86
  /**
@@ -80,7 +92,8 @@ class DoubleClickBidManager {
80
92
  * @return {!Promise<boolean>} Whether it starts successfully.
81
93
  */
82
94
  async runQuery(queryId, requestBody = undefined) {
83
- const response = await this.instance.queries.runquery(
95
+ const doubleclickbidmanager = await this.getApiClient_();
96
+ const response = await doubleclickbidmanager.queries.runquery(
84
97
  {queryId, requestBody});
85
98
  return response.status >= 200 && response.status < 300;
86
99
  }
@@ -88,12 +101,13 @@ class DoubleClickBidManager {
88
101
  /**
89
102
  * Gets a query resource.
90
103
  * See https://developers.google.com/bid-manager/v1.1/queries/getquery
91
- * @param {number} queryId
104
+ * @param {number} queryId Id of the query.
92
105
  * @return {!Promise<!QueryResource>} Query resource, see
93
106
  * https://developers.google.com/bid-manager/v1.1/queries#resource
94
107
  */
95
108
  async getQuery(queryId) {
96
- const response = await this.instance.queries.getquery({queryId});
109
+ const doubleclickbidmanager = await this.getApiClient_();
110
+ const response = await doubleclickbidmanager.queries.getquery({ queryId });
97
111
  return response.data.metadata;
98
112
  }
99
113
 
@@ -104,7 +118,8 @@ class DoubleClickBidManager {
104
118
  * @return {!Promise<number>} Id of created query.
105
119
  */
106
120
  async createQuery(query) {
107
- const response = await this.instance.queries.createquery(
121
+ const doubleclickbidmanager = await this.getApiClient_();
122
+ const response = await doubleclickbidmanager.queries.createquery(
108
123
  {requestBody: query});
109
124
  return response.data.queryId;
110
125
  }
@@ -115,8 +130,9 @@ class DoubleClickBidManager {
115
130
  * @return {!Promise<boolean>} Whether the query was deleted.
116
131
  */
117
132
  async deleteQuery(queryId) {
133
+ const doubleclickbidmanager = await this.getApiClient_();
118
134
  try {
119
- await this.instance.queries.deletequery({ queryId });
135
+ await doubleclickbidmanager.queries.deletequery({ queryId });
120
136
  return true;
121
137
  } catch (error) {
122
138
  console.error(error);
@@ -126,17 +126,34 @@ class DoubleClickSearch {
126
126
  * variables.
127
127
  */
128
128
  constructor(env = process.env) {
129
- const authClient = new AuthClient(API_SCOPES, env);
130
- this.auth = authClient.getDefaultAuth();
131
- /** @const {!google.doubleclicksearch} */
132
- this.instance = google.doubleclicksearch({
129
+ this.authClient = new AuthClient(API_SCOPES, env);
130
+ this.logger = getLogger('API.DS');
131
+ }
132
+
133
+ /**
134
+ * Prepares the Google SA360 instance.
135
+ * @return {!google.doubleclicksearch}
136
+ * @private
137
+ */
138
+ async getApiClient_() {
139
+ if (this.doubleclicksearch) return this.doubleclicksearch;
140
+ this.logger.debug(`Initialized ${this.constructor.name} instance.`);
141
+ this.doubleclicksearch = google.doubleclicksearch({
133
142
  version: API_VERSION,
134
- auth: this.auth,
143
+ auth: await this.getAuth_(),
135
144
  });
136
- /**
137
- * Logger object from 'log4js' package where this type is not exported.
138
- */
139
- this.logger = getLogger('API.DS');
145
+ return this.doubleclicksearch;
146
+ }
147
+
148
+ /**
149
+ * Gets the auth object.
150
+ * @return {!Promise<{!OAuth2Client|!JWT|!Compute}>}
151
+ */
152
+ async getAuth_() {
153
+ if (this.auth) return this.auth;
154
+ await this.authClient.prepareCredentials();
155
+ this.auth = this.authClient.getDefaultAuth();
156
+ return this.auth;
140
157
  }
141
158
 
142
159
  /**
@@ -155,7 +172,8 @@ class DoubleClickSearch {
155
172
  });
156
173
  this.logger.debug('Sending out availabilities', availabilities);
157
174
  try {
158
- const response = await this.instance.conversion.updateAvailability(
175
+ const doubleclicksearch = await this.getApiClient_();
176
+ const response = await doubleclicksearch.conversion.updateAvailability(
159
177
  {requestBody: {availabilities}});
160
178
  this.logger.debug('Get response: ', response);
161
179
  return response.status === 200;
@@ -203,7 +221,8 @@ class DoubleClickSearch {
203
221
  numberOfLines: lines.length,
204
222
  };
205
223
  try {
206
- const response = await this.instance.conversion.insert(
224
+ const doubleclicksearch = await this.getApiClient_();
225
+ const response = await doubleclicksearch.conversion.insert(
207
226
  {requestBody: {conversion: conversions}}
208
227
  );
209
228
  this.logger.debug('Response: ', response);
@@ -303,7 +322,8 @@ class DoubleClickSearch {
303
322
  * @return {!Promise<string>}
304
323
  */
305
324
  async requestReports(requestBody) {
306
- const {status, data} = await this.instance.reports.request({requestBody});
325
+ const doubleclicksearch = await this.getApiClient_();
326
+ const { status, data } = await doubleclicksearch.reports.request({ requestBody });
307
327
  if (status >= 200 && status < 300) {
308
328
  return data.id;
309
329
  }
@@ -321,7 +341,8 @@ class DoubleClickSearch {
321
341
  * }>>}
322
342
  */
323
343
  async getReportUrls(reportId) {
324
- const {status, data} = await this.instance.reports.get({reportId});
344
+ const doubleclicksearch = await this.getApiClient_();
345
+ const { status, data } = await doubleclicksearch.reports.get({ reportId });
325
346
  switch (status) {
326
347
  case 200:
327
348
  const {rowCount, files} = data;
@@ -346,7 +367,8 @@ class DoubleClickSearch {
346
367
  * @return {!Promise<string>}
347
368
  */
348
369
  async getReportFile(reportId, reportFragment) {
349
- const response = await this.instance.reports.getFile(
370
+ const doubleclicksearch = await this.getApiClient_();
371
+ const response = await doubleclicksearch.reports.getFile(
350
372
  {reportId, reportFragment});
351
373
  if (response.status === 200) return response.data;
352
374
  const errorMsg =
@@ -363,7 +385,8 @@ class DoubleClickSearch {
363
385
  * @return {!Promise<ReadableStream>}
364
386
  */
365
387
  async getReportFileStream(url) {
366
- const headers = await this.auth.getRequestHeaders();
388
+ const auth = await this.getAuth_();
389
+ const headers = await auth.getRequestHeaders();
367
390
  const response = await request({
368
391
  method: 'GET',
369
392
  headers,
@@ -251,14 +251,9 @@ class GoogleAds {
251
251
  * variables.
252
252
  */
253
253
  constructor(developerToken, debugMode = false, env = process.env) {
254
+ this.developerToken = developerToken;
254
255
  this.debugMode = debugMode;
255
- const oauthClient = new AuthClient(API_SCOPES, env).getOAuth2Token();
256
- /** @const {GoogleAdsApi} */ this.apiClient = new GoogleAdsApi({
257
- client_id: oauthClient.clientId,
258
- client_secret: oauthClient.clientSecret,
259
- developer_token: developerToken,
260
- });
261
- /** @const {string} */ this.refreshToken = oauthClient.refreshToken;
256
+ this.authClient = new AuthClient(API_SCOPES, env);
262
257
  this.logger = getLogger('API.ADS');
263
258
  this.logger.debug(`Init ${this.constructor.name} with Debug Mode.`);
264
259
  }
@@ -272,7 +267,8 @@ class GoogleAds {
272
267
  * @return {!ReadableStream}
273
268
  */
274
269
  async getReport(customerId, loginCustomerId, reportQueryConfig) {
275
- const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
270
+ const customer = await this.getGoogleAdsApiCustomer_(
271
+ loginCustomerId, customerId);
276
272
  return customer.report(reportQueryConfig);
277
273
  }
278
274
 
@@ -284,7 +280,8 @@ class GoogleAds {
284
280
  * @return {!ReadableStream}
285
281
  */
286
282
  async generatorReport(customerId, loginCustomerId, reportQueryConfig) {
287
- const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
283
+ const customer = await this.getGoogleAdsApiCustomer_(
284
+ loginCustomerId, customerId);
288
285
  return customer.reportStream(reportQueryConfig);
289
286
  }
290
287
 
@@ -296,7 +293,8 @@ class GoogleAds {
296
293
  * @return {!ReadableStream}
297
294
  */
298
295
  async streamReport(customerId, loginCustomerId, reportQueryConfig) {
299
- const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
296
+ const customer = await this.getGoogleAdsApiCustomer_(
297
+ loginCustomerId, customerId);
300
298
  return customer.reportStreamRaw(reportQueryConfig);
301
299
  }
302
300
 
@@ -313,7 +311,7 @@ class GoogleAds {
313
311
  */
314
312
  async searchMetaData(loginCustomerId, adFields, metadata = [
315
313
  'name', 'data_type', 'is_repeated', 'type_url',]) {
316
- const customer = this.getGoogleAdsApiCustomer_(loginCustomerId);
314
+ const customer = await this.getGoogleAdsApiCustomer_(loginCustomerId);
317
315
  const selectClause = metadata.join(',');
318
316
  const fields = adFields.join('","');
319
317
  const query = `SELECT ${selectClause} WHERE name IN ("${fields}")`;
@@ -560,9 +558,10 @@ class GoogleAds {
560
558
  * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
561
559
  * @return {!Promise<!UploadCallConversionsResponse>}
562
560
  */
563
- uploadCallConversions(callConversions, customerId, loginCustomerId) {
561
+ async uploadCallConversions(callConversions, customerId, loginCustomerId) {
564
562
  this.logger.debug('Upload call conversions for customerId:', customerId);
565
- const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
563
+ const customer = await this.getGoogleAdsApiCustomer_(
564
+ loginCustomerId, customerId);
566
565
  const request = new UploadCallConversionsRequest({
567
566
  conversions: callConversions,
568
567
  customer_id: customerId,
@@ -581,9 +580,10 @@ class GoogleAds {
581
580
  * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
582
581
  * @return {!Promise<!UploadClickConversionsResponse>}
583
582
  */
584
- uploadClickConversions(clickConversions, customerId, loginCustomerId) {
583
+ async uploadClickConversions(clickConversions, customerId, loginCustomerId) {
585
584
  this.logger.debug('Upload click conversions for customerId:', customerId);
586
- const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
585
+ const customer = await this.getGoogleAdsApiCustomer_(
586
+ loginCustomerId, customerId);
587
587
  const request = new UploadClickConversionsRequest({
588
588
  conversions: clickConversions,
589
589
  customer_id: customerId,
@@ -603,11 +603,12 @@ class GoogleAds {
603
603
  * @param {string} loginCustomerId Login customer account ID (Mcc Account id).
604
604
  * @return {!Promise<!UploadConversionAdjustmentsResponse>}
605
605
  */
606
- uploadConversionAdjustments(conversionAdjustments, customerId,
606
+ async uploadConversionAdjustments(conversionAdjustments, customerId,
607
607
  loginCustomerId) {
608
608
  this.logger.debug('Upload conversion adjustments for customerId:',
609
609
  customerId);
610
- const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
610
+ const customer = await this.getGoogleAdsApiCustomer_(
611
+ loginCustomerId, customerId);
611
612
  const request = new UploadConversionAdjustmentsRequest({
612
613
  conversion_adjustments: conversionAdjustments,
613
614
  customer_id: customerId,
@@ -626,7 +627,8 @@ class GoogleAds {
626
627
  * @return {Promise<number|undefined>} Returns undefined if can't find tag.
627
628
  */
628
629
  async getConversionCustomVariableId(tag, customerId, loginCustomerId) {
629
- const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
630
+ const customer = await this.getGoogleAdsApiCustomer_(
631
+ loginCustomerId, customerId);
630
632
  const customVariables = await customer.query(`
631
633
  SELECT conversion_custom_variable.id,
632
634
  conversion_custom_variable.tag
@@ -692,7 +694,8 @@ class GoogleAds {
692
694
  validate_only: this.debugMode, // when true makes no changes
693
695
  partial_failure: true, // Will still create the non-failed entities
694
696
  };
695
- const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
697
+ const customer = await this.getGoogleAdsApiCustomer_(
698
+ loginCustomerId, customerId);
696
699
  const response = await customer.userLists.create([userList], options);
697
700
  const { results, partial_failure_error: failed } = response;
698
701
  if (this.logger.isDebugEnabled()) {
@@ -775,7 +778,8 @@ class GoogleAds {
775
778
  const userListId = customerMatchConfig.list_id;
776
779
  const operation = customerMatchConfig.operation;
777
780
 
778
- const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
781
+ const customer = await this.getGoogleAdsApiCustomer_(
782
+ loginCustomerId, customerId);
779
783
  const operationsList = this.buildOperationsList_(operation,
780
784
  customerMatchRecords);
781
785
  const metadata = this.buildCustomerMatchUserListMetadata_(customerId,
@@ -893,8 +897,8 @@ class GoogleAds {
893
897
  const customerId = this.getCleanCid_(config.customer_id);
894
898
  const { list_id: userListId, type } = config;
895
899
  this.logger.debug('Creating OfflineUserDataJob for CID:', customerId);
896
- const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
897
- // if()CUSTOMER_MATCH_USER_LIST
900
+ const customer = await this.getGoogleAdsApiCustomer_(
901
+ loginCustomerId, customerId);
898
902
  const job = OfflineUserDataJob.create({
899
903
  type,
900
904
  });
@@ -936,7 +940,8 @@ class GoogleAds {
936
940
  const loginCustomerId = this.getCleanCid_(config.login_customer_id);
937
941
  const customerId = this.getCleanCid_(config.customer_id);
938
942
  const operation = config.operation;
939
- const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
943
+ const customer = await this.getGoogleAdsApiCustomer_(
944
+ loginCustomerId, customerId);
940
945
  const operationsList = this.buildOperationsList_(operation, records);
941
946
  const request = AddOfflineUserDataJobOperationsRequest.create({
942
947
  resource_name: jobResourceName,
@@ -961,7 +966,8 @@ class GoogleAds {
961
966
  async runOfflineUserDataJob(config, jobResourceName) {
962
967
  const loginCustomerId = this.getCleanCid_(config.login_customer_id);
963
968
  const customerId = this.getCleanCid_(config.customer_id);
964
- const customer = this.getGoogleAdsApiCustomer_(loginCustomerId, customerId);
969
+ const customer = await this.getGoogleAdsApiCustomer_(
970
+ loginCustomerId, customerId);
965
971
  const request = RunOfflineUserDataJobRequest.create({
966
972
  resource_name: jobResourceName,
967
973
  validate_only: false,//this.debugMode,
@@ -1025,13 +1031,22 @@ class GoogleAds {
1025
1031
  * @return {GoogleAdsApi.Customer}
1026
1032
  * @private
1027
1033
  */
1028
- getGoogleAdsApiCustomer_(loginCustomerId, customerId = loginCustomerId) {
1029
- const googleAdsApiClient = this.apiClient;
1030
- return googleAdsApiClient.Customer({
1034
+ async getGoogleAdsApiCustomer_(loginCustomerId, customerId = loginCustomerId) {
1035
+ if (this.googleAds) return this.googleAds;
1036
+ await this.authClient.prepareCredentials();
1037
+ const oauthClient = await this.authClient.getOAuth2Token();
1038
+ /** @const {GoogleAdsApi} */
1039
+ const googleAdsApiClient = new GoogleAdsApi({
1040
+ client_id: oauthClient.clientId,
1041
+ client_secret: oauthClient.clientSecret,
1042
+ developer_token: this.developerToken,
1043
+ });
1044
+ this.googleAds = googleAdsApiClient.Customer({
1031
1045
  customer_id: customerId,
1032
1046
  login_customer_id: loginCustomerId,
1033
- refresh_token: this.refreshToken,
1047
+ refresh_token: oauthClient.refreshToken,
1034
1048
  });
1049
+ return this.googleAds;
1035
1050
  }
1036
1051
 
1037
1052
  }
@@ -73,33 +73,43 @@ class Spreadsheets {
73
73
  constructor(spreadsheetId, env = process.env) {
74
74
  /** @const {string} */
75
75
  this.spreadsheetId = spreadsheetId;
76
- const authClient = new AuthClient(API_SCOPES, env);
77
- const auth = authClient.getDefaultAuth();
78
- /** @const {!!google.sheets} */
79
- this.instance = google.sheets({
80
- version: API_VERSION,
81
- auth,
82
- });
76
+ this.authClient = new AuthClient(API_SCOPES, env);
83
77
  /**
84
78
  * Logger object from 'log4js' package where this type is not exported.
85
79
  */
86
80
  this.logger = getLogger('API.GS');
87
- this.logger.debug(`Init ${this.constructor.name} for ${
88
- this.spreadsheetId} in Debug Mode.`);
81
+ }
82
+
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;
89
97
  }
90
98
 
91
99
  /**
92
100
  * Gets the Sheet Id of the given Spreadsheet and possible Sheet name. If the
93
101
  * Sheet name is missing, it will return the first Sheet's Id.
94
102
  * @param {string} sheetName Name of the Sheet.
95
- * @return {!Promise<number>} Sheet Id.
103
+ * @return {!Promise<number>} The ID for the sheet that is unique to the
104
+ * spreadsheet.
96
105
  */
97
106
  async getSheetId(sheetName) {
98
107
  const request = /** @type{Params$Resource$Spreadsheets$Get} */ {
99
108
  spreadsheetId: this.spreadsheetId,
100
109
  ranges: sheetName,
101
110
  };
102
- const response = await this.instance.spreadsheets.get(request);
111
+ const sheets = await this.getApiClient_();
112
+ const response = await sheets.spreadsheets.get(request);
103
113
  const sheet = response.data.sheets[0];
104
114
  this.logger.debug(`Get sheet[${sheetName}]: `, sheet);
105
115
  return sheet.properties.sheetId;
@@ -117,7 +127,8 @@ class Spreadsheets {
117
127
  range: sheetName,
118
128
  };
119
129
  try {
120
- const response = await this.instance.spreadsheets.values.clear(request);
130
+ const sheets = await this.getApiClient_();
131
+ const response = await sheets.spreadsheets.values.clear(request);
121
132
  const data = response.data;
122
133
  this.logger.debug(`Clear sheet[${sheetName}}]: `, data);
123
134
  } catch (error) {
@@ -169,7 +180,8 @@ class Spreadsheets {
169
180
  ranges: sheetName,
170
181
  };
171
182
  try {
172
- const response = await this.instance.spreadsheets.get(request);
183
+ const sheets = await this.getApiClient_();
184
+ const response = await sheets.spreadsheets.get(request);
173
185
  const sheet = response.data.sheets[0];
174
186
  const sheetId = sheet.properties.sheetId;
175
187
  const rowCount = sheet.properties.gridProperties.rowCount;
@@ -191,7 +203,7 @@ class Spreadsheets {
191
203
  columnCount}] to [${targetRows}, ${targetColumns}]`,
192
204
  JSON.stringify(requests.resource.requests));
193
205
  if (requests.resource.requests.length > 0) {
194
- const {data} = await this.instance.spreadsheets.batchUpdate(requests);
206
+ const { data } = await sheets.spreadsheets.batchUpdate(requests);
195
207
  this.logger.debug(`Reshape Sheet [${sheetName}]: `, data);
196
208
  } else {
197
209
  this.logger.debug('No need to reshape.');
@@ -220,7 +232,8 @@ class Spreadsheets {
220
232
  numberOfLines: data.trim().split('\n').length,
221
233
  };
222
234
  try {
223
- const response = await this.instance.spreadsheets.batchUpdate(request);
235
+ const sheets = await this.getApiClient_();
236
+ const response = await sheets.spreadsheets.batchUpdate(request);
224
237
  const data = response.data;
225
238
  this.logger.debug(`Batch[${batchId}] uploaded: `, data);
226
239
  batchResult.result = true;
@@ -166,18 +166,29 @@ class YouTube {
166
166
  * variables.
167
167
  */
168
168
  constructor(env = process.env) {
169
- const authClient = new AuthClient(API_SCOPES, env);
170
- this.auth = authClient.getDefaultAuth();
171
- /** @const {!google.youtube} */
172
- this.instance = google.youtube({
173
- version: API_VERSION,
174
- });
169
+ this.authClient = new AuthClient(API_SCOPES, env);
175
170
  /**
176
171
  * Logger object from 'log4js' package where this type is not exported.
177
172
  */
178
173
  this.logger = getLogger('API.YT');
179
174
  }
180
175
 
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;
190
+ }
191
+
181
192
  /**
182
193
  * Returns a collection of zero or more channel resources that match the
183
194
  * request criteria.
@@ -186,12 +197,11 @@ class YouTube {
186
197
  * @return {!Promise<Array<Schema$Channel>>}
187
198
  */
188
199
  async listChannels(config) {
189
- const channelListRequest = Object.assign({
190
- auth: this.auth,
191
- }, config);
200
+ const channelListRequest = Object.assign({}, config);
192
201
  channelListRequest.part = channelListRequest.part.join(',')
193
202
  try {
194
- const response = await this.instance.channels.list(channelListRequest);
203
+ const youtube = await this.getApiClient_();
204
+ const response = await youtube.channels.list(channelListRequest);
195
205
  this.logger.debug('Response: ', response);
196
206
  return response.data.items;
197
207
  } catch (error) {
@@ -210,12 +220,11 @@ class YouTube {
210
220
  * @return {!Promise<Array<Schema$Video>>}
211
221
  */
212
222
  async listVideos(config) {
213
- const videoListRequest = Object.assign({
214
- auth: this.auth,
215
- }, config);
223
+ const videoListRequest = Object.assign({}, config);
216
224
  videoListRequest.part = videoListRequest.part.join(',')
217
225
  try {
218
- const response = await this.instance.videos.list(videoListRequest);
226
+ const youtube = await this.getApiClient_();
227
+ const response = await youtube.videos.list(videoListRequest);
219
228
  this.logger.debug('Response: ', response);
220
229
  return response.data.items;
221
230
  } catch (error) {
@@ -235,13 +244,11 @@ class YouTube {
235
244
  * @return {!Promise<Array<Schema$CommentThread>>}
236
245
  */
237
246
  async listCommentThreads(config) {
238
- const commentThreadsRequest = Object.assign({
239
- auth: this.auth,
240
- }, config);
247
+ const commentThreadsRequest = Object.assign({}, config);
241
248
  commentThreadsRequest.part = commentThreadsRequest.part.join(',')
242
249
  try {
243
- const response = await this.instance.commentThreads.list(
244
- commentThreadsRequest);
250
+ const youtube = await this.getApiClient_();
251
+ const response = await youtube.commentThreads.list(commentThreadsRequest);
245
252
  this.logger.debug('Response: ', response.data);
246
253
  return response.data.items;
247
254
  } catch (error) {
@@ -265,7 +272,7 @@ class YouTube {
265
272
  if (resultLimit <= 0) return [];
266
273
 
267
274
  const playlistsRequest = Object.assign({
268
- auth: this.auth,
275
+ // auth: this.auth,
269
276
  }, config, {
270
277
  pageToken
271
278
  });
@@ -275,8 +282,8 @@ class YouTube {
275
282
  }
276
283
 
277
284
  try {
278
- const response = await this.instance.playlists.list(
279
- playlistsRequest);
285
+ const youtube = await this.getApiClient_();
286
+ const response = await youtube.playlists.list(playlistsRequest);
280
287
  this.logger.debug('Response: ', response.data);
281
288
  if (response.data.nextPageToken) {
282
289
  this.logger.debug(
@@ -310,18 +317,15 @@ class YouTube {
310
317
  async listSearchResults(config, resultLimit = 1000, pageToken = null) {
311
318
  if (resultLimit <= 0) return [];
312
319
 
313
- const searchRequest = Object.assign({
314
- auth: this.auth,
315
- }, config, {
316
- pageToken
317
- });
320
+ const searchRequest = Object.assign({}, config, { pageToken });
318
321
 
319
322
  if (Array.isArray(searchRequest.part)) {
320
323
  searchRequest.part = searchRequest.part.join(',');
321
324
  }
322
325
 
323
326
  try {
324
- const response = await this.instance.search.list(searchRequest);
327
+ const youtube = await this.getApiClient_();
328
+ const response = await youtube.search.list(searchRequest);
325
329
  this.logger.debug('Response: ', response.data);
326
330
  if (response.data.nextPageToken) {
327
331
  this.logger.debug(
@@ -20,7 +20,13 @@
20
20
  'use strict';
21
21
 
22
22
  const {
23
- PubSub, Message, Topic, Subscription, ClientConfig, CreateSubscriptionOptions,
23
+ PubSub,
24
+ Message,
25
+ Topic,
26
+ Subscription,
27
+ ClientConfig,
28
+ CreateSubscriptionOptions,
29
+ v1: { SubscriberClient },
24
30
  } = require('@google-cloud/pubsub');
25
31
 
26
32
  /**
@@ -28,6 +34,7 @@ const {
28
34
  * 1. gets or creates a topic.
29
35
  * 2. gets or creates a subscription.
30
36
  * 3. publishes a message after confirms the topic exists.
37
+ * 4. acknowledge message(s).
31
38
  */
32
39
  class EnhancedPubSub {
33
40
  /**
@@ -38,6 +45,8 @@ class EnhancedPubSub {
38
45
  constructor(options = undefined) {
39
46
  /** @type {!PubSub} */
40
47
  this.pubsub = new PubSub(options);
48
+ /** @type {!SubscriberClient} */
49
+ this.subClient = new SubscriberClient(options);
41
50
  }
42
51
 
43
52
  /**
@@ -118,6 +127,23 @@ class EnhancedPubSub {
118
127
  }
119
128
  };
120
129
 
130
+ /**
131
+ * Using `SubscriberClient` to acknowledge messages.
132
+ * 2022.11.02 The methon `ack()` in Message doesn't work properly due to a
133
+ * unknown reason. Use this function to acknowledge a message for now.
134
+ *
135
+ * @param {string} subscription Subscription name.
136
+ * @param {(string|!Array<string>)} ackIds Message ackIds.
137
+ */
138
+ async acknowledge(subscription, ackIds) {
139
+ const projectId = await this.subClient.getProjectId();
140
+ const ackRequest = {
141
+ subscription: this.subClient.subscriptionPath(projectId, subscription),
142
+ ackIds: Array.isArray(ackIds) ? ackIds : [ackIds],
143
+ };
144
+ await this.subClient.acknowledge(ackRequest);
145
+ }
146
+
121
147
  /**
122
148
  * Returns a new instance of this class. Using this function to replace
123
149
  * constructor to be more friendly to unit tests.
@@ -0,0 +1,54 @@
1
+ // Copyright 2022 Google Inc.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file 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
+ /**
16
+ * @fileoverview Secret Manager wrapper class.
17
+ */
18
+
19
+ const { GoogleAuthOptions } = require('google-auth-library');
20
+ const { SecretManagerServiceClient } = require('@google-cloud/secret-manager');
21
+
22
+ /**
23
+ * Google Cloud Secret Manager class based on cloud client library.
24
+ * @see https://cloud.google.com/nodejs/docs/reference/secret-manager/latest
25
+ * @see https://cloud.google.com/secret-manager/docs/reference/libraries
26
+ */
27
+ class SecretManager {
28
+ /**
29
+ * @constructor
30
+ * @param {GoogleAuthOptions=} options
31
+ */
32
+ constructor(options = {}) {
33
+ if (!options.projectId) {
34
+ options.projectId = process.env['GCP_PROJECT'];
35
+ }
36
+ this.client = new SecretManagerServiceClient(options);
37
+ }
38
+
39
+ async access(secret, version = 'latest') {
40
+ const projectId = await this.client.getProjectId();
41
+ const name = `projects/${projectId}/secrets/${secret}/versions/${version}`;
42
+ try {
43
+ const [secretObj] = await this.client.accessSecretVersion({ name });
44
+ return secretObj.payload.data.toString('utf8');
45
+ } catch (error) {
46
+ if (error.details.indexOf('not found') > -1) {
47
+ return;
48
+ }
49
+ throw error;
50
+ }
51
+ }
52
+ }
53
+
54
+ exports.SecretManager = SecretManager;