retold-data-service 2.0.14 → 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 (26) hide show
  1. package/.claude/launch.json +11 -0
  2. package/bin/retold-data-service-clone.js +286 -0
  3. package/package.json +14 -9
  4. package/source/Retold-Data-Service.js +52 -2
  5. package/source/services/data-cloner/DataCloner-Command-Connection.js +138 -0
  6. package/source/services/data-cloner/DataCloner-Command-Headless.js +357 -0
  7. package/source/services/data-cloner/DataCloner-Command-Schema.js +367 -0
  8. package/source/services/data-cloner/DataCloner-Command-Session.js +229 -0
  9. package/source/services/data-cloner/DataCloner-Command-Sync.js +491 -0
  10. package/source/services/data-cloner/DataCloner-Command-WebUI.js +40 -0
  11. package/source/services/data-cloner/DataCloner-ProviderRegistry.js +20 -0
  12. package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +751 -0
  13. package/source/services/data-cloner/data-cloner-web.html +2706 -0
  14. package/source/services/integration-telemetry/IntegrationTelemetry-Command-Dashboard.js +60 -0
  15. package/source/services/integration-telemetry/IntegrationTelemetry-Command-Integrations.js +132 -0
  16. package/source/services/integration-telemetry/IntegrationTelemetry-Command-Runs.js +93 -0
  17. package/source/services/integration-telemetry/IntegrationTelemetry-StorageProvider-Base.js +116 -0
  18. package/source/services/integration-telemetry/IntegrationTelemetry-StorageProvider-Bibliograph.js +495 -0
  19. package/source/services/integration-telemetry/Retold-Data-Service-IntegrationTelemetry.js +224 -0
  20. package/debug/data/books.csv +0 -10001
  21. package/example_applications/data-cloner/data/cloned.sqlite +0 -0
  22. package/example_applications/data-cloner/data/cloned.sqlite-shm +0 -0
  23. package/example_applications/data-cloner/data/cloned.sqlite-wal +0 -0
  24. package/example_applications/data-cloner/data-cloner-web.html +0 -935
  25. package/example_applications/data-cloner/data-cloner.js +0 -1047
  26. package/example_applications/data-cloner/package.json +0 -19
