@andrew_l/search-query-language
Version: 
Converts human-readable query strings into structured representations.
479 lines (468 loc) • 12.8 kB
text/typescript
import { Arrayable } from '@andrew_l/toolkit';
import mongoose from 'mongoose';
interface TokenTypeOptions {
    keyword?: string;
}
/**
 * @group Utils
 */
declare class TokenType {
    readonly label: string;
    readonly keyword: string | undefined;
    constructor(label: string, options?: TokenTypeOptions);
}
/**
 * Node types
 * @group Constants
 */
declare const NODE: Readonly<{
    PROGRAM: "program";
    IDENTIFIER: "identifier";
    LITERAL: "literal";
    LOGICAL_EXPRESSION: "logical-expression";
    BINARY_EXPRESSION: "binary-expression";
}>;
/**
 * Keyword tokens.
 * @group Constants
 */
declare const KEYWORDS: Record<string, TokenType>;
/**
 * Token types
 * @group Constants
 */
declare const TOKEN: Readonly<{
    /**
     * Start of File token.
     */
    readonly SOF: TokenType;
    /**
     * End of File token.
     */
    readonly EOF: TokenType;
    /**
     * Number token.
     */
    readonly NUM: TokenType;
    /**
     * String token.
     */
    readonly STRING: TokenType;
    /**
     * Identifier token.
     */
    readonly NAME: TokenType;
    /**
     * Parenthesis token.
     */
    readonly PAREN_L: TokenType;
    /**
     * Parenthesis token.
     */
    readonly PAREN_R: TokenType;
    /**
     * Logical operator token.
     */
    readonly LOGICAL_OR: TokenType;
    /**
     * Logical operator token.
     */
    readonly LOGICAL_AND: TokenType;
    /**
     * Equality operator token.
     */
    readonly EQUALITY: TokenType;
    /**
     * Relational operator token.
     */
    readonly RELATIONAL: TokenType;
    /**
     * Minus operator token.
     */
    readonly MINUS: TokenType;
    /**
     * Null literal token.
     */
    readonly NULL: TokenType;
    /**
     * True literal tokens.
     */
    readonly TRUE: TokenType;
    /**
     * False literal tokens.
     */
    readonly FALSE: TokenType;
}>;
interface TokenOptions {
    type: TokenType;
    value: unknown;
    start: number;
    end: number;
}
/**
 * @group Utils
 */
declare class Token {
    readonly type: TokenType;
    readonly value: unknown;
    readonly start: number;
    readonly end: number;
    constructor(p: TokenOptions);
}
/**
 * @group Utils
 */
declare class Tokenizer implements Iterable<Token> {
    protected input: string;
    protected pos: number;
    protected length: number;
    protected state: TokenOptions;
    protected prev: TokenOptions;
    constructor(input: string);
    [Symbol.iterator](): {
        next: () => {
            done: boolean;
            value: Token;
        };
    };
    /**
     * Get the next token.
     */
    getToken(): Token;
    protected nextToken(): void;
    protected skipSpace(): void;
    protected finishToken(type: TokenType, value?: unknown): void;
    protected finishOp(type: TokenType, size: number): void;
    protected readToken(code: number): void;
    protected getTokenFromCode(code: number): void;
    protected charCodeAtPos(): number;
    protected readEquality(code: number): void;
    protected readLtGt(): void;
    protected readInt(): number | null;
    protected raise(pos: number, message: string): never;
    protected readString(): void;
    protected readWord(): void;
    protected readEscapedChar(): string;
    protected readNumber(): void;
}
/**
 * @group Types
 */
interface Node {
    /**
     * Start positions of the node in the source code.
     */
    start: number;
    /**
     * End positions of the node in the source code.
     */
    end: number;
    /**
     * Type of the node.
     */
    type: string;
}
/**
 * @group Types
 */
type NodeMap = {
    [NODE.PROGRAM]: NodeProgram;
    [NODE.LITERAL]: NodeLiteral;
    [NODE.BINARY_EXPRESSION]: NodeBinaryExpression;
    [NODE.LOGICAL_EXPRESSION]: NodeLogicalExpression;
    [NODE.IDENTIFIER]: NodeIdentifier;
};
/**
 * The node type.
 * @group Types
 */
type NodeType = (typeof NODE)[keyof typeof NODE];
/**
 * The node expression.
 * @group Types
 */
type NodeExpression = NodeBinaryExpression | NodeLogicalExpression;
/**
 * Root node of the AST.
 * @group Types
 */
interface NodeProgram extends Node {
    type: typeof NODE.PROGRAM;
    body: NodeExpression[];
}
/**
 * Literal node of the AST.
 * @group Types
 */
