meadow-integration 1.0.21 → 1.0.24

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meadow-integration",
3
- "version": "1.0.21",
3
+ "version": "1.0.24",
4
4
  "description": "Meadow Data Integration",
5
5
  "bin": {
6
6
  "mdwint": "source/cli/Meadow-Integration-CLI-Run.js"
@@ -258,15 +258,12 @@ class MeadowIntegrationAdapter extends libFableServiceProviderBase
258
258
  let tmpSchemaURL = `${this.Entity}/Schema`;
259
259
 
260
260
  tmpClient.getJSON(tmpSchemaURL,
261
- (pError, pBody) =>
261
+ (pError, pResponse, pParsedBody) =>
262
262
  {
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)
267
- {
268
- tmpBody = arguments[2];
269
- }
263
+ // getJSON returns (pError, pResponse, pParsedBody).
264
+ // Use the parsed body (3rd arg) if present; fall back to
265
+ // pResponse for clients that return (pError, pBody) only.
266
+ let tmpBody = (typeof(pParsedBody) === 'object') ? pParsedBody : pResponse;
270
267
 
271
268
  if (tmpBody && (typeof(tmpBody) == 'object'))
272
269
  {
@@ -1,72 +1,455 @@
1
1
  /*
2
2
  Unit tests for Retold Integration Adapter
3
+
4
+ Validates that the integration adapter correctly fetches the remote
5
+ schema, marshals source records, and upserts them to the server.
6
+
7
+ Uses a mock HTTP server to simulate meadow-endpoints API responses.
3
8
  */
4
9
 
5
10
  const Chai = require('chai');
6
11
  const Expect = Chai.expect;
7
12
 
13
+ const libHTTP = require('http');
8
14
  const libPict = require('pict');
9
15
  const libIntegrationAdapter = require('../source/Meadow-Service-Integration-Adapter.js');
16
+ const libMeadowCloneRestClient = require('../source/services/clone/Meadow-Service-RestClient.js');
10
17
 
11
- suite
12
- (
13
- 'Integration Adapter Basic',
14
- () =>
18
+ // ── Test Constants ──────────────────────────────────────────────────────────────
19
+
20
+ const MOCK_PORT = 18199;
21
+ const MOCK_BASE_URL = `http://localhost:${MOCK_PORT}/1.0/`;
22
+
23
+ // ── Mock Schema ─────────────────────────────────────────────────────────────────
24
+ // This is what the Schema endpoint returns — a JSON schema with properties.
25
+
26
+ const MOCK_SCHEMA =
27
+ {
28
+ title: 'TestEntity',
29
+ type: 'object',
30
+ properties:
31
+ {
32
+ IDTestEntity: { type: 'integer' },
33
+ GUIDTestEntity: { type: 'string', size: 128 },
34
+ Name: { type: 'string', size: 200 },
35
+ Value: { type: 'integer' }
36
+ },
37
+ required: ['IDTestEntity']
38
+ };
39
+
40
+ // ── Mock Server State ───────────────────────────────────────────────────────────
41
+
42
+ let _MockState =
43
+ {
44
+ NextID: 1,
45
+ UpsertedRecords: [],
46
+ StoredRecords: {}
47
+ };
48
+
49
+ function resetMockState()
50
+ {
51
+ _MockState.NextID = 1;
52
+ _MockState.UpsertedRecords = [];
53
+ _MockState.StoredRecords = {};
54
+ }
55
+
56
+ // ── Mock HTTP Server ────────────────────────────────────────────────────────────
57
+ // Simulates meadow-endpoints API responses for the TestEntity entity.
58
+
59
+ function createMockServer()
60
+ {
61
+ return libHTTP.createServer(
62
+ (pRequest, pResponse) =>
15
63
  {
16
- setup(() => { });
64
+ let tmpURL = pRequest.url;
65
+ let tmpBody = '';
17
66
 
18
- suite
19
- (
20
- 'Basic Tests',
67
+ pResponse.setHeader('Content-Type', 'application/json');
68
+
69
+ pRequest.on('data',
70
+ (pChunk) =>
71
+ {
72
+ tmpBody += pChunk;
73
+ });
74
+
75
+ pRequest.on('end',
76
+ () =>
77
+ {
78
+ // GET /1.0/TestEntity/Schema
79
+ if (pRequest.method === 'GET' && tmpURL.match(/\/1\.0\/TestEntity\/Schema/))
80
+ {
81
+ pResponse.end(JSON.stringify(MOCK_SCHEMA));
82
+ return;
83
+ }
84
+
85
+ // PUT /1.0/TestEntity/Upsert
86
+ if (pRequest.method === 'PUT' && tmpURL.match(/\/1\.0\/TestEntity\/Upsert/))
87
+ {
88
+ let tmpRecord = {};
89
+ try
90
+ {
91
+ tmpRecord = JSON.parse(tmpBody);
92
+ }
93
+ catch (pError)
94
+ {
95
+ pResponse.statusCode = 400;
96
+ pResponse.end(JSON.stringify({ Error: 'Invalid JSON' }));
97
+ return;
98
+ }
99
+
100
+ // Assign an auto-incremented ID if not present or zero
101
+ if (!tmpRecord.IDTestEntity || tmpRecord.IDTestEntity === 0)
102
+ {
103
+ tmpRecord.IDTestEntity = _MockState.NextID++;
104
+ }
105
+
106
+ // Store and track the upserted record
107
+ _MockState.UpsertedRecords.push(JSON.parse(JSON.stringify(tmpRecord)));
108
+ _MockState.StoredRecords[tmpRecord.GUIDTestEntity] = tmpRecord;
109
+
110
+ pResponse.end(JSON.stringify(tmpRecord));
111
+ return;
112
+ }
113
+
114
+ // GET /1.0/TestEntitys/By/GUIDTestEntity/{guid}/0/1
115
+ if (pRequest.method === 'GET' && tmpURL.match(/\/1\.0\/TestEntitys\/By\/GUIDTestEntity\//))
116
+ {
117
+ let tmpParts = tmpURL.split('/');
118
+ // URL pattern: /1.0/TestEntitys/By/GUIDTestEntity/{guid}/0/1
119
+ let tmpGUID = tmpParts[5];
120
+ let tmpRecord = _MockState.StoredRecords[tmpGUID];
121
+ if (tmpRecord)
122
+ {
123
+ pResponse.end(JSON.stringify([tmpRecord]));
124
+ }
125
+ else
126
+ {
127
+ pResponse.end(JSON.stringify([]));
128
+ }
129
+ return;
130
+ }
131
+
132
+ // Fallback — 404
133
+ pResponse.statusCode = 404;
134
+ pResponse.end(JSON.stringify({ Error: `Unknown endpoint: ${pRequest.method} ${tmpURL}` }));
135
+ });
136
+ });
137
+ }
138
+
139
+ suite
140
+ (
141
+ 'Integration Adapter Basic',
142
+ () =>
143
+ {
144
+ setup(() => { });
145
+
146
+ suite
147
+ (
148
+ 'Basic Tests',
149
+ () =>
150
+ {
151
+ test(
152
+ 'Object Instantiation',
153
+ (fDone) =>
154
+ {
155
+ let _Fable = new libPict();
156
+ _Fable.addServiceType('IntegrationAdapter', libIntegrationAdapter);
157
+ let tmpIntegrationAdapter = _Fable.instantiateServiceProvider('IntegrationAdapter', { Entity: 'TestEntity' }, 'TestEntity');
158
+ Expect(tmpIntegrationAdapter).to.be.an('object');
159
+ return fDone();
160
+ });
161
+ }
162
+ );
163
+ }
164
+ );
165
+
166
+ suite
167
+ (
168
+ 'Integration Adapter with Mock Server',
169
+ () =>
170
+ {
171
+ let _MockServer = null;
172
+
173
+ suiteSetup
174
+ (
175
+ (fDone) =>
176
+ {
177
+ _MockServer = createMockServer();
178
+ _MockServer.listen(MOCK_PORT,
21
179
  () =>
22
180
  {
23
- test(
24
- 'Object Instantiation',
25
- (fDone) =>
26
- {
27
- let _Fable = new libPict();
28
- _Fable.addServiceType('IntegrationAdapter', libIntegrationAdapter);
29
- let tmpIntegrationAdapter = _Fable.instantiateServiceProvider('IntegrationAdapter', { Entity: 'TestEntity' }, 'TestEntity');
30
- Expect(tmpIntegrationAdapter).to.be.an('object');
181
+ return fDone();
182
+ });
183
+ }
184
+ );
185
+
186
+ suiteTeardown
187
+ (
188
+ (fDone) =>
189
+ {
190
+ if (_MockServer)
191
+ {
192
+ _MockServer.close(fDone);
193
+ }
194
+ else
195
+ {
196
+ return fDone();
197
+ }
198
+ }
199
+ );
200
+
201
+ setup
202
+ (
203
+ () =>
204
+ {
205
+ resetMockState();
206
+ }
207
+ );
208
+
209
+ // ── Schema Fetch ────────────────────────────────────────────────────
210
+
211
+ suite
212
+ (
213
+ 'Schema Fetch',
214
+ () =>
215
+ {
216
+ test
217
+ (
218
+ 'meadowSchema should be populated after integrateRecords',
219
+ (fDone) =>
220
+ {
221
+ let tmpFable = new libPict(
222
+ {
223
+ Product: 'AdapterTest',
224
+ ProductVersion: '1.0.0',
225
+ LogStreams: [{ streamtype: 'console', level: 'error' }]
226
+ });
227
+ tmpFable.addServiceType('IntegrationAdapter', libIntegrationAdapter);
228
+ tmpFable.serviceManager.addServiceType('MeadowCloneRestClient', libMeadowCloneRestClient);
229
+ tmpFable.serviceManager.instantiateServiceProvider('MeadowCloneRestClient',
230
+ {
231
+ ServerURL: MOCK_BASE_URL
232
+ });
233
+
234
+ let tmpAdapter = tmpFable.instantiateServiceProvider('IntegrationAdapter',
235
+ {
236
+ Entity: 'TestEntity',
237
+ SimpleMarshal: true
238
+ }, 'TestEntity');
239
+
240
+ tmpAdapter.addSourceRecord({ GUIDTestEntity: 'test-schema-1', Name: 'SchemaCheck', Value: 1 });
241
+
242
+ // Before integration, meadowSchema should not be set
243
+ Expect(tmpAdapter.meadowSchema).to.not.be.ok;
244
+
245
+ tmpAdapter.integrateRecords(
246
+ (pError) =>
247
+ {
248
+ Expect(pError).to.not.exist;
249
+
250
+ // After integration, meadowSchema should be populated
251
+ Expect(tmpAdapter.meadowSchema).to.be.an('object');
252
+ Expect(tmpAdapter.meadowSchema).to.have.property('properties');
253
+ Expect(tmpAdapter.meadowSchema.properties).to.have.property('Name');
254
+ Expect(tmpAdapter.meadowSchema.properties).to.have.property('Value');
255
+ Expect(tmpAdapter.meadowSchema.properties).to.have.property('IDTestEntity');
256
+ Expect(tmpAdapter.meadowSchema.properties).to.have.property('GUIDTestEntity');
257
+
31
258
  return fDone();
32
259
  });
33
- /* This works if you have the right database set up; will adapt for real tests later when time arises
34
- test(
35
- 'Integrate some Book Prices',
36
- (fDone) =>
37
- {
38
- let _Fable = new libFable();
39
- _Fable.addServiceType('IntegrationAdapter', libIntegrationAdapter);
40
- let tmpIntegrationAdapter = _Fable.instantiateServiceProvider('IntegrationAdapter', { Entity: 'BookPrice' }, 'BookPrice');
41
-
42
- tmpIntegrationAdapter.addSourceRecord({ GUIDBookPrice:'GUID-3', CouponCode:'TestyCoupon', Price:3.50 });
43
- tmpIntegrationAdapter.addSourceRecord({ GUIDBookPrice:'GUID-4', CouponCode:'TestyCoupon', Price:3.22232 });
44
- tmpIntegrationAdapter.addSourceRecord({ GUIDBookPrice:'GUID-5', CouponCode:'None', Price:3.50 });
45
- tmpIntegrationAdapter.addSourceRecord({ GUIDBookPrice:'GUID-6', CouponCode:'', Price:3.57 });
46
- tmpIntegrationAdapter.addSourceRecord({ GUIDBookPrice:'GUID-7', Price:3.50 });
47
- tmpIntegrationAdapter.addSourceRecord({ GUIDBookPrice:'GUID-8', CouponCode:'TestyCoupon', Price:183.77 });
48
- tmpIntegrationAdapter.integrateRecords(fDone);
260
+ }
261
+ );
262
+ }
263
+ );
264
+
265
+ // ── Full Integration Pipeline ───────────────────────────────────────
266
+
267
+ suite
268
+ (
269
+ 'Full Integration Pipeline',
270
+ () =>
271
+ {
272
+ test
273
+ (
274
+ 'upsert body should include marshaled field values (not just the GUID)',
275
+ function (fDone)
276
+ {
277
+ this.timeout(10000);
278
+
279
+ let tmpFable = new libPict(
280
+ {
281
+ Product: 'AdapterTest',
282
+ ProductVersion: '1.0.0',
283
+ LogStreams: [{ streamtype: 'console', level: 'error' }]
284
+ });
285
+ tmpFable.addServiceType('IntegrationAdapter', libIntegrationAdapter);
286
+ tmpFable.serviceManager.addServiceType('MeadowCloneRestClient', libMeadowCloneRestClient);
287
+ tmpFable.serviceManager.instantiateServiceProvider('MeadowCloneRestClient',
288
+ {
289
+ ServerURL: MOCK_BASE_URL
49
290
  });
50
- test(
51
- 'Integrate some Book Prices via the static method',
52
- (fDone) =>
291
+
292
+ let tmpAdapter = tmpFable.instantiateServiceProvider('IntegrationAdapter',
53
293
  {
54
- let _Fable = new libFable({});
294
+ Entity: 'TestEntity',
295
+ SimpleMarshal: true
296
+ }, 'TestEntity');
297
+
298
+ tmpAdapter.addSourceRecord({ GUIDTestEntity: 'test-1', Name: 'Alice', Value: 42 });
299
+
300
+ tmpAdapter.integrateRecords(
301
+ (pError) =>
302
+ {
303
+ Expect(pError).to.not.exist;
304
+
305
+ // The mock server should have received exactly one upsert
306
+ Expect(_MockState.UpsertedRecords.length).to.equal(1,
307
+ `Expected 1 upserted record, got ${_MockState.UpsertedRecords.length}`);
55
308
 
56
- _Fable.addServiceType('IntegrationAdapter', libIntegrationAdapter);
309
+ let tmpUpsertedRecord = _MockState.UpsertedRecords[0];
310
+
311
+ // The GUID should be present (with the adapter prefix)
312
+ Expect(tmpUpsertedRecord).to.have.property('GUIDTestEntity');
313
+ Expect(tmpUpsertedRecord.GUIDTestEntity).to.be.a('string');
314
+ Expect(tmpUpsertedRecord.GUIDTestEntity).to.include('test-1');
315
+
316
+ // CRITICAL: Verify field values were marshaled through.
317
+ // If the arrow-function arguments bug is present, the schema
318
+ // fetch would fail silently, meadowSchema would be null, and
319
+ // SimpleMarshal would not copy Name and Value through.
320
+ Expect(tmpUpsertedRecord).to.have.property('Name');
321
+ Expect(tmpUpsertedRecord.Name).to.equal('Alice');
322
+ Expect(tmpUpsertedRecord).to.have.property('Value');
323
+ Expect(tmpUpsertedRecord.Value).to.equal(42);
324
+
325
+ // Verify the schema was also populated
326
+ Expect(tmpAdapter.meadowSchema).to.be.an('object');
327
+ Expect(tmpAdapter.meadowSchema.properties).to.have.property('Name');
328
+
329
+ return fDone();
330
+ });
331
+ }
332
+ );
333
+
334
+ test
335
+ (
336
+ 'should integrate multiple records with correct field values',
337
+ function (fDone)
338
+ {
339
+ this.timeout(10000);
340
+
341
+ let tmpFable = new libPict(
342
+ {
343
+ Product: 'AdapterTest',
344
+ ProductVersion: '1.0.0',
345
+ LogStreams: [{ streamtype: 'console', level: 'error' }]
346
+ });
347
+ tmpFable.addServiceType('IntegrationAdapter', libIntegrationAdapter);
348
+ tmpFable.serviceManager.addServiceType('MeadowCloneRestClient', libMeadowCloneRestClient);
349
+ tmpFable.serviceManager.instantiateServiceProvider('MeadowCloneRestClient',
350
+ {
351
+ ServerURL: MOCK_BASE_URL
352
+ });
353
+
354
+ let tmpAdapter = tmpFable.instantiateServiceProvider('IntegrationAdapter',
355
+ {
356
+ Entity: 'TestEntity',
357
+ SimpleMarshal: true
358
+ }, 'TestEntity');
359
+
360
+ tmpAdapter.addSourceRecord({ GUIDTestEntity: 'multi-1', Name: 'Alice', Value: 42 });
361
+ tmpAdapter.addSourceRecord({ GUIDTestEntity: 'multi-2', Name: 'Bob', Value: 99 });
362
+ tmpAdapter.addSourceRecord({ GUIDTestEntity: 'multi-3', Name: 'Charlie', Value: 7 });
363
+
364
+ tmpAdapter.integrateRecords(
365
+ (pError) =>
366
+ {
367
+ Expect(pError).to.not.exist;
57
368
 
58
- let tmpIntegrationAdapter = libIntegrationAdapter.getAdapter(_Fable, 'BookPrice', 'BP');
369
+ Expect(_MockState.UpsertedRecords.length).to.equal(3,
370
+ `Expected 3 upserted records, got ${_MockState.UpsertedRecords.length}`);
59
371
 
60
- tmpIntegrationAdapter.addSourceRecord({ GUIDBookPrice:'GUID-dd3', CouponCode:'TestyCoupon', Price:3.50 });
61
- tmpIntegrationAdapter.addSourceRecord({ GUIDBookPrice:'GUID-dd4', CouponCode:'TestyCoupon', Price:3.22232 });
62
- tmpIntegrationAdapter.addSourceRecord({ GUIDBookPrice:'GUID-dd5', CouponCode:'None', Price:3.50 });
63
- tmpIntegrationAdapter.addSourceRecord({ GUIDBookPrice:'GUID-dd6', CouponCode:'', Price:3.57 });
64
- tmpIntegrationAdapter.addSourceRecord({ GUIDBookPrice:'GUID-dd7', Price:3.540 });
65
- tmpIntegrationAdapter.addSourceRecord({ GUIDBookPrice:'GUID-dd8', CouponCode:'TestyCoupon', Price:183.77 });
66
- tmpIntegrationAdapter.integrateRecords(fDone);
372
+ // Build a lookup by the original GUID suffix for easy verification
373
+ let tmpByGUID = {};
374
+ for (let i = 0; i < _MockState.UpsertedRecords.length; i++)
375
+ {
376
+ let tmpRec = _MockState.UpsertedRecords[i];
377
+ if (tmpRec.GUIDTestEntity.indexOf('multi-1') > -1) tmpByGUID['multi-1'] = tmpRec;
378
+ if (tmpRec.GUIDTestEntity.indexOf('multi-2') > -1) tmpByGUID['multi-2'] = tmpRec;
379
+ if (tmpRec.GUIDTestEntity.indexOf('multi-3') > -1) tmpByGUID['multi-3'] = tmpRec;
380
+ }
381
+
382
+ Expect(tmpByGUID['multi-1'].Name).to.equal('Alice');
383
+ Expect(tmpByGUID['multi-1'].Value).to.equal(42);
384
+
385
+ Expect(tmpByGUID['multi-2'].Name).to.equal('Bob');
386
+ Expect(tmpByGUID['multi-2'].Value).to.equal(99);
387
+
388
+ Expect(tmpByGUID['multi-3'].Name).to.equal('Charlie');
389
+ Expect(tmpByGUID['multi-3'].Value).to.equal(7);
390
+
391
+ // Each record should have been assigned a server-side ID
392
+ Expect(tmpByGUID['multi-1'].IDTestEntity).to.be.above(0);
393
+ Expect(tmpByGUID['multi-2'].IDTestEntity).to.be.above(0);
394
+ Expect(tmpByGUID['multi-3'].IDTestEntity).to.be.above(0);
395
+
396
+ return fDone();
397
+ });
398
+ }
399
+ );
400
+
401
+ test
402
+ (
403
+ 'without SimpleMarshal, fields not in schema properties are excluded',
404
+ function (fDone)
405
+ {
406
+ this.timeout(10000);
407
+
408
+ let tmpFable = new libPict(
409
+ {
410
+ Product: 'AdapterTest',
411
+ ProductVersion: '1.0.0',
412
+ LogStreams: [{ streamtype: 'console', level: 'error' }]
413
+ });
414
+ tmpFable.addServiceType('IntegrationAdapter', libIntegrationAdapter);
415
+ tmpFable.serviceManager.addServiceType('MeadowCloneRestClient', libMeadowCloneRestClient);
416
+ tmpFable.serviceManager.instantiateServiceProvider('MeadowCloneRestClient',
417
+ {
418
+ ServerURL: MOCK_BASE_URL
419
+ });
420
+
421
+ let tmpAdapter = tmpFable.instantiateServiceProvider('IntegrationAdapter',
422
+ {
423
+ Entity: 'TestEntity',
424
+ SimpleMarshal: false
425
+ }, 'TestEntity');
426
+
427
+ // ExtraField is not in the mock schema
428
+ tmpAdapter.addSourceRecord({ GUIDTestEntity: 'test-extra', Name: 'Diana', Value: 55, ExtraField: 'should-not-appear' });
429
+
430
+ tmpAdapter.integrateRecords(
431
+ (pError) =>
432
+ {
433
+ Expect(pError).to.not.exist;
434
+
435
+ Expect(_MockState.UpsertedRecords.length).to.equal(1);
436
+
437
+ let tmpUpsertedRecord = _MockState.UpsertedRecords[0];
438
+
439
+ // Name and Value should be marshaled through the schema
440
+ Expect(tmpUpsertedRecord).to.have.property('Name');
441
+ Expect(tmpUpsertedRecord.Name).to.equal('Diana');
442
+ Expect(tmpUpsertedRecord).to.have.property('Value');
443
+ Expect(tmpUpsertedRecord.Value).to.equal(55);
444
+
445
+ // ExtraField should NOT be in the upserted record
446
+ Expect(tmpUpsertedRecord).to.not.have.property('ExtraField');
447
+
448
+ return fDone();
67
449
  });
68
- */
69
450
  }
70
451
  );
71
- }
72
- );
452
+ }
453
+ );
454
+ }
455
+ );