leo-connector-common 5.0.0 → 5.0.3

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/checksum/index.js CHANGED
@@ -1,793 +1,793 @@
1
- let leo = require("leo-sdk");
2
- const { Lambda } = require("@aws-sdk/client-lambda");
3
- const checksum = require("./lib/checksumNibbler.js");
4
- const leoaws = require('leo-aws');
5
- let cron = leo.bot;
6
-
7
- /** These are for file connector **/
8
- let httpsObj = require("https");
9
- let httpObj = require("http");
10
- let URL = require("url");
11
- const crypto = require('crypto');
12
-
13
- const moment = require("moment");
14
- const uuid = require("uuid");
15
- const ls = leo.streams;
16
-
17
- const async = require("async");
18
-
19
- const tableName = leo.configuration.resources.LeoCron;
20
- let logger = require('leo-logger')('leo-checksum');
21
-
22
- function saveProgress(systemId, botId, data) {
23
- return leoaws.dynamodb.merge(tableName, botId, {
24
- checksum: data,
25
- system: {
26
- id: systemId
27
- }
28
- });
29
- }
30
-
31
- function stats(array, other) {
32
- array = array || [];
33
-
34
- let ids = array.slice(0).sort(() => {
35
- return 0.5 - Math.random();
36
- }).slice(0, 4);
37
-
38
- if (ids.length < 4 && other && other.length) {
39
- ids = ids.concat(other.slice(-1000).slice(0).sort(() => {
40
- return 0.5 - Math.random();
41
- }).filter(id => ids.indexOf(id) == -1).slice(0, 4 - ids.length));
42
- }
43
-
44
- return {
45
- count: array.length,
46
- ids: ids
47
- };
48
- }
49
-
50
- module.exports = {
51
- configuration: leo.configuration,
52
- getSession: function(systemId, botId, opts) {
53
- opts = Object.assign({
54
- restart: false
55
- }, opts || {});
56
- let emptySession = {
57
- master: {},
58
- slave: {},
59
- sample: {
60
- "missing": [],
61
- "incorrect": [],
62
- "extra": []
63
- },
64
- id: uuid.v4(),
65
- status: 'initializing',
66
- startTime: moment.now(),
67
- totals: {
68
- progress: 0,
69
- correct: 0,
70
- incorrect: 0,
71
- missing: 0,
72
- extra: 0,
73
- streak: 0
74
- }
75
- };
76
- if (opts.restart) {
77
- return saveProgress(systemId, botId, emptySession);
78
- } else {
79
-
80
- logger.log("Getting Session", systemId, botId);
81
-
82
- return leoaws.dynamodb.get(tableName, botId)
83
- .then(result => {
84
- let session = emptySession;
85
- if (result && result.checksum && result.checksum.restart !== true && result.checksum.status !== 'complete') {
86
- session = result.checksum;
87
- }
88
-
89
- return session;
90
- });
91
- }
92
- },
93
- checksum: function(system, botId, master, slave, opts) {
94
- return new Promise((resolve, reject) => {
95
- function logError(err) {
96
- saveProgress(system, botId, {
97
- status: "error",
98
- statusReason: err.toString()
99
- }).then(d => reject(err)).catch(e => reject(err));
100
- }
101
- this.getSession(system, botId, opts).then((session) => {
102
- logger.log("Session:", session);
103
- let tasks = [];
104
- master.setSession(session.master);
105
- slave.setSession(session.slave);
106
-
107
- if (session.status === 'initializing') {
108
- tasks.push(Promise.all([
109
- master.init({}),
110
- slave.init({})
111
- ]).catch(logError));
112
- }
113
- Promise.all(tasks).then((result) => {
114
- if (session.min || session.max) {
115
- opts.min = session.min;
116
- opts.max = session.max;
117
- opts.start = session.start || opts.start;
118
- opts.end = session.end || opts.end;
119
- opts.total = session.total;
120
- master.range = (opts) => {
121
- logger.log("Using Cached Range Value");
122
- return Promise.resolve({
123
- min: opts.min,
124
- max: opts.max,
125
- total: opts.total
126
- });
127
- };
128
- }
129
-
130
- let ids = {
131
- missing: [],
132
- incorrect: [],
133
- extra: [],
134
- map: {}
135
- };
136
- let loopStart = Date.now();
137
- opts.until = function() {
138
- let lastLoopDuration = Date.now() - loopStart;
139
- loopStart = Date.now();
140
- let neededTime = lastLoopDuration * 1.33;
141
-
142
- //logger.log("Check", loopStart + neededTime >= opts.stop_at, loopStart + neededTime, opts.stop_at)
143
- if (loopStart + neededTime >= opts.stop_at) {
144
- return "Out Of Time";
145
- }
146
-
147
- return false;
148
- };
149
- opts.stats = function(nibble, result, total, done) {
150
- let percent = (nibble.progress / nibble.total) * 100;
151
- let fixed = percent.toFixed(2);
152
-
153
- if (fixed === "100.00" && percent < 100) {
154
- fixed = 99.99;
155
- }
156
-
157
- ids.missing = ids.missing.concat(result.missing || []);
158
- ids.incorrect = ids.incorrect.concat(result.incorrect || []);
159
- ids.extra = ids.extra.concat(result.extra || []);
160
- if (result.map) {
161
- Object.keys(result.map).forEach(k => {
162
- ids.map[k] = result.map[k];
163
- });
164
- }
165
-
166
- let data = Object.assign(session, {
167
- endTime: null,
168
- lastUpdate: moment.now(),
169
- status: "running",
170
- statusReason: null,
171
- totals: {
172
- missing: total.totalMissing,
173
- incorrect: total.totalIncorrect,
174
- extra: total.totalExtra,
175
- correct: total.totalCorrect,
176
- progress: nibble.progress,
177
- streak: total.streak
178
- },
179
- log: {
180
- correct: {
181
- count: result.correct
182
- },
183
- missing: stats(result.missing, ids.missing),
184
- incorrect: stats(result.incorrect, ids.incorrect),
185
- extra: stats(result.extra, ids.extra),
186
- percent: fixed
187
- },
188
- start: nibble.start,
189
- end: nibble.end,
190
- min: nibble.min,
191
- max: nibble.max,
192
- next: nibble.next,
193
- total: nibble.total,
194
- reset: null
195
- });
196
- // logger.log(JSON.stringify(data, null, 2));
197
- saveProgress(system, botId, data).then(result => done(null, result), done);
198
- };
199
-
200
- opts.onSample = opts.sample && function(type, diff, done) {
201
- session.sample[type] = diff.concat(session.sample[type]).slice(0, 4);
202
- if (done) {
203
- done();
204
- }
205
- };
206
-
207
- let stream;
208
- if (opts.queue) {
209
- let load = leo.load(botId, opts.queue.name, {
210
- useS3: true,
211
- debug: true
212
- });
213
- if (opts.queue.transform) {
214
- stream = ls.pipeline(opts.queue.transform, load);
215
- } else {
216
- stream = load;
217
- }
218
- }
219
- checksum(master, slave, {
220
- showOutput: opts.showOutput,
221
- totals: {
222
- progress: session.totals.progress,
223
- totalCorrect: session.totals.correct,
224
- totalMissing: session.totals.missing,
225
- totalIncorrect: session.totals.incorrect,
226
- totalExtra: session.totals.extra,
227
- streak: session.totals.streak
228
- }
229
- }).sync(opts, function(result, done) {
230
- if (stream) {
231
- if (!stream.write(Object.assign({}, result))) {
232
- stream.once('drain', done);
233
- } else {
234
- done();
235
- }
236
- } else {
237
- done();
238
- }
239
- }, function(err, data, stopReason) {
240
- let status = err ? ("error") : (stopReason === "Out Of Time" ? "running" : "complete");
241
- let tasks = [];
242
- logger.log('stopReason', stopReason);
243
-
244
- if (status === "complete") {
245
- tasks.push(done => {
246
- master.destroy({
247
- status: status
248
- }).then(result => done(), done);
249
- });
250
- tasks.push(done => {
251
- slave.destroy({
252
- status: status
253
- }).then(result => done(), done);
254
- });
255
- } else if (status === "running") {
256
- cron.runAgain();
257
- }
258
-
259
- let streamEnd = (done) => {
260
- if (!stream) {
261
- return done();
262
- }
263
-
264
- stream.end(done);
265
- };
266
-
267
- streamEnd((err) => {
268
- saveProgress(system, botId,
269
- Object.assign(session, {
270
- endTime: status !== "running" ? moment.now() : null,
271
- lastUpdate: moment.now(),
272
- status: status,
273
- statusReason: err ? err.toString() : stopReason
274
- }))
275
- .then(result => {
276
- async.parallel(tasks, (err, data) => {
277
- if (err) {
278
- reject(err);
279
- } else {
280
- resolve(session);
281
- }
282
- });
283
- }, err => {
284
- reject(err);
285
- });
286
- });
287
- });
288
- }, logError);
289
- }, logError);
290
- });
291
- },
292
- lambdaConnector: function(id, lambdaName, settings) {
293
- let region = (lambdaName.match(/arn:aws:lambda:(.*?):/) || [])[1];
294
- const lambdaInvoker = new Lambda({
295
- region: region || this.configuration._meta.region,
296
- credentials: this.configuration ? this.configuration.credentials : null
297
- });
298
- let qualifier = null;
299
-
300
- let session = null;
301
-
302
- function invoke(method) {
303
- return (data) => {
304
- return new Promise((resolve, reject) => {
305
- logger.debug(id, method, "invoke", data);
306
- lambdaInvoker.invoke({
307
- FunctionName: lambdaName,
308
- InvocationType: 'RequestResponse',
309
- Payload: JSON.stringify({
310
- params: {
311
- querystring: {
312
- method: method
313
- }
314
- },
315
- body: {
316
- data: data,
317
- settings: settings,
318
- session: session
319
- }
320
- }),
321
- Qualifier: qualifier
322
- }, function(err, data) {
323
- logger.debug(id, method, "response", data);
324
- let payload = undefined;
325
- if (!err && data.FunctionError) {
326
- err = data.Payload;
327
- } else if (!err && data.Payload != undefined && data.Payload != 'null') {
328
- let obj = JSON.parse(data.Payload);
329
- if (obj.statusCode == 500) {
330
- err = new Error(obj.body);
331
- } else {
332
- payload = obj.response;
333
- if (obj.session) {
334
- let o = {
335
- id: session.id,
336
- type: session.type
337
- };
338
- Object.assign(session, obj.session, o);
339
- }
340
- }
341
- }
342
- if (err) {
343
- reject(err);
344
- } else {
345
- resolve(payload);
346
- }
347
- });
348
- });
349
- };
350
- }
351
-
352
- return {
353
- id: id,
354
- name: id,
355
- init: invoke("initialize"),
356
- range: invoke("range"),
357
- nibble: invoke("nibble"),
358
- getChecksum: invoke("batch"),
359
- getIndividualChecksums: invoke("individual"),
360
- destroy: invoke("destroy"),
361
- sample: invoke("sample"),
362
- delete: invoke("delete"),
363
- setSession: (s) => {
364
- session = s;
365
- }
366
- };
367
- },
368
- urlConnector: function(settings) {
369
- let requestSettings = settings.request;
370
- delete settings.request;
371
-
372
- let urlMethod = (method) => `${settings.url}?method=${method}`;
373
- if (typeof settings.url === "function") {
374
- urlMethod = settings.url;
375
- }
376
- let postProcessResponse = (method, postBody, data) => JSON.parse(data);
377
- if (settings.postProcessResponse) {
378
- postProcessResponse = settings.postProcessResponse;
379
- }
380
-
381
- let session = null;
382
-
383
- function invoke(method) {
384
- return (data) => {
385
- let http = httpObj;
386
- let url = urlMethod(method, data);
387
- if (url instanceof Error) {
388
- return Promise.reject(url);
389
- }
390
-
391
- if (typeof url !== "string") {
392
- return Promise.resolve(postProcessResponse(method, {
393
- data: url,
394
- session: session,
395
- settings: settings
396
- }, url));
397
- }
398
-
399
- if (url.match(/^https/)) {
400
- http = httpsObj;
401
- }
402
- return new Promise((resolve, reject) => {
403
- //logger.log("URL:", url)
404
- let requestOptions = Object.assign(URL.parse(url), {
405
- method: 'POST',
406
- headers: {
407
- 'Content-Type': 'application/json',
408
- }
409
- }, requestSettings);
410
-
411
- let postBody = {
412
- data: data,
413
- settings: settings,
414
- session: session
415
- };
416
-
417
- let req = http.request(requestOptions, function(res) {
418
- res.setEncoding('utf8');
419
- let data = '';
420
- res.on('data', function(chunk) {
421
- data += chunk;
422
- });
423
- res.on('end', function() {
424
- let payload = undefined;
425
- if (data) {
426
- let obj = postProcessResponse(method, postBody, data);
427
- payload = obj.response;
428
- if (payload === undefined) {
429
- payload = obj;
430
- }
431
- if (obj.session) {
432
- let o = {
433
- id: session.id,
434
- type: session.type
435
- };
436
- Object.assign(session, obj.session, o);
437
- }
438
- }
439
- resolve(payload);
440
- });
441
- });
442
- req.on('error', function(e) {
443
- logger.error('problem with request: ' + e.message);
444
- reject(e);
445
- });
446
-
447
- if (requestOptions.method !== "GET")
448
- // write data to request body
449
- req.write(JSON.stringify(postBody));
450
- req.end();
451
- });
452
- };
453
- }
454
-
455
- function empty() {
456
- return (data) => {
457
- return Promise.resolve({});
458
- };
459
- }
460
-
461
- return {
462
- id: settings.id,
463
- name: settings.name || settings.id,
464
- init: settings.initialize === false ? empty() : invoke("initialize"),
465
- range: invoke("range"),
466
- nibble: invoke("nibble"),
467
- getChecksum: invoke("batch"),
468
- getIndividualChecksums: invoke("individual"),
469
- destroy: invoke("destroy"),
470
- sample: invoke("sample"),
471
- delete: invoke("delete"),
472
- setSession: (s) => {
473
- session = s;
474
- }
475
- };
476
- },
477
- mockConnector: function(settings) {
478
- return function(data, callback) {
479
- let rand = Object.assign({
480
- batch: 10,
481
- single: 10,
482
- sample: 10
483
- }, settings.mock.random);
484
-
485
- callback(null, {
486
- session: {
487
- id: Math.random() * 10000000000
488
- },
489
- name: settings.name || settings.url,
490
- id: settings.id,
491
- destroy: function(data, done) {
492
- if (!done && typeof data === "function") {
493
- done = data;
494
- data = {};
495
- }
496
- done();
497
- },
498
- getChecksum: function(data, callback) {
499
- logger.log(" BATCH", settings.name, data, rand.batch);
500
- callback(null, {
501
- qty: data.end - data.start + 1,
502
- hash: [1, 2, 3, Math.round(Math.random() * rand.batch)],
503
- duration: 1000
504
- });
505
- },
506
- getIndividualChecksums: function(data, callback) {
507
- let result = {
508
- qty: 0,
509
- start: data.start,
510
- end: data.end,
511
- checksums: []
512
- };
513
-
514
- for (let i = data.start; i <= data.end; i++) {
515
- result.qty++;
516
- result.checksums.push({
517
- id: i,
518
- hash: "1-2-3-" + (Math.round(Math.random() * rand.single))
519
- });
520
- }
521
- logger.log(" INDIVIDUAL", settings.name, data);
522
- callback(null, result);
523
- },
524
- sample: function(data, callback) {
525
- logger.log(" SAMPLE", settings.name, data);
526
- let result = {
527
- qty: 0,
528
- ids: [],
529
- start: data.start,
530
- end: data.end,
531
- checksums: []
532
- };
533
-
534
- data.ids.forEach(id => {
535
- result.ids.push(id);
536
- result.checksums.push([1, 2, Math.round(Math.random() * rand.sample), Math.round(Math.random() * rand.sample)]);
537
- result.qty++;
538
- });
539
-
540
- callback(null, result);
541
- },
542
- range: function(data, callback) {
543
- var result = {
544
- min: settings.mock.min,
545
- max: settings.mock.max,
546
- total: settings.mock.max - settings.mock.min + 1
547
- };
548
- logger.log(" RANGE", settings.name, data, result);
549
- callback(null, result);
550
- },
551
- nibble: function(data, callback) {
552
- setTimeout(function() {
553
- logger.log(" NIBBLE", settings.name, data);
554
- data.end = Math.min(data.start + data.limit - 1, settings.mock.max);
555
- data.next = data.start + data.limit < settings.mock.max ? data.start + data.limit : null;
556
- callback(null, data);
557
- }, settings.mock.timeout || 2000);
558
-
559
- }
560
- });
561
- };
562
- },
563
- fileConnector: function(id, file, settings) {
564
- let reverse = settings.reverse ? -1 : 1;
565
- let db = require(file).sort((a, b) => {
566
- return a[settings.id_column] > b[settings.id_column] ? 1 : -1;
567
- });
568
- let session = null;
569
-
570
- function invoke(method, func) {
571
- func = func || ((d) => { });
572
- return (data) => {
573
- return new Promise((resolve, reject) => {
574
- //logger.log(method, data)
575
- try {
576
- let result = func(data);
577
- //logger.log(`${id} ${method}: ${JSON.stringify(result)}`);
578
- resolve(result);
579
- } catch (err) {
580
- logger.log(`${id} ${method} Error: ${err}`);
581
- reject(err);
582
- }
583
- });
584
- };
585
- }
586
-
587
- return {
588
- id: id,
589
- name: id,
590
- init: invoke("initialize", () => {
591
- return {};
592
- }),
593
- range: invoke("range", (data) => {
594
- return {
595
- min: db[0].id,
596
- max: db[db.length - 1].id,
597
- total: db.length
598
- };
599
- }),
600
- nibble: invoke("nibble", (data) => {
601
- let set = db.filter(a => a[settings.id_column] >= data.start && a[settings.id_column] <= data.end);
602
- if (reverse) {
603
- set = set.reverse();
604
- }
605
- data.current = set[data.limit - 1] ? set[data.limit - 1][settings.id_column] : null;
606
- data.next = set[data.limit] ? set[data.limit][settings.id_column] : null;
607
- return data;
608
- }),
609
- getChecksum: invoke("batch", (data) => {
610
- let result = {
611
- qty: 0,
612
- ids: data.ids,
613
- start: data.start,
614
- end: data.end,
615
- hash: [0, 0, 0, 0]
616
- };
617
-
618
- let set = db.filter(a => a[settings.id_column] >= data.start && a[settings.id_column] <= data.end);
619
-
620
- if (reverse) {
621
- set = set.reverse();
622
- }
623
-
624
- let extract = (obj) => {
625
- return settings.fields.map(f => obj[f]);
626
- };
627
-
628
- set.map(obj => {
629
- let allFields = "";
630
- extract(obj).forEach(value => {
631
- if (value instanceof Date) {
632
- allFields += crypto.createHash('md5').update(Math.round(value.getTime() / 1000).toString()).digest('hex');
633
- } else if (value !== null && value !== undefined && value.toString) {
634
- allFields += crypto.createHash('md5').update(value.toString()).digest('hex');
635
- } else {
636
- allFields += " ";
637
- }
638
- });
639
- let hash = crypto.createHash('md5').update(allFields).digest();
640
-
641
- result.hash[0] += hash.readUInt32BE(0);
642
- result.hash[1] += hash.readUInt32BE(4);
643
- result.hash[2] += hash.readUInt32BE(8);
644
- result.hash[3] += hash.readUInt32BE(12);
645
- result.qty += 1;
646
- });
647
-
648
- return result;
649
- }),
650
- getIndividualChecksums: invoke("individual", (data) => {
651
- let set = db.filter(a => a[settings.id_column] >= data.start && a[settings.id_column] <= data.end);
652
- if (reverse) {
653
- set = set.reverse();
654
- }
655
-
656
- let extract = (obj) => {
657
- return settings.fields.map(f => obj[f]);
658
- };
659
-
660
- let results = {
661
- ids: data.ids,
662
- start: data.start,
663
- end: data.end,
664
- qty: 0,
665
- checksums: []
666
- };
667
-
668
- set.map(obj => {
669
- let allFields = "";
670
-
671
- extract(obj).forEach(value => {
672
- if (value instanceof Date) {
673
- allFields += crypto.createHash('md5').update(Math.round(value.getTime() / 1000).toString()).digest('hex');
674
- } else if (value !== null && value !== undefined && value.toString) {
675
- allFields += crypto.createHash('md5').update(value.toString()).digest('hex');
676
- } else {
677
- allFields += " ";
678
- }
679
- });
680
-
681
- results.checksums.push({
682
- id: obj[settings.id_column],
683
- _id: settings._id_column ? obj[settings._id_column] : undefined,
684
- hash: crypto.createHash('md5').update(allFields).digest('hex')
685
- });
686
- results.qty += 1;
687
- });
688
- return results;
689
- }),
690
- destroy: invoke("destroy"),
691
- sample: invoke("sample", (data) => {
692
- let lookup = {};
693
- data.ids.map(i => lookup[i] = true);
694
- let set = db.filter(a => lookup[a[settings.id_column]]);
695
-
696
- let results = {
697
- qty: 0,
698
- ids: [],
699
- checksums: []
700
- };
701
-
702
- let extract = (obj) => {
703
- return settings.fields.map(f => obj[f]);
704
- };
705
-
706
- set.map(obj => {
707
- let out = [obj[settings.id_column]];
708
- extract(obj).forEach(value => {
709
- if (value instanceof Date) {
710
- out.push(Math.round(value.getTime() / 1000) + " " + moment(value).utc().format());
711
- } else if (value && typeof value === "object" && value.toHexString) {
712
- out.push(value.toString());
713
- } else {
714
- out.push(value);
715
- }
716
- });
717
-
718
- results.ids.push(obj[settings.id_column]);
719
- results.checksums.push(out);
720
- results.qty += 1;
721
- });
722
- return results;
723
-
724
- }),
725
- delete: invoke("delete"),
726
- setSession: (s) => {
727
- session = s;
728
- }
729
- };
730
- },
731
- basicConnector: function(id, settings, handlers) {
732
- let basicConnector = require("./lib/basicConnector")(handlers);
733
-
734
- let session = null;
735
-
736
- function invoke(method) {
737
- return (data) => {
738
- return new Promise((resolve, reject) => {
739
- basicConnector.handler({
740
- params: {
741
- querystring: {
742
- method: method
743
- }
744
- },
745
- body: {
746
- data: data,
747
- settings: settings,
748
- session: session
749
- }
750
- }, {}, function(err, data) {
751
- let payload = undefined;
752
- if (!err && data.response != undefined) {
753
- let obj = typeof data === "string" ? JSON.parse(data) : data;
754
- if (obj.statusCode == 500) {
755
- err = new Error(obj.body || obj.response);
756
- } else {
757
- payload = obj.body || obj.response;
758
- if (obj.session) {
759
- let o = {
760
- id: session.id,
761
- type: session.type
762
- };
763
- Object.assign(session, obj.session, o);
764
- }
765
- }
766
- }
767
-
768
- if (err) {
769
- reject(err);
770
- } else {
771
- resolve(payload);
772
- }
773
- });
774
- });
775
- };
776
- }
777
- return {
778
- id: id,
779
- name: id,
780
- init: invoke("initialize"),
781
- range: invoke("range"),
782
- nibble: invoke("nibble"),
783
- getChecksum: invoke("batch"),
784
- getIndividualChecksums: invoke("individual"),
785
- destroy: invoke("destroy"),
786
- sample: invoke("sample"),
787
- delete: invoke("delete"),
788
- setSession: (s) => {
789
- session = s;
790
- }
791
- };
792
- }
793
- };
1
+ let leo = require("leo-sdk");
2
+ const { Lambda } = require("@aws-sdk/client-lambda");
3
+ const checksum = require("./lib/checksumNibbler.js");
4
+ const leoaws = require('leo-aws');
5
+ let cron = leo.bot;
6
+
7
+ /** These are for file connector **/
8
+ let httpsObj = require("https");
9
+ let httpObj = require("http");
10
+ let URL = require("url");
11
+ const crypto = require('crypto');
12
+
13
+ const moment = require("moment");
14
+ const uuid = require("uuid");
15
+ const ls = leo.streams;
16
+
17
+ const async = require("async");
18
+
19
+ const tableName = leo.configuration.resources.LeoCron;
20
+ let logger = require('leo-logger')('leo-checksum');
21
+
22
+ function saveProgress(systemId, botId, data) {
23
+ return leoaws.dynamodb.merge(tableName, botId, {
24
+ checksum: data,
25
+ system: {
26
+ id: systemId
27
+ }
28
+ });
29
+ }
30
+
31
+ function stats(array, other) {
32
+ array = array || [];
33
+
34
+ let ids = array.slice(0).sort(() => {
35
+ return 0.5 - Math.random();
36
+ }).slice(0, 4);
37
+
38
+ if (ids.length < 4 && other && other.length) {
39
+ ids = ids.concat(other.slice(-1000).slice(0).sort(() => {
40
+ return 0.5 - Math.random();
41
+ }).filter(id => ids.indexOf(id) == -1).slice(0, 4 - ids.length));
42
+ }
43
+
44
+ return {
45
+ count: array.length,
46
+ ids: ids
47
+ };
48
+ }
49
+
50
+ module.exports = {
51
+ configuration: leo.configuration,
52
+ getSession: function(systemId, botId, opts) {
53
+ opts = Object.assign({
54
+ restart: false
55
+ }, opts || {});
56
+ let emptySession = {
57
+ master: {},
58
+ slave: {},
59
+ sample: {
60
+ "missing": [],
61
+ "incorrect": [],
62
+ "extra": []
63
+ },
64
+ id: uuid.v4(),
65
+ status: 'initializing',
66
+ startTime: moment.now(),
67
+ totals: {
68
+ progress: 0,
69
+ correct: 0,
70
+ incorrect: 0,
71
+ missing: 0,
72
+ extra: 0,
73
+ streak: 0
74
+ }
75
+ };
76
+ if (opts.restart) {
77
+ return saveProgress(systemId, botId, emptySession);
78
+ } else {
79
+
80
+ logger.log("Getting Session", systemId, botId);
81
+
82
+ return leoaws.dynamodb.get(tableName, botId)
83
+ .then(result => {
84
+ let session = emptySession;
85
+ if (result && result.checksum && result.checksum.restart !== true && result.checksum.status !== 'complete') {
86
+ session = result.checksum;
87
+ }
88
+
89
+ return session;
90
+ });
91
+ }
92
+ },
93
+ checksum: function(system, botId, master, slave, opts) {
94
+ return new Promise((resolve, reject) => {
95
+ function logError(err) {
96
+ saveProgress(system, botId, {
97
+ status: "error",
98
+ statusReason: err.toString()
99
+ }).then(d => reject(err)).catch(e => reject(err));
100
+ }
101
+ this.getSession(system, botId, opts).then((session) => {
102
+ logger.log("Session:", session);
103
+ let tasks = [];
104
+ master.setSession(session.master);
105
+ slave.setSession(session.slave);
106
+
107
+ if (session.status === 'initializing') {
108
+ tasks.push(Promise.all([
109
+ master.init({}),
110
+ slave.init({})
111
+ ]).catch(logError));
112
+ }
113
+ Promise.all(tasks).then((result) => {
114
+ if (session.min || session.max) {
115
+ opts.min = session.min;
116
+ opts.max = session.max;
117
+ opts.start = session.start || opts.start;
118
+ opts.end = session.end || opts.end;
119
+ opts.total = session.total;
120
+ master.range = (opts) => {
121
+ logger.log("Using Cached Range Value");
122
+ return Promise.resolve({
123
+ min: opts.min,
124
+ max: opts.max,
125
+ total: opts.total
126
+ });
127
+ };
128
+ }
129
+
130
+ let ids = {
131
+ missing: [],
132
+ incorrect: [],
133
+ extra: [],
134
+ map: {}
135
+ };
136
+ let loopStart = Date.now();
137
+ opts.until = function() {
138
+ let lastLoopDuration = Date.now() - loopStart;
139
+ loopStart = Date.now();
140
+ let neededTime = lastLoopDuration * 1.33;
141
+
142
+ //logger.log("Check", loopStart + neededTime >= opts.stop_at, loopStart + neededTime, opts.stop_at)
143
+ if (loopStart + neededTime >= opts.stop_at) {
144
+ return "Out Of Time";
145
+ }
146
+
147
+ return false;
148
+ };
149
+ opts.stats = function(nibble, result, total, done) {
150
+ let percent = (nibble.progress / nibble.total) * 100;
151
+ let fixed = percent.toFixed(2);
152
+
153
+ if (fixed === "100.00" && percent < 100) {
154
+ fixed = 99.99;
155
+ }
156
+
157
+ ids.missing = ids.missing.concat(result.missing || []);
158
+ ids.incorrect = ids.incorrect.concat(result.incorrect || []);
159
+ ids.extra = ids.extra.concat(result.extra || []);
160
+ if (result.map) {
161
+ Object.keys(result.map).forEach(k => {
162
+ ids.map[k] = result.map[k];
163
+ });
164
+ }
165
+
166
+ let data = Object.assign(session, {
167
+ endTime: null,
168
+ lastUpdate: moment.now(),
169
+ status: "running",
170
+ statusReason: null,
171
+ totals: {
172
+ missing: total.totalMissing,
173
+ incorrect: total.totalIncorrect,
174
+ extra: total.totalExtra,
175
+ correct: total.totalCorrect,
176
+ progress: nibble.progress,
177
+ streak: total.streak
178
+ },
179
+ log: {
180
+ correct: {
181
+ count: result.correct
182
+ },
183
+ missing: stats(result.missing, ids.missing),
184
+ incorrect: stats(result.incorrect, ids.incorrect),
185
+ extra: stats(result.extra, ids.extra),
186
+ percent: fixed
187
+ },
188
+ start: nibble.start,
189
+ end: nibble.end,
190
+ min: nibble.min,
191
+ max: nibble.max,
192
+ next: nibble.next,
193
+ total: nibble.total,
194
+ reset: null
195
+ });
196
+ // logger.log(JSON.stringify(data, null, 2));
197
+ saveProgress(system, botId, data).then(result => done(null, result), done);
198
+ };
199
+
200
+ opts.onSample = opts.sample && function(type, diff, done) {
201
+ session.sample[type] = diff.concat(session.sample[type]).slice(0, 4);
202
+ if (done) {
203
+ done();
204
+ }
205
+ };
206
+
207
+ let stream;
208
+ if (opts.queue) {
209
+ let load = leo.load(botId, opts.queue.name, {
210
+ useS3: true,
211
+ debug: true
212
+ });
213
+ if (opts.queue.transform) {
214
+ stream = ls.pipeline(opts.queue.transform, load);
215
+ } else {
216
+ stream = load;
217
+ }
218
+ }
219
+ checksum(master, slave, {
220
+ showOutput: opts.showOutput,
221
+ totals: {
222
+ progress: session.totals.progress,
223
+ totalCorrect: session.totals.correct,
224
+ totalMissing: session.totals.missing,
225
+ totalIncorrect: session.totals.incorrect,
226
+ totalExtra: session.totals.extra,
227
+ streak: session.totals.streak
228
+ }
229
+ }).sync(opts, function(result, done) {
230
+ if (stream) {
231
+ if (!stream.write(Object.assign({}, result))) {
232
+ stream.once('drain', done);
233
+ } else {
234
+ done();
235
+ }
236
+ } else {
237
+ done();
238
+ }
239
+ }, function(err, data, stopReason) {
240
+ let status = err ? ("error") : (stopReason === "Out Of Time" ? "running" : "complete");
241
+ let tasks = [];
242
+ logger.log('stopReason', stopReason);
243
+
244
+ if (status === "complete") {
245
+ tasks.push(done => {
246
+ master.destroy({
247
+ status: status
248
+ }).then(result => done(), done);
249
+ });
250
+ tasks.push(done => {
251
+ slave.destroy({
252
+ status: status
253
+ }).then(result => done(), done);
254
+ });
255
+ } else if (status === "running") {
256
+ cron.runAgain();
257
+ }
258
+
259
+ let streamEnd = (done) => {
260
+ if (!stream) {
261
+ return done();
262
+ }
263
+
264
+ stream.end(done);
265
+ };
266
+
267
+ streamEnd((err) => {
268
+ saveProgress(system, botId,
269
+ Object.assign(session, {
270
+ endTime: status !== "running" ? moment.now() : null,
271
+ lastUpdate: moment.now(),
272
+ status: status,
273
+ statusReason: err ? err.toString() : stopReason
274
+ }))
275
+ .then(result => {
276
+ async.parallel(tasks, (err, data) => {
277
+ if (err) {
278
+ reject(err);
279
+ } else {
280
+ resolve(session);
281
+ }
282
+ });
283
+ }, err => {
284
+ reject(err);
285
+ });
286
+ });
287
+ });
288
+ }, logError);
289
+ }, logError);
290
+ });
291
+ },
292
+ lambdaConnector: function(id, lambdaName, settings) {
293
+ let region = (lambdaName.match(/arn:aws:lambda:(.*?):/) || [])[1];
294
+ const lambdaInvoker = new Lambda({
295
+ region: region || this.configuration._meta.region,
296
+ credentials: this.configuration ? this.configuration.credentials : null
297
+ });
298
+ let qualifier = null;
299
+
300
+ let session = null;
301
+
302
+ function invoke(method) {
303
+ return (data) => {
304
+ return new Promise((resolve, reject) => {
305
+ logger.debug(id, method, "invoke", data);
306
+ lambdaInvoker.invoke({
307
+ FunctionName: lambdaName,
308
+ InvocationType: 'RequestResponse',
309
+ Payload: JSON.stringify({
310
+ params: {
311
+ querystring: {
312
+ method: method
313
+ }
314
+ },
315
+ body: {
316
+ data: data,
317
+ settings: settings,
318
+ session: session
319
+ }
320
+ }),
321
+ Qualifier: qualifier
322
+ }, function(err, data) {
323
+ logger.debug(id, method, "response", data);
324
+ let payload = undefined;
325
+ if (!err && data.FunctionError) {
326
+ err = data.Payload;
327
+ } else if (!err && data.Payload != undefined && data.Payload != 'null') {
328
+ let obj = JSON.parse(data.Payload);
329
+ if (obj.statusCode == 500) {
330
+ err = new Error(obj.body);
331
+ } else {
332
+ payload = obj.response;
333
+ if (obj.session) {
334
+ let o = {
335
+ id: session.id,
336
+ type: session.type
337
+ };
338
+ Object.assign(session, obj.session, o);
339
+ }
340
+ }
341
+ }
342
+ if (err) {
343
+ reject(err);
344
+ } else {
345
+ resolve(payload);
346
+ }
347
+ });
348
+ });
349
+ };
350
+ }
351
+
352
+ return {
353
+ id: id,
354
+ name: id,
355
+ init: invoke("initialize"),
356
+ range: invoke("range"),
357
+ nibble: invoke("nibble"),
358
+ getChecksum: invoke("batch"),
359
+ getIndividualChecksums: invoke("individual"),
360
+ destroy: invoke("destroy"),
361
+ sample: invoke("sample"),
362
+ delete: invoke("delete"),
363
+ setSession: (s) => {
364
+ session = s;
365
+ }
366
+ };
367
+ },
368
+ urlConnector: function(settings) {
369
+ let requestSettings = settings.request;
370
+ delete settings.request;
371
+
372
+ let urlMethod = (method) => `${settings.url}?method=${method}`;
373
+ if (typeof settings.url === "function") {
374
+ urlMethod = settings.url;
375
+ }
376
+ let postProcessResponse = (method, postBody, data) => JSON.parse(data);
377
+ if (settings.postProcessResponse) {
378
+ postProcessResponse = settings.postProcessResponse;
379
+ }
380
+
381
+ let session = null;
382
+
383
+ function invoke(method) {
384
+ return (data) => {
385
+ let http = httpObj;
386
+ let url = urlMethod(method, data);
387
+ if (url instanceof Error) {
388
+ return Promise.reject(url);
389
+ }
390
+
391
+ if (typeof url !== "string") {
392
+ return Promise.resolve(postProcessResponse(method, {
393
+ data: url,
394
+ session: session,
395
+ settings: settings
396
+ }, url));
397
+ }
398
+
399
+ if (url.match(/^https/)) {
400
+ http = httpsObj;
401
+ }
402
+ return new Promise((resolve, reject) => {
403
+ //logger.log("URL:", url)
404
+ let requestOptions = Object.assign(URL.parse(url), {
405
+ method: 'POST',
406
+ headers: {
407
+ 'Content-Type': 'application/json',
408
+ }
409
+ }, requestSettings);
410
+
411
+ let postBody = {
412
+ data: data,
413
+ settings: settings,
414
+ session: session
415
+ };
416
+
417
+ let req = http.request(requestOptions, function(res) {
418
+ res.setEncoding('utf8');
419
+ let data = '';
420
+ res.on('data', function(chunk) {
421
+ data += chunk;
422
+ });
423
+ res.on('end', function() {
424
+ let payload = undefined;
425
+ if (data) {
426
+ let obj = postProcessResponse(method, postBody, data);
427
+ payload = obj.response;
428
+ if (payload === undefined) {
429
+ payload = obj;
430
+ }
431
+ if (obj.session) {
432
+ let o = {
433
+ id: session.id,
434
+ type: session.type
435
+ };
436
+ Object.assign(session, obj.session, o);
437
+ }
438
+ }
439
+ resolve(payload);
440
+ });
441
+ });
442
+ req.on('error', function(e) {
443
+ logger.error('problem with request: ' + e.message);
444
+ reject(e);
445
+ });
446
+
447
+ if (requestOptions.method !== "GET")
448
+ // write data to request body
449
+ req.write(JSON.stringify(postBody));
450
+ req.end();
451
+ });
452
+ };
453
+ }
454
+
455
+ function empty() {
456
+ return (data) => {
457
+ return Promise.resolve({});
458
+ };
459
+ }
460
+
461
+ return {
462
+ id: settings.id,
463
+ name: settings.name || settings.id,
464
+ init: settings.initialize === false ? empty() : invoke("initialize"),
465
+ range: invoke("range"),
466
+ nibble: invoke("nibble"),
467
+ getChecksum: invoke("batch"),
468
+ getIndividualChecksums: invoke("individual"),
469
+ destroy: invoke("destroy"),
470
+ sample: invoke("sample"),
471
+ delete: invoke("delete"),
472
+ setSession: (s) => {
473
+ session = s;
474
+ }
475
+ };
476
+ },
477
+ mockConnector: function(settings) {
478
+ return function(data, callback) {
479
+ let rand = Object.assign({
480
+ batch: 10,
481
+ single: 10,
482
+ sample: 10
483
+ }, settings.mock.random);
484
+
485
+ callback(null, {
486
+ session: {
487
+ id: Math.random() * 10000000000
488
+ },
489
+ name: settings.name || settings.url,
490
+ id: settings.id,
491
+ destroy: function(data, done) {
492
+ if (!done && typeof data === "function") {
493
+ done = data;
494
+ data = {};
495
+ }
496
+ done();
497
+ },
498
+ getChecksum: function(data, callback) {
499
+ logger.log(" BATCH", settings.name, data, rand.batch);
500
+ callback(null, {
501
+ qty: data.end - data.start + 1,
502
+ hash: [1, 2, 3, Math.round(Math.random() * rand.batch)],
503
+ duration: 1000
504
+ });
505
+ },
506
+ getIndividualChecksums: function(data, callback) {
507
+ let result = {
508
+ qty: 0,
509
+ start: data.start,
510
+ end: data.end,
511
+ checksums: []
512
+ };
513
+
514
+ for (let i = data.start; i <= data.end; i++) {
515
+ result.qty++;
516
+ result.checksums.push({
517
+ id: i,
518
+ hash: "1-2-3-" + (Math.round(Math.random() * rand.single))
519
+ });
520
+ }
521
+ logger.log(" INDIVIDUAL", settings.name, data);
522
+ callback(null, result);
523
+ },
524
+ sample: function(data, callback) {
525
+ logger.log(" SAMPLE", settings.name, data);
526
+ let result = {
527
+ qty: 0,
528
+ ids: [],
529
+ start: data.start,
530
+ end: data.end,
531
+ checksums: []
532
+ };
533
+
534
+ data.ids.forEach(id => {
535
+ result.ids.push(id);
536
+ result.checksums.push([1, 2, Math.round(Math.random() * rand.sample), Math.round(Math.random() * rand.sample)]);
537
+ result.qty++;
538
+ });
539
+
540
+ callback(null, result);
541
+ },
542
+ range: function(data, callback) {
543
+ var result = {
544
+ min: settings.mock.min,
545
+ max: settings.mock.max,
546
+ total: settings.mock.max - settings.mock.min + 1
547
+ };
548
+ logger.log(" RANGE", settings.name, data, result);
549
+ callback(null, result);
550
+ },
551
+ nibble: function(data, callback) {
552
+ setTimeout(function() {
553
+ logger.log(" NIBBLE", settings.name, data);
554
+ data.end = Math.min(data.start + data.limit - 1, settings.mock.max);
555
+ data.next = data.start + data.limit < settings.mock.max ? data.start + data.limit : null;
556
+ callback(null, data);
557
+ }, settings.mock.timeout || 2000);
558
+
559
+ }
560
+ });
561
+ };
562
+ },
563
+ fileConnector: function(id, file, settings) {
564
+ let reverse = settings.reverse ? -1 : 1;
565
+ let db = require(file).sort((a, b) => {
566
+ return a[settings.id_column] > b[settings.id_column] ? 1 : -1;
567
+ });
568
+ let session = null;
569
+
570
+ function invoke(method, func) {
571
+ func = func || ((d) => { });
572
+ return (data) => {
573
+ return new Promise((resolve, reject) => {
574
+ //logger.log(method, data)
575
+ try {
576
+ let result = func(data);
577
+ //logger.log(`${id} ${method}: ${JSON.stringify(result)}`);
578
+ resolve(result);
579
+ } catch (err) {
580
+ logger.log(`${id} ${method} Error: ${err}`);
581
+ reject(err);
582
+ }
583
+ });
584
+ };
585
+ }
586
+
587
+ return {
588
+ id: id,
589
+ name: id,
590
+ init: invoke("initialize", () => {
591
+ return {};
592
+ }),
593
+ range: invoke("range", (data) => {
594
+ return {
595
+ min: db[0].id,
596
+ max: db[db.length - 1].id,
597
+ total: db.length
598
+ };
599
+ }),
600
+ nibble: invoke("nibble", (data) => {
601
+ let set = db.filter(a => a[settings.id_column] >= data.start && a[settings.id_column] <= data.end);
602
+ if (reverse) {
603
+ set = set.reverse();
604
+ }
605
+ data.current = set[data.limit - 1] ? set[data.limit - 1][settings.id_column] : null;
606
+ data.next = set[data.limit] ? set[data.limit][settings.id_column] : null;
607
+ return data;
608
+ }),
609
+ getChecksum: invoke("batch", (data) => {
610
+ let result = {
611
+ qty: 0,
612
+ ids: data.ids,
613
+ start: data.start,
614
+ end: data.end,
615
+ hash: [0, 0, 0, 0]
616
+ };
617
+
618
+ let set = db.filter(a => a[settings.id_column] >= data.start && a[settings.id_column] <= data.end);
619
+
620
+ if (reverse) {
621
+ set = set.reverse();
622
+ }
623
+
624
+ let extract = (obj) => {
625
+ return settings.fields.map(f => obj[f]);
626
+ };
627
+
628
+ set.map(obj => {
629
+ let allFields = "";
630
+ extract(obj).forEach(value => {
631
+ if (value instanceof Date) {
632
+ allFields += crypto.createHash('md5').update(Math.round(value.getTime() / 1000).toString()).digest('hex');
633
+ } else if (value !== null && value !== undefined && value.toString) {
634
+ allFields += crypto.createHash('md5').update(value.toString()).digest('hex');
635
+ } else {
636
+ allFields += " ";
637
+ }
638
+ });
639
+ let hash = crypto.createHash('md5').update(allFields).digest();
640
+
641
+ result.hash[0] += hash.readUInt32BE(0);
642
+ result.hash[1] += hash.readUInt32BE(4);
643
+ result.hash[2] += hash.readUInt32BE(8);
644
+ result.hash[3] += hash.readUInt32BE(12);
645
+ result.qty += 1;
646
+ });
647
+
648
+ return result;
649
+ }),
650
+ getIndividualChecksums: invoke("individual", (data) => {
651
+ let set = db.filter(a => a[settings.id_column] >= data.start && a[settings.id_column] <= data.end);
652
+ if (reverse) {
653
+ set = set.reverse();
654
+ }
655
+
656
+ let extract = (obj) => {
657
+ return settings.fields.map(f => obj[f]);
658
+ };
659
+
660
+ let results = {
661
+ ids: data.ids,
662
+ start: data.start,
663
+ end: data.end,
664
+ qty: 0,
665
+ checksums: []
666
+ };
667
+
668
+ set.map(obj => {
669
+ let allFields = "";
670
+
671
+ extract(obj).forEach(value => {
672
+ if (value instanceof Date) {
673
+ allFields += crypto.createHash('md5').update(Math.round(value.getTime() / 1000).toString()).digest('hex');
674
+ } else if (value !== null && value !== undefined && value.toString) {
675
+ allFields += crypto.createHash('md5').update(value.toString()).digest('hex');
676
+ } else {
677
+ allFields += " ";
678
+ }
679
+ });
680
+
681
+ results.checksums.push({
682
+ id: obj[settings.id_column],
683
+ _id: settings._id_column ? obj[settings._id_column] : undefined,
684
+ hash: crypto.createHash('md5').update(allFields).digest('hex')
685
+ });
686
+ results.qty += 1;
687
+ });
688
+ return results;
689
+ }),
690
+ destroy: invoke("destroy"),
691
+ sample: invoke("sample", (data) => {
692
+ let lookup = {};
693
+ data.ids.map(i => lookup[i] = true);
694
+ let set = db.filter(a => lookup[a[settings.id_column]]);
695
+
696
+ let results = {
697
+ qty: 0,
698
+ ids: [],
699
+ checksums: []
700
+ };
701
+
702
+ let extract = (obj) => {
703
+ return settings.fields.map(f => obj[f]);
704
+ };
705
+
706
+ set.map(obj => {
707
+ let out = [obj[settings.id_column]];
708
+ extract(obj).forEach(value => {
709
+ if (value instanceof Date) {
710
+ out.push(Math.round(value.getTime() / 1000) + " " + moment(value).utc().format());
711
+ } else if (value && typeof value === "object" && value.toHexString) {
712
+ out.push(value.toString());
713
+ } else {
714
+ out.push(value);
715
+ }
716
+ });
717
+
718
+ results.ids.push(obj[settings.id_column]);
719
+ results.checksums.push(out);
720
+ results.qty += 1;
721
+ });
722
+ return results;
723
+
724
+ }),
725
+ delete: invoke("delete"),
726
+ setSession: (s) => {
727
+ session = s;
728
+ }
729
+ };
730
+ },
731
+ basicConnector: function(id, settings, handlers) {
732
+ let basicConnector = require("./lib/basicConnector")(handlers);
733
+
734
+ let session = null;
735
+
736
+ function invoke(method) {
737
+ return (data) => {
738
+ return new Promise((resolve, reject) => {
739
+ basicConnector.handler({
740
+ params: {
741
+ querystring: {
742
+ method: method
743
+ }
744
+ },
745
+ body: {
746
+ data: data,
747
+ settings: settings,
748
+ session: session
749
+ }
750
+ }, {}, function(err, data) {
751
+ let payload = undefined;
752
+ if (!err && data.response != undefined) {
753
+ let obj = typeof data === "string" ? JSON.parse(data) : data;
754
+ if (obj.statusCode == 500) {
755
+ err = new Error(obj.body || obj.response);
756
+ } else {
757
+ payload = obj.body || obj.response;
758
+ if (obj.session) {
759
+ let o = {
760
+ id: session.id,
761
+ type: session.type
762
+ };
763
+ Object.assign(session, obj.session, o);
764
+ }
765
+ }
766
+ }
767
+
768
+ if (err) {
769
+ reject(err);
770
+ } else {
771
+ resolve(payload);
772
+ }
773
+ });
774
+ });
775
+ };
776
+ }
777
+ return {
778
+ id: id,
779
+ name: id,
780
+ init: invoke("initialize"),
781
+ range: invoke("range"),
782
+ nibble: invoke("nibble"),
783
+ getChecksum: invoke("batch"),
784
+ getIndividualChecksums: invoke("individual"),
785
+ destroy: invoke("destroy"),
786
+ sample: invoke("sample"),
787
+ delete: invoke("delete"),
788
+ setSession: (s) => {
789
+ session = s;
790
+ }
791
+ };
792
+ }
793
+ };