mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 05:40:31 +00:00
WIP
This commit is contained in:
172
server/src/lib/LogicEvaluation/Lexer.js
Normal file
172
server/src/lib/LogicEvaluation/Lexer.js
Normal file
@@ -0,0 +1,172 @@
|
||||
|
||||
const OperationType = {
|
||||
LOGIC: 'LOGIC',
|
||||
STRING: 'STRING',
|
||||
COMPARISON: 'COMPARISON',
|
||||
MATH: 'MATH',
|
||||
};
|
||||
|
||||
export class Lexer {
|
||||
// operation table
|
||||
static get optable() {
|
||||
return {
|
||||
'=': OperationType.LOGIC,
|
||||
'&': OperationType.LOGIC,
|
||||
'|': OperationType.LOGIC,
|
||||
'?': OperationType.LOGIC,
|
||||
':': OperationType.LOGIC,
|
||||
|
||||
'\'': OperationType.STRING,
|
||||
'"': OperationType.STRING,
|
||||
|
||||
'!': OperationType.COMPARISON,
|
||||
'>': OperationType.COMPARISON,
|
||||
'<': OperationType.COMPARISON,
|
||||
|
||||
'(': OperationType.MATH,
|
||||
')': OperationType.MATH,
|
||||
'+': OperationType.MATH,
|
||||
'-': OperationType.MATH,
|
||||
'*': OperationType.MATH,
|
||||
'/': OperationType.MATH,
|
||||
'%': OperationType.MATH,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param {*} expression -
|
||||
*/
|
||||
constructor(expression) {
|
||||
this.currentIndex = 0;
|
||||
this.input = expression;
|
||||
this.tokenList = [];
|
||||
}
|
||||
|
||||
getTokens() {
|
||||
let tok;
|
||||
do {
|
||||
// read current token, so step should be -1
|
||||
tok = this.pickNext(-1);
|
||||
const pos = this.currentIndex;
|
||||
switch (Lexer.optable[tok]) {
|
||||
case OperationType.LOGIC:
|
||||
// == && || ===
|
||||
this.readLogicOpt(tok);
|
||||
break;
|
||||
|
||||
case OperationType.STRING:
|
||||
this.readString(tok);
|
||||
break;
|
||||
|
||||
case OperationType.COMPARISON:
|
||||
this.readCompare(tok);
|
||||
break;
|
||||
|
||||
case OperationType.MATH:
|
||||
this.receiveToken();
|
||||
break;
|
||||
|
||||
default:
|
||||
this.readValue(tok);
|
||||
}
|
||||
|
||||
// if the pos not changed, this loop will go into a infinite loop, every step of while loop,
|
||||
// we must move the pos forward
|
||||
// so here we should throw error, for example `1 & 2`
|
||||
if (pos === this.currentIndex && tok !== undefined) {
|
||||
const err = new Error(`unkonw token ${tok} from input string ${this.input}`);
|
||||
err.name = 'UnknowToken';
|
||||
throw err;
|
||||
}
|
||||
} while (tok !== undefined)
|
||||
|
||||
return this.tokenList;
|
||||
}
|
||||
|
||||
/**
|
||||
* read next token, the index param can set next step, default go foward 1 step
|
||||
*
|
||||
* @param index next postion
|
||||
*/
|
||||
pickNext(index = 0) {
|
||||
return this.input[index + this.currentIndex + 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Store token into result tokenList, and move the pos index
|
||||
*
|
||||
* @param index
|
||||
*/
|
||||
receiveToken(index = 1) {
|
||||
const tok = this.input.slice(this.currentIndex, this.currentIndex + index).trim();
|
||||
// skip empty string
|
||||
if (tok) {
|
||||
this.tokenList.push(tok);
|
||||
}
|
||||
|
||||
this.currentIndex += index;
|
||||
}
|
||||
|
||||
// ' or "
|
||||
readString(tok) {
|
||||
let next;
|
||||
let index = 0;
|
||||
do {
|
||||
next = this.pickNext(index);
|
||||
index += 1;
|
||||
} while (next !== tok && next !== undefined);
|
||||
this.receiveToken(index + 1);
|
||||
}
|
||||
|
||||
// > or < or >= or <= or !==
|
||||
// tok in (>, <, !)
|
||||
readCompare(tok) {
|
||||
if (this.pickNext() !== '=') {
|
||||
this.receiveToken(1);
|
||||
return;
|
||||
}
|
||||
// !==
|
||||
if (tok === '!' && this.pickNext(1) === '=') {
|
||||
this.receiveToken(3);
|
||||
return;
|
||||
}
|
||||
this.receiveToken(2);
|
||||
}
|
||||
|
||||
// === or ==
|
||||
// && ||
|
||||
readLogicOpt(tok) {
|
||||
if (this.pickNext() === tok) {
|
||||
// ===
|
||||
if (tok === '=' && this.pickNext(1) === tok) {
|
||||
return this.receiveToken(3);
|
||||
}
|
||||
// == && ||
|
||||
return this.receiveToken(2);
|
||||
}
|
||||
// handle as &&
|
||||
// a ? b : c is equal to a && b || c
|
||||
if (tok === '?' || tok === ':') {
|
||||
return this.receiveToken(1);
|
||||
}
|
||||
}
|
||||
|
||||
readValue(tok) {
|
||||
if (!tok) {
|
||||
return;
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
while (!Lexer.optable[tok] && tok !== undefined) {
|
||||
tok = this.pickNext(index);
|
||||
index += 1;
|
||||
}
|
||||
this.receiveToken(index);
|
||||
}
|
||||
}
|
||||
|
||||
export default function token(expression) {
|
||||
const lexer = new Lexer(expression);
|
||||
return lexer.getTokens();
|
||||
}
|
||||
159
server/src/lib/LogicEvaluation/Parser.js
Normal file
159
server/src/lib/LogicEvaluation/Parser.js
Normal file
@@ -0,0 +1,159 @@
|
||||
export const OPERATION = {
|
||||
'!': 5,
|
||||
'*': 4,
|
||||
'/': 4,
|
||||
'%': 4,
|
||||
'+': 3,
|
||||
'-': 3,
|
||||
'>': 2,
|
||||
'<': 2,
|
||||
'>=': 2,
|
||||
'<=': 2,
|
||||
'===': 2,
|
||||
'!==': 2,
|
||||
'==': 2,
|
||||
'!=': 2,
|
||||
'&&': 1,
|
||||
'||': 1,
|
||||
'?': 1,
|
||||
':': 1,
|
||||
};
|
||||
|
||||
// export interface Node {
|
||||
// left: Node | string | null;
|
||||
// right: Node | string | null;
|
||||
// operation: string;
|
||||
// grouped?: boolean;
|
||||
// };
|
||||
|
||||
export default class Parser {
|
||||
|
||||
constructor(token) {
|
||||
this.index = -1;
|
||||
this.blockLevel = 0;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return {Node | string} =-
|
||||
*/
|
||||
parse() {
|
||||
let tok;
|
||||
let root = {
|
||||
left: null,
|
||||
right: null,
|
||||
operation: null,
|
||||
};
|
||||
|
||||
do {
|
||||
tok = this.parseStatement();
|
||||
|
||||
if (tok === null || tok === undefined) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (root.left === null) {
|
||||
root.left = tok;
|
||||
root.operation = this.nextToken();
|
||||
|
||||
if (!root.operation) {
|
||||
return tok;
|
||||
}
|
||||
|
||||
root.right = this.parseStatement();
|
||||
} else {
|
||||
if (typeof tok !== 'string') {
|
||||
throw new Error('operation must be string, but get ' + JSON.stringify(tok));
|
||||
}
|
||||
root = this.addNode(tok, this.parseStatement(), root);
|
||||
}
|
||||
} while (tok);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
nextToken() {
|
||||
this.index += 1;
|
||||
return this.token[this.index];
|
||||
}
|
||||
|
||||
prevToken() {
|
||||
return this.token[this.index - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} operation
|
||||
* @param {Node|String|null} right
|
||||
* @param {Node} root
|
||||
*/
|
||||
addNode(operation, right, root) {
|
||||
let pre = root;
|
||||
|
||||
if (this.compare(pre.operation, operation) < 0 && !pre.grouped) {
|
||||
|
||||
while (pre.right !== null &&
|
||||
typeof pre.right !== 'string' &&
|
||||
this.compare(pre.right.operation, operation) < 0 && !pre.right.grouped) {
|
||||
pre = pre.right;
|
||||
}
|
||||
|
||||
pre.right = {
|
||||
operation,
|
||||
left: pre.right,
|
||||
right,
|
||||
};
|
||||
return root;
|
||||
}
|
||||
return {
|
||||
left: pre,
|
||||
right,
|
||||
operation,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} a
|
||||
* @param {String} b
|
||||
*/
|
||||
compare(a, b) {
|
||||
if (!OPERATION.hasOwnProperty(a) || !OPERATION.hasOwnProperty(b)) {
|
||||
throw new Error(`unknow operation ${a} or ${b}`);
|
||||
}
|
||||
return OPERATION[a] - OPERATION[b];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string | Node | null
|
||||
*/
|
||||
parseStatement() {
|
||||
const token = this.nextToken();
|
||||
if (token === '(') {
|
||||
this.blockLevel += 1;
|
||||
const node = this.parse();
|
||||
this.blockLevel -= 1;
|
||||
|
||||
if (typeof node !== 'string') {
|
||||
node.grouped = true;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
if (token === ')') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (token === '!') {
|
||||
return { left: null, operation: token, right: this.parseStatement() }
|
||||
}
|
||||
|
||||
// 3 > -12 or -12 + 10
|
||||
if (token === '-' && (OPERATION[this.prevToken()] > 0 || this.prevToken() === undefined)) {
|
||||
return { left: '0', operation: token, right: this.parseStatement(), grouped: true };
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
}
|
||||
61
server/src/lib/LogicEvaluation/QueryParser.js
Normal file
61
server/src/lib/LogicEvaluation/QueryParser.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { OPERATION } from './Parser';
|
||||
|
||||
export default class QueryParser {
|
||||
|
||||
constructor(tree, queries) {
|
||||
this.tree = tree;
|
||||
this.queries = queries;
|
||||
this.query = null;
|
||||
}
|
||||
|
||||
setQuery(query) {
|
||||
this.query = query.clone();
|
||||
}
|
||||
|
||||
parse() {
|
||||
return this.parseNode(this.tree);
|
||||
}
|
||||
|
||||
parseNode(node) {
|
||||
if (typeof node === 'string') {
|
||||
const nodeQuery = this.getQuery(node);
|
||||
return (query) => { nodeQuery(query); };
|
||||
}
|
||||
if (OPERATION[node.operation] === undefined) {
|
||||
throw new Error(`unknow expression ${node.operation}`);
|
||||
}
|
||||
const leftQuery = this.getQuery(node.left);
|
||||
const rightQuery = this.getQuery(node.right);
|
||||
|
||||
switch (node.operation) {
|
||||
case '&&':
|
||||
case 'AND':
|
||||
default:
|
||||
return (nodeQuery) => nodeQuery.where((query) => {
|
||||
query.where((q) => { leftQuery(q); });
|
||||
query.andWhere((q) => { rightQuery(q); });
|
||||
});
|
||||
case '||':
|
||||
case 'OR':
|
||||
return (nodeQuery) => nodeQuery.where((query) => {
|
||||
query.where((q) => { leftQuery(q); });
|
||||
query.orWhere((q) => { rightQuery(q); });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getQuery(node) {
|
||||
if (typeof node !== 'string' && node !== null) {
|
||||
return this.parseNode(node);
|
||||
}
|
||||
const value = parseFloat(node);
|
||||
|
||||
if (!isNaN(value)) {
|
||||
if (typeof this.queries[node] === 'undefined') {
|
||||
throw new Error(`unknow query under index ${node}`);
|
||||
}
|
||||
return this.queries[node];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
0
server/src/lib/LogicEvaluation/index.js
Normal file
0
server/src/lib/LogicEvaluation/index.js
Normal file
89
server/src/lib/ViewRolesBuilder/index.js
Normal file
89
server/src/lib/ViewRolesBuilder/index.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import { difference } from 'lodash';
|
||||
import { Lexer } from '@/lib/LogicEvaluation/Lexer';
|
||||
import Parser from '@/lib/LogicEvaluation/Parser';
|
||||
import QueryParser from '@/lib/LogicEvaluation/QueryParser';
|
||||
import resourceFieldsKeys from '@/data/ResourceFieldsKeys';
|
||||
|
||||
// const role = {
|
||||
// compatotor: '',
|
||||
// value: '',
|
||||
// columnKey: '',
|
||||
// columnSlug: '',
|
||||
// index: 1,
|
||||
// }
|
||||
|
||||
export function buildRoleQuery(role) {
|
||||
const columnName = resourceFieldsKeys[role.columnKey];
|
||||
|
||||
switch (role.comparator) {
|
||||
case 'equals':
|
||||
default:
|
||||
return (builder) => {
|
||||
builder.where(columnName, role.value);
|
||||
};
|
||||
case 'not_equal':
|
||||
case 'not_equals':
|
||||
return (builder) => {
|
||||
builder.whereNot(columnName, role.value);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds database query from stored view roles.
|
||||
*
|
||||
* @param {Array} roles -
|
||||
* @return {Function}
|
||||
*/
|
||||
export function viewRolesBuilder(roles, logicExpression = '') {
|
||||
const rolesIndexSet = {};
|
||||
|
||||
roles.forEach((role) => {
|
||||
rolesIndexSet[role.index] = buildRoleQuery(role);
|
||||
});
|
||||
// Lexer for logic expression.
|
||||
const lexer = new Lexer(logicExpression);
|
||||
const tokens = lexer.getTokens();
|
||||
|
||||
// Parse the logic expression.
|
||||
const parser = new Parser(tokens);
|
||||
const parsedTree = parser.parse();
|
||||
|
||||
const queryParser = new QueryParser(parsedTree, rolesIndexSet);
|
||||
return queryParser.parse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the view logic expression.
|
||||
* @param {String} logicExpression
|
||||
* @param {Array} indexes
|
||||
*/
|
||||
export function validateViewLogicExpression(logicExpression, indexes) {
|
||||
const logicExpIndexes = logicExpression.match(/\d+/g) || [];
|
||||
return !difference(logicExpIndexes.map(Number), indexes).length;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Array} roles -
|
||||
* @param {String} logicExpression -
|
||||
* @return {Boolean}
|
||||
*/
|
||||
export function validateViewRoles(roles, logicExpression) {
|
||||
return validateViewLogicExpression(logicExpression, roles.map((r) => r.index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapes the view roles to view conditionals.
|
||||
* @param {Array} viewRoles -
|
||||
* @return {Array}
|
||||
*/
|
||||
export function mapViewRolesToConditionals(viewRoles) {
|
||||
return viewRoles.map((viewRole) => ({
|
||||
comparator: viewRole.comparator,
|
||||
value: viewRole.value,
|
||||
columnKey: viewRole.field.columnKey,
|
||||
slug: viewRole.field.slug,
|
||||
index: viewRole.index,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user