retold-data-service 2.0.16 → 2.0.17

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 (28) hide show
  1. package/.claude/launch.json +2 -2
  2. package/.quackage.json +19 -0
  3. package/package.json +13 -6
  4. package/source/services/data-cloner/DataCloner-Command-Sync.js +83 -50
  5. package/source/services/data-cloner/DataCloner-Command-WebUI.js +27 -10
  6. package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +281 -4
  7. package/source/services/data-cloner/pict-app/Pict-Application-DataCloner-Configuration.json +9 -0
  8. package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +102 -0
  9. package/source/services/data-cloner/pict-app/Pict-DataCloner-Bundle.js +6 -0
  10. package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +974 -0
  11. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Connection.js +407 -0
  12. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Deploy.js +126 -0
  13. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Export.js +483 -0
  14. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +390 -0
  15. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Schema.js +241 -0
  16. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Session.js +268 -0
  17. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Sync.js +575 -0
  18. package/source/services/data-cloner/pict-app/views/PictView-DataCloner-ViewData.js +176 -0
  19. package/source/services/data-cloner/web/data-cloner.js +7935 -0
  20. package/source/services/data-cloner/web/data-cloner.js.map +1 -0
  21. package/source/services/data-cloner/web/data-cloner.min.js +2 -0
  22. package/source/services/data-cloner/web/data-cloner.min.js.map +1 -0
  23. package/source/services/data-cloner/web/index.html +17 -0
  24. package/test/DataCloner-Integration_tests.js +1205 -0
  25. package/test/DataCloner-Puppeteer_tests.js +502 -0
  26. package/test/integration-report.json +311 -0
  27. package/test/run-integration-tests.js +501 -0
  28. package/source/services/data-cloner/data-cloner-web.html +0 -2706
@@ -3,8 +3,8 @@
3
3
  "configurations": [
4
4
  {
5
5
  "name": "data-cloner",
6
- "runtimeExecutable": "node",
7
- "runtimeArgs": ["bin/retold-data-service-clone.js"],
6
+ "runtimeExecutable": "npm",
7
+ "runtimeArgs": ["start"],
8
8
  "port": 8095
9
9
  }
10
10
  ]
package/.quackage.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "GulpfileConfiguration":
3
+ {
4
+ "EntrypointInputSourceFile": "{~Data:AppData.CWD~}/source/services/data-cloner/pict-app/Pict-DataCloner-Bundle.js",
5
+ "LibraryObjectName": "dataCloner",
6
+ "LibraryOutputFolder": "{~Data:AppData.CWD~}/source/services/data-cloner/web/",
7
+ "LibraryUniminifiedFileName": "data-cloner.{~Data:Record.BuildFileLabel~}js",
8
+ "LibraryMinifiedFileName": "data-cloner.{~Data:Record.BuildFileLabel~}min.js"
9
+ },
10
+ "GulpExecutions":
11
+ [
12
+ {
13
+ "Hash": "default",
14
+ "Name": "Default standard build.",
15
+ "BuildFileLabel": "",
16
+ "BrowsersListRC": "since 2022"
17
+ }
18
+ ]
19
+ }
package/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "retold-data-service",
3
- "version": "2.0.16",
3
+ "version": "2.0.17",
4
4
  "description": "Serve up a whole model!",
5
5
  "main": "source/Retold-Data-Service.js",
6
6
  "bin": {
7
7
  "retold-data-service-clone": "bin/retold-data-service-clone.js"
8
8
  },
