meadow-integration 1.0.1 → 1.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/CONTRIBUTING.md +50 -0
- package/README.md +223 -7
- package/docs/README.md +107 -7
- package/docs/_sidebar.md +38 -0
- package/docs/_topbar.md +7 -0
- package/docs/cli-reference.md +242 -0
- package/docs/comprehensions.md +98 -0
- package/docs/cover.md +11 -0
- package/docs/css/docuserve.css +73 -0
- package/docs/examples-walkthrough.md +138 -0
- package/docs/index.html +37 -20
- package/docs/integration-adapter.md +109 -0
- package/docs/mapping-files.md +140 -0
- package/docs/programmatic-api.md +173 -0
- package/docs/rest-api-reference.md +731 -0
- package/docs/retold-catalog.json +153 -0
- package/docs/retold-keyword-index.json +4828 -0
- package/examples/Example-001-CSV-Check.sh +29 -0
- package/examples/Example-002-CSV-Transform-Implicit.sh +31 -0
- package/examples/Example-003-CSV-Transform-CLI-Options.sh +39 -0
- package/examples/Example-004-CSV-Transform-Mapping-File.sh +41 -0
- package/examples/Example-005-Multi-Entity-Bookstore.sh +60 -0
- package/examples/Example-006-Multi-CSV-Intersect.sh +74 -0
- package/examples/Example-007-Comprehension-To-Array.sh +41 -0
- package/examples/Example-008-Comprehension-To-CSV.sh +51 -0
- package/examples/Example-009-JSON-Array-Transform.sh +46 -0
- package/examples/Example-010-Programmatic-API.js +138 -0
- package/examples/README.md +44 -0
- package/examples/output/.gitignore +2 -0
- package/package.json +7 -4
- package/source/Meadow-Integration.js +3 -1
- package/source/cli/Meadow-Integration-CLI-Program.js +4 -1
- package/source/cli/commands/Meadow-Integration-Command-ObjectArrayToCSV.js +49 -32
- package/source/cli/commands/Meadow-Integration-Command-Serve.js +51 -0
- package/source/restserver/Meadow-Integration-Server-Endpoints.js +83 -0
- package/source/restserver/Meadow-Integration-Server.js +86 -0
- package/source/restserver/endpoints/Endpoint-CSVCheck.js +91 -0
- package/source/restserver/endpoints/Endpoint-CSVTransform.js +189 -0
- package/source/restserver/endpoints/Endpoint-ComprehensionArray.js +121 -0
- package/source/restserver/endpoints/Endpoint-ComprehensionIntersect.js +166 -0
- package/source/restserver/endpoints/Endpoint-ComprehensionPush.js +209 -0
- package/source/restserver/endpoints/Endpoint-EntityFromTabularFolder.js +252 -0
- package/source/restserver/endpoints/Endpoint-JSONArrayTransform.js +238 -0
- package/source/restserver/endpoints/Endpoint-ObjectArrayToCSV.js +231 -0
- package/source/restserver/endpoints/Endpoint-TSVCheck.js +93 -0
- package/source/restserver/endpoints/Endpoint-TSVTransform.js +191 -0
- package/test/Meadow-Integration-Server_test.js +1170 -0
- package/test/data/test-comprehension-secondary.json +8 -0
- package/test/data/test-comprehension.json +8 -0
- package/test/data/test-small.csv +6 -0
- package/test/data/test-small.json +7 -0
- package/test/data/test-small.tsv +6 -0
|
@@ -32,7 +32,7 @@ class CommandConvertComprehensionToArray extends libCommandLineCommand
|
|
|
32
32
|
const pPropertyPath = pAddressPrefix ? `${pAddressPrefix}.${pKey}` : pKey;
|
|
33
33
|
if (pValue && typeof pValue === 'object' && !Array.isArray(pValue))
|
|
34
34
|
{
|
|
35
|
-
Object.assign(tmpFlattenedObject,
|
|
35
|
+
Object.assign(tmpFlattenedObject, this.flattenObject(pValue, pPropertyPath));
|
|
36
36
|
}
|
|
37
37
|
else
|
|
38
38
|
{
|
|
@@ -117,6 +117,7 @@ class CommandConvertComprehensionToArray extends libCommandLineCommand
|
|
|
117
117
|
if (!this.fable.FilePersistence.existsSync(tmpRawInputFilePath))
|
|
118
118
|
{
|
|
119
119
|
this.fable.log.error(`File [${tmpRawInputFilePath}] does not exist.`);
|
|
120
|
+
return fCallback();
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
const tmpMappingOutcome = {};
|
|
@@ -132,10 +133,10 @@ class CommandConvertComprehensionToArray extends libCommandLineCommand
|
|
|
132
133
|
// Check for the entity
|
|
133
134
|
if ((typeof(tmpMappingOutcome.RawRecordSet) == 'object') && (tmpEntity in tmpMappingOutcome.RawRecordSet))
|
|
134
135
|
{
|
|
135
|
-
this.fable.log.info(`Entity [${tmpEntity} found in the raw recordset comprehension.`);
|
|
136
|
+
this.fable.log.info(`Entity [${tmpEntity}] found in the raw recordset comprehension.`);
|
|
136
137
|
if (!Array.isArray(tmpMappingOutcome.RawRecordSet[tmpEntity]))
|
|
137
138
|
{
|
|
138
|
-
let tmpErrorMessage = `Expected an Array at Entity location in comprehension; data type was: ${typeof(tmpMappingOutcome.RawRecordSet)}`;
|
|
139
|
+
let tmpErrorMessage = `Expected an Array at Entity location in comprehension; data type was: ${typeof(tmpMappingOutcome.RawRecordSet[tmpEntity])}`;
|
|
139
140
|
this.fable.log.error(tmpErrorMessage);
|
|
140
141
|
return fCallback();
|
|
141
142
|
}
|
|
@@ -146,18 +147,42 @@ class CommandConvertComprehensionToArray extends libCommandLineCommand
|
|
|
146
147
|
this.fable.log.info(`Raw recordset is an array.`);
|
|
147
148
|
tmpMappingOutcome.RecordArray = tmpMappingOutcome.RawRecordSet;
|
|
148
149
|
}
|
|
149
|
-
|
|
150
|
-
|
|
150
|
+
}
|
|
151
|
+
else
|
|
152
|
+
{
|
|
153
|
+
// No entity specified -- expect the file to be a plain JSON array
|
|
154
|
+
if (Array.isArray(tmpMappingOutcome.RawRecordSet))
|
|
151
155
|
{
|
|
152
|
-
this.fable.log.
|
|
153
|
-
|
|
156
|
+
this.fable.log.info(`Raw recordset is an array.`);
|
|
157
|
+
tmpMappingOutcome.RecordArray = tmpMappingOutcome.RawRecordSet;
|
|
154
158
|
}
|
|
155
|
-
if (tmpMappingOutcome.
|
|
159
|
+
else if (typeof(tmpMappingOutcome.RawRecordSet) == 'object')
|
|
156
160
|
{
|
|
157
|
-
|
|
158
|
-
|
|
161
|
+
// Auto-detect: if the object has a single key whose value is an object, treat it as a comprehension
|
|
162
|
+
let tmpKeys = Object.keys(tmpMappingOutcome.RawRecordSet);
|
|
163
|
+
if (tmpKeys.length === 1 && typeof(tmpMappingOutcome.RawRecordSet[tmpKeys[0]]) === 'object' && !Array.isArray(tmpMappingOutcome.RawRecordSet[tmpKeys[0]]))
|
|
164
|
+
{
|
|
165
|
+
this.fable.log.info(`Auto-detected entity [${tmpKeys[0]}] from comprehension; converting object values to array.`);
|
|
166
|
+
tmpMappingOutcome.RecordArray = Object.values(tmpMappingOutcome.RawRecordSet[tmpKeys[0]]);
|
|
167
|
+
}
|
|
168
|
+
else if (tmpKeys.length > 0)
|
|
169
|
+
{
|
|
170
|
+
this.fable.log.error(`Input file is an object with keys [${tmpKeys.join(', ')}] but no -e entity flag was provided. Please specify which entity to export with -e.`);
|
|
171
|
+
return fCallback();
|
|
172
|
+
}
|
|
159
173
|
}
|
|
160
174
|
}
|
|
175
|
+
|
|
176
|
+
if (!tmpMappingOutcome.RecordArray || !Array.isArray(tmpMappingOutcome.RecordArray))
|
|
177
|
+
{
|
|
178
|
+
this.fable.log.error(`Could not locate a valid record array in JSON file.`);
|
|
179
|
+
return fCallback();
|
|
180
|
+
}
|
|
181
|
+
if (tmpMappingOutcome.RecordArray.length < 1)
|
|
182
|
+
{
|
|
183
|
+
this.fable.log.error(`No records in the record array.`);
|
|
184
|
+
return fCallback();
|
|
185
|
+
}
|
|
161
186
|
}
|
|
162
187
|
catch (pError)
|
|
163
188
|
{
|
|
@@ -166,28 +191,20 @@ class CommandConvertComprehensionToArray extends libCommandLineCommand
|
|
|
166
191
|
}
|
|
167
192
|
}
|
|
168
193
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
()
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
catch (pError)
|
|
184
|
-
{
|
|
185
|
-
this.pict.log.error(`Error processing file ${tmpRawInputFilePath}:`, pError);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
this.fable.log.info(`CSV File written to [${tmpRawOutputFilePath}].`);
|
|
189
|
-
this.fable.log.info(`Have a nice day!`);
|
|
190
|
-
return fCallback();
|
|
194
|
+
this.streamFlattenedJSONToCSV(tmpMappingOutcome.RecordArray, tmpRawOutputFilePath)
|
|
195
|
+
.then(
|
|
196
|
+
() =>
|
|
197
|
+
{
|
|
198
|
+
this.fable.log.info(`CSV file created successfully: ${tmpRawOutputFilePath}`);
|
|
199
|
+
this.fable.log.info(`Have a nice day!`);
|
|
200
|
+
return fCallback();
|
|
201
|
+
})
|
|
202
|
+
.catch(
|
|
203
|
+
(pError) =>
|
|
204
|
+
{
|
|
205
|
+
this.fable.log.error(`Error generating or writing CSV:`, pError);
|
|
206
|
+
return fCallback();
|
|
207
|
+
});
|
|
191
208
|
};
|
|
192
209
|
}
|
|
193
210
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const libCommandLineCommand = require('pict-service-commandlineutility').ServiceCommandLineCommand;
|
|
2
|
+
|
|
3
|
+
const MeadowIntegrationServer = require('../../restserver/Meadow-Integration-Server.js');
|
|
4
|
+
|
|
5
|
+
class MeadowIntegrationCommandServe extends libCommandLineCommand
|
|
6
|
+
{
|
|
7
|
+
constructor(pFable, pManifest, pServiceHash)
|
|
8
|
+
{
|
|
9
|
+
super(pFable, pManifest, pServiceHash);
|
|
10
|
+
|
|
11
|
+
this.options.CommandKeyword = 'serve';
|
|
12
|
+
this.options.Description = 'Start the Meadow Integration REST API server.';
|
|
13
|
+
this.options.Aliases.push('server');
|
|
14
|
+
this.options.Aliases.push('rest');
|
|
15
|
+
|
|
16
|
+
this.options.CommandOptions.push({ Name: '-p, --port [port]', Description: 'The port to listen on. Defaults to 8086.', Default: '8086' });
|
|
17
|
+
|
|
18
|
+
this.addCommand();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
onRunAsync(fCallback)
|
|
22
|
+
{
|
|
23
|
+
let tmpPort = parseInt(this.CommandOptions.port, 10) || 8086;
|
|
24
|
+
|
|
25
|
+
// Also respect the environment variable
|
|
26
|
+
if (process.env.MEADOW_INTEGRATION_PORT)
|
|
27
|
+
{
|
|
28
|
+
tmpPort = parseInt(process.env.MEADOW_INTEGRATION_PORT, 10) || tmpPort;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.log.info(`Starting Meadow Integration REST server on port ${tmpPort}...`);
|
|
32
|
+
|
|
33
|
+
let tmpServer = new MeadowIntegrationServer(
|
|
34
|
+
{
|
|
35
|
+
APIServerPort: tmpPort
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
tmpServer.start(
|
|
39
|
+
(pError) =>
|
|
40
|
+
{
|
|
41
|
+
if (pError)
|
|
42
|
+
{
|
|
43
|
+
this.log.error(`Failed to start server: ${pError}`, pError);
|
|
44
|
+
return fCallback(pError);
|
|
45
|
+
}
|
|
46
|
+
// Server is running; don't call fCallback so the process stays alive.
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = MeadowIntegrationCommandServe;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meadow Integration Server - Endpoint Registration
|
|
3
|
+
*
|
|
4
|
+
* Registers all REST API endpoints with the Orator service server.
|
|
5
|
+
*
|
|
6
|
+
* Endpoint Summary:
|
|
7
|
+
*
|
|
8
|
+
* --- Status ---
|
|
9
|
+
* GET /1.0/Status Server status and available endpoints
|
|
10
|
+
*
|
|
11
|
+
* --- CSV Operations ---
|
|
12
|
+
* POST /1.0/CSV/Check Analyze a CSV file for statistics
|
|
13
|
+
* POST /1.0/CSV/Transform Transform a CSV file into a comprehension
|
|
14
|
+
*
|
|
15
|
+
* --- TSV Operations ---
|
|
16
|
+
* POST /1.0/TSV/Check Analyze a TSV file for statistics
|
|
17
|
+
* POST /1.0/TSV/Transform Transform a TSV file into a comprehension
|
|
18
|
+
*
|
|
19
|
+
* --- JSON Array Operations ---
|
|
20
|
+
* POST /1.0/JSONArray/Transform Transform a JSON Array file into a comprehension
|
|
21
|
+
* POST /1.0/JSONArray/TransformRecords Transform an in-memory JSON array into a comprehension
|
|
22
|
+
*
|
|
23
|
+
* --- Comprehension Operations ---
|
|
24
|
+
* POST /1.0/Comprehension/Intersect Merge two comprehension objects (in-memory)
|
|
25
|
+
* POST /1.0/Comprehension/IntersectFiles Merge two comprehension files
|
|
26
|
+
* POST /1.0/Comprehension/ToArray Convert comprehension to array (in-memory)
|
|
27
|
+
* POST /1.0/Comprehension/ToArrayFromFile Convert comprehension file to array
|
|
28
|
+
* POST /1.0/Comprehension/ToCSV Convert comprehension/array to CSV (in-memory)
|
|
29
|
+
* POST /1.0/Comprehension/ToCSVFromFile Convert comprehension/array file to CSV
|
|
30
|
+
* POST /1.0/Comprehension/Push Push comprehension to Meadow REST APIs (in-memory)
|
|
31
|
+
* POST /1.0/Comprehension/PushFile Push comprehension file to Meadow REST APIs
|
|
32
|
+
*
|
|
33
|
+
* --- Entity Generation ---
|
|
34
|
+
* POST /1.0/Entity/FromTabularFolder Generate comprehensions from a folder of tabular files
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
module.exports.connectRoutes = function(pFable, pOrator)
|
|
38
|
+
{
|
|
39
|
+
// Status endpoint
|
|
40
|
+
pOrator.serviceServer.get('/1.0/Status',
|
|
41
|
+
(pRequest, pResponse, fNext) =>
|
|
42
|
+
{
|
|
43
|
+
pResponse.send(200,
|
|
44
|
+
{
|
|
45
|
+
Product: pFable.settings.Product,
|
|
46
|
+
Version: pFable.settings.ProductVersion || require('../../package.json').version,
|
|
47
|
+
Status: 'Running',
|
|
48
|
+
Endpoints:
|
|
49
|
+
[
|
|
50
|
+
'POST /1.0/CSV/Check',
|
|
51
|
+
'POST /1.0/CSV/Transform',
|
|
52
|
+
'POST /1.0/TSV/Check',
|
|
53
|
+
'POST /1.0/TSV/Transform',
|
|
54
|
+
'POST /1.0/JSONArray/Transform',
|
|
55
|
+
'POST /1.0/JSONArray/TransformRecords',
|
|
56
|
+
'POST /1.0/Comprehension/Intersect',
|
|
57
|
+
'POST /1.0/Comprehension/IntersectFiles',
|
|
58
|
+
'POST /1.0/Comprehension/ToArray',
|
|
59
|
+
'POST /1.0/Comprehension/ToArrayFromFile',
|
|
60
|
+
'POST /1.0/Comprehension/ToCSV',
|
|
61
|
+
'POST /1.0/Comprehension/ToCSVFromFile',
|
|
62
|
+
'POST /1.0/Comprehension/Push',
|
|
63
|
+
'POST /1.0/Comprehension/PushFile',
|
|
64
|
+
'POST /1.0/Entity/FromTabularFolder'
|
|
65
|
+
]
|
|
66
|
+
});
|
|
67
|
+
return fNext();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Register all endpoint handlers
|
|
71
|
+
require('./endpoints/Endpoint-CSVCheck.js')(pFable, pOrator);
|
|
72
|
+
require('./endpoints/Endpoint-CSVTransform.js')(pFable, pOrator);
|
|
73
|
+
require('./endpoints/Endpoint-TSVCheck.js')(pFable, pOrator);
|
|
74
|
+
require('./endpoints/Endpoint-TSVTransform.js')(pFable, pOrator);
|
|
75
|
+
require('./endpoints/Endpoint-JSONArrayTransform.js')(pFable, pOrator);
|
|
76
|
+
require('./endpoints/Endpoint-ComprehensionIntersect.js')(pFable, pOrator);
|
|
77
|
+
require('./endpoints/Endpoint-ComprehensionArray.js')(pFable, pOrator);
|
|
78
|
+
require('./endpoints/Endpoint-ObjectArrayToCSV.js')(pFable, pOrator);
|
|
79
|
+
require('./endpoints/Endpoint-ComprehensionPush.js')(pFable, pOrator);
|
|
80
|
+
require('./endpoints/Endpoint-EntityFromTabularFolder.js')(pFable, pOrator);
|
|
81
|
+
|
|
82
|
+
pFable.log.info(`Meadow Integration Server: ${15} REST endpoints registered.`);
|
|
83
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const libPict = require('pict');
|
|
2
|
+
const libOrator = require('orator');
|
|
3
|
+
const libOratorServiceServerRestify = require('orator-serviceserver-restify');
|
|
4
|
+
|
|
5
|
+
const libEndpoints = require('./Meadow-Integration-Server-Endpoints.js');
|
|
6
|
+
|
|
7
|
+
class MeadowIntegrationServer
|
|
8
|
+
{
|
|
9
|
+
constructor(pSettings)
|
|
10
|
+
{
|
|
11
|
+
let tmpSettings = Object.assign(
|
|
12
|
+
{
|
|
13
|
+
Product: 'Meadow-Integration-Server',
|
|
14
|
+
ProductVersion: require('../../package.json').version,
|
|
15
|
+
|
|
16
|
+
APIServerPort: 8086,
|
|
17
|
+
|
|
18
|
+
// The default working directory for file operations
|
|
19
|
+
WorkingDirectory: process.cwd()
|
|
20
|
+
}, pSettings);
|
|
21
|
+
|
|
22
|
+
this._Settings = tmpSettings;
|
|
23
|
+
|
|
24
|
+
// Use Pict instead of Fable so parseTemplate and ExpressionParser are available
|
|
25
|
+
this._Fable = new libPict(tmpSettings);
|
|
26
|
+
|
|
27
|
+
// Register the Restify service server type
|
|
28
|
+
this._Fable.serviceManager.addServiceType('OratorServiceServer', libOratorServiceServerRestify);
|
|
29
|
+
|
|
30
|
+
// Create the Orator service
|
|
31
|
+
this._Orator = new libOrator(this._Fable, {});
|
|
32
|
+
|
|
33
|
+
// Initialize core fable services that the endpoints rely on
|
|
34
|
+
this._Fable.instantiateServiceProvider('CSVParser');
|
|
35
|
+
this._Fable.instantiateServiceProvider('FilePersistence');
|
|
36
|
+
this._Fable.instantiateServiceProvider('DataGeneration');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
start(fCallback)
|
|
40
|
+
{
|
|
41
|
+
let tmpCallback = (typeof(fCallback) === 'function') ? fCallback : () => {};
|
|
42
|
+
|
|
43
|
+
this._Orator.initialize(
|
|
44
|
+
(pError) =>
|
|
45
|
+
{
|
|
46
|
+
if (pError)
|
|
47
|
+
{
|
|
48
|
+
this._Fable.log.error(`Error initializing Orator: ${pError}`, pError);
|
|
49
|
+
return tmpCallback(pError);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Register all endpoints
|
|
53
|
+
libEndpoints.connectRoutes(this._Fable, this._Orator);
|
|
54
|
+
|
|
55
|
+
this._Orator.startService(
|
|
56
|
+
(pStartError) =>
|
|
57
|
+
{
|
|
58
|
+
if (pStartError)
|
|
59
|
+
{
|
|
60
|
+
this._Fable.log.error(`Error starting Orator service: ${pStartError}`, pStartError);
|
|
61
|
+
return tmpCallback(pStartError);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this._Fable.log.info(`Meadow Integration Server running on port ${this._Settings.APIServerPort}`);
|
|
65
|
+
return tmpCallback();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
stop(fCallback)
|
|
71
|
+
{
|
|
72
|
+
let tmpCallback = (typeof(fCallback) === 'function') ? fCallback : () => {};
|
|
73
|
+
|
|
74
|
+
this._Orator.stopService(
|
|
75
|
+
(pError) =>
|
|
76
|
+
{
|
|
77
|
+
if (pError)
|
|
78
|
+
{
|
|
79
|
+
this._Fable.log.error(`Error stopping Orator service: ${pError}`, pError);
|
|
80
|
+
}
|
|
81
|
+
return tmpCallback(pError);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = MeadowIntegrationServer;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const libFS = require('fs');
|
|
2
|
+
const libPath = require('path');
|
|
3
|
+
const libReadline = require('readline');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* POST /1.0/CSV/Check
|
|
7
|
+
*
|
|
8
|
+
* Analyze a CSV file for statistics.
|
|
9
|
+
*
|
|
10
|
+
* Request body (JSON):
|
|
11
|
+
* {
|
|
12
|
+
* "File": "/absolute/path/to/file.csv", // The CSV file to analyze
|
|
13
|
+
* "Records": false, // (optional) Include full record dump
|
|
14
|
+
* "QuoteDelimiter": "\"" // (optional) Quote delimiter character
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* Response: JSON statistics object
|
|
18
|
+
*/
|
|
19
|
+
module.exports = function(pFable, pOrator)
|
|
20
|
+
{
|
|
21
|
+
pOrator.serviceServer.postWithBodyParser('/1.0/CSV/Check',
|
|
22
|
+
(pRequest, pResponse, fNext) =>
|
|
23
|
+
{
|
|
24
|
+
let tmpBody = pRequest.body || {};
|
|
25
|
+
|
|
26
|
+
if (!tmpBody.File || (typeof(tmpBody.File) !== 'string'))
|
|
27
|
+
{
|
|
28
|
+
pResponse.send(400, { Error: 'No valid File path provided in request body.' });
|
|
29
|
+
return fNext();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pFable.instantiateServiceProvider('FilePersistence');
|
|
33
|
+
pFable.addAndInstantiateServiceTypeIfNotExists('MeadowIntegrationTabularCheck', require('../../services/tabular/Service-TabularCheck.js'));
|
|
34
|
+
|
|
35
|
+
let tmpInputFilePath = pFable.FilePersistence.resolvePath(tmpBody.File);
|
|
36
|
+
|
|
37
|
+
if (!pFable.FilePersistence.existsSync(tmpInputFilePath))
|
|
38
|
+
{
|
|
39
|
+
pResponse.send(404, { Error: `File [${tmpInputFilePath}] does not exist.` });
|
|
40
|
+
return fNext();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Create a fresh CSVParser for each request to reset header state
|
|
44
|
+
let tmpCSVParser = pFable.instantiateServiceProviderWithoutRegistration('CSVParser');
|
|
45
|
+
|
|
46
|
+
if (tmpBody.QuoteDelimiter)
|
|
47
|
+
{
|
|
48
|
+
tmpCSVParser.QuoteCharacter = tmpBody.QuoteDelimiter;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let tmpStatistics = pFable.MeadowIntegrationTabularCheck.newStatisticsObject(tmpInputFilePath);
|
|
52
|
+
let tmpStoreFullRecord = (tmpBody.Records === true);
|
|
53
|
+
|
|
54
|
+
if (tmpStoreFullRecord)
|
|
55
|
+
{
|
|
56
|
+
tmpStatistics.Records = [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tmpReadline = libReadline.createInterface(
|
|
60
|
+
{
|
|
61
|
+
input: libFS.createReadStream(tmpInputFilePath),
|
|
62
|
+
crlfDelay: Infinity,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
tmpReadline.on('line',
|
|
66
|
+
(pLine) =>
|
|
67
|
+
{
|
|
68
|
+
const tmpRecord = tmpCSVParser.parseCSVLine(pLine);
|
|
69
|
+
if (tmpRecord)
|
|
70
|
+
{
|
|
71
|
+
pFable.MeadowIntegrationTabularCheck.collectStatistics(tmpRecord, tmpStatistics, tmpStoreFullRecord);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
tmpReadline.on('close',
|
|
76
|
+
() =>
|
|
77
|
+
{
|
|
78
|
+
pFable.log.info(`CSV Check: ${tmpStatistics.RowCount} rows, ${tmpStatistics.ColumnCount} columns in [${tmpInputFilePath}].`);
|
|
79
|
+
pResponse.send(200, tmpStatistics);
|
|
80
|
+
return fNext();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
tmpReadline.on('error',
|
|
84
|
+
(pError) =>
|
|
85
|
+
{
|
|
86
|
+
pFable.log.error(`CSV Check error reading file [${tmpInputFilePath}]: ${pError}`, pError);
|
|
87
|
+
pResponse.send(500, { Error: `Error reading CSV file: ${pError.message}` });
|
|
88
|
+
return fNext();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
const libFS = require('fs');
|
|
2
|
+
const libPath = require('path');
|
|
3
|
+
const libReadline = require('readline');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* POST /1.0/CSV/Transform
|
|
7
|
+
*
|
|
8
|
+
* Transform a CSV file into a comprehension.
|
|
9
|
+
*
|
|
10
|
+
* Request body (JSON):
|
|
11
|
+
* {
|
|
12
|
+
* "File": "/absolute/path/to/file.csv", // The CSV file to transform
|
|
13
|
+
* "Entity": "MyEntity", // (optional) Entity name
|
|
14
|
+
* "GUIDName": "GUIDMyEntity", // (optional) GUID column name
|
|
15
|
+
* "GUIDTemplate": "{~D:Record.id~}", // (optional) Pict template for GUID
|
|
16
|
+
* "Mappings": { "Col1": "{~D:Record.col1~}" }, // (optional) Column mappings object
|
|
17
|
+
* "MappingConfiguration": { ... }, // (optional) Full explicit mapping config
|
|
18
|
+
* "IncomingComprehension": { ... }, // (optional) Existing comprehension to merge into
|
|
19
|
+
* "Extended": false, // (optional) Return full operation state
|
|
20
|
+
* "QuoteDelimiter": "\"" // (optional) Quote delimiter character
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* Response: Comprehension JSON (or extended state if Extended=true)
|
|
24
|
+
*/
|
|
25
|
+
module.exports = function(pFable, pOrator)
|
|
26
|
+
{
|
|
27
|
+
pOrator.serviceServer.postWithBodyParser('/1.0/CSV/Transform',
|
|
28
|
+
(pRequest, pResponse, fNext) =>
|
|
29
|
+
{
|
|
30
|
+
let tmpBody = pRequest.body || {};
|
|
31
|
+
|
|
32
|
+
if (!tmpBody.File || (typeof(tmpBody.File) !== 'string'))
|
|
33
|
+
{
|
|
34
|
+
pResponse.send(400, { Error: 'No valid File path provided in request body.' });
|
|
35
|
+
return fNext();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pFable.instantiateServiceProvider('FilePersistence');
|
|
39
|
+
pFable.addAndInstantiateServiceTypeIfNotExists('MeadowIntegrationTabularTransform', require('../../services/tabular/Service-TabularTransform.js'));
|
|
40
|
+
|
|
41
|
+
let tmpInputFilePath = pFable.FilePersistence.resolvePath(tmpBody.File);
|
|
42
|
+
|
|
43
|
+
if (!pFable.FilePersistence.existsSync(tmpInputFilePath))
|
|
44
|
+
{
|
|
45
|
+
pResponse.send(404, { Error: `File [${tmpInputFilePath}] does not exist.` });
|
|
46
|
+
return fNext();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create a fresh CSVParser for each request to reset header state
|
|
50
|
+
let tmpCSVParser = pFable.instantiateServiceProviderWithoutRegistration('CSVParser');
|
|
51
|
+
|
|
52
|
+
if (tmpBody.QuoteDelimiter)
|
|
53
|
+
{
|
|
54
|
+
tmpCSVParser.QuoteCharacter = tmpBody.QuoteDelimiter;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let tmpMappingOutcome = pFable.MeadowIntegrationTabularTransform.newMappingOutcomeObject();
|
|
58
|
+
|
|
59
|
+
// Apply user configuration from request body
|
|
60
|
+
if (tmpBody.Entity)
|
|
61
|
+
{
|
|
62
|
+
tmpMappingOutcome.UserConfiguration.Entity = tmpBody.Entity;
|
|
63
|
+
}
|
|
64
|
+
if (tmpBody.GUIDName)
|
|
65
|
+
{
|
|
66
|
+
tmpMappingOutcome.UserConfiguration.GUIDName = tmpBody.GUIDName;
|
|
67
|
+
}
|
|
68
|
+
if (tmpBody.GUIDTemplate)
|
|
69
|
+
{
|
|
70
|
+
tmpMappingOutcome.UserConfiguration.GUIDTemplate = tmpBody.GUIDTemplate;
|
|
71
|
+
}
|
|
72
|
+
if (tmpBody.Mappings && (typeof(tmpBody.Mappings) === 'object'))
|
|
73
|
+
{
|
|
74
|
+
tmpMappingOutcome.UserConfiguration.Mappings = tmpBody.Mappings;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Apply explicit mapping configuration
|
|
78
|
+
if (tmpBody.MappingConfiguration && (typeof(tmpBody.MappingConfiguration) === 'object'))
|
|
79
|
+
{
|
|
80
|
+
tmpMappingOutcome.ExplicitConfiguration = tmpBody.MappingConfiguration;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Apply incoming comprehension
|
|
84
|
+
if (tmpBody.IncomingComprehension && (typeof(tmpBody.IncomingComprehension) === 'object'))
|
|
85
|
+
{
|
|
86
|
+
tmpMappingOutcome.ExistingComprehension = tmpBody.IncomingComprehension;
|
|
87
|
+
tmpMappingOutcome.Comprehension = JSON.parse(JSON.stringify(tmpBody.IncomingComprehension));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const tmpReadline = libReadline.createInterface(
|
|
91
|
+
{
|
|
92
|
+
input: libFS.createReadStream(tmpInputFilePath),
|
|
93
|
+
crlfDelay: Infinity,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
tmpReadline.on('line',
|
|
97
|
+
(pLine) =>
|
|
98
|
+
{
|
|
99
|
+
const tmpIncomingRecord = tmpCSVParser.parseCSVLine(pLine);
|
|
100
|
+
tmpMappingOutcome.ParsedRowCount++;
|
|
101
|
+
|
|
102
|
+
if (tmpIncomingRecord)
|
|
103
|
+
{
|
|
104
|
+
if (!tmpMappingOutcome.ImplicitConfiguration)
|
|
105
|
+
{
|
|
106
|
+
tmpMappingOutcome.ImplicitConfiguration = pFable.MeadowIntegrationTabularTransform.generateMappingConfigurationPrototype(libPath.basename(tmpInputFilePath), tmpIncomingRecord);
|
|
107
|
+
|
|
108
|
+
if ((!tmpMappingOutcome.ExplicitConfiguration) || (typeof(tmpMappingOutcome.ExplicitConfiguration) != 'object'))
|
|
109
|
+
{
|
|
110
|
+
tmpMappingOutcome.Configuration = Object.assign({}, tmpMappingOutcome.ImplicitConfiguration, tmpMappingOutcome.UserConfiguration);
|
|
111
|
+
}
|
|
112
|
+
else
|
|
113
|
+
{
|
|
114
|
+
tmpMappingOutcome.Configuration = Object.assign({}, tmpMappingOutcome.ImplicitConfiguration, tmpMappingOutcome.ExplicitConfiguration, tmpMappingOutcome.UserConfiguration);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!('GUIDName' in tmpMappingOutcome.Configuration))
|
|
118
|
+
{
|
|
119
|
+
tmpMappingOutcome.Configuration.GUIDName = `GUID${tmpMappingOutcome.Configuration.Entity}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!(tmpMappingOutcome.Configuration.Entity in tmpMappingOutcome.Comprehension))
|
|
123
|
+
{
|
|
124
|
+
tmpMappingOutcome.Comprehension[tmpMappingOutcome.Configuration.Entity] = {};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let tmpMappingRecordSolution = (
|
|
129
|
+
{
|
|
130
|
+
IncomingRecord: tmpIncomingRecord,
|
|
131
|
+
MappingConfiguration: tmpMappingOutcome.Configuration,
|
|
132
|
+
MappingOutcome: tmpMappingOutcome,
|
|
133
|
+
RowIndex: tmpMappingOutcome.ParsedRowCount,
|
|
134
|
+
NewRecordsGUIDUniqueness: [],
|
|
135
|
+
NewRecordPrototype: {},
|
|
136
|
+
Fable: pFable,
|
|
137
|
+
Pict: pFable,
|
|
138
|
+
AppData: pFable.AppData
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Run the solvers for this record
|
|
142
|
+
let tmpSolverResultsObject = {};
|
|
143
|
+
if (tmpMappingOutcome.Configuration.Solvers && Array.isArray(tmpMappingOutcome.Configuration.Solvers))
|
|
144
|
+
{
|
|
145
|
+
for (let i = 0; i < tmpMappingOutcome.Configuration.Solvers.length; i++)
|
|
146
|
+
{
|
|
147
|
+
let tmpSolver = tmpMappingOutcome.Configuration.Solvers[i];
|
|
148
|
+
pFable.ExpressionParser.solve(tmpSolver, tmpMappingRecordSolution, tmpSolverResultsObject, pFable.manifest, tmpMappingRecordSolution);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (tmpMappingOutcome.Configuration.MultipleGUIDUniqueness && tmpMappingRecordSolution.NewRecordsGUIDUniqueness.length > 0)
|
|
153
|
+
{
|
|
154
|
+
for (let i = 0; i < tmpMappingRecordSolution.NewRecordsGUIDUniqueness.length; i++)
|
|
155
|
+
{
|
|
156
|
+
pFable.MeadowIntegrationTabularTransform.addRecordToComprehension(tmpIncomingRecord, tmpMappingOutcome, tmpMappingRecordSolution.NewRecordPrototype, tmpMappingRecordSolution.NewRecordsGUIDUniqueness[i]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else if (!tmpMappingOutcome.Configuration.MultipleGUIDUniqueness)
|
|
160
|
+
{
|
|
161
|
+
pFable.MeadowIntegrationTabularTransform.addRecordToComprehension(tmpIncomingRecord, tmpMappingOutcome, tmpMappingRecordSolution.NewRecordPrototype);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
tmpReadline.on('close',
|
|
167
|
+
() =>
|
|
168
|
+
{
|
|
169
|
+
pFable.log.info(`CSV Transform: Parsed ${tmpMappingOutcome.ParsedRowCount} rows from [${tmpInputFilePath}].`);
|
|
170
|
+
if (tmpBody.Extended)
|
|
171
|
+
{
|
|
172
|
+
pResponse.send(200, tmpMappingOutcome);
|
|
173
|
+
}
|
|
174
|
+
else
|
|
175
|
+
{
|
|
176
|
+
pResponse.send(200, tmpMappingOutcome.Comprehension);
|
|
177
|
+
}
|
|
178
|
+
return fNext();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
tmpReadline.on('error',
|
|
182
|
+
(pError) =>
|
|
183
|
+
{
|
|
184
|
+
pFable.log.error(`CSV Transform error reading file [${tmpInputFilePath}]: ${pError}`, pError);
|
|
185
|
+
pResponse.send(500, { Error: `Error reading CSV file: ${pError.message}` });
|
|
186
|
+
return fNext();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
};
|