meadow-integration 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.9",
3
+ "version": "1.0.11",
4
4
  "description": "Meadow Data Integration",
5
5
  "bin": {
6
6
  "mdwint": "source/cli/Meadow-Integration-CLI-Run.js"
@@ -40,10 +40,11 @@
40
40
  "fable": "^3.1.63",
41
41
  "fable-serviceproviderbase": "^3.0.19",
42
42
  "meadow": "^2.0.30",
43
- "meadow-connection-mysql": "^1.0.14",
44
43
  "meadow-connection-mssql": "^1.0.16",
44
+ "meadow-connection-mysql": "^1.0.14",
45
45
  "orator": "^6.0.4",
46
46
  "orator-serviceserver-restify": "^2.0.9",
47
- "pict-service-commandlineutility": "^1.0.19"
47
+ "pict-service-commandlineutility": "^1.0.19",
48
+ "pict-sessionmanager": "^1.0.2"
48
49
  }
49
50
  }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Meadow Integration - Session Manager Setup
3
+ *
4
+ * Utility to initialize pict-sessionmanager from configuration.
5
+ * Used by CLI, Console UI, and REST server entry points.
6
+ * Modules that use meadow-integration services directly should
7
+ * manage their own session lifecycle.
8
+ *
9
+ * @author Steven Velozo <steven@velozo.com>
10
+ */
11
+ const libPictSessionManager = require('pict-sessionmanager');
12
+
13
+ /**
14
+ * Initialize a SessionManager on a fable/pict instance from a config object.
15
+ *
16
+ * @param {object} pFable - A fable or pict instance
17
+ * @param {object} pSessionManagerConfig - The SessionManager configuration block
18
+ * @param {object} pSessionManagerConfig.Sessions - Map of session name to session configuration
19
+ * @returns {object|false} The instantiated SessionManager, or false if no sessions configured
20
+ */
21
+ function initializeSessionManager(pFable, pSessionManagerConfig)
22
+ {
23
+ if (!pFable)
24
+ {
25
+ return false;
26
+ }
27
+ if (!pSessionManagerConfig || typeof(pSessionManagerConfig) !== 'object')
28
+ {
29
+ return false;
30
+ }
31
+ if (!pSessionManagerConfig.Sessions || typeof(pSessionManagerConfig.Sessions) !== 'object')
32
+ {
33
+ return false;
34
+ }
35
+
36
+ let tmpSessionNames = Object.keys(pSessionManagerConfig.Sessions);
37
+ if (tmpSessionNames.length < 1)
38
+ {
39
+ return false;
40
+ }
41
+
42
+ pFable.serviceManager.addServiceType('SessionManager', libPictSessionManager);
43
+ pFable.serviceManager.instantiateServiceProvider('SessionManager');
44
+
45
+ for (let i = 0; i < tmpSessionNames.length; i++)
46
+ {
47
+ let tmpSessionName = tmpSessionNames[i];
48
+ let tmpSessionConfig = pSessionManagerConfig.Sessions[tmpSessionName];
49
+ pFable.SessionManager.addSession(tmpSessionName, tmpSessionConfig);
50
+ }
51
+
52
+ return pFable.SessionManager;
53
+ }
54
+
55
+ /**
56
+ * Authenticate all configured sessions that have Credentials set.
57
+ *
58
+ * @param {object} pFable - A fable or pict instance with SessionManager instantiated
59
+ * @param {function} fCallback - Callback (pError)
60
+ */
61
+ function authenticateSessions(pFable, fCallback)
62
+ {
63
+ if (!pFable || !pFable.SessionManager)
64
+ {
65
+ return fCallback();
66
+ }
67
+
68
+ let tmpSessionNames = Object.keys(pFable.SessionManager.sessions);
69
+ if (tmpSessionNames.length < 1)
70
+ {
71
+ return fCallback();
72
+ }
73
+
74
+ pFable.Utility.eachLimit(tmpSessionNames, 1,
75
+ (pSessionName, fSessionCallback) =>
76
+ {
77
+ let tmpSession = pFable.SessionManager.getSession(pSessionName);
78
+ let tmpCredentials = tmpSession.Configuration.Credentials;
79
+
80
+ if (!tmpCredentials || typeof(tmpCredentials) !== 'object' || Object.keys(tmpCredentials).length < 1)
81
+ {
82
+ pFable.log.info(`SessionManager setup: Session [${pSessionName}] has no credentials configured; skipping authentication.`);
83
+ return fSessionCallback();
84
+ }
85
+
86
+ pFable.log.info(`SessionManager setup: Authenticating session [${pSessionName}]...`);
87
+ pFable.SessionManager.authenticate(pSessionName, tmpCredentials,
88
+ (pError, pSessionState) =>
89
+ {
90
+ if (pError)
91
+ {
92
+ pFable.log.error(`SessionManager setup: Failed to authenticate session [${pSessionName}]: ${pError.message}`);
93
+ return fSessionCallback(pError);
94
+ }
95
+
96
+ pFable.log.info(`SessionManager setup: Session [${pSessionName}] authenticated successfully.`);
97
+ return fSessionCallback();
98
+ });
99
+ },
100
+ (pError) =>
101
+ {
102
+ return fCallback(pError);
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Connect the SessionManager to a RestClient so credentials are injected automatically.
108
+ *
109
+ * If no RestClient is provided, SessionManager will use the default
110
+ * RestClient on the fable/pict instance (instantiating it if needed).
111
+ *
112
+ * @param {object} pFable - A fable or pict instance with SessionManager instantiated
113
+ * @param {object} [pRestClient] - A fable RestClient instance to connect to (optional)
114
+ * @returns {boolean} True if connected
115
+ */
116
+ function connectSessionManagerToRestClient(pFable, pRestClient)
117
+ {
118
+ if (!pFable || !pFable.SessionManager)
119
+ {
120
+ return false;
121
+ }
122
+
123
+ // SessionManager.connectToRestClient handles null by using the default
124
+ // pict RestClient (instantiating it if necessary).
125
+ pFable.SessionManager.connectToRestClient(pRestClient);
126
+ return true;
127
+ }
128
+
129
+ module.exports = (
130
+ {
131
+ initializeSessionManager: initializeSessionManager,
132
+ authenticateSessions: authenticateSessions,
133
+ connectSessionManagerToRestClient: connectSessionManagerToRestClient
134
+ });
@@ -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)
@@ -40,10 +40,13 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
40
40
  this.DefaultIdentifier = this.EntitySchema.MeadowSchema.DefaultIdentifier;
41
41
  this.PageSize = this.options.PageSize || 100;
42
42
  this.SyncDeletedRecords = this.options.SyncDeletedRecords || false;
43
+ this.MaxRecordsPerEntity = this.options.MaxRecordsPerEntity || 0;
43
44
 
44
45
  this.Meadow = false;
45
46
 
46
47
  this.operation = new libMeadowOperation(this.fable);
48
+
49
+ this.skipSync = false;
47
50
  }
48
51
 
49
52
  initialize(fCallback)
@@ -71,6 +74,33 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
71
74
 
72
75
  return tmpProvider.createTable(this.EntitySchema, (pCreateError) =>
73
76
  {
77
+ let fValidateAndCallback = (pPriorError) =>
78
+ {
79
+ // Validate local table schema with a lightweight read
80
+ const tmpValidationQuery = this.Meadow.query;
81
+ tmpValidationQuery.setCap(1);
82
+ tmpValidationQuery.setDisableDeleteTracking(true);
83
+ this.Meadow.doRead(tmpValidationQuery,
84
+ (pReadError) =>
85
+ {
86
+ if (pReadError)
87
+ {
88
+ let tmpErrorStr = (typeof(pReadError) === 'string') ? pReadError : JSON.stringify(pReadError);
89
+ // Only skip sync for schema-specific errors (invalid column/object name)
90
+ // Generic provider errors (e.g. prepared statement failures) should not block sync
91
+ if (tmpErrorStr.indexOf('Invalid column') > -1 || tmpErrorStr.indexOf('Invalid object') > -1 || tmpErrorStr.indexOf('no such column') > -1 || tmpErrorStr.indexOf('no such table') > -1)
92
+ {
93
+ this.log.warn(`${this.EntitySchema.TableName}: local table schema validation failed (${pReadError}); this entity will be skipped during sync.`);
94
+ this.skipSync = true;
95
+ }
96
+ else
97
+ {
98
+ this.log.warn(`${this.EntitySchema.TableName}: validation read returned error (${pReadError}); sync will proceed.`);
99
+ }
100
+ }
101
+ return fCallback(pPriorError);
102
+ });
103
+ };
74
104
  if (pCreateError)
75
105
  {
76
106
  this.log.warn(`${this.EntitySchema.TableName}: createTable returned error: ${pCreateError}`);
@@ -82,13 +112,13 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
82
112
  if (!tmpGUIDColumn && !tmpDeletedColumn)
83
113
  {
84
114
  this.log.info(`No GUID or Deleted columns for ${this.EntitySchema.TableName}; skipping index creation`);
85
- return fCallback(pCreateError);
115
+ return fValidateAndCallback(pCreateError);
86
116
  }
87
117
 
88
118
  if (!this.fable.MeadowConnectionManager || !this.fable.MeadowConnectionManager.ConnectionPool)
89
119
  {
90
120
  this.log.info(`No connection manager available; skipping index creation for ${this.EntitySchema.TableName}`);
91
- return fCallback(pCreateError);
121
+ return fValidateAndCallback(pCreateError);
92
122
  }
93
123
 
94
124
  let tmpAnticipate = this.fable.newAnticipate();
@@ -114,7 +144,7 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
114
144
  {
115
145
  this.log.warn(`${this.EntitySchema.TableName}: Index creation error: ${pIndexError}`);
116
146
  }
117
- return fCallback(pIndexError || pCreateError);
147
+ return fValidateAndCallback(pIndexError || pCreateError);
118
148
  });
119
149
  });
