@restorecommerce/gql-bot 0.1.16 → 0.2.0

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,36 @@
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.0](https://github.com/restorecommerce/libs/compare/@restorecommerce/gql-bot@0.1.18...@restorecommerce/gql-bot@0.2.0) (2022-06-20)
7
+
8
+
9
+ ### Features
10
+
11
+ * **gql-bot:** correctly handle errors, more readable output ([b773d2b](https://github.com/restorecommerce/libs/commit/b773d2b94d41233f9660a96de5cac4e85f305e66))
12
+
13
+
14
+
15
+
16
+
17
+ ## [0.1.18](https://github.com/restorecommerce/libs/compare/@restorecommerce/gql-bot@0.1.17...@restorecommerce/gql-bot@0.1.18) (2022-06-10)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * fix merge issues ([cc37d83](https://github.com/restorecommerce/libs/commit/cc37d8356df3b494af8c6af9e39304a49073301c))
23
+
24
+
25
+
26
+
27
+
28
+ ## [0.1.17](https://github.com/restorecommerce/libs/compare/@restorecommerce/gql-bot@0.1.16...@restorecommerce/gql-bot@0.1.17) (2022-05-16)
29
+
30
+ **Note:** Version bump only for package @restorecommerce/gql-bot
31
+
32
+
33
+
34
+
35
+
6
36
  ## [0.1.16](https://github.com/restorecommerce/libs/compare/@restorecommerce/gql-bot@0.1.15...@restorecommerce/gql-bot@0.1.16) (2022-02-14)
7
37
 
8
38
  **Note:** Version bump only for package @restorecommerce/gql-bot
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): Promise<any>;
9
9
  }
package/lib/client.js CHANGED
@@ -18,7 +18,7 @@ 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 _checkVariableMutation = (mutation) => {
22
22
  const mutationName = mutation.slice(mutation.indexOf(' '), mutation.indexOf('($'));
23
23
  if (mutationName.indexOf('$') > 0) {
24
24
  return false;
@@ -26,12 +26,12 @@ function _checkVariableMutation(mutation) {
26
26
  else {
27
27
  return new RegExp('\\b' + mutationName + '\\(', 'i').test(mutation);
28
28
  }
29
- }
30
- function _replaceInlineVars(mutation, args) {
29
+ };
30
+ const _replaceInlineVars = (mutation, args) => {
31
31
  if (mutation)
32
32
  return mutation.replace(/\${(\w+)}/g, (_, v) => args[v]);
33
- }
34
- function _createQueryVariables(inputVarName, queryVarKey, varValue) {
33
+ };
34
+ const _createQueryVariables = (inputVarName, queryVarKey, varValue) => {
35
35
  if (queryVarKey) {
36
36
  return {
37
37
  [inputVarName]: {
@@ -42,7 +42,44 @@ function _createQueryVariables(inputVarName, queryVarKey, varValue) {
42
42
  return {
43
43
  [inputVarName]: JSON.parse(varValue)
44
44
  };
45
- }
45
+ };
46
+ const checkError = (data) => {
47
+ if (typeof data === 'object') {
48
+ if (Array.isArray(data)) {
49
+ const result = data.map(value => {
50
+ const inner = checkError(value);
51
+ if (inner) {
52
+ return inner;
53
+ }
54
+ }).filter(value => !!value);
55
+ if (result.length > 0) {
56
+ return result;
57
+ }
58
+ }
59
+ else {
60
+ if ('__typename' in data) {
61
+ switch (data['__typename']) {
62
+ case 'IoRestorecommerceStatusOperationStatus':
63
+ case 'IoRestorecommerceStatusStatus':
64
+ if ('code' in data) {
65
+ const code = data['code'];
66
+ if (code != '' && code != '200' && code != 0 && code != 200) {
67
+ return data;
68
+ }
69
+ }
70
+ break;
71
+ }
72
+ }
73
+ for (const value of Object.values(data)) {
74
+ const inner = checkError(value);
75
+ if (inner) {
76
+ return inner;
77
+ }
78
+ }
79
+ }
80
+ }
81
+ return undefined;
82
+ };
46
83
  class Client {
47
84
  constructor(opts) {
48
85
  if (_.isNil(opts)) {
@@ -95,7 +132,7 @@ class Client {
95
132
  }
96
133
  return url.resolve(this.entryBaseUrl, extendURL);
97
134
  }
98
- post(source, job, accessControl, formOptions) {
135
+ post(source, job, verbose = false) {
99
136
  return __awaiter(this, void 0, void 0, function* () {
100
137
  const normalUrl = this._normalizeUrl();
101
138
  let mutation;
@@ -103,7 +140,7 @@ class Client {
103
140
  mutation = JSON.stringify(job.mutation);
104
141
  }
105
142
  else {
106
- throw new Error('mutation not present in job config');
143
+ throw new Error(`mutation not present in job config (${job.name})`);
107
144
  }
108
145
  const apiKey = JSON.stringify(this.opts.apiKey);
109
146
  let resource_list = JSON.stringify(source);
@@ -140,10 +177,22 @@ class Client {
140
177
  cache: apolloCache,
141
178
  link: apolloLink
142
179
  });
143
- return apolloClient.mutate({
180
+ const response = yield apolloClient.mutate({
144
181
  mutation: graphql_tag_1.default `${mutation}`,
145
182
  variables
146
183
  });
184
+ const error = checkError(response);
185
+ if (error) {
186
+ if (verbose) {
187
+ console.error(JSON.stringify({
188
+ request: mutation,
189
+ variables,
190
+ response
191
+ }));
192
+ }
193
+ throw new Error(JSON.stringify(error));
194
+ }
195
+ return response;
147
196
  });
148
197
  }
149
198
  }
@@ -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): 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) {
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).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): 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,117 @@ 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) {
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;
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));
93
+ yamlStream.resume();
94
+ }
95
+ catch (e) {
96
+ !ignoreErrors && reject(e);
97
+ }
98
+ }));
99
+ let runOnResume;
100
+ yamlStream.on('resume', () => __awaiter(this, void 0, void 0, function* () {
101
+ if (runOnResume) {
102
+ runOnResume();
103
+ }
104
+ }));
105
+ // On 'end' if we still have data accumulated inside the 'docArr'
106
+ // dataset, we create a final post request to import this data as-well,
107
+ // store the response inside the array and finally resolve this as a
108
+ // Promise to return all the responses back to the initial caller.
109
+ const endFunc = () => __awaiter(this, void 0, void 0, function* () {
110
+ if (data === false) {
111
+ throw new Error(`Could not import resources from ${jobPath}. Readable stream is empty. Please provide a file with YAML multi-document format.`);
112
+ }
113
+ if (docArr && !_.isEmpty(docArr)) {
114
+ let batchText = '';
115
+ if (batchsize > 0) {
116
+ const from = batchCounter * batchsize;
117
+ const to = from + (docArr.length - 1);
118
+ batchText = from == to ? ` (${from})` : ` (${from} - ${to})`;
119
+ }
120
+ batchCounter++;
121
+ console.log(`[${logColor(task.name)}] Processing batch: ${batchCounter}${batchText}`);
122
+ try {
123
+ resultArr.push(yield this.client.post(docArr, task, verbose));
124
+ }
125
+ catch (e) {
126
+ !ignoreErrors && reject(e);
127
+ }
128
+ docArr = [];
129
+ }
97
130
  resolve(resultArr);
98
- }
99
- }));
131
+ });
132
+ yamlStream.on('end', () => {
133
+ if (yamlStream.isPaused()) {
134
+ runOnResume = endFunc;
135
+ }
136
+ else {
137
+ endFunc();
138
+ }
139
+ });
140
+ }
141
+ catch (e) {
142
+ !ignoreErrors && reject(e);
143
+ }
100
144
  });
