@grepr/cli 1.1.0
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/LICENSE +21 -0
- package/README.md +71 -0
- package/build/dist/commands/base-command.d.ts +13 -0
- package/build/dist/commands/base-command.d.ts.map +1 -0
- package/build/dist/commands/base-command.js +28 -0
- package/build/dist/commands/base-command.js.map +1 -0
- package/build/dist/commands/config-command.d.ts +19 -0
- package/build/dist/commands/config-command.d.ts.map +1 -0
- package/build/dist/commands/config-command.js +108 -0
- package/build/dist/commands/config-command.js.map +1 -0
- package/build/dist/commands/crud-command.d.ts +117 -0
- package/build/dist/commands/crud-command.d.ts.map +1 -0
- package/build/dist/commands/crud-command.js +264 -0
- package/build/dist/commands/crud-command.js.map +1 -0
- package/build/dist/commands/dataset-command.d.ts +45 -0
- package/build/dist/commands/dataset-command.d.ts.map +1 -0
- package/build/dist/commands/dataset-command.js +139 -0
- package/build/dist/commands/dataset-command.js.map +1 -0
- package/build/dist/commands/integration-command.d.ts +30 -0
- package/build/dist/commands/integration-command.d.ts.map +1 -0
- package/build/dist/commands/integration-command.js +80 -0
- package/build/dist/commands/integration-command.js.map +1 -0
- package/build/dist/commands/job-command.d.ts +92 -0
- package/build/dist/commands/job-command.d.ts.map +1 -0
- package/build/dist/commands/job-command.js +294 -0
- package/build/dist/commands/job-command.js.map +1 -0
- package/build/dist/commands/list-command.d.ts +69 -0
- package/build/dist/commands/list-command.d.ts.map +1 -0
- package/build/dist/commands/list-command.js +130 -0
- package/build/dist/commands/list-command.js.map +1 -0
- package/build/dist/commands/query-command.d.ts +20 -0
- package/build/dist/commands/query-command.d.ts.map +1 -0
- package/build/dist/commands/query-command.js +167 -0
- package/build/dist/commands/query-command.js.map +1 -0
- package/build/dist/grepr.d.ts +3 -0
- package/build/dist/grepr.d.ts.map +1 -0
- package/build/dist/grepr.js +155 -0
- package/build/dist/grepr.js.map +1 -0
- package/build/dist/lib/api-client-factory.d.ts +21 -0
- package/build/dist/lib/api-client-factory.d.ts.map +1 -0
- package/build/dist/lib/api-client-factory.js +23 -0
- package/build/dist/lib/api-client-factory.js.map +1 -0
- package/build/dist/lib/api-client.d.ts +2 -0
- package/build/dist/lib/api-client.d.ts.map +1 -0
- package/build/dist/lib/api-client.js +3 -0
- package/build/dist/lib/api-client.js.map +1 -0
- package/build/dist/lib/auth.d.ts +104 -0
- package/build/dist/lib/auth.d.ts.map +1 -0
- package/build/dist/lib/auth.js +312 -0
- package/build/dist/lib/auth.js.map +1 -0
- package/build/dist/lib/command-registry.d.ts +35 -0
- package/build/dist/lib/command-registry.d.ts.map +1 -0
- package/build/dist/lib/command-registry.js +33 -0
- package/build/dist/lib/command-registry.js.map +1 -0
- package/build/dist/lib/config.d.ts +40 -0
- package/build/dist/lib/config.d.ts.map +1 -0
- package/build/dist/lib/config.js +114 -0
- package/build/dist/lib/config.js.map +1 -0
- package/build/dist/lib/grepr-api-client.d.ts +216 -0
- package/build/dist/lib/grepr-api-client.d.ts.map +1 -0
- package/build/dist/lib/grepr-api-client.js +424 -0
- package/build/dist/lib/grepr-api-client.js.map +1 -0
- package/build/dist/lib/heartbeat.d.ts +17 -0
- package/build/dist/lib/heartbeat.d.ts.map +1 -0
- package/build/dist/lib/heartbeat.js +56 -0
- package/build/dist/lib/heartbeat.js.map +1 -0
- package/build/dist/lib/json-formatter.d.ts +135 -0
- package/build/dist/lib/json-formatter.d.ts.map +1 -0
- package/build/dist/lib/json-formatter.js +658 -0
- package/build/dist/lib/json-formatter.js.map +1 -0
- package/build/dist/lib/parser.d.ts +26 -0
- package/build/dist/lib/parser.d.ts.map +1 -0
- package/build/dist/lib/parser.js +95 -0
- package/build/dist/lib/parser.js.map +1 -0
- package/build/dist/lib/streaming-job-executor.d.ts +31 -0
- package/build/dist/lib/streaming-job-executor.d.ts.map +1 -0
- package/build/dist/lib/streaming-job-executor.js +281 -0
- package/build/dist/lib/streaming-job-executor.js.map +1 -0
- package/build/dist/lib/time-utils.d.ts +28 -0
- package/build/dist/lib/time-utils.d.ts.map +1 -0
- package/build/dist/lib/time-utils.js +87 -0
- package/build/dist/lib/time-utils.js.map +1 -0
- package/build/dist/openapi/openApiTypes.d.ts +10430 -0
- package/build/dist/openapi/openApiTypes.d.ts.map +1 -0
- package/build/dist/openapi/openApiTypes.js +571 -0
- package/build/dist/openapi/openApiTypes.js.map +1 -0
- package/build/dist/types.d.ts +168 -0
- package/build/dist/types.d.ts.map +1 -0
- package/build/dist/types.js +20 -0
- package/build/dist/types.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
/**
|
|
3
|
+
* Generic formatter for any JSON objects (jobs, integrations, datasets, etc.)
|
|
4
|
+
* Replaces the LogEvent-specific OutputFormatter
|
|
5
|
+
*/
|
|
6
|
+
export class JsonFormatter {
|
|
7
|
+
options;
|
|
8
|
+
tableData = [];
|
|
9
|
+
tableHeaders = [];
|
|
10
|
+
csvHeaders = [];
|
|
11
|
+
csvHeadersPrinted = false;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
this.reset();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Reset formatter state
|
|
18
|
+
*/
|
|
19
|
+
reset() {
|
|
20
|
+
this.tableData = [];
|
|
21
|
+
this.tableHeaders = [];
|
|
22
|
+
this.csvHeaders = [];
|
|
23
|
+
this.csvHeadersPrinted = false;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Format a single object based on selected format
|
|
27
|
+
*/
|
|
28
|
+
formatObject(data) {
|
|
29
|
+
switch (this.options.format) {
|
|
30
|
+
case 'table':
|
|
31
|
+
return this.addToTable(data);
|
|
32
|
+
case 'csv':
|
|
33
|
+
return this.addToCsv(data);
|
|
34
|
+
case 'pretty':
|
|
35
|
+
return this.formatPrettyJson(data);
|
|
36
|
+
case 'raw':
|
|
37
|
+
return JSON.stringify(data);
|
|
38
|
+
case 'compact':
|
|
39
|
+
return JSON.stringify(data, null, 0);
|
|
40
|
+
default:
|
|
41
|
+
return JSON.stringify(data, null, 2);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Format an array of objects
|
|
46
|
+
*/
|
|
47
|
+
formatObjects(data) {
|
|
48
|
+
if (!data || data.length === 0) {
|
|
49
|
+
return 'No data to display';
|
|
50
|
+
}
|
|
51
|
+
this.reset();
|
|
52
|
+
switch (this.options.format) {
|
|
53
|
+
case 'table':
|
|
54
|
+
// Accumulate all data then render
|
|
55
|
+
data.forEach(obj => this.addToTable(obj));
|
|
56
|
+
return this.renderTable();
|
|
57
|
+
case 'csv':
|
|
58
|
+
// First pass: collect all headers from all objects
|
|
59
|
+
data.forEach(obj => {
|
|
60
|
+
const flattenedRow = this.flattenObject(obj);
|
|
61
|
+
const newKeys = Object.keys(flattenedRow);
|
|
62
|
+
if (this.csvHeaders.length === 0) {
|
|
63
|
+
this.csvHeaders = [...newKeys];
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const missingKeys = newKeys.filter(key => !this.csvHeaders.includes(key));
|
|
67
|
+
this.csvHeaders.push(...missingKeys);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// Second pass: generate CSV rows with consistent headers
|
|
71
|
+
const csvRows = [];
|
|
72
|
+
data.forEach(obj => {
|
|
73
|
+
const csvRow = this.addToCsv(obj);
|
|
74
|
+
if (csvRow) {
|
|
75
|
+
csvRows.push(csvRow);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return csvRows.join('\n');
|
|
79
|
+
case 'pretty':
|
|
80
|
+
return data.map(obj => this.formatPrettyJson(obj)).join('\n\n');
|
|
81
|
+
case 'raw':
|
|
82
|
+
return data.map(obj => JSON.stringify(obj)).join('\n');
|
|
83
|
+
case 'compact':
|
|
84
|
+
return data.map(obj => JSON.stringify(obj, null, 0)).join('\n');
|
|
85
|
+
default:
|
|
86
|
+
return JSON.stringify(data, null, 2);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Add data to table and return empty string (table will be rendered at the end)
|
|
91
|
+
*/
|
|
92
|
+
addToTable(data) {
|
|
93
|
+
const flattenedRow = this.flattenObject(data);
|
|
94
|
+
this.tableData.push(flattenedRow);
|
|
95
|
+
// Update headers with any new keys
|
|
96
|
+
const newKeys = Object.keys(flattenedRow);
|
|
97
|
+
if (this.tableHeaders.length === 0) {
|
|
98
|
+
this.tableHeaders = [...newKeys];
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const missingKeys = newKeys.filter(key => !this.tableHeaders.includes(key));
|
|
102
|
+
this.tableHeaders.push(...missingKeys);
|
|
103
|
+
}
|
|
104
|
+
return ''; // Don't output anything yet, table will be rendered at the end
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Add data to CSV and return CSV row string (with headers on first call)
|
|
108
|
+
*/
|
|
109
|
+
addToCsv(data) {
|
|
110
|
+
const flattenedRow = this.flattenObject(data);
|
|
111
|
+
// Update headers with any new keys
|
|
112
|
+
const newKeys = Object.keys(flattenedRow);
|
|
113
|
+
if (this.csvHeaders.length === 0) {
|
|
114
|
+
this.csvHeaders = [...newKeys];
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const missingKeys = newKeys.filter(key => !this.csvHeaders.includes(key));
|
|
118
|
+
this.csvHeaders.push(...missingKeys);
|
|
119
|
+
}
|
|
120
|
+
let output = '';
|
|
121
|
+
// Print headers on first row
|
|
122
|
+
if (!this.csvHeadersPrinted) {
|
|
123
|
+
const orderedHeaders = this.orderHeaders(this.csvHeaders);
|
|
124
|
+
output += this.formatCsvRow(orderedHeaders) + '\n';
|
|
125
|
+
this.csvHeadersPrinted = true;
|
|
126
|
+
}
|
|
127
|
+
// Format and return the data row
|
|
128
|
+
const orderedHeaders = this.orderHeaders(this.csvHeaders);
|
|
129
|
+
const rowValues = orderedHeaders.map(header => flattenedRow[header] || '');
|
|
130
|
+
output += this.formatCsvRow(rowValues);
|
|
131
|
+
return output;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Flatten nested object into dot notation
|
|
135
|
+
*/
|
|
136
|
+
flattenObject(obj, prefix = '', depth = 0) {
|
|
137
|
+
const maxDepth = this.options.maxDepth ?? 2;
|
|
138
|
+
const flattened = {};
|
|
139
|
+
Object.keys(obj).forEach(key => {
|
|
140
|
+
const value = obj[key];
|
|
141
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
142
|
+
if (value === null || value === undefined) {
|
|
143
|
+
flattened[fullKey] = '';
|
|
144
|
+
}
|
|
145
|
+
else if (typeof value === 'object' && !Array.isArray(value) && depth < maxDepth - 1) {
|
|
146
|
+
// Recursively flatten nested objects only if we haven't reached max depth
|
|
147
|
+
Object.assign(flattened, this.flattenObject(value, fullKey, depth + 1));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// At max depth or not an object - format as cell value
|
|
151
|
+
flattened[fullKey] = this.formatCellValue(value, fullKey);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return flattened;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Format cell value - keep JSON as formatted JSON, primitives as strings
|
|
158
|
+
* Special handling for timestamp columns to show readable local time
|
|
159
|
+
*/
|
|
160
|
+
formatCellValue(value, columnName = '') {
|
|
161
|
+
if (value === null || value === undefined) {
|
|
162
|
+
return '';
|
|
163
|
+
}
|
|
164
|
+
if (Array.isArray(value)) {
|
|
165
|
+
return JSON.stringify(value, null, 2);
|
|
166
|
+
}
|
|
167
|
+
if (typeof value === 'object') {
|
|
168
|
+
return JSON.stringify(value, null, 2);
|
|
169
|
+
}
|
|
170
|
+
// Check if this is a timestamp column and the value looks like a timestamp
|
|
171
|
+
if (this.options.showTimestamps && this.isTimestampColumn(columnName)) {
|
|
172
|
+
return this.formatTimestamp(value);
|
|
173
|
+
}
|
|
174
|
+
return String(value);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Check if a column name indicates it contains timestamps
|
|
178
|
+
*/
|
|
179
|
+
isTimestampColumn(columnName) {
|
|
180
|
+
const timestampColumns = [
|
|
181
|
+
'timestamp', 'eventTimestamp', 'receivedTimestamp',
|
|
182
|
+
'createdAt', 'updatedAt', 'created_at', 'updated_at',
|
|
183
|
+
'time', 'date', 'startTime', 'endTime'
|
|
184
|
+
];
|
|
185
|
+
const lowerColumnName = columnName.toLowerCase();
|
|
186
|
+
// Use exact matches or specific patterns to avoid false positives like "timeout"
|
|
187
|
+
return timestampColumns.some(tsCol => lowerColumnName === tsCol.toLowerCase() ||
|
|
188
|
+
lowerColumnName === `${tsCol.toLowerCase()}stamp` ||
|
|
189
|
+
(lowerColumnName.endsWith('at') && lowerColumnName.length > 2) ||
|
|
190
|
+
(lowerColumnName.endsWith('time') && !lowerColumnName.includes('timeout')));
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Format timestamp value to readable time string with timezone support
|
|
194
|
+
*/
|
|
195
|
+
formatTimestamp(value) {
|
|
196
|
+
try {
|
|
197
|
+
const timestampMillis = this.parseTimestamp(value);
|
|
198
|
+
if (timestampMillis === null || isNaN(timestampMillis)) {
|
|
199
|
+
return String(value); // Return original if parsing fails
|
|
200
|
+
}
|
|
201
|
+
const date = new Date(timestampMillis);
|
|
202
|
+
// Use specified timezone or default to system locale
|
|
203
|
+
if (this.options.timezone && this.options.timezone !== 'system') {
|
|
204
|
+
return date.toLocaleString('en-US', {
|
|
205
|
+
timeZone: this.options.timezone,
|
|
206
|
+
year: 'numeric',
|
|
207
|
+
month: '2-digit',
|
|
208
|
+
day: '2-digit',
|
|
209
|
+
hour: '2-digit',
|
|
210
|
+
minute: '2-digit',
|
|
211
|
+
second: '2-digit',
|
|
212
|
+
hour12: true
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
// Use system locale and timezone
|
|
217
|
+
return date.toLocaleString();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
return String(value); // Return original if formatting fails
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Parse a timestamp value for sorting (returns milliseconds since epoch or null)
|
|
226
|
+
*/
|
|
227
|
+
parseTimestamp(value) {
|
|
228
|
+
if (!value)
|
|
229
|
+
return null;
|
|
230
|
+
try {
|
|
231
|
+
const strValue = String(value);
|
|
232
|
+
if (/^\d+$/.test(strValue)) {
|
|
233
|
+
// Unix timestamp
|
|
234
|
+
const timestamp = parseInt(strValue);
|
|
235
|
+
// Handle both seconds (10 digits) and milliseconds (13 digits)
|
|
236
|
+
return strValue.length === 10 ? timestamp * 1000 : timestamp;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
// ISO string or other date format
|
|
240
|
+
const date = new Date(strValue);
|
|
241
|
+
return isNaN(date.getTime()) ? null : date.getTime();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Order headers according to common column preferences
|
|
250
|
+
*/
|
|
251
|
+
orderHeaders(headers) {
|
|
252
|
+
if (!headers || headers.length === 0) {
|
|
253
|
+
return headers;
|
|
254
|
+
}
|
|
255
|
+
const orderedHeaders = [];
|
|
256
|
+
// Define the desired order for common columns (works for jobs, integrations, log events)
|
|
257
|
+
const fixedOrder = [
|
|
258
|
+
'id', 'name', 'type', 'state', 'status', 'version',
|
|
259
|
+
'createdAt', 'updatedAt', 'eventTimestamp', 'receivedTimestamp',
|
|
260
|
+
'severity', 'tags', 'organizationId', 'execution', 'processing'
|
|
261
|
+
];
|
|
262
|
+
// Add fixed order columns if they exist (case-insensitive)
|
|
263
|
+
fixedOrder.forEach(column => {
|
|
264
|
+
const found = headers.find(header => header.toLowerCase() === column.toLowerCase());
|
|
265
|
+
if (found) {
|
|
266
|
+
orderedHeaders.push(found);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
// Find all nested/attribute columns and sort them alphabetically
|
|
270
|
+
const nestedColumns = headers
|
|
271
|
+
.filter(header => header.includes('.'))
|
|
272
|
+
.filter(header => !orderedHeaders.includes(header))
|
|
273
|
+
.sort((a, b) => a.localeCompare(b));
|
|
274
|
+
orderedHeaders.push(...nestedColumns);
|
|
275
|
+
// Add description/message columns if they exist
|
|
276
|
+
const descriptionColumns = headers.filter(header => {
|
|
277
|
+
const lower = header.toLowerCase();
|
|
278
|
+
return (lower.includes('description') || lower.includes('message')) && !orderedHeaders.includes(header);
|
|
279
|
+
}).sort((a, b) => a.localeCompare(b));
|
|
280
|
+
orderedHeaders.push(...descriptionColumns);
|
|
281
|
+
// Add any remaining columns that weren't covered above, sorted alphabetically
|
|
282
|
+
const remainingColumns = headers
|
|
283
|
+
.filter(header => !orderedHeaders.includes(header))
|
|
284
|
+
.sort((a, b) => a.localeCompare(b));
|
|
285
|
+
orderedHeaders.push(...remainingColumns);
|
|
286
|
+
return orderedHeaders;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Render the accumulated table data
|
|
290
|
+
*/
|
|
291
|
+
renderTable() {
|
|
292
|
+
if (!this.tableData.length) {
|
|
293
|
+
return 'No data to display';
|
|
294
|
+
}
|
|
295
|
+
// Sort the table data before rendering
|
|
296
|
+
const sortedData = this.sortData(this.tableData);
|
|
297
|
+
// Order the headers according to the specified column order
|
|
298
|
+
const orderedHeaders = this.orderHeaders(this.tableHeaders);
|
|
299
|
+
return this.formatTable(orderedHeaders, sortedData);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Sort data based on the sortBy option
|
|
303
|
+
*/
|
|
304
|
+
sortData(data) {
|
|
305
|
+
if (!this.options.sortBy || data.length <= 1) {
|
|
306
|
+
return data;
|
|
307
|
+
}
|
|
308
|
+
const [sortColumn, sortOrder = 'asc'] = this.options.sortBy.split(':');
|
|
309
|
+
const isAscending = sortOrder.toLowerCase() === 'asc';
|
|
310
|
+
// Find the actual column name (case-insensitive)
|
|
311
|
+
const actualColumn = this.findActualColumnName(sortColumn || '');
|
|
312
|
+
if (!actualColumn) {
|
|
313
|
+
console.warn(`Warning: Sort column '${sortColumn}' not found in data`);
|
|
314
|
+
return data;
|
|
315
|
+
}
|
|
316
|
+
return [...data].sort((a, b) => {
|
|
317
|
+
const valueA = a[actualColumn];
|
|
318
|
+
const valueB = b[actualColumn];
|
|
319
|
+
// Handle null/undefined values
|
|
320
|
+
if (valueA == null && valueB == null)
|
|
321
|
+
return 0;
|
|
322
|
+
if (valueA == null)
|
|
323
|
+
return isAscending ? -1 : 1;
|
|
324
|
+
if (valueB == null)
|
|
325
|
+
return isAscending ? 1 : -1;
|
|
326
|
+
// Try to parse as timestamps first for timestamp columns
|
|
327
|
+
if (this.isTimestampColumn(actualColumn)) {
|
|
328
|
+
const timestampA = this.parseTimestamp(valueA);
|
|
329
|
+
const timestampB = this.parseTimestamp(valueB);
|
|
330
|
+
if (timestampA !== null && timestampB !== null) {
|
|
331
|
+
const result = timestampA - timestampB;
|
|
332
|
+
return isAscending ? result : -result;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Try numeric comparison first
|
|
336
|
+
const numA = parseFloat(String(valueA));
|
|
337
|
+
const numB = parseFloat(String(valueB));
|
|
338
|
+
if (!isNaN(numA) && !isNaN(numB)) {
|
|
339
|
+
const result = numA - numB;
|
|
340
|
+
return isAscending ? result : -result;
|
|
341
|
+
}
|
|
342
|
+
// Fall back to string comparison
|
|
343
|
+
const strA = String(valueA).toLowerCase();
|
|
344
|
+
const strB = String(valueB).toLowerCase();
|
|
345
|
+
const result = strA.localeCompare(strB);
|
|
346
|
+
return isAscending ? result : -result;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Find the actual column name in the data (case-insensitive search)
|
|
351
|
+
*/
|
|
352
|
+
findActualColumnName(searchColumn) {
|
|
353
|
+
if (!this.tableHeaders)
|
|
354
|
+
return null;
|
|
355
|
+
const lowerSearchColumn = searchColumn.toLowerCase();
|
|
356
|
+
// Exact match first
|
|
357
|
+
const exactMatch = this.tableHeaders.find(header => header === searchColumn);
|
|
358
|
+
if (exactMatch)
|
|
359
|
+
return exactMatch;
|
|
360
|
+
// Case-insensitive match
|
|
361
|
+
const caseInsensitiveMatch = this.tableHeaders.find(header => header.toLowerCase() === lowerSearchColumn);
|
|
362
|
+
if (caseInsensitiveMatch)
|
|
363
|
+
return caseInsensitiveMatch;
|
|
364
|
+
// Partial match for common variations
|
|
365
|
+
const partialMatch = this.tableHeaders.find(header => header.toLowerCase().includes(lowerSearchColumn) ||
|
|
366
|
+
lowerSearchColumn.includes(header.toLowerCase()));
|
|
367
|
+
return partialMatch || null;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Format a CSV row with proper escaping
|
|
371
|
+
*/
|
|
372
|
+
formatCsvRow(values) {
|
|
373
|
+
return values.map(value => this.escapeCsvValue(String(value || ''))).join(',');
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Escape a CSV value according to RFC 4180
|
|
377
|
+
*/
|
|
378
|
+
escapeCsvValue(value) {
|
|
379
|
+
const stringValue = String(value);
|
|
380
|
+
// If the value contains comma, newline, double quote, or literal \n, wrap in quotes
|
|
381
|
+
if (stringValue.includes(',') || stringValue.includes('\n') || stringValue.includes('\r') || stringValue.includes('"') || stringValue.includes('\\n')) {
|
|
382
|
+
// Escape existing double quotes by doubling them
|
|
383
|
+
const escaped = stringValue.replace(/"/g, '""');
|
|
384
|
+
return `"${escaped}"`;
|
|
385
|
+
}
|
|
386
|
+
return stringValue;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Format data as a table
|
|
390
|
+
*/
|
|
391
|
+
formatTable(headers, rows) {
|
|
392
|
+
if (!headers.length || !rows.length) {
|
|
393
|
+
return 'No data to display';
|
|
394
|
+
}
|
|
395
|
+
// Calculate column widths
|
|
396
|
+
const columnWidths = headers.map(header => {
|
|
397
|
+
const maxContentWidth = Math.max(header.length, ...rows.map(row => {
|
|
398
|
+
const cellValue = String(row[header] || '');
|
|
399
|
+
// For multi-line content, use the longest line length
|
|
400
|
+
const lines = cellValue.split('\n');
|
|
401
|
+
return Math.max(...lines.map(line => line.length));
|
|
402
|
+
}));
|
|
403
|
+
return Math.min(maxContentWidth, 80); // Max width of 80 chars per column
|
|
404
|
+
});
|
|
405
|
+
// Format header row
|
|
406
|
+
const headerRow = '| ' + headers.map((header, i) => this.wrapAndPad(header, columnWidths[i])).join(' | ') + ' |';
|
|
407
|
+
// Format data rows with proper wrapping
|
|
408
|
+
const formattedRows = [];
|
|
409
|
+
for (const row of rows) {
|
|
410
|
+
const cellLines = headers.map((header, i) => {
|
|
411
|
+
const cellValue = String(row[header] || '');
|
|
412
|
+
const colWidth = columnWidths[i];
|
|
413
|
+
if (colWidth === undefined) {
|
|
414
|
+
return [''];
|
|
415
|
+
}
|
|
416
|
+
return this.wrapText(cellValue, colWidth);
|
|
417
|
+
});
|
|
418
|
+
// Find the maximum number of lines needed for this row
|
|
419
|
+
const maxLines = Math.max(...cellLines.map(lines => lines.length));
|
|
420
|
+
// Create each line of the row
|
|
421
|
+
for (let lineIndex = 0; lineIndex < maxLines; lineIndex++) {
|
|
422
|
+
const rowLine = '| ' + headers.map((_, i) => {
|
|
423
|
+
const lines = cellLines[i];
|
|
424
|
+
const lineContent = (lines && lineIndex < lines.length) ? lines[lineIndex] : '';
|
|
425
|
+
const colWidth = columnWidths[i];
|
|
426
|
+
if (colWidth === undefined) {
|
|
427
|
+
return '';
|
|
428
|
+
}
|
|
429
|
+
return (lineContent || '').padEnd(colWidth);
|
|
430
|
+
}).join(' | ') + ' |';
|
|
431
|
+
formattedRows.push(rowLine);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// Create separator line
|
|
435
|
+
const separator = '+-' + columnWidths.map(width => '-'.repeat(width)).join('-+-') + '-+';
|
|
436
|
+
// Combine all parts
|
|
437
|
+
const tableLines = [
|
|
438
|
+
separator,
|
|
439
|
+
headerRow,
|
|
440
|
+
separator,
|
|
441
|
+
...formattedRows,
|
|
442
|
+
separator
|
|
443
|
+
];
|
|
444
|
+
return tableLines.join('\n');
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Wrap text to fit column width and pad with spaces
|
|
448
|
+
*/
|
|
449
|
+
wrapAndPad(str, width) {
|
|
450
|
+
if (width === undefined) {
|
|
451
|
+
return str;
|
|
452
|
+
}
|
|
453
|
+
if (str.length > width) {
|
|
454
|
+
return str.substring(0, width - 3) + '...';
|
|
455
|
+
}
|
|
456
|
+
return str.padEnd(width);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Wrap text to multiple lines based on column width
|
|
460
|
+
*/
|
|
461
|
+
wrapText(text, width) {
|
|
462
|
+
if (!text || width <= 0) {
|
|
463
|
+
return [''];
|
|
464
|
+
}
|
|
465
|
+
// Handle text that's already multi-line (like pretty-printed JSON)
|
|
466
|
+
const existingLines = text.split('\n');
|
|
467
|
+
const wrappedLines = [];
|
|
468
|
+
for (const line of existingLines) {
|
|
469
|
+
if (line.length <= width) {
|
|
470
|
+
wrappedLines.push(line);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
// Break long lines at word boundaries when possible
|
|
474
|
+
const words = line.split(' ');
|
|
475
|
+
let currentLine = '';
|
|
476
|
+
for (const word of words) {
|
|
477
|
+
if (word.length > width) {
|
|
478
|
+
// If a single word is longer than width, break it
|
|
479
|
+
if (currentLine) {
|
|
480
|
+
wrappedLines.push(currentLine.trim());
|
|
481
|
+
currentLine = '';
|
|
482
|
+
}
|
|
483
|
+
// Break the long word into chunks
|
|
484
|
+
for (let i = 0; i < word.length; i += width) {
|
|
485
|
+
wrappedLines.push(word.substring(i, i + width));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
else if ((currentLine + ' ' + word).length <= width) {
|
|
489
|
+
currentLine += (currentLine ? ' ' : '') + word;
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
if (currentLine) {
|
|
493
|
+
wrappedLines.push(currentLine.trim());
|
|
494
|
+
}
|
|
495
|
+
currentLine = word;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (currentLine) {
|
|
499
|
+
wrappedLines.push(currentLine.trim());
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return wrappedLines.length > 0 ? wrappedLines : [''];
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Format JSON with pretty printing and syntax highlighting
|
|
507
|
+
*/
|
|
508
|
+
formatPrettyJson(data) {
|
|
509
|
+
if (!this.options.colorize) {
|
|
510
|
+
return JSON.stringify(data, null, 2);
|
|
511
|
+
}
|
|
512
|
+
const json = JSON.stringify(data, null, 2);
|
|
513
|
+
// Simple syntax highlighting
|
|
514
|
+
return json
|
|
515
|
+
.replace(/"([^"]+)":/g, chalk.blue('"$1"') + ':') // Keys
|
|
516
|
+
.replace(/: "([^"]+)"/g, ': ' + chalk.green('"$1"')) // String values
|
|
517
|
+
.replace(/: (\d+\.?\d*)/g, ': ' + chalk.yellow('$1')) // Numbers
|
|
518
|
+
.replace(/: (true|false)/g, ': ' + chalk.magenta('$1')) // Booleans
|
|
519
|
+
.replace(/: null/g, ': ' + chalk.gray('null')); // Null values
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Format log data based on selected format (alias for formatObject for backwards compatibility)
|
|
523
|
+
*/
|
|
524
|
+
formatLogData(data) {
|
|
525
|
+
return this.formatObject(data);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Format job state message
|
|
529
|
+
*/
|
|
530
|
+
formatJobState(state) {
|
|
531
|
+
if (!this.options.showTimestamps) {
|
|
532
|
+
return '';
|
|
533
|
+
}
|
|
534
|
+
const timestamp = this.options.showTimestamps ?
|
|
535
|
+
`${new Date().toISOString()} ` : '';
|
|
536
|
+
let stateColor;
|
|
537
|
+
let message;
|
|
538
|
+
switch (state) {
|
|
539
|
+
case 'HEARTBEAT':
|
|
540
|
+
stateColor = chalk.yellow;
|
|
541
|
+
message = 'Heartbeat received from server';
|
|
542
|
+
break;
|
|
543
|
+
case 'RUNNING':
|
|
544
|
+
stateColor = chalk.green;
|
|
545
|
+
message = 'Job is running, processing data...';
|
|
546
|
+
break;
|
|
547
|
+
case 'FINISHED':
|
|
548
|
+
stateColor = chalk.green.bold;
|
|
549
|
+
message = 'Job completed successfully';
|
|
550
|
+
break;
|
|
551
|
+
case 'FAILED':
|
|
552
|
+
stateColor = chalk.red.bold;
|
|
553
|
+
message = 'Job failed';
|
|
554
|
+
break;
|
|
555
|
+
case 'CANCELLED':
|
|
556
|
+
stateColor = chalk.yellow.bold;
|
|
557
|
+
message = 'Job was cancelled';
|
|
558
|
+
break;
|
|
559
|
+
case 'TIMED_OUT':
|
|
560
|
+
stateColor = chalk.red;
|
|
561
|
+
message = 'Job timed out';
|
|
562
|
+
break;
|
|
563
|
+
case 'SCANNED_MAX':
|
|
564
|
+
stateColor = chalk.yellow;
|
|
565
|
+
message = 'Job reached maximum scan limit';
|
|
566
|
+
break;
|
|
567
|
+
default:
|
|
568
|
+
stateColor = chalk.gray;
|
|
569
|
+
message = `Unknown job state: ${state}`;
|
|
570
|
+
}
|
|
571
|
+
return this.options.colorize ?
|
|
572
|
+
`${timestamp}${stateColor(`[${state}]`)} ${message}` :
|
|
573
|
+
`${timestamp}[${state}] ${message}`;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Format heartbeat status
|
|
577
|
+
*/
|
|
578
|
+
formatHeartbeatStatus(action, details = '') {
|
|
579
|
+
if (!this.options.colorize) {
|
|
580
|
+
return `[HEARTBEAT] ${action} ${details}`.trim();
|
|
581
|
+
}
|
|
582
|
+
const timestamp = this.options.showTimestamps ?
|
|
583
|
+
`${chalk.gray(new Date().toISOString())} ` : '';
|
|
584
|
+
switch (action) {
|
|
585
|
+
case 'SENT':
|
|
586
|
+
return `${timestamp}${chalk.cyan('[♥]')} Heartbeat sent ${details}`;
|
|
587
|
+
case 'FAILED':
|
|
588
|
+
return `${timestamp}${chalk.red('[✗]')} Heartbeat failed ${details}`;
|
|
589
|
+
case 'RETRY':
|
|
590
|
+
return `${timestamp}${chalk.yellow('[↻]')} Retrying heartbeat ${details}`;
|
|
591
|
+
default:
|
|
592
|
+
return `${timestamp}${chalk.blue('[♥]')} ${action} ${details}`;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Format connection status
|
|
597
|
+
*/
|
|
598
|
+
formatConnectionStatus(status, details = '') {
|
|
599
|
+
if (!this.options.colorize) {
|
|
600
|
+
return `[CONNECTION] ${status} ${details}`.trim();
|
|
601
|
+
}
|
|
602
|
+
const timestamp = this.options.showTimestamps ?
|
|
603
|
+
`${chalk.gray(new Date().toISOString())} ` : '';
|
|
604
|
+
switch (status) {
|
|
605
|
+
case 'CONNECTING':
|
|
606
|
+
return `${timestamp}${chalk.blue('[⚡]')} Connecting to Grepr API ${details}`;
|
|
607
|
+
case 'CONNECTED':
|
|
608
|
+
return `${timestamp}${chalk.green('[✓]')} Connected successfully ${details}`;
|
|
609
|
+
case 'DISCONNECTED':
|
|
610
|
+
return `${timestamp}${chalk.yellow('[⚡]')} Disconnected ${details}`;
|
|
611
|
+
case 'ERROR':
|
|
612
|
+
return `${timestamp}${chalk.red('[✗]')} Connection error ${details}`;
|
|
613
|
+
default:
|
|
614
|
+
return `${timestamp}${chalk.blue('[⚡]')} ${status} ${details}`;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Format error messages
|
|
619
|
+
*/
|
|
620
|
+
formatError(error, context = '') {
|
|
621
|
+
const timestamp = this.options.showTimestamps ?
|
|
622
|
+
`[${new Date().toISOString()}] ` : '';
|
|
623
|
+
const prefix = this.options.colorize ?
|
|
624
|
+
chalk.red.bold('[ERROR]') : '[ERROR]';
|
|
625
|
+
const contextStr = context ? ` (${context})` : '';
|
|
626
|
+
return `${timestamp}${prefix}${contextStr} ${error.message || error}`;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Format summary statistics
|
|
630
|
+
*/
|
|
631
|
+
formatSummary(stats) {
|
|
632
|
+
const lines = [];
|
|
633
|
+
if (this.options.colorize) {
|
|
634
|
+
lines.push(chalk.bold.underline('\nSummary:'));
|
|
635
|
+
lines.push(`${chalk.cyan('Records processed:')} ${stats.recordsProcessed || 0}`);
|
|
636
|
+
lines.push(`${chalk.cyan('Duration:')} ${stats.duration || 'Unknown'}`);
|
|
637
|
+
if (stats.heartbeatsSent > 0) {
|
|
638
|
+
lines.push(`${chalk.cyan('Heartbeats sent:')} ${stats.heartbeatsSent}`);
|
|
639
|
+
}
|
|
640
|
+
if (stats.errors > 0) {
|
|
641
|
+
lines.push(`${chalk.red('Errors:')} ${stats.errors}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
lines.push('\nSummary:');
|
|
646
|
+
lines.push(`Records processed: ${stats.recordsProcessed || 0}`);
|
|
647
|
+
lines.push(`Duration: ${stats.duration || 'Unknown'}`);
|
|
648
|
+
if (stats.heartbeatsSent > 0) {
|
|
649
|
+
lines.push(`Heartbeats sent: ${stats.heartbeatsSent}`);
|
|
650
|
+
}
|
|
651
|
+
if (stats.errors > 0) {
|
|
652
|
+
lines.push(`Errors: ${stats.errors}`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return lines.join('\n');
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
//# sourceMappingURL=json-formatter.js.map
|