meadow-integration 1.0.35 → 1.0.37
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/package.json
CHANGED
|
@@ -1,7 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meadow-integration",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.37",
|
|
4
4
|
"description": "Meadow Data Integration",
|
|
5
|
+
"retoldBeacon": {
|
|
6
|
+
"displayName": "Meadow Integration",
|
|
7
|
+
"description": "Parse / transform / LabWriter capabilities that seed operations dispatch to via the Ultravisor.",
|
|
8
|
+
"category": "integration",
|
|
9
|
+
"mode": "capability-provider",
|
|
10
|
+
"providerPath": "./source/Meadow-Integration-BeaconProvider.js",
|
|
11
|
+
"capability": "MeadowIntegration",
|
|
12
|
+
"healthCheck": {
|
|
13
|
+
"path": "/"
|
|
14
|
+
},
|
|
15
|
+
"defaultPort": 54400,
|
|
16
|
+
"requiresUltravisor": true,
|
|
17
|
+
"configForm": {
|
|
18
|
+
"Fields": []
|
|
19
|
+
},
|
|
20
|
+
"docker": {
|
|
21
|
+
"image": "retold-beacon-host-meadow-integration",
|
|
22
|
+
"dockerfile": "retold-beacon-host-meadow-integration.Dockerfile",
|
|
23
|
+
"exposedPort": 54400,
|
|
24
|
+
"hostPackage": "retold-beacon-host",
|
|
25
|
+
"dataMountPath": "/app/data",
|
|
26
|
+
"configMountPath": "/app/data/config.json",
|
|
27
|
+
"extraMounts": [
|
|
28
|
+
{
|
|
29
|
+
"Source": "seed_datasets",
|
|
30
|
+
"Target": "/app/seed_datasets",
|
|
31
|
+
"ReadOnly": true
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
5
36
|
"bin": {
|
|
6
37
|
"mdwint": "source/cli/Meadow-Integration-CLI-Run.js"
|
|
7
38
|
},
|
|
@@ -19,7 +50,7 @@
|
|
|
19
50
|
"devDependencies": {
|
|
20
51
|
"meadow-connection-sqlite": "^1.0.18",
|
|
21
52
|
"pict-docuserve": "^0.1.5",
|
|
22
|
-
"quackage": "^1.1.
|
|
53
|
+
"quackage": "^1.1.2"
|
|
23
54
|
},
|
|
24
55
|
"mocha": {
|
|
25
56
|
"diff": true,
|
|
@@ -40,13 +71,13 @@
|
|
|
40
71
|
]
|
|
41
72
|
},
|
|
42
73
|
"dependencies": {
|
|
43
|
-
"fable": "^3.1.
|
|
74
|
+
"fable": "^3.1.71",
|
|
44
75
|
"fable-serviceproviderbase": "^3.0.19",
|
|
45
76
|
"fast-xml-parser": "^4.4.1",
|
|
46
|
-
"meadow": "^2.0.
|
|
77
|
+
"meadow": "^2.0.37",
|
|
47
78
|
"meadow-connection-mssql": "^1.0.21",
|
|
48
79
|
"meadow-connection-mysql": "^1.0.17",
|
|
49
|
-
"orator": "^6.0
|
|
80
|
+
"orator": "^6.1.0",
|
|
50
81
|
"orator-serviceserver-restify": "^2.0.10",
|
|
51
82
|
"pict-section-flow": "^0.0.17",
|
|
52
83
|
"pict-service-commandlineutility": "^1.0.19",
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meadow Integration — Ultravisor Beacon Capability Provider
|
|
3
|
+
*
|
|
4
|
+
* Wraps meadow-integration's FileParser + TabularTransform services, plus a
|
|
5
|
+
* small LabWriter action that posts records to a databeacon's dynamic
|
|
6
|
+
* /1.0/<Entity> endpoint, as an ultravisor-beacon CapabilityProvider.
|
|
7
|
+
*
|
|
8
|
+
* Loaded by retold-beacon-host via `--provider meadow-integration/source/Meadow-Integration-BeaconProvider.js`
|
|
9
|
+
* at container runtime. Discovery + Dockerfile selection is driven by the
|
|
10
|
+
* `retoldBeacon` stanza in this module's package.json; ultravisor-lab
|
|
11
|
+
* scans that stanza the same way it does for every other published beacon.
|
|
12
|
+
*
|
|
13
|
+
* Capabilities (two are registered so a single host process can serve both):
|
|
14
|
+
* MeadowIntegration.ParseContent raw content (CSV/JSON/XML) → records
|
|
15
|
+
* MeadowIntegration.ParseFile JSON file on disk → records
|
|
16
|
+
* MeadowIntegration.TransformRecords apply a MappingConfiguration
|
|
17
|
+
* LabWriter.BulkInsertViaBeacon POST records to a beacon endpoint
|
|
18
|
+
*
|
|
19
|
+
* Constructor signature: `(pProviderConfig, pPict)`.
|
|
20
|
+
* pProviderConfig -- user-saved config blob from the lab's form ({} is fine).
|
|
21
|
+
* pPict -- the host's pict instance; meadow-integration's
|
|
22
|
+
* services register through its serviceManager and we
|
|
23
|
+
* grab the instances off the pict afterward.
|
|
24
|
+
*/
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const libFS = require('fs');
|
|
28
|
+
|
|
29
|
+
const libMeadowIntegrationFileParser = require('./services/parser/Service-FileParser.js');
|
|
30
|
+
const libMeadowIntegrationTabularTransform = require('./services/tabular/Service-TabularTransform.js');
|
|
31
|
+
|
|
32
|
+
class MeadowIntegrationBeaconProvider
|
|
33
|
+
{
|
|
34
|
+
constructor(pProviderConfig, pPict)
|
|
35
|
+
{
|
|
36
|
+
this._Config = pProviderConfig || {};
|
|
37
|
+
|
|
38
|
+
if (!pPict)
|
|
39
|
+
{
|
|
40
|
+
throw new Error('meadow-integration beacon provider requires the host pict instance.');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pPict.serviceManager.addServiceType('MeadowIntegrationFileParser', libMeadowIntegrationFileParser);
|
|
44
|
+
pPict.serviceManager.instantiateServiceProvider('MeadowIntegrationFileParser');
|
|
45
|
+
pPict.serviceManager.addServiceType('TabularTransform', libMeadowIntegrationTabularTransform);
|
|
46
|
+
pPict.serviceManager.instantiateServiceProvider('TabularTransform');
|
|
47
|
+
|
|
48
|
+
this._parser = pPict.MeadowIntegrationFileParser;
|
|
49
|
+
this._transform = pPict.TabularTransform;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Called by retold-beacon-host. Registers both capabilities on the
|
|
54
|
+
* given ultravisor-beacon. Using the explicit register() hook (rather
|
|
55
|
+
* than the `Capability` + `actions` pair the base class convention
|
|
56
|
+
* expects) so a single provider module can contribute more than one
|
|
57
|
+
* capability.
|
|
58
|
+
*/
|
|
59
|
+
register(pBeacon)
|
|
60
|
+
{
|
|
61
|
+
pBeacon.registerCapability(
|
|
62
|
+
{
|
|
63
|
+
Capability: 'LabWriter',
|
|
64
|
+
Name: 'LabWriterProvider',
|
|
65
|
+
actions:
|
|
66
|
+
{
|
|
67
|
+
BulkInsertViaBeacon: this._buildBulkInsertAction()
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
pBeacon.registerCapability(
|
|
72
|
+
{
|
|
73
|
+
Capability: 'MeadowIntegration',
|
|
74
|
+
Name: 'MeadowIntegrationProvider',
|
|
75
|
+
actions:
|
|
76
|
+
{
|
|
77
|
+
ParseContent: this._buildParseContentAction(),
|
|
78
|
+
ParseFile: this._buildParseFileAction(),
|
|
79
|
+
TransformRecords: this._buildTransformRecordsAction()
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── LabWriter actions ──────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
_buildBulkInsertAction()
|
|
87
|
+
{
|
|
88
|
+
return {
|
|
89
|
+
Description: 'POSTs records to a target beacon\'s /1.0/<Entity> endpoint',
|
|
90
|
+
SettingsSchema:
|
|
91
|
+
[
|
|
92
|
+
{ Name: 'BeaconURL', DataType: 'String', Required: true },
|
|
93
|
+
{ Name: 'EntityName', DataType: 'String', Required: true },
|
|
94
|
+
{ Name: 'Records', DataType: 'Array', Required: true }
|
|
95
|
+
],
|
|
96
|
+
Handler: (pWorkItem, pContext, fCallback) =>
|
|
97
|
+
{
|
|
98
|
+
try
|
|
99
|
+
{
|
|
100
|
+
let tmpSettings = pWorkItem.Settings || {};
|
|
101
|
+
let tmpBeaconURL = tmpSettings.BeaconURL;
|
|
102
|
+
let tmpEntity = tmpSettings.EntityName;
|
|
103
|
+
let tmpRecords = tmpSettings.Records;
|
|
104
|
+
if (!tmpBeaconURL) { return fCallback(new Error('BeaconURL is required.')); }
|
|
105
|
+
if (!tmpEntity) { return fCallback(new Error('EntityName is required.')); }
|
|
106
|
+
// Defensive: catch the "engine left an unresolved {~D:~} string
|
|
107
|
+
// in Records" failure mode loudly instead of iterating over
|
|
108
|
+
// the string's characters and POSTing garbage. The execution
|
|
109
|
+
// engine's template resolver SHOULD substitute Array-typed
|
|
110
|
+
// settings whose value is a single whole-string reference,
|
|
111
|
+
// but if it doesn't (older engine, mis-wired graph) this
|
|
112
|
+
// catches it at the boundary with a useful message.
|
|
113
|
+
if (tmpRecords === undefined || tmpRecords === null) { tmpRecords = []; }
|
|
114
|
+
if (!Array.isArray(tmpRecords))
|
|
115
|
+
{
|
|
116
|
+
return fCallback(new Error(
|
|
117
|
+
`BulkInsertViaBeacon: Records must be an array, got ${typeof(tmpRecords)}` +
|
|
118
|
+
(typeof(tmpRecords) === 'string'
|
|
119
|
+
? ` (looks like an unresolved template: "${tmpRecords.slice(0, 80)}")`
|
|
120
|
+
: '')));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let tmpURL = new URL(tmpBeaconURL);
|
|
124
|
+
let tmpHttp = tmpURL.protocol === 'https:' ? require('https') : require('http');
|
|
125
|
+
let tmpBase = (tmpURL.pathname || '').replace(/\/$/, '');
|
|
126
|
+
|
|
127
|
+
let tmpInserted = 0;
|
|
128
|
+
let tmpFailed = 0;
|
|
129
|
+
let tmpIdx = 0;
|
|
130
|
+
let tmpLastErr = '';
|
|
131
|
+
|
|
132
|
+
let tmpNext = () =>
|
|
133
|
+
{
|
|
134
|
+
if (tmpIdx >= tmpRecords.length)
|
|
135
|
+
{
|
|
136
|
+
return fCallback(null,
|
|
137
|
+
{
|
|
138
|
+
Outputs:
|
|
139
|
+
{
|
|
140
|
+
InsertedCount: tmpInserted,
|
|
141
|
+
FailedCount: tmpFailed,
|
|
142
|
+
TotalCount: tmpRecords.length,
|
|
143
|
+
LastError: tmpLastErr
|
|
144
|
+
},
|
|
145
|
+
Log: []
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
let tmpRec = tmpRecords[tmpIdx++];
|
|
149
|
+
let tmpBody = JSON.stringify(tmpRec);
|
|
150
|
+
let tmpReq = tmpHttp.request(
|
|
151
|
+
{
|
|
152
|
+
host: tmpURL.hostname,
|
|
153
|
+
port: tmpURL.port,
|
|
154
|
+
path: `${tmpBase}/${tmpEntity}`,
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(tmpBody) },
|
|
157
|
+
timeout: 15000
|
|
158
|
+
},
|
|
159
|
+
(pRes) =>
|
|
160
|
+
{
|
|
161
|
+
let tmpChunks = [];
|
|
162
|
+
pRes.on('data', (c) => tmpChunks.push(c));
|
|
163
|
+
pRes.on('end', () =>
|
|
164
|
+
{
|
|
165
|
+
if (pRes.statusCode >= 400)
|
|
166
|
+
{
|
|
167
|
+
tmpFailed++;
|
|
168
|
+
tmpLastErr = `HTTP ${pRes.statusCode}: ` + Buffer.concat(tmpChunks).toString('utf8').slice(0, 200);
|
|
169
|
+
}
|
|
170
|
+
else { tmpInserted++; }
|
|
171
|
+
setImmediate(tmpNext);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
tmpReq.on('error', (pErr) => { tmpFailed++; tmpLastErr = pErr.message; setImmediate(tmpNext); });
|
|
175
|
+
tmpReq.on('timeout', () => { tmpReq.destroy(); tmpFailed++; tmpLastErr = 'timeout'; setImmediate(tmpNext); });
|
|
176
|
+
tmpReq.write(tmpBody);
|
|
177
|
+
tmpReq.end();
|
|
178
|
+
};
|
|
179
|
+
tmpNext();
|
|
180
|
+
}
|
|
181
|
+
catch (pEx) { return fCallback(pEx); }
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── MeadowIntegration actions ──────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
_buildParseContentAction()
|
|
189
|
+
{
|
|
190
|
+
return {
|
|
191
|
+
Description: 'Parse raw content (CSV/JSON/XML) into records',
|
|
192
|
+
SettingsSchema:
|
|
193
|
+
[
|
|
194
|
+
{ Name: 'Content', DataType: 'String', Required: true },
|
|
195
|
+
{ Name: 'Format', DataType: 'String', Required: false }
|
|
196
|
+
],
|
|
197
|
+
Handler: (pWorkItem, pContext, fCallback) =>
|
|
198
|
+
{
|
|
199
|
+
try
|
|
200
|
+
{
|
|
201
|
+
let tmpSettings = pWorkItem.Settings || {};
|
|
202
|
+
this._parser.parseContent(tmpSettings.Content || '',
|
|
203
|
+
{ format: tmpSettings.Format || 'auto' },
|
|
204
|
+
(pErr, pResult) =>
|
|
205
|
+
{
|
|
206
|
+
if (pErr) { return fCallback(pErr); }
|
|
207
|
+
let tmpRecords = Array.isArray(pResult) ? pResult : (pResult && pResult.Records) || [];
|
|
208
|
+
return fCallback(null, { Outputs: { Records: tmpRecords, Count: tmpRecords.length }, Log: [] });
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch (pEx) { return fCallback(pEx); }
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
_buildParseFileAction()
|
|
217
|
+
{
|
|
218
|
+
return {
|
|
219
|
+
Description: 'Parse a file from disk into records',
|
|
220
|
+
SettingsSchema:
|
|
221
|
+
[
|
|
222
|
+
{ Name: 'FilePath', DataType: 'String', Required: true },
|
|
223
|
+
{ Name: 'Format', DataType: 'String', Required: false }
|
|
224
|
+
],
|
|
225
|
+
Handler: (pWorkItem, pContext, fCallback) =>
|
|
226
|
+
{
|
|
227
|
+
try
|
|
228
|
+
{
|
|
229
|
+
let tmpSettings = pWorkItem.Settings || {};
|
|
230
|
+
if (!tmpSettings.FilePath) { return fCallback(new Error('FilePath is required.')); }
|
|
231
|
+
|
|
232
|
+
// Seed fixtures are small JSON files; simplest reliable
|
|
233
|
+
// path is to load + parse directly rather than stream.
|
|
234
|
+
let tmpContent;
|
|
235
|
+
try { tmpContent = libFS.readFileSync(tmpSettings.FilePath, 'utf8'); }
|
|
236
|
+
catch (pReadErr) { return fCallback(pReadErr); }
|
|
237
|
+
|
|
238
|
+
let tmpRecords;
|
|
239
|
+
try { tmpRecords = JSON.parse(tmpContent); }
|
|
240
|
+
catch (pParseErr) { return fCallback(pParseErr); }
|
|
241
|
+
if (!Array.isArray(tmpRecords)) { tmpRecords = [tmpRecords]; }
|
|
242
|
+
|
|
243
|
+
return fCallback(null, { Outputs: { Records: tmpRecords, Count: tmpRecords.length }, Log: [] });
|
|
244
|
+
}
|
|
245
|
+
catch (pEx) { return fCallback(pEx); }
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_buildTransformRecordsAction()
|
|
251
|
+
{
|
|
252
|
+
return {
|
|
253
|
+
Description: 'Apply a MappingConfiguration to records via TabularTransform',
|
|
254
|
+
SettingsSchema:
|
|
255
|
+
[
|
|
256
|
+
{ Name: 'Records', DataType: 'Array', Required: true },
|
|
257
|
+
{ Name: 'MappingConfiguration', DataType: 'Object', Required: true },
|
|
258
|
+
{ Name: 'EntityName', DataType: 'String', Required: false }
|
|
259
|
+
],
|
|
260
|
+
Handler: (pWorkItem, pContext, fCallback) =>
|
|
261
|
+
{
|
|
262
|
+
try
|
|
263
|
+
{
|
|
264
|
+
let tmpSettings = pWorkItem.Settings || {};
|
|
265
|
+
let tmpRecords = tmpSettings.Records || [];
|
|
266
|
+
let tmpMapping = tmpSettings.MappingConfiguration || {};
|
|
267
|
+
let tmpEntity = tmpSettings.EntityName || (tmpMapping && tmpMapping.Entity) || 'Record';
|
|
268
|
+
this._transform.transform(tmpRecords, tmpMapping,
|
|
269
|
+
(pErr, pResult) =>
|
|
270
|
+
{
|
|
271
|
+
if (pErr) { return fCallback(pErr); }
|
|
272
|
+
let tmpOut = (pResult && pResult.Records) ? pResult : { Records: pResult || [] };
|
|
273
|
+
return fCallback(null,
|
|
274
|
+
{
|
|
275
|
+
Outputs:
|
|
276
|
+
{
|
|
277
|
+
Records: tmpOut.Records || [],
|
|
278
|
+
Count: (tmpOut.Records || []).length,
|
|
279
|
+
ParsedCount: tmpRecords.length,
|
|
280
|
+
BadRecords: tmpOut.BadRecords || [],
|
|
281
|
+
Entity: tmpEntity
|
|
282
|
+
},
|
|
283
|
+
Log: []
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
catch (pEx) { return fCallback(pEx); }
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = MeadowIntegrationBeaconProvider;
|