@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 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, accessControl?: any, formOptions?: any): Promise<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
- function _checkVariableMutation(mutation) {
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
- function _replaceInlineVars(mutation, args) {
30
+ };
31
+ const _replaceInlineVars = (mutation, args) => {
31
32
  if (mutation)
32
33
  return mutation.replace(/\${(\w+)}/g, (_, v) => args[v]);
33
- }
34
- function _createQueryVariables(inputVarName, queryVarKey, varValue) {
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, accessControl, formOptions) {
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('mutation not present in job config');
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
- return apolloClient.mutate({
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
  }
@@ -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
  }
@@ -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
- console.log('Processed task [' + this.processed + '] and status is:' + JSON.stringify(body));
65
- this.processed++;
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) => { throw err; });
82
- this.taskStream.on('end', () => {
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) => { return true; },
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
- this.taskStream.emit('end');
196
+ this.taskStream._flush(() => {
197
+ });
198
+ this.taskStream.end();
146
199
  }
147
200
  })
148
201
  .pipe(this.taskStream, { end: false });
@@ -6,5 +6,5 @@ export declare class GraphQLProcessor {
6
6
  opts: any;
7
7
  client: Client;
8
8
  constructor(opts: any);
9
- process(job: any): Promise<any>;
9
+ process(task: any, verbose?: boolean, ignoreErrors?: boolean, ignoreSelfSigned?: boolean): Promise<any>;
10
10
  }
@@ -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 { YamlStreamReadTransformer } = require('yaml-document-stream');
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(job) {
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 = job.path;
35
+ let yamlStream = new yaml_document_stream_1.YamlStreamReadTransformer();
36
+ let jobPath = task.path;
36
37
  let data = false;
37
- switch (job.operation) {
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
- yamlStream.on('end', () => __awaiter(this, void 0, void 0, function* () {
88
- if (data === false) {
89
- throw new Error(`Could not import resources from ${jobPath}. Readable stream is empty. Please provide a file with YAML multi-document format.`);
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
- if (docArr && !_.isEmpty(docArr)) {
92
- batchCounter++;
93
- console.log('Processing batch:', batchCounter);
94
- let result = yield this.client.post(docArr, job);
95
- resultArr.push(result);
96
- docArr = [];
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
@@ -0,0 +1,2 @@
1
+ import * as chalk from 'chalk';
2
+ export declare const stringToChalk: (str: any) => chalk.Chalk;
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.1.18",
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
- "tsctests": "tsc test/*.ts --module commonjs -target ES6",
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
- "gitHead": "2d453885b1c82df3e6aabbfbfe3b5a9c0b240277"
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
- function _checkVariableMutation(mutation: string): Boolean {
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
- function _replaceInlineVars(mutation: string, args: any): string {
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
- function _createQueryVariables(inputVarName: string, queryVarKey: string, varValue: any): Object {
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, accessControl?: 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('mutation not present in job config');
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
- return apolloClient.mutate({
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
  }
@@ -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({ concurrent: concurrency }, (task: any) => {
60
- return this.jobInfo.options.processor.process(task).then((body) => {
61
- console.log('Processed task [' + this.processed + '] and status is:' + JSON.stringify(body));
62
- this.processed++;
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) => { throw err; });
83
- this.taskStream.on('end', () => {
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) => { return true; },
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
- this.taskStream.emit('end');
208
+ this.taskStream._flush(() => {
209
+ });
210
+ this.taskStream.end();
152
211
  }
153
212
  })
154
- .pipe(this.taskStream, { end: false });
213
+ .pipe(this.taskStream, {end: false});
155
214
  });
156
215
  }
157
216
  }
@@ -1,7 +1,8 @@
1
1
  import * as _ from 'lodash';
2
2
  import * as fs from 'fs';
3
3
  import { Client } from './index';
4
- const {YamlStreamReadTransformer} = require('yaml-document-stream');
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(job: any): Promise<any> {
26
+ async process(task: any, verbose = false, ignoreErrors = false, ignoreSelfSigned = false): Promise<any> {
25
27
  let yamlStream = new YamlStreamReadTransformer();
26
- let jobPath = job.path;
28
+ let jobPath = task.path;
27
29
  let data = false;
28
- switch (job.operation) {
30
+ const logColor = stringToChalk(task.name);
31
+ switch (task.operation) {
29
32
  case 'sync': { // synchronous operation
30
- const fileStream = fs.createReadStream(job.fullPath);
31
- fileStream.pipe(yamlStream);
32
- let batchsize;
33
- if (job.batchSize) {
34
- batchsize = job.batchSize;
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
- let counter = 0;
39
- let batchCounter = 0;
40
- let docArr: any[] = [];
41
- let resultArr: any[] = [];
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
- // Here we read from the readable stream each yaml document parsed
44
- // as an object, and if we have batching enabled we first batch this
45
- // documents into smaller sets. When a dataset is ready to be imported,
46
- // we pause the readable stream and emit a 'pause' event and
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
- if (counter === batchsize) {
55
- counter = 0;
56
- batchCounter++;
57
- console.log('Processing batch:', batchCounter);
58
- yamlStream.pause();
59
- docArr = [];
60
- }
61
- } else {
62
- docArr.push(doc);
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
- // On 'pause' event we create a post request using the GQL Client
67
- // using the accumulated resources inside the 'docArr' dataset,
68
- // we wait for the response, store the response inside an array and
69
- // only then we emit a 'resume' event, resuming reading from the
70
- // 'yamlStream' readable stream.
71
- yamlStream.on('pause', async () => {
72
- let result = await this.client.post(docArr, job);
73
- resultArr.push(result);
74
- yamlStream.resume();
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
- // On 'end' if we still have data accumulated inside the 'docArr'
78
- // dataset, we create a final post request to import this data as-well,
79
- // store the response inside the array and finally resolve this as a
80
- // Promise to return all the responses back to the initial caller.
81
- return new Promise<any>((resolve, reject) => {
82
- yamlStream.on('end', async () => {
83
- if (data === false) {
84
- throw new Error(`Could not import resources from ${jobPath}. Readable stream is empty. Please provide a file with YAML multi-document format.`);
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 (docArr && !_.isEmpty(docArr)) {
88
- batchCounter++;
89
- console.log('Processing batch:', batchCounter);
90
- let result = await this.client.post(docArr, job);
91
- resultArr.push(result);
92
- docArr = [];
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
@@ -0,0 +1,10 @@
1
+ import ColorHash from 'color-hash';
2
+ import * as chalk from 'chalk';
3
+
4
+ const colorHash = new ColorHash({
5
+ lightness: [0.45, 0.6, 0.75]
6
+ });
7
+
8
+ export const stringToChalk = (str) => {
9
+ return chalk.hex(colorHash.hex(str));
10
+ };