UNPKG

nhb-express

Version:

Minimal Express + TypeScript scaffolder with modular structure and flexible stack choices.

199 lines (164 loc) 5.03 kB
import { Model, type Query, type QueryFilter } from 'mongoose'; import { normalizeNumber } from 'nhb-toolbox'; import type { ExcludeField, NumericKeys, SearchField } from '@/types'; /** * @class QueryBuilder * @description Utility class to build MongoDB queries. */ export class QueryBuilder<T> { /** * Creates an instance of `QueryBuilder`. * @param modelQuery Mongoose model on which the query will be performed. * @param query Query parameters for filtering, sorting and paginating. */ constructor( public modelQuery: Query<T[], T> = Model.find(), public query?: Record<string, unknown> ) { this.modelQuery = modelQuery; this.query = query; } /** * Method to search for a keyword in the specified fields. * @param searchFields An array of field names to search in. * @returns The current instance of QueryBuilder. */ search(searchFields: SearchField<T>[]) { const keyword = this?.query?.search as string; if (keyword) { this.modelQuery = this.modelQuery.find({ $or: searchFields.map( (field) => ({ [field]: { $regex: keyword, $options: 'i' }, }) as QueryFilter<T> ), }); } return this; } /** * Method to filter the query based on the provided query parameters. * @returns The current instance of QueryBuilder. */ filter() { const queryObj = { ...this?.query }; const excludeFields = [ 'search', 'sort_by', 'sort_order', 'filter', 'page', 'limit', 'min', 'max', 'ids', 'exclude', 'select', ]; excludeFields.forEach((field) => delete queryObj[field]); if (Object.keys(queryObj).length > 0) { this.modelQuery = this.modelQuery.find(queryObj as QueryFilter<T>); } return this; } /** * Sorts the query based on `sort_by` and `sort_order` parameters. * @returns The current instance of QueryBuilder. */ sort() { const sortField = (this?.query?.sort_by as string) || 'created_at'; const sort_order = (this?.query?.sort_order as string) || 'desc'; const sort_by: { [key: string]: 1 | -1 } = {}; sort_by[sortField] = sort_order === 'asc' ? 1 : -1; this.modelQuery = this.modelQuery.sort(sort_by); return this; } /** * Method to filter results based on a field's range (min & max values). * Ensures that only numeric fields are allowed. * @param field The numeric field to filter by range (e.g., "price"). * @returns The current instance of QueryBuilder. */ getRange(field: NumericKeys<T> & keyof QueryFilter<T>) { const min = normalizeNumber(this?.query?.min); const max = normalizeNumber(this?.query?.max); if (min != null || max != null) { const rangeFilter: QueryFilter<T> = {}; if (min != null) { rangeFilter[field] = { ...rangeFilter[field], $gte: min, }; } if (max != null) { rangeFilter[field] = { ...rangeFilter[field], $lte: max, }; } this.modelQuery = this.modelQuery.find(rangeFilter); } return this; } /** * Adds a filter to the query based on a specific field and a corresponding value from the query parameters. * @param field The field to filter on. * @param valueKey The key in the query parameters to extract the filter value. * @returns The current instance of QueryBuilder. */ customFilter(field: keyof T, valueKey: string) { const value = this?.query?.[valueKey] as string; if (value) { this.modelQuery = this.modelQuery.find({ [field]: value, } as QueryFilter<T>); } return this; } /** * Fetches documents based on the provided Object IDs as array. * @returns The current instance of QueryBuilder. */ getDocumentsByIds() { let ids: string[] = []; if (typeof this?.query?.ids === 'string') { ids = [this?.query?.ids]; } else if (Array.isArray(this?.query?.ids)) { ids = this?.query?.ids as string[]; } if (ids.length > 0) { this.modelQuery = this.modelQuery.find({ _id: { $in: ids } }); } return this; } /** * Method to paginate the query based on `page` and `limit` from the query object. * @returns The current instance of QueryBuilder. */ paginate() { const page = Number(this?.query?.page) || 1; const limit = Number(this?.query?.limit) || 10; const skip = (page - 1) * limit; this.modelQuery = this.modelQuery.skip(skip).limit(limit); return this; } /** * Method to exclude field(s) from the document(s). * @param fixedFields An array of fields to always exclude from the document(s). * @returns The current instance of QueryBuilder. */ excludeFields(fixedFields?: ExcludeField<T>[]) { const fields: string[] = ['-__v']; if (fixedFields && fixedFields.length > 0) { fixedFields.forEach((field) => fields.push(field)); } if (typeof this?.query?.exclude === 'string') { fields.push(this?.query?.exclude); } else if (Array.isArray(this?.query?.exclude)) { (this?.query?.exclude as string[]).forEach((field) => fields.push(field)); } this.modelQuery = this.modelQuery.select([...new Set(fields)].join(' ')); return this; } }