9
9
  "scripts": {
10
- "start": "node source/Retold-Data-Service.js",
10
+ "start": "node bin/retold-data-service-clone.js",
11
11
  "coverage": "npx quack coverage",
12
12
  "test": "npx quack test",
13
13
  "build": "npx quack build",
14
+ "prepublishOnly": "npx quack build",
14
15
  "build-test-model": "cd test && npx stricture -i model/ddl/BookStore.ddl",
15
16
  "docker-dev-build": "docker build ./ -f Dockerfile_LUXURYCode -t retold-data-service-image:local",
16
17
  "docker-dev-run": "docker run -it -d --name retold-data-service-dev -p 44444:8080 -p 43306:3306 -p 48086:8086 -v \"$PWD/.config:/home/coder/.config\" -v \"$PWD:/home/coder/retold-data-service\" -u \"$(id -u):$(id -g)\" -e \"DOCKER_USER=$USER\" retold-data-service-image:local",
@@ -18,9 +19,13 @@
18
19
  "docker-service-build": "docker build ./ -f Dockerfile_Service -t retold-data-service-server-image:local",
19
20
  "docker-service-run-test": "docker run -it --init -d --name retold-data-service -p 8086:8086 -p 43306:3306 -v \"$(pwd):/retold-data-service:z\" -u \"$(id -u):$(id -g)\" retold-data-service-server-image:local",
20
21
  "docker-service-run": "docker run -it --init -d --name retold-data-service -p 8086:8086 -p 43306:3306 -v \"$(pwd):/retold-data-service:z\" retold-data-service-server-image:local",
21
- "docker-service-shell": "docker exec -it retold-data-service /bin/bash"
22
+ "docker-service-shell": "docker exec -it retold-data-service /bin/bash",
23
+ "test:integration": "node test/run-integration-tests.js",
24
+ "test:integration:no-browser": "node test/run-integration-tests.js --skip-puppeteer",
25
+ "test:all": "npx quack test && node test/run-integration-tests.js --skip-puppeteer"
22
26
  },
