meadow-integration 1.0.15 → 1.0.17
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meadow-integration",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"description": "Meadow Data Integration",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mdwint": "source/cli/Meadow-Integration-CLI-Run.js"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"author": "steven velozo <steven@velozo.com>",
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"quackage": "^1.0.
|
|
19
|
+
"quackage": "^1.0.64"
|
|
20
20
|
},
|
|
21
21
|
"mocha": {
|
|
22
22
|
"diff": true,
|
|
@@ -41,6 +41,7 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
41
41
|
this.PageSize = this.options.PageSize || 100;
|
|
42
42
|
this.SyncDeletedRecords = this.options.SyncDeletedRecords || false;
|
|
43
43
|
this.MaxRecordsPerEntity = this.options.MaxRecordsPerEntity || 0;
|
|
44
|
+
this.UseAdvancedIDPagination = this.options.UseAdvancedIDPagination || false;
|
|
44
45
|
|
|
45
46
|
this.Meadow = false;
|
|
46
47
|
|
|
@@ -459,14 +460,21 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
459
460
|
this.operation.createProgressTracker(tmpSyncState.EstimatedRecordCount, `FullSync-${this.EntitySchema.TableName}`);
|
|
460
461
|
this.operation.printProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`);
|
|
461
462
|
|
|
462
|
-
|
|
463
|
-
tmpSyncState.URLPartials = [];
|
|
464
|
-
for (let i = 0; i < tmpRecordCap; i += this.PageSize)
|
|
463
|
+
if (this.UseAdvancedIDPagination)
|
|
465
464
|
{
|
|
466
|
-
|
|
465
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: using advanced ID pagination (local: ${tmpSyncState.Local.RecordCount}/${tmpSyncState.Local.MaxIDEntity}, server: ${tmpSyncState.Server.RecordCount}/${tmpSyncState.Server.MaxIDEntity}, estimated new: ${tmpSyncState.EstimatedRecordCount}${this.MaxRecordsPerEntity > 0 ? `, capped at ${this.MaxRecordsPerEntity}` : ''})`);
|
|
467
466
|
}
|
|
467
|
+
else
|
|
468
|
+
{
|
|
469
|
+
// Generate paginated URL partials
|
|
470
|
+
tmpSyncState.URLPartials = [];
|
|
471
|
+
for (let i = 0; i < tmpRecordCap; i += this.PageSize)
|
|
472
|
+
{
|
|
473
|
+
tmpSyncState.URLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~${this.DefaultIdentifier}~GT~${tmpSyncState.Local.MaxIDEntity}~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
|
|
474
|
+
}
|
|
468
475
|
|
|
469
|
-
|
|
476
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: downloading ${tmpSyncState.URLPartials.length} pages (local: ${tmpSyncState.Local.RecordCount}/${tmpSyncState.Local.MaxIDEntity}, server: ${tmpSyncState.Server.RecordCount}/${tmpSyncState.Server.MaxIDEntity}, estimated new: ${tmpSyncState.EstimatedRecordCount}${this.MaxRecordsPerEntity > 0 ? `, capped at ${this.MaxRecordsPerEntity}` : ''})`);
|
|
477
|
+
}
|
|
470
478
|
|
|
471
479
|
return fStageComplete();
|
|
472
480
|
},
|
|
@@ -477,149 +485,216 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
477
485
|
let tmpRecordsSkipped = 0;
|
|
478
486
|
let tmpRecordsErrored = 0;
|
|
479
487
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
488
|
+
// Shared record-processing function used by both pagination modes
|
|
489
|
+
const fProcessPageRecords = (pBody, fPageProcessComplete) =>
|
|
490
|
+
{
|
|
491
|
+
this.fable.Utility.eachLimit(pBody, 5,
|
|
492
|
+
(pEntityRecord, fEntitySyncComplete) =>
|
|
493
|
+
{
|
|
494
|
+
const tmpRecord = pEntityRecord;
|
|
495
|
+
const tmpQuery = this.Meadow.query;
|
|
484
496
|
|
|
485
|
-
|
|
486
|
-
(pDownloadError, pResponse, pBody) =>
|
|
497
|
+
if ((typeof(tmpRecord[this.DefaultIdentifier]) !== 'undefined') && (tmpRecord[this.DefaultIdentifier] > 0))
|
|
487
498
|
{
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
this.fable.log.error(`${this.EntitySchema.TableName}: page ${tmpPageIndex} download error: ${pDownloadError}`);
|
|
491
|
-
return fDownloadComplete();
|
|
492
|
-
}
|
|
493
|
-
if (pBody && Array.isArray(pBody) && pBody.length > 0)
|
|
494
|
-
{
|
|
495
|
-
this.fable.Utility.eachLimit(pBody, 5,
|
|
496
|
-
(pEntityRecord, fEntitySyncComplete) =>
|
|
497
|
-
{
|
|
498
|
-
const tmpRecord = pEntityRecord;
|
|
499
|
-
const tmpQuery = this.Meadow.query;
|
|
500
|
-
|
|
501
|
-
if ((typeof(tmpRecord[this.DefaultIdentifier]) !== 'undefined') && (tmpRecord[this.DefaultIdentifier] > 0))
|
|
502
|
-
{
|
|
503
|
-
tmpQuery.addFilter(this.DefaultIdentifier, tmpRecord[this.DefaultIdentifier]);
|
|
504
|
-
}
|
|
499
|
+
tmpQuery.addFilter(this.DefaultIdentifier, tmpRecord[this.DefaultIdentifier]);
|
|
500
|
+
}
|
|
505
501
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
502
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
503
|
+
{
|
|
504
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
505
|
+
}
|
|
510
506
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
507
|
+
this.Meadow.doRead(tmpQuery,
|
|
508
|
+
(pReadError, pQuery, pRecord) =>
|
|
509
|
+
{
|
|
510
|
+
if (pReadError)
|
|
511
|
+
{
|
|
512
|
+
tmpRecordsErrored++;
|
|
513
|
+
return fEntitySyncComplete();
|
|
514
|
+
}
|
|
515
|
+
if (!pRecord)
|
|
516
|
+
{
|
|
517
|
+
// Record not found -- create it
|
|
518
|
+
const tmpRecordToCommit = this.marshalRecord(tmpRecord);
|
|
523
519
|
|
|
524
|
-
|
|
520
|
+
const tmpCreateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
525
521
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
522
|
+
tmpCreateQuery.setDisableAutoIdentity(true);
|
|
523
|
+
tmpCreateQuery.setDisableAutoDateStamp(true);
|
|
524
|
+
tmpCreateQuery.setDisableAutoUserStamp(true);
|
|
525
|
+
tmpCreateQuery.setDisableDeleteTracking(true);
|
|
530
526
|
|
|
531
|
-
|
|
527
|
+
tmpCreateQuery.AllowIdentityInsert = true;
|
|
532
528
|
|
|
533
|
-
|
|
534
|
-
|
|
529
|
+
this.Meadow.doCreate(tmpCreateQuery,
|
|
530
|
+
(pCreateError) =>
|
|
531
|
+
{
|
|
532
|
+
if (pCreateError)
|
|
533
|
+
{
|
|
534
|
+
let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
|
|
535
|
+
if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
|
|
536
|
+
{
|
|
537
|
+
// Duplicate key (likely GUID conflict) -- fall back to update
|
|
538
|
+
this.log.warn(`${this.EntitySchema.TableName}: duplicate key on create for ID ${tmpRecord[this.DefaultIdentifier]}; falling back to update.`);
|
|
539
|
+
const tmpUpdateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
540
|
+
tmpUpdateQuery.setDisableAutoIdentity(true);
|
|
541
|
+
tmpUpdateQuery.setDisableAutoDateStamp(true);
|
|
542
|
+
tmpUpdateQuery.setDisableAutoUserStamp(true);
|
|
543
|
+
tmpUpdateQuery.setDisableDeleteTracking(true);
|
|
544
|
+
this.Meadow.doUpdate(tmpUpdateQuery,
|
|
545
|
+
(pUpdateError) =>
|
|
535
546
|
{
|
|
536
|
-
if (
|
|
547
|
+
if (pUpdateError)
|
|
537
548
|
{
|
|
538
|
-
let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
|
|
539
|
-
if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
|
|
540
|
-
{
|
|
541
|
-
// Duplicate key (likely GUID conflict) -- fall back to update
|
|
542
|
-
this.log.warn(`${this.EntitySchema.TableName}: duplicate key on create for ID ${tmpRecord[this.DefaultIdentifier]}; falling back to update.`);
|
|
543
|
-
const tmpUpdateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
544
|
-
tmpUpdateQuery.setDisableAutoIdentity(true);
|
|
545
|
-
tmpUpdateQuery.setDisableAutoDateStamp(true);
|
|
546
|
-
tmpUpdateQuery.setDisableAutoUserStamp(true);
|
|
547
|
-
tmpUpdateQuery.setDisableDeleteTracking(true);
|
|
548
|
-
this.Meadow.doUpdate(tmpUpdateQuery,
|
|
549
|
-
(pUpdateError) =>
|
|
550
|
-
{
|
|
551
|
-
if (pUpdateError)
|
|
552
|
-
{
|
|
553
|
-
tmpRecordsErrored++;
|
|
554
|
-
this.log.error(`${this.EntitySchema.TableName}: fallback update also failed for ID ${tmpRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
|
|
555
|
-
return fEntitySyncComplete();
|
|
556
|
-
}
|
|
557
|
-
tmpRecordsCreated++;
|
|
558
|
-
this.operation.incrementProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`, 1);
|
|
559
|
-
return fEntitySyncComplete();
|
|
560
|
-
});
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
549
|
tmpRecordsErrored++;
|
|
564
|
-
this.log.error(`${this.EntitySchema.TableName}:
|
|
550
|
+
this.log.error(`${this.EntitySchema.TableName}: fallback update also failed for ID ${tmpRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
|
|
565
551
|
return fEntitySyncComplete();
|
|
566
552
|
}
|
|
567
553
|
tmpRecordsCreated++;
|
|
568
554
|
this.operation.incrementProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`, 1);
|
|
569
555
|
return fEntitySyncComplete();
|
|
570
556
|
});
|
|
557
|
+
return;
|
|
571
558
|
}
|
|
572
|
-
|
|
573
|
-
{
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (pEntitySyncError)
|
|
583
|
-
{
|
|
584
|
-
this.log.error(`Problem or early completion syncing entity ${this.EntitySchema.TableName}: ${pEntitySyncError}`, pEntitySyncError);
|
|
585
|
-
}
|
|
586
|
-
return fDownloadComplete();
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
else
|
|
590
|
-
{
|
|
591
|
-
if (Array.isArray(pBody) && pBody.length == 0)
|
|
559
|
+
tmpRecordsErrored++;
|
|
560
|
+
this.log.error(`${this.EntitySchema.TableName}: doCreate error for ID ${tmpRecord[this.DefaultIdentifier]}: ${pCreateError}`);
|
|
561
|
+
return fEntitySyncComplete();
|
|
562
|
+
}
|
|
563
|
+
tmpRecordsCreated++;
|
|
564
|
+
this.operation.incrementProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`, 1);
|
|
565
|
+
return fEntitySyncComplete();
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
else
|
|
592
569
|
{
|
|
593
|
-
|
|
570
|
+
tmpRecordsSkipped++;
|
|
571
|
+
return fEntitySyncComplete();
|
|
594
572
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
573
|
+
});
|
|
574
|
+
},
|
|
575
|
+
(pEntitySyncError) =>
|
|
576
|
+
{
|
|
577
|
+
this.operation.printProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`);
|
|
578
|
+
if (pEntitySyncError)
|
|
579
|
+
{
|
|
580
|
+
this.log.error(`Problem or early completion syncing entity ${this.EntitySchema.TableName}: ${pEntitySyncError}`, pEntitySyncError);
|
|
581
|
+
}
|
|
582
|
+
return fPageProcessComplete(pEntitySyncError);
|
|
583
|
+
});
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const fSyncComplete = (pDownloadError) =>
|
|
587
|
+
{
|
|
588
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: sync complete — created: ${tmpRecordsCreated}, skipped: ${tmpRecordsSkipped}, errors: ${tmpRecordsErrored}`);
|
|
589
|
+
if (pDownloadError)
|
|
590
|
+
{
|
|
591
|
+
this.fable.log.error(`Error returned URL Partial .. this may not be an error: ${pDownloadError}`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Store sync results on the entity so callers can inspect the breakdown
|
|
595
|
+
this.syncResults = (
|
|
596
|
+
{
|
|
597
|
+
Created: tmpRecordsCreated,
|
|
598
|
+
Skipped: tmpRecordsSkipped,
|
|
599
|
+
Errors: tmpRecordsErrored,
|
|
600
|
+
Deleted: 0,
|
|
601
|
+
ServerRecordCount: tmpSyncState.Server.RecordCount,
|
|
602
|
+
LocalRecordCount: tmpSyncState.Local.RecordCount,
|
|
603
|
+
ServerMaxID: tmpSyncState.Server.MaxIDEntity,
|
|
604
|
+
LocalMaxID: tmpSyncState.Local.MaxIDEntity,
|
|
605
|
+
EstimatedNew: tmpSyncState.EstimatedRecordCount
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
fStageComplete();
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
if (this.UseAdvancedIDPagination)
|
|
612
|
+
{
|
|
613
|
+
// Advanced ID pagination: use keyset pagination (WHERE ID > lastMaxID)
|
|
614
|
+
// instead of OFFSET to avoid progressive table scan slowdown on large datasets.
|
|
615
|
+
let tmpLastMaxID = tmpSyncState.Local.MaxIDEntity;
|
|
616
|
+
let tmpTotalFetched = 0;
|
|
617
|
+
|
|
618
|
+
const fFetchPage = () =>
|
|
600
619
|
{
|
|
601
|
-
|
|
602
|
-
if (pDownloadError)
|
|
620
|
+
if (tmpTotalFetched >= tmpRecordCap)
|
|
603
621
|
{
|
|
604
|
-
|
|
622
|
+
return fSyncComplete();
|
|
605
623
|
}
|
|
606
624
|
|
|
607
|
-
|
|
608
|
-
|
|
625
|
+
tmpPageIndex++;
|
|
626
|
+
let tmpURL = `${this.EntitySchema.TableName}s/FilteredTo/FBV~${this.DefaultIdentifier}~GT~${tmpLastMaxID}~FSF~${this.DefaultIdentifier}~ASC~ASC/0/${this.PageSize}`;
|
|
627
|
+
|
|
628
|
+
this.fable.MeadowCloneRestClient.getJSON(tmpURL,
|
|
629
|
+
(pDownloadError, pResponse, pBody) =>
|
|
609
630
|
{
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
631
|
+
if (pDownloadError)
|
|
632
|
+
{
|
|
633
|
+
this.fable.log.error(`${this.EntitySchema.TableName}: page ${tmpPageIndex} download error: ${pDownloadError}`);
|
|
634
|
+
return fSyncComplete(pDownloadError);
|
|
635
|
+
}
|
|
636
|
+
if (pBody && Array.isArray(pBody) && pBody.length > 0)
|
|
637
|
+
{
|
|
638
|
+
// Track the max ID from this page for the next page filter
|
|
639
|
+
for (let r = 0; r < pBody.length; r++)
|
|
640
|
+
{
|
|
641
|
+
if (pBody[r][this.DefaultIdentifier] > tmpLastMaxID)
|
|
642
|
+
{
|
|
643
|
+
tmpLastMaxID = pBody[r][this.DefaultIdentifier];
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
tmpTotalFetched += pBody.length;
|
|
647
|
+
|
|
648
|
+
fProcessPageRecords(pBody, (pProcessError) =>
|
|
649
|
+
{
|
|
650
|
+
if (pProcessError)
|
|
651
|
+
{
|
|
652
|
+
return fSyncComplete(pProcessError);
|
|
653
|
+
}
|
|
654
|
+
return setImmediate(fFetchPage);
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
else
|
|
658
|
+
{
|
|
659
|
+
// No more records
|
|
660
|
+
return fSyncComplete();
|
|
661
|
+
}
|
|
619
662
|
});
|
|
663
|
+
};
|
|
664
|
+
fFetchPage();
|
|
665
|
+
}
|
|
666
|
+
else
|
|
667
|
+
{
|
|
668
|
+
// Standard offset-based pagination
|
|
669
|
+
this.fable.Utility.eachLimit(tmpSyncState.URLPartials, 1,
|
|
670
|
+
(pURLPartial, fDownloadComplete) =>
|
|
671
|
+
{
|
|
672
|
+
tmpPageIndex++;
|
|
620
673
|
|
|
621
|
-
|
|
622
|
-
|
|
674
|
+
this.fable.MeadowCloneRestClient.getJSON(pURLPartial,
|
|
675
|
+
(pDownloadError, pResponse, pBody) =>
|
|
676
|
+
{
|
|
677
|
+
if (pDownloadError)
|
|
678
|
+
{
|
|
679
|
+
this.fable.log.error(`${this.EntitySchema.TableName}: page ${tmpPageIndex} download error: ${pDownloadError}`);
|
|
680
|
+
return fDownloadComplete();
|
|
681
|
+
}
|
|
682
|
+
if (pBody && Array.isArray(pBody) && pBody.length > 0)
|
|
683
|
+
{
|
|
684
|
+
fProcessPageRecords(pBody, fDownloadComplete);
|
|
685
|
+
}
|
|
686
|
+
else
|
|
687
|
+
{
|
|
688
|
+
if (Array.isArray(pBody) && pBody.length == 0)
|
|
689
|
+
{
|
|
690
|
+
return fDownloadComplete(new Error('Records depleted!'));
|
|
691
|
+
}
|
|
692
|
+
return fDownloadComplete();
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
},
|
|
696
|
+
fSyncComplete);
|
|
697
|
+
}
|
|
623
698
|
},
|
|
624
699
|
],
|
|
625
700
|
(pError) =>
|
|
@@ -372,6 +372,10 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
372
372
|
{
|
|
373
373
|
this.log.error(`Fallback update also failed for ${this.EntitySchema.TableName} ID ${pServerRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
|
|
374
374
|
}
|
|
375
|
+
else
|
|
376
|
+
{
|
|
377
|
+
this._recordsUpdated++;
|
|
378
|
+
}
|
|
375
379
|
return fCallback();
|
|
376
380
|
});
|
|
377
381
|
return;
|
|
@@ -379,6 +383,10 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
379
383
|
this.log.error(`Error creating record ${this.EntitySchema.TableName}: ${pCreateError}`, pCreateError);
|
|
380
384
|
return fCallback();
|
|
381
385
|
}
|
|
386
|
+
else
|
|
387
|
+
{
|
|
388
|
+
this._recordsCreated++;
|
|
389
|
+
}
|
|
382
390
|
return fCallback();
|
|
383
391
|
});
|
|
384
392
|
}
|
|
@@ -392,6 +400,10 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
392
400
|
{
|
|
393
401
|
this.log.error(`Error updating record ${this.EntitySchema.TableName}: ${pUpdateError}`, pUpdateError);
|
|
394
402
|
}
|
|
403
|
+
else
|
|
404
|
+
{
|
|
405
|
+
this._recordsUpdated++;
|
|
406
|
+
}
|
|
395
407
|
return fCallback();
|
|
396
408
|
});
|
|
397
409
|
}
|
|
@@ -407,12 +419,22 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
407
419
|
return fCallback(null, 0);
|
|
408
420
|
}
|
|
409
421
|
|
|
422
|
+
// Check the global cap before starting -- if we have already synced
|
|
423
|
+
// MaxRecordsPerEntity records across all stages, stop immediately.
|
|
424
|
+
if (this.MaxRecordsPerEntity > 0 && this._totalSyncedThisSync >= this.MaxRecordsPerEntity)
|
|
425
|
+
{
|
|
426
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: global record cap reached (${this._totalSyncedThisSync}/${this.MaxRecordsPerEntity}); skipping pull.`);
|
|
427
|
+
return fCallback(null, 0);
|
|
428
|
+
}
|
|
429
|
+
|
|
410
430
|
let tmpSyncedCount = 0;
|
|
411
431
|
let tmpOffset = 0;
|
|
412
432
|
let tmpDone = false;
|
|
413
433
|
|
|
434
|
+
// Apply per-call cap based on estimated count, then further limit by
|
|
435
|
+
// how many records remain before hitting the global MaxRecordsPerEntity.
|
|
414
436
|
let tmpRecordCap = (this.MaxRecordsPerEntity > 0)
|
|
415
|
-
? Math.min(pEstimatedCount, this.MaxRecordsPerEntity)
|
|
437
|
+
? Math.min(pEstimatedCount, this.MaxRecordsPerEntity - this._totalSyncedThisSync)
|
|
416
438
|
: pEstimatedCount;
|
|
417
439
|
|
|
418
440
|
const fFetchPage = () =>
|
|
@@ -443,6 +465,7 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
443
465
|
() =>
|
|
444
466
|
{
|
|
445
467
|
tmpSyncedCount++;
|
|
468
|
+
this._totalSyncedThisSync++;
|
|
446
469
|
// Use setImmediate to yield the event loop and prevent
|
|
447
470
|
// stack overflow when SQLite callbacks complete synchronously
|
|
448
471
|
return setImmediate(fRecordDone);
|
|
@@ -485,6 +508,12 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
485
508
|
// the range from the server to bring local in sync.
|
|
486
509
|
_bisectRange(pMinID, pMaxID, pDepth, fCallback)
|
|
487
510
|
{
|
|
511
|
+
// If the global record cap has been reached, stop bisecting
|
|
512
|
+
if (this.MaxRecordsPerEntity > 0 && this._totalSyncedThisSync >= this.MaxRecordsPerEntity)
|
|
513
|
+
{
|
|
514
|
+
return fCallback();
|
|
515
|
+
}
|
|
516
|
+
|
|
488
517
|
const tmpRangeSize = pMaxID - pMinID + 1;
|
|
489
518
|
const tmpIDCol = this.DefaultIdentifier;
|
|
490
519
|
const tmpRangeFilter = `FBV~${tmpIDCol}~GE~${pMinID}~FBV~${tmpIDCol}~LE~${pMaxID}`;
|
|
@@ -768,7 +797,7 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
768
797
|
{
|
|
769
798
|
if (pReadError)
|
|
770
799
|
{
|
|
771
|
-
let tmpErrorStr = (typeof(pReadError) === 'string') ? pReadError :
|
|
800
|
+
let tmpErrorStr = (typeof(pReadError) === 'string') ? pReadError : String(pReadError);
|
|
772
801
|
if (tmpErrorStr.indexOf('Invalid column') > -1 || tmpErrorStr.indexOf('Invalid object') > -1 || tmpErrorStr.indexOf('no such column') > -1 || tmpErrorStr.indexOf('no such table') > -1)
|
|
773
802
|
{
|
|
774
803
|
this.log.warn(`${this.EntitySchema.TableName}: local table schema mismatch (${pReadError}); skipping sync.`);
|
|
@@ -783,6 +812,13 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
783
812
|
{
|
|
784
813
|
this.operation.createTimeStamp('EntityOngoingSync');
|
|
785
814
|
|
|
815
|
+
// Track total records synced across all stages to enforce MaxRecordsPerEntity globally
|
|
816
|
+
this._totalSyncedThisSync = 0;
|
|
817
|
+
|
|
818
|
+
// Track per-record create vs update counts for the sync report
|
|
819
|
+
this._recordsCreated = 0;
|
|
820
|
+
this._recordsUpdated = 0;
|
|
821
|
+
|
|
786
822
|
const tmpSyncState = (
|
|
787
823
|
{
|
|
788
824
|
Local: { MaxIDEntity: -1, RecordCount: 0 },
|
|
@@ -941,7 +977,10 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
941
977
|
// Create a progress tracker so callers (e.g. data-cloner UI) can see Total/Synced
|
|
942
978
|
(fStageComplete) =>
|
|
943
979
|
{
|
|
944
|
-
this.
|
|
980
|
+
let tmpTrackerTotal = (this.MaxRecordsPerEntity > 0)
|
|
981
|
+
? Math.min(tmpSyncState.Server.RecordCount, this.MaxRecordsPerEntity)
|
|
982
|
+
: tmpSyncState.Server.RecordCount;
|
|
983
|
+
this.operation.createProgressTracker(tmpTrackerTotal, `FullSync-${this.EntitySchema.TableName}`);
|
|
945
984
|
return fStageComplete();
|
|
946
985
|
},
|
|
947
986
|
|
|
@@ -1126,6 +1165,15 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
1126
1165
|
|
|
1127
1166
|
this.fable.log.info(`${this.EntitySchema.TableName}: ongoing sync complete.`);
|
|
1128
1167
|
|
|
1168
|
+
// Store sync results so callers can inspect the breakdown
|
|
1169
|
+
this.syncResults = {
|
|
1170
|
+
Created: this._recordsCreated,
|
|
1171
|
+
Updated: this._recordsUpdated,
|
|
1172
|
+
Deleted: 0,
|
|
1173
|
+
ServerRecordCount: tmpSyncState.Server.RecordCount,
|
|
1174
|
+
LocalRecordCount: tmpSyncState.Local.RecordCount
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1129
1177
|
if (this.SyncDeletedRecords)
|
|
1130
1178
|
{
|
|
1131
1179
|
return this.syncDeletedRecords(() => { return fCallback(); });
|
|
@@ -65,6 +65,18 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
65
65
|
this.MaxRecordsPerEntity = parseInt(this.options.MaxRecordsPerEntity, 10) || 0;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// When true, use ID-based keyset pagination instead of OFFSET pagination.
|
|
69
|
+
// This avoids table scans on large datasets by filtering WHERE ID > lastMaxID.
|
|
70
|
+
this.UseAdvancedIDPagination = false;
|
|
71
|
+
if (this.fable.ProgramConfiguration.hasOwnProperty('UseAdvancedIDPagination'))
|
|
72
|
+
{
|
|
73
|
+
this.UseAdvancedIDPagination = !!this.fable.ProgramConfiguration.UseAdvancedIDPagination;
|
|
74
|
+
}
|
|
75
|
+
else if (this.options.hasOwnProperty('UseAdvancedIDPagination'))
|
|
76
|
+
{
|
|
77
|
+
this.UseAdvancedIDPagination = !!this.options.UseAdvancedIDPagination;
|
|
78
|
+
}
|
|
79
|
+
|
|
68
80
|
// Tolerance window in milliseconds for cross-database timestamp precision differences.
|
|
69
81
|
// Passed through to Ongoing sync entities for bisection date comparison.
|
|
70
82
|
this.DateTimePrecisionMS = 1000;
|
|
@@ -114,6 +126,7 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
114
126
|
SyncDeletedRecords: this.SyncDeletedRecords,
|
|
115
127
|
MaxRecordsPerEntity: this.MaxRecordsPerEntity,
|
|
116
128
|
DateTimePrecisionMS: this.DateTimePrecisionMS,
|
|
129
|
+
UseAdvancedIDPagination: this.UseAdvancedIDPagination,
|
|
117
130
|
};
|
|
118
131
|
|
|
119
132
|
let tmpSyncEntity;
|