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.
@@ -9,6 +9,30 @@ const defaultMeadowIntegrationAdapterOptions = (
9
9
 
10
10
  "EntityGUIDMarshalPrefix": false,
11
11
 
12
+ // Maximum allowed length for generated GUIDs.
13
+ // When 0, the adapter falls back to DefaultGUIDColumnSize.
14
+ // When a positive integer, this explicit value overrides everything.
15
+ "GUIDMaxLength": 0,
16
+
17
+ // Schema-driven per-entity GUID column sizes.
18
+ // Keys are entity names, values are max GUID column sizes.
19
+ // Pass the full map in options or let it default to empty.
20
+ "GUIDColumnSizes": {},
21
+
22
+ // Default GUID column size when not available in GUIDColumnSizes.
23
+ "DefaultGUIDColumnSize": 36,
24
+
25
+ // When false (default), the adapter will throw an error if a generated GUID
26
+ // exceeds the maximum allowed length.
27
+ // When true, the prefix is truncated to fit while preserving the full external GUID.
28
+ "AllowGUIDTruncation": false,
29
+
30
+ // When true, only marshal fields that are present in the schema (no passthrough of unknown fields).
31
+ "SimpleMarshal": false,
32
+
33
+ // When true, pass through all fields regardless of schema presence.
34
+ "ForceMarshal": false,
35
+
12
36
  "PerformUpserts": true,
13
37
  "PerformDeletes": true,
14
38
 
@@ -17,6 +41,9 @@ const defaultMeadowIntegrationAdapterOptions = (
17
41
  "RecordThresholdForBulkUpsert": 1000,
18
42
  "BulkUpsertBatchSize": 100,
19
43
 
44
+ // How often (in records) to log per-entity progress (0 = disabled).
45
+ "ProgressLogInterval": 100,
46
+
20
47
  "ApiURLPrefix": '/1.0/'
21
48
  });
22
49
 
@@ -30,22 +57,16 @@ class MeadowIntegrationAdapter extends libFableServiceProviderBase
30
57
  this.serviceType = 'IntegrationAdapter';
31
58
 
32
59
  // Check if there is a GUIDMap .. if not make one
33
- if (!this.fable.hasOwnProperty('MeadowGUIDMap'))
60
+ if (!this.fable.MeadowGUIDMap)
34
61
  {
35
62
  this.fable.addAndInstantiateServiceType('MeadowGUIDMap', libGUIDMap);
36
63
  }
37
64
 
38
- // Check if there is a REST client ... if not make one
39
- if (!this.fable.hasOwnProperty('RestClient'))
40
- {
41
- this.fable.addServiceType('RestClient', );
42
- if (!this.options.hasOwnProperty('ApiURLPrefix'))
43
- {
44
- this.options.ApiURLPrefix = '/1.0/';
45
- }
46
- this.fable.instantiateServiceProvider('RestClient', { ServerURL: this.options.ApiURLPrefix });
47
- }
48
- this.fable.EntityProvider.options.urlPrefix = this.getServerURL();
65
+ // REST client: prefer explicit injection, then MeadowCloneRestClient, then
66
+ // fall back to creating a bare RestClient as the original code did.
67
+ this.client = this.options.Client || null;
68
+
69
+ this.fable.instantiateServiceProviderIfNotExists('ProgressTrackerSet');
49
70
 
50
71
  this.Entity = this.options.Entity;
51
72
  this.EntityGUIDName = `GUID${this.Entity}`;
@@ -53,19 +74,37 @@ class MeadowIntegrationAdapter extends libFableServiceProviderBase
53
74
 
54
75
  // Automagic GUID Components
55
76
  this.AdapterSetGUIDMarshalPrefix = this.options.AdapterSetGUIDMarshalPrefix;
56
- if (!this.AdapterSetGUIDMarshalPrefix && (typeof(this.fable.settings.AdapterSetGUIDMarshalPrefix) == 'string'))
77
+ if (typeof this.AdapterSetGUIDMarshalPrefix !== 'string')
78
+ {
79
+ if (typeof this.fable.settings.AdapterSetGUIDMarshalPrefix === 'string')
80
+ {
81
+ this.AdapterSetGUIDMarshalPrefix = this.fable.settings.AdapterSetGUIDMarshalPrefix;
82
+ }
83
+ else
84
+ {
85
+ this.AdapterSetGUIDMarshalPrefix = 'INTG-DEF';
86
+ }
87
+ }
88
+ this.EntityGUIDMarshalPrefix = this.options.EntityGUIDMarshalPrefix;
89
+ if (typeof this.EntityGUIDMarshalPrefix !== 'string')
57
90
  {
58
- this.AdapterSetGUIDMarshalPrefix = this.fable.settings.AdapterSetGUIDMarshalPrefix;
91
+ this.EntityGUIDMarshalPrefix = `E-${this.Entity}`;
59
92
  }
60
- else
93
+
94
+ // Resolve GUID max length: explicit option > GUIDColumnSizes > DefaultGUIDColumnSize
95
+ if (this.options.GUIDMaxLength > 0)
61
96
  {
62
- this.AdapterSetGUIDMarshalPrefix = 'INTG-DEF';
97
+ this.GUIDMaxLength = this.options.GUIDMaxLength;
63
98
  }
64
- this.EntityGUIDMarshalPrefix = this.options.EntityGUIDMarshalPrefix
65
- if (!this.EntityGUIDMarshalPrefix)
99
+ else if (this.options.GUIDColumnSizes && this.options.GUIDColumnSizes.hasOwnProperty(this.Entity))
66
100
  {
67
- this.EntityGUIDMarshalPrefix = `E-${this.Entity}`;
101
+ this.GUIDMaxLength = this.options.GUIDColumnSizes[this.Entity];
102
+ }
103
+ else
104
+ {
105
+ this.GUIDMaxLength = this.options.DefaultGUIDColumnSize;
68
106
  }
107
+ this.AllowGUIDTruncation = this.options.AllowGUIDTruncation;
69
108
 
70
109
  // Integration Adapter Controls
71
110
  this._PerformUpserts = this.options.PerformUpserts;
@@ -73,6 +112,13 @@ class MeadowIntegrationAdapter extends libFableServiceProviderBase
73
112
 
74
113
  this._RecordPushRetryThreshold = this.options.RecordPushRetryThreshold;
75
114
 
115
+ // Meta progress tracker — an optional external progress tracker hash that is
116
+ // incremented alongside the per-entity tracker so callers can monitor overall
117
+ // progress across multiple adapters/entities.
118
+ this.MetaProgressTrackerHash = false;
119
+ // How often (in records) to log the meta progress tracker status (0 = never auto-log).
120
+ this.MetaProgressTrackerLogInterval = 0;
121
+
76
122
  // The source records (coming from the external system)
77
123
  this._SourceRecords = {};
78
124
 
@@ -81,35 +127,171 @@ class MeadowIntegrationAdapter extends libFableServiceProviderBase
81
127
  this._DeletedRecords = {};
82
128
  }
83
129
 
84
- getServerURL()
130
+ /**
131
+ * Set the REST client for this adapter.
132
+ * The client should expose: upsertEntity, upsertEntities, getEntityByGUID,
133
+ * getEntity, deleteEntity, getJSON, and have a serverURL + restClient property.
134
+ *
135
+ * @param {object} pClient - The REST client instance (e.g. MeadowCloneRestClient)
136
+ */
137
+ setRestClient(pClient)
85
138
  {
86
- let tmpServerURL = ((typeof(this.fable.RestClient.serverURL) == 'string') && (this.fable.RestClient.serverURL.length > 0)) ? this.fable.RestClient.serverURL
87
- : ((typeof(this.options.ServerURL) == 'string') && (this.options.ServerURL.length > 0)) ? this.options.ServerURL
88
- : `http://localhost:8086${this.options.ApiURLPrefix}`;
139
+ this.client = pClient;
140
+ }
89
141
 