@@ -1,1047 +0,0 @@
1
- /**
2
- * Example Application: Data Cloner
3
- *
4
- * Clones a remote retold-based database to a local SQLite database.
5
- * Demonstrates retold-data-service, pict-sessionmanager, meadow-integration
6
- * (clone sync services), and meadow-migrationmanager working together.
7
- *
8
- * Flow:
9
- * 1. Configure and authenticate a remote session (cookie mode)
10
- * 2. Fetch the remote schema (model definition)
11
- * 3. Deploy selected tables to local SQLite
12
- * 4. Synchronize data via meadow-integration's MeadowSync
13
- *
14
- * Available endpoints:
15
- * /clone/ Web UI
16
- * /clone/session/configure POST Configure remote session
17
- * /clone/session/authenticate POST Authenticate
18
- * /clone/session/check GET Check session
19
- * /clone/session/deauthenticate POST Deauthenticate
20
- * /clone/schema/fetch POST Fetch remote schema
21
- * /clone/schema GET Get fetched schema
22
- * /clone/schema/deploy POST Deploy tables to local DB
23
- * /clone/sync/start POST Start data sync
24
- * /clone/sync/status GET Get sync progress
25
- * /clone/sync/stop POST Stop sync
26
- * /meadow-migrationmanager/ Migration manager web UI
27
- * /meadow-migrationmanager/api/* Migration manager API
28
- * Per-entity CRUD (after deploy) /1.0/{Entity}s, etc.
29
- *
30
- * Usage:
31
- * cd example_applications/data-cloner
32
- * npm install
33
- * node data-cloner.js
34
- *
35
- * @author Steven Velozo <steven@velozo.com>
36
- */
37
- const libFable = require('fable');
38
- const libPict = require('pict');
39
- const libPictSessionManager = require('pict-sessionmanager');
40
- const libMeadowConnectionSQLite = require('meadow-connection-sqlite');
41
- const libRetoldDataService = require('retold-data-service');
42
-
43
- const libMeadowCloneRestClient = require('meadow-integration/source/services/clone/Meadow-Service-RestClient');
44
- const libMeadowSync = require('meadow-integration/source/services/clone/Meadow-Service-Sync');
45
-
46
- const libFs = require('fs');
47
- const libPath = require('path');
48
-
49
- // ================================================================
50
- // Configuration
51
- // ================================================================
52
-
53
- let _Settings = (
54
- {
55
- Product: 'RetoldDataCloner',
56
- ProductVersion: '1.0.0',
57
- APIServerPort: parseInt(process.env.PORT, 10) || 8095,
58
- LogStreams:
59
- [
60
- {
61
- streamtype: 'console'
62
- }
63
- ],
64
-
65
- SQLite:
66
- {
67
- SQLiteFilePath: libPath.join(__dirname, 'data', 'cloned.sqlite')
68
- }
69
- });
70
-
71
- // Ensure the data directory exists
72
- let _DataDir = libPath.join(__dirname, 'data');
73
- if (!libFs.existsSync(_DataDir))
74
- {
75
- libFs.mkdirSync(_DataDir, { recursive: true });
76
- }
77
-
78
- let _Fable = new libFable(_Settings);
79
-
80
- // ================================================================
81
- // Session Manager (separate Pict instance for remote auth)
82
- // ================================================================
83
-
84
- let _Pict = new libPict(
85
- {
86
- Product: 'DataClonerSession',
87
- TraceLog: true,
88
- LogStreams:
89
- [
90
- {
91
- streamtype: 'console'
92
- }
93
- ]
94
- });
95
-
96
- _Pict.serviceManager.addServiceType('SessionManager', libPictSessionManager);
97
- _Pict.serviceManager.instantiateServiceProvider('SessionManager');
98
-
99
- // ================================================================
100
- // Cloner State
101
- // ================================================================
102
-
103
- let _CloneState = (
104
- {
105
- // Remote session configuration
106
- SessionConfigured: false,
107
- SessionAuthenticated: false,
108
- RemoteServerURL: '',
109
-
110
- // Fetched remote schema
111
- RemoteSchema: false,
112
- RemoteModelObject: false,
113
-
114
- // Sync progress
115
- SyncRunning: false,
116
- SyncStopping: false,
117
- SyncProgress: {},
118
-
119
- // Per-table REST error counters (set up during sync)
120
- SyncRESTErrors: {}
121
- });
122
-
123
- // ================================================================
124
- // SQLite Setup
125
- // ================================================================
126
-
127
- _Fable.serviceManager.addServiceType('MeadowSQLiteProvider', libMeadowConnectionSQLite);
128
- _Fable.serviceManager.instantiateServiceProvider('MeadowSQLiteProvider');
129
-
130
- _Fable.MeadowSQLiteProvider.connectAsync(
131
- (pError) =>
132
- {
133
- if (pError)
134
- {
135
- _Fable.log.error(`SQLite connection error: ${pError}`);
136
- process.exit(1);
137
- }
138
-
139
- // Set default Meadow provider to SQLite so all Meadow DAL instances
140
- // created by MeadowSync will use the SQLite connection automatically.
141
- _Fable.settings.MeadowProvider = 'SQLite';
142
-
143
- // Register meadow-integration services for clone sync
144
- _Fable.serviceManager.addServiceType('MeadowCloneRestClient', libMeadowCloneRestClient);
145
- _Fable.serviceManager.addServiceType('MeadowSync', libMeadowSync);
146
-
147
- _Fable.serviceManager.addServiceType('RetoldDataService', libRetoldDataService);
148
- let tmpDataService = _Fable.serviceManager.instantiateServiceProvider('RetoldDataService',
149
- {
150
- StorageProvider: 'SQLite',
151
- StorageProviderModule: 'meadow-connection-sqlite',
152
-
153
- // No default schema — models loaded at runtime after fetch
154
- FullMeadowSchemaFilename: false,
155
-
156
- Endpoints:
157
- {
158
- ConnectionManager: true,
159
- ModelManagerWrite: true,
160
- Stricture: false,
161
- MeadowIntegration: false,
162
- MeadowEndpoints: true,
163
- MigrationManager: true,
164
- MigrationManagerWebUI: true
165
- }
166
- });
167
-
168
- // Enable JSON body parsing for POST/PUT requests
169
- tmpDataService.onBeforeInitialize = (fCallback) =>
170
- {
171
- _Fable.OratorServiceServer.server.use(_Fable.OratorServiceServer.bodyParser());
172
- return fCallback();
173
- };
174
-
175
- // ================================================================
176
- // Custom /clone/* Endpoints
177
- // ================================================================
178
-
179
- tmpDataService.onAfterInitialize = (fCallback) =>
180
- {
181
- let tmpServer = _Fable.OratorServiceServer;
182
-
183
- // ---- Web UI ----
184
-
185
- let tmpHTMLPath = libPath.join(__dirname, 'data-cloner-web.html');
186
- tmpServer.get('/clone/',
187
- (pRequest, pResponse, fNext) =>
188
- {
189
- try
190
- {
191
- let tmpHTML = libFs.readFileSync(tmpHTMLPath, 'utf8');
192
- pResponse.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
193
- pResponse.write(tmpHTML);
194
- pResponse.end();
195
- }
196
- catch (pReadError)
197
- {
198
- pResponse.send(500, { Success: false, Error: 'Failed to load web UI.' });
199
- }
200
- return fNext();
201
- });
202
-
203
- tmpServer.get('/clone',
204
- (pRequest, pResponse, fNext) =>
205
- {
206
- pResponse.redirect('/clone/', fNext);
207
- });
208
-
209
- // ---- Session Management ----
210
-
211
- // POST /clone/session/configure
212
- tmpServer.post('/clone/session/configure',
213
- (pRequest, pResponse, fNext) =>
214
- {
215
- let tmpBody = pRequest.body || {};
216
- let tmpServerURL = tmpBody.ServerURL;
217
-
218
- if (!tmpServerURL)
219
- {
220
- pResponse.send(400, { Success: false, Error: 'ServerURL is required.' });
221
- return fNext();
222
- }
223
-
224
- _CloneState.RemoteServerURL = tmpServerURL;
225
-
226
- // Remove existing session if reconfiguring
227
- if (_Pict.SessionManager.getSession('Remote'))
228
- {
229
- _Pict.SessionManager.deauthenticate('Remote');
230
- // Remove the old session by clearing and re-adding
231
- delete _Pict.SessionManager.sessions['Remote'];
232
- }
233
-
234
- // Extract domain from ServerURL for cookie matching
235
- let tmpDomainMatch = tmpBody.DomainMatch;
236
- if (!tmpDomainMatch)
237
- {
238
- try
239
- {
240
- let tmpURL = new URL(tmpServerURL);
241
- tmpDomainMatch = tmpURL.hostname;
242
- }
243
- catch (pParseError)
244
- {
245
- tmpDomainMatch = tmpServerURL;
246
- }
247
- }
248
-
249
- // Helper: ensure a URI template is fully qualified with the server URL
250
- let fQualifyURI = (pTemplate, pDefault) =>
251
- {
252
- if (!pTemplate)
253
- {
254
- return tmpServerURL + pDefault;
255
- }
256
- // If the template starts with '/' it is a relative path -- prepend the server URL
257
- if (pTemplate.charAt(0) === '/')
258
- {
259
- return tmpServerURL + pTemplate;
260
- }
261
- return pTemplate;
262
- };
263
-
264
- let tmpSessionConfig = (
265
- {
266
- Type: 'Cookie',
267
-
268
- // Authentication
269
- AuthenticationMethod: tmpBody.AuthenticationMethod || 'get',
270
- AuthenticationURITemplate: fQualifyURI(tmpBody.AuthenticationURITemplate, '/1.0/Authenticate/{~D:Record.UserName~}/{~D:Record.Password~}'),
271
- AuthenticationRetryCount: 2,
272
- AuthenticationRetryDebounce: 200,
273
-
274
- // Session check
275
- CheckSessionURITemplate: fQualifyURI(tmpBody.CheckSessionURITemplate, '/1.0/CheckSession'),
276
- CheckSessionLoginMarkerType: tmpBody.CheckSessionLoginMarkerType || 'boolean',
277
- CheckSessionLoginMarker: tmpBody.CheckSessionLoginMarker || 'LoggedIn',
278
-
279
- // Cookie injection
280
- DomainMatch: tmpDomainMatch,
281
- CookieName: tmpBody.CookieName || 'SessionID',
282
- CookieValueAddress: tmpBody.CookieValueAddress || 'SessionID',
283
- CookieValueTemplate: tmpBody.CookieValueTemplate || false
284
- });
285
-
286
- // If authentication is POST-based, set up the request body template
287
- if (tmpBody.AuthenticationMethod === 'post')
288
- {
289
- tmpSessionConfig.AuthenticationRequestBody = tmpBody.AuthenticationRequestBody || (
290
- {
291
- username: '{~D:Record.UserName~}',
292
- password: '{~D:Record.Password~}'
293
- });
294
- }
295
-
296
- _Pict.SessionManager.addSession('Remote', tmpSessionConfig);
297
- _Pict.SessionManager.connectToRestClient();
298
-
299
- _CloneState.SessionConfigured = true;
300
- _CloneState.SessionAuthenticated = false;
301
-
302
- _Fable.log.info(`Data Cloner: Session configured for ${tmpServerURL} (domain: ${tmpDomainMatch})`);
303
-
304
- pResponse.send(200,
305
- {
306
- Success: true,
307
- ServerURL: tmpServerURL,
308
- DomainMatch: tmpDomainMatch,
309
- AuthenticationMethod: tmpSessionConfig.AuthenticationMethod
310
- });
311
- return fNext();
312
- });
313
-
314
- // POST /clone/session/authenticate
315
- tmpServer.post('/clone/session/authenticate',
316
- (pRequest, pResponse, fNext) =>
317
- {
318
- if (!_CloneState.SessionConfigured)
319
- {
320
- pResponse.send(400, { Success: false, Error: 'Session not configured. Call POST /clone/session/configure first.' });
321
- return fNext();
322
- }
323
-
324
- let tmpBody = pRequest.body || {};
325
- let tmpCredentials = (
326
- {
327
- UserName: tmpBody.UserName || tmpBody.username,
328
- Password: tmpBody.Password || tmpBody.password
329
- });
330
-
331
- if (!tmpCredentials.UserName || !tmpCredentials.Password)
332
- {
333
- pResponse.send(400, { Success: false, Error: 'UserName and Password are required.' });
334
- return fNext();
335
- }
336
-
337
- _Fable.log.info(`Data Cloner: Authenticating as ${tmpCredentials.UserName}...`);
338
-
339
- _Pict.SessionManager.authenticate('Remote', tmpCredentials,
340
- (pAuthError, pSessionState) =>
341
- {
342
- if (pAuthError)
343
- {
344
- _Fable.log.error(`Data Cloner: Authentication failed: ${pAuthError.message || pAuthError}`);
345
- _CloneState.SessionAuthenticated = false;
346
- pResponse.send(401,
347
- {
348
- Success: false,
349
- Error: `Authentication failed: ${pAuthError.message || pAuthError}`
350
- });
351
- return fNext();
352
- }
353
-
354
- _CloneState.SessionAuthenticated = pSessionState && pSessionState.Authenticated;
355
-
356
- _Fable.log.info(`Data Cloner: Authentication ${_CloneState.SessionAuthenticated ? 'succeeded' : 'failed'}.`);
357
-
358
- pResponse.send(200,
359
- {
360
- Success: _CloneState.SessionAuthenticated,
361
- Authenticated: _CloneState.SessionAuthenticated,
362
- SessionData: pSessionState ? pSessionState.SessionData : {}
363
- });
364
- return fNext();
365
- });
366
- });
367
-
368
- // GET /clone/session/check
369
- tmpServer.get('/clone/session/check',
370
- (pRequest, pResponse, fNext) =>
371
- {
372
- if (!_CloneState.SessionConfigured)
373
- {
374
- pResponse.send(200,
375
- {
376
- Configured: false,
377
- Authenticated: false
378
- });
379
- return fNext();
380
- }
381
-
382
- _Pict.SessionManager.checkSession('Remote',
383
- (pCheckError, pAuthenticated, pCheckData) =>
384
- {
385
- _CloneState.SessionAuthenticated = pAuthenticated;
386
-
387
- pResponse.send(200,
388
- {
389
- Configured: _CloneState.SessionConfigured,
390
- Authenticated: pAuthenticated,
391
- ServerURL: _CloneState.RemoteServerURL,
392
- CheckData: pCheckData || {}
393
- });
394
- return fNext();
395
- });
396
- });
397
-
398
- // POST /clone/session/deauthenticate
399
- tmpServer.post('/clone/session/deauthenticate',
400
- (pRequest, pResponse, fNext) =>
401
- {
402
- if (_CloneState.SessionConfigured)
403
- {
404
- _Pict.SessionManager.deauthenticate('Remote');
405
- }
406
- _CloneState.SessionAuthenticated = false;
407
-
408
- _Fable.log.info('Data Cloner: Session deauthenticated.');
409
-
410
- pResponse.send(200, { Success: true, Authenticated: false });
411
- return fNext();
412
- });
413
-
414
- // ---- Schema Management ----
415
-
416
- // POST /clone/schema/fetch
417
- tmpServer.post('/clone/schema/fetch',
418
- (pRequest, pResponse, fNext) =>
419
- {
420
- let tmpBody = pRequest.body || {};
421
- let tmpSchemaURL = tmpBody.SchemaURL;
422
-
423
- if (!tmpSchemaURL)
424
- {
425
- // Default to the standard retold model endpoint
426
- if (_CloneState.RemoteServerURL)
427
- {
428
- tmpSchemaURL = _CloneState.RemoteServerURL + '/1.0/Retold/Models';
429
- }
430
- else
431
- {
432
- pResponse.send(400, { Success: false, Error: 'SchemaURL is required (or configure a session first).' });
433
- return fNext();
434
- }
435
- }
436
-
437
- _Fable.log.info(`Data Cloner: Fetching remote schema from ${tmpSchemaURL}...`);
438
-
439
- _Pict.RestClient.getJSON(tmpSchemaURL,
440
- (pError, pHTTPResponse, pData) =>
441
- {
442
- if (pError)
443
- {
444
- _Fable.log.error(`Data Cloner: Schema fetch error: ${pError.message || pError}`);
445
- pResponse.send(500, { Success: false, Error: `Schema fetch error: ${pError.message || pError}` });
446
- return fNext();
447
- }
448
-
449
- if (!pHTTPResponse || pHTTPResponse.statusCode !== 200)
450
- {
451
- let tmpStatus = pHTTPResponse ? pHTTPResponse.statusCode : 'unknown';
452
- _Fable.log.error(`Data Cloner: Schema fetch returned HTTP ${tmpStatus} — body: ${JSON.stringify(pData)}`);
453
- pResponse.send(500, { Success: false, Error: `Schema fetch returned HTTP ${tmpStatus}` });
454
- return fNext();
455
- }
456
-
457
- _CloneState.RemoteSchema = pData;
458
- _CloneState.RemoteModelObject = pData;
459
-
460
- // Extract table names for the UI
461
- let tmpTableNames = [];
462
- if (pData && pData.Tables)
463
- {
464
- tmpTableNames = Object.keys(pData.Tables);
465
- }
466
-
467
- _Fable.log.info(`Data Cloner: Fetched schema with ${tmpTableNames.length} tables: [${tmpTableNames.join(', ')}]`);
468
-
469
- pResponse.send(200,
470
- {
471
- Success: true,
472
- SchemaURL: tmpSchemaURL,
473
- TableCount: tmpTableNames.length,
474
- Tables: tmpTableNames
475
- });
476
- return fNext();
477
- });
478
- });
479
-
480
- // GET /clone/schema
481
- tmpServer.get('/clone/schema',
482
- (pRequest, pResponse, fNext) =>
483
- {
484
- if (!_CloneState.RemoteSchema)
485
- {
486
- pResponse.send(200, { Fetched: false, Tables: [] });
487
- return fNext();
488
- }
489
-
490
- let tmpTableNames = [];
491
- if (_CloneState.RemoteSchema.Tables)
492
- {
493
- tmpTableNames = Object.keys(_CloneState.RemoteSchema.Tables);
494
- }
495
-
496
- pResponse.send(200,
497
- {
498
- Fetched: true,
499
- TableCount: tmpTableNames.length,
500
- Tables: tmpTableNames
501
- });
502
- return fNext();
503
- });
504
-
505
- // POST /clone/reset — Delete the local SQLite database and start fresh
506
- tmpServer.post('/clone/reset',
507
- (pRequest, pResponse, fNext) =>
508
- {
509
- let tmpSQLitePath = _Settings.SQLite.SQLiteFilePath;
510
- _Fable.log.info(`Data Cloner: Resetting local database [${tmpSQLitePath}]...`);
511
-
512
- try
513
- {
514
- // Close the existing SQLite connection and reset the provider state
515
- if (_Fable.MeadowSQLiteProvider)
516
- {
517
- if (_Fable.MeadowSQLiteProvider._database)
518
- {
519
- _Fable.MeadowSQLiteProvider._database.close();
520
- }
521
- // Reset the connected flag so connectAsync will re-open
522
- _Fable.MeadowSQLiteProvider.connected = false;
523
- }
524
- }
525
- catch (pCloseError)
526
- {
527
- _Fable.log.warn(`Data Cloner: Error closing SQLite connection: ${pCloseError}`);
528
- }
529
-
530
- try
531
- {
532
- // Delete the database file
533
- if (libFs.existsSync(tmpSQLitePath))
534
- {
535
- libFs.unlinkSync(tmpSQLitePath);
536
- _Fable.log.info('Data Cloner: SQLite database file deleted.');
537
- }
538
- }
539
- catch (pDeleteError)
540
- {
541
- _Fable.log.error(`Data Cloner: Error deleting SQLite file: ${pDeleteError}`);
542
- pResponse.send(500, { Success: false, Error: `Failed to delete database: ${pDeleteError}` });
543
- return fNext();
544
- }
545
-
546
- // Reconnect to create a fresh database
547
- _Fable.MeadowSQLiteProvider.connectAsync(
548
- (pReconnectError) =>
549
- {
550
- if (pReconnectError)
551
- {
552
- _Fable.log.error(`Data Cloner: Error reconnecting SQLite: ${pReconnectError}`);
553
- pResponse.send(500, { Success: false, Error: `Failed to reconnect: ${pReconnectError}` });
554
- return fNext();
555
- }
556
-
557
- // Clear sync state
558
- _CloneState.SyncProgress = {};
559
- _CloneState.SyncRESTErrors = {};
560
-
561
- _Fable.log.info('Data Cloner: Database reset complete — fresh SQLite file ready.');
562
-
563
- pResponse.send(200, { Success: true, Message: 'Database reset. Deploy a schema to recreate tables.' });
564
- return fNext();
565
- });
566
- });
567
-
568
- // POST /clone/schema/deploy
569
- tmpServer.post('/clone/schema/deploy',
570
- (pRequest, pResponse, fNext) =>
571
- {
572
- if (!_CloneState.RemoteModelObject)
573
- {
574
- pResponse.send(400, { Success: false, Error: 'No schema fetched. Call POST /clone/schema/fetch first.' });
575
- return fNext();
576
- }
577
-
578
- let tmpBody = pRequest.body || {};
579
- let tmpSelectedTables = tmpBody.Tables || null;
580
-
581
- let tmpFullModel = _CloneState.RemoteModelObject;
582
-
583
- // Build a filtered model with ONLY the selected tables.
584
- // Avoid deep-cloning the entire remote model (can be huge).
585
- let tmpFilteredTables = {};
586
- let tmpFilteredSequence = [];
587
- let tmpSourceTables = tmpFullModel.Tables || {};
588
-
589
- if (tmpSelectedTables && Array.isArray(tmpSelectedTables) && tmpSelectedTables.length > 0)
590
- {
591
- for (let i = 0; i < tmpSelectedTables.length; i++)
592
- {
593
- let tmpTableName = tmpSelectedTables[i];
594
- if (tmpSourceTables[tmpTableName])
595
- {
596
- tmpFilteredTables[tmpTableName] = tmpSourceTables[tmpTableName];
597
- tmpFilteredSequence.push(tmpTableName);
598
- }
599
- }
600
- }
601
- else
602
- {
603
- // No selection — use all tables
604
- tmpFilteredTables = tmpSourceTables;
605
- tmpFilteredSequence = Object.keys(tmpSourceTables);
606
- }
607
-
608
- let tmpModelObject = { Tables: tmpFilteredTables, TablesSequence: tmpFilteredSequence };
609
-
610
- let tmpTableNames = Object.keys(tmpModelObject.Tables || {});
611
- _Fable.log.info(`Data Cloner: Deploying ${tmpTableNames.length} tables to local SQLite: [${tmpTableNames.join(', ')}]`);
612
-
613
- // ---- Set up MeadowCloneRestClient ----
614
- // Bridge remote data fetching to pict-sessionmanager's
615
- // authenticated RestClient so session cookies are injected.
616
- if (!_Fable.MeadowCloneRestClient)
617
- {
618
- _Fable.serviceManager.instantiateServiceProvider('MeadowCloneRestClient',
619
- {
620
- ServerURL: _CloneState.RemoteServerURL + '/1.0/'
621
- });
622
- }
623
- else
624
- {
625
- // Update the server URL if reconfigured
626
- _Fable.MeadowCloneRestClient.serverURL = _CloneState.RemoteServerURL + '/1.0/';
627
- }
628
-
629
- // Override getJSON to delegate through pict-sessionmanager's
630
- // RestClient, which already handles cookie injection & domain matching.
631
- // Also adds debug logging and error tracking per entity.
632
- _Fable.MeadowCloneRestClient.getJSON = (pURL, fCallback) =>
633
- {
634
- let tmpFullURL = _Fable.MeadowCloneRestClient.serverURL + pURL;
635
- _Fable.log.trace(`Data Cloner: [REST] GET ${tmpFullURL}`);
636
-
637
- // Extract the entity name from the URL for error tracking
638
- // URLs look like: "Customer/Max/IDCustomer" or "Customers/FilteredTo/..."
639
- let tmpEntityHint = pURL.split('/')[0].replace(/s$/, '');
640
-
641
- _Pict.RestClient.getJSON(tmpFullURL,
642
- (pError, pResponse, pBody) =>
643
- {
644
- if (pError)
645
- {
646
- _Fable.log.error(`Data Cloner: [REST] Error for ${pURL}: ${pError}`);
647
- if (_CloneState.SyncRESTErrors[tmpEntityHint] !== undefined)
648
- {
649
- _CloneState.SyncRESTErrors[tmpEntityHint]++;
650
- }
651
- }
652
- else
653
- {
654
- let tmpIsArray = Array.isArray(pBody);
655
- let tmpPreview = tmpIsArray ? `Array[${pBody.length}]` : (pBody ? `${typeof(pBody)}:${JSON.stringify(pBody).substring(0, 200)}` : 'null/undefined');
656
- _Fable.log.info(`Data Cloner: [REST] Response for ${pURL}: ${tmpPreview}`);
657
-
658
- // Track when the server returns a non-array for list endpoints
659
- // (FilteredTo, Count, etc.) — this usually indicates an auth or permission error
660
- if (pURL.indexOf('FilteredTo') > -1 && !tmpIsArray)
661
- {
662
- _Fable.log.warn(`Data Cloner: [REST] FilteredTo response for ${tmpEntityHint} is NOT an array — expected Array, got ${typeof(pBody)}`);
663
- if (_CloneState.SyncRESTErrors[tmpEntityHint] !== undefined)
664
- {
665
- _CloneState.SyncRESTErrors[tmpEntityHint]++;
666
- }
667
- }
668
- }
669
- return fCallback(pError, pResponse, pBody);
670
- });
671
- };
672
-
673
- // ---- Set up MeadowSync ----
674
- // MeadowSync.loadMeadowSchema handles:
675
- // 1. Creating a MeadowSyncEntityInitial per table
676
- // 2. Each entity's initialize() calls createTable via
677
- // Meadow.provider.getProvider().createTable()
678
- // 3. Index creation (skips gracefully when no ConnectionManager)
679
- // This replaces manual createTables/createAllIndices calls.
680
-
681
- // MeadowSync constructor reads ProgramConfiguration for sync settings.
682
- // Ensure it exists so the hasOwnProperty checks don't fail.
683
- if (!_Fable.ProgramConfiguration)
684
- {
685
- _Fable.ProgramConfiguration = {};
686
- }
687
-
688
- _Fable.serviceManager.instantiateServiceProvider('MeadowSync',
689
- {
690
- SyncEntityList: tmpTableNames,
691
- PageSize: 100
692
- });
693
-
694
- // Load the schema into MeadowSync — this creates a
695
- // MeadowSyncEntityInitial for each entity with a Meadow DAL
696
- // connected to our SQLite provider, and creates the tables.
697
- _Fable.MeadowSync.loadMeadowSchema(tmpModelObject,
698
- (pSyncInitError) =>
699
- {
700
- if (pSyncInitError)
701
- {
702
- _Fable.log.warn(`Data Cloner: MeadowSync schema init warning: ${pSyncInitError}`);
703
- }
704
-
705
- let tmpInitializedEntities = Object.keys(_Fable.MeadowSync.MeadowSyncEntities);
706
- _Fable.log.info(`Data Cloner: MeadowSync initialized ${tmpInitializedEntities.length} sync entities: [${tmpInitializedEntities.join(', ')}]`);
707
-
708
- // ---- Instrument sync entity DAL operations ----
709
- // Wrap each entity's Meadow doRead/doCreate so we can trace
710
- // exactly what happens to each record during sync.
711
- for (let e = 0; e < tmpInitializedEntities.length; e++)
712
- {
713
- let tmpEntityName = tmpInitializedEntities[e];
714
- let tmpSyncEntity = _Fable.MeadowSync.MeadowSyncEntities[tmpEntityName];
715
-
716
- if (tmpSyncEntity && tmpSyncEntity.Meadow)
717
- {
718
- let tmpEntityMeadow = tmpSyncEntity.Meadow;
719
-
720
- // Wrap doRead
721
- let tmpOriginalDoRead = tmpEntityMeadow.doRead.bind(tmpEntityMeadow);
722
- tmpEntityMeadow.doRead = (pQuery, fCallback) =>
723
- {
724
- tmpOriginalDoRead(pQuery,
725
- (pError, pQuery, pRecord) =>
726
- {
727
- if (pError)
728
- {
729
- _Fable.log.info(`Data Cloner: [DAL] doRead ${tmpEntityName}: ERROR ${pError}`);
730
- }
731
- else
732
- {
733
- _Fable.log.info(`Data Cloner: [DAL] doRead ${tmpEntityName}: found=${!!pRecord} (type=${typeof(pRecord)})`);
734
- }
735
- return fCallback(pError, pQuery, pRecord);
736
- });
737
- };
738
-
739
- // Wrap doCreate
740
- let tmpOriginalDoCreate = tmpEntityMeadow.doCreate.bind(tmpEntityMeadow);
741
- tmpEntityMeadow.doCreate = (pQuery, fCallback) =>
742
- {
743
- tmpOriginalDoCreate(pQuery,
744
- (pError, pQuery, pRecord) =>
745
- {
746
- if (pError)
747
- {
748
- _Fable.log.info(`Data Cloner: [DAL] doCreate ${tmpEntityName}: ERROR ${pError}`);
749
- }
750
- else
751
- {
752
- let tmpID = pRecord ? pRecord[tmpSyncEntity.DefaultIdentifier] : '?';
753
- _Fable.log.info(`Data Cloner: [DAL] doCreate ${tmpEntityName}: success ID=${tmpID}`);
754
- }
755
- return fCallback(pError, pQuery, pRecord);
756
- });
757
- };
758
- }
759
- }
760
-
761
- _Fable.log.info(`Data Cloner: Loading model for CRUD endpoints...`);
762
-
763
- // Load the filtered model so CRUD endpoints are available
764
- _Fable.RetoldDataServiceMeadowEndpoints.loadModel('RemoteClone', tmpModelObject, 'SQLite',
765
- (pLoadError) =>
766
- {
767
- if (pLoadError)
768
- {
769
- _Fable.log.error(`Data Cloner: Model load error: ${pLoadError}`);
770
- }
771
- else
772
- {
773
- _Fable.log.info(`Data Cloner: CRUD endpoints available for: [${tmpTableNames.join(', ')}]`);
774
- }
775
-
776
- pResponse.send(200,
777
- {
778
- Success: true,
779
- TablesDeployed: tmpTableNames,
780
- SyncEntities: tmpInitializedEntities,
781
- Message: `${tmpInitializedEntities.length} / ${tmpTableNames.length} tables deployed. meadow-integration sync ready.`
782
- });
783
- return fNext();
784
- });
785
- });
786
- });
787
-
788
- // ---- Data Synchronization ----
789
-
790
- // POST /clone/sync/start
791
- tmpServer.post('/clone/sync/start',
792
- (pRequest, pResponse, fNext) =>
793
- {
794
- if (_CloneState.SyncRunning)
795
- {
796
- pResponse.send(400, { Success: false, Error: 'Sync is already running.' });
797
- return fNext();
798
- }
799
-
800
- if (!_CloneState.RemoteServerURL)
801
- {
802
- pResponse.send(400, { Success: false, Error: 'No remote server configured.' });
803
- return fNext();
804
- }
805
-
806
- if (!_Fable.MeadowSync || !_Fable.MeadowSync.MeadowSyncEntities)
807
- {
808
- pResponse.send(400, { Success: false, Error: 'No sync entities available. Deploy a schema first.' });
809
- return fNext();
810
- }
811
-
812
- let tmpBody = pRequest.body || {};
813
- let tmpSelectedTables = tmpBody.Tables || [];
814
-
815
- // If no tables specified, sync all entities that MeadowSync knows about
816
- if (tmpSelectedTables.length === 0)
817
- {
818
- tmpSelectedTables = _Fable.MeadowSync.SyncEntityList || Object.keys(_Fable.MeadowSync.MeadowSyncEntities);
819
- }
820
-
821
- if (tmpSelectedTables.length === 0)
822
- {
823
- pResponse.send(400, { Success: false, Error: 'No tables available for sync. Deploy a schema first.' });
824
- return fNext();
825
- }
826
-
827
- _Fable.log.info(`Data Cloner: Starting sync for ${tmpSelectedTables.length} tables via meadow-integration`);
828
-
829
- // Initialize progress tracking
830
- _CloneState.SyncRunning = true;
831
- _CloneState.SyncStopping = false;
832
- _CloneState.SyncProgress = {};
833
- _CloneState.SyncRESTErrors = {};
834
-
835
- for (let i = 0; i < tmpSelectedTables.length; i++)
836
- {
837
- _CloneState.SyncProgress[tmpSelectedTables[i]] = (
838
- {
839
- Status: 'Pending',
840
- Total: 0,
841
- Synced: 0,
842
- Errors: 0,
843
- StartTime: null,
844
- EndTime: null
845
- });
846
- _CloneState.SyncRESTErrors[tmpSelectedTables[i]] = 0;
847
- }
848
-
849
- // Start the sync process asynchronously
850
- fSyncTables(tmpSelectedTables);
851
-
852
- pResponse.send(200,
853
- {
854
- Success: true,
855
- Tables: tmpSelectedTables,
856
- Message: 'Sync started via meadow-integration.'
857
- });
858
- return fNext();
859
- });
860
-
861
- // GET /clone/sync/status
862
- tmpServer.get('/clone/sync/status',
863
- (pRequest, pResponse, fNext) =>
864
- {
865
- // Update progress from MeadowSync operation trackers
866
- if (_Fable.MeadowSync && _Fable.MeadowSync.MeadowSyncEntities)
867
- {
868
- let tmpEntityNames = Object.keys(_Fable.MeadowSync.MeadowSyncEntities);
869
- for (let i = 0; i < tmpEntityNames.length; i++)
870
- {
871
- let tmpEntityName = tmpEntityNames[i];
872
- let tmpProgress = _CloneState.SyncProgress[tmpEntityName];
873
- if (tmpProgress && (tmpProgress.Status === 'Syncing' || tmpProgress.Status === 'Pending'))
874
- {
875
- let tmpSyncEntity = _Fable.MeadowSync.MeadowSyncEntities[tmpEntityName];
876
- if (tmpSyncEntity && tmpSyncEntity.operation)
877
- {
878
- let tmpTracker = tmpSyncEntity.operation.progressTrackers[`FullSync-${tmpEntityName}`];
879
- if (tmpTracker)
880
- {
881
- tmpProgress.Total = tmpTracker.TotalCount || 0;
882
- tmpProgress.Synced = Math.max(tmpTracker.CurrentCount || 0, 0);
883
- }
884
- }
885
- // Include REST error count in real-time status
886
- let tmpRESTErrors = _CloneState.SyncRESTErrors[tmpEntityName] || 0;
887
- if (tmpRESTErrors > 0)
888
- {
889
- tmpProgress.Errors = tmpRESTErrors;
890
- }
891
- }
892
- }
893
- }
894
-
895
- pResponse.send(200,
896
- {
897
- Running: _CloneState.SyncRunning,
898
- Stopping: _CloneState.SyncStopping,
899
- Tables: _CloneState.SyncProgress
900
- });
901
- return fNext();
902
- });
903
-
904
- // POST /clone/sync/stop
905
- tmpServer.post('/clone/sync/stop',
906
- (pRequest, pResponse, fNext) =>
907
- {
908
- if (_CloneState.SyncRunning)
909
- {
910
- _CloneState.SyncStopping = true;
911
- _Fable.log.info('Data Cloner: Sync stop requested.');
912
- }
913
-
914
- pResponse.send(200, { Success: true, Message: 'Sync stop requested.' });
915
- return fNext();
916
- });
917
-
918
- _Fable.log.info('Data Cloner: Custom /clone/* endpoints registered.');
919
- return fCallback();
920
- };
921
-
922
- // ================================================================
923
- // Sync Engine (powered by meadow-integration)
924
- // ================================================================
925
-
926
- /**
927
- * Synchronize data for a list of tables sequentially using
928
- * meadow-integration's MeadowSyncEntityInitial service.
929
- *
930
- * Each entity's sync() method handles:
931
- * - Counting local vs remote records
932
- * - Paginated downloads via MeadowCloneRestClient
933
- * - Record marshaling (objects→JSON, DateTime normalization, etc.)
934
- * - Writing via Meadow DAL (doCreate with identity insert)
935
- *
936
- * @param {Array<string>} pTables - Table names to sync
937
- */
938
- function fSyncTables(pTables)
939
- {
940
- let tmpTableIndex = 0;
941
-
942
- let fSyncNextTable = () =>
943
- {
944
- if (_CloneState.SyncStopping || tmpTableIndex >= pTables.length)
945
- {
946
- _CloneState.SyncRunning = false;
947
- _CloneState.SyncStopping = false;
948
- _Fable.log.info('Data Cloner: Sync complete.');
949
- return;
950
- }
951
-
952
- let tmpTableName = pTables[tmpTableIndex];
953
- tmpTableIndex++;
954
-
955
- let tmpProgress = _CloneState.SyncProgress[tmpTableName];
956
- if (!tmpProgress)
957
- {
958
- fSyncNextTable();
959
- return;
960
- }
961
-
962
- tmpProgress.Status = 'Syncing';
963
- tmpProgress.StartTime = new Date().toJSON();
964
-
965
- _Fable.log.info(`Data Cloner: Sync [${tmpTableName}] — starting via meadow-integration...`);
966
-
967
- // Use meadow-integration's sync entity
968
- _Fable.MeadowSync.syncEntity(tmpTableName,
969
- (pError) =>
970
- {
971
- // Extract final progress from the operation tracker.
972
- // NOTE: meadow-integration's sync() always calls fCallback()
973
- // without propagating errors, so pError will typically be
974
- // undefined even when errors occurred. We detect issues via
975
- // the progress tracker and our REST error counters.
976
- let tmpSyncEntity = _Fable.MeadowSync.MeadowSyncEntities[tmpTableName];
977
- if (tmpSyncEntity && tmpSyncEntity.operation)
978
- {
979
- let tmpTracker = tmpSyncEntity.operation.progressTrackers[`FullSync-${tmpTableName}`];
980
- if (tmpTracker)
981
- {
982
- tmpProgress.Total = tmpTracker.TotalCount || 0;
983
- tmpProgress.Synced = Math.max(tmpTracker.CurrentCount || 0, 0);
984
- }
985
- }
986
-
987
- // Incorporate REST-level errors tracked by our getJSON override
988
- let tmpRESTErrors = _CloneState.SyncRESTErrors[tmpTableName] || 0;
989
- tmpProgress.Errors = tmpRESTErrors;
990
-
991
- // Calculate how many records were expected but not synced
992
- // (due to GUID conflicts, create errors, etc.)
993
- let tmpMissing = tmpProgress.Total - tmpProgress.Synced;
994
-
995
- if (pError)
996
- {
997
- _Fable.log.error(`Data Cloner: Error syncing [${tmpTableName}]: ${pError}`);
998
- tmpProgress.Status = 'Error';
999
- tmpProgress.ErrorMessage = `${pError}`;
1000
- }
1001
- else if (tmpRESTErrors > 0)
1002
- {
1003
- // REST errors occurred (timeouts, auth failures, non-array responses)
1004
- tmpProgress.Status = 'Error';
1005
- tmpProgress.ErrorMessage = `${tmpRESTErrors} REST error(s) during sync`;
1006
- _Fable.log.warn(`Data Cloner: Sync [${tmpTableName}] — completed with ${tmpRESTErrors} REST error(s). ${tmpProgress.Synced}/${tmpProgress.Total} records synced.`);
1007
- }
1008
- else if (tmpProgress.Total > 0 && tmpMissing > 0)
1009
- {
1010
- // Some records were expected but not created
1011
- // (GUID conflicts, other Meadow create errors)
1012
- tmpProgress.Status = 'Partial';
1013
- tmpProgress.Skipped = tmpMissing;
1014
- _Fable.log.warn(`Data Cloner: Sync [${tmpTableName}] — partial. ${tmpProgress.Synced}/${tmpProgress.Total} records synced, ${tmpMissing} skipped (GUID conflicts or other errors).`);
1015
- }
1016
- else
1017
- {
1018
- tmpProgress.Status = 'Complete';
1019
- _Fable.log.info(`Data Cloner: Sync [${tmpTableName}] — complete. ${tmpProgress.Synced}/${tmpProgress.Total} records synced.`);
1020
- }
1021
- tmpProgress.EndTime = new Date().toJSON();
1022
-
1023
- // Continue to next table regardless of error
1024
- fSyncNextTable();
1025
- });
1026
- };
1027
-
1028
- fSyncNextTable();
1029
- }
1030
-
1031
- // ================================================================
1032
- // Start the service
1033
- // ================================================================
1034
-
1035
- tmpDataService.initializeService(
1036
- (pInitError) =>
1037
- {
1038
- if (pInitError)
1039
- {
1040
- _Fable.log.error(`Initialization error: ${pInitError}`);
1041
- process.exit(1);
1042
- }
1043
- _Fable.log.info(`Data Cloner running on port ${_Settings.APIServerPort}`);
1044
- _Fable.log.info(`Web UI: http://localhost:${_Settings.APIServerPort}/clone/`);
1045
- _Fable.log.info(`Migration Mgr: http://localhost:${_Settings.APIServerPort}/meadow-migrationmanager/`);
1046
- });
1047
- });