closer-cli 0.0.4 → 0.4.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/csv/users.csv ADDED
@@ -0,0 +1,8 @@
1
+ UserCode,Name,Email,Password,GroupCodes,MacroFilterCodes,CanSendOrder,CanCloseOrder,SharingUsers,Role
2
+ MR,Mario Rossi,mario.rossicloser.sale,projectx,AI|WA,SS2020|FW2020|SS2021,TRUE,TRUE,EZ|EF|NR|FP|NDR|FC,
3
+ EZ,Enrico Zeffiro,enrico.zeffiro@h-farm.com,projectx,AI|WA,SS2020|FW2020|SS2021,TRUE,TRUE,MR|EF|NR|FP|NDR|FC,
4
+ EF,Emanuele Fornaro,emanuele.fornaro@h-farm.com,projectx,AI|WA,SS2020|FW2020,TRUE,TRUE,MR|EZ|NR|FP|NDR|FC,
5
+ NR,Nicole Raffino,nicole.raffino@h-farm.com,projectx,AI,SS2020|FW2020,TRUE,TRUE,MR|EZ|EF|FP|NDR|FC,ADMIN
6
+ FP,Francesco Pasqua,francesco.pasqua@h-farm.com,projectx,AI,SS2020|FW2020,TRUE,TRUE,MR|EZ|EF|NR|NDR|FC,ADMIN
7
+ NDR,Nicola Dei Rossi,nicola.deirossih-farm.com,projectx,AI,FW2020,TRUE,TRUE,MR|EZ|EF|NR|FP|FC,
8
+ FC,Fabio Carraro,fabio.carraro@h-farm.com,projectx,AI,SS2020|FW2020,TRUE,TRUE,MR|EZ|EF|NR|FP|NDR,
package/helpers.js ADDED
@@ -0,0 +1,706 @@
1
+ /* eslint-disable no-console */
2
+ /* eslint-disable no-param-reassign */
3
+ /* eslint-disable implicit-arrow-linebreak */
4
+ const fs = require('fs')
5
+ const path = require('path')
6
+ const WebSocket = require('ws')
7
+ const chalk = require('chalk')
8
+ const es = require('event-stream')
9
+ const parse = require('csv-parse/lib/sync')
10
+ const stringify = require('csv-stringify/lib/sync')
11
+ const got = require('got')
12
+ const { createWriteStream } = require('fs')
13
+
14
+ exports.createSocketConnection = (endpoint, adminSecret) => {
15
+ const protocol = endpoint.includes('localhost') ? 'ws' : 'wss'
16
+ const socket = new WebSocket(`${protocol}://${endpoint}/import`, {
17
+ headers: { 'x-closer-admin-secret': adminSecret }
18
+ })
19
+
20
+ return socket
21
+ }
22
+
23
+ exports.ALLOWED_CSV_FILES = {
24
+ product: 'products.csv',
25
+ price: 'prices.csv',
26
+ group: 'groups.csv',
27
+ lookup: 'lookups.csv',
28
+ user: 'users.csv',
29
+ customer: 'customers.csv',
30
+ label: 'labels.csv',
31
+ filterPosition: 'filterPosition.csv',
32
+ fieldConfiguration: 'fieldConfigurations.csv',
33
+ userCustomer: 'userCustomers.csv'
34
+ }
35
+
36
+ exports.getCsvFilesFromDir = dir =>
37
+ fs.readdirSync(dir).filter(file => path.extname(file).toLowerCase() === '.csv')
38
+
39
+ exports.getGoogleSheetCsvUrl = (docId, sheet) =>
40
+ `https://docs.google.com/spreadsheets/d/${docId}/gviz/tq?tqx=out:csv&sheet=${sheet}`
41
+
42
+ exports.importCsvFromFile = (file, csvType, outputDir, spinner, ws, delimiter = ',') =>
43
+ new Promise(resolve => {
44
+ const socket = this.createSocketConnection(ws.endpoint, ws.adminSecret)
45
+
46
+ const hrstart = process.hrtime()
47
+
48
+ let readStream = null
49
+
50
+ const errorCsvFile = path.resolve(outputDir, `${csvType}_errors.csv`)
51
+ const errorFileStream = fs.createWriteStream(errorCsvFile)
52
+
53
+ const skippedCsvFile = path.resolve(outputDir, `${csvType}_skipped.csv`)
54
+ const skippedFileStream = fs.createWriteStream(skippedCsvFile)
55
+
56
+ const filename = path.basename(file)
57
+ const dirname = path.dirname(errorCsvFile)
58
+ if (!fs.existsSync(dirname)) {
59
+ fs.mkdirSync(dirname, { recursive: true })
60
+ }
61
+
62
+ let header = ''
63
+ let lineNumber = 0
64
+ let countImported = 0
65
+ let countFailed = 0
66
+ let countSkipped = 0
67
+
68
+ spinner.start('Loading...')
69
+
70
+ const importRowSuccess = () => {
71
+ countImported += 1
72
+
73
+ spinner.text = this.printImportProcessResult({
74
+ countImported,
75
+ countFailed,
76
+ countSkipped,
77
+ executionTime: this.getExecutionTime(hrstart),
78
+ name: filename
79
+ })
80
+
81
+ readStream.resume()
82
+ }
83
+
84
+ const importRowError = csvRow => {
85
+ countFailed += 1
86
+
87
+ spinner.text = this.printImportProcessResult({
88
+ countImported,
89
+ countFailed,
90
+ countSkipped,
91
+ executionTime: this.getExecutionTime(hrstart),
92
+ name: filename
93
+ })
94
+
95
+ errorFileStream.write(
96
+ stringify([csvRow], {
97
+ header: countFailed === 1,
98
+ quoted: true,
99
+ delimiter: ';'
100
+ })
101
+ )
102
+
103
+ readStream.resume()
104
+ }
105
+
106
+ const importFatal = errorMessage => {
107
+ spinner.fail(
108
+ this.printImportProcessResult({
109
+ countImported,
110
+ countFailed,
111
+ countSkipped,
112
+ executionTime: this.getExecutionTime(hrstart),
113
+ name: filename,
114
+ fatalError: errorMessage
115
+ })
116
+ )
117
+
118
+ resolve({
119
+ csvType,
120
+ countImported,
121
+ countFailed,
122
+ countSkipped,
123
+ executionTime: this.getExecutionTime(hrstart),
124
+ inputFile: file,
125
+ fatalError: `FATAL: ${errorMessage}`
126
+ })
127
+
128
+ readStream.destroy()
129
+ errorFileStream.end()
130
+ skippedFileStream.end()
131
+ }
132
+
133
+ const importSuccess = () => {
134
+ if (countFailed > 0 || countSkipped > 0) {
135
+ spinner.warn()
136
+ } else {
137
+ spinner.succeed()
138
+ }
139
+
140
+ errorFileStream.end()
141
+ skippedFileStream.end()
142
+
143
+ resolve({
144
+ csvType,
145
+ countImported,
146
+ countFailed,
147
+ countSkipped,
148
+ executionTime: this.getExecutionTime(hrstart),
149
+ inputFile: file
150
+ })
151
+ }
152
+
153
+ socket.on('open', () => {
154
+ readStream = fs
155
+ .createReadStream(file)
156
+ .pipe(es.split())
157
+ .pipe(
158
+ es.mapSync(line => {
159
+ readStream.pause()
160
+
161
+ if (lineNumber === 0) {
162
+ header = line
163
+ readStream.resume()
164
+ } else {
165
+ try {
166
+ const rows = parse(`${header}\n${line}`, {
167
+ columns: true,
168
+ skip_empty_lines: true,
169
+ skipLinesWithEmptyValues: true,
170
+ delimiter
171
+ })
172
+
173
+ if (rows.length > 0) {
174
+ const packet = {
175
+ csvRow: rows[0],
176
+ csvType,
177
+ lineNumber,
178
+ type: 'importCsvRow'
179
+ }
180
+ socket.send(JSON.stringify(packet))
181
+ } else {
182
+ readStream.resume()
183
+ }
184
+ } catch (e) {
185
+ countSkipped += 1
186
+ skippedFileStream.write(`${line},${e.message}`)
187
+ readStream.resume()
188
+ }
189
+ }
190
+
191
+ lineNumber += 1
192
+ })
193
+ )
194
+ .on('error', e => importFatal(e.message))
195
+ .on('end', importSuccess)
196
+ })
197
+
198
+ socket.on('message', packet => {
199
+ const { type, payload } = JSON.parse(packet)
200
+
201
+ switch (type) {
202
+ case 'importRowSuccess':
203
+ importRowSuccess()
204
+ break
205
+ case 'importRowError':
206
+ importRowError(payload.csvRow)
207
+ break
208
+ case 'importRowFatal':
209
+ importFatal(payload.error)
210
+ break
211
+ default:
212
+ break
213
+ }
214
+ })
215
+
216
+ socket.on('error', e => {
217
+ importFatal(e.message)
218
+ })
219
+ })
220
+
221
+ exports.validateCsvFromFile = (file, csvType, ws, delimiter = ',') =>
222
+ new Promise(resolve => {
223
+ const socket = this.createSocketConnection(ws.endpoint, ws.adminSecret)
224
+
225
+ const hrstart = process.hrtime()
226
+
227
+ let readStream = null
228
+
229
+ let header = ''
230
+ let lineNumber = 0
231
+ const filename = path.basename(file)
232
+
233
+ socket.on('open', () => {
234
+ readStream = fs
235
+ .createReadStream(file)
236
+ .pipe(es.split())
237
+ .pipe(
238
+ es.mapSync(line => {
239
+ readStream.pause()
240
+
241
+ if (lineNumber === 0) {
242
+ header = line
243
+ readStream.resume()
244
+ }
245
+
246
+ if (lineNumber === 1) {
247
+ const rows = parse(`${header}\n${line}`, {
248
+ columns: true,
249
+ skip_empty_lines: true,
250
+ skipLinesWithEmptyValues: true,
251
+ delimiter
252
+ })
253
+
254
+ const packet = {
255
+ csvColumns: Object.keys(rows[0]),
256
+ csvType,
257
+ type: 'validateCsvHeaderRequest'
258
+ }
259
+
260
+ socket.send(JSON.stringify(packet))
261
+ readStream.destroy()
262
+ }
263
+
264
+ lineNumber += 1
265
+ })
266
+ )
267
+ .on('error', e => {
268
+ resolve({
269
+ csvType,
270
+ executionTime: this.getExecutionTime(hrstart),
271
+ inputFile: filename,
272
+ error: e.message
273
+ })
274
+ })
275
+ })
276
+
277
+ socket.on('message', packet => {
278
+ const { type, payload } = JSON.parse(packet)
279
+ switch (type) {
280
+ case 'validateCsvHeaderResponse': {
281
+ const result = {
282
+ csvType,
283
+ executionTime: this.getExecutionTime(hrstart),
284
+ inputFile: filename
285
+ }
286
+
287
+ if (payload.error) {
288
+ result.error = payload.error
289
+ }
290
+
291
+ resolve(result)
292
+ break
293
+ }
294
+ default:
295
+ break
296
+ }
297
+ })
298
+
299
+ socket.on('error', e => {
300
+ resolve({
301
+ csvType,
302
+ executionTime: this.getExecutionTime(hrstart),
303
+ inputFile: file,
304
+ error: e.message
305
+ })
306
+ })
307
+ })
308
+
309
+ exports.importCsvFromUrl = async (url, csvType, outputDir, spinner, ws) => {
310
+ spinner.start('Downloading file...')
311
+ const downloadedFile = await this.download(url, outputDir, `${csvType}.csv`)
312
+ spinner.succeed('Downloaded file')
313
+ return this.importCsvFromFile(downloadedFile, csvType, outputDir, spinner, ws)
314
+ }
315
+
316
+ exports.getExecutionTime = start => {
317
+ const hrend = process.hrtime(start)
318
+ return ((hrend[0] * 1e9 + hrend[1]) / 1e9).toFixed(1)
319
+ }
320
+
321
+ exports.printValidationProcessHeader = () => {
322
+ console.log('\n=== Validation Process Result ===\n')
323
+ }
324
+
325
+ exports.printImportProcessHeader = () => {
326
+ console.log('\n=== Import Process Result ===\n')
327
+ }
328
+
329
+ exports.printValidationProcessResult = ({
330
+ inputFile,
331
+ error
332
+ // eslint-disable-next-line consistent-return
333
+ }) => {
334
+ let message = ''
335
+ const filename = path.basename(inputFile)
336
+
337
+ if (error) {
338
+ message = `[${chalk.red(filename)}] not valid ${chalk.gray(`[${error}]`)}`
339
+ } else {
340
+ message = `[${chalk.green(filename)}] valid`
341
+ }
342
+
343
+ console.log(message)
344
+ }
345
+
346
+ exports.printImportProcessResult = ({
347
+ countImported,
348
+ countFailed,
349
+ countSkipped,
350
+ executionTime,
351
+ name,
352
+ fatalError
353
+ // eslint-disable-next-line consistent-return
354
+ }) => {
355
+ let message = ''
356
+
357
+ if (fatalError) {
358
+ message = `[${chalk.red(name)}] ${chalk.red(fatalError)}`
359
+ return message
360
+ }
361
+
362
+ message = `[${this.colorByResult(
363
+ name,
364
+ countImported,
365
+ countFailed,
366
+ countSkipped
367
+ )}] imported ${this.colorGreaterThanZero(countImported, 'green')} items`
368
+
369
+ if (countFailed) {
370
+ message += `, failed ${this.colorGreaterThanZero(countFailed, 'red')} items`
371
+ }
372
+
373
+ if (countSkipped) {
374
+ message += `, skipped ${this.colorGreaterThanZero(countSkipped, 'red')} items`
375
+ }
376
+
377
+ message += ` ${chalk.gray(`[${executionTime}s]`)}`
378
+
379
+ return message
380
+ }
381
+
382
+ exports.colorByResult = (value, countImported, countFailed, countSkipped) => {
383
+ let color = 'green'
384
+
385
+ if (countImported > 0 && (countFailed > 0 || countSkipped > 0)) {
386
+ color = 'yellow'
387
+ }
388
+
389
+ if (countImported === 0 && (countFailed > 0 || countSkipped > 0)) {
390
+ color = 'red'
391
+ }
392
+
393
+ return chalk[color](value)
394
+ }
395
+
396
+ exports.colorGreaterThanZero = (value, color) => {
397
+ if (value > 0) {
398
+ return chalk[color](value)
399
+ }
400
+
401
+ return value
402
+ }
403
+
404
+ exports.refreshOrders = (spinner, ws) =>
405
+ new Promise(resolve => {
406
+ const hrstart = process.hrtime()
407
+
408
+ const socket = this.createSocketConnection(ws.endpoint, ws.adminSecret)
409
+
410
+ spinner.start('Refreshing draft orders...')
411
+
412
+ socket.on('open', () => {
413
+ socket.send(JSON.stringify({ type: 'refreshDraftOrdersRequest' }))
414
+ })
415
+
416
+ socket.on('message', packet => {
417
+ const { type, payload } = JSON.parse(packet)
418
+ switch (type) {
419
+ case 'refreshDraftOrdersResponse': {
420
+ spinner.text = this.printRefreshedOrders({
421
+ ...payload,
422
+ executionTime: this.getExecutionTime(hrstart)
423
+ })
424
+ spinner.succeed()
425
+ resolve({
426
+ totalCount: payload.totalCount,
427
+ refreshed: payload.refreshed,
428
+ errored: payload.errored,
429
+ executionTime: this.getExecutionTime(hrstart)
430
+ })
431
+ break
432
+ }
433
+ default:
434
+ break
435
+ }
436
+ })
437
+
438
+ socket.on('error', e => {
439
+ resolve({ error: e.message })
440
+ })
441
+ })
442
+
443
+ // eslint-disable-next-line consistent-return
444
+ exports.printRefreshedOrders = ({ refreshed, errored, executionTime }) => {
445
+ let message = ''
446
+
447
+ const stringRefreshed = this.colorGreaterThanZero(refreshed.length, 'green')
448
+ const stringErrored = this.colorGreaterThanZero(errored.length, 'red')
449
+
450
+ message += `Refreshed ${stringRefreshed} orders (draft)`
451
+
452
+ if (errored.length) {
453
+ message += `, refresh failed for ${stringErrored} orders.`
454
+ }
455
+
456
+ message += ` ${chalk.gray(`[${executionTime}s]`)}`
457
+
458
+ return message
459
+ }
460
+
461
+ exports.truncateTablesByCsvTypes = (csvTypes, spinner, ws) =>
462
+ new Promise(resolve => {
463
+ const socket = this.createSocketConnection(ws.endpoint, ws.adminSecret)
464
+
465
+ spinner.start('Deleting database records...')
466
+
467
+ socket.on('open', () => {
468
+ socket.send(JSON.stringify({ type: 'truncateTablesByCsvTypesRequest', csvTypes }))
469
+ })
470
+
471
+ socket.on('message', packet => {
472
+ const { type } = JSON.parse(packet)
473
+ switch (type) {
474
+ case 'truncateTablesByCsvTypesResponse': {
475
+ spinner.succeed('Database records deleted (soft)')
476
+ resolve()
477
+ break
478
+ }
479
+ default:
480
+ break
481
+ }
482
+ })
483
+
484
+ socket.on('error', e => {
485
+ resolve({ error: e.message })
486
+ })
487
+ })
488
+
489
+ exports.download = async (url, destPath, fileName) =>
490
+ new Promise((resolve, reject) => {
491
+ const filePath = path.resolve(destPath, fileName)
492
+
493
+ const dirname = path.dirname(filePath)
494
+ if (!fs.existsSync(dirname)) {
495
+ fs.mkdirSync(dirname, { recursive: true })
496
+ }
497
+
498
+ const downloadStream = got.stream(url)
499
+ const fileWriterStream = createWriteStream(filePath)
500
+
501
+ downloadStream
502
+ // .on('downloadProgress', ({ transferred, total, percent }) => {
503
+ // const percentage = Math.round(percent * 100)
504
+ // console.error(`progress: ${transferred}/${total} (${percentage}%)`)
505
+ // })
506
+ .on('error', reject)
507
+
508
+ fileWriterStream.on('error', reject).on('finish', () => {
509
+ resolve(filePath)
510
+ })
511
+
512
+ downloadStream.pipe(fileWriterStream)
513
+ })
514
+
515
+ exports.callBeforeDataImportProcess = (spinner, ws) =>
516
+ new Promise((resolve, reject) => {
517
+ const socket = this.createSocketConnection(ws.endpoint, ws.adminSecret)
518
+
519
+ spinner.start('Executing tasks before start...')
520
+
521
+ socket.on('open', () => {
522
+ socket.send(JSON.stringify({ type: 'callBeforeDataImportProcessRequest' }))
523
+ })
524
+
525
+ socket.on('message', packet => {
526
+ const { type, payload } = JSON.parse(packet)
527
+ switch (type) {
528
+ case 'callBeforeDataImportProcessResponse': {
529
+ if (payload.error) {
530
+ spinner.fail(payload.error)
531
+ reject()
532
+ } else {
533
+ spinner.succeed('Tasks executed before start')
534
+ resolve()
535
+ }
536
+ break
537
+ }
538
+ default:
539
+ break
540
+ }
541
+ })
542
+
543
+ socket.on('error', e => {
544
+ resolve({ error: e.message })
545
+ })
546
+ })
547
+
548
+ exports.callAfterDataImportProcess = (spinner, ws, report) =>
549
+ new Promise(resolve => {
550
+ const socket = this.createSocketConnection(ws.endpoint, ws.adminSecret)
551
+
552
+ spinner.start('Executing tasks after the end process...')
553
+
554
+ socket.on('open', () => {
555
+ socket.send(JSON.stringify({ type: 'callAfterDataImportProcessRequest', payload: report }))
556
+ })
557
+
558
+ socket.on('message', packet => {
559
+ const { type } = JSON.parse(packet)
560
+ switch (type) {
561
+ case 'callAfterDataImportProcessResponse': {
562
+ spinner.succeed('Tasks executed after the end process')
563
+ resolve()
564
+ break
565
+ }
566
+ default:
567
+ break
568
+ }
569
+ })
570
+
571
+ socket.on('error', e => {
572
+ resolve({ error: e.message })
573
+ })
574
+ })
575
+
576
+ exports.callAfterMediaImportProcess = (spinner, ws, report) =>
577
+ new Promise(resolve => {
578
+ const socket = this.createSocketConnection(ws.endpoint, ws.adminSecret)
579
+
580
+ spinner.start('Executing tasks after the end process...')
581
+
582
+ socket.on('open', () => {
583
+ socket.send(JSON.stringify({ type: 'callAfterMediaImportProcessRequest', payload: report }))
584
+ })
585
+
586
+ socket.on('message', packet => {
587
+ const { type } = JSON.parse(packet)
588
+ switch (type) {
589
+ case 'callAfterMediaImportProcessResponse': {
590
+ spinner.succeed('Tasks executed after the end process')
591
+ resolve()
592
+ break
593
+ }
594
+ default:
595
+ break
596
+ }
597
+ })
598
+
599
+ socket.on('error', e => {
600
+ resolve({ error: e.message })
601
+ })
602
+ })
603
+
604
+ exports.importMedia = (reprocess, spinner, ws) =>
605
+ new Promise(resolve => {
606
+ const hrstart = process.hrtime()
607
+ const socket = this.createSocketConnection(ws.endpoint, ws.adminSecret)
608
+
609
+ let countAssetImported = 0
610
+ let countAssetSkipped = 0
611
+ let countAssetFailed = 0
612
+ let countProductAssetImported = 0
613
+
614
+ const updateSpinnerText = () => {
615
+ spinner.text = this.printImportProcessResult({
616
+ countImported: countAssetImported,
617
+ countSkipped: countAssetSkipped,
618
+ countFailed: countAssetFailed,
619
+ executionTime: this.getExecutionTime(hrstart),
620
+ name: 'Asset'
621
+ })
622
+ }
623
+
624
+ socket.on('open', () => {
625
+ socket.send(JSON.stringify({ type: 'import.images', payload: { reprocess } }))
626
+ })
627
+
628
+ socket.on('message', packet => {
629
+ const { type, payload } = JSON.parse(packet)
630
+
631
+ switch (type) {
632
+ case 'import.images.start':
633
+ spinner.start('[Asset] Importing Assets...')
634
+ break
635
+ case 'import.images.imageAsset.success':
636
+ countAssetImported += 1
637
+ updateSpinnerText()
638
+ break
639
+ case 'import.images.imageAsset.skipped':
640
+ countAssetSkipped += 1
641
+ updateSpinnerText()
642
+ break
643
+ case 'import.images.imageAsset.failed':
644
+ countAssetFailed += 1
645
+ updateSpinnerText()
646
+ break
647
+ case 'import.images.productAssets.start':
648
+ countProductAssetImported = payload.count
649
+ spinner.succeed()
650
+ spinner.start(`[ProductAsset] Importing ${countProductAssetImported} Product Assets...`)
651
+ break
652
+ case 'import.images.productAssets.end':
653
+ spinner.succeed(
654
+ this.printImportProcessResult({
655
+ countImported: payload.count,
656
+ countFailed: 0,
657
+ executionTime: this.getExecutionTime(hrstart),
658
+ name: 'ProductAsset'
659
+ })
660
+ )
661
+ break
662
+ case 'import.images.end':
663
+ resolve({
664
+ countAssetImported,
665
+ countAssetSkipped,
666
+ countAssetFailed,
667
+ countProductAssetImported
668
+ })
669
+ break
670
+ default:
671
+ break
672
+ }
673
+ })
674
+
675
+ socket.on('error', e => {
676
+ resolve({ error: e.message })
677
+ })
678
+ })
679
+
680
+ exports.flushMedia = (spinner, ws) =>
681
+ new Promise(resolve => {
682
+ const socket = this.createSocketConnection(ws.endpoint, ws.adminSecret)
683
+
684
+ spinner.start('Flushing... ')
685
+
686
+ socket.on('open', () => {
687
+ socket.send(JSON.stringify({ type: 'flush.media.request' }))
688
+ })
689
+
690
+ socket.on('message', packet => {
691
+ const { type } = JSON.parse(packet)
692
+ switch (type) {
693
+ case 'flush.media.response': {
694
+ spinner.succeed('Flushing completed')
695
+ resolve()
696
+ break
697
+ }
698
+ default:
699
+ break
700
+ }
701
+ })
702
+
703
+ socket.on('error', e => {
704
+ resolve({ error: e.message })
705
+ })
706
+ })