retold-data-service 2.0.12 → 2.0.14
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/README.md +64 -55
- package/docs/README.md +24 -66
- package/docs/_cover.md +7 -7
- package/docs/_sidebar.md +24 -11
- package/docs/_topbar.md +2 -0
- package/docs/api/constructor.md +67 -0
- package/docs/api/initializeDataEndpoints.md +54 -0
- package/docs/api/initializePersistenceEngine.md +34 -0
- package/docs/api/initializeService.md +44 -0
- package/docs/api/onAfterInitialize.md +48 -0
- package/docs/api/onBeforeInitialize.md +45 -0
- package/docs/api/onInitialize.md +38 -0
- package/docs/api/reference.md +35 -0
- package/docs/api/stopService.md +34 -0
- package/docs/architecture.md +78 -14
- package/docs/behavior-injection.md +121 -78
- package/docs/css/docuserve.css +73 -0
- package/docs/dal-access.md +135 -56
- package/docs/index.html +1 -1
- package/docs/quick-start.md +169 -0
- package/docs/retold-catalog.json +144 -0
- package/docs/retold-keyword-index.json +3530 -0
- package/example_applications/data-cloner/data/cloned.sqlite +0 -0
- package/example_applications/data-cloner/data/cloned.sqlite-shm +0 -0
- package/example_applications/data-cloner/data/cloned.sqlite-wal +0 -0
- package/example_applications/data-cloner/data-cloner-web.html +935 -0
- package/example_applications/data-cloner/data-cloner.js +1047 -0
- package/example_applications/data-cloner/package.json +19 -0
- package/package.json +13 -9
- package/source/Retold-Data-Service.js +225 -73
- package/source/services/Retold-Data-Service-ConnectionManager.js +277 -0
- package/source/services/Retold-Data-Service-MeadowEndpoints.js +217 -0
- package/source/services/Retold-Data-Service-ModelManager.js +335 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-CSVCheck.js +85 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-CSVTransform.js +180 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionIntersect.js +153 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionPush.js +190 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionToArray.js +113 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-ComprehensionToCSV.js +211 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-EntityFromTabularFolder.js +244 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-JSONArrayTransform.js +213 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-TSVCheck.js +80 -0
- package/source/services/meadow-integration/MeadowIntegration-Command-TSVTransform.js +166 -0
- package/source/services/meadow-integration/Retold-Data-Service-MeadowIntegration.js +113 -0
- package/source/services/migration-manager/MigrationManager-Command-Connections.js +220 -0
- package/source/services/migration-manager/MigrationManager-Command-DiffMigrate.js +169 -0
- package/source/services/migration-manager/MigrationManager-Command-Schemas.js +532 -0
- package/source/services/migration-manager/MigrationManager-Command-WebUI.js +123 -0
- package/source/services/migration-manager/Retold-Data-Service-MigrationManager.js +357 -0
- package/source/services/stricture/Retold-Data-Service-Stricture.js +303 -0
- package/source/services/stricture/Stricture-Command-Compile.js +39 -0
- package/source/services/stricture/Stricture-Command-Generate-AuthorizationChart.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-DictionaryCSV.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-LaTeX.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-Markdown.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-Meadow.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-ModelGraph.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-MySQL.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-MySQLMigrate.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-Pict.js +14 -0
- package/source/services/stricture/Stricture-Command-Generate-TestObjectContainers.js +14 -0
- package/test/RetoldDataService_tests.js +161 -1
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retold Data Service - Model Manager
|
|
3
|
+
*
|
|
4
|
+
* Fable service that manages named model definitions and provides
|
|
5
|
+
* REST endpoints for uploading models, inspecting schemas, and
|
|
6
|
+
* connecting models to database connections.
|
|
7
|
+
*
|
|
8
|
+
* Routes are split into read (schema inspection, always available)
|
|
9
|
+
* and write (model upload/delete/connect, configurable) groups.
|
|
10
|
+
*
|
|
11
|
+
* Delegates to RetoldDataServiceMeadowEndpoints for DAL/endpoint
|
|
12
|
+
* creation and RetoldDataServiceConnectionManager for connection lookups.
|
|
13
|
+
*
|
|
14
|
+
* @author Steven Velozo <steven@velozo.com>
|
|
15
|
+
*/
|
|
16
|
+
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
17
|
+
|
|
18
|
+
class RetoldDataServiceModelManager extends libFableServiceProviderBase
|
|
19
|
+
{
|
|
20
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
21
|
+
{
|
|
22
|
+
super(pFable, pOptions, pServiceHash);
|
|
23
|
+
|
|
24
|
+
this.serviceType = 'RetoldDataServiceModelManager';
|
|
25
|
+
|
|
26
|
+
// Named model storage: { modelName: { Name, Model, ConnectionName, Initialized } }
|
|
27
|
+
this.modelDefinitions = {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Add a named model definition. This stores the model but does NOT
|
|
32
|
+
* create DAL objects or endpoints. Use connectModelToConnection()
|
|
33
|
+
* to activate a model's endpoints.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} pName - Model name
|
|
36
|
+
* @param {Object} pModelObject - Parsed stricture model (MeadowModel-Extended.json format)
|
|
37
|
+
* @param {function} fCallback - Callback invoked as fCallback(pError)
|
|
38
|
+
*/
|
|
39
|
+
addModel(pName, pModelObject, fCallback)
|
|
40
|
+
{
|
|
41
|
+
if (!pName || typeof(pName) !== 'string')
|
|
42
|
+
{
|
|
43
|
+
return fCallback(new Error('Model name is required and must be a string.'));
|
|
44
|
+
}
|
|
45
|
+
if (!pModelObject || !pModelObject.Tables || typeof(pModelObject.Tables) !== 'object')
|
|
46
|
+
{
|
|
47
|
+
return fCallback(new Error('Model must include a Tables object.'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (this.modelDefinitions.hasOwnProperty(pName))
|
|
51
|
+
{
|
|
52
|
+
this.fable.log.warn(`Model [${pName}] already exists; overwriting.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.modelDefinitions[pName] = (
|
|
56
|
+
{
|
|
57
|
+
Name: pName,
|
|
58
|
+
Model: pModelObject,
|
|
59
|
+
ConnectionName: false,
|
|
60
|
+
Initialized: false
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.fable.log.info(`Model [${pName}] added with ${Object.keys(pModelObject.Tables).length} entities.`);
|
|
64
|
+
return fCallback();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Remove a named model definition.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} pName - Model name
|
|
71
|
+
* @param {function} fCallback - Callback invoked as fCallback(pError)
|
|
72
|
+
*/
|
|
73
|
+
removeModel(pName, fCallback)
|
|
74
|
+
{
|
|
75
|
+
if (!this.modelDefinitions.hasOwnProperty(pName))
|
|
76
|
+
{
|
|
77
|
+
return fCallback(new Error(`Model [${pName}] does not exist.`));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
delete this.modelDefinitions[pName];
|
|
81
|
+
this.fable.log.info(`Model [${pName}] removed.`);
|
|
82
|
+
return fCallback();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get a single model's metadata.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} pName - Model name
|
|
89
|
+
* @return {Object|false} Model metadata or false if not found
|
|
90
|
+
*/
|
|
91
|
+
getModel(pName)
|
|
92
|
+
{
|
|
93
|
+
if (!this.modelDefinitions.hasOwnProperty(pName))
|
|
94
|
+
{
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return this.modelDefinitions[pName];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get summary metadata for all models.
|
|
103
|
+
*
|
|
104
|
+
* @return {Array} Array of model summaries
|
|
105
|
+
*/
|
|
106
|
+
getModels()
|
|
107
|
+
{
|
|
108
|
+
let tmpModelList = [];
|
|
109
|
+
let tmpModelNames = Object.keys(this.modelDefinitions);
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < tmpModelNames.length; i++)
|
|
112
|
+
{
|
|
113
|
+
let tmpModelDef = this.modelDefinitions[tmpModelNames[i]];
|
|
114
|
+
tmpModelList.push(
|
|
115
|
+
{
|
|
116
|
+
Name: tmpModelDef.Name,
|
|
117
|
+
EntityCount: Object.keys(tmpModelDef.Model.Tables).length,
|
|
118
|
+
ConnectionName: tmpModelDef.ConnectionName,
|
|
119
|
+
Initialized: tmpModelDef.Initialized
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return tmpModelList;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Connect a named model to a named connection, activating its
|
|
128
|
+
* DAL objects and CRUD endpoints.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} pModelName - Model name (must exist in modelDefinitions)
|
|
131
|
+
* @param {string} pConnectionName - Connection name (must exist in ConnectionManager)
|
|
132
|
+
* @param {function} fCallback - Callback invoked as fCallback(pError)
|
|
133
|
+
*/
|
|
134
|
+
connectModelToConnection(pModelName, pConnectionName, fCallback)
|
|
135
|
+
{
|
|
136
|
+
if (!this.modelDefinitions.hasOwnProperty(pModelName))
|
|
137
|
+
{
|
|
138
|
+
return fCallback(new Error(`Model [${pModelName}] does not exist.`));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let tmpConnectionManager = this.fable.RetoldDataServiceConnectionManager;
|
|
142
|
+
let tmpConnection = tmpConnectionManager.getConnection(pConnectionName);
|
|
143
|
+
|
|
144
|
+
if (!tmpConnection)
|
|
145
|
+
{
|
|
146
|
+
return fCallback(new Error(`Connection [${pConnectionName}] does not exist.`));
|
|
147
|
+
}
|
|
148
|
+
if (!tmpConnection.Active)
|
|
149
|
+
{
|
|
150
|
+
return fCallback(new Error(`Connection [${pConnectionName}] is not active.`));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let tmpModelDef = this.modelDefinitions[pModelName];
|
|
154
|
+
|
|
155
|
+
this.fable.log.info(`Connecting model [${pModelName}] to connection [${pConnectionName}] (provider: ${tmpConnection.Provider})...`);
|
|
156
|
+
|
|
157
|
+
// Delegate to MeadowEndpoints to create DAL objects and wire routes
|
|
158
|
+
this.fable.RetoldDataServiceMeadowEndpoints.loadModel(pModelName, tmpModelDef.Model, tmpConnection.Provider,
|
|
159
|
+
(pError) =>
|
|
160
|
+
{
|
|
161
|
+
if (pError)
|
|
162
|
+
{
|
|
163
|
+
return fCallback(pError);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
tmpModelDef.ConnectionName = pConnectionName;
|
|
167
|
+
tmpModelDef.Initialized = true;
|
|
168
|
+
this.fable.log.info(`Model [${pModelName}] connected to [${pConnectionName}] and endpoints activated.`);
|
|
169
|
+
return fCallback();
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Register schema READ routes on the Orator service server.
|
|
175
|
+
* These routes are always available regardless of endpoint configuration.
|
|
176
|
+
*
|
|
177
|
+
* @param {Object} pOratorServiceServer - The Orator ServiceServer instance
|
|
178
|
+
*/
|
|
179
|
+
connectReadRoutes(pOratorServiceServer)
|
|
180
|
+
{
|
|
181
|
+
let tmpSelf = this;
|
|
182
|
+
|
|
183
|
+
// GET /1.0/Retold/Models — list all models
|
|
184
|
+
pOratorServiceServer.get('/1.0/Retold/Models',
|
|
185
|
+
(pRequest, pResponse, fNext) =>
|
|
186
|
+
{
|
|
187
|
+
pResponse.send(200, tmpSelf.getModels());
|
|
188
|
+
return fNext();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// GET /1.0/Retold/Model/:Name — get a single model's full definition
|
|
192
|
+
pOratorServiceServer.get('/1.0/Retold/Model/:Name',
|
|
193
|
+
(pRequest, pResponse, fNext) =>
|
|
194
|
+
{
|
|
195
|
+
let tmpModel = tmpSelf.getModel(pRequest.params.Name);
|
|
196
|
+
if (!tmpModel)
|
|
197
|
+
{
|
|
198
|
+
pResponse.send(404, { Error: `Model [${pRequest.params.Name}] not found.` });
|
|
199
|
+
return fNext();
|
|
200
|
+
}
|
|
201
|
+
pResponse.send(200, tmpModel);
|
|
202
|
+
return fNext();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// GET /1.0/Retold/Model/:Name/Entities — list entities in a model
|
|
206
|
+
pOratorServiceServer.get('/1.0/Retold/Model/:Name/Entities',
|
|
207
|
+
(pRequest, pResponse, fNext) =>
|
|
208
|
+
{
|
|
209
|
+
let tmpModel = tmpSelf.getModel(pRequest.params.Name);
|
|
210
|
+
if (!tmpModel)
|
|
211
|
+
{
|
|
212
|
+
pResponse.send(404, { Error: `Model [${pRequest.params.Name}] not found.` });
|
|
213
|
+
return fNext();
|
|
214
|
+
}
|
|
215
|
+
let tmpEntities = Object.keys(tmpModel.Model.Tables);
|
|
216
|
+
pResponse.send(200, { Name: pRequest.params.Name, Entities: tmpEntities });
|
|
217
|
+
return fNext();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// GET /1.0/Retold/Model/:Name/Entity/:EntityName — entity detail
|
|
221
|
+
pOratorServiceServer.get('/1.0/Retold/Model/:Name/Entity/:EntityName',
|
|
222
|
+
(pRequest, pResponse, fNext) =>
|
|
223
|
+
{
|
|
224
|
+
let tmpModel = tmpSelf.getModel(pRequest.params.Name);
|
|
225
|
+
if (!tmpModel)
|
|
226
|
+
{
|
|
227
|
+
pResponse.send(404, { Error: `Model [${pRequest.params.Name}] not found.` });
|
|
228
|
+
return fNext();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let tmpEntityName = pRequest.params.EntityName;
|
|
232
|
+
if (!tmpModel.Model.Tables.hasOwnProperty(tmpEntityName))
|
|
233
|
+
{
|
|
234
|
+
pResponse.send(404, { Error: `Entity [${tmpEntityName}] not found in model [${pRequest.params.Name}].` });
|
|
235
|
+
return fNext();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let tmpEntity = tmpModel.Model.Tables[tmpEntityName];
|
|
239
|
+
pResponse.send(200, tmpEntity);
|
|
240
|
+
return fNext();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
this.fable.log.info('Retold Data Service ModelManager read routes registered.');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Register model management WRITE routes on the Orator service server.
|
|
248
|
+
* These routes allow runtime model upload, deletion, and activation.
|
|
249
|
+
*
|
|
250
|
+
* @param {Object} pOratorServiceServer - The Orator ServiceServer instance
|
|
251
|
+
*/
|
|
252
|
+
connectWriteRoutes(pOratorServiceServer)
|
|
253
|
+
{
|
|
254
|
+
let tmpSelf = this;
|
|
255
|
+
|
|
256
|
+
// POST /1.0/Retold/Model — upload a named model
|
|
257
|
+
pOratorServiceServer.postWithBodyParser('/1.0/Retold/Model',
|
|
258
|
+
(pRequest, pResponse, fNext) =>
|
|
259
|
+
{
|
|
260
|
+
let tmpBody = pRequest.body;
|
|
261
|
+
if (!tmpBody || !tmpBody.Name || !tmpBody.Model)
|
|
262
|
+
{
|
|
263
|
+
pResponse.send(400, { Error: 'Request body must include Name and Model.' });
|
|
264
|
+
return fNext();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
tmpSelf.addModel(tmpBody.Name, tmpBody.Model,
|
|
268
|
+
(pError) =>
|
|
269
|
+
{
|
|
270
|
+
if (pError)
|
|
271
|
+
{
|
|
272
|
+
pResponse.send(500, { Error: pError.message });
|
|
273
|
+
return fNext();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let tmpModelDef = tmpSelf.modelDefinitions[tmpBody.Name];
|
|
277
|
+
pResponse.send(200,
|
|
278
|
+
{
|
|
279
|
+
Name: tmpModelDef.Name,
|
|
280
|
+
EntityCount: Object.keys(tmpModelDef.Model.Tables).length,
|
|
281
|
+
ConnectionName: tmpModelDef.ConnectionName,
|
|
282
|
+
Initialized: tmpModelDef.Initialized
|
|
283
|
+
});
|
|
284
|
+
return fNext();
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// DEL /1.0/Retold/Model/:Name — remove a model
|
|
289
|
+
pOratorServiceServer.del('/1.0/Retold/Model/:Name',
|
|
290
|
+
(pRequest, pResponse, fNext) =>
|
|
291
|
+
{
|
|
292
|
+
tmpSelf.removeModel(pRequest.params.Name,
|
|
293
|
+
(pError) =>
|
|
294
|
+
{
|
|
295
|
+
if (pError)
|
|
296
|
+
{
|
|
297
|
+
pResponse.send(404, { Error: pError.message });
|
|
298
|
+
return fNext();
|
|
299
|
+
}
|
|
300
|
+
pResponse.send(200, { Message: `Model [${pRequest.params.Name}] removed.` });
|
|
301
|
+
return fNext();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// POST /1.0/Retold/Model/:Name/Connect/:ConnectionName — connect model to connection
|
|
306
|
+
pOratorServiceServer.postWithBodyParser('/1.0/Retold/Model/:Name/Connect/:ConnectionName',
|
|
307
|
+
(pRequest, pResponse, fNext) =>
|
|
308
|
+
{
|
|
309
|
+
tmpSelf.connectModelToConnection(pRequest.params.Name, pRequest.params.ConnectionName,
|
|
310
|
+
(pError) =>
|
|
311
|
+
{
|
|
312
|
+
if (pError)
|
|
313
|
+
{
|
|
314
|
+
pResponse.send(500, { Error: pError.message });
|
|
315
|
+
return fNext();
|
|
316
|
+
}
|
|
317
|
+
let tmpModelDef = tmpSelf.modelDefinitions[pRequest.params.Name];
|
|
318
|
+
pResponse.send(200,
|
|
319
|
+
{
|
|
320
|
+
Name: tmpModelDef.Name,
|
|
321
|
+
EntityCount: Object.keys(tmpModelDef.Model.Tables).length,
|
|
322
|
+
ConnectionName: tmpModelDef.ConnectionName,
|
|
323
|
+
Initialized: tmpModelDef.Initialized
|
|
324
|
+
});
|
|
325
|
+
return fNext();
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
this.fable.log.info('Retold Data Service ModelManager write routes registered.');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
module.exports = RetoldDataServiceModelManager;
|
|
334
|
+
module.exports.serviceType = 'RetoldDataServiceModelManager';
|
|
335
|
+
module.exports.default_configuration = {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MeadowIntegration Command - CSV Check
|
|
3
|
+
*
|
|
4
|
+
* POST /1.0/Retold/MeadowIntegration/CSV/Check
|
|
5
|
+
*
|
|
6
|
+
* Analyze a CSV file for statistics.
|
|
7
|
+
*
|
|
8
|
+
* Body: { "File": "/path/to/file.csv", "Records": false, "QuoteDelimiter": "\"" }
|
|
9
|
+
* Returns: Statistics JSON
|
|
10
|
+
*/
|
|
11
|
+
const libFS = require('fs');
|
|
12
|
+
const libReadline = require('readline');
|
|
13
|
+
|
|
14
|
+
module.exports = function(pIntegrationService, pOratorServiceServer)
|
|
15
|
+
{
|
|
16
|
+
let tmpPict = pIntegrationService.pict;
|
|
17
|
+
|
|
18
|
+
pOratorServiceServer.postWithBodyParser(`${pIntegrationService.routePrefix}/CSV/Check`,
|
|
19
|
+
(pRequest, pResponse, fNext) =>
|
|
20
|
+
{
|
|
21
|
+
let tmpBody = pRequest.body || {};
|
|
22
|
+
|
|
23
|
+
if (!tmpBody.File || (typeof(tmpBody.File) !== 'string'))
|
|
24
|
+
{
|
|
25
|
+
pResponse.send(400, { Error: 'No valid File path provided in request body.' });
|
|
26
|
+
return fNext();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let tmpInputFilePath = tmpPict.FilePersistence.resolvePath(tmpBody.File);
|
|
30
|
+
|
|
31
|
+
if (!tmpPict.FilePersistence.existsSync(tmpInputFilePath))
|
|
32
|
+
{
|
|
33
|
+
pResponse.send(404, { Error: `File [${tmpInputFilePath}] does not exist.` });
|
|
34
|
+
return fNext();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Create a fresh CSVParser for each request to reset header state
|
|
38
|
+
let tmpCSVParser = tmpPict.instantiateServiceProviderWithoutRegistration('CSVParser');
|
|
39
|
+
|
|
40
|
+
if (tmpBody.QuoteDelimiter)
|
|
41
|
+
{
|
|
42
|
+
tmpCSVParser.QuoteCharacter = tmpBody.QuoteDelimiter;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let tmpStatistics = tmpPict.MeadowIntegrationTabularCheck.newStatisticsObject(tmpInputFilePath);
|
|
46
|
+
let tmpStoreFullRecord = (tmpBody.Records === true);
|
|
47
|
+
|
|
48
|
+
if (tmpStoreFullRecord)
|
|
49
|
+
{
|
|
50
|
+
tmpStatistics.Records = [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const tmpReadline = libReadline.createInterface(
|
|
54
|
+
{
|
|
55
|
+
input: libFS.createReadStream(tmpInputFilePath),
|
|
56
|
+
crlfDelay: Infinity,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
tmpReadline.on('line',
|
|
60
|
+
(pLine) =>
|
|
61
|
+
{
|
|
62
|
+
const tmpRecord = tmpCSVParser.parseCSVLine(pLine);
|
|
63
|
+
if (tmpRecord)
|
|
64
|
+
{
|
|
65
|
+
tmpPict.MeadowIntegrationTabularCheck.collectStatistics(tmpRecord, tmpStatistics, tmpStoreFullRecord);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
tmpReadline.on('close',
|
|
70
|
+
() =>
|
|
71
|
+
{
|
|
72
|
+
tmpPict.log.info(`CSV Check: ${tmpStatistics.RowCount} rows, ${tmpStatistics.ColumnCount} columns in [${tmpInputFilePath}].`);
|
|
73
|
+
pResponse.send(200, tmpStatistics);
|
|
74
|
+
return fNext();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
tmpReadline.on('error',
|
|
78
|
+
(pError) =>
|
|
79
|
+
{
|
|
80
|
+
tmpPict.log.error(`CSV Check error reading file [${tmpInputFilePath}]: ${pError}`, pError);
|
|
81
|
+
pResponse.send(500, { Error: `Error reading CSV file: ${pError.message}` });
|
|
82
|
+
return fNext();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MeadowIntegration Command - CSV Transform
|
|
3
|
+
*
|
|
4
|
+
* POST /1.0/Retold/MeadowIntegration/CSV/Transform
|
|
5
|
+
*
|
|
6
|
+
* Transform a CSV file into a comprehension using mapping configuration.
|
|
7
|
+
*
|
|
8
|
+
* Body: {
|
|
9
|
+
* "File": "/path/to/file.csv",
|
|
10
|
+
* "Entity": "MyEntity",
|
|
11
|
+
* "GUIDTemplate": "{~D:Record.id~}",
|
|
12
|
+
* "Mappings": { "Col1": "{~D:Record.col1~}" },
|
|
13
|
+
* "MappingConfiguration": { ... },
|
|
14
|
+
* "IncomingComprehension": { ... },
|
|
15
|
+
* "Extended": false,
|
|
16
|
+
* "QuoteDelimiter": "\""
|
|
17
|
+
* }
|
|
18
|
+
* Returns: Comprehension JSON (or extended state if Extended=true)
|
|
19
|
+
*/
|
|
20
|
+
const libFS = require('fs');
|
|
21
|
+
const libPath = require('path');
|
|
22
|
+
const libReadline = require('readline');
|
|
23
|
+
|
|
24
|
+
module.exports = function(pIntegrationService, pOratorServiceServer)
|
|
25
|
+
{
|
|
26
|
+
let tmpPict = pIntegrationService.pict;
|
|
27
|
+
|
|
28
|
+
pOratorServiceServer.postWithBodyParser(`${pIntegrationService.routePrefix}/CSV/Transform`,
|
|
29
|
+
(pRequest, pResponse, fNext) =>
|
|
30
|
+
{
|
|
31
|
+
let tmpBody = pRequest.body || {};
|
|
32
|
+
|
|
33
|
+
if (!tmpBody.File || (typeof(tmpBody.File) !== 'string'))
|
|
34
|
+
{
|
|
35
|
+
pResponse.send(400, { Error: 'No valid File path provided in request body.' });
|
|
36
|
+
return fNext();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let tmpInputFilePath = tmpPict.FilePersistence.resolvePath(tmpBody.File);
|
|
40
|
+
|
|
41
|
+
if (!tmpPict.FilePersistence.existsSync(tmpInputFilePath))
|
|
42
|
+
{
|
|
43
|
+
pResponse.send(404, { Error: `File [${tmpInputFilePath}] does not exist.` });
|
|
44
|
+
return fNext();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let tmpCSVParser = tmpPict.instantiateServiceProviderWithoutRegistration('CSVParser');
|
|
48
|
+
|
|
49
|
+
if (tmpBody.QuoteDelimiter)
|
|
50
|
+
{
|
|
51
|
+
tmpCSVParser.QuoteCharacter = tmpBody.QuoteDelimiter;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let tmpMappingOutcome = tmpPict.MeadowIntegrationTabularTransform.newMappingOutcomeObject();
|
|
55
|
+
|
|
56
|
+
// Apply user configuration from request body
|
|
57
|
+
if (tmpBody.Entity)
|
|
58
|
+
{
|
|
59
|
+
tmpMappingOutcome.UserConfiguration.Entity = tmpBody.Entity;
|
|
60
|
+
}
|
|
61
|
+
if (tmpBody.GUIDName)
|
|
62
|
+
{
|
|
63
|
+
tmpMappingOutcome.UserConfiguration.GUIDName = tmpBody.GUIDName;
|
|
64
|
+
}
|
|
65
|
+
if (tmpBody.GUIDTemplate)
|
|
66
|
+
{
|
|
67
|
+
tmpMappingOutcome.UserConfiguration.GUIDTemplate = tmpBody.GUIDTemplate;
|
|
68
|
+
}
|
|
69
|
+
if (tmpBody.Mappings && (typeof(tmpBody.Mappings) === 'object'))
|
|
70
|
+
{
|
|
71
|
+
tmpMappingOutcome.UserConfiguration.Mappings = tmpBody.Mappings;
|
|
72
|
+
}
|
|
73
|
+
if (tmpBody.MappingConfiguration && (typeof(tmpBody.MappingConfiguration) === 'object'))
|
|
74
|
+
{
|
|
75
|
+
tmpMappingOutcome.ExplicitConfiguration = tmpBody.MappingConfiguration;
|
|
76
|
+
}
|
|
77
|
+
if (tmpBody.IncomingComprehension && (typeof(tmpBody.IncomingComprehension) === 'object'))
|
|
78
|
+
{
|
|
79
|
+
tmpMappingOutcome.ExistingComprehension = tmpBody.IncomingComprehension;
|
|
80
|
+
tmpMappingOutcome.Comprehension = JSON.parse(JSON.stringify(tmpBody.IncomingComprehension));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const tmpReadline = libReadline.createInterface(
|
|
84
|
+
{
|
|
85
|
+
input: libFS.createReadStream(tmpInputFilePath),
|
|
86
|
+
crlfDelay: Infinity,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
tmpReadline.on('line',
|
|
90
|
+
(pLine) =>
|
|
91
|
+
{
|
|
92
|
+
const tmpIncomingRecord = tmpCSVParser.parseCSVLine(pLine);
|
|
93
|
+
tmpMappingOutcome.ParsedRowCount++;
|
|
94
|
+
|
|
95
|
+
if (tmpIncomingRecord)
|
|
96
|
+
{
|
|
97
|
+
if (!tmpMappingOutcome.ImplicitConfiguration)
|
|
98
|
+
{
|
|
99
|
+
tmpMappingOutcome.ImplicitConfiguration = tmpPict.MeadowIntegrationTabularTransform.generateMappingConfigurationPrototype(libPath.basename(tmpInputFilePath), tmpIncomingRecord);
|
|
100
|
+
|
|
101
|
+
if ((!tmpMappingOutcome.ExplicitConfiguration) || (typeof(tmpMappingOutcome.ExplicitConfiguration) != 'object'))
|
|
102
|
+
{
|
|
103
|
+
tmpMappingOutcome.Configuration = Object.assign({}, tmpMappingOutcome.ImplicitConfiguration, tmpMappingOutcome.UserConfiguration);
|
|
104
|
+
}
|
|
105
|
+
else
|
|
106
|
+
{
|
|
107
|
+
tmpMappingOutcome.Configuration = Object.assign({}, tmpMappingOutcome.ImplicitConfiguration, tmpMappingOutcome.ExplicitConfiguration, tmpMappingOutcome.UserConfiguration);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!('GUIDName' in tmpMappingOutcome.Configuration))
|
|
111
|
+
{
|
|
112
|
+
tmpMappingOutcome.Configuration.GUIDName = `GUID${tmpMappingOutcome.Configuration.Entity}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!(tmpMappingOutcome.Configuration.Entity in tmpMappingOutcome.Comprehension))
|
|
116
|
+
{
|
|
117
|
+
tmpMappingOutcome.Comprehension[tmpMappingOutcome.Configuration.Entity] = {};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let tmpMappingRecordSolution = (
|
|
122
|
+
{
|
|
123
|
+
IncomingRecord: tmpIncomingRecord,
|
|
124
|
+
MappingConfiguration: tmpMappingOutcome.Configuration,
|
|
125
|
+
MappingOutcome: tmpMappingOutcome,
|
|
126
|
+
RowIndex: tmpMappingOutcome.ParsedRowCount,
|
|
127
|
+
NewRecordsGUIDUniqueness: [],
|
|
128
|
+
NewRecordPrototype: {},
|
|
129
|
+
Fable: tmpPict,
|
|
130
|
+
Pict: tmpPict,
|
|
131
|
+
AppData: tmpPict.AppData
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
let tmpSolverResultsObject = {};
|
|
135
|
+
if (tmpMappingOutcome.Configuration.Solvers && Array.isArray(tmpMappingOutcome.Configuration.Solvers))
|
|
136
|
+
{
|
|
137
|
+
for (let i = 0; i < tmpMappingOutcome.Configuration.Solvers.length; i++)
|
|
138
|
+
{
|
|
139
|
+
tmpPict.ExpressionParser.solve(tmpMappingOutcome.Configuration.Solvers[i], tmpMappingRecordSolution, tmpSolverResultsObject, tmpPict.manifest, tmpMappingRecordSolution);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (tmpMappingOutcome.Configuration.MultipleGUIDUniqueness && tmpMappingRecordSolution.NewRecordsGUIDUniqueness.length > 0)
|
|
144
|
+
{
|
|
145
|
+
for (let i = 0; i < tmpMappingRecordSolution.NewRecordsGUIDUniqueness.length; i++)
|
|
146
|
+
{
|
|
147
|
+
tmpPict.MeadowIntegrationTabularTransform.addRecordToComprehension(tmpIncomingRecord, tmpMappingOutcome, tmpMappingRecordSolution.NewRecordPrototype, tmpMappingRecordSolution.NewRecordsGUIDUniqueness[i]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (!tmpMappingOutcome.Configuration.MultipleGUIDUniqueness)
|
|
151
|
+
{
|
|
152
|
+
tmpPict.MeadowIntegrationTabularTransform.addRecordToComprehension(tmpIncomingRecord, tmpMappingOutcome, tmpMappingRecordSolution.NewRecordPrototype);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
tmpReadline.on('close',
|
|
158
|
+
() =>
|
|
159
|
+
{
|
|
160
|
+
tmpPict.log.info(`CSV Transform: Parsed ${tmpMappingOutcome.ParsedRowCount} rows from [${tmpInputFilePath}].`);
|
|
161
|
+
if (tmpBody.Extended)
|
|
162
|
+
{
|
|
163
|
+
pResponse.send(200, tmpMappingOutcome);
|
|
164
|
+
}
|
|
165
|
+
else
|
|
166
|
+
{
|
|
167
|
+
pResponse.send(200, tmpMappingOutcome.Comprehension);
|
|
168
|
+
}
|
|
169
|
+
return fNext();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
tmpReadline.on('error',
|
|
173
|
+
(pError) =>
|
|
174
|
+
{
|
|
175
|
+
tmpPict.log.error(`CSV Transform error reading file [${tmpInputFilePath}]: ${pError}`, pError);
|
|
176
|
+
pResponse.send(500, { Error: `Error reading CSV file: ${pError.message}` });
|
|
177
|
+
return fNext();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
};
|