@google-cloud/nodejs-common 0.8.3 → 0.9.2-beta2

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.
@@ -138,13 +138,6 @@ EXTERNAL_API_SCOPES=(
138
138
  # Enabled APIs' OAuth scopes.
139
139
  ENABLED_OAUTH_SCOPES=()
140
140
 
141
- # https://cloud.google.com/iam/docs/understanding-roles#service-accounts-roles
142
- declare -A GOOGLE_SERVICE_ACCOUNT_PERMISSIONS
143
- GOOGLE_SERVICE_ACCOUNT_PERMISSIONS=(
144
- ["Service Account Admin"]="iam.serviceAccounts.create"
145
- ["Service Account Key Admin"]="iam.serviceAccounts.create"
146
- )
147
-
148
141
  # Preparation functions.
149
142
  #######################################
150
143
  # Mimic a Cloud Shell environment to enable local running.
@@ -437,8 +430,8 @@ select_functions_location() {
437
430
  local regionVariableName defaultValue locations
438
431
  regionVariableName="${1:-"REGION"}"
439
432
  defaultValue="${!regionVariableName}"
440
- locations=($(gcloud functions regions list | grep "projects"|sed \
441
- 's/projects\/.*\/locations\///'))
433
+ locations=($(gcloud functions regions list --format="csv[no-heading](name)"| \
434
+ sed 's/projects\/.*\/locations\///'))
442
435
  local region
443
436
  while :; do
444
437
  local exist_functions
@@ -561,7 +554,6 @@ need to be enabled."
561
554
  # NEED_OAUTH
562
555
  # NEED_SERVICE_ACCOUNT
563
556
  # GOOGLE_CLOUD_PERMISSIONS
564
- # GOOGLE_SERVICE_ACCOUNT_PERMISSIONS
565
557
  # Arguments:
566
558
  # None
567
559
  #######################################
@@ -590,11 +582,6 @@ authentication method:"
590
582
  printf '%s\n' "... OAuth is selected."
591
583
  else
592
584
  NEED_SERVICE_ACCOUNT="true"
593
- local role
594
- for role in "${!GOOGLE_SERVICE_ACCOUNT_PERMISSIONS[@]}"; do
595
- GOOGLE_CLOUD_PERMISSIONS["${role}"]=\
596
- "${GOOGLE_SERVICE_ACCOUNT_PERMISSIONS["${role}"]}"
597
- done
598
585
  printf '%s\n' "... Service Account is selected."
599
586
  fi
600
587
  }
@@ -1192,6 +1179,87 @@ EOF
1192
1179
  printf '%s\n' "OK. Continue with monitored folder [${folder}]."
1193
1180
  }
1194
1181
 
