meadow-integration 1.0.1 → 1.0.4

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.
Files changed (52) hide show
  1. package/CONTRIBUTING.md +50 -0
  2. package/README.md +223 -7
  3. package/docs/README.md +107 -7
  4. package/docs/_sidebar.md +38 -0
  5. package/docs/_topbar.md +7 -0
  6. package/docs/cli-reference.md +242 -0
  7. package/docs/comprehensions.md +98 -0
  8. package/docs/cover.md +11 -0
  9. package/docs/css/docuserve.css +73 -0
  10. package/docs/examples-walkthrough.md +138 -0
  11. package/docs/index.html +37 -20
  12. package/docs/integration-adapter.md +109 -0
  13. package/docs/mapping-files.md +140 -0
  14. package/docs/programmatic-api.md +173 -0
  15. package/docs/rest-api-reference.md +731 -0
  16. package/docs/retold-catalog.json +153 -0
  17. package/docs/retold-keyword-index.json +4828 -0
  18. package/examples/Example-001-CSV-Check.sh +29 -0
  19. package/examples/Example-002-CSV-Transform-Implicit.sh +31 -0
  20. package/examples/Example-003-CSV-Transform-CLI-Options.sh +39 -0
  21. package/examples/Example-004-CSV-Transform-Mapping-File.sh +41 -0
  22. package/examples/Example-005-Multi-Entity-Bookstore.sh +60 -0
  23. package/examples/Example-006-Multi-CSV-Intersect.sh +74 -0
  24. package/examples/Example-007-Comprehension-To-Array.sh +41 -0
  25. package/examples/Example-008-Comprehension-To-CSV.sh +51 -0
  26. package/examples/Example-009-JSON-Array-Transform.sh +46 -0
  27. package/examples/Example-010-Programmatic-API.js +138 -0
  28. package/examples/README.md +44 -0
  29. package/examples/output/.gitignore +2 -0
  30. package/package.json +7 -4
  31. package/source/Meadow-Integration.js +3 -1
  32. package/source/cli/Meadow-Integration-CLI-Program.js +4 -1
  33. package/source/cli/commands/Meadow-Integration-Command-ObjectArrayToCSV.js +49 -32
  34. package/source/cli/commands/Meadow-Integration-Command-Serve.js +51 -0
  35. package/source/restserver/Meadow-Integration-Server-Endpoints.js +83 -0
  36. package/source/restserver/Meadow-Integration-Server.js +86 -0
  37. package/source/restserver/endpoints/Endpoint-CSVCheck.js +91 -0
  38. package/source/restserver/endpoints/Endpoint-CSVTransform.js +189 -0
  39. package/source/restserver/endpoints/Endpoint-ComprehensionArray.js +121 -0
  40. package/source/restserver/endpoints/Endpoint-ComprehensionIntersect.js +166 -0
  41. package/source/restserver/endpoints/Endpoint-ComprehensionPush.js +209 -0
  42. package/source/restserver/endpoints/Endpoint-EntityFromTabularFolder.js +252 -0
  43. package/source/restserver/endpoints/Endpoint-JSONArrayTransform.js +238 -0
  44. package/source/restserver/endpoints/Endpoint-ObjectArrayToCSV.js +231 -0
  45. package/source/restserver/endpoints/Endpoint-TSVCheck.js +93 -0
  46. package/source/restserver/endpoints/Endpoint-TSVTransform.js +191 -0
  47. package/test/Meadow-Integration-Server_test.js +1170 -0
  48. package/test/data/test-comprehension-secondary.json +8 -0
  49. package/test/data/test-comprehension.json +8 -0
  50. package/test/data/test-small.csv +6 -0
  51. package/test/data/test-small.json +7 -0
  52. package/test/data/test-small.tsv +6 -0
