meadow-integration 1.0.9 → 1.0.11
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/examples/Example-011-Clone-With-SessionManager.js +291 -0
- package/package.json +4 -3
- package/source/Meadow-Integration-SessionManagerSetup.js +134 -0
- package/source/cli/Default-Meadow-Integration-Configuration.json +4 -0
- package/source/cli/commands/Meadow-Integration-Command-DataClone.js +32 -2
- package/source/cli/commands/Meadow-Integration-Command-Serve.js +9 -4
- package/source/restserver/Meadow-Integration-Server.js +48 -21
- package/source/services/clone/Meadow-Service-Sync-Entity-Initial.js +112 -9
- package/source/services/clone/Meadow-Service-Sync-Entity-Ongoing.js +119 -16
- package/source/services/clone/Meadow-Service-Sync.js +12 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Example 011: Clone with SessionManager
|
|
4
|
+
* ----------------------------------------
|
|
5
|
+
* Demonstrates using pict-sessionmanager to manage credentials for a
|
|
6
|
+
* data clone operation. The SessionManager handles authentication with
|
|
7
|
+
* the source API and automatically injects session credentials into
|
|
8
|
+
* all outbound REST requests during cloning.
|
|
9
|
+
*
|
|
10
|
+
* This example uses the programmatic API to set up SessionManager and
|
|
11
|
+
* run a clone, which is equivalent to what the CLI `data-clone` command
|
|
12
|
+
* does when SessionManager configuration is present in .meadow.config.json.
|
|
13
|
+
*
|
|
14
|
+
* Usage: node Example-011-Clone-With-SessionManager.js
|
|
15
|
+
*
|
|
16
|
+
* Prerequisites:
|
|
17
|
+
* - A running Meadow API server as the clone source
|
|
18
|
+
* - A destination database (MySQL or MSSQL)
|
|
19
|
+
* - A Meadow extended schema JSON file
|
|
20
|
+
*
|
|
21
|
+
* Configuration:
|
|
22
|
+
* This example shows both inline configuration and file-based configuration.
|
|
23
|
+
* In production, put your SessionManager config in .meadow.config.json.
|
|
24
|
+
*/
|
|
25
|
+
const libPict = require('pict');
|
|
26
|
+
const libPath = require('path');
|
|
27
|
+
|
|
28
|
+
const libMeadowConnectionManager = require('../source/services/clone/Meadow-Service-ConnectionManager.js');
|
|
29
|
+
const libMeadowCloneRestClient = require('../source/services/clone/Meadow-Service-RestClient.js');
|
|
30
|
+
const libMeadowSync = require('../source/services/clone/Meadow-Service-Sync.js');
|
|
31
|
+
const libSessionManagerSetup = require('../source/Meadow-Integration-SessionManagerSetup.js');
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Configuration
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
// Source API configuration
|
|
38
|
+
const tmpSourceConfig = {
|
|
39
|
+
ServerURL: 'https://myapp.example.com/1.0/',
|
|
40
|
+
// Note: UserID and Password are NOT set here because SessionManager
|
|
41
|
+
// handles authentication instead of the built-in mechanism.
|
|
42
|
+
UserID: false,
|
|
43
|
+
Password: false
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Destination database configuration
|
|
47
|
+
const tmpDestinationConfig = {
|
|
48
|
+
Provider: 'MySQL',
|
|
49
|
+
MySQL: {
|
|
50
|
+
server: '127.0.0.1',
|
|
51
|
+
port: 3306,
|
|
52
|
+
user: 'root',
|
|
53
|
+
password: '',
|
|
54
|
+
database: 'meadow_clone',
|
|
55
|
+
connectionLimit: 20
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Sync configuration
|
|
60
|
+
const tmpSyncConfig = {
|
|
61
|
+
DefaultSyncMode: 'Initial',
|
|
62
|
+
PageSize: 100,
|
|
63
|
+
SyncEntityList: [],
|
|
64
|
+
SyncEntityOptions: {}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// SessionManager configuration
|
|
68
|
+
// This is the key addition -- it defines how to authenticate with the
|
|
69
|
+
// source API and how credentials are injected into REST requests.
|
|
70
|
+
const tmpSessionManagerConfig = {
|
|
71
|
+
Sessions: {
|
|
72
|
+
// Each key is a session name. You can have multiple sessions
|
|
73
|
+
// for different APIs or security contexts.
|
|
74
|
+
SourceAPI: {
|
|
75
|
+
// Type can be 'Header', 'Cookie', or 'Both'
|
|
76
|
+
Type: 'Cookie',
|
|
77
|
+
|
|
78
|
+
// Authentication configuration
|
|
79
|
+
AuthenticationMethod: 'post',
|
|
80
|
+
AuthenticationURITemplate: 'https://myapp.example.com/1.0/Authenticate',
|
|
81
|
+
AuthenticationRequestBody: {
|
|
82
|
+
// Templates use Pict's {~D:Record.Key~} syntax.
|
|
83
|
+
// Record refers to the Credentials object.
|
|
84
|
+
UserName: '{~D:Record.UserName~}',
|
|
85
|
+
Password: '{~D:Record.Password~}'
|
|
86
|
+
},
|
|
87
|
+
AuthenticationRetryCount: 2,
|
|
88
|
+
AuthenticationRetryDebounce: 500,
|
|
89
|
+
|
|
90
|
+
// Session check configuration (optional, for verifying session validity)
|
|
91
|
+
CheckSessionURITemplate: 'https://myapp.example.com/1.0/CheckSession',
|
|
92
|
+
CheckSessionLoginMarkerType: 'boolean',
|
|
93
|
+
CheckSessionLoginMarker: 'LoggedIn',
|
|
94
|
+
|
|
95
|
+
// Credential injection -- how the session token gets into requests
|
|
96
|
+
// DomainMatch: requests to URLs containing this string get credentials
|
|
97
|
+
DomainMatch: 'myapp.example.com',
|
|
98
|
+
|
|
99
|
+
// For cookie-based auth, specify the cookie name and where in
|
|
100
|
+
// the authentication response to find the value
|
|
101
|
+
CookieName: 'UserSession',
|
|
102
|
+
CookieValueAddress: 'SessionID',
|
|
103
|
+
|
|
104
|
+
// Credentials to use for authentication
|
|
105
|
+
// In .meadow.config.json these would be in the file.
|
|
106
|
+
// You can also pass them at authenticate() time.
|
|
107
|
+
Credentials: {
|
|
108
|
+
UserName: 'admin',
|
|
109
|
+
Password: 'my-secret-password'
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Alternative: Header-based authentication
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// If your API uses header-based tokens instead of cookies, configure like this:
|
|
119
|
+
//
|
|
120
|
+
// const tmpSessionManagerConfigHeader = {
|
|
121
|
+
// Sessions: {
|
|
122
|
+
// SourceAPI: {
|
|
123
|
+
// Type: 'Header',
|
|
124
|
+
// AuthenticationMethod: 'get',
|
|
125
|
+
// AuthenticationURITemplate: 'https://myapp.example.com/1.0/Authenticate/{~D:Record.LoginID~}/{~D:Record.LoginPassword~}',
|
|
126
|
+
// DomainMatch: 'myapp.example.com',
|
|
127
|
+
// HeaderName: 'x-session-token',
|
|
128
|
+
// HeaderValueTemplate: '{~D:Record.Token~}',
|
|
129
|
+
// Credentials: {
|
|
130
|
+
// LoginID: 'admin',
|
|
131
|
+
// LoginPassword: 'my-secret-password'
|
|
132
|
+
// }
|
|
133
|
+
// }
|
|
134
|
+
// }
|
|
135
|
+
// };
|
|
136
|
+
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// Equivalent .meadow.config.json
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// When using the CLI, all of the above goes into .meadow.config.json:
|
|
141
|
+
//
|
|
142
|
+
// {
|
|
143
|
+
// "Source": {
|
|
144
|
+
// "ServerURL": "https://myapp.example.com/1.0/"
|
|
145
|
+
// },
|
|
146
|
+
// "Destination": {
|
|
147
|
+
// "Provider": "MySQL",
|
|
148
|
+
// "MySQL": {
|
|
149
|
+
// "server": "127.0.0.1",
|
|
150
|
+
// "port": 3306,
|
|
151
|
+
// "user": "root",
|
|
152
|
+
// "password": "",
|
|
153
|
+
// "database": "meadow_clone",
|
|
154
|
+
// "connectionLimit": 20
|
|
155
|
+
// }
|
|
156
|
+
// },
|
|
157
|
+
// "SchemaPath": "./schema/Model-Extended.json",
|
|
158
|
+
// "Sync": {
|
|
159
|
+
// "DefaultSyncMode": "Initial",
|
|
160
|
+
// "PageSize": 100,
|
|
161
|
+
// "SyncEntityList": [],
|
|
162
|
+
// "SyncEntityOptions": {}
|
|
163
|
+
// },
|
|
164
|
+
// "SessionManager": {
|
|
165
|
+
// "Sessions": {
|
|
166
|
+
// "SourceAPI": {
|
|
167
|
+
// "Type": "Cookie",
|
|
168
|
+
// "AuthenticationMethod": "post",
|
|
169
|
+
// "AuthenticationURITemplate": "https://myapp.example.com/1.0/Authenticate",
|
|
170
|
+
// "AuthenticationRequestBody": {
|
|
171
|
+
// "UserName": "{~D:Record.UserName~}",
|
|
172
|
+
// "Password": "{~D:Record.Password~}"
|
|
173
|
+
// },
|
|
174
|
+
// "CheckSessionURITemplate": "https://myapp.example.com/1.0/CheckSession",
|
|
175
|
+
// "CheckSessionLoginMarker": "LoggedIn",
|
|
176
|
+
// "DomainMatch": "myapp.example.com",
|
|
177
|
+
// "CookieName": "UserSession",
|
|
178
|
+
// "CookieValueAddress": "SessionID",
|
|
179
|
+
// "Credentials": {
|
|
180
|
+
// "UserName": "admin",
|
|
181
|
+
// "Password": "my-secret-password"
|
|
182
|
+
// }
|
|
183
|
+
// }
|
|
184
|
+
// }
|
|
185
|
+
// }
|
|
186
|
+
// }
|
|
187
|
+
//
|
|
188
|
+
// Then run: mdwint clone --schema_path ./schema/Model-Extended.json
|
|
189
|
+
|
|
190
|
+
// ============================================================================
|
|
191
|
+
// Clone Execution
|
|
192
|
+
// ============================================================================
|
|
193
|
+
|
|
194
|
+
console.log('=== Example 011: Clone with SessionManager ===\n');
|
|
195
|
+
|
|
196
|
+
// Create a Pict instance (needed for SessionManager's template engine)
|
|
197
|
+
let _Fable = new libPict({ LogLevel: 3 });
|
|
198
|
+
|
|
199
|
+
// Register clone services
|
|
200
|
+
_Fable.serviceManager.addServiceType('MeadowCloneRestClient', libMeadowCloneRestClient);
|
|
201
|
+
_Fable.serviceManager.instantiateServiceProvider('MeadowCloneRestClient', tmpSourceConfig);
|
|
202
|
+
|
|
203
|
+
_Fable.serviceManager.addServiceType('MeadowConnectionManager', libMeadowConnectionManager);
|
|
204
|
+
_Fable.serviceManager.instantiateServiceProvider('MeadowConnectionManager', tmpDestinationConfig);
|
|
205
|
+
|
|
206
|
+
_Fable.serviceManager.addServiceType('MeadowSync', libMeadowSync);
|
|
207
|
+
|
|
208
|
+
// Initialize SessionManager from config
|
|
209
|
+
let tmpSessionManager = libSessionManagerSetup.initializeSessionManager(_Fable, tmpSessionManagerConfig);
|
|
210
|
+
if (tmpSessionManager)
|
|
211
|
+
{
|
|
212
|
+
console.log('SessionManager initialized with sessions:', Object.keys(tmpSessionManager.sessions).join(', '));
|
|
213
|
+
|
|
214
|
+
// Connect SessionManager to the clone RestClient's internal HTTP client.
|
|
215
|
+
// This means every REST request made during cloning will automatically
|
|
216
|
+
// have session credentials injected.
|
|
217
|
+
libSessionManagerSetup.connectSessionManagerToRestClient(_Fable, _Fable.MeadowCloneRestClient.restClient);
|
|
218
|
+
console.log('SessionManager connected to clone RestClient.\n');
|
|
219
|
+
}
|
|
220
|
+
else
|
|
221
|
+
{
|
|
222
|
+
console.log('No SessionManager sessions configured.\n');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Run the clone workflow
|
|
226
|
+
_Fable.Utility.waterfall(
|
|
227
|
+
[
|
|
228
|
+
// Step 1: Authenticate SessionManager sessions
|
|
229
|
+
(fStageComplete) =>
|
|
230
|
+
{
|
|
231
|
+
if (!tmpSessionManager)
|
|
232
|
+
{
|
|
233
|
+
return fStageComplete();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.log('Step 1: Authenticating SessionManager sessions...');
|
|
237
|
+
libSessionManagerSetup.authenticateSessions(_Fable,
|
|
238
|
+
(pError) =>
|
|
239
|
+
{
|
|
240
|
+
if (pError)
|
|
241
|
+
{
|
|
242
|
+
console.error(` Authentication failed: ${pError.message}`);
|
|
243
|
+
console.error(' (This is expected if there is no actual server running.)');
|
|
244
|
+
return fStageComplete(pError);
|
|
245
|
+
}
|
|
246
|
+
console.log(' All sessions authenticated.\n');
|
|
247
|
+
return fStageComplete();
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
// Step 2: Connect to destination database
|
|
252
|
+
// (fStageComplete) =>
|
|
253
|
+
// {
|
|
254
|
+
// console.log('Step 2: Connecting to destination database...');
|
|
255
|
+
// _Fable.MeadowConnectionManager.connect(
|
|
256
|
+
// (pError, pConnectionPool) =>
|
|
257
|
+
// {
|
|
258
|
+
// if (pError)
|
|
259
|
+
// {
|
|
260
|
+
// console.error(` Database connection failed: ${pError.message}`);
|
|
261
|
+
// return fStageComplete(pError);
|
|
262
|
+
// }
|
|
263
|
+
// console.log(' Connected.\n');
|
|
264
|
+
// return fStageComplete(null, pConnectionPool);
|
|
265
|
+
// });
|
|
266
|
+
// },
|
|
267
|
+
|
|
268
|
+
// Step 3: Load schema and sync
|
|
269
|
+
// (pConnectionPool, fStageComplete) =>
|
|
270
|
+
// {
|
|
271
|
+
// const tmpSchemaPath = libPath.resolve('./schema/Model-Extended.json');
|
|
272
|
+
// console.log(`Step 3: Loading schema from ${tmpSchemaPath}...`);
|
|
273
|
+
// // ... load schema and run sync
|
|
274
|
+
// },
|
|
275
|
+
],
|
|
276
|
+
(pError) =>
|
|
277
|
+
{
|
|
278
|
+
if (pError)
|
|
279
|
+
{
|
|
280
|
+
console.error(`\nClone ended with error: ${pError.message}`);
|
|
281
|
+
console.log('\nNote: This example is designed to demonstrate the configuration');
|
|
282
|
+
console.log('pattern. To run a real clone, point the Source and Destination');
|
|
283
|
+
console.log('settings at real servers and uncomment the database/sync steps.');
|
|
284
|
+
}
|
|
285
|
+
else
|
|
286
|
+
{
|
|
287
|
+
console.log('\nClone complete.');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
console.log('\n=== Example Complete ===');
|
|
291
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meadow-integration",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "Meadow Data Integration",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mdwint": "source/cli/Meadow-Integration-CLI-Run.js"
|
|
@@ -40,10 +40,11 @@
|
|
|
40
40
|
"fable": "^3.1.63",
|
|
41
41
|
"fable-serviceproviderbase": "^3.0.19",
|
|
42
42
|
"meadow": "^2.0.30",
|
|
43
|
-
"meadow-connection-mysql": "^1.0.14",
|
|
44
43
|
"meadow-connection-mssql": "^1.0.16",
|
|
44
|
+
"meadow-connection-mysql": "^1.0.14",
|
|
45
45
|
"orator": "^6.0.4",
|
|
46
46
|
"orator-serviceserver-restify": "^2.0.9",
|
|
47
|
-
"pict-service-commandlineutility": "^1.0.19"
|
|
47
|
+
"pict-service-commandlineutility": "^1.0.19",
|
|
48
|
+
"pict-sessionmanager": "^1.0.2"
|
|
48
49
|
}
|
|
49
50
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meadow Integration - Session Manager Setup
|
|
3
|
+
*
|
|
4
|
+
* Utility to initialize pict-sessionmanager from configuration.
|
|
5
|
+
* Used by CLI, Console UI, and REST server entry points.
|
|
6
|
+
* Modules that use meadow-integration services directly should
|
|
7
|
+
* manage their own session lifecycle.
|
|
8
|
+
*
|
|
9
|
+
* @author Steven Velozo <steven@velozo.com>
|
|
10
|
+
*/
|
|
11
|
+
const libPictSessionManager = require('pict-sessionmanager');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Initialize a SessionManager on a fable/pict instance from a config object.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} pFable - A fable or pict instance
|
|
17
|
+
* @param {object} pSessionManagerConfig - The SessionManager configuration block
|
|
18
|
+
* @param {object} pSessionManagerConfig.Sessions - Map of session name to session configuration
|
|
19
|
+
* @returns {object|false} The instantiated SessionManager, or false if no sessions configured
|
|
20
|
+
*/
|
|
21
|
+
function initializeSessionManager(pFable, pSessionManagerConfig)
|
|
22
|
+
{
|
|
23
|
+
if (!pFable)
|
|
24
|
+
{
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (!pSessionManagerConfig || typeof(pSessionManagerConfig) !== 'object')
|
|
28
|
+
{
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (!pSessionManagerConfig.Sessions || typeof(pSessionManagerConfig.Sessions) !== 'object')
|
|
32
|
+
{
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let tmpSessionNames = Object.keys(pSessionManagerConfig.Sessions);
|
|
37
|
+
if (tmpSessionNames.length < 1)
|
|
38
|
+
{
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pFable.serviceManager.addServiceType('SessionManager', libPictSessionManager);
|
|
43
|
+
pFable.serviceManager.instantiateServiceProvider('SessionManager');
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < tmpSessionNames.length; i++)
|
|
46
|
+
{
|
|
47
|
+
let tmpSessionName = tmpSessionNames[i];
|
|
48
|
+
let tmpSessionConfig = pSessionManagerConfig.Sessions[tmpSessionName];
|
|
49
|
+
pFable.SessionManager.addSession(tmpSessionName, tmpSessionConfig);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return pFable.SessionManager;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Authenticate all configured sessions that have Credentials set.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} pFable - A fable or pict instance with SessionManager instantiated
|
|
59
|
+
* @param {function} fCallback - Callback (pError)
|
|
60
|
+
*/
|
|
61
|
+
function authenticateSessions(pFable, fCallback)
|
|
62
|
+
{
|
|
63
|
+
if (!pFable || !pFable.SessionManager)
|
|
64
|
+
{
|
|
65
|
+
return fCallback();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let tmpSessionNames = Object.keys(pFable.SessionManager.sessions);
|
|
69
|
+
if (tmpSessionNames.length < 1)
|
|
70
|
+
{
|
|
71
|
+
return fCallback();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
pFable.Utility.eachLimit(tmpSessionNames, 1,
|
|
75
|
+
(pSessionName, fSessionCallback) =>
|
|
76
|
+
{
|
|
77
|
+
let tmpSession = pFable.SessionManager.getSession(pSessionName);
|
|
78
|
+
let tmpCredentials = tmpSession.Configuration.Credentials;
|
|
79
|
+
|
|
80
|
+
if (!tmpCredentials || typeof(tmpCredentials) !== 'object' || Object.keys(tmpCredentials).length < 1)
|
|
81
|
+
{
|
|
82
|
+
pFable.log.info(`SessionManager setup: Session [${pSessionName}] has no credentials configured; skipping authentication.`);
|
|
83
|
+
return fSessionCallback();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pFable.log.info(`SessionManager setup: Authenticating session [${pSessionName}]...`);
|
|
87
|
+
pFable.SessionManager.authenticate(pSessionName, tmpCredentials,
|
|
88
|
+
(pError, pSessionState) =>
|
|
89
|
+
{
|
|
90
|
+
if (pError)
|
|
91
|
+
{
|
|
92
|
+
pFable.log.error(`SessionManager setup: Failed to authenticate session [${pSessionName}]: ${pError.message}`);
|
|
93
|
+
return fSessionCallback(pError);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
pFable.log.info(`SessionManager setup: Session [${pSessionName}] authenticated successfully.`);
|
|
97
|
+
return fSessionCallback();
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
(pError) =>
|
|
101
|
+
{
|
|
102
|
+
return fCallback(pError);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Connect the SessionManager to a RestClient so credentials are injected automatically.
|
|
108
|
+
*
|
|
109
|
+
* If no RestClient is provided, SessionManager will use the default
|
|
110
|
+
* RestClient on the fable/pict instance (instantiating it if needed).
|
|
111
|
+
*
|
|
112
|
+
* @param {object} pFable - A fable or pict instance with SessionManager instantiated
|
|
113
|
+
* @param {object} [pRestClient] - A fable RestClient instance to connect to (optional)
|
|
114
|
+
* @returns {boolean} True if connected
|
|
115
|
+
*/
|
|
116
|
+
function connectSessionManagerToRestClient(pFable, pRestClient)
|
|
117
|
+
{
|
|
118
|
+
if (!pFable || !pFable.SessionManager)
|
|
119
|
+
{
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// SessionManager.connectToRestClient handles null by using the default
|
|
124
|
+
// pict RestClient (instantiating it if necessary).
|
|
125
|
+
pFable.SessionManager.connectToRestClient(pRestClient);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = (
|
|
130
|
+
{
|
|
131
|
+
initializeSessionManager: initializeSessionManager,
|
|
132
|
+
authenticateSessions: authenticateSessions,
|
|
133
|
+
connectSessionManagerToRestClient: connectSessionManagerToRestClient
|
|
134
|
+
});
|
|
@@ -5,6 +5,7 @@ const libPath = require('path');
|
|
|
5
5
|
const libMeadowConnectionManager = require('../../services/clone/Meadow-Service-ConnectionManager.js');
|
|
6
6
|
const libMeadowCloneRestClient = require('../../services/clone/Meadow-Service-RestClient.js');
|
|
7
7
|
const libMeadowSync = require('../../services/clone/Meadow-Service-Sync.js');
|
|
8
|
+
const libSessionManagerSetup = require('../../Meadow-Integration-SessionManagerSetup.js');
|
|
8
9
|
|
|
9
10
|
class DataClone extends libCLICommandLineCommand
|
|
10
11
|
{
|
|
@@ -163,11 +164,40 @@ class DataClone extends libCLICommandLineCommand
|
|
|
163
164
|
|
|
164
165
|
this.fable.serviceManager.addServiceType('MeadowSync', libMeadowSync);
|
|
165
166
|
|
|
167
|
+
// Initialize SessionManager if configured
|
|
168
|
+
let tmpSessionManager = libSessionManagerSetup.initializeSessionManager(this.fable, tmpConfig.SessionManager);
|
|
169
|
+
if (tmpSessionManager)
|
|
170
|
+
{
|
|
171
|
+
// Connect SessionManager to the clone RestClient so credentials are auto-injected
|
|
172
|
+
libSessionManagerSetup.connectSessionManagerToRestClient(this.fable, this.fable.MeadowCloneRestClient.restClient);
|
|
173
|
+
}
|
|
174
|
+
|
|
166
175
|
this.fable.Utility.waterfall(
|
|
167
176
|
[
|
|
168
177
|
(fStageComplete) =>
|
|
169
178
|
{
|
|
170
|
-
// Authenticate
|
|
179
|
+
// Authenticate SessionManager sessions (if configured)
|
|
180
|
+
if (tmpSessionManager)
|
|
181
|
+
{
|
|
182
|
+
this.log.info('Authenticating SessionManager sessions...');
|
|
183
|
+
libSessionManagerSetup.authenticateSessions(this.fable,
|
|
184
|
+
(pError) =>
|
|
185
|
+
{
|
|
186
|
+
if (pError)
|
|
187
|
+
{
|
|
188
|
+
this.log.error('Error authenticating SessionManager sessions.', pError);
|
|
189
|
+
}
|
|
190
|
+
return fStageComplete();
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
else
|
|
194
|
+
{
|
|
195
|
+
return fStageComplete();
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
(fStageComplete) =>
|
|
199
|
+
{
|
|
200
|
+
// Authenticate with the source API using built-in credentials
|
|
171
201
|
if (tmpConfig.Source.UserID && tmpConfig.Source.Password)
|
|
172
202
|
{
|
|
173
203
|
this.log.info('Authenticating with source API...');
|
|
@@ -187,7 +217,7 @@ class DataClone extends libCLICommandLineCommand
|
|
|
187
217
|
}
|
|
188
218
|
else
|
|
189
219
|
{
|
|
190
|
-
this.log.info('No credentials configured; skipping authentication.');
|
|
220
|
+
this.log.info('No Source credentials configured; skipping built-in authentication.');
|
|
191
221
|
return fStageComplete();
|
|
192
222
|
}
|
|
193
223
|
},
|
|
@@ -30,10 +30,15 @@ class MeadowIntegrationCommandServe extends libCommandLineCommand
|
|
|
30
30
|
|
|
31
31
|
this.log.info(`Starting Meadow Integration REST server on port ${tmpPort}...`);
|
|
32
32
|
|
|
33
|
-
let
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
let tmpServerSettings = { APIServerPort: tmpPort };
|
|
34
|
+
|
|
35
|
+
// Pass SessionManager config from program configuration to the server
|
|
36
|
+
if (this.fable.ProgramConfiguration && this.fable.ProgramConfiguration.SessionManager)
|
|
37
|
+
{
|
|
38
|
+
tmpServerSettings.SessionManager = this.fable.ProgramConfiguration.SessionManager;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let tmpServer = new MeadowIntegrationServer(tmpServerSettings);
|
|
37
42
|
|
|
38
43
|
tmpServer.start(
|
|
39
44
|
(pError) =>
|
|
@@ -3,6 +3,7 @@ const libOrator = require('orator');
|
|
|
3
3
|
const libOratorServiceServerRestify = require('orator-serviceserver-restify');
|
|
4
4
|
|
|
5
5
|
const libEndpoints = require('./Meadow-Integration-Server-Endpoints.js');
|
|
6
|
+
const libSessionManagerSetup = require('../Meadow-Integration-SessionManagerSetup.js');
|
|
6
7
|
|
|
7
8
|
class MeadowIntegrationServer
|
|
8
9
|
{
|
|
@@ -34,37 +35,63 @@ class MeadowIntegrationServer
|
|
|
34
35
|
this._Fable.instantiateServiceProvider('CSVParser');
|
|
35
36
|
this._Fable.instantiateServiceProvider('FilePersistence');
|
|
36
37
|
this._Fable.instantiateServiceProvider('DataGeneration');
|
|
38
|
+
|
|
39
|
+
// Initialize SessionManager if configured
|
|
40
|
+
this._SessionManager = libSessionManagerSetup.initializeSessionManager(this._Fable, tmpSettings.SessionManager);
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
start(fCallback)
|
|
40
44
|
{
|
|
41
45
|
let tmpCallback = (typeof(fCallback) === 'function') ? fCallback : () => {};
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
// Authenticate SessionManager sessions before starting the server
|
|
48
|
+
let fStartServer = () =>
|
|
49
|
+
{
|
|
50
|
+
this._Orator.initialize(
|
|
51
|
+
(pError) =>
|
|
47
52
|
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
if (pError)
|
|
54
|
+
{
|
|
55
|
+
this._Fable.log.error(`Error initializing Orator: ${pError}`, pError);
|
|
56
|
+
return tmpCallback(pError);
|
|
57
|
+
}
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
// Register all endpoints
|
|
60
|
+
libEndpoints.connectRoutes(this._Fable, this._Orator);
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{
|
|
58
|
-
if (pStartError)
|
|
62
|
+
this._Orator.startService(
|
|
63
|
+
(pStartError) =>
|
|
59
64
|
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
if (pStartError)
|
|
66
|
+
{
|
|
67
|
+
this._Fable.log.error(`Error starting Orator service: ${pStartError}`, pStartError);
|
|
68
|
+
return tmpCallback(pStartError);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this._Fable.log.info(`Meadow Integration Server running on port ${this._Settings.APIServerPort}`);
|
|
72
|
+
return tmpCallback();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (this._SessionManager)
|
|
78
|
+
{
|
|
79
|
+
// Connect SessionManager to the default RestClient for outbound calls
|
|
80
|
+
libSessionManagerSetup.connectSessionManagerToRestClient(this._Fable);
|
|
81
|
+
libSessionManagerSetup.authenticateSessions(this._Fable,
|
|
82
|
+
(pAuthError) =>
|
|
83
|
+
{
|
|
84
|
+
if (pAuthError)
|
|
85
|
+
{
|
|
86
|
+
this._Fable.log.error(`Error authenticating SessionManager sessions: ${pAuthError.message}`);
|
|
87
|
+
}
|
|
88
|
+
fStartServer();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else
|
|
92
|
+
{
|
|
93
|
+
fStartServer();
|
|
94
|
+
}
|
|
68
95
|
}
|
|
69
96
|
|
|
70
97
|
stop(fCallback)
|
|
@@ -40,10 +40,13 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
40
40
|
this.DefaultIdentifier = this.EntitySchema.MeadowSchema.DefaultIdentifier;
|
|
41
41
|
this.PageSize = this.options.PageSize || 100;
|
|
42
42
|
this.SyncDeletedRecords = this.options.SyncDeletedRecords || false;
|
|
43
|
+
this.MaxRecordsPerEntity = this.options.MaxRecordsPerEntity || 0;
|
|
43
44
|
|
|
44
45
|
this.Meadow = false;
|
|
45
46
|
|
|
46
47
|
this.operation = new libMeadowOperation(this.fable);
|
|
48
|
+
|
|
49
|
+
this.skipSync = false;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
initialize(fCallback)
|
|
@@ -71,6 +74,33 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
71
74
|
|
|
72
75
|
return tmpProvider.createTable(this.EntitySchema, (pCreateError) =>
|
|
73
76
|
{
|
|
77
|
+
let fValidateAndCallback = (pPriorError) =>
|
|
78
|
+
{
|
|
79
|
+
// Validate local table schema with a lightweight read
|
|
80
|
+
const tmpValidationQuery = this.Meadow.query;
|
|
81
|
+
tmpValidationQuery.setCap(1);
|
|
82
|
+
tmpValidationQuery.setDisableDeleteTracking(true);
|
|
83
|
+
this.Meadow.doRead(tmpValidationQuery,
|
|
84
|
+
(pReadError) =>
|
|
85
|
+
{
|
|
86
|
+
if (pReadError)
|
|
87
|
+
{
|
|
88
|
+
let tmpErrorStr = (typeof(pReadError) === 'string') ? pReadError : JSON.stringify(pReadError);
|
|
89
|
+
// Only skip sync for schema-specific errors (invalid column/object name)
|
|
90
|
+
// Generic provider errors (e.g. prepared statement failures) should not block sync
|
|
91
|
+
if (tmpErrorStr.indexOf('Invalid column') > -1 || tmpErrorStr.indexOf('Invalid object') > -1 || tmpErrorStr.indexOf('no such column') > -1 || tmpErrorStr.indexOf('no such table') > -1)
|
|
92
|
+
{
|
|
93
|
+
this.log.warn(`${this.EntitySchema.TableName}: local table schema validation failed (${pReadError}); this entity will be skipped during sync.`);
|
|
94
|
+
this.skipSync = true;
|
|
95
|
+
}
|
|
96
|
+
else
|
|
97
|
+
{
|
|
98
|
+
this.log.warn(`${this.EntitySchema.TableName}: validation read returned error (${pReadError}); sync will proceed.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return fCallback(pPriorError);
|
|
102
|
+
});
|
|
103
|
+
};
|
|
74
104
|
if (pCreateError)
|
|
75
105
|
{
|
|
76
106
|
this.log.warn(`${this.EntitySchema.TableName}: createTable returned error: ${pCreateError}`);
|
|
@@ -82,13 +112,13 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
82
112
|
if (!tmpGUIDColumn && !tmpDeletedColumn)
|
|
83
113
|
{
|
|
84
114
|
this.log.info(`No GUID or Deleted columns for ${this.EntitySchema.TableName}; skipping index creation`);
|
|
85
|
-
return
|
|
115
|
+
return fValidateAndCallback(pCreateError);
|
|
86
116
|
}
|
|
87
117
|
|
|
88
118
|
if (!this.fable.MeadowConnectionManager || !this.fable.MeadowConnectionManager.ConnectionPool)
|
|
89
119
|
{
|
|
90
120
|
this.log.info(`No connection manager available; skipping index creation for ${this.EntitySchema.TableName}`);
|
|
91
|
-
return
|
|
121
|
+
return fValidateAndCallback(pCreateError);
|
|
92
122
|
}
|
|
93
123
|
|
|
94
124
|
let tmpAnticipate = this.fable.newAnticipate();
|
|
@@ -114,7 +144,7 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
114
144
|
{
|
|
115
145
|
this.log.warn(`${this.EntitySchema.TableName}: Index creation error: ${pIndexError}`);
|
|
116
146
|
}
|
|
117
|
-
return
|
|
147
|
+
return fValidateAndCallback(pIndexError || pCreateError);
|
|
118
148
|
});
|
|
119
149
|
});
|
|
120
150
|
}
|
|
@@ -197,8 +227,11 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
197
227
|
this.fable.log.info(`Found ${tmpDeletedCount} deleted records on server for ${this.EntitySchema.TableName}; syncing deletions...`);
|
|
198
228
|
|
|
199
229
|
// Generate paginated URLs for deleted records
|
|
230
|
+
let tmpDeleteCap = (this.MaxRecordsPerEntity > 0)
|
|
231
|
+
? Math.min(tmpDeletedCount, this.MaxRecordsPerEntity)
|
|
232
|
+
: tmpDeletedCount;
|
|
200
233
|
const tmpDeleteURLPartials = [];
|
|
201
|
-
for (let i = 0; i <
|
|
234
|
+
for (let i = 0; i < tmpDeleteCap; i += this.PageSize)
|
|
202
235
|
{
|
|
203
236
|
tmpDeleteURLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~Deleted~EQ~1~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
|
|
204
237
|
}
|
|
@@ -297,6 +330,12 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
297
330
|
|
|
298
331
|
sync(fCallback)
|
|
299
332
|
{
|
|
333
|
+
if (this.skipSync)
|
|
334
|
+
{
|
|
335
|
+
this.log.warn(`Skipping sync for ${this.EntitySchema.TableName} -- local table schema does not match expected schema.`);
|
|
336
|
+
return fCallback();
|
|
337
|
+
}
|
|
338
|
+
|
|
300
339
|
this.operation.createTimeStamp('EntityInitialSync');
|
|
301
340
|
|
|
302
341
|
this.log.info(`Syncing ${this.EntitySchema.TableName} (PageSize: ${this.PageSize}, SyncDeletedRecords: ${this.SyncDeletedRecords})`);
|
|
@@ -307,6 +346,19 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
307
346
|
Server: { MaxIDEntity: -1, RecordCount: 0 },
|
|
308
347
|
});
|
|
309
348
|
|
|
349
|
+
// Detect whether the table has a Deleted column
|
|
350
|
+
if (this.EntitySchema.MeadowSchema && Array.isArray(this.EntitySchema.MeadowSchema.Schema))
|
|
351
|
+
{
|
|
352
|
+
for (let i = 0; i < this.EntitySchema.MeadowSchema.Schema.length; i++)
|
|
353
|
+
{
|
|
354
|
+
const tmpColumn = this.EntitySchema.MeadowSchema.Schema[i];
|
|
355
|
+
if (tmpColumn.Type == 'Deleted' || tmpColumn.Column == 'Deleted')
|
|
356
|
+
{
|
|
357
|
+
tmpSyncState.HasDeletedColumn = true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
310
362
|
this.fable.Utility.waterfall(
|
|
311
363
|
[
|
|
312
364
|
(fStageComplete) =>
|
|
@@ -315,6 +367,10 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
315
367
|
const tmpQuery = this.Meadow.query;
|
|
316
368
|
tmpQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
|
|
317
369
|
tmpQuery.setCap(1);
|
|
370
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
371
|
+
{
|
|
372
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
373
|
+
}
|
|
318
374
|
this.Meadow.doRead(tmpQuery,
|
|
319
375
|
(pReadError, pQuery, pRecord) =>
|
|
320
376
|
{
|
|
@@ -333,6 +389,10 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
333
389
|
{
|
|
334
390
|
// Get the count from local database
|
|
335
391
|
const tmpQuery = this.Meadow.query;
|
|
392
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
393
|
+
{
|
|
394
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
395
|
+
}
|
|
336
396
|
this.Meadow.doCount(tmpQuery,
|
|
337
397
|
(pCountError, pQuery, pCount) =>
|
|
338
398
|
{
|
|
@@ -352,7 +412,8 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
352
412
|
{
|
|
353
413
|
if (pError)
|
|
354
414
|
{
|
|
355
|
-
|
|
415
|
+
this.fable.log.warn(`Could not get server max entity ID for ${this.EntitySchema.TableName} (${pError}); continuing sync.`);
|
|
416
|
+
return fStageComplete();
|
|
356
417
|
}
|
|
357
418
|
if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
|
|
358
419
|
{
|
|
@@ -369,7 +430,9 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
369
430
|
{
|
|
370
431
|
if (pError)
|
|
371
432
|
{
|
|
372
|
-
|
|
433
|
+
this.fable.log.warn(`Could not get server count for ${this.EntitySchema.TableName} (${pError}); estimating from max ID.`);
|
|
434
|
+
tmpSyncState.Server.RecordCount = tmpSyncState.Server.MaxIDEntity > 0 ? tmpSyncState.Server.MaxIDEntity : 0;
|
|
435
|
+
return fStageComplete();
|
|
373
436
|
}
|
|
374
437
|
if (pBody && pBody.hasOwnProperty('Count'))
|
|
375
438
|
{
|
|
@@ -382,17 +445,27 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
382
445
|
{
|
|
383
446
|
tmpSyncState.EstimatedRecordCount = tmpSyncState.Server.RecordCount - tmpSyncState.Local.RecordCount;
|
|
384
447
|
|
|
448
|
+
// Apply MaxRecordsPerEntity cap if configured
|
|
449
|
+
let tmpRecordCap = (this.MaxRecordsPerEntity > 0)
|
|
450
|
+
? Math.min(tmpSyncState.Server.RecordCount, this.MaxRecordsPerEntity)
|
|
451
|
+
: tmpSyncState.Server.RecordCount;
|
|
452
|
+
|
|
453
|
+
if (this.MaxRecordsPerEntity > 0 && tmpSyncState.EstimatedRecordCount > this.MaxRecordsPerEntity)
|
|
454
|
+
{
|
|
455
|
+
tmpSyncState.EstimatedRecordCount = this.MaxRecordsPerEntity;
|
|
456
|
+
}
|
|
457
|
+
|
|
385
458
|
this.operation.createProgressTracker(tmpSyncState.EstimatedRecordCount, `FullSync-${this.EntitySchema.TableName}`);
|
|
386
459
|
this.operation.printProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`);
|
|
387
460
|
|
|
388
461
|
// Generate paginated URL partials
|
|
389
462
|
tmpSyncState.URLPartials = [];
|
|
390
|
-
for (let i = 0; i <
|
|
463
|
+
for (let i = 0; i < tmpRecordCap; i += this.PageSize)
|
|
391
464
|
{
|
|
392
465
|
tmpSyncState.URLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~${this.DefaultIdentifier}~GT~${tmpSyncState.Local.MaxIDEntity}~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
|
|
393
466
|
}
|
|
394
467
|
|
|
395
|
-
this.fable.log.info(`${this.EntitySchema.TableName}: downloading ${tmpSyncState.URLPartials.length} pages (local: ${tmpSyncState.Local.RecordCount}/${tmpSyncState.Local.MaxIDEntity}, server: ${tmpSyncState.Server.RecordCount}/${tmpSyncState.Server.MaxIDEntity}, estimated new: ${tmpSyncState.EstimatedRecordCount})`);
|
|
468
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: downloading ${tmpSyncState.URLPartials.length} pages (local: ${tmpSyncState.Local.RecordCount}/${tmpSyncState.Local.MaxIDEntity}, server: ${tmpSyncState.Server.RecordCount}/${tmpSyncState.Server.MaxIDEntity}, estimated new: ${tmpSyncState.EstimatedRecordCount}${this.MaxRecordsPerEntity > 0 ? `, capped at ${this.MaxRecordsPerEntity}` : ''})`);
|
|
396
469
|
|
|
397
470
|
return fStageComplete();
|
|
398
471
|
},
|
|
@@ -416,7 +489,7 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
416
489
|
this.fable.log.error(`${this.EntitySchema.TableName}: page ${tmpPageIndex} download error: ${pDownloadError}`);
|
|
417
490
|
return fDownloadComplete();
|
|
418
491
|
}
|
|
419
|
-
if (pBody && pBody.length > 0)
|
|
492
|
+
if (pBody && Array.isArray(pBody) && pBody.length > 0)
|
|
420
493
|
{
|
|
421
494
|
this.fable.Utility.eachLimit(pBody, 5,
|
|
422
495
|
(pEntityRecord, fEntitySyncComplete) =>
|
|
@@ -429,6 +502,11 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
429
502
|
tmpQuery.addFilter(this.DefaultIdentifier, tmpRecord[this.DefaultIdentifier]);
|
|
430
503
|
}
|
|
431
504
|
|
|
505
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
506
|
+
{
|
|
507
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
508
|
+
}
|
|
509
|
+
|
|
432
510
|
this.Meadow.doRead(tmpQuery,
|
|
433
511
|
(pReadError, pQuery, pRecord) =>
|
|
434
512
|
{
|
|
@@ -456,6 +534,31 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
456
534
|
{
|
|
457
535
|
if (pCreateError)
|
|
458
536
|
{
|
|
537
|
+
let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
|
|
538
|
+
if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
|
|
539
|
+
{
|
|
540
|
+
// Duplicate key (likely GUID conflict) -- fall back to update
|
|
541
|
+
this.log.warn(`${this.EntitySchema.TableName}: duplicate key on create for ID ${tmpRecord[this.DefaultIdentifier]}; falling back to update.`);
|
|
542
|
+
const tmpUpdateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
543
|
+
tmpUpdateQuery.setDisableAutoIdentity(true);
|
|
544
|
+
tmpUpdateQuery.setDisableAutoDateStamp(true);
|
|
545
|
+
tmpUpdateQuery.setDisableAutoUserStamp(true);
|
|
546
|
+
tmpUpdateQuery.setDisableDeleteTracking(true);
|
|
547
|
+
this.Meadow.doUpdate(tmpUpdateQuery,
|
|
548
|
+
(pUpdateError) =>
|
|
549
|
+
{
|
|
550
|
+
if (pUpdateError)
|
|
551
|
+
{
|
|
552
|
+
tmpRecordsErrored++;
|
|
553
|
+
this.log.error(`${this.EntitySchema.TableName}: fallback update also failed for ID ${tmpRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
|
|
554
|
+
return fEntitySyncComplete();
|
|
555
|
+
}
|
|
556
|
+
tmpRecordsCreated++;
|
|
557
|
+
this.operation.incrementProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`, 1);
|
|
558
|
+
return fEntitySyncComplete();
|
|
559
|
+
});
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
459
562
|
tmpRecordsErrored++;
|
|
460
563
|
this.log.error(`${this.EntitySchema.TableName}: doCreate error for ID ${tmpRecord[this.DefaultIdentifier]}: ${pCreateError}`);
|
|
461
564
|
return fEntitySyncComplete();
|
|
@@ -40,10 +40,13 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
40
40
|
this.DefaultIdentifier = this.EntitySchema.MeadowSchema.DefaultIdentifier;
|
|
41
41
|
this.PageSize = this.options.PageSize || 100;
|
|
42
42
|
this.SyncDeletedRecords = this.options.SyncDeletedRecords || false;
|
|
43
|
+
this.MaxRecordsPerEntity = this.options.MaxRecordsPerEntity || 0;
|
|
43
44
|
|
|
44
45
|
this.Meadow = false;
|
|
45
46
|
|
|
46
47
|
this.operation = new libMeadowOperation(this.fable);
|
|
48
|
+
|
|
49
|
+
this.skipSync = false;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
initialize(fCallback)
|
|
@@ -59,19 +62,46 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
59
62
|
{
|
|
60
63
|
return this.Meadow.provider.getProvider().createTable(this.EntitySchema, (pCreateError) =>
|
|
61
64
|
{
|
|
65
|
+
let fValidateAndCallback = (pPriorError) =>
|
|
66
|
+
{
|
|
67
|
+
// Validate local table schema with a lightweight read
|
|
68
|
+
const tmpValidationQuery = this.Meadow.query;
|
|
69
|
+
tmpValidationQuery.setCap(1);
|
|
70
|
+
tmpValidationQuery.setDisableDeleteTracking(true);
|
|
71
|
+
this.Meadow.doRead(tmpValidationQuery,
|
|
72
|
+
(pReadError) =>
|
|
73
|
+
{
|
|
74
|
+
if (pReadError)
|
|
75
|
+
{
|
|
76
|
+
let tmpErrorStr = (typeof(pReadError) === 'string') ? pReadError : JSON.stringify(pReadError);
|
|
77
|
+
// Only skip sync for schema-specific errors (invalid column/object name)
|
|
78
|
+
// Generic provider errors (e.g. prepared statement failures) should not block sync
|
|
79
|
+
if (tmpErrorStr.indexOf('Invalid column') > -1 || tmpErrorStr.indexOf('Invalid object') > -1 || tmpErrorStr.indexOf('no such column') > -1 || tmpErrorStr.indexOf('no such table') > -1)
|
|
80
|
+
{
|
|
81
|
+
this.log.warn(`${this.EntitySchema.TableName}: local table schema validation failed (${pReadError}); this entity will be skipped during sync.`);
|
|
82
|
+
this.skipSync = true;
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
{
|
|
86
|
+
this.log.warn(`${this.EntitySchema.TableName}: validation read returned error (${pReadError}); sync will proceed.`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return fCallback(pPriorError);
|
|
90
|
+
});
|
|
91
|
+
};
|
|
62
92
|
const tmpGUIDColumn = this.EntitySchema.Columns.find((c) => c.DataType == 'GUID');
|
|
63
93
|
const tmpDeletedColumn = this.EntitySchema.Columns.find((c) => c.Column == 'Deleted');
|
|
64
94
|
|
|
65
95
|
if (!tmpGUIDColumn && !tmpDeletedColumn)
|
|
66
96
|
{
|
|
67
97
|
this.log.info(`No GUID or Deleted columns for ${this.EntitySchema.TableName}; skipping index creation`);
|
|
68
|
-
return
|
|
98
|
+
return fValidateAndCallback(pCreateError);
|
|
69
99
|
}
|
|
70
100
|
|
|
71
101
|
if (!this.fable.MeadowConnectionManager || !this.fable.MeadowConnectionManager.ConnectionPool)
|
|
72
102
|
{
|
|
73
103
|
this.log.info(`No connection manager available; skipping index creation for ${this.EntitySchema.TableName}`);
|
|
74
|
-
return
|
|
104
|
+
return fValidateAndCallback(pCreateError);
|
|
75
105
|
}
|
|
76
106
|
|
|
77
107
|
let tmpAnticipate = this.fable.newAnticipate();
|
|
@@ -91,7 +121,7 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
91
121
|
return this.fable.MeadowConnectionManager.createIndex(this.EntitySchema, tmpDeletedColumn, false, fNext);
|
|
92
122
|
});
|
|
93
123
|
}
|
|
94
|
-
tmpAnticipate.wait(
|
|
124
|
+
tmpAnticipate.wait((pIndexError) => { return fValidateAndCallback(pCreateError); });
|
|
95
125
|
});
|
|
96
126
|
}
|
|
97
127
|
return fCallback();
|
|
@@ -117,7 +147,14 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
117
147
|
}
|
|
118
148
|
break;
|
|
119
149
|
default:
|
|
120
|
-
if (
|
|
150
|
+
if (tmpColumn.DataType == 'DateTime')
|
|
151
|
+
{
|
|
152
|
+
if ((typeof(pSourceRecord[tmpColumn.Column]) == 'string') && (pSourceRecord[tmpColumn.Column].length > 0))
|
|
153
|
+
{
|
|
154
|
+
tmpRecordToCommit[tmpColumn.Column] = this.fable.Dates.dayJS.utc(pSourceRecord[tmpColumn.Column]).format('YYYY-MM-DD HH:mm:ss.SSS');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (pSourceRecord[tmpColumn.Column] !== '')
|
|
121
158
|
{
|
|
122
159
|
tmpRecordToCommit[tmpColumn.Column] = pSourceRecord[tmpColumn.Column];
|
|
123
160
|
}
|
|
@@ -165,8 +202,11 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
165
202
|
this.fable.log.info(`Found ${tmpDeletedCount} deleted records on server for ${this.EntitySchema.TableName}; syncing deletions...`);
|
|
166
203
|
|
|
167
204
|
// Generate paginated URLs for deleted records
|
|
205
|
+
let tmpDeleteCap = (this.MaxRecordsPerEntity > 0)
|
|
206
|
+
? Math.min(tmpDeletedCount, this.MaxRecordsPerEntity)
|
|
207
|
+
: tmpDeletedCount;
|
|
168
208
|
const tmpDeleteURLPartials = [];
|
|
169
|
-
for (let i = 0; i <
|
|
209
|
+
for (let i = 0; i < tmpDeleteCap; i += this.PageSize)
|
|
170
210
|
{
|
|
171
211
|
tmpDeleteURLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~Deleted~EQ~1~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
|
|
172
212
|
}
|
|
@@ -277,7 +317,7 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
277
317
|
this.fable.log.error(`Error getting URL Partial [${tmpURLPartial}]: ${pDownloadError}`, { Error: pDownloadError });
|
|
278
318
|
return fNext();
|
|
279
319
|
}
|
|
280
|
-
if (pBody && pBody.length > 0)
|
|
320
|
+
if (pBody && Array.isArray(pBody) && pBody.length > 0)
|
|
281
321
|
{
|
|
282
322
|
for (let i = 0; i < pBody.length; i++)
|
|
283
323
|
{
|
|
@@ -298,6 +338,11 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
298
338
|
tmpQuery.addFilter(this.DefaultIdentifier, tmpRecord[this.DefaultIdentifier]);
|
|
299
339
|
}
|
|
300
340
|
|
|
341
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
342
|
+
{
|
|
343
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
344
|
+
}
|
|
345
|
+
|
|
301
346
|
this.Meadow.doRead(tmpQuery,
|
|
302
347
|
(pReadError, pQuery, pRecord) =>
|
|
303
348
|
{
|
|
@@ -338,6 +383,29 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
338
383
|
{
|
|
339
384
|
if (pCreateError)
|
|
340
385
|
{
|
|
386
|
+
let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
|
|
387
|
+
if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
|
|
388
|
+
{
|
|
389
|
+
// Duplicate key (likely GUID conflict) -- fall back to update
|
|
390
|
+
this.log.warn(`Duplicate key on create for ${this.EntitySchema.TableName} ID ${tmpRecord[this.DefaultIdentifier]}; falling back to update.`);
|
|
391
|
+
const tmpFallbackQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
392
|
+
tmpFallbackQuery.setDisableAutoIdentity(true);
|
|
393
|
+
tmpFallbackQuery.setDisableAutoDateStamp(true);
|
|
394
|
+
tmpFallbackQuery.setDisableAutoUserStamp(true);
|
|
395
|
+
tmpFallbackQuery.setDisableDeleteTracking(true);
|
|
396
|
+
this.Meadow.doUpdate(tmpFallbackQuery,
|
|
397
|
+
(pUpdateError) =>
|
|
398
|
+
{
|
|
399
|
+
if (pUpdateError)
|
|
400
|
+
{
|
|
401
|
+
this.log.error(`Fallback update also failed for ${this.EntitySchema.TableName} ID ${tmpRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
|
|
402
|
+
return fNextEntityRecordSync();
|
|
403
|
+
}
|
|
404
|
+
this.operation.incrementProgressTrackerStatus(`UpdateSync-${this.EntitySchema.TableName}`, 1);
|
|
405
|
+
return fNextEntityRecordSync();
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
341
409
|
this.log.error(`Error creating record ${this.EntitySchema.TableName}: ${pCreateError}`, pCreateError);
|
|
342
410
|
return fNextEntityRecordSync();
|
|
343
411
|
}
|
|
@@ -381,6 +449,12 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
381
449
|
|
|
382
450
|
sync(fCallback)
|
|
383
451
|
{
|
|
452
|
+
if (this.skipSync)
|
|
453
|
+
{
|
|
454
|
+
this.log.warn(`Skipping sync for ${this.EntitySchema.TableName} -- local table schema does not match expected schema.`);
|
|
455
|
+
return fCallback();
|
|
456
|
+
}
|
|
457
|
+
|
|
384
458
|
this.operation.createTimeStamp('EntityOngoingSync');
|
|
385
459
|
|
|
386
460
|
let tmpAnticipate = this.fable.newAnticipate();
|
|
@@ -407,9 +481,16 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
407
481
|
{
|
|
408
482
|
tmpSyncState.Local.HasUpdateDate = true;
|
|
409
483
|
tmpSyncState.Server.HasUpdateDate = true;
|
|
410
|
-
this.log.info(`Entity ${this.EntitySchema.TableName} has UpdateDate column.`);
|
|
411
|
-
break;
|
|
412
484
|
}
|
|
485
|
+
if (tmpColumn.Type == 'Deleted' || tmpColumn.Column == 'Deleted')
|
|
486
|
+
{
|
|
487
|
+
tmpSyncState.HasDeletedColumn = true;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (tmpSyncState.Local.HasUpdateDate)
|
|
492
|
+
{
|
|
493
|
+
this.log.info(`Entity ${this.EntitySchema.TableName} has UpdateDate column.`);
|
|
413
494
|
}
|
|
414
495
|
|
|
415
496
|
this.log.info(`Syncing with UPDATE STRATEGY entity ${this.EntitySchema.TableName}...`);
|
|
@@ -421,6 +502,11 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
421
502
|
const tmpQuery = this.Meadow.query;
|
|
422
503
|
tmpQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
|
|
423
504
|
tmpQuery.setCap(1);
|
|
505
|
+
// Disable delete tracking if the table has no Deleted column
|
|
506
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
507
|
+
{
|
|
508
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
509
|
+
}
|
|
424
510
|
this.Meadow.doRead(tmpQuery,
|
|
425
511
|
(pReadError, pQuery, pRecord) =>
|
|
426
512
|
{
|
|
@@ -441,10 +527,19 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
441
527
|
},
|
|
442
528
|
(fStageComplete) =>
|
|
443
529
|
{
|
|
444
|
-
// Get the Max UpdateDate from local database
|
|
530
|
+
// Get the Max UpdateDate from local database — skip if table has no UpdateDate column
|
|
531
|
+
if (!tmpSyncState.Local.HasUpdateDate)
|
|
532
|
+
{
|
|
533
|
+
this.fable.log.info(`No UpdateDate column for ${this.EntitySchema.TableName}; skipping UpdateDate check.`);
|
|
534
|
+
return fStageComplete();
|
|
535
|
+
}
|
|
445
536
|
const tmpQuery = this.Meadow.query;
|
|
446
537
|
tmpQuery.setSort({ Column: 'UpdateDate', Direction: 'Descending' });
|
|
447
538
|
tmpQuery.setCap(1);
|
|
539
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
540
|
+
{
|
|
541
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
542
|
+
}
|
|
448
543
|
this.Meadow.doRead(tmpQuery,
|
|
449
544
|
(pReadError, pQuery, pRecord) =>
|
|
450
545
|
{
|
|
@@ -467,6 +562,10 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
467
562
|
{
|
|
468
563
|
// Get the count from local database
|
|
469
564
|
const tmpQuery = this.Meadow.query;
|
|
565
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
566
|
+
{
|
|
567
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
568
|
+
}
|
|
470
569
|
this.Meadow.doCount(tmpQuery,
|
|
471
570
|
(pCountError, pQuery, pCount) =>
|
|
472
571
|
{
|
|
@@ -487,8 +586,8 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
487
586
|
{
|
|
488
587
|
if (pError)
|
|
489
588
|
{
|
|
490
|
-
this.fable.log.
|
|
491
|
-
return fStageComplete(
|
|
589
|
+
this.fable.log.warn(`Could not get server max entity ID for ${this.EntitySchema.TableName} (${pError}); continuing sync.`);
|
|
590
|
+
return fStageComplete();
|
|
492
591
|
}
|
|
493
592
|
if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
|
|
494
593
|
{
|
|
@@ -510,8 +609,8 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
510
609
|
{
|
|
511
610
|
if (pError)
|
|
512
611
|
{
|
|
513
|
-
this.fable.log.
|
|
514
|
-
return fStageComplete(
|
|
612
|
+
this.fable.log.warn(`Could not get server max UpdateDate for ${this.EntitySchema.TableName} (${pError}); will sync by ID only.`);
|
|
613
|
+
return fStageComplete();
|
|
515
614
|
}
|
|
516
615
|
if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
|
|
517
616
|
{
|
|
@@ -533,8 +632,9 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
533
632
|
{
|
|
534
633
|
if (pError)
|
|
535
634
|
{
|
|
536
|
-
this.fable.log.
|
|
537
|
-
|
|
635
|
+
this.fable.log.warn(`Could not get server count for ${this.EntitySchema.TableName} (${pError}); estimating from max ID.`);
|
|
636
|
+
tmpSyncState.Server.RecordCount = tmpSyncState.Server.MaxIDEntity > 0 ? tmpSyncState.Server.MaxIDEntity : 0;
|
|
637
|
+
return fStageComplete();
|
|
538
638
|
}
|
|
539
639
|
if (pBody && pBody.hasOwnProperty('Count'))
|
|
540
640
|
{
|
|
@@ -550,7 +650,10 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
550
650
|
},
|
|
551
651
|
(fStageComplete) =>
|
|
552
652
|
{
|
|
553
|
-
|
|
653
|
+
let tmpTotalRecords = (this.MaxRecordsPerEntity > 0)
|
|
654
|
+
? Math.min(tmpSyncState.Server.RecordCount, this.MaxRecordsPerEntity)
|
|
655
|
+
: tmpSyncState.Server.RecordCount;
|
|
656
|
+
tmpSyncState.EstimatedRequestCount = Math.ceil(tmpTotalRecords / this.PageSize);
|
|
554
657
|
tmpSyncState.RequestsPerformed = 0;
|
|
555
658
|
tmpSyncState.LastRequestedID = 0;
|
|
556
659
|
|
|
@@ -54,6 +54,17 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
54
54
|
this.SyncDeletedRecords = !!this.options.SyncDeletedRecords;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// When > 0, limit sync to at most this many records per entity.
|
|
58
|
+
this.MaxRecordsPerEntity = 0;
|
|
59
|
+
if (this.fable.ProgramConfiguration.hasOwnProperty('MaxRecordsPerEntity'))
|
|
60
|
+
{
|
|
61
|
+
this.MaxRecordsPerEntity = parseInt(this.fable.ProgramConfiguration.MaxRecordsPerEntity, 10) || 0;
|
|
62
|
+
}
|
|
63
|
+
else if (this.options.hasOwnProperty('MaxRecordsPerEntity'))
|
|
64
|
+
{
|
|
65
|
+
this.MaxRecordsPerEntity = parseInt(this.options.MaxRecordsPerEntity, 10) || 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
this.MeadowSchema = false;
|
|
58
69
|
this.MeadowSchemaTableList = false;
|
|
59
70
|
|
|
@@ -89,6 +100,7 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
89
100
|
ConnectionPool: this.options.ConnectionPool,
|
|
90
101
|
PageSize: this.options.PageSize || 100,
|
|
91
102
|
SyncDeletedRecords: this.SyncDeletedRecords,
|
|
103
|
+
MaxRecordsPerEntity: this.MaxRecordsPerEntity,
|
|
92
104
|
};
|
|
93
105
|
|
|
94
106
|
let tmpSyncEntity;
|