1182
+ #######################################
1183
+ # Create or update a Log router sink.
1184
+ # Globals:
1185
+ # None
1186
+ # Arguments:
1187
+ # Name of the sink
1188
+ # Filter conditions
1189
+ # Sink destination
1190
+ #######################################
1191
+ create_or_update_sink() {
1192
+ local sinkName=${1}
1193
+ local logFilter=${2}
1194
+ local sinkDestAndFlags=(${3})
1195
+ local existingFilter
1196
+ existingFilter=$(gcloud logging sinks list --filter="name:${sinkName}" \
1197
+ --format="value(filter)")
1198
+ if [[ "${existingFilter}" != "${logFilter}" ]]; then
1199
+ local action
1200
+ if [[ -z "${existingFilter}" ]]; then
1201
+ action="create"
1202
+ printf '%s\n' " Logging Export [${sinkName}] doesn't exist. Creating..."
1203
+ else
1204
+ action="update"
1205
+ printf '%s\n' " Logging Export [${sinkName}] exists with a different \
1206
+ filter. Updating..."
1207
+ fi
1208
+ gcloud -q logging sinks ${action} "${sinkName}" "${sinkDestAndFlags[@]}" \
1209
+ --log-filter="${logFilter}"
1210
+ else
1211
+ printf '%s\n' " Logging Export [${sinkName}] exists. Continue..."
1212
+ fi
1213
+ if [[ $? -gt 0 ]];then
1214
+ printf '%s\n' "Failed to create or update Logs router sink."
1215
+ return 1
1216
+ fi
1217
+ }
1218
+
1219
+ #######################################
1220
+ # Confirm the service account of the given sink has proper permission to dump
1221
+ # the logs.
1222
+ # Globals:
1223
+ # None
1224
+ # Arguments:
1225
+ # Name of the sink
1226
+ # Bindings role, e.g. "pubsub.publisher" or "bigquery.dataEditor"
1227
+ # Role name, e.g. "Pub/Sub Publisher" or "BigQuery Data Editor"
1228
+ #######################################
1229
+ confirm_sink_service_account_permission() {
1230
+ local sinkName=${1}
1231
+ local bindingsRole=${2}
1232
+ local roleName=${3}
1233
+ local serviceAccount existingRole
1234
+ serviceAccount=$(gcloud logging sinks describe "${sinkName}" \
1235
+ --format="get(writerIdentity)")
1236
+ while :;do
1237
+ printf '%s\n' " Checking the role of the sink's service account \
1238
+ [${serviceAccount}]..."
1239
+ existingRole=$(gcloud projects get-iam-policy "${GCP_PROJECT}" \
1240
+ --flatten=bindings --filter="bindings.members:${serviceAccount} AND \
1241
+ bindings.role:roles/${bindingsRole}" --format="get(bindings.members)")
1242
+ if [[ -z "${existingRole}" ]];then
1243
+ printf '%s\n' " Granting Role '${roleName}' to the service \
1244
+ account..."
1245
+ gcloud -q projects add-iam-policy-binding "${GCP_PROJECT}" --member \
1246
+ "${serviceAccount}" --role roles/${bindingsRole}
1247
+ if [[ $? -gt 0 ]];then
1248
+ printf '%s\n' "Failed to grant the role. Use this link \
1249
+ https://console.cloud.google.com/iam-admin/iam?project=${GCP_PROJECT} to \
1250
+ manually grant Role '${roleName}' to ${serviceAccount}."
1251
+ printf '%s' "Press any key to continue after you grant the access..."
1252
+ local any
1253
+ read -n1 -s any
1254
+ continue
1255
+ fi
1256
+ else
1257
+ printf '%s\n' " The role has already been granted."
1258
+ return 0
1259
+ fi
1260
+ done
1261
+ }
1262
+
1195
1263
  #######################################
1196
1264
  # Save the configuration to a local file.
1197
1265
  # Globals:
@@ -1228,9 +1296,15 @@ save_config() {
1228
1296
 
1229
1297
  #######################################
1230
1298
  # Based on the authentication method, guide user complete authentication
1231
- # process. For service account, confirm to use/create a service account and
1232
- # download key file; for OAuth 2.0, guide user to complete OAuth authentication
1233
- # and save the refresh token.
1299
+ # process.
1300
+ # For service account, given that Cloud Functions can extend the authorized API
1301
+ # scopes now, it will use the default service account rather than an explicit
1302
+ # service account with the key file downloaded. By doing so, we can reduce the
1303
+ # risk of leaking service account key. If there is a service key in the current
1304
+ # folder from previous installation, the code will continue using it, otherwise
1305
+ # it will use the Cloud Functions' default service account.
1306
+ # For OAuth 2.0, guide user to complete OAuth authentication and save the
1307
+ # refresh token.
1234
1308
  # Globals:
1235
1309
  # NEED_SERVICE_ACCOUNT
1236
1310
  # NEED_OAUTH
@@ -1238,152 +1312,11 @@ save_config() {
1238
1312
  # None
1239
1313
  #######################################
1240
1314
  do_authentication(){
1241
- if [[ ${NEED_SERVICE_ACCOUNT} == "true" ]]; then
1242
- download_service_account_key
1243
- fi
1244
1315
  if [[ ${NEED_OAUTH} == "true" ]]; then
1245
1316
  do_oauth
1246
1317
  fi
1247
1318
  }
1248
1319
 
1249
- #######################################
1250
- # Download a service account key file and save as `$SA_KEY_FILE`.
1251
- # Globals:
1252
- # SA_NAME
1253
- # GCP_PROJECT
1254
- # SA_KEY_FILE
1255
- # Arguments:
1256
- # None
1257
- # Returns:
1258
- # 0 if service key files exists or created, non-zero on error.
1259
- #######################################
1260
- download_service_account_key() {
1261
- (( STEP += 1 ))
1262
- printf '%s\n' "Step ${STEP}: Downloading the key file for the service \
1263
- account..."
1264
- if [[ -z ${SA_NAME} ]];then
1265
- confirm_service_account
1266
- fi
1267
- local suffix exist
1268
- suffix=$(get_sa_domain_from_gcp_id "${GCP_PROJECT}")
1269
- local email="${SA_NAME}@${suffix}"
1270
- local prompt="Would you like to download the key file for [${email}] and \
1271
- save it as ${SA_KEY_FILE}? [Y/n]: "
1272
- local default_value="y"
1273
- if [[ -f "${SA_KEY_FILE}" && -s "${SA_KEY_FILE}" ]]; then
1274
- exist=$(get_value_from_json_file ${SA_KEY_FILE} 'client_email' 2>&1)
1275
- if [[ ${exist} =~ .*("@${suffix}") ]]; then
1276
- prompt="A key file for [${exist}] with the key ID '\
1277
- $(get_value_from_json_file ${SA_KEY_FILE} 'private_key_id') already exists'. \
1278
- Would you like to create a new key to overwrite it? [N/y]: "
1279
- default_value="n"
1280
- fi
1281
- fi
1282
- printf '%s' "${prompt}"
1283
- local input
1284
- read -r input
1285
- input=${input:-"${default_value}"}
1286
- if [[ ${input} == 'y' || ${input} == 'Y' ]];then
1287
- printf '%s\n' "Downloading a new key file for [${email}]..."
1288
- gcloud iam service-accounts keys create "${SA_KEY_FILE}" --iam-account \
1289
- "${email}"
1290
- if [[ $? -gt 0 ]]; then
1291
- printf '%s\n' "Failed to download new key files for [${email}]."
1292
- return 1
1293
- else
1294
- printf '%s\n' "OK. New key file is saved at [${SA_KEY_FILE}]."
1295
- return 0
1296
- fi
1297
- else
1298
- printf '%s\n' "Skipped downloading new key file. See \
1299
- https://cloud.google.com/iam/docs/creating-managing-service-account-keys \
1300
- to learn more about service account key files."
1301
- return 0
1302
- fi
1303
- }
1304
-
1305
- #######################################
1306
- # Make sure a service account for this integration exists and set the email of
1307
- # the service account to the global variable `SA_NAME`.
1308
- # Globals:
1309
- # GCP_PROJECT
1310
- # SA_KEY_FILE
1311
- # SA_NAME
1312
- # DEFAULT_SERVICE_ACCOUNT
1313
- # Arguments:
1314
- # None
1315
- #######################################
1316
- confirm_service_account() {
1317
- cat <<EOF
1318
- Some external APIs might require authentication based on OAuth or \
1319
- JWT(service account), for example, Google Analytics or Campaign Manager. \
1320
- In this step, you prepare the service account. For more information, see \
1321
- https://cloud.google.com/iam/docs/creating-managing-service-accounts
1322
- EOF
1323
-
1324
- local suffix
1325
- suffix=$(get_sa_domain_from_gcp_id "${GCP_PROJECT}")
1326
- local email
1327
- if [[ -f "${SA_KEY_FILE}" && -s "${SA_KEY_FILE}" ]]; then
1328
- email=$(get_value_from_json_file "${SA_KEY_FILE}" 'client_email')
1329
- if [[ ${email} =~ .*("@${suffix}") ]]; then
1330
- printf '%s' "A key file for service account [${email}] already exists. \
1331
- Would you like to create a new service account? [N/y]: "
1332
- local input
1333
- read -r input
1334
- if [[ ${input} != 'y' && ${input} != 'Y' ]]; then
1335
- printf '%s\n' "OK. Will use existing service account [${email}]."
1336
- SA_NAME=$(printf "${email}" | cut -d@ -f1)
1337
- return 0
1338
- fi
1339
- fi
1340
- fi
1341
-
1342
- SA_NAME="${SA_NAME:-"${PROJECT_NAMESPACE}-api"}"
1343
- while :; do
1344
- printf '%s' "Enter the name of service account [${SA_NAME}]: "
1345
- local input sa_elements=() sa
1346
- read -r input
1347
- input=${input:-"${SA_NAME}"}
1348
- IFS='@' read -a sa_elements <<< "${input}"
1349
- if [[ ${#sa_elements[@]} == 1 ]]; then
1350
- echo " Append default suffix to service account name and get: ${email}"
1351
- sa="${input}"
1352
- email="${sa}@${suffix}"
1353
- else
1354
- if [[ ${sa_elements[1]} != "${suffix}" ]]; then
1355
- printf '%s\n' " Error: Service account domain name ${sa_elements[1]} \
1356
- doesn't belong to the current project. The service account domain name for the \
1357
- current project should be: ${suffix}."
1358
- continue
1359
- fi
1360
- sa="${sa_elements[0]}"
1361
- email="${input}"
1362
- fi
1363
-
1364
- printf '%s\n' "Checking the existence of the service account [${email}]..."
1365
- if ! result=$(gcloud iam service-accounts describe "${email}" 2>&1); then
1366
- printf '%s\n' " Service account [${email}] does not exist. Trying to \
1367
- create..."
1368
- gcloud iam service-accounts create "${sa}" --display-name \
1369
- "Tentacles API requester"
1370
- if [[ $? -gt 0 ]]; then
1371
- printf '%s\n' "Creating the service account [${email}] failed. Please \
1372
- try again..."
1373
- else
1374
- printf '%s\n' "The service account [${email}] was successfully created."
1375
- SA_NAME=${sa}
1376
- break
1377
- fi
1378
- else
1379
- printf ' found.\n'
1380
- SA_NAME=${sa}
1381
- break
1382
- fi
1383
- done
1384
- printf '%s\n' "OK. Service account [${SA_NAME}] is ready."
1385
- }
1386
-
1387
1320
  #######################################
1388
1321
  # Guide an OAuth process and save the token to file.
1389
1322
  # The whole process will be:
@@ -1393,8 +1326,8 @@ try again..."
1393
1326
  # 3. Prompt user to enter the OAuth Client secret.
1394
1327
  # 4. Print the OAuth authentication URL which users should open in a browser \
1395
1328
  # and complete the process.
1396
- # 5. Copy the authentication code from the browser and paste here.
1397
- # 6. Use the authentication code to redeem an OAuth token and save it.
1329
+ # 5. Copy the authorization code from the browser and paste here.
1330
+ # 6. Use the authorization code to redeem an OAuth token and save it.
1398
1331
  # Globals:
1399
1332
  # ENABLED_OAUTH_SCOPES
1400
1333
  # OAUTH2_TOKEN_JSON
@@ -1480,10 +1413,10 @@ ${auth_url}"
1480
1413
  "Error 400: redirect_uri_mismatch" shown up on the page. In this case, press \
1481
1414
  "Enter" to start again with a native application OAuth client ID.
1482
1415
  EOF
1483
- printf '%s' "4. Copy the authentication code from browser and paste here: "
1416
+ printf '%s' "4. Copy the authorization code from browser and paste here: "
1484
1417
  read -r auth_code
1485
1418
  if [[ -z ${auth_code} ]]; then
1486
- printf '%s\n\n' "No authentication code. Starting from beginning again..."
1419
+ printf '%s\n\n' "No authorization code. Starting from beginning again..."
1487
1420
  continue
1488
1421
  fi
1489
1422
  auth_response=$(curl -s -d "code=${auth_code}" -d "client_id=${client_id}" \
@@ -1492,7 +1425,7 @@ EOF
1492
1425
  auth_error=$(node -e "console.log(!!JSON.parse(process.argv[1]).error)" \
1493
1426
  "${auth_response}")
1494
1427
  if [[ ${auth_error} == "true" ]]; then
1495
- printf '%s\n' "Error happened in redeem the authentication code: \
1428
+ printf '%s\n' "Error happened in redeem the authorization code: \
1496
1429
  ${auth_response}"
1497
1430
  continue
1498
1431
  fi
@@ -1549,7 +1482,7 @@ set_authentication_env_for_cloud_functions() {
1549
1482
  }
1550
1483
 
1551
1484
  #######################################
1552
- # Create or update a Cloud Schdduleer
1485
+ # Create or update a Cloud Scheduler
1553
1486
  # Globals:
1554
1487
  # None
1555
1488
  # Arguments:
@@ -1635,8 +1568,27 @@ print_finished(){
1635
1568
  print_service_account(){
1636
1569
  printf '%s\n' "=========================="
1637
1570
  local email
1638
- email=$(get_value_from_json_file "${SA_KEY_FILE}" 'client_email')
1639
- printf '%s\n' "The email address of the current service account is ${email}."
1571
+ email=$(get_service_account)
1572
+ printf '%s\n' "The email address of the current service account is: ${email}."
1573
+ }
1574
+
1575
+ #######################################
1576
+ # Returns the email address of the service account. If there is a key file of
1577
+ # service account, it returns the email of that service account; otherwise it
1578
+ # it will get the default service account of Cloud Functions.
1579
+ # Globals:
1580
+ # SA_KEY_FILE
1581
+ # Arguments:
1582
+ # None
1583
+ #######################################
1584
+ get_service_account(){
1585
+ local email
1586
+ if [[ -f "${SA_KEY_FILE}" ]]; then
1587
+ email=$(get_value_from_json_file "${SA_KEY_FILE}" 'client_email')
1588
+ else
1589
+ email=$(get_cloud_functions_service_account)
1590
+ fi
1591
+ printf '%s' "${email}"
1640
1592
  }
1641
1593
 
1642
1594
  #######################################
@@ -1843,25 +1795,25 @@ sed -r 's/^([^:]*):(.*)$/\2.\1/').iam.gserviceaccount.com"
1843
1795
 
1844
1796
  #######################################
1845
1797
  # Returns the default service account of the given Cloud Functions.
1846
- # Use cases:
1847
- # 1. When BigQuery query Google Sheet based external tables, this service
1848
- # account needs to be added to the Google Sheet as a viewer.
1849
1798
  # Globals:
1850
1799
  # None
1851
1800
  # Arguments:
1852
- # None
1801
+ # The name of Cloud Functions, default value is a random one starts with the
1802
+ # namespace.
1853
1803
  # Returns:
1854
1804
  # The default service account of the given Cloud Functions.
1855
1805
  #######################################
1856
1806
  get_cloud_functions_service_account() {
1857
- local region=$(gcloud functions list --format="csv[no-heading](name,REGION)" \
1858
- | grep "${1}" | cut -d, -f2 | uniq)
1859
- if [[ -z ${region} ]]; then
1860
- printf '%s\n' "Cloud Functions [$1] doesn't exist."
1807
+ local cf=($(gcloud functions list --format="csv[no-heading,separator=\
1808
+ ' '](name,REGION)" | grep "${1:-"${PROJECT_NAMESPACE}"}" | head -1))
1809
+ if [[ ${#cf[@]} -lt 1 ]]; then
1810
+ printf '%s\n' "Cloud Functions [${1}] doesn't exist."
1861
1811
  else
1862
- local service_account=$(gcloud functions describe "$1" \
1812
+ local name="${cf[0]}"
1813
+ local region="${cf[1]}"
1814
+ local service_account=$(gcloud functions describe "${name}" \
1863
1815
  --region="${region}" --format="get(serviceAccountEmail)")
1864
- printf '%s' "$service_account"
1816
+ printf '%s' "${service_account}"
1865
1817
  fi
1866
1818
  }
1867
1819
 
@@ -1903,7 +1855,7 @@ EOF
1903
1855
  #######################################
1904
1856
  quit_if_failed() {
1905
1857
  printf '\n'
1906
- if [[ $1 -gt 0 ]];then
1858
+ if [[ ${1} -gt 0 ]];then
1907
1859
  printf '%s\n' "[Error] Quit."
1908
1860
  exit 1
1909
1861
  fi
package/index.js CHANGED
@@ -26,3 +26,4 @@ exports.pubsub = require('./src/components/pubsub.js');
26
26
  exports.scheduler = require('./src/components/scheduler.js');
27
27
  exports.storage = require('./src/components/storage.js');
28
28
  exports.utils = require('./src/components/utils.js');
29
+ 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": "0.8.3",
3
+ "version": "0.9.2-beta2",
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,6 +16,7 @@
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",
19
20
  "@google-cloud/automl": "^2.4.2",
20
21
  "@google-cloud/bigquery": "^5.6.0",
21
22
  "@google-cloud/datastore": "^6.4.0",
@@ -24,8 +25,8 @@
24
25
  "@google-cloud/pubsub": "^2.12.0",
25
26
  "@google-cloud/storage": "^5.8.5",
26
27
  "gaxios": "^4.3.0",
27
- "google-ads-api": "^5.2.0",
28
- "google-ads-node": "^5.0.0",
28
+ "google-ads-api": "^8.1.0",
29
+ "google-ads-node": "^6.1.3",
29
30
  "google-auth-library": "^7.1.0",
30
31
  "googleapis": "^74.2.0",
31
32
  "soap": "^0.38.0",
@@ -23,7 +23,7 @@ const stream = require('stream');
23
23
  const {google} = require('googleapis');
24
24
  const {Schema$Upload} = google.analytics;
25
25
  const AuthClient = require('./auth_client.js');
26
- const {wait, getLogger} = require('../components/utils.js');
26
+ const {wait, getLogger, BatchResult} = require('../components/utils.js');
27
27
 
28
28
  const API_SCOPES = Object.freeze([
29
29
  'https://www.googleapis.com/auth/analytics',
@@ -56,8 +56,13 @@ let DataImportClearConfig;
56
56
  * Google Analytics API v3 stub.
57
57
  */
58
58
  class Analytics {
59
- constructor() {
60
- const authClient = new AuthClient(API_SCOPES);
59
+ /**
60
+ * @constructor
61
+ * @param {!Object<string,string>=} env The environment object to hold env
62
+ * variables.
63
+ */
64
+ constructor(env = process.env) {
65
+ const authClient = new AuthClient(API_SCOPES, env);
61
66
  const auth = authClient.getDefaultAuth();
62
67
  /** @type {!google.analytics} */
63
68
  this.instance = google.analytics({
@@ -76,8 +81,7 @@ class Analytics {
76
81
  * @param {string|!stream.Readable} data A string or a stream to be uploaded.
77
82
  * @param {!DataImportConfig} config GA data import configuration.
78
83
  * @param {string=} batchId A tag for log.
79
- * @return {!Promise<boolean>} Promise returning whether data import
80
- * succeeded.
84
+ * @return {!Promise<!BatchResult>}
81
85
  */
82
86
  async uploadData(data, config, batchId = 'unnamed') {
83
87
  const uploadConfig = Object.assign(
@@ -95,28 +99,38 @@ class Analytics {
95
99
  this.logger.debug('Response: ', response);
96
100
  const job = /** @type {Schema$Upload} */ response.data;
97
101
  const uploadId = (/** @type {Schema$Upload} */job).id;
98
- console.log(`Task [${batchId}] creates GA Data import job: ${uploadId}`);
102
+ this.logger.info(
103
+ `Task [${batchId}] creates GA Data import job: ${uploadId}`);
99
104
  const jobConfig = Object.assign({uploadId}, config);
100
105
  const result = await Promise.race([
101
106
  this.checkJobStatus(jobConfig),
102
107
  wait(8 * 60 * 1000, job), // wait up to 8 minutes here
103
108
  ]);
109
+ /** @type {BatchResult} */ const batchResult = {};
104
110
  switch ((/** @type {Schema$Upload} */ result).status) {
105
111
  case 'FAILED':
106
- this.logger.error('GA Data Import failed', job);
107
- return false;
112
+ this.logger.error('GA Data Import failed', result);
113
+ batchResult.result = false;
114
+ batchResult.errors = result.errors
115
+ || [`Unknown reason. ID: ${uploadId}`];
116
+ break;
108
117
  case 'COMPLETED':
109
118
  this.logger.info(`GA Data Import job[${uploadId}] completed.`);
110
- this.logger.debug('Response: ', job);
111
- return true;
119
+ this.logger.debug('Response: ', result);
120
+ batchResult.result = true;
121
+ break;
112
122
  case 'PENDING':
113
- this.logger.info('GA Data Import pending.', job);
123
+ this.logger.info('GA Data Import pending.', result);
114
124
  this.logger.info('Still will return true here.');
115
- return true;
125
+ batchResult.result = true;
126
+ break;
116
127
  default:
117
- this.logger.error('Unknown results of GA Data Import: ', job);
118
- return false;
128
+ this.logger.error('Unknown results of GA Data Import: ', result);
129
+ batchResult.result = false;
130
+ batchResult.errors = [`Unknown status. ID: ${uploadId}`];
119
131
  }
132
+ console.log(batchResult);
133
+ return batchResult;
120
134
  }
121
135
 
122
136
  /**
@@ -55,11 +55,13 @@ class AuthClient {
55
55
  /**
56
56
  * Create a new instance with given API scopes.
57
57
  * @param {string|!Array<string>|!ReadonlyArray<string>} scopes
58
+ * @param {!Object<string,string>=} env The environment object to hold env
59
+ * variables.
58
60
  */
59
- constructor(scopes) {
61
+ constructor(scopes, env = process.env) {
60
62
  this.scopes = scopes;
61
- this.oauthTokenFile = getFullPathForEnv(DEFAULT_ENV_OAUTH);
62
- this.serviceAccountKeyFile = getFullPathForEnv(DEFAULT_ENV_KEYFILE);
63
+ this.oauthTokenFile = getFullPathForEnv(DEFAULT_ENV_OAUTH, env);
64
+ this.serviceAccountKeyFile = getFullPathForEnv(DEFAULT_ENV_KEYFILE, env);
63
65
  }
64
66
 
65
67
  /**
@@ -109,7 +111,7 @@ class AuthClient {
109
111
  */
110
112
  getOAuth2Client(keyFile = this.oauthTokenFile) {
111
113
  const key = JSON.parse(fs.readFileSync(keyFile).toString());
112
- console.log(`Get OAuth token with Email: ${key.client_id}`);
114
+ console.log(`Get OAuth token with client Id: ${key.client_id}`);
113
115
  const oAuth2Client = new OAuth2Client(key.client_id, key.client_secret);
114
116
  oAuth2Client.setCredentials({refresh_token: key.token.refresh_token});
115
117
  return oAuth2Client;
@@ -152,15 +154,18 @@ class AuthClient {
152
154
  * Returns the full path of a existent file whose path (relative or
153
155
  * absolute) is the value of the given environment variable.
154
156
  * @param {string} envName The name of environment variable for the file path.
157
+ * @param {!Object<string,string>=} env The environment object to hold env
158
+ * variables.
155
159
  * @return {?string} Full path of the file what set as an environment variable.
156
160
  */
157
- function getFullPathForEnv(envName) {
158
- if (typeof process.env[envName] === 'undefined') {
161
+ function getFullPathForEnv(envName, env) {
162
+ const envValue = env[envName];
163
+ if (typeof envValue === 'undefined') {
159
164
  console.log(`Env[${envName}] doesn't have a value.`);
160
165
  } else {
161
- const fullPath = process.env[envName].startsWith('/') ?
162
- process.env[envName] :
163
- path.join(__dirname, process.env[envName]);
166
+ const fullPath = envValue.startsWith('/') ?
167
+ envValue :
168
+ path.join(__dirname, envValue);
164
169
  if (fs.existsSync(fullPath)) {
165
170
  console.log(`Find file '${fullPath}' set in env as [${envName}].`);
166
171
  return fullPath;
@@ -32,11 +32,11 @@ const API_VERSION = 'v1';
32
32
  * Google cloud client libraries.
33
33
  */
34
34
  class CloudPlatformApis {
35
- constructor() {
35
+ constructor(projectId = process.env['GCP_PROJECT']) {
36
36
  /** @const {!AuthClient} */
37
37
  const authClient = new AuthClient(API_SCOPES);
38
38
  this.auth = authClient.getApplicationDefaultCredentials();
39
- this.projectId = process.env['GCP_PROJECT'];
39
+ this.projectId = projectId;
40
40
  }
41
41
 
42
42
  /**
@@ -18,24 +18,23 @@
18
18
  'use strict';
19
19
 
20
20
  const {protos: {google: {ads: {googleads}}}} = require('google-ads-node');
21
+ const googleAdsLib = googleads[Object.keys(googleads)[0]];
21
22
  const {
22
- v7: {
23
- common: {
24
- UserData,
25
- UserIdentifier,
26
- CustomerMatchUserListMetadata,
27
- },
28
- resources: {
29
- GoogleAdsField,
30
- },
31
- services: {
32
- UploadClickConversionsRequest,
33
- UploadUserDataRequest,
34
- UserDataOperation,
35
- SearchGoogleAdsFieldsRequest,
36
- },
37
- }
38
- } = googleads;
23
+ common: {
24
+ UserData,
25
+ UserIdentifier,
26
+ CustomerMatchUserListMetadata,
27
+ },
28
+ resources: {
29
+ GoogleAdsField,
30
+ },
31
+ services: {
32
+ UploadClickConversionsRequest,
33
+ UploadUserDataRequest,
34
+ UserDataOperation,
35
+ SearchGoogleAdsFieldsRequest,
36
+ },
37
+ } = googleAdsLib;
39
38
  const {GoogleAdsApi} = require('google-ads-api');
40
39
  const AuthClient = require('./auth_client.js');
41
40
  const {getLogger} = require('../components/utils.js');
@@ -64,7 +64,7 @@ class MeasurementProtocol {
64
64
  * @param {string} batchId The tag for log.
65
65
  * @return {!Promise<boolean>}
66
66
  */
67
- return (lines, batchId) => {
67
+ return async (lines, batchId) => {
68
68
  const payload =
69
69
  lines
70
70
  .map((line) => {
@@ -85,24 +85,37 @@ class MeasurementProtocol {
85
85
  body: payload,
86
86
  headers: {'User-Agent': 'Tentacles/MeasurementProtocol-v1'}
87
87
  };
88
- return request(requestOptions).then((response) => {
89
- if (response.status < 200 || response.status >= 300) {
90
- const errorMessages = [
91
- `Measurement Protocol [${batchId}] didn't succeed.`,
92
- `Get response code: ${response.status}`,
93
- `response: ${response.data}`,
94
- ];
95
- console.error(errorMessages.join('\n'));
96
- throw new Error(`Status code not 2XX`);
97
- }
98
- this.logger.debug(`Configuration:`, config);
99
- this.logger.debug(`Input Data: `, lines);
100
- this.logger.debug(`Batch[${batchId}] status: ${response.status}`);
101
- this.logger.debug(response.data);
102
- // There is not enough information from the non-debug mode.
103
- if (!this.debugMode) return true;
104
- return response.data.hitParsingResult.every((result) => result.valid);
105
- });
88
+ const response = await request(requestOptions);
89
+ console.log(response);
90
+ if (response.status < 200 || response.status >= 300) {
91
+ const errorMessages = [
92
+ `Measurement Protocol [${batchId}] didn't succeed.`,
93
+ `Get response code: ${response.status}`,
94
+ `response: ${response.data}`,
95
+ ];
96
+ console.error(errorMessages.join('\n'));
97
+ throw new Error(`Status code not 2XX`);
98
+ }
99
+ this.logger.debug(`Configuration:`, config);
100
+ this.logger.debug(`Input Data: `, lines);
101
+ this.logger.debug(`Batch[${batchId}] status: ${response.status}`);
102
+ this.logger.debug(response.data);
103
+ // There is not enough information from the non-debug mode.
104
+ if (!this.debugMode) return true;
105
+ const failedHits = [];
106
+ const failedReasons = new Set();
107
+ const errorMessage = response.data.hitParsingResult
108
+ .forEach((result, index) => {
109
+ if (!result.valid) {
110
+ failedHits.push(lines[index]);
111
+ result.parserMessage.forEach(({description}) => {
112
+ failedReasons.add(description);
113
+ })
114
+ }
115
+ });
116
+ console.log(failedHits);
117
+ console.log(failedReasons);
118
+ return errorMessage;
106
119
  };
107
120
  };
108
121
 
@@ -217,11 +217,14 @@ class FirestoreAccessBase {
217
217
  * Returns whether the mode of Firestore is 'Native'.
218
218
  * @return {!Promise<boolean>}
219
219
  */
220
- static isNativeMode() {
221
- return new Firestore().listCollections().then(() => true).catch((error) => {
220
+ static async isNativeMode(projectId = process.env['GCP_PROJECT']) {
221
+ try {
222
+ await new Firestore({projectId}).listCollections();
223
+ return true;
224
+ } catch (error) {
222
225
  console.log(`In detecting Firestore mode: `, error.message);
223
226
  return false;
224
- });
227
+ }
225
228
  };
226
229
  }
227
230
 
@@ -54,19 +54,22 @@ class DataAccessObject {
54
54
  * @param {string} kind The data model name.
55
55
  * @param {string} namespace The namespace of the data.
56
56
  * @param {!DataSource} dataSource The data source type.
57
+ * @param {string} projectId The Id of Cloud project.
57
58
  */
58
59
  constructor(kind, namespace,
59
- dataSource = DataAccessObject.getDataSourceFromEnvironment()) {
60
+ dataSource = DataAccessObject.getDataSourceFromEnvironment(),
61
+ projectId = process.env['GCP_PROJECT']) {
60
62
  /** @const {string} */ this.namespace = namespace;
61
63
  /** @const {!DataSource} */ this.dataSource = dataSource;
62
64
  /** @type {!FirestoreAccessBase} */ this.accessObject = undefined;
63
65
  switch (this.dataSource) {
64
66
  case DataSource.FIRESTORE:
65
67
  this.accessObject = new NativeModeAccess(
66
- `${this.namespace}/database/${kind}`);
68
+ `${this.namespace}/database/${kind}`, projectId);
67
69
  break;
68
70
  case DataSource.DATASTORE:
69
- this.accessObject = new DatastoreModeAccess(this.namespace, kind);
71
+ this.accessObject = new DatastoreModeAccess(this.namespace, kind,
72
+ projectId);
70
73
  break;
71
74
  default:
72
75
  throw new Error(`Unknown DataSource item: ${this.dataSource}.`);
@@ -46,10 +46,11 @@ class DatastoreModeAccess {
46
46
  * Initializes DatastoreModeAccess instance.
47
47
  * @param {string} namespace The namespace for data.
48
48
  * @param {string} kind The kind of this entity.
49
+ * @param {string} projectId The Id of Cloud project.
49
50
  */
50
- constructor(namespace, kind) {
51
+ constructor(namespace, kind, projectId = process.env['GCP_PROJECT']) {
51
52
  /** @type{Datastore} */
52
- this.datastore = new Datastore();
53
+ this.datastore = new Datastore({projectId});
53
54
  this.kind = kind;
54
55
  this.namespace = namespace;
55
56
  this.logger = getLogger('DS.ACC');
@@ -41,10 +41,11 @@ class NativeModeAccess {
41
41
  * way. This constructor will check the path to make sure it presents a
42
42
  * 'collection', otherwise an Error will be thrown.
43
43
  * @param {string} path Path for the 'collection'.
44
+ * @param {string} projectId The Id of Cloud project.
44
45
  */
45
- constructor(path) {
46
+ constructor(path, projectId = process.env['GCP_PROJECT']) {
46
47
  /** @type {!Firestore} */
47
- this.firestore = new Firestore();
48
+ this.firestore = new Firestore({projectId});
48
49
  if (path.split('/').length % 2 === 0) {
49
50
  throw new Error(`Invalid path for Collection: ${path}`);
50
51
  }
@@ -30,11 +30,11 @@ const API_VERSION = 'v1';
30
30
  * to get/pause/resume a job.
31
31
  */
32
32
  class CloudScheduler {
33
- constructor() {
33
+ constructor(projectId = process.env['GCP_PROJECT']) {
34
34
  /** @const {!AuthClient} */
35
35
  const authClient = new AuthClient(API_SCOPES);
36
36
  this.auth = authClient.getApplicationDefaultCredentials();
37
- this.projectId = process.env['GCP_PROJECT'];
37
+ this.projectId = projectId;
38
38
  this.instance = cloudscheduler({
39
39
  version: API_VERSION,
40
40
  auth: this.auth,
@@ -23,12 +23,23 @@ const {inspect} = require('util');
23
23
  const {LoggingWinston} = require('@google-cloud/logging-winston');
24
24
  const {CloudPlatformApis} = require('../apis/cloud_platform_apis.js');
25
25
 
26
+ /**
27
+ * The result of a batch of data sent to target API.
28
+ *
29
+ * @typedef {{
30
+ * result: boolean,
31
+ * errors: (Array<string>|undefined),
32
+ * output: (Array<string>|undefined),
33
+ * }}
34
+ */
35
+ let BatchResult;
36
+
26
37
  /**
27
38
  * Function which sends a batch of data. Takes two parameters:
28
39
  * {!Array<string>} Data for single request. It should be guaranteed that it
29
40
  * doesn't exceed quota limitation.
30
41
  * {string} The tag for log.
31
- * @typedef {function(!Array<string>,string): !Promise<boolean>}
42
+ * @typedef {function(!Array<string>,string): !Promise<!BatchResult>}
32
43
  */
33
44
  let SendSingleBatch;
34
45
 
@@ -414,10 +425,12 @@ const extractObject = (paths) => {
414
425
  * status code 1 to let the invoker (the Bash installation script) know that it
415
426
  * doesn't pass.
416
427
  * @param {!Array<string>} permissions Array of permissions to check.
428
+ * @param {string} projectId The Id of Cloud project.
417
429
  * @return {!Promise<undefined>}
418
430
  */
419
- const checkPermissions = (permissions) => {
420
- const cloudPlatformApis = new CloudPlatformApis();
431
+ const checkPermissions = (permissions,
432
+ projectId = process.env['GCP_PROJECT']) => {
433
+ const cloudPlatformApis = new CloudPlatformApis(projectId);
421
434
  return cloudPlatformApis.testIamPermissions(permissions)
422
435
  .then((grantedPermissions) => {
423
436
  console.log(grantedPermissions);
@@ -461,6 +474,7 @@ const changeNamingFromSnakeToLowerCamel = (name) => {
461
474
  module.exports = {
462
475
  getLogger,
463
476
  wait,
477
+ BatchResult,
464
478
  SendSingleBatch,
465
479
  apiSpeedControl,
466
480
  splitArray,
@@ -0,0 +1,115 @@
1
+ // Copyright 2021 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 Google Cloud Vertex AI API helper.
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const {JobServiceClient} = require('@google-cloud/aiplatform');
22
+
23
+ /**
24
+ * Wrapper class for AI Platform (Vertex AI) API.
25
+ * 1. Create or get batch prediction jobs.
26
+ * @see https://cloud.google.com/vertex-ai/docs/reference/rest/v1/projects.locations.batchPredictionJobs#BatchPredictionJob
27
+ */
28
+ class VertexAi {
29
+
30
+ /**
31
+ * Initialize an instance.
32
+ * @param {{keyFilename:(string|undefined)}} options
33
+ */
34
+ constructor(options = {}) {
35
+ this.options = options;
36
+ }
37
+
38
+ /**
39
+ * Starts a BatchPredictionJob and returns the job name.
40
+ * @see https://googleapis.dev/nodejs/aiplatform/latest/v1.JobServiceClient.html#createBatchPredictionJob
41
+ * @see https://googleapis.dev/nodejs/aiplatform/latest/google.cloud.aiplatform.v1.BatchPredictionJob.html
42
+ * @param {string} projectId
43
+ * @param {string} location
44
+ * @param {string} modelId
45
+ * @param {google.cloud.aiplatform.v1.BatchPredictionJob.IInputConfig} inputConfig
46
+ * @param {google.cloud.aiplatform.v1.BatchPredictionJob.IOutputConfig} outputConfig
47
+ * @param {string=} displayName
48
+ * @return {Promise<string>} BatchPredictionJob name.
49
+ */
50
+ async batchPredict(projectId, location, modelId, inputConfig,
51
+ outputConfig, displayName = 'Batch Prediction Job') {
52
+ const jobServiceClient = this.getJobServiceClient_(location);
53
+ const parent = `projects/${projectId}/locations/${location}`;
54
+ const model = jobServiceClient.modelPath(projectId, location, modelId);
55
+ const batchPredictionJob = {
56
+ displayName,
57
+ model,
58
+ inputConfig,
59
+ outputConfig,
60
+ };
61
+ const request = {
62
+ parent,
63
+ batchPredictionJob,
64
+ };
65
+ const [response] = await jobServiceClient.createBatchPredictionJob(request);
66
+ return response.name;
67
+ }
68
+
69
+ /**
70
+ * Gets a BatchPredictionJob.
71
+ * @see https://googleapis.dev/nodejs/aiplatform/latest/v1.JobServiceClient.html#getBatchPredictionJob
72
+ * @param {string} jobName
73
+ * @param {(string|undefined)=} explicitLocation Location of the service
74
+ * endpoint. A location will be extracted from jobName if omitted.
75
+ * @return {Promise<google.cloud.aiplatform.v1.BatchPredictionJob>}
76
+ */
77
+ async getBatchPredictionJob(jobName, explicitLocation = undefined) {
78
+ let location = explicitLocation;
79
+ if (!location) {
80
+ location = /projects\/[^\/]*\/locations\/([^\/]*)\/.*/.exec(jobName)[1];
81
+ }
82
+ const jobServiceClient = this.getJobServiceClient_(location);
83
+ const request = {name: jobName};
84
+ const [response] = await jobServiceClient.getBatchPredictionJob(request);
85
+ return response;
86
+ }
87
+
88
+ /**
89
+ * Gets the service endpoint for Vertex AI.
90
+ * @see https://cloud.google.com/vertex-ai/docs/general/locations
91
+ * @see https://cloud.google.com/vertex-ai/docs/reference/rest#service-endpoint
92
+ * @param {string} location
93
+ * @return {string} The service endpoint.
94
+ * @private
95
+ */
96
+ getServiceEndpoint_(location) {
97
+ return `${location}-aiplatform.googleapis.com`;
98
+ }
99
+
100
+ /**
101
+ * Gets the service for creating and managing AI Platform's jobs.
102
+ * @see https://googleapis.dev/nodejs/aiplatform/latest/v1.JobServiceClient.html
103
+ * @param {string} location
104
+ * @return {!JobServiceClient}
105
+ * @private
106
+ */
107
+ getJobServiceClient_(location) {
108
+ const clientOptions = {
109
+ apiEndpoint: this.getServiceEndpoint_(location),
110
+ };
111
+ return new JobServiceClient(clientOptions);
112
+ }
113
+ }
114
+
115
+ exports.VertexAi = VertexAi;