@google-cloud/nodejs-common 0.9.2-beta → 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.
@@ -15,7 +15,7 @@
15
15
  # limitations under the License.
16
16
 
17
17
  # Cloud Functions Runtime Environment.
18
- CF_RUNTIME="${CF_RUNTIME:=nodejs14}"
18
+ CF_RUNTIME="${CF_RUNTIME:=nodejs10}"
19
19
 
20
20
  # Counter for steps.
21
21
  STEP=0
@@ -138,14 +138,6 @@ EXTERNAL_API_SCOPES=(
138
138
  # Enabled APIs' OAuth scopes.
139
139
  ENABLED_OAUTH_SCOPES=()
140
140
 
141
- # No explicit service account is required.
142
- # https://cloud.google.com/iam/docs/understanding-roles#service-accounts-roles
143
- #declare -A GOOGLE_SERVICE_ACCOUNT_PERMISSIONS
144
- #GOOGLE_SERVICE_ACCOUNT_PERMISSIONS=(
145
- # ["Service Account Admin"]="iam.serviceAccounts.create"
146
- # ["Service Account Key Admin"]="iam.serviceAccounts.create"
147
- #)
148
-
149
141
  # Preparation functions.
150
142
  #######################################
151
143
  # Mimic a Cloud Shell environment to enable local running.
@@ -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:
@@ -1232,7 +1300,9 @@ save_config() {
1232
1300
  # For service account, given that Cloud Functions can extend the authorized API
1233
1301
  # scopes now, it will use the default service account rather than an explicit
1234
1302
  # service account with the key file downloaded. By doing so, we can reduce the
1235
- # risk of leaking service account key.
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.
1236
1306
  # For OAuth 2.0, guide user to complete OAuth authentication and save the
1237
1307
  # refresh token.
1238
1308
  # Globals:
@@ -1242,155 +1312,11 @@ save_config() {
1242
1312
  # None
1243
1313
  #######################################
1244
1314
  do_authentication(){
1245
- # If there is a service key in the current folder from previous installation,
1246
- # the code will continue using it, otherwise it will use the Cloud Functions'
1247
- # default service account.
1248
- # if [[ ${NEED_SERVICE_ACCOUNT} == "true" ]]; then
1249
- # download_service_account_key
1250
- # fi
1251
1315
  if [[ ${NEED_OAUTH} == "true" ]]; then
1252
1316
  do_oauth
1253
1317
  fi
1254
1318
  }
1255
1319
 
1256
- #######################################
1257
- # Download a service account key file and save as `$SA_KEY_FILE`.
1258
- # Globals:
1259
- # SA_NAME
1260
- # GCP_PROJECT
1261
- # SA_KEY_FILE
1262
- # Arguments:
1263
- # None
1264
- # Returns:
1265
- # 0 if service key files exists or created, non-zero on error.
1266
- #######################################
1267
- download_service_account_key() {
1268
- (( STEP += 1 ))
1269
- printf '%s\n' "Step ${STEP}: Downloading the key file for the service \
1270
- account..."
1271
- if [[ -z ${SA_NAME} ]];then
1272
- confirm_service_account
1273
- fi
1274
- local suffix exist
1275
- suffix=$(get_sa_domain_from_gcp_id "${GCP_PROJECT}")
1276
- local email="${SA_NAME}@${suffix}"
1277
- local prompt="Would you like to download the key file for [${email}] and \
1278
- save it as ${SA_KEY_FILE}? [Y/n]: "
1279
- local default_value="y"
1280
- if [[ -f "${SA_KEY_FILE}" && -s "${SA_KEY_FILE}" ]]; then
1281
- exist=$(get_value_from_json_file ${SA_KEY_FILE} 'client_email' 2>&1)
1282
- if [[ ${exist} =~ .*("@${suffix}") ]]; then
1283
- prompt="A key file for [${exist}] with the key ID '\
1284
- $(get_value_from_json_file ${SA_KEY_FILE} 'private_key_id') already exists'. \
1285
- Would you like to create a new key to overwrite it? [N/y]: "
1286
- default_value="n"
1287
- fi
1288
- fi
1289
- printf '%s' "${prompt}"
1290
- local input
1291
- read -r input
1292
- input=${input:-"${default_value}"}
1293
- if [[ ${input} == 'y' || ${input} == 'Y' ]];then
1294
- printf '%s\n' "Downloading a new key file for [${email}]..."
1295
- gcloud iam service-accounts keys create "${SA_KEY_FILE}" --iam-account \
1296
- "${email}"
1297
- if [[ $? -gt 0 ]]; then
1298
- printf '%s\n' "Failed to download new key files for [${email}]."
1299
- return 1
1300
- else
1301
- printf '%s\n' "OK. New key file is saved at [${SA_KEY_FILE}]."
1302
- return 0
1303
- fi
1304
- else
1305
- printf '%s\n' "Skipped downloading new key file. See \
1306
- https://cloud.google.com/iam/docs/creating-managing-service-account-keys \
1307
- to learn more about service account key files."
1308
- return 0
1309
- fi
1310
- }
1311
-
1312
- #######################################
1313
- # Make sure a service account for this integration exists and set the email of
1314
- # the service account to the global variable `SA_NAME`.
1315
- # Globals:
1316
- # GCP_PROJECT
1317
- # SA_KEY_FILE
1318
- # SA_NAME
1319
- # DEFAULT_SERVICE_ACCOUNT
1320
- # Arguments:
1321
- # None
1322
- #######################################
1323
- confirm_service_account() {
1324
- cat <<EOF
1325
- Some external APIs might require authentication based on OAuth or \
1326
- JWT(service account), for example, Google Analytics or Campaign Manager. \
1327
- In this step, you prepare the service account. For more information, see \
1328
- https://cloud.google.com/iam/docs/creating-managing-service-accounts
1329
- EOF
1330
-
1331
- local suffix
1332
- suffix=$(get_sa_domain_from_gcp_id "${GCP_PROJECT}")
1333
- local email
1334
- if [[ -f "${SA_KEY_FILE}" && -s "${SA_KEY_FILE}" ]]; then
1335
- email=$(get_value_from_json_file "${SA_KEY_FILE}" 'client_email')
1336
- if [[ ${email} =~ .*("@${suffix}") ]]; then
1337
- printf '%s' "A key file for service account [${email}] already exists. \
1338
- Would you like to create a new service account? [N/y]: "
1339
- local input
1340
- read -r input
1341
- if [[ ${input} != 'y' && ${input} != 'Y' ]]; then
1342
- printf '%s\n' "OK. Will use existing service account [${email}]."
1343
- SA_NAME=$(printf "${email}" | cut -d@ -f1)
1344
- return 0
1345
- fi
1346
- fi
1347
- fi
1348
-
1349
- SA_NAME="${SA_NAME:-"${PROJECT_NAMESPACE}-api"}"
1350
- while :; do
1351
- printf '%s' "Enter the name of service account [${SA_NAME}]: "
1352
- local input sa_elements=() sa
1353
- read -r input
1354
- input=${input:-"${SA_NAME}"}
1355
- IFS='@' read -a sa_elements <<< "${input}"
1356
- if [[ ${#sa_elements[@]} == 1 ]]; then
1357
- echo " Append default suffix to service account name and get: ${email}"
1358
- sa="${input}"
1359
- email="${sa}@${suffix}"
1360
- else
1361
- if [[ ${sa_elements[1]} != "${suffix}" ]]; then
1362
- printf '%s\n' " Error: Service account domain name ${sa_elements[1]} \
1363
- doesn't belong to the current project. The service account domain name for the \
1364
- current project should be: ${suffix}."
1365
- continue
1366
- fi
1367
- sa="${sa_elements[0]}"
1368
- email="${input}"
1369
- fi
1370
-
1371
- printf '%s\n' "Checking the existence of the service account [${email}]..."
1372
- if ! result=$(gcloud iam service-accounts describe "${email}" 2>&1); then
1373
- printf '%s\n' " Service account [${email}] does not exist. Trying to \
1374
- create..."
1375
- gcloud iam service-accounts create "${sa}" --display-name \
1376
- "Tentacles API requester"
1377
- if [[ $? -gt 0 ]]; then
1378
- printf '%s\n' "Creating the service account [${email}] failed. Please \
1379
- try again..."
1380
- else
1381
- printf '%s\n' "The service account [${email}] was successfully created."
1382
- SA_NAME=${sa}
1383
- break
1384
- fi
1385
- else
1386
- printf ' found.\n'
1387
- SA_NAME=${sa}
1388
- break
1389
- fi
1390
- done
1391
- printf '%s\n' "OK. Service account [${SA_NAME}] is ready."
1392
- }
1393
-
1394
1320
  #######################################
1395
1321
  # Guide an OAuth process and save the token to file.
1396
1322
  # The whole process will be:
@@ -1517,87 +1443,6 @@ EOF
1517
1443
  done
1518
1444
  }
1519
1445
 
1520
- #######################################
1521
- # Create or update a Log router sink.
1522
- # Globals:
1523
- # None
1524
- # Arguments:
1525
- # Name of the sink
1526
- # Filter conditions
1527
- # Sink destination
1528
- #######################################
1529
- create_or_update_sink() {
1530
- local sinkName=${1}
1531
- local logFilter=${2}
1532
- local sinkDestAndFlags=(${3})
1533
- local existingFilter
1534
- existingFilter=$(gcloud logging sinks list --filter="name:${sinkName}" \
1535
- --format="value(filter)")
1536
- if [[ "${existingFilter}" != "${logFilter}" ]]; then
1537
- local action
1538
- if [[ -z "${existingFilter}" ]]; then
1539
- action="create"
1540
- printf '%s\n' " Logging Export [${sinkName}] doesn't exist. Creating..."
1541
- else
1542
- action="update"
1543
- printf '%s\n' " Logging Export [${sinkName}] exists with a different \
1544
- filter. Updating..."
1545
- fi
1546
- gcloud -q logging sinks ${action} "${sinkName}" "${sinkDestAndFlags[@]}" \
1547
- --log-filter="${logFilter}"
1548
- else
1549
- printf '%s\n' " Logging Export [${sinkName}] exists. Continue..."
1550
- fi
1551
- if [[ $? -gt 0 ]];then
1552
- printf '%s\n' "Failed to create or update Logs router sink."
1553
- return 1
1554
- fi
1555
- }
1556
-
1557
- #######################################
1558
- # Confirm the service account of the given sink has proper permission to dump
1559
- # the logs.
1560
- # Globals:
1561
- # None
1562
- # Arguments:
1563
- # Name of the sink
1564
- # Bindings role, e.g. "pubsub.publisher" or "bigquery.dataEditor"
1565
- # Role name, e.g. "Pub/Sub Publisher" or "BigQuery Data Editor"
1566
- #######################################
1567
- confirm_sink_service_account_permission() {
1568
- local sinkName=${1}
1569
- local bindingsRole=${2}
1570
- local roleName=${3}
1571
- local serviceAccount existingRole
1572
- serviceAccount=$(gcloud logging sinks describe "${sinkName}" \
1573
- --format="get(writerIdentity)")
1574
- while :;do
1575
- printf '%s\n' " Checking the role of the sink's service account \
1576
- [${serviceAccount}]..."
1577
- existingRole=$(gcloud projects get-iam-policy "${GCP_PROJECT}" \
1578
- --flatten=bindings --filter="bindings.members:${serviceAccount} AND \
1579
- bindings.role:roles/${bindingsRole}" --format="get(bindings.members)")
1580
- if [[ -z "${existingRole}" ]];then
1581
- printf '%s\n' " Granting Role '${roleName}' to the service \
1582
- account..."
1583
- gcloud -q projects add-iam-policy-binding "${GCP_PROJECT}" --member \
1584
- "${serviceAccount}" --role roles/bigquery.dataEditor
1585
- if [[ $? -gt 0 ]];then
1586
- printf '%s\n' "Failed to grant the role. Use this link \
1587
- https://console.cloud.google.com/iam-admin/iam?project=${GCP_PROJECT} to \
1588
- manually grant Role '${roleName}' to ${serviceAccount}."
1589
- printf '%s' "Press any key to continue after you grant the access..."
1590
- local any
1591
- read -n1 -s any
1592
- continue
1593
- fi
1594
- else
1595
- printf '%s\n' " The role has already been granted."
1596
- return 0
1597
- fi
1598
- done
1599
- }
1600
-
1601
1446
  #######################################
1602
1447
  # Set default configuration for a Cloud Functions.
1603
1448
  # Globals:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@google-cloud/nodejs-common",
3
- "version": "0.9.2-beta",
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",
@@ -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',
@@ -81,8 +81,7 @@ class Analytics {
81
81
  * @param {string|!stream.Readable} data A string or a stream to be uploaded.
82
82
  * @param {!DataImportConfig} config GA data import configuration.
83
83
  * @param {string=} batchId A tag for log.
84
- * @return {!Promise<boolean>} Promise returning whether data import
85
- * succeeded.
84
+ * @return {!Promise<!BatchResult>}
86
85
  */
87
86
  async uploadData(data, config, batchId = 'unnamed') {
88
87
  const uploadConfig = Object.assign(
@@ -100,29 +99,38 @@ class Analytics {
100
99
  this.logger.debug('Response: ', response);
101
100
  const job = /** @type {Schema$Upload} */ response.data;
102
101
  const uploadId = (/** @type {Schema$Upload} */job).id;
103
- console.log(`Task [${batchId}] creates GA Data import job: ${uploadId}`);
102
+ this.logger.info(
103
+ `Task [${batchId}] creates GA Data import job: ${uploadId}`);
104
104
  const jobConfig = Object.assign({uploadId}, config);
105
105
  const result = await Promise.race([
106
106
  this.checkJobStatus(jobConfig),
107
107
  wait(8 * 60 * 1000, job), // wait up to 8 minutes here
108
108
  ]);
109
+ /** @type {BatchResult} */ const batchResult = {};
109
110
  switch ((/** @type {Schema$Upload} */ result).status) {
110
111
  case 'FAILED':
111
112
  this.logger.error('GA Data Import failed', result);
112
- const errors = result.errors || [`Unknown reason. ID: ${uploadId}`];
113
- throw new Error(errors.join('\n'));
113
+ batchResult.result = false;
114
+ batchResult.errors = result.errors
115
+ || [`Unknown reason. ID: ${uploadId}`];
116
+ break;
114
117
  case 'COMPLETED':
115
118
  this.logger.info(`GA Data Import job[${uploadId}] completed.`);
116
119
  this.logger.debug('Response: ', result);
117
- return true;
120
+ batchResult.result = true;
121
+ break;
118
122
  case 'PENDING':
119
123
  this.logger.info('GA Data Import pending.', result);
120
124
  this.logger.info('Still will return true here.');
121
- return true;
125
+ batchResult.result = true;
126
+ break;
122
127
  default:
123
128
  this.logger.error('Unknown results of GA Data Import: ', result);
124
- throw new Error(`Unknown status. ID: ${uploadId}`);
129
+ batchResult.result = false;
130
+ batchResult.errors = [`Unknown status. ID: ${uploadId}`];
125
131
  }
132
+ console.log(batchResult);
133
+ return batchResult;
126
134
  }
127
135
 
128
136
  /**
@@ -55,6 +55,8 @@ 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
61
  constructor(scopes, env = process.env) {
60
62
  this.scopes = scopes;
@@ -152,7 +154,7 @@ 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.
155
- * @param {!Object<string,string>} env The environment object to hold env
157
+ * @param {!Object<string,string>=} env The environment object to hold env
156
158
  * variables.
157
159
  * @return {?string} Full path of the file what set as an environment variable.
158
160
  */
@@ -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
 
@@ -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
 
@@ -463,6 +474,7 @@ const changeNamingFromSnakeToLowerCamel = (name) => {
463
474
  module.exports = {
464
475
  getLogger,
465
476
  wait,
477
+ BatchResult,
466
478
  SendSingleBatch,
467
479
  apiSpeedControl,
468
480
  splitArray,