90
- return tmpServerURL;
142
+ /**
143
+ * Resolve the REST client to use. Checks (in order):
144
+ * 1. Explicitly set client via setRestClient / options.Client
145
+ * 2. MeadowCloneRestClient on fable
146
+ * 3. EntityProvider on fable (legacy)
147
+ *
148
+ * @returns {object} A REST client instance
149
+ */
150
+ _resolveClient()
151
+ {
152
+ if (this.client)
153
+ {
154
+ return this.client;
155
+ }
156
+ if (this.fable.MeadowCloneRestClient)
157
+ {
158
+ return this.fable.MeadowCloneRestClient;
159
+ }
160
+ if (this.fable.EntityProvider)
161
+ {
162
+ return this.fable.EntityProvider;
163
+ }
164
+ this.log.error(`No REST client available for Integration Adapter [${this.Entity}]. Call setRestClient() or ensure MeadowCloneRestClient is registered.`);
165
+ return null;
91
166
  }
92
167
 
93
- // TODO: A More Elegane Streaming Solution (tm)
168
+ /**
169
+ * The combined GUID prefix string.
170
+ *
171
+ * @returns {string} The GUID prefix
172
+ */
173
+ get GUIDPrefix()
174
+ {
175
+ let tmpPrefix = '';
176
+ if (this.AdapterSetGUIDMarshalPrefix)
177
+ {
178
+ tmpPrefix += `${this.AdapterSetGUIDMarshalPrefix}-`;
179
+ }
180
+ if (this.EntityGUIDMarshalPrefix)
181
+ {
182
+ tmpPrefix += `${this.EntityGUIDMarshalPrefix}-`;
183
+ }
184
+ return tmpPrefix;
185
+ }
186
+
187
+ /**
188
+ * Generate a Meadow GUID from an external system GUID.
189
+ *
190
+ * If the generated GUID would exceed GUIDMaxLength and AllowGUIDTruncation
191
+ * is false (the default), an error is thrown so the integration stops immediately.
192
+ *
193
+ * When AllowGUIDTruncation is true, the prefix is progressively truncated to
194
+ * fit while preserving the full external GUID.
195
+ *
196
+ * @param {string} pExternalGUID - The external system GUID
197
+ * @returns {string} The generated Meadow GUID
198
+ */
199
+ generateMeadowGUIDFromExternalGUID(pExternalGUID)
200
+ {
201
+ let tmpFullGUID = `${this.GUIDPrefix}${pExternalGUID}`;
202
+
203
+ if (this.GUIDMaxLength > 0 && tmpFullGUID.length > this.GUIDMaxLength)
204
+ {
205
+ if (!this.AllowGUIDTruncation)
206
+ {
207
+ let tmpMessage = `Generated GUID for [${this.Entity}] exceeds the maximum allowed length of ${this.GUIDMaxLength} characters.\n`
208
+ + ` Comprehension GUID: [${pExternalGUID}] (${pExternalGUID.length} chars)\n`
209
+ + ` Server GUID: [${tmpFullGUID}] (${tmpFullGUID.length} chars)\n`
210
+ + ` Prefix: [${this.GUIDPrefix}] (${this.GUIDPrefix.length} chars)\n`
211
+ + ` To allow automatic prefix truncation for one-time imports, set AllowGUIDTruncation to true (CLI: --allowguidtruncation).`;
212
+ this.log.error(tmpMessage);
213
+ throw new Error(tmpMessage);
214
+ }
215
+
216
+ // AllowGUIDTruncation is on — the external GUID is sacrosanct, so truncate the prefix instead.
217
+ let tmpAvailablePrefixLength = this.GUIDMaxLength - pExternalGUID.length;
218
+
219
+ if (tmpAvailablePrefixLength <= 0)
220
+ {
221
+ // External GUID alone meets or exceeds the limit; drop the prefix entirely.
222
+ this.log.warn(`External GUID [${pExternalGUID}] for [${this.Entity}] is ${pExternalGUID.length} characters which meets or exceeds the GUID max length of ${this.GUIDMaxLength}. Using external GUID with no prefix.`);
223
+ tmpFullGUID = pExternalGUID.substring(0, this.GUIDMaxLength);
224
+ }
225
+ else
226
+ {
227
+ let tmpTruncatedPrefix = this.GUIDPrefix.substring(0, tmpAvailablePrefixLength);
228
+ tmpFullGUID = `${tmpTruncatedPrefix}${pExternalGUID}`;
229
+ this.log.warn(`Generated GUID for [${this.Entity}] would be ${this.GUIDPrefix.length + pExternalGUID.length} characters (limit ${this.GUIDMaxLength}); prefix truncated from [${this.GUIDPrefix}] to [${tmpTruncatedPrefix}].`);
230
+ }
231
+ }
232
+
233
+ return tmpFullGUID;
234
+ }
235
+
236
+ /**
237
+ * Integrate records: fetch schema, marshal, push.
238
+ *
239
+ * @param {(error?: Error) => void} fCallback - Callback when done
240
+ * @param {(source: any, marshalled: any) => void} [fMarshalExtraData] - Optional per-record extra marshal function
241
+ */
94
242
  integrateRecords(fCallback, fMarshalExtraData)
