UNPKG

@andrew_l/search-query-language

Version:

Converts human-readable query strings into structured representations.

479 lines (468 loc) 12.8 kB
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 };