meadow-integration 1.0.5 → 1.0.7
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/.dockerignore +11 -0
- package/Docker-Build.sh +2 -0
- package/Docker-Compose.sh +2 -0
- package/Docker-Push.sh +2 -0
- package/Docker-Tag.sh +2 -0
- package/Dockerfile +28 -0
- package/Dockerfile_LUXURYCode +23 -0
- package/README.md +139 -25
- package/docker-compose.yml +16 -0
- package/docs/README.md +65 -18
- package/docs/_cover.md +3 -2
- package/docs/_sidebar.md +52 -7
- package/docs/_topbar.md +2 -0
- package/docs/api/clone-rest-client.md +278 -0
- package/docs/api/connection-manager.md +179 -0
- package/docs/api/guid-map.md +234 -0
- package/docs/api/integration-adapter.md +283 -0
- package/docs/api/operation.md +241 -0
- package/docs/api/sync-entity-initial.md +227 -0
- package/docs/api/sync-entity-ongoing.md +244 -0
- package/docs/api/sync.md +213 -0
- package/docs/api/tabular-check.md +213 -0
- package/docs/api/tabular-transform.md +316 -0
- package/docs/architecture.md +423 -0
- package/docs/cli/comprehensionarray.md +111 -0
- package/docs/cli/comprehensionintersect.md +132 -0
- package/docs/cli/csvcheck.md +111 -0
- package/docs/cli/csvtransform.md +170 -0
- package/docs/cli/data-clone.md +277 -0
- package/docs/cli/jsonarraytransform.md +166 -0
- package/docs/cli/load-comprehension.md +129 -0
- package/docs/cli/objectarraytocsv.md +159 -0
- package/docs/cli/overview.md +96 -0
- package/docs/cli/serve.md +102 -0
- package/docs/cli/tsvtransform.md +144 -0
- package/docs/data-clone/configuration.md +357 -0
- package/docs/data-clone/connection-manager.md +206 -0
- package/docs/data-clone/docker.md +290 -0
- package/docs/data-clone/overview.md +173 -0
- package/docs/data-clone/sync-modes.md +186 -0
- package/docs/implementation-reference.md +311 -0
- package/docs/overview.md +156 -0
- package/docs/quickstart.md +233 -0
- package/docs/rest/comprehension-push.md +209 -0
- package/docs/rest/comprehension.md +506 -0
- package/docs/rest/csv.md +255 -0
- package/docs/rest/entity-generation.md +158 -0
- package/docs/rest/json-array.md +243 -0
- package/docs/rest/overview.md +120 -0
- package/docs/rest/status.md +63 -0
- package/docs/rest/tsv.md +241 -0
- package/docs/retold-catalog.json +93 -3
- package/docs/retold-keyword-index.json +23683 -1901
- package/package.json +6 -3
- package/scripts/run.sh +18 -0
- package/source/Meadow-Integration.js +15 -1
- package/source/cli/Default-Meadow-Integration-Configuration.json +37 -2
- package/source/cli/Meadow-Integration-CLI-Program.js +4 -1
- package/source/cli/commands/Meadow-Integration-Command-DataClone.js +284 -0
- package/source/services/clone/Meadow-Service-ConnectionManager.js +251 -0
- package/source/services/clone/Meadow-Service-Operation.js +196 -0
- package/source/services/clone/Meadow-Service-RestClient.js +364 -0
- package/source/services/clone/Meadow-Service-Sync-Entity-Initial.js +502 -0
- package/source/services/clone/Meadow-Service-Sync-Entity-Ongoing.js +592 -0
- package/source/services/clone/Meadow-Service-Sync.js +154 -0
- /package/docs/examples/bookstore/{mapping_books_Author.json → mapping_books_author.json} +0 -0
- /package/docs/examples/bookstore/{mapping_books_Book.json → mapping_books_book.json} +0 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
2
|
+
const libMeadowOperation = require('./Meadow-Service-Operation.js');
|
|
3
|
+
|
|
4
|
+
class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
5
|
+
{
|
|
6
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
7
|
+
{
|
|
8
|
+
super(pFable, pOptions, pServiceHash);
|
|
9
|
+
|
|
10
|
+
this.serviceType = 'MeadowSyncEntityInitial';
|
|
11
|
+
|
|
12
|
+
if (!this.options.hasOwnProperty('MeadowEntitySchema'))
|
|
13
|
+
{
|
|
14
|
+
throw new Error('MeadowSyncEntityInitial requires a valid MeadowEntitySchema option.');
|
|
15
|
+
}
|
|
16
|
+
if (typeof(this.options.MeadowEntitySchema) != 'object')
|
|
17
|
+
{
|
|
18
|
+
throw new Error(`MeadowSyncEntityInitial requires MeadowEntitySchema to be an object; got ${typeof(this.options.MeadowEntitySchema)}.`);
|
|
19
|
+
}
|
|
20
|
+
if (!this.options.MeadowEntitySchema.hasOwnProperty('TableName') ||
|
|
21
|
+
typeof(this.options.MeadowEntitySchema.TableName) != 'string' ||
|
|
22
|
+
this.options.MeadowEntitySchema.TableName.length < 1)
|
|
23
|
+
{
|
|
24
|
+
throw new Error('MeadowSyncEntityInitial requires a valid MeadowEntitySchema.TableName.');
|
|
25
|
+
}
|
|
26
|
+
if (!this.options.MeadowEntitySchema.hasOwnProperty('Columns') ||
|
|
27
|
+
!Array.isArray(this.options.MeadowEntitySchema.Columns) ||
|
|
28
|
+
this.options.MeadowEntitySchema.Columns.length < 1)
|
|
29
|
+
{
|
|
30
|
+
throw new Error('MeadowSyncEntityInitial requires a valid MeadowEntitySchema.Columns array.');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.EntitySchema = JSON.parse(JSON.stringify(this.options.MeadowEntitySchema));
|
|
34
|
+
|
|
35
|
+
if (!this.EntitySchema.hasOwnProperty('MeadowSchema'))
|
|
36
|
+
{
|
|
37
|
+
throw new Error('MeadowSyncEntityInitial requires MeadowEntitySchema.MeadowSchema; please update stricture and recompile the extended JSON.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.DefaultIdentifier = this.EntitySchema.MeadowSchema.DefaultIdentifier;
|
|
41
|
+
this.PageSize = this.options.PageSize || 100;
|
|
42
|
+
this.SyncDeletedRecords = this.options.SyncDeletedRecords || false;
|
|
43
|
+
|
|
44
|
+
this.Meadow = false;
|
|
45
|
+
|
|
46
|
+
this.operation = new libMeadowOperation(this.fable);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
initialize(fCallback)
|
|
50
|
+
{
|
|
51
|
+
if (this.fable.hasOwnProperty('Meadow'))
|
|
52
|
+
{
|
|
53
|
+
this.Meadow = this.fable.Meadow.loadFromPackageObject(this.EntitySchema.MeadowSchema);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.log.info(`Sync for ${this.EntitySchema.TableName} creating table if it doesn't exist...`);
|
|
57
|
+
|
|
58
|
+
if (this.Meadow && this.Meadow.provider)
|
|
59
|
+
{
|
|
60
|
+
return this.Meadow.provider.getProvider().createTable(this.EntitySchema, (pCreateError) =>
|
|
61
|
+
{
|
|
62
|
+
const tmpGUIDColumn = this.EntitySchema.Columns.find((c) => c.DataType == 'GUID');
|
|
63
|
+
const tmpDeletedColumn = this.EntitySchema.Columns.find((c) => c.Column == 'Deleted');
|
|
64
|
+
|
|
65
|
+
if (!tmpGUIDColumn && !tmpDeletedColumn)
|
|
66
|
+
{
|
|
67
|
+
this.log.info(`No GUID or Deleted columns for ${this.EntitySchema.TableName}; skipping index creation`);
|
|
68
|
+
return fCallback(pCreateError);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!this.fable.MeadowConnectionManager || !this.fable.MeadowConnectionManager.ConnectionPool)
|
|
72
|
+
{
|
|
73
|
+
this.log.info(`No connection manager available; skipping index creation for ${this.EntitySchema.TableName}`);
|
|
74
|
+
return fCallback(pCreateError);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let tmpAnticipate = this.fable.newAnticipate();
|
|
78
|
+
if (tmpGUIDColumn)
|
|
79
|
+
{
|
|
80
|
+
tmpAnticipate.anticipate(
|
|
81
|
+
(fNext) =>
|
|
82
|
+
{
|
|
83
|
+
return this.fable.MeadowConnectionManager.createIndex(this.EntitySchema, tmpGUIDColumn, true, fNext);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (tmpDeletedColumn)
|
|
87
|
+
{
|
|
88
|
+
tmpAnticipate.anticipate(
|
|
89
|
+
(fNext) =>
|
|
90
|
+
{
|
|
91
|
+
return this.fable.MeadowConnectionManager.createIndex(this.EntitySchema, tmpDeletedColumn, false, fNext);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
tmpAnticipate.wait(fCallback);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return fCallback();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
marshalRecord(pSourceRecord)
|
|
101
|
+
{
|
|
102
|
+
const tmpRecordToCommit = {};
|
|
103
|
+
|
|
104
|
+
for (const tmpColumn of this.EntitySchema.Columns)
|
|
105
|
+
{
|
|
106
|
+
if (pSourceRecord.hasOwnProperty(tmpColumn.Column))
|
|
107
|
+
{
|
|
108
|
+
switch (typeof(pSourceRecord[tmpColumn.Column]))
|
|
109
|
+
{
|
|
110
|
+
case 'null':
|
|
111
|
+
case 'undefined':
|
|
112
|
+
break;
|
|
113
|
+
case 'object':
|
|
114
|
+
if (pSourceRecord[tmpColumn.Column])
|
|
115
|
+
{
|
|
116
|
+
tmpRecordToCommit[tmpColumn.Column] = JSON.stringify(pSourceRecord[tmpColumn.Column]);
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
default:
|
|
120
|
+
if (tmpColumn.DataType == 'DateTime')
|
|
121
|
+
{
|
|
122
|
+
if ((typeof(pSourceRecord[tmpColumn.Column]) == 'string') && (pSourceRecord[tmpColumn.Column].length > 0))
|
|
123
|
+
{
|
|
124
|
+
tmpRecordToCommit[tmpColumn.Column] = this.fable.Dates.dayJS.utc(pSourceRecord[tmpColumn.Column]).format('YYYY-MM-DD HH:mm:ss.SSS');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (pSourceRecord[tmpColumn.Column] !== '')
|
|
128
|
+
{
|
|
129
|
+
tmpRecordToCommit[tmpColumn.Column] = pSourceRecord[tmpColumn.Column];
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (tmpColumn.Column.endsWith('JSON') && typeof pSourceRecord[tmpColumn.Column.substring(0, tmpColumn.Column.length - 4)] === 'object')
|
|
135
|
+
{
|
|
136
|
+
tmpRecordToCommit[tmpColumn.Column] = JSON.stringify(pSourceRecord[tmpColumn.Column.substring(0, tmpColumn.Column.length - 4)]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return tmpRecordToCommit;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
syncDeletedRecords(fCallback)
|
|
144
|
+
{
|
|
145
|
+
const tmpDeletedColumn = this.EntitySchema.Columns.find((c) => c.Column == 'Deleted');
|
|
146
|
+
if (!tmpDeletedColumn)
|
|
147
|
+
{
|
|
148
|
+
this.fable.log.info(`No Deleted column for ${this.EntitySchema.TableName}; skipping delete sync.`);
|
|
149
|
+
return fCallback();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.fable.log.info(`Checking for deleted records on server for ${this.EntitySchema.TableName}...`);
|
|
153
|
+
|
|
154
|
+
// Get the count of deleted records from the server.
|
|
155
|
+
// The explicit FBV~Deleted~EQ~1 filter overrides foxhound's automatic Deleted=0 filter.
|
|
156
|
+
this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}s/Count/FilteredTo/FBV~Deleted~EQ~1`,
|
|
157
|
+
(pError, pResponse, pBody) =>
|
|
158
|
+
{
|
|
159
|
+
if (pError || !pBody || !pBody.hasOwnProperty('Count'))
|
|
160
|
+
{
|
|
161
|
+
this.fable.log.warn(`Could not get deleted record count for ${this.EntitySchema.TableName}; skipping delete sync.`);
|
|
162
|
+
return fCallback();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const tmpDeletedCount = pBody.Count;
|
|
166
|
+
if (tmpDeletedCount < 1)
|
|
167
|
+
{
|
|
168
|
+
this.fable.log.info(`No deleted records on server for ${this.EntitySchema.TableName}.`);
|
|
169
|
+
return fCallback();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.fable.log.info(`Found ${tmpDeletedCount} deleted records on server for ${this.EntitySchema.TableName}; syncing deletions...`);
|
|
173
|
+
|
|
174
|
+
// Generate paginated URLs for deleted records
|
|
175
|
+
const tmpDeleteURLPartials = [];
|
|
176
|
+
for (let i = 0; i < tmpDeletedCount; i += this.PageSize)
|
|
177
|
+
{
|
|
178
|
+
tmpDeleteURLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~Deleted~EQ~1~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.fable.Utility.eachLimit(tmpDeleteURLPartials, 1,
|
|
182
|
+
(pURLPartial, fPageComplete) =>
|
|
183
|
+
{
|
|
184
|
+
this.fable.MeadowCloneRestClient.getJSON(pURLPartial,
|
|
185
|
+
(pDownloadError, pResponse, pBody) =>
|
|
186
|
+
{
|
|
187
|
+
if (pDownloadError || !pBody || !Array.isArray(pBody) || pBody.length < 1)
|
|
188
|
+
{
|
|
189
|
+
return fPageComplete();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.fable.Utility.eachLimit(pBody, 5,
|
|
193
|
+
(pEntityRecord, fRecordComplete) =>
|
|
194
|
+
{
|
|
195
|
+
const tmpRecordID = pEntityRecord[this.DefaultIdentifier];
|
|
196
|
+
if (!tmpRecordID || tmpRecordID < 1)
|
|
197
|
+
{
|
|
198
|
+
return fRecordComplete();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Read local record with delete tracking disabled so we can see all records
|
|
202
|
+
const tmpQuery = this.Meadow.query;
|
|
203
|
+
tmpQuery.addFilter(this.DefaultIdentifier, tmpRecordID);
|
|
204
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
205
|
+
|
|
206
|
+
this.Meadow.doRead(tmpQuery,
|
|
207
|
+
(pReadError, pQuery, pRecord) =>
|
|
208
|
+
{
|
|
209
|
+
if (pReadError || !pRecord)
|
|
210
|
+
{
|
|
211
|
+
// Record doesn't exist locally -- create it as deleted
|
|
212
|
+
const tmpRecordToCommit = this.marshalRecord(pEntityRecord);
|
|
213
|
+
|
|
214
|
+
const tmpCreateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
215
|
+
tmpCreateQuery.setDisableAutoIdentity(true);
|
|
216
|
+
tmpCreateQuery.setDisableAutoDateStamp(true);
|
|
217
|
+
tmpCreateQuery.setDisableAutoUserStamp(true);
|
|
218
|
+
tmpCreateQuery.setDisableDeleteTracking(true);
|
|
219
|
+
tmpCreateQuery.AllowIdentityInsert = true;
|
|
220
|
+
|
|
221
|
+
this.Meadow.doCreate(tmpCreateQuery,
|
|
222
|
+
(pCreateError) =>
|
|
223
|
+
{
|
|
224
|
+
if (pCreateError)
|
|
225
|
+
{
|
|
226
|
+
this.log.error(`Error creating deleted record ${this.EntitySchema.TableName} ID ${tmpRecordID}: ${pCreateError}`);
|
|
227
|
+
}
|
|
228
|
+
return fRecordComplete();
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (pRecord.Deleted == 1)
|
|
234
|
+
{
|
|
235
|
+
// Already marked deleted locally
|
|
236
|
+
return fRecordComplete();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Record exists locally but is not deleted -- update it
|
|
240
|
+
const tmpRecordToCommit = this.marshalRecord(pEntityRecord);
|
|
241
|
+
|
|
242
|
+
const tmpUpdateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
243
|
+
tmpUpdateQuery.setDisableAutoIdentity(true);
|
|
244
|
+
tmpUpdateQuery.setDisableAutoDateStamp(true);
|
|
245
|
+
tmpUpdateQuery.setDisableAutoUserStamp(true);
|
|
246
|
+
tmpUpdateQuery.setDisableDeleteTracking(true);
|
|
247
|
+
|
|
248
|
+
this.Meadow.doUpdate(tmpUpdateQuery,
|
|
249
|
+
(pUpdateError) =>
|
|
250
|
+
{
|
|
251
|
+
if (pUpdateError)
|
|
252
|
+
{
|
|
253
|
+
this.log.error(`Error marking record as deleted ${this.EntitySchema.TableName} ID ${tmpRecordID}: ${pUpdateError}`);
|
|
254
|
+
}
|
|
255
|
+
return fRecordComplete();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
(pRecordSyncError) =>
|
|
260
|
+
{
|
|
261
|
+
return fPageComplete();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
(pDeleteSyncError) =>
|
|
266
|
+
{
|
|
267
|
+
this.fable.log.info(`Delete sync complete for ${this.EntitySchema.TableName} (${tmpDeletedCount} deleted records processed).`);
|
|
268
|
+
return fCallback();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
sync(fCallback)
|
|
274
|
+
{
|
|
275
|
+
this.operation.createTimeStamp('EntityInitialSync');
|
|
276
|
+
|
|
277
|
+
const tmpSyncState = (
|
|
278
|
+
{
|
|
279
|
+
Local: { MaxIDEntity: -1, RecordCount: 0 },
|
|
280
|
+
Server: { MaxIDEntity: -1, RecordCount: 0 },
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
this.fable.Utility.waterfall(
|
|
284
|
+
[
|
|
285
|
+
(fStageComplete) =>
|
|
286
|
+
{
|
|
287
|
+
// Get the Max ID from local database
|
|
288
|
+
const tmpQuery = this.Meadow.query;
|
|
289
|
+
tmpQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
|
|
290
|
+
tmpQuery.setCap(1);
|
|
291
|
+
this.Meadow.doRead(tmpQuery,
|
|
292
|
+
(pReadError, pQuery, pRecord) =>
|
|
293
|
+
{
|
|
294
|
+
if (pReadError)
|
|
295
|
+
{
|
|
296
|
+
this.fable.log.error(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`, { Error: pReadError });
|
|
297
|
+
return fStageComplete(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
298
|
+
}
|
|
299
|
+
if (!pRecord)
|
|
300
|
+
{
|
|
301
|
+
this.fable.log.warn(`No records found in local ${this.EntitySchema.TableName}.`);
|
|
302
|
+
return fStageComplete();
|
|
303
|
+
}
|
|
304
|
+
this.fable.log.info(`Found local max entity ID ${this.EntitySchema.TableName}: ${pRecord[this.DefaultIdentifier]}`);
|
|
305
|
+
tmpSyncState.Local.MaxIDEntity = pRecord[this.DefaultIdentifier];
|
|
306
|
+
return fStageComplete();
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
(fStageComplete) =>
|
|
310
|
+
{
|
|
311
|
+
// Get the count from local database
|
|
312
|
+
const tmpQuery = this.Meadow.query;
|
|
313
|
+
this.Meadow.doCount(tmpQuery,
|
|
314
|
+
(pCountError, pQuery, pCount) =>
|
|
315
|
+
{
|
|
316
|
+
if (pCountError)
|
|
317
|
+
{
|
|
318
|
+
this.fable.log.error(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`, { Error: pCountError });
|
|
319
|
+
return fStageComplete(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`);
|
|
320
|
+
}
|
|
321
|
+
tmpSyncState.Local.RecordCount = pCount;
|
|
322
|
+
return fStageComplete();
|
|
323
|
+
});
|
|
324
|
+
},
|
|
325
|
+
(fStageComplete) =>
|
|
326
|
+
{
|
|
327
|
+
// Get the Max ID from the server
|
|
328
|
+
this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}/Max/${this.DefaultIdentifier}`,
|
|
329
|
+
(pError, pResponse, pBody) =>
|
|
330
|
+
{
|
|
331
|
+
if (pError)
|
|
332
|
+
{
|
|
333
|
+
this.fable.log.error(`Error getting server max entity ID ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
|
|
334
|
+
return fStageComplete(`Error getting server max entity ID ${this.EntitySchema.TableName}: ${pError}`);
|
|
335
|
+
}
|
|
336
|
+
if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
|
|
337
|
+
{
|
|
338
|
+
this.fable.log.info(`Found server max entity ID ${this.EntitySchema.TableName}: ${pBody[this.DefaultIdentifier]}`);
|
|
339
|
+
tmpSyncState.Server.MaxIDEntity = pBody[this.DefaultIdentifier];
|
|
340
|
+
}
|
|
341
|
+
else
|
|
342
|
+
{
|
|
343
|
+
this.fable.log.warn(`No records found in server for max entity ID of ${this.EntitySchema.TableName}.`);
|
|
344
|
+
}
|
|
345
|
+
return fStageComplete();
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
(fStageComplete) =>
|
|
349
|
+
{
|
|
350
|
+
// Get the count from the server
|
|
351
|
+
this.fable.MeadowCloneRestClient.getJSON(`${this.EntitySchema.TableName}s/Count`,
|
|
352
|
+
(pError, pResponse, pBody) =>
|
|
353
|
+
{
|
|
354
|
+
if (pError)
|
|
355
|
+
{
|
|
356
|
+
this.fable.log.error(`Error getting server count for ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
|
|
357
|
+
return fStageComplete(`Error getting server count for ${this.EntitySchema.TableName}: ${pError}`);
|
|
358
|
+
}
|
|
359
|
+
if (pBody && pBody.hasOwnProperty('Count'))
|
|
360
|
+
{
|
|
361
|
+
this.fable.log.info(`Found server count for ${this.EntitySchema.TableName}: ${pBody.Count}`);
|
|
362
|
+
tmpSyncState.Server.RecordCount = pBody.Count;
|
|
363
|
+
}
|
|
364
|
+
else
|
|
365
|
+
{
|
|
366
|
+
this.fable.log.warn(`No records found in server based on count for ${this.EntitySchema.TableName}.`);
|
|
367
|
+
}
|
|
368
|
+
return fStageComplete();
|
|
369
|
+
});
|
|
370
|
+
},
|
|
371
|
+
(fStageComplete) =>
|
|
372
|
+
{
|
|
373
|
+
tmpSyncState.EstimatedRecordCount = tmpSyncState.Server.RecordCount - tmpSyncState.Local.RecordCount;
|
|
374
|
+
|
|
375
|
+
this.operation.createProgressTracker(tmpSyncState.EstimatedRecordCount, `FullSync-${this.EntitySchema.TableName}`);
|
|
376
|
+
this.operation.printProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`);
|
|
377
|
+
|
|
378
|
+
// Generate paginated URL partials
|
|
379
|
+
tmpSyncState.URLPartials = [];
|
|
380
|
+
for (let i = 0; i < tmpSyncState.Server.RecordCount; i += this.PageSize)
|
|
381
|
+
{
|
|
382
|
+
tmpSyncState.URLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~${this.DefaultIdentifier}~GT~${tmpSyncState.Local.MaxIDEntity}~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
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.`);
|
|
386
|
+
|
|
387
|
+
return fStageComplete();
|
|
388
|
+
},
|
|
389
|
+
(fStageComplete) =>
|
|
390
|
+
{
|
|
391
|
+
this.fable.Utility.eachLimit(tmpSyncState.URLPartials, 1,
|
|
392
|
+
(pURLPartial, fDownloadComplete) =>
|
|
393
|
+
{
|
|
394
|
+
this.fable.MeadowCloneRestClient.getJSON(pURLPartial,
|
|
395
|
+
(pDownloadError, pResponse, pBody) =>
|
|
396
|
+
{
|
|
397
|
+
if (pDownloadError)
|
|
398
|
+
{
|
|
399
|
+
this.fable.log.error(`Error getting URL Partial [${pURLPartial}]: ${pDownloadError}`, { Error: pDownloadError });
|
|
400
|
+
return fDownloadComplete();
|
|
401
|
+
}
|
|
402
|
+
if (pBody && pBody.length > 0)
|
|
403
|
+
{
|
|
404
|
+
this.fable.Utility.eachLimit(pBody, 5,
|
|
405
|
+
(pEntityRecord, fEntitySyncComplete) =>
|
|
406
|
+
{
|
|
407
|
+
const tmpRecord = pEntityRecord;
|
|
408
|
+
const tmpQuery = this.Meadow.query;
|
|
409
|
+
|
|
410
|
+
if ((typeof(tmpRecord[this.DefaultIdentifier]) !== 'undefined') && (tmpRecord[this.DefaultIdentifier] > 0))
|
|
411
|
+
{
|
|
412
|
+
tmpQuery.addFilter(this.DefaultIdentifier, tmpRecord[this.DefaultIdentifier]);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
this.Meadow.doRead(tmpQuery,
|
|
416
|
+
(pReadError, pQuery, pRecord) =>
|
|
417
|
+
{
|
|
418
|
+
if (pReadError)
|
|
419
|
+
{
|
|
420
|
+
this.fable.log.error(`Error reading record ${this.EntitySchema.TableName}: ${pReadError}`, { Error: pReadError, PassedRecord: tmpRecord });
|
|
421
|
+
return fEntitySyncComplete();
|
|
422
|
+
}
|
|
423
|
+
if (!pRecord)
|
|
424
|
+
{
|
|
425
|
+
// Record not found -- create it
|
|
426
|
+
const tmpRecordToCommit = this.marshalRecord(tmpRecord);
|
|
427
|
+
|
|
428
|
+
const tmpCreateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
429
|
+
|
|
430
|
+
tmpCreateQuery.setDisableAutoIdentity(true);
|
|
431
|
+
tmpCreateQuery.setDisableAutoDateStamp(true);
|
|
432
|
+
tmpCreateQuery.setDisableAutoUserStamp(true);
|
|
433
|
+
tmpCreateQuery.setDisableDeleteTracking(true);
|
|
434
|
+
|
|
435
|
+
tmpCreateQuery.AllowIdentityInsert = true;
|
|
436
|
+
|
|
437
|
+
this.Meadow.doCreate(tmpCreateQuery,
|
|
438
|
+
(pCreateError) =>
|
|
439
|
+
{
|
|
440
|
+
if (pCreateError)
|
|
441
|
+
{
|
|
442
|
+
this.log.error(`Error creating record ${this.EntitySchema.TableName}: ${pCreateError}`, pCreateError);
|
|
443
|
+
return fEntitySyncComplete();
|
|
444
|
+
}
|
|
445
|
+
this.operation.incrementProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`, 1);
|
|
446
|
+
return fEntitySyncComplete();
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
else
|
|
450
|
+
{
|
|
451
|
+
return fEntitySyncComplete();
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
},
|
|
455
|
+
(pEntitySyncError) =>
|
|
456
|
+
{
|
|
457
|
+
this.operation.printProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`);
|
|
458
|
+
if (pEntitySyncError)
|
|
459
|
+
{
|
|
460
|
+
this.log.error(`Problem or early completion syncing entity ${this.EntitySchema.TableName}: ${pEntitySyncError}`, pEntitySyncError);
|
|
461
|
+
}
|
|
462
|
+
return fDownloadComplete();
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
else
|
|
466
|
+
{
|
|
467
|
+
if (Array.isArray(pBody) && pBody.length == 0)
|
|
468
|
+
{
|
|
469
|
+
return fDownloadComplete(new Error('Records depleted!'));
|
|
470
|
+
}
|
|
471
|
+
return fDownloadComplete();
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
},
|
|
475
|
+
(pDownloadError) =>
|
|
476
|
+
{
|
|
477
|
+
if (pDownloadError)
|
|
478
|
+
{
|
|
479
|
+
this.fable.log.error(`Error returned URL Partial .. this may not be an error: ${pDownloadError}`);
|
|
480
|
+
}
|
|
481
|
+
fStageComplete();
|
|
482
|
+
});
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
(pError) =>
|
|
486
|
+
{
|
|
487
|
+
if (pError)
|
|
488
|
+
{
|
|
489
|
+
this.fable.log.error(`Error performing sync ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (this.SyncDeletedRecords)
|
|
493
|
+
{
|
|
494
|
+
return this.syncDeletedRecords(() => { return fCallback(); });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return fCallback();
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
module.exports = MeadowSyncEntityInitial;
|