meadow-integration 1.0.9 → 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 +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 +68 -3
- package/source/services/clone/Meadow-Service-Sync-Entity-Ongoing.js +73 -6
- 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.10",
|
|
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,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
|
|
|
@@ -197,8 +198,11 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
197
198
|
this.fable.log.info(`Found ${tmpDeletedCount} deleted records on server for ${this.EntitySchema.TableName}; syncing deletions...`);
|
|
198
199
|
|
|
199
200
|
// Generate paginated URLs for deleted records
|
|
201
|
+
let tmpDeleteCap = (this.MaxRecordsPerEntity > 0)
|
|
202
|
+
? Math.min(tmpDeletedCount, this.MaxRecordsPerEntity)
|
|
203
|
+
: tmpDeletedCount;
|
|
200
204
|
const tmpDeleteURLPartials = [];
|
|
201
|
-
for (let i = 0; i <
|
|
205
|
+
for (let i = 0; i < tmpDeleteCap; i += this.PageSize)
|
|
202
206
|
{
|
|
203
207
|
tmpDeleteURLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~Deleted~EQ~1~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
|
|
204
208
|
}
|
|
@@ -307,6 +311,19 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
307
311
|
Server: { MaxIDEntity: -1, RecordCount: 0 },
|
|
308
312
|
});
|
|
309
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
|
+
|
|
310
327
|
this.fable.Utility.waterfall(
|
|
311
328
|
[
|
|
312
329
|
(fStageComplete) =>
|
|
@@ -315,6 +332,10 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
315
332
|
const tmpQuery = this.Meadow.query;
|
|
316
333
|
tmpQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
|
|
317
334
|
tmpQuery.setCap(1);
|
|
335
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
336
|
+
{
|
|
337
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
338
|
+
}
|
|
318
339
|
this.Meadow.doRead(tmpQuery,
|
|
319
340
|
(pReadError, pQuery, pRecord) =>
|
|
320
341
|
{
|
|
@@ -333,6 +354,10 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
333
354
|
{
|
|
334
355
|
// Get the count from local database
|
|
335
356
|
const tmpQuery = this.Meadow.query;
|
|
357
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
358
|
+
{
|
|
359
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
360
|
+
}
|
|
336
361
|
this.Meadow.doCount(tmpQuery,
|
|
337
362
|
(pCountError, pQuery, pCount) =>
|
|
338
363
|
{
|
|
@@ -382,17 +407,27 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
382
407
|
{
|
|
383
408
|
tmpSyncState.EstimatedRecordCount = tmpSyncState.Server.RecordCount - tmpSyncState.Local.RecordCount;
|
|
384
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
|
+
|
|
385
420
|
this.operation.createProgressTracker(tmpSyncState.EstimatedRecordCount, `FullSync-${this.EntitySchema.TableName}`);
|
|
386
421
|
this.operation.printProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`);
|
|
387
422
|
|
|
388
423
|
// Generate paginated URL partials
|
|
389
424
|
tmpSyncState.URLPartials = [];
|
|
390
|
-
for (let i = 0; i <
|
|
425
|
+
for (let i = 0; i < tmpRecordCap; i += this.PageSize)
|
|
391
426
|
{
|
|
392
427
|
tmpSyncState.URLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~${this.DefaultIdentifier}~GT~${tmpSyncState.Local.MaxIDEntity}~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
|
|
393
428
|
}
|
|
394
429
|
|
|
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})`);
|
|
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}` : ''})`);
|
|
396
431
|
|
|
397
432
|
return fStageComplete();
|
|
398
433
|
},
|
|
@@ -429,6 +464,11 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
429
464
|
tmpQuery.addFilter(this.DefaultIdentifier, tmpRecord[this.DefaultIdentifier]);
|
|
430
465
|
}
|
|
431
466
|
|
|
467
|
+
if (!tmpSyncState.HasDeletedColumn)
|
|
468
|
+
{
|
|
469
|
+
tmpQuery.setDisableDeleteTracking(true);
|
|
470
|
+
}
|
|
471
|
+
|
|
432
472
|
this.Meadow.doRead(tmpQuery,
|
|
433
473
|
(pReadError, pQuery, pRecord) =>
|
|
434
474
|
{
|
|
@@ -456,6 +496,31 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
|
|
|
456
496
|
{
|
|
457
497
|
if (pCreateError)
|
|
458
498
|
{
|
|
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
|
+
}
|
|
459
524
|
tmpRecordsErrored++;
|
|
460
525
|
this.log.error(`${this.EntitySchema.TableName}: doCreate error for ID ${tmpRecord[this.DefaultIdentifier]}: ${pCreateError}`);
|
|
461
526
|
return fEntitySyncComplete();
|
|
@@ -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
|
|
|
@@ -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;
|