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.
@@ -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
- getIDFromGUIDAsync(pEntity, pGUID, fCallback)
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
- this.fable.MeadowRestClient.getEntityByGUID(pEntity, pGUID,
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
- tmpAnticipate.anticipate(
87
- function (fCallback)
88
- {
89
- try
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
- this.fable.log.info(`Loading Comprehension File...`);
92
- tmpComprehensionPath = libPath.resolve(tmpComprehensionPath);
93
- this.comprehension = require(tmpComprehensionPath);
94
- return fCallback();
95
- }
96
- catch(pError)
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
- this.fable.log.error(`Error loading comprehension file [${tmpComprehensionPath}]: ${pError}`, pError);
99
- return fCallback(pError);
100
- }
101
- }.bind(this));
102
-
103
- tmpAnticipate.anticipate(
104
- (fCallback) =>
105
- {
106
- this.fable.log.info(`Wiring up Integration Adapters...`);
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
- let tmpIntegrationAdapterSet = Object.keys(this.comprehension);
236
+ let tmpIntegrationAdapterSet = Object.keys(this.comprehension);
109
237
 
110
- try
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
- libIntegrationAdapter.getAdapter(this.fable, tmpAdapterKey, this.getCapitalLettersAsString(tmpAdapterKey), { SimpleMarshal: true, ForceMarshal: true });
116
- this.runAdapter(tmpAnticipate, tmpAdapterKey, this.comprehension[tmpAdapterKey]);
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
- return fCallback();
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
- tmpAnticipate.wait(
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;