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.
- package/.claude/launch.json +11 -0
- package/.dockerignore +8 -0
- package/.quackage.json +19 -0
- package/Dockerfile +26 -0
- package/bin/retold-facto.js +909 -0
- package/examples/facto-government-data.sqlite +0 -0
- package/examples/government-data-catalog.json +137 -0
- package/examples/government-data-loader.js +1432 -0
- package/package.json +91 -0
- package/scripts/facto-download.js +425 -0
- package/source/Retold-Facto.js +1042 -0
- package/source/services/Retold-Facto-BeaconProvider.js +511 -0
- package/source/services/Retold-Facto-CatalogManager.js +1252 -0
- package/source/services/Retold-Facto-DataLakeService.js +1642 -0
- package/source/services/Retold-Facto-DatasetManager.js +417 -0
- package/source/services/Retold-Facto-IngestEngine.js +1315 -0
- package/source/services/Retold-Facto-ProjectionEngine.js +3960 -0
- package/source/services/Retold-Facto-RecordManager.js +360 -0
- package/source/services/Retold-Facto-SchemaManager.js +1110 -0
- package/source/services/Retold-Facto-SourceFolderScanner.js +2243 -0
- package/source/services/Retold-Facto-SourceManager.js +730 -0
- package/source/services/Retold-Facto-StoreConnectionManager.js +441 -0
- package/source/services/Retold-Facto-ThroughputMonitor.js +478 -0
- package/source/services/web-app/codemirror-entry.js +7 -0
- package/source/services/web-app/pict-app/Pict-Application-Facto-Configuration.json +9 -0
- package/source/services/web-app/pict-app/Pict-Application-Facto.js +70 -0
- package/source/services/web-app/pict-app/Pict-Facto-Bundle.js +11 -0
- package/source/services/web-app/pict-app/providers/Pict-Provider-Facto-UI.js +66 -0
- package/source/services/web-app/pict-app/providers/Pict-Provider-Facto.js +69 -0
- package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Catalog.js +93 -0
- package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Connections.js +42 -0
- package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Datasets.js +605 -0
- package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Projections.js +188 -0
- package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Scanner.js +80 -0
- package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Schema.js +116 -0
- package/source/services/web-app/pict-app/providers/facto-api/Facto-API-Sources.js +104 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Catalog.js +526 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Datasets.js +173 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Ingest.js +259 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Layout.js +191 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Projections.js +231 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Records.js +326 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Scanner.js +624 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Sources.js +201 -0
- package/source/services/web-app/pict-app/views/PictView-Facto-Throughput.js +456 -0
- package/source/services/web-app/pict-app-full/Pict-Application-Facto-Full-Configuration.json +14 -0
- package/source/services/web-app/pict-app-full/Pict-Application-Facto-Full.js +391 -0
- package/source/services/web-app/pict-app-full/providers/PictRouter-Facto-Configuration.json +56 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-BottomBar.js +68 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Connections.js +340 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Dashboard.js +149 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Dashboards.js +819 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Datasets.js +178 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-IngestJobs.js +99 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Layout.js +62 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-MappingEditor.js +158 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-ProjectionDetail.js +1120 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Projections.js +172 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-QueryPanel.js +119 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-RecordViewer.js +663 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Records.js +648 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Scanner.js +1017 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaDetail.js +1404 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaDocEditor.js +1036 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaEditor.js +636 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SchemaResearch.js +357 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceDetail.js +822 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceEditor.js +1036 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-SourceResearch.js +487 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Sources.js +165 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-Throughput.js +439 -0
- package/source/services/web-app/pict-app-full/views/PictView-Facto-Full-TopBar.js +335 -0
- package/source/services/web-app/pict-app-full/views/projections/Facto-Projections-Constants.js +71 -0
- package/source/services/web-app/web/chart.min.js +20 -0
- package/source/services/web-app/web/codemirror-bundle.js +30099 -0
- package/source/services/web-app/web/css/facto-themes.css +467 -0
- package/source/services/web-app/web/css/facto.css +502 -0
- package/source/services/web-app/web/index.html +28 -0
- package/source/services/web-app/web/retold-facto.js +12138 -0
- package/source/services/web-app/web/retold-facto.js.map +1 -0
- package/source/services/web-app/web/retold-facto.min.js +2 -0
- package/source/services/web-app/web/retold-facto.min.js.map +1 -0
- package/source/services/web-app/web/simple/index.html +17 -0
- package/test/Facto_Browser_Integration_tests.js +798 -0
- package/test/RetoldFacto_tests.js +4117 -0
- package/test/fixtures/weather-readings.csv +17 -0
- package/test/fixtures/weather-stations.csv +9 -0
- package/test/model/MeadowModel-Extended.json +8497 -0
- package/test/model/MeadowModel-PICT.json +1 -0
- package/test/model/MeadowModel.json +1355 -0
- package/test/model/ddl/Facto.ddl +225 -0
- 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
|
+
);
|