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
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /1.0/Comprehension/ToCSV
|
|
3
|
+
*
|
|
4
|
+
* Convert a comprehension or array of objects to CSV format.
|
|
5
|
+
*
|
|
6
|
+
* Request body (JSON):
|
|
7
|
+
* {
|
|
8
|
+
* "Records": [ {...}, {...}, ... ], // Array of record objects to convert
|
|
9
|
+
* "Entity": "MyEntity" // (optional) Entity name if passing a comprehension
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* OR comprehension-based:
|
|
13
|
+
* {
|
|
14
|
+
* "Comprehension": { ... }, // Comprehension object
|
|
15
|
+
* "Entity": "MyEntity" // (optional) Entity; auto-inferred if single
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* OR file-based:
|
|
19
|
+
*
|
|
20
|
+
* POST /1.0/Comprehension/ToCSVFromFile
|
|
21
|
+
* {
|
|
22
|
+
* "File": "/path/to/records.json", // JSON file (array or comprehension)
|
|
23
|
+
* "Entity": "MyEntity" // (optional) Entity name
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* Response: CSV text (Content-Type: text/csv)
|
|
27
|
+
*/
|
|
28
|
+
module.exports = function(pFable, pOrator)
|
|
29
|
+
{
|
|
30
|
+
// In-memory conversion
|
|
31
|
+
pOrator.serviceServer.postWithBodyParser('/1.0/Comprehension/ToCSV',
|
|
32
|
+
(pRequest, pResponse, fNext) =>
|
|
33
|
+
{
|
|
34
|
+
let tmpBody = pRequest.body || {};
|
|
35
|
+
|
|
36
|
+
let tmpRecordArray = extractRecordArray(pFable, tmpBody);
|
|
37
|
+
if (tmpRecordArray.Error)
|
|
38
|
+
{
|
|
39
|
+
pResponse.send(400, tmpRecordArray);
|
|
40
|
+
return fNext();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let tmpCSV = generateCSV(tmpRecordArray.Records);
|
|
44
|
+
pResponse.setHeader('Content-Type', 'text/csv');
|
|
45
|
+
pResponse.setHeader('Content-Disposition', 'attachment; filename="export.csv"');
|
|
46
|
+
pResponse.sendRaw(200, tmpCSV);
|
|
47
|
+
return fNext();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// File-based conversion
|
|
51
|
+
pOrator.serviceServer.postWithBodyParser('/1.0/Comprehension/ToCSVFromFile',
|
|
52
|
+
(pRequest, pResponse, fNext) =>
|
|
53
|
+
{
|
|
54
|
+
let tmpBody = pRequest.body || {};
|
|
55
|
+
|
|
56
|
+
if (!tmpBody.File || (typeof(tmpBody.File) !== 'string'))
|
|
57
|
+
{
|
|
58
|
+
pResponse.send(400, { Error: 'No valid File path provided in request body.' });
|
|
59
|
+
return fNext();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pFable.instantiateServiceProvider('FilePersistence');
|
|
63
|
+
|
|
64
|
+
let tmpFilePath = pFable.FilePersistence.resolvePath(tmpBody.File);
|
|
65
|
+
|
|
66
|
+
if (!pFable.FilePersistence.existsSync(tmpFilePath))
|
|
67
|
+
{
|
|
68
|
+
pResponse.send(404, { Error: `File [${tmpFilePath}] does not exist.` });
|
|
69
|
+
return fNext();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let tmpRawRecordSet;
|
|
73
|
+
try
|
|
74
|
+
{
|
|
75
|
+
tmpRawRecordSet = JSON.parse(pFable.FilePersistence.readFileSync(tmpFilePath));
|
|
76
|
+
}
|
|
77
|
+
catch (pError)
|
|
78
|
+
{
|
|
79
|
+
pResponse.send(400, { Error: `Error parsing JSON file: ${pError.message}` });
|
|
80
|
+
return fNext();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Build a body-like object for extraction
|
|
84
|
+
let tmpExtractBody = { Entity: tmpBody.Entity };
|
|
85
|
+
if (Array.isArray(tmpRawRecordSet))
|
|
86
|
+
{
|
|
87
|
+
tmpExtractBody.Records = tmpRawRecordSet;
|
|
88
|
+
}
|
|
89
|
+
else if (typeof(tmpRawRecordSet) === 'object')
|
|
90
|
+
{
|
|
91
|
+
tmpExtractBody.Comprehension = tmpRawRecordSet;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let tmpRecordArray = extractRecordArray(pFable, tmpExtractBody);
|
|
95
|
+
if (tmpRecordArray.Error)
|
|
96
|
+
{
|
|
97
|
+
pResponse.send(400, tmpRecordArray);
|
|
98
|
+
return fNext();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let tmpCSV = generateCSV(tmpRecordArray.Records);
|
|
102
|
+
pResponse.setHeader('Content-Type', 'text/csv');
|
|
103
|
+
pResponse.setHeader('Content-Disposition', 'attachment; filename="export.csv"');
|
|
104
|
+
pResponse.sendRaw(200, tmpCSV);
|
|
105
|
+
return fNext();
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function extractRecordArray(pFable, pBody)
|
|
110
|
+
{
|
|
111
|
+
// Direct array
|
|
112
|
+
if (pBody.Records && Array.isArray(pBody.Records))
|
|
113
|
+
{
|
|
114
|
+
if (pBody.Records.length < 1)
|
|
115
|
+
{
|
|
116
|
+
return { Error: 'Records array is empty.' };
|
|
117
|
+
}
|
|
118
|
+
return { Records: pBody.Records };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Comprehension
|
|
122
|
+
if (pBody.Comprehension && (typeof(pBody.Comprehension) === 'object'))
|
|
123
|
+
{
|
|
124
|
+
let tmpEntity = pBody.Entity;
|
|
125
|
+
|
|
126
|
+
if (tmpEntity)
|
|
127
|
+
{
|
|
128
|
+
if (pBody.Comprehension[tmpEntity])
|
|
129
|
+
{
|
|
130
|
+
let tmpRecords;
|
|
131
|
+
if (Array.isArray(pBody.Comprehension[tmpEntity]))
|
|
132
|
+
{
|
|
133
|
+
tmpRecords = pBody.Comprehension[tmpEntity];
|
|
134
|
+
}
|
|
135
|
+
else
|
|
136
|
+
{
|
|
137
|
+
tmpRecords = Object.values(pBody.Comprehension[tmpEntity]);
|
|
138
|
+
}
|
|
139
|
+
if (tmpRecords.length < 1)
|
|
140
|
+
{
|
|
141
|
+
return { Error: 'No records found for the specified entity.' };
|
|
142
|
+
}
|
|
143
|
+
return { Records: tmpRecords };
|
|
144
|
+
}
|
|
145
|
+
else
|
|
146
|
+
{
|
|
147
|
+
return { Error: `Entity [${tmpEntity}] not found in comprehension.` };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else
|
|
151
|
+
{
|
|
152
|
+
// Auto-detect single entity
|
|
153
|
+
let tmpKeys = Object.keys(pBody.Comprehension);
|
|
154
|
+
if (tmpKeys.length === 1 && typeof(pBody.Comprehension[tmpKeys[0]]) === 'object' && !Array.isArray(pBody.Comprehension[tmpKeys[0]]))
|
|
155
|
+
{
|
|
156
|
+
pFable.log.info(`Auto-detected entity [${tmpKeys[0]}] from comprehension.`);
|
|
157
|
+
let tmpRecords = Object.values(pBody.Comprehension[tmpKeys[0]]);
|
|
158
|
+
if (tmpRecords.length < 1)
|
|
159
|
+
{
|
|
160
|
+
return { Error: 'No records found in the auto-detected entity.' };
|
|
161
|
+
}
|
|
162
|
+
return { Records: tmpRecords };
|
|
163
|
+
}
|
|
164
|
+
else if (tmpKeys.length > 1)
|
|
165
|
+
{
|
|
166
|
+
return { Error: `Multiple entities found [${tmpKeys.join(', ')}]. Please specify an Entity.` };
|
|
167
|
+
}
|
|
168
|
+
else
|
|
169
|
+
{
|
|
170
|
+
return { Error: 'No entities found in the comprehension.' };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { Error: 'No valid Records array or Comprehension object provided.' };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function flattenObject(pObject, pAddressPrefix)
|
|
179
|
+
{
|
|
180
|
+
let tmpPrefix = pAddressPrefix || '';
|
|
181
|
+
let tmpFlattenedObject = {};
|
|
182
|
+
for (const [pKey, pValue] of Object.entries(pObject))
|
|
183
|
+
{
|
|
184
|
+
const pPropertyPath = tmpPrefix ? `${tmpPrefix}.${pKey}` : pKey;
|
|
185
|
+
if (pValue && typeof pValue === 'object' && !Array.isArray(pValue))
|
|
186
|
+
{
|
|
187
|
+
Object.assign(tmpFlattenedObject, flattenObject(pValue, pPropertyPath));
|
|
188
|
+
}
|
|
189
|
+
else
|
|
190
|
+
{
|
|
191
|
+
tmpFlattenedObject[pPropertyPath] = pValue;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return tmpFlattenedObject;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function escapeCSVValue(pValue)
|
|
198
|
+
{
|
|
199
|
+
if (pValue === null || pValue === undefined) return '';
|
|
200
|
+
const str = String(pValue);
|
|
201
|
+
return /[",\n]/.test(str) ? `"${str.replace(/"/g, '""')}"` : str;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function generateCSV(pRecords)
|
|
205
|
+
{
|
|
206
|
+
const tmpAllKeysSet = new Set();
|
|
207
|
+
const tmpFlattenedRecords = [];
|
|
208
|
+
|
|
209
|
+
// First pass: flatten and collect all keys
|
|
210
|
+
for (const tmpRecord of pRecords)
|
|
211
|
+
{
|
|
212
|
+
const tmpFlattenedObject = flattenObject(tmpRecord);
|
|
213
|
+
tmpFlattenedRecords.push(tmpFlattenedObject);
|
|
214
|
+
for (const tmpKey of Object.keys(tmpFlattenedObject))
|
|
215
|
+
{
|
|
216
|
+
tmpAllKeysSet.add(tmpKey);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const tmpAllObjectKeys = Array.from(tmpAllKeysSet).sort();
|
|
221
|
+
|
|
222
|
+
// Build CSV string
|
|
223
|
+
let tmpCSV = tmpAllObjectKeys.join(',') + '\n';
|
|
224
|
+
for (const tmpFlatRecord of tmpFlattenedRecords)
|
|
225
|
+
{
|
|
226
|
+
const tmpRow = tmpAllObjectKeys.map((pKey) => escapeCSVValue(tmpFlatRecord[pKey])).join(',');
|
|
227
|
+
tmpCSV += tmpRow + '\n';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return tmpCSV;
|
|
231
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const libFS = require('fs');
|
|
2
|
+
const libPath = require('path');
|
|
3
|
+
const libReadline = require('readline');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* POST /1.0/TSV/Check
|
|
7
|
+
*
|
|
8
|
+
* Analyze a TSV file for statistics.
|
|
9
|
+
*
|
|
10
|
+
* Request body (JSON):
|
|
11
|
+
* {
|
|
12
|
+
* "File": "/absolute/path/to/file.tsv", // The TSV 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/TSV/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
|
+
// Create a fresh CSVParser for each request to reset header state
|
|
36
|
+
let tmpCSVParser = pFable.instantiateServiceProviderWithoutRegistration('CSVParser');
|
|
37
|
+
|
|
38
|
+
// Configure for TSV
|
|
39
|
+
tmpCSVParser.Delimiter = '\t';
|
|
40
|
+
if (tmpBody.QuoteDelimiter)
|
|
41
|
+
{
|
|
42
|
+
tmpCSVParser.QuoteCharacter = tmpBody.QuoteDelimiter;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let tmpInputFilePath = pFable.FilePersistence.resolvePath(tmpBody.File);
|
|
46
|
+
|
|
47
|
+
if (!pFable.FilePersistence.existsSync(tmpInputFilePath))
|
|
48
|
+
{
|
|
49
|
+
pResponse.send(404, { Error: `File [${tmpInputFilePath}] does not exist.` });
|
|
50
|
+
return fNext();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let tmpStatistics = pFable.MeadowIntegrationTabularCheck.newStatisticsObject(tmpInputFilePath);
|
|
54
|
+
let tmpStoreFullRecord = (tmpBody.Records === true);
|
|
55
|
+
|
|
56
|
+
if (tmpStoreFullRecord)
|
|
57
|
+
{
|
|
58
|
+
tmpStatistics.Records = [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const tmpReadline = libReadline.createInterface(
|
|
62
|
+
{
|
|
63
|
+
input: libFS.createReadStream(tmpInputFilePath),
|
|
64
|
+
crlfDelay: Infinity,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
tmpReadline.on('line',
|
|
68
|
+
(pLine) =>
|
|
69
|
+
{
|
|
70
|
+
const tmpRecord = tmpCSVParser.parseCSVLine(pLine);
|
|
71
|
+
if (tmpRecord)
|
|
72
|
+
{
|
|
73
|
+
pFable.MeadowIntegrationTabularCheck.collectStatistics(tmpRecord, tmpStatistics, tmpStoreFullRecord);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
tmpReadline.on('close',
|
|
78
|
+
() =>
|
|
79
|
+
{
|
|
80
|
+
pFable.log.info(`TSV Check: ${tmpStatistics.RowCount} rows, ${tmpStatistics.ColumnCount} columns in [${tmpInputFilePath}].`);
|
|
81
|
+
pResponse.send(200, tmpStatistics);
|
|
82
|
+
return fNext();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
tmpReadline.on('error',
|
|
86
|
+
(pError) =>
|
|
87
|
+
{
|
|
88
|
+
pFable.log.error(`TSV Check error reading file [${tmpInputFilePath}]: ${pError}`, pError);
|
|
89
|
+
pResponse.send(500, { Error: `Error reading TSV file: ${pError.message}` });
|
|
90
|
+
return fNext();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const libFS = require('fs');
|
|
2
|
+
const libPath = require('path');
|
|
3
|
+
const libReadline = require('readline');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* POST /1.0/TSV/Transform
|
|
7
|
+
*
|
|
8
|
+
* Transform a TSV file into a comprehension.
|
|
9
|
+
*
|
|
10
|
+
* Request body (JSON):
|
|
11
|
+
* {
|
|
12
|
+
* "File": "/absolute/path/to/file.tsv", // The TSV 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/TSV/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
|
+
// Create a fresh CSVParser for each request to reset header state
|
|
42
|
+
let tmpCSVParser = pFable.instantiateServiceProviderWithoutRegistration('CSVParser');
|
|
43
|
+
|
|
44
|
+
// Configure for TSV
|
|
45
|
+
tmpCSVParser.Delimiter = '\t';
|
|
46
|
+
if (tmpBody.QuoteDelimiter)
|
|
47
|
+
{
|
|
48
|
+
tmpCSVParser.QuoteCharacter = tmpBody.QuoteDelimiter;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let tmpInputFilePath = pFable.FilePersistence.resolvePath(tmpBody.File);
|
|
52
|
+
|
|
53
|
+
if (!pFable.FilePersistence.existsSync(tmpInputFilePath))
|
|
54
|
+
{
|
|
55
|
+
pResponse.send(404, { Error: `File [${tmpInputFilePath}] does not exist.` });
|
|
56
|
+
return fNext();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let tmpMappingOutcome = pFable.MeadowIntegrationTabularTransform.newMappingOutcomeObject();
|
|
60
|
+
|
|
61
|
+
// Apply user configuration from request body
|
|
62
|
+
if (tmpBody.Entity)
|
|
63
|
+
{
|
|
64
|
+
tmpMappingOutcome.UserConfiguration.Entity = tmpBody.Entity;
|
|
65
|
+
}
|
|
66
|
+
if (tmpBody.GUIDName)
|
|
67
|
+
{
|
|
68
|
+
tmpMappingOutcome.UserConfiguration.GUIDName = tmpBody.GUIDName;
|
|
69
|
+
}
|
|
70
|
+
if (tmpBody.GUIDTemplate)
|
|
71
|
+
{
|
|
72
|
+
tmpMappingOutcome.UserConfiguration.GUIDTemplate = tmpBody.GUIDTemplate;
|
|
73
|
+
}
|
|
74
|
+
if (tmpBody.Mappings && (typeof(tmpBody.Mappings) === 'object'))
|
|
75
|
+
{
|
|
76
|
+
tmpMappingOutcome.UserConfiguration.Mappings = tmpBody.Mappings;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Apply explicit mapping configuration
|
|
80
|
+
if (tmpBody.MappingConfiguration && (typeof(tmpBody.MappingConfiguration) === 'object'))
|
|
81
|
+
{
|
|
82
|
+
tmpMappingOutcome.ExplicitConfiguration = tmpBody.MappingConfiguration;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Apply incoming comprehension
|
|
86
|
+
if (tmpBody.IncomingComprehension && (typeof(tmpBody.IncomingComprehension) === 'object'))
|
|
87
|
+
{
|
|
88
|
+
tmpMappingOutcome.ExistingComprehension = tmpBody.IncomingComprehension;
|
|
89
|
+
tmpMappingOutcome.Comprehension = JSON.parse(JSON.stringify(tmpBody.IncomingComprehension));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const tmpReadline = libReadline.createInterface(
|
|
93
|
+
{
|
|
94
|
+
input: libFS.createReadStream(tmpInputFilePath),
|
|
95
|
+
crlfDelay: Infinity,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
tmpReadline.on('line',
|
|
99
|
+
(pLine) =>
|
|
100
|
+
{
|
|
101
|
+
const tmpIncomingRecord = tmpCSVParser.parseCSVLine(pLine);
|
|
102
|
+
tmpMappingOutcome.ParsedRowCount++;
|
|
103
|
+
|
|
104
|
+
if (tmpIncomingRecord)
|
|
105
|
+
{
|
|
106
|
+
if (!tmpMappingOutcome.ImplicitConfiguration)
|
|
107
|
+
{
|
|
108
|
+
tmpMappingOutcome.ImplicitConfiguration = pFable.MeadowIntegrationTabularTransform.generateMappingConfigurationPrototype(libPath.basename(tmpInputFilePath), tmpIncomingRecord);
|
|
109
|
+
|
|
110
|
+
if ((!tmpMappingOutcome.ExplicitConfiguration) || (typeof(tmpMappingOutcome.ExplicitConfiguration) != 'object'))
|
|
111
|
+
{
|
|
112
|
+
tmpMappingOutcome.Configuration = Object.assign({}, tmpMappingOutcome.ImplicitConfiguration, tmpMappingOutcome.UserConfiguration);
|
|
113
|
+
}
|
|
114
|
+
else
|
|
115
|
+
{
|
|
116
|
+
tmpMappingOutcome.Configuration = Object.assign({}, tmpMappingOutcome.ImplicitConfiguration, tmpMappingOutcome.ExplicitConfiguration, tmpMappingOutcome.UserConfiguration);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!('GUIDName' in tmpMappingOutcome.Configuration))
|
|
120
|
+
{
|
|
121
|
+
tmpMappingOutcome.Configuration.GUIDName = `GUID${tmpMappingOutcome.Configuration.Entity}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!(tmpMappingOutcome.Configuration.Entity in tmpMappingOutcome.Comprehension))
|
|
125
|
+
{
|
|
126
|
+
tmpMappingOutcome.Comprehension[tmpMappingOutcome.Configuration.Entity] = {};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let tmpMappingRecordSolution = (
|
|
131
|
+
{
|
|
132
|
+
IncomingRecord: tmpIncomingRecord,
|
|
133
|
+
MappingConfiguration: tmpMappingOutcome.Configuration,
|
|
134
|
+
MappingOutcome: tmpMappingOutcome,
|
|
135
|
+
RowIndex: tmpMappingOutcome.ParsedRowCount,
|
|
136
|
+
NewRecordsGUIDUniqueness: [],
|
|
137
|
+
NewRecordPrototype: {},
|
|
138
|
+
Fable: pFable,
|
|
139
|
+
Pict: pFable,
|
|
140
|
+
AppData: pFable.AppData
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Run the solvers for this record
|
|
144
|
+
let tmpSolverResultsObject = {};
|
|
145
|
+
if (tmpMappingOutcome.Configuration.Solvers && Array.isArray(tmpMappingOutcome.Configuration.Solvers))
|
|
146
|
+
{
|
|
147
|
+
for (let i = 0; i < tmpMappingOutcome.Configuration.Solvers.length; i++)
|
|
148
|
+
{
|
|
149
|
+
let tmpSolver = tmpMappingOutcome.Configuration.Solvers[i];
|
|
150
|
+
pFable.ExpressionParser.solve(tmpSolver, tmpMappingRecordSolution, tmpSolverResultsObject, pFable.manifest, tmpMappingRecordSolution);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (tmpMappingOutcome.Configuration.MultipleGUIDUniqueness && tmpMappingRecordSolution.NewRecordsGUIDUniqueness.length > 0)
|
|
155
|
+
{
|
|
156
|
+
for (let i = 0; i < tmpMappingRecordSolution.NewRecordsGUIDUniqueness.length; i++)
|
|
157
|
+
{
|
|
158
|
+
pFable.MeadowIntegrationTabularTransform.addRecordToComprehension(tmpIncomingRecord, tmpMappingOutcome, tmpMappingRecordSolution.NewRecordPrototype, tmpMappingRecordSolution.NewRecordsGUIDUniqueness[i]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else if (!tmpMappingOutcome.Configuration.MultipleGUIDUniqueness)
|
|
162
|
+
{
|
|
163
|
+
pFable.MeadowIntegrationTabularTransform.addRecordToComprehension(tmpIncomingRecord, tmpMappingOutcome, tmpMappingRecordSolution.NewRecordPrototype);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
tmpReadline.on('close',
|
|
169
|
+
() =>
|
|
170
|
+
{
|
|
171
|
+
pFable.log.info(`TSV Transform: Parsed ${tmpMappingOutcome.ParsedRowCount} rows from [${tmpInputFilePath}].`);
|
|
172
|
+
if (tmpBody.Extended)
|
|
173
|
+
{
|
|
174
|
+
pResponse.send(200, tmpMappingOutcome);
|
|
175
|
+
}
|
|
176
|
+
else
|
|
177
|
+
{
|
|
178
|
+
pResponse.send(200, tmpMappingOutcome.Comprehension);
|
|
179
|
+
}
|
|
180
|
+
return fNext();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
tmpReadline.on('error',
|
|
184
|
+
(pError) =>
|
|
185
|
+
{
|
|
186
|
+
pFable.log.error(`TSV Transform error reading file [${tmpInputFilePath}]: ${pError}`, pError);
|
|
187
|
+
pResponse.send(500, { Error: `Error reading TSV file: ${pError.message}` });
|
|
188
|
+
return fNext();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
};
|