interface NodeLiteral extends Node {
    type: typeof NODE.LITERAL;
    /**
     * Value of the literal.
     */
    value: string | boolean | null | number;
    /**
     * Raw value of the literal.
     */
    raw?: string;
}
/**
 * Identifier node of the AST.
 * @group Types
 */
interface NodeIdentifier extends Node {
    type: typeof NODE.IDENTIFIER;
    /**
     * Name of the identifier.
     */
    name: string;
}
/**
 * Binary expression node of the AST.
 * @group Types
 */
interface NodeBinaryExpression extends Node {
    type: typeof NODE.BINARY_EXPRESSION;
    /**
     * Operator of the binary expression.
     */
    operator: BinaryOperator;
    /**
     * Left operand of the binary expression.
     */
    left: NodeIdentifier;
    /**
     * Right operand of the binary expression.
     */
    right: NodeLiteral;
}
/**
 * Binary operator.
 * @group Types
 */
type BinaryOperator = '=' | '!=' | '<' | '<=' | '>' | '>=';
/**
 * Logical expression node of the AST.
 * @group Types
 */
/**
 * @group Types
 */
interface NodeLogicalExpression extends Node {
    type: typeof NODE.LOGICAL_EXPRESSION;
    /**
     * Operator of the logical expression.
     */
    operator: LogicalOperator;
    /**
     * Left operand of the logical expression.
     */
    left: NodeExpression;
    /**
     * Right operand of the logical expression.
     */
    right: NodeExpression;
}
/**
 * Logical operator.
 * @group Types
 */
type LogicalOperator = 'OR' | 'AND';
/**
 * Parse an expression class.
 *
 * @group Utils
 */
declare class Expression extends Tokenizer {
    constructor(input: string);
    /**
     * Parse input into a program AST.
     */
    parse(): NodeProgram;
    protected finishNode<T extends Node>(node: T): T;
    protected parseTopLevel(node: NodeProgram): NodeProgram;
    protected parseExpression(): NodeExpression;
    protected parseExprOp(left: NodeExpression | NodeIdentifier): NodeBinaryExpression | NodeLogicalExpression;
    protected maybeLiteral(): NodeLiteral | null;
    protected maybeIdentifier(): NodeIdentifier | null;
    protected parseExprAtom(): NodeIdentifier | NodeLiteral;
    protected startNode<T extends NodeType>(type: T): NodeMap[T];
}
/**
 * Parses a query string into a NodeProgram representation.
 *
 * @param {string} value - The query string to be parsed.
 * @returns {NodeProgram} - The parsed representation of the query.
 *
 * @example
 * const program = parseQuery('age > 30');
 * console.log(program);
 * // {
 * //   type: 'program',
 * //   body: [
 * //     {
 * //       type: 'binary-expression',
 * //       operator: '>',
 * //       left: { type: 'identifier', name: 'age' },
 * //       right: { type: 'literal', value: 30 }
 * //     }
 * //   ]
 * // }
 *
 * @group Main
 */
declare function parseQuery(value: string): NodeProgram;
type ParseToMongoTransformFn = (value: unknown, key: string) => unknown;
interface ParseToMongoOptions {
    /**
     * Determines whether empty search queries are allowed.
     * If `true`, an empty query will return an unfiltered result.
     * If `false`, an empty query will be rejected.
     *
     * @default false
     */
    allowEmpty?: boolean;
    /**
     * A list of allowed keys that can be used in the search query.
     * If provided, any query using keys outside this list will be rejected.
     */
    allowedKeys?: string[];
    /**
     * Max of query ops combination.
     * @default Infinity
     */
    maxOps?: number;
    /**
     * A transformation function or a mapping of transformation functions
     * to modify query values before they are converted into a MongoDB query.
     *
     * - If an array is provided, all functions in the array will be applied.
     * - If a record object is provided, transformations will be applied
     *   based on the corresponding field key.
     *
     * @example
     * {
    'age': MONGO_TRANSFORM.NUMBER
    'customer._id': [MONGO_TRANSFORM.OBJECT_ID, MONGO_TRANSFORM.NOT_NULLABLE]
  }
     */
    transform?: Readonly<Arrayable<ParseToMongoTransformFn>> | Readonly<Record<string, Readonly<Arrayable<ParseToMongoTransformFn>>>>;
}
/**
 * Parses a query string and converts it into a MongoDB-compatible query object.
 *
 * @param {string} input - The query string to be parsed.
 * @returns {Record<string, any>} - The MongoDB query representation.
 *
 * @example
 * const query = parseToMongo('age > 30');
 * console.log(query);
 * // { age: { $gt: 30 } }
 *
 * @example
 * const query = parseToMongo('name = "Alice" AND age > 18');
 * console.log(query);
 * // { $and: [{ name: 'Alice' }, { age: { $gt: 18 } }] }
 *
 * @example
 * const query = parseToMongo('_id = "67d737b73af3ff3e00a3bbf1"', {
 *   transform: {
 *     _id: [MONGO_TRANSFORM.OBJECT_ID, MONGO_TRANSFORM.NOT_NULLABLE]
 *   }
 * });
 * console.log(query);
 * // { $and: [{ name: 'Alice' }, { age: { $gt: 18 } }] }
 *
 * @group Main
 */
