retold-facto 0.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 (92) hide show
  1. package/.claude/launch.json +11 -0
  2. package/.dockerignore +8 -0
  3. package/.quackage.json +19 -0
  4. package/Dockerfile +26 -0
  5. package/bin/retold-facto.js +909 -0
  6. package/examples/facto-government-data.sqlite +0 -0
  7. package/examples/government-data-catalog.json +137 -0
  8. package/examples/government-data-loader.js +1432 -0
  9. package/package.json +91 -0
  10. package/scripts/facto-download.js +425 -0
  11. package/source/Retold-Facto.js +1042 -0
  12. package/source/services/Retold-Facto-BeaconProvider.js +511 -0
  13. package/source/services/Retold-Facto-CatalogManager.js +1252 -0
  14. package/source/services/Retold-Facto-DataLakeService.js +1642 -0
  15. package/source/services/Retold-Facto-DatasetManager.js +417 -0
  16. package/source/services/Retold-Facto-IngestEngine.js +1315 -0
  17. package/source/services/Retold-Facto-ProjectionEngine.js +3960 -0
  18. package/source/services/Retold-Facto-RecordManager.js +360 -0
  19. package/source/services/Retold-Facto-SchemaManager.js +1110 -0
  20. package/source/services/Retold-Facto-SourceFolderScanner.js +2243 -0
  21. package/source/services/Retold-Facto-SourceManager.js +730 -0
  22. package/source/services/Retold-Facto-StoreConnectionManager.js +441 -0
  23. package/source/services/Retold-Facto-ThroughputMonitor.js +478 -0
  24. package/source/services/web-app/codemirror-entry.js +7 -0
  25. package/source/services/web-app/pict-app/Pict-Application-Facto-Configuration.json +9 -0
  26. package/source/services/web-app/pict-app/Pict-Application-Facto.js +70 -0
  27. package/source/services/web-app/pict-app/Pict-Facto-Bundle.js +11 -0
  28. package/source/services/web-app/pict-app/providers/Pict-Provider-Facto-UI.js +66 -0
  29. package/source/services/web-app/pict-app/providers/Pict-Provider-Facto.js +69 -0
  30. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Catalog.js +93 -0
  31. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Connections.js +42 -0
  32. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Datasets.js +605 -0
  33. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Projections.js +188 -0
  34. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Scanner.js +80 -0
  35. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Schema.js +116 -0
  36. package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Sources.js +104 -0
  37. package/source/services/web-app/pict-app/views/PictView-Facto-Catalog.js +526 -0
  38. package/source/services/web-app/pict-app/views/PictView-Facto-Datasets.js +173 -0
  39. package/source/services/web-app/pict-app/views/PictView-Facto-Ingest.js +259 -0
  40. package/source/services/web-app/pict-app/views/PictView-Facto-Layout.js +191 -0
  41. package/source/services/web-app/pict-app/views/PictView-Facto-Projections.js +231 -0
  42. package/source/services/web-app/pict-app/views/PictView-Facto-Records.js +326 -0
  43. package/source/services/web-app/pict-app/views/PictView-Facto-Scanner.js +624 -0
  44. package/source/services/web-app/pict-app/views/PictView-Facto-Sources.js +201 -0
  45. package/source/services/web-app/pict-app/views/PictView-Facto-Throughput.js +456 -0
  46. package/source/services/web-app/pict-app-full/Pict-Application-Facto-Full-Configuration.json +14 -0
  47. package/source/services/web-app/pict-app-full/Pict-Application-Facto-Full.js +391 -0
  48. package/source/services/web-app/pict-app-full/providers/PictRouter-Facto-Configuration.json +56 -0
  49. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-BottomBar.js +68 -0
  50. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Connections.js +340 -0
  51. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Dashboard.js +149 -0
  52. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Dashboards.js +819 -0
  53. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Datasets.js +178 -0
  54. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-IngestJobs.js +99 -0
  55. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Layout.js +62 -0
  56. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-MappingEditor.js +158 -0
  57. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-ProjectionDetail.js +1120 -0
  58. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Projections.js +172 -0
  59. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-QueryPanel.js +119 -0
  60. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-RecordViewer.js +663 -0
  61. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Records.js +648 -0
  62. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Scanner.js +1017 -0
  63. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaDetail.js +1404 -0
  64. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaDocEditor.js +1036 -0
  65. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaEditor.js +636 -0
  66. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaResearch.js +357 -0
  67. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceDetail.js +822 -0
  68. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceEditor.js +1036 -0
  69. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceResearch.js +487 -0
  70. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Sources.js +165 -0
  71. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Throughput.js +439 -0
  72. package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-TopBar.js +335 -0
  73. package/source/services/web-app/pict-app-full/views/projections/Facto-Projections-Constants.js +71 -0
  74. package/source/services/web-app/web/chart.min.js +20 -0
  75. package/source/services/web-app/web/codemirror-bundle.js +30099 -0
  76. package/source/services/web-app/web/css/facto-themes.css +467 -0
  77. package/source/services/web-app/web/css/facto.css +502 -0
  78. package/source/services/web-app/web/index.html +28 -0
  79. package/source/services/web-app/web/retold-facto.js +12138 -0
  80. package/source/services/web-app/web/retold-facto.js.map +1 -0
  81. package/source/services/web-app/web/retold-facto.min.js +2 -0
  82. package/source/services/web-app/web/retold-facto.min.js.map +1 -0
  83. package/source/services/web-app/web/simple/index.html +17 -0
  84. package/test/Facto_Browser_Integration_tests.js +798 -0
  85. package/test/RetoldFacto_tests.js +4117 -0
  86. package/test/fixtures/weather-readings.csv +17 -0
  87. package/test/fixtures/weather-stations.csv +9 -0
  88. package/test/model/MeadowModel-Extended.json +8497 -0
  89. package/test/model/MeadowModel-PICT.json +1 -0
  90. package/test/model/MeadowModel.json +1355 -0
  91. package/test/model/ddl/Facto.ddl +225 -0
  92. package/test/model/fable-configuration.json +14 -0
