retold-data-service 2.0.13 → 2.0.16

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.
Files changed (52) hide show
  1. package/.claude/launch.json +11 -0
  2. package/bin/retold-data-service-clone.js +286 -0
  3. package/package.json +18 -9
  4. package/source/Retold-Data-Service.js +275 -73
  5. package/source/services/Retold-Data-Service-ConnectionManager.js +277 -0
  6. package/source/services/Retold-Data-Service-MeadowEndpoints.js +217 -0
  7. package/source/services/Retold-Data-Service-ModelManager.js +335 -0
  8. package/source/services/data-cloner/DataCloner-Command-Connection.js +138 -0
  9. package/source/services/data-cloner/DataCloner-Command-Headless.js +357 -0
  10. package/source/services/data-cloner/DataCloner-Command-Schema.js +367 -0
  11. package/source/services/data-cloner/DataCloner-Command-Session.js +229 -0
  12. package/source/services/data-cloner/DataCloner-Command-Sync.js +491 -0
  13. package/source/services/data-cloner/DataCloner-Command-WebUI.js +40 -0
  14. package/source/services/data-cloner/DataCloner-ProviderRegistry.js +20 -0
  15. package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +751 -0
  16. package/source/services/data-cloner/data-cloner-web.html +2706 -0
  17. package/source/services/integration-telemetry/IntegrationTelemetry-Command-Dashboard.js +60 -0
  18. package/source/services/integration-telemetry/IntegrationTelemetry-Command-Integrations.js +132 -0
  19. package/source/services/integration-telemetry/IntegrationTelemetry-Command-Runs.js +93 -0
  20. package/source/services/integration-telemetry/IntegrationTelemetry-StorageProvider-Base.js +116 -0
  21. package/source/services/integration-telemetry/IntegrationTelemetry-StorageProvider-Bibliograph.js +495 -0
  22. package/source/services/integration-telemetry/Retold-Data-Service-IntegrationTelemetry.js +224 -0
  23. package/source/services/meadow-integration/MeadowIntegration-Command-CSVCheck.js +85 -0
  24. package/source/services/meadow-integration/MeadowIntegration-Command-CSVTransform.js +180 -0
  25. package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionIntersect.js +153 -0
  26. package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionPush.js +190 -0
  27. package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionToArray.js +113 -0
  28. package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionToCSV.js +211 -0
  29. package/source/services/meadow-integration/MeadowIntegration-Command-EntityFromTabularFolder.js +244 -0
  30. package/source/services/meadow-integration/MeadowIntegration-Command-JSONArrayTransform.js +213 -0
  31. package/source/services/meadow-integration/MeadowIntegration-Command-TSVCheck.js +80 -0
  32. package/source/services/meadow-integration/MeadowIntegration-Command-TSVTransform.js +166 -0
  33. package/source/services/meadow-integration/Retold-Data-Service-MeadowIntegration.js +113 -0
  34. package/source/services/migration-manager/MigrationManager-Command-Connections.js +220 -0
  35. package/source/services/migration-manager/MigrationManager-Command-DiffMigrate.js +169 -0
  36. package/source/services/migration-manager/MigrationManager-Command-Schemas.js +532 -0
  37. package/source/services/migration-manager/MigrationManager-Command-WebUI.js +123 -0
  38. package/source/services/migration-manager/Retold-Data-Service-MigrationManager.js +357 -0
  39. package/source/services/stricture/Retold-Data-Service-Stricture.js +303 -0
  40. package/source/services/stricture/Stricture-Command-Compile.js +39 -0
  41. package/source/services/stricture/Stricture-Command-Generate-AuthorizationChart.js +14 -0
  42. package/source/services/stricture/Stricture-Command-Generate-DictionaryCSV.js +14 -0
  43. package/source/services/stricture/Stricture-Command-Generate-LaTeX.js +14 -0
  44. package/source/services/stricture/Stricture-Command-Generate-Markdown.js +14 -0
  45. package/source/services/stricture/Stricture-Command-Generate-Meadow.js +14 -0
  46. package/source/services/stricture/Stricture-Command-Generate-ModelGraph.js +14 -0
  47. package/source/services/stricture/Stricture-Command-Generate-MySQL.js +14 -0
  48. package/source/services/stricture/Stricture-Command-Generate-MySQLMigrate.js +14 -0
  49. package/source/services/stricture/Stricture-Command-Generate-Pict.js +14 -0
  50. package/source/services/stricture/Stricture-Command-Generate-TestObjectContainers.js +14 -0
  51. package/test/RetoldDataService_tests.js +161 -1
  52. package/debug/data/books.csv +0 -10001
