@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 };