95
243
  {
96
244
  let tmpMarshalExtraData = fMarshalExtraData;
97
245
  let tmpAnticipate = this.fable.newAnticipate();
246
+ let tmpClient = this._resolveClient();
247
+
248
+ if (!tmpClient)
249
+ {
250
+ return fCallback(new Error(`No REST client available for [${this.Entity}].`));
251
+ }
252
+
98
253
  tmpAnticipate.anticipate(
99
- (fStageComplete)=>
254
+ (fStageComplete) =>
100
255
  {
101
256
  this.fable.log.info(`Getting schema for ${this.Entity}....`);
102
- let tmpRequestOptions = (
103
- {
104
- url: `${this.getServerURL()}${this.Entity}/Schema`
105
- });
106
- //tmpRequestOptions = this.fable.RestClient._prepareRequestOptions(tmpRequestOptions);
107
- return this.fable.RestClient.getJSON(tmpRequestOptions,
108
- (pError, pResponse, pBody) =>
257
+
258
+ let tmpSchemaURL = `${this.Entity}/Schema`;
259
+
260
+ tmpClient.getJSON(tmpSchemaURL,
261
+ (pError, pBody) =>
109
262
  {
110
- if (pBody && (typeof(pBody) == 'object'))
263
+ // getJSON on MeadowCloneRestClient returns (pError, pResponse, pBody)
264
+ // but some clients return (pError, pBody). Handle both:
265
+ let tmpBody = pBody;
266
+ if (arguments.length >= 3)
111
267
  {
112
- this.meadowSchema = pBody;
268
+ tmpBody = arguments[2];
269
+ }
270
+
271
+ if (tmpBody && (typeof(tmpBody) == 'object'))
272
+ {
273
+ this.meadowSchema = tmpBody;
274
+
275
+ // Override the GUID column size from the live server schema when
276
+ // the caller has not explicitly set a positive GUIDMaxLength option.
277
+ if (this.options.GUIDMaxLength <= 0 && Array.isArray(tmpBody.Columns))
278
+ {
279
+ let tmpGuidColumn = tmpBody.Columns.find((c) => c.Column === this.EntityGUIDName);
280
+ if (tmpGuidColumn && Number(tmpGuidColumn.Size) > 0)
281
+ {
282
+ let tmpServerSize = Number(tmpGuidColumn.Size);
283
+ if (tmpServerSize !== this.GUIDMaxLength)
284
+ {
285
+ this.log.info(`Server schema GUID column size for [${this.Entity}] is ${tmpServerSize} (local had ${this.GUIDMaxLength}); using server value.`);
286
+ }
287
+ else
288
+ {
289
+ this.log.trace(`Server schema confirms GUID column size for [${this.Entity}]: ${tmpServerSize}`);
290
+ }
291
+ this.GUIDMaxLength = tmpServerSize;
292
+ }
293
+ }
294
+
113
295
  return fStageComplete(pError);
114
296
  }
115
297
  else
@@ -119,13 +301,13 @@ class MeadowIntegrationAdapter extends libFableServiceProviderBase
119
301
  });
120
302
  });
121
303
  tmpAnticipate.anticipate(
122
- (fStageComplete)=>
304
+ (fStageComplete) =>
123
305
  {
124
306
  this.fable.log.info(`Marshaling ${this.Entity} records....`);
125
307
  this.marshalSourceRecords(fStageComplete, tmpMarshalExtraData);
126
308
  });
127
309
  tmpAnticipate.anticipate(
128
- (fStageComplete)=>
310
+ (fStageComplete) =>
129
311
  {
130
312
  this.fable.log.info(`Posting ${this.Entity} records....`);
131
313
  this.pushRecordsToServer(fStageComplete);
@@ -133,81 +315,178 @@ class MeadowIntegrationAdapter extends libFableServiceProviderBase
133
315
  tmpAnticipate.wait(fCallback);
134
316
  }
135
317
 
136
- // Add a record to the adapter's Source Records buffer to be pushed
318
+ /**
319
+ * Add a record to the adapter's Source Records buffer to be pushed.
320
+ *
321
+ * @param {Object} pRecord - The record to add
322
+ */
137
323
  addSourceRecord(pRecord)
138
324
  {
139
- if (typeof(pRecord) !== 'object')
325
+ if (!pRecord || typeof(pRecord) !== 'object' || Array.isArray(pRecord))
140
326
  {
141
- this.log.error(`Passed-in record was not of type "object" (${typeof(pRecord)}), therefore it was not added to the Source Record buffer.`);
327
+ this.log.error(`Passed-in record was null, or not of type "object" (${typeof(pRecord)}), therefore it was not added to the Source Record buffer.`);
142
328
  return false;
143
329
  }
144
- if (!pRecord.hasOwnProperty(this.EntityGUIDName) || !(pRecord[this.EntityGUIDName]))
330
+
331
+ let tmpRecordGUID = pRecord[this.EntityGUIDName];
332
+ if (!tmpRecordGUID && (!pRecord.hasOwnProperty(this.EntityIDName) || !(pRecord[this.EntityIDName])))
145
333
  {
146
- this.log.error(`Passed-in record did not contain a source system GUID data element [${this.Entity}].[${this.EntityGUIDName}], therefore it was not added to the Source Record Buffer:`, pRecord);
147
- return false;
334
+ tmpRecordGUID = pRecord[`_${this.EntityGUIDName}`];
335
+ if (!tmpRecordGUID)
336
+ {
337
+ this.log.error(`Passed-in record did not contain a source system GUID data element [${this.Entity}].[${this.EntityGUIDName}], therefore it was not added to the Source Record Buffer:`, pRecord);
338
+ return false;
339
+ }
148
340
  }
149
341
 
150
- this._SourceRecords[pRecord[this.EntityGUIDName]] = pRecord;
151
- }
152
-
153
- generateMeadowGUIDFromExternalGUID(pExternalGUID)
154
- {
155
- return `${this.AdapterSetGUIDMarshalPrefix}-${this.EntityGUIDMarshalPrefix}-${pExternalGUID}`;
342
+ if (tmpRecordGUID)
343
+ {
344
+ this._SourceRecords[tmpRecordGUID] = pRecord;
345
+ }
346
+ else
347
+ {
348
+ this._SourceRecords[pRecord[this.EntityIDName]] = pRecord;
349
+ }
156
350
  }
157
351
 
158
- marshalRecord(pSourceRecord)
352
+ /**
353
+ * Marshal a source record to a Meadow record (async).
354
+ *
355
+ * Handles:
356
+ * - GUID prefix generation and validation/truncation
357
+ * - External GUID mapping (GUID* fields → ID lookup from session map)
358
+ * - Server GUID mapping (_GUID* fields → async server lookup)
359
+ * - _Dest_IDEntity_*_Via_* pattern for explicit FK destination fields
360
+ * - Schema-based field type/length enforcement
361
+ * - SimpleMarshal and ForceMarshal options
362
+ *
363
+ * @param {Object} pSourceRecord - The source record to marshal
364
+ * @returns {Promise<Object>} The marshaled record
365
+ */
366
+ async marshalRecord(pSourceRecord)
159
367
  {
160
368
  let tmpRecord = {};
369
+ let tmpClient = this._resolveClient();
161
370
 
162
371
  // Create the new GUID
372
+ let tmpRecordMeadowGUID;
163
373
  let tmpRecordExternalGUID = pSourceRecord[this.EntityGUIDName];
164
- let tmpRecordInternalMeadowGUID = this.generateMeadowGUIDFromExternalGUID(tmpRecordExternalGUID);
374
+ if (tmpRecordExternalGUID)
375
+ {
376
+ tmpRecordMeadowGUID = this.generateMeadowGUIDFromExternalGUID(tmpRecordExternalGUID);
377
+ }
378
+ else
379
+ {
380
+ tmpRecordExternalGUID = pSourceRecord[`_${this.EntityGUIDName}`];
381
+ tmpRecordMeadowGUID = tmpRecordExternalGUID;
382
+ }
165
383
 
166
- // Mapping table for going between internal Meadow system GUIDs and External system GUIDs.
167
- this.fable.MeadowGUIDMap.mapExternalGUIDtoMeadowGUID(this.Entity, tmpRecordExternalGUID, tmpRecordInternalMeadowGUID);
384
+ if (!tmpRecordMeadowGUID && !pSourceRecord.hasOwnProperty(this.EntityIDName))
385
+ {
386
+ throw new Error(`Could not marshal record for [${this.Entity}] because no external system GUID was found in source record.`);
387
+ }
168
388
 
169
- tmpRecord[this.EntityGUIDName] = tmpRecordInternalMeadowGUID;
389
+ if (tmpRecordMeadowGUID)
390
+ {
391
+ // Mapping table for going between Meadow system GUIDs and External system GUIDs.
392
+ tmpRecord[this.EntityGUIDName] = tmpRecordMeadowGUID;
393
+ this.fable.MeadowGUIDMap.mapExternalGUIDtoMeadowGUID(this.Entity, tmpRecordExternalGUID, tmpRecordMeadowGUID);
394
+ }
170
395
 
171
396
  // Now that we've dealt with basic identifiers, time to see if there are other Mapped GUIDs to look up.
172
- // TODO: This can be the path through to a recursive integration -- if
173
- // the GUIDs are not found, then the appropriate adapter can launch
174
- // (and integrate just the proper records!)
175
- let tmpRecordKeys = Object.keys(pSourceRecord);
176
- for (let i = 0; i < tmpRecordKeys.length; i++)
397
+ for (const tmpRecordKey of Object.keys(pSourceRecord))
177
398
  {
178
- if (tmpRecordKeys[i] == this.EntityGUIDName)
399
+ if (tmpRecordKey == this.EntityGUIDName || tmpRecordKey == `_${this.EntityGUIDName}`)
179
400
  {
180
- // Don't do anything for the GUID whose already set...
401
+ // Don't do anything for the GUID that's already set...
181
402
  }
182
- else if (tmpRecordKeys[i].startsWith('GUID'))
403
+ else if (tmpRecordKey.startsWith('_Dest_IDEntity_') && tmpRecordKey.includes('_Via_'))
404
+ {
405
+ // This is a destination-explicit FK with a GUID to resolve via server lookup.
406
+ let tmpMappedEntityGUIDName = tmpRecordKey.split('_Via_')[1];
407
+ let tmpMappedEntityName = tmpMappedEntityGUIDName.substring(4);
408
+ let tmpMappedEntityGUIDValue = pSourceRecord[tmpRecordKey];
409
+ let tmpMeadowIDValue = await new Promise((resolve) =>
410
+ {
411
+ this.fable.MeadowGUIDMap.getIDFromGUIDAsync(tmpMappedEntityName, tmpMappedEntityGUIDValue,
412
+ (pError, pResponse) =>
413
+ {
414
+ if (pResponse)
415
+ {
416
+ resolve(pResponse);
417
+ }
418
+ else
419
+ {
420
+ if (pError)
421
+ {
422
+ this.fable.log.warn(`Error getting Meadow id for ${tmpMappedEntityName} GUID [${tmpMappedEntityGUIDValue}]: ${pError.message || pError}`, { Stack: pError.stack });
423
+ }
424
+ resolve(0);
425
+ }
426
+ }, tmpClient);
427
+ });
428
+ if (tmpMeadowIDValue)
429
+ {
430
+ const tmpIDDestinationField = tmpRecordKey.split('_Via_')[0].split('_Dest_IDEntity_')[1];
431
+ tmpRecord[tmpIDDestinationField] = tmpMeadowIDValue;
432
+ }
433
+ else
434
+ {
435
+ this.fable.log.warn(`Could not find Meadow ID for [${tmpMappedEntityName}] with GUID [${tmpMappedEntityGUIDValue}] while integrating [${this.Entity}] record [${tmpRecord[this.EntityGUIDName]}].`)
436
+ }
437
+ }
438
+ else if (tmpRecordKey.startsWith('_Dest_'))
439
+ {
440
+ // skip this, it's a destination field override for a GUID
441
+ }
442
+ else if (tmpRecordKey.startsWith('GUID'))
183
443
  {
184
444
  // This is an external system GUID
185
445
  // Because external system GUIDs require adapters to look up, it should be mapped if the tree traversal worked.
186
- let tmpMappedEntityExternalGUIDName = tmpRecordKeys[i];
446
+ let tmpMappedEntityExternalGUIDName = tmpRecordKey;
187
447
  let tmpMappedEntityName = tmpMappedEntityExternalGUIDName.substring(4);
188
448
  let tmpMappedEntityExternalGUIDValue = pSourceRecord[tmpMappedEntityExternalGUIDName];
189
449
 
190
450
  let tmpMeadowIDValue = this.fable.MeadowGUIDMap.getMeadowIDFromExternalGUID(tmpMappedEntityName, tmpMappedEntityExternalGUIDValue);
191
451
  if (tmpMeadowIDValue)
192
452
  {
193
- tmpRecord[`ID${tmpMappedEntityName}`] = tmpMeadowIDValue;
453
+ const tmpIDDestinationField = pSourceRecord[`_Dest_${tmpRecordKey}`] || `ID${tmpMappedEntityName}`;
454
+ tmpRecord[tmpIDDestinationField] = tmpMeadowIDValue;
194
455
  }
195
456
  else
196
457
  {
197
458
  this.fable.log.warn(`Could not find Meadow ID for [${tmpMappedEntityName}] with External GUID [${tmpMappedEntityExternalGUIDValue}] while integrating [${this.Entity}] record [${tmpRecord[this.EntityGUIDName]}].`)
198
459
  }
199
460
  }
200
- else if (tmpRecordKeys[i].startsWith('_GUID'))
461
+ else if (tmpRecordKey.startsWith('_GUID'))
201
462
  {
202
- // This is a Meadow GUID.
203
- // TODO: Eventualy this whole codepath needs to be async so it can look up records.
204
- let tmpMappedEntityGUIDName = tmpRecordKeys[i];
463
+ // This is a Meadow GUID that needs async server lookup.
464
+ let tmpMappedEntityGUIDName = tmpRecordKey;
205
465
  let tmpMappedEntityName = tmpMappedEntityGUIDName.substring(5);
206
466
  let tmpMappedEntityGUIDValue = pSourceRecord[tmpMappedEntityGUIDName];
207
-
208
- let tmpMeadowIDValue = this.fable.MeadowGUIDMap.getIDFromGuid(tmpMappedEntityName, tmpMappedEntityGUIDValue);
467
+ let tmpMeadowIDValue = await new Promise((resolve) =>
468
+ {
469
+ this.fable.MeadowGUIDMap.getIDFromGUIDAsync(tmpMappedEntityName, tmpMappedEntityGUIDValue,
470
+ (pError, pResponse) =>
471
+ {
472
+ if (pResponse)
473
+ {
474
+ resolve(pResponse);
475
+ }
476
+ else
477
+ {
478
+ if (pError)
479
+ {
480
+ this.fable.log.warn(`Error getting Meadow id for ${tmpMappedEntityName} GUID [${tmpMappedEntityGUIDValue}]: ${pError.message || pError}`, { Stack: pError.stack });
481
+ }
482
+ resolve(0);
483
+ }
484
+ }, tmpClient);
485
+ });
209
486
  if (tmpMeadowIDValue)
210
487
  {
488
+ const tmpIDDestinationField = pSourceRecord[`_Dest_${tmpRecordKey}`] || `ID${tmpMappedEntityName}`;
489
+ tmpRecord[tmpIDDestinationField] = tmpMeadowIDValue;
211
490
  tmpRecord[`ID${tmpMappedEntityName}`] = tmpMeadowIDValue;
212
491
  }
213
492
  else
@@ -215,36 +494,55 @@ class MeadowIntegrationAdapter extends libFableServiceProviderBase
215
494
  this.fable.log.warn(`Could not find Meadow ID for [${tmpMappedEntityName}] with GUID [${tmpMappedEntityGUIDValue}] while integrating [${this.Entity}] record [${tmpRecord[this.EntityGUIDName]}].`)
216
495
  }
217
496
  }
218
- else if ((this.meadowSchema && this.meadowSchema.hasOwnProperty('properties')) && (this.meadowSchema.properties.hasOwnProperty(tmpRecordKeys[i])))
497
+ else if ((this.meadowSchema && this.meadowSchema.hasOwnProperty('properties')) && (this.meadowSchema.properties.hasOwnProperty(tmpRecordKey)))
219
498
  {
220
499
  // Check the length if it's a string -- truncate if it isn't there for now.
221
- // TODO: MAKE THIS CONFIGURABLE AND OVERRIDABLE
222
- if ((this.meadowSchema.properties[tmpRecordKeys[i]].type == 'string')
223
- && (pSourceRecord[tmpRecordKeys[i]].toString().length > this.meadowSchema.properties[tmpRecordKeys[i]].size))
500
+ if (this.options.SimpleMarshal)
501
+ {
502
+ tmpRecord[tmpRecordKey] = pSourceRecord[tmpRecordKey];
503
+ }
504
+ else if ((this.meadowSchema.properties[tmpRecordKey].type == 'string')
505
+ && pSourceRecord.hasOwnProperty(tmpRecordKey)
506
+ && (pSourceRecord[tmpRecordKey] != null)
507
+ && (pSourceRecord[tmpRecordKey].toString().length > this.meadowSchema.properties[tmpRecordKey].size))
224
508
  {
225
- tmpRecord[tmpRecordKeys[i]] = pSourceRecord[tmpRecordKeys[i]].substring(0, this.meadowSchema.properties[tmpRecordKeys[i]].size);
509
+ tmpRecord[tmpRecordKey] = pSourceRecord[tmpRecordKey].substring(0, this.meadowSchema.properties[tmpRecordKey].size);
226
510
  }
227
- else if (this.meadowSchema.properties[tmpRecordKeys[i]].type == 'string')
511
+ else if (this.meadowSchema.properties[tmpRecordKey].type == 'string')
228
512
  {
229
- tmpRecord[tmpRecordKeys[i]] = pSourceRecord[tmpRecordKeys[i]].toString();
513
+ if ((pSourceRecord[tmpRecordKey] !== null) && (pSourceRecord[tmpRecordKey] !== undefined))
514
+ {
515
+ tmpRecord[tmpRecordKey] = pSourceRecord[tmpRecordKey].toString();
516
+ }
230
517
  }
231
518
  else
232
519
  {
233
- tmpRecord[tmpRecordKeys[i]] = pSourceRecord[tmpRecordKeys[i]];
520
+ tmpRecord[tmpRecordKey] = pSourceRecord[tmpRecordKey];
234
521
  }
235
522
  }
523
+ else if (this.options.ForceMarshal)
524
+ {
525
+ tmpRecord[tmpRecordKey] = pSourceRecord[tmpRecordKey];
526
+ }
236
527
  }
237
528
 
238
529
  // Clean any elements in the record that are reserved by Meadow
239
530
  if (tmpRecord.hasOwnProperty('CreateDate')) delete tmpRecord.CreateDate;
240
531
  if (tmpRecord.hasOwnProperty('UpdateDate')) delete tmpRecord.UpdateDate;
241
532
  if (tmpRecord.hasOwnProperty('Deleted')) delete tmpRecord.Deleted;
242
- if (tmpRecord.hasOwnProperty('DeleteDate')) delete tmpRecord.UpdateDate;
533
+ if (tmpRecord.hasOwnProperty('DeleteDate')) delete tmpRecord.DeleteDate;
243
534
 
244
535
  return tmpRecord;
245
536
  }
246
537
 
247
- marshalSingleSourceRecord(pSourceRecordGUID, fMarshalExtraData)
538
+ /**
539
+ * Marshal a single source record (async).
540
+ *
541
+ * @param {string} pSourceRecordGUID - The GUID of the source record
542
+ * @param {(source: any, marshalled: any) => void} [fMarshalExtraData] - Optional extra marshal function
543
+ * @returns {Promise<boolean>}
544
+ */
545
+ async marshalSingleSourceRecord(pSourceRecordGUID, fMarshalExtraData)
248
546
  {
249
547
  // 0. Get the original GUID
250
548
  let tmpSourceRecordGUID = pSourceRecordGUID;
@@ -252,20 +550,33 @@ class MeadowIntegrationAdapter extends libFableServiceProviderBase
252
550
  // 0.3 Get the Source Record
253
551
  let tmpSourceRecord = this._SourceRecords[tmpSourceRecordGUID];
254
552
 
553
+ if (!tmpSourceRecord)
554
+ {
555
+ this.log.fatal(`Could not marshal source record for [${this.Entity}] because source record with GUID [${tmpSourceRecordGUID}] was not found in Source Records buffer.`);
556
+ return;
557
+ }
558
+
255
559
  // 0.5 Check if this is a delete
256
560
  let tmpDeleteOperation = (tmpSourceRecord.Deleted === true);
257
561
 
258
- // 1. Marshal the Source record into a Meadow entity record
259
- let tmpMarshaledRecord = this.marshalRecord(tmpSourceRecord);
562
+ // 1. Marshal the Source record into a Meadow record
563
+ let tmpMarshaledRecord = await this.marshalRecord(tmpSourceRecord);
260
564
 
261
565
  // 2. Get the GUID of the record after Marshaling...
262
- let tmpMarshaledRecordGUID = tmpMarshaledRecord[this.EntityGUIDName];
566
+ let tmpMarshaledRecordGUID = tmpMarshaledRecord[this.EntityIDName];
567
+ if (tmpMarshaledRecord.hasOwnProperty(this.EntityGUIDName))
568
+ {
569
+ tmpMarshaledRecordGUID = tmpMarshaledRecord[this.EntityGUIDName];
570
+ }
571
+ else if (tmpMarshaledRecord.hasOwnProperty(`_${this.EntityGUIDName}`))
572
+ {
573
+ tmpMarshaledRecordGUID = tmpMarshaledRecord[`_${this.EntityGUIDName}`];
574
+ }
263
575
 
264
576
  // 3. Get a new Object or the existing object as start of the append operation
265
577
  let tmpOriginalRecord = (this._MarshaledRecords.hasOwnProperty(tmpMarshaledRecordGUID)) ? this._MarshaledRecords[tmpMarshaledRecordGUID] : {};
266
578
 
267
579
  // 4. Now merge the properties of the two records, using the new one as the more important values
268
- // Yes this will guaranteed overwrite the GUID like a zillion times, but, whatevers
269
580
  if (tmpDeleteOperation)
270
581
  {
271
582
  this._DeletedRecords[tmpMarshaledRecordGUID] = tmpMarshaledRecord;
@@ -287,84 +598,160 @@ class MeadowIntegrationAdapter extends libFableServiceProviderBase
287
598
  return true;
288
599
  }
289
600
 
290
- // Take all Source Records in the buffer and marshal them
291
- marshalSourceRecords(fCallback, fMarshalExtraData)
601
+ /**
602
+ * Take all Source Records in the buffer and marshal them (async).
603
+ *
604
+ * @param {(error?: Error) => void} fCallback - Callback when done
605
+ * @param {(source: any, marshalled: any) => void} [fMarshalExtraData] - Optional extra marshal function
606
+ */
607
+ async marshalSourceRecords(fCallback, fMarshalExtraData)
292
608
  {
293
- let tmpRecordKeys = Object.keys(this._SourceRecords);
609
+ try
610
+ {
611
+ let tmpRecordKeys = Object.keys(this._SourceRecords);
294
612
 
295
- // Later switch to something paralleler?
296
- for (let i = 0; i < tmpRecordKeys.length; i++)
613
+ for (let i = 0; i < tmpRecordKeys.length; i++)
614
+ {
615
+ await this.marshalSingleSourceRecord(tmpRecordKeys[i], fMarshalExtraData);
616
+ }
617
+
618
+ return fCallback();
619
+ }
620
+ catch (error)
297
621
  {
298
- this.marshalSingleSourceRecord(tmpRecordKeys[i], fMarshalExtraData);
622
+ return fCallback(error);
299
623
  }
300
-
301
- return fCallback();
302
624
  }
303
625
 
626
+ /**
627
+ * Take a single record and push it to the server.
628
+ *
629
+ * @param {(error?: Error) => void} fCallback - Callback when done
630
+ * @param {string} pRecordGUID - The GUID of the record to push
631
+ * @param {number} [pRetryCount] - The number of times this record has been retried
632
+ */
304
633
  upsertSingleRecord(fCallback, pRecordGUID, pRetryCount)
305
634
  {
306
- // TODO: The retry count recursion method here is very brute force. Fix it!
307
635
  let tmpRetryCount = (typeof(pRetryCount) === 'undefined') ? 0 : pRetryCount;
636
+ let tmpClient = this._resolveClient();
637
+
638
+ if (!tmpClient)
639
+ {
640
+ return fCallback(new Error(`No REST client available for [${this.Entity}].`));
641
+ }
308
642
 
309
643
  // Non-configurable cap for a bit here... until better logic is written.
310
644
  if ((tmpRetryCount > this._RecordPushRetryThreshold) || (tmpRetryCount > 50))
311
645
  {
312
646
  this.log.error(`Upsert error sending [${this.Entity}].[${pRecordGUID}] to server... retry threshold of ${this._RecordPushRetryThreshold} reached.`);
647
+
648
+ // upsert failed — try to read the record so we can at least have a valid ID / GUID mapping
649
+ if (typeof(tmpClient.getEntityByGUID) === 'function')
650
+ {
651
+ let tmpIdentifierType = 'GUID';
652
+ const tmpFallbackCallback = (pError, pBody) =>
653
+ {
654
+ if (pError)
655
+ {
656
+ this.log.error(`Error reading record ${tmpIdentifierType} [${pRecordGUID}] after upsert failures: ${pError.message || pError}`, { Stack: pError.stack });
657
+ }
658
+ else if (pBody &&
659
+ (pBody.hasOwnProperty(this.EntityIDName)) &&
660
+ (pBody[this.EntityIDName] > 0) &&
661
+ (pBody.hasOwnProperty(this.EntityGUIDName)) &&
662
+ ((pBody[this.EntityGUIDName] === pRecordGUID) || (pBody[this.EntityIDName] == pRecordGUID))
663
+ )
664
+ {
665
+ this.log.info(`Fallback: Loaded and mapping record ${tmpIdentifierType} [${pRecordGUID}] after upsert failure.`);
666
+ this.fable.MeadowGUIDMap.mapGUIDToID(this.Entity, pBody[this.EntityGUIDName], pBody[this.EntityIDName]);
667
+ }
668
+ else
669
+ {
670
+ this.log.error(`Could not verify record ${tmpIdentifierType} [${pRecordGUID}] after upsert failures; record not found or invalid response.`, { Record: pBody });
671
+ }
672
+ return fCallback();
673
+ };
674
+ if (this._MarshaledRecords[pRecordGUID] && this._MarshaledRecords[pRecordGUID][this.EntityIDName] == pRecordGUID)
675
+ {
676
+ tmpIdentifierType = 'ID';
677
+ return tmpClient.getEntity(this.Entity, pRecordGUID, tmpFallbackCallback);
678
+ }
679
+ return tmpClient.getEntityByGUID(this.Entity, pRecordGUID, tmpFallbackCallback);
680
+ }
681
+
313
682
  return fCallback();
314
683
  }
315
684
 
316
- this.fable.EntityProvider.upsertEntity(this.Entity, this._MarshaledRecords[pRecordGUID],
317
- (pError, pBody)=>
685
+ tmpClient.upsertEntity(this.Entity, this._MarshaledRecords[pRecordGUID],
686
+ (pError, pBody) =>
318
687
  {
319
688
  if (pError)
320
689
  {
321
690
  this.log.error(`Error sending PUT [${this.Entity}].[${pRecordGUID}] to server: ${pError}`);
322
- this.upsertSingleRecord(fCallback, pRecordGUID, tmpRetryCount++);
323
- }
324
- else
325
- {
326
- // TODO: Deal with the odd old API service errors
327
- if (pBody.hasOwnProperty('Error') || pBody.hasOwnProperty('code'))
691
+ let tmpErrorMessage = (typeof(pError.message) === 'string') ? pError.message : String(pError);
692
+ if (tmpErrorMessage.indexOf('Error in DAL create: Error: Duplicate entry') > 0)
328
693
  {
329
- let tmpProblemMessage = (pBody.hasOwnProperty('Error')) ? pBody.Error : pBody.code;
330
- this.log.error(`Error sending PUT [${this.Entity}].[${pRecordGUID}] to server: ${tmpProblemMessage}`, pBody);
331
- // TODO: Should this blow up or not.......
694
+ this.log.warn(`Duplicate record attempted when sending PUT to server record GUID [${pRecordGUID}]: ${tmpErrorMessage}`, pBody);
332
695
  return fCallback();
333
696
  }
334
- else if (tmpRetryCount > this._RecordPushRetryThreshold)
697
+ if (tmpErrorMessage.indexOf('exceeds the maximum allowed length') > 0)
335
698
  {
336
- this.log.error(`Retry count exceeded max of ${this._RecordPushRetryThreshold} retries (${tmpRetryCount} retries happened) while sending PUT [${this.Entity}].[${pRecordGUID}] to server: ${pBody.Error}`, pBody);
699
+ this.log.error(`GUID length rejected by server for [${this.Entity}].[${pRecordGUID}] (${pRecordGUID.length} chars): ${tmpErrorMessage}`);
337
700
  return fCallback();
338
701
  }
339
- else if (
340
- // Check that the server returned a valid record (look for the Identity column)
341
- (pBody.hasOwnProperty(this.EntityIDName)) &&
342
- // Check that the server returned a record that has a numeric ID which is nonzero
343
- (pBody[this.EntityIDName] > 0) &&
344
- // Check that the server also returned a GUID
345
- (pBody.hasOwnProperty(this.EntityGUIDName)) &&
346
- // Check that the GUID matches what we expect
347
- pBody[this.EntityGUIDName] === pRecordGUID
348
- )
702
+ if (tmpErrorMessage.indexOf('Rejected Create') > -1 && tmpErrorMessage.indexOf('GUID') > -1)
349
703
  {
350
- // Add the record ID to the lookup table
351
- this.fable.MeadowGUIDMap.mapGUIDToID(this.Entity, pRecordGUID, pBody[this.EntityIDName]);
704
+ this.log.error(`Server rejected create for [${this.Entity}].[${pRecordGUID}] due to GUID issue: ${tmpErrorMessage}`);
352
705
  return fCallback();
353
706
  }
354
- else
707
+
708
+ // simple delay for retries to not spam the server
709
+ setTimeout(() =>
355
710
  {
356
- // Try again...
357
- this.log.error(`Problem sending PUT [${this.Entity}].[${pRecordGUID}] to server. Incrementing retry count and trying again.`, pBody);
358
- this.upsertSingleRecord(fCallback, pRecordGUID, tmpRetryCount++);
359
- }
711
+ this.upsertSingleRecord(fCallback, pRecordGUID, tmpRetryCount + 1);
712
+ }, 500);
713
+ return;
714
+ }
715
+
716
+ if (
717
+ pBody &&
718
+ // Check that the server returned a valid record (look for the Identity column)
719
+ (pBody.hasOwnProperty(this.EntityIDName)) &&
720
+ // Check that the server returned a record that has a numeric ID which is nonzero
721
+ (pBody[this.EntityIDName] > 0) &&
722
+ // Check that the server also returned a GUID
723
+ (pBody.hasOwnProperty(this.EntityGUIDName)) &&
724
+ // Check that the GUID or ID matches what we expect
725
+ ((pBody[this.EntityGUIDName] === pRecordGUID) || (pBody[this.EntityIDName] == pRecordGUID))
726
+ )
727
+ {
728
+ // Add the record ID to the lookup table
729
+ this.fable.MeadowGUIDMap.mapGUIDToID(this.Entity, pBody[this.EntityGUIDName], pBody[this.EntityIDName]);
730
+ return fCallback();
360
731
  }
732
+
733
+ // Try again...
734
+ this.log.error(`Problem sending PUT [${this.Entity}].[${pRecordGUID}] to server. Incrementing retry count and trying again.`, { Record: pBody, RetryCount: tmpRetryCount });
735
+ this.upsertSingleRecord(fCallback, pRecordGUID, tmpRetryCount + 1);
361
736
  });
362
737
  }
363
738
 
739
+ /**
740
+ * Take a set of records and push them to the server in bulk.
741
+ *
742
+ * @param {(error?: Error) => void} fCallback - Callback when done
743
+ * @param {string[]} pRecordGUIDs - The GUIDs of the records to push
744
+ * @param {number} [pRetryCount] - The number of times this batch has been retried
745
+ */
364
746
  upsertBulkRecords(fCallback, pRecordGUIDs, pRetryCount)
365
747
  {
366
- // TODO: The retry count recursion method here is very brute force. Fix it!
367
748
  let tmpRetryCount = (typeof(pRetryCount) === 'undefined') ? 0 : pRetryCount;
749
+ let tmpClient = this._resolveClient();
750
+
751
+ if (!tmpClient)
752
+ {
753
+ return fCallback(new Error(`No REST client available for [${this.Entity}].`));
754
+ }
368
755
 
369
756
  // Non-configurable cap for a bit here... until better logic is written.
370
757
  if ((tmpRetryCount > this._RecordPushRetryThreshold) || (tmpRetryCount > 50))
@@ -380,159 +767,198 @@ class MeadowIntegrationAdapter extends libFableServiceProviderBase
380
767
  tmpRecordsToUpsert.push(this._MarshaledRecords[pRecordGUIDs[i]]);
381
768
  }
382
769
 
383
- this.fable.EntityProvider.upsertEntities(this.Entity, tmpRecordsToUpsert,
384
- (pError, pBody)=>
770
+ tmpClient.upsertEntities(this.Entity, tmpRecordsToUpsert,
771
+ (pError, pBody) =>
385
772
  {
386
773
  if (pError)
387
774
  {
388
775
  this.log.error(`Error sending PUT [${this.Entity}] to server: ${pError}`);
389
- this.upsertBulkRecords(fCallback, pRecordGUIDs, tmpRetryCount++);
776
+ return this.upsertBulkRecords(fCallback, pRecordGUIDs, tmpRetryCount + 1);
390
777
  }
391
- else
778
+
779
+ if (Array.isArray(pBody))
392
780
  {
393
- // TODO: Deal with the odd old API service errors
394
- if (pBody.hasOwnProperty('Error') || pBody.hasOwnProperty('code'))
395
- {
396
- let tmpProblemMessage = (pBody.hasOwnProperty('Error')) ? pBody.Error : pBody.code;
397
- this.log.error(`Error sending PUT [${this.Entity}] to server: ${tmpProblemMessage}`, pBody);
398
- // TODO: Should this blow up or not.......
399
- return fCallback();
400
- }
401
- else if (tmpRetryCount > this._RecordPushRetryThreshold)
402
- {
403
- this.log.error(`Retry count exceeded max of ${this._RecordPushRetryThreshold} retries (${tmpRetryCount} retries happened) while sending PUT [${this.Entity}] to server: ${pBody.Error}`, pBody);
404
- return fCallback();
405
- }
406
- else if (Array.isArray(pBody))
407
- // TODO: This is a bit of a hack... but it's a good start
408
- // // Check that the server returned a valid record (look for the Identity column)
409
- // (pBody.hasOwnProperty(this.EntityIDName)) &&
410
- // // Check that the server returned a record that has a numeric ID which is nonzero
411
- // (pBody[this.EntityIDName] > 0) &&
412
- // // Check that the server also returned a GUID
413
- // (pBody.hasOwnProperty(this.EntityGUIDName)) &&
414
- // // Check that the GUID matches what we expect
415
- // pBody[this.EntityGUIDName] === pRecordGUID
416
- {
417
- // Add the record IDs to the lookup table
418
- for (let i = 0; i < pBody.length; i++)
419
- {
420
- this.fable.MeadowGUIDMap.mapGUIDToID(this.Entity, pBody[i][this.EntityGUIDName], pBody[i][this.EntityIDName]);
421
- }
422
- return fCallback();
423
- }
424
- else
781
+ // Add the record IDs to the lookup table
782
+ for (let i = 0; i < pBody.length; i++)
425
783
  {
426
- // Try again...
427
- this.log.error(`Problem sending PUT [${this.Entity}] to server. Incrementing retry count and trying again.`, pBody);
428
- this.upsertBulkRecords(fCallback, pRecordGUIDs, tmpRetryCount++);
784
+ this.fable.MeadowGUIDMap.mapGUIDToID(this.Entity, pBody[i][this.EntityGUIDName], pBody[i][this.EntityIDName]);
429
785
  }
786
+ return fCallback();
430
787
  }
788
+
789
+ // Try again...
790
+ this.log.error(`Problem sending PUT [${this.Entity}] to server. Incrementing retry count and trying again.`, { Records: pBody, RetryCount: tmpRetryCount });
791
+ this.upsertBulkRecords(fCallback, pRecordGUIDs, tmpRetryCount + 1);
431
792
  });
432
793
  }
433
794
 
795
+ /**
796
+ * Increment the meta progress tracker (if configured) and conditionally log its status.
797
+ *
798
+ * Uses a threshold-crossing check rather than exact modulo so that bulk increments
799
+ * (e.g. +100) reliably trigger a log when they cross an interval boundary.
800
+ *
801
+ * @param {number} pAmount - The number of operations to increment by
802
+ */
803
+ _incrementMetaProgressTracker(pAmount)
804
+ {
805
+ if (!this.MetaProgressTrackerHash)
806
+ {
807
+ return;
808
+ }
809
+ let tmpTracker = this.fable.ProgressTrackerSet;
810
+ let tmpStatus = tmpTracker.incrementProgressTracker(this.MetaProgressTrackerHash, pAmount);
434
811
 
435
- // Push any records in the adapter buffer to the server
812
+ if (this.MetaProgressTrackerLogInterval > 0 && tmpStatus)
813
+ {
814
+ let tmpPreviousCount = tmpStatus.CurrentCount - pAmount;
815
+ let tmpPreviousInterval = Math.floor(tmpPreviousCount / this.MetaProgressTrackerLogInterval);
816
+ let tmpCurrentInterval = Math.floor(tmpStatus.CurrentCount / this.MetaProgressTrackerLogInterval);
817
+
818
+ if (tmpCurrentInterval > tmpPreviousInterval
819
+ || tmpStatus.CurrentCount >= tmpStatus.TotalCount)
820
+ {
821
+ tmpTracker.logProgressTrackerStatus(this.MetaProgressTrackerHash);
822
+ }
823
+ }
824
+ }
825
+
826
+ /**
827
+ * Push any records in the adapter buffer to the server.
828
+ *
829
+ * @param {(error?: Error) => void} fCallback - Callback when done
830
+ */
436
831
  pushRecordsToServer(fCallback)
437
832
  {
438
- if (this._PerformUpserts)
833
+ if (!this._PerformUpserts)
834
+ {
835
+ return fCallback();
836
+ }
837
+ // Run the upserts...
838
+ let tmpRecordKeys = Object.keys(this._MarshaledRecords);
839
+ let tmpAnticipate = this.fable.instantiateServiceProviderWithoutRegistration('Anticipate');
840
+ let tmpProgressTrackerGUID = this.fable.getUUID();
841
+ let tmpProgressTracker = this.fable.ProgressTrackerSet;
842
+ tmpProgressTracker.createProgressTracker(tmpProgressTrackerGUID, tmpRecordKeys.length);
843
+ tmpProgressTracker.startProgressTracker(tmpProgressTrackerGUID);
844
+
845
+ // Log meta tracker at the start of each entity
846
+ if (this.MetaProgressTrackerHash)
439
847
  {
440
- // Run the upserts...
441
- let tmpRecordKeys = Object.keys(this._MarshaledRecords);
442
- let tmpAnticipate = this.fable.instantiateServiceProviderWithoutRegistration('Anticipate');
848
+ this.log.info(`[${this.Entity}] Starting push of ${tmpRecordKeys.length} records...`);
849
+ tmpProgressTracker.logProgressTrackerStatus(this.MetaProgressTrackerHash);
850
+ }
443
851
 
444
- // Determine if bulk upserts are possible
445
- if (tmpRecordKeys.length > this.options.RecordThresholdForBulkUpsert)
852
+ // Determine if bulk upserts are possible
853
+ if (tmpRecordKeys.length > this.options.RecordThresholdForBulkUpsert)
854
+ {
855
+ let tmpBulkUpsertBatchCount = Math.ceil(tmpRecordKeys.length / this.options.BulkUpsertBatchSize);
856
+ for (let i = 0; i < tmpBulkUpsertBatchCount; i++)
446
857
  {
447
- let tmpBulkUpsertBatchCount = Math.ceil(tmpRecordKeys.length / this.options.BulkUpsertBatchSize);
448
- for (let i = 0; i < tmpBulkUpsertBatchCount; i++)
858
+ let tmpBatchRecordKeys = [];
859
+ for (let j = 0; j < this.options.BulkUpsertBatchSize; j++)
449
860
  {
450
- let tmpBatchRecordKeys = [];
451
- for (let j = 0; j < this.options.BulkUpsertBatchSize; j++)
861
+ let tmpRecordKey = tmpRecordKeys[(i * this.options.BulkUpsertBatchSize) + j];
862
+ if (tmpRecordKey)
452
863
  {
453
- let tmpRecordKey = tmpRecordKeys[(i * this.options.BulkUpsertBatchSize) + j];
454
- if (tmpRecordKey)
455
- {
456
- tmpBatchRecordKeys.push(tmpRecordKey);
457
- }
864
+ tmpBatchRecordKeys.push(tmpRecordKey);
458
865
  }
866
+ }
459
867
 
460
- tmpAnticipate.anticipate(
461
- function(fDone)
462
- {
463
- this.log.trace(`[${this.Entity}] Bulk Upserting ${tmpBatchRecordKeys.length} records to server...`);
464
- this.upsertBulkRecords(fDone, tmpBatchRecordKeys,
465
- (pError, pBody)=>
868
+ tmpAnticipate.anticipate(
869
+ function(fDone)
870
+ {
871
+ this.log.trace(`[${this.Entity}] Bulk Upserting ${tmpBatchRecordKeys.length} records to server for transaction set [${tmpProgressTrackerGUID}]...`);
872
+ this.upsertBulkRecords(
873
+ (pError, pBody) =>
874
+ {
875
+ if (pError)
466
876
  {
467
- if (pError)
468
- {
469
- this.log.error(`Error sending Bulk Upserts for [${this.Entity}] to server: ${pError}`);
470
- }
471
- return fDone();
472
- });
473
- }.bind(this));
474
- }
877
+ this.log.error(`Error sending Bulk Upserts for [${this.Entity}] to server: ${pError}`);
878
+ }
879
+ tmpProgressTracker.incrementProgressTracker(tmpProgressTrackerGUID, tmpBatchRecordKeys.length);
880
+ tmpProgressTracker.logProgressTrackerStatus(tmpProgressTrackerGUID);
881
+ this._incrementMetaProgressTracker(tmpBatchRecordKeys.length);
882
+ return fDone();
883
+ }, tmpBatchRecordKeys);
884
+ }.bind(this));
475
885
  }
476
- else
886
+ }
887
+ else
888
+ {
889
+ for (let i = 0; i < tmpRecordKeys.length; i++)
477
890
  {
478
- for (let i = 0; i < tmpRecordKeys.length; i++)
479
- {
480
- let tmpRecordKey = tmpRecordKeys[i];
481
- tmpAnticipate.anticipate(
482
- function(fDone)
483
- {
484
- this.log.trace(`[${this.Entity}] Record [${tmpRecordKey}] pushing to server...`);
485
- this.upsertSingleRecord(fDone, tmpRecordKey);
486
- }.bind(this));
487
- }
891
+ let tmpRecordKey = tmpRecordKeys[i];
892
+ tmpAnticipate.anticipate(
893
+ function(fDone)
894
+ {
895
+ this.log.trace(`[${this.Entity}] Record [${tmpRecordKey}] pushing to server...`);
896
+ tmpProgressTracker.incrementProgressTracker(tmpProgressTrackerGUID, 1);
897
+ tmpProgressTracker.logProgressTrackerStatus(tmpProgressTrackerGUID);
898
+ this.upsertSingleRecord(
899
+ () =>
900
+ {
901
+ this._incrementMetaProgressTracker(1);
902
+ return fDone();
903
+ }, tmpRecordKey);
904
+ }.bind(this));
488
905
  }
489
-
490
- tmpAnticipate.wait(fCallback);
491
906
  }
907
+
908
+ tmpAnticipate.wait(fCallback);
492
909
  }
493
910
 
911
+ /**
912
+ * Delete records from the server.
913
+ *
914
+ * @param {(error?: Error) => void} fCallback - Callback when done
915
+ */
494
916
  deleteRecordsFromServer(fCallback)
495
917
  {
496
- if (this._PerformDeletes)
918
+ if (!this._PerformDeletes)
497
919
  {
498
- // Run the deletes...
499
- // TODO: THIS IS DANGEROUS
500
- let tmpRecordKeys = Object.keys(this._DeletedRecords);
501
- libAsync.eachSeries(tmpRecordKeys,
502
- (pRecordGUID, fDeleteComplete) =>
503
- {
504
- this.log.trace(`[${this.Entity}] Record [${this._MarshaledRecords[this.EntityGUIDName]}] deleting from server...`);
505
- // Now lookup the entity ID for this GUID...
506
- // TODO: Should this be overridable by entity?
507
- this.fable.RestClient.getEntityByGUID(this.Entity, pRecordGUID,
508
- (pReadError, pReadBody)=>
920
+ return fCallback();
921
+ }
922
+
923
+ let tmpClient = this._resolveClient();
924
+ if (!tmpClient)
925
+ {
926
+ return fCallback(new Error(`No REST client available for [${this.Entity}].`));
927
+ }
928
+
929
+ let tmpRecordKeys = Object.keys(this._DeletedRecords);
930
+ this.fable.Utility.eachLimit(tmpRecordKeys, 1,
931
+ (pRecordGUID, fDeleteComplete) =>
932
+ {
933
+ this.log.trace(`[${this.Entity}] Record [${pRecordGUID}] deleting from server...`);
934
+ // Now lookup the Meadow ID for it...
935
+ tmpClient.getEntityByGUID(this.Entity, pRecordGUID,
936
+ (pReadError, pReadBody) =>
937
+ {
938
+ if (pReadError)
509
939
  {
510
- if (pReadError)
511
- {
512
- this.log.warning(`Could not read [${this.Entity}] GUID [${pRecordGUID}] for DELETE operation: ${pReadError}`);
513
- return fDeleteComplete();
514
- }
940
+ this.log.warn(`Could not read [${this.Entity}] GUID [${pRecordGUID}] for DELETE operation: ${pReadError}`);
941
+ return fDeleteComplete();
942
+ }
515
943
 
516
- if (pReadBody && pReadBody.hasOwnProperty(this.EntityIDName))
517
- {
518
- this._API.deleteEntity(this.Entity, pReadBody[this.EntityIDName], fDeleteComplete);
519
- }
520
- else
521
- {
522
- this.log.warning(`Could not delete [${this.Entity}] GUID [${pRecordGUID}] because Meadow Entity lookup did not return an IDRecord.`);
523
- return fDeleteComplete();
524
- }
525
- });
526
- },
527
- (pError)=>
944
+ if (pReadBody && pReadBody.hasOwnProperty(this.EntityIDName))
945
+ {
946
+ tmpClient.deleteEntity(this.Entity, pReadBody[this.EntityIDName], fDeleteComplete);
947
+ return;
948
+ }
949
+
950
+ this.log.warn(`Could not delete [${this.Entity}] GUID [${pRecordGUID}] because lookup did not return an IDRecord.`);
951
+ return fDeleteComplete();
952
+ });
953
+ },
954
+ (pError) =>
955
+ {
956
+ if (pError)
528
957
  {
529
- if (pError)
530
- {
531
- this.log.error(`Error sending trying to Delete from [${this.Entity}]: ${pError}`);
532
- }
533
- return fCallback(pError);
534
- });
535
- }
958
+ this.log.error(`Error sending trying to Delete from [${this.Entity}]: ${pError}`);
959
+ }
960
+ return fCallback(pError);
961
+ });
536
962
  }