@@ -0,0 +1,491 @@
1
+ /**
2
+ * DataCloner Sync Routes
3
+ *
4
+ * Registers /clone/sync/* endpoints for starting, monitoring, and stopping
5
+ * data synchronization via meadow-integration.
6
+ *
7
+ * @param {Object} pDataClonerService - The RetoldDataServiceDataCloner instance
8
+ * @param {Object} pOratorServiceServer - The Orator ServiceServer instance
9
+ */
10
+ module.exports = (pDataClonerService, pOratorServiceServer) =>
11
+ {
12
+ let tmpFable = pDataClonerService.fable;
13
+ let tmpCloneState = pDataClonerService.cloneState;
14
+ let tmpPrefix = pDataClonerService.routePrefix;
15
+
16
+ // POST /clone/sync/start
17
+ pOratorServiceServer.post(`${tmpPrefix}/sync/start`,
18
+ (pRequest, pResponse, fNext) =>
19
+ {
20
+ if (tmpCloneState.SyncRunning)
21
+ {
22
+ pResponse.send(400, { Success: false, Error: 'Sync is already running.' });
23
+ return fNext();
24
+ }
25
+
26
+ if (!tmpCloneState.RemoteServerURL)
27
+ {
28
+ pResponse.send(400, { Success: false, Error: 'No remote server configured.' });
29
+ return fNext();
30
+ }
31
+
32
+ if (!tmpFable.MeadowSync || !tmpFable.MeadowSync.MeadowSyncEntities)
33
+ {
34
+ pResponse.send(400, { Success: false, Error: 'No sync entities available. Deploy a schema first.' });
35
+ return fNext();
36
+ }
37
+
38
+ let tmpBody = pRequest.body || {};
39
+ let tmpSelectedTables = tmpBody.Tables || [];
40
+ let tmpRequestedMode = tmpBody.SyncMode || 'Initial';
41
+ let tmpMaxRecords = parseInt(tmpBody.MaxRecordsPerEntity, 10) || 0;
42
+
43
+ // Update SyncDeletedRecords from request if provided
44
+ if (tmpBody.hasOwnProperty('SyncDeletedRecords'))
45
+ {
46
+ tmpCloneState.SyncDeletedRecords = !!tmpBody.SyncDeletedRecords;
47
+ let tmpEntityNames = Object.keys(tmpFable.MeadowSync.MeadowSyncEntities);
48
+ for (let i = 0; i < tmpEntityNames.length; i++)
49
+ {
50
+ tmpFable.MeadowSync.MeadowSyncEntities[tmpEntityNames[i]].SyncDeletedRecords = tmpCloneState.SyncDeletedRecords;
51
+ }
52
+ }
53
+
54
+ // Update MaxRecordsPerEntity on sync entities
55
+ if (tmpMaxRecords > 0)
56
+ {
57
+ let tmpEntityNames = Object.keys(tmpFable.MeadowSync.MeadowSyncEntities);
58
+ for (let i = 0; i < tmpEntityNames.length; i++)
59
+ {
60
+ tmpFable.MeadowSync.MeadowSyncEntities[tmpEntityNames[i]].MaxRecordsPerEntity = tmpMaxRecords;
61
+ }
62
+ }
63
+
64
+ // Update DateTimePrecisionMS on MeadowSync and all sync entities
65
+ if (tmpBody.hasOwnProperty('DateTimePrecisionMS'))
66
+ {
67
+ let tmpPrecision = parseInt(tmpBody.DateTimePrecisionMS, 10);
68
+ if (!isNaN(tmpPrecision))
69
+ {
70
+ tmpFable.MeadowSync.DateTimePrecisionMS = tmpPrecision;
71
+ let tmpEntityNames = Object.keys(tmpFable.MeadowSync.MeadowSyncEntities);
72
+ for (let i = 0; i < tmpEntityNames.length; i++)
73
+ {
74
+ tmpFable.MeadowSync.MeadowSyncEntities[tmpEntityNames[i]].DateTimePrecisionMS = tmpPrecision;
75
+ }
76
+ }
77
+ }
78
+
79
+ // If no tables specified, sync all entities
80
+ if (tmpSelectedTables.length === 0)
81
+ {
82
+ tmpSelectedTables = tmpFable.MeadowSync.SyncEntityList || Object.keys(tmpFable.MeadowSync.MeadowSyncEntities);
83
+ }
84
+
85
+ if (tmpSelectedTables.length === 0)
86
+ {
87
+ pResponse.send(400, { Success: false, Error: 'No tables available for sync. Deploy a schema first.' });
88
+ return fNext();
89
+ }
90
+
91
+ // ---- Handle Sync Mode switching ----
92
+ let fStartSync = () =>
93
+ {
94
+ tmpFable.log.info(`Data Cloner: Starting ${tmpCloneState.SyncMode} sync for ${tmpSelectedTables.length} tables via meadow-integration (SyncDeletedRecords: ${tmpCloneState.SyncDeletedRecords})`);
95
+
96
+ // Initialize progress tracking
97
+ tmpCloneState.SyncRunning = true;
98
+ tmpCloneState.SyncStopping = false;
99
+ tmpCloneState.SyncProgress = {};
100
+ tmpCloneState.SyncRESTErrors = {};
101
+
102
+ for (let i = 0; i < tmpSelectedTables.length; i++)
103
+ {
104
+ tmpCloneState.SyncProgress[tmpSelectedTables[i]] = (
105
+ {
106
+ Status: 'Pending',
107
+ Total: 0,
108
+ Synced: 0,
109
+ Errors: 0,
110
+ StartTime: null,
111
+ EndTime: null
112
+ });
113
+ tmpCloneState.SyncRESTErrors[tmpSelectedTables[i]] = 0;
114
+ }
115
+
116
+ // Start the sync process asynchronously
117
+ pDataClonerService.syncTables(tmpSelectedTables);
118
+
119
+ pResponse.send(200,
120
+ {
121
+ Success: true,
122
+ Tables: tmpSelectedTables,
123
+ SyncMode: tmpCloneState.SyncMode,
124
+ SyncDeletedRecords: tmpCloneState.SyncDeletedRecords,
125
+ Message: `${tmpCloneState.SyncMode} sync started via meadow-integration.`
126
+ });
127
+ return fNext();
128
+ };
129
+
130
+ if (tmpRequestedMode !== tmpCloneState.SyncMode && tmpCloneState.DeployedModelObject)
131
+ {
132
+ tmpFable.log.info(`Data Cloner: Switching sync mode from ${tmpCloneState.SyncMode} to ${tmpRequestedMode} — re-creating sync entities...`);
133
+ tmpCloneState.SyncMode = tmpRequestedMode;
134
+ tmpFable.MeadowSync.SyncMode = tmpRequestedMode;
135
+
136
+ tmpFable.MeadowSync.loadMeadowSchema(tmpCloneState.DeployedModelObject,
137
+ (pReinitError) =>
138
+ {
139
+ if (pReinitError)
140
+ {
141
+ tmpFable.log.warn(`Data Cloner: Mode switch schema re-init warning: ${pReinitError}`);
142
+ }
143
+ let tmpReinitEntities = Object.keys(tmpFable.MeadowSync.MeadowSyncEntities);
144
+ tmpFable.log.info(`Data Cloner: Re-created ${tmpReinitEntities.length} sync entities in ${tmpRequestedMode} mode`);
145
+
146
+ // Update SyncDeletedRecords and MaxRecordsPerEntity on new entities
147
+ for (let i = 0; i < tmpReinitEntities.length; i++)
148
+ {
149
+ tmpFable.MeadowSync.MeadowSyncEntities[tmpReinitEntities[i]].SyncDeletedRecords = tmpCloneState.SyncDeletedRecords;
150
+ if (tmpMaxRecords > 0)
151
+ {
152
+ tmpFable.MeadowSync.MeadowSyncEntities[tmpReinitEntities[i]].MaxRecordsPerEntity = tmpMaxRecords;
153
+ }
154
+ }
155
+
156
+ return fStartSync();
157
+ });
158
+ }
159
+ else
160
+ {
161
+ tmpCloneState.SyncMode = tmpRequestedMode;
162
+ return fStartSync();
163
+ }
164
+ });
165
+
166
+ // GET /clone/sync/status
167
+ pOratorServiceServer.get(`${tmpPrefix}/sync/status`,
168
+ (pRequest, pResponse, fNext) =>
169
+ {
170
+ // Update progress from MeadowSync operation trackers
171
+ if (tmpFable.MeadowSync && tmpFable.MeadowSync.MeadowSyncEntities)
172
+ {
173
+ let tmpEntityNames = Object.keys(tmpFable.MeadowSync.MeadowSyncEntities);
174
+ for (let i = 0; i < tmpEntityNames.length; i++)
175
+ {
176
+ let tmpEntityName = tmpEntityNames[i];
177
+ let tmpProgress = tmpCloneState.SyncProgress[tmpEntityName];
178
+ if (tmpProgress && (tmpProgress.Status === 'Syncing' || tmpProgress.Status === 'Pending'))
179
+ {
180
+ let tmpSyncEntity = tmpFable.MeadowSync.MeadowSyncEntities[tmpEntityName];
181
+ if (tmpSyncEntity && tmpSyncEntity.operation)
182
+ {
183
+ let tmpTracker = tmpSyncEntity.operation.progressTrackers[`FullSync-${tmpEntityName}`];
184
+ if (tmpTracker)
185
+ {
186
+ tmpProgress.Total = tmpTracker.TotalCount || 0;
187
+ tmpProgress.Synced = Math.max(tmpTracker.CurrentCount || 0, 0);
188
+ }
189
+ }
190
+ let tmpRESTErrors = tmpCloneState.SyncRESTErrors[tmpEntityName] || 0;
191
+ if (tmpRESTErrors > 0)
192
+ {
193
+ tmpProgress.Errors = tmpRESTErrors;
194
+ }
195
+ }
196
+ }
197
+ }
198
+
199
+ pResponse.send(200,
200
+ {
201
+ Running: tmpCloneState.SyncRunning,
202
+ Stopping: tmpCloneState.SyncStopping,
203
+ SyncMode: tmpCloneState.SyncMode,
204
+ Tables: tmpCloneState.SyncProgress
205
+ });
206
+ return fNext();
207
+ });
208
+
209
+ // GET /clone/sync/live-status
210
+ // Returns a human-readable narrative of what the data cloner is doing right now.
211
+ pOratorServiceServer.get(`${tmpPrefix}/sync/live-status`,
212
+ (pRequest, pResponse, fNext) =>
213
+ {
214
+ let fFormatNumber = (pNum) =>
215
+ {
216
+ return pNum.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
217
+ };
218
+
219
+ let fFormatDuration = (pMs) =>
220
+ {
221
+ let tmpSeconds = Math.floor(pMs / 1000);
222
+ if (tmpSeconds < 60) return `${tmpSeconds}s`;
223
+ let tmpMin = Math.floor(tmpSeconds / 60);
224
+ let tmpSec = tmpSeconds % 60;
225
+ if (tmpMin < 60) return `${tmpMin}m ${tmpSec}s`;
226
+ let tmpHr = Math.floor(tmpMin / 60);
227
+ tmpMin = tmpMin % 60;
228
+ return `${tmpHr}h ${tmpMin}m`;
229
+ };
230
+
231
+ // Determine overall phase
232
+ let tmpPhase = 'idle';
233
+ let tmpMessage = 'Idle';
234
+ let tmpActiveEntity = null;
235
+ let tmpActiveProgress = null;
236
+ let tmpCompleted = [];
237
+ let tmpPending = [];
238
+ let tmpErrors = [];
239
+ let tmpTotalSynced = 0;
240
+ let tmpTotalRecords = 0;
241
+ let tmpElapsed = null;
242
+ let tmpETA = null;
243
+
244
+ if (!tmpCloneState.ConnectionConnected)
245
+ {
246
+ tmpPhase = 'disconnected';
247
+ tmpMessage = 'No database connected';
248
+ }
249
+ else if (!tmpCloneState.SessionAuthenticated && !tmpCloneState.SessionConfigured)
250
+ {
251
+ tmpPhase = 'idle';
252
+ tmpMessage = `Connected to ${tmpCloneState.ConnectionProvider} — waiting for remote session configuration`;
253
+ }
254
+ else if (tmpCloneState.SessionConfigured && !tmpCloneState.SessionAuthenticated)
255
+ {
256
+ tmpPhase = 'idle';
257
+ tmpMessage = `Connected to ${tmpCloneState.ConnectionProvider} — waiting for authentication`;
258
+ }
259
+ else if (tmpCloneState.SyncStopping)
260
+ {
261
+ tmpPhase = 'stopping';
262
+ tmpMessage = 'Stopping sync...';
263
+ }
264
+ else if (tmpCloneState.SyncRunning)
265
+ {
266
+ tmpPhase = 'syncing';
267
+
268
+ // Check for pre-count phase
269
+ if (tmpCloneState.SyncPhase === 'counting')
270
+ {
271
+ let tmpPC = tmpCloneState.PreCountProgress || { Counted: 0, TotalTables: 0 };
272
+ tmpMessage = `Analyzing tables: counted ${tmpPC.Counted} / ${tmpPC.TotalTables}...`;
273
+
274
+ pResponse.send(200,
275
+ {
276
+ Phase: tmpPhase,
277
+ Message: tmpMessage,
278
+ ActiveEntity: null,
279
+ ActiveProgress: null,
280
+ Completed: 0,
281
+ Pending: tmpPC.TotalTables,
282
+ Errors: 0,
283
+ TotalTables: tmpPC.TotalTables,
284
+ TotalSynced: 0,
285
+ TotalRecords: 0,
286
+ Elapsed: null,
287
+ SyncMode: tmpCloneState.SyncMode,
288
+ ETA: null,
289
+ PreCountGrandTotal: 0,
290
+ PreCountProgress: tmpPC
291
+ });
292
+ return fNext();
293
+ }
294
+
295
+ // Update progress from MeadowSync operation trackers (same as /sync/status)
296
+ if (tmpFable.MeadowSync && tmpFable.MeadowSync.MeadowSyncEntities)
297
+ {
298
+ let tmpEntityNames = Object.keys(tmpFable.MeadowSync.MeadowSyncEntities);
299
+ for (let i = 0; i < tmpEntityNames.length; i++)
300
+ {
301
+ let tmpEntityName = tmpEntityNames[i];
302
+ let tmpProgress = tmpCloneState.SyncProgress[tmpEntityName];
303
+ if (tmpProgress && (tmpProgress.Status === 'Syncing' || tmpProgress.Status === 'Pending'))
304
+ {
305
+ let tmpSyncEntity = tmpFable.MeadowSync.MeadowSyncEntities[tmpEntityName];
306
+ if (tmpSyncEntity && tmpSyncEntity.operation)
307
+ {
308
+ let tmpTracker = tmpSyncEntity.operation.progressTrackers[`FullSync-${tmpEntityName}`];
309
+ if (tmpTracker)
310
+ {
311
+ tmpProgress.Total = tmpTracker.TotalCount || 0;
312
+ tmpProgress.Synced = Math.max(tmpTracker.CurrentCount || 0, 0);
313
+ }
314
+ }
315
+ let tmpRESTErrors = tmpCloneState.SyncRESTErrors[tmpEntityName] || 0;
316
+ if (tmpRESTErrors > 0)
317
+ {
318
+ tmpProgress.Errors = tmpRESTErrors;
319
+ }
320
+ }
321
+ }
322
+ }
323
+
324
+ // Categorize tables
325
+ let tmpTableNames = Object.keys(tmpCloneState.SyncProgress);
326
+ for (let i = 0; i < tmpTableNames.length; i++)
327
+ {
328
+ let tmpName = tmpTableNames[i];
329
+ let tmpP = tmpCloneState.SyncProgress[tmpName];
330
+ tmpTotalSynced += (tmpP.Synced || 0);
331
+ tmpTotalRecords += (tmpP.Total || 0);
332
+
333
+ if (tmpP.Status === 'Syncing')
334
+ {
335
+ tmpActiveEntity = tmpName;
336
+ tmpActiveProgress = tmpP;
337
+ }
338
+ else if (tmpP.Status === 'Complete' || tmpP.Status === 'Partial')
339
+ {
340
+ tmpCompleted.push({ Name: tmpName, Synced: tmpP.Synced || 0, Total: tmpP.Total || 0, Status: tmpP.Status });
341
+ }
342
+ else if (tmpP.Status === 'Error')
343
+ {
344
+ tmpErrors.push({ Name: tmpName, Error: tmpP.ErrorMessage || 'Unknown error' });
345
+ }
346
+ else if (tmpP.Status === 'Pending')
347
+ {
348
+ tmpPending.push(tmpName);
349
+ }
350
+ }
351
+
352
+ // Build elapsed time and ETA
353
+ if (tmpCloneState.SyncStartTime)
354
+ {
355
+ let tmpElapsedMs = Date.now() - new Date(tmpCloneState.SyncStartTime).getTime();
356
+ tmpElapsed = fFormatDuration(tmpElapsedMs);
357
+
358
+ // Compute ETA using pre-counted grand total (or running total) and records synced so far
359
+ let tmpETATotalRecords = tmpCloneState.PreCountGrandTotal || tmpTotalRecords;
360
+ if (tmpETATotalRecords > 0 && tmpTotalSynced > 0 && tmpElapsedMs > 5000)
361
+ {
362
+ let tmpRate = tmpTotalSynced / tmpElapsedMs; // records per ms
363
+ let tmpRemaining = tmpETATotalRecords - tmpTotalSynced;
364
+ if (tmpRate > 0 && tmpRemaining > 0)
365
+ {
366
+ tmpETA = fFormatDuration(tmpRemaining / tmpRate);
367
+ }
368
+ }
369
+ }
370
+
371
+ // Build the narrative
372
+ let tmpParts = [];
373
+
374
+ if (tmpActiveEntity && tmpActiveProgress)
375
+ {
376
+ let tmpRecordProgress = '';
377
+ if (tmpActiveProgress.Total > 0)
378
+ {
379
+ tmpRecordProgress = ` — record ${fFormatNumber(tmpActiveProgress.Synced)} / ${fFormatNumber(tmpActiveProgress.Total)}`;
380
+ }
381
+ else
382
+ {
383
+ tmpRecordProgress = ' — counting records...';
384
+ }
385
+ tmpParts.push(`${tmpCloneState.SyncMode} sync: ${tmpActiveEntity}${tmpRecordProgress}`);
386
+ }
387
+ else
388
+ {
389
+ tmpParts.push(`${tmpCloneState.SyncMode} sync in progress`);
390
+ }
391
+
392
+ // Summarize completed tables
393
+ if (tmpCompleted.length > 0)
394
+ {
395
+ // Show a few completed entities by name, then summarize the rest
396
+ let tmpCompletedSummary = [];
397
+ let tmpShowCount = Math.min(3, tmpCompleted.length);
398
+ // Show the most recently completed (last in the list)
399
+ for (let i = tmpCompleted.length - tmpShowCount; i < tmpCompleted.length; i++)
400
+ {
401
+ let tmpC = tmpCompleted[i];
402
+ tmpCompletedSummary.push(`${tmpC.Name} (${fFormatNumber(tmpC.Synced)})`);
403
+ }
404
+ let tmpCompletedStr = tmpCompletedSummary.join(', ');
405
+ if (tmpCompleted.length > tmpShowCount)
406
+ {
407
+ tmpCompletedStr = `${tmpCompleted.length - tmpShowCount} others, ` + tmpCompletedStr;
408
+ }
409
+ tmpParts.push(`Synced: ${tmpCompletedStr}`);
410
+ }
411
+
412
+ if (tmpPending.length > 0)
413
+ {
414
+ tmpParts.push(`${tmpPending.length} table${tmpPending.length === 1 ? '' : 's'} remaining`);
415
+ }
416
+
417
+ if (tmpErrors.length > 0)
418
+ {
419
+ tmpParts.push(`${tmpErrors.length} error${tmpErrors.length === 1 ? '' : 's'}`);
420
+ }
421
+
422
+ tmpMessage = tmpParts.join('. ') + '.';
423
+ }
424
+ else if (tmpCloneState.SyncReport)
425
+ {
426
+ // Sync finished — show summary
427
+ tmpPhase = 'complete';
428
+ let tmpR = tmpCloneState.SyncReport;
429
+ tmpMessage = `Sync ${tmpR.Outcome.toLowerCase()}: ${fFormatNumber(tmpR.Summary.TotalSynced)} records across ${tmpR.Summary.TotalTables} tables`;
430
+ if (tmpR.RunTimestamps && tmpR.RunTimestamps.DurationSeconds)
431
+ {
432
+ tmpMessage += ` in ${fFormatDuration(tmpR.RunTimestamps.DurationSeconds * 1000)}`;
433
+ }
434
+ tmpTotalSynced = tmpR.Summary.TotalSynced;
435
+ tmpTotalRecords = tmpR.Summary.TotalRecords;
436
+ }
437
+ else if (tmpCloneState.SessionAuthenticated)
438
+ {
439
+ tmpPhase = 'ready';
440
+ tmpMessage = `Connected to ${tmpCloneState.ConnectionProvider}, authenticated to ${tmpCloneState.RemoteServerURL || 'remote'} — ready to sync`;
441
+ }
442
+
443
+ pResponse.send(200,
444
+ {
445
+ Phase: tmpPhase,
446
+ Message: tmpMessage,
447
+ ActiveEntity: tmpActiveEntity,
448
+ ActiveProgress: tmpActiveProgress ? { Synced: tmpActiveProgress.Synced, Total: tmpActiveProgress.Total } : null,
449
+ Completed: tmpCompleted.length,
450
+ Pending: tmpPending.length,
451
+ Errors: tmpErrors.length,
452
+ TotalTables: tmpCompleted.length + tmpPending.length + tmpErrors.length + (tmpActiveEntity ? 1 : 0),
453
+ TotalSynced: tmpTotalSynced,
454
+ TotalRecords: tmpTotalRecords,
455
+ Elapsed: tmpElapsed,
456
+ SyncMode: tmpCloneState.SyncMode,
457
+ ETA: tmpETA,
458
+ PreCountGrandTotal: tmpCloneState.PreCountGrandTotal || 0
459
+ });
460
+ return fNext();
461
+ });
462
+
463
+ // POST /clone/sync/stop
464
+ pOratorServiceServer.post(`${tmpPrefix}/sync/stop`,
465
+ (pRequest, pResponse, fNext) =>
466
+ {
467
+ if (tmpCloneState.SyncRunning)
468
+ {
469
+ tmpCloneState.SyncStopping = true;
470
+ tmpFable.log.info('Data Cloner: Sync stop requested.');
471
+ }
472
+
473
+ pResponse.send(200, { Success: true, Message: 'Sync stop requested.' });
474
+ return fNext();
475
+ });
476
+
477
+ // GET /clone/sync/report
478
+ pOratorServiceServer.get(`${tmpPrefix}/sync/report`,
479
+ (pRequest, pResponse, fNext) =>
480
+ {
481
+ if (tmpCloneState.SyncReport)
482
+ {
483
+ pResponse.send(200, tmpCloneState.SyncReport);
484
+ }
485
+ else
486
+ {
487
+ pResponse.send(200, { Success: false, Error: 'No report available. Run a sync first.' });
488
+ }
489
+ return fNext();
490
+ });
491
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * DataCloner Web UI Routes
3
+ *
4
+ * Serves the data-cloner-web.html file at /clone/ and handles the redirect
5
+ * from /clone to /clone/.
6
+ *
7
+ * @param {Object} pDataClonerService - The RetoldDataServiceDataCloner instance
8
+ * @param {Object} pOratorServiceServer - The Orator ServiceServer instance
9
+ */
10
+ module.exports = (pDataClonerService, pOratorServiceServer) =>
11
+ {
12
+ let libFs = require('fs');
13
+ let libPath = require('path');
14
+
15
+ let tmpPrefix = pDataClonerService.routePrefix;
16
+ let tmpHTMLPath = libPath.join(__dirname, 'data-cloner-web.html');
17
+
18
+ pOratorServiceServer.get(`${tmpPrefix}/`,
19
+ (pRequest, pResponse, fNext) =>
20
+ {
21
+ try
22
+ {
23
+ let tmpHTML = libFs.readFileSync(tmpHTMLPath, 'utf8');
24
+ pResponse.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
25
+ pResponse.write(tmpHTML);
26
+ pResponse.end();
27
+ }
28
+ catch (pReadError)
29
+ {
30
+ pResponse.send(500, { Success: false, Error: 'Failed to load web UI.' });
31
+ }
32
+ return fNext();
33
+ });
34
+
35
+ pOratorServiceServer.get(`${tmpPrefix}`,
36
+ (pRequest, pResponse, fNext) =>
37
+ {
38
+ pResponse.redirect(`${tmpPrefix}/`, fNext);
39
+ });
40
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * DataCloner Provider Registry
3
+ *
4
+ * Maps provider names to their module names and fable service names.
5
+ *
6
+ * @author Steven Velozo <steven@velozo.com>
7
+ */
8
+
9
+ const _ProviderRegistry = {
10
+ SQLite: { moduleName: 'meadow-connection-sqlite', serviceName: 'MeadowSQLiteProvider', configKey: 'SQLite' },
11
+ MySQL: { moduleName: 'meadow-connection-mysql', serviceName: 'MeadowMySQLProvider', configKey: 'MySQL' },
12
+ MSSQL: { moduleName: 'meadow-connection-mssql', serviceName: 'MeadowMSSQLProvider', configKey: 'MSSQL' },
13
+ PostgreSQL: { moduleName: 'meadow-connection-postgresql', serviceName: 'MeadowConnectionPostgreSQL', configKey: 'PostgreSQL' },
14
+ Solr: { moduleName: 'meadow-connection-solr', serviceName: 'MeadowConnectionSolr', configKey: 'Solr' },
15
+ MongoDB: { moduleName: 'meadow-connection-mongodb', serviceName: 'MeadowConnectionMongoDB', configKey: 'MongoDB' },
16
+ RocksDB: { moduleName: 'meadow-connection-rocksdb', serviceName: 'MeadowConnectionRocksDB', configKey: 'RocksDB' },
17
+ Bibliograph: { moduleName: 'bibliograph', serviceName: 'Bibliograph', configKey: 'Bibliograph' },
18
+ };
19
+
20
+ module.exports = _ProviderRegistry;