meadow-integration 1.0.9 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.10",
4
4
  "description": "Meadow Data Integration",
5
5
  "bin": {
6
6
  "mdwint": "source/cli/Meadow-Integration-CLI-Run.js"
@@ -40,10 +40,11 @@
40
40
  "fable": "^3.1.63",
41
41
  "fable-serviceproviderbase": "^3.0.19",
42
42
  "meadow": "^2.0.30",
43
- "meadow-connection-mysql": "^1.0.14",
44
43
  "meadow-connection-mssql": "^1.0.16",
44
+ "meadow-connection-mysql": "^1.0.14",
45
45
  "orator": "^6.0.4",
46
46
  "orator-serviceserver-restify": "^2.0.9",
47
- "pict-service-commandlineutility": "^1.0.19"
47
+ "pict-service-commandlineutility": "^1.0.19",
48
+ "pict-sessionmanager": "^1.0.2"
48
49
  }
49
50
  }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Meadow Integration - Session Manager Setup
3
+ *
4
+ * Utility to initialize pict-sessionmanager from configuration.
5
+ * Used by CLI, Console UI, and REST server entry points.
6
+ * Modules that use meadow-integration services directly should
7
+ * manage their own session lifecycle.
8
+ *
9
+ * @author Steven Velozo <steven@velozo.com>
10
+ */
11
+ const libPictSessionManager = require('pict-sessionmanager');
12
+
13
+ /**
14
+ * Initialize a SessionManager on a fable/pict instance from a config object.
15
+ *
16
+ * @param {object} pFable - A fable or pict instance
17
+ * @param {object} pSessionManagerConfig - The SessionManager configuration block
18
+ * @param {object} pSessionManagerConfig.Sessions - Map of session name to session configuration
19
+ * @returns {object|false} The instantiated SessionManager, or false if no sessions configured
20
+ */
21
+ function initializeSessionManager(pFable, pSessionManagerConfig)
22
+ {
23
+ if (!pFable)
24
+ {
25
+ return false;
26
+ }
27
+ if (!pSessionManagerConfig || typeof(pSessionManagerConfig) !== 'object')
28
+ {
29
+ return false;
30
+ }
31
+ if (!pSessionManagerConfig.Sessions || typeof(pSessionManagerConfig.Sessions) !== 'object')
32
+ {
33
+ return false;
34
+ }
35
+
36
+ let tmpSessionNames = Object.keys(pSessionManagerConfig.Sessions);
37
+ if (tmpSessionNames.length < 1)
38
+ {
39
+ return false;
40
+ }
41
+
42
+ pFable.serviceManager.addServiceType('SessionManager', libPictSessionManager);
43
+ pFable.serviceManager.instantiateServiceProvider('SessionManager');
44
+
45
+ for (let i = 0; i < tmpSessionNames.length; i++)
46
+ {
47
+ let tmpSessionName = tmpSessionNames[i];
48
+ let tmpSessionConfig = pSessionManagerConfig.Sessions[tmpSessionName];
49
+ pFable.SessionManager.addSession(tmpSessionName, tmpSessionConfig);
50
+ }
51
+
52
+ return pFable.SessionManager;
53
+ }
54
+
55
+ /**
56
+ * Authenticate all configured sessions that have Credentials set.
57
+ *
58
+ * @param {object} pFable - A fable or pict instance with SessionManager instantiated
59
+ * @param {function} fCallback - Callback (pError)
60
+ */
61
+ function authenticateSessions(pFable, fCallback)
62
+ {
63
+ if (!pFable || !pFable.SessionManager)
64
+ {
65
+ return fCallback();
66
+ }
67
+
68
+ let tmpSessionNames = Object.keys(pFable.SessionManager.sessions);
69
+ if (tmpSessionNames.length < 1)
70
+ {
71
+ return fCallback();
72
+ }
73
+
74
+ pFable.Utility.eachLimit(tmpSessionNames, 1,
75
+ (pSessionName, fSessionCallback) =>
76
+ {
77
+ let tmpSession = pFable.SessionManager.getSession(pSessionName);
78
+ let tmpCredentials = tmpSession.Configuration.Credentials;
79
+
80
+ if (!tmpCredentials || typeof(tmpCredentials) !== 'object' || Object.keys(tmpCredentials).length < 1)
81
+ {
82
+ pFable.log.info(`SessionManager setup: Session [${pSessionName}] has no credentials configured; skipping authentication.`);
83
+ return fSessionCallback();
84
+ }
85
+
86
+ pFable.log.info(`SessionManager setup: Authenticating session [${pSessionName}]...`);
87
+ pFable.SessionManager.authenticate(pSessionName, tmpCredentials,
88
+ (pError, pSessionState) =>
89
+ {
90
+ if (pError)
91
+ {
92
+ pFable.log.error(`SessionManager setup: Failed to authenticate session [${pSessionName}]: ${pError.message}`);
93
+ return fSessionCallback(pError);
94
+ }
95
+
96
+ pFable.log.info(`SessionManager setup: Session [${pSessionName}] authenticated successfully.`);
97
+ return fSessionCallback();
98
+ });
99
+ },
100
+ (pError) =>
101
+ {
102
+ return fCallback(pError);
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Connect the SessionManager to a RestClient so credentials are injected automatically.
108
+ *
109
+ * If no RestClient is provided, SessionManager will use the default
110
+ * RestClient on the fable/pict instance (instantiating it if needed).
111
+ *
112
+ * @param {object} pFable - A fable or pict instance with SessionManager instantiated
113
+ * @param {object} [pRestClient] - A fable RestClient instance to connect to (optional)
114
+ * @returns {boolean} True if connected
115
+ */
116
+ function connectSessionManagerToRestClient(pFable, pRestClient)
117
+ {
118
+ if (!pFable || !pFable.SessionManager)
119
+ {
120
+ return false;
121
+ }
122
+
123
+ // SessionManager.connectToRestClient handles null by using the default
124
+ // pict RestClient (instantiating it if necessary).
125
+ pFable.SessionManager.connectToRestClient(pRestClient);
126
+ return true;
127
+ }
128
+
129
+ module.exports = (
130
+ {
131
+ initializeSessionManager: initializeSessionManager,
132
+ authenticateSessions: authenticateSessions,
133
+ connectSessionManagerToRestClient: connectSessionManagerToRestClient
134
+ });
@@ -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,6 +40,7 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
40
40
  this.DefaultIdentifier = this.EntitySchema.MeadowSchema.DefaultIdentifier;
41
41
  this.PageSize = this.options.PageSize || 100;
42
42
  this.SyncDeletedRecords = this.options.SyncDeletedRecords || false;
43
+ this.MaxRecordsPerEntity = this.options.MaxRecordsPerEntity || 0;
43
44
 
44
45
  this.Meadow = false;
45
46
 
@@ -197,8 +198,11 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
197
198
  this.fable.log.info(`Found ${tmpDeletedCount} deleted records on server for ${this.EntitySchema.TableName}; syncing deletions...`);
198
199
 
199
200
  // Generate paginated URLs for deleted records
201
+ let tmpDeleteCap = (this.MaxRecordsPerEntity > 0)
202
+ ? Math.min(tmpDeletedCount, this.MaxRecordsPerEntity)
203
+ : tmpDeletedCount;
200
204
  const tmpDeleteURLPartials = [];
201
- for (let i = 0; i < tmpDeletedCount; i += this.PageSize)
205
+ for (let i = 0; i < tmpDeleteCap; i += this.PageSize)
202
206
  {
203
207
  tmpDeleteURLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~Deleted~EQ~1~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
204
208
  }
@@ -307,6 +311,19 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
307
311
  Server: { MaxIDEntity: -1, RecordCount: 0 },
308
312
  });
309
313
 
314
+ // Detect whether the table has a Deleted column
315
+ if (this.EntitySchema.MeadowSchema && Array.isArray(this.EntitySchema.MeadowSchema.Schema))
316
+ {
317
+ for (let i = 0; i < this.EntitySchema.MeadowSchema.Schema.length; i++)
318
+ {
319
+ const tmpColumn = this.EntitySchema.MeadowSchema.Schema[i];
320
+ if (tmpColumn.Type == 'Deleted' || tmpColumn.Column == 'Deleted')
321
+ {
322
+ tmpSyncState.HasDeletedColumn = true;
323
+ }
324
+ }
325
+ }
326
+
310
327
  this.fable.Utility.waterfall(
311
328
  [
312
329
  (fStageComplete) =>
@@ -315,6 +332,10 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
315
332
  const tmpQuery = this.Meadow.query;
316
333
  tmpQuery.setSort({ Column: this.DefaultIdentifier, Direction: 'Descending' });
317
334
  tmpQuery.setCap(1);
335
+ if (!tmpSyncState.HasDeletedColumn)
336
+ {
337
+ tmpQuery.setDisableDeleteTracking(true);
338
+ }
318
339
  this.Meadow.doRead(tmpQuery,
319
340
  (pReadError, pQuery, pRecord) =>
320
341
  {
@@ -333,6 +354,10 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
333
354
  {
334
355
  // Get the count from local database
335
356
  const tmpQuery = this.Meadow.query;
357
+ if (!tmpSyncState.HasDeletedColumn)
358
+ {
359
+ tmpQuery.setDisableDeleteTracking(true);
360
+ }
336
361
  this.Meadow.doCount(tmpQuery,
337
362
  (pCountError, pQuery, pCount) =>
338
363
  {
@@ -382,17 +407,27 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
382
407
  {
383
408
  tmpSyncState.EstimatedRecordCount = tmpSyncState.Server.RecordCount - tmpSyncState.Local.RecordCount;
384
409
 
410
+ // Apply MaxRecordsPerEntity cap if configured
411
+ let tmpRecordCap = (this.MaxRecordsPerEntity > 0)
412
+ ? Math.min(tmpSyncState.Server.RecordCount, this.MaxRecordsPerEntity)
413
+ : tmpSyncState.Server.RecordCount;
414
+
415
+ if (this.MaxRecordsPerEntity > 0 && tmpSyncState.EstimatedRecordCount > this.MaxRecordsPerEntity)
416
+ {
417
+ tmpSyncState.EstimatedRecordCount = this.MaxRecordsPerEntity;
418
+ }
419
+
385
420
  this.operation.createProgressTracker(tmpSyncState.EstimatedRecordCount, `FullSync-${this.EntitySchema.TableName}`);
386
421
  this.operation.printProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`);
387
422
 
388
423
  // Generate paginated URL partials
389
424
  tmpSyncState.URLPartials = [];
390
- for (let i = 0; i < tmpSyncState.Server.RecordCount; i += this.PageSize)
425
+ for (let i = 0; i < tmpRecordCap; i += this.PageSize)
391
426
  {
392
427
  tmpSyncState.URLPartials.push(`${this.EntitySchema.TableName}s/FilteredTo/FBV~${this.DefaultIdentifier}~GT~${tmpSyncState.Local.MaxIDEntity}~FSF~${this.DefaultIdentifier}~ASC~ASC/${i}/${this.PageSize}`);
393
428
  }
394
429
 
395
- this.fable.log.info(`${this.EntitySchema.TableName}: downloading ${tmpSyncState.URLPartials.length} pages (local: ${tmpSyncState.Local.RecordCount}/${tmpSyncState.Local.MaxIDEntity}, server: ${tmpSyncState.Server.RecordCount}/${tmpSyncState.Server.MaxIDEntity}, estimated new: ${tmpSyncState.EstimatedRecordCount})`);
430
+ this.fable.log.info(`${this.EntitySchema.TableName}: downloading ${tmpSyncState.URLPartials.length} pages (local: ${tmpSyncState.Local.RecordCount}/${tmpSyncState.Local.MaxIDEntity}, server: ${tmpSyncState.Server.RecordCount}/${tmpSyncState.Server.MaxIDEntity}, estimated new: ${tmpSyncState.EstimatedRecordCount}${this.MaxRecordsPerEntity > 0 ? `, capped at ${this.MaxRecordsPerEntity}` : ''})`);
396
431
 
397
432
  return fStageComplete();
398
433
  },
@@ -429,6 +464,11 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
429
464
  tmpQuery.addFilter(this.DefaultIdentifier, tmpRecord[this.DefaultIdentifier]);
430
465
  }
431
466
 
467
+ if (!tmpSyncState.HasDeletedColumn)
468
+ {
469
+ tmpQuery.setDisableDeleteTracking(true);
470
+ }
471
+
432
472
  this.Meadow.doRead(tmpQuery,
433
473
  (pReadError, pQuery, pRecord) =>
434
474
  {
@@ -456,6 +496,31 @@ class MeadowSyncEntityInitial extends libFableServiceProviderBase
456
496
  {
457
497
  if (pCreateError)
458
498
  {
499
+ let tmpErrorStr = (typeof(pCreateError) === 'string') ? pCreateError : JSON.stringify(pCreateError);
500
+ if (tmpErrorStr.toLowerCase().indexOf('duplicate') > -1 || tmpErrorStr.toLowerCase().indexOf('unique') > -1)
501
+ {
502
+ // Duplicate key (likely GUID conflict) -- fall back to update
503
+ this.log.warn(`${this.EntitySchema.TableName}: duplicate key on create for ID ${tmpRecord[this.DefaultIdentifier]}; falling back to update.`);
504
+ const tmpUpdateQuery = this.Meadow.query.addRecord(tmpRecordToCommit);
505
+ tmpUpdateQuery.setDisableAutoIdentity(true);
506
+ tmpUpdateQuery.setDisableAutoDateStamp(true);
507
+ tmpUpdateQuery.setDisableAutoUserStamp(true);
508
+ tmpUpdateQuery.setDisableDeleteTracking(true);
509
+ this.Meadow.doUpdate(tmpUpdateQuery,
510
+ (pUpdateError) =>
511
+ {
512
+ if (pUpdateError)
513
+ {
514
+ tmpRecordsErrored++;
515
+ this.log.error(`${this.EntitySchema.TableName}: fallback update also failed for ID ${tmpRecord[this.DefaultIdentifier]}: ${pUpdateError}`);
516
+ return fEntitySyncComplete();
517
+ }
518
+ tmpRecordsCreated++;
519
+ this.operation.incrementProgressTrackerStatus(`FullSync-${this.EntitySchema.TableName}`, 1);
520
+ return fEntitySyncComplete();
521
+ });
522
+ return;
523
+ }
459
524
  tmpRecordsErrored++;
460
525
  this.log.error(`${this.EntitySchema.TableName}: doCreate error for ID ${tmpRecord[this.DefaultIdentifier]}: ${pCreateError}`);
461
526
  return fEntitySyncComplete();
@@ -40,6 +40,7 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
40
40
  this.DefaultIdentifier = this.EntitySchema.MeadowSchema.DefaultIdentifier;
41
41
  this.PageSize = this.options.PageSize || 100;
42
42
  this.SyncDeletedRecords = this.options.SyncDeletedRecords || false;
43
+ this.MaxRecordsPerEntity = this.options.MaxRecordsPerEntity || 0;
43
44
 
44
45
  this.Meadow = false;
45
46
 
@@ -117,7 +118,14 @@ class MeadowSyncEntityOngoing extends libFableServiceProviderBase
117
118
  }
118
119
  break;
119
120
  default:
120
- if (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
 
@@ -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;