meadow-integration 1.0.6 → 1.0.9
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 +5 -5
- package/source/services/clone/Meadow-Service-RestClient.js +14 -1
- package/source/services/clone/Meadow-Service-Sync-Entity-Initial.js +185 -29
- package/source/services/clone/Meadow-Service-Sync-Entity-Ongoing.js +136 -1
- package/source/services/clone/Meadow-Service-Sync.js +43 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meadow-integration",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
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.63"
|
|
20
20
|
},
|
|
21
21
|
"mocha": {
|
|
22
22
|
"diff": true,
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"fable": "^3.1.63",
|
|
41
41
|
"fable-serviceproviderbase": "^3.0.19",
|
|
42
|
-
"meadow": "^2.0.
|
|
43
|
-
"meadow-connection-mysql": "^1.0.
|
|
44
|
-
"meadow-connection-mssql": "^1.0.
|
|
42
|
+
"meadow": "^2.0.30",
|
|
43
|
+
"meadow-connection-mysql": "^1.0.14",
|
|
44
|
+
"meadow-connection-mssql": "^1.0.16",
|
|
45
45
|
"orator": "^6.0.4",
|
|
46
46
|
"orator-serviceserver-restify": "^2.0.9",
|
|
47
47
|
"pict-service-commandlineutility": "^1.0.19"
|
|
@@ -6,6 +6,14 @@ const defaultRestClientOptions = (
|
|
|
6
6
|
{
|
|
7
7
|
DownloadBatchSize: 100,
|
|
8
8
|
|
|
9
|
+
// Request timeout in milliseconds for normal remote API calls.
|
|
10
|
+
// Default: 60 seconds.
|
|
11
|
+
RequestTimeout: 60000,
|
|
12
|
+
|
|
13
|
+
// Request timeout in milliseconds for MAX(column) queries,
|
|
14
|
+
// which can be very slow on large tables. Default: 5 minutes.
|
|
15
|
+
MaxRequestTimeout: 300000,
|
|
16
|
+
|
|
9
17
|
ServerURL: 'https://localhost:8080/1.0/',
|
|
10
18
|
UserID: false,
|
|
11
19
|
Password: false,
|
|
@@ -36,7 +44,12 @@ class MeadowCloneRestClient extends libFableServiceProviderBase
|
|
|
36
44
|
this.restClient = this.fable.serviceManager.instantiateServiceProvider('RestClient', {}, 'MeadowCloneRestClient-RestClient');
|
|
37
45
|
this.cache = {};
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
this.requestTimeout = this.options.RequestTimeout;
|
|
48
|
+
this.maxRequestTimeout = this.options.MaxRequestTimeout;
|
|
49
|
+
|
|
50
|
+
// Use the longer of the two timeouts for the agent's socket timeout
|
|
51
|
+
// so that MAX queries don't get killed at the socket level.
|
|
52
|
+
const agentOptions = { keepAlive: true, timeout: Math.max(this.requestTimeout, this.maxRequestTimeout) };
|
|
40
53
|
|
|
41
54
|
if (this.serverURL && this.serverURL.startsWith('http:'))
|
|
42
55
|
{
|
|
@@ -39,6 +39,7 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
39
39
|
|
|
40
40
|
this.DefaultIdentifier = this.EntitySchema.MeadowSchema.DefaultIdentifier;
|
|
41
41
|
this.PageSize = this.options.PageSize || 100;
|
|
42
|
+
this.SyncDeletedRecords = this.options.SyncDeletedRecords || false;
|
|
42
43
|
|
|
43
44
|
this.Meadow = false;
|
|
44
45
|
|
|
@@ -52,12 +53,29 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
52
53
|
this.Meadow = this.fable.Meadow.loadFromPackageObject(this.EntitySchema.MeadowSchema);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
this.log.info(`Sync for ${this.EntitySchema.TableName} creating table if it doesn't exist...`);
|
|
56
|
-
|
|
57
56
|
if (this.Meadow && this.Meadow.provider)
|
|
58
57
|
{
|
|
59
|
-
|
|
58
|
+
let tmpProvider = this.Meadow.provider.getProvider();
|
|
59
|
+
|
|
60
|
+
if (!tmpProvider)
|
|
61
|
+
{
|
|
62
|
+
this.log.error(`No provider returned by getProvider() for ${this.EntitySchema.TableName}`);
|
|
63
|
+
return fCallback(new Error(`No provider returned by getProvider() for ${this.EntitySchema.TableName}`));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!tmpProvider.createTable)
|
|
60
67
|
{
|
|
68
|
+
this.log.error(`Provider for ${this.EntitySchema.TableName} has no createTable method.`);
|
|
69
|
+
return fCallback(new Error(`Provider for ${this.EntitySchema.TableName} has no createTable method`));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return tmpProvider.createTable(this.EntitySchema, (pCreateError) =>
|
|
73
|
+
{
|
|
74
|
+
if (pCreateError)
|
|
75
|
+
{
|
|
76
|
+
this.log.warn(`${this.EntitySchema.TableName}: createTable returned error: ${pCreateError}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
61
79
|
const tmpGUIDColumn = this.EntitySchema.Columns.find((c) => c.DataType == 'GUID');
|
|
62
80
|
const tmpDeletedColumn = this.EntitySchema.Columns.find((c) => c.Column == 'Deleted');
|
|
63
81
|
|
|
@@ -90,9 +108,17 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
90
108
|
return this.fable.MeadowConnectionManager.createIndex(this.EntitySchema, tmpDeletedColumn, false, fNext);
|
|
91
109
|
});
|
|
92
110
|
}
|
|
93
|
-
tmpAnticipate.wait(
|
|
111
|
+
tmpAnticipate.wait((pIndexError) =>
|
|
112
|
+
{
|
|
113
|
+
if (pIndexError)
|
|
114
|
+
{
|
|
115
|
+
this.log.warn(`${this.EntitySchema.TableName}: Index creation error: ${pIndexError}`);
|
|
116
|
+
}
|
|
117
|
+
return fCallback(pIndexError || pCreateError);
|
|
118
|
+
});
|
|
94
119
|
});
|
|
95
120
|
}
|
|
121
|
+
|
|
96
122
|
return fCallback();
|
|
97
123
|
}
|
|
98
124
|
|
|
@@ -139,10 +165,142 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
139
165
|
return tmpRecordToCommit;
|
|
140
166
|
}
|
|
141
167
|
|
|
168
|
+
syncDeletedRecords(fCallback)
|
|
169
|
+
{
|
|
170
|
+
const tmpDeletedColumn = this.EntitySchema.Columns.find((c) => c.Column == 'Deleted');
|
|
171
|
+
if (!tmpDeletedColumn)
|
|
172
|
+
{
|
|
173
|
+
this.fable.log.info(`No Deleted column for ${this.EntitySchema.TableName}; skipping delete sync.`);
|
|
174
|
+
return fCallback();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.fable.log.info(`Checking for deleted records on server for ${this.EntitySchema.TableName}...`);
|
|
178
|
+
|
|
179
|
+
// Get the count of deleted records from the server.
|
|
180
|
+
// The explicit FBV~Deleted~EQ~1 filter overrides foxhound's automatic Deleted=0 filter.
|
|
181
|
+
this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}s/Count/FilteredTo/FBV~Deleted~EQ~1`,
|
|
182
|
+
(pError, pResponse, pBody) =>
|
|
183
|
+
{
|
|
184
|
+
if (pError || !pBody || !pBody.hasOwnProperty('Count'))
|
|
185
|
+
{
|
|
186
|
+
this.fable.log.warn(`Could not get deleted record count for ${this.EntitySchema.TableName}; skipping delete sync.`);
|
|
187
|
+
return fCallback();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const tmpDeletedCount = pBody.Count;
|
|
191
|
+
if (tmpDeletedCount < 1)
|
|
192
|
+
{
|
|
193
|
+
this.fable.log.info(`No deleted records on server for ${this.EntitySchema.TableName}.`);
|
|
194
|
+
return fCallback();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.fable.log.info(`Found ${tmpDeletedCount} deleted records on server for ${this.EntitySchema.TableName}; syncing deletions...`);
|
|
198
|
+
|
|
199
|
+
// Generate paginated URLs for deleted records
|
|
200
|
+
const tmpDeleteURLPartials = [];
|
|
201
|
+
for (let i = 0; i < tmpDeletedCount; i += this.PageSize)
|
|
202
|
+
{
|
|
203
|
+
tmpDeleteURLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~Deleted~EQ~1~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
this.fable.Utility.eachLimit(tmpDeleteURLPartials, 1,
|
|
207
|
+
(pURLPartial, fPageComplete) =>
|
|
208
|
+
{
|
|
209
|
+
this.fable.MeadowCloneRestClient.getJSON(pURLPartial,
|
|
210
|
+
(pDownloadError, pResponse, pBody) =>
|
|
211
|
+
{
|
|
212
|
+
if (pDownloadError || !pBody || !Array.isArray(pBody) || pBody.length < 1)
|
|
213
|
+
{
|
|
214
|
+
return fPageComplete();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.fable.Utility.eachLimit(pBody, 5,
|
|
218
|
+
(pEntityRecord, fRecordComplete) =>
|
|
219
|
+
{
|
|
220
|
+
const tmpRecordID = pEntityRecord[this.DefaultIdentifier];
|
|
221
|
+
if (!tmpRecordID || tmpRecordID < 1)
|
|
222
|
+
{
|
|
223
|
+
return fRecordComplete();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Read local record with delete tracking disabled so we can see all records
|
|
227
|
+
const tmpQuery = this.Meadow.query;
|
|
228
|
+
tmpQuery.addFilter(this.DefaultIdentifier, tmpRecordID);
|
|
229
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
230
|
+
|
|
231
|
+
this.Meadow.doRead(tmpQuery,
|
|
232
|
+
(pReadError, pQuery, pRecord) =>
|
|
233
|
+
{
|
|
234
|
+
if (pReadError || !pRecord)
|
|
235
|
+
{
|
|
236
|
+
// Record doesn't exist locally -- create it as deleted
|
|
237
|
+
const tmpRecordToCommit = this.marshalRecord(pEntityRecord);
|
|
238
|
+
|
|
239
|
+
const tmpCreateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
240
|
+
tmpCreateQuery.setDisableAutoIdentity(true);
|
|
241
|
+
tmpCreateQuery.setDisableAutoDateStamp(true);
|
|
242
|
+
tmpCreateQuery.setDisableAutoUserStamp(true);
|
|
243
|
+
tmpCreateQuery.setDisableDeleteTracking(true);
|
|
244
|
+
tmpCreateQuery.AllowIdentityInsert = true;
|
|
245
|
+
|
|
246
|
+
this.Meadow.doCreate(tmpCreateQuery,
|
|
247
|
+
(pCreateError) =>
|
|
248
|
+
{
|
|
249
|
+
if (pCreateError)
|
|
250
|
+
{
|
|
251
|
+
this.log.error(`Error creating deleted record ${this.EntitySchema.TableName} ID ${tmpRecordID}: ${pCreateError}`);
|
|
252
|
+
}
|
|
253
|
+
return fRecordComplete();
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (pRecord.Deleted == 1)
|
|
259
|
+
{
|
|
260
|
+
// Already marked deleted locally
|
|
261
|
+
return fRecordComplete();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Record exists locally but is not deleted -- update it
|
|
265
|
+
const tmpRecordToCommit = this.marshalRecord(pEntityRecord);
|
|
266
|
+
|
|
267
|
+
const tmpUpdateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
268
|
+
tmpUpdateQuery.setDisableAutoIdentity(true);
|
|
269
|
+
tmpUpdateQuery.setDisableAutoDateStamp(true);
|
|
270
|
+
tmpUpdateQuery.setDisableAutoUserStamp(true);
|
|
271
|
+
tmpUpdateQuery.setDisableDeleteTracking(true);
|
|
272
|
+
|
|
273
|
+
this.Meadow.doUpdate(tmpUpdateQuery,
|
|
274
|
+
(pUpdateError) =>
|
|
275
|
+
{
|
|
276
|
+
if (pUpdateError)
|
|
277
|
+
{
|
|
278
|
+
this.log.error(`Error marking record as deleted ${this.EntitySchema.TableName} ID ${tmpRecordID}: ${pUpdateError}`);
|
|
279
|
+
}
|
|
280
|
+
return fRecordComplete();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
},
|
|
284
|
+
(pRecordSyncError) =>
|
|
285
|
+
{
|
|
286
|
+
return fPageComplete();
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
},
|
|
290
|
+
(pDeleteSyncError) =>
|
|
291
|
+
{
|
|
292
|
+
this.fable.log.info(`Delete sync complete for ${this.EntitySchema.TableName} (${tmpDeletedCount} deleted records processed).`);
|
|
293
|
+
return fCallback();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
142
298
|
sync(fCallback)
|
|
143
299
|
{
|
|
144
300
|
this.operation.createTimeStamp('EntityInitialSync');
|
|
145
301
|
|
|
302
|
+
this.log.info(`Syncing ${this.EntitySchema.TableName} (PageSize: ${this.PageSize}, SyncDeletedRecords: ${this.SyncDeletedRecords})`);
|
|
303
|
+
|
|
146
304
|
const tmpSyncState = (
|
|
147
305
|
{
|
|
148
306
|
Local: { MaxIDEntity: -1, RecordCount: 0 },
|
|
@@ -162,16 +320,12 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
162
320
|
{
|
|
163
321
|
if (pReadError)
|
|
164
322
|
{
|
|
165
|
-
this.fable.log.error(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`, { Error: pReadError });
|
|
166
323
|
return fStageComplete(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
167
324
|
}
|
|
168
|
-
if (
|
|
325
|
+
if (pRecord)
|
|
169
326
|
{
|
|
170
|
-
|
|
171
|
-
return fStageComplete();
|
|
327
|
+
tmpSyncState.Local.MaxIDEntity = pRecord[this.DefaultIdentifier];
|
|
172
328
|
}
|
|
173
|
-
this.fable.log.info(`Found local max entity ID ${this.EntitySchema.TableName}: ${pRecord[this.DefaultIdentifier]}`);
|
|
174
|
-
tmpSyncState.Local.MaxIDEntity = pRecord[this.DefaultIdentifier];
|
|
175
329
|
return fStageComplete();
|
|
176
330
|
});
|
|
177
331
|
},
|
|
@@ -184,7 +338,6 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
184
338
|
{
|
|
185
339
|
if (pCountError)
|
|
186
340
|
{
|
|
187
|
-
this.fable.log.error(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`, { Error: pCountError });
|
|
188
341
|
return fStageComplete(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`);
|
|
189
342
|
}
|
|
190
343
|
tmpSyncState.Local.RecordCount = pCount;
|
|
@@ -199,18 +352,12 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
199
352
|
{
|
|
200
353
|
if (pError)
|
|
201
354
|
{
|
|
202
|
-
this.fable.log.error(`Error getting server max entity ID ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
|
|
203
355
|
return fStageComplete(`Error getting server max entity ID ${this.EntitySchema.TableName}: ${pError}`);
|
|
204
356
|
}
|
|
205
357
|
if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
|
|
206
358
|
{
|
|
207
|
-
this.fable.log.info(`Found server max entity ID ${this.EntitySchema.TableName}: ${pBody[this.DefaultIdentifier]}`);
|
|
208
359
|
tmpSyncState.Server.MaxIDEntity = pBody[this.DefaultIdentifier];
|
|
209
360
|
}
|
|
210
|
-
else
|
|
211
|
-
{
|
|
212
|
-
this.fable.log.warn(`No records found in server for max entity ID of ${this.EntitySchema.TableName}.`);
|
|
213
|
-
}
|
|
214
361
|
return fStageComplete();
|
|
215
362
|
});
|
|
216
363
|
},
|
|
@@ -222,18 +369,12 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
222
369
|
{
|
|
223
370
|
if (pError)
|
|
224
371
|
{
|
|
225
|
-
this.fable.log.error(`Error getting server count for ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
|
|
226
372
|
return fStageComplete(`Error getting server count for ${this.EntitySchema.TableName}: ${pError}`);
|
|
227
373
|
}
|
|
228
374
|
if (pBody && pBody.hasOwnProperty('Count'))
|
|
229
375
|
{
|
|
230
|
-
this.fable.log.info(`Found server count for ${this.EntitySchema.TableName}: ${pBody.Count}`);
|
|
231
376
|
tmpSyncState.Server.RecordCount = pBody.Count;
|
|
232
377
|
}
|
|
233
|
-
else
|
|
234
|
-
{
|
|
235
|
-
this.fable.log.warn(`No records found in server based on count for ${this.EntitySchema.TableName}.`);
|
|
236
|
-
}
|
|
237
378
|
return fStageComplete();
|
|
238
379
|
});
|
|
239
380
|
},
|
|
@@ -251,21 +392,28 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
251
392
|
tmpSyncState.URLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~${this.DefaultIdentifier}~GT~${tmpSyncState.Local.MaxIDEntity}~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
|
|
252
393
|
}
|
|
253
394
|
|
|
254
|
-
this.fable.log.info(
|
|
395
|
+
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})`);
|
|
255
396
|
|
|
256
397
|
return fStageComplete();
|
|
257
398
|
},
|
|
258
399
|
(fStageComplete) =>
|
|
259
400
|
{
|
|
401
|
+
let tmpPageIndex = 0;
|
|
402
|
+
let tmpRecordsCreated = 0;
|
|
403
|
+
let tmpRecordsSkipped = 0;
|
|
404
|
+
let tmpRecordsErrored = 0;
|
|
405
|
+
|
|
260
406
|
this.fable.Utility.eachLimit(tmpSyncState.URLPartials, 1,
|
|
261
407
|
(pURLPartial, fDownloadComplete) =>
|
|
262
408
|
{
|
|
409
|
+
tmpPageIndex++;
|
|
410
|
+
|
|
263
411
|
this.fable.MeadowCloneRestClient.getJSON(pURLPartial,
|
|
264
412
|
(pDownloadError, pResponse, pBody) =>
|
|
265
413
|
{
|
|
266
414
|
if (pDownloadError)
|
|
267
415
|
{
|
|
268
|
-
this.fable.log.error(
|
|
416
|
+
this.fable.log.error(`${this.EntitySchema.TableName}: page ${tmpPageIndex} download error: ${pDownloadError}`);
|
|
269
417
|
return fDownloadComplete();
|
|
270
418
|
}
|
|
271
419
|
if (pBody && pBody.length > 0)
|
|
@@ -286,7 +434,7 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
286
434
|
{
|
|
287
435
|
if (pReadError)
|
|
288
436
|
{
|
|
289
|
-
|
|
437
|
+
tmpRecordsErrored++;
|
|
290
438
|
return fEntitySyncComplete();
|
|
291
439
|
}
|
|
292
440
|
if (!pRecord)
|
|
@@ -308,15 +456,18 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
308
456
|
{
|
|
309
457
|
if (pCreateError)
|
|
310
458
|
{
|
|
311
|
-
|
|
459
|
+
tmpRecordsErrored++;
|
|
460
|
+
this.log.error(`${this.EntitySchema.TableName}: doCreate error for ID ${tmpRecord[this.DefaultIdentifier]}: ${pCreateError}`);
|
|
312
461
|
return fEntitySyncComplete();
|
|
313
462
|
}
|
|
463
|
+
tmpRecordsCreated++;
|
|
314
464
|
this.operation.incrementProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`, 1);
|
|
315
465
|
return fEntitySyncComplete();
|
|
316
466
|
});
|
|
317
467
|
}
|
|
318
468
|
else
|
|
319
469
|
{
|
|
470
|
+
tmpRecordsSkipped++;
|
|
320
471
|
return fEntitySyncComplete();
|
|
321
472
|
}
|
|
322
473
|
});
|
|
@@ -343,6 +494,7 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
343
494
|
},
|
|
344
495
|
(pDownloadError) =>
|
|
345
496
|
{
|
|
497
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: sync complete — created: ${tmpRecordsCreated}, skipped: ${tmpRecordsSkipped}, errors: ${tmpRecordsErrored}`);
|
|
346
498
|
if (pDownloadError)
|
|
347
499
|
{
|
|
348
500
|
this.fable.log.error(`Error returned URL Partial .. this may not be an error: ${pDownloadError}`);
|
|
@@ -355,8 +507,12 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
355
507
|
{
|
|
356
508
|
if (pError)
|
|
357
509
|
{
|
|
358
|
-
this.fable.log.error(
|
|
359
|
-
|
|
510
|
+
this.fable.log.error(`${this.EntitySchema.TableName}: sync error: ${pError}`);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (this.SyncDeletedRecords)
|
|
514
|
+
{
|
|
515
|
+
return this.syncDeletedRecords(() => { return fCallback(); });
|
|
360
516
|
}
|
|
361
517
|
|
|
362
518
|
return fCallback();
|
|
@@ -39,6 +39,7 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
39
39
|
|
|
40
40
|
this.DefaultIdentifier = this.EntitySchema.MeadowSchema.DefaultIdentifier;
|
|
41
41
|
this.PageSize = this.options.PageSize || 100;
|
|
42
|
+
this.SyncDeletedRecords = this.options.SyncDeletedRecords || false;
|
|
42
43
|
|
|
43
44
|
this.Meadow = false;
|
|
44
45
|
|
|
@@ -132,6 +133,136 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
132
133
|
return tmpRecordToCommit;
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
syncDeletedRecords(fCallback)
|
|
137
|
+
{
|
|
138
|
+
const tmpDeletedColumn = this.EntitySchema.Columns.find((c) => c.Column == 'Deleted');
|
|
139
|
+
if (!tmpDeletedColumn)
|
|
140
|
+
{
|
|
141
|
+
this.fable.log.info(`No Deleted column for ${this.EntitySchema.TableName}; skipping delete sync.`);
|
|
142
|
+
return fCallback();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this.fable.log.info(`Checking for deleted records on server for ${this.EntitySchema.TableName}...`);
|
|
146
|
+
|
|
147
|
+
// Get the count of deleted records from the server.
|
|
148
|
+
// The explicit FBV~Deleted~EQ~1 filter overrides foxhound's automatic Deleted=0 filter.
|
|
149
|
+
this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}s/Count/FilteredTo/FBV~Deleted~EQ~1`,
|
|
150
|
+
(pError, pResponse, pBody) =>
|
|
151
|
+
{
|
|
152
|
+
if (pError || !pBody || !pBody.hasOwnProperty('Count'))
|
|
153
|
+
{
|
|
154
|
+
this.fable.log.warn(`Could not get deleted record count for ${this.EntitySchema.TableName}; skipping delete sync.`);
|
|
155
|
+
return fCallback();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const tmpDeletedCount = pBody.Count;
|
|
159
|
+
if (tmpDeletedCount < 1)
|
|
160
|
+
{
|
|
161
|
+
this.fable.log.info(`No deleted records on server for ${this.EntitySchema.TableName}.`);
|
|
162
|
+
return fCallback();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.fable.log.info(`Found ${tmpDeletedCount} deleted records on server for ${this.EntitySchema.TableName}; syncing deletions...`);
|
|
166
|
+
|
|
167
|
+
// Generate paginated URLs for deleted records
|
|
168
|
+
const tmpDeleteURLPartials = [];
|
|
169
|
+
for (let i = 0; i < tmpDeletedCount; i += this.PageSize)
|
|
170
|
+
{
|
|
171
|
+
tmpDeleteURLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~Deleted~EQ~1~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.fable.Utility.eachLimit(tmpDeleteURLPartials, 1,
|
|
175
|
+
(pURLPartial, fPageComplete) =>
|
|
176
|
+
{
|
|
177
|
+
this.fable.MeadowCloneRestClient.getJSON(pURLPartial,
|
|
178
|
+
(pDownloadError, pResponse, pBody) =>
|
|
179
|
+
{
|
|
180
|
+
if (pDownloadError || !pBody || !Array.isArray(pBody) || pBody.length < 1)
|
|
181
|
+
{
|
|
182
|
+
return fPageComplete();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.fable.Utility.eachLimit(pBody, 5,
|
|
186
|
+
(pEntityRecord, fRecordComplete) =>
|
|
187
|
+
{
|
|
188
|
+
const tmpRecordID = pEntityRecord[this.DefaultIdentifier];
|
|
189
|
+
if (!tmpRecordID || tmpRecordID < 1)
|
|
190
|
+
{
|
|
191
|
+
return fRecordComplete();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Read local record with delete tracking disabled so we can see all records
|
|
195
|
+
const tmpQuery = this.Meadow.query;
|
|
196
|
+
tmpQuery.addFilter(this.DefaultIdentifier, tmpRecordID);
|
|
197
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
198
|
+
|
|
199
|
+
this.Meadow.doRead(tmpQuery,
|
|
200
|
+
(pReadError, pQuery, pRecord) =>
|
|
201
|
+
{
|
|
202
|
+
if (pReadError || !pRecord)
|
|
203
|
+
{
|
|
204
|
+
// Record doesn't exist locally -- create it as deleted
|
|
205
|
+
const tmpRecordToCommit = this.marshalRecord(pEntityRecord);
|
|
206
|
+
|
|
207
|
+
const tmpCreateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
208
|
+
tmpCreateQuery.setDisableAutoIdentity(true);
|
|
209
|
+
tmpCreateQuery.setDisableAutoDateStamp(true);
|
|
210
|
+
tmpCreateQuery.setDisableAutoUserStamp(true);
|
|
211
|
+
tmpCreateQuery.setDisableDeleteTracking(true);
|
|
212
|
+
tmpCreateQuery.AllowIdentityInsert = true;
|
|
213
|
+
|
|
214
|
+
this.Meadow.doCreate(tmpCreateQuery,
|
|
215
|
+
(pCreateError) =>
|
|
216
|
+
{
|
|
217
|
+
if (pCreateError)
|
|
218
|
+
{
|
|
219
|
+
this.log.error(`Error creating deleted record ${this.EntitySchema.TableName} ID ${tmpRecordID}: ${pCreateError}`);
|
|
220
|
+
}
|
|
221
|
+
return fRecordComplete();
|
|
222
|
+
});
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (pRecord.Deleted == 1)
|
|
227
|
+
{
|
|
228
|
+
// Already marked deleted locally
|
|
229
|
+
return fRecordComplete();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Record exists locally but is not deleted -- update it
|
|
233
|
+
const tmpRecordToCommit = this.marshalRecord(pEntityRecord);
|
|
234
|
+
|
|
235
|
+
const tmpUpdateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
236
|
+
tmpUpdateQuery.setDisableAutoIdentity(true);
|
|
237
|
+
tmpUpdateQuery.setDisableAutoDateStamp(true);
|
|
238
|
+
tmpUpdateQuery.setDisableAutoUserStamp(true);
|
|
239
|
+
tmpUpdateQuery.setDisableDeleteTracking(true);
|
|
240
|
+
|
|
241
|
+
this.Meadow.doUpdate(tmpUpdateQuery,
|
|
242
|
+
(pUpdateError) =>
|
|
243
|
+
{
|
|
244
|
+
if (pUpdateError)
|
|
245
|
+
{
|
|
246
|
+
this.log.error(`Error marking record as deleted ${this.EntitySchema.TableName} ID ${tmpRecordID}: ${pUpdateError}`);
|
|
247
|
+
}
|
|
248
|
+
return fRecordComplete();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
},
|
|
252
|
+
(pRecordSyncError) =>
|
|
253
|
+
{
|
|
254
|
+
return fPageComplete();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
(pDeleteSyncError) =>
|
|
259
|
+
{
|
|
260
|
+
this.fable.log.info(`Delete sync complete for ${this.EntitySchema.TableName} (${tmpDeletedCount} deleted records processed).`);
|
|
261
|
+
return fCallback();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
135
266
|
addSyncAnticipateEntry(tmpSyncState, tmpAnticipate)
|
|
136
267
|
{
|
|
137
268
|
tmpAnticipate.anticipate(
|
|
@@ -446,7 +577,11 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
446
577
|
if (pError)
|
|
447
578
|
{
|
|
448
579
|
this.fable.log.error(`Error performing Update sync ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
|
|
449
|
-
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (this.SyncDeletedRecords)
|
|
583
|
+
{
|
|
584
|
+
return this.syncDeletedRecords(() => { return fCallback(); });
|
|
450
585
|
}
|
|
451
586
|
|
|
452
587
|
return fCallback();
|
|
@@ -43,6 +43,17 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
43
43
|
this.SyncEntityOptions = JSON.parse(JSON.stringify(this.options.SyncEntityOptions));
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// When true, after syncing active records, also sync records marked Deleted=1 on the source.
|
|
47
|
+
this.SyncDeletedRecords = false;
|
|
48
|
+
if (this.fable.ProgramConfiguration.hasOwnProperty('SyncDeletedRecords'))
|
|
49
|
+
{
|
|
50
|
+
this.SyncDeletedRecords = !!this.fable.ProgramConfiguration.SyncDeletedRecords;
|
|
51
|
+
}
|
|
52
|
+
else if (this.options.hasOwnProperty('SyncDeletedRecords'))
|
|
53
|
+
{
|
|
54
|
+
this.SyncDeletedRecords = !!this.options.SyncDeletedRecords;
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
this.MeadowSchema = false;
|
|
47
58
|
this.MeadowSchemaTableList = false;
|
|
48
59
|
|
|
@@ -59,9 +70,16 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
59
70
|
this.meadowSchema = pSchema;
|
|
60
71
|
this.MeadowSchemaTableList = Object.keys(this.meadowSchema.Tables);
|
|
61
72
|
|
|
73
|
+
this.log.info(`Loading schema for ${this.MeadowSchemaTableList.length} tables (mode: ${this.SyncMode})`);
|
|
74
|
+
|
|
75
|
+
let tmpEntityIndex = 0;
|
|
76
|
+
let tmpErrorCount = 0;
|
|
77
|
+
let tmpSuccessCount = 0;
|
|
78
|
+
|
|
62
79
|
this.fable.Utility.eachLimit(this.MeadowSchemaTableList, 1,
|
|
63
80
|
(pEntitySchemaName, fSyncInitializationComplete) =>
|
|
64
81
|
{
|
|
82
|
+
tmpEntityIndex++;
|
|
65
83
|
const tmpEntitySchema = this.meadowSchema.Tables[pEntitySchemaName];
|
|
66
84
|
// If this is in the entity list or none is specified, create the sync entity object.
|
|
67
85
|
if (this.SyncEntityList.length < 1 || this.SyncEntityList.indexOf(tmpEntitySchema.TableName) > -1)
|
|
@@ -70,6 +88,7 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
70
88
|
MeadowEntitySchema: tmpEntitySchema,
|
|
71
89
|
ConnectionPool: this.options.ConnectionPool,
|
|
72
90
|
PageSize: this.options.PageSize || 100,
|
|
91
|
+
SyncDeletedRecords: this.SyncDeletedRecords,
|
|
73
92
|
};
|
|
74
93
|
|
|
75
94
|
let tmpSyncEntity;
|
|
@@ -85,7 +104,20 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
85
104
|
|
|
86
105
|
this.MeadowSyncEntities[tmpEntitySchema.TableName] = tmpSyncEntity;
|
|
87
106
|
|
|
88
|
-
return tmpSyncEntity.initialize(
|
|
107
|
+
return tmpSyncEntity.initialize((pInitError) =>
|
|
108
|
+
{
|
|
109
|
+
if (pInitError)
|
|
110
|
+
{
|
|
111
|
+
tmpErrorCount++;
|
|
112
|
+
this.log.warn(`Failed to initialize ${tmpEntitySchema.TableName}: ${pInitError}`);
|
|
113
|
+
}
|
|
114
|
+
else
|
|
115
|
+
{
|
|
116
|
+
tmpSuccessCount++;
|
|
117
|
+
}
|
|
118
|
+
// Always continue to next entity regardless of individual errors
|
|
119
|
+
return fSyncInitializationComplete();
|
|
120
|
+
});
|
|
89
121
|
}
|
|
90
122
|
else
|
|
91
123
|
{
|
|
@@ -99,7 +131,7 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
99
131
|
this.log.error(`MeadowSync Error creating sync objects: ${pSyncInitializationError}`, pSyncInitializationError);
|
|
100
132
|
}
|
|
101
133
|
|
|
102
|
-
this.log.info(
|
|
134
|
+
this.log.info(`Entity sync objects created: ${tmpSuccessCount} succeeded, ${tmpErrorCount} failed.`);
|
|
103
135
|
|
|
104
136
|
if (this.SyncEntityList.length < 1)
|
|
105
137
|
{
|
|
@@ -117,7 +149,15 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
117
149
|
this.log.warn(`MeadowSync.syncEntity called for an entity that does not exist: ${pEntityHash}`);
|
|
118
150
|
return fCallback();
|
|
119
151
|
}
|
|
120
|
-
|
|
152
|
+
|
|
153
|
+
this.MeadowSyncEntities[pEntityHash].sync((pError) =>
|
|
154
|
+
{
|
|
155
|
+
if (pError)
|
|
156
|
+
{
|
|
157
|
+
this.log.error(`Sync failed for ${pEntityHash}: ${pError}`);
|
|
158
|
+
}
|
|
159
|
+
return fCallback(pError);
|
|
160
|
+
});
|
|
121
161
|
}
|
|
122
162
|
|
|
123
163
|
syncAll(fCallback)
|