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.
@@ -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.7",
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.61"
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.28",
43
- "meadow-connection-mysql": "^1.0.13",
44
- "meadow-connection-mssql": "^1.0.15",
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
+ });
@@ -34,5 +34,9 @@
34
34
  "PageSize": 100,
35
35
  "SyncEntityList": [],
36
36
  "SyncEntityOptions": {}
37
+ },
38
+ "SessionManager":
39
+ {
40
+ "Sessions": {}
37
41
  }
38
42
  }
@@ -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 with the source API
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 tmpServer = new MeadowIntegrationServer(
34
- {
35
- APIServerPort: tmpPort
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
- this._Orator.initialize(
44
- (pError) =>
45
- {
46
- if (pError)
47
+ // Authenticate SessionManager sessions before starting the server
48
+ let fStartServer = () =>
49
+ {
50
+ this._Orator.initialize(
51
+ (pError) =>
47
52
  {
48
- this._Fable.log.error(`Error initializing Orator: ${pError}`, pError);
49
- return tmpCallback(pError);
50
- }
53
+ if (pError)
54
+ {
55
+ this._Fable.log.error(`Error initializing Orator: ${pError}`, pError);
56
+ return tmpCallback(pError);
57
+ }
51
58
 
52
- // Register all endpoints
53
- libEndpoints.connectRoutes(this._Fable, this._Orator);
59
+ // Register all endpoints
60
+ libEndpoints.connectRoutes(this._Fable, this._Orator);
54
61
 
55
- this._Orator.startService(
56
- (pStartError) =>
57
- {
58
- if (pStartError)
62
+ this._Orator.startService(
63
+ (pStartError) =>
59
64
  {
60
- this._Fable.log.error(`Error starting Orator service: ${pStartError}`, pStartError);
61
- return tmpCallback(pStartError);
62
- }
63
-
64
- this._Fable.log.info(`Meadow Integration Server running on port ${this._Settings.APIServerPort}`);
65
- return tmpCallback();
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
- const agentOptions = { keepAlive: true };
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
- return this.Meadow.provider.getProvider().createTable(this.EntitySchema, (pCreateError) =>
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(fCallback);
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 < tmpDeletedCount; i += this.PageSize)
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 (!pRecord)
346
+ if (pRecord)
300
347
  {
301
- this.fable.log.warn(`No records found in local ${this.EntitySchema.TableName}.`);
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 < tmpSyncState.Server.RecordCount; i += this.PageSize)
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(`Syncing with ${tmpSyncState.URLPartials.length} requests for ${this.EntitySchema.TableName} with local max ID ${tmpSyncState.Local.MaxIDEntity} and server max ID ${tmpSyncState.Server.MaxIDEntity}; estimated ${tmpSyncState.EstimatedRecordCount} records to sync.`);
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(`Error getting URL Partial [${pURLPartial}]: ${pDownloadError}`, { Error: pDownloadError });
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
- this.fable.log.error(`Error reading record ${this.EntitySchema.TableName}: ${pReadError}`, { Error: pReadError, PassedRecord: tmpRecord });
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
- this.log.error(`Error creating record ${this.EntitySchema.TableName}: ${pCreateError}`, pCreateError);
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(`Error performing sync ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
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 (pSourceRecord[tmpColumn.Column] !== '')
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 < tmpDeletedCount; i += this.PageSize)
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
- this.log.info(`Entity ${this.EntitySchema.TableName} has UpdateDate column.`);
411
- break;
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
- tmpSyncState.EstimatedRequestCount = Math.ceil(tmpSyncState.Server.RecordCount / this.PageSize);
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(fSyncInitializationComplete);
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('Entity sync objects created!');
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
- this.MeadowSyncEntities[pEntityHash].sync(fCallback);
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)