meadow-integration 1.0.18 → 1.0.20
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/docs/README.md +1 -0
- package/docs/_sidebar.md +6 -1
- package/docs/comprehension-push/configuration.md +308 -0
- package/docs/integration-adapter.md +164 -15
- package/examples/example-comprehension-push.meadow.config.json +23 -0
- package/package.json +4 -2
- package/source/Meadow-Integration.js +21 -1
- package/source/Meadow-Service-Integration-Adapter.js +678 -245
- package/source/Meadow-Service-Integration-GUIDMap.js +19 -2
- package/source/cli/commands/Meadow-Integration-Command-ComprehensionPush.js +210 -38
- package/source/services/clone/Meadow-Service-RestClient.js +46 -0
- package/source/services/parser/Service-FileParser-CSV.js +263 -0
- package/source/services/parser/Service-FileParser-FixedWidth.js +158 -0
- package/source/services/parser/Service-FileParser-JSON.js +255 -0
- package/source/services/parser/Service-FileParser-XLSX.js +194 -0
- package/source/services/parser/Service-FileParser-XML.js +190 -0
- package/source/services/parser/Service-FileParser.js +142 -0
- package/test/Meadow-Integration-ComprehensionPush_test.js +580 -0
|
@@ -65,7 +65,16 @@ class MeadowGUIDMap extends libFableServiceProviderBase
|
|
|
65
65
|
return (this._GUIDMap[pEntity].hasOwnProperty(pGUID)) ? this._GUIDMap[pEntity][pGUID] : false;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Get an ID from a GUID, loading from the server if not already cached.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} pEntity - The entity name
|
|
72
|
+
* @param {string} pGUID - The GUID to look up
|
|
73
|
+
* @param {function} fCallback - Callback (pError, pID)
|
|
74
|
+
* @param {object} [pClient] - Optional REST client to use for the server lookup
|
|
75
|
+
* (defaults to this.fable.MeadowRestClient or this.fable.MeadowCloneRestClient)
|
|
76
|
+
*/
|
|
77
|
+
getIDFromGUIDAsync(pEntity, pGUID, fCallback, pClient)
|
|
69
78
|
{
|
|
70
79
|
if (!this._GUIDMap.hasOwnProperty(pEntity))
|
|
71
80
|
{
|
|
@@ -79,8 +88,16 @@ class MeadowGUIDMap extends libFableServiceProviderBase
|
|
|
79
88
|
}
|
|
80
89
|
else
|
|
81
90
|
{
|
|
91
|
+
// Resolve the REST client to use for the server lookup
|
|
92
|
+
let tmpClient = pClient || this.fable.MeadowRestClient || this.fable.MeadowCloneRestClient;
|
|
93
|
+
|
|
94
|
+
if (!tmpClient || typeof(tmpClient.getEntityByGUID) !== 'function')
|
|
95
|
+
{
|
|
96
|
+
return fCallback(new Error(`No REST client with getEntityByGUID available for GUID lookup of [${pEntity}].[${pGUID}].`), false);
|
|
97
|
+
}
|
|
98
|
+
|
|
82
99
|
// Try to load it from the server
|
|
83
|
-
|
|
100
|
+
tmpClient.getEntityByGUID(pEntity, pGUID,
|
|
84
101
|
(pError, pBody) =>
|
|
85
102
|
{
|
|
86
103
|
if (pError)
|
|
@@ -3,6 +3,8 @@ const libCLICommandLineCommand = require('pict-service-commandlineutility').Serv
|
|
|
3
3
|
const libPath = require('path');
|
|
4
4
|
|
|
5
5
|
const libIntegrationAdapter = require('../../Meadow-Service-Integration-Adapter.js');
|
|
6
|
+
const libMeadowCloneRestClient = require('../../services/clone/Meadow-Service-RestClient.js');
|
|
7
|
+
const libSessionManagerSetup = require('../../Meadow-Integration-SessionManagerSetup.js');
|
|
6
8
|
|
|
7
9
|
class PushComprehensionsViaIntegration extends libCLICommandLineCommand
|
|
8
10
|
{
|
|
@@ -15,17 +17,52 @@ class PushComprehensionsViaIntegration extends libCLICommandLineCommand
|
|
|
15
17
|
this.options.Aliases.push('load');
|
|
16
18
|
this.options.Aliases.push('push');
|
|
17
19
|
|
|
18
|
-
|
|
19
20
|
this.options.CommandArguments.push({ Name: '<comprehension_file>', Description: 'The comprehension file path.' });
|
|
20
21
|
|
|
21
|
-
this.options.CommandOptions.push({ Name: '-p, --prefix [guid_prefix]', Description: 'GUID Prefix for the comprehension push.'});
|
|
22
|
-
this.options.CommandOptions.push({ Name: '-e, --entityguidprefix [entity_guid_prefix]', Description: 'GUID Prefix for each entity.'});
|
|
22
|
+
this.options.CommandOptions.push({ Name: '-p, --prefix [guid_prefix]', Description: 'GUID Prefix for the comprehension push.' });
|
|
23
|
+
this.options.CommandOptions.push({ Name: '-e, --entityguidprefix [entity_guid_prefix]', Description: 'GUID Prefix for each entity.' });
|
|
24
|
+
|
|
25
|
+
this.options.CommandOptions.push({ Name: '-a, --api_server [api_server]', Description: 'The API server URL.' });
|
|
26
|
+
this.options.CommandOptions.push({ Name: '-u, --api_username [api_username]', Description: 'The API username to authenticate with.' });
|
|
27
|
+
this.options.CommandOptions.push({ Name: '-w, --api_password [api_password]', Description: 'The API password to authenticate with.' });
|
|
28
|
+
|
|
29
|
+
this.options.CommandOptions.push({ Name: '--bulkupsert [bulk_upsert]', Description: 'Enable bulk upsert mode (true/false). Default: true.', DefaultValue: 'true' });
|
|
30
|
+
this.options.CommandOptions.push({ Name: '--batchsize [batch_size]', Description: 'Bulk upsert batch size. Default: 100.', DefaultValue: '100' });
|
|
31
|
+
this.options.CommandOptions.push({ Name: '--progressinterval [progress_interval]', Description: 'Per-entity progress log interval (records). Default: 100.', DefaultValue: '100' });
|
|
32
|
+
this.options.CommandOptions.push({ Name: '--metaprogressinterval [meta_progress_interval]', Description: 'Meta (cross-entity) progress log interval. Default: 0 (disabled).', DefaultValue: '0' });
|
|
33
|
+
this.options.CommandOptions.push({ Name: '--allowguidtruncation', Description: 'Allow automatic GUID prefix truncation when GUIDs exceed max length.' });
|
|
34
|
+
this.options.CommandOptions.push({ Name: '--logfile [logfile_path]', Description: 'Path to write log output.' });
|
|
23
35
|
|
|
24
36
|
this.addCommand();
|
|
25
37
|
|
|
26
38
|
this.comprehension = {};
|
|
27
39
|
}
|
|
28
40
|
|
|
41
|
+
_resolveConfig()
|
|
42
|
+
{
|
|
43
|
+
const tmpConfig = JSON.parse(JSON.stringify(this.fable.ProgramConfiguration));
|
|
44
|
+
|
|
45
|
+
// Apply command-line overrides for Source (API)
|
|
46
|
+
if (!tmpConfig.Source)
|
|
47
|
+
{
|
|
48
|
+
tmpConfig.Source = {};
|
|
49
|
+
}
|
|
50
|
+
if (this.CommandOptions.api_server)
|
|
51
|
+
{
|
|
52
|
+
tmpConfig.Source.ServerURL = this.CommandOptions.api_server;
|
|
53
|
+
}
|
|
54
|
+
if (this.CommandOptions.api_username)
|
|
55
|
+
{
|
|
56
|
+
tmpConfig.Source.UserID = this.CommandOptions.api_username;
|
|
57
|
+
}
|
|
58
|
+
if (this.CommandOptions.api_password)
|
|
59
|
+
{
|
|
60
|
+
tmpConfig.Source.Password = this.CommandOptions.api_password;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return tmpConfig;
|
|
64
|
+
}
|
|
65
|
+
|
|
29
66
|
runAdapter(pAnticipate, pAdapter, pDataMap, fMarshalRecord)
|
|
30
67
|
{
|
|
31
68
|
let tmpAdapter = this.fable.servicesMap.IntegrationAdapter[pAdapter];
|
|
@@ -76,56 +113,191 @@ class PushComprehensionsViaIntegration extends libCLICommandLineCommand
|
|
|
76
113
|
pushComprehension(fCallback)
|
|
77
114
|
{
|
|
78
115
|
let tmpComprehensionPath = this.ArgumentString;
|
|
116
|
+
let tmpConfig = this._resolveConfig();
|
|
79
117
|
|
|
80
|
-
let tmpAnticipate = this.fable.newAnticipate();
|
|
81
118
|
this.fable.log.info(`Pushing comprehension file [${tmpComprehensionPath}] to the Meadow Endpoints APIs.`);
|
|
82
119
|
|
|
120
|
+
// --- Setup REST client ---
|
|
121
|
+
this.fable.serviceManager.addServiceType('MeadowCloneRestClient', libMeadowCloneRestClient);
|
|
122
|
+
|
|
123
|
+
let tmpRestClientOptions = {};
|
|
124
|
+
if (tmpConfig.Source && tmpConfig.Source.ServerURL)
|
|
125
|
+
{
|
|
126
|
+
tmpRestClientOptions.ServerURL = tmpConfig.Source.ServerURL;
|
|
127
|
+
}
|
|
128
|
+
if (tmpConfig.Source && tmpConfig.Source.UserID)
|
|
129
|
+
{
|
|
130
|
+
tmpRestClientOptions.UserID = tmpConfig.Source.UserID;
|
|
131
|
+
}
|
|
132
|
+
if (tmpConfig.Source && tmpConfig.Source.Password)
|
|
133
|
+
{
|
|
134
|
+
tmpRestClientOptions.Password = tmpConfig.Source.Password;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.fable.serviceManager.instantiateServiceProvider('MeadowCloneRestClient', tmpRestClientOptions);
|
|
138
|
+
|
|
139
|
+
// --- Initialize SessionManager if configured ---
|
|
140
|
+
let tmpSessionManager = libSessionManagerSetup.initializeSessionManager(this.fable, tmpConfig.SessionManager);
|
|
141
|
+
if (tmpSessionManager)
|
|
142
|
+
{
|
|
143
|
+
libSessionManagerSetup.connectSessionManagerToRestClient(this.fable, this.fable.MeadowCloneRestClient.restClient);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// --- Setup Integration Adapter service type ---
|
|
83
147
|
this.fable.log.info(`Initializing and configuring data integration adapters...`);
|
|
84
148
|
this.fable.serviceManager.addServiceType('IntegrationAdapter', libIntegrationAdapter);
|
|
85
149
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
150
|
+
// --- Resolve adapter options from CLI flags + config ---
|
|
151
|
+
let tmpBulkUpsertEnabled = (this.CommandOptions.bulkupsert !== 'false');
|
|
152
|
+
let tmpBatchSize = parseInt(this.CommandOptions.batchsize) || 100;
|
|
153
|
+
let tmpProgressInterval = parseInt(this.CommandOptions.progressinterval) || 100;
|
|
154
|
+
let tmpMetaProgressInterval = parseInt(this.CommandOptions.metaprogressinterval) || 0;
|
|
155
|
+
let tmpAllowGUIDTruncation = !!this.CommandOptions.allowguidtruncation;
|
|
156
|
+
|
|
157
|
+
// Build shared adapter options
|
|
158
|
+
let tmpAdapterOptions = {
|
|
159
|
+
SimpleMarshal: true,
|
|
160
|
+
ForceMarshal: true,
|
|
161
|
+
BulkUpsertBatchSize: tmpBatchSize,
|
|
162
|
+
RecordThresholdForBulkUpsert: tmpBulkUpsertEnabled ? 1000 : Infinity,
|
|
163
|
+
ProgressLogInterval: tmpProgressInterval,
|
|
164
|
+
AllowGUIDTruncation: tmpAllowGUIDTruncation
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
this.fable.Utility.waterfall(
|
|
168
|
+
[
|
|
169
|
+
(fStageComplete) =>
|
|
90
170
|
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
171
|
+
// Authenticate SessionManager sessions (if configured)
|
|
172
|
+
if (tmpSessionManager)
|
|
173
|
+
{
|
|
174
|
+
this.log.info('Authenticating SessionManager sessions...');
|
|
175
|
+
libSessionManagerSetup.authenticateSessions(this.fable,
|
|
176
|
+
(pError) =>
|
|
177
|
+
{
|
|
178
|
+
if (pError)
|
|
179
|
+
{
|
|
180
|
+
this.log.error('Error authenticating SessionManager sessions.', pError);
|
|
181
|
+
}
|
|
182
|
+
return fStageComplete();
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
else
|
|
186
|
+
{
|
|
187
|
+
return fStageComplete();
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
(fStageComplete) =>
|
|
97
191
|
{
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
192
|
+
// Authenticate with the API server using built-in credentials
|
|
193
|
+
if (tmpConfig.Source && tmpConfig.Source.UserID && tmpConfig.Source.Password)
|
|
194
|
+
{
|
|
195
|
+
this.log.info('Authenticating with API server...');
|
|
196
|
+
this.fable.MeadowCloneRestClient.authenticate(
|
|
197
|
+
(pError, pResponse) =>
|
|
198
|
+
{
|
|
199
|
+
if (pError)
|
|
200
|
+
{
|
|
201
|
+
this.log.error('Error authenticating with API server.', pError);
|
|
202
|
+
}
|
|
203
|
+
else
|
|
204
|
+
{
|
|
205
|
+
this.log.info(`Authenticated with API server as [${tmpConfig.Source.UserID}] at [${tmpConfig.Source.ServerURL}]`);
|
|
206
|
+
}
|
|
207
|
+
return fStageComplete();
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
else
|
|
211
|
+
{
|
|
212
|
+
this.log.info('No Source credentials configured; skipping built-in authentication.');
|
|
213
|
+
return fStageComplete();
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
(fStageComplete) =>
|
|
217
|
+
{
|
|
218
|
+
// Load comprehension file
|
|
219
|
+
try
|
|
220
|
+
{
|
|
221
|
+
this.fable.log.info(`Loading Comprehension File...`);
|
|
222
|
+
tmpComprehensionPath = libPath.resolve(tmpComprehensionPath);
|
|
223
|
+
this.comprehension = require(tmpComprehensionPath);
|
|
224
|
+
return fStageComplete();
|
|
225
|
+
}
|
|
226
|
+
catch (pError)
|
|
227
|
+
{
|
|
228
|
+
this.fable.log.error(`Error loading comprehension file [${tmpComprehensionPath}]: ${pError}`, pError);
|
|
229
|
+
return fStageComplete(pError);
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
(fStageComplete) =>
|
|
233
|
+
{
|
|
234
|
+
this.fable.log.info(`Wiring up Integration Adapters...`);
|
|
107
235
|
|
|
108
|
-
|
|
236
|
+
let tmpIntegrationAdapterSet = Object.keys(this.comprehension);
|
|
109
237
|
|
|
110
|
-
|
|
111
|
-
|
|
238
|
+
// Count total records for meta progress tracking
|
|
239
|
+
let tmpTotalRecords = 0;
|
|
112
240
|
for (let i = 0; i < tmpIntegrationAdapterSet.length; i++)
|
|
113
241
|
{
|
|
114
242
|
let tmpAdapterKey = tmpIntegrationAdapterSet[i];
|
|
115
|
-
|
|
116
|
-
|
|
243
|
+
if (this.comprehension[tmpAdapterKey] && typeof(this.comprehension[tmpAdapterKey]) === 'object')
|
|
244
|
+
{
|
|
245
|
+
tmpTotalRecords += Object.keys(this.comprehension[tmpAdapterKey]).length;
|
|
246
|
+
}
|
|
117
247
|
}
|
|
118
|
-
}
|
|
119
|
-
catch (pError)
|
|
120
|
-
{
|
|
121
|
-
this.fable.log.error(`Error wiring up integration adapters: ${pError}`, pError);
|
|
122
|
-
return fCallback(pError);
|
|
123
|
-
}
|
|
124
248
|
|
|
125
|
-
|
|
126
|
-
|
|
249
|
+
// Start meta progress tracker if interval is configured
|
|
250
|
+
let tmpMetaProgressHash = false;
|
|
251
|
+
if (tmpMetaProgressInterval > 0 && tmpTotalRecords > 0)
|
|
252
|
+
{
|
|
253
|
+
this.fable.instantiateServiceProviderIfNotExists('ProgressTrackerSet');
|
|
254
|
+
tmpMetaProgressHash = this.fable.getUUID();
|
|
255
|
+
this.fable.ProgressTrackerSet.createProgressTracker(tmpMetaProgressHash, tmpTotalRecords);
|
|
256
|
+
this.fable.ProgressTrackerSet.startProgressTracker(tmpMetaProgressHash);
|
|
257
|
+
this.fable.log.info(`Meta progress: ${tmpTotalRecords} total records across ${tmpIntegrationAdapterSet.length} entities.`);
|
|
258
|
+
}
|
|
127
259
|
|
|
128
|
-
|
|
260
|
+
let tmpAnticipate = this.fable.newAnticipate();
|
|
261
|
+
|
|
262
|
+
try
|
|
263
|
+
{
|
|
264
|
+
for (let i = 0; i < tmpIntegrationAdapterSet.length; i++)
|
|
265
|
+
{
|
|
266
|
+
let tmpAdapterKey = tmpIntegrationAdapterSet[i];
|
|
267
|
+
let tmpAdapter = libIntegrationAdapter.getAdapter(this.fable, tmpAdapterKey, this.getCapitalLettersAsString(tmpAdapterKey), tmpAdapterOptions);
|
|
268
|
+
|
|
269
|
+
// Inject the REST client
|
|
270
|
+
tmpAdapter.setRestClient(this.fable.MeadowCloneRestClient);
|
|
271
|
+
|
|
272
|
+
// Wire up meta progress tracking
|
|
273
|
+
if (tmpMetaProgressHash)
|
|
274
|
+
{
|
|
275
|
+
tmpAdapter.MetaProgressTrackerHash = tmpMetaProgressHash;
|
|
276
|
+
tmpAdapter.MetaProgressTrackerLogInterval = tmpMetaProgressInterval;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
this.runAdapter(tmpAnticipate, tmpAdapterKey, this.comprehension[tmpAdapterKey]);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch (pError)
|
|
283
|
+
{
|
|
284
|
+
this.fable.log.error(`Error wiring up integration adapters: ${pError}`, pError);
|
|
285
|
+
return fStageComplete(pError);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
tmpAnticipate.wait(
|
|
289
|
+
(pError) =>
|
|
290
|
+
{
|
|
291
|
+
// End meta progress tracker
|
|
292
|
+
if (tmpMetaProgressHash)
|
|
293
|
+
{
|
|
294
|
+
this.fable.ProgressTrackerSet.endProgressTracker(tmpMetaProgressHash);
|
|
295
|
+
this.fable.ProgressTrackerSet.logProgressTrackerStatus(tmpMetaProgressHash);
|
|
296
|
+
}
|
|
297
|
+
return fStageComplete(pError);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
],
|
|
129
301
|
(pError) =>
|
|
130
302
|
{
|
|
131
303
|
if (pError)
|
|
@@ -140,7 +312,7 @@ class PushComprehensionsViaIntegration extends libCLICommandLineCommand
|
|
|
140
312
|
|
|
141
313
|
onRunAsync(fCallback)
|
|
142
314
|
{
|
|
143
|
-
return this.pushComprehension((pError)=>
|
|
315
|
+
return this.pushComprehension((pError) =>
|
|
144
316
|
{
|
|
145
317
|
return fCallback(pError);
|
|
146
318
|
});
|
|
@@ -155,6 +155,52 @@ class MeadowCloneRestClient extends libFableServiceProviderBase
|
|
|
155
155
|
});
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
upsertEntities(pEntity, pRecordArray, fCallback)
|
|
159
|
+
{
|
|
160
|
+
let tmpRequestOptions = (
|
|
161
|
+
{
|
|
162
|
+
url: `${this.serverURL}${pEntity}s/Upserts`,
|
|
163
|
+
body: pRecordArray,
|
|
164
|
+
});
|
|
165
|
+
tmpRequestOptions = this._prepareRequestOptions(tmpRequestOptions);
|
|
166
|
+
|
|
167
|
+
this.restClient.putJSON(tmpRequestOptions,
|
|
168
|
+
(pError, pResponse, pBody) =>
|
|
169
|
+
{
|
|
170
|
+
if (pError)
|
|
171
|
+
{
|
|
172
|
+
this.log.error(`Error bulk upserting ${pEntity} records: ${pError.message}`);
|
|
173
|
+
}
|
|
174
|
+
return fCallback(pError, pBody);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getEntityByGUID(pEntity, pGUID, fCallback)
|
|
179
|
+
{
|
|
180
|
+
// Meadow's "ReadBy" endpoint uses the plural entity name with pagination:
|
|
181
|
+
// GET /{Entity}s/By/GUID{Entity}/{Value}/{Start}/{Cap}
|
|
182
|
+
let tmpRequestOptions = (
|
|
183
|
+
{
|
|
184
|
+
url: `${this.serverURL}${pEntity}s/By/GUID${pEntity}/${pGUID}/0/1`,
|
|
185
|
+
});
|
|
186
|
+
tmpRequestOptions = this._prepareRequestOptions(tmpRequestOptions);
|
|
187
|
+
|
|
188
|
+
return this.restClient.getJSON(tmpRequestOptions,
|
|
189
|
+
(pError, pResponse, pBody) =>
|
|
190
|
+
{
|
|
191
|
+
if (pError)
|
|
192
|
+
{
|
|
193
|
+
this.log.error(`Error getting ${pEntity} by GUID [${pGUID}]: ${pError.message}`);
|
|
194
|
+
}
|
|
195
|
+
// The /By/ endpoint returns an array; extract the first record for convenience.
|
|
196
|
+
if (Array.isArray(pBody) && pBody.length > 0)
|
|
197
|
+
{
|
|
198
|
+
return fCallback(pError, pBody[0]);
|
|
199
|
+
}
|
|
200
|
+
return fCallback(pError, pBody);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
158
204
|
deleteEntity(pEntity, pIDRecord, fCallback)
|
|
159
205
|
{
|
|
160
206
|
let tmpRequestOptions = (
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
4
|
+
const libFS = require('fs');
|
|
5
|
+
const libReadline = require('readline');
|
|
6
|
+
|
|
7
|
+
const defaultCSVParserOptions = (
|
|
8
|
+
{
|
|
9
|
+
delimiter: ',',
|
|
10
|
+
quoteChar: '"',
|
|
11
|
+
hasHeaders: true,
|
|
12
|
+
skipRows: 0,
|
|
13
|
+
commentPrefix: '',
|
|
14
|
+
trim: true,
|
|
15
|
+
chunkSize: 100
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
class MeadowIntegrationFileParserCSV extends libFableServiceProviderBase
|
|
19
|
+
{
|
|
20
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
21
|
+
{
|
|
22
|
+
let tmpOptions = Object.assign({}, defaultCSVParserOptions, pOptions);
|
|
23
|
+
super(pFable, tmpOptions, pServiceHash);
|
|
24
|
+
|
|
25
|
+
this.serviceType = 'MeadowIntegrationFileParserCSV';
|
|
26
|
+
|
|
27
|
+
this._headers = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse a single CSV line into an array of values.
|
|
32
|
+
* Handles quoted fields (including embedded commas and escaped quotes).
|
|
33
|
+
*
|
|
34
|
+
* @param {string} pLine - Raw CSV line
|
|
35
|
+
* @param {string} pDelimiter - Field delimiter character
|
|
36
|
+
* @param {string} pQuoteChar - Quote character
|
|
37
|
+
* @param {boolean} pTrim - Whether to trim field values
|
|
38
|
+
* @returns {Array<string>} Parsed field values
|
|
39
|
+
*/
|
|
40
|
+
_parseCSVLine(pLine, pDelimiter, pQuoteChar, pTrim)
|
|
41
|
+
{
|
|
42
|
+
let tmpDelimiter = pDelimiter || ',';
|
|
43
|
+
let tmpQuoteChar = pQuoteChar || '"';
|
|
44
|
+
let tmpValues = [];
|
|
45
|
+
let tmpCurrent = '';
|
|
46
|
+
let tmpInQuotes = false;
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < pLine.length; i++)
|
|
49
|
+
{
|
|
50
|
+
let tmpChar = pLine[i];
|
|
51
|
+
|
|
52
|
+
if (tmpChar === tmpQuoteChar)
|
|
53
|
+
{
|
|
54
|
+
if (tmpInQuotes && pLine[i + 1] === tmpQuoteChar)
|
|
55
|
+
{
|
|
56
|
+
// Escaped quote (doubled)
|
|
57
|
+
tmpCurrent += tmpQuoteChar;
|
|
58
|
+
i++;
|
|
59
|
+
}
|
|
60
|
+
else
|
|
61
|
+
{
|
|
62
|
+
tmpInQuotes = !tmpInQuotes;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (tmpChar === tmpDelimiter && !tmpInQuotes)
|
|
66
|
+
{
|
|
67
|
+
tmpValues.push(pTrim ? tmpCurrent.trim() : tmpCurrent);
|
|
68
|
+
tmpCurrent = '';
|
|
69
|
+
}
|
|
70
|
+
else
|
|
71
|
+
{
|
|
72
|
+
tmpCurrent += tmpChar;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
tmpValues.push(pTrim ? tmpCurrent.trim() : tmpCurrent);
|
|
77
|
+
return tmpValues;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse a CSV file using streaming readline.
|
|
82
|
+
* Fires chunkCallback with arrays of records as they accumulate.
|
|
83
|
+
* Fires completionCallback when the file is fully consumed.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} pFilePath - Absolute path to the CSV file
|
|
86
|
+
* @param {object} pOptions - Parser options (overrides instance options)
|
|
87
|
+
* @param {function} pChunkCallback - Called with (pError, pRecords) per chunk
|
|
88
|
+
* @param {function} pCompletionCallback - Called with (pError, pTotalCount) when done
|
|
89
|
+
*/
|
|
90
|
+
parseFile(pFilePath, pOptions, pChunkCallback, pCompletionCallback)
|
|
91
|
+
{
|
|
92
|
+
let tmpOptions = Object.assign({}, this.options, pOptions);
|
|
93
|
+
let tmpChunkSize = tmpOptions.chunkSize || 100;
|
|
94
|
+
let tmpHasHeaders = tmpOptions.hasHeaders !== false;
|
|
95
|
+
let tmpSkipRows = parseInt(tmpOptions.skipRows, 10) || 0;
|
|
96
|
+
let tmpCommentPrefix = tmpOptions.commentPrefix || '';
|
|
97
|
+
let tmpTrim = tmpOptions.trim !== false;
|
|
98
|
+
let tmpDelimiter = tmpOptions.delimiter || ',';
|
|
99
|
+
let tmpQuoteChar = tmpOptions.quoteChar || '"';
|
|
100
|
+
|
|
101
|
+
this._headers = null;
|
|
102
|
+
|
|
103
|
+
let tmpLineIndex = 0;
|
|
104
|
+
let tmpRecordCount = 0;
|
|
105
|
+
let tmpChunkBuffer = [];
|
|
106
|
+
|
|
107
|
+
const tmpReadline = libReadline.createInterface(
|
|
108
|
+
{
|
|
109
|
+
input: libFS.createReadStream(pFilePath),
|
|
110
|
+
crlfDelay: Infinity
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
tmpReadline.on('line',
|
|
114
|
+
(pLine) =>
|
|
115
|
+
{
|
|
116
|
+
// Skip comment lines
|
|
117
|
+
if (tmpCommentPrefix && pLine.startsWith(tmpCommentPrefix))
|
|
118
|
+
{
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Skip header/preamble rows
|
|
123
|
+
if (tmpLineIndex < tmpSkipRows)
|
|
124
|
+
{
|
|
125
|
+
tmpLineIndex++;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let tmpValues = this._parseCSVLine(pLine, tmpDelimiter, tmpQuoteChar, tmpTrim);
|
|
130
|
+
|
|
131
|
+
// First non-skipped, non-comment line becomes headers
|
|
132
|
+
if (tmpHasHeaders && !this._headers)
|
|
133
|
+
{
|
|
134
|
+
this._headers = tmpValues;
|
|
135
|
+
tmpLineIndex++;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let tmpRecord;
|
|
140
|
+
if (this._headers)
|
|
141
|
+
{
|
|
142
|
+
tmpRecord = {};
|
|
143
|
+
for (let i = 0; i < this._headers.length; i++)
|
|
144
|
+
{
|
|
145
|
+
tmpRecord[this._headers[i]] = (tmpValues && tmpValues[i] !== undefined) ? tmpValues[i] : '';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
{
|
|
150
|
+
tmpRecord = tmpValues || [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
tmpChunkBuffer.push(tmpRecord);
|
|
154
|
+
tmpRecordCount++;
|
|
155
|
+
tmpLineIndex++;
|
|
156
|
+
|
|
157
|
+
if (tmpChunkBuffer.length >= tmpChunkSize)
|
|
158
|
+
{
|
|
159
|
+
pChunkCallback(null, tmpChunkBuffer.splice(0, tmpChunkBuffer.length));
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
tmpReadline.on('close',
|
|
164
|
+
() =>
|
|
165
|
+
{
|
|
166
|
+
if (tmpChunkBuffer.length > 0)
|
|
167
|
+
{
|
|
168
|
+
pChunkCallback(null, tmpChunkBuffer.splice(0, tmpChunkBuffer.length));
|
|
169
|
+
}
|
|
170
|
+
return pCompletionCallback(null, tmpRecordCount);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
tmpReadline.on('error',
|
|
174
|
+
(pError) =>
|
|
175
|
+
{
|
|
176
|
+
return pCompletionCallback(pError);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Parse CSV content string into a full array of records.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} pContent - Raw CSV text
|
|
184
|
+
* @param {object} pOptions - Parser options
|
|
185
|
+
* @param {function} fCallback - Called with (pError, pRecords)
|
|
186
|
+
*/
|
|
187
|
+
parseContent(pContent, pOptions, fCallback)
|
|
188
|
+
{
|
|
189
|
+
let tmpOptions = Object.assign({}, this.options, pOptions);
|
|
190
|
+
let tmpHasHeaders = tmpOptions.hasHeaders !== false;
|
|
191
|
+
let tmpSkipRows = parseInt(tmpOptions.skipRows, 10) || 0;
|
|
192
|
+
let tmpCommentPrefix = tmpOptions.commentPrefix || '';
|
|
193
|
+
let tmpTrim = tmpOptions.trim !== false;
|
|
194
|
+
let tmpDelimiter = tmpOptions.delimiter || ',';
|
|
195
|
+
let tmpQuoteChar = tmpOptions.quoteChar || '"';
|
|
196
|
+
|
|
197
|
+
let tmpLines = pContent.split('\n');
|
|
198
|
+
let tmpHeaders = null;
|
|
199
|
+
let tmpRecords = [];
|
|
200
|
+
let tmpLineIndex = 0;
|
|
201
|
+
|
|
202
|
+
for (let i = 0; i < tmpLines.length; i++)
|
|
203
|
+
{
|
|
204
|
+
let tmpLine = tmpLines[i];
|
|
205
|
+
|
|
206
|
+
// Strip trailing \r for Windows line endings
|
|
207
|
+
if (tmpLine.length > 0 && tmpLine[tmpLine.length - 1] === '\r')
|
|
208
|
+
{
|
|
209
|
+
tmpLine = tmpLine.slice(0, -1);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Skip comment lines
|
|
213
|
+
if (tmpCommentPrefix && tmpLine.startsWith(tmpCommentPrefix))
|
|
214
|
+
{
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Skip preamble rows
|
|
219
|
+
if (tmpLineIndex < tmpSkipRows)
|
|
220
|
+
{
|
|
221
|
+
tmpLineIndex++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Skip blank lines
|
|
226
|
+
if (!tmpLine || tmpLine.trim().length === 0)
|
|
227
|
+
{
|
|
228
|
+
tmpLineIndex++;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let tmpValues = this._parseCSVLine(tmpLine, tmpDelimiter, tmpQuoteChar, tmpTrim);
|
|
233
|
+
|
|
234
|
+
if (tmpHasHeaders && !tmpHeaders)
|
|
235
|
+
{
|
|
236
|
+
tmpHeaders = tmpValues;
|
|
237
|
+
tmpLineIndex++;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let tmpRecord;
|
|
242
|
+
if (tmpHeaders)
|
|
243
|
+
{
|
|
244
|
+
tmpRecord = {};
|
|
245
|
+
for (let j = 0; j < tmpHeaders.length; j++)
|
|
246
|
+
{
|
|
247
|
+
tmpRecord[tmpHeaders[j]] = (tmpValues && tmpValues[j] !== undefined) ? tmpValues[j] : '';
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else
|
|
251
|
+
{
|
|
252
|
+
tmpRecord = tmpValues || [];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
tmpRecords.push(tmpRecord);
|
|
256
|
+
tmpLineIndex++;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return fCallback(null, tmpRecords);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = MeadowIntegrationFileParserCSV;
|