23
27
  "mocha": {
28
+ "spec": "test/RetoldDataService_tests.js",
24
29
  "diff": true,
25
30
  "extension": [
26
31
  "js"
@@ -55,6 +60,7 @@
55
60
  "homepage": "https://github.com/stevenvelozo/retold-data-service",
56
61
  "devDependencies": {
57
62
  "meadow-connection-sqlite": "^1.0.18",
63
+ "puppeteer": "^24.38.0",
58
64
  "quackage": "^1.0.63",
59
65
  "stricture": "^4.0.2",
60
66
  "supertest": "^7.2.2"
@@ -63,16 +69,17 @@
63
69
  "bibliograph": "^0.1.4",
64
70
  "fable": "^3.1.63",
65
71
  "fable-serviceproviderbase": "^3.0.19",
66
- "meadow": "^2.0.31",
72
+ "meadow": "^2.0.33",
67
73
  "meadow-connection-mysql": "^1.0.14",
68
74
  "meadow-endpoints": "^4.0.14",
75
+ "meadow-integration": "^1.0.14",
76
+ "meadow-migrationmanager": "^0.0.4",
69
77
  "orator": "^6.0.4",
70
78
  "orator-http-proxy": "^1.0.5",
71
79
  "orator-serviceserver-restify": "^2.0.9",
72
- "meadow-integration": "^1.0.14",
73
- "meadow-migrationmanager": "^0.0.4",
74
80
  "orator-static-server": "^2.0.4",
75
81
  "pict": "^1.0.357",
82
+ "pict-section-histogram": "^1.0.0",
76
83
  "pict-sessionmanager": "^1.0.2",
77
84
  "stricture": "^4.0.2"
78
85
  }
@@ -7,6 +7,9 @@
7
7
  * @param {Object} pDataClonerService - The RetoldDataServiceDataCloner instance
8
8
  * @param {Object} pOratorServiceServer - The Orator ServiceServer instance
9
9
  */
10
+ const libFableLog = require('fable-log');
11
+ const libPath = require('path');
12
+
10
13
  module.exports = (pDataClonerService, pOratorServiceServer) =>
11
14
  {
12
15
  let tmpFable = pDataClonerService.fable;
@@ -88,6 +91,33 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
88
91
  return fNext();
89
92
  }
90
93
 
94
+ // ---- Enable log-to-file if requested ----
95
+ if (tmpBody.LogToFile)
96
+ {
97
+ // Remove any previous sync log stream
98
+ if (tmpCloneState.SyncLogFileLogger)
99
+ {
100
+ try { tmpCloneState.SyncLogFileLogger.closeWriter(() => {}); }
101
+ catch (pErr) { /* ignore */ }
102
+ tmpCloneState.SyncLogFileLogger = null;
103
+ }
104
+
105
+ let tmpTimestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
106
+ let tmpLogPath = libPath.join(process.cwd(), `DataCloner-Run-${tmpTimestamp}.log`);
107
+ let tmpFileLogger = new libFableLog.LogProviderFlatfile(
108
+ {
109
+ path: tmpLogPath,
110
+ showtimestamps: true,
111
+ formattedtimestamps: true,
112
+ outputloglinestoconsole: false
113
+ }, tmpFable.log);
114
+ tmpFileLogger.initialize();
115
+ tmpFable.log.addLogger(tmpFileLogger, 'trace');
116
+ tmpCloneState.SyncLogFileLogger = tmpFileLogger;
117
+ tmpCloneState.SyncLogFilePath = tmpLogPath;
118
+ tmpFable.log.info(`Data Cloner: Log file enabled — writing to ${tmpLogPath}`);
119
+ }
120
+
91
121
  // ---- Handle Sync Mode switching ----
92
122
  let fStartSync = () =>
93
123
  {
@@ -167,33 +197,35 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
167
197
  pOratorServiceServer.get(`${tmpPrefix}/sync/status`,
168
198
  (pRequest, pResponse, fNext) =>
169
199
  {
170
- // Update progress from MeadowSync operation trackers
171
- if (tmpFable.MeadowSync && tmpFable.MeadowSync.MeadowSyncEntities)
200
+ // Build a read-only snapshot of progress (never mutate SyncProgress during a poll)
201
+ let tmpTablesSnapshot = {};
202
+ let tmpTableNames = Object.keys(tmpCloneState.SyncProgress);
203
+ for (let i = 0; i < tmpTableNames.length; i++)
172
204
  {
173
- let tmpEntityNames = Object.keys(tmpFable.MeadowSync.MeadowSyncEntities);
174
- for (let i = 0; i < tmpEntityNames.length; i++)
205
+ let tmpName = tmpTableNames[i];
206
+ let tmpP = tmpCloneState.SyncProgress[tmpName];
207
+ let tmpSnap = { Status: tmpP.Status, Total: tmpP.Total || 0, Synced: tmpP.Synced || 0, Errors: tmpP.Errors || 0 };
208
+ if (tmpP.ErrorMessage) tmpSnap.ErrorMessage = tmpP.ErrorMessage;
209
+
210
+ if ((tmpP.Status === 'Syncing' || tmpP.Status === 'Pending') && tmpFable.MeadowSync && tmpFable.MeadowSync.MeadowSyncEntities)
175
211
  {
176
- let tmpEntityName = tmpEntityNames[i];
177
- let tmpProgress = tmpCloneState.SyncProgress[tmpEntityName];
178
- if (tmpProgress && (tmpProgress.Status === 'Syncing' || tmpProgress.Status === 'Pending'))
212
+ let tmpSyncEntity = tmpFable.MeadowSync.MeadowSyncEntities[tmpName];
213
+ if (tmpSyncEntity && tmpSyncEntity.operation)
179
214
  {
180
- let tmpSyncEntity = tmpFable.MeadowSync.MeadowSyncEntities[tmpEntityName];
181
- if (tmpSyncEntity && tmpSyncEntity.operation)
215
+ let tmpTracker = tmpSyncEntity.operation.progressTrackers[`FullSync-${tmpName}`];
216
+ if (tmpTracker)
182
217
  {
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;
218
+ tmpSnap.Total = tmpTracker.TotalCount || tmpSnap.Total;
219
+ tmpSnap.Synced = Math.max(tmpTracker.CurrentCount || 0, 0);
194
220
  }
195
221
  }
222
+ let tmpRESTErrors = tmpCloneState.SyncRESTErrors[tmpName] || 0;
223
+ if (tmpRESTErrors > 0)
224
+ {
225
+ tmpSnap.Errors = tmpRESTErrors;
226
+ }
196
227
  }
228
+ tmpTablesSnapshot[tmpName] = tmpSnap;
197
229
  }
198
230
 
199
231
  pResponse.send(200,
@@ -201,7 +233,7 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
201
233
  Running: tmpCloneState.SyncRunning,
202
234
  Stopping: tmpCloneState.SyncStopping,
203
235
  SyncMode: tmpCloneState.SyncMode,
204
- Tables: tmpCloneState.SyncProgress
236
+ Tables: tmpTablesSnapshot
205
237
  });
206
238
  return fNext();
207
239
  });
@@ -287,57 +319,57 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
287
319
  SyncMode: tmpCloneState.SyncMode,
288
320
  ETA: null,
289
321
  PreCountGrandTotal: 0,
290
- PreCountProgress: tmpPC
322
+ PreCountProgress: tmpPC,
323
+ ThroughputSamples: []
291
324
  });
