meadow-integration 1.0.18 → 1.0.20
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.
- package/docs/README.md +1 -0
- package/docs/_sidebar.md +6 -1
- package/docs/comprehension-push/configuration.md +308 -0
- package/docs/integration-adapter.md +164 -15
- package/examples/example-comprehension-push.meadow.config.json +23 -0
- package/package.json +4 -2
- package/source/Meadow-Integration.js +21 -1
- package/source/Meadow-Service-Integration-Adapter.js +678 -245
- package/source/Meadow-Service-Integration-GUIDMap.js +19 -2
- package/source/cli/commands/Meadow-Integration-Command-ComprehensionPush.js +210 -38
- package/source/services/clone/Meadow-Service-RestClient.js +46 -0
- package/source/services/parser/Service-FileParser-CSV.js +263 -0
- package/source/services/parser/Service-FileParser-FixedWidth.js +158 -0
- package/source/services/parser/Service-FileParser-JSON.js +255 -0
- package/source/services/parser/Service-FileParser-XLSX.js +194 -0
- package/source/services/parser/Service-FileParser-XML.js +190 -0
- package/source/services/parser/Service-FileParser.js +142 -0
- package/test/Meadow-Integration-ComprehensionPush_test.js +580 -0
|
@@ -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.
|
|
60
|
+
if (!this.fable.MeadowGUIDMap)
|
|
34
61
|
{
|
|
35
62
|
this.fable.addAndInstantiateServiceType('MeadowGUIDMap', libGUIDMap);
|
|
36
63
|
}
|
|
37
64
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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 (
|
|
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.
|
|
91
|
+
this.EntityGUIDMarshalPrefix = `E-${this.Entity}`;
|
|
59
92
|
}
|
|
60
|
-
|
|
93
|
+
|
|
94
|
+
// Resolve GUID max length: explicit option > GUIDColumnSizes > DefaultGUIDColumnSize
|
|
95
|
+
if (this.options.GUIDMaxLength > 0)
|
|
61
96
|
{
|
|
62
|
-
this.
|
|
97
|
+
this.GUIDMaxLength = this.options.GUIDMaxLength;
|
|
63
98
|
}
|
|
64
|
-
this.
|
|
65
|
-
if (!this.EntityGUIDMarshalPrefix)
|
|
99
|
+
else if (this.options.GUIDColumnSizes && this.options.GUIDColumnSizes.hasOwnProperty(this.Entity))
|
|
66
100
|
{
|
|
67
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
: `http://localhost:8086${this.options.ApiURLPrefix}`;
|
|
139
|
+
this.client = pClient;
|
|
140
|
+
}
|
|
89
141
|
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
+
|
|
331
|
+
let tmpRecordGUID = pRecord[this.EntityGUIDName];
|
|
332
|
+
if (!tmpRecordGUID && (!pRecord.hasOwnProperty(this.EntityIDName) || !(pRecord[this.EntityIDName])))
|
|
145
333
|
{
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
399
|
+
if (tmpRecordKey == this.EntityGUIDName || tmpRecordKey == `_${this.EntityGUIDName}`)
|
|
179
400
|
{
|
|
180
|
-
// Don't do anything for the GUID
|
|
401
|
+
// Don't do anything for the GUID that's already set...
|
|
181
402
|
}
|
|
182
|
-
else if (
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
461
|
+
else if (tmpRecordKey.startsWith('_GUID'))
|
|
201
462
|
{
|
|
202
|
-
// This is a Meadow GUID.
|
|
203
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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[
|
|
509
|
+
tmpRecord[tmpRecordKey] = pSourceRecord[tmpRecordKey].substring(0, this.meadowSchema.properties[tmpRecordKey].size);
|
|
226
510
|
}
|
|
227
|
-
else if (this.meadowSchema.properties[
|
|
511
|
+
else if (this.meadowSchema.properties[tmpRecordKey].type == 'string')
|
|
228
512
|
{
|
|
229
|
-
|
|
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[
|
|
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.
|
|
533
|
+
if (tmpRecord.hasOwnProperty('DeleteDate')) delete tmpRecord.DeleteDate;
|
|
243
534
|
|
|
244
535
|
return tmpRecord;
|
|
245
536
|
}
|
|
246
537
|
|
|
247
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
609
|
+
try
|
|
610
|
+
{
|
|
611
|
+
let tmpRecordKeys = Object.keys(this._SourceRecords);
|
|
294
612
|
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
697
|
+
if (tmpErrorMessage.indexOf('exceeds the maximum allowed length') > 0)
|
|
335
698
|
{
|
|
336
|
-
this.log.error(`
|
|
699
|
+
this.log.error(`GUID length rejected by server for [${this.Entity}].[${pRecordGUID}] (${pRecordGUID.length} chars): ${tmpErrorMessage}`);
|
|
337
700
|
return fCallback();
|
|
338
701
|
}
|
|
339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
707
|
+
|
|
708
|
+
// simple delay for retries to not spam the server
|
|
709
|
+
setTimeout(() =>
|
|
355
710
|
{
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
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
|
-
|
|
778
|
+
|
|
779
|
+
if (Array.isArray(pBody))
|
|
392
780
|
{
|
|
393
|
-
//
|
|
394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
848
|
+
this.log.info(`[${this.Entity}] Starting push of ${tmpRecordKeys.length} records...`);
|
|
849
|
+
tmpProgressTracker.logProgressTrackerStatus(this.MetaProgressTrackerHash);
|
|
850
|
+
}
|
|
443
851
|
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
448
|
-
for (let
|
|
858
|
+
let tmpBatchRecordKeys = [];
|
|
859
|
+
for (let j = 0; j < this.options.BulkUpsertBatchSize; j++)
|
|
449
860
|
{
|
|
450
|
-
let
|
|
451
|
-
|
|
861
|
+
let tmpRecordKey = tmpRecordKeys[(i * this.options.BulkUpsertBatchSize) + j];
|
|
862
|
+
if (tmpRecordKey)
|
|
452
863
|
{
|
|
453
|
-
|
|
454
|
-
if (tmpRecordKey)
|
|
455
|
-
{
|
|
456
|
-
tmpBatchRecordKeys.push(tmpRecordKey);
|
|
457
|
-
}
|
|
864
|
+
tmpBatchRecordKeys.push(tmpRecordKey);
|
|
458
865
|
}
|
|
866
|
+
}
|
|
459
867
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
886
|
+
}
|
|
887
|
+
else
|
|
888
|
+
{
|
|
889
|
+
for (let i = 0; i < tmpRecordKeys.length; i++)
|
|
477
890
|
{
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|