@google-cloud/nodejs-common 2.2.2 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -17
- package/src/apis/auth_client.js +10 -10
- package/src/apis/base/ads_api_common.js +1 -1
- package/src/apis/base/google_api_client.js +1 -1
- package/src/apis/doubleclick_bidmanager.js +3 -3
- package/src/apis/gmail.js +113 -0
- package/src/apis/google_ads_api.js +17 -17
- package/src/apis/index.js +10 -15
- package/src/apis/sendgrid.js +62 -1
- package/src/apis/spreadsheets.js +27 -6
- package/src/apis/youtube.js +0 -284
- package/src/components/firestore/access_base.js +3 -3
- package/src/components/firestore/data_access_object.js +4 -4
- package/src/components/firestore/datastore_mode_access.js +3 -3
- package/src/components/pubsub.js +1 -1
- package/src/components/scheduler.js +1 -1
- package/src/components/storage.js +1 -1
- package/src/components/utils.js +1 -1
- package/src/apis/google_ads.js +0 -1145
package/src/apis/youtube.js
CHANGED
|
@@ -34,130 +34,9 @@ const API_SCOPES = Object.freeze([
|
|
|
34
34
|
]);
|
|
35
35
|
const API_VERSION = 'v3';
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
* Configuration for listing youtube channels.
|
|
39
|
-
* @see https://developers.google.com/youtube/v3/docs/channels/list
|
|
40
|
-
* @typedef {{
|
|
41
|
-
* part: Array<string>,
|
|
42
|
-
* categoryId: (string|undefined),
|
|
43
|
-
* forUsername: (string|undefined),
|
|
44
|
-
* id: (string|undefined),
|
|
45
|
-
* managedByMe: (boolean|undefined),
|
|
46
|
-
* mine: (boolean|undefined),
|
|
47
|
-
* hl: (string|undefined),
|
|
48
|
-
* onBehalfOfContentOwner: (string|undefined),
|
|
49
|
-
* pageToken: (string|undefined),
|
|
50
|
-
* }}
|
|
51
|
-
*/
|
|
52
|
-
let ListChannelsConfig;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Configuration for listing youtube videos.
|
|
56
|
-
* @see https://developers.google.com/youtube/v3/docs/videos/list
|
|
57
|
-
* @typedef {{
|
|
58
|
-
* part: Array<string>,
|
|
59
|
-
* id: (string|undefined),
|
|
60
|
-
* chart: (string|undefined),
|
|
61
|
-
* hl: (string|undefined),
|
|
62
|
-
* maxHeight: (unsigned integer|undefined),
|
|
63
|
-
* maxResults: (unsigned integer|undefined),
|
|
64
|
-
* maxWidth: (unsigned integer|undefined),
|
|
65
|
-
* onBehalfOfContentOwner: (string|undefined),
|
|
66
|
-
* pageToken: (string|undefined),
|
|
67
|
-
* regionCode: (string|undefined),
|
|
68
|
-
* }}
|
|
69
|
-
*/
|
|
70
|
-
let ListVideosConfig;
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Configuration for listing youtube comment threads by the given id.
|
|
74
|
-
* @see https://developers.google.com/youtube/v3/docs/commentThreads/list
|
|
75
|
-
* @typedef {{
|
|
76
|
-
* part: Array<string>,
|
|
77
|
-
* id: (string|undefined),
|
|
78
|
-
* allThreadsRelatedToChannelId: (string|undefined),
|
|
79
|
-
* channelId: (string|undefined),
|
|
80
|
-
* videoId: (string|undefined),
|
|
81
|
-
* maxResults: (unsigned integer|undefined),
|
|
82
|
-
* moderationStatus: (string|undefined),
|
|
83
|
-
* order: (string|undefined),
|
|
84
|
-
* pageToken: (string|undefined),
|
|
85
|
-
* searchTerms: (string|undefined),
|
|
86
|
-
* textFormat: (string|undefined),
|
|
87
|
-
* limit: (unsigned integer|undefined),
|
|
88
|
-
* }}
|
|
89
|
-
*/
|
|
90
|
-
let ListCommentThreadsConfig;
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Configuration for listing youtube play list.
|
|
94
|
-
* @see https://developers.google.com/youtube/v3/docs/Playlists/list
|
|
95
|
-
* @typedef {{
|
|
96
|
-
* part: Array<string>,
|
|
97
|
-
* channelId: (string|undefined),
|
|
98
|
-
* id: (string|undefined),
|
|
99
|
-
* mine: (boolean|undefined),
|
|
100
|
-
* hl: (string|undefined),
|
|
101
|
-
* maxResults: (unsigned integer|undefined),
|
|
102
|
-
* onBehalfOfContentOwner: (string|undefined),
|
|
103
|
-
* onBehalfOfContentOwnerChannel: (string|undefined),
|
|
104
|
-
* pageToken: (string|undefined)
|
|
105
|
-
* }}
|
|
106
|
-
*/
|
|
107
|
-
let ListPlaylistConfig;
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Configuration for listing youtube search results.
|
|
111
|
-
* @see https://developers.google.com/youtube/v3/docs/search/list
|
|
112
|
-
* @typedef {{
|
|
113
|
-
* part: Array<string>,
|
|
114
|
-
* forContentOwner: (boolean|undefined),
|
|
115
|
-
* forDeveloper: (boolean|undefined),
|
|
116
|
-
* forMine: (boolean|undefined),
|
|
117
|
-
* relatedToVideoId: (string|undefined),
|
|
118
|
-
* channelId: (string|undefined),
|
|
119
|
-
* channelType: (string|undefined),
|
|
120
|
-
* eventType: (string|undefined),
|
|
121
|
-
* location: (string|undefined),
|
|
122
|
-
* locationRadius: (string|undefined),
|
|
123
|
-
* maxResults: (unsigned integer|undefined),
|
|
124
|
-
* onBehalfOfContentOwner: (string|undefined),
|
|
125
|
-
* order: (string|undefined),
|
|
126
|
-
* pageToken: (string|undefined),
|
|
127
|
-
* publishedAfter: (datetime|undefined),
|
|
128
|
-
* publishedBefore: (datetime|undefined),
|
|
129
|
-
* q: (string|undefined),
|
|
130
|
-
* regionCode: (string|undefined),
|
|
131
|
-
* relevanceLanguage: (string|undefined),
|
|
132
|
-
* safeSearch: (string|undefined),
|
|
133
|
-
* topicId: (string|undefined),
|
|
134
|
-
* type: (string|undefined),
|
|
135
|
-
* videoCaption: (string|undefined),
|
|
136
|
-
* videoCategoryId: (string|undefined),
|
|
137
|
-
* videoDefinition: (string|undefined),
|
|
138
|
-
* videoDimension: (string|undefined),
|
|
139
|
-
* videoDuration: (string|undefined),
|
|
140
|
-
* videoEmbeddable: (string|undefined),
|
|
141
|
-
* videoLicense: (string|undefined),
|
|
142
|
-
* videoSyndicated: (string|undefined),
|
|
143
|
-
* videoType: (string|undefined)
|
|
144
|
-
* }}
|
|
145
|
-
*/
|
|
146
|
-
let ListSearchConfig;
|
|
147
|
-
|
|
148
37
|
/**
|
|
149
38
|
* Youtube API v3 stub.
|
|
150
39
|
* See: https://developers.google.com/youtube/v3/docs
|
|
151
|
-
* Channel list type definition, see:
|
|
152
|
-
* https://developers.google.com/youtube/v3/docs/channels/list
|
|
153
|
-
* Video list type definition, see:
|
|
154
|
-
* https://developers.google.com/youtube/v3/docs/videos/list
|
|
155
|
-
* CommentThread list type definition, see:
|
|
156
|
-
* https://developers.google.com/youtube/v3/docs/commentThreads/list
|
|
157
|
-
* Playlist list type definition, see:
|
|
158
|
-
* https://developers.google.com/youtube/v3/docs/playlists/list
|
|
159
|
-
* Search list type definition, see:
|
|
160
|
-
* https://developers.google.com/youtube/v3/docs/search/list
|
|
161
40
|
*/
|
|
162
41
|
class YouTube extends GoogleApiClient {
|
|
163
42
|
/**
|
|
@@ -180,173 +59,10 @@ class YouTube extends GoogleApiClient {
|
|
|
180
59
|
getVersion() {
|
|
181
60
|
return API_VERSION;
|
|
182
61
|
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Returns a collection of zero or more channel resources that match the
|
|
186
|
-
* request criteria.
|
|
187
|
-
* @see https://developers.google.com/youtube/v3/docs/channels/list
|
|
188
|
-
* @param {!ListChannelsConfig} config List channels configuration.
|
|
189
|
-
* @return {!Promise<Array<Schema$Channel>>}
|
|
190
|
-
*/
|
|
191
|
-
async listChannels(config) {
|
|
192
|
-
const channelListRequest = Object.assign({}, config);
|
|
193
|
-
channelListRequest.part = channelListRequest.part.join(',')
|
|
194
|
-
try {
|
|
195
|
-
const youtube = await this.getApiClient();
|
|
196
|
-
const response = await youtube.channels.list(channelListRequest);
|
|
197
|
-
this.logger.debug('Response: ', response);
|
|
198
|
-
return response.data.items;
|
|
199
|
-
} catch (error) {
|
|
200
|
-
const errorMsg =
|
|
201
|
-
`Fail to list channels: ${JSON.stringify(channelListRequest)}`;
|
|
202
|
-
this.logger.error('YouTube list channels failed.', error.message);
|
|
203
|
-
this.logger.debug('Errors in response:', error);
|
|
204
|
-
throw new Error(errorMsg);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Returns a list of videos that match the API request parameters.
|
|
210
|
-
* @see https://developers.google.com/youtube/v3/docs/videos/list
|
|
211
|
-
* @param {!ListVideosConfig} config List videos configuration.
|
|
212
|
-
* @return {!Promise<Array<Schema$Video>>}
|
|
213
|
-
*/
|
|
214
|
-
async listVideos(config) {
|
|
215
|
-
const videoListRequest = Object.assign({}, config);
|
|
216
|
-
videoListRequest.part = videoListRequest.part.join(',')
|
|
217
|
-
try {
|
|
218
|
-
const youtube = await this.getApiClient();
|
|
219
|
-
const response = await youtube.videos.list(videoListRequest);
|
|
220
|
-
this.logger.debug('Response: ', response);
|
|
221
|
-
return response.data.items;
|
|
222
|
-
} catch (error) {
|
|
223
|
-
const errorMsg =
|
|
224
|
-
`Fail to list videos: ${JSON.stringify(videoListRequest)}`;
|
|
225
|
-
this.logger.error('YouTube list videos failed.', error.message);
|
|
226
|
-
this.logger.debug('Errors in response:', error);
|
|
227
|
-
throw new Error(errorMsg);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Returns a list of comment threads that match the API request parameters.
|
|
233
|
-
* @see https://developers.google.com/youtube/v3/docs/videos/list
|
|
234
|
-
* @param {!ListCommentThreadsConfig} config List comment threads
|
|
235
|
-
* configuration.
|
|
236
|
-
* @return {!Promise<Array<Schema$CommentThread>>}
|
|
237
|
-
*/
|
|
238
|
-
async listCommentThreads(config) {
|
|
239
|
-
const commentThreadsRequest = Object.assign({}, config);
|
|
240
|
-
commentThreadsRequest.part = commentThreadsRequest.part.join(',')
|
|
241
|
-
try {
|
|
242
|
-
const youtube = await this.getApiClient();
|
|
243
|
-
const response = await youtube.commentThreads.list(commentThreadsRequest);
|
|
244
|
-
this.logger.debug('Response: ', response.data);
|
|
245
|
-
return response.data.items;
|
|
246
|
-
} catch (error) {
|
|
247
|
-
const errorMsg =
|
|
248
|
-
`Fail to list comment threads: ${JSON.stringify(commentThreadsRequest)}`;
|
|
249
|
-
this.logger.error('YouTube list comment threads failed.', error.message);
|
|
250
|
-
this.logger.debug('Errors in response:', error);
|
|
251
|
-
throw new Error(errorMsg);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Returns a collection of playlists that match the API request parameters.
|
|
257
|
-
* @see https://developers.google.com/youtube/v3/docs/playlists/list
|
|
258
|
-
* @param {!ListPlaylistConfig} config List playlist configuration.
|
|
259
|
-
* @param {number} resultLimit The limit of the number of results.
|
|
260
|
-
* @param {string} pageToken Token to identify a specific page in the result.
|
|
261
|
-
* @return {!Promise<Array<Schema$Playlist>>}
|
|
262
|
-
*/
|
|
263
|
-
async listPlaylists(config, resultLimit = 1000, pageToken = null) {
|
|
264
|
-
if (resultLimit <= 0) return [];
|
|
265
|
-
|
|
266
|
-
const playlistsRequest = Object.assign({
|
|
267
|
-
// auth: this.auth,
|
|
268
|
-
}, config, {
|
|
269
|
-
pageToken
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
if (Array.isArray(playlistsRequest.part)) {
|
|
273
|
-
playlistsRequest.part = playlistsRequest.part.join(',');
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
try {
|
|
277
|
-
const youtube = await this.getApiClient();
|
|
278
|
-
const response = await youtube.playlists.list(playlistsRequest);
|
|
279
|
-
this.logger.debug('Response: ', response.data);
|
|
280
|
-
if (response.data.nextPageToken) {
|
|
281
|
-
this.logger.debug(
|
|
282
|
-
'Call youtube playlist:list API agian with Token: ',
|
|
283
|
-
response.data.nextPageToken);
|
|
284
|
-
const nextResponse = await this.listPlaylists(
|
|
285
|
-
config,
|
|
286
|
-
resultLimit - response.data.items.length,
|
|
287
|
-
response.data.nextPageToken);
|
|
288
|
-
return response.data.items.concat(nextResponse);
|
|
289
|
-
}
|
|
290
|
-
return response.data.items;
|
|
291
|
-
} catch (error) {
|
|
292
|
-
const errorMsg =
|
|
293
|
-
`Fail to list playlists: ${JSON.stringify(playlistsRequest)}`;
|
|
294
|
-
this.logger.error('YouTube list playlists failed.', error.message);
|
|
295
|
-
this.logger.debug('Errors in response:', error);
|
|
296
|
-
throw new Error(errorMsg);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Returns a collection of search results that match the query parameters
|
|
302
|
-
* specified in the API request.
|
|
303
|
-
* @see https://developers.google.com/youtube/v3/docs/search/list
|
|
304
|
-
* @param {!ListSearchConfig} config List search result configuration.
|
|
305
|
-
* @param {number} resultLimit The limit of the number of results.
|
|
306
|
-
* @param {string} pageToken Token to identify a specific page in the result.
|
|
307
|
-
* @return {!Promise<Array<Schema$Search>>}
|
|
308
|
-
*/
|
|
309
|
-
async listSearchResults(config, resultLimit = 1000, pageToken = null) {
|
|
310
|
-
if (resultLimit <= 0) return [];
|
|
311
|
-
|
|
312
|
-
const searchRequest = Object.assign({}, config, { pageToken });
|
|
313
|
-
|
|
314
|
-
if (Array.isArray(searchRequest.part)) {
|
|
315
|
-
searchRequest.part = searchRequest.part.join(',');
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
try {
|
|
319
|
-
const youtube = await this.getApiClient();
|
|
320
|
-
const response = await youtube.search.list(searchRequest);
|
|
321
|
-
this.logger.debug('Response: ', response.data);
|
|
322
|
-
if (response.data.nextPageToken) {
|
|
323
|
-
this.logger.debug(
|
|
324
|
-
'Call youtube search:list API agian with Token: ',
|
|
325
|
-
response.data.nextPageToken);
|
|
326
|
-
const nextResponse = await this.listSearchResults(
|
|
327
|
-
config,
|
|
328
|
-
resultLimit - response.data.items.length,
|
|
329
|
-
response.data.nextPageToken);
|
|
330
|
-
return response.data.items.concat(nextResponse);
|
|
331
|
-
}
|
|
332
|
-
return response.data.items;
|
|
333
|
-
} catch (error) {
|
|
334
|
-
const errorMsg =
|
|
335
|
-
`Fail to list search results: ${JSON.stringify(searchRequest)}`;
|
|
336
|
-
this.logger.error('YouTube list search results failed.', error.message);
|
|
337
|
-
this.logger.debug('Errors in response:', error);
|
|
338
|
-
throw new Error(errorMsg);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
62
|
}
|
|
342
63
|
|
|
343
64
|
module.exports = {
|
|
344
65
|
YouTube,
|
|
345
|
-
ListChannelsConfig,
|
|
346
|
-
ListVideosConfig,
|
|
347
|
-
ListCommentThreadsConfig,
|
|
348
|
-
ListPlaylistConfig,
|
|
349
|
-
ListSearchConfig,
|
|
350
66
|
API_VERSION,
|
|
351
67
|
API_SCOPES,
|
|
352
68
|
};
|
|
@@ -65,7 +65,7 @@ let Filter;
|
|
|
65
65
|
*/
|
|
66
66
|
class DatastoreDocumentFacade {
|
|
67
67
|
/**
|
|
68
|
-
* Initializes DocumentFacade for
|
|
68
|
+
* Initializes DocumentFacade for Datastore.
|
|
69
69
|
* @param {!DatastoreModeEntity} entity
|
|
70
70
|
*/
|
|
71
71
|
constructor(entity) {
|
|
@@ -87,7 +87,7 @@ class DatastoreDocumentFacade {
|
|
|
87
87
|
*/
|
|
88
88
|
class DatastoreTransactionFacade {
|
|
89
89
|
/**
|
|
90
|
-
* Initializes
|
|
90
|
+
* Initializes Firestore TransactionFacade for Datastore Transaction.
|
|
91
91
|
* @param {!DatastoreModeTransaction} transaction
|
|
92
92
|
* @param {!DatastoreModeEntity} entity
|
|
93
93
|
*/
|
|
@@ -145,7 +145,7 @@ let Database;
|
|
|
145
145
|
* interface offers unified operations on the data objects in both of these two
|
|
146
146
|
* modes.
|
|
147
147
|
*
|
|
148
|
-
* Firestore Native mode ('Firestore') and Firestore Datastore mode ('
|
|
148
|
+
* Firestore Native mode ('Firestore') and Firestore Datastore mode ('Datastore')
|
|
149
149
|
* have different interfaces:
|
|
150
150
|
* 1. 'Firestore' has two kinds objects: 'document' stands for an object (data
|
|
151
151
|
* entity) and 'collection' stands for a group of 'documents'. The
|
|
@@ -34,7 +34,7 @@ const DatastoreModeAccess = require('./datastore_mode_access.js');
|
|
|
34
34
|
/**
|
|
35
35
|
* This is data access object base class on Firestore. It seals the details of
|
|
36
36
|
* different underlying databases, Firestore and Datastore.
|
|
37
|
-
* This class relies on an initial parameter in
|
|
37
|
+
* This class relies on an initial parameter in constructor to indicate the
|
|
38
38
|
* Firestore type.
|
|
39
39
|
*
|
|
40
40
|
* Firestore and Datastore have different transaction APIs. In that case,
|
|
@@ -54,16 +54,16 @@ class DataAccessObject {
|
|
|
54
54
|
/** @const {string} */ this.namespace = namespace;
|
|
55
55
|
/** @const {!DataSource} */ this.dataSource =
|
|
56
56
|
typeof database === 'string' ? database : database.source;
|
|
57
|
-
|
|
57
|
+
this.databaseId = database.id || DEFAULT_DATABASE;
|
|
58
58
|
/** @type {!FirestoreAccessBase} */ this.accessObject = undefined;
|
|
59
59
|
switch (this.dataSource) {
|
|
60
60
|
case DataSource.FIRESTORE:
|
|
61
61
|
this.accessObject = new NativeModeAccess(
|
|
62
|
-
`${this.namespace}/database/${kind}`, projectId, databaseId);
|
|
62
|
+
`${this.namespace}/database/${kind}`, projectId, this.databaseId);
|
|
63
63
|
break;
|
|
64
64
|
case DataSource.DATASTORE:
|
|
65
65
|
this.accessObject = new DatastoreModeAccess(this.namespace, kind,
|
|
66
|
-
projectId, databaseId);
|
|
66
|
+
projectId, this.databaseId);
|
|
67
67
|
break;
|
|
68
68
|
default:
|
|
69
69
|
throw new Error(`Unknown DataSource item: ${this.dataSource}.`);
|
|
@@ -33,7 +33,7 @@ const {
|
|
|
33
33
|
} = require('./access_base.js');
|
|
34
34
|
const {getLogger, wait} = require('../utils.js');
|
|
35
35
|
|
|
36
|
-
/** @const {number} Max retry times when commit failed in a
|
|
36
|
+
/** @const {number} Max retry times when commit failed in a transaction. */
|
|
37
37
|
const MAX_RETRY_TIMES = 5;
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -66,7 +66,7 @@ class DatastoreModeAccess {
|
|
|
66
66
|
* Datastore uses 'Key' to identify entities. A 'Key' composes of Id, entity
|
|
67
67
|
* kind and namespace. The 'id' can be 'undefined' if the next operation is
|
|
68
68
|
* creating a new entity.
|
|
69
|
-
* The default Id of Datastore is
|
|
69
|
+
* The default Id of Datastore is an integer. However, Pub/sub can only send
|
|
70
70
|
* attributes with string values. This will cause the Datastore Ids to be
|
|
71
71
|
* converted to strings. So here will try to change the id back to number if
|
|
72
72
|
* possible.
|
|
@@ -106,7 +106,7 @@ class DatastoreModeAccess {
|
|
|
106
106
|
async waitUntilGetObject(id) {
|
|
107
107
|
const entity = await this.getObject(id);
|
|
108
108
|
if (entity) return id;
|
|
109
|
-
this.logger.debug(`Wait 1 more second until the
|
|
109
|
+
this.logger.debug(`Wait 1 more second until the entity@${id} is ready`);
|
|
110
110
|
return wait(1000, this.waitUntilGetObject(id));
|
|
111
111
|
}
|
|
112
112
|
|
package/src/components/pubsub.js
CHANGED
|
@@ -129,7 +129,7 @@ class EnhancedPubSub {
|
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
131
|
* Using `SubscriberClient` to acknowledge messages.
|
|
132
|
-
* 2022.11.02 The
|
|
132
|
+
* 2022.11.02 The method `ack()` in Message doesn't work properly due to a
|
|
133
133
|
* unknown reason. Use this function to acknowledge a message for now.
|
|
134
134
|
*
|
|
135
135
|
* @param {string} subscription Subscription name.
|
|
@@ -116,7 +116,7 @@ class StorageFile {
|
|
|
116
116
|
*/
|
|
117
117
|
async getLastLineBreaker(start, end, checkPoint = -1) {
|
|
118
118
|
/**
|
|
119
|
-
* How many characters to look back to find a
|
|
119
|
+
* How many characters to look back to find a possible line breaker. If no
|
|
120
120
|
* link break in this range, it will extend to find the last one.
|
|
121
121
|
*/
|
|
122
122
|
const possibleLineBreakRange = 1000;
|
package/src/components/utils.js
CHANGED
|
@@ -29,7 +29,7 @@ const lodash = require('lodash');
|
|
|
29
29
|
* data that will be sent out in one single request.
|
|
30
30
|
* Some APIs allows partial failure: it will take those correct data and
|
|
31
31
|
* response with reasons for those failed ones. 'groupedFailed' uses error
|
|
32
|
-
* message as the key, and
|
|
32
|
+
* message as the key, and the array of related failed lines(records) as value.
|
|
33
33
|
* Some APIs upload whole file. In this case, there will be not 'numberOfLines'
|
|
34
34
|
* or 'failedLines', etc.
|
|
35
35
|
* @typedef {{
|