@@ -0,0 +1,1170 @@
1
+ /*
2
+ Unit tests for Meadow Integration REST Server
3
+
4
+ Exercises all REST API endpoints provided by the integration server.
5
+ */
6
+
7
+ const Chai = require('chai');
8
+ const Expect = Chai.expect;
9
+
10
+ const libHTTP = require('http');
11
+ const libPath = require('path');
12
+
13
+ const MeadowIntegrationServer = require('../source/restserver/Meadow-Integration-Server.js');
14
+
15
+ const TEST_PORT = 18086;
16
+ const TEST_BASE_URL = `http://localhost:${TEST_PORT}`;
17
+
18
+ const TEST_DATA_DIR = libPath.join(__dirname, 'data');
19
+ const EXAMPLE_DATA_DIR = libPath.join(__dirname, '..', 'docs', 'examples', 'data');
20
+
21
+ let _Server = null;
22
+
23
+ // Helper to make HTTP requests
24
+ function makeRequest(pMethod, pPath, pBody, fCallback)
25
+ {
26
+ let tmpBodyString = (pBody !== null && pBody !== undefined) ? JSON.stringify(pBody) : null;
27
+ let tmpOptions =
28
+ {
29
+ hostname: 'localhost',
30
+ port: TEST_PORT,
31
+ path: pPath,
32
+ method: pMethod,
33
+ headers: {}
34
+ };
35
+
36
+ if (tmpBodyString)
37
+ {
38
+ tmpOptions.headers['Content-Type'] = 'application/json';
39
+ tmpOptions.headers['Content-Length'] = Buffer.byteLength(tmpBodyString);
40
+ }
41
+
42
+ const tmpRequest = libHTTP.request(tmpOptions,
43
+ (pResponse) =>
44
+ {
45
+ let tmpData = '';
46
+ pResponse.on('data', (pChunk) => { tmpData += pChunk; });
47
+ pResponse.on('end',
48
+ () =>
49
+ {
50
+ let tmpParsed = null;
51
+ try
52
+ {
53
+ tmpParsed = JSON.parse(tmpData);
54
+ }
55
+ catch (pError)
56
+ {
57
+ // Not JSON — return raw string (e.g. CSV)
58
+ tmpParsed = tmpData;
59
+ }
60
+ return fCallback(null, pResponse.statusCode, tmpParsed, pResponse.headers);
61
+ });
62
+ });
63
+
64
+ tmpRequest.on('error', (pError) => { return fCallback(pError); });
65
+
66
+ if (tmpBodyString)
67
+ {
68
+ tmpRequest.write(tmpBodyString);
69
+ }
70
+
71
+ tmpRequest.end();
72
+ }
73
+
74
+ suite
75
+ (
76
+ 'Meadow Integration REST Server',
77
+ () =>
78
+ {
79
+ suiteSetup
80
+ (
81
+ (fDone) =>
82
+ {
83
+ _Server = new MeadowIntegrationServer(
84
+ {
85
+ APIServerPort: TEST_PORT,
86
+ LogLevel: 1
87
+ });
88
+
89
+ _Server.start(
90
+ (pError) =>
91
+ {
92
+ if (pError)
93
+ {
94
+ console.log('Error starting test server: ', pError);
95
+ }
96
+ Expect(pError).to.not.exist;
97
+ return fDone();
98
+ });
99
+ }
100
+ );
101
+
102
+ suiteTeardown
103
+ (
104
+ function(fDone)
105
+ {
106
+ this.timeout(10000);
107
+ if (_Server)
108
+ {
109
+ _Server.stop(
110
+ (pError) =>
111
+ {
112
+ return fDone();
113
+ });
114
+ }
115
+ else
116
+ {
117
+ return fDone();
118
+ }
119
+ }
120
+ );
121
+
122
+ // ===== Status Endpoint =====
123
+ suite
124
+ (
125
+ 'GET /1.0/Status',
126
+ () =>
127
+ {
128
+ test('Should return server status and endpoint list',
129
+ (fDone) =>
130
+ {
131
+ makeRequest('GET', '/1.0/Status', null,
132
+ (pError, pStatusCode, pBody) =>
133
+ {
134
+ Expect(pError).to.not.exist;
135
+ Expect(pStatusCode).to.equal(200);
136
+ Expect(pBody).to.be.an('object');
137
+ Expect(pBody.Status).to.equal('Running');
138
+ Expect(pBody.Product).to.equal('Meadow-Integration-Server');
139
+ Expect(pBody.Endpoints).to.be.an('array');
140
+ Expect(pBody.Endpoints.length).to.equal(15);
141
+ return fDone();
142
+ });
143
+ });
144
+ }
145
+ );
146
+
147
+ // ===== CSV Check Endpoint =====
148
+ suite
149
+ (
150
+ 'POST /1.0/CSV/Check',
151
+ () =>
152
+ {
153
+ test('Should analyze a CSV file and return statistics',
154
+ (fDone) =>
155
+ {
156
+ makeRequest('POST', '/1.0/CSV/Check',
157
+ { File: libPath.join(TEST_DATA_DIR, 'test-small.csv') },
158
+ (pError, pStatusCode, pBody) =>
159
+ {
160
+ Expect(pError).to.not.exist;
161
+ Expect(pStatusCode).to.equal(200);
162
+ Expect(pBody).to.be.an('object');
163
+ Expect(pBody.RowCount).to.equal(5);
164
+ Expect(pBody.ColumnCount).to.equal(5);
165
+ Expect(pBody.Headers).to.be.an('array');
166
+ Expect(pBody.Headers).to.include('id');
167
+ Expect(pBody.Headers).to.include('name');
168
+ return fDone();
169
+ });
170
+ });
171
+
172
+ test('Should return 400 when no File is provided',
173
+ (fDone) =>
174
+ {
175
+ makeRequest('POST', '/1.0/CSV/Check', {},
176
+ (pError, pStatusCode, pBody) =>
177
+ {
178
+ Expect(pError).to.not.exist;
179
+ Expect(pStatusCode).to.equal(400);
180
+ Expect(pBody.Error).to.be.a('string');
181
+ return fDone();
182
+ });
183
+ });
184
+
185
+ test('Should return 404 when file does not exist',
186
+ (fDone) =>
187
+ {
188
+ makeRequest('POST', '/1.0/CSV/Check',
189
+ { File: '/tmp/nonexistent-test-file-99999.csv' },
190
+ (pError, pStatusCode, pBody) =>
191
+ {
192
+ Expect(pError).to.not.exist;
193
+ Expect(pStatusCode).to.equal(404);
194
+ Expect(pBody.Error).to.be.a('string');
195
+ return fDone();
196
+ });
197
+ });
198
+
199
+ test('Should include records when Records flag is true',
200
+ (fDone) =>
201
+ {
202
+ makeRequest('POST', '/1.0/CSV/Check',
203
+ { File: libPath.join(TEST_DATA_DIR, 'test-small.csv'), Records: true },
204
+ (pError, pStatusCode, pBody) =>
205
+ {
206
+ Expect(pError).to.not.exist;
207
+ Expect(pStatusCode).to.equal(200);
208
+ Expect(pBody.Records).to.be.an('array');
209
+ // Records are pushed per-column in collectStatistics, so count = rows * columns
210
+ Expect(pBody.Records.length).to.be.greaterThan(0);
211
+ Expect(pBody.RowCount).to.equal(5);
212
+ return fDone();
213
+ });
214
+ });
215
+
216
+ test('Should analyze the airports CSV',
217
+ (fDone) =>
218
+ {
219
+ makeRequest('POST', '/1.0/CSV/Check',
220
+ { File: libPath.join(TEST_DATA_DIR, 'vega', 'airports.csv') },
221
+ (pError, pStatusCode, pBody) =>
222
+ {
223
+ Expect(pError).to.not.exist;
224
+ Expect(pStatusCode).to.equal(200);
225
+ Expect(pBody.RowCount).to.equal(3376);
226
+ Expect(pBody.ColumnCount).to.equal(7);
227
+ return fDone();
228
+ });
229
+ });
230
+ }
231
+ );
232
+
233
+ // ===== CSV Transform Endpoint =====
234
+ suite
235
+ (
236
+ 'POST /1.0/CSV/Transform',
237
+ () =>
238
+ {
239
+ test('Should transform a CSV file into a comprehension with implicit config',
240
+ (fDone) =>
241
+ {
242
+ makeRequest('POST', '/1.0/CSV/Transform',
243
+ { File: libPath.join(TEST_DATA_DIR, 'test-small.csv') },
244
+ (pError, pStatusCode, pBody) =>
245
+ {
246
+ Expect(pError).to.not.exist;
247
+ Expect(pStatusCode).to.equal(200);
248
+ Expect(pBody).to.be.an('object');
249
+ // Implicit config should auto-detect entity from filename
250
+ let tmpEntityKeys = Object.keys(pBody);
251
+ Expect(tmpEntityKeys.length).to.be.greaterThan(0);
252
+ // Check that records exist in the entity
253
+ let tmpFirstEntity = pBody[tmpEntityKeys[0]];
254
+ Expect(Object.keys(tmpFirstEntity).length).to.equal(5);
255
+ return fDone();
256
+ });
257
+ });
258
+
259
+ test('Should transform with explicit entity and GUID template',
260
+ (fDone) =>
261
+ {
262
+ makeRequest('POST', '/1.0/CSV/Transform',
263
+ {
264
+ File: libPath.join(TEST_DATA_DIR, 'test-small.csv'),
265
+ Entity: 'Person',
266
+ GUIDName: 'GUIDPerson',
267
+ GUIDTemplate: 'Person_{~D:Record.id~}'
268
+ },
269
+ (pError, pStatusCode, pBody) =>
270
+ {
271
+ Expect(pError).to.not.exist;
272
+ Expect(pStatusCode).to.equal(200);
273
+ Expect(pBody).to.have.property('Person');
274
+ Expect(pBody.Person).to.have.property('Person_1');
275
+ Expect(pBody.Person).to.have.property('Person_5');
276
+ Expect(pBody.Person['Person_1'].name).to.equal('Alice');
277
+ return fDone();
278
+ });
279
+ });
280
+
281
+ test('Should transform with custom column mappings',
282
+ (fDone) =>
283
+ {
284
+ makeRequest('POST', '/1.0/CSV/Transform',
285
+ {
286
+ File: libPath.join(TEST_DATA_DIR, 'test-small.csv'),
287
+ Entity: 'Person',
288
+ GUIDName: 'GUIDPerson',
289
+ GUIDTemplate: 'Person_{~D:Record.id~}',
290
+ Mappings:
291
+ {
292
+ FullName: '{~D:Record.name~}',
293
+ Location: '{~D:Record.city~}'
294
+ }
295
+ },
296
+ (pError, pStatusCode, pBody) =>
297
+ {
298
+ Expect(pError).to.not.exist;
299
+ Expect(pStatusCode).to.equal(200);
300
+ Expect(pBody.Person['Person_1'].FullName).to.equal('Alice');
301
+ Expect(pBody.Person['Person_1'].Location).to.equal('Seattle');
302
+ return fDone();
303
+ });
304
+ });
305
+
306
+ test('Should return extended state when Extended flag is set',
307
+ (fDone) =>
308
+ {
309
+ makeRequest('POST', '/1.0/CSV/Transform',
310
+ {
311
+ File: libPath.join(TEST_DATA_DIR, 'test-small.csv'),
312
+ Entity: 'Person',
313
+ Extended: true
314
+ },
315
+ (pError, pStatusCode, pBody) =>
316
+ {
317
+ Expect(pError).to.not.exist;
318
+ Expect(pStatusCode).to.equal(200);
319
+ Expect(pBody).to.have.property('Comprehension');
320
+ Expect(pBody).to.have.property('ParsedRowCount');
321
+ Expect(pBody).to.have.property('Configuration');
322
+ Expect(pBody.ParsedRowCount).to.be.greaterThan(0);
323
+ return fDone();
324
+ });
325
+ });
326
+
327
+ test('Should return 400 when no File is provided',
328
+ (fDone) =>
329
+ {
330
+ makeRequest('POST', '/1.0/CSV/Transform', {},
331
+ (pError, pStatusCode, pBody) =>
332
+ {
333
+ Expect(pError).to.not.exist;
334
+ Expect(pStatusCode).to.equal(400);
335
+ return fDone();
336
+ });
337
+ });
338
+
339
+ test('Should return 404 when file does not exist',
340
+ (fDone) =>
341
+ {
342
+ makeRequest('POST', '/1.0/CSV/Transform',
343
+ { File: '/tmp/nonexistent-test-file-99999.csv' },
344
+ (pError, pStatusCode, pBody) =>
345
+ {
346
+ Expect(pError).to.not.exist;
347
+ Expect(pStatusCode).to.equal(404);
348
+ return fDone();
349
+ });
350
+ });
351
+ }
352
+ );
353
+
354
+ // ===== TSV Check Endpoint =====
355
+ suite
356
+ (
357
+ 'POST /1.0/TSV/Check',
358
+ () =>
359
+ {
360
+ test('Should analyze a TSV file and return statistics',
361
+ (fDone) =>
362
+ {
363
+ makeRequest('POST', '/1.0/TSV/Check',
364
+ { File: libPath.join(TEST_DATA_DIR, 'test-small.tsv') },
365
+ (pError, pStatusCode, pBody) =>
366
+ {
367
+ Expect(pError).to.not.exist;
368
+ Expect(pStatusCode).to.equal(200);
369
+ Expect(pBody.RowCount).to.equal(5);
370
+ Expect(pBody.ColumnCount).to.equal(5);
371
+ Expect(pBody.Headers).to.include('id');
372
+ Expect(pBody.Headers).to.include('name');
373
+ return fDone();
374
+ });
375
+ });
376
+
377
+ test('Should return 400 when no File is provided',
378
+ (fDone) =>
379
+ {
380
+ makeRequest('POST', '/1.0/TSV/Check', {},
381
+ (pError, pStatusCode, pBody) =>
382
+ {
383
+ Expect(pError).to.not.exist;
384
+ Expect(pStatusCode).to.equal(400);
385
+ return fDone();
386
+ });
387
+ });
388
+
389
+ test('Should return 404 for nonexistent file',
390
+ (fDone) =>
391
+ {
392
+ makeRequest('POST', '/1.0/TSV/Check',
393
+ { File: '/tmp/nonexistent-test-file-99999.tsv' },
394
+ (pError, pStatusCode, pBody) =>
395
+ {
396
+ Expect(pError).to.not.exist;
397
+ Expect(pStatusCode).to.equal(404);
398
+ return fDone();
399
+ });
400
+ });
401
+ }
402
+ );
403
+
404
+ // ===== TSV Transform Endpoint =====
405
+ suite
406
+ (
407
+ 'POST /1.0/TSV/Transform',
408
+ () =>
409
+ {
410
+ test('Should transform a TSV file into a comprehension',
411
+ (fDone) =>
412
+ {
413
+ makeRequest('POST', '/1.0/TSV/Transform',
414
+ {
415
+ File: libPath.join(TEST_DATA_DIR, 'test-small.tsv'),
416
+ Entity: 'Person',
417
+ GUIDName: 'GUIDPerson',
418
+ GUIDTemplate: 'Person_{~D:Record.id~}'
419
+ },
420
+ (pError, pStatusCode, pBody) =>
421
+ {
422
+ Expect(pError).to.not.exist;
423
+ Expect(pStatusCode).to.equal(200);
424
+ Expect(pBody).to.have.property('Person');
425
+ Expect(pBody.Person).to.have.property('Person_1');
426
+ Expect(pBody.Person['Person_1'].name).to.equal('Alice');
427
+ Expect(Object.keys(pBody.Person).length).to.equal(5);
428
+ return fDone();
429
+ });
430
+ });
431
+
432
+ test('Should return 400 when no File is provided',
433
+ (fDone) =>
434
+ {
435
+ makeRequest('POST', '/1.0/TSV/Transform', {},
436
+ (pError, pStatusCode, pBody) =>
437
+ {
438
+ Expect(pError).to.not.exist;
439
+ Expect(pStatusCode).to.equal(400);
440
+ return fDone();
441
+ });
442
+ });
443
+ }
444
+ );
445
+
446
+ // ===== JSON Array Transform Endpoint =====
447
+ suite
448
+ (
449
+ 'POST /1.0/JSONArray/Transform',
450
+ () =>
451
+ {
452
+ test('Should transform a JSON array file into a comprehension',
453
+ (fDone) =>
454
+ {
455
+ makeRequest('POST', '/1.0/JSONArray/Transform',
456
+ {
457
+ File: libPath.join(TEST_DATA_DIR, 'test-small.json'),
458
+ Entity: 'Person',
459
+ GUIDName: 'GUIDPerson',
460
+ GUIDTemplate: 'Person_{~D:Record.id~}'
461
+ },
462
+ (pError, pStatusCode, pBody) =>
463
+ {
464
+ Expect(pError).to.not.exist;
465
+ Expect(pStatusCode).to.equal(200);
466
+ Expect(pBody).to.have.property('Person');
467
+ Expect(pBody.Person).to.have.property('Person_1');
468
+ Expect(pBody.Person['Person_1'].name).to.equal('Alice');
469
+ Expect(Object.keys(pBody.Person).length).to.equal(5);
470
+ return fDone();
471
+ });
472
+ });
473
+
474
+ test('Should return 400 when no File is provided',
475
+ (fDone) =>
476
+ {
477
+ makeRequest('POST', '/1.0/JSONArray/Transform', {},
478
+ (pError, pStatusCode, pBody) =>
479
+ {
480
+ Expect(pError).to.not.exist;
481
+ Expect(pStatusCode).to.equal(400);
482
+ return fDone();
483
+ });
484
+ });
485
+
486
+ test('Should return 404 for nonexistent file',
487
+ (fDone) =>
488
+ {
489
+ makeRequest('POST', '/1.0/JSONArray/Transform',
490
+ { File: '/tmp/nonexistent-test-file-99999.json' },
491
+ (pError, pStatusCode, pBody) =>
492
+ {
493
+ Expect(pError).to.not.exist;
494
+ Expect(pStatusCode).to.equal(404);
495
+ return fDone();
496
+ });
497
+ });
498
+ }
499
+ );
500
+
501
+ // ===== JSON Array Transform Records Endpoint =====
502
+ suite
503
+ (
504
+ 'POST /1.0/JSONArray/TransformRecords',
505
+ () =>
506
+ {
507
+ test('Should transform in-memory records into a comprehension',
508
+ (fDone) =>
509
+ {
510
+ makeRequest('POST', '/1.0/JSONArray/TransformRecords',
511
+ {
512
+ Records:
513
+ [
514
+ { id: '1', name: 'Alice', city: 'Seattle' },
515
+ { id: '2', name: 'Bob', city: 'Portland' },
516
+ { id: '3', name: 'Carol', city: 'Vancouver' }
517
+ ],
518
+ Entity: 'Person',
519
+ GUIDName: 'GUIDPerson',
520
+ GUIDTemplate: 'Person_{~D:Record.id~}',
521
+ Mappings:
522
+ {
523
+ FullName: '{~D:Record.name~}',
524
+ Location: '{~D:Record.city~}'
525
+ }
526
+ },
527
+ (pError, pStatusCode, pBody) =>
528
+ {
529
+ Expect(pError).to.not.exist;
530
+ Expect(pStatusCode).to.equal(200);
531
+ Expect(pBody).to.have.property('Person');
532
+ Expect(pBody.Person).to.have.property('Person_1');
533
+ Expect(pBody.Person['Person_1'].FullName).to.equal('Alice');
534
+ Expect(pBody.Person['Person_1'].Location).to.equal('Seattle');
535
+ Expect(Object.keys(pBody.Person).length).to.equal(3);
536
+ return fDone();
537
+ });
538
+ });
539
+
540
+ test('Should return 400 when no Records array is provided',
541
+ (fDone) =>
542
+ {
543
+ makeRequest('POST', '/1.0/JSONArray/TransformRecords', {},
544
+ (pError, pStatusCode, pBody) =>
545
+ {
546
+ Expect(pError).to.not.exist;
547
+ Expect(pStatusCode).to.equal(400);
548
+ Expect(pBody.Error).to.contain('Records');
549
+ return fDone();
550
+ });
551
+ });
552
+
553
+ test('Should return 400 when Records array is empty',
554
+ (fDone) =>
555
+ {
556
+ makeRequest('POST', '/1.0/JSONArray/TransformRecords',
557
+ { Records: [] },
558
+ (pError, pStatusCode, pBody) =>
559
+ {
560
+ Expect(pError).to.not.exist;
561
+ Expect(pStatusCode).to.equal(400);
562
+ Expect(pBody.Error).to.contain('empty');
563
+ return fDone();
564
+ });
565
+ });
566
+
567
+ test('Should return extended state when requested',
568
+ (fDone) =>
569
+ {
570
+ makeRequest('POST', '/1.0/JSONArray/TransformRecords',
571
+ {
572
+ Records:
573
+ [
574
+ { id: '1', name: 'Alice' }
575
+ ],
576
+ Entity: 'Person',
577
+ Extended: true
578
+ },
579
+ (pError, pStatusCode, pBody) =>
580
+ {
581
+ Expect(pError).to.not.exist;
582
+ Expect(pStatusCode).to.equal(200);
583
+ Expect(pBody).to.have.property('Comprehension');
584
+ Expect(pBody).to.have.property('ParsedRowCount');
585
+ Expect(pBody.ParsedRowCount).to.equal(1);
586
+ return fDone();
587
+ });
588
+ });
589
+ }
590
+ );
591
+
592
+ // ===== Comprehension Intersect Endpoint =====
593
+ suite
594
+ (
595
+ 'POST /1.0/Comprehension/Intersect',
596
+ () =>
597
+ {
598
+ test('Should merge two in-memory comprehensions',
599
+ (fDone) =>
600
+ {
601
+ makeRequest('POST', '/1.0/Comprehension/Intersect',
602
+ {
603
+ PrimaryComprehension:
604
+ {
605
+ Person:
606
+ {
607
+ Person_1: { GUIDPerson: 'Person_1', Name: 'Alice', City: 'Seattle' },
608
+ Person_2: { GUIDPerson: 'Person_2', Name: 'Bob', City: 'Portland' }
609
+ }
610
+ },
611
+ SecondaryComprehension:
612
+ {
613
+ Person:
614
+ {
615
+ Person_1: { GUIDPerson: 'Person_1', Score: '95' },
616
+ Person_3: { GUIDPerson: 'Person_3', Name: 'Carol', City: 'Vancouver' }
617
+ }
618
+ },
619
+ Entity: 'Person'
620
+ },
621
+ (pError, pStatusCode, pBody) =>
622
+ {
623
+ Expect(pError).to.not.exist;
624
+ Expect(pStatusCode).to.equal(200);
625
+ Expect(pBody.Person).to.have.property('Person_1');
626
+ Expect(pBody.Person).to.have.property('Person_2');
627
+ Expect(pBody.Person).to.have.property('Person_3');
628
+ // Merged record should have both original and secondary properties
629
+ Expect(pBody.Person['Person_1'].Name).to.equal('Alice');
630
+ Expect(pBody.Person['Person_1'].Score).to.equal('95');
631
+ Expect(Object.keys(pBody.Person).length).to.equal(3);
632
+ return fDone();
633
+ });
634
+ });
635
+
636
+ test('Should auto-detect entity when not specified',
637
+ (fDone) =>
638
+ {
639
+ makeRequest('POST', '/1.0/Comprehension/Intersect',
640
+ {
641
+ PrimaryComprehension:
642
+ {
643
+ Book:
644
+ {
645
+ Book_1: { GUIDBook: 'Book_1', Title: 'Test Book' }
646
+ }
647
+ },
648
+ SecondaryComprehension:
649
+ {
650
+ Book:
651
+ {
652
+ Book_1: { GUIDBook: 'Book_1', Rating: '4.5' }
653
+ }
654
+ }
655
+ },
656
+ (pError, pStatusCode, pBody) =>
657
+ {
658
+ Expect(pError).to.not.exist;
659
+ Expect(pStatusCode).to.equal(200);
660
+ Expect(pBody.Book['Book_1'].Title).to.equal('Test Book');
661
+ Expect(pBody.Book['Book_1'].Rating).to.equal('4.5');
662
+ return fDone();
663
+ });
664
+ });
665
+
666
+ test('Should return 400 when PrimaryComprehension is missing',
667
+ (fDone) =>
668
+ {
669
+ makeRequest('POST', '/1.0/Comprehension/Intersect',
670
+ { SecondaryComprehension: { Book: {} } },
671
+ (pError, pStatusCode, pBody) =>
672
+ {
673
+ Expect(pError).to.not.exist;
674
+ Expect(pStatusCode).to.equal(400);
675
+ Expect(pBody.Error).to.contain('PrimaryComprehension');
676
+ return fDone();
677
+ });
678
+ });
679
+
680
+ test('Should return 400 when SecondaryComprehension is missing',
681
+ (fDone) =>
682
+ {
683
+ makeRequest('POST', '/1.0/Comprehension/Intersect',
684
+ { PrimaryComprehension: { Book: {} } },
685
+ (pError, pStatusCode, pBody) =>
686
+ {
687
+ Expect(pError).to.not.exist;
688
+ Expect(pStatusCode).to.equal(400);
689
+ Expect(pBody.Error).to.contain('SecondaryComprehension');
690
+ return fDone();
691
+ });
692
+ });
693
+ }
694
+ );
695
+
696
+ // ===== Comprehension Intersect Files Endpoint =====
697
+ suite
698
+ (
699
+ 'POST /1.0/Comprehension/IntersectFiles',
700
+ () =>
701
+ {
702
+ test('Should merge two comprehension files',
703
+ (fDone) =>
704
+ {
705
+ makeRequest('POST', '/1.0/Comprehension/IntersectFiles',
706
+ {
707
+ File: libPath.join(TEST_DATA_DIR, 'test-comprehension.json'),
708
+ IntersectFile: libPath.join(TEST_DATA_DIR, 'test-comprehension-secondary.json'),
709
+ Entity: 'Person'
710
+ },
711
+ (pError, pStatusCode, pBody) =>
712
+ {
713
+ Expect(pError).to.not.exist;
714
+ Expect(pStatusCode).to.equal(200);
715
+ Expect(pBody.Person).to.have.property('Person_1');
716
+ Expect(pBody.Person).to.have.property('Person_2');
717
+ Expect(pBody.Person).to.have.property('Person_3');
718
+ Expect(pBody.Person).to.have.property('Person_4');
719
+ // Merged fields
720
+ Expect(pBody.Person['Person_1'].Name).to.equal('Alice');
721
+ Expect(pBody.Person['Person_1'].Score).to.equal('95');
722
+ return fDone();
723
+ });
724
+ });
725
+
726
+ test('Should return 400 when File is missing',
727
+ (fDone) =>
728
+ {
729
+ makeRequest('POST', '/1.0/Comprehension/IntersectFiles',
730
+ { IntersectFile: libPath.join(TEST_DATA_DIR, 'test-comprehension-secondary.json') },
731
+ (pError, pStatusCode, pBody) =>
732
+ {
733
+ Expect(pError).to.not.exist;
734
+ Expect(pStatusCode).to.equal(400);
735
+ return fDone();
736
+ });
737
+ });
738
+
739
+ test('Should return 400 when IntersectFile is missing',
740
+ (fDone) =>
741
+ {
742
+ makeRequest('POST', '/1.0/Comprehension/IntersectFiles',
743
+ { File: libPath.join(TEST_DATA_DIR, 'test-comprehension.json') },
744
+ (pError, pStatusCode, pBody) =>
745
+ {
746
+ Expect(pError).to.not.exist;
747
+ Expect(pStatusCode).to.equal(400);
748
+ return fDone();
749
+ });
750
+ });
751
+
752
+ test('Should return 404 for nonexistent primary file',
753
+ (fDone) =>
754
+ {
755
+ makeRequest('POST', '/1.0/Comprehension/IntersectFiles',
756
+ {
757
+ File: '/tmp/nonexistent-primary-99999.json',
758
+ IntersectFile: libPath.join(TEST_DATA_DIR, 'test-comprehension-secondary.json')
759
+ },
760
+ (pError, pStatusCode, pBody) =>
761
+ {
762
+ Expect(pError).to.not.exist;
763
+ Expect(pStatusCode).to.equal(404);
764
+ return fDone();
765
+ });
766
+ });
767
+ }
768
+ );
769
+
770
+ // ===== Comprehension ToArray Endpoint =====
771
+ suite
772
+ (
773
+ 'POST /1.0/Comprehension/ToArray',
774
+ () =>
775
+ {
776
+ test('Should convert comprehension to an array',
777
+ (fDone) =>
778
+ {
779
+ makeRequest('POST', '/1.0/Comprehension/ToArray',
780
+ {
781
+ Comprehension:
782
+ {
783
+ Person:
784
+ {
785
+ Person_1: { GUIDPerson: 'Person_1', Name: 'Alice' },
786
+ Person_2: { GUIDPerson: 'Person_2', Name: 'Bob' },
787
+ Person_3: { GUIDPerson: 'Person_3', Name: 'Carol' }
788
+ }
789
+ },
790
+ Entity: 'Person'
791
+ },
792
+ (pError, pStatusCode, pBody) =>
793
+ {
794
+ Expect(pError).to.not.exist;
795
+ Expect(pStatusCode).to.equal(200);
796
+ Expect(pBody).to.be.an('array');
797
+ Expect(pBody.length).to.equal(3);
798
+ let tmpNames = pBody.map((r) => r.Name);
799
+ Expect(tmpNames).to.include('Alice');
800
+ Expect(tmpNames).to.include('Bob');
801
+ Expect(tmpNames).to.include('Carol');
802
+ return fDone();
803
+ });
804
+ });
805
+
806
+ test('Should auto-detect entity when not specified',
807
+ (fDone) =>
808
+ {
809
+ makeRequest('POST', '/1.0/Comprehension/ToArray',
810
+ {
811
+ Comprehension:
812
+ {
813
+ Book:
814
+ {
815
+ Book_1: { Title: 'Test Book' }
816
+ }
817
+ }
818
+ },
819
+ (pError, pStatusCode, pBody) =>
820
+ {
821
+ Expect(pError).to.not.exist;
822
+ Expect(pStatusCode).to.equal(200);
823
+ Expect(pBody).to.be.an('array');
824
+ Expect(pBody.length).to.equal(1);
825
+ Expect(pBody[0].Title).to.equal('Test Book');
826
+ return fDone();
827
+ });
828
+ });
829
+
830
+ test('Should return 400 when no Comprehension is provided',
831
+ (fDone) =>
832
+ {
833
+ makeRequest('POST', '/1.0/Comprehension/ToArray', {},
834
+ (pError, pStatusCode, pBody) =>
835
+ {
836
+ Expect(pError).to.not.exist;
837
+ Expect(pStatusCode).to.equal(400);
838
+ return fDone();
839
+ });
840
+ });
841
+ }
842
+ );
843
+
844
+ // ===== Comprehension ToArrayFromFile Endpoint =====
845
+ suite
846
+ (
847
+ 'POST /1.0/Comprehension/ToArrayFromFile',
848
+ () =>
849
+ {
850
+ test('Should convert comprehension file to an array',
851
+ (fDone) =>
852
+ {
853
+ makeRequest('POST', '/1.0/Comprehension/ToArrayFromFile',
854
+ {
855
+ File: libPath.join(TEST_DATA_DIR, 'test-comprehension.json'),
856
+ Entity: 'Person'
857
+ },
858
+ (pError, pStatusCode, pBody) =>
859
+ {
860
+ Expect(pError).to.not.exist;
861
+ Expect(pStatusCode).to.equal(200);
862
+ Expect(pBody).to.be.an('array');
863
+ Expect(pBody.length).to.equal(3);
864
+ return fDone();
865
+ });
866
+ });
867
+
868
+ test('Should return 400 when no File is provided',
869
+ (fDone) =>
870
+ {
871
+ makeRequest('POST', '/1.0/Comprehension/ToArrayFromFile', {},
872
+ (pError, pStatusCode, pBody) =>
873
+ {
874
+ Expect(pError).to.not.exist;
875
+ Expect(pStatusCode).to.equal(400);
876
+ return fDone();
877
+ });
878
+ });
879
+
880
+ test('Should return 404 for nonexistent file',
881
+ (fDone) =>
882
+ {
883
+ makeRequest('POST', '/1.0/Comprehension/ToArrayFromFile',
884
+ { File: '/tmp/nonexistent-test-99999.json' },
885
+ (pError, pStatusCode, pBody) =>
886
+ {
887
+ Expect(pError).to.not.exist;
888
+ Expect(pStatusCode).to.equal(404);
889
+ return fDone();
890
+ });
891
+ });
892
+ }
893
+ );
894
+
895
+ // ===== Comprehension ToCSV Endpoint =====
896
+ suite
897
+ (
898
+ 'POST /1.0/Comprehension/ToCSV',
899
+ () =>
900
+ {
901
+ test('Should convert a Records array to CSV',
902
+ (fDone) =>
903
+ {
904
+ makeRequest('POST', '/1.0/Comprehension/ToCSV',
905
+ {
906
+ Records:
907
+ [
908
+ { Name: 'Alice', City: 'Seattle', Score: 95 },
909
+ { Name: 'Bob', City: 'Portland', Score: 87 }
910
+ ]
911
+ },
912
+ (pError, pStatusCode, pBody, pHeaders) =>
913
+ {
914
+ Expect(pError).to.not.exist;
915
+ Expect(pStatusCode).to.equal(200);
916
+ // Body should be raw CSV text
917
+ Expect(pBody).to.be.a('string');
918
+ Expect(pBody).to.contain('City');
919
+ Expect(pBody).to.contain('Name');
920
+ Expect(pBody).to.contain('Alice');
921
+ Expect(pBody).to.contain('Bob');
922
+ return fDone();
923
+ });
924
+ });
925
+
926
+ test('Should convert a Comprehension object to CSV',
927
+ (fDone) =>
928
+ {
929
+ makeRequest('POST', '/1.0/Comprehension/ToCSV',
930
+ {
931
+ Comprehension:
932
+ {
933
+ Person:
934
+ {
935
+ Person_1: { Name: 'Alice', City: 'Seattle' },
936
+ Person_2: { Name: 'Bob', City: 'Portland' }
937
+ }
938
+ },
939
+ Entity: 'Person'
940
+ },
941
+ (pError, pStatusCode, pBody) =>
942
+ {
943
+ Expect(pError).to.not.exist;
944
+ Expect(pStatusCode).to.equal(200);
945
+ Expect(pBody).to.be.a('string');
946
+ Expect(pBody).to.contain('Alice');
947
+ Expect(pBody).to.contain('Seattle');
948
+ return fDone();
949
+ });
950
+ });
951
+
952
+ test('Should return 400 when no data is provided',
953
+ (fDone) =>
954
+ {
955
+ makeRequest('POST', '/1.0/Comprehension/ToCSV', {},
956
+ (pError, pStatusCode, pBody) =>
957
+ {
958
+ Expect(pError).to.not.exist;
959
+ Expect(pStatusCode).to.equal(400);
960
+ return fDone();
961
+ });
962
+ });
963
+ }
964
+ );
965
+
966
+ // ===== Comprehension ToCSVFromFile Endpoint =====
967
+ suite
968
+ (
969
+ 'POST /1.0/Comprehension/ToCSVFromFile',
970
+ () =>
971
+ {
972
+ test('Should convert a comprehension file to CSV',
973
+ (fDone) =>
974
+ {
975
+ makeRequest('POST', '/1.0/Comprehension/ToCSVFromFile',
976
+ {
977
+ File: libPath.join(TEST_DATA_DIR, 'test-comprehension.json'),
978
+ Entity: 'Person'
979
+ },
980
+ (pError, pStatusCode, pBody) =>
981
+ {
982
+ Expect(pError).to.not.exist;
983
+ Expect(pStatusCode).to.equal(200);
984
+ Expect(pBody).to.be.a('string');
985
+ Expect(pBody).to.contain('Alice');
986
+ Expect(pBody).to.contain('GUIDPerson');
987
+ return fDone();
988
+ });
989
+ });
990
+
991
+ test('Should convert a JSON array file to CSV',
992
+ (fDone) =>
993
+ {
994
+ makeRequest('POST', '/1.0/Comprehension/ToCSVFromFile',
995
+ {
996
+ File: libPath.join(TEST_DATA_DIR, 'test-small.json')
997
+ },
998
+ (pError, pStatusCode, pBody) =>
999
+ {
1000
+ Expect(pError).to.not.exist;
1001
+ Expect(pStatusCode).to.equal(200);
1002
+ Expect(pBody).to.be.a('string');
1003
+ Expect(pBody).to.contain('Alice');
1004
+ Expect(pBody).to.contain('Seattle');
1005
+ return fDone();
1006
+ });
1007
+ });
1008
+
1009
+ test('Should return 400 when no File is provided',
1010
+ (fDone) =>
1011
+ {
1012
+ makeRequest('POST', '/1.0/Comprehension/ToCSVFromFile', {},
1013
+ (pError, pStatusCode, pBody) =>
1014
+ {
1015
+ Expect(pError).to.not.exist;
1016
+ Expect(pStatusCode).to.equal(400);
1017
+ return fDone();
1018
+ });
1019
+ });
1020
+
1021
+ test('Should return 404 for nonexistent file',
1022
+ (fDone) =>
1023
+ {
1024
+ makeRequest('POST', '/1.0/Comprehension/ToCSVFromFile',
1025
+ { File: '/tmp/nonexistent-test-99999.json' },
1026
+ (pError, pStatusCode, pBody) =>
1027
+ {
1028
+ Expect(pError).to.not.exist;
1029
+ Expect(pStatusCode).to.equal(404);
1030
+ return fDone();
1031
+ });
1032
+ });
1033
+ }
1034
+ );
1035
+
1036
+ // ===== Comprehension Push Endpoint =====
1037
+ suite
1038
+ (
1039
+ 'POST /1.0/Comprehension/Push',
1040
+ () =>
1041
+ {
1042
+ test('Should return 400 when no Comprehension is provided',
1043
+ (fDone) =>
1044
+ {
1045
+ makeRequest('POST', '/1.0/Comprehension/Push', {},
1046
+ (pError, pStatusCode, pBody) =>
1047
+ {
1048
+ Expect(pError).to.not.exist;
1049
+ Expect(pStatusCode).to.equal(400);
1050
+ Expect(pBody.Error).to.contain('Comprehension');
1051
+ return fDone();
1052
+ });
1053
+ });
1054
+ }
1055
+ );
1056
+
1057
+ // ===== Comprehension PushFile Endpoint =====
1058
+ suite
1059
+ (
1060
+ 'POST /1.0/Comprehension/PushFile',
1061
+ () =>
1062
+ {
1063
+ test('Should return 400 when no File is provided',
1064
+ (fDone) =>
1065
+ {
1066
+ makeRequest('POST', '/1.0/Comprehension/PushFile', {},
1067
+ (pError, pStatusCode, pBody) =>
1068
+ {
1069
+ Expect(pError).to.not.exist;
1070
+ Expect(pStatusCode).to.equal(400);
1071
+ return fDone();
1072
+ });
1073
+ });
1074
+
1075
+ test('Should return 404 for nonexistent file',
1076
+ (fDone) =>
1077
+ {
1078
+ makeRequest('POST', '/1.0/Comprehension/PushFile',
1079
+ { File: '/tmp/nonexistent-test-99999.json' },
1080
+ (pError, pStatusCode, pBody) =>
1081
+ {
1082
+ Expect(pError).to.not.exist;
1083
+ Expect(pStatusCode).to.equal(404);
1084
+ return fDone();
1085
+ });
1086
+ });
1087
+ }
1088
+ );
1089
+
1090
+ // ===== Entity FromTabularFolder Endpoint =====
1091
+ suite
1092
+ (
1093
+ 'POST /1.0/Entity/FromTabularFolder',
1094
+ () =>
1095
+ {
1096
+ test('Should generate comprehensions from a folder of CSV files',
1097
+ (fDone) =>
1098
+ {
1099
+ makeRequest('POST', '/1.0/Entity/FromTabularFolder',
1100
+ {
1101
+ Folder: libPath.join(EXAMPLE_DATA_DIR, 'seattle_neighborhoods')
1102
+ },
1103
+ (pError, pStatusCode, pBody) =>
1104
+ {
1105
+ Expect(pError).to.not.exist;
1106
+ Expect(pStatusCode).to.equal(200);
1107
+ Expect(pBody).to.be.an('object');
1108
+ // Should have entities derived from the 3 CSV files
1109
+ let tmpEntityKeys = Object.keys(pBody);
1110
+ Expect(tmpEntityKeys.length).to.be.greaterThan(0);
1111
+ return fDone();
1112
+ });
1113
+ });
1114
+
1115
+ test('Should return 400 when no Folder is provided',
1116
+ (fDone) =>
1117
+ {
1118
+ makeRequest('POST', '/1.0/Entity/FromTabularFolder', {},
1119
+ (pError, pStatusCode, pBody) =>
1120
+ {
1121
+ Expect(pError).to.not.exist;
1122
+ Expect(pStatusCode).to.equal(400);
1123
+ Expect(pBody.Error).to.contain('Folder');
1124
+ return fDone();
1125
+ });
1126
+ });
1127
+
1128
+ test('Should return 404 for nonexistent folder',
1129
+ (fDone) =>
1130
+ {
1131
+ makeRequest('POST', '/1.0/Entity/FromTabularFolder',
1132
+ { Folder: '/tmp/nonexistent-folder-99999/' },
1133
+ (pError, pStatusCode, pBody) =>
1134
+ {
1135
+ Expect(pError).to.not.exist;
1136
+ Expect(pStatusCode).to.equal(404);
1137
+ return fDone();
1138
+ });
1139
+ });
1140
+ }
1141
+ );
1142
+
1143
+ // ===== Server Instantiation Tests =====
1144
+ suite
1145
+ (
1146
+ 'Server Instantiation',
1147
+ () =>
1148
+ {
1149
+ test('Should be able to instantiate a MeadowIntegrationServer',
1150
+ (fDone) =>
1151
+ {
1152
+ let tmpServer = new MeadowIntegrationServer({ APIServerPort: 19999 });
1153
+ Expect(tmpServer).to.be.an('object');
1154
+ Expect(tmpServer._Fable).to.be.an('object');
1155
+ Expect(tmpServer._Orator).to.be.an('object');
1156
+ return fDone();
1157
+ });
1158
+
1159
+ test('Should export MeadowIntegrationServer from main module',
1160
+ (fDone) =>
1161
+ {
1162
+ let tmpModule = require('../source/Meadow-Integration.js');
1163
+ Expect(tmpModule).to.have.property('IntegrationServer');
1164
+ Expect(tmpModule.IntegrationServer).to.be.a('function');
1165
+ return fDone();
1166
+ });
1167
+ }
1168
+ );
1169
+ }
1170
+ );