declare function parseToMongo(input: string, options?: ParseToMongoOptions): Record<string, any>;
/**
 * Utility transform functions
 * @group Constants
 */
declare const MONGO_TRANSFORM: Readonly<{
    /**
     * Ensures the value is not null.
     * Throws an error if the value is null.
     *
     * @throws {Error} If the value is null.
     */
    readonly NOT_NULLABLE: ParseToMongoTransformFn;
    /**
     * Validates that the value is a number.
     * If the value is `null`, it returns `null` without throwing an error.
     *
     * @throws {Error} If the value is not a valid number.
     */
    readonly NUMBER: ParseToMongoTransformFn;
    /**
     * Validates that the value is a string.
     * If the value is `null`, it returns `null` without throwing an error.
     *
     * @throws {Error} If the value is not a valid string.
     */
    readonly STRING: ParseToMongoTransformFn;
    /**
     * Validates that the value is a boolean.
     * If the value is `null`, it returns `null` without throwing an error.
     *
     * @throws {Error} If the value is not a valid boolean.
     */
    readonly BOOLEAN: ParseToMongoTransformFn;
    /**
     * Validates and converts the value to a MongoDB ObjectId.
     * If the value is `null`, it returns `null` without throwing an error.
     *
     * @throws {Error} If the value is not a valid ObjectId.
     */
    readonly OBJECT_ID: ParseToMongoTransformFn;
    /**
     * Validates and converts the value to a Date.
     * Supports string and number inputs for conversion.
     * If the value is `null`, it returns `null` without throwing an error.
     *
     * @throws {Error} If the value is not a valid date.
     */
    readonly DATE: ParseToMongoTransformFn;
}>;
type MongooseSchema = mongoose.Schema;
type MongooseModel = mongoose.Model<any>;
/**
 * Parses a query string and converts it into a MongoDB-compatible query object,
 * using a provided Mongoose schema or model for field validation and transformation.
 *
 * @param {MongooseSchema | MongooseModel} reference - The Mongoose schema or model
 * used to infer field types and transformations.
 * @param {string} input - The query string to be parsed.
 * @param {ParseToMongoOptions} [options={}] - Optional configuration for parsing behavior.
 * @returns {Record<string, any>} - The MongoDB query representation.
 *
 * @example
 * // Type transformations are automatically inferred from the schema.
 * const schema = new mongoose.Schema({
 *   age: { type: Number },
 * });
 *
 * const query = parseToMongoose(schema, '_id = "67d737b73af3ff3e00a3bbf1"');
 * console.log(query);
 * // Output: { _id: new ObjectId("67d737b73af3ff3e00a3bbf1") }
 *
 * @example
 * // Complex queries with logical operators
 * const schema = new mongoose.Schema({
 *   age: { type: Number },
 *   customer: {
 *     name: { type: String },
 *     active: { type: Boolean },
 *   }
 * });
 *
 * const query = parseToMongoose(schema, 'customer.active = true AND age >= 18');
 * console.log(query);
 * // Output: { $and: [{ 'customer.active': true }, { age: { $gte: 18 } }] }
 *
 * @throws {Error} If the input query contains invalid syntax or references disallowed fields.
 *
 * @group Main
 */
declare function parseToMongoose(reference: MongooseSchema | MongooseModel, input: string, options?: ParseToMongoOptions): Record<string, any>;
export { type BinaryOperator, Expression, KEYWORDS, type LogicalOperator, MONGO_TRANSFORM, NODE, type Node, type NodeBinaryExpression, type NodeExpression, type NodeIdentifier, type NodeLiteral, type NodeLogicalExpression, type NodeMap, type NodeProgram, type NodeType, type ParseToMongoOptions, type ParseToMongoTransformFn, TOKEN, Tokenizer, parseQuery, parseToMongo, parseToMongoose };