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.
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
4
+ const libFS = require('fs');
5
+ const libReadline = require('readline');
6
+
7
+ const defaultFixedWidthParserOptions = (
8
+ {
9
+ skipLines: 0,
10
+ columns: []
11
+ });
12
+
13
+ class MeadowIntegrationFileParserFixedWidth extends libFableServiceProviderBase
14
+ {
15
+ constructor(pFable, pOptions, pServiceHash)
16
+ {
17
+ let tmpOptions = Object.assign({}, defaultFixedWidthParserOptions, pOptions);
18
+ super(pFable, tmpOptions, pServiceHash);
19
+
20
+ this.serviceType = 'MeadowIntegrationFileParserFixedWidth';
21
+ }
22
+
23
+ /**
24
+ * Extract fields from a fixed-width line using a columns definition.
25
+ * Column start positions are 1-based.
26
+ *
27
+ * @param {string} pLine - Raw text line
28
+ * @param {Array} pColumns - Array of {name, start, width}
29
+ * @returns {object} Extracted record
30
+ */
31
+ _parseLine(pLine, pColumns)
32
+ {
33
+ let tmpRecord = {};
34
+ for (let i = 0; i < pColumns.length; i++)
35
+ {
36
+ let tmpCol = pColumns[i];
37
+ // start is 1-based
38
+ let tmpStartIdx = (parseInt(tmpCol.start, 10) || 1) - 1;
39
+ let tmpWidth = parseInt(tmpCol.width, 10) || 0;
40
+ let tmpValue = pLine.substring(tmpStartIdx, tmpStartIdx + tmpWidth).trim();
41
+ tmpRecord[tmpCol.name] = tmpValue;
42
+ }
43
+ return tmpRecord;
44
+ }
45
+
46
+ /**
47
+ * Parse a fixed-width file using streaming readline.
48
+ *
49
+ * @param {string} pFilePath - Absolute path to the fixed-width file
50
+ * @param {object} pOptions - Parser options: skipLines, columns, chunkSize
51
+ * @param {function} pChunkCallback - Called with (pError, pRecords) per chunk
52
+ * @param {function} pCompletionCallback - Called with (pError, pTotalCount) when done
53
+ */
54
+ parseFile(pFilePath, pOptions, pChunkCallback, pCompletionCallback)
55
+ {
56
+ let tmpOptions = Object.assign({}, this.options, pOptions);
57
+ let tmpColumns = tmpOptions.columns || [];
58
+ let tmpSkipLines = parseInt(tmpOptions.skipLines, 10) || 0;
59
+ let tmpChunkSize = parseInt(tmpOptions.chunkSize, 10) || 100;
60
+
61
+ if (!tmpColumns || tmpColumns.length === 0)
62
+ {
63
+ return pCompletionCallback(new Error('FixedWidth parser requires options.columns array of {name, start, width}'));
64
+ }
65
+
66
+ let tmpLineIndex = 0;
67
+ let tmpRecordCount = 0;
68
+ let tmpChunkBuffer = [];
69
+
70
+ const tmpReadline = libReadline.createInterface(
71
+ {
72
+ input: libFS.createReadStream(pFilePath),
73
+ crlfDelay: Infinity
74
+ });
75
+
76
+ tmpReadline.on('line',
77
+ (pLine) =>
78
+ {
79
+ if (tmpLineIndex < tmpSkipLines)
80
+ {
81
+ tmpLineIndex++;
82
+ return;
83
+ }
84
+
85
+ // Skip blank lines
86
+ if (!pLine || pLine.trim().length === 0)
87
+ {
88
+ tmpLineIndex++;
89
+ return;
90
+ }
91
+
92
+ let tmpRecord = this._parseLine(pLine, tmpColumns);
93
+ tmpChunkBuffer.push(tmpRecord);
94
+ tmpRecordCount++;
95
+ tmpLineIndex++;
96
+
97
+ if (tmpChunkBuffer.length >= tmpChunkSize)
98
+ {
99
+ pChunkCallback(null, tmpChunkBuffer.splice(0, tmpChunkBuffer.length));
100
+ }
101
+ });
102
+
103
+ tmpReadline.on('close',
104
+ () =>
105
+ {
106
+ if (tmpChunkBuffer.length > 0)
107
+ {
108
+ pChunkCallback(null, tmpChunkBuffer.splice(0, tmpChunkBuffer.length));
109
+ }
110
+ return pCompletionCallback(null, tmpRecordCount);
111
+ });
112
+
113
+ tmpReadline.on('error',
114
+ (pError) =>
115
+ {
116
+ return pCompletionCallback(pError);
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Parse fixed-width content string into a full array of records.
122
+ *
123
+ * @param {string} pContent - Raw fixed-width text
124
+ * @param {object} pOptions - Parser options
125
+ * @param {function} fCallback - Called with (pError, pRecords)
126
+ */
127
+ parseContent(pContent, pOptions, fCallback)
128
+ {
129
+ let tmpOptions = Object.assign({}, this.options, pOptions);
130
+ let tmpColumns = tmpOptions.columns || [];
131
+ let tmpSkipLines = parseInt(tmpOptions.skipLines, 10) || 0;
132
+
133
+ if (!tmpColumns || tmpColumns.length === 0)
134
+ {
135
+ return fCallback(new Error('FixedWidth parser requires options.columns array of {name, start, width}'));
136
+ }
137
+
138
+ let tmpLines = pContent.split('\n');
139
+ let tmpRecords = [];
140
+
141
+ for (let i = tmpSkipLines; i < tmpLines.length; i++)
142
+ {
143
+ let tmpLine = tmpLines[i];
144
+
145
+ // Skip blank lines
146
+ if (!tmpLine || tmpLine.trim().length === 0)
147
+ {
148
+ continue;
149
+ }
150
+
151
+ tmpRecords.push(this._parseLine(tmpLine, tmpColumns));
152
+ }
153
+
154
+ return fCallback(null, tmpRecords);
155
+ }
156
+ }
157
+
158
+ module.exports = MeadowIntegrationFileParserFixedWidth;
@@ -0,0 +1,255 @@
1
+ 'use strict';
2
+
3
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
4
+ const libFS = require('fs');
5
+
6
+ const defaultJSONParserOptions = (
7
+ {
8
+ rootPath: '',
9
+ flattenNested: false,
10
+ flattenDelimiter: '_'
11
+ });
12
+
13
+ class MeadowIntegrationFileParserJSON extends libFableServiceProviderBase
14
+ {
15
+ constructor(pFable, pOptions, pServiceHash)
16
+ {
17
+ let tmpOptions = Object.assign({}, defaultJSONParserOptions, pOptions);
18
+ super(pFable, tmpOptions, pServiceHash);
19
+
20
+ this.serviceType = 'MeadowIntegrationFileParserJSON';
21
+ }
22
+
23
+ /**
24
+ * Navigate a nested object using a dot-separated path with optional
25
+ * array index notation (e.g. "Results.series[0].data").
26
+ *
27
+ * @param {object} pObject - The object to navigate
28
+ * @param {string} pPath - Dot-separated path, segments may include [n]
29
+ * @returns {*} The resolved value, or null if the path is invalid
30
+ */
31
+ _resolveDataPath(pObject, pPath)
32
+ {
33
+ if (!pPath || typeof pPath !== 'string')
34
+ {
35
+ return pObject;
36
+ }
37
+
38
+ let tmpSegments = pPath.split('.');
39
+ let tmpCurrent = pObject;
40
+
41
+ for (let i = 0; i < tmpSegments.length; i++)
42
+ {
43
+ if (tmpCurrent === null || tmpCurrent === undefined || typeof tmpCurrent !== 'object')
44
+ {
45
+ return null;
46
+ }
47
+
48
+ let tmpSegment = tmpSegments[i];
49
+ // Check for array index notation: name[index]
50
+ let tmpMatch = tmpSegment.match(/^([^\[]+)\[(\d+)\]$/);
51
+ if (tmpMatch)
52
+ {
53
+ let tmpKey = tmpMatch[1];
54
+ let tmpIndex = parseInt(tmpMatch[2], 10);
55
+ if (!(tmpKey in tmpCurrent) || !Array.isArray(tmpCurrent[tmpKey]))
56
+ {
57
+ return null;
58
+ }
59
+ tmpCurrent = tmpCurrent[tmpKey][tmpIndex];
60
+ }
61
+ else
62
+ {
63
+ if (!(tmpSegment in tmpCurrent))
64
+ {
65
+ return null;
66
+ }
67
+ tmpCurrent = tmpCurrent[tmpSegment];
68
+ }
69
+ }
70
+
71
+ return tmpCurrent;
72
+ }
73
+
74
+ /**
75
+ * Flatten a nested object into a single-level object using a delimiter.
76
+ *
77
+ * @param {object} pObject - Nested object
78
+ * @param {string} pDelimiter - Key delimiter (default '_')
79
+ * @param {string} pPrefix - Key prefix for recursion
80
+ * @returns {object} Flat object
81
+ */
82
+ _flattenObject(pObject, pDelimiter, pPrefix)
83
+ {
84
+ let tmpDelimiter = pDelimiter || '_';
85
+ let tmpPrefix = pPrefix || '';
86
+ let tmpResult = {};
87
+
88
+ let tmpKeys = Object.keys(pObject);
89
+ for (let i = 0; i < tmpKeys.length; i++)
90
+ {
91
+ let tmpKey = tmpKeys[i];
92
+ let tmpFullKey = tmpPrefix ? `${tmpPrefix}${tmpDelimiter}${tmpKey}` : tmpKey;
93
+ let tmpValue = pObject[tmpKey];
94
+
95
+ if (tmpValue !== null && typeof tmpValue === 'object' && !Array.isArray(tmpValue))
96
+ {
97
+ let tmpNested = this._flattenObject(tmpValue, tmpDelimiter, tmpFullKey);
98
+ let tmpNestedKeys = Object.keys(tmpNested);
99
+ for (let j = 0; j < tmpNestedKeys.length; j++)
100
+ {
101
+ tmpResult[tmpNestedKeys[j]] = tmpNested[tmpNestedKeys[j]];
102
+ }
103
+ }
104
+ else
105
+ {
106
+ tmpResult[tmpFullKey] = tmpValue;
107
+ }
108
+ }
109
+
110
+ return tmpResult;
111
+ }
112
+
113
+ /**
114
+ * Resolve parsed JSON to a records array, applying rootPath navigation.
115
+ *
116
+ * @param {*} pParsed - Parsed JSON value
117
+ * @param {object} pOptions - Parser options
118
+ * @returns {Array|null} Array of records or null on failure
119
+ */
120
+ _resolveRecords(pParsed, pOptions)
121
+ {
122
+ let tmpData = pParsed;
123
+
124
+ if (pOptions && pOptions.rootPath)
125
+ {
126
+ tmpData = this._resolveDataPath(pParsed, pOptions.rootPath);
127
+ if (tmpData === null || tmpData === undefined)
128
+ {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ let tmpRecords;
134
+ if (Array.isArray(tmpData))
135
+ {
136
+ tmpRecords = tmpData;
137
+ }
138
+ else if (typeof tmpData === 'object' && tmpData !== null)
139
+ {
140
+ // Common envelope keys
141
+ if (Array.isArray(tmpData.data))
142
+ {
143
+ tmpRecords = tmpData.data;
144
+ }
145
+ else if (Array.isArray(tmpData.records))
146
+ {
147
+ tmpRecords = tmpData.records;
148
+ }
149
+ else if (Array.isArray(tmpData.rows))
150
+ {
151
+ tmpRecords = tmpData.rows;
152
+ }
153
+ else
154
+ {
155
+ tmpRecords = [tmpData];
156
+ }
157
+ }
158
+ else
159
+ {
160
+ return null;
161
+ }
162
+
163
+ return tmpRecords;
164
+ }
165
+
166
+ /**
167
+ * Parse a JSON file into an array of records.
168
+ * Reads the entire file into memory.
169
+ *
170
+ * @param {string} pFilePath - Absolute path to the JSON file
171
+ * @param {object} pOptions - Parser options
172
+ * @param {function} pChunkCallback - Called with (pError, pRecords) once with all records
173
+ * @param {function} pCompletionCallback - Called with (pError, pTotalCount) when done
174
+ */
175
+ parseFile(pFilePath, pOptions, pChunkCallback, pCompletionCallback)
176
+ {
177
+ let tmpOptions = Object.assign({}, this.options, pOptions);
178
+
179
+ let tmpContent;
180
+ try
181
+ {
182
+ tmpContent = libFS.readFileSync(pFilePath, 'utf8');
183
+ }
184
+ catch (pError)
185
+ {
186
+ return pCompletionCallback(new Error(`JSON file read error: ${pError.message}`));
187
+ }
188
+
189
+ this.parseContent(tmpContent, tmpOptions,
190
+ (pError, pRecords) =>
191
+ {
192
+ if (pError)
193
+ {
194
+ return pCompletionCallback(pError);
195
+ }
196
+ pChunkCallback(null, pRecords);
197
+ return pCompletionCallback(null, pRecords.length);
198
+ });
199
+ }
200
+
201
+ /**
202
+ * Parse JSON content string into a full array of records.
203
+ *
204
+ * @param {string} pContent - Raw JSON text
205
+ * @param {object} pOptions - Parser options
206
+ * @param {function} fCallback - Called with (pError, pRecords)
207
+ */
208
+ parseContent(pContent, pOptions, fCallback)
209
+ {
210
+ let tmpOptions = Object.assign({}, this.options, pOptions);
211
+ let tmpFlattenNested = tmpOptions.flattenNested || false;
212
+ let tmpFlattenDelimiter = tmpOptions.flattenDelimiter || '_';
213
+
214
+ let tmpParsed;
215
+ try
216
+ {
217
+ tmpParsed = JSON.parse(pContent);
218
+ }
219
+ catch (pError)
220
+ {
221
+ return fCallback(new Error(`JSON parse error: ${pError.message}`));
222
+ }
223
+
224
+ let tmpRecords = this._resolveRecords(tmpParsed, tmpOptions);
225
+ if (tmpRecords === null)
226
+ {
227
+ if (tmpOptions.rootPath)
228
+ {
229
+ return fCallback(new Error(`rootPath '${tmpOptions.rootPath}' not found in JSON content`));
230
+ }
231
+ return fCallback(new Error(`Could not resolve records from JSON content`));
232
+ }
233
+
234
+ if (tmpFlattenNested)
235
+ {
236
+ let tmpFlattened = [];
237
+ for (let i = 0; i < tmpRecords.length; i++)
238
+ {
239
+ if (tmpRecords[i] !== null && typeof tmpRecords[i] === 'object')
240
+ {
241
+ tmpFlattened.push(this._flattenObject(tmpRecords[i], tmpFlattenDelimiter));
242
+ }
243
+ else
244
+ {
245
+ tmpFlattened.push(tmpRecords[i]);
246
+ }
247
+ }
248
+ return fCallback(null, tmpFlattened);
249
+ }
250
+
251
+ return fCallback(null, tmpRecords);
252
+ }
253
+ }
254
+
255
+ module.exports = MeadowIntegrationFileParserJSON;
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+
3
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
4
+ const libFS = require('fs');
5
+
6
+ const defaultXLSXParserOptions = (
7
+ {
8
+ sheetName: '',
9
+ sheetIndex: 0,
10
+ headerRow: 1,
11
+ dataStartRow: 2,
12
+ maxFileSizeMB: 50
13
+ });
14
+
15
+ class MeadowIntegrationFileParserXLSX extends libFableServiceProviderBase
16
+ {
17
+ constructor(pFable, pOptions, pServiceHash)
18
+ {
19
+ let tmpOptions = Object.assign({}, defaultXLSXParserOptions, pOptions);
20
+ super(pFable, tmpOptions, pServiceHash);
21
+
22
+ this.serviceType = 'MeadowIntegrationFileParserXLSX';
23
+ }
24
+
25
+ /**
26
+ * Parse an XLSX file into an array of records.
27
+ * Entire file is read into memory. Enforces maxFileSizeMB guard.
28
+ *
29
+ * @param {string} pFilePath - Absolute path to the XLSX file
30
+ * @param {object} pOptions - Parser options
31
+ * @param {function} pChunkCallback - Called with (pError, pRecords) once with all records
32
+ * @param {function} pCompletionCallback - Called with (pError, pTotalCount) when done
33
+ */
34
+ parseFile(pFilePath, pOptions, pChunkCallback, pCompletionCallback)
35
+ {
36
+ let tmpOptions = Object.assign({}, this.options, pOptions);
37
+ let tmpMaxFileSizeMB = parseFloat(tmpOptions.maxFileSizeMB) || 50;
38
+ let tmpMaxBytes = tmpMaxFileSizeMB * 1024 * 1024;
39
+
40
+ let tmpStat;
41
+ try
42
+ {
43
+ tmpStat = libFS.statSync(pFilePath);
44
+ }
45
+ catch (pError)
46
+ {
47
+ return pCompletionCallback(new Error(`XLSX file stat error: ${pError.message}`));
48
+ }
49
+
50
+ if (tmpStat.size > tmpMaxBytes)
51
+ {
52
+ return pCompletionCallback(new Error(`XLSX file size ${(tmpStat.size / 1024 / 1024).toFixed(1)}MB exceeds maxFileSizeMB limit of ${tmpMaxFileSizeMB}MB`));
53
+ }
54
+
55
+ let tmpBuffer;
56
+ try
57
+ {
58
+ tmpBuffer = libFS.readFileSync(pFilePath);
59
+ }
60
+ catch (pError)
61
+ {
62
+ return pCompletionCallback(new Error(`XLSX file read error: ${pError.message}`));
63
+ }
64
+
65
+ this._parseBuffer(tmpBuffer, tmpOptions,
66
+ (pError, pRecords) =>
67
+ {
68
+ if (pError)
69
+ {
70
+ return pCompletionCallback(pError);
71
+ }
72
+ pChunkCallback(null, pRecords);
73
+ return pCompletionCallback(null, pRecords.length);
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Parse XLSX content (Buffer) into a full array of records.
79
+ * Content must be a Buffer containing xlsx file bytes.
80
+ *
81
+ * @param {Buffer|string} pContent - XLSX file as Buffer (or base64 string)
82
+ * @param {object} pOptions - Parser options
83
+ * @param {function} fCallback - Called with (pError, pRecords)
84
+ */
85
+ parseContent(pContent, pOptions, fCallback)
86
+ {
87
+ let tmpOptions = Object.assign({}, this.options, pOptions);
88
+ let tmpBuffer = Buffer.isBuffer(pContent) ? pContent : Buffer.from(pContent, 'base64');
89
+ return this._parseBuffer(tmpBuffer, tmpOptions, fCallback);
90
+ }
91
+
92
+ /**
93
+ * Internal: parse an xlsx Buffer into records using the xlsx library.
94
+ *
95
+ * @param {Buffer} pBuffer - XLSX bytes
96
+ * @param {object} pOptions - Merged options
97
+ * @param {function} fCallback - Called with (pError, pRecords)
98
+ */
99
+ _parseBuffer(pBuffer, pOptions, fCallback)
100
+ {
101
+ let tmpXLSX;
102
+ try
103
+ {
104
+ tmpXLSX = require('xlsx');
105
+ }
106
+ catch (pError)
107
+ {
108
+ return fCallback(new Error(`xlsx library not available: ${pError.message}`));
109
+ }
110
+
111
+ let tmpWorkbook;
112
+ try
113
+ {
114
+ tmpWorkbook = tmpXLSX.read(pBuffer, { type: 'buffer' });
115
+ }
116
+ catch (pError)
117
+ {
118
+ return fCallback(new Error(`XLSX parse error: ${pError.message}`));
119
+ }
120
+
121
+ // Determine sheet to use
122
+ let tmpSheetName;
123
+ if (pOptions.sheetName && typeof pOptions.sheetName === 'string' && pOptions.sheetName.length > 0)
124
+ {
125
+ tmpSheetName = pOptions.sheetName;
126
+ }
127
+ else
128
+ {
129
+ let tmpSheetIndex = parseInt(pOptions.sheetIndex, 10) || 0;
130
+ tmpSheetName = tmpWorkbook.SheetNames[tmpSheetIndex];
131
+ }
132
+
133
+ if (!tmpSheetName || !tmpWorkbook.Sheets[tmpSheetName])
134
+ {
135
+ return fCallback(new Error(`XLSX sheet '${tmpSheetName}' not found in workbook`));
136
+ }
137
+
138
+ let tmpSheet = tmpWorkbook.Sheets[tmpSheetName];
139
+ let tmpHeaderRow = parseInt(pOptions.headerRow, 10) || 1;
140
+ let tmpDataStartRow = parseInt(pOptions.dataStartRow, 10) || 2;
141
+
142
+ // When headerRow and dataStartRow are at their defaults (1 and 2),
143
+ // use xlsx's built-in sheet_to_json which handles this automatically
144
+ if (tmpHeaderRow === 1 && tmpDataStartRow === 2)
145
+ {
146
+ try
147
+ {
148
+ let tmpRecords = tmpXLSX.utils.sheet_to_json(tmpSheet);
149
+ return fCallback(null, tmpRecords);
150
+ }
151
+ catch (pError)
152
+ {
153
+ return fCallback(new Error(`XLSX sheet_to_json error: ${pError.message}`));
154
+ }
155
+ }
156
+
157
+ // Custom header/data row offsets: read as raw array first
158
+ try
159
+ {
160
+ let tmpRawRows = tmpXLSX.utils.sheet_to_json(tmpSheet, { header: 1 });
161
+
162
+ // Header row is 1-based; convert to 0-based index
163
+ let tmpHeaderIdx = tmpHeaderRow - 1;
164
+ let tmpDataIdx = tmpDataStartRow - 1;
165
+
166
+ if (tmpHeaderIdx >= tmpRawRows.length)
167
+ {
168
+ return fCallback(new Error(`XLSX headerRow ${tmpHeaderRow} is beyond sheet row count`));
169
+ }
170
+
171
+ let tmpHeaders = tmpRawRows[tmpHeaderIdx];
172
+ let tmpRecords = [];
173
+
174
+ for (let i = tmpDataIdx; i < tmpRawRows.length; i++)
175
+ {
176
+ let tmpRow = tmpRawRows[i];
177
+ let tmpRecord = {};
178
+ for (let j = 0; j < tmpHeaders.length; j++)
179
+ {
180
+ tmpRecord[tmpHeaders[j]] = (tmpRow && tmpRow[j] !== undefined) ? tmpRow[j] : '';
181
+ }
182
+ tmpRecords.push(tmpRecord);
183
+ }
184
+
185
+ return fCallback(null, tmpRecords);
186
+ }
187
+ catch (pError)
188
+ {
189
+ return fCallback(new Error(`XLSX custom row parse error: ${pError.message}`));
190
+ }
191
+ }
192
+ }
193
+
194
+ module.exports = MeadowIntegrationFileParserXLSX;