120
150
  }
@@ -197,8 +227,11 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
197
227
  this.fable.log.info(`Found ${tmpDeletedCount} deleted records on server for ${this.EntitySchema.TableName}; syncing deletions...`);
198
228
 
199
229
  // Generate paginated URLs for deleted records
230
+ let tmpDeleteCap = (this.MaxRecordsPerEntity > 0)
231
+ ? Math.min(tmpDeletedCount, this.MaxRecordsPerEntity)
232
+ : tmpDeletedCount;
200
233
  const tmpDeleteURLPartials = [];
201
- for (let i = 0; i < tmpDeletedCount; i += this.PageSize)
234
+ for (let i = 0; i < tmpDeleteCap; i += this.PageSize)
202
235
  {
203
236
  tmpDeleteURLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~Deleted~EQ~1~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
204
237
  }
@@ -297,6 +330,12 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
297
330
 
298
331
  sync(fCallback)
299
332
  {
333
+ if (this.skipSync)
334
+ {
335
+ this.log.warn(`Skipping sync for ${this.EntitySchema.TableName} -- local table schema does not match expected schema.`);
336
+ return fCallback();
337
+ }
338
+
300
339
  this.operation.createTimeStamp('EntityInitialSync');
301
340
 
302
341
  this.log.info(`Syncing ${this.EntitySchema.TableName} (PageSize: ${this.PageSize}, SyncDeletedRecords: ${this.SyncDeletedRecords})`);
@@ -307,6 +346,19 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
307
346
  Server: { MaxIDEntity: -1, RecordCount: 0 },
308
347
  });
