UNPKG

loqs

Version:

Utility to parse json logs and a CLI to help filter them

372 lines (323 loc) 8.7 kB
const validationRegex = /^(\[(.*?)(:|$))?((.*?\(?[a-z0-9._-]+\)?[=~!<>]{1,2}(.*\('.*?'\)|(['\/])?.*?\7)(&|\||$)?)*)/i; const operatorRegex = /^([a-z0-9_.()-]+)([=~!<>]{1,2})((.*\('.*?'\))|(['\/])(.*)?\5)([|&])?/i; const expressionRegex = /^[a-z]+\([żąółźńćęśa-z0-9_.-]+\)(!?[=~]|[<>]=?)('|\/)[ żąółźńćęśa-z0-9(){}\[\]_:.,-]+\2$|^[żąółźńćęśa-z0-9_.-]+(!?=|[<>]=?)[a-z]+\('[[{]?[ żąółźńćęśa-z0-9(){}\[\]_:.,-]*[}\]]?'\)$|^[żąółźńćęśa-z0-9_.-]+(!?[=~]|[<>]=?)(['\/])[ żąółźńćęśa-z0-9(){}\[\]_:.,-]+\5$/; const dateRegex = /^\d{4}(-\d{1,2}(-\d{1,2})?)?( \d{1,2}(:\d{1,2})?(:\d{1,2})?)?$/; const getFunctionFromValue = (value) => { const indexOfOpeningBracket = value.indexOf('('); const indexOfClosingBracket = value.lastIndexOf(')'); const method = indexOfOpeningBracket !== -1 && value.slice(0, indexOfOpeningBracket); return { method, argument: method ? value.slice(indexOfOpeningBracket + 1, indexOfClosingBracket) : value, }; }; const parseValue = (value) => { if (!value) { return value; } if (parseFloat(value) == value) { return parseFloat(value); } if (typeof value === 'object') { return JSON.stringify(value); } return value; }; const parseEntries = (value, padding = 1) => { return value .slice(padding, value.length - padding) .split(',') .filter(Boolean); }; const checkIfEntryIsMissing = (input1, input2, callback) => { for (const entry in input1) { if (!callback(entry, input1[entry], input2)) { return false; } } for (const entry in input2) { if (!callback(entry, input2[entry], input1)) { return false; } } return true; }; const isValueMatchedByExpression = ( existingValue, value, operation, isRegex, column, ) => { existingValue = parseValue(existingValue); switch (column.method) { case 'floor': existingValue = Math.floor(existingValue); break; case 'ceil': existingValue = Math.ceil(existingValue); break; case 'round': existingValue = Math.round(existingValue); break; } if (!existingValue) { return false; } switch (value.method) { case 'date': if (!value.argument.match(dateRegex)) { console.error( `Following value is not valid date: ${value.argument}`, ); process.exit(1); } value.argument = new Date(value.argument); existingValue = new Date(existingValue); break; case 'array': if (!['=', '!='].includes(operation)) { console.error( `Following expression is not valid: ${ column.method ? `${column.method}(${column.argument})` : column.argument }${operation}${ value.method ? `${value.method}(${value.argument})` : value.argument }`, ); process.exit(1); } value.argument = parseEntries(value.argument, 2); try { existingValue = JSON.parse(existingValue); } catch (error) { // It is not valid JSON object return operation[0] === '!'; } if (!Array.isArray(existingValue)) { return operation[0] === '!'; } const index = value.argument.indexOf('...'); if (index !== -1) { value.argument.splice(index, 1); value.argument.push(...existingValue); value.argument = [...new Set(value.argument)]; } return ( (operation[0] === '!') !== checkIfEntryIsMissing( value.argument, existingValue, (_, value, input) => input.includes(value), ) ); case 'object': if (!['=', '!='].includes(operation)) { console.error( `Following expression is not valid: ${ column.method ? `${column.method}(${column.argument})` : column.argument }${operation}${ value.method ? `${value.method}(${value.argument})` : value.argument }`, ); process.exit(1); } let parsedValue = {}; try { existingValue = JSON.parse(existingValue); } catch (error) { // It is not valid JSON object return operation[0] === '!'; } if (Array.isArray(existingValue)) { return operation[0] === '!'; } parseEntries(value.argument, 2).forEach((entry) => { const keyValuePair = entry.split(':'); if (keyValuePair[0] === '...') { parsedValue = { ...parsedValue, ...existingValue, }; return; } parsedValue[keyValuePair[0]] = keyValuePair[1]; }); return ( (operation[0] === '!') !== checkIfEntryIsMissing( parsedValue, existingValue, (key) => parsedValue[key] === existingValue[key], ) ); } switch (operation) { case '<': case '>': case '<=': case '>=': case '!=': case '=': if (operation.match(/^!?=$/) && isRegex) { return ( (operation[0] === '!') !== !!existingValue.match(new RegExp(value.argument)) ); } switch (operation[0]) { case '<': return ( existingValue && (operation[1] === '=' ? existingValue <= value.argument : existingValue < value.argument) ); case '>': return ( existingValue && (operation[1] === '=' ? existingValue >= value.argument : existingValue > value.argument) ); case '!': return existingValue !== value.argument; default: return existingValue === value.argument; } case '~': case '!~': return ( existingValue && (operation[0] === '!') !== existingValue.includes(value.argument) ); } return false; }; const getConditionsFromExpressions = (expressions) => { if (!expressions) { return [[]]; } const conditions = [[]]; expressions.split(/(?<=[&|])/g).forEach((expression) => { if (!expression.match(expressionRegex)) { console.error(`Following expression is not valid: ${expression}`); process.exit(1); } const result = operatorRegex.exec(expression); const column = getFunctionFromValue(result[1]); const operation = result[2]; const isRegex = result[5] === '/'; const value = getFunctionFromValue(result[4] || result[6]); value.argument = value.argument.slice(1, value.argument.length - 1); conditions[conditions.length - 1].push({ [column.argument]: { matches: (existingValue) => { return isValueMatchedByExpression( existingValue, { ...value }, operation, isRegex, { ...column }, ); }, }, }); // If the expressions are split by a '|' it means that we have to create another 'AND' group result[7] === '|' && conditions.push([]); }); return conditions; }; const parseNestedColumn = (entry) => { const startIndex = entry.indexOf('['); const endIndex = entry.lastIndexOf(']'); if (startIndex === -1 || endIndex === -1) { return [entry]; } const parent = entry.slice(0, startIndex); const columns = parseColumns(entry.slice(startIndex + 1, endIndex)); return columns.map((n) => (n === '...' ? parent : parent + '.' + n)); }; const parseColumns = (columns) => { const parsedColumns = columns.split(','); const openingBracketRegex = /\[/g; const closingBracketRegex = /\]/g; for (let i = parsedColumns.length - 1; i >= 0; i--) { if ( (parsedColumns[i].match(closingBracketRegex) || []).length !== (parsedColumns[i].match(openingBracketRegex) || []).length ) { parsedColumns[Math.max(i - 1, 0)] = parsedColumns[i - 1] + ',' + parsedColumns[i]; i > 0 && delete parsedColumns[i]; } } return parsedColumns.flatMap( (column) => column && parseNestedColumn(column), ); }; const parseQuery = (options) => { if (!options.query) { return { matches: (_) => true, columns: (_) => _, }; } const result = validationRegex.exec(options.query); const columns = result[2] !== undefined ? result[2].slice(0, result[2].length - 1) : '...'; const parsedColumns = parseColumns(columns); const conditions = getConditionsFromExpressions(result[4]); const columnMatcher = (original) => { if (parsedColumns.includes('...')) { return [ ...parsedColumns.filter((column) => column !== '...'), ...original.filter((column) => !parsedColumns.includes(column)), ]; } return parsedColumns; }; const valueMatcher = (value) => { let result = false; for (const group of conditions) { let tempResult = true; for (const condition of group) { for (let column in condition) { let v = value[column]; if (column.includes('.')) { v = value; const tree = column.split('.'); tree.forEach((c) => (v = v && v[c])); } if (!condition[column].matches(v)) { tempResult = false; } } } if (tempResult) { result = tempResult; } } return result; }; return { matches: valueMatcher, columns: columnMatcher, }; }; module.exports = { parse: parseQuery, };