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,1252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retold Facto - Catalog Manager Service
|
|
3
|
+
*
|
|
4
|
+
* Manages a research catalog of known data sources and their available
|
|
5
|
+
* datasets ("database of databases"). Catalog entries describe agencies,
|
|
6
|
+
* URLs, categories, and per-dataset ingest hints (format, parseOptions,
|
|
7
|
+
* auth requirements). A "provision" action bridges catalog entries into
|
|
8
|
+
* runtime Source + Dataset + DatasetSource records.
|
|
9
|
+
*
|
|
10
|
+
* @author Steven Velozo <steven@velozo.com>
|
|
11
|
+
*/
|
|
12
|
+
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
13
|
+
|
|
14
|
+
const defaultCatalogManagerOptions = (
|
|
15
|
+
{
|
|
16
|
+
RoutePrefix: '/facto'
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
class RetoldFactoCatalogManager extends libFableServiceProviderBase
|
|
20
|
+
{
|
|
21
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
22
|
+
{
|
|
23
|
+
let tmpOptions = Object.assign({}, defaultCatalogManagerOptions, pOptions);
|
|
24
|
+
super(pFable, tmpOptions, pServiceHash);
|
|
25
|
+
|
|
26
|
+
this.serviceType = 'RetoldFactoCatalogManager';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Find a Source by name, or create one if it doesn't exist.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} pName - Source name to find or create
|
|
33
|
+
* @param {object} pDefaults - Default fields for creation (Type, URL, Protocol, Description)
|
|
34
|
+
* @param {function} fCallback - Callback(pError, pSource)
|
|
35
|
+
*/
|
|
36
|
+
findOrCreateSource(pName, pDefaults, fCallback)
|
|
37
|
+
{
|
|
38
|
+
if (!this.fable.DAL || !this.fable.DAL.Source)
|
|
39
|
+
{
|
|
40
|
+
return fCallback(new Error('Source DAL not initialized'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let tmpQuery = this.fable.DAL.Source.query.clone()
|
|
44
|
+
.addFilter('Name', pName)
|
|
45
|
+
.addFilter('Deleted', 0);
|
|
46
|
+
|
|
47
|
+
this.fable.DAL.Source.doReads(tmpQuery,
|
|
48
|
+
(pError, pQuery, pRecords) =>
|
|
49
|
+
{
|
|
50
|
+
if (!pError && pRecords && pRecords.length > 0)
|
|
51
|
+
{
|
|
52
|
+
return fCallback(null, pRecords[0]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Create new source
|
|
56
|
+
let tmpSourceData = Object.assign(
|
|
57
|
+
{
|
|
58
|
+
Name: pName,
|
|
59
|
+
Hash: this.fable.RetoldFacto.generateHash(pName),
|
|
60
|
+
Active: 1
|
|
61
|
+
}, pDefaults || {});
|
|
62
|
+
|
|
63
|
+
let tmpCreateQuery = this.fable.DAL.Source.query.clone()
|
|
64
|
+
.addRecord(tmpSourceData);
|
|
65
|
+
|
|
66
|
+
this.fable.DAL.Source.doCreate(tmpCreateQuery,
|
|
67
|
+
(pCreateError, pCreateQuery, pCreateQueryRead, pRecord) =>
|
|
68
|
+
{
|
|
69
|
+
if (pCreateError)
|
|
70
|
+
{
|
|
71
|
+
return fCallback(pCreateError);
|
|
72
|
+
}
|
|
73
|
+
return fCallback(null, pRecord);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Find a Dataset by name, or create one if it doesn't exist.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} pName - Dataset name to find or create
|
|
82
|
+
* @param {object} pDefaults - Default fields for creation (Type, Description, VersionPolicy)
|
|
83
|
+
* @param {function} fCallback - Callback(pError, pDataset)
|
|
84
|
+
*/
|
|
85
|
+
findOrCreateDataset(pName, pDefaults, fCallback)
|
|
86
|
+
{
|
|
87
|
+
if (!this.fable.DAL || !this.fable.DAL.Dataset)
|
|
88
|
+
{
|
|
89
|
+
return fCallback(new Error('Dataset DAL not initialized'));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let tmpQuery = this.fable.DAL.Dataset.query.clone()
|
|
93
|
+
.addFilter('Name', pName)
|
|
94
|
+
.addFilter('Deleted', 0);
|
|
95
|
+
|
|
96
|
+
this.fable.DAL.Dataset.doReads(tmpQuery,
|
|
97
|
+
(pError, pQuery, pRecords) =>
|
|
98
|
+
{
|
|
99
|
+
if (!pError && pRecords && pRecords.length > 0)
|
|
100
|
+
{
|
|
101
|
+
return fCallback(null, pRecords[0]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Create new dataset
|
|
105
|
+
let tmpDatasetData = Object.assign(
|
|
106
|
+
{
|
|
107
|
+
Name: pName,
|
|
108
|
+
Hash: this.fable.RetoldFacto.generateHash(pName),
|
|
109
|
+
Type: 'Raw'
|
|
110
|
+
}, pDefaults || {});
|
|
111
|
+
|
|
112
|
+
let tmpCreateQuery = this.fable.DAL.Dataset.query.clone()
|
|
113
|
+
.addRecord(tmpDatasetData);
|
|
114
|
+
|
|
115
|
+
this.fable.DAL.Dataset.doCreate(tmpCreateQuery,
|
|
116
|
+
(pCreateError, pCreateQuery, pCreateQueryRead, pRecord) =>
|
|
117
|
+
{
|
|
118
|
+
if (pCreateError)
|
|
119
|
+
{
|
|
120
|
+
return fCallback(pCreateError);
|
|
121
|
+
}
|
|
122
|
+
return fCallback(null, pRecord);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Ensure a DatasetSource link exists between a dataset and a source.
|
|
129
|
+
*
|
|
130
|
+
* @param {number} pIDDataset - Dataset ID
|
|
131
|
+
* @param {number} pIDSource - Source ID
|
|
132
|
+
* @param {function} fCallback - Callback(pError, pDatasetSource)
|
|
133
|
+
*/
|
|
134
|
+
ensureDatasetSourceLink(pIDDataset, pIDSource, fCallback)
|
|
135
|
+
{
|
|
136
|
+
if (!this.fable.DAL || !this.fable.DAL.DatasetSource)
|
|
137
|
+
{
|
|
138
|
+
return fCallback(new Error('DatasetSource DAL not initialized'));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let tmpQuery = this.fable.DAL.DatasetSource.query.clone()
|
|
142
|
+
.addFilter('IDDataset', pIDDataset)
|
|
143
|
+
.addFilter('IDSource', pIDSource)
|
|
144
|
+
.addFilter('Deleted', 0);
|
|
145
|
+
|
|
146
|
+
this.fable.DAL.DatasetSource.doReads(tmpQuery,
|
|
147
|
+
(pError, pQuery, pRecords) =>
|
|
148
|
+
{
|
|
149
|
+
if (!pError && pRecords && pRecords.length > 0)
|
|
150
|
+
{
|
|
151
|
+
return fCallback(null, pRecords[0]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Create link
|
|
155
|
+
let tmpCreateQuery = this.fable.DAL.DatasetSource.query.clone()
|
|
156
|
+
.addRecord(
|
|
157
|
+
{
|
|
158
|
+
IDDataset: pIDDataset,
|
|
159
|
+
IDSource: pIDSource,
|
|
160
|
+
ReliabilityWeight: 1.0
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
this.fable.DAL.DatasetSource.doCreate(tmpCreateQuery,
|
|
164
|
+
(pCreateError, pCreateQuery, pCreateQueryRead, pRecord) =>
|
|
165
|
+
{
|
|
166
|
+
if (pCreateError)
|
|
167
|
+
{
|
|
168
|
+
return fCallback(pCreateError);
|
|
169
|
+
}
|
|
170
|
+
return fCallback(null, pRecord);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Connect REST API routes for catalog management.
|
|
177
|
+
*
|
|
178
|
+
* @param {object} pOratorServiceServer - The Orator service server instance
|
|
179
|
+
*/
|
|
180
|
+
connectRoutes(pOratorServiceServer)
|
|
181
|
+
{
|
|
182
|
+
let tmpRoutePrefix = this.options.RoutePrefix;
|
|
183
|
+
|
|
184
|
+
// ================================================================
|
|
185
|
+
// Catalog Entry CRUD
|
|
186
|
+
// ================================================================
|
|
187
|
+
|
|
188
|
+
// GET /facto/catalog/entries -- list all catalog entries (non-deleted)
|
|
189
|
+
pOratorServiceServer.doGet(`${tmpRoutePrefix}/catalog/entries`,
|
|
190
|
+
(pRequest, pResponse, fNext) =>
|
|
191
|
+
{
|
|
192
|
+
if (!this.fable.DAL || !this.fable.DAL.SourceCatalogEntry)
|
|
193
|
+
{
|
|
194
|
+
pResponse.send({ Error: 'SourceCatalogEntry DAL not initialized', Entries: [] });
|
|
195
|
+
return fNext();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let tmpQuery = this.fable.DAL.SourceCatalogEntry.query.clone()
|
|
199
|
+
.addFilter('Deleted', 0)
|
|
200
|
+
.setCap(500);
|
|
201
|
+
|
|
202
|
+
this.fable.DAL.SourceCatalogEntry.doReads(tmpQuery,
|
|
203
|
+
(pError, pQuery, pRecords) =>
|
|
204
|
+
{
|
|
205
|
+
if (pError)
|
|
206
|
+
{
|
|
207
|
+
this.fable.log.error(`CatalogManager error listing entries: ${pError}`);
|
|
208
|
+
pResponse.send({ Error: pError.message || pError, Entries: [] });
|
|
209
|
+
return fNext();
|
|
210
|
+
}
|
|
211
|
+
pResponse.send({ Count: pRecords.length, Entries: pRecords });
|
|
212
|
+
return fNext();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// GET /facto/catalog/entry/:IDSourceCatalogEntry -- get single entry with dataset definitions
|
|
217
|
+
pOratorServiceServer.doGet(`${tmpRoutePrefix}/catalog/entry/:IDSourceCatalogEntry`,
|
|
218
|
+
(pRequest, pResponse, fNext) =>
|
|
219
|
+
{
|
|
220
|
+
let tmpID = parseInt(pRequest.params.IDSourceCatalogEntry, 10);
|
|
221
|
+
if (isNaN(tmpID) || tmpID < 1)
|
|
222
|
+
{
|
|
223
|
+
pResponse.send({ Error: 'Invalid IDSourceCatalogEntry parameter' });
|
|
224
|
+
return fNext();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!this.fable.DAL || !this.fable.DAL.SourceCatalogEntry)
|
|
228
|
+
{
|
|
229
|
+
pResponse.send({ Error: 'SourceCatalogEntry DAL not initialized' });
|
|
230
|
+
return fNext();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let tmpAnticipate = this.fable.newAnticipate();
|
|
234
|
+
let tmpResult = { Entry: null, Datasets: [] };
|
|
235
|
+
|
|
236
|
+
// Load the entry
|
|
237
|
+
tmpAnticipate.anticipate(
|
|
238
|
+
(fStep) =>
|
|
239
|
+
{
|
|
240
|
+
let tmpQuery = this.fable.DAL.SourceCatalogEntry.query.clone()
|
|
241
|
+
.addFilter('IDSourceCatalogEntry', tmpID);
|
|
242
|
+
|
|
243
|
+
this.fable.DAL.SourceCatalogEntry.doRead(tmpQuery,
|
|
244
|
+
(pError, pQuery, pRecord) =>
|
|
245
|
+
{
|
|
246
|
+
if (!pError && pRecord)
|
|
247
|
+
{
|
|
248
|
+
tmpResult.Entry = pRecord;
|
|
249
|
+
}
|
|
250
|
+
return fStep();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Load dataset definitions
|
|
255
|
+
tmpAnticipate.anticipate(
|
|
256
|
+
(fStep) =>
|
|
257
|
+
{
|
|
258
|
+
if (!this.fable.DAL.CatalogDatasetDefinition)
|
|
259
|
+
{
|
|
260
|
+
return fStep();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let tmpQuery = this.fable.DAL.CatalogDatasetDefinition.query.clone()
|
|
264
|
+
.addFilter('IDSourceCatalogEntry', tmpID)
|
|
265
|
+
.addFilter('Deleted', 0);
|
|
266
|
+
|
|
267
|
+
this.fable.DAL.CatalogDatasetDefinition.doReads(tmpQuery,
|
|
268
|
+
(pError, pQuery, pRecords) =>
|
|
269
|
+
{
|
|
270
|
+
if (!pError && pRecords)
|
|
271
|
+
{
|
|
272
|
+
tmpResult.Datasets = pRecords;
|
|
273
|
+
}
|
|
274
|
+
return fStep();
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
tmpAnticipate.wait(
|
|
279
|
+
(pError) =>
|
|
280
|
+
{
|
|
281
|
+
if (pError)
|
|
282
|
+
{
|
|
283
|
+
pResponse.send({ Error: pError.message || pError });
|
|
284
|
+
return fNext();
|
|
285
|
+
}
|
|
286
|
+
if (!tmpResult.Entry)
|
|
287
|
+
{
|
|
288
|
+
pResponse.send({ Error: 'Catalog entry not found' });
|
|
289
|
+
return fNext();
|
|
290
|
+
}
|
|
291
|
+
pResponse.send(tmpResult);
|
|
292
|
+
return fNext();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// POST /facto/catalog/entry -- create a catalog entry
|
|
297
|
+
pOratorServiceServer.doPost(`${tmpRoutePrefix}/catalog/entry`,
|
|
298
|
+
(pRequest, pResponse, fNext) =>
|
|
299
|
+
{
|
|
300
|
+
if (!this.fable.DAL || !this.fable.DAL.SourceCatalogEntry)
|
|
301
|
+
{
|
|
302
|
+
pResponse.send({ Error: 'SourceCatalogEntry DAL not initialized' });
|
|
303
|
+
return fNext();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let tmpBody = pRequest.body || {};
|
|
307
|
+
|
|
308
|
+
let tmpEntryData = {
|
|
309
|
+
Agency: tmpBody.Agency || '',
|
|
310
|
+
Name: tmpBody.Name || '',
|
|
311
|
+
Type: tmpBody.Type || '',
|
|
312
|
+
URL: tmpBody.URL || '',
|
|
313
|
+
Protocol: tmpBody.Protocol || '',
|
|
314
|
+
Category: tmpBody.Category || '',
|
|
315
|
+
Region: tmpBody.Region || '',
|
|
316
|
+
UpdateFrequency: tmpBody.UpdateFrequency || '',
|
|
317
|
+
Description: tmpBody.Description || '',
|
|
318
|
+
Notes: tmpBody.Notes || '',
|
|
319
|
+
Verified: tmpBody.Verified ? 1 : 0
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
let tmpQuery = this.fable.DAL.SourceCatalogEntry.query.clone()
|
|
323
|
+
.addRecord(tmpEntryData);
|
|
324
|
+
|
|
325
|
+
this.fable.DAL.SourceCatalogEntry.doCreate(tmpQuery,
|
|
326
|
+
(pError, pQuery, pQueryRead, pRecord) =>
|
|
327
|
+
{
|
|
328
|
+
if (pError)
|
|
329
|
+
{
|
|
330
|
+
this.fable.log.error(`CatalogManager error creating entry: ${pError}`);
|
|
331
|
+
pResponse.send({ Error: pError.message || pError });
|
|
332
|
+
return fNext();
|
|
333
|
+
}
|
|
334
|
+
pResponse.send({ Success: true, Entry: pRecord });
|
|
335
|
+
return fNext();
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// PUT /facto/catalog/entry/:IDSourceCatalogEntry -- update a catalog entry
|
|
340
|
+
pOratorServiceServer.doPut(`${tmpRoutePrefix}/catalog/entry/:IDSourceCatalogEntry`,
|
|
341
|
+
(pRequest, pResponse, fNext) =>
|
|
342
|
+
{
|
|
343
|
+
let tmpID = parseInt(pRequest.params.IDSourceCatalogEntry, 10);
|
|
344
|
+
if (isNaN(tmpID) || tmpID < 1)
|
|
345
|
+
{
|
|
346
|
+
pResponse.send({ Error: 'Invalid IDSourceCatalogEntry parameter' });
|
|
347
|
+
return fNext();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!this.fable.DAL || !this.fable.DAL.SourceCatalogEntry)
|
|
351
|
+
{
|
|
352
|
+
pResponse.send({ Error: 'SourceCatalogEntry DAL not initialized' });
|
|
353
|
+
return fNext();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let tmpBody = pRequest.body || {};
|
|
357
|
+
let tmpUpdateData = { IDSourceCatalogEntry: tmpID };
|
|
358
|
+
|
|
359
|
+
// Only update fields that are present in the body
|
|
360
|
+
let tmpFields = ['Agency', 'Name', 'Type', 'URL', 'Protocol', 'Category', 'Region', 'UpdateFrequency', 'Description', 'Notes'];
|
|
361
|
+
for (let i = 0; i < tmpFields.length; i++)
|
|
362
|
+
{
|
|
363
|
+
if (tmpBody.hasOwnProperty(tmpFields[i]))
|
|
364
|
+
{
|
|
365
|
+
tmpUpdateData[tmpFields[i]] = tmpBody[tmpFields[i]];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (tmpBody.hasOwnProperty('Verified'))
|
|
369
|
+
{
|
|
370
|
+
tmpUpdateData.Verified = tmpBody.Verified ? 1 : 0;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
let tmpQuery = this.fable.DAL.SourceCatalogEntry.query.clone()
|
|
374
|
+
.addRecord(tmpUpdateData);
|
|
375
|
+
|
|
376
|
+
this.fable.DAL.SourceCatalogEntry.doUpdate(tmpQuery,
|
|
377
|
+
(pError, pQuery, pQueryRead, pRecord) =>
|
|
378
|
+
{
|
|
379
|
+
if (pError)
|
|
380
|
+
{
|
|
381
|
+
this.fable.log.error(`CatalogManager error updating entry ${tmpID}: ${pError}`);
|
|
382
|
+
pResponse.send({ Error: pError.message || pError });
|
|
383
|
+
return fNext();
|
|
384
|
+
}
|
|
385
|
+
pResponse.send({ Success: true, Entry: pRecord });
|
|
386
|
+
return fNext();
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// DELETE /facto/catalog/entry/:IDSourceCatalogEntry -- soft-delete a catalog entry
|
|
391
|
+
pOratorServiceServer.doDel(`${tmpRoutePrefix}/catalog/entry/:IDSourceCatalogEntry`,
|
|
392
|
+
(pRequest, pResponse, fNext) =>
|
|
393
|
+
{
|
|
394
|
+
let tmpID = parseInt(pRequest.params.IDSourceCatalogEntry, 10);
|
|
395
|
+
if (isNaN(tmpID) || tmpID < 1)
|
|
396
|
+
{
|
|
397
|
+
pResponse.send({ Error: 'Invalid IDSourceCatalogEntry parameter' });
|
|
398
|
+
return fNext();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!this.fable.DAL || !this.fable.DAL.SourceCatalogEntry)
|
|
402
|
+
{
|
|
403
|
+
pResponse.send({ Error: 'SourceCatalogEntry DAL not initialized' });
|
|
404
|
+
return fNext();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let tmpQuery = this.fable.DAL.SourceCatalogEntry.query.clone()
|
|
408
|
+
.addFilter('IDSourceCatalogEntry', tmpID);
|
|
409
|
+
|
|
410
|
+
this.fable.DAL.SourceCatalogEntry.doDelete(tmpQuery,
|
|
411
|
+
(pError) =>
|
|
412
|
+
{
|
|
413
|
+
if (pError)
|
|
414
|
+
{
|
|
415
|
+
pResponse.send({ Error: pError.message || pError });
|
|
416
|
+
return fNext();
|
|
417
|
+
}
|
|
418
|
+
pResponse.send({ Success: true, Deleted: tmpID });
|
|
419
|
+
return fNext();
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// ================================================================
|
|
424
|
+
// Catalog Dataset Definition CRUD
|
|
425
|
+
// ================================================================
|
|
426
|
+
|
|
427
|
+
// GET /facto/catalog/entry/:IDSourceCatalogEntry/datasets -- list dataset definitions
|
|
428
|
+
pOratorServiceServer.doGet(`${tmpRoutePrefix}/catalog/entry/:IDSourceCatalogEntry/datasets`,
|
|
429
|
+
(pRequest, pResponse, fNext) =>
|
|
430
|
+
{
|
|
431
|
+
let tmpID = parseInt(pRequest.params.IDSourceCatalogEntry, 10);
|
|
432
|
+
if (isNaN(tmpID) || tmpID < 1)
|
|
433
|
+
{
|
|
434
|
+
pResponse.send({ Error: 'Invalid IDSourceCatalogEntry parameter', Datasets: [] });
|
|
435
|
+
return fNext();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (!this.fable.DAL || !this.fable.DAL.CatalogDatasetDefinition)
|
|
439
|
+
{
|
|
440
|
+
pResponse.send({ Error: 'CatalogDatasetDefinition DAL not initialized', Datasets: [] });
|
|
441
|
+
return fNext();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let tmpQuery = this.fable.DAL.CatalogDatasetDefinition.query.clone()
|
|
445
|
+
.addFilter('IDSourceCatalogEntry', tmpID)
|
|
446
|
+
.addFilter('Deleted', 0);
|
|
447
|
+
|
|
448
|
+
this.fable.DAL.CatalogDatasetDefinition.doReads(tmpQuery,
|
|
449
|
+
(pError, pQuery, pRecords) =>
|
|
450
|
+
{
|
|
451
|
+
if (pError)
|
|
452
|
+
{
|
|
453
|
+
pResponse.send({ Error: pError.message || pError, Datasets: [] });
|
|
454
|
+
return fNext();
|
|
455
|
+
}
|
|
456
|
+
pResponse.send({ IDSourceCatalogEntry: tmpID, Count: pRecords.length, Datasets: pRecords });
|
|
457
|
+
return fNext();
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// POST /facto/catalog/entry/:IDSourceCatalogEntry/dataset -- add dataset definition
|
|
462
|
+
pOratorServiceServer.doPost(`${tmpRoutePrefix}/catalog/entry/:IDSourceCatalogEntry/dataset`,
|
|
463
|
+
(pRequest, pResponse, fNext) =>
|
|
464
|
+
{
|
|
465
|
+
let tmpID = parseInt(pRequest.params.IDSourceCatalogEntry, 10);
|
|
466
|
+
if (isNaN(tmpID) || tmpID < 1)
|
|
467
|
+
{
|
|
468
|
+
pResponse.send({ Error: 'Invalid IDSourceCatalogEntry parameter' });
|
|
469
|
+
return fNext();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!this.fable.DAL || !this.fable.DAL.CatalogDatasetDefinition)
|
|
473
|
+
{
|
|
474
|
+
pResponse.send({ Error: 'CatalogDatasetDefinition DAL not initialized' });
|
|
475
|
+
return fNext();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
let tmpBody = pRequest.body || {};
|
|
479
|
+
|
|
480
|
+
let tmpDefData = {
|
|
481
|
+
IDSourceCatalogEntry: tmpID,
|
|
482
|
+
Name: tmpBody.Name || '',
|
|
483
|
+
Format: tmpBody.Format || '',
|
|
484
|
+
MimeType: tmpBody.MimeType || '',
|
|
485
|
+
EndpointURL: tmpBody.EndpointURL || '',
|
|
486
|
+
Description: tmpBody.Description || '',
|
|
487
|
+
ParseOptions: (typeof tmpBody.ParseOptions === 'object') ? JSON.stringify(tmpBody.ParseOptions) : (tmpBody.ParseOptions || ''),
|
|
488
|
+
AuthRequirements: (typeof tmpBody.AuthRequirements === 'object') ? JSON.stringify(tmpBody.AuthRequirements) : (tmpBody.AuthRequirements || ''),
|
|
489
|
+
VersionPolicy: tmpBody.VersionPolicy || 'Append',
|
|
490
|
+
Provisioned: 0,
|
|
491
|
+
IDSource: 0,
|
|
492
|
+
IDDataset: 0
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
let tmpQuery = this.fable.DAL.CatalogDatasetDefinition.query.clone()
|
|
496
|
+
.addRecord(tmpDefData);
|
|
497
|
+
|
|
498
|
+
this.fable.DAL.CatalogDatasetDefinition.doCreate(tmpQuery,
|
|
499
|
+
(pError, pQuery, pQueryRead, pRecord) =>
|
|
500
|
+
{
|
|
501
|
+
if (pError)
|
|
502
|
+
{
|
|
503
|
+
this.fable.log.error(`CatalogManager error creating dataset definition: ${pError}`);
|
|
504
|
+
pResponse.send({ Error: pError.message || pError });
|
|
505
|
+
return fNext();
|
|
506
|
+
}
|
|
507
|
+
pResponse.send({ Success: true, DatasetDefinition: pRecord });
|
|
508
|
+
return fNext();
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// PUT /facto/catalog/dataset/:IDCatalogDatasetDefinition -- update a dataset definition
|
|
513
|
+
pOratorServiceServer.doPut(`${tmpRoutePrefix}/catalog/dataset/:IDCatalogDatasetDefinition`,
|
|
514
|
+
(pRequest, pResponse, fNext) =>
|
|
515
|
+
{
|
|
516
|
+
let tmpID = parseInt(pRequest.params.IDCatalogDatasetDefinition, 10);
|
|
517
|
+
if (isNaN(tmpID) || tmpID < 1)
|
|
518
|
+
{
|
|
519
|
+
pResponse.send({ Error: 'Invalid IDCatalogDatasetDefinition parameter' });
|
|
520
|
+
return fNext();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (!this.fable.DAL || !this.fable.DAL.CatalogDatasetDefinition)
|
|
524
|
+
{
|
|
525
|
+
pResponse.send({ Error: 'CatalogDatasetDefinition DAL not initialized' });
|
|
526
|
+
return fNext();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
let tmpBody = pRequest.body || {};
|
|
530
|
+
let tmpUpdateData = { IDCatalogDatasetDefinition: tmpID };
|
|
531
|
+
|
|
532
|
+
let tmpFields = ['Name', 'Format', 'MimeType', 'EndpointURL', 'Description', 'VersionPolicy'];
|
|
533
|
+
for (let i = 0; i < tmpFields.length; i++)
|
|
534
|
+
{
|
|
535
|
+
if (tmpBody.hasOwnProperty(tmpFields[i]))
|
|
536
|
+
{
|
|
537
|
+
tmpUpdateData[tmpFields[i]] = tmpBody[tmpFields[i]];
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (tmpBody.hasOwnProperty('ParseOptions'))
|
|
542
|
+
{
|
|
543
|
+
tmpUpdateData.ParseOptions = (typeof tmpBody.ParseOptions === 'object') ? JSON.stringify(tmpBody.ParseOptions) : tmpBody.ParseOptions;
|
|
544
|
+
}
|
|
545
|
+
if (tmpBody.hasOwnProperty('AuthRequirements'))
|
|
546
|
+
{
|
|
547
|
+
tmpUpdateData.AuthRequirements = (typeof tmpBody.AuthRequirements === 'object') ? JSON.stringify(tmpBody.AuthRequirements) : tmpBody.AuthRequirements;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
let tmpQuery = this.fable.DAL.CatalogDatasetDefinition.query.clone()
|
|
551
|
+
.addRecord(tmpUpdateData);
|
|
552
|
+
|
|
553
|
+
this.fable.DAL.CatalogDatasetDefinition.doUpdate(tmpQuery,
|
|
554
|
+
(pError, pQuery, pQueryRead, pRecord) =>
|
|
555
|
+
{
|
|
556
|
+
if (pError)
|
|
557
|
+
{
|
|
558
|
+
pResponse.send({ Error: pError.message || pError });
|
|
559
|
+
return fNext();
|
|
560
|
+
}
|
|
561
|
+
pResponse.send({ Success: true, DatasetDefinition: pRecord });
|
|
562
|
+
return fNext();
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// DELETE /facto/catalog/dataset/:IDCatalogDatasetDefinition -- soft-delete dataset definition
|
|
567
|
+
pOratorServiceServer.doDel(`${tmpRoutePrefix}/catalog/dataset/:IDCatalogDatasetDefinition`,
|
|
568
|
+
(pRequest, pResponse, fNext) =>
|
|
569
|
+
{
|
|
570
|
+
let tmpID = parseInt(pRequest.params.IDCatalogDatasetDefinition, 10);
|
|
571
|
+
if (isNaN(tmpID) || tmpID < 1)
|
|
572
|
+
{
|
|
573
|
+
pResponse.send({ Error: 'Invalid IDCatalogDatasetDefinition parameter' });
|
|
574
|
+
return fNext();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!this.fable.DAL || !this.fable.DAL.CatalogDatasetDefinition)
|
|
578
|
+
{
|
|
579
|
+
pResponse.send({ Error: 'CatalogDatasetDefinition DAL not initialized' });
|
|
580
|
+
return fNext();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
let tmpQuery = this.fable.DAL.CatalogDatasetDefinition.query.clone()
|
|
584
|
+
.addFilter('IDCatalogDatasetDefinition', tmpID);
|
|
585
|
+
|
|
586
|
+
this.fable.DAL.CatalogDatasetDefinition.doDelete(tmpQuery,
|
|
587
|
+
(pError) =>
|
|
588
|
+
{
|
|
589
|
+
if (pError)
|
|
590
|
+
{
|
|
591
|
+
pResponse.send({ Error: pError.message || pError });
|
|
592
|
+
return fNext();
|
|
593
|
+
}
|
|
594
|
+
pResponse.send({ Success: true, Deleted: tmpID });
|
|
595
|
+
return fNext();
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// ================================================================
|
|
600
|
+
// Search
|
|
601
|
+
// ================================================================
|
|
602
|
+
|
|
603
|
+
// GET /facto/catalog/search?q=term -- search catalog entries
|
|
604
|
+
pOratorServiceServer.doGet(`${tmpRoutePrefix}/catalog/search`,
|
|
605
|
+
(pRequest, pResponse, fNext) =>
|
|
606
|
+
{
|
|
607
|
+
if (!this.fable.DAL || !this.fable.DAL.SourceCatalogEntry)
|
|
608
|
+
{
|
|
609
|
+
pResponse.send({ Error: 'SourceCatalogEntry DAL not initialized', Entries: [] });
|
|
610
|
+
return fNext();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
let tmpSearchTerm = '';
|
|
614
|
+
if (pRequest.query && pRequest.query.q)
|
|
615
|
+
{
|
|
616
|
+
tmpSearchTerm = pRequest.query.q.toLowerCase();
|
|
617
|
+
}
|
|
618
|
+
else if (pRequest.params && pRequest.params.q)
|
|
619
|
+
{
|
|
620
|
+
tmpSearchTerm = pRequest.params.q.toLowerCase();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
let tmpQuery = this.fable.DAL.SourceCatalogEntry.query.clone()
|
|
624
|
+
.addFilter('Deleted', 0)
|
|
625
|
+
.setCap(500);
|
|
626
|
+
|
|
627
|
+
this.fable.DAL.SourceCatalogEntry.doReads(tmpQuery,
|
|
628
|
+
(pError, pQuery, pRecords) =>
|
|
629
|
+
{
|
|
630
|
+
if (pError)
|
|
631
|
+
{
|
|
632
|
+
pResponse.send({ Error: pError.message || pError, Entries: [] });
|
|
633
|
+
return fNext();
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (!tmpSearchTerm)
|
|
637
|
+
{
|
|
638
|
+
pResponse.send({ Query: '', Count: pRecords.length, Entries: pRecords });
|
|
639
|
+
return fNext();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Client-side filter across Agency, Name, Category, Description
|
|
643
|
+
let tmpFiltered = pRecords.filter(
|
|
644
|
+
(pEntry) =>
|
|
645
|
+
{
|
|
646
|
+
let tmpSearchable = [
|
|
647
|
+
pEntry.Agency || '',
|
|
648
|
+
pEntry.Name || '',
|
|
649
|
+
pEntry.Category || '',
|
|
650
|
+
pEntry.Description || ''
|
|
651
|
+
].join(' ').toLowerCase();
|
|
652
|
+
return tmpSearchable.indexOf(tmpSearchTerm) >= 0;
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
pResponse.send({ Query: tmpSearchTerm, Count: tmpFiltered.length, Entries: tmpFiltered });
|
|
656
|
+
return fNext();
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// ================================================================
|
|
661
|
+
// Provision
|
|
662
|
+
// ================================================================
|
|
663
|
+
|
|
664
|
+
// POST /facto/catalog/dataset/:IDCatalogDatasetDefinition/provision
|
|
665
|
+
pOratorServiceServer.doPost(`${tmpRoutePrefix}/catalog/dataset/:IDCatalogDatasetDefinition/provision`,
|
|
666
|
+
(pRequest, pResponse, fNext) =>
|
|
667
|
+
{
|
|
668
|
+
let tmpID = parseInt(pRequest.params.IDCatalogDatasetDefinition, 10);
|
|
669
|
+
if (isNaN(tmpID) || tmpID < 1)
|
|
670
|
+
{
|
|
671
|
+
pResponse.send({ Error: 'Invalid IDCatalogDatasetDefinition parameter' });
|
|
672
|
+
return fNext();
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (!this.fable.DAL || !this.fable.DAL.CatalogDatasetDefinition || !this.fable.DAL.SourceCatalogEntry)
|
|
676
|
+
{
|
|
677
|
+
pResponse.send({ Error: 'Catalog DAL not initialized' });
|
|
678
|
+
return fNext();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
let tmpAnticipate = this.fable.newAnticipate();
|
|
682
|
+
let tmpCatalogDef = null;
|
|
683
|
+
let tmpCatalogEntry = null;
|
|
684
|
+
let tmpSource = null;
|
|
685
|
+
let tmpDataset = null;
|
|
686
|
+
let tmpDatasetSource = null;
|
|
687
|
+
|
|
688
|
+
// Step 1: Load the catalog dataset definition
|
|
689
|
+
tmpAnticipate.anticipate(
|
|
690
|
+
(fStep) =>
|
|
691
|
+
{
|
|
692
|
+
let tmpQuery = this.fable.DAL.CatalogDatasetDefinition.query.clone()
|
|
693
|
+
.addFilter('IDCatalogDatasetDefinition', tmpID);
|
|
694
|
+
|
|
695
|
+
this.fable.DAL.CatalogDatasetDefinition.doRead(tmpQuery,
|
|
696
|
+
(pError, pQuery, pRecord) =>
|
|
697
|
+
{
|
|
698
|
+
if (pError || !pRecord)
|
|
699
|
+
{
|
|
700
|
+
return fStep(pError || new Error('CatalogDatasetDefinition not found'));
|
|
701
|
+
}
|
|
702
|
+
tmpCatalogDef = pRecord;
|
|
703
|
+
return fStep();
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// Step 2: Load the parent catalog entry
|
|
708
|
+
tmpAnticipate.anticipate(
|
|
709
|
+
(fStep) =>
|
|
710
|
+
{
|
|
711
|
+
if (!tmpCatalogDef)
|
|
712
|
+
{
|
|
713
|
+
return fStep();
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
let tmpQuery = this.fable.DAL.SourceCatalogEntry.query.clone()
|
|
717
|
+
.addFilter('IDSourceCatalogEntry', tmpCatalogDef.IDSourceCatalogEntry);
|
|
718
|
+
|
|
719
|
+
this.fable.DAL.SourceCatalogEntry.doRead(tmpQuery,
|
|
720
|
+
(pError, pQuery, pRecord) =>
|
|
721
|
+
{
|
|
722
|
+
if (pError || !pRecord)
|
|
723
|
+
{
|
|
724
|
+
return fStep(pError || new Error('Parent SourceCatalogEntry not found'));
|
|
725
|
+
}
|
|
726
|
+
tmpCatalogEntry = pRecord;
|
|
727
|
+
return fStep();
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// Step 3: Find-or-create runtime Source (by Agency name)
|
|
732
|
+
tmpAnticipate.anticipate(
|
|
733
|
+
(fStep) =>
|
|
734
|
+
{
|
|
735
|
+
if (!tmpCatalogEntry)
|
|
736
|
+
{
|
|
737
|
+
return fStep();
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
let tmpSourceName = tmpCatalogEntry.Agency || tmpCatalogEntry.Name;
|
|
741
|
+
this.findOrCreateSource(tmpSourceName,
|
|
742
|
+
{
|
|
743
|
+
Type: tmpCatalogEntry.Type || 'API',
|
|
744
|
+
URL: tmpCatalogEntry.URL || '',
|
|
745
|
+
Protocol: tmpCatalogEntry.Protocol || 'HTTPS',
|
|
746
|
+
Description: tmpCatalogEntry.Description || ''
|
|
747
|
+
},
|
|
748
|
+
(pError, pSource) =>
|
|
749
|
+
{
|
|
750
|
+
if (pError)
|
|
751
|
+
{
|
|
752
|
+
return fStep(pError);
|
|
753
|
+
}
|
|
754
|
+
tmpSource = pSource;
|
|
755
|
+
return fStep();
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// Step 4: Find-or-create runtime Dataset
|
|
760
|
+
tmpAnticipate.anticipate(
|
|
761
|
+
(fStep) =>
|
|
762
|
+
{
|
|
763
|
+
if (!tmpCatalogDef)
|
|
764
|
+
{
|
|
765
|
+
return fStep();
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
this.findOrCreateDataset(tmpCatalogDef.Name,
|
|
769
|
+
{
|
|
770
|
+
Type: 'Raw',
|
|
771
|
+
Description: tmpCatalogDef.Description || '',
|
|
772
|
+
VersionPolicy: tmpCatalogDef.VersionPolicy || 'Append'
|
|
773
|
+
},
|
|
774
|
+
(pError, pDataset) =>
|
|
775
|
+
{
|
|
776
|
+
if (pError)
|
|
777
|
+
{
|
|
778
|
+
return fStep(pError);
|
|
779
|
+
}
|
|
780
|
+
tmpDataset = pDataset;
|
|
781
|
+
return fStep();
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
// Step 5: Ensure DatasetSource link + set VersionPolicy
|
|
786
|
+
tmpAnticipate.anticipate(
|
|
787
|
+
(fStep) =>
|
|
788
|
+
{
|
|
789
|
+
if (!tmpSource || !tmpDataset)
|
|
790
|
+
{
|
|
791
|
+
return fStep();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
this.ensureDatasetSourceLink(tmpDataset.IDDataset, tmpSource.IDSource,
|
|
795
|
+
(pError, pLink) =>
|
|
796
|
+
{
|
|
797
|
+
if (pError)
|
|
798
|
+
{
|
|
799
|
+
return fStep(pError);
|
|
800
|
+
}
|
|
801
|
+
tmpDatasetSource = pLink;
|
|
802
|
+
|
|
803
|
+
// Update VersionPolicy on the dataset
|
|
804
|
+
let tmpPolicy = tmpCatalogDef.VersionPolicy || 'Append';
|
|
805
|
+
if (tmpPolicy === 'Append' || tmpPolicy === 'Replace')
|
|
806
|
+
{
|
|
807
|
+
let tmpUpdateQuery = this.fable.DAL.Dataset.query.clone()
|
|
808
|
+
.addRecord({ IDDataset: tmpDataset.IDDataset, VersionPolicy: tmpPolicy });
|
|
809
|
+
|
|
810
|
+
this.fable.DAL.Dataset.doUpdate(tmpUpdateQuery,
|
|
811
|
+
(pUpdateError) =>
|
|
812
|
+
{
|
|
813
|
+
return fStep();
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
else
|
|
817
|
+
{
|
|
818
|
+
return fStep();
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
// Step 6: Mark catalog definition as provisioned
|
|
824
|
+
tmpAnticipate.anticipate(
|
|
825
|
+
(fStep) =>
|
|
826
|
+
{
|
|
827
|
+
if (!tmpSource || !tmpDataset)
|
|
828
|
+
{
|
|
829
|
+
return fStep();
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
let tmpUpdateQuery = this.fable.DAL.CatalogDatasetDefinition.query.clone()
|
|
833
|
+
.addRecord(
|
|
834
|
+
{
|
|
835
|
+
IDCatalogDatasetDefinition: tmpID,
|
|
836
|
+
Provisioned: 1,
|
|
837
|
+
IDSource: tmpSource.IDSource,
|
|
838
|
+
IDDataset: tmpDataset.IDDataset
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
this.fable.DAL.CatalogDatasetDefinition.doUpdate(tmpUpdateQuery,
|
|
842
|
+
(pError) =>
|
|
843
|
+
{
|
|
844
|
+
return fStep();
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
tmpAnticipate.wait(
|
|
849
|
+
(pError) =>
|
|
850
|
+
{
|
|
851
|
+
if (pError)
|
|
852
|
+
{
|
|
853
|
+
pResponse.send({ Error: pError.message || pError });
|
|
854
|
+
return fNext();
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
pResponse.send(
|
|
858
|
+
{
|
|
859
|
+
Success: true,
|
|
860
|
+
Source: tmpSource,
|
|
861
|
+
Dataset: tmpDataset,
|
|
862
|
+
DatasetSource: tmpDatasetSource,
|
|
863
|
+
CatalogDatasetDefinition: tmpCatalogDef
|
|
864
|
+
});
|
|
865
|
+
return fNext();
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
// ================================================================
|
|
870
|
+
// Import / Export
|
|
871
|
+
// ================================================================
|
|
872
|
+
|
|
873
|
+
// POST /facto/catalog/import -- bulk import catalog entries from JSON array
|
|
874
|
+
pOratorServiceServer.doPost(`${tmpRoutePrefix}/catalog/import`,
|
|
875
|
+
(pRequest, pResponse, fNext) =>
|
|
876
|
+
{
|
|
877
|
+
if (!this.fable.DAL || !this.fable.DAL.SourceCatalogEntry || !this.fable.DAL.CatalogDatasetDefinition)
|
|
878
|
+
{
|
|
879
|
+
pResponse.send({ Error: 'Catalog DAL not initialized' });
|
|
880
|
+
return fNext();
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
let tmpEntries = pRequest.body;
|
|
884
|
+
this.fable.log.info(`CatalogManager import: body type=${typeof tmpEntries}, isArray=${Array.isArray(tmpEntries)}`);
|
|
885
|
+
if (!Array.isArray(tmpEntries))
|
|
886
|
+
{
|
|
887
|
+
// Allow { Entries: [...] } wrapper
|
|
888
|
+
if (tmpEntries && Array.isArray(tmpEntries.Entries))
|
|
889
|
+
{
|
|
890
|
+
tmpEntries = tmpEntries.Entries;
|
|
891
|
+
}
|
|
892
|
+
else
|
|
893
|
+
{
|
|
894
|
+
pResponse.send({ Error: 'Request body must be a JSON array of catalog entries (received ' + (typeof tmpEntries) + ')' });
|
|
895
|
+
return fNext();
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
let tmpAnticipate = this.fable.newAnticipate();
|
|
900
|
+
let tmpCreatedEntries = 0;
|
|
901
|
+
let tmpCreatedDatasets = 0;
|
|
902
|
+
let tmpErrors = 0;
|
|
903
|
+
|
|
904
|
+
for (let i = 0; i < tmpEntries.length; i++)
|
|
905
|
+
{
|
|
906
|
+
let tmpEntryData = tmpEntries[i];
|
|
907
|
+
|
|
908
|
+
tmpAnticipate.anticipate(
|
|
909
|
+
(fStep) =>
|
|
910
|
+
{
|
|
911
|
+
let tmpRecord = {
|
|
912
|
+
Agency: tmpEntryData.Agency || '',
|
|
913
|
+
Name: tmpEntryData.Name || '',
|
|
914
|
+
Type: tmpEntryData.Type || '',
|
|
915
|
+
URL: tmpEntryData.URL || '',
|
|
916
|
+
Protocol: tmpEntryData.Protocol || '',
|
|
917
|
+
Category: tmpEntryData.Category || '',
|
|
918
|
+
Region: tmpEntryData.Region || '',
|
|
919
|
+
UpdateFrequency: tmpEntryData.UpdateFrequency || '',
|
|
920
|
+
Description: tmpEntryData.Description || '',
|
|
921
|
+
Notes: tmpEntryData.Notes || '',
|
|
922
|
+
Verified: tmpEntryData.Verified ? 1 : 0
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
let tmpQuery = this.fable.DAL.SourceCatalogEntry.query.clone()
|
|
926
|
+
.addRecord(tmpRecord);
|
|
927
|
+
|
|
928
|
+
this.fable.DAL.SourceCatalogEntry.doCreate(tmpQuery,
|
|
929
|
+
(pError, pQuery, pQueryRead, pCreatedEntry) =>
|
|
930
|
+
{
|
|
931
|
+
if (pError || !pCreatedEntry)
|
|
932
|
+
{
|
|
933
|
+
tmpErrors++;
|
|
934
|
+
return fStep();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
tmpCreatedEntries++;
|
|
938
|
+
|
|
939
|
+
// Create dataset definitions if provided
|
|
940
|
+
let tmpDatasets = tmpEntryData.Datasets || [];
|
|
941
|
+
if (tmpDatasets.length === 0)
|
|
942
|
+
{
|
|
943
|
+
return fStep();
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
let tmpDatasetAnticipate = this.fable.newAnticipate();
|
|
947
|
+
|
|
948
|
+
for (let j = 0; j < tmpDatasets.length; j++)
|
|
949
|
+
{
|
|
950
|
+
let tmpDsData = tmpDatasets[j];
|
|
951
|
+
|
|
952
|
+
tmpDatasetAnticipate.anticipate(
|
|
953
|
+
(fDsStep) =>
|
|
954
|
+
{
|
|
955
|
+
let tmpDefRecord = {
|
|
956
|
+
IDSourceCatalogEntry: pCreatedEntry.IDSourceCatalogEntry,
|
|
957
|
+
Name: tmpDsData.Name || '',
|
|
958
|
+
Format: tmpDsData.Format || '',
|
|
959
|
+
MimeType: tmpDsData.MimeType || '',
|
|
960
|
+
EndpointURL: tmpDsData.EndpointURL || '',
|
|
961
|
+
Description: tmpDsData.Description || '',
|
|
962
|
+
ParseOptions: (typeof tmpDsData.ParseOptions === 'object') ? JSON.stringify(tmpDsData.ParseOptions) : (tmpDsData.ParseOptions || ''),
|
|
963
|
+
AuthRequirements: (typeof tmpDsData.AuthRequirements === 'object') ? JSON.stringify(tmpDsData.AuthRequirements) : (tmpDsData.AuthRequirements || ''),
|
|
964
|
+
VersionPolicy: tmpDsData.VersionPolicy || 'Append',
|
|
965
|
+
Provisioned: 0,
|
|
966
|
+
IDSource: 0,
|
|
967
|
+
IDDataset: 0
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
let tmpDefQuery = this.fable.DAL.CatalogDatasetDefinition.query.clone()
|
|
971
|
+
.addRecord(tmpDefRecord);
|
|
972
|
+
|
|
973
|
+
this.fable.DAL.CatalogDatasetDefinition.doCreate(tmpDefQuery,
|
|
974
|
+
(pDefError) =>
|
|
975
|
+
{
|
|
976
|
+
if (!pDefError)
|
|
977
|
+
{
|
|
978
|
+
tmpCreatedDatasets++;
|
|
979
|
+
}
|
|
980
|
+
else
|
|
981
|
+
{
|
|
982
|
+
tmpErrors++;
|
|
983
|
+
}
|
|
984
|
+
return fDsStep();
|
|
985
|
+
});
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
tmpDatasetAnticipate.wait(
|
|
990
|
+
() =>
|
|
991
|
+
{
|
|
992
|
+
return fStep();
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
tmpAnticipate.wait(
|
|
999
|
+
(pError) =>
|
|
1000
|
+
{
|
|
1001
|
+
pResponse.send(
|
|
1002
|
+
{
|
|
1003
|
+
Success: true,
|
|
1004
|
+
EntriesCreated: tmpCreatedEntries,
|
|
1005
|
+
DatasetsCreated: tmpCreatedDatasets,
|
|
1006
|
+
Errors: tmpErrors
|
|
1007
|
+
});
|
|
1008
|
+
return fNext();
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
// GET /facto/catalog/export -- export full catalog as JSON
|
|
1013
|
+
pOratorServiceServer.doGet(`${tmpRoutePrefix}/catalog/export`,
|
|
1014
|
+
(pRequest, pResponse, fNext) =>
|
|
1015
|
+
{
|
|
1016
|
+
if (!this.fable.DAL || !this.fable.DAL.SourceCatalogEntry || !this.fable.DAL.CatalogDatasetDefinition)
|
|
1017
|
+
{
|
|
1018
|
+
pResponse.send({ Error: 'Catalog DAL not initialized' });
|
|
1019
|
+
return fNext();
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
let tmpAnticipate = this.fable.newAnticipate();
|
|
1023
|
+
let tmpEntries = [];
|
|
1024
|
+
let tmpDatasets = [];
|
|
1025
|
+
|
|
1026
|
+
// Load all entries
|
|
1027
|
+
tmpAnticipate.anticipate(
|
|
1028
|
+
(fStep) =>
|
|
1029
|
+
{
|
|
1030
|
+
let tmpQuery = this.fable.DAL.SourceCatalogEntry.query.clone()
|
|
1031
|
+
.addFilter('Deleted', 0)
|
|
1032
|
+
.setCap(500);
|
|
1033
|
+
|
|
1034
|
+
this.fable.DAL.SourceCatalogEntry.doReads(tmpQuery,
|
|
1035
|
+
(pError, pQuery, pRecords) =>
|
|
1036
|
+
{
|
|
1037
|
+
if (!pError && pRecords)
|
|
1038
|
+
{
|
|
1039
|
+
tmpEntries = pRecords;
|
|
1040
|
+
}
|
|
1041
|
+
return fStep();
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
// Load all dataset definitions
|
|
1046
|
+
tmpAnticipate.anticipate(
|
|
1047
|
+
(fStep) =>
|
|
1048
|
+
{
|
|
1049
|
+
let tmpQuery = this.fable.DAL.CatalogDatasetDefinition.query.clone()
|
|
1050
|
+
.addFilter('Deleted', 0)
|
|
1051
|
+
.setCap(2000);
|
|
1052
|
+
|
|
1053
|
+
this.fable.DAL.CatalogDatasetDefinition.doReads(tmpQuery,
|
|
1054
|
+
(pError, pQuery, pRecords) =>
|
|
1055
|
+
{
|
|
1056
|
+
if (!pError && pRecords)
|
|
1057
|
+
{
|
|
1058
|
+
tmpDatasets = pRecords;
|
|
1059
|
+
}
|
|
1060
|
+
return fStep();
|
|
1061
|
+
});
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
tmpAnticipate.wait(
|
|
1065
|
+
(pError) =>
|
|
1066
|
+
{
|
|
1067
|
+
// Group datasets by parent entry
|
|
1068
|
+
let tmpDatasetsByEntry = {};
|
|
1069
|
+
for (let i = 0; i < tmpDatasets.length; i++)
|
|
1070
|
+
{
|
|
1071
|
+
let tmpEntryID = tmpDatasets[i].IDSourceCatalogEntry;
|
|
1072
|
+
if (!tmpDatasetsByEntry[tmpEntryID])
|
|
1073
|
+
{
|
|
1074
|
+
tmpDatasetsByEntry[tmpEntryID] = [];
|
|
1075
|
+
}
|
|
1076
|
+
tmpDatasetsByEntry[tmpEntryID].push(
|
|
1077
|
+
{
|
|
1078
|
+
Name: tmpDatasets[i].Name,
|
|
1079
|
+
Format: tmpDatasets[i].Format,
|
|
1080
|
+
MimeType: tmpDatasets[i].MimeType,
|
|
1081
|
+
EndpointURL: tmpDatasets[i].EndpointURL,
|
|
1082
|
+
Description: tmpDatasets[i].Description,
|
|
1083
|
+
ParseOptions: tmpDatasets[i].ParseOptions,
|
|
1084
|
+
AuthRequirements: tmpDatasets[i].AuthRequirements,
|
|
1085
|
+
VersionPolicy: tmpDatasets[i].VersionPolicy,
|
|
1086
|
+
Provisioned: tmpDatasets[i].Provisioned,
|
|
1087
|
+
IDSource: tmpDatasets[i].IDSource,
|
|
1088
|
+
IDDataset: tmpDatasets[i].IDDataset
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Build export structure
|
|
1093
|
+
let tmpExport = [];
|
|
1094
|
+
for (let i = 0; i < tmpEntries.length; i++)
|
|
1095
|
+
{
|
|
1096
|
+
let tmpEntry = tmpEntries[i];
|
|
1097
|
+
tmpExport.push(
|
|
1098
|
+
{
|
|
1099
|
+
Agency: tmpEntry.Agency,
|
|
1100
|
+
Name: tmpEntry.Name,
|
|
1101
|
+
Type: tmpEntry.Type,
|
|
1102
|
+
URL: tmpEntry.URL,
|
|
1103
|
+
Protocol: tmpEntry.Protocol,
|
|
1104
|
+
Category: tmpEntry.Category,
|
|
1105
|
+
Region: tmpEntry.Region,
|
|
1106
|
+
UpdateFrequency: tmpEntry.UpdateFrequency,
|
|
1107
|
+
Description: tmpEntry.Description,
|
|
1108
|
+
Notes: tmpEntry.Notes,
|
|
1109
|
+
Verified: tmpEntry.Verified,
|
|
1110
|
+
Datasets: tmpDatasetsByEntry[tmpEntry.IDSourceCatalogEntry] || []
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
pResponse.send({ Count: tmpExport.length, Entries: tmpExport });
|
|
1115
|
+
return fNext();
|
|
1116
|
+
});
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
// ================================================================
|
|
1120
|
+
// Fetch and Ingest from Catalog
|
|
1121
|
+
// ================================================================
|
|
1122
|
+
|
|
1123
|
+
// POST /facto/catalog/dataset/:IDCatalogDatasetDefinition/fetch
|
|
1124
|
+
// Downloads data from the catalog entry's EndpointURL, parses it
|
|
1125
|
+
// using the entry's Format and ParseOptions, and ingests it into
|
|
1126
|
+
// the provisioned Dataset/Source.
|
|
1127
|
+
pOratorServiceServer.doPost(`${tmpRoutePrefix}/catalog/dataset/:IDCatalogDatasetDefinition/fetch`,
|
|
1128
|
+
(pRequest, pResponse, fNext) =>
|
|
1129
|
+
{
|
|
1130
|
+
let tmpID = parseInt(pRequest.params.IDCatalogDatasetDefinition, 10);
|
|
1131
|
+
if (isNaN(tmpID) || tmpID < 1)
|
|
1132
|
+
{
|
|
1133
|
+
pResponse.send({ Error: 'Invalid IDCatalogDatasetDefinition parameter' });
|
|
1134
|
+
return fNext();
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (!this.fable.DAL || !this.fable.DAL.CatalogDatasetDefinition)
|
|
1138
|
+
{
|
|
1139
|
+
pResponse.send({ Error: 'Catalog DAL not initialized' });
|
|
1140
|
+
return fNext();
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Step 1: Load the catalog dataset definition
|
|
1144
|
+
let tmpQuery = this.fable.DAL.CatalogDatasetDefinition.query.clone()
|
|
1145
|
+
.addFilter('IDCatalogDatasetDefinition', tmpID);
|
|
1146
|
+
|
|
1147
|
+
this.fable.DAL.CatalogDatasetDefinition.doRead(tmpQuery,
|
|
1148
|
+
(pError, pQuery, pRecord) =>
|
|
1149
|
+
{
|
|
1150
|
+
if (pError || !pRecord)
|
|
1151
|
+
{
|
|
1152
|
+
pResponse.send({ Error: 'CatalogDatasetDefinition not found' });
|
|
1153
|
+
return fNext();
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Step 2: Validate
|
|
1157
|
+
if (!pRecord.Provisioned)
|
|
1158
|
+
{
|
|
1159
|
+
pResponse.send({ Error: 'Dataset definition must be provisioned before fetching. Call provision first.' });
|
|
1160
|
+
return fNext();
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if (!pRecord.EndpointURL)
|
|
1164
|
+
{
|
|
1165
|
+
pResponse.send({ Error: 'No EndpointURL configured for this dataset definition' });
|
|
1166
|
+
return fNext();
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Parse ParseOptions from JSON string
|
|
1170
|
+
let tmpParseOptions = {};
|
|
1171
|
+
if (pRecord.ParseOptions)
|
|
1172
|
+
{
|
|
1173
|
+
try
|
|
1174
|
+
{
|
|
1175
|
+
tmpParseOptions = JSON.parse(pRecord.ParseOptions);
|
|
1176
|
+
}
|
|
1177
|
+
catch (pParseError)
|
|
1178
|
+
{
|
|
1179
|
+
this.fable.log.warn(`CatalogManager: could not parse ParseOptions for definition ${tmpID}: ${pParseError.message}`);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Step 3: Download
|
|
1184
|
+
this.fable.log.info(`CatalogManager: fetching ${pRecord.EndpointURL} for definition ${tmpID}`);
|
|
1185
|
+
|
|
1186
|
+
this.fable.RetoldFactoIngestEngine.downloadURL(pRecord.EndpointURL,
|
|
1187
|
+
(pDownloadError, pBuffer) =>
|
|
1188
|
+
{
|
|
1189
|
+
if (pDownloadError)
|
|
1190
|
+
{
|
|
1191
|
+
pResponse.send({ Error: 'Download failed: ' + pDownloadError.message });
|
|
1192
|
+
return fNext();
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
this.fable.log.info(`CatalogManager: downloaded ${pBuffer.length} bytes, ingesting as ${pRecord.Format || 'auto'}`);
|
|
1196
|
+
|
|
1197
|
+
// Step 4: Build ingest options from catalog metadata and ingest
|
|
1198
|
+
let tmpFormat = (pRecord.Format || '').toLowerCase();
|
|
1199
|
+
let tmpIngestOptions = {
|
|
1200
|
+
format: tmpFormat,
|
|
1201
|
+
type: 'catalog-fetch',
|
|
1202
|
+
delimiter: tmpParseOptions.delimiter || ',',
|
|
1203
|
+
stripCommentLines: tmpParseOptions.stripCommentLines || false,
|
|
1204
|
+
dataPath: tmpParseOptions.dataPath || '',
|
|
1205
|
+
recordPath: tmpParseOptions.recordPath || '',
|
|
1206
|
+
columns: tmpParseOptions.columns || null
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
let tmpContent = (tmpFormat === 'excel') ? pBuffer : pBuffer.toString('utf8');
|
|
1210
|
+
|
|
1211
|
+
this.fable.RetoldFactoIngestEngine.ingestContent(
|
|
1212
|
+
tmpContent,
|
|
1213
|
+
pRecord.IDDataset,
|
|
1214
|
+
pRecord.IDSource,
|
|
1215
|
+
tmpIngestOptions,
|
|
1216
|
+
(pIngestError, pResult) =>
|
|
1217
|
+
{
|
|
1218
|
+
if (pIngestError)
|
|
1219
|
+
{
|
|
1220
|
+
pResponse.send({ Error: 'Ingest failed: ' + pIngestError.message });
|
|
1221
|
+
return fNext();
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
pResponse.send(
|
|
1225
|
+
{
|
|
1226
|
+
Success: true,
|
|
1227
|
+
IDCatalogDatasetDefinition: tmpID,
|
|
1228
|
+
IDDataset: pRecord.IDDataset,
|
|
1229
|
+
IDSource: pRecord.IDSource,
|
|
1230
|
+
EndpointURL: pRecord.EndpointURL,
|
|
1231
|
+
Ingested: pResult.Ingested,
|
|
1232
|
+
Errors: pResult.Errors,
|
|
1233
|
+
Total: pResult.Total,
|
|
1234
|
+
DatasetVersion: pResult.DatasetVersion,
|
|
1235
|
+
ContentSignature: pResult.ContentSignature,
|
|
1236
|
+
IsDuplicate: pResult.IsDuplicate,
|
|
1237
|
+
Format: pResult.Format,
|
|
1238
|
+
IngestJob: pResult.IngestJob
|
|
1239
|
+
});
|
|
1240
|
+
return fNext();
|
|
1241
|
+
});
|
|
1242
|
+
});
|
|
1243
|
+
});
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
this.fable.log.info(`CatalogManager routes connected at ${tmpRoutePrefix}/catalog/*`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
module.exports = RetoldFactoCatalogManager;
|
|
1251
|
+
module.exports.serviceType = 'RetoldFactoCatalogManager';
|
|
1252
|
+
module.exports.default_configuration = defaultCatalogManagerOptions;
|