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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meadow-integration",
3
- "version": "1.0.6",
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.61"
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.28",
43
- "meadow-connection-mysql": "^1.0.13",
44
- "meadow-connection-mssql": "^1.0.15",
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
- const agentOptions = { keepAlive: true };
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
- return this.Meadow.provider.getProvider().createTable(this.EntitySchema, (pCreateError) =>
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(fCallback);
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 (!pRecord)
325
+ if (pRecord)
169
326
  {
170
- this.fable.log.warn(`No records found in local ${this.EntitySchema.TableName}.`);
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(`Syncing with ${tmpSyncState.URLPartials.length} requests for ${this.EntitySchema.TableName} with local max ID ${tmpSyncState.Local.MaxIDEntity} and server max ID ${tmpSyncState.Server.MaxIDEntity}; estimated ${tmpSyncState.EstimatedRecordCount} records to sync.`);
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(`Error getting URL Partial [${pURLPartial}]: ${pDownloadError}`, { Error: pDownloadError });
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
- this.fable.log.error(`Error reading record ${this.EntitySchema.TableName}: ${pReadError}`, { Error: pReadError, PassedRecord: tmpRecord });
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
- this.log.error(`Error creating record ${this.EntitySchema.TableName}: ${pCreateError}`, pCreateError);
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(`Error performing sync ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
359
- return fCallback();
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
- return fCallback();
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(fSyncInitializationComplete);
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('Entity sync objects created!');
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
- this.MeadowSyncEntities[pEntityHash].sync(fCallback);
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)