UNPKG

mongoose-query-toolkit

Version:

A toolkit for handling Mongoose queries with support for search, filtering, pagination, sorting, field selection, and population

163 lines (160 loc) 6.15 kB
import { Document, Model } from 'mongoose'; interface QueryOptions { q?: string; page?: number; limit?: number; sort?: string; select?: string; populate?: string; lean?: boolean; [key: string]: any; } interface PaginationResult<T> { docs: T[]; totalDocs: number; limit: number; page: number; totalPages: number; hasNextPage: boolean; hasPrevPage: boolean; } interface CursorOptions { q?: string; limit?: number; cursor?: string | null; cursorField?: string; direction?: 'asc' | 'desc'; select?: string; populate?: string; lean?: boolean; [key: string]: any; } interface CursorResult<T> { docs: T[]; limit: number; nextCursor: string | null; hasNextPage: boolean; } interface PopulateConfig { path: string; select?: string; } type SearchMode = 'regex' | 'text'; declare class QueryToolkit<T extends Document> { private readonly model; private searchFields; private filterableFields; private selectableFields; private populatableFields; private defaultLimit; private maxLimit; private searchMode; private leanByDefault; private splitCommaValues; private presets; constructor(model: Model<T>, options?: { searchFields?: string[]; filterableFields?: string[]; selectableFields?: string[]; populatableFields?: string[]; defaultLimit?: number; maxLimit?: number; searchMode?: SearchMode; lean?: boolean; splitCommaValues?: boolean; }); /** * Escapes regex special characters to prevent regex injection and * catastrophic backtracking (ReDoS) from user-supplied search terms. */ private escapeRegex; private buildSearchQuery; private buildFilterQuery; /** * Translates a single filter value into a safe Mongo query fragment: * - arrays become `$in` (multi-value filter); comma-separated strings also * become `$in` only when `splitCommaValues` is enabled (off by default, so * values that legitimately contain commas keep exact-match semantics) * - objects are treated as operator filters, keeping only whitelisted * operators (gte, lte, ne, in, ...) and dropping anything unrecognized * to block NoSQL operator injection ($where, $function, raw $-keys, ...) * - primitives become exact-match * Returns `undefined` when nothing safe could be derived. */ private buildFilterValue; /** * Splits an array or comma-separated string into a deduped list of safe * primitive values (used by `$in`/`$nin` and comma-value filters). */ private toMultiValue; private buildOperatorFilter; private isPrimitive; /** * Coerces and clamps pagination input. Query-string params arrive as * strings, so values are normalized to integers, page is forced to >= 1, * and limit is bounded to [1, maxLimit] to prevent unbounded scans. */ private normalizePagination; private normalizeLimit; private parseSortString; private buildSelectQuery; /** * Parses the populate string into populate configs, optionally with * per-path field selection. * * Grammar (deterministic — `;` always separates paths, `,` separates the * selected fields of a single path): * - `profile,posts` → populate both paths (legacy, no `:`) * - `profile:name,avatar` → populate `profile` selecting name+avatar * - `profile:name;posts:title,body` → multiple paths, each with selection * - `profile;posts:title` → mix paths with and without selection */ private buildPopulateFields; private addPopulateConfig; /** * Assembles the base match query shared by every read method from the * search term and the whitelisted filter options. */ private buildBaseQuery; /** Returns the filter fields of an options object, excluding reserved keys. */ private extractFilters; /** Reads a possibly dotted path (e.g. `profile.score`) off a document. */ private getValueByPath; /** Encodes a cursor field value into a string that round-trips losslessly. */ private encodeCursor; private applyCommonModifiers; findWithOptions(options?: QueryOptions): Promise<PaginationResult<T>>; /** * Cursor-based (keyset) pagination. Scales to large collections because it * avoids the growing `skip` cost of offset pagination. Pass the `nextCursor` * from the previous result back in as `cursor` to fetch the following page. */ findWithCursor(options?: CursorOptions): Promise<CursorResult<T>>; /** * Returns a single document matching the search/filter options, or null. * Supports select, populate and lean; pagination/sort options are ignored. */ findOne(options?: QueryOptions): Promise<T | null>; /** * Returns true if at least one document matches the search/filter options. */ exists(options?: QueryOptions): Promise<boolean>; countWithOptions(options?: QueryOptions): Promise<number>; definePreset(name: string, options: QueryOptions): void; getPreset(name: string): QueryOptions | undefined; hasPreset(name: string): boolean; deletePreset(name: string): boolean; listPresets(): string[]; /** * Looks up a preset and merges it with overrides. Overrides take precedence; * when both preset and override hold a plain object for the same key (e.g. an * operator filter `{ gte: 10 }`), the two objects are merged rather than the * preset's value being wholly replaced — so `{ gte: 10 }` + `{ lte: 100 }` * yields `{ gte: 10, lte: 100 }`. */ private resolvePreset; private isPlainObject; findWithPreset(presetName: string, overrides?: QueryOptions): Promise<PaginationResult<T>>; countWithPreset(presetName: string, overrides?: QueryOptions): Promise<number>; } export { type CursorOptions, type CursorResult, type PaginationResult, type PopulateConfig, type QueryOptions, QueryToolkit, type SearchMode };