meadow-integration 1.0.24 → 1.0.25

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.24",
3
+ "version": "1.0.25",
4
4
  "description": "Meadow Data Integration",
5
5
  "bin": {
6
6
  "mdwint": "source/cli/Meadow-Integration-CLI-Run.js"
@@ -18,7 +18,8 @@
18
18
  "license": "MIT",
19
19
  "devDependencies": {
20
20
  "meadow-connection-sqlite": "^1.0.18",
21
- "quackage": "^1.0.65"
21
+ "pict-docuserve": "^0.1.5",
22
+ "quackage": "^1.1.0"
22
23
  },
23
24
  "mocha": {
24
25
  "diff": true,
@@ -0,0 +1,435 @@
1
+ const libMeadowSyncEntityOngoing = require('./Meadow-Service-Sync-Entity-Ongoing.js');
2
+
3
+ class MeadowSyncEntityComparisonOnly extends libMeadowSyncEntityOngoing
4
+ {
5
+ constructor(pFable, pOptions, pServiceHash)
6
+ {
7
+ super(pFable, pOptions, pServiceHash);
8
+
9
+ this.serviceType = 'MeadowSyncEntityComparisonOnly';
10
+
11
+ this.ComparisonReport = null;
12
+ }
13
+
14
+ // Compare a local ID range against the server and record the result in the
15
+ // report. Same logic as _bisectRange but never pulls or upserts records.
16
+ _compareRange(pMinID, pMaxID, pDepth, pReport, fCallback)
17
+ {
18
+ const tmpRangeSize = pMaxID - pMinID + 1;
19
+ const tmpIDCol = this.DefaultIdentifier;
20
+ const tmpRangeFilter = `FBV~${tmpIDCol}~GE~${pMinID}~FBV~${tmpIDCol}~LE~${pMaxID}`;
21
+
22
+ this._getLocalCount(pMinID, pMaxID,
23
+ (pLocalCountError, pLocalCount) =>
24
+ {
25
+ if (pLocalCountError)
26
+ {
27
+ this.fable.log.warn(`${this.EntitySchema.TableName}: compare local count error for range ${pMinID}-${pMaxID}: ${pLocalCountError}`);
28
+ pReport.Ranges.push(
29
+ {
30
+ MinID: pMinID,
31
+ MaxID: pMaxID,
32
+ Status: 'error',
33
+ Error: `Local count error: ${pLocalCountError}`
34
+ });
35
+ return fCallback();
36
+ }
37
+
38
+ this._getServerCount(tmpRangeFilter,
39
+ (pServerCountError, pServerCount) =>
40
+ {
41
+ if (pServerCountError)
42
+ {
43
+ this.fable.log.warn(`${this.EntitySchema.TableName}: compare server count error for range ${pMinID}-${pMaxID}: ${pServerCountError}`);
44
+ pReport.Ranges.push(
45
+ {
46
+ MinID: pMinID,
47
+ MaxID: pMaxID,
48
+ Status: 'error',
49
+ Error: `Server count error: ${pServerCountError}`
50
+ });
51
+ return fCallback();
52
+ }
53
+
54
+ if (pLocalCount === pServerCount)
55
+ {
56
+ if (!this._hasUpdateDate)
57
+ {
58
+ // Counts match, no UpdateDate to check -- record as match
59
+ pReport.Ranges.push(
60
+ {
61
+ MinID: pMinID,
62
+ MaxID: pMaxID,
63
+ Status: 'match',
64
+ LocalCount: pLocalCount,
65
+ ServerCount: pServerCount
66
+ });
67
+ this._incrementProgress(pServerCount);
68
+ return fCallback();
69
+ }
70
+
71
+ // Compare UpdateDate boundaries
72
+ this._getLocalMaxUpdateDate(pMinID, pMaxID,
73
+ (pLocalMaxErr, pLocalMaxDate) =>
74
+ {
75
+ if (pLocalMaxErr || !pLocalMaxDate)
76
+ {
77
+ // Can't determine UpdateDate -- treat as match by count
78
+ pReport.Ranges.push(
79
+ {
80
+ MinID: pMinID,
81
+ MaxID: pMaxID,
82
+ Status: 'match',
83
+ LocalCount: pLocalCount,
84
+ ServerCount: pServerCount
85
+ });
86
+ this._incrementProgress(pServerCount);
87
+ return fCallback();
88
+ }
89
+
90
+ const tmpMaxDateFilter = `${tmpRangeFilter}~FSF~UpdateDate~DESC~DESC`;
91
+ this._getServerRecords(tmpMaxDateFilter, 0, 1,
92
+ (pServerMaxErr, pServerMaxRecords) =>
93
+ {
94
+ if (pServerMaxErr || !pServerMaxRecords || pServerMaxRecords.length < 1)
95
+ {
96
+ pReport.Ranges.push(
97
+ {
98
+ MinID: pMinID,
99
+ MaxID: pMaxID,
100
+ Status: 'match',
101
+ LocalCount: pLocalCount,
102
+ ServerCount: pServerCount
103
+ });
104
+ this._incrementProgress(pServerCount);
105
+ return fCallback();
106
+ }
107
+
108
+ const tmpServerMaxDate = pServerMaxRecords[0].UpdateDate;
109
+ const tmpLocalNorm = this._normalizeDateUTC(pLocalMaxDate);
110
+ const tmpServerNorm = this._normalizeDateUTC(tmpServerMaxDate);
111
+ const tmpMaxDateDiff = Math.abs(tmpLocalNorm.diff(tmpServerNorm));
112
+
113
+ if (tmpMaxDateDiff <= this.DateTimePrecisionMS)
114
+ {
115
+ // Counts and dates match -- in sync
116
+ pReport.Ranges.push(
117
+ {
118
+ MinID: pMinID,
119
+ MaxID: pMaxID,
120
+ Status: 'match',
121
+ LocalCount: pLocalCount,
122
+ ServerCount: pServerCount
123
+ });
124
+ this._incrementProgress(pServerCount);
125
+ return fCallback();
126
+ }
127
+
128
+ // Dates differ even though counts match
129
+ if (tmpRangeSize <= this.BisectMinRangeSize)
130
+ {
131
+ pReport.Ranges.push(
132
+ {
133
+ MinID: pMinID,
134
+ MaxID: pMaxID,
135
+ Status: 'mismatch',
136
+ LocalCount: pLocalCount,
137
+ ServerCount: pServerCount,
138
+ CountDifference: pServerCount - pLocalCount,
139
+ LocalMaxUpdateDate: tmpLocalNorm.toISOString(),
140
+ ServerMaxUpdateDate: tmpServerNorm.toISOString(),
141
+ UpdateDateDifferenceMS: tmpMaxDateDiff
142
+ });
143
+ this._incrementProgress(pServerCount);
144
+ return fCallback();
145
+ }
146
+
147
+ return this._compareSubdivideRange(pMinID, pMaxID, pDepth, pReport, fCallback);
148
+ });
149
+ });
150
+ return;
151
+ }
152
+
153
+ // Counts differ
154
+ if (tmpRangeSize <= this.BisectMinRangeSize)
155
+ {
156
+ const tmpMismatchEntry = {
157
+ MinID: pMinID,
158
+ MaxID: pMaxID,
159
+ Status: 'mismatch',
160
+ LocalCount: pLocalCount,
161
+ ServerCount: pServerCount,
162
+ CountDifference: pServerCount - pLocalCount
163
+ };
164
+
165
+ // Try to get UpdateDate info for the mismatch entry
166
+ if (this._hasUpdateDate)
167
+ {
168
+ this._getLocalMaxUpdateDate(pMinID, pMaxID,
169
+ (pLocalMaxErr, pLocalMaxDate) =>
170
+ {
171
+ if (!pLocalMaxErr && pLocalMaxDate)
172
+ {
173
+ const tmpMaxDateFilter = `${tmpRangeFilter}~FSF~UpdateDate~DESC~DESC`;
174
+ this._getServerRecords(tmpMaxDateFilter, 0, 1,
175
+ (pServerMaxErr, pServerMaxRecords) =>
176
+ {
177
+ if (!pServerMaxErr && pServerMaxRecords && pServerMaxRecords.length > 0)
178
+ {
179
+ const tmpLocalNorm = this._normalizeDateUTC(pLocalMaxDate);
180
+ const tmpServerNorm = this._normalizeDateUTC(pServerMaxRecords[0].UpdateDate);
181
+ tmpMismatchEntry.LocalMaxUpdateDate = tmpLocalNorm.toISOString();
182
+ tmpMismatchEntry.ServerMaxUpdateDate = tmpServerNorm.toISOString();
183
+ tmpMismatchEntry.UpdateDateDifferenceMS = Math.abs(tmpLocalNorm.diff(tmpServerNorm));
184
+ }
185
+ pReport.Ranges.push(tmpMismatchEntry);
186
+ this._incrementProgress(Math.max(pLocalCount, pServerCount));
187
+ return fCallback();
188
+ });
189
+ return;
190
+ }
191
+ pReport.Ranges.push(tmpMismatchEntry);
192
+ this._incrementProgress(Math.max(pLocalCount, pServerCount));
193
+ return fCallback();
194
+ });
195
+ return;
196
+ }
197
+
198
+ pReport.Ranges.push(tmpMismatchEntry);
199
+ this._incrementProgress(Math.max(pLocalCount, pServerCount));
200
+ return fCallback();
201
+ }
202
+
203
+ return this._compareSubdivideRange(pMinID, pMaxID, pDepth, pReport, fCallback);
204
+ });
205
+ });
206
+ }
207
+
208
+ _compareSubdivideRange(pMinID, pMaxID, pDepth, pReport, fCallback)
209
+ {
210
+ const tmpMidID = Math.floor((pMinID + pMaxID) / 2);
211
+
212
+ this.fable.log.info(`${this.EntitySchema.TableName}: compare subdividing range ${pMinID}-${pMaxID} at ID ${tmpMidID} (depth ${pDepth})`);
213
+
214
+ this._compareRange(pMinID, tmpMidID, pDepth + 1, pReport,
215
+ () =>
216
+ {
217
+ this._compareRange(tmpMidID + 1, pMaxID, pDepth + 1, pReport, fCallback);
218
+ });
219
+ }
220
+
221
+ _syncInternal(fCallback)
222
+ {
223
+ this.operation.createTimeStamp('EntityComparisonOnlySync');
224
+
225
+ this._totalSyncedThisSync = 0;
226
+ this._recordsCreated = 0;
227
+ this._recordsUpdated = 0;
228
+
229
+ const tmpSyncState = (
230
+ {
231
+ Local: { MaxIDEntity: -1, RecordCount: 0 },
232
+ Server: { MaxIDEntity: -1, RecordCount: 0 },
233
+ });
234
+
235
+ this._hasUpdateDate = false;
236
+ this._hasDeletedColumn = false;
237
+
238
+ if (this.EntitySchema && this.EntitySchema.MeadowSchema && Array.isArray(this.EntitySchema.MeadowSchema.Schema))
239
+ {
240
+ for (let i = 0; i < this.EntitySchema.MeadowSchema.Schema.length; i++)
241
+ {
242
+ const tmpColumn = this.EntitySchema.MeadowSchema.Schema[i];
243
+ if (tmpColumn.Column == 'UpdateDate')
244
+ {
245
+ this._hasUpdateDate = true;
246
+ }
247
+ if (tmpColumn.Type == 'Deleted' || tmpColumn.Column == 'Deleted')
248
+ {
249
+ this._hasDeletedColumn = true;
250
+ }
251
+ }
252
+ }
253
+
254
+ this.fable.log.info(`Syncing with COMPARISON ONLY STRATEGY entity ${this.EntitySchema.TableName} (UpdateDate: ${this._hasUpdateDate})...`);
255
+
256
+ this.ComparisonReport = {
257
+ Entity: this.EntitySchema.TableName,
258
+ Timestamp: new Date().toISOString(),
259
+ Summary: {
260
+ LocalRecordCount: 0,
261
+ ServerRecordCount: 0,
262
+ LocalMaxID: 0,
263
+ ServerMaxID: 0,
264
+ TotalRangesChecked: 0,
265
+ MatchingRanges: 0,
266
+ MismatchedRanges: 0,
267
+ ErrorRanges: 0,
268
+ TotalCountDifference: 0
269
+ },
270
+ Ranges: []
271
+ };
272
+
273
+ this.fable.Utility.waterfall(
274
+ [
275
+ // ---- Stage 1: Gather local stats ----
276
+ (fStageComplete) =>
277
+ {
278
+ const tmpQuery = this.Meadow.query;
279
+ tmpQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
280
+ tmpQuery.setCap(1);
281
+ if (!this._hasDeletedColumn)
282
+ {
283
+ tmpQuery.setDisableDeleteTracking(true);
284
+ }
285
+ this.Meadow.doRead(tmpQuery,
286
+ (pReadError, pQuery, pRecord) =>
287
+ {
288
+ if (pReadError)
289
+ {
290
+ this.fable.log.error(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`);
291
+ return fStageComplete(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`);
292
+ }
293
+ if (pRecord)
294
+ {
295
+ tmpSyncState.Local.MaxIDEntity = pRecord[this.DefaultIdentifier];
296
+ }
297
+ return fStageComplete();
298
+ });
299
+ },
300
+ (fStageComplete) =>
301
+ {
302
+ const tmpQuery = this.Meadow.query;
303
+ if (!this._hasDeletedColumn)
304
+ {
305
+ tmpQuery.setDisableDeleteTracking(true);
306
+ }
307
+ this.Meadow.doCount(tmpQuery,
308
+ (pCountError, pQuery, pCount) =>
309
+ {
310
+ if (pCountError)
311
+ {
312
+ this.fable.log.error(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`);
313
+ return fStageComplete(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`);
314
+ }
315
+ tmpSyncState.Local.RecordCount = pCount;
316
+ return fStageComplete();
317
+ });
318
+ },
319
+
320
+ // ---- Stage 2: Gather server stats ----
321
+ (fStageComplete) =>
322
+ {
323
+ this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}/Max/${this.DefaultIdentifier}`,
324
+ (pError, pResponse, pBody) =>
325
+ {
326
+ if (pError)
327
+ {
328
+ this.fable.log.warn(`Could not get server max entity ID for ${this.EntitySchema.TableName} (${pError}).`);
329
+ return fStageComplete();
330
+ }
331
+ if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
332
+ {
333
+ tmpSyncState.Server.MaxIDEntity = pBody[this.DefaultIdentifier];
334
+ }
335
+ return fStageComplete();
336
+ });
337
+ },
338
+ (fStageComplete) =>
339
+ {
340
+ this._getServerCount(null,
341
+ (pError, pCount) =>
342
+ {
343
+ if (pError)
344
+ {
345
+ this.fable.log.warn(`Could not get server count for ${this.EntitySchema.TableName} (${pError}); estimating from max ID.`);
346
+ tmpSyncState.Server.RecordCount = tmpSyncState.Server.MaxIDEntity > 0 ? tmpSyncState.Server.MaxIDEntity : 0;
347
+ return fStageComplete();
348
+ }
349
+ tmpSyncState.Server.RecordCount = pCount;
350
+ return fStageComplete();
351
+ });
352
+ },
353
+
354
+ // Create progress tracker
355
+ (fStageComplete) =>
356
+ {
357
+ let tmpTrackerTotal = Math.max(tmpSyncState.Server.RecordCount, tmpSyncState.Local.RecordCount);
358
+ this.operation.createProgressTracker(tmpTrackerTotal, `FullSync-${this.EntitySchema.TableName}`);
359
+ return fStageComplete();
360
+ },
361
+
362
+ // ---- Stage 3: Comparison bisection ----
363
+ (fStageComplete) =>
364
+ {
365
+ const tmpMaxID = Math.max(tmpSyncState.Local.MaxIDEntity, tmpSyncState.Server.MaxIDEntity);
366
+
367
+ if (tmpMaxID < 1)
368
+ {
369
+ this.fable.log.info(`${this.EntitySchema.TableName}: no records on either side; nothing to compare.`);
370
+ return fStageComplete();
371
+ }
372
+
373
+ this.fable.log.info(`${this.EntitySchema.TableName}: starting comparison bisection (ID range 1-${tmpMaxID})...`);
374
+
375
+ this._compareRange(1, tmpMaxID, 0, this.ComparisonReport, fStageComplete);
376
+ },
377
+ ],
378
+ (pError) =>
379
+ {
380
+ if (pError)
381
+ {
382
+ this.fable.log.error(`Error performing comparison sync ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
383
+ }
384
+
385
+ let tmpTracker = this.operation.progressTrackers[`FullSync-${this.EntitySchema.TableName}`];
386
+ if (tmpTracker)
387
+ {
388
+ tmpTracker.CurrentCount = tmpTracker.TotalCount;
389
+ }
390
+
391
+ // Finalize report summary
392
+ this.ComparisonReport.Summary.LocalRecordCount = tmpSyncState.Local.RecordCount;
393
+ this.ComparisonReport.Summary.ServerRecordCount = tmpSyncState.Server.RecordCount;
394
+ this.ComparisonReport.Summary.LocalMaxID = tmpSyncState.Local.MaxIDEntity;
395
+ this.ComparisonReport.Summary.ServerMaxID = tmpSyncState.Server.MaxIDEntity;
396
+ this.ComparisonReport.Summary.TotalRangesChecked = this.ComparisonReport.Ranges.length;
397
+
398
+ let tmpTotalCountDifference = 0;
399
+ for (let i = 0; i < this.ComparisonReport.Ranges.length; i++)
400
+ {
401
+ const tmpRange = this.ComparisonReport.Ranges[i];
402
+ if (tmpRange.Status === 'match')
403
+ {
404
+ this.ComparisonReport.Summary.MatchingRanges++;
405
+ }
406
+ else if (tmpRange.Status === 'mismatch')
407
+ {
408
+ this.ComparisonReport.Summary.MismatchedRanges++;
409
+ tmpTotalCountDifference += Math.abs(tmpRange.CountDifference || 0);
410
+ }
411
+ else if (tmpRange.Status === 'error')
412
+ {
413
+ this.ComparisonReport.Summary.ErrorRanges++;
414
+ }
415
+ }
416
+ this.ComparisonReport.Summary.TotalCountDifference = tmpTotalCountDifference;
417
+
418
+ this.fable.log.info(`${this.EntitySchema.TableName}: comparison complete -- ${this.ComparisonReport.Summary.MatchingRanges} matching, ${this.ComparisonReport.Summary.MismatchedRanges} mismatched out of ${this.ComparisonReport.Summary.TotalRangesChecked} ranges.`);
419
+
420
+ this.syncResults = {
421
+ Created: 0,
422
+ Updated: 0,
423
+ Deleted: 0,
424
+ ServerRecordCount: tmpSyncState.Server.RecordCount,
425
+ LocalRecordCount: tmpSyncState.Local.RecordCount,
426
+ ComparisonReport: this.ComparisonReport
427
+ };
428
+
429
+ // No deleted record sync for comparison-only mode
430
+ return fCallback();
431
+ });
432
+ }
433
+ }
434
+
435
+ module.exports = MeadowSyncEntityComparisonOnly;