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,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
|
+
};
|