meadow-integration 1.0.7 → 1.0.10
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 +7 -6
- 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-RestClient.js +14 -1
- package/source/services/clone/Meadow-Service-Sync-Entity-Initial.js +116 -30
- package/source/services/clone/Meadow-Service-Sync-Entity-Ongoing.js +73 -6
- package/source/services/clone/Meadow-Service-Sync.js +43 -3
|
@@ -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.10",
|
|
4
4
|
"description": "Meadow Data Integration",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mdwint": "source/cli/Meadow-Integration-CLI-Run.js"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"author": "steven velozo <steven@velozo.com>",
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"quackage": "^1.0.
|
|
19
|
+
"quackage": "^1.0.63"
|
|
20
20
|
},
|
|
21
21
|
"mocha": {
|
|
22
22
|
"diff": true,
|
|
@@ -39,11 +39,12 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"fable": "^3.1.63",
|
|
41
41
|
"fable-serviceproviderbase": "^3.0.19",
|
|
42
|
-
"meadow": "^2.0.
|
|
43
|
-
"meadow-connection-
|
|
44
|
-
"meadow-connection-
|
|
42
|
+
"meadow": "^2.0.30",
|
|
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)
|
|
@@ -6,6 +6,14 @@ const defaultRestClientOptions = (
|
|
|
6
6
|
{
|
|
7
7
|
DownloadBatchSize: 100,
|
|
8
8
|
|
|
9
|
+
// Request timeout in milliseconds for normal remote API calls.
|
|
10
|
+
// Default: 60 seconds.
|
|
11
|
+
RequestTimeout: 60000,
|
|
12
|
+
|
|
13
|
+
// Request timeout in milliseconds for MAX(column) queries,
|
|
14
|
+
// which can be very slow on large tables. Default: 5 minutes.
|
|
15
|
+
MaxRequestTimeout: 300000,
|
|
16
|
+
|
|
9
17
|
ServerURL: 'https://localhost:8080/1.0/',
|
|
10
18
|
UserID: false,
|
|
11
19
|
Password: false,
|
|
@@ -36,7 +44,12 @@ class MeadowCloneRestClient extends libFableServiceProviderBase
|
|
|
36
44
|
this.restClient = this.fable.serviceManager.instantiateServiceProvider('RestClient', {}, 'MeadowCloneRestClient-RestClient');
|
|
37
45
|
this.cache = {};
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
this.requestTimeout = this.options.RequestTimeout;
|
|
48
|
+
this.maxRequestTimeout = this.options.MaxRequestTimeout;
|
|
49
|
+
|
|
50
|
+
// Use the longer of the two timeouts for the agent's socket timeout
|
|
51
|
+
// so that MAX queries don't get killed at the socket level.
|
|
52
|
+
const agentOptions = { keepAlive: true, timeout: Math.max(this.requestTimeout, this.maxRequestTimeout) };
|
|
40
53
|
|
|
41
54
|
if (this.serverURL && this.serverURL.startsWith('http:'))
|
|
42
55
|
{
|
|
@@ -40,6 +40,7 @@ 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
|
|
|
@@ -53,12 +54,29 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
53
54
|
this.Meadow = this.fable.Meadow.loadFromPackageObject(this.EntitySchema.MeadowSchema);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
this.log.info(`Sync for ${this.EntitySchema.TableName} creating table if it doesn't exist...`);
|
|
57
|
-
|
|
58
57
|
if (this.Meadow && this.Meadow.provider)
|
|
59
58
|
{
|
|
60
|
-
|
|
59
|
+
let tmpProvider = this.Meadow.provider.getProvider();
|
|
60
|
+
|
|
61
|
+
if (!tmpProvider)
|
|
62
|
+
{
|
|
63
|
+
this.log.error(`No provider returned by getProvider() for ${this.EntitySchema.TableName}`);
|
|
64
|
+
return fCallback(new Error(`No provider returned by getProvider() for ${this.EntitySchema.TableName}`));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!tmpProvider.createTable)
|
|
61
68
|
{
|
|
69
|
+
this.log.error(`Provider for ${this.EntitySchema.TableName} has no createTable method.`);
|
|
70
|
+
return fCallback(new Error(`Provider for ${this.EntitySchema.TableName} has no createTable method`));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return tmpProvider.createTable(this.EntitySchema, (pCreateError) =>
|
|
74
|
+
{
|
|
75
|
+
if (pCreateError)
|
|
76
|
+
{
|
|
77
|
+
this.log.warn(`${this.EntitySchema.TableName}: createTable returned error: ${pCreateError}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
62
80
|
const tmpGUIDColumn = this.EntitySchema.Columns.find((c) => c.DataType == 'GUID');
|
|
63
81
|
const tmpDeletedColumn = this.EntitySchema.Columns.find((c) => c.Column == 'Deleted');
|
|
64
82
|
|
|
@@ -91,9 +109,17 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
91
109
|
return this.fable.MeadowConnectionManager.createIndex(this.EntitySchema, tmpDeletedColumn, false, fNext);
|
|
92
110
|
});
|
|
93
111
|
}
|
|
94
|
-
tmpAnticipate.wait(
|
|
112
|
+
tmpAnticipate.wait((pIndexError) =>
|
|
113
|
+
{
|
|
114
|
+
if (pIndexError)
|
|
115
|
+
{
|
|
116
|
+
this.log.warn(`${this.EntitySchema.TableName}: Index creation error: ${pIndexError}`);
|
|
117
|
+
}
|
|
118
|
+
return fCallback(pIndexError || pCreateError);
|
|
119
|
+
});
|
|
95
120
|
});
|
|
96
121
|
}
|
|
122
|
+
|
|
97
123
|
return fCallback();
|
|
98
124
|
}
|
|
99
125
|
|
|
@@ -172,8 +198,11 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
172
198
|
this.fable.log.info(`Found ${tmpDeletedCount} deleted records on server for ${this.EntitySchema.TableName}; syncing deletions...`);
|
|
173
199
|
|
|
174
200
|
// Generate paginated URLs for deleted records
|
|
201
|
+
let tmpDeleteCap = (this.MaxRecordsPerEntity > 0)
|
|
202
|
+
? Math.min(tmpDeletedCount, this.MaxRecordsPerEntity)
|
|
203
|
+
: tmpDeletedCount;
|
|
175
204
|
const tmpDeleteURLPartials = [];
|
|
176
|
-
for (let i = 0; i <
|
|
205
|
+
for (let i = 0; i < tmpDeleteCap; i += this.PageSize)
|
|
177
206
|
{
|
|
178
207
|
tmpDeleteURLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~Deleted~EQ~1~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
|
|
179
208
|
}
|
|
@@ -274,12 +303,27 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
274
303
|
{
|
|
275
304
|
this.operation.createTimeStamp('EntityInitialSync');
|
|
276
305
|
|
|
306
|
+
this.log.info(`Syncing ${this.EntitySchema.TableName} (PageSize: ${this.PageSize}, SyncDeletedRecords: ${this.SyncDeletedRecords})`);
|
|
307
|
+
|
|
277
308
|
const tmpSyncState = (
|
|
278
309
|
{
|
|
279
310
|
Local: { MaxIDEntity: -1, RecordCount: 0 },
|
|
280
311
|
Server: { MaxIDEntity: -1, RecordCount: 0 },
|
|
281
312
|
});
|
|
282
313
|
|
|
314
|
+
// Detect whether the table has a Deleted column
|
|
315
|
+
if (this.EntitySchema.MeadowSchema && Array.isArray(this.EntitySchema.MeadowSchema.Schema))
|
|
316
|
+
{
|
|
317
|
+
for (let i = 0; i < this.EntitySchema.MeadowSchema.Schema.length; i++)
|
|
318
|
+
{
|
|
319
|
+
const tmpColumn = this.EntitySchema.MeadowSchema.Schema[i];
|
|
320
|
+
if (tmpColumn.Type == 'Deleted' || tmpColumn.Column == 'Deleted')
|
|
321
|
+
{
|
|
322
|
+
tmpSyncState.HasDeletedColumn = true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
283
327
|
this.fable.Utility.waterfall(
|
|
284
328
|
[
|
|
285
329
|
(fStageComplete) =>
|
|
@@ -288,21 +332,21 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
288
332
|
const tmpQuery = this.Meadow.query;
|
|
289
333
|
tmpQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
|
|
290
334
|
tmpQuery.setCap(1);
|
|
335
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
336
|
+
{
|
|
337
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
338
|
+
}
|
|
291
339
|
this.Meadow.doRead(tmpQuery,
|
|
292
340
|
(pReadError, pQuery, pRecord) =>
|
|
293
341
|
{
|
|
294
342
|
if (pReadError)
|
|
295
343
|
{
|
|
296
|
-
this.fable.log.error(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`, { Error: pReadError });
|
|
297
344
|
return fStageComplete(`Error reading local max entity ID ${this.EntitySchema.TableName}: ${pReadError}`);
|
|
298
345
|
}
|
|
299
|
-
if (
|
|
346
|
+
if (pRecord)
|
|
300
347
|
{
|
|
301
|
-
|
|
302
|
-
return fStageComplete();
|
|
348
|
+
tmpSyncState.Local.MaxIDEntity = pRecord[this.DefaultIdentifier];
|
|
303
349
|
}
|
|
304
|
-
this.fable.log.info(`Found local max entity ID ${this.EntitySchema.TableName}: ${pRecord[this.DefaultIdentifier]}`);
|
|
305
|
-
tmpSyncState.Local.MaxIDEntity = pRecord[this.DefaultIdentifier];
|
|
306
350
|
return fStageComplete();
|
|
307
351
|
});
|
|
308
352
|
},
|
|
@@ -310,12 +354,15 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
310
354
|
{
|
|
311
355
|
// Get the count from local database
|
|
312
356
|
const tmpQuery = this.Meadow.query;
|
|
357
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
358
|
+
{
|
|
359
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
360
|
+
}
|
|
313
361
|
this.Meadow.doCount(tmpQuery,
|
|
314
362
|
(pCountError, pQuery, pCount) =>
|
|
315
363
|
{
|
|
316
364
|
if (pCountError)
|
|
317
365
|
{
|
|
318
|
-
this.fable.log.error(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`, { Error: pCountError });
|
|
319
366
|
return fStageComplete(`Error getting local count of ${this.EntitySchema.TableName}: ${pCountError}`);
|
|
320
367
|
}
|
|
321
368
|
tmpSyncState.Local.RecordCount = pCount;
|
|
@@ -330,18 +377,12 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
330
377
|
{
|
|
331
378
|
if (pError)
|
|
332
379
|
{
|
|
333
|
-
this.fable.log.error(`Error getting server max entity ID ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
|
|
334
380
|
return fStageComplete(`Error getting server max entity ID ${this.EntitySchema.TableName}: ${pError}`);
|
|
335
381
|
}
|
|
336
382
|
if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
|
|
337
383
|
{
|
|
338
|
-
this.fable.log.info(`Found server max entity ID ${this.EntitySchema.TableName}: ${pBody[this.DefaultIdentifier]}`);
|
|
339
384
|
tmpSyncState.Server.MaxIDEntity = pBody[this.DefaultIdentifier];
|
|
340
385
|
}
|
|
341
|
-
else
|
|
342
|
-
{
|
|
343
|
-
this.fable.log.warn(`No records found in server for max entity ID of ${this.EntitySchema.TableName}.`);
|
|
344
|
-
}
|
|
345
386
|
return fStageComplete();
|
|
346
387
|
});
|
|
347
388
|
},
|
|
@@ -353,18 +394,12 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
353
394
|
{
|
|
354
395
|
if (pError)
|
|
355
396
|
{
|
|
356
|
-
this.fable.log.error(`Error getting server count for ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
|
|
357
397
|
return fStageComplete(`Error getting server count for ${this.EntitySchema.TableName}: ${pError}`);
|
|
358
398
|
}
|
|
359
399
|
if (pBody && pBody.hasOwnProperty('Count'))
|
|
360
400
|
{
|
|
361
|
-
this.fable.log.info(`Found server count for ${this.EntitySchema.TableName}: ${pBody.Count}`);
|
|
362
401
|
tmpSyncState.Server.RecordCount = pBody.Count;
|
|
363
402
|
}
|
|
364
|
-
else
|
|
365
|
-
{
|
|
366
|
-
this.fable.log.warn(`No records found in server based on count for ${this.EntitySchema.TableName}.`);
|
|
367
|
-
}
|
|
368
403
|
return fStageComplete();
|
|
369
404
|
});
|
|
370
405
|
},
|
|
@@ -372,31 +407,48 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
372
407
|
{
|
|
373
408
|
tmpSyncState.EstimatedRecordCount = tmpSyncState.Server.RecordCount - tmpSyncState.Local.RecordCount;
|
|
374
409
|
|
|
410
|
+
// Apply MaxRecordsPerEntity cap if configured
|
|
411
|
+
let tmpRecordCap = (this.MaxRecordsPerEntity > 0)
|
|
412
|
+
? Math.min(tmpSyncState.Server.RecordCount, this.MaxRecordsPerEntity)
|
|
413
|
+
: tmpSyncState.Server.RecordCount;
|
|
414
|
+
|
|
415
|
+
if (this.MaxRecordsPerEntity > 0 && tmpSyncState.EstimatedRecordCount > this.MaxRecordsPerEntity)
|
|
416
|
+
{
|
|
417
|
+
tmpSyncState.EstimatedRecordCount = this.MaxRecordsPerEntity;
|
|
418
|
+
}
|
|
419
|
+
|
|
375
420
|
this.operation.createProgressTracker(tmpSyncState.EstimatedRecordCount, `FullSync-${this.EntitySchema.TableName}`);
|
|
376
421
|
this.operation.printProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`);
|
|
377
422
|
|
|
378
423
|
// Generate paginated URL partials
|
|
379
424
|
tmpSyncState.URLPartials = [];
|
|
380
|
-
for (let i = 0; i <
|
|
425
|
+
for (let i = 0; i < tmpRecordCap; i += this.PageSize)
|
|
381
426
|
{
|
|
382
427
|
tmpSyncState.URLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~${this.DefaultIdentifier}~GT~${tmpSyncState.Local.MaxIDEntity}~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
|
|
383
428
|
}
|
|
384
429
|
|
|
385
|
-
this.fable.log.info(
|
|
430
|
+
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}` : ''})`);
|
|
386
431
|
|
|
387
432
|
return fStageComplete();
|
|
388
433
|
},
|
|
389
434
|
(fStageComplete) =>
|
|
390
435
|
{
|
|
436
|
+
let tmpPageIndex = 0;
|
|
437
|
+
let tmpRecordsCreated = 0;
|
|
438
|
+
let tmpRecordsSkipped = 0;
|
|
439
|
+
let tmpRecordsErrored = 0;
|
|
440
|
+
|
|
391
441
|
this.fable.Utility.eachLimit(tmpSyncState.URLPartials, 1,
|
|
392
442
|
(pURLPartial, fDownloadComplete) =>
|
|
393
443
|
{
|
|
444
|
+
tmpPageIndex++;
|
|
445
|
+
|
|
394
446
|
this.fable.MeadowCloneRestClient.getJSON(pURLPartial,
|
|
395
447
|
(pDownloadError, pResponse, pBody) =>
|
|
396
448
|
{
|
|
397
449
|
if (pDownloadError)
|
|
398
450
|
{
|
|
399
|
-
this.fable.log.error(
|
|
451
|
+
this.fable.log.error(`${this.EntitySchema.TableName}: page ${tmpPageIndex} download error: ${pDownloadError}`);
|
|
400
452
|
return fDownloadComplete();
|
|
401
453
|
}
|
|
402
454
|
if (pBody && pBody.length > 0)
|
|
@@ -412,12 +464,17 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
412
464
|
tmpQuery.addFilter(this.DefaultIdentifier, tmpRecord[this.DefaultIdentifier]);
|
|
413
465
|
}
|
|
414
466
|
|
|
467
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
468
|
+
{
|
|
469
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
470
|
+
}
|
|
471
|
+
|
|
415
472
|
this.Meadow.doRead(tmpQuery,
|
|
416
473
|
(pReadError, pQuery, pRecord) =>
|
|
417
474
|
{
|
|
418
475
|
if (pReadError)
|
|
419
476
|
{
|
|
420
|
-
|
|
477
|
+
tmpRecordsErrored++;
|
|
421
478
|
return fEntitySyncComplete();
|
|
422
479
|
}
|
|
423
480
|
if (!pRecord)
|
|
@@ -439,15 +496,43 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
439
496
|
{
|
|
440
497
|
if (pCreateError)
|
|
441
498
|
{
|
|
442
|
-
|
|
499
|
+
let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
|
|
500
|
+
if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
|
|
501
|
+
{
|
|
502
|
+
// Duplicate key (likely GUID conflict) -- fall back to update
|
|
503
|
+
this.log.warn(`${this.EntitySchema.TableName}: duplicate key on create for ID ${tmpRecord[this.DefaultIdentifier]}; falling back to update.`);
|
|
504
|
+
const tmpUpdateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
505
|
+
tmpUpdateQuery.setDisableAutoIdentity(true);
|
|
506
|
+
tmpUpdateQuery.setDisableAutoDateStamp(true);
|
|
507
|
+
tmpUpdateQuery.setDisableAutoUserStamp(true);
|
|
508
|
+
tmpUpdateQuery.setDisableDeleteTracking(true);
|
|
509
|
+
this.Meadow.doUpdate(tmpUpdateQuery,
|
|
510
|
+
(pUpdateError) =>
|
|
511
|
+
{
|
|
512
|
+
if (pUpdateError)
|
|
513
|
+
{
|
|
514
|
+
tmpRecordsErrored++;
|
|
515
|
+
this.log.error(`${this.EntitySchema.TableName}: fallback update also failed for ID ${tmpRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
|
|
516
|
+
return fEntitySyncComplete();
|
|
517
|
+
}
|
|
518
|
+
tmpRecordsCreated++;
|
|
519
|
+
this.operation.incrementProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`, 1);
|
|
520
|
+
return fEntitySyncComplete();
|
|
521
|
+
});
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
tmpRecordsErrored++;
|
|
525
|
+
this.log.error(`${this.EntitySchema.TableName}: doCreate error for ID ${tmpRecord[this.DefaultIdentifier]}: ${pCreateError}`);
|
|
443
526
|
return fEntitySyncComplete();
|
|
444
527
|
}
|
|
528
|
+
tmpRecordsCreated++;
|
|
445
529
|
this.operation.incrementProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`, 1);
|
|
446
530
|
return fEntitySyncComplete();
|
|
447
531
|
});
|
|
448
532
|
}
|
|
449
533
|
else
|
|
450
534
|
{
|
|
535
|
+
tmpRecordsSkipped++;
|
|
451
536
|
return fEntitySyncComplete();
|
|
452
537
|
}
|
|
453
538
|
});
|
|
@@ -474,6 +559,7 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
474
559
|
},
|
|
475
560
|
(pDownloadError) =>
|
|
476
561
|
{
|
|
562
|
+
this.fable.log.info(`${this.EntitySchema.TableName}: sync complete — created: ${tmpRecordsCreated}, skipped: ${tmpRecordsSkipped}, errors: ${tmpRecordsErrored}`);
|
|
477
563
|
if (pDownloadError)
|
|
478
564
|
{
|
|
479
565
|
this.fable.log.error(`Error returned URL Partial .. this may not be an error: ${pDownloadError}`);
|
|
@@ -486,7 +572,7 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
486
572
|
{
|
|
487
573
|
if (pError)
|
|
488
574
|
{
|
|
489
|
-
this.fable.log.error(
|
|
575
|
+
this.fable.log.error(`${this.EntitySchema.TableName}: sync error: ${pError}`);
|
|
490
576
|
}
|
|
491
577
|
|
|
492
578
|
if (this.SyncDeletedRecords)
|
|
@@ -40,6 +40,7 @@ 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
|
|
|
@@ -117,7 +118,14 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
117
118
|
}
|
|
118
119
|
break;
|
|
119
120
|
default:
|
|
120
|
-
if (
|
|
121
|
+
if (tmpColumn.DataType == 'DateTime')
|
|
122
|
+
{
|
|
123
|
+
if ((typeof(pSourceRecord[tmpColumn.Column]) == 'string') && (pSourceRecord[tmpColumn.Column].length > 0))
|
|
124
|
+
{
|
|
125
|
+
tmpRecordToCommit[tmpColumn.Column] = this.fable.Dates.dayJS.utc(pSourceRecord[tmpColumn.Column]).format('YYYY-MM-DD HH:mm:ss.SSS');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else if (pSourceRecord[tmpColumn.Column] !== '')
|
|
121
129
|
{
|
|
122
130
|
tmpRecordToCommit[tmpColumn.Column] = pSourceRecord[tmpColumn.Column];
|
|
123
131
|
}
|
|
@@ -165,8 +173,11 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
165
173
|
this.fable.log.info(`Found ${tmpDeletedCount} deleted records on server for ${this.EntitySchema.TableName}; syncing deletions...`);
|
|
166
174
|
|
|
167
175
|
// Generate paginated URLs for deleted records
|
|
176
|
+
let tmpDeleteCap = (this.MaxRecordsPerEntity > 0)
|
|
177
|
+
? Math.min(tmpDeletedCount, this.MaxRecordsPerEntity)
|
|
178
|
+
: tmpDeletedCount;
|
|
168
179
|
const tmpDeleteURLPartials = [];
|
|
169
|
-
for (let i = 0; i <
|
|
180
|
+
for (let i = 0; i < tmpDeleteCap; i += this.PageSize)
|
|
170
181
|
{
|
|
171
182
|
tmpDeleteURLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~Deleted~EQ~1~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
|
|
172
183
|
}
|
|
@@ -298,6 +309,11 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
298
309
|
tmpQuery.addFilter(this.DefaultIdentifier, tmpRecord[this.DefaultIdentifier]);
|
|
299
310
|
}
|
|
300
311
|
|
|
312
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
313
|
+
{
|
|
314
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
315
|
+
}
|
|
316
|
+
|
|
301
317
|
this.Meadow.doRead(tmpQuery,
|
|
302
318
|
(pReadError, pQuery, pRecord) =>
|
|
303
319
|
{
|
|
@@ -338,6 +354,29 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
338
354
|
{
|
|
339
355
|
if (pCreateError)
|
|
340
356
|
{
|
|
357
|
+
let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
|
|
358
|
+
if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
|
|
359
|
+
{
|
|
360
|
+
// Duplicate key (likely GUID conflict) -- fall back to update
|
|
361
|
+
this.log.warn(`Duplicate key on create for ${this.EntitySchema.TableName} ID ${tmpRecord[this.DefaultIdentifier]}; falling back to update.`);
|
|
362
|
+
const tmpFallbackQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
|
|
363
|
+
tmpFallbackQuery.setDisableAutoIdentity(true);
|
|
364
|
+
tmpFallbackQuery.setDisableAutoDateStamp(true);
|
|
365
|
+
tmpFallbackQuery.setDisableAutoUserStamp(true);
|
|
366
|
+
tmpFallbackQuery.setDisableDeleteTracking(true);
|
|
367
|
+
this.Meadow.doUpdate(tmpFallbackQuery,
|
|
368
|
+
(pUpdateError) =>
|
|
369
|
+
{
|
|
370
|
+
if (pUpdateError)
|
|
371
|
+
{
|
|
372
|
+
this.log.error(`Fallback update also failed for ${this.EntitySchema.TableName} ID ${tmpRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
|
|
373
|
+
return fNextEntityRecordSync();
|
|
374
|
+
}
|
|
375
|
+
this.operation.incrementProgressTrackerStatus(`UpdateSync-${this.EntitySchema.TableName}`, 1);
|
|
376
|
+
return fNextEntityRecordSync();
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
341
380
|
this.log.error(`Error creating record ${this.EntitySchema.TableName}: ${pCreateError}`, pCreateError);
|
|
342
381
|
return fNextEntityRecordSync();
|
|
343
382
|
}
|
|
@@ -407,11 +446,18 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
407
446
|
{
|
|
408
447
|
tmpSyncState.Local.HasUpdateDate = true;
|
|
409
448
|
tmpSyncState.Server.HasUpdateDate = true;
|
|
410
|
-
|
|
411
|
-
|
|
449
|
+
}
|
|
450
|
+
if (tmpColumn.Type == 'Deleted' || tmpColumn.Column == 'Deleted')
|
|
451
|
+
{
|
|
452
|
+
tmpSyncState.HasDeletedColumn = true;
|
|
412
453
|
}
|
|
413
454
|
}
|
|
414
455
|
|
|
456
|
+
if (tmpSyncState.Local.HasUpdateDate)
|
|
457
|
+
{
|
|
458
|
+
this.log.info(`Entity ${this.EntitySchema.TableName} has UpdateDate column.`);
|
|
459
|
+
}
|
|
460
|
+
|
|
415
461
|
this.log.info(`Syncing with UPDATE STRATEGY entity ${this.EntitySchema.TableName}...`);
|
|
416
462
|
return fStageComplete();
|
|
417
463
|
},
|
|
@@ -421,6 +467,11 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
421
467
|
const tmpQuery = this.Meadow.query;
|
|
422
468
|
tmpQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
|
|
423
469
|
tmpQuery.setCap(1);
|
|
470
|
+
// Disable delete tracking if the table has no Deleted column
|
|
471
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
472
|
+
{
|
|
473
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
474
|
+
}
|
|
424
475
|
this.Meadow.doRead(tmpQuery,
|
|
425
476
|
(pReadError, pQuery, pRecord) =>
|
|
426
477
|
{
|
|
@@ -441,10 +492,19 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
441
492
|
},
|
|
442
493
|
(fStageComplete) =>
|
|
443
494
|
{
|
|
444
|
-
// Get the Max UpdateDate from local database
|
|
495
|
+
// Get the Max UpdateDate from local database — skip if table has no UpdateDate column
|
|
496
|
+
if (!tmpSyncState.Local.HasUpdateDate)
|
|
497
|
+
{
|
|
498
|
+
this.fable.log.info(`No UpdateDate column for ${this.EntitySchema.TableName}; skipping UpdateDate check.`);
|
|
499
|
+
return fStageComplete();
|
|
500
|
+
}
|
|
445
501
|
const tmpQuery = this.Meadow.query;
|
|
446
502
|
tmpQuery.setSort({ Column: 'UpdateDate', Direction: 'Descending' });
|
|
447
503
|
tmpQuery.setCap(1);
|
|
504
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
505
|
+
{
|
|
506
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
507
|
+
}
|
|
448
508
|
this.Meadow.doRead(tmpQuery,
|
|
449
509
|
(pReadError, pQuery, pRecord) =>
|
|
450
510
|
{
|
|
@@ -467,6 +527,10 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
467
527
|
{
|
|
468
528
|
// Get the count from local database
|
|
469
529
|
const tmpQuery = this.Meadow.query;
|
|
530
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
531
|
+
{
|
|
532
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
533
|
+
}
|
|
470
534
|
this.Meadow.doCount(tmpQuery,
|
|
471
535
|
(pCountError, pQuery, pCount) =>
|
|
472
536
|
{
|
|
@@ -550,7 +614,10 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
|
|
|
550
614
|
},
|
|
551
615
|
(fStageComplete) =>
|
|
552
616
|
{
|
|
553
|
-
|
|
617
|
+
let tmpTotalRecords = (this.MaxRecordsPerEntity > 0)
|
|
618
|
+
? Math.min(tmpSyncState.Server.RecordCount, this.MaxRecordsPerEntity)
|
|
619
|
+
: tmpSyncState.Server.RecordCount;
|
|
620
|
+
tmpSyncState.EstimatedRequestCount = Math.ceil(tmpTotalRecords / this.PageSize);
|
|
554
621
|
tmpSyncState.RequestsPerformed = 0;
|
|
555
622
|
tmpSyncState.LastRequestedID = 0;
|
|
556
623
|
|
|
@@ -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
|
|
|
@@ -70,9 +81,16 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
70
81
|
this.meadowSchema = pSchema;
|
|
71
82
|
this.MeadowSchemaTableList = Object.keys(this.meadowSchema.Tables);
|
|
72
83
|
|
|
84
|
+
this.log.info(`Loading schema for ${this.MeadowSchemaTableList.length} tables (mode: ${this.SyncMode})`);
|
|
85
|
+
|
|
86
|
+
let tmpEntityIndex = 0;
|
|
87
|
+
let tmpErrorCount = 0;
|
|
88
|
+
let tmpSuccessCount = 0;
|
|
89
|
+
|
|
73
90
|
this.fable.Utility.eachLimit(this.MeadowSchemaTableList, 1,
|
|
74
91
|
(pEntitySchemaName, fSyncInitializationComplete) =>
|
|
75
92
|
{
|
|
93
|
+
tmpEntityIndex++;
|
|
76
94
|
const tmpEntitySchema = this.meadowSchema.Tables[pEntitySchemaName];
|
|
77
95
|
// If this is in the entity list or none is specified, create the sync entity object.
|
|
78
96
|
if (this.SyncEntityList.length < 1 || this.SyncEntityList.indexOf(tmpEntitySchema.TableName) > -1)
|
|
@@ -82,6 +100,7 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
82
100
|
ConnectionPool: this.options.ConnectionPool,
|
|
83
101
|
PageSize: this.options.PageSize || 100,
|
|
84
102
|
SyncDeletedRecords: this.SyncDeletedRecords,
|
|
103
|
+
MaxRecordsPerEntity: this.MaxRecordsPerEntity,
|
|
85
104
|
};
|
|
86
105
|
|
|
87
106
|
let tmpSyncEntity;
|
|
@@ -97,7 +116,20 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
97
116
|
|
|
98
117
|
this.MeadowSyncEntities[tmpEntitySchema.TableName] = tmpSyncEntity;
|
|
99
118
|
|
|
100
|
-
return tmpSyncEntity.initialize(
|
|
119
|
+
return tmpSyncEntity.initialize((pInitError) =>
|
|
120
|
+
{
|
|
121
|
+
if (pInitError)
|
|
122
|
+
{
|
|
123
|
+
tmpErrorCount++;
|
|
124
|
+
this.log.warn(`Failed to initialize ${tmpEntitySchema.TableName}: ${pInitError}`);
|
|
125
|
+
}
|
|
126
|
+
else
|
|
127
|
+
{
|
|
128
|
+
tmpSuccessCount++;
|
|
129
|
+
}
|
|
130
|
+
// Always continue to next entity regardless of individual errors
|
|
131
|
+
return fSyncInitializationComplete();
|
|
132
|
+
});
|
|
101
133
|
}
|
|
102
134
|
else
|
|
103
135
|
{
|
|
@@ -111,7 +143,7 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
111
143
|
this.log.error(`MeadowSync Error creating sync objects: ${pSyncInitializationError}`, pSyncInitializationError);
|
|
112
144
|
}
|
|
113
145
|
|
|
114
|
-
this.log.info(
|
|
146
|
+
this.log.info(`Entity sync objects created: ${tmpSuccessCount} succeeded, ${tmpErrorCount} failed.`);
|
|
115
147
|
|
|
116
148
|
if (this.SyncEntityList.length < 1)
|
|
117
149
|
{
|
|
@@ -129,7 +161,15 @@ class MeadowSync extends libFableServiceProviderBase
|
|
|
129
161
|
this.log.warn(`MeadowSync.syncEntity called for an entity that does not exist: ${pEntityHash}`);
|
|
130
162
|
return fCallback();
|
|
131
163
|
}
|
|
132
|
-
|
|
164
|
+
|
|
165
|
+
this.MeadowSyncEntities[pEntityHash].sync((pError) =>
|
|
166
|
+
{
|
|
167
|
+
if (pError)
|
|
168
|
+
{
|
|
169
|
+
this.log.error(`Sync failed for ${pEntityHash}: ${pError}`);
|
|
170
|
+
}
|
|
171
|
+
return fCallback(pError);
|
|
172
|
+
});
|
|
133
173
|
}
|
|
134
174
|
|
|
135
175
|
syncAll(fCallback)
|