@restorecommerce/gql-bot 0.1.18 → 0.2.2
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/CHANGELOG.md +33 -0
- package/lib/client.d.ts +1 -1
- package/lib/client.js +64 -9
- package/lib/job_processor.d.ts +6 -3
- package/lib/job_processor.js +64 -11
- package/lib/job_processor_gql.d.ts +1 -1
- package/lib/job_processor_gql.js +108 -63
- package/lib/utils.d.ts +2 -0
- package/lib/utils.js +12 -0
- package/package.json +17 -5
- package/src/client.ts +69 -13
- package/src/job_processor.ts +74 -15
- package/src/job_processor_gql.ts +108 -64
- package/src/utils.ts +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,39 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.2.2](https://github.com/restorecommerce/libs/compare/@restorecommerce/gql-bot@0.2.1...@restorecommerce/gql-bot@0.2.2) (2022-06-20)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **gql-bot:** resume if errors are skipped ([7f01a84](https://github.com/restorecommerce/libs/commit/7f01a84f19a81360b94d8dc9421cb572d0fb3cd5))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## [0.2.1](https://github.com/restorecommerce/libs/compare/@restorecommerce/gql-bot@0.2.0...@restorecommerce/gql-bot@0.2.1) (2022-06-20)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **gql-bot:** add option to ignore ssl errors ([b82d302](https://github.com/restorecommerce/libs/commit/b82d3020318ec5495ad1c6143cd5c80cb1657f80))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# [0.2.0](https://github.com/restorecommerce/libs/compare/@restorecommerce/gql-bot@0.1.18...@restorecommerce/gql-bot@0.2.0) (2022-06-20)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Features
|
|
32
|
+
|
|
33
|
+
* **gql-bot:** correctly handle errors, more readable output ([b773d2b](https://github.com/restorecommerce/libs/commit/b773d2b94d41233f9660a96de5cac4e85f305e66))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
6
39
|
## [0.1.18](https://github.com/restorecommerce/libs/compare/@restorecommerce/gql-bot@0.1.17...@restorecommerce/gql-bot@0.1.18) (2022-06-10)
|
|
7
40
|
|
|
8
41
|
|
package/lib/client.d.ts
CHANGED
|
@@ -5,5 +5,5 @@ export declare class Client {
|
|
|
5
5
|
constructor(opts: any);
|
|
6
6
|
_buildURLs(): any;
|
|
7
7
|
_normalizeUrl(source?: any): string;
|
|
8
|
-
post(source: any, job?: any,
|
|
8
|
+
post(source: any, job?: any, verbose?: boolean, ignoreSelfSigned?: boolean): Promise<any>;
|
|
9
9
|
}
|
package/lib/client.js
CHANGED
|
@@ -18,7 +18,8 @@ const apollo_client_1 = require("apollo-client");
|
|
|
18
18
|
const apollo_cache_inmemory_1 = require("apollo-cache-inmemory");
|
|
19
19
|
const node_fetch_1 = require("node-fetch"); // required for apollo-link-http
|
|
20
20
|
const apollo_link_http_1 = require("apollo-link-http");
|
|
21
|
-
|
|
21
|
+
const https = require("https");
|
|
22
|
+
const _checkVariableMutation = (mutation) => {
|
|
22
23
|
const mutationName = mutation.slice(mutation.indexOf(' '), mutation.indexOf('($'));
|
|
23
24
|
if (mutationName.indexOf('$') > 0) {
|
|
24
25
|
return false;
|
|
@@ -26,12 +27,12 @@ function _checkVariableMutation(mutation) {
|
|
|
26
27
|
else {
|
|
27
28
|
return new RegExp('\\b' + mutationName + '\\(', 'i').test(mutation);
|
|
28
29
|
}
|
|
29
|
-
}
|
|
30
|
-
|
|
30
|
+
};
|
|
31
|
+
const _replaceInlineVars = (mutation, args) => {
|
|
31
32
|
if (mutation)
|
|
32
33
|
return mutation.replace(/\${(\w+)}/g, (_, v) => args[v]);
|
|
33
|
-
}
|
|
34
|
-
|
|
34
|
+
};
|
|
35
|
+
const _createQueryVariables = (inputVarName, queryVarKey, varValue) => {
|
|
35
36
|
if (queryVarKey) {
|
|
36
37
|
return {
|
|
37
38
|
[inputVarName]: {
|
|
@@ -42,7 +43,44 @@ function _createQueryVariables(inputVarName, queryVarKey, varValue) {
|
|
|
42
43
|
return {
|
|
43
44
|
[inputVarName]: JSON.parse(varValue)
|
|
44
45
|
};
|
|
45
|
-
}
|
|
46
|
+
};
|
|
47
|
+
const checkError = (data) => {
|
|
48
|
+
if (typeof data === 'object') {
|
|
49
|
+
if (Array.isArray(data)) {
|
|
50
|
+
const result = data.map(value => {
|
|
51
|
+
const inner = checkError(value);
|
|
52
|
+
if (inner) {
|
|
53
|
+
return inner;
|
|
54
|
+
}
|
|
55
|
+
}).filter(value => !!value);
|
|
56
|
+
if (result.length > 0) {
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
if ('__typename' in data) {
|
|
62
|
+
switch (data['__typename']) {
|
|
63
|
+
case 'IoRestorecommerceStatusOperationStatus':
|
|
64
|
+
case 'IoRestorecommerceStatusStatus':
|
|
65
|
+
if ('code' in data) {
|
|
66
|
+
const code = data['code'];
|
|
67
|
+
if (code != '' && code != '200' && code != 0 && code != 200) {
|
|
68
|
+
return data;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const value of Object.values(data)) {
|
|
75
|
+
const inner = checkError(value);
|
|
76
|
+
if (inner) {
|
|
77
|
+
return inner;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
};
|
|
46
84
|
class Client {
|
|
47
85
|
constructor(opts) {
|
|
48
86
|
if (_.isNil(opts)) {
|
|
@@ -95,7 +133,7 @@ class Client {
|
|
|
95
133
|
}
|
|
96
134
|
return url.resolve(this.entryBaseUrl, extendURL);
|
|
97
135
|
}
|
|
98
|
-
post(source, job,
|
|
136
|
+
post(source, job, verbose = false, ignoreSelfSigned = false) {
|
|
99
137
|
return __awaiter(this, void 0, void 0, function* () {
|
|
100
138
|
const normalUrl = this._normalizeUrl();
|
|
101
139
|
let mutation;
|
|
@@ -103,7 +141,7 @@ class Client {
|
|
|
103
141
|
mutation = JSON.stringify(job.mutation);
|
|
104
142
|
}
|
|
105
143
|
else {
|
|
106
|
-
throw new Error(
|
|
144
|
+
throw new Error(`mutation not present in job config (${job.name})`);
|
|
107
145
|
}
|
|
108
146
|
const apiKey = JSON.stringify(this.opts.apiKey);
|
|
109
147
|
let resource_list = JSON.stringify(source);
|
|
@@ -134,16 +172,33 @@ class Client {
|
|
|
134
172
|
if (this.opts.headers) {
|
|
135
173
|
apolloLinkOpts['headers'] = this.opts.headers;
|
|
136
174
|
}
|
|
175
|
+
if (ignoreSelfSigned) {
|
|
176
|
+
apolloLinkOpts.fetchOptions = {
|
|
177
|
+
agent: new https.Agent({ rejectUnauthorized: false }),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
137
180
|
let apolloLink = apollo_link_http_1.createHttpLink(apolloLinkOpts);
|
|
138
181
|
const apolloCache = new apollo_cache_inmemory_1.InMemoryCache();
|
|
139
182
|
const apolloClient = new apollo_client_1.ApolloClient({
|
|
140
183
|
cache: apolloCache,
|
|
141
184
|
link: apolloLink
|
|
142
185
|
});
|
|
143
|
-
|
|
186
|
+
const response = yield apolloClient.mutate({
|
|
144
187
|
mutation: graphql_tag_1.default `${mutation}`,
|
|
145
188
|
variables
|
|
146
189
|
});
|
|
190
|
+
const error = checkError(response);
|
|
191
|
+
if (error) {
|
|
192
|
+
if (verbose) {
|
|
193
|
+
console.error(JSON.stringify({
|
|
194
|
+
request: mutation,
|
|
195
|
+
variables,
|
|
196
|
+
response
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
throw new Error(JSON.stringify(error));
|
|
200
|
+
}
|
|
201
|
+
return response;
|
|
147
202
|
});
|
|
148
203
|
}
|
|
149
204
|
}
|
package/lib/job_processor.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
+
import * as ps from 'promise-streams';
|
|
2
3
|
import { Readable } from 'stream';
|
|
3
4
|
import { EventEmitter } from 'events';
|
|
4
5
|
export declare class ReadArrayStream extends Readable {
|
|
@@ -8,14 +9,16 @@ export declare class ReadArrayStream extends Readable {
|
|
|
8
9
|
}
|
|
9
10
|
export declare class Job extends EventEmitter {
|
|
10
11
|
opts: any;
|
|
12
|
+
done: boolean;
|
|
13
|
+
error: any;
|
|
11
14
|
constructor(opts?: any);
|
|
15
|
+
wait(): Promise<unknown>;
|
|
12
16
|
}
|
|
13
17
|
export declare class JobProcessor {
|
|
14
18
|
jobInfo: any;
|
|
15
|
-
processed: number;
|
|
16
19
|
processedTasks: number;
|
|
17
|
-
taskStream: any
|
|
20
|
+
taskStream: ps.PromiseStream<any>;
|
|
18
21
|
constructor(jobInfo: any);
|
|
19
|
-
start(tasks?: any, job?: Job): Promise<any>;
|
|
22
|
+
start(tasks?: any, job?: Job, verbose?: boolean, ignoreErrors?: boolean, ignoreSelfSigned?: boolean): Promise<any>;
|
|
20
23
|
sync(task: any, job: Job): Promise<any>;
|
|
21
24
|
}
|
package/lib/job_processor.js
CHANGED
|
@@ -17,6 +17,34 @@ const through2 = require("through2");
|
|
|
17
17
|
const readdirp = require("readdirp");
|
|
18
18
|
const path = require("path");
|
|
19
19
|
const events_1 = require("events");
|
|
20
|
+
const utils_1 = require("./utils");
|
|
21
|
+
const unwrap = (data) => {
|
|
22
|
+
let result = data;
|
|
23
|
+
while (typeof result === 'object' && Object.keys(result).length == 1) {
|
|
24
|
+
result = result[Object.keys(result)[0]];
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
};
|
|
28
|
+
const removeType = (data) => {
|
|
29
|
+
if (typeof data === 'object') {
|
|
30
|
+
if (Array.isArray(data)) {
|
|
31
|
+
data = data.map(removeType);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
delete data['__typename'];
|
|
35
|
+
Object.keys(data).forEach(k => data[k] = removeType(data[k]));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return data;
|
|
39
|
+
};
|
|
40
|
+
const processResponse = (body) => {
|
|
41
|
+
const result = [];
|
|
42
|
+
for (const response of Array.isArray(body) ? body : [body]) {
|
|
43
|
+
const clean = unwrap(removeType(response));
|
|
44
|
+
result.push(clean);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
20
48
|
class ReadArrayStream extends stream_1.Readable {
|
|
21
49
|
constructor(opts, array) {
|
|
22
50
|
super(opts);
|
|
@@ -40,29 +68,52 @@ exports.ReadArrayStream = ReadArrayStream;
|
|
|
40
68
|
class Job extends events_1.EventEmitter {
|
|
41
69
|
constructor(opts) {
|
|
42
70
|
super();
|
|
71
|
+
this.done = false;
|
|
72
|
+
this.error = undefined;
|
|
43
73
|
this.setMaxListeners(100);
|
|
44
74
|
this.opts = opts || {};
|
|
75
|
+
this.once('done', () => this.done = true);
|
|
76
|
+
this.once('error', (err) => this.error = err);
|
|
77
|
+
}
|
|
78
|
+
wait() {
|
|
79
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
80
|
+
if (this.error) {
|
|
81
|
+
throw this.error;
|
|
82
|
+
}
|
|
83
|
+
if (this.done) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
this.once('done', resolve);
|
|
88
|
+
this.once('error', reject);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
45
91
|
}
|
|
46
92
|
}
|
|
47
93
|
exports.Job = Job;
|
|
48
94
|
class JobProcessor {
|
|
49
95
|
constructor(jobInfo) {
|
|
50
96
|
this.jobInfo = jobInfo;
|
|
51
|
-
this.processed = 0;
|
|
52
97
|
_.defaults(this.jobInfo, {
|
|
53
98
|
concurrency: 3,
|
|
54
99
|
processor: null
|
|
55
100
|
});
|
|
56
101
|
}
|
|
57
|
-
start(tasks, job) {
|
|
102
|
+
start(tasks, job, verbose = false, ignoreErrors = false, ignoreSelfSigned = false) {
|
|
58
103
|
return __awaiter(this, void 0, void 0, function* () {
|
|
59
104
|
job = job || new Job();
|
|
60
105
|
tasks = tasks || this.jobInfo.tasks;
|
|
61
106
|
const concurrency = this.jobInfo.options.concurrency;
|
|
62
107
|
this.taskStream = ps.map({ concurrent: concurrency }, (task) => {
|
|
63
|
-
return this.jobInfo.options.processor.process(task).then((body) => {
|
|
64
|
-
|
|
65
|
-
|
|
108
|
+
return this.jobInfo.options.processor.process(task, verbose, ignoreErrors, ignoreSelfSigned).then((body) => {
|
|
109
|
+
const logColor = utils_1.stringToChalk(task.name);
|
|
110
|
+
if (verbose) {
|
|
111
|
+
const processed = processResponse(body);
|
|
112
|
+
console.log(`[${logColor(task.name)}] Completed successfully`, JSON.stringify(processed));
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.log(`[${logColor(task.name)}] Completed successfully`);
|
|
116
|
+
}
|
|
66
117
|
task.inputTask.processing--;
|
|
67
118
|
task.progress.value = 100; // task complete
|
|
68
119
|
job.emit('progress', task);
|
|
@@ -78,9 +129,8 @@ class JobProcessor {
|
|
|
78
129
|
cb();
|
|
79
130
|
})));
|
|
80
131
|
yield ps.wait(inputTaskStream);
|
|
81
|
-
this.taskStream.on('error', (err) => {
|
|
82
|
-
|
|
83
|
-
// console.log('Stream ended');
|
|
132
|
+
this.taskStream.on('error', (err) => {
|
|
133
|
+
job.emit('error', err);
|
|
84
134
|
});
|
|
85
135
|
const tasksStreamEnded = ps.wait(this.taskStream);
|
|
86
136
|
// Wait until the task stream emitted 'end'
|
|
@@ -93,7 +143,9 @@ class JobProcessor {
|
|
|
93
143
|
sync(task, job) {
|
|
94
144
|
return __awaiter(this, void 0, void 0, function* () {
|
|
95
145
|
const pathOptions = {
|
|
96
|
-
fileFilter: (entry) => {
|
|
146
|
+
fileFilter: (entry) => {
|
|
147
|
+
return true;
|
|
148
|
+
},
|
|
97
149
|
depth: 1,
|
|
98
150
|
lstat: true
|
|
99
151
|
};
|
|
@@ -141,8 +193,9 @@ class JobProcessor {
|
|
|
141
193
|
// Check processedTasks tasks
|
|
142
194
|
// Manually emit `end` event for all tasks finished
|
|
143
195
|
if (this.processedTasks === this.jobInfo.tasks.length) {
|
|
144
|
-
this.taskStream._flush(() => {
|
|
145
|
-
|
|
196
|
+
this.taskStream._flush(() => {
|
|
197
|
+
});
|
|
198
|
+
this.taskStream.end();
|
|
146
199
|
}
|
|
147
200
|
})
|
|
148
201
|
.pipe(this.taskStream, { end: false });
|
package/lib/job_processor_gql.js
CHANGED
|
@@ -13,7 +13,8 @@ exports.GraphQLProcessor = void 0;
|
|
|
13
13
|
const _ = require("lodash");
|
|
14
14
|
const fs = require("fs");
|
|
15
15
|
const index_1 = require("./index");
|
|
16
|
-
const
|
|
16
|
+
const yaml_document_stream_1 = require("yaml-document-stream");
|
|
17
|
+
const utils_1 = require("./utils");
|
|
17
18
|
/**
|
|
18
19
|
* GraphQL-specific job processor.
|
|
19
20
|
*/
|
|
@@ -29,74 +30,118 @@ class GraphQLProcessor {
|
|
|
29
30
|
this.opts = opts;
|
|
30
31
|
this.client = new index_1.Client(opts);
|
|
31
32
|
}
|
|
32
|
-
process(
|
|
33
|
+
process(task, verbose = false, ignoreErrors = false, ignoreSelfSigned = false) {
|
|
33
34
|
return __awaiter(this, void 0, void 0, function* () {
|
|
34
|
-
let yamlStream = new YamlStreamReadTransformer();
|
|
35
|
-
let jobPath =
|
|
35
|
+
let yamlStream = new yaml_document_stream_1.YamlStreamReadTransformer();
|
|
36
|
+
let jobPath = task.path;
|
|
36
37
|
let data = false;
|
|
37
|
-
|
|
38
|
+
const logColor = utils_1.stringToChalk(task.name);
|
|
39
|
+
switch (task.operation) {
|
|
38
40
|
case 'sync': { // synchronous operation
|
|
39
|
-
const fileStream = fs.createReadStream(job.fullPath);
|
|
40
|
-
fileStream.pipe(yamlStream);
|
|
41
|
-
let batchsize;
|
|
42
|
-
if (job.batchSize) {
|
|
43
|
-
batchsize = job.batchSize;
|
|
44
|
-
console.log('Batch size:', batchsize);
|
|
45
|
-
}
|
|
46
|
-
let counter = 0;
|
|
47
|
-
let batchCounter = 0;
|
|
48
|
-
let docArr = [];
|
|
49
|
-
let resultArr = [];
|
|
50
|
-
// Here we read from the readable stream each yaml document parsed
|
|
51
|
-
// as an object, and if we have batching enabled we first batch this
|
|
52
|
-
// documents into smaller sets. When a dataset is ready to be imported,
|
|
53
|
-
// we pause the readable stream and emit a 'pause' event and
|
|
54
|
-
// right after that we reset the dataset object 'docArr'.
|
|
55
|
-
yamlStream.on('data', (doc) => {
|
|
56
|
-
data = true;
|
|
57
|
-
if (batchsize && batchsize != undefined) {
|
|
58
|
-
docArr.push(doc);
|
|
59
|
-
counter++;
|
|
60
|
-
if (counter === batchsize) {
|
|
61
|
-
counter = 0;
|
|
62
|
-
batchCounter++;
|
|
63
|
-
console.log('Processing batch:', batchCounter);
|
|
64
|
-
yamlStream.pause();
|
|
65
|
-
docArr = [];
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
docArr.push(doc);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
// On 'pause' event we create a post request using the GQL Client
|
|
73
|
-
// using the accumulated resources inside the 'docArr' dataset,
|
|
74
|
-
// we wait for the response, store the response inside an array and
|
|
75
|
-
// only then we emit a 'resume' event, resuming reading from the
|
|
76
|
-
// 'yamlStream' readable stream.
|
|
77
|
-
yamlStream.on('pause', () => __awaiter(this, void 0, void 0, function* () {
|
|
78
|
-
let result = yield this.client.post(docArr, job);
|
|
79
|
-
resultArr.push(result);
|
|
80
|
-
yamlStream.resume();
|
|
81
|
-
}));
|
|
82
|
-
// On 'end' if we still have data accumulated inside the 'docArr'
|
|
83
|
-
// dataset, we create a final post request to import this data as-well,
|
|
84
|
-
// store the response inside the array and finally resolve this as a
|
|
85
|
-
// Promise to return all the responses back to the initial caller.
|
|
86
41
|
return new Promise((resolve, reject) => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
42
|
+
try {
|
|
43
|
+
yamlStream.on('error', (err) => {
|
|
44
|
+
!ignoreErrors && reject(err);
|
|
45
|
+
});
|
|
46
|
+
const fileStream = fs.createReadStream(task.fullPath);
|
|
47
|
+
fileStream.pipe(yamlStream);
|
|
48
|
+
let batchsize;
|
|
49
|
+
if (task.batchSize) {
|
|
50
|
+
batchsize = task.batchSize;
|
|
51
|
+
console.log(`[${logColor(task.name)}] Batch size:`, batchsize);
|
|
90
52
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
53
|
+
let counter = 0;
|
|
54
|
+
let batchCounter = 0;
|
|
55
|
+
let docArr = [];
|
|
56
|
+
let resultArr = [];
|
|
57
|
+
// Here we read from the readable stream each yaml document parsed
|
|
58
|
+
// as an object, and if we have batching enabled we first batch this
|
|
59
|
+
// documents into smaller sets. When a dataset is ready to be imported,
|
|
60
|
+
// we pause the readable stream and emit a 'pause' event and
|
|
61
|
+
// right after that we reset the dataset object 'docArr'.
|
|
62
|
+
yamlStream.on('data', (doc) => {
|
|
63
|
+
data = true;
|
|
64
|
+
if (batchsize) {
|
|
65
|
+
docArr.push(doc);
|
|
66
|
+
counter++;
|
|
67
|
+
if (counter === batchsize) {
|
|
68
|
+
counter = 0;
|
|
69
|
+
let batchText = '';
|
|
70
|
+
if (batchsize > 0) {
|
|
71
|
+
const from = batchCounter * batchsize + 1;
|
|
72
|
+
const to = from + (docArr.length - 1);
|
|
73
|
+
batchText = from == to ? ` (${from})` : ` (${from} - ${to})`;
|
|
74
|
+
}
|
|
75
|
+
batchCounter++;
|
|
76
|
+
console.log(`[${logColor(task.name)}] Processing batch: ${batchCounter}${batchText}`);
|
|
77
|
+
yamlStream.pause();
|
|
78
|
+
docArr = [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
docArr.push(doc);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
// On 'pause' event we create a post request using the GQL Client
|
|
86
|
+
// using the accumulated resources inside the 'docArr' dataset,
|
|
87
|
+
// we wait for the response, store the response inside an array and
|
|
88
|
+
// only then we emit a 'resume' event, resuming reading from the
|
|
89
|
+
// 'yamlStream' readable stream.
|
|
90
|
+
yamlStream.on('pause', () => __awaiter(this, void 0, void 0, function* () {
|
|
91
|
+
try {
|
|
92
|
+
resultArr.push(yield this.client.post(docArr, task, verbose, ignoreSelfSigned));
|
|
93
|
+
yamlStream.resume();
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
!ignoreErrors && reject(e);
|
|
97
|
+
ignoreErrors && yamlStream.resume();
|
|
98
|
+
}
|
|
99
|
+
}));
|
|
100
|
+
let runOnResume;
|
|
101
|
+
yamlStream.on('resume', () => __awaiter(this, void 0, void 0, function* () {
|
|
102
|
+
if (runOnResume) {
|
|
103
|
+
runOnResume();
|
|
104
|
+
}
|
|
105
|
+
}));
|
|
106
|
+
// On 'end' if we still have data accumulated inside the 'docArr'
|
|
107
|
+
// dataset, we create a final post request to import this data as-well,
|
|
108
|
+
// store the response inside the array and finally resolve this as a
|
|
109
|
+
// Promise to return all the responses back to the initial caller.
|
|
110
|
+
const endFunc = () => __awaiter(this, void 0, void 0, function* () {
|
|
111
|
+
if (data === false) {
|
|
112
|
+
throw new Error(`Could not import resources from ${jobPath}. Readable stream is empty. Please provide a file with YAML multi-document format.`);
|
|
113
|
+
}
|
|
114
|
+
if (docArr && !_.isEmpty(docArr)) {
|
|
115
|
+
let batchText = '';
|
|
116
|
+
if (batchsize > 0) {
|
|
117
|
+
const from = batchCounter * batchsize + 1;
|
|
118
|
+
const to = from + (docArr.length - 1);
|
|
119
|
+
batchText = from == to ? ` (${from})` : ` (${from} - ${to})`;
|
|
120
|
+
}
|
|
121
|
+
batchCounter++;
|
|
122
|
+
console.log(`[${logColor(task.name)}] Processing batch: ${batchCounter}${batchText}`);
|
|
123
|
+
try {
|
|
124
|
+
resultArr.push(yield this.client.post(docArr, task, verbose, ignoreSelfSigned));
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
!ignoreErrors && reject(e);
|
|
128
|
+
}
|
|
129
|
+
docArr = [];
|
|
130
|
+
}
|
|
97
131
|
resolve(resultArr);
|
|
98
|
-
}
|
|
99
|
-
|
|
132
|
+
});
|
|
133
|
+
yamlStream.on('end', () => {
|
|
134
|
+
if (yamlStream.isPaused()) {
|
|
135
|
+
runOnResume = endFunc;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
endFunc();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
!ignoreErrors && reject(e);
|
|
144
|
+
}
|
|
100
145
|
});
|
|
101
146
|
}
|
|
102
147
|
default: {
|
package/lib/utils.d.ts
ADDED
package/lib/utils.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stringToChalk = void 0;
|
|
4
|
+
const color_hash_1 = require("color-hash");
|
|
5
|
+
const chalk = require("chalk");
|
|
6
|
+
const colorHash = new color_hash_1.default({
|
|
7
|
+
lightness: [0.45, 0.6, 0.75]
|
|
8
|
+
});
|
|
9
|
+
const stringToChalk = (str) => {
|
|
10
|
+
return chalk.hex(colorHash.hex(str));
|
|
11
|
+
};
|
|
12
|
+
exports.stringToChalk = stringToChalk;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@restorecommerce/gql-bot",
|
|
3
3
|
"description": "GraphQL Client Automated Task Processor",
|
|
4
4
|
"main": "lib/index",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.2.2",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/restorecommerce/libs.git"
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"apollo-cache-inmemory": "^1.6.6",
|
|
20
20
|
"apollo-client": "^2.6.10",
|
|
21
21
|
"apollo-link-http": "^1.5.17",
|
|
22
|
+
"chalk": "^4.1.2",
|
|
23
|
+
"color-hash": "^2.0.1",
|
|
22
24
|
"graphql": "^15.5.0",
|
|
23
25
|
"graphql-tag": "^2.11.0",
|
|
24
26
|
"js-yaml": "^4.1.0",
|
|
@@ -30,28 +32,29 @@
|
|
|
30
32
|
"yaml-document-stream": "^1.1.0"
|
|
31
33
|
},
|
|
32
34
|
"devDependencies": {
|
|
35
|
+
"@types/color-hash": "^1.0.2",
|
|
33
36
|
"@types/mocha": "^8.2.2",
|
|
34
37
|
"@types/node": "^14.14.41",
|
|
35
38
|
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
|
36
39
|
"@typescript-eslint/parser": "^4.22.0",
|
|
37
40
|
"coveralls": "^3.1.0",
|
|
38
41
|
"eslint": "^7.24.0",
|
|
42
|
+
"eslint-plugin-prefer-arrow-functions": "^3.1.4",
|
|
39
43
|
"mocha": "^8.3.2",
|
|
40
44
|
"nock": "^13.0.11",
|
|
41
45
|
"npm-run-all": "^4.1.5",
|
|
42
46
|
"nyc": "^15.1.0",
|
|
43
47
|
"should": "^13.2.3",
|
|
48
|
+
"ts-node": "^10.5.0",
|
|
44
49
|
"typescript": "^4.2.4"
|
|
45
50
|
},
|
|
46
51
|
"scripts": {
|
|
47
|
-
"
|
|
48
|
-
"pretest": "npm run tsctests && npm run lint",
|
|
52
|
+
"pretest": "npm run lint",
|
|
49
53
|
"test": "nyc mocha",
|
|
50
54
|
"lint": "eslint './src/**/*.ts'",
|
|
51
55
|
"build:tsc": "tsc -d",
|
|
52
56
|
"build:clean": "rimraf lib",
|
|
53
57
|
"build": "npm-run-all lint build:clean build:tsc",
|
|
54
|
-
"mocha": "mocha --timeout 5000 ./test/*.js --trace-warnings",
|
|
55
58
|
"mocha-debug": "mocha -R spec test/*.js --full-trace --inspect-brk",
|
|
56
59
|
"lcov-report": "nyc report --reporter=lcov",
|
|
57
60
|
"coveralls": "nyc report --reporter=text-lcov | coveralls"
|
|
@@ -59,5 +62,14 @@
|
|
|
59
62
|
"engines": {
|
|
60
63
|
"node": ">= 12.0.0"
|
|
61
64
|
},
|
|
62
|
-
"
|
|
65
|
+
"nx": {
|
|
66
|
+
"targets": {
|
|
67
|
+
"build": {
|
|
68
|
+
"outputs": [
|
|
69
|
+
"./lib"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"gitHead": "a40423a5fca103646c2adbc2530cf096a190139f"
|
|
63
75
|
}
|
package/src/client.ts
CHANGED
|
@@ -5,9 +5,10 @@ import gql from 'graphql-tag';
|
|
|
5
5
|
import { ApolloClient } from 'apollo-client';
|
|
6
6
|
import { InMemoryCache } from 'apollo-cache-inmemory';
|
|
7
7
|
import fetch from 'node-fetch'; // required for apollo-link-http
|
|
8
|
-
import { createHttpLink } from 'apollo-link-http';
|
|
8
|
+
import { createHttpLink, HttpLink } from 'apollo-link-http';
|
|
9
|
+
import * as https from 'https';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
const _checkVariableMutation = (mutation: string): Boolean => {
|
|
11
12
|
const mutationName = mutation.slice(mutation.indexOf(' '),
|
|
12
13
|
mutation.indexOf('($'));
|
|
13
14
|
if (mutationName.indexOf('$') > 0) {
|
|
@@ -15,14 +16,14 @@ function _checkVariableMutation(mutation: string): Boolean {
|
|
|
15
16
|
} else {
|
|
16
17
|
return new RegExp('\\b' + mutationName + '\\(', 'i').test(mutation);
|
|
17
18
|
}
|
|
18
|
-
}
|
|
19
|
+
};
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
const _replaceInlineVars = (mutation: string, args: any): string => {
|
|
21
22
|
if (mutation)
|
|
22
23
|
return mutation.replace(/\${(\w+)}/g, (_, v) => args[v]);
|
|
23
|
-
}
|
|
24
|
+
};
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
const _createQueryVariables = (inputVarName: string, queryVarKey: string, varValue: any): Object => {
|
|
26
27
|
if (queryVarKey) {
|
|
27
28
|
return {
|
|
28
29
|
[inputVarName]: {
|
|
@@ -34,7 +35,45 @@ function _createQueryVariables(inputVarName: string, queryVarKey: string, varVal
|
|
|
34
35
|
return {
|
|
35
36
|
[inputVarName]: JSON.parse(varValue)
|
|
36
37
|
};
|
|
37
|
-
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const checkError = (data: any): any => {
|
|
41
|
+
if (typeof data === 'object') {
|
|
42
|
+
if (Array.isArray(data)) {
|
|
43
|
+
const result = data.map(value => {
|
|
44
|
+
const inner = checkError(value);
|
|
45
|
+
if (inner) {
|
|
46
|
+
return inner;
|
|
47
|
+
}
|
|
48
|
+
}).filter(value => !!value);
|
|
49
|
+
if (result.length > 0) {
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
if ('__typename' in data) {
|
|
54
|
+
switch (data['__typename']) {
|
|
55
|
+
case 'IoRestorecommerceStatusOperationStatus':
|
|
56
|
+
case 'IoRestorecommerceStatusStatus':
|
|
57
|
+
if ('code' in data) {
|
|
58
|
+
const code = data['code'];
|
|
59
|
+
if (code != '' && code != '200' && code != 0 && code != 200) {
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const value of Object.values(data)) {
|
|
68
|
+
const inner = checkError(value);
|
|
69
|
+
if (inner) {
|
|
70
|
+
return inner;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
};
|
|
38
77
|
|
|
39
78
|
export class Client {
|
|
40
79
|
opts: any;
|
|
@@ -100,16 +139,14 @@ export class Client {
|
|
|
100
139
|
return url.resolve(this.entryBaseUrl, extendURL);
|
|
101
140
|
}
|
|
102
141
|
|
|
103
|
-
async post(source: any, job?: any,
|
|
104
|
-
formOptions?: any): Promise<any> {
|
|
105
|
-
|
|
142
|
+
async post(source: any, job?: any, verbose = false, ignoreSelfSigned = false): Promise<any> {
|
|
106
143
|
const normalUrl = this._normalizeUrl();
|
|
107
144
|
|
|
108
145
|
let mutation;
|
|
109
146
|
if (job && job.mutation) {
|
|
110
147
|
mutation = JSON.stringify(job.mutation);
|
|
111
148
|
} else {
|
|
112
|
-
throw new Error(
|
|
149
|
+
throw new Error(`mutation not present in job config (${job.name})`);
|
|
113
150
|
}
|
|
114
151
|
|
|
115
152
|
const apiKey = JSON.stringify(this.opts.apiKey);
|
|
@@ -136,7 +173,7 @@ export class Client {
|
|
|
136
173
|
mutation = _replaceInlineVars(mutation, { resource_list, apiKey });
|
|
137
174
|
}
|
|
138
175
|
|
|
139
|
-
const apolloLinkOpts = {
|
|
176
|
+
const apolloLinkOpts: HttpLink.Options = {
|
|
140
177
|
uri: normalUrl,
|
|
141
178
|
fetch
|
|
142
179
|
};
|
|
@@ -145,6 +182,12 @@ export class Client {
|
|
|
145
182
|
apolloLinkOpts['headers'] = this.opts.headers;
|
|
146
183
|
}
|
|
147
184
|
|
|
185
|
+
if (ignoreSelfSigned) {
|
|
186
|
+
apolloLinkOpts.fetchOptions = {
|
|
187
|
+
agent: new https.Agent({rejectUnauthorized: false}),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
148
191
|
let apolloLink = createHttpLink(apolloLinkOpts);
|
|
149
192
|
|
|
150
193
|
const apolloCache = new InMemoryCache();
|
|
@@ -153,10 +196,23 @@ export class Client {
|
|
|
153
196
|
link: apolloLink
|
|
154
197
|
});
|
|
155
198
|
|
|
156
|
-
|
|
199
|
+
const response = await apolloClient.mutate({
|
|
157
200
|
mutation: gql`${mutation}`,
|
|
158
201
|
variables
|
|
159
202
|
});
|
|
160
203
|
|
|
204
|
+
const error = checkError(response);
|
|
205
|
+
if (error) {
|
|
206
|
+
if (verbose) {
|
|
207
|
+
console.error(JSON.stringify({
|
|
208
|
+
request: mutation,
|
|
209
|
+
variables,
|
|
210
|
+
response
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
throw new Error(JSON.stringify(error));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return response;
|
|
161
217
|
}
|
|
162
218
|
}
|
package/src/job_processor.ts
CHANGED
|
@@ -5,9 +5,40 @@ import * as through2 from 'through2';
|
|
|
5
5
|
import * as readdirp from 'readdirp';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { EventEmitter } from 'events';
|
|
8
|
+
import { stringToChalk } from './utils';
|
|
9
|
+
|
|
10
|
+
const unwrap = (data: any): any => {
|
|
11
|
+
let result = data;
|
|
12
|
+
while (typeof result === 'object' && Object.keys(result).length == 1) {
|
|
13
|
+
result = result[Object.keys(result)[0]];
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const removeType = (data: any): any => {
|
|
19
|
+
if (typeof data === 'object') {
|
|
20
|
+
if (Array.isArray(data)) {
|
|
21
|
+
data = data.map(removeType);
|
|
22
|
+
} else {
|
|
23
|
+
delete data['__typename'];
|
|
24
|
+
Object.keys(data).forEach(k => data[k] = removeType(data[k]));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return data;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const processResponse = (body: any | any[]): any => {
|
|
31
|
+
const result = [];
|
|
32
|
+
for (const response of Array.isArray(body) ? body : [body]) {
|
|
33
|
+
const clean = unwrap(removeType(response));
|
|
34
|
+
result.push(clean);
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
8
38
|
|
|
9
39
|
export class ReadArrayStream extends Readable {
|
|
10
40
|
array: any[];
|
|
41
|
+
|
|
11
42
|
constructor(opts: any, array: any[]) {
|
|
12
43
|
super(opts);
|
|
13
44
|
this.array = _.clone(array);
|
|
@@ -29,21 +60,40 @@ export class ReadArrayStream extends Readable {
|
|
|
29
60
|
*/
|
|
30
61
|
export class Job extends EventEmitter {
|
|
31
62
|
opts: any;
|
|
63
|
+
done = false;
|
|
64
|
+
error = undefined;
|
|
65
|
+
|
|
32
66
|
constructor(opts?: any) {
|
|
33
67
|
super();
|
|
34
68
|
this.setMaxListeners(100);
|
|
35
69
|
this.opts = opts || {};
|
|
70
|
+
this.once('done', () => this.done = true);
|
|
71
|
+
this.once('error', (err) => this.error = err);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async wait() {
|
|
75
|
+
if (this.error) {
|
|
76
|
+
throw this.error;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this.done) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
this.once('done', resolve);
|
|
85
|
+
this.once('error', reject);
|
|
86
|
+
});
|
|
36
87
|
}
|
|
37
88
|
}
|
|
38
89
|
|
|
39
90
|
export class JobProcessor {
|
|
40
91
|
jobInfo: any;
|
|
41
|
-
processed: number;
|
|
42
92
|
processedTasks: number;
|
|
43
|
-
taskStream: any
|
|
93
|
+
taskStream: ps.PromiseStream<any>;
|
|
94
|
+
|
|
44
95
|
constructor(jobInfo: any) {
|
|
45
96
|
this.jobInfo = jobInfo;
|
|
46
|
-
this.processed = 0;
|
|
47
97
|
|
|
48
98
|
_.defaults(this.jobInfo, {
|
|
49
99
|
concurrency: 3,
|
|
@@ -51,15 +101,22 @@ export class JobProcessor {
|
|
|
51
101
|
});
|
|
52
102
|
}
|
|
53
103
|
|
|
54
|
-
async start(tasks?: any, job?: Job): Promise<any> {
|
|
104
|
+
async start(tasks?: any, job?: Job, verbose = false, ignoreErrors = false, ignoreSelfSigned = false): Promise<any> {
|
|
55
105
|
job = job || new Job();
|
|
56
106
|
tasks = tasks || this.jobInfo.tasks;
|
|
57
107
|
|
|
58
108
|
const concurrency = this.jobInfo.options.concurrency;
|
|
59
|
-
this.taskStream = ps.map({
|
|
60
|
-
return this.jobInfo.options.processor.process(task).then((body) => {
|
|
61
|
-
|
|
62
|
-
|
|
109
|
+
this.taskStream = ps.map({concurrent: concurrency}, (task: any) => {
|
|
110
|
+
return this.jobInfo.options.processor.process(task, verbose, ignoreErrors, ignoreSelfSigned).then((body) => {
|
|
111
|
+
const logColor = stringToChalk(task.name);
|
|
112
|
+
|
|
113
|
+
if (verbose) {
|
|
114
|
+
const processed = processResponse(body);
|
|
115
|
+
console.log(`[${logColor(task.name)}] Completed successfully`, JSON.stringify(processed));
|
|
116
|
+
} else {
|
|
117
|
+
console.log(`[${logColor(task.name)}] Completed successfully`);
|
|
118
|
+
}
|
|
119
|
+
|
|
63
120
|
task.inputTask.processing--;
|
|
64
121
|
task.progress.value = 100; // task complete
|
|
65
122
|
job.emit('progress', task);
|
|
@@ -79,9 +136,8 @@ export class JobProcessor {
|
|
|
79
136
|
|
|
80
137
|
await ps.wait(inputTaskStream);
|
|
81
138
|
|
|
82
|
-
this.taskStream.on('error', (err) => {
|
|
83
|
-
|
|
84
|
-
// console.log('Stream ended');
|
|
139
|
+
this.taskStream.on('error', (err) => {
|
|
140
|
+
job.emit('error', err);
|
|
85
141
|
});
|
|
86
142
|
|
|
87
143
|
const tasksStreamEnded = ps.wait(this.taskStream);
|
|
@@ -96,7 +152,9 @@ export class JobProcessor {
|
|
|
96
152
|
|
|
97
153
|
async sync(task: any, job: Job): Promise<any> {
|
|
98
154
|
const pathOptions = {
|
|
99
|
-
fileFilter: (entry) => {
|
|
155
|
+
fileFilter: (entry) => {
|
|
156
|
+
return true;
|
|
157
|
+
},
|
|
100
158
|
depth: 1,
|
|
101
159
|
lstat: true
|
|
102
160
|
};
|
|
@@ -147,11 +205,12 @@ export class JobProcessor {
|
|
|
147
205
|
// Check processedTasks tasks
|
|
148
206
|
// Manually emit `end` event for all tasks finished
|
|
149
207
|
if (this.processedTasks === this.jobInfo.tasks.length) {
|
|
150
|
-
this.taskStream._flush(() => {
|
|
151
|
-
|
|
208
|
+
this.taskStream._flush(() => {
|
|
209
|
+
});
|
|
210
|
+
this.taskStream.end();
|
|
152
211
|
}
|
|
153
212
|
})
|
|
154
|
-
.pipe(this.taskStream, {
|
|
213
|
+
.pipe(this.taskStream, {end: false});
|
|
155
214
|
});
|
|
156
215
|
}
|
|
157
216
|
}
|
package/src/job_processor_gql.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as _ from 'lodash';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import { Client } from './index';
|
|
4
|
-
|
|
4
|
+
import { YamlStreamReadTransformer } from 'yaml-document-stream';
|
|
5
|
+
import { stringToChalk } from './utils';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* GraphQL-specific job processor.
|
|
@@ -9,6 +10,7 @@ const {YamlStreamReadTransformer} = require('yaml-document-stream');
|
|
|
9
10
|
export class GraphQLProcessor {
|
|
10
11
|
opts: any;
|
|
11
12
|
client: Client;
|
|
13
|
+
|
|
12
14
|
constructor(opts: any) {
|
|
13
15
|
if (_.isNil(opts)) {
|
|
14
16
|
throw new Error('Missing options parameter');
|
|
@@ -21,81 +23,123 @@ export class GraphQLProcessor {
|
|
|
21
23
|
this.client = new Client(opts);
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
async process(
|
|
26
|
+
async process(task: any, verbose = false, ignoreErrors = false, ignoreSelfSigned = false): Promise<any> {
|
|
25
27
|
let yamlStream = new YamlStreamReadTransformer();
|
|
26
|
-
let jobPath =
|
|
28
|
+
let jobPath = task.path;
|
|
27
29
|
let data = false;
|
|
28
|
-
|
|
30
|
+
const logColor = stringToChalk(task.name);
|
|
31
|
+
switch (task.operation) {
|
|
29
32
|
case 'sync': { // synchronous operation
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
console.log('Batch size:', batchsize);
|
|
36
|
-
}
|
|
33
|
+
return new Promise<any>((resolve, reject) => {
|
|
34
|
+
try {
|
|
35
|
+
yamlStream.on('error', (err) => {
|
|
36
|
+
!ignoreErrors && reject(err);
|
|
37
|
+
});
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const fileStream = fs.createReadStream(task.fullPath);
|
|
40
|
+
fileStream.pipe(yamlStream);
|
|
41
|
+
let batchsize;
|
|
42
|
+
if (task.batchSize) {
|
|
43
|
+
batchsize = task.batchSize;
|
|
44
|
+
console.log(`[${logColor(task.name)}] Batch size:`, batchsize);
|
|
45
|
+
}
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// right after that we reset the dataset object 'docArr'.
|
|
48
|
-
yamlStream.on('data', (doc) => {
|
|
49
|
-
data = true;
|
|
50
|
-
if (batchsize && batchsize != undefined) {
|
|
51
|
-
docArr.push(doc);
|
|
52
|
-
counter++;
|
|
47
|
+
let counter = 0;
|
|
48
|
+
let batchCounter = 0;
|
|
49
|
+
let docArr: any[] = [];
|
|
50
|
+
let resultArr: any[] = [];
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
52
|
+
// Here we read from the readable stream each yaml document parsed
|
|
53
|
+
// as an object, and if we have batching enabled we first batch this
|
|
54
|
+
// documents into smaller sets. When a dataset is ready to be imported,
|
|
55
|
+
// we pause the readable stream and emit a 'pause' event and
|
|
56
|
+
// right after that we reset the dataset object 'docArr'.
|
|
57
|
+
yamlStream.on('data', (doc) => {
|
|
58
|
+
data = true;
|
|
59
|
+
if (batchsize) {
|
|
60
|
+
docArr.push(doc);
|
|
61
|
+
counter++;
|
|
65
62
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
63
|
+
if (counter === batchsize) {
|
|
64
|
+
counter = 0;
|
|
65
|
+
let batchText = '';
|
|
66
|
+
if (batchsize > 0) {
|
|
67
|
+
const from = batchCounter * batchsize + 1;
|
|
68
|
+
const to = from + (docArr.length - 1);
|
|
69
|
+
batchText = from == to ? ` (${from})` : ` (${from} - ${to})`;
|
|
70
|
+
}
|
|
71
|
+
batchCounter++;
|
|
72
|
+
console.log(`[${logColor(task.name)}] Processing batch: ${batchCounter}${batchText}`);
|
|
73
|
+
yamlStream.pause();
|
|
74
|
+
docArr = [];
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
docArr.push(doc);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
76
80
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
// On 'pause' event we create a post request using the GQL Client
|
|
82
|
+
// using the accumulated resources inside the 'docArr' dataset,
|
|
83
|
+
// we wait for the response, store the response inside an array and
|
|
84
|
+
// only then we emit a 'resume' event, resuming reading from the
|
|
85
|
+
// 'yamlStream' readable stream.
|
|
86
|
+
yamlStream.on('pause', async () => {
|
|
87
|
+
try {
|
|
88
|
+
resultArr.push(await this.client.post(docArr, task, verbose, ignoreSelfSigned));
|
|
89
|
+
yamlStream.resume();
|
|
90
|
+
} catch (e) {
|
|
91
|
+
!ignoreErrors && reject(e);
|
|
92
|
+
ignoreErrors && yamlStream.resume();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
let runOnResume: undefined | (() => void);
|
|
97
|
+
yamlStream.on('resume', async () => {
|
|
98
|
+
if (runOnResume) {
|
|
99
|
+
runOnResume();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
86
102
|
|
|
87
|
-
if
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
// On 'end' if we still have data accumulated inside the 'docArr'
|
|
104
|
+
// dataset, we create a final post request to import this data as-well,
|
|
105
|
+
// store the response inside the array and finally resolve this as a
|
|
106
|
+
// Promise to return all the responses back to the initial caller.
|
|
107
|
+
const endFunc = async () => {
|
|
108
|
+
if (data === false) {
|
|
109
|
+
throw new Error(`Could not import resources from ${jobPath}. Readable stream is empty. Please provide a file with YAML multi-document format.`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (docArr && !_.isEmpty(docArr)) {
|
|
113
|
+
let batchText = '';
|
|
114
|
+
if (batchsize > 0) {
|
|
115
|
+
const from = batchCounter * batchsize + 1;
|
|
116
|
+
const to = from + (docArr.length - 1);
|
|
117
|
+
batchText = from == to ? ` (${from})` : ` (${from} - ${to})`;
|
|
118
|
+
}
|
|
119
|
+
batchCounter++;
|
|
120
|
+
console.log(`[${logColor(task.name)}] Processing batch: ${batchCounter}${batchText}`);
|
|
121
|
+
try {
|
|
122
|
+
resultArr.push(await this.client.post(docArr, task, verbose, ignoreSelfSigned));
|
|
123
|
+
} catch (e) {
|
|
124
|
+
!ignoreErrors && reject(e);
|
|
125
|
+
}
|
|
126
|
+
docArr = [];
|
|
127
|
+
}
|
|
93
128
|
|
|
94
129
|
resolve(resultArr as any);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
});
|
|
130
|
+
};
|
|
98
131
|
|
|
132
|
+
yamlStream.on('end', () => {
|
|
133
|
+
if (yamlStream.isPaused()) {
|
|
134
|
+
runOnResume = endFunc;
|
|
135
|
+
} else {
|
|
136
|
+
endFunc();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
} catch (e) {
|
|
140
|
+
!ignoreErrors && reject(e);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
99
143
|
}
|
|
100
144
|
default: {
|
|
101
145
|
throw new Error('Unsupported job operation');
|
package/src/utils.ts
ADDED