teableio/teable

View on GitHub
packages/core/src/query/json.visitor.ts

Summary

Maintainability
A
0 mins
Test Coverage
/* eslint-disable @typescript-eslint/no-explicit-any */
import { CharStreams, CommonTokenStream } from 'antlr4ts';
import { AbstractParseTreeVisitor } from 'antlr4ts/tree';
import { JsonErrorStrategy } from './json-error.strategy';
import type {
  BinaryExprContext,
  BooleanLiteralContext,
  FieldIdentifierContext,
  NullLiteralContext,
  NumberLiteralContext,
  ParenQueryExprContext,
  PredicateExprEqArrayContext,
  PredicateExprHasContext,
  PredicateExprInContext,
  PredicateExprLikeContext,
  PrimaryExprCompareContext,
  PrimaryExprIsContext,
  PrimaryExprPredicateContext,
  QueryExprContext,
  StartContext,
  StringLiteralContext,
  ValueContext,
} from './parser/Query';
import { Query, ValueListContext } from './parser/Query';
import { QueryLexer } from './parser/QueryLexer';
import type { QueryVisitor } from './parser/QueryVisitor';

export class JsonVisitor extends AbstractParseTreeVisitor<any> implements QueryVisitor<any> {
  defaultResult() {
    return null;
  }

  visitStart(ctx: StartContext) {
    let result = this.visit(ctx.expr());
    if (!result) {
      return this.defaultResult();
    }

    // If the result does not contain the filterSet and conjunction properties, then we need to create a new object
    if (!result.filterSet) {
      result = {
        filterSet: [result],
        conjunction: 'and',
      };
    }
    return result;
  }

  visitQueryExpr(ctx: QueryExprContext): any {
    return this.visit(ctx.queryStatement());
  }

  visitParenQueryExpr(ctx: ParenQueryExprContext): any {
    return this.visit(ctx.expr());
  }

  visitBinaryExpr(ctx: BinaryExprContext): any {
    const operator = ctx?._op?.text?.toLowerCase();
    const expressions = ctx.expr();
    let leftExpr = this.visit(expressions[0]);
    const rightExpr = this.visit(expressions[1]);

    // If the expression is not a filter set, we convert it to a filter set
    if (!leftExpr.conjunction) {
      leftExpr = {
        filterSet: [leftExpr],
        conjunction: operator,
      };
    }

    // If the operator of the current left-hand expression is not the same as the given operator
    if (leftExpr.conjunction !== operator) {
      // If inconsistent, create a new object that contains a filter set with the left and right expressions
      // and set the concatenation of the new object to the given operator
      leftExpr = {
        filterSet: [leftExpr, rightExpr],
        conjunction: operator,
      };
    } else if (leftExpr.conjunction === rightExpr.conjunction) {
      leftExpr.filterSet.push(...rightExpr.filterSet);
    } else {
      leftExpr.filterSet.push(rightExpr);
    }

    return leftExpr;
  }

  visitFieldIdentifier(ctx: FieldIdentifierContext): any {
    return ctx.text.replace(/[{}]/g, '');
  }

  visitPrimaryExprPredicate(ctx: PrimaryExprPredicateContext): any {
    return this.visit(ctx.predicate());
  }

  visitPrimaryExprIs(ctx: PrimaryExprIsContext): any {
    return this.createResult(ctx.fieldIdentifier(), ctx.isOp().text);
  }

  visitPrimaryExprCompare(ctx: PrimaryExprCompareContext): any {
    return this.createResult(ctx.fieldIdentifier(), ctx.compOp().text, ctx.value());
  }

  visitPredicateExprLike(ctx: PredicateExprLikeContext): any {
    return this.createResult(ctx.fieldIdentifier(), ctx.likeOp().text, ctx.value());
  }

  visitPredicateExprIn(ctx: PredicateExprInContext): any {
    return this.createResult(ctx.fieldIdentifier(), ctx.inOp().text, ctx.valueList());
  }

  visitPredicateExprHas(ctx: PredicateExprHasContext): any {
    return this.createResult(ctx.fieldIdentifier(), ctx.HAS_SYMBOL().text, ctx.valueList());
  }

  visitPredicateExprEqArray(ctx: PredicateExprEqArrayContext): any {
    return this.createResult(ctx.fieldIdentifier(), ctx.EQUAL_OPERATOR().text, ctx.valueList());
  }

  visitValue(ctx: ValueContext): any {
    return this.visit(ctx.literal());
  }

  visitValueList(ctx: ValueListContext): any {
    return ctx.literal().map((value) => this.visit(value));
  }

  visitStringLiteral(ctx: StringLiteralContext): any {
    return ctx.text.slice(1, -1);
  }

  visitNumberLiteral(ctx: NumberLiteralContext): any {
    return Number(ctx.text);
  }

  visitBooleanLiteral(ctx: BooleanLiteralContext): any {
    return ctx.text.toUpperCase() === 'TRUE';
  }

  visitNullLiteral(_ctx: NullLiteralContext): any {
    return null;
  }

  private createResult(
    fieldCtx: FieldIdentifierContext,
    operatorCtx: string,
    valueCtx?: ValueContext | ValueListContext
  ) {
    const fieldId = this.visit(fieldCtx);

    const operator = operatorCtx.toUpperCase() === '<>' ? '!=' : operatorCtx.toUpperCase();

    let value = null;
    if (valueCtx) {
      if (valueCtx instanceof ValueListContext) {
        value = this.visitValueList(valueCtx);
      } else {
        value = this.visitValue(valueCtx);
      }
    }

    return {
      isSymbol: true,
      fieldId: fieldId,
      operator: operator,
      value: value,
    };
  }
}

// parse Teable Query Language
export const parseTQL = (input: string) => {
  const inputStream = CharStreams.fromString(input);
  const lexer = new QueryLexer(inputStream);
  const tokenStream = new CommonTokenStream(lexer);
  const parser = new Query(tokenStream);

  parser.errorHandler = new JsonErrorStrategy();

  const tree = parser.start();
  const visitor = new JsonVisitor();
  return visitor.visit(tree);
};