meadow-integration 1.0.11 → 1.0.13
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
|
@@ -74,33 +74,7 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
74
74
|
|
|
75
75
|
return tmpProvider.createTable(this.EntitySchema, (pCreateError) =>
|
|
76
76
|
{
|
|
77
|
-
|
|
78
|
-
{
|
|
79
|
-
// Validate local table schema with a lightweight read
|
|
80
|
-
const tmpValidationQuery = this.Meadow.query;
|
|
81
|
-
tmpValidationQuery.setCap(1);
|
|
82
|
-
tmpValidationQuery.setDisableDeleteTracking(true);
|
|
83
|
-
this.Meadow.doRead(tmpValidationQuery,
|
|
84
|
-
(pReadError) =>
|
|
85
|
-
{
|
|
86
|
-
if (pReadError)
|
|
87
|
-
{
|
|
88
|
-
let tmpErrorStr = (typeof(pReadError) === 'string') ? pReadError : JSON.stringify(pReadError);
|
|
89
|
-
// Only skip sync for schema-specific errors (invalid column/object name)
|
|
90
|
-
// Generic provider errors (e.g. prepared statement failures) should not block sync
|
|
91
|
-
if (tmpErrorStr.indexOf('Invalid column') > -1 || tmpErrorStr.indexOf('Invalid object') > -1 || tmpErrorStr.indexOf('no such column') > -1 || tmpErrorStr.indexOf('no such table') > -1)
|
|
92
|
-
{
|
|
93
|
-
this.log.warn(`${this.EntitySchema.TableName}: local table schema validation failed (${pReadError}); this entity will be skipped during sync.`);
|
|
94
|
-
this.skipSync = true;
|
|
95
|
-
}
|
|
96
|
-
else
|
|
97
|
-
{
|
|
98
|
-
this.log.warn(`${this.EntitySchema.TableName}: validation read returned error (${pReadError}); sync will proceed.`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return fCallback(pPriorError);
|
|
102
|
-
});
|
|
103
|
-
};
|
|
77
|
+
|
|
104
78
|
if (pCreateError)
|
|
105
79
|
{
|
|
106
80
|
this.log.warn(`${this.EntitySchema.TableName}: createTable returned error: ${pCreateError}`);
|
|
@@ -112,13 +86,13 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
112
86
|
if (!tmpGUIDColumn && !tmpDeletedColumn)
|
|
113
87
|
{
|
|
114
88
|
this.log.info(`No GUID or Deleted columns for ${this.EntitySchema.TableName}; skipping index creation`);
|
|
115
|
-
return
|
|
89
|
+
return fCallback(pCreateError);
|
|
116
90
|
}
|
|
117
91
|
|
|
118
92
|
if (!this.fable.MeadowConnectionManager || !this.fable.MeadowConnectionManager.ConnectionPool)
|
|
119
93
|
{
|
|
120
94
|
this.log.info(`No connection manager available; skipping index creation for ${this.EntitySchema.TableName}`);
|
|
121
|
-
return
|
|
95
|
+
return fCallback(pCreateError);
|
|
122
96
|
}
|
|
123
97
|
|
|
124
98
|
let tmpAnticipate = this.fable.newAnticipate();
|
|
@@ -336,6 +310,29 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
336
310
|
return fCallback();
|
|
337
311
|
}
|
|
338
312
|
|
|
313
|
+
// Validate local table schema with a lightweight read before syncing
|
|
314
|
+
const tmpValidationQuery = this.Meadow.query;
|
|
315
|
+
tmpValidationQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
|
|
316
|
+
tmpValidationQuery.setCap(1);
|
|
317
|
+
tmpValidationQuery.setDisableDeleteTracking(true);
|
|
318
|
+
this.Meadow.doRead(tmpValidationQuery,
|
|
319
|
+
(pReadError) =>
|
|
320
|
+
{
|
|
321
|
+
if (pReadError)
|
|
322
|
+
{
|
|
323
|
+
let tmpErrorStr = (typeof(pReadError) === 'string') ? pReadError : JSON.stringify(pReadError);
|
|
324
|
+
if (tmpErrorStr.indexOf('Invalid column') > -1 || tmpErrorStr.indexOf('Invalid object') > -1 || tmpErrorStr.indexOf('no such column') > -1 || tmpErrorStr.indexOf('no such table') > -1)
|
|
325
|
+
{
|
|
326
|
+
this.log.warn(`${this.EntitySchema.TableName}: local table schema mismatch (${pReadError}); skipping sync.`);
|
|
327
|
+
return fCallback();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return this._syncInternal(fCallback);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
_syncInternal(fCallback)
|
|
335
|
+
{
|
|
339
336
|
this.operation.createTimeStamp('EntityInitialSync');
|
|
340
337
|
|
|
341
338
|
this.log.info(`Syncing ${this.EntitySchema.TableName} (PageSize: ${this.PageSize}, SyncDeletedRecords: ${this.SyncDeletedRecords})`);
|
|
@@ -42,6 +42,10 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
42
42
|
this.SyncDeletedRecords = this.options.SyncDeletedRecords || false;
|
|
43
43
|
this.MaxRecordsPerEntity = this.options.MaxRecordsPerEntity || 0;
|
|
44
44
|
|
|
45
|
+
// Minimum range size for bisection -- when a range is this small or smaller,
|
|
46
|
+
// pull all records in the range from the server instead of subdividing further.
|
|
47
|
+
this.BisectMinRangeSize = this.options.BisectMinRangeSize || 1000;
|
|
48
|
+
|
|
45
49
|
this.Meadow = false;
|
|
46
50
|
|
|
47
51
|
this.operation = new libMeadowOperation(this.fable);
|
|
@@ -62,46 +66,20 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
62
66
|
{
|
|
63
67
|
return this.Meadow.provider.getProvider().createTable(this.EntitySchema, (pCreateError) =>
|
|
64
68
|
{
|
|
65
|
-
|
|
66
|
-
{
|
|
67
|
-
// Validate local table schema with a lightweight read
|
|
68
|
-
const tmpValidationQuery = this.Meadow.query;
|
|
69
|
-
tmpValidationQuery.setCap(1);
|
|
70
|
-
tmpValidationQuery.setDisableDeleteTracking(true);
|
|
71
|
-
this.Meadow.doRead(tmpValidationQuery,
|
|
72
|
-
(pReadError) =>
|
|
73
|
-
{
|
|
74
|
-
if (pReadError)
|
|
75
|
-
{
|
|
76
|
-
let tmpErrorStr = (typeof(pReadError) === 'string') ? pReadError : JSON.stringify(pReadError);
|
|
77
|
-
// Only skip sync for schema-specific errors (invalid column/object name)
|
|
78
|
-
// Generic provider errors (e.g. prepared statement failures) should not block sync
|
|
79
|
-
if (tmpErrorStr.indexOf('Invalid column') > -1 || tmpErrorStr.indexOf('Invalid object') > -1 || tmpErrorStr.indexOf('no such column') > -1 || tmpErrorStr.indexOf('no such table') > -1)
|
|
80
|
-
{
|
|
81
|
-
this.log.warn(`${this.EntitySchema.TableName}: local table schema validation failed (${pReadError}); this entity will be skipped during sync.`);
|
|
82
|
-
this.skipSync = true;
|
|
83
|
-
}
|
|
84
|
-
else
|
|
85
|
-
{
|
|
86
|
-
this.log.warn(`${this.EntitySchema.TableName}: validation read returned error (${pReadError}); sync will proceed.`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return fCallback(pPriorError);
|
|
90
|
-
});
|
|
91
|
-
};
|
|
69
|
+
|
|
92
70
|
const tmpGUIDColumn = this.EntitySchema.Columns.find((c) => c.DataType == 'GUID');
|
|
93
71
|
const tmpDeletedColumn = this.EntitySchema.Columns.find((c) => c.Column == 'Deleted');
|
|
94
72
|
|
|
95
73
|
if (!tmpGUIDColumn && !tmpDeletedColumn)
|
|
96
74
|
{
|
|
97
75
|
this.log.info(`No GUID or Deleted columns for ${this.EntitySchema.TableName}; skipping index creation`);
|
|
98
|
-
return
|
|
76
|
+
return fCallback(pCreateError);
|
|
99
77
|
}
|
|
100
78
|
|
|
101
79
|
if (!this.fable.MeadowConnectionManager || !this.fable.MeadowConnectionManager.ConnectionPool)
|
|
102
80
|
{
|
|
103
81
|
this.log.info(`No connection manager available; skipping index creation for ${this.EntitySchema.TableName}`);
|
|
104
|
-
return
|
|
82
|
+
return fCallback(pCreateError);
|
|
105
83
|
}
|
|
106
84
|
|
|
107
85
|
let tmpAnticipate = this.fable.newAnticipate();
|
|
@@ -121,7 +99,7 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
121
99
|
return this.fable.MeadowConnectionManager.createIndex(this.EntitySchema, tmpDeletedColumn, false, fNext);
|
|
122
100
|
});
|
|
123
101
|
}
|
|
124
|
-
tmpAnticipate.wait((pIndexError) => { return
|
|
102
|
+
tmpAnticipate.wait((pIndexError) => { return fCallback(pCreateError); });
|
|
125
103
|
});
|
|
126
104
|
}
|
|
127
105
|
return fCallback();
|
|
@@ -170,6 +148,426 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
170
148
|
return tmpRecordToCommit;
|
|
171
149
|
}
|
|
172
150
|
|
|
151
|
+
// ---- REST / Local query helpers ----
|
|
152
|
+
|
|
153
|
+
// Format a date value for use in Meadow REST filter expressions (FBV).
|
|
154
|
+
_formatDateForFilter(pDate)
|
|
155
|
+
{
|
|
156
|
+
return this.fable.Dates.dayJS.utc(pDate).format('YYYY-MM-DDTHH:mm:ss.SSS');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Get a count from the remote server, optionally filtered.
|
|
160
|
+
_getServerCount(pFilter, fCallback)
|
|
161
|
+
{
|
|
162
|
+
const tmpURL = pFilter
|
|
163
|
+
? `${this.EntitySchema.TableName}s/Count/FilteredTo/${pFilter}`
|
|
164
|
+
: `${this.EntitySchema.TableName}s/Count`;
|
|
165
|
+
this.fable.MeadowCloneRestClient.getJSON(tmpURL,
|
|
166
|
+
(pError, pResponse, pBody) =>
|
|
167
|
+
{
|
|
168
|
+
if (pError)
|
|
169
|
+
{
|
|
170
|
+
return fCallback(pError);
|
|
171
|
+
}
|
|
172
|
+
if (pBody && pBody.hasOwnProperty('Count'))
|
|
173
|
+
{
|
|
174
|
+
return fCallback(null, pBody.Count);
|
|
175
|
+
}
|
|
176
|
+
return fCallback(null, 0);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Get a page of records from the remote server with a Meadow filter expression.
|
|
181
|
+
_getServerRecords(pFilter, pOffset, pPageSize, fCallback)
|
|
182
|
+
{
|
|
183
|
+
const tmpURL = `${this.EntitySchema.TableName}s/FilteredTo/${pFilter}/${pOffset}/${pPageSize}`;
|
|
184
|
+
this.fable.MeadowCloneRestClient.getJSON(tmpURL,
|
|
185
|
+
(pError, pResponse, pBody) =>
|
|
186
|
+
{
|
|
187
|
+
if (pError)
|
|
188
|
+
{
|
|
189
|
+
return fCallback(pError);
|
|
190
|
+
}
|
|
191
|
+
if (pBody && Array.isArray(pBody))
|
|
192
|
+
{
|
|
193
|
+
return fCallback(null, pBody);
|
|
194
|
+
}
|
|
195
|
+
return fCallback(null, []);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Get a count from the local database with optional ID range filters.
|
|
200
|
+
_getLocalCount(pMinID, pMaxID, fCallback)
|
|
201
|
+
{
|
|
202
|
+
const tmpQuery = this.Meadow.query;
|
|
203
|
+
if (pMinID > 0)
|
|
204
|
+
{
|
|
205
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMinID, '>=');
|
|
206
|
+
}
|
|
207
|
+
if (pMaxID > 0)
|
|
208
|
+
{
|
|
209
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMaxID, '<=');
|
|
210
|
+
}
|
|
211
|
+
if (!this._hasDeletedColumn)
|
|
212
|
+
{
|
|
213
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
214
|
+
}
|
|
215
|
+
this.Meadow.doCount(tmpQuery,
|
|
216
|
+
(pError, pQuery, pCount) =>
|
|
217
|
+
{
|
|
218
|
+
if (pError)
|
|
219
|
+
{
|
|
220
|
+
return fCallback(pError);
|
|
221
|
+
}
|
|
222
|
+
return fCallback(null, pCount);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Get the max UpdateDate from local records in an ID range.
|
|
227
|
+
_getLocalMaxUpdateDate(pMinID, pMaxID, fCallback)
|
|
228
|
+
{
|
|
229
|
+
const tmpQuery = this.Meadow.query;
|
|
230
|
+
if (pMinID > 0)
|
|
231
|
+
{
|
|
232
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMinID, '>=');
|
|
233
|
+
}
|
|
234
|
+
if (pMaxID > 0)
|
|
235
|
+
{
|
|
236
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMaxID, '<=');
|
|
237
|
+
}
|
|
238
|
+
tmpQuery.setSort({ Column: 'UpdateDate', Direction: 'Descending' });
|
|
239
|
+
tmpQuery.setCap(1);
|
|
240
|
+
if (!this._hasDeletedColumn)
|
|
241
|
+
{
|
|
242
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
243
|
+
}
|
|
244
|
+
this.Meadow.doRead(tmpQuery,
|
|
245
|
+
(pError, pQuery, pRecord) =>
|
|
246
|
+
{
|
|
247
|
+
if (pError)
|
|
248
|
+
{
|
|
249
|
+
return fCallback(pError);
|
|
250
|
+
}
|
|
251
|
+
if (!pRecord || !pRecord.UpdateDate)
|
|
252
|
+
{
|
|
253
|
+
return fCallback(null, false);
|
|
254
|
+
}
|
|
255
|
+
return fCallback(null, pRecord.UpdateDate);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Get the min UpdateDate from local records in an ID range.
|
|
260
|
+
_getLocalMinUpdateDate(pMinID, pMaxID, fCallback)
|
|
261
|
+
{
|
|
262
|
+
const tmpQuery = this.Meadow.query;
|
|
263
|
+
if (pMinID > 0)
|
|
264
|
+
{
|
|
265
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMinID, '>=');
|
|
266
|
+
}
|
|
267
|
+
if (pMaxID > 0)
|
|
268
|
+
{
|
|
269
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pMaxID, '<=');
|
|
270
|
+
}
|
|
271
|
+
tmpQuery.setSort({ Column: 'UpdateDate', Direction: 'Ascending' });
|
|
272
|
+
tmpQuery.setCap(1);
|
|
273
|
+
if (!this._hasDeletedColumn)
|
|
274
|
+
{
|
|
275
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
276
|
+
}
|
|
277
|
+
this.Meadow.doRead(tmpQuery,
|
|
278
|
+
(pError, pQuery, pRecord) =>
|
|
279
|
+
{
|
|
280
|
+
if (pError)
|
|
281
|
+
{
|
|
282
|
+
return fCallback(pError);
|
|
283
|
+
}
|
|
284
|
+
if (!pRecord || !pRecord.UpdateDate)
|
|
285
|
+
{
|
|
286
|
+
return fCallback(null, false);
|
|
287
|
+
}
|
|
288
|
+
return fCallback(null, pRecord.UpdateDate);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Upsert a single record from the server into the local database.
|
|
293
|
+
_upsertRecord(pServerRecord, fCallback)
|
|
294
|
+
{
|
|
295
|
+
const tmpRecordToCommit = this.marshalRecord(pServerRecord);
|
|
296
|
+
|
|
297
|
+
const tmpQuery = this.Meadow.query;
|
|
298
|
+
tmpQuery.addFilter(this.DefaultIdentifier, pServerRecord[this.DefaultIdentifier]);
|
|
299
|
+
if (!this._hasDeletedColumn)
|
|
300
|
+
{
|
|
301
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.Meadow.doRead(tmpQuery,
|
|
305
|
+
(pReadError, pQuery, pLocalRecord) =>
|
|
306
|
+
{
|
|
307
|
+
if (pReadError)
|
|
308
|
+
{
|
|
309
|
+
this.fable.log.error(`Error reading local record ${this.EntitySchema.TableName} ID ${pServerRecord[this.DefaultIdentifier]}: ${pReadError}`);
|
|
310
|
+
return fCallback();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const tmpSyncQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
314
|
+
tmpSyncQuery.setDisableAutoIdentity(true);
|
|
315
|
+
tmpSyncQuery.setDisableAutoDateStamp(true);
|
|
316
|
+
tmpSyncQuery.setDisableAutoUserStamp(true);
|
|
317
|
+
tmpSyncQuery.setDisableDeleteTracking(true);
|
|
318
|
+
|
|
319
|
+
if (!pLocalRecord)
|
|
320
|
+
{
|
|
321
|
+
// Record does not exist locally -- create
|
|
322
|
+
tmpSyncQuery.AllowIdentityInsert = true;
|
|
323
|
+
this.Meadow.doCreate(tmpSyncQuery,
|
|
324
|
+
(pCreateError) =>
|
|
325
|
+
{
|
|
326
|
+
if (pCreateError)
|
|
327
|
+
{
|
|
328
|
+
let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
|
|
329
|
+
if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
|
|
330
|
+
{
|
|
331
|
+
// GUID conflict -- fall back to update
|
|
332
|
+
this.log.warn(`Duplicate key on create for ${this.EntitySchema.TableName} ID ${pServerRecord[this.DefaultIdentifier]}; falling back to update.`);
|
|
333
|
+
const tmpFallbackQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
334
|
+
tmpFallbackQuery.setDisableAutoIdentity(true);
|
|
335
|
+
tmpFallbackQuery.setDisableAutoDateStamp(true);
|
|
336
|
+
tmpFallbackQuery.setDisableAutoUserStamp(true);
|
|
337
|
+
tmpFallbackQuery.setDisableDeleteTracking(true);
|
|
338
|
+
this.Meadow.doUpdate(tmpFallbackQuery,
|
|
339
|
+
(pUpdateError) =>
|
|
340
|
+
{
|
|
341
|
+
if (pUpdateError)
|
|
342
|
+
{
|
|
343
|
+
this.log.error(`Fallback update also failed for ${this.EntitySchema.TableName} ID ${pServerRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
|
|
344
|
+
}
|
|
345
|
+
return fCallback();
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
this.log.error(`Error creating record ${this.EntitySchema.TableName}: ${pCreateError}`, pCreateError);
|
|
350
|
+
return fCallback();
|
|
351
|
+
}
|
|
352
|
+
return fCallback();
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
else
|
|
356
|
+
{
|
|
357
|
+
// Record exists locally -- update
|
|
358
|
+
this.Meadow.doUpdate(tmpSyncQuery,
|
|
359
|
+
(pUpdateError) =>
|
|
360
|
+
{
|
|
361
|
+
if (pUpdateError)
|
|
362
|
+
{
|
|
363
|
+
this.log.error(`Error updating record ${this.EntitySchema.TableName}: ${pUpdateError}`, pUpdateError);
|
|
364
|
+
}
|
|
365
|
+
return fCallback();
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Pull all records from server matching a filter expression and upsert them locally.
|
|
372
|
+
// Fetches in pages of this.PageSize.
|
|
373
|
+
_pullServerRecords(pFilter, pEstimatedCount, fCallback)
|
|
374
|
+
{
|
|
375
|
+
if (pEstimatedCount < 1)
|
|
376
|
+
{
|
|
377
|
+
return fCallback(null, 0);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let tmpSyncedCount = 0;
|
|
381
|
+
let tmpOffset = 0;
|
|
382
|
+
let tmpDone = false;
|
|
383
|
+
|
|
384
|
+
let tmpRecordCap = (this.MaxRecordsPerEntity > 0)
|
|
385
|
+
? Math.min(pEstimatedCount, this.MaxRecordsPerEntity)
|
|
386
|
+
: pEstimatedCount;
|
|
387
|
+
|
|
388
|
+
const fFetchPage = () =>
|
|
389
|
+
{
|
|
390
|
+
if (tmpDone || tmpOffset >= tmpRecordCap)
|
|
391
|
+
{
|
|
392
|
+
return fCallback(null, tmpSyncedCount);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
this._getServerRecords(pFilter, tmpOffset, this.PageSize,
|
|
396
|
+
(pError, pRecords) =>
|
|
397
|
+
{
|
|
398
|
+
if (pError)
|
|
399
|
+
{
|
|
400
|
+
this.fable.log.error(`Error fetching ${this.EntitySchema.TableName} page at offset ${tmpOffset}: ${pError}`);
|
|
401
|
+
return fCallback(pError, tmpSyncedCount);
|
|
402
|
+
}
|
|
403
|
+
if (!pRecords || pRecords.length < 1)
|
|
404
|
+
{
|
|
405
|
+
tmpDone = true;
|
|
406
|
+
return fCallback(null, tmpSyncedCount);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
this.fable.Utility.eachLimit(pRecords, 5,
|
|
410
|
+
(pRecord, fRecordDone) =>
|
|
411
|
+
{
|
|
412
|
+
this._upsertRecord(pRecord,
|
|
413
|
+
() =>
|
|
414
|
+
{
|
|
415
|
+
tmpSyncedCount++;
|
|
416
|
+
return fRecordDone();
|
|
417
|
+
});
|
|
418
|
+
},
|
|
419
|
+
(pUpsertError) =>
|
|
420
|
+
{
|
|
421
|
+
tmpOffset += this.PageSize;
|
|
422
|
+
if (pRecords.length < this.PageSize)
|
|
423
|
+
{
|
|
424
|
+
tmpDone = true;
|
|
425
|
+
return fCallback(null, tmpSyncedCount);
|
|
426
|
+
}
|
|
427
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulled ${tmpSyncedCount} of ~${tmpRecordCap} records...`);
|
|
428
|
+
return fFetchPage();
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
fFetchPage();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ---- Bisection logic ----
|
|
437
|
+
|
|
438
|
+
// Compare a local ID range against the server. If counts or date boundaries
|
|
439
|
+
// differ, subdivide until the range is small enough, then pull all records in
|
|
440
|
+
// the range from the server to bring local in sync.
|
|
441
|
+
_bisectRange(pMinID, pMaxID, pDepth, fCallback)
|
|
442
|
+
{
|
|
443
|
+
const tmpRangeSize = pMaxID - pMinID + 1;
|
|
444
|
+
const tmpIDCol = this.DefaultIdentifier;
|
|
445
|
+
const tmpRangeFilter = `FBV~${tmpIDCol}~GE~${pMinID}~FBV~${tmpIDCol}~LE~${pMaxID}`;
|
|
446
|
+
|
|
447
|
+
// Get local stats for this range
|
|
448
|
+
this._getLocalCount(pMinID, pMaxID,
|
|
449
|
+
(pLocalCountError, pLocalCount) =>
|
|
450
|
+
{
|
|
451
|
+
if (pLocalCountError)
|
|
452
|
+
{
|
|
453
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: bisect local count error for range ${pMinID}-${pMaxID}: ${pLocalCountError}`);
|
|
454
|
+
return fCallback();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Get server count for this range
|
|
458
|
+
this._getServerCount(tmpRangeFilter,
|
|
459
|
+
(pServerCountError, pServerCount) =>
|
|
460
|
+
{
|
|
461
|
+
if (pServerCountError)
|
|
462
|
+
{
|
|
463
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: bisect server count error for range ${pMinID}-${pMaxID}: ${pServerCountError}`);
|
|
464
|
+
return fCallback();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// If counts match, check UpdateDate boundaries for this range
|
|
468
|
+
if (pLocalCount === pServerCount)
|
|
469
|
+
{
|
|
470
|
+
if (!this._hasUpdateDate)
|
|
471
|
+
{
|
|
472
|
+
// No UpdateDate column -- counts match, assume in sync
|
|
473
|
+
return fCallback();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Compare max and min UpdateDate for this range
|
|
477
|
+
this._getLocalMaxUpdateDate(pMinID, pMaxID,
|
|
478
|
+
(pLocalMaxErr, pLocalMaxDate) =>
|
|
479
|
+
{
|
|
480
|
+
if (pLocalMaxErr || !pLocalMaxDate)
|
|
481
|
+
{
|
|
482
|
+
return fCallback();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Get server max UpdateDate for this range (1 record, sorted desc)
|
|
486
|
+
const tmpMaxDateFilter = `${tmpRangeFilter}~FSF~UpdateDate~DESC~DESC`;
|
|
487
|
+
this._getServerRecords(tmpMaxDateFilter, 0, 1,
|
|
488
|
+
(pServerMaxErr, pServerMaxRecords) =>
|
|
489
|
+
{
|
|
490
|
+
if (pServerMaxErr || !pServerMaxRecords || pServerMaxRecords.length < 1)
|
|
491
|
+
{
|
|
492
|
+
return fCallback();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const tmpServerMaxDate = pServerMaxRecords[0].UpdateDate;
|
|
496
|
+
const tmpMaxDateDiff = Math.abs(this.fable.Dates.dayJS.utc(pLocalMaxDate).diff(this.fable.Dates.dayJS.utc(tmpServerMaxDate)));
|
|
497
|
+
|
|
498
|
+
if (tmpMaxDateDiff < 5)
|
|
499
|
+
{
|
|
500
|
+
// Max dates match and counts match -- this range is in sync
|
|
501
|
+
return fCallback();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Dates differ even though counts match -- records have been modified.
|
|
505
|
+
// If range is small enough, pull all records; otherwise subdivide.
|
|
506
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: date mismatch in range ${pMinID}-${pMaxID} (local max: ${pLocalMaxDate}, server max: ${tmpServerMaxDate})`);
|
|
507
|
+
if (tmpRangeSize <= this.BisectMinRangeSize)
|
|
508
|
+
{
|
|
509
|
+
return this._pullRangeFromServer(pMinID, pMaxID, fCallback);
|
|
510
|
+
}
|
|
511
|
+
return this._subdivideRange(pMinID, pMaxID, pDepth, fCallback);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Counts differ
|
|
518
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: count mismatch in range ${pMinID}-${pMaxID} (local: ${pLocalCount}, server: ${pServerCount})`);
|
|
519
|
+
|
|
520
|
+
if (tmpRangeSize <= this.BisectMinRangeSize)
|
|
521
|
+
{
|
|
522
|
+
return this._pullRangeFromServer(pMinID, pMaxID, fCallback);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return this._subdivideRange(pMinID, pMaxID, pDepth, fCallback);
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Split an ID range in half and bisect each half.
|
|
531
|
+
_subdivideRange(pMinID, pMaxID, pDepth, fCallback)
|
|
532
|
+
{
|
|
533
|
+
const tmpMidID = Math.floor((pMinID + pMaxID) / 2);
|
|
534
|
+
|
|
535
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: subdividing range ${pMinID}-${pMaxID} at ID ${tmpMidID} (depth ${pDepth})`);
|
|
536
|
+
|
|
537
|
+
// Bisect lower half, then upper half
|
|
538
|
+
this._bisectRange(pMinID, tmpMidID, pDepth + 1,
|
|
539
|
+
() =>
|
|
540
|
+
{
|
|
541
|
+
this._bisectRange(tmpMidID + 1, pMaxID, pDepth + 1, fCallback);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Pull all records from the server in an ID range and upsert them locally.
|
|
546
|
+
_pullRangeFromServer(pMinID, pMaxID, fCallback)
|
|
547
|
+
{
|
|
548
|
+
const tmpIDCol = this.DefaultIdentifier;
|
|
549
|
+
const tmpFilter = `FBV~${tmpIDCol}~GE~${pMinID}~FBV~${tmpIDCol}~LE~${pMaxID}~FSF~${tmpIDCol}~ASC~ASC`;
|
|
550
|
+
const tmpEstimatedCount = pMaxID - pMinID + 1;
|
|
551
|
+
|
|
552
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulling range ${pMinID}-${pMaxID} from server (~${tmpEstimatedCount} records)`);
|
|
553
|
+
|
|
554
|
+
this._pullServerRecords(tmpFilter, tmpEstimatedCount,
|
|
555
|
+
(pError, pSyncedCount) =>
|
|
556
|
+
{
|
|
557
|
+
if (pError)
|
|
558
|
+
{
|
|
559
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: error pulling range ${pMinID}-${pMaxID}: ${pError}`);
|
|
560
|
+
}
|
|
561
|
+
else
|
|
562
|
+
{
|
|
563
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: synced ${pSyncedCount} records in range ${pMinID}-${pMaxID}`);
|
|
564
|
+
}
|
|
565
|
+
return fCallback();
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ---- Deleted records sync ----
|
|
570
|
+
|
|
173
571
|
syncDeletedRecords(fCallback)
|
|
174
572
|
{
|
|
175
573
|
const tmpDeletedColumn = this.EntitySchema.Columns.find((c) => c.Column == 'Deleted');
|
|
@@ -181,7 +579,6 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
181
579
|
|
|
182
580
|
this.fable.log.info(`Checking for deleted records on server for ${this.EntitySchema.TableName}...`);
|
|
183
581
|
|
|
184
|
-
// Get the count of deleted records from the server.
|
|
185
582
|
// The explicit FBV~Deleted~EQ~1 filter overrides foxhound's automatic Deleted=0 filter.
|
|
186
583
|
this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}s/Count/FilteredTo/FBV~Deleted~EQ~1`,
|
|
187
584
|
(pError, pResponse, pBody) =>
|
|
@@ -303,149 +700,7 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
303
700
|
});
|
|
304
701
|
}
|
|
305
702
|
|
|
306
|
-
|
|
307
|
-
{
|
|
308
|
-
tmpAnticipate.anticipate(
|
|
309
|
-
(fNext) =>
|
|
310
|
-
{
|
|
311
|
-
const tmpURLPartial = `${this.EntitySchema.TableName}s/FilteredTo/FBV~${this.DefaultIdentifier}~GT~${tmpSyncState.LastRequestedID}~FSF~${this.DefaultIdentifier}~ASC~ASC/0/${this.PageSize}`;
|
|
312
|
-
this.fable.MeadowCloneRestClient.getJSON(tmpURLPartial,
|
|
313
|
-
(pDownloadError, pResponse, pBody) =>
|
|
314
|
-
{
|
|
315
|
-
if (pDownloadError)
|
|
316
|
-
{
|
|
317
|
-
this.fable.log.error(`Error getting URL Partial [${tmpURLPartial}]: ${pDownloadError}`, { Error: pDownloadError });
|
|
318
|
-
return fNext();
|
|
319
|
-
}
|
|
320
|
-
if (pBody && Array.isArray(pBody) && pBody.length > 0)
|
|
321
|
-
{
|
|
322
|
-
for (let i = 0; i < pBody.length; i++)
|
|
323
|
-
{
|
|
324
|
-
const tmpRecord = pBody[i];
|
|
325
|
-
|
|
326
|
-
tmpAnticipate.anticipate(
|
|
327
|
-
(fNextEntityRecordSync) =>
|
|
328
|
-
{
|
|
329
|
-
const tmpQuery = this.Meadow.query;
|
|
330
|
-
|
|
331
|
-
if (tmpRecord[this.DefaultIdentifier] > tmpSyncState.LastRequestedID)
|
|
332
|
-
{
|
|
333
|
-
tmpSyncState.LastRequestedID = tmpRecord[this.DefaultIdentifier];
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if ((typeof(tmpRecord[this.DefaultIdentifier]) !== 'undefined') && (tmpRecord[this.DefaultIdentifier] > 0))
|
|
337
|
-
{
|
|
338
|
-
tmpQuery.addFilter(this.DefaultIdentifier, tmpRecord[this.DefaultIdentifier]);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (!tmpSyncState.HasDeletedColumn)
|
|
342
|
-
{
|
|
343
|
-
tmpQuery.setDisableDeleteTracking(true);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
this.Meadow.doRead(tmpQuery,
|
|
347
|
-
(pReadError, pQuery, pRecord) =>
|
|
348
|
-
{
|
|
349
|
-
if (pReadError)
|
|
350
|
-
{
|
|
351
|
-
this.fable.log.error(`Error reading record ${this.EntitySchema.TableName}: ${pReadError}`, { Error: pReadError, PassedRecord: tmpRecord });
|
|
352
|
-
return fNextEntityRecordSync();
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (pRecord)
|
|
356
|
-
{
|
|
357
|
-
const tmpAgeDifference = this.fable.Dates.dayJS(tmpRecord.UpdateDate).diff(this.fable.Dates.dayJS(pRecord.UpdateDate));
|
|
358
|
-
|
|
359
|
-
if (Math.abs(tmpAgeDifference) < 5)
|
|
360
|
-
{
|
|
361
|
-
return fNextEntityRecordSync();
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
this.fable.log.info(`Syncing ${this.EntitySchema.TableName} record ${tmpRecord[this.DefaultIdentifier]} with age difference of ${tmpAgeDifference} ms.`);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const tmpRecordToCommit = this.marshalRecord(tmpRecord);
|
|
368
|
-
|
|
369
|
-
const tmpSyncQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
370
|
-
|
|
371
|
-
tmpSyncQuery.setDisableAutoIdentity(true);
|
|
372
|
-
tmpSyncQuery.setDisableAutoDateStamp(true);
|
|
373
|
-
tmpSyncQuery.setDisableAutoUserStamp(true);
|
|
374
|
-
tmpSyncQuery.setDisableDeleteTracking(true);
|
|
375
|
-
|
|
376
|
-
if (!pRecord)
|
|
377
|
-
{
|
|
378
|
-
// Record not found -- create
|
|
379
|
-
tmpSyncQuery.AllowIdentityInsert = true;
|
|
380
|
-
|
|
381
|
-
this.Meadow.doCreate(tmpSyncQuery,
|
|
382
|
-
(pCreateError) =>
|
|
383
|
-
{
|
|
384
|
-
if (pCreateError)
|
|
385
|
-
{
|
|
386
|
-
let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
|
|
387
|
-
if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
|
|
388
|
-
{
|
|
389
|
-
// Duplicate key (likely GUID conflict) -- fall back to update
|
|
390
|
-
this.log.warn(`Duplicate key on create for ${this.EntitySchema.TableName} ID ${tmpRecord[this.DefaultIdentifier]}; falling back to update.`);
|
|
391
|
-
const tmpFallbackQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
392
|
-
tmpFallbackQuery.setDisableAutoIdentity(true);
|
|
393
|
-
tmpFallbackQuery.setDisableAutoDateStamp(true);
|
|
394
|
-
tmpFallbackQuery.setDisableAutoUserStamp(true);
|
|
395
|
-
tmpFallbackQuery.setDisableDeleteTracking(true);
|
|
396
|
-
this.Meadow.doUpdate(tmpFallbackQuery,
|
|
397
|
-
(pUpdateError) =>
|
|
398
|
-
{
|
|
399
|
-
if (pUpdateError)
|
|
400
|
-
{
|
|
401
|
-
this.log.error(`Fallback update also failed for ${this.EntitySchema.TableName} ID ${tmpRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
|
|
402
|
-
return fNextEntityRecordSync();
|
|
403
|
-
}
|
|
404
|
-
this.operation.incrementProgressTrackerStatus(`UpdateSync-${this.EntitySchema.TableName}`, 1);
|
|
405
|
-
return fNextEntityRecordSync();
|
|
406
|
-
});
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
this.log.error(`Error creating record ${this.EntitySchema.TableName}: ${pCreateError}`, pCreateError);
|
|
410
|
-
return fNextEntityRecordSync();
|
|
411
|
-
}
|
|
412
|
-
this.operation.incrementProgressTrackerStatus(`UpdateSync-${this.EntitySchema.TableName}`, 1);
|
|
413
|
-
return fNextEntityRecordSync();
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
else
|
|
417
|
-
{
|
|
418
|
-
// Record found -- update
|
|
419
|
-
this.Meadow.doUpdate(tmpSyncQuery,
|
|
420
|
-
(pUpdateError) =>
|
|
421
|
-
{
|
|
422
|
-
if (pUpdateError)
|
|
423
|
-
{
|
|
424
|
-
this.log.error(`Error updating record ${this.EntitySchema.TableName}: ${pUpdateError}`, pUpdateError);
|
|
425
|
-
return fNextEntityRecordSync();
|
|
426
|
-
}
|
|
427
|
-
this.operation.incrementProgressTrackerStatus(`UpdateSync-${this.EntitySchema.TableName}`, 1);
|
|
428
|
-
return fNextEntityRecordSync();
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
});
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
tmpSyncState.RequestsPerformed++;
|
|
435
|
-
if (tmpSyncState.RequestsPerformed < tmpSyncState.EstimatedRequestCount)
|
|
436
|
-
{
|
|
437
|
-
this.fable.log.info(`Syncing ${this.EntitySchema.TableName} request ${tmpSyncState.RequestsPerformed} of ${tmpSyncState.EstimatedRequestCount}...`);
|
|
438
|
-
this.addSyncAnticipateEntry(tmpSyncState, tmpAnticipate);
|
|
439
|
-
}
|
|
440
|
-
return fNext();
|
|
441
|
-
}
|
|
442
|
-
else
|
|
443
|
-
{
|
|
444
|
-
return fNext();
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
});
|
|
448
|
-
}
|
|
703
|
+
// ---- Main sync entry point ----
|
|
449
704
|
|
|
450
705
|
sync(fCallback)
|
|
451
706
|
{
|
|
@@ -455,55 +710,69 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
455
710
|
return fCallback();
|
|
456
711
|
}
|
|
457
712
|
|
|
458
|
-
|
|
713
|
+
// Validate local table schema with a lightweight read before syncing
|
|
714
|
+
const tmpValidationQuery = this.Meadow.query;
|
|
715
|
+
tmpValidationQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
|
|
716
|
+
tmpValidationQuery.setCap(1);
|
|
717
|
+
tmpValidationQuery.setDisableDeleteTracking(true);
|
|
718
|
+
this.Meadow.doRead(tmpValidationQuery,
|
|
719
|
+
(pReadError) =>
|
|
720
|
+
{
|
|
721
|
+
if (pReadError)
|
|
722
|
+
{
|
|
723
|
+
let tmpErrorStr = (typeof(pReadError) === 'string') ? pReadError : JSON.stringify(pReadError);
|
|
724
|
+
if (tmpErrorStr.indexOf('Invalid column') > -1 || tmpErrorStr.indexOf('Invalid object') > -1 || tmpErrorStr.indexOf('no such column') > -1 || tmpErrorStr.indexOf('no such table') > -1)
|
|
725
|
+
{
|
|
726
|
+
this.log.warn(`${this.EntitySchema.TableName}: local table schema mismatch (${pReadError}); skipping sync.`);
|
|
727
|
+
return fCallback();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return this._syncInternal(fCallback);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
459
733
|
|
|
460
|
-
|
|
734
|
+
_syncInternal(fCallback)
|
|
735
|
+
{
|
|
736
|
+
this.operation.createTimeStamp('EntityOngoingSync');
|
|
461
737
|
|
|
462
738
|
const tmpSyncState = (
|
|
463
739
|
{
|
|
464
|
-
Local: { MaxIDEntity: -1, RecordCount: 0
|
|
465
|
-
Server: { MaxIDEntity: -1, RecordCount: 0
|
|
740
|
+
Local: { MaxIDEntity: -1, RecordCount: 0 },
|
|
741
|
+
Server: { MaxIDEntity: -1, RecordCount: 0 },
|
|
466
742
|
});
|
|
467
743
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
{
|
|
472
|
-
if (!this.EntitySchema || !this.EntitySchema.MeadowSchema || !Array.isArray(this.EntitySchema.MeadowSchema.Schema))
|
|
473
|
-
{
|
|
474
|
-
return fStageComplete('MeadowSyncEntityOngoing requires a valid MeadowEntitySchema.MeadowSchema.Schema.');
|
|
475
|
-
}
|
|
744
|
+
// Detect schema capabilities
|
|
745
|
+
this._hasUpdateDate = false;
|
|
746
|
+
this._hasDeletedColumn = false;
|
|
476
747
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
748
|
+
if (this.EntitySchema && this.EntitySchema.MeadowSchema && Array.isArray(this.EntitySchema.MeadowSchema.Schema))
|
|
749
|
+
{
|
|
750
|
+
for (let i = 0; i < this.EntitySchema.MeadowSchema.Schema.length; i++)
|
|
751
|
+
{
|
|
752
|
+
const tmpColumn = this.EntitySchema.MeadowSchema.Schema[i];
|
|
753
|
+
if (tmpColumn.Column == 'UpdateDate')
|
|
754
|
+
{
|
|
755
|
+
this._hasUpdateDate = true;
|
|
756
|
+
}
|
|
757
|
+
if (tmpColumn.Type == 'Deleted' || tmpColumn.Column == 'Deleted')
|
|
758
|
+
{
|
|
759
|
+
this._hasDeletedColumn = true;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
490
763
|
|
|
491
|
-
|
|
492
|
-
{
|
|
493
|
-
this.log.info(`Entity ${this.EntitySchema.TableName} has UpdateDate column.`);
|
|
494
|
-
}
|
|
764
|
+
this.fable.log.info(`Syncing with ONGOING STRATEGY entity ${this.EntitySchema.TableName} (UpdateDate: ${this._hasUpdateDate}, Deleted: ${this._hasDeletedColumn})...`);
|
|
495
765
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
766
|
+
this.fable.Utility.waterfall(
|
|
767
|
+
[
|
|
768
|
+
// ---- Stage 1: Gather local stats ----
|
|
499
769
|
(fStageComplete) =>
|
|
500
770
|
{
|
|
501
|
-
//
|
|
771
|
+
// Local max ID
|
|
502
772
|
const tmpQuery = this.Meadow.query;
|
|
503
773
|
tmpQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
|
|
504
774
|
tmpQuery.setCap(1);
|
|
505
|
-
|
|
506
|
-
if (!tmpSyncState.HasDeletedColumn)
|
|
775
|
+
if (!this._hasDeletedColumn)
|
|
507
776
|
{
|
|
508
777
|
tmpQuery.setDisableDeleteTracking(true);
|
|
509
778
|
}
|
|
@@ -512,31 +781,32 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
512
781
|
{
|
|
513
782
|
if (pReadError)
|
|
514
783
|
{
|
|
515
|
-
this.fable.log.error(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}
|
|
784
|
+
this.fable.log.error(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
516
785
|
return fStageComplete(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
517
786
|
}
|
|
518
|
-
if (
|
|
787
|
+
if (pRecord)
|
|
519
788
|
{
|
|
520
|
-
|
|
521
|
-
|
|
789
|
+
tmpSyncState.Local.MaxIDEntity = pRecord[this.DefaultIdentifier];
|
|
790
|
+
this.fable.log.info(`Found local max entity ID ${this.EntitySchema.TableName}: ${tmpSyncState.Local.MaxIDEntity}`);
|
|
791
|
+
}
|
|
792
|
+
else
|
|
793
|
+
{
|
|
794
|
+
this.fable.log.info(`No local records for ${this.EntitySchema.TableName}.`);
|
|
522
795
|
}
|
|
523
|
-
this.fable.log.info(`Found local max entity ID ${this.EntitySchema.TableName}: ${pRecord[this.DefaultIdentifier]}`);
|
|
524
|
-
tmpSyncState.Local.MaxIDEntity = pRecord[this.DefaultIdentifier];
|
|
525
796
|
return fStageComplete();
|
|
526
797
|
});
|
|
527
798
|
},
|
|
528
799
|
(fStageComplete) =>
|
|
529
800
|
{
|
|
530
|
-
//
|
|
531
|
-
if (!
|
|
801
|
+
// Local max UpdateDate
|
|
802
|
+
if (!this._hasUpdateDate)
|
|
532
803
|
{
|
|
533
|
-
this.fable.log.info(`No UpdateDate column for ${this.EntitySchema.TableName}; skipping UpdateDate check.`);
|
|
534
804
|
return fStageComplete();
|
|
535
805
|
}
|
|
536
806
|
const tmpQuery = this.Meadow.query;
|
|
537
807
|
tmpQuery.setSort({ Column: 'UpdateDate', Direction: 'Descending' });
|
|
538
808
|
tmpQuery.setCap(1);
|
|
539
|
-
if (!
|
|
809
|
+
if (!this._hasDeletedColumn)
|
|
540
810
|
{
|
|
541
811
|
tmpQuery.setDisableDeleteTracking(true);
|
|
542
812
|
}
|
|
@@ -545,24 +815,22 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
545
815
|
{
|
|
546
816
|
if (pReadError)
|
|
547
817
|
{
|
|
548
|
-
this.fable.log.error(`Error reading local max UpdateDate ${this.EntitySchema.TableName}: ${pReadError}
|
|
818
|
+
this.fable.log.error(`Error reading local max UpdateDate ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
549
819
|
return fStageComplete(`Error reading local max UpdateDate ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
550
820
|
}
|
|
551
|
-
if (
|
|
821
|
+
if (pRecord && pRecord.UpdateDate)
|
|
552
822
|
{
|
|
553
|
-
|
|
554
|
-
|
|
823
|
+
tmpSyncState.Local.MaxUpdateDate = pRecord.UpdateDate;
|
|
824
|
+
this.fable.log.info(`Found local max UpdateDate ${this.EntitySchema.TableName}: ${tmpSyncState.Local.MaxUpdateDate}`);
|
|
555
825
|
}
|
|
556
|
-
this.fable.log.info(`Found local max UpdateDate ${this.EntitySchema.TableName}: ${pRecord.UpdateDate}`);
|
|
557
|
-
tmpSyncState.Local.MaxUpdateDate = pRecord.UpdateDate;
|
|
558
826
|
return fStageComplete();
|
|
559
827
|
});
|
|
560
828
|
},
|
|
561
829
|
(fStageComplete) =>
|
|
562
830
|
{
|
|
563
|
-
//
|
|
831
|
+
// Local count
|
|
564
832
|
const tmpQuery = this.Meadow.query;
|
|
565
|
-
if (!
|
|
833
|
+
if (!this._hasDeletedColumn)
|
|
566
834
|
{
|
|
567
835
|
tmpQuery.setDisableDeleteTracking(true);
|
|
568
836
|
}
|
|
@@ -571,16 +839,19 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
571
839
|
{
|
|
572
840
|
if (pCountError)
|
|
573
841
|
{
|
|
574
|
-
this.fable.log.error(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}
|
|
842
|
+
this.fable.log.error(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`);
|
|
575
843
|
return fStageComplete(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`);
|
|
576
844
|
}
|
|
577
845
|
tmpSyncState.Local.RecordCount = pCount;
|
|
846
|
+
this.fable.log.info(`Local count ${this.EntitySchema.TableName}: ${tmpSyncState.Local.RecordCount}`);
|
|
578
847
|
return fStageComplete();
|
|
579
848
|
});
|
|
580
849
|
},
|
|
850
|
+
|
|
851
|
+
// ---- Stage 2: Gather server stats ----
|
|
581
852
|
(fStageComplete) =>
|
|
582
853
|
{
|
|
583
|
-
//
|
|
854
|
+
// Server max ID
|
|
584
855
|
this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}/Max/${this.DefaultIdentifier}`,
|
|
585
856
|
(pError, pResponse, pBody) =>
|
|
586
857
|
{
|
|
@@ -591,8 +862,8 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
591
862
|
}
|
|
592
863
|
if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
|
|
593
864
|
{
|
|
594
|
-
this.fable.log.info(`Found server max entity ID ${this.EntitySchema.TableName}: ${pBody[this.DefaultIdentifier]}`);
|
|
595
865
|
tmpSyncState.Server.MaxIDEntity = pBody[this.DefaultIdentifier];
|
|
866
|
+
this.fable.log.info(`Found server max entity ID ${this.EntitySchema.TableName}: ${tmpSyncState.Server.MaxIDEntity}`);
|
|
596
867
|
}
|
|
597
868
|
else
|
|
598
869
|
{
|
|
@@ -603,85 +874,192 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
603
874
|
},
|
|
604
875
|
(fStageComplete) =>
|
|
605
876
|
{
|
|
606
|
-
//
|
|
607
|
-
this.
|
|
608
|
-
(pError,
|
|
877
|
+
// Server count
|
|
878
|
+
this._getServerCount(null,
|
|
879
|
+
(pError, pCount) =>
|
|
609
880
|
{
|
|
610
881
|
if (pError)
|
|
611
882
|
{
|
|
612
|
-
this.fable.log.warn(`Could not get server
|
|
883
|
+
this.fable.log.warn(`Could not get server count for ${this.EntitySchema.TableName} (${pError}); estimating from max ID.`);
|
|
884
|
+
tmpSyncState.Server.RecordCount = tmpSyncState.Server.MaxIDEntity > 0 ? tmpSyncState.Server.MaxIDEntity : 0;
|
|
613
885
|
return fStageComplete();
|
|
614
886
|
}
|
|
615
|
-
|
|
616
|
-
{
|
|
617
|
-
this.fable.log.info(`Found server max UpdateDate ${this.EntitySchema.TableName}: ${pBody['UpdateDate']}`);
|
|
618
|
-
tmpSyncState.Server.MaxUpdateDate = pBody.UpdateDate;
|
|
619
|
-
}
|
|
620
|
-
else
|
|
621
|
-
{
|
|
622
|
-
this.fable.log.warn(`No records found in server for max UpdateDate of ${this.EntitySchema.TableName}.`);
|
|
623
|
-
}
|
|
887
|
+
tmpSyncState.Server.RecordCount = pCount;
|
|
888
|
+
this.fable.log.info(`Server count ${this.EntitySchema.TableName}: ${tmpSyncState.Server.RecordCount}`);
|
|
624
889
|
return fStageComplete();
|
|
625
890
|
});
|
|
626
891
|
},
|
|
892
|
+
|
|
893
|
+
// ---- Stage 3: UpdateDate-based fast sync ----
|
|
894
|
+
// If we have UpdateDate, compare server record count up to our local
|
|
895
|
+
// max UpdateDate. If it matches local count, existing records are in
|
|
896
|
+
// sync and we only need to pull records newer than that date.
|
|
627
897
|
(fStageComplete) =>
|
|
628
898
|
{
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
(
|
|
899
|
+
if (!this._hasUpdateDate || !tmpSyncState.Local.MaxUpdateDate)
|
|
900
|
+
{
|
|
901
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: no UpdateDate available; skipping UpdateDate fast-sync.`);
|
|
902
|
+
tmpSyncState.UpdateDateSyncDone = false;
|
|
903
|
+
return fStageComplete();
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const tmpDateStr = this._formatDateForFilter(tmpSyncState.Local.MaxUpdateDate);
|
|
907
|
+
const tmpIDCol = this.DefaultIdentifier;
|
|
908
|
+
const tmpBeforeFilter = `FBV~UpdateDate~LE~${tmpDateStr}`;
|
|
909
|
+
|
|
910
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: checking server count with UpdateDate <= ${tmpDateStr}...`);
|
|
911
|
+
|
|
912
|
+
this._getServerCount(tmpBeforeFilter,
|
|
913
|
+
(pError, pServerCountBefore) =>
|
|
632
914
|
{
|
|
633
915
|
if (pError)
|
|
634
916
|
{
|
|
635
|
-
this.fable.log.warn(
|
|
636
|
-
tmpSyncState.
|
|
917
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: could not get server count before UpdateDate (${pError}); falling back to bisection.`);
|
|
918
|
+
tmpSyncState.UpdateDateSyncDone = false;
|
|
637
919
|
return fStageComplete();
|
|
638
920
|
}
|
|
639
|
-
|
|
921
|
+
|
|
922
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: server has ${pServerCountBefore} records with UpdateDate <= ${tmpDateStr} (local has ${tmpSyncState.Local.RecordCount})`);
|
|
923
|
+
|
|
924
|
+
if (pServerCountBefore === tmpSyncState.Local.RecordCount)
|
|
640
925
|
{
|
|
641
|
-
|
|
642
|
-
|
|
926
|
+
// Record counts match up to our max UpdateDate -- existing records are in sync.
|
|
927
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: counts match up to local max UpdateDate; existing records appear in sync.`);
|
|
928
|
+
tmpSyncState.ExistingRecordsInSync = true;
|
|
643
929
|
}
|
|
644
930
|
else
|
|
645
931
|
{
|
|
646
|
-
this.fable.log.
|
|
932
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: count mismatch before max UpdateDate (local: ${tmpSyncState.Local.RecordCount}, server: ${pServerCountBefore}); will bisect existing records.`);
|
|
933
|
+
tmpSyncState.ExistingRecordsInSync = false;
|
|
647
934
|
}
|
|
648
|
-
|
|
935
|
+
|
|
936
|
+
// Now pull records with UpdateDate > local max UpdateDate (new + modified on server)
|
|
937
|
+
const tmpAfterFilter = `FBV~UpdateDate~GT~${tmpDateStr}~FSF~${tmpIDCol}~ASC~ASC`;
|
|
938
|
+
|
|
939
|
+
this._getServerCount(`FBV~UpdateDate~GT~${tmpDateStr}`,
|
|
940
|
+
(pAfterError, pServerCountAfter) =>
|
|
941
|
+
{
|
|
942
|
+
if (pAfterError)
|
|
943
|
+
{
|
|
944
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: could not get server count after UpdateDate (${pAfterError}).`);
|
|
945
|
+
tmpSyncState.UpdateDateSyncDone = false;
|
|
946
|
+
return fStageComplete();
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: ${pServerCountAfter} records on server with UpdateDate > ${tmpDateStr}; pulling...`);
|
|
950
|
+
|
|
951
|
+
if (pServerCountAfter < 1)
|
|
952
|
+
{
|
|
953
|
+
tmpSyncState.UpdateDateSyncDone = true;
|
|
954
|
+
return fStageComplete();
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
this._pullServerRecords(tmpAfterFilter, pServerCountAfter,
|
|
958
|
+
(pPullError, pSyncedCount) =>
|
|
959
|
+
{
|
|
960
|
+
if (pPullError)
|
|
961
|
+
{
|
|
962
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: error pulling new records: ${pPullError}`);
|
|
963
|
+
}
|
|
964
|
+
else
|
|
965
|
+
{
|
|
966
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulled ${pSyncedCount} new/modified records via UpdateDate.`);
|
|
967
|
+
}
|
|
968
|
+
tmpSyncState.UpdateDateSyncDone = true;
|
|
969
|
+
return fStageComplete();
|
|
970
|
+
});
|
|
971
|
+
});
|
|
649
972
|
});
|
|
650
973
|
},
|
|
974
|
+
|
|
975
|
+
// ---- Stage 4: Bisect existing records if counts did not match ----
|
|
651
976
|
(fStageComplete) =>
|
|
652
977
|
{
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
978
|
+
// If UpdateDate sync found existing records in sync, or if we have
|
|
979
|
+
// no local data yet, skip bisection.
|
|
980
|
+
if (tmpSyncState.ExistingRecordsInSync)
|
|
981
|
+
{
|
|
982
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: existing records in sync; skipping bisection.`);
|
|
983
|
+
return fStageComplete();
|
|
984
|
+
}
|
|
659
985
|
|
|
660
|
-
|
|
661
|
-
|
|
986
|
+
// If we have no local records, there is nothing to bisect
|
|
987
|
+
if (tmpSyncState.Local.MaxIDEntity < 1)
|
|
988
|
+
{
|
|
989
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: no local records; skipping bisection.`);
|
|
990
|
+
return fStageComplete();
|
|
991
|
+
}
|
|
662
992
|
|
|
663
|
-
|
|
993
|
+
// If the UpdateDate fast-sync already ran and pulled new records,
|
|
994
|
+
// refresh local count to see if we are now in sync
|
|
995
|
+
if (tmpSyncState.UpdateDateSyncDone)
|
|
996
|
+
{
|
|
997
|
+
return this._getLocalCount(0, 0,
|
|
998
|
+
(pError, pNewLocalCount) =>
|
|
999
|
+
{
|
|
1000
|
+
if (pError || pNewLocalCount === tmpSyncState.Server.RecordCount)
|
|
1001
|
+
{
|
|
1002
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: counts now match after UpdateDate pull (${pNewLocalCount} local, ${tmpSyncState.Server.RecordCount} server); skipping bisection.`);
|
|
1003
|
+
return fStageComplete();
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: counts still differ after UpdateDate pull (${pNewLocalCount} local, ${tmpSyncState.Server.RecordCount} server); bisecting existing records...`);
|
|
1007
|
+
return this._bisectRange(1, tmpSyncState.Local.MaxIDEntity, 0, fStageComplete);
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// No UpdateDate available -- bisect the full ID range
|
|
1012
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: bisecting full ID range 1-${tmpSyncState.Local.MaxIDEntity}...`);
|
|
1013
|
+
return this._bisectRange(1, tmpSyncState.Local.MaxIDEntity, 0, fStageComplete);
|
|
664
1014
|
},
|
|
1015
|
+
|
|
1016
|
+
// ---- Stage 5: Pull any remaining new records by ID ----
|
|
1017
|
+
// If no UpdateDate sync ran (table lacks UpdateDate), pull records
|
|
1018
|
+
// with ID > local max ID.
|
|
665
1019
|
(fStageComplete) =>
|
|
666
1020
|
{
|
|
667
|
-
if (tmpSyncState.
|
|
1021
|
+
if (tmpSyncState.UpdateDateSyncDone)
|
|
1022
|
+
{
|
|
1023
|
+
// UpdateDate sync already handled new records
|
|
1024
|
+
return fStageComplete();
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (tmpSyncState.Server.MaxIDEntity <= tmpSyncState.Local.MaxIDEntity)
|
|
668
1028
|
{
|
|
669
|
-
this.fable.log.info(
|
|
1029
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: no new records by ID (server max ${tmpSyncState.Server.MaxIDEntity} <= local max ${tmpSyncState.Local.MaxIDEntity}).`);
|
|
670
1030
|
return fStageComplete();
|
|
671
1031
|
}
|
|
672
1032
|
|
|
673
|
-
this.
|
|
1033
|
+
const tmpIDCol = this.DefaultIdentifier;
|
|
1034
|
+
const tmpFilter = `FBV~${tmpIDCol}~GT~${tmpSyncState.Local.MaxIDEntity}~FSF~${tmpIDCol}~ASC~ASC`;
|
|
1035
|
+
const tmpEstimated = tmpSyncState.Server.MaxIDEntity - tmpSyncState.Local.MaxIDEntity;
|
|
674
1036
|
|
|
675
|
-
|
|
1037
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulling new records with ID > ${tmpSyncState.Local.MaxIDEntity} (~${tmpEstimated} estimated)...`);
|
|
1038
|
+
|
|
1039
|
+
this._pullServerRecords(tmpFilter, tmpEstimated,
|
|
1040
|
+
(pError, pSyncedCount) =>
|
|
1041
|
+
{
|
|
1042
|
+
if (pError)
|
|
1043
|
+
{
|
|
1044
|
+
this.fable.log.warn(`${this.EntitySchema.TableName}: error pulling new records by ID: ${pError}`);
|
|
1045
|
+
}
|
|
1046
|
+
else
|
|
1047
|
+
{
|
|
1048
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: pulled ${pSyncedCount} new records by ID.`);
|
|
1049
|
+
}
|
|
1050
|
+
return fStageComplete();
|
|
1051
|
+
});
|
|
676
1052
|
},
|
|
677
1053
|
],
|
|
678
1054
|
(pError) =>
|
|
679
1055
|
{
|
|
680
1056
|
if (pError)
|
|
681
1057
|
{
|
|
682
|
-
this.fable.log.error(`Error performing
|
|
1058
|
+
this.fable.log.error(`Error performing ongoing sync ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
|
|
683
1059
|
}
|
|
684
1060
|
|
|
1061
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: ongoing sync complete.`);
|
|
1062
|
+
|
|
685
1063
|
if (this.SyncDeletedRecords)
|
|
686
1064
|
{
|
|
687
1065
|
return this.syncDeletedRecords(() => { return fCallback(); });
|
|
@@ -87,7 +87,7 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
87
87
|
let tmpErrorCount = 0;
|
|
88
88
|
let tmpSuccessCount = 0;
|
|
89
89
|
|
|
90
|
-
this.fable.Utility.eachLimit(this.MeadowSchemaTableList,
|
|
90
|
+
this.fable.Utility.eachLimit(this.MeadowSchemaTableList, 5,
|
|
91
91
|
(pEntitySchemaName, fSyncInitializationComplete) =>
|
|
92
92
|
{
|
|
93
93
|
tmpEntityIndex++;
|