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
@@ -0,0 +1,121 @@
1
+ /**
2
+ * POST /1.0/Comprehension/ToArray
3
+ *
4
+ * Convert an object-based comprehension into an array.
5
+ *
6
+ * Request body (JSON):
7
+ * {
8
+ * "Comprehension": { ... }, // The comprehension object to convert
9
+ * "Entity": "MyEntity" // (optional) Entity name; auto-inferred if omitted
10
+ * }
11
+ *
12
+ * OR file-based:
13
+ *
14
+ * POST /1.0/Comprehension/ToArrayFromFile
15
+ * {
16
+ * "File": "/path/to/comprehension.json", // Comprehension file path
17
+ * "Entity": "MyEntity" // (optional) Entity name
18
+ * }
19
+ *
20
+ * Response: JSON array of records
21
+ */
22
+ module.exports = function(pFable, pOrator)
23
+ {
24
+ // In-memory conversion
25
+ pOrator.serviceServer.postWithBodyParser('/1.0/Comprehension/ToArray',
26
+ (pRequest, pResponse, fNext) =>
27
+ {
28
+ let tmpBody = pRequest.body || {};
29
+
30
+ if (!tmpBody.Comprehension || (typeof(tmpBody.Comprehension) !== 'object'))
31
+ {
32
+ pResponse.send(400, { Error: 'No valid Comprehension object provided in request body.' });
33
+ return fNext();
34
+ }
35
+
36
+ let tmpResult = processComprehensionToArray(pFable, tmpBody.Comprehension, tmpBody.Entity);
37
+ if (tmpResult.Error)
38
+ {
39
+ pResponse.send(400, tmpResult);
40
+ return fNext();
41
+ }
42
+
43
+ pResponse.send(200, tmpResult.RecordArray);
44
+ return fNext();
45
+ });
46
+
47
+ // File-based conversion
48
+ pOrator.serviceServer.postWithBodyParser('/1.0/Comprehension/ToArrayFromFile',
49
+ (pRequest, pResponse, fNext) =>
50
+ {
51
+ let tmpBody = pRequest.body || {};
52
+
53
+ if (!tmpBody.File || (typeof(tmpBody.File) !== 'string'))
54
+ {
55
+ pResponse.send(400, { Error: 'No valid File path provided in request body.' });
56
+ return fNext();
57
+ }
58
+
59
+ pFable.instantiateServiceProvider('FilePersistence');
60
+
61
+ let tmpFilePath = pFable.FilePersistence.resolvePath(tmpBody.File);
62
+
63
+ if (!pFable.FilePersistence.existsSync(tmpFilePath))
64
+ {
65
+ pResponse.send(404, { Error: `File [${tmpFilePath}] does not exist.` });
66
+ return fNext();
67
+ }
68
+
69
+ let tmpComprehension;
70
+ try
71
+ {
72
+ tmpComprehension = JSON.parse(pFable.FilePersistence.readFileSync(tmpFilePath));
73
+ }
74
+ catch (pError)
75
+ {
76
+ pResponse.send(400, { Error: `Error parsing comprehension file: ${pError.message}` });
77
+ return fNext();
78
+ }
79
+
80
+ let tmpResult = processComprehensionToArray(pFable, tmpComprehension, tmpBody.Entity);
81
+ if (tmpResult.Error)
82
+ {
83
+ pResponse.send(400, tmpResult);
84
+ return fNext();
85
+ }
86
+
87
+ pResponse.send(200, tmpResult.RecordArray);
88
+ return fNext();
89
+ });
90
+ };
91
+
92
+ function processComprehensionToArray(pFable, pComprehension, pEntity)
93
+ {
94
+ let tmpEntity = pEntity;
95
+
96
+ if (!tmpEntity)
97
+ {
98
+ let tmpEntityInference = Object.keys(pComprehension);
99
+ if (tmpEntityInference.length > 0)
100
+ {
101
+ tmpEntity = tmpEntityInference[0];
102
+ pFable.log.info(`No entity specified. Using [${tmpEntity}] as the inferred entity.`);
103
+ }
104
+ else
105
+ {
106
+ return { Error: 'No entity specified and no entities found in the comprehension.' };
107
+ }
108
+ }
109
+
110
+ let tmpEntityRecords = pComprehension[tmpEntity] || {};
111
+ let tmpRecordArray = [];
112
+
113
+ let tmpRecordKeys = Object.keys(tmpEntityRecords);
114
+ for (let i = 0; i < tmpRecordKeys.length; i++)
115
+ {
116
+ tmpRecordArray.push(tmpEntityRecords[tmpRecordKeys[i]]);
117
+ }
118
+
119
+ pFable.log.info(`Comprehension ToArray: Converted ${tmpRecordArray.length} records for entity [${tmpEntity}].`);
120
+ return { RecordArray: tmpRecordArray };
121
+ }
@@ -0,0 +1,166 @@
1
+ const libPath = require('path');
2
+
3
+ /**
4
+ * POST /1.0/Comprehension/Intersect
5
+ *
6
+ * Merge two comprehension objects together.
7
+ *
8
+ * Request body (JSON):
9
+ * {
10
+ * "PrimaryComprehension": { ... }, // The primary comprehension object
11
+ * "SecondaryComprehension": { ... }, // The secondary comprehension to merge in
12
+ * "Entity": "MyEntity" // (optional) Entity name; auto-inferred if omitted
13
+ * }
14
+ *
15
+ * OR file-based:
16
+ *
17
+ * POST /1.0/Comprehension/IntersectFiles
18
+ * {
19
+ * "File": "/path/to/primary.json", // Primary comprehension file
20
+ * "IntersectFile": "/path/to/secondary.json",// Secondary comprehension file
21
+ * "Entity": "MyEntity" // (optional) Entity name
22
+ * }
23
+ *
24
+ * Response: Merged comprehension JSON
25
+ */
26
+ module.exports = function(pFable, pOrator)
27
+ {
28
+ // In-memory intersection
29
+ pOrator.serviceServer.postWithBodyParser('/1.0/Comprehension/Intersect',
30
+ (pRequest, pResponse, fNext) =>
31
+ {
32
+ let tmpBody = pRequest.body || {};
33
+
34
+ if (!tmpBody.PrimaryComprehension || (typeof(tmpBody.PrimaryComprehension) !== 'object'))
35
+ {
36
+ pResponse.send(400, { Error: 'No valid PrimaryComprehension object provided in request body.' });
37
+ return fNext();
38
+ }
39
+ if (!tmpBody.SecondaryComprehension || (typeof(tmpBody.SecondaryComprehension) !== 'object'))
40
+ {
41
+ pResponse.send(400, { Error: 'No valid SecondaryComprehension object provided in request body.' });
42
+ return fNext();
43
+ }
44
+
45
+ let tmpResult = processComprehensionIntersect(pFable, tmpBody.PrimaryComprehension, tmpBody.SecondaryComprehension, tmpBody.Entity);
46
+ if (tmpResult.Error)
47
+ {
48
+ pResponse.send(400, tmpResult);
49
+ return fNext();
50
+ }
51
+
52
+ pResponse.send(200, tmpResult.Comprehension);
53
+ return fNext();
54
+ });
55
+
56
+ // File-based intersection
57
+ pOrator.serviceServer.postWithBodyParser('/1.0/Comprehension/IntersectFiles',
58
+ (pRequest, pResponse, fNext) =>
59
+ {
60
+ let tmpBody = pRequest.body || {};
61
+
62
+ if (!tmpBody.File || (typeof(tmpBody.File) !== 'string'))
63
+ {
64
+ pResponse.send(400, { Error: 'No valid File path provided in request body.' });
65
+ return fNext();
66
+ }
67
+ if (!tmpBody.IntersectFile || (typeof(tmpBody.IntersectFile) !== 'string'))
68
+ {
69
+ pResponse.send(400, { Error: 'No valid IntersectFile path provided in request body.' });
70
+ return fNext();
71
+ }
72
+
73
+ pFable.instantiateServiceProvider('FilePersistence');
74
+
75
+ let tmpPrimaryFilePath = pFable.FilePersistence.resolvePath(tmpBody.File);
76
+ let tmpSecondaryFilePath = pFable.FilePersistence.resolvePath(tmpBody.IntersectFile);
77
+
78
+ if (!pFable.FilePersistence.existsSync(tmpPrimaryFilePath))
79
+ {
80
+ pResponse.send(404, { Error: `Primary file [${tmpPrimaryFilePath}] does not exist.` });
81
+ return fNext();
82
+ }
83
+ if (!pFable.FilePersistence.existsSync(tmpSecondaryFilePath))
84
+ {
85
+ pResponse.send(404, { Error: `Secondary file [${tmpSecondaryFilePath}] does not exist.` });
86
+ return fNext();
87
+ }
88
+
89
+ let tmpPrimaryComprehension;
90
+ let tmpSecondaryComprehension;
91
+
92
+ try
93
+ {
94
+ tmpPrimaryComprehension = JSON.parse(pFable.FilePersistence.readFileSync(tmpPrimaryFilePath));
95
+ }
96
+ catch (pError)
97
+ {
98
+ pResponse.send(400, { Error: `Error parsing primary comprehension file: ${pError.message}` });
99
+ return fNext();
100
+ }
101
+
102
+ try
103
+ {
104
+ tmpSecondaryComprehension = JSON.parse(pFable.FilePersistence.readFileSync(tmpSecondaryFilePath));
105
+ }
106
+ catch (pError)
107
+ {
108
+ pResponse.send(400, { Error: `Error parsing secondary comprehension file: ${pError.message}` });
109
+ return fNext();
110
+ }
111
+
112
+ let tmpResult = processComprehensionIntersect(pFable, tmpPrimaryComprehension, tmpSecondaryComprehension, tmpBody.Entity);
113
+ if (tmpResult.Error)
114
+ {
115
+ pResponse.send(400, tmpResult);
116
+ return fNext();
117
+ }
118
+
119
+ pResponse.send(200, tmpResult.Comprehension);
120
+ return fNext();
121
+ });
122
+ };
123
+
124
+ function processComprehensionIntersect(pFable, pPrimaryComprehension, pSecondaryComprehension, pEntity)
125
+ {
126
+ let tmpEntity = pEntity;
127
+
128
+ if (!tmpEntity)
129
+ {
130
+ let tmpEntityInference = Object.keys(pPrimaryComprehension);
131
+ if (tmpEntityInference.length > 0)
132
+ {
133
+ tmpEntity = tmpEntityInference[0];
134
+ pFable.log.info(`No entity specified. Using [${tmpEntity}] as the inferred entity.`);
135
+ }
136
+ else
137
+ {
138
+ return { Error: 'No entity specified and no entities found in the primary comprehension.' };
139
+ }
140
+ }
141
+
142
+ // Deep clone the primary to avoid mutation
143
+ let tmpResultComprehension = JSON.parse(JSON.stringify(pPrimaryComprehension));
144
+
145
+ if (!tmpResultComprehension[tmpEntity])
146
+ {
147
+ tmpResultComprehension[tmpEntity] = {};
148
+ }
149
+
150
+ let tmpIntersectingKeys = Object.keys(pSecondaryComprehension[tmpEntity] || {});
151
+ for (let i = 0; i < tmpIntersectingKeys.length; i++)
152
+ {
153
+ const tmpRecordGUID = tmpIntersectingKeys[i];
154
+ if (tmpResultComprehension[tmpEntity][tmpRecordGUID])
155
+ {
156
+ tmpResultComprehension[tmpEntity][tmpRecordGUID] = Object.assign(tmpResultComprehension[tmpEntity][tmpRecordGUID], pSecondaryComprehension[tmpEntity][tmpRecordGUID]);
157
+ }
158
+ else
159
+ {
160
+ tmpResultComprehension[tmpEntity][tmpRecordGUID] = pSecondaryComprehension[tmpEntity][tmpRecordGUID];
161
+ }
162
+ }
163
+
164
+ pFable.log.info(`Comprehension Intersect: Merged ${tmpIntersectingKeys.length} records for entity [${tmpEntity}].`);
165
+ return { Comprehension: tmpResultComprehension };
166
+ }
@@ -0,0 +1,209 @@
1
+ const libPath = require('path');
2
+
3
+ const libIntegrationAdapter = require('../../Meadow-Service-Integration-Adapter.js');
4
+
5
+ /**
6
+ * POST /1.0/Comprehension/Push
7
+ *
8
+ * Push a comprehension to Meadow REST APIs via the Integration Adapter.
9
+ *
10
+ * Request body (JSON):
11
+ * {
12
+ * "Comprehension": { ... }, // The comprehension object to push
13
+ * "GUIDPrefix": "INTG-", // (optional) GUID prefix for the comprehension push
14
+ * "EntityGUIDPrefix": "E-", // (optional) GUID prefix per entity
15
+ * "ServerURL": "http://localhost:8086/1.0/" // (optional) Target Meadow API server URL
16
+ * }
17
+ *
18
+ * OR file-based:
19
+ *
20
+ * POST /1.0/Comprehension/PushFile
21
+ * {
22
+ * "File": "/path/to/comprehension.json", // Comprehension file path
23
+ * "GUIDPrefix": "INTG-", // (optional) GUID prefix
24
+ * "EntityGUIDPrefix": "E-", // (optional) Entity GUID prefix
25
+ * "ServerURL": "http://localhost:8086/1.0/" // (optional) Target Meadow API server URL
26
+ * }
27
+ *
28
+ * Response: { "Success": true, "EntitiesPushed": [...], "Message": "..." }
29
+ */
30
+ module.exports = function(pFable, pOrator)
31
+ {
32
+ // In-memory push
33
+ pOrator.serviceServer.postWithBodyParser('/1.0/Comprehension/Push',
34
+ (pRequest, pResponse, fNext) =>
35
+ {
36
+ let tmpBody = pRequest.body || {};
37
+
38
+ if (!tmpBody.Comprehension || (typeof(tmpBody.Comprehension) !== 'object'))
39
+ {
40
+ pResponse.send(400, { Error: 'No valid Comprehension object provided in request body.' });
41
+ return fNext();
42
+ }
43
+
44
+ pushComprehension(pFable, tmpBody.Comprehension, tmpBody,
45
+ (pError, pResult) =>
46
+ {
47
+ if (pError)
48
+ {
49
+ pResponse.send(500, { Error: `Error pushing comprehension: ${pError.message || pError}` });
50
+ return fNext();
51
+ }
52
+ pResponse.send(200, pResult);
53
+ return fNext();
54
+ });
55
+ });
56
+
57
+ // File-based push
58
+ pOrator.serviceServer.postWithBodyParser('/1.0/Comprehension/PushFile',
59
+ (pRequest, pResponse, fNext) =>
60
+ {
61
+ let tmpBody = pRequest.body || {};
62
+
63
+ if (!tmpBody.File || (typeof(tmpBody.File) !== 'string'))
64
+ {
65
+ pResponse.send(400, { Error: 'No valid File path provided in request body.' });
66
+ return fNext();
67
+ }
68
+
69
+ pFable.instantiateServiceProvider('FilePersistence');
70
+
71
+ let tmpFilePath = pFable.FilePersistence.resolvePath(tmpBody.File);
72
+
73
+ if (!pFable.FilePersistence.existsSync(tmpFilePath))
74
+ {
75
+ pResponse.send(404, { Error: `File [${tmpFilePath}] does not exist.` });
76
+ return fNext();
77
+ }
78
+
79
+ let tmpComprehension;
80
+ try
81
+ {
82
+ tmpComprehension = JSON.parse(pFable.FilePersistence.readFileSync(tmpFilePath));
83
+ }
84
+ catch (pError)
85
+ {
86
+ pResponse.send(400, { Error: `Error parsing comprehension file: ${pError.message}` });
87
+ return fNext();
88
+ }
89
+
90
+ pushComprehension(pFable, tmpComprehension, tmpBody,
91
+ (pError, pResult) =>
92
+ {
93
+ if (pError)
94
+ {
95
+ pResponse.send(500, { Error: `Error pushing comprehension: ${pError.message || pError}` });
96
+ return fNext();
97
+ }
98
+ pResponse.send(200, pResult);
99
+ return fNext();
100
+ });
101
+ });
102
+ };
103
+
104
+ function getCapitalLettersAsString(pInputString)
105
+ {
106
+ let tmpRegex = /[A-Z]/g;
107
+ let tmpMatch = pInputString.match(tmpRegex);
108
+ return tmpMatch ? tmpMatch.join('') : 'UNK';
109
+ }
110
+
111
+ function pushComprehension(pFable, pComprehension, pOptions, fCallback)
112
+ {
113
+ pFable.serviceManager.addServiceType('IntegrationAdapter', libIntegrationAdapter);
114
+
115
+ let tmpAnticipate = pFable.newAnticipate();
116
+ let tmpEntitiesPushed = [];
117
+
118
+ let tmpIntegrationAdapterSet = Object.keys(pComprehension);
119
+
120
+ pFable.log.info(`Pushing comprehension with ${tmpIntegrationAdapterSet.length} entity(ies) to Meadow APIs...`);
121
+
122
+ tmpAnticipate.anticipate(
123
+ (fDone) =>
124
+ {
125
+ try
126
+ {
127
+ for (let i = 0; i < tmpIntegrationAdapterSet.length; i++)
128
+ {
129
+ let tmpAdapterKey = tmpIntegrationAdapterSet[i];
130
+ let tmpAdapterOptions = { Entity: tmpAdapterKey, EntityGUIDMarshalPrefix: getCapitalLettersAsString(tmpAdapterKey) };
131
+
132
+ if (pOptions.ServerURL)
133
+ {
134
+ tmpAdapterOptions.ServerURL = pOptions.ServerURL;
135
+ }
136
+
137
+ libIntegrationAdapter.getAdapter(pFable, tmpAdapterKey, getCapitalLettersAsString(tmpAdapterKey), { SimpleMarshal: true, ForceMarshal: true });
138
+
139
+ let tmpAdapter = pFable.servicesMap.IntegrationAdapter[tmpAdapterKey];
140
+
141
+ if (pOptions.GUIDPrefix)
142
+ {
143
+ tmpAdapter.AdapterSetGUIDMarshalPrefix = pOptions.GUIDPrefix;
144
+ }
145
+ if (pOptions.EntityGUIDPrefix)
146
+ {
147
+ tmpAdapter.EntityGUIDMarshalPrefix = pOptions.EntityGUIDPrefix;
148
+ }
149
+
150
+ let tmpDataMap = pComprehension[tmpAdapterKey];
151
+ if (!tmpDataMap)
152
+ {
153
+ pFable.log.info(`No records to push for [${tmpAdapterKey}].`);
154
+ continue;
155
+ }
156
+
157
+ tmpEntitiesPushed.push(tmpAdapterKey);
158
+
159
+ // Add source records
160
+ tmpAnticipate.anticipate(
161
+ (function(pAdapter, pDataMap)
162
+ {
163
+ return function(fRecordDone)
164
+ {
165
+ for (const tmpRecord in pDataMap)
166
+ {
167
+ pAdapter.addSourceRecord(pDataMap[tmpRecord]);
168
+ }
169
+ return fRecordDone();
170
+ };
171
+ })(tmpAdapter, tmpDataMap));
172
+
173
+ // Integrate records
174
+ tmpAnticipate.anticipate(
175
+ (function(pAdapter)
176
+ {
177
+ return function(fIntegrateDone)
178
+ {
179
+ pAdapter.integrateRecords(fIntegrateDone);
180
+ };
181
+ })(tmpAdapter));
182
+ }
183
+ }
184
+ catch (pError)
185
+ {
186
+ pFable.log.error(`Error wiring up integration adapters: ${pError}`, pError);
187
+ return fDone(pError);
188
+ }
189
+
190
+ return fDone();
191
+ });
192
+
193
+ tmpAnticipate.wait(
194
+ (pError) =>
195
+ {
196
+ if (pError)
197
+ {
198
+ pFable.log.error(`Error pushing comprehension.`, pError);
199
+ return fCallback(pError);
200
+ }
201
+ pFable.log.info(`Finished pushing comprehension for entities: [${tmpEntitiesPushed.join(', ')}].`);
202
+ return fCallback(null,
203
+ {
204
+ Success: true,
205
+ EntitiesPushed: tmpEntitiesPushed,
206
+ Message: `Pushed comprehension for ${tmpEntitiesPushed.length} entity(ies).`
207
+ });
208
+ });
209
+ }