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.
- package/.claude/launch.json +11 -0
- package/bin/retold-data-service-clone.js +286 -0
- package/package.json +18 -9
- package/source/Retold-Data-Service.js +275 -73
- package/source/services/Retold-Data-Service-ConnectionManager.js +277 -0
- package/source/services/Retold-Data-Service-MeadowEndpoints.js +217 -0
- package/source/services/Retold-Data-Service-ModelManager.js +335 -0
- package/source/services/data-cloner/DataCloner-Command-Connection.js +138 -0
- package/source/services/data-cloner/DataCloner-Command-Headless.js +357 -0
- package/source/services/data-cloner/DataCloner-Command-Schema.js +367 -0
- package/source/services/data-cloner/DataCloner-Command-Session.js +229 -0
- package/source/services/data-cloner/DataCloner-Command-Sync.js +491 -0
- package/source/services/data-cloner/DataCloner-Command-WebUI.js +40 -0
- package/source/services/data-cloner/DataCloner-ProviderRegistry.js +20 -0
- package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +751 -0
- package/source/services/data-cloner/data-cloner-web.html +2706 -0
- package/source/services/integration-telemetry/IntegrationTelemetry-Command-Dashboard.js +60 -0
- package/source/services/integration-telemetry/IntegrationTelemetry-Command-Integrations.js +132 -0
- package/source/services/integration-telemetry/IntegrationTelemetry-Command-Runs.js +93 -0
- package/source/services/integration-telemetry/IntegrationTelemetry-StorageProvider-Base.js +116 -0
- package/source/services/integration-telemetry/IntegrationTelemetry-StorageProvider-Bibliograph.js +495 -0
- package/source/services/integration-telemetry/Retold-Data-Service-IntegrationTelemetry.js +224 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-CSVCheck.js +85 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-CSVTransform.js +180 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionIntersect.js +153 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionPush.js +190 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionToArray.js +113 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionToCSV.js +211 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-EntityFromTabularFolder.js +244 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-JSONArrayTransform.js +213 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-TSVCheck.js +80 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-TSVTransform.js +166 -0
- package/source/services/meadow-integration/Retold-Data-Service-MeadowIntegration.js +113 -0
- package/source/services/migration-manager/MigrationManager-Command-Connections.js +220 -0
- package/source/services/migration-manager/MigrationManager-Command-DiffMigrate.js +169 -0
- package/source/services/migration-manager/MigrationManager-Command-Schemas.js +532 -0
- package/source/services/migration-manager/MigrationManager-Command-WebUI.js +123 -0
- package/source/services/migration-manager/Retold-Data-Service-MigrationManager.js +357 -0
- package/source/services/stricture/Retold-Data-Service-Stricture.js +303 -0
- package/source/services/stricture/Stricture-Command-Compile.js +39 -0
- package/source/services/stricture/Stricture-Command-Generate-AuthorizationChart.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-DictionaryCSV.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-LaTeX.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-Markdown.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-Meadow.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-ModelGraph.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-MySQL.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-MySQLMigrate.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-Pict.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-TestObjectContainers.js +14 -0
- package/test/RetoldDataService_tests.js +161 -1
- 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;
|