292
325
  return fNext();
293
326
  }
294
327
 
295
- // Update progress from MeadowSync operation trackers (same as /sync/status)
296
- if (tmpFable.MeadowSync && tmpFable.MeadowSync.MeadowSyncEntities)
328
+ // Read progress from MeadowSync operation trackers (read-only snapshot — never write back to SyncProgress during a poll)
329
+ let tmpTableNames = Object.keys(tmpCloneState.SyncProgress);
330
+ for (let i = 0; i < tmpTableNames.length; i++)
297
331
  {
298
- let tmpEntityNames = Object.keys(tmpFable.MeadowSync.MeadowSyncEntities);
299
- for (let i = 0; i < tmpEntityNames.length; i++)
332
+ let tmpName = tmpTableNames[i];
333
+ let tmpP = tmpCloneState.SyncProgress[tmpName];
334
+
335
+ // Snapshot live tracker values without mutating SyncProgress
336
+ let tmpLiveTotal = tmpP.Total || 0;
337
+ let tmpLiveSynced = tmpP.Synced || 0;
338
+ let tmpLiveErrors = tmpP.Errors || 0;
339
+
340
+ if (tmpP.Status === 'Syncing' || tmpP.Status === 'Pending')
300
341
  {
301
- let tmpEntityName = tmpEntityNames[i];
302
- let tmpProgress = tmpCloneState.SyncProgress[tmpEntityName];
303
- if (tmpProgress && (tmpProgress.Status === 'Syncing' || tmpProgress.Status === 'Pending'))
342
+ if (tmpFable.MeadowSync && tmpFable.MeadowSync.MeadowSyncEntities)
304
343
  {
305
- let tmpSyncEntity = tmpFable.MeadowSync.MeadowSyncEntities[tmpEntityName];
344
+ let tmpSyncEntity = tmpFable.MeadowSync.MeadowSyncEntities[tmpName];
306
345
  if (tmpSyncEntity && tmpSyncEntity.operation)
307
346
  {
308
- let tmpTracker = tmpSyncEntity.operation.progressTrackers[`FullSync-${tmpEntityName}`];
347
+ let tmpTracker = tmpSyncEntity.operation.progressTrackers[`FullSync-${tmpName}`];
309
348
  if (tmpTracker)
310
349
  {
311
- tmpProgress.Total = tmpTracker.TotalCount || 0;
312
- tmpProgress.Synced = Math.max(tmpTracker.CurrentCount || 0, 0);
350
+ tmpLiveTotal = tmpTracker.TotalCount || tmpLiveTotal;
351
+ tmpLiveSynced = Math.max(tmpTracker.CurrentCount || 0, 0);
313
352
  }
314
353
  }
315
- let tmpRESTErrors = tmpCloneState.SyncRESTErrors[tmpEntityName] || 0;
316
- if (tmpRESTErrors > 0)
317
- {
318
- tmpProgress.Errors = tmpRESTErrors;
319
- }
354
+ }
355
+ let tmpRESTErrors = tmpCloneState.SyncRESTErrors[tmpName] || 0;
356
+ if (tmpRESTErrors > 0)
357
+ {
358
+ tmpLiveErrors = tmpRESTErrors;
320
359
  }
321
360
  }
322
- }
323
361
 
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);
362
+ tmpTotalSynced += tmpLiveSynced;
363
+ tmpTotalRecords += tmpLiveTotal;
332
364
 
333
365
  if (tmpP.Status === 'Syncing')
334
366
  {
335
367
  tmpActiveEntity = tmpName;
336
- tmpActiveProgress = tmpP;
368
+ tmpActiveProgress = { Synced: tmpLiveSynced, Total: tmpLiveTotal };
337
369
  }
