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.
- package/.claude/launch.json +11 -0
- package/bin/retold-data-service-clone.js +286 -0
- package/package.json +14 -9
- package/source/Retold-Data-Service.js +52 -2
- 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/debug/data/books.csv +0 -10001
- package/example_applications/data-cloner/data/cloned.sqlite +0 -0
- package/example_applications/data-cloner/data/cloned.sqlite-shm +0 -0
- package/example_applications/data-cloner/data/cloned.sqlite-wal +0 -0
- package/example_applications/data-cloner/data-cloner-web.html +0 -935
- package/example_applications/data-cloner/data-cloner.js +0 -1047
- 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
|
-
});
|