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,605 @@
1
+ module.exports =
2
+ {
3
+ // ================================================================
4
+ // Dataset Operations
5
+ // ================================================================
6
+
7
+ loadDatasets: function()
8
+ {
9
+ return this.api('GET', '/1.0/Datasets/0/100').then(
10
+ (pResponse) =>
11
+ {
12
+ this.pict.AppData.Facto.Datasets = Array.isArray(pResponse) ? pResponse : [];
13
+ });
14
+ },
15
+
16
+ createDataset: function(pDatasetData)
17
+ {
18
+ return this.api('POST', '/1.0/Dataset', pDatasetData).then(
19
+ (pResponse) =>
20
+ {
21
+ return pResponse;
22
+ });
23
+ },
24
+
25
+ loadDatasetStats: function(pIDDataset)
26
+ {
27
+ return this.api('GET', `/facto/dataset/${pIDDataset}/stats`).then(
28
+ (pResponse) =>
29
+ {
30
+ return pResponse;
31
+ });
32
+ },
33
+
34
+ loadDatasetSources: function(pIDDataset)
35
+ {
36
+ return this.api('GET', `/facto/dataset/${pIDDataset}/sources`).then(
37
+ (pResponse) =>
38
+ {
39
+ return pResponse;
40
+ });
41
+ },
42
+
43
+ linkDatasetSource: function(pIDDataset, pIDSource, pReliabilityWeight)
44
+ {
45
+ return this.api('POST', `/facto/dataset/${pIDDataset}/source`,
46
+ {
47
+ IDSource: pIDSource,
48
+ ReliabilityWeight: pReliabilityWeight || 1.0
49
+ }).then(
50
+ (pResponse) =>
51
+ {
52
+ return pResponse;
53
+ });
54
+ },
55
+
56
+ loadDatasetRecords: function(pIDDataset, pBegin, pCap)
57
+ {
58
+ return this.api('GET', `/facto/dataset/${pIDDataset}/records/${pBegin || 0}/${pCap || 50}`).then(
59
+ (pResponse) =>
60
+ {
61
+ return pResponse;
62
+ });
63
+ },
64
+
65
+ // ================================================================
66
+ // Record Operations
67
+ // ================================================================
68
+
69
+ loadRecords: function(pPage)
70
+ {
71
+ let tmpPageSize = this.pict.AppData.Facto.RecordPageSize;
72
+ let tmpBegin = (pPage || 0) * tmpPageSize;
73
+ return this.api('GET', `/1.0/Records/${tmpBegin}/${tmpPageSize}`).then(
74
+ (pResponse) =>
75
+ {
76
+ this.pict.AppData.Facto.Records = Array.isArray(pResponse) ? pResponse : [];
77
+ });
78
+ },
79
+
80
+ expandDateFilter: function(pDateStr, pIsEnd)
81
+ {
82
+ if (!pDateStr || typeof pDateStr !== 'string')
83
+ {
84
+ return null;
85
+ }
86
+ let tmpTrimmed = pDateStr.trim();
87
+ if (!tmpTrimmed)
88
+ {
89
+ return null;
90
+ }
91
+
92
+ // Year only: YYYY
93
+ let tmpYearMatch = tmpTrimmed.match(/^(\d{4})$/);
94
+ if (tmpYearMatch)
95
+ {
96
+ return pIsEnd ? tmpYearMatch[1] + '-12-31' : tmpYearMatch[1] + '-01-01';
97
+ }
98
+
99
+ // Year-Month: YYYY-MM
100
+ let tmpMonthMatch = tmpTrimmed.match(/^(\d{4})-(\d{1,2})$/);
101
+ if (tmpMonthMatch)
102
+ {
103
+ let tmpYear = parseInt(tmpMonthMatch[1], 10);
104
+ let tmpMonth = parseInt(tmpMonthMatch[2], 10);
105
+ if (tmpMonth < 1 || tmpMonth > 12)
106
+ {
107
+ return null;
108
+ }
109
+ let tmpMonthStr = String(tmpMonth).padStart(2, '0');
110
+ if (pIsEnd)
111
+ {
112
+ let tmpLastDay = new Date(tmpYear, tmpMonth, 0).getDate();
113
+ return tmpYear + '-' + tmpMonthStr + '-' + String(tmpLastDay).padStart(2, '0');
114
+ }
115
+ return tmpYear + '-' + tmpMonthStr + '-01';
116
+ }
117
+
118
+ // Full date: YYYY-MM-DD
119
+ let tmpDayMatch = tmpTrimmed.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
120
+ if (tmpDayMatch)
121
+ {
122
+ let tmpMonth = parseInt(tmpDayMatch[2], 10);
123
+ let tmpDay = parseInt(tmpDayMatch[3], 10);
124
+ if (tmpMonth < 1 || tmpMonth > 12 || tmpDay < 1 || tmpDay > 31)
125
+ {
126
+ return null;
127
+ }
128
+ return tmpDayMatch[1] + '-' + String(tmpMonth).padStart(2, '0') + '-' + String(tmpDay).padStart(2, '0');
129
+ }
130
+
131
+ return null;
132
+ },
133
+
134
+ loadFilteredRecords: function(pPage, pSourceIDs, pDateFrom, pDateTo, pDatasetIDs)
135
+ {
136
+ let tmpPageSize = this.pict.AppData.Facto.RecordPageSize;
137
+ let tmpBegin = (pPage || 0) * tmpPageSize;
138
+
139
+ // Split selected datasets into raw vs projection
140
+ let tmpRawDatasetIDs = [];
141
+ let tmpProjectionDatasets = [];
142
+ let tmpAllDatasets = this.pict.AppData.Facto.Datasets || [];
143
+
144
+ if (Array.isArray(pDatasetIDs) && pDatasetIDs.length > 0)
145
+ {
146
+ for (let i = 0; i < pDatasetIDs.length; i++)
147
+ {
148
+ let tmpID = parseInt(pDatasetIDs[i], 10);
149
+ let tmpDataset = tmpAllDatasets.find(function(d) { return d.IDDataset === tmpID; });
150
+ if (tmpDataset && tmpDataset.Type === 'Projection')
151
+ {
152
+ tmpProjectionDatasets.push(tmpDataset);
153
+ }
154
+ else
155
+ {
156
+ tmpRawDatasetIDs.push(tmpID);
157
+ }
158
+ }
159
+ }
160
+
161
+ let tmpPromises = [];
162
+
163
+ // Query raw records from the Record table
164
+ if (tmpRawDatasetIDs.length > 0 || tmpProjectionDatasets.length === 0)
165
+ {
166
+ let tmpFilterParts = [];
167
+
168
+ if (Array.isArray(pSourceIDs) && pSourceIDs.length > 0)
169
+ {
170
+ tmpFilterParts.push('FBV~IDSource~INN~' + pSourceIDs.join(','));
171
+ }
172
+ if (tmpRawDatasetIDs.length > 0)
173
+ {
174
+ tmpFilterParts.push('FBV~IDDataset~INN~' + tmpRawDatasetIDs.join(','));
175
+ }
176
+
177
+ let tmpExpandedFrom = this.expandDateFilter(pDateFrom, false);
178
+ let tmpExpandedTo = this.expandDateFilter(pDateTo, true);
179
+ if (tmpExpandedFrom)
180
+ {
181
+ tmpFilterParts.push('FBV~IngestDate~GE~' + encodeURIComponent(tmpExpandedFrom));
182
+ }
183
+ if (tmpExpandedTo)
184
+ {
185
+ tmpFilterParts.push('FBV~IngestDate~LE~' + encodeURIComponent(tmpExpandedTo));
186
+ }
187
+
188
+ let tmpURL;
189
+ if (tmpFilterParts.length > 0)
190
+ {
191
+ tmpURL = '/1.0/Records/FilteredTo/' + tmpFilterParts.join('~') + '/' + tmpBegin + '/' + tmpPageSize;
192
+ }
193
+ else
194
+ {
195
+ tmpURL = '/1.0/Records/' + tmpBegin + '/' + tmpPageSize;
196
+ }
197
+
198
+ // Only query raw records if no projection-only filter is active
199
+ if (tmpProjectionDatasets.length === 0 || tmpRawDatasetIDs.length > 0)
200
+ {
201
+ tmpPromises.push(this.api('GET', tmpURL));
202
+ }
203
+ }
204
+
205
+ // Query each projection dataset from its own Meadow endpoint
206
+ for (let i = 0; i < tmpProjectionDatasets.length; i++)
207
+ {
208
+ let tmpProjName = tmpProjectionDatasets[i].Name;
209
+ let tmpProjURL = '/1.0/' + tmpProjName + 's/' + tmpBegin + '/' + tmpPageSize;
210
+ tmpPromises.push(
211
+ this.api('GET', tmpProjURL).then(
212
+ function(pRecords)
213
+ {
214
+ // Normalize projection records to look like Record entities
215
+ // so the table can display them consistently
216
+ if (!Array.isArray(pRecords)) return [];
217
+ return pRecords.map(function(pRec)
218
+ {
219
+ return {
220
+ IDRecord: pRec['ID' + tmpProjName] || 0,
221
+ IDDataset: tmpProjectionDatasets[i].IDDataset,
222
+ IDSource: 0,
223
+ GUIDRecord: pRec['GUID' + tmpProjName] || '',
224
+ Content: JSON.stringify(pRec),
225
+ Type: 'projection',
226
+ IngestDate: pRec.CreateDate || ''
227
+ };
228
+ });
229
+ }).catch(function() { return []; })
230
+ );
231
+ }
232
+
233
+ return Promise.all(tmpPromises).then(
234
+ (pResults) =>
235
+ {
236
+ let tmpMerged = [];
237
+ for (let i = 0; i < pResults.length; i++)
238
+ {
239
+ if (Array.isArray(pResults[i]))
240
+ {
241
+ tmpMerged = tmpMerged.concat(pResults[i]);
242
+ }
243
+ }
244
+ this.pict.AppData.Facto.Records = tmpMerged;
245
+ });
246
+ },
247
+
248
+ ingestRecords: function(pRecords, pIDDataset, pIDSource)
249
+ {
250
+ return this.api('POST', '/facto/record/ingest',
251
+ {
252
+ Records: pRecords,
253
+ IDDataset: pIDDataset,
254
+ IDSource: pIDSource
255
+ }).then(
256
+ (pResponse) =>
257
+ {
258
+ return pResponse;
259
+ });
260
+ },
261
+
262
+ loadRecordCertainty: function(pIDRecord)
263
+ {
264
+ return this.api('GET', `/facto/record/${pIDRecord}/certainty`).then(
265
+ (pResponse) =>
266
+ {
267
+ return pResponse;
268
+ });
269
+ },
270
+
271
+ addRecordCertainty: function(pIDRecord, pCertaintyValue, pDimension, pJustification)
272
+ {
273
+ return this.api('POST', `/facto/record/${pIDRecord}/certainty`,
274
+ {
275
+ CertaintyValue: pCertaintyValue,
276
+ Dimension: pDimension || 'overall',
277
+ Justification: pJustification || ''
278
+ }).then(
279
+ (pResponse) =>
280
+ {
281
+ return pResponse;
282
+ });
283
+ },
284
+
285
+ loadRecordVersions: function(pIDRecord)
286
+ {
287
+ return this.api('GET', `/facto/record/${pIDRecord}/versions`).then(
288
+ (pResponse) =>
289
+ {
290
+ return pResponse;
291
+ });
292
+ },
293
+
294
+ // ================================================================
295
+ // Ingest Job Operations
296
+ // ================================================================
297
+
298
+ // ================================================================
299
+ // Count Helpers (for filter UI)
300
+ // ================================================================
301
+
302
+ loadSourceCounts: function()
303
+ {
304
+ let tmpSources = this.pict.AppData.Facto.Sources;
305
+ if (!Array.isArray(tmpSources) || tmpSources.length === 0)
306
+ {
307
+ return Promise.resolve();
308
+ }
309
+ let tmpPromises = tmpSources.map(
310
+ (pSource) =>
311
+ {
312
+ return this.loadSourceSummary(pSource.IDSource).then(
313
+ (pSummary) =>
314
+ {
315
+ if (pSummary && typeof pSummary.RecordCount !== 'undefined')
316
+ {
317
+ pSource.RecordCount = pSummary.RecordCount;
318
+ }
319
+ else
320
+ {
321
+ pSource.RecordCount = 0;
322
+ }
323
+ }).catch(
324
+ () =>
325
+ {
326
+ pSource.RecordCount = 0;
327
+ });
328
+ });
329
+ return Promise.all(tmpPromises);
330
+ },
331
+
332
+ loadDatasetCounts: function()
333
+ {
334
+ let tmpDatasets = this.pict.AppData.Facto.Datasets;
335
+ if (!Array.isArray(tmpDatasets) || tmpDatasets.length === 0)
336
+ {
337
+ return Promise.resolve();
338
+ }
339
+ let tmpPromises = tmpDatasets.map(
340
+ (pDataset) =>
341
+ {
342
+ return this.loadDatasetStats(pDataset.IDDataset).then(
343
+ (pStats) =>
344
+ {
345
+ if (pStats && typeof pStats.RecordCount !== 'undefined')
346
+ {
347
+ pDataset.RecordCount = pStats.RecordCount;
348
+ }
349
+ else
350
+ {
351
+ pDataset.RecordCount = 0;
352
+ }
353
+ }).catch(
354
+ () =>
355
+ {
356
+ pDataset.RecordCount = 0;
357
+ });
358
+ });
359
+ return Promise.all(tmpPromises);
360
+ },
361
+
362
+ // ================================================================
363
+ // Ingest Job Operations
364
+ // ================================================================
365
+
366
+ loadIngestJobs: function()
367
+ {
368
+ return this.api('GET', '/facto/ingest/jobs').then(
369
+ (pResponse) =>
370
+ {
371
+ this.pict.AppData.Facto.IngestJobs = (pResponse && pResponse.Jobs) ? pResponse.Jobs : [];
372
+ });
373
+ },
374
+
375
+ createIngestJob: function(pIDSource, pIDDataset, pConfiguration)
376
+ {
377
+ return this.api('POST', '/facto/ingest/job',
378
+ {
379
+ IDSource: pIDSource,
380
+ IDDataset: pIDDataset,
381
+ Configuration: pConfiguration || {}
382
+ }).then(
383
+ (pResponse) =>
384
+ {
385
+ return pResponse;
386
+ });
387
+ },
388
+
389
+ startIngestJob: function(pIDIngestJob)
390
+ {
391
+ return this.api('PUT', `/facto/ingest/job/${pIDIngestJob}/start`).then(
392
+ (pResponse) =>
393
+ {
394
+ return pResponse;
395
+ });
396
+ },
397
+
398
+ completeIngestJob: function(pIDIngestJob, pCounters, pStatus)
399
+ {
400
+ let tmpBody = Object.assign({}, pCounters || {});
401
+ if (pStatus)
402
+ {
403
+ tmpBody.Status = pStatus;
404
+ }
405
+ return this.api('PUT', `/facto/ingest/job/${pIDIngestJob}/complete`, tmpBody).then(
406
+ (pResponse) =>
407
+ {
408
+ return pResponse;
409
+ });
410
+ },
411
+
412
+ loadIngestJobDetails: function(pIDIngestJob)
413
+ {
414
+ return this.api('GET', `/facto/ingest/job/${pIDIngestJob}`).then(
415
+ (pResponse) =>
416
+ {
417
+ return pResponse;
418
+ });
419
+ },
420
+
421
+ // ================================================================
422
+ // FilteredTo URL Parser/Builder
423
+ // ================================================================
424
+
425
+ // Operator mappings between JS operators and URL shortcodes
426
+ _FILTER_OP_TO_URL:
427
+ {
428
+ '=': 'EQ', '!=': 'NE', '>': 'GT', '>=': 'GE', '<': 'LT', '<=': 'LE',
429
+ 'LIKE': 'LK', 'IN': 'INN', 'NOT IN': 'NI', 'IS NULL': 'IN', 'IS NOT NULL': 'NN'
430
+ },
431
+
432
+ _FILTER_URL_TO_OP:
433
+ {
434
+ 'EQ': '=', 'NE': '!=', 'GT': '>', 'GE': '>=', 'LT': '<', 'LE': '<=',
435
+ 'LK': 'LIKE', 'INN': 'IN', 'NI': 'NOT IN', 'IN': 'IS NULL', 'NN': 'IS NOT NULL'
436
+ },
437
+
438
+ // Known FilteredTo instructions (4-part tilde groups)
439
+ _FILTER_INSTRUCTIONS: { 'FBV': 'AND', 'FBVOR': 'OR', 'FBL': 'AND', 'FSF': 'SORT', 'FOP': 'AND', 'FOPOR': 'OR', 'FCP': 'AND', 'FCC': 'AND', 'FCB': 'AND' },
440
+
441
+ /**
442
+ * Build a FilteredTo URL segment from an array of filter objects.
443
+ *
444
+ * @param {Array} pFilters - [{Column, Operator, Value, Connector}]
445
+ * Operator should be a URL shortcode (EQ, GE, INN, etc.)
446
+ * Connector is optional, defaults to FBV (AND)
447
+ * @returns {string} e.g. "FBV~IDSource~INN~1,3~FBV~IngestDate~GE~2025-01-01"
448
+ */
449
+ buildFilteredToString: function(pFilters)
450
+ {
451
+ if (!Array.isArray(pFilters) || pFilters.length === 0) return '';
452
+
453
+ let tmpParts = [];
454
+ for (let i = 0; i < pFilters.length; i++)
455
+ {
456
+ let tmpFilter = pFilters[i];
457
+ let tmpInstruction = tmpFilter.Connector === 'OR' ? 'FBVOR' : 'FBV';
458
+ let tmpOperator = tmpFilter.Operator || 'EQ';
459
+ let tmpValue = (tmpFilter.Value !== undefined && tmpFilter.Value !== null) ? String(tmpFilter.Value) : '';
460
+ tmpParts.push(tmpInstruction + '~' + tmpFilter.Column + '~' + tmpOperator + '~' + tmpValue);
461
+ }
462
+ return tmpParts.join('~');
463
+ },
464
+
465
+ /**
466
+ * Parse a FilteredTo URL segment into an array of filter objects.
467
+ *
468
+ * @param {string} pFilterString - e.g. "FBV~IDSource~INN~1,3~FBV~IngestDate~GE~2025-01-01"
469
+ * @returns {Array} [{Column, Operator, Value, Connector}]
470
+ * Operator is the URL shortcode (EQ, GE, INN, etc.)
471
+ */
472
+ parseFilteredToString: function(pFilterString)
473
+ {
474
+ if (!pFilterString || typeof pFilterString !== 'string') return [];
475
+
476
+ let tmpParts = pFilterString.split('~');
477
+ let tmpFilters = [];
478
+ let tmpIdx = 0;
479
+
480
+ while (tmpIdx < tmpParts.length)
481
+ {
482
+ let tmpInstruction = tmpParts[tmpIdx];
483
+
484
+ // Check if this is a recognized instruction
485
+ if (this._FILTER_INSTRUCTIONS.hasOwnProperty(tmpInstruction))
486
+ {
487
+ // Read the next 3 parts: Column, Operator, Value
488
+ let tmpColumn = (tmpIdx + 1 < tmpParts.length) ? tmpParts[tmpIdx + 1] : '';
489
+ let tmpOperator = (tmpIdx + 2 < tmpParts.length) ? tmpParts[tmpIdx + 2] : 'EQ';
490
+ let tmpValue = (tmpIdx + 3 < tmpParts.length) ? decodeURIComponent(tmpParts[tmpIdx + 3]) : '';
491
+
492
+ tmpFilters.push(
493
+ {
494
+ Column: tmpColumn,
495
+ Operator: tmpOperator,
496
+ Value: tmpValue,
497
+ Connector: this._FILTER_INSTRUCTIONS[tmpInstruction]
498
+ });
499
+
500
+ tmpIdx += 4;
501
+ }
502
+ else
503
+ {
504
+ // Skip unrecognized parts
505
+ tmpIdx++;
506
+ }
507
+ }
508
+
509
+ return tmpFilters;
510
+ },
511
+
512
+ /**
513
+ * Build a full browser route for filtered Records.
514
+ *
515
+ * @param {Array} pFilters - [{Column, Operator, Value}]
516
+ * @param {number} pBegin - Offset (default 0)
517
+ * @param {number} pCap - Page size (default from AppData)
518
+ * @returns {string} e.g. "/Records/FilteredTo/FBV~IDSource~INN~1,3/0/50"
519
+ */
520
+ buildRecordsFilterRoute: function(pFilters, pBegin, pCap)
521
+ {
522
+ let tmpPageSize = pCap || this.pict.AppData.Facto.RecordPageSize || 50;
523
+ let tmpBegin = pBegin || 0;
524
+
525
+ if (!Array.isArray(pFilters) || pFilters.length === 0)
526
+ {
527
+ return '/Records';
528
+ }
529
+
530
+ let tmpFilterStr = this.buildFilteredToString(pFilters);
531
+ return '/Records/FilteredTo/' + tmpFilterStr + '/' + tmpBegin + '/' + tmpPageSize;
532
+ },
533
+
534
+ /**
535
+ * Build filter objects from the current Records page UI state.
536
+ * Reads sources, datasets, and dates and returns a flat filter array.
537
+ *
538
+ * @param {Array} pSourceIDs - Selected source IDs
539
+ * @param {Array} pDatasetIDs - Selected dataset IDs
540
+ * @param {string} pDateFrom - Date from string (YYYY, YYYY-MM, or YYYY-MM-DD)
541
+ * @param {string} pDateTo - Date to string
542
+ * @returns {Array} [{Column, Operator, Value}]
543
+ */
544
+ buildRecordFiltersFromState: function(pSourceIDs, pDatasetIDs, pDateFrom, pDateTo)
545
+ {
546
+ let tmpFilters = [];
547
+
548
+ if (Array.isArray(pSourceIDs) && pSourceIDs.length > 0)
549
+ {
550
+ tmpFilters.push({ Column: 'IDSource', Operator: 'INN', Value: pSourceIDs.join(',') });
551
+ }
552
+
553
+ if (Array.isArray(pDatasetIDs) && pDatasetIDs.length > 0)
554
+ {
555
+ tmpFilters.push({ Column: 'IDDataset', Operator: 'INN', Value: pDatasetIDs.join(',') });
556
+ }
557
+
558
+ let tmpExpandedFrom = this.expandDateFilter(pDateFrom, false);
559
+ let tmpExpandedTo = this.expandDateFilter(pDateTo, true);
560
+ if (tmpExpandedFrom)
561
+ {
562
+ tmpFilters.push({ Column: 'IngestDate', Operator: 'GE', Value: tmpExpandedFrom });
563
+ }
564
+ if (tmpExpandedTo)
565
+ {
566
+ tmpFilters.push({ Column: 'IngestDate', Operator: 'LE', Value: tmpExpandedTo });
567
+ }
568
+
569
+ return tmpFilters;
570
+ },
571
+
572
+ /**
573
+ * Extract Records UI state from parsed filter objects.
574
+ *
575
+ * @param {Array} pFilters - [{Column, Operator, Value}]
576
+ * @returns {object} { SourceIDs, DatasetIDs, DateFrom, DateTo }
577
+ */
578
+ extractRecordStateFromFilters: function(pFilters)
579
+ {
580
+ let tmpState = { SourceIDs: [], DatasetIDs: [], DateFrom: '', DateTo: '' };
581
+
582
+ for (let i = 0; i < pFilters.length; i++)
583
+ {
584
+ let tmpFilter = pFilters[i];
585
+ if (tmpFilter.Column === 'IDSource' && (tmpFilter.Operator === 'INN' || tmpFilter.Operator === 'EQ'))
586
+ {
587
+ tmpState.SourceIDs = tmpFilter.Value.split(',').map(function(v) { return parseInt(v, 10); }).filter(function(v) { return !isNaN(v); });
588
+ }
589
+ else if (tmpFilter.Column === 'IDDataset' && (tmpFilter.Operator === 'INN' || tmpFilter.Operator === 'EQ'))
590
+ {
591
+ tmpState.DatasetIDs = tmpFilter.Value.split(',').map(function(v) { return parseInt(v, 10); }).filter(function(v) { return !isNaN(v); });
592
+ }
593
+ else if (tmpFilter.Column === 'IngestDate' && tmpFilter.Operator === 'GE')
594
+ {
595
+ tmpState.DateFrom = tmpFilter.Value;
596
+ }
597
+ else if (tmpFilter.Column === 'IngestDate' && tmpFilter.Operator === 'LE')
598
+ {
599
+ tmpState.DateTo = tmpFilter.Value;
600
+ }
601
+ }
602
+
603
+ return tmpState;
604
+ }
605
+ };