@@ -0,0 +1,798 @@
1
+ /**
2
+ * Retold Facto — Browser Integration Tests
3
+ *
4
+ * End-to-end scenario: ingest two CSV files (weather stations + readings),
5
+ * create projection mappings (one graphically, one automatically via API),
6
+ * deploy projections to SQLite, execute the multi-set pipeline, and
7
+ * verify the merged result.
8
+ *
9
+ * Takes screenshots at each major step into test/screenshots/.
10
+ *
11
+ * Requires:
12
+ * npm run build (build the web UI bundle)
13
+ * npm install (puppeteer in devDependencies)
14
+ *
15
+ * Run:
16
+ * npm run test-browser
17
+ *
18
+ * @license MIT
19
+ * @author Steven Velozo <steven@velozo.com>
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ const Chai = require('chai');
25
+ const Expect = Chai.expect;
26
+
27
+ const libFS = require('fs');
28
+ const libPath = require('path');
29
+ const libHTTP = require('http');
30
+
31
+ const _TestPort = 9350;
32
+ const _BaseURL = `http://127.0.0.1:${_TestPort}`;
33
+ const _ScreenshotDir = libPath.join(__dirname, 'screenshots');
34
+
35
+ // ══════════════════════════════════════════════════════════════
36
+ // Helpers
37
+ // ══════════════════════════════════════════════════════════════
38
+
39
+ /**
40
+ * POST JSON to the facto API.
41
+ */
42
+ function apiPost(pPath, pBody)
43
+ {
44
+ return new Promise(
45
+ (fResolve, fReject) =>
46
+ {
47
+ let tmpData = JSON.stringify(pBody);
48
+ let tmpOptions =
49
+ {
50
+ hostname: '127.0.0.1',
51
+ port: _TestPort,
52
+ path: pPath,
53
+ method: 'POST',
54
+ headers:
55
+ {
56
+ 'Content-Type': 'application/json',
57
+ 'Content-Length': Buffer.byteLength(tmpData),
58
+ },
59
+ };
60
+
61
+ let tmpReq = libHTTP.request(tmpOptions,
62
+ (pRes) =>
63
+ {
64
+ let tmpChunks = [];
65
+ pRes.on('data', (pChunk) => tmpChunks.push(pChunk));
66
+ pRes.on('end', () =>
67
+ {
68
+ let tmpRaw = Buffer.concat(tmpChunks).toString();
69
+ try
70
+ {
71
+ fResolve(JSON.parse(tmpRaw));
72
+ }
73
+ catch (e)
74
+ {
75
+ fResolve(tmpRaw);
76
+ }
77
+ });
78
+ });
79
+ tmpReq.on('error', fReject);
80
+ tmpReq.write(tmpData);
81
+ tmpReq.end();
82
+ });
83
+ }
84
+
85
+ /**
86
+ * GET JSON from the facto API.
87
+ */
88
+ function apiGet(pPath)
89
+ {
90
+ return new Promise(
91
+ (fResolve, fReject) =>
92
+ {
93
+ libHTTP.get(`${_BaseURL}${pPath}`,
94
+ (pRes) =>
95
+ {
96
+ let tmpChunks = [];
97
+ pRes.on('data', (pChunk) => tmpChunks.push(pChunk));
98
+ pRes.on('end', () =>
99
+ {
100
+ let tmpRaw = Buffer.concat(tmpChunks).toString();
101
+ try
102
+ {
103
+ fResolve(JSON.parse(tmpRaw));
104
+ }
105
+ catch (e)
106
+ {
107
+ fResolve(tmpRaw);
108
+ }
109
+ });
110
+ }).on('error', fReject);
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Save a screenshot with a numbered prefix for ordering.
116
+ */
117
+ async function screenshot(pPage, pName)
118
+ {
119
+ if (!libFS.existsSync(_ScreenshotDir))
120
+ {
121
+ libFS.mkdirSync(_ScreenshotDir, { recursive: true });
122
+ }
123
+ let tmpPath = libPath.join(_ScreenshotDir, pName + '.png');
124
+ await pPage.screenshot({ path: tmpPath, fullPage: true });
125
+ console.log(` 📸 ${pName}.png`);
126
+ }
127
+
128
+ /**
129
+ * Wait for navigation to settle after a hash change.
130
+ */
131
+ async function waitForRender(pPage, pMs)
132
+ {
133
+ await pPage.evaluate((pDelay) => new Promise((r) => setTimeout(r, pDelay)), pMs || 500);
134
+ }
135
+
136
+ // ══════════════════════════════════════════════════════════════
137
+ // Server bootstrap
138
+ // ══════════════════════════════════════════════════════════════
139
+
140
+ function startFactoServer(fCallback)
141
+ {
142
+ const libFable = require('pict');
143
+ const libMeadowConnectionManager = require('meadow-connection-manager');
144
+ const libRetoldFacto = require('../source/Retold-Facto.js');
145
+
146
+ let tmpSettings =
147
+ {
148
+ Product: 'FactoBrowserTest',
149
+ ProductVersion: '0.0.1',
150
+ APIServerPort: _TestPort,
151
+ SQLite:
152
+ {
153
+ SQLiteFilePath: ':memory:',
154
+ },
155
+ LogStreams:
156
+ [
157
+ {
158
+ streamtype: 'console',
159
+ level: 'warn',
160
+ },
161
+ ],
162
+ };
163
+
164
+ let tmpFable = new libFable(tmpSettings);
165
+
166
+ // Bootstrap via connection manager (matches production bin/retold-facto.js)
167
+ tmpFable.serviceManager.addServiceType('MeadowConnectionManager', libMeadowConnectionManager);
168
+ tmpFable.serviceManager.instantiateServiceProvider('MeadowConnectionManager');
169
+
170
+ tmpFable.MeadowConnectionManager.connect('facto',
171
+ {
172
+ Type: 'SQLite',
173
+ SQLiteFilePath: ':memory:',
174
+ },
175
+ (pError, pConnection) =>
176
+ {
177
+ if (pError) return fCallback(pError);
178
+
179
+ tmpFable.MeadowSQLiteProvider = pConnection.instance;
180
+ tmpFable.settings.MeadowProvider = 'SQLite';
181
+
182
+ let tmpDB = tmpFable.MeadowSQLiteProvider.db;
183
+ tmpDB.exec(libRetoldFacto.FACTO_SCHEMA_SQL);
184
+
185
+ tmpFable.serviceManager.addServiceType('RetoldFacto', libRetoldFacto);
186
+ let tmpFacto = tmpFable.serviceManager.instantiateServiceProvider('RetoldFacto',
187
+ {
188
+ StorageProvider: 'SQLite',
189
+ AutoCreateSchema: false,
190
+
191
+ FullMeadowSchemaPath: libPath.join(__dirname, 'model') + '/',
192
+ FullMeadowSchemaFilename: 'MeadowModel-Extended.json',
193
+
194
+ AutoStartOrator: true,
195
+
196
+ Endpoints:
197
+ {
198
+ MeadowEndpoints: true,
199
+ SourceManager: true,
200
+ RecordManager: true,
201
+ DatasetManager: true,
202
+ IngestEngine: true,
203
+ ProjectionEngine: true,
204
+ CatalogManager: true,
205
+ StoreConnectionManager: true,
206
+ SchemaManager: true,
207
+ WebUI: true,
208
+ },
209
+ });
210
+
211
+ tmpFacto.onBeforeInitialize = (fCB) =>
212
+ {
213
+ tmpFable.OratorServiceServer.server.use(require('restify').plugins.bodyParser());
214
+ tmpFable.OratorServiceServer.server.use(require('restify').plugins.queryParser());
215
+ return fCB();
216
+ };
217
+
218
+ tmpFacto.initializeService(
219
+ (pInitError) =>
220
+ {
221
+ if (pInitError) return fCallback(pInitError);
222
+ return fCallback(null, tmpFable, tmpFacto);
223
+ });
224
+ });
225
+ }
226
+
227
+ // ══════════════════════════════════════════════════════════════
228
+ // Test suite
229
+ // ══════════════════════════════════════════════════════════════
230
+
231
+ suite
232
+ (
233
+ 'Facto-Browser-Integration',
234
+ function ()
235
+ {
236
+ this.timeout(120000);
237
+
238
+ let _Fable;
239
+ let _Facto;
240
+ let _Browser;
241
+ let _Page;
242
+ let _Puppeteer;
243
+
244
+ // IDs collected during the test flow
245
+ let _IDSourceStations;
246
+ let _IDSourceReadings;
247
+ let _IDDatasetStations;
248
+ let _IDDatasetReadings;
249
+ let _IDDatasetProjection;
250
+ let _IDStoreConnection;
251
+ let _IDProjectionStore;
252
+
253
+ suiteSetup
254
+ (
255
+ function (fDone)
256
+ {
257
+ // Verify web assets exist
258
+ let tmpWebDir = libPath.join(__dirname, '..', 'source', 'services', 'web-app', 'web');
259
+ if (!libFS.existsSync(libPath.join(tmpWebDir, 'retold-facto.js')))
260
+ {
261
+ return fDone(new Error('Web UI not built. Run "npm run build" first.'));
262
+ }
263
+
264
+ // Clean screenshot directory
265
+ if (libFS.existsSync(_ScreenshotDir))
266
+ {
267
+ let tmpFiles = libFS.readdirSync(_ScreenshotDir);
268
+ for (let i = 0; i < tmpFiles.length; i++)
269
+ {
270
+ if (tmpFiles[i].endsWith('.png'))
271
+ {
272
+ libFS.unlinkSync(libPath.join(_ScreenshotDir, tmpFiles[i]));
273
+ }
274
+ }
275
+ }
276
+
277
+ // Start the server
278
+ startFactoServer(
279
+ (pError, pFable, pFacto) =>
280
+ {
281
+ if (pError) return fDone(pError);
282
+ _Fable = pFable;
283
+ _Facto = pFacto;
284
+
285
+ try
286
+ {
287
+ _Puppeteer = require('puppeteer');
288
+ }
289
+ catch (e)
290
+ {
291
+ return fDone(new Error('puppeteer is not installed.'));
292
+ }
293
+
294
+ _Puppeteer.launch(
295
+ {
296
+ headless: true,
297
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
298
+ })
299
+ .then(
300
+ (pBrowser) =>
301
+ {
302
+ _Browser = pBrowser;
303
+ return _Browser.newPage();
304
+ })
305
+ .then(
306
+ (pPage) =>
307
+ {
308
+ _Page = pPage;
309
+ return _Page.setViewport({ width: 1440, height: 900 });
310
+ })
311
+ .then(() => fDone())
312
+ .catch(fDone);
313
+ });
314
+ }
315
+ );
316
+
317
+ suiteTeardown
318
+ (
319
+ function (fDone)
320
+ {
321
+ let tmpSteps = [];
322
+ if (_Browser) tmpSteps.push(_Browser.close().catch(() => {}));
323
+
324
+ Promise.all(tmpSteps).then(
325
+ () =>
326
+ {
327
+ if (_Facto && _Facto.serviceInitialized)
328
+ {
329
+ _Facto.stopService(fDone);
330
+ }
331
+ else
332
+ {
333
+ fDone();
334
+ }
335
+ });
336
+ }
337
+ );
338
+
339
+ // ─────────────────────────────────────────────────
340
+ // Phase 1: Data Setup via API
341
+ // ─────────────────────────────────────────────────
342
+
343
+ test
344
+ (
345
+ 'Create sources for weather stations and readings',
346
+ async function ()
347
+ {
348
+ let tmpStations = await apiPost('/1.0/Source',
349
+ { Name: 'NOAA Weather Stations', Type: 'File', Active: 1 });
350
+ _IDSourceStations = tmpStations.IDSource;
351
+ Expect(_IDSourceStations).to.be.above(0);
352
+
353
+ let tmpReadings = await apiPost('/1.0/Source',
354
+ { Name: 'Daily Weather Readings', Type: 'File', Active: 1 });
355
+ _IDSourceReadings = tmpReadings.IDSource;
356
+ Expect(_IDSourceReadings).to.be.above(0);
357
+ }
358
+ );
359
+
360
+ test
361
+ (
362
+ 'Create raw datasets for stations and readings',
363
+ async function ()
364
+ {
365
+ let tmpDS1 = await apiPost('/1.0/Dataset',
366
+ { Name: 'Weather Stations', Type: 'Raw', Description: 'NOAA station locations and metadata' });
367
+ _IDDatasetStations = tmpDS1.IDDataset;
368
+ Expect(_IDDatasetStations).to.be.above(0);
369
+
370
+ let tmpDS2 = await apiPost('/1.0/Dataset',
371
+ { Name: 'Weather Readings', Type: 'Raw', Description: 'Daily weather observations per station' });
372
+ _IDDatasetReadings = tmpDS2.IDDataset;
373
+ Expect(_IDDatasetReadings).to.be.above(0);
374
+ }
375
+ );
376
+
377
+ test
378
+ (
379
+ 'Ingest weather-stations.csv',
380
+ async function ()
381
+ {
382
+ let tmpCSV = libFS.readFileSync(libPath.join(__dirname, 'fixtures', 'weather-stations.csv'), 'utf8');
383
+ let tmpResult = await apiPost('/facto/ingest/file',
384
+ {
385
+ IDDataset: _IDDatasetStations,
386
+ IDSource: _IDSourceStations,
387
+ Content: tmpCSV,
388
+ Format: 'csv',
389
+ Type: 'station',
390
+ });
391
+ Expect(tmpResult.Ingested).to.equal(8);
392
+ }
393
+ );
394
+
395
+ test
396
+ (
397
+ 'Ingest weather-readings.csv',
398
+ async function ()
399
+ {
400
+ let tmpCSV = libFS.readFileSync(libPath.join(__dirname, 'fixtures', 'weather-readings.csv'), 'utf8');
401
+ let tmpResult = await apiPost('/facto/ingest/file',
402
+ {
403
+ IDDataset: _IDDatasetReadings,
404
+ IDSource: _IDSourceReadings,
405
+ Content: tmpCSV,
406
+ Format: 'csv',
407
+ Type: 'reading',
408
+ });
409
+ Expect(tmpResult.Ingested).to.equal(16);
410
+ }
411
+ );
412
+
413
+ test
414
+ (
415
+ 'Create a projection dataset for the combined weather view',
416
+ async function ()
417
+ {
418
+ let tmpDS = await apiPost('/1.0/Dataset',
419
+ {
420
+ Name: 'Weather Summary',
421
+ Type: 'Projection',
422
+ Description: 'Combined station metadata + daily readings',
423
+ SchemaDefinition: [
424
+ '! WeatherSummary',
425
+ '@ IDWeatherSummary',
426
+ '% GUIDWeatherSummary',
427
+ '$ StationID 64',
428
+ '$ StationName 200',
429
+ '$ State 4',
430
+ '$ Date 20',
431
+ '# TempHighF',
432
+ '# TempLowF',
433
+ '. PrecipInches',
434
+ '# WindMPH',
435
+ '$ Condition 64',
436
+ '. Latitude',
437
+ '. Longitude',
438
+ '# Elevation',
439
+ ].join('\n'),
440
+ });
441
+ _IDDatasetProjection = tmpDS.IDDataset;
442
+ Expect(_IDDatasetProjection).to.be.above(0);
443
+ }
444
+ );
445
+
446
+ test
447
+ (
448
+ 'Create an in-memory SQLite store connection',
449
+ async function ()
450
+ {
451
+ let tmpConn = await apiPost('/1.0/StoreConnection',
452
+ {
453
+ Name: 'Test SQLite',
454
+ Type: 'SQLite',
455
+ Config: JSON.stringify({ SQLiteFilePath: ':memory:' }),
456
+ Status: 'OK',
457
+ });
458
+ _IDStoreConnection = tmpConn.IDStoreConnection;
459
+ Expect(_IDStoreConnection).to.be.above(0);
460
+ }
461
+ );
462
+
463
+ test
464
+ (
465
+ 'Deploy the projection schema to the SQLite store',
466
+ async function ()
467
+ {
468
+ let tmpResult = await apiPost(`/facto/projection/${_IDDatasetProjection}/deploy`,
469
+ {
470
+ IDStoreConnection: _IDStoreConnection,
471
+ TargetTableName: 'WeatherSummary',
472
+ });
473
+ Expect(tmpResult.Success).to.equal(true);
474
+ // ProjectionStore may be a Meadow query wrapper; extract the ID
475
+ let tmpPS = tmpResult.ProjectionStore || {};
476
+ _IDProjectionStore = tmpPS.IDProjectionStore
477
+ || (tmpPS.result && tmpPS.result.value && tmpPS.result.value[0] && tmpPS.result.value[0].IDProjectionStore)
478
+ || 0;
479
+
480
+ // If still 0, try to find it by querying
481
+ if (!_IDProjectionStore)
482
+ {
483
+ let tmpStores = await apiGet(`/facto/projection/${_IDDatasetProjection}/stores`);
484
+ if (tmpStores.Stores && tmpStores.Stores.length > 0)
485
+ {
486
+ _IDProjectionStore = tmpStores.Stores[0].IDProjectionStore;
487
+ }
488
+ }
489
+ Expect(_IDProjectionStore).to.be.above(0);
490
+ }
491
+ );
492
+
493
+ // ─────────────────────────────────────────────────
494
+ // Phase 2: Create mappings via API
495
+ // ─────────────────────────────────────────────────
496
+
497
+ test
498
+ (
499
+ 'Create mapping for stations (auto-mapped via API)',
500
+ async function ()
501
+ {
502
+ let tmpResult = await apiPost(`/facto/projection/${_IDDatasetProjection}/mapping`,
503
+ {
504
+ IDSource: _IDSourceStations,
505
+ Name: 'Stations Auto-Map',
506
+ MappingConfiguration: JSON.stringify(
507
+ {
508
+ Entity: 'WeatherSummary',
509
+ GUIDTemplate: '{~D:Record.StationID~}',
510
+ Mappings:
511
+ {
512
+ StationID: 'StationID',
513
+ StationName: 'Name',
514
+ State: 'State',
515
+ Latitude: 'Latitude',
516
+ Longitude: 'Longitude',
517
+ Elevation: 'Elevation',
518
+ },
519
+ }),
520
+ });
521
+ Expect(tmpResult.Success).to.equal(true);
522
+ Expect(tmpResult.Mapping).to.be.an('object');
523
+ // Meadow may return a query wrapper; try the direct property first
524
+ let tmpID = tmpResult.Mapping.IDProjectionMapping
525
+ || (tmpResult.Mapping.result && tmpResult.Mapping.result.value && tmpResult.Mapping.result.value[0] && tmpResult.Mapping.result.value[0].IDProjectionMapping)
526
+ || 0;
527
+ Expect(tmpID).to.be.above(0);
528
+ }
529
+ );
530
+
531
+ test
532
+ (
533
+ 'Create mapping for readings (auto-mapped via API)',
534
+ async function ()
535
+ {
536
+ let tmpResult = await apiPost(`/facto/projection/${_IDDatasetProjection}/mapping`,
537
+ {
538
+ IDSource: _IDSourceReadings,
539
+ Name: 'Readings Auto-Map',
540
+ MappingConfiguration: JSON.stringify(
541
+ {
542
+ Entity: 'WeatherSummary',
543
+ GUIDTemplate: '{~D:Record.StationID~}-{~D:Record.Date~}',
544
+ Mappings:
545
+ {
546
+ StationID: 'StationID',
547
+ Date: 'Date',
548
+ TempHighF: 'TempHighF',
549
+ TempLowF: 'TempLowF',
550
+ PrecipInches: 'PrecipInches',
551
+ WindMPH: 'WindMPH',
552
+ Condition: 'Condition',
553
+ },
554
+ }),
555
+ });
556
+ Expect(tmpResult.Success).to.equal(true);
557
+ Expect(tmpResult.Mapping).to.be.an('object');
558
+ let tmpID = tmpResult.Mapping.IDProjectionMapping
559
+ || (tmpResult.Mapping.result && tmpResult.Mapping.result.value && tmpResult.Mapping.result.value[0] && tmpResult.Mapping.result.value[0].IDProjectionMapping)
560
+ || 0;
561
+ Expect(tmpID).to.be.above(0);
562
+ }
563
+ );
564
+
565
+ // ─────────────────────────────────────────────────
566
+ // Phase 3: Browse the UI and screenshot
567
+ // ─────────────────────────────────────────────────
568
+
569
+ test
570
+ (
571
+ 'Screenshot: Dashboard',
572
+ async function ()
573
+ {
574
+ await _Page.goto(`${_BaseURL}/`, { waitUntil: 'networkidle0', timeout: 30000 });
575
+ await waitForRender(_Page, 1000);
576
+ await screenshot(_Page, '01-dashboard');
577
+ }
578
+ );
579
+
580
+ test
581
+ (
582
+ 'Screenshot: Sources list shows our 2 sources',
583
+ async function ()
584
+ {
585
+ await _Page.goto(`${_BaseURL}/#/Sources`, { waitUntil: 'networkidle0', timeout: 15000 });
586
+ await waitForRender(_Page, 1000);
587
+ await screenshot(_Page, '02-sources');
588
+
589
+ // Verify sources rendered
590
+ let tmpContent = await _Page.content();
591
+ Expect(tmpContent).to.include('NOAA Weather Stations');
592
+ Expect(tmpContent).to.include('Daily Weather Readings');
593
+ }
594
+ );
595
+
596
+ test
597
+ (
598
+ 'Screenshot: Datasets list shows our 3 datasets',
599
+ async function ()
600
+ {
601
+ await _Page.goto(`${_BaseURL}/#/Datasets`, { waitUntil: 'networkidle0', timeout: 15000 });
602
+ await waitForRender(_Page, 1000);
603
+ await screenshot(_Page, '03-datasets');
604
+
605
+ let tmpContent = await _Page.content();
606
+ Expect(tmpContent).to.include('Weather Stations');
607
+ Expect(tmpContent).to.include('Weather Readings');
608
+ Expect(tmpContent).to.include('Weather Summary');
609
+ }
610
+ );
611
+
612
+ test
613
+ (
614
+ 'Screenshot: Records list shows ingested data',
615
+ async function ()
616
+ {
617
+ await _Page.goto(`${_BaseURL}/#/Records`, { waitUntil: 'networkidle0', timeout: 15000 });
618
+ await waitForRender(_Page, 1000);
619
+ await screenshot(_Page, '04-records');
620
+ }
621
+ );
622
+
623
+ test
624
+ (
625
+ 'Screenshot: Projections list shows the Weather Summary projection',
626
+ async function ()
627
+ {
628
+ await _Page.goto(`${_BaseURL}/#/Projections`, { waitUntil: 'networkidle0', timeout: 15000 });
629
+ await waitForRender(_Page, 1000);
630
+ await screenshot(_Page, '05-projections');
631
+
632
+ let tmpContent = await _Page.content();
633
+ Expect(tmpContent).to.include('Weather Summary');
634
+ }
635
+ );
636
+
637
+ test
638
+ (
639
+ 'Screenshot: Projection detail with schema and mappings',
640
+ async function ()
641
+ {
642
+ await _Page.goto(`${_BaseURL}/#/Projection/${_IDDatasetProjection}`, { waitUntil: 'networkidle0', timeout: 15000 });
643
+ await waitForRender(_Page, 1500);
644
+ await screenshot(_Page, '06-projection-detail');
645
+ }
646
+ );
647
+
648
+ test
649
+ (
650
+ 'Screenshot: Connections page shows the SQLite store',
651
+ async function ()
652
+ {
653
+ await _Page.goto(`${_BaseURL}/#/Connections`, { waitUntil: 'networkidle0', timeout: 15000 });
654
+ await waitForRender(_Page, 1000);
655
+ await screenshot(_Page, '07-connections');
656
+
657
+ let tmpContent = await _Page.content();
658
+ Expect(tmpContent).to.include('Test SQLite');
659
+ }
660
+ );
661
+
662
+ // ─────────────────────────────────────────────────
663
+ // Phase 4: Execute projection pipeline
664
+ // ─────────────────────────────────────────────────
665
+
666
+ test
667
+ (
668
+ 'Create a multi-set projection to merge stations + readings',
669
+ async function ()
670
+ {
671
+ let tmpResult = await apiPost(`/facto/projection/${_IDDatasetProjection}/multi-set-projection`,
672
+ {
673
+ Name: 'Weather Merge',
674
+ IDProjectionStore: _IDProjectionStore,
675
+ PipelineConfiguration: JSON.stringify(
676
+ {
677
+ MergeStrategy: 'WriteAll',
678
+ }),
679
+ });
680
+ Expect(tmpResult.Success).to.equal(true);
681
+ Expect(tmpResult.MultiSetProjection).to.be.an('object');
682
+ }
683
+ );
684
+
685
+ test
686
+ (
687
+ 'Execute the stations mapping import',
688
+ async function ()
689
+ {
690
+ let tmpMappings = await apiGet(`/facto/projection/${_IDDatasetProjection}/mappings`);
691
+ Expect(tmpMappings.Mappings.length).to.be.above(0);
692
+
693
+ // Import stations first
694
+ let tmpStationsMapping = tmpMappings.Mappings.find((m) => m.Name === 'Stations Auto-Map');
695
+ Expect(tmpStationsMapping).to.be.an('object');
696
+
697
+ let tmpResult = await apiPost(`/facto/projection/${_IDDatasetProjection}/import`,
698
+ {
699
+ IDProjectionMapping: tmpStationsMapping.IDProjectionMapping,
700
+ IDProjectionStore: _IDProjectionStore,
701
+ IDSource: _IDSourceStations,
702
+ });
703
+ Expect(tmpResult.Imported || tmpResult.RecordsProcessed || 0).to.be.at.least(0);
704
+ }
705
+ );
706
+
707
+ test
708
+ (
709
+ 'Execute the readings mapping import',
710
+ async function ()
711
+ {
712
+ let tmpMappings = await apiGet(`/facto/projection/${_IDDatasetProjection}/mappings`);
713
+ let tmpReadingsMapping = tmpMappings.Mappings.find((m) => m.Name === 'Readings Auto-Map');
714
+ Expect(tmpReadingsMapping).to.be.an('object');
715
+
716
+ let tmpResult = await apiPost(`/facto/projection/${_IDDatasetProjection}/import`,
717
+ {
718
+ IDProjectionMapping: tmpReadingsMapping.IDProjectionMapping,
719
+ IDProjectionStore: _IDProjectionStore,
720
+ IDSource: _IDSourceReadings,
721
+ });
722
+ Expect(tmpResult.Imported || tmpResult.RecordsProcessed || 0).to.be.at.least(0);
723
+ }
724
+ );
725
+
726
+ // ─────────────────────────────────────────────────
727
+ // Phase 5: Verify results in UI
728
+ // ─────────────────────────────────────────────────
729
+
730
+ test
731
+ (
732
+ 'Screenshot: Projection detail after import execution',
733
+ async function ()
734
+ {
735
+ await _Page.goto(`${_BaseURL}/#/Projection/${_IDDatasetProjection}`, { waitUntil: 'networkidle0', timeout: 15000 });
736
+ await waitForRender(_Page, 1500);
737
+ await screenshot(_Page, '08-projection-after-import');
738
+ }
739
+ );
740
+
741
+ test
742
+ (
743
+ 'Screenshot: Dashboard with populated data',
744
+ async function ()
745
+ {
746
+ await _Page.goto(`${_BaseURL}/#/Home`, { waitUntil: 'networkidle0', timeout: 15000 });
747
+ await waitForRender(_Page, 1500);
748
+ await screenshot(_Page, '09-dashboard-final');
749
+ }
750
+ );
751
+
752
+ test
753
+ (
754
+ 'Screenshot: Dashboards / histograms for ingestion',
755
+ async function ()
756
+ {
757
+ await _Page.goto(`${_BaseURL}/#/Dashboards`, { waitUntil: 'networkidle0', timeout: 15000 });
758
+ await waitForRender(_Page, 2000);
759
+ await screenshot(_Page, '09b-dashboards-histogram');
760
+ }
761
+ );
762
+
763
+ test
764
+ (
765
+ 'Screenshot: Throughput monitor',
766
+ async function ()
767
+ {
768
+ await _Page.goto(`${_BaseURL}/#/Throughput`, { waitUntil: 'networkidle0', timeout: 15000 });
769
+ await waitForRender(_Page, 1500);
770
+ await screenshot(_Page, '09c-throughput');
771
+ }
772
+ );
773
+
774
+ test
775
+ (
776
+ 'Verify ingested record counts via API',
777
+ async function ()
778
+ {
779
+ let tmpStationRecords = await apiGet(`/facto/dataset/${_IDDatasetStations}/stats`);
780
+ Expect(tmpStationRecords.RecordCount).to.equal(8);
781
+
782
+ let tmpReadingRecords = await apiGet(`/facto/dataset/${_IDDatasetReadings}/stats`);
783
+ Expect(tmpReadingRecords.RecordCount).to.equal(16);
784
+ }
785
+ );
786
+
787
+ test
788
+ (
789
+ 'Screenshot: IngestJobs showing completed imports',
790
+ async function ()
791
+ {
792
+ await _Page.goto(`${_BaseURL}/#/IngestJobs`, { waitUntil: 'networkidle0', timeout: 15000 });
793
+ await waitForRender(_Page, 1000);
794
+ await screenshot(_Page, '10-ingest-jobs-final');
795
+ }
796
+ );
797
+ }
798
+ );