537
963
  }
538
964
 
@@ -540,7 +966,13 @@ module.exports = MeadowIntegrationAdapter;
540
966
 
541
967
  // Macro for backwards compatibility
542
968
  module.exports.getAdapter = (
543
- function(pFable, pEntity, pEntityPrefix)
969
+ /**
970
+ * @param {object} pFable - A fable instance
971
+ * @param {string} pEntity - The entity name
972
+ * @param {string} [pEntityPrefix] - The entity GUID marshal prefix
973
+ * @param {object} [pCustomOptions] - Additional options to merge in (e.g. SimpleMarshal, ForceMarshal)
974
+ */
975
+ function(pFable, pEntity, pEntityPrefix, pCustomOptions)
544
976
  {
545
977
  if (pFable.servicesMap.IntegrationAdapter && pFable.servicesMap.IntegrationAdapter.hasOwnProperty(pEntity))
546
978
  {
@@ -548,8 +980,9 @@ module.exports.getAdapter = (
548
980
  }
549
981
  else
550
982
  {
551
- return pFable.instantiateServiceProvider('IntegrationAdapter', { Entity: pEntity, EntityGUIDMarshalPrefix: pEntityPrefix }, pEntity);
983
+ const tmpOptions = Object.assign({}, pCustomOptions, { Entity: pEntity, EntityGUIDMarshalPrefix: pEntityPrefix });
984
+ return pFable.instantiateServiceProvider('IntegrationAdapter', tmpOptions, pEntity);
552
985
  }
553
986
  });
554
987
 
555
- module.exports.default_configuration = defaultMeadowIntegrationAdapterOptions;
988
+ module.exports.default_configuration = defaultMeadowIntegrationAdapterOptions;