101
145
  }
102
146
  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.16",
5
+ "version": "0.2.0",
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": "e97bbfe2fe8166dfe1cd47ae60bce54347a4f1c9"
65
+ "nx": {
66
+ "targets": {
67
+ "build": {
68
+ "outputs": [
69
+ "./lib"
70
+ ]
71
+ }
72
+ }
73
+ },
74
+ "gitHead": "65fa379f6d4a215fb6c760431f9718c25b6f51ad"
63
75
  }
package/src/client.ts CHANGED
@@ -7,7 +7,7 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
7
7
  import fetch from 'node-fetch'; // required for apollo-link-http
8
8
  import { createHttpLink } from 'apollo-link-http';
9
9
 
10
- function _checkVariableMutation(mutation: string): Boolean {
10
+ const _checkVariableMutation = (mutation: string): Boolean => {
11
11
  const mutationName = mutation.slice(mutation.indexOf(' '),
12
12
  mutation.indexOf('($'));
13
13
  if (mutationName.indexOf('$') > 0) {
@@ -15,14 +15,14 @@ function _checkVariableMutation(mutation: string): Boolean {
15
15
  } else {
16
16
  return new RegExp('\\b' + mutationName + '\\(', 'i').test(mutation);
17
17
  }
18
- }
18
+ };
19
19
 
20
- function _replaceInlineVars(mutation: string, args: any): string {
20
+ const _replaceInlineVars = (mutation: string, args: any): string => {
21
21
  if (mutation)
22
22
  return mutation.replace(/\${(\w+)}/g, (_, v) => args[v]);
23
- }
23
+ };
24
24
 
25
- function _createQueryVariables(inputVarName: string, queryVarKey: string, varValue: any): Object {
25
+ const _createQueryVariables = (inputVarName: string, queryVarKey: string, varValue: any): Object => {
26
26
  if (queryVarKey) {
27
27
  return {
28
28
  [inputVarName]: {
@@ -34,7 +34,45 @@ function _createQueryVariables(inputVarName: string, queryVarKey: string, varVal
34
34
  return {
35
35
  [inputVarName]: JSON.parse(varValue)
36
36
  };
37
- }
37
+ };
38
+
39
+ const checkError = (data: any): any => {
40
+ if (typeof data === 'object') {
41
+ if (Array.isArray(data)) {
42
+ const result = data.map(value => {
43
+ const inner = checkError(value);
44
+ if (inner) {
45
+ return inner;
46
+ }
47
+ }).filter(value => !!value);
48
+ if (result.length > 0) {
49
+ return result;
50
+ }
51
+ } else {
52
+ if ('__typename' in data) {
53
+ switch (data['__typename']) {
54
+ case 'IoRestorecommerceStatusOperationStatus':
55
+ case 'IoRestorecommerceStatusStatus':
56
+ if ('code' in data) {
57
+ const code = data['code'];
58
+ if (code != '' && code != '200' && code != 0 && code != 200) {
59
+ return data;
60
+ }
61
+ }
62
+ break;
63
+ }
64
+ }
65
+
66
+ for (const value of Object.values(data)) {
67
+ const inner = checkError(value);
68
+ if (inner) {
69
+ return inner;
70
+ }
71
+ }
72
+ }
73
+ }
74
+ return undefined;
75
+ };
38
76
 
39
77
  export class Client {
40
78
  opts: any;
@@ -100,16 +138,14 @@ export class Client {
100
138
  return url.resolve(this.entryBaseUrl, extendURL);
101
139
  }
102
140
 
103
- async post(source: any, job?: any, accessControl?: any,
104
- formOptions?: any): Promise<any> {
105
-
141
+ async post(source: any, job?: any, verbose = false): Promise<any> {
106
142
  const normalUrl = this._normalizeUrl();
107
143
 
108
144
  let mutation;
109
145
  if (job && job.mutation) {
110
146
  mutation = JSON.stringify(job.mutation);
111
147
  } else {
112
- throw new Error('mutation not present in job config');
148
+ throw new Error(`mutation not present in job config (${job.name})`);
113
149
  }
114
150
 
115
151
  const apiKey = JSON.stringify(this.opts.apiKey);
@@ -153,10 +189,23 @@ export class Client {
153
189
  link: apolloLink
154
190
  });
155
191
 
156
- return apolloClient.mutate({
192
+ const response = await apolloClient.mutate({
157
193
  mutation: gql`${mutation}`,
158
194
  variables
159
195
  });
160
196
 
197
+ const error = checkError(response);
198
+ if (error) {
199
+ if (verbose) {
200
+ console.error(JSON.stringify({
201
+ request: mutation,
202
+ variables,
203
+ response
204
+ }));
205
+ }
206
+ throw new Error(JSON.stringify(error));
207
+ }
208
+
209
+ return response;
161
210
  }
162
211
  }
@@ -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): 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).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,122 @@ 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): 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;
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));
89
+ yamlStream.resume();
90
+ } catch (e) {
91
+ !ignoreErrors && reject(e);
92
+ }
93
+ });
94
+
95
+ let runOnResume: undefined | (() => void);
96
+ yamlStream.on('resume', async () => {
97
+ if (runOnResume) {
98
+ runOnResume();
99
+ }
100
+ });
86
101
 
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 = [];
102
+ // On 'end' if we still have data accumulated inside the 'docArr'
103
+ // dataset, we create a final post request to import this data as-well,
104
+ // store the response inside the array and finally resolve this as a
105
+ // Promise to return all the responses back to the initial caller.
106
+ const endFunc = async () => {
107
+ if (data === false) {
108
+ throw new Error(`Could not import resources from ${jobPath}. Readable stream is empty. Please provide a file with YAML multi-document format.`);
109
+ }
110
+
111
+ if (docArr && !_.isEmpty(docArr)) {
112
+ let batchText = '';
113
+ if (batchsize > 0) {
114
+ const from = batchCounter * batchsize;
115
+ const to = from + (docArr.length - 1);
116
+ batchText = from == to ? ` (${from})` : ` (${from} - ${to})`;
117
+ }
118
+ batchCounter++;
119
+ console.log(`[${logColor(task.name)}] Processing batch: ${batchCounter}${batchText}`);
120
+ try {
121
+ resultArr.push(await this.client.post(docArr, task, verbose));
122
+ } catch (e) {
123
+ !ignoreErrors && reject(e);
124
+ }
125
+ docArr = [];
126
+ }
93
127
 
94
128
  resolve(resultArr as any);
95
- }
96
- });
97
- });
129
+ };
98
130
 
131
+ yamlStream.on('end', () => {
132
+ if (yamlStream.isPaused()) {
133
+ runOnResume = endFunc;
134
+ } else {
135
+ endFunc();
136
+ }
137
+ });
138
+ } catch (e) {
139
+ !ignoreErrors && reject(e);
140
+ }
141
+ });
99
142
  }
100
143
  default: {
101
144
  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
+ };