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.35",
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.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.70",
74
+ "fable": "^3.1.71",
44
75
  "fable-serviceproviderbase": "^3.0.19",
45
76
  "fast-xml-parser": "^4.4.1",
46
- "meadow": "^2.0.35",
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.4",
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;