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.
Files changed (52) hide show
  1. package/CONTRIBUTING.md +50 -0
  2. package/README.md +223 -7
  3. package/docs/README.md +107 -7
  4. package/docs/_sidebar.md +38 -0
  5. package/docs/_topbar.md +7 -0
  6. package/docs/cli-reference.md +242 -0
  7. package/docs/comprehensions.md +98 -0
  8. package/docs/cover.md +11 -0
  9. package/docs/css/docuserve.css +73 -0
  10. package/docs/examples-walkthrough.md +138 -0
  11. package/docs/index.html +37 -20
  12. package/docs/integration-adapter.md +109 -0
  13. package/docs/mapping-files.md +140 -0
  14. package/docs/programmatic-api.md +173 -0
  15. package/docs/rest-api-reference.md +731 -0
  16. package/docs/retold-catalog.json +153 -0
  17. package/docs/retold-keyword-index.json +4828 -0
  18. package/examples/Example-001-CSV-Check.sh +29 -0
  19. package/examples/Example-002-CSV-Transform-Implicit.sh +31 -0
  20. package/examples/Example-003-CSV-Transform-CLI-Options.sh +39 -0
  21. package/examples/Example-004-CSV-Transform-Mapping-File.sh +41 -0
  22. package/examples/Example-005-Multi-Entity-Bookstore.sh +60 -0
  23. package/examples/Example-006-Multi-CSV-Intersect.sh +74 -0
  24. package/examples/Example-007-Comprehension-To-Array.sh +41 -0
  25. package/examples/Example-008-Comprehension-To-CSV.sh +51 -0
  26. package/examples/Example-009-JSON-Array-Transform.sh +46 -0
  27. package/examples/Example-010-Programmatic-API.js +138 -0
  28. package/examples/README.md +44 -0
  29. package/examples/output/.gitignore +2 -0
  30. package/package.json +7 -4
  31. package/source/Meadow-Integration.js +3 -1
  32. package/source/cli/Meadow-Integration-CLI-Program.js +4 -1
  33. package/source/cli/commands/Meadow-Integration-Command-ObjectArrayToCSV.js +49 -32
  34. package/source/cli/commands/Meadow-Integration-Command-Serve.js +51 -0
  35. package/source/restserver/Meadow-Integration-Server-Endpoints.js +83 -0
  36. package/source/restserver/Meadow-Integration-Server.js +86 -0
  37. package/source/restserver/endpoints/Endpoint-CSVCheck.js +91 -0
  38. package/source/restserver/endpoints/Endpoint-CSVTransform.js +189 -0
  39. package/source/restserver/endpoints/Endpoint-ComprehensionArray.js +121 -0
  40. package/source/restserver/endpoints/Endpoint-ComprehensionIntersect.js +166 -0
  41. package/source/restserver/endpoints/Endpoint-ComprehensionPush.js +209 -0
  42. package/source/restserver/endpoints/Endpoint-EntityFromTabularFolder.js +252 -0
  43. package/source/restserver/endpoints/Endpoint-JSONArrayTransform.js +238 -0
  44. package/source/restserver/endpoints/Endpoint-ObjectArrayToCSV.js +231 -0
  45. package/source/restserver/endpoints/Endpoint-TSVCheck.js +93 -0
  46. package/source/restserver/endpoints/Endpoint-TSVTransform.js +191 -0
  47. package/test/Meadow-Integration-Server_test.js +1170 -0
  48. package/test/data/test-comprehension-secondary.json +8 -0
  49. package/test/data/test-comprehension.json +8 -0
  50. package/test/data/test-small.csv +6 -0
  51. package/test/data/test-small.json +7 -0
  52. 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, flatten(pValue, pPropertyPath));
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
- if (!tmpMappingOutcome.RecordArray || !Array.isArray(tmpMappingOutcome.RecordArray))
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.error(`Could not locate a valid record array in JSON file.`);
153
- return fCallback();
156
+ this.fable.log.info(`Raw recordset is an array.`);
157
+ tmpMappingOutcome.RecordArray = tmpMappingOutcome.RawRecordSet;
154
158
  }
155
- if (tmpMappingOutcome.RecordArray.length < 1)
159
+ else if (typeof(tmpMappingOutcome.RawRecordSet) == 'object')
156
160
  {
157
- this.fable.log.error(`No records in the record array.`);
158
- return fCallback();
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
- try
170
- {
171
- this.streamFlattenedJSONToCSV(tmpMappingOutcome.RecordArray, tmpRawOutputFilePath)
172
- .then(
173
- () =>
174
- {
175
- this.pict.log.info(`CSV file created successfully: ${tmpRawOutputFilePath}`);
176
- })
177
- .catch(
178
- (pError) =>
179
- {
180
- this.pict.log.error(`Error generating or writing CSV:`, pError);
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
+ };