309
348
 
349
+ // Detect whether the table has a Deleted column
350
+ if (this.EntitySchema.MeadowSchema && Array.isArray(this.EntitySchema.MeadowSchema.Schema))
351
+ {
352
+ for (let i = 0; i < this.EntitySchema.MeadowSchema.Schema.length; i++)
353
+ {
354
+ const tmpColumn = this.EntitySchema.MeadowSchema.Schema[i];
355
+ if (tmpColumn.Type == 'Deleted' || tmpColumn.Column == 'Deleted')
356
+ {
357
+ tmpSyncState.HasDeletedColumn = true;
358
+ }
359
+ }
360
+ }
361
+
310
362
  this.fable.Utility.waterfall(
311
363
  [
312
364
  (fStageComplete) =>
@@ -315,6 +367,10 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
315
367
  const tmpQuery = this.Meadow.query;
316
368
  tmpQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
317
369
  tmpQuery.setCap(1);
370
+ if (!tmpSyncState.HasDeletedColumn)
371
+ {
372
+ tmpQuery.setDisableDeleteTracking(true);
373
+ }
318
374
  this.Meadow.doRead(tmpQuery,
319
375
  (pReadError, pQuery, pRecord) =>
320
376
  {
@@ -333,6 +389,10 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
333
389
  {
334
390
  // Get the count from local database
335
391
  const tmpQuery = this.Meadow.query;
392
+ if (!tmpSyncState.HasDeletedColumn)
393
+ {
394
+ tmpQuery.setDisableDeleteTracking(true);
395
+ }
336
396
  this.Meadow.doCount(tmpQuery,
337
397
  (pCountError, pQuery, pCount) =>
338
398
  {
@@ -352,7 +412,8 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
352
412
  {
353
413
  if (pError)
354
414
  {
355
- return fStageComplete(`Error getting server max entity ID ${this.EntitySchema.TableName}: ${pError}`);
415
+ this.fable.log.warn(`Could not get server max entity ID for ${this.EntitySchema.TableName} (${pError}); continuing sync.`);
416
+ return fStageComplete();
356
417
  }
357
418
  if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
358
419
  {
@@ -369,7 +430,9 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
369
430
  {
370
431
  if (pError)
371
432
  {
372
- return fStageComplete(`Error getting server count for ${this.EntitySchema.TableName}: ${pError}`);
433
+ this.fable.log.warn(`Could not get server count for ${this.EntitySchema.TableName} (${pError}); estimating from max ID.`);
434
+ tmpSyncState.Server.RecordCount = tmpSyncState.Server.MaxIDEntity > 0 ? tmpSyncState.Server.MaxIDEntity : 0;
435
+ return fStageComplete();
373
436
  }
374
437
  if (pBody && pBody.hasOwnProperty('Count'))
375
438
  {
@@ -382,17 +445,27 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
382
445
  {
383
446
  tmpSyncState.EstimatedRecordCount = tmpSyncState.Server.RecordCount - tmpSyncState.Local.RecordCount;
384
447
 
448
+ // Apply MaxRecordsPerEntity cap if configured
449
+ let tmpRecordCap = (this.MaxRecordsPerEntity > 0)
450
+ ? Math.min(tmpSyncState.Server.RecordCount, this.MaxRecordsPerEntity)
451
+ : tmpSyncState.Server.RecordCount;
452
+
453
+ if (this.MaxRecordsPerEntity > 0 && tmpSyncState.EstimatedRecordCount > this.MaxRecordsPerEntity)
454
+ {
455
+ tmpSyncState.EstimatedRecordCount = this.MaxRecordsPerEntity;
456
+ }
457
+
385
458
  this.operation.createProgressTracker(tmpSyncState.EstimatedRecordCount, `FullSync-${this.EntitySchema.TableName}`);
386
459
  this.operation.printProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`);
387
460
 
388
461
  // Generate paginated URL partials
389
462
  tmpSyncState.URLPartials = [];
390
- for (let i = 0; i < tmpSyncState.Server.RecordCount; i += this.PageSize)
463
+ for (let i = 0; i < tmpRecordCap; i += this.PageSize)
391
464
  {
392
465
  tmpSyncState.URLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~${this.DefaultIdentifier}~GT~${tmpSyncState.Local.MaxIDEntity}~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
393
466
  }
394
467
 
395
- this.fable.log.info(`${this.EntitySchema.TableName}: downloading ${tmpSyncState.URLPartials.length} pages (local: ${tmpSyncState.Local.RecordCount}/${tmpSyncState.Local.MaxIDEntity}, server: ${tmpSyncState.Server.RecordCount}/${tmpSyncState.Server.MaxIDEntity}, estimated new: ${tmpSyncState.EstimatedRecordCount})`);
468
+ this.fable.log.info(`${this.EntitySchema.TableName}: downloading ${tmpSyncState.URLPartials.length} pages (local: ${tmpSyncState.Local.RecordCount}/${tmpSyncState.Local.MaxIDEntity}, server: ${tmpSyncState.Server.RecordCount}/${tmpSyncState.Server.MaxIDEntity}, estimated new: ${tmpSyncState.EstimatedRecordCount}${this.MaxRecordsPerEntity > 0 ? `, capped at ${this.MaxRecordsPerEntity}` : ''})`);
396
469
 
397
470
  return fStageComplete();
398
471
  },
@@ -416,7 +489,7 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
416
489
  this.fable.log.error(`${this.EntitySchema.TableName}: page ${tmpPageIndex} download error: ${pDownloadError}`);
417
490
  return fDownloadComplete();
418
491
  }
419
- if (pBody && pBody.length > 0)
492
+ if (pBody && Array.isArray(pBody) && pBody.length > 0)
420
493
  {
421
494
  this.fable.Utility.eachLimit(pBody, 5,
422
495
  (pEntityRecord, fEntitySyncComplete) =>
@@ -429,6 +502,11 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
429
502
  tmpQuery.addFilter(this.DefaultIdentifier, tmpRecord[this.DefaultIdentifier]);
430
503
  }
431
504
 
505
+ if (!tmpSyncState.HasDeletedColumn)
506
+ {
507
+ tmpQuery.setDisableDeleteTracking(true);
508
+ }
509
+
432
510
  this.Meadow.doRead(tmpQuery,
433
511
  (pReadError, pQuery, pRecord) =>
434
512
  {
@@ -456,6 +534,31 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
456
534
  {
457
535
  if (pCreateError)
458
536
  {
537
+ let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
538
+ if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
539
+ {
540
+ // Duplicate key (likely GUID conflict) -- fall back to update
541
+ this.log.warn(`${this.EntitySchema.TableName}: duplicate key on create for ID ${tmpRecord[this.DefaultIdentifier]}; falling back to update.`);
542
+ const tmpUpdateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
543
+ tmpUpdateQuery.setDisableAutoIdentity(true);
544
+ tmpUpdateQuery.setDisableAutoDateStamp(true);
545
+ tmpUpdateQuery.setDisableAutoUserStamp(true);
546
+ tmpUpdateQuery.setDisableDeleteTracking(true);
547
+ this.Meadow.doUpdate(tmpUpdateQuery,
548
+ (pUpdateError) =>
549
+ {
550
+ if (pUpdateError)
551
+ {
552
+ tmpRecordsErrored++;
553
+ this.log.error(`${this.EntitySchema.TableName}: fallback update also failed for ID ${tmpRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
554
+ return fEntitySyncComplete();
555
+ }
556
+ tmpRecordsCreated++;
557
+ this.operation.incrementProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`, 1);
558
+ return fEntitySyncComplete();
559
+ });
560
+ return;
561
+ }
459
562
  tmpRecordsErrored++;
460
563
  this.log.error(`${this.EntitySchema.TableName}: doCreate error for ID ${tmpRecord[this.DefaultIdentifier]}: ${pCreateError}`);
461
564
  return fEntitySyncComplete();
@@ -40,10 +40,13 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
40
40
  this.DefaultIdentifier = this.EntitySchema.MeadowSchema.DefaultIdentifier;
41
41
  this.PageSize = this.options.PageSize || 100;
42
42
  this.SyncDeletedRecords = this.options.SyncDeletedRecords || false;
43
+ this.MaxRecordsPerEntity = this.options.MaxRecordsPerEntity || 0;
43
44
 
44
45
  this.Meadow = false;
45
46
 
46
47
  this.operation = new libMeadowOperation(this.fable);
48
+
49
+ this.skipSync = false;
47
50
  }
48
51
 
49
52
  initialize(fCallback)
@@ -59,19 +62,46 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
59
62
  {
60
63
  return this.Meadow.provider.getProvider().createTable(this.EntitySchema, (pCreateError) =>
61
64
  {
65
+ let fValidateAndCallback = (pPriorError) =>
66
+ {
67
+ // Validate local table schema with a lightweight read
68
+ const tmpValidationQuery = this.Meadow.query;
69
+ tmpValidationQuery.setCap(1);
70
+ tmpValidationQuery.setDisableDeleteTracking(true);
71
+ this.Meadow.doRead(tmpValidationQuery,
72
+ (pReadError) =>
73
+ {
74
+ if (pReadError)
75
+ {
76
+ let tmpErrorStr = (typeof(pReadError) === 'string') ? pReadError : JSON.stringify(pReadError);
77
+ // Only skip sync for schema-specific errors (invalid column/object name)
78
+ // Generic provider errors (e.g. prepared statement failures) should not block sync
79
+ if (tmpErrorStr.indexOf('Invalid column') > -1 || tmpErrorStr.indexOf('Invalid object') > -1 || tmpErrorStr.indexOf('no such column') > -1 || tmpErrorStr.indexOf('no such table') > -1)
80
+ {
81
+ this.log.warn(`${this.EntitySchema.TableName}: local table schema validation failed (${pReadError}); this entity will be skipped during sync.`);
82
+ this.skipSync = true;
83
+ }
84
+ else
85
+ {
86
+ this.log.warn(`${this.EntitySchema.TableName}: validation read returned error (${pReadError}); sync will proceed.`);
87
+ }
88
+ }
89
+ return fCallback(pPriorError);
90
+ });
91
+ };
62
92
  const tmpGUIDColumn = this.EntitySchema.Columns.find((c) => c.DataType == 'GUID');
63
93
  const tmpDeletedColumn = this.EntitySchema.Columns.find((c) => c.Column == 'Deleted');
64
94
 
65
95
  if (!tmpGUIDColumn && !tmpDeletedColumn)
66
96
  {
67
97
  this.log.info(`No GUID or Deleted columns for ${this.EntitySchema.TableName}; skipping index creation`);
68
- return fCallback(pCreateError);
98
+ return fValidateAndCallback(pCreateError);
69
99
  }
70
100
 
71
101
  if (!this.fable.MeadowConnectionManager || !this.fable.MeadowConnectionManager.ConnectionPool)
72
102
  {
73
103
  this.log.info(`No connection manager available; skipping index creation for ${this.EntitySchema.TableName}`);
74
- return fCallback(pCreateError);
104
+ return fValidateAndCallback(pCreateError);
75
105
  }
76
106
 
77
107
  let tmpAnticipate = this.fable.newAnticipate();
@@ -91,7 +121,7 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
91
121
  return this.fable.MeadowConnectionManager.createIndex(this.EntitySchema, tmpDeletedColumn, false, fNext);
92
122
  });
93
123
  }
94
- tmpAnticipate.wait(fCallback);
124
+ tmpAnticipate.wait((pIndexError) => { return fValidateAndCallback(pCreateError); });
95
125
  });
96
126
  }
97
127
  return fCallback();
@@ -117,7 +147,14 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
117
147
  }
118
148
  break;
119
149
  default:
120
- if (pSourceRecord[tmpColumn.Column] !== '')
150
+ if (tmpColumn.DataType == 'DateTime')
151
+ {
152
+ if ((typeof(pSourceRecord[tmpColumn.Column]) == 'string') && (pSourceRecord[tmpColumn.Column].length > 0))
153
+ {
154
+ tmpRecordToCommit[tmpColumn.Column] = this.fable.Dates.dayJS.utc(pSourceRecord[tmpColumn.Column]).format('YYYY-MM-DD HH:mm:ss.SSS');
155
+ }
156
+ }
157
+ else if (pSourceRecord[tmpColumn.Column] !== '')
121
158
  {
122
159
  tmpRecordToCommit[tmpColumn.Column] = pSourceRecord[tmpColumn.Column];
123
160
  }
@@ -165,8 +202,11 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
165
202
  this.fable.log.info(`Found ${tmpDeletedCount} deleted records on server for ${this.EntitySchema.TableName}; syncing deletions...`);
166
203
 
167
204
  // Generate paginated URLs for deleted records
205
+ let tmpDeleteCap = (this.MaxRecordsPerEntity > 0)
206
+ ? Math.min(tmpDeletedCount, this.MaxRecordsPerEntity)
207
+ : tmpDeletedCount;
168
208
  const tmpDeleteURLPartials = [];
169
- for (let i = 0; i < tmpDeletedCount; i += this.PageSize)
209
+ for (let i = 0; i < tmpDeleteCap; i += this.PageSize)
170
210
  {
171
211
  tmpDeleteURLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~Deleted~EQ~1~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
172
212
  }
@@ -277,7 +317,7 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
277
317
  this.fable.log.error(`Error getting URL Partial [${tmpURLPartial}]: ${pDownloadError}`, { Error: pDownloadError });
278
318
  return fNext();
279
319
  }
280
- if (pBody && pBody.length > 0)
320
+ if (pBody && Array.isArray(pBody) && pBody.length > 0)
281
321
  {
282
322
  for (let i = 0; i < pBody.length; i++)
283
323
  {
@@ -298,6 +338,11 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
298
338
  tmpQuery.addFilter(this.DefaultIdentifier, tmpRecord[this.DefaultIdentifier]);
299
339
  }
300
340
 
341
+ if (!tmpSyncState.HasDeletedColumn)
342
+ {
343
+ tmpQuery.setDisableDeleteTracking(true);
344
+ }
345
+
301
346
  this.Meadow.doRead(tmpQuery,
302
347
  (pReadError, pQuery, pRecord) =>
303
348
  {
@@ -338,6 +383,29 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
338
383
  {
339
384
  if (pCreateError)
340
385
  {
386
+ let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
387
+ if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
388
+ {
389
+ // Duplicate key (likely GUID conflict) -- fall back to update
390
+ this.log.warn(`Duplicate key on create for ${this.EntitySchema.TableName} ID ${tmpRecord[this.DefaultIdentifier]}; falling back to update.`);
391
+ const tmpFallbackQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
392
+ tmpFallbackQuery.setDisableAutoIdentity(true);
393
+ tmpFallbackQuery.setDisableAutoDateStamp(true);
394
+ tmpFallbackQuery.setDisableAutoUserStamp(true);
395
+ tmpFallbackQuery.setDisableDeleteTracking(true);
396
+ this.Meadow.doUpdate(tmpFallbackQuery,
397
+ (pUpdateError) =>
398
+ {
399
+ if (pUpdateError)
400
+ {
401
+ this.log.error(`Fallback update also failed for ${this.EntitySchema.TableName} ID ${tmpRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
402
+ return fNextEntityRecordSync();
403
+ }
404
+ this.operation.incrementProgressTrackerStatus(`UpdateSync-${this.EntitySchema.TableName}`, 1);
405
+ return fNextEntityRecordSync();
406
+ });
407
+ return;
408
+ }
341
409
  this.log.error(`Error creating record ${this.EntitySchema.TableName}: ${pCreateError}`, pCreateError);
342
410
  return fNextEntityRecordSync();
343
411
  }
@@ -381,6 +449,12 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
381
449
 
382
450
  sync(fCallback)
383
451
  {
452
+ if (this.skipSync)
453
+ {
454
+ this.log.warn(`Skipping sync for ${this.EntitySchema.TableName} -- local table schema does not match expected schema.`);
455
+ return fCallback();
456
+ }
457
+
384
458
  this.operation.createTimeStamp('EntityOngoingSync');
385
459
 
386
460
  let tmpAnticipate = this.fable.newAnticipate();
@@ -407,9 +481,16 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
407
481
  {
408
482
  tmpSyncState.Local.HasUpdateDate = true;
409
483
  tmpSyncState.Server.HasUpdateDate = true;
410
- this.log.info(`Entity ${this.EntitySchema.TableName} has UpdateDate column.`);
411
- break;
412
484
  }
485
+ if (tmpColumn.Type == 'Deleted' || tmpColumn.Column == 'Deleted')
486
+ {
487
+ tmpSyncState.HasDeletedColumn = true;
488
+ }
489
+ }
490
+
491
+ if (tmpSyncState.Local.HasUpdateDate)
492
+ {
493
+ this.log.info(`Entity ${this.EntitySchema.TableName} has UpdateDate column.`);
413
494
  }
414
495
 
415
496
  this.log.info(`Syncing with UPDATE STRATEGY entity ${this.EntitySchema.TableName}...`);
@@ -421,6 +502,11 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
421
502
  const tmpQuery = this.Meadow.query;
422
503
  tmpQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
423
504
  tmpQuery.setCap(1);
505
+ // Disable delete tracking if the table has no Deleted column
506
+ if (!tmpSyncState.HasDeletedColumn)
507
+ {
508
+ tmpQuery.setDisableDeleteTracking(true);
509
+ }
424
510
  this.Meadow.doRead(tmpQuery,
425
511
  (pReadError, pQuery, pRecord) =>
426
512
  {
@@ -441,10 +527,19 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
441
527
  },
442
528
  (fStageComplete) =>
443
529
  {
444
- // Get the Max UpdateDate from local database
530
+ // Get the Max UpdateDate from local database — skip if table has no UpdateDate column
531
+ if (!tmpSyncState.Local.HasUpdateDate)
532
+ {
533
+ this.fable.log.info(`No UpdateDate column for ${this.EntitySchema.TableName}; skipping UpdateDate check.`);
534
+ return fStageComplete();
535
+ }
445
536
  const tmpQuery = this.Meadow.query;
446
537
  tmpQuery.setSort({ Column: 'UpdateDate', Direction: 'Descending' });
447
538
  tmpQuery.setCap(1);
539
+ if (!tmpSyncState.HasDeletedColumn)
540
+ {
541
+ tmpQuery.setDisableDeleteTracking(true);
542
+ }
448
543
  this.Meadow.doRead(tmpQuery,
449
544
  (pReadError, pQuery, pRecord) =>
450
545
  {
@@ -467,6 +562,10 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
467
562
  {
468
563
  // Get the count from local database
469
564
  const tmpQuery = this.Meadow.query;
565
+ if (!tmpSyncState.HasDeletedColumn)
566
+ {
567
+ tmpQuery.setDisableDeleteTracking(true);
568
+ }
470
569
  this.Meadow.doCount(tmpQuery,
471
570
  (pCountError, pQuery, pCount) =>
472
571
  {
@@ -487,8 +586,8 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
487
586
  {
488
587
  if (pError)
489
588
  {
490
- this.fable.log.error(`Error getting server max entity ID ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
491
- return fStageComplete(`Error getting server max entity ID ${this.EntitySchema.TableName}: ${pError}`);
589
+ this.fable.log.warn(`Could not get server max entity ID for ${this.EntitySchema.TableName} (${pError}); continuing sync.`);
590
+ return fStageComplete();
492
591
  }
493
592
  if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
494
593
  {
@@ -510,8 +609,8 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
510
609
  {
511
610
  if (pError)
512
611
  {
513
- this.fable.log.error(`Error getting server max UpdateDate ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
514
- return fStageComplete(`Error getting server max UpdateDate ${this.EntitySchema.TableName}: ${pError}`);
612
+ this.fable.log.warn(`Could not get server max UpdateDate for ${this.EntitySchema.TableName} (${pError}); will sync by ID only.`);
613
+ return fStageComplete();
515
614
  }
516
615
  if (pBody && pBody.hasOwnProperty(this.DefaultIdentifier))
517
616
  {
@@ -533,8 +632,9 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
533
632
  {
534
633
  if (pError)
535
634
  {
536
- this.fable.log.error(`Error getting server count for ${this.EntitySchema.TableName}: ${pError}`, { Error: pError });
537
- return fStageComplete(`Error getting server count for ${this.EntitySchema.TableName}: ${pError}`);
635
+ this.fable.log.warn(`Could not get server count for ${this.EntitySchema.TableName} (${pError}); estimating from max ID.`);
636
+ tmpSyncState.Server.RecordCount = tmpSyncState.Server.MaxIDEntity > 0 ? tmpSyncState.Server.MaxIDEntity : 0;
637
+ return fStageComplete();
538
638
  }
539
639
  if (pBody && pBody.hasOwnProperty('Count'))
540
640
  {
@@ -550,7 +650,10 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
550
650
  },
551
651
  (fStageComplete) =>
552
652
  {
553
- tmpSyncState.EstimatedRequestCount = Math.ceil(tmpSyncState.Server.RecordCount / this.PageSize);
653
+ let tmpTotalRecords = (this.MaxRecordsPerEntity > 0)
654
+ ? Math.min(tmpSyncState.Server.RecordCount, this.MaxRecordsPerEntity)
655
+ : tmpSyncState.Server.RecordCount;
656
+ tmpSyncState.EstimatedRequestCount = Math.ceil(tmpTotalRecords / this.PageSize);
554
657
  tmpSyncState.RequestsPerformed = 0;
555
658
  tmpSyncState.LastRequestedID = 0;
556
659
 
@@ -54,6 +54,17 @@ class MeadowSync extends libFableServiceProviderBase
54
54
  this.SyncDeletedRecords = !!this.options.SyncDeletedRecords;
55
55
  }
56
56
 
57
+ // When > 0, limit sync to at most this many records per entity.
58
+ this.MaxRecordsPerEntity = 0;
59
+ if (this.fable.ProgramConfiguration.hasOwnProperty('MaxRecordsPerEntity'))
60
+ {
61
+ this.MaxRecordsPerEntity = parseInt(this.fable.ProgramConfiguration.MaxRecordsPerEntity, 10) || 0;
62
+ }
63
+ else if (this.options.hasOwnProperty('MaxRecordsPerEntity'))
64
+ {
65
+ this.MaxRecordsPerEntity = parseInt(this.options.MaxRecordsPerEntity, 10) || 0;
66
+ }
67
+
57
68
  this.MeadowSchema = false;
58
69
  this.MeadowSchemaTableList = false;
59
70
 
@@ -89,6 +100,7 @@ class MeadowSync extends libFableServiceProviderBase
89
100
  ConnectionPool: this.options.ConnectionPool,
90
101
  PageSize: this.options.PageSize || 100,
91
102
  SyncDeletedRecords: this.SyncDeletedRecords,
103
+ MaxRecordsPerEntity: this.MaxRecordsPerEntity,
92
104
  };
93
105
 
94
106
  let tmpSyncEntity;