338
370
  else if (tmpP.Status === 'Complete' || tmpP.Status === 'Partial')
339
371
  {
340
- tmpCompleted.push({ Name: tmpName, Synced: tmpP.Synced || 0, Total: tmpP.Total || 0, Status: tmpP.Status });
372
+ tmpCompleted.push({ Name: tmpName, Synced: tmpLiveSynced, Total: tmpLiveTotal, Status: tmpP.Status });
341
373
  }
342
374
  else if (tmpP.Status === 'Error')
343
375
  {
@@ -455,7 +487,8 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
455
487
  Elapsed: tmpElapsed,
456
488
  SyncMode: tmpCloneState.SyncMode,
457
489
  ETA: tmpETA,
458
- PreCountGrandTotal: tmpCloneState.PreCountGrandTotal || 0
490
+ PreCountGrandTotal: tmpCloneState.PreCountGrandTotal || 0,
491
+ ThroughputSamples: tmpCloneState.ThroughputSamples || []
459
492
  });
460
493
  return fNext();
461
494
  });
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * DataCloner Web UI Routes
3
3
  *
4
- * Serves the data-cloner-web.html file at /clone/ and handles the redirect
5
- * from /clone to /clone/.
4
+ * Serves the Pict-application-based data cloner web UI.
5
+ * The HTML entry point loads pict.min.js (from node_modules) and the
6
+ * Quackage-built application bundle.
6
7
  *
7
8
  * @param {Object} pDataClonerService - The RetoldDataServiceDataCloner instance
8
9
  * @param {Object} pOratorServiceServer - The Orator ServiceServer instance
@@ -13,28 +14,44 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
13
14
  let libPath = require('path');
14
15
 
15
16
  let tmpPrefix = pDataClonerService.routePrefix;
16
- let tmpHTMLPath = libPath.join(__dirname, 'data-cloner-web.html');
17
+ let tmpWebDir = libPath.join(__dirname, 'web');
17
18
 
18
- pOratorServiceServer.get(`${tmpPrefix}/`,
19
- (pRequest, pResponse, fNext) =>
19
+ // Helper: serve a static file with the given content type
20
+ let fServeFile = (pFilePath, pContentType) =>
21
+ {
22
+ return (pRequest, pResponse, fNext) =>
20
23
  {
21
24
  try
22
25
  {
23
- let tmpHTML = libFs.readFileSync(tmpHTMLPath, 'utf8');
24
- pResponse.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
25
- pResponse.write(tmpHTML);
26
+ let tmpContent = libFs.readFileSync(pFilePath, 'utf8');
27
+ pResponse.writeHead(200, { 'Content-Type': pContentType + '; charset=utf-8' });
28
+ pResponse.write(tmpContent);
26
29
  pResponse.end();
27
30
  }
28
31
  catch (pReadError)
29
32
  {
30
- pResponse.send(500, { Success: false, Error: 'Failed to load web UI.' });
33
+ pResponse.send(500, { Success: false, Error: 'Failed to load ' + libPath.basename(pFilePath) });
31
34
  }
32
35
  return fNext();
33
- });
36
+ };
37
+ };
34
38
 
39
+ // ---- HTML ----
40
+ pOratorServiceServer.get(`${tmpPrefix}/`,
41
+ fServeFile(libPath.join(tmpWebDir, 'index.html'), 'text/html'));
42
+
43
+ // Redirect /clone → /clone/
35
44
  pOratorServiceServer.get(`${tmpPrefix}`,
36
45
  (pRequest, pResponse, fNext) =>
37
46
  {
38
47
  pResponse.redirect(`${tmpPrefix}/`, fNext);
39
48
  });
49
+
50
+ // ---- Pict library (from node_modules) ----
51
+ pOratorServiceServer.get(`${tmpPrefix}/pict.min.js`,
52
+ fServeFile(require.resolve('pict/dist/pict.min.js'), 'application/javascript'));
53
+
54
+ // ---- Application bundle ----
55
+ pOratorServiceServer.get(`${tmpPrefix}/data-cloner.js`,
56
+ fServeFile(libPath.join(tmpWebDir, 'data-cloner.js'), 'application/javascript'));
40
57
  };