meadow-integration 1.0.18 → 1.0.19

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.
@@ -65,7 +65,16 @@ class MeadowGUIDMap extends libFableServiceProviderBase
65
65
  return (this._GUIDMap[pEntity].hasOwnProperty(pGUID)) ? this._GUIDMap[pEntity][pGUID] : false;
66
66
  }
67
67
 
68
- getIDFromGUIDAsync(pEntity, pGUID, fCallback)
68
+ /**
69
+ * Get an ID from a GUID, loading from the server if not already cached.
70
+ *
71
+ * @param {string} pEntity - The entity name
72
+ * @param {string} pGUID - The GUID to look up
73
+ * @param {function} fCallback - Callback (pError, pID)
74
+ * @param {object} [pClient] - Optional REST client to use for the server lookup
75
+ * (defaults to this.fable.MeadowRestClient or this.fable.MeadowCloneRestClient)
76
+ */
77
+ getIDFromGUIDAsync(pEntity, pGUID, fCallback, pClient)
69
78
  {
70
79
  if (!this._GUIDMap.hasOwnProperty(pEntity))
71
80
  {
@@ -79,8 +88,16 @@ class MeadowGUIDMap extends libFableServiceProviderBase
79
88
  }
80
89
  else
81
90
  {
91
+ // Resolve the REST client to use for the server lookup
92
+ let tmpClient = pClient || this.fable.MeadowRestClient || this.fable.MeadowCloneRestClient;
93
+
94
+ if (!tmpClient || typeof(tmpClient.getEntityByGUID) !== 'function')
95
+ {
96
+ return fCallback(new Error(`No REST client with getEntityByGUID available for GUID lookup of [${pEntity}].[${pGUID}].`), false);
97
+ }
98
+
82
99
  // Try to load it from the server
83
- this.fable.MeadowRestClient.getEntityByGUID(pEntity, pGUID,
100
+ tmpClient.getEntityByGUID(pEntity, pGUID,
84
101
  (pError, pBody) =>
85
102
  {
86
103
  if (pError)
@@ -3,6 +3,8 @@ const libCLICommandLineCommand = require('pict-service-commandlineutility').Serv
3
3
  const libPath = require('path');
4
4
 
5
5
  const libIntegrationAdapter = require('../../Meadow-Service-Integration-Adapter.js');
6
+ const libMeadowCloneRestClient = require('../../services/clone/Meadow-Service-RestClient.js');
7
+ const libSessionManagerSetup = require('../../Meadow-Integration-SessionManagerSetup.js');
6
8
 
7
9
  class PushComprehensionsViaIntegration extends libCLICommandLineCommand
8
10
  {
@@ -15,17 +17,52 @@ class PushComprehensionsViaIntegration extends libCLICommandLineCommand
15
17
  this.options.Aliases.push('load');
16
18
  this.options.Aliases.push('push');
17
19
 
18
-
19
20
  this.options.CommandArguments.push({ Name: '<comprehension_file>', Description: 'The comprehension file path.' });
20
21
 
21
- this.options.CommandOptions.push({ Name: '-p, --prefix [guid_prefix]', Description: 'GUID Prefix for the comprehension push.'});
22
- this.options.CommandOptions.push({ Name: '-e, --entityguidprefix [entity_guid_prefix]', Description: 'GUID Prefix for each entity.'});
22
+ this.options.CommandOptions.push({ Name: '-p, --prefix [guid_prefix]', Description: 'GUID Prefix for the comprehension push.' });
23
+ this.options.CommandOptions.push({ Name: '-e, --entityguidprefix [entity_guid_prefix]', Description: 'GUID Prefix for each entity.' });
24
+
25
+ this.options.CommandOptions.push({ Name: '-a, --api_server [api_server]', Description: 'The API server URL.' });
26
+ this.options.CommandOptions.push({ Name: '-u, --api_username [api_username]', Description: 'The API username to authenticate with.' });
27
+ this.options.CommandOptions.push({ Name: '-w, --api_password [api_password]', Description: 'The API password to authenticate with.' });
28
+
29
+ this.options.CommandOptions.push({ Name: '--bulkupsert [bulk_upsert]', Description: 'Enable bulk upsert mode (true/false). Default: true.', DefaultValue: 'true' });
30
+ this.options.CommandOptions.push({ Name: '--batchsize [batch_size]', Description: 'Bulk upsert batch size. Default: 100.', DefaultValue: '100' });
31
+ this.options.CommandOptions.push({ Name: '--progressinterval [progress_interval]', Description: 'Per-entity progress log interval (records). Default: 100.', DefaultValue: '100' });
32
+ this.options.CommandOptions.push({ Name: '--metaprogressinterval [meta_progress_interval]', Description: 'Meta (cross-entity) progress log interval. Default: 0 (disabled).', DefaultValue: '0' });
33
+ this.options.CommandOptions.push({ Name: '--allowguidtruncation', Description: 'Allow automatic GUID prefix truncation when GUIDs exceed max length.' });
34
+ this.options.CommandOptions.push({ Name: '--logfile [logfile_path]', Description: 'Path to write log output.' });
23
35
 
24
36
  this.addCommand();
25
37
 
26
38
  this.comprehension = {};
27
39
  }
28
40
 
41
+ _resolveConfig()
42
+ {
43
+ const tmpConfig = JSON.parse(JSON.stringify(this.fable.ProgramConfiguration));
44
+
45
+ // Apply command-line overrides for Source (API)
46
+ if (!tmpConfig.Source)
47
+ {
48
+ tmpConfig.Source = {};
49
+ }
50
+ if (this.CommandOptions.api_server)
51
+ {
52
+ tmpConfig.Source.ServerURL = this.CommandOptions.api_server;
53
+ }
54
+ if (this.CommandOptions.api_username)
55
+ {
56
+ tmpConfig.Source.UserID = this.CommandOptions.api_username;
57
+ }
58
+ if (this.CommandOptions.api_password)
59
+ {
60
+ tmpConfig.Source.Password = this.CommandOptions.api_password;
61
+ }
62
+
63
+ return tmpConfig;
64
+ }
65
+
29
66
  runAdapter(pAnticipate, pAdapter, pDataMap, fMarshalRecord)
30
67
  {
31
68
  let tmpAdapter = this.fable.servicesMap.IntegrationAdapter[pAdapter];
@@ -76,56 +113,191 @@ class PushComprehensionsViaIntegration extends libCLICommandLineCommand
76
113
  pushComprehension(fCallback)
77
114
  {
78
115
  let tmpComprehensionPath = this.ArgumentString;
116
+ let tmpConfig = this._resolveConfig();
79
117
 
80
- let tmpAnticipate = this.fable.newAnticipate();
81
118
  this.fable.log.info(`Pushing comprehension file [${tmpComprehensionPath}] to the Meadow Endpoints APIs.`);
82
119
 
120
+ // --- Setup REST client ---
121
+ this.fable.serviceManager.addServiceType('MeadowCloneRestClient', libMeadowCloneRestClient);
122
+
123
+ let tmpRestClientOptions = {};
124
+ if (tmpConfig.Source && tmpConfig.Source.ServerURL)
125
+ {
126
+ tmpRestClientOptions.ServerURL = tmpConfig.Source.ServerURL;
127
+ }
128
+ if (tmpConfig.Source && tmpConfig.Source.UserID)
129
+ {
130
+ tmpRestClientOptions.UserID = tmpConfig.Source.UserID;
131
+ }
132
+ if (tmpConfig.Source && tmpConfig.Source.Password)
133
+ {
134
+ tmpRestClientOptions.Password = tmpConfig.Source.Password;
135
+ }
136
+
137
+ this.fable.serviceManager.instantiateServiceProvider('MeadowCloneRestClient', tmpRestClientOptions);
138
+
139
+ // --- Initialize SessionManager if configured ---
140
+ let tmpSessionManager = libSessionManagerSetup.initializeSessionManager(this.fable, tmpConfig.SessionManager);
141
+ if (tmpSessionManager)
142
+ {
143
+ libSessionManagerSetup.connectSessionManagerToRestClient(this.fable, this.fable.MeadowCloneRestClient.restClient);
144
+ }
145
+
146
+ // --- Setup Integration Adapter service type ---
83
147
  this.fable.log.info(`Initializing and configuring data integration adapters...`);
84
148
  this.fable.serviceManager.addServiceType('IntegrationAdapter', libIntegrationAdapter);
85
149
 
86
- tmpAnticipate.anticipate(
87
- function (fCallback)
88
- {
89
- try
150
+ // --- Resolve adapter options from CLI flags + config ---
151
+ let tmpBulkUpsertEnabled = (this.CommandOptions.bulkupsert !== 'false');
152
+ let tmpBatchSize = parseInt(this.CommandOptions.batchsize) || 100;
153
+ let tmpProgressInterval = parseInt(this.CommandOptions.progressinterval) || 100;
154
+ let tmpMetaProgressInterval = parseInt(this.CommandOptions.metaprogressinterval) || 0;
155
+ let tmpAllowGUIDTruncation = !!this.CommandOptions.allowguidtruncation;
156
+
157
+ // Build shared adapter options
158
+ let tmpAdapterOptions = {
159
+ SimpleMarshal: true,
160
+ ForceMarshal: true,
161
+ BulkUpsertBatchSize: tmpBatchSize,
162
+ RecordThresholdForBulkUpsert: tmpBulkUpsertEnabled ? 1000 : Infinity,
163
+ ProgressLogInterval: tmpProgressInterval,
164
+ AllowGUIDTruncation: tmpAllowGUIDTruncation
165
+ };
166
+
167
+ this.fable.Utility.waterfall(
168
+ [
169
+ (fStageComplete) =>
90
170
  {
91
- this.fable.log.info(`Loading Comprehension File...`);
92
- tmpComprehensionPath = libPath.resolve(tmpComprehensionPath);
93
- this.comprehension = require(tmpComprehensionPath);
94
- return fCallback();
95
- }
96
- catch(pError)
171
+ // Authenticate SessionManager sessions (if configured)
172
+ if (tmpSessionManager)
173
+ {
174
+ this.log.info('Authenticating SessionManager sessions...');
175
+ libSessionManagerSetup.authenticateSessions(this.fable,
176
+ (pError) =>
177
+ {
178
+ if (pError)
179
+ {
180
+ this.log.error('Error authenticating SessionManager sessions.', pError);
181
+ }
182
+ return fStageComplete();
183
+ });
184
+ }
185
+ else
186
+ {
187
+ return fStageComplete();
188
+ }
189
+ },
190
+ (fStageComplete) =>
97
191
  {
98
- this.fable.log.error(`Error loading comprehension file [${tmpComprehensionPath}]: ${pError}`, pError);
99
- return fCallback(pError);
100
- }
101
- }.bind(this));
102
-
103
- tmpAnticipate.anticipate(
104
- (fCallback) =>
105
- {
106
- this.fable.log.info(`Wiring up Integration Adapters...`);
192
+ // Authenticate with the API server using built-in credentials
193
+ if (tmpConfig.Source && tmpConfig.Source.UserID && tmpConfig.Source.Password)
194
+ {
195
+ this.log.info('Authenticating with API server...');
196
+ this.fable.MeadowCloneRestClient.authenticate(
197
+ (pError, pResponse) =>
198
+ {
199
+ if (pError)
200
+ {
201
+ this.log.error('Error authenticating with API server.', pError);
202
+ }
203
+ else
204
+ {
205
+ this.log.info(`Authenticated with API server as [${tmpConfig.Source.UserID}] at [${tmpConfig.Source.ServerURL}]`);
206
+ }
207
+ return fStageComplete();
208
+ });
209
+ }
210
+ else
211
+ {
212
+ this.log.info('No Source credentials configured; skipping built-in authentication.');
213
+ return fStageComplete();
214
+ }
215
+ },
216
+ (fStageComplete) =>
217
+ {
218
+ // Load comprehension file
219
+ try
220
+ {
221
+ this.fable.log.info(`Loading Comprehension File...`);
222
+ tmpComprehensionPath = libPath.resolve(tmpComprehensionPath);
223
+ this.comprehension = require(tmpComprehensionPath);
224
+ return fStageComplete();
225
+ }
226
+ catch (pError)
227
+ {
228
+ this.fable.log.error(`Error loading comprehension file [${tmpComprehensionPath}]: ${pError}`, pError);
229
+ return fStageComplete(pError);
230
+ }
231
+ },
232
+ (fStageComplete) =>
233
+ {
234
+ this.fable.log.info(`Wiring up Integration Adapters...`);
107
235
 
108
- let tmpIntegrationAdapterSet = Object.keys(this.comprehension);
236
+ let tmpIntegrationAdapterSet = Object.keys(this.comprehension);
109
237
 
110
- try
111
- {
238
+ // Count total records for meta progress tracking
239
+ let tmpTotalRecords = 0;
112
240
  for (let i = 0; i < tmpIntegrationAdapterSet.length; i++)
113
241
  {
114
242
  let tmpAdapterKey = tmpIntegrationAdapterSet[i];
115
- libIntegrationAdapter.getAdapter(this.fable, tmpAdapterKey, this.getCapitalLettersAsString(tmpAdapterKey), { SimpleMarshal: true, ForceMarshal: true });
116
- this.runAdapter(tmpAnticipate, tmpAdapterKey, this.comprehension[tmpAdapterKey]);
243
+ if (this.comprehension[tmpAdapterKey] && typeof(this.comprehension[tmpAdapterKey]) === 'object')
244
+ {
245
+ tmpTotalRecords += Object.keys(this.comprehension[tmpAdapterKey]).length;
246
+ }
117
247
  }
118
- }
119
- catch (pError)
120
- {
121
- this.fable.log.error(`Error wiring up integration adapters: ${pError}`, pError);
122
- return fCallback(pError);
123
- }
124
248
 
125
- return fCallback();
126
- });
249
+ // Start meta progress tracker if interval is configured
250
+ let tmpMetaProgressHash = false;
251
+ if (tmpMetaProgressInterval > 0 && tmpTotalRecords > 0)
252
+ {
253
+ this.fable.instantiateServiceProviderIfNotExists('ProgressTrackerSet');
254
+ tmpMetaProgressHash = this.fable.getUUID();
255
+ this.fable.ProgressTrackerSet.createProgressTracker(tmpMetaProgressHash, tmpTotalRecords);
256
+ this.fable.ProgressTrackerSet.startProgressTracker(tmpMetaProgressHash);
257
+ this.fable.log.info(`Meta progress: ${tmpTotalRecords} total records across ${tmpIntegrationAdapterSet.length} entities.`);
258
+ }
127
259
 
128
- tmpAnticipate.wait(
260
+ let tmpAnticipate = this.fable.newAnticipate();
261
+
262
+ try
263
+ {
264
+ for (let i = 0; i < tmpIntegrationAdapterSet.length; i++)
265
+ {
266
+ let tmpAdapterKey = tmpIntegrationAdapterSet[i];
267
+ let tmpAdapter = libIntegrationAdapter.getAdapter(this.fable, tmpAdapterKey, this.getCapitalLettersAsString(tmpAdapterKey), tmpAdapterOptions);
268
+
269
+ // Inject the REST client
270
+ tmpAdapter.setRestClient(this.fable.MeadowCloneRestClient);
271
+
272
+ // Wire up meta progress tracking
273
+ if (tmpMetaProgressHash)
274
+ {
275
+ tmpAdapter.MetaProgressTrackerHash = tmpMetaProgressHash;
276
+ tmpAdapter.MetaProgressTrackerLogInterval = tmpMetaProgressInterval;
277
+ }
278
+
279
+ this.runAdapter(tmpAnticipate, tmpAdapterKey, this.comprehension[tmpAdapterKey]);
280
+ }
281
+ }
282
+ catch (pError)
283
+ {
284
+ this.fable.log.error(`Error wiring up integration adapters: ${pError}`, pError);
285
+ return fStageComplete(pError);
286
+ }
287
+
288
+ tmpAnticipate.wait(
289
+ (pError) =>
290
+ {
291
+ // End meta progress tracker
292
+ if (tmpMetaProgressHash)
293
+ {
294
+ this.fable.ProgressTrackerSet.endProgressTracker(tmpMetaProgressHash);
295
+ this.fable.ProgressTrackerSet.logProgressTrackerStatus(tmpMetaProgressHash);
296
+ }
297
+ return fStageComplete(pError);
298
+ });
299
+ }
300
+ ],
129
301
  (pError) =>
130
302
  {
131
303
  if (pError)
@@ -140,7 +312,7 @@ class PushComprehensionsViaIntegration extends libCLICommandLineCommand
140
312
 
141
313
  onRunAsync(fCallback)
142
314
  {
143
- return this.pushComprehension((pError)=>
315
+ return this.pushComprehension((pError) =>
144
316
  {
145
317
  return fCallback(pError);
146
318
  });
@@ -155,6 +155,52 @@ class MeadowCloneRestClient extends libFableServiceProviderBase
155
155
  });
156
156
  }
157
157
 
158
+ upsertEntities(pEntity, pRecordArray, fCallback)
159
+ {
160
+ let tmpRequestOptions = (
161
+ {
162
+ url: `${this.serverURL}${pEntity}s/Upserts`,
163
+ body: pRecordArray,
164
+ });
165
+ tmpRequestOptions = this._prepareRequestOptions(tmpRequestOptions);
166
+
167
+ this.restClient.putJSON(tmpRequestOptions,
168
+ (pError, pResponse, pBody) =>
169
+ {
170
+ if (pError)
171
+ {
172
+ this.log.error(`Error bulk upserting ${pEntity} records: ${pError.message}`);
173
+ }
174
+ return fCallback(pError, pBody);
175
+ });
176
+ }
177
+
178
+ getEntityByGUID(pEntity, pGUID, fCallback)
179
+ {
180
+ // Meadow's "ReadBy" endpoint uses the plural entity name with pagination:
181
+ // GET /{Entity}s/By/GUID{Entity}/{Value}/{Start}/{Cap}
182
+ let tmpRequestOptions = (
183
+ {
184
+ url: `${this.serverURL}${pEntity}s/By/GUID${pEntity}/${pGUID}/0/1`,
185
+ });
186
+ tmpRequestOptions = this._prepareRequestOptions(tmpRequestOptions);
187
+
188
+ return this.restClient.getJSON(tmpRequestOptions,
189
+ (pError, pResponse, pBody) =>
190
+ {
191
+ if (pError)
192
+ {
193
+ this.log.error(`Error getting ${pEntity} by GUID [${pGUID}]: ${pError.message}`);
194
+ }
195
+ // The /By/ endpoint returns an array; extract the first record for convenience.
196
+ if (Array.isArray(pBody) && pBody.length > 0)
197
+ {
198
+ return fCallback(pError, pBody[0]);
199
+ }
200
+ return fCallback(pError, pBody);
201
+ });
202
+ }
203
+
158
204
  deleteEntity(pEntity, pIDRecord, fCallback)
159
205
  {
160
206
  let tmpRequestOptions = (