meadow-integration 1.0.34 → 1.0.36

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.34",
3
+ "version": "1.0.36",
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.0"
53
+ "quackage": "^1.1.2"
23
54
  },
24
55
  "mocha": {
25
56
  "diff": true,
@@ -40,11 +71,11 @@
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",
47
- "meadow-connection-mssql": "^1.0.20",
77
+ "meadow": "^2.0.37",
78
+ "meadow-connection-mssql": "^1.0.21",
48
79
  "meadow-connection-mysql": "^1.0.17",
49
80
  "orator": "^6.0.4",
50
81
  "orator-serviceserver-restify": "^2.0.10",
@@ -0,0 +1,277 @@
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
+
107
+ let tmpURL = new URL(tmpBeaconURL);
108
+ let tmpHttp = tmpURL.protocol === 'https:' ? require('https') : require('http');
109
+ let tmpBase = (tmpURL.pathname || '').replace(/\/$/, '');
110
+
111
+ let tmpInserted = 0;
112
+ let tmpFailed = 0;
113
+ let tmpIdx = 0;
114
+ let tmpLastErr = '';
115
+
116
+ let tmpNext = () =>
117
+ {
118
+ if (tmpIdx >= tmpRecords.length)
119
+ {
120
+ return fCallback(null,
121
+ {
122
+ Outputs:
123
+ {
124
+ InsertedCount: tmpInserted,
125
+ FailedCount: tmpFailed,
126
+ TotalCount: tmpRecords.length,
127
+ LastError: tmpLastErr
128
+ },
129
+ Log: []
130
+ });
131
+ }
132
+ let tmpRec = tmpRecords[tmpIdx++];
133
+ let tmpBody = JSON.stringify(tmpRec);
134
+ let tmpReq = tmpHttp.request(
135
+ {
136
+ host: tmpURL.hostname,
137
+ port: tmpURL.port,
138
+ path: `${tmpBase}/${tmpEntity}`,
139
+ method: 'POST',
140
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(tmpBody) },
141
+ timeout: 15000
142
+ },
143
+ (pRes) =>
144
+ {
145
+ let tmpChunks = [];
146
+ pRes.on('data', (c) => tmpChunks.push(c));
147
+ pRes.on('end', () =>
148
+ {
149
+ if (pRes.statusCode >= 400)
150
+ {
151
+ tmpFailed++;
152
+ tmpLastErr = `HTTP ${pRes.statusCode}: ` + Buffer.concat(tmpChunks).toString('utf8').slice(0, 200);
153
+ }
154
+ else { tmpInserted++; }
155
+ setImmediate(tmpNext);
156
+ });
157
+ });
158
+ tmpReq.on('error', (pErr) => { tmpFailed++; tmpLastErr = pErr.message; setImmediate(tmpNext); });
159
+ tmpReq.on('timeout', () => { tmpReq.destroy(); tmpFailed++; tmpLastErr = 'timeout'; setImmediate(tmpNext); });
160
+ tmpReq.write(tmpBody);
161
+ tmpReq.end();
162
+ };
163
+ tmpNext();
164
+ }
165
+ catch (pEx) { return fCallback(pEx); }
166
+ }
167
+ };
168
+ }
169
+
170
+ // ── MeadowIntegration actions ──────────────────────────────────────────
171
+
172
+ _buildParseContentAction()
173
+ {
174
+ return {
175
+ Description: 'Parse raw content (CSV/JSON/XML) into records',
176
+ SettingsSchema:
177
+ [
178
+ { Name: 'Content', DataType: 'String', Required: true },
179
+ { Name: 'Format', DataType: 'String', Required: false }
180
+ ],
181
+ Handler: (pWorkItem, pContext, fCallback) =>
182
+ {
183
+ try
184
+ {
185
+ let tmpSettings = pWorkItem.Settings || {};
186
+ this._parser.parseContent(tmpSettings.Content || '',
187
+ { format: tmpSettings.Format || 'auto' },
188
+ (pErr, pResult) =>
189
+ {
190
+ if (pErr) { return fCallback(pErr); }
191
+ let tmpRecords = Array.isArray(pResult) ? pResult : (pResult && pResult.Records) || [];
192
+ return fCallback(null, { Outputs: { Records: tmpRecords, Count: tmpRecords.length }, Log: [] });
193
+ });
194
+ }
195
+ catch (pEx) { return fCallback(pEx); }
196
+ }
197
+ };
198
+ }
199
+
200
+ _buildParseFileAction()
201
+ {
202
+ return {
203
+ Description: 'Parse a file from disk into records',
204
+ SettingsSchema:
205
+ [
206
+ { Name: 'FilePath', DataType: 'String', Required: true },
207
+ { Name: 'Format', DataType: 'String', Required: false }
208
+ ],
209
+ Handler: (pWorkItem, pContext, fCallback) =>
210
+ {
211
+ try
212
+ {
213
+ let tmpSettings = pWorkItem.Settings || {};
214
+ if (!tmpSettings.FilePath) { return fCallback(new Error('FilePath is required.')); }
215
+
216
+ // Seed fixtures are small JSON files; simplest reliable
217
+ // path is to load + parse directly rather than stream.
218
+ let tmpContent;
219
+ try { tmpContent = libFS.readFileSync(tmpSettings.FilePath, 'utf8'); }
220
+ catch (pReadErr) { return fCallback(pReadErr); }
221
+
222
+ let tmpRecords;
223
+ try { tmpRecords = JSON.parse(tmpContent); }
224
+ catch (pParseErr) { return fCallback(pParseErr); }
225
+ if (!Array.isArray(tmpRecords)) { tmpRecords = [tmpRecords]; }
226
+
227
+ return fCallback(null, { Outputs: { Records: tmpRecords, Count: tmpRecords.length }, Log: [] });
228
+ }
229
+ catch (pEx) { return fCallback(pEx); }
230
+ }
231
+ };
232
+ }
233
+
234
+ _buildTransformRecordsAction()
235
+ {
236
+ return {
237
+ Description: 'Apply a MappingConfiguration to records via TabularTransform',
238
+ SettingsSchema:
239
+ [
240
+ { Name: 'Records', DataType: 'Array', Required: true },
241
+ { Name: 'MappingConfiguration', DataType: 'Object', Required: true },
242
+ { Name: 'EntityName', DataType: 'String', Required: false }
243
+ ],
244
+ Handler: (pWorkItem, pContext, fCallback) =>
245
+ {
246
+ try
247
+ {
248
+ let tmpSettings = pWorkItem.Settings || {};
249
+ let tmpRecords = tmpSettings.Records || [];
250
+ let tmpMapping = tmpSettings.MappingConfiguration || {};
251
+ let tmpEntity = tmpSettings.EntityName || (tmpMapping && tmpMapping.Entity) || 'Record';
252
+ this._transform.transform(tmpRecords, tmpMapping,
253
+ (pErr, pResult) =>
254
+ {
255
+ if (pErr) { return fCallback(pErr); }
256
+ let tmpOut = (pResult && pResult.Records) ? pResult : { Records: pResult || [] };
257
+ return fCallback(null,
258
+ {
259
+ Outputs:
260
+ {
261
+ Records: tmpOut.Records || [],
262
+ Count: (tmpOut.Records || []).length,
263
+ ParsedCount: tmpRecords.length,
264
+ BadRecords: tmpOut.BadRecords || [],
265
+ Entity: tmpEntity
266
+ },
267
+ Log: []
268
+ });
269
+ });
270
+ }
271
+ catch (pEx) { return fCallback(pEx); }
272
+ }
273
+ };
274
+ }
275
+ }
276
+
277
+ module.exports = MeadowIntegrationBeaconProvider;