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
@@ -0,0 +1,367 @@
1
+ /**
2
+ * DataCloner Schema Management Routes
3
+ *
4
+ * Registers /clone/schema/* endpoints for fetching remote schemas,
5
+ * deploying tables to the local database, and resetting the database.
6
+ *
7
+ * @param {Object} pDataClonerService - The RetoldDataServiceDataCloner instance
8
+ * @param {Object} pOratorServiceServer - The Orator ServiceServer instance
9
+ */
10
+ module.exports = (pDataClonerService, pOratorServiceServer) =>
11
+ {
12
+ let tmpFable = pDataClonerService.fable;
13
+ let tmpCloneState = pDataClonerService.cloneState;
14
+ let tmpPict = pDataClonerService.pict;
15
+ let tmpPrefix = pDataClonerService.routePrefix;
16
+
17
+ let libFs = require('fs');
18
+
19
+ // POST /clone/schema/fetch
20
+ // Accepts either:
21
+ // { SchemaURL: "..." } — fetch from a remote URL
22
+ // { Schema: { Tables: {...} } } — use a pre-loaded schema object directly
23
+ // {} — default to RemoteServerURL + 'Retold/Models'
24
+ pOratorServiceServer.post(`${tmpPrefix}/schema/fetch`,
25
+ (pRequest, pResponse, fNext) =>
26
+ {
27
+ let tmpBody = pRequest.body || {};
28
+
29
+ // If a raw Schema object was provided, use it directly (no remote fetch)
30
+ if (tmpBody.Schema && typeof tmpBody.Schema === 'object')
31
+ {
32
+ tmpFable.log.info('Data Cloner: Using pre-loaded schema object...');
33
+
34
+ tmpCloneState.RemoteSchema = tmpBody.Schema;
35
+ tmpCloneState.RemoteModelObject = tmpBody.Schema;
36
+
37
+ let tmpTableNames = [];
38
+ if (tmpBody.Schema.Tables)
39
+ {
40
+ tmpTableNames = Object.keys(tmpBody.Schema.Tables);
41
+ }
42
+
43
+ tmpFable.log.info(`Data Cloner: Loaded schema with ${tmpTableNames.length} tables: [${tmpTableNames.join(', ')}]`);
44
+
45
+ pResponse.send(200,
46
+ {
47
+ Success: true,
48
+ Source: 'local',
49
+ TableCount: tmpTableNames.length,
50
+ Tables: tmpTableNames
51
+ });
52
+ return fNext();
53
+ }
54
+
55
+ let tmpSchemaURL = tmpBody.SchemaURL;
56
+
57
+ if (!tmpSchemaURL)
58
+ {
59
+ // Default to the standard retold model endpoint
60
+ if (tmpCloneState.RemoteServerURL)
61
+ {
62
+ tmpSchemaURL = tmpCloneState.RemoteServerURL + 'Retold/Models';
63
+ }
64
+ else
65
+ {
66
+ pResponse.send(400, { Success: false, Error: 'SchemaURL is required (or configure a session first).' });
67
+ return fNext();
68
+ }
69
+ }
70
+
71
+ tmpFable.log.info(`Data Cloner: Fetching remote schema from ${tmpSchemaURL}...`);
72
+
73
+ tmpPict.RestClient.getJSON(tmpSchemaURL,
74
+ (pError, pHTTPResponse, pData) =>
75
+ {
76
+ if (pError)
77
+ {
78
+ tmpFable.log.error(`Data Cloner: Schema fetch error: ${pError.message || pError}`);
79
+ pResponse.send(500, { Success: false, Error: `Schema fetch error: ${pError.message || pError}` });
80
+ return fNext();
81
+ }
82
+
83
+ if (!pHTTPResponse || pHTTPResponse.statusCode !== 200)
84
+ {
85
+ let tmpStatus = pHTTPResponse ? pHTTPResponse.statusCode : 'unknown';
86
+ tmpFable.log.error(`Data Cloner: Schema fetch returned HTTP ${tmpStatus} — body: ${JSON.stringify(pData)}`);
87
+ pResponse.send(500, { Success: false, Error: `Schema fetch returned HTTP ${tmpStatus}` });
88
+ return fNext();
89
+ }
90
+
91
+ tmpCloneState.RemoteSchema = pData;
92
+ tmpCloneState.RemoteModelObject = pData;
93
+
94
+ // Extract table names for the UI
95
+ let tmpTableNames = [];
96
+ if (pData && pData.Tables)
97
+ {
98
+ tmpTableNames = Object.keys(pData.Tables);
99
+ }
100
+
101
+ tmpFable.log.info(`Data Cloner: Fetched schema with ${tmpTableNames.length} tables: [${tmpTableNames.join(', ')}]`);
102
+
103
+ pResponse.send(200,
104
+ {
105
+ Success: true,
106
+ SchemaURL: tmpSchemaURL,
107
+ TableCount: tmpTableNames.length,
108
+ Tables: tmpTableNames
109
+ });
110
+ return fNext();
111
+ });
112
+ });
113
+
114
+ // GET /clone/schema
115
+ pOratorServiceServer.get(`${tmpPrefix}/schema`,
116
+ (pRequest, pResponse, fNext) =>
117
+ {
118
+ if (!tmpCloneState.RemoteSchema)
119
+ {
120
+ pResponse.send(200, { Fetched: false, Tables: [] });
121
+ return fNext();
122
+ }
123
+
124
+ let tmpTableNames = [];
125
+ if (tmpCloneState.RemoteSchema.Tables)
126
+ {
127
+ tmpTableNames = Object.keys(tmpCloneState.RemoteSchema.Tables);
128
+ }
129
+
130
+ pResponse.send(200,
131
+ {
132
+ Fetched: true,
133
+ TableCount: tmpTableNames.length,
134
+ Tables: tmpTableNames
135
+ });
136
+ return fNext();
137
+ });
138
+
139
+ // POST /clone/reset — Delete the local SQLite database and start fresh
140
+ pOratorServiceServer.post(`${tmpPrefix}/reset`,
141
+ (pRequest, pResponse, fNext) =>
142
+ {
143
+ let tmpSQLitePath = tmpFable.settings.SQLite && tmpFable.settings.SQLite.SQLiteFilePath;
144
+ if (!tmpSQLitePath)
145
+ {
146
+ pResponse.send(400, { Success: false, Error: 'No SQLite file path configured.' });
147
+ return fNext();
148
+ }
149
+
150
+ tmpFable.log.info(`Data Cloner: Resetting local database [${tmpSQLitePath}]...`);
151
+
152
+ try
153
+ {
154
+ // Close the existing SQLite connection and reset the provider state
155
+ if (tmpFable.MeadowSQLiteProvider)
156
+ {
157
+ if (tmpFable.MeadowSQLiteProvider._database)
158
+ {
159
+ tmpFable.MeadowSQLiteProvider._database.close();
160
+ }
161
+ tmpFable.MeadowSQLiteProvider.connected = false;
162
+ }
163
+ }
164
+ catch (pCloseError)
165
+ {
166
+ tmpFable.log.warn(`Data Cloner: Error closing SQLite connection: ${pCloseError}`);
167
+ }
168
+
169
+ try
170
+ {
171
+ // Delete the database file
172
+ if (libFs.existsSync(tmpSQLitePath))
173
+ {
174
+ libFs.unlinkSync(tmpSQLitePath);
175
+ tmpFable.log.info('Data Cloner: SQLite database file deleted.');
176
+ }
177
+ }
178
+ catch (pDeleteError)
179
+ {
180
+ tmpFable.log.error(`Data Cloner: Error deleting SQLite file: ${pDeleteError}`);
181
+ pResponse.send(500, { Success: false, Error: `Failed to delete database: ${pDeleteError}` });
182
+ return fNext();
183
+ }
184
+
185
+ // Reconnect to create a fresh database
186
+ tmpFable.MeadowSQLiteProvider.connectAsync(
187
+ (pReconnectError) =>
188
+ {
189
+ if (pReconnectError)
190
+ {
191
+ tmpFable.log.error(`Data Cloner: Error reconnecting SQLite: ${pReconnectError}`);
192
+ pResponse.send(500, { Success: false, Error: `Failed to reconnect: ${pReconnectError}` });
193
+ return fNext();
194
+ }
195
+
196
+ // Clear sync state
197
+ tmpCloneState.SyncProgress = {};
198
+ tmpCloneState.SyncRESTErrors = {};
199
+
200
+ tmpFable.log.info('Data Cloner: Database reset complete — fresh SQLite file ready.');
201
+
202
+ pResponse.send(200, { Success: true, Message: 'Database reset. Deploy a schema to recreate tables.' });
203
+ return fNext();
204
+ });
205
+ });
206
+
207
+ // POST /clone/schema/deploy
208
+ pOratorServiceServer.post(`${tmpPrefix}/schema/deploy`,
209
+ (pRequest, pResponse, fNext) =>
210
+ {
211
+ if (!tmpCloneState.RemoteModelObject)
212
+ {
213
+ pResponse.send(400, { Success: false, Error: 'No schema fetched. Call POST /clone/schema/fetch first.' });
214
+ return fNext();
215
+ }
216
+
217
+ let tmpBody = pRequest.body || {};
218
+ let tmpSelectedTables = tmpBody.Tables || null;
219
+
220
+ let tmpFullModel = tmpCloneState.RemoteModelObject;
221
+
222
+ // Build a filtered model with ONLY the selected tables
223
+ let tmpFilteredTables = {};
224
+ let tmpFilteredSequence = [];
225
+ let tmpSourceTables = tmpFullModel.Tables || {};
226
+
227
+ if (tmpSelectedTables && Array.isArray(tmpSelectedTables) && tmpSelectedTables.length > 0)
228
+ {
229
+ for (let i = 0; i < tmpSelectedTables.length; i++)
230
+ {
231
+ let tmpTableName = tmpSelectedTables[i];
232
+ if (tmpSourceTables[tmpTableName])
233
+ {
234
+ tmpFilteredTables[tmpTableName] = tmpSourceTables[tmpTableName];
235
+ tmpFilteredSequence.push(tmpTableName);
236
+ }
237
+ }
238
+ }
239
+ else
240
+ {
241
+ // No selection — use all tables
242
+ tmpFilteredTables = tmpSourceTables;
243
+ tmpFilteredSequence = Object.keys(tmpSourceTables);
244
+ }
245
+
246
+ let tmpModelObject = { Tables: tmpFilteredTables, TablesSequence: tmpFilteredSequence };
247
+
248
+ let tmpTableNames = Object.keys(tmpModelObject.Tables || {});
249
+ tmpFable.log.info(`Data Cloner: Deploying ${tmpTableNames.length} tables to local ${tmpCloneState.ConnectionProvider}: [${tmpTableNames.join(', ')}]`);
250
+
251
+ // ---- Set up MeadowCloneRestClient ----
252
+ if (!tmpFable.MeadowCloneRestClient)
253
+ {
254
+ tmpFable.serviceManager.instantiateServiceProvider('MeadowCloneRestClient',
255
+ {
256
+ ServerURL: tmpCloneState.RemoteServerURL,
257
+ RequestTimeout: 60000,
258
+ MaxRequestTimeout: 300000
259
+ });
260
+ }
261
+ else
262
+ {
263
+ tmpFable.MeadowCloneRestClient.serverURL = tmpCloneState.RemoteServerURL;
264
+ }
265
+
266
+ // Override getJSON to delegate through pict-sessionmanager's
267
+ // RestClient, which already handles cookie injection & domain matching.
268
+ let libHttps = require('https');
269
+ libHttps.globalAgent.options.timeout = tmpFable.MeadowCloneRestClient.maxRequestTimeout;
270
+ tmpFable.MeadowCloneRestClient.getJSON = (pURL, fCallback) =>
271
+ {
272
+ let tmpFullURL = tmpFable.MeadowCloneRestClient.serverURL + pURL;
273
+
274
+ // Extract the entity name from the URL for error tracking
275
+ let tmpEntityHint = pURL.split('/')[0].replace(/s$/, '');
276
+
277
+ // Use the longer timeout for MAX(column) queries only
278
+ let tmpIsMaxRequest = (pURL.indexOf('/Max/') > -1);
279
+ let tmpPreviousTimeout = libHttps.globalAgent.options.timeout;
280
+ libHttps.globalAgent.options.timeout = tmpIsMaxRequest
281
+ ? tmpFable.MeadowCloneRestClient.maxRequestTimeout
282
+ : tmpFable.MeadowCloneRestClient.requestTimeout;
283
+
284
+ tmpPict.RestClient.getJSON(tmpFullURL,
285
+ (pError, pHTTPResponse, pBody) =>
286
+ {
287
+ if (pError)
288
+ {
289
+ tmpFable.log.error(`Data Cloner: REST error for ${pURL}: ${pError}`);
290
+ if (tmpCloneState.SyncRESTErrors[tmpEntityHint] !== undefined)
291
+ {
292
+ tmpCloneState.SyncRESTErrors[tmpEntityHint]++;
293
+ }
294
+ }
295
+ else
296
+ {
297
+ // Track when the server returns a non-array for list endpoints
298
+ // Count/FilteredTo returns {"Count":N} (an object, not an array) which is expected
299
+ if (pURL.indexOf('FilteredTo') > -1 && pURL.indexOf('Count/FilteredTo') < 0 && !Array.isArray(pBody))
300
+ {
301
+ let tmpBodyPreview = (typeof(pBody) === 'string') ? pBody.substring(0, 300) : JSON.stringify(pBody).substring(0, 300);
302
+ tmpFable.log.warn(`Data Cloner: FilteredTo response for ${tmpEntityHint} is not an array — URL: ${pURL} — Response: ${tmpBodyPreview}`);
303
+ if (tmpCloneState.SyncRESTErrors[tmpEntityHint] !== undefined)
304
+ {
305
+ tmpCloneState.SyncRESTErrors[tmpEntityHint]++;
306
+ }
307
+ }
308
+ }
309
+ libHttps.globalAgent.options.timeout = tmpPreviousTimeout;
310
+ return fCallback(pError, pHTTPResponse, pBody);
311
+ });
312
+ };
313
+
314
+ // ---- Set up MeadowSync ----
315
+ if (!tmpFable.ProgramConfiguration)
316
+ {
317
+ tmpFable.ProgramConfiguration = {};
318
+ }
319
+
320
+ tmpFable.serviceManager.instantiateServiceProvider('MeadowSync',
321
+ {
322
+ SyncEntityList: tmpTableNames,
323
+ PageSize: 100,
324
+ SyncDeletedRecords: tmpCloneState.SyncDeletedRecords
325
+ });
326
+
327
+ tmpFable.MeadowSync.loadMeadowSchema(tmpModelObject,
328
+ (pSyncInitError) =>
329
+ {
330
+ if (pSyncInitError)
331
+ {
332
+ tmpFable.log.warn(`Data Cloner: MeadowSync schema init warning: ${pSyncInitError}`);
333
+ }
334
+
335
+ let tmpInitializedEntities = Object.keys(tmpFable.MeadowSync.MeadowSyncEntities);
336
+ tmpFable.log.info(`Data Cloner: MeadowSync initialized ${tmpInitializedEntities.length} sync entities: [${tmpInitializedEntities.join(', ')}]`);
337
+
338
+ // Store the deployed model so sync mode switches can re-create entities
339
+ tmpCloneState.DeployedModelObject = tmpModelObject;
340
+
341
+ tmpFable.log.info(`Data Cloner: Loading model for CRUD endpoints...`);
342
+
343
+ // Load the filtered model so CRUD endpoints are available
344
+ tmpFable.RetoldDataServiceMeadowEndpoints.loadModel('RemoteClone', tmpModelObject, tmpCloneState.ConnectionProvider,
345
+ (pLoadError) =>
346
+ {
347
+ if (pLoadError)
348
+ {
349
+ tmpFable.log.error(`Data Cloner: Model load error: ${pLoadError}`);
350
+ }
351
+ else
352
+ {
353
+ tmpFable.log.info(`Data Cloner: CRUD endpoints available for: [${tmpTableNames.join(', ')}]`);
354
+ }
355
+
356
+ pResponse.send(200,
357
+ {
358
+ Success: true,
359
+ TablesDeployed: tmpTableNames,
360
+ SyncEntities: tmpInitializedEntities,
361
+ Message: `${tmpInitializedEntities.length} / ${tmpTableNames.length} tables deployed. meadow-integration sync ready.`
362
+ });
363
+ return fNext();
364
+ });
365
+ });
366
+ });
367
+ };
@@ -0,0 +1,229 @@
1
+ /**
2
+ * DataCloner Session Management Routes
3
+ *
4
+ * Registers /clone/session/* endpoints for remote session configuration,
5
+ * authentication, check, and deauthentication via pict-sessionmanager.
6
+ *
7
+ * @param {Object} pDataClonerService - The RetoldDataServiceDataCloner instance
8
+ * @param {Object} pOratorServiceServer - The Orator ServiceServer instance
9
+ */
10
+ module.exports = (pDataClonerService, pOratorServiceServer) =>
11
+ {
12
+ let tmpFable = pDataClonerService.fable;
13
+ let tmpCloneState = pDataClonerService.cloneState;
14
+ let tmpPict = pDataClonerService.pict;
15
+ let tmpPrefix = pDataClonerService.routePrefix;
16
+
17
+ // POST /clone/session/configure
18
+ pOratorServiceServer.post(`${tmpPrefix}/session/configure`,
19
+ (pRequest, pResponse, fNext) =>
20
+ {
21
+ let tmpBody = pRequest.body || {};
22
+ let tmpServerURL = tmpBody.ServerURL;
23
+
24
+ if (!tmpServerURL)
25
+ {
26
+ pResponse.send(400, { Success: false, Error: 'ServerURL is required.' });
27
+ return fNext();
28
+ }
29
+
30
+ tmpCloneState.RemoteServerURL = tmpServerURL;
31
+
32
+ // Remove existing session if reconfiguring
33
+ if (tmpPict.SessionManager.getSession('Remote'))
34
+ {
35
+ tmpPict.SessionManager.deauthenticate('Remote');
36
+ delete tmpPict.SessionManager.sessions['Remote'];
37
+ }
38
+
39
+ // Extract domain from ServerURL for cookie matching
40
+ let tmpDomainMatch = tmpBody.DomainMatch;
41
+ if (!tmpDomainMatch)
42
+ {
43
+ try
44
+ {
45
+ let tmpURL = new URL(tmpServerURL);
46
+ tmpDomainMatch = tmpURL.hostname;
47
+ }
48
+ catch (pParseError)
49
+ {
50
+ tmpDomainMatch = tmpServerURL;
51
+ }
52
+ }
53
+
54
+ // Helper: ensure a URI template is fully qualified with the server URL
55
+ let fQualifyURI = (pTemplate, pDefault) =>
56
+ {
57
+ let tmpTemplate = pTemplate || pDefault;
58
+ // Already a full URL — use as-is
59
+ if (tmpTemplate.indexOf('://') > -1)
60
+ {
61
+ return tmpTemplate;
62
+ }
63
+ // Strip leading slash
64
+ let tmpPath = tmpTemplate.replace(/^\//, '');
65
+ // If the server URL already ends with a version prefix (e.g. /1.0/)
66
+ // and the path redundantly starts with the same prefix, strip it
67
+ // to avoid double-prefixing (e.g. /1.0/1.0/Authenticate).
68
+ let tmpVersionMatch = tmpServerURL.match(/\/(\d+\.\d+)\/?$/);
69
+ if (tmpVersionMatch)
70
+ {
71
+ let tmpURLPrefix = tmpVersionMatch[1] + '/';
72
+ if (tmpPath.indexOf(tmpURLPrefix) === 0)
73
+ {
74
+ tmpPath = tmpPath.substring(tmpURLPrefix.length);
75
+ }
76
+ }
77
+ return tmpServerURL + tmpPath;
78
+ };
79
+
80
+ let tmpSessionConfig = (
81
+ {
82
+ Type: 'Cookie',
83
+
84
+ // Authentication
85
+ AuthenticationMethod: tmpBody.AuthenticationMethod || 'get',
86
+ AuthenticationURITemplate: fQualifyURI(tmpBody.AuthenticationURITemplate, 'Authenticate/{~D:Record.UserName~}/{~D:Record.Password~}'),
87
+ AuthenticationRetryCount: 2,
88
+ AuthenticationRetryDebounce: 200,
89
+
90
+ // Session check
91
+ CheckSessionURITemplate: fQualifyURI(tmpBody.CheckSessionURITemplate, 'CheckSession'),
92
+ CheckSessionLoginMarkerType: tmpBody.CheckSessionLoginMarkerType || 'boolean',
93
+ CheckSessionLoginMarker: tmpBody.CheckSessionLoginMarker || 'LoggedIn',
94
+
95
+ // Cookie injection
96
+ DomainMatch: tmpDomainMatch,
97
+ CookieName: tmpBody.CookieName || 'SessionID',
98
+ CookieValueAddress: tmpBody.CookieValueAddress || 'SessionID',
99
+ CookieValueTemplate: tmpBody.CookieValueTemplate || false
100
+ });
101
+
102
+ // If authentication is POST-based, set up the request body template
103
+ if (tmpBody.AuthenticationMethod === 'post')
104
+ {
105
+ tmpSessionConfig.AuthenticationRequestBody = tmpBody.AuthenticationRequestBody || (
106
+ {
107
+ username: '{~D:Record.UserName~}',
108
+ password: '{~D:Record.Password~}'
109
+ });
110
+ }
111
+
112
+ tmpPict.SessionManager.addSession('Remote', tmpSessionConfig);
113
+ tmpPict.SessionManager.connectToRestClient();
114
+
115
+ tmpCloneState.SessionConfigured = true;
116
+ tmpCloneState.SessionAuthenticated = false;
117
+
118
+ tmpFable.log.info(`Data Cloner: Session configured for ${tmpServerURL} (domain: ${tmpDomainMatch})`);
119
+
120
+ pResponse.send(200,
121
+ {
122
+ Success: true,
123
+ ServerURL: tmpServerURL,
124
+ DomainMatch: tmpDomainMatch,
125
+ AuthenticationMethod: tmpSessionConfig.AuthenticationMethod
126
+ });
127
+ return fNext();
128
+ });
129
+
130
+ // POST /clone/session/authenticate
131
+ pOratorServiceServer.post(`${tmpPrefix}/session/authenticate`,
132
+ (pRequest, pResponse, fNext) =>
133
+ {
134
+ if (!tmpCloneState.SessionConfigured)
135
+ {
136
+ pResponse.send(400, { Success: false, Error: 'Session not configured. Call POST /clone/session/configure first.' });
137
+ return fNext();
138
+ }
139
+
140
+ let tmpBody = pRequest.body || {};
141
+ let tmpCredentials = (
142
+ {
143
+ UserName: tmpBody.UserName || tmpBody.username,
144
+ Password: tmpBody.Password || tmpBody.password
145
+ });
146
+
147
+ if (!tmpCredentials.UserName || !tmpCredentials.Password)
148
+ {
149
+ pResponse.send(400, { Success: false, Error: 'UserName and Password are required.' });
150
+ return fNext();
151
+ }
152
+
153
+ tmpFable.log.info(`Data Cloner: Authenticating as ${tmpCredentials.UserName}...`);
154
+
155
+ tmpPict.SessionManager.authenticate('Remote', tmpCredentials,
156
+ (pAuthError, pSessionState) =>
157
+ {
158
+ if (pAuthError)
159
+ {
160
+ tmpFable.log.error(`Data Cloner: Authentication failed: ${pAuthError.message || pAuthError}`);
161
+ tmpCloneState.SessionAuthenticated = false;
162
+ pResponse.send(401,
163
+ {
164
+ Success: false,
165
+ Error: `Authentication failed: ${pAuthError.message || pAuthError}`
166
+ });
167
+ return fNext();
168
+ }
169
+
170
+ tmpCloneState.SessionAuthenticated = pSessionState && pSessionState.Authenticated;
171
+
172
+ tmpFable.log.info(`Data Cloner: Authentication ${tmpCloneState.SessionAuthenticated ? 'succeeded' : 'failed'}.`);
173
+
174
+ pResponse.send(200,
175
+ {
176
+ Success: tmpCloneState.SessionAuthenticated,
177
+ Authenticated: tmpCloneState.SessionAuthenticated,
178
+ SessionData: pSessionState ? pSessionState.SessionData : {}
179
+ });
180
+ return fNext();
181
+ });
182
+ });
183
+
184
+ // GET /clone/session/check
185
+ pOratorServiceServer.get(`${tmpPrefix}/session/check`,
186
+ (pRequest, pResponse, fNext) =>
187
+ {
188
+ if (!tmpCloneState.SessionConfigured)
189
+ {
190
+ pResponse.send(200,
191
+ {
192
+ Configured: false,
193
+ Authenticated: false
194
+ });
195
+ return fNext();
196
+ }
197
+
198
+ tmpPict.SessionManager.checkSession('Remote',
199
+ (pCheckError, pAuthenticated, pCheckData) =>
200
+ {
201
+ tmpCloneState.SessionAuthenticated = pAuthenticated;
202
+
203
+ pResponse.send(200,
204
+ {
205
+ Configured: tmpCloneState.SessionConfigured,
206
+ Authenticated: pAuthenticated,
207
+ ServerURL: tmpCloneState.RemoteServerURL,
208
+ CheckData: pCheckData || {}
209
+ });
210
+ return fNext();
211
+ });
212
+ });
213
+
214
+ // POST /clone/session/deauthenticate
215
+ pOratorServiceServer.post(`${tmpPrefix}/session/deauthenticate`,
216
+ (pRequest, pResponse, fNext) =>
217
+ {
218
+ if (tmpCloneState.SessionConfigured)
219
+ {
220
+ tmpPict.SessionManager.deauthenticate('Remote');
221
+ }
222
+ tmpCloneState.SessionAuthenticated = false;
223
+
224
+ tmpFable.log.info('Data Cloner: Session deauthenticated.');
225
+
226
+ pResponse.send(200, { Success: true, Authenticated: false });
227
+ return fNext();
228
+ });
229
+ };