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
text/typescript
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 };