UNPKG

@resk/core

Version:

An innovative TypeScript framework that empowers developers to build applications with a fully decorator-based architecture for efficient resource management. By combining the power of decorators with a resource-oriented design, DecorRes enhances code cla

537 lines (536 loc) 20.3 kB
import { IResourcePaginationMetaData, IResourceQueryOptions, IResourceQueryOrderBy, NestedPaths } from "./types"; export declare class ResourcePaginationHelper { /** * Normalizes pagination parameters into a consistent format, calculating missing values and ensuring valid pagination state. * * This method takes pagination options and normalizes them by calculating the missing pagination parameters * (`page`, `skip`, `limit`) based on the provided values. It handles the relationship between page-based * and offset-based pagination, ensuring that all three parameters are consistent and valid. * * **Normalization Logic:** * - If `limit` is not a valid number, returns an empty object (no pagination) * - If `skip` is provided, calculates the corresponding `page` number * - If `page` is provided, calculates the corresponding `skip` offset * - If neither `skip` nor `page` are provided, defaults to page 1 with skip 0 * - Ensures all returned values are valid numbers * * **Parameter Relationships:** * - `page`: 1-based page number (first page = 1) * - `skip`: 0-based offset (number of records to skip) * - `limit`: Maximum records per page * - Formula: `skip = (page - 1) * limit` * * @template DataType - The resource data type (used for type consistency with IResourceQueryOptions) * @param {IResourceQueryOptions<DataType>} [options] - Pagination options containing page, skip, and/or limit * @param {number} [options.page] - The page number (1-based) * @param {number} [options.skip] - The number of records to skip (0-based offset) * @param {number} [options.limit] - The maximum number of records per page * @returns {{page?: number, skip?: number, limit?: number}} Normalized pagination parameters: * - `page`: Calculated or provided page number * - `skip`: Calculated or provided skip offset * - `limit`: The limit value (unchanged if valid, omitted if invalid) * * @example * ```typescript * // Page-based pagination - calculates skip automatically * const result = ResourcePaginationHelper.normalizePagination({ * page: 3, * limit: 10 * }); * // Result: { page: 3, skip: 20, limit: 10 } * // (skip = (3-1) * 10 = 20) * ``` * * @example * ```typescript * // Offset-based pagination - calculates page automatically * const result = ResourcePaginationHelper.normalizePagination({ * skip: 50, * limit: 25 * }); * // Result: { page: 3, skip: 50, limit: 25 } * // (page = floor(50/25) + 1 = 3) * ``` * * @example * ```typescript * // Default pagination - no parameters provided * const result = ResourcePaginationHelper.normalizePagination({ * limit: 20 * }); * // Result: { page: 1, skip: 0, limit: 20 } * ``` * * @example * ```typescript * // Invalid limit - returns empty object (no pagination) * const result = ResourcePaginationHelper.normalizePagination({ * page: 2, * limit: "invalid" * }); * // Result: {} * ``` * * @example * ```typescript * // Zero or negative values - defaults to first page * const result = ResourcePaginationHelper.normalizePagination({ * page: 0, * limit: 15 * }); * // Result: { page: 1, skip: 0, limit: 15 } * ``` * * @example * ```typescript * // Large skip values * const result = ResourcePaginationHelper.normalizePagination({ * skip: 1000, * limit: 50 * }); * // Result: { page: 21, skip: 1000, limit: 50 } * // (page = floor(1000/50) + 1 = 21) * ``` * * @example * ```typescript * // Fractional skip values (floor operation) * const result = ResourcePaginationHelper.normalizePagination({ * skip: 37, * limit: 10 * }); * // Result: { page: 4, skip: 37, limit: 10 } * // (page = floor(37/10) + 1 = 4) * ``` * * @example * ```typescript * // Mixed parameters - skip takes precedence over page * const result = ResourcePaginationHelper.normalizePagination({ * page: 2, * skip: 30, * limit: 15 * }); * // Result: { page: 3, skip: 30, limit: 15 } * // (skip provided, so page is recalculated from skip) * ``` * * @example * ```typescript * // Database cursor scenario * const result = ResourcePaginationHelper.normalizePagination({ * skip: 500, * limit: 100 * }); * // Result: { page: 6, skip: 500, limit: 100 } * // Useful for database queries with OFFSET * ``` * * @example * ```typescript * // API pagination with user input * const userOptions = { page: 5, limit: 20 }; * const result = ResourcePaginationHelper.normalizePagination(userOptions); * // Result: { page: 5, skip: 80, limit: 20 } * // Ready for database query: LIMIT 20 OFFSET 80 * ``` * * @remarks * - Page numbers are 1-based (first page = 1, not 0) * - Skip values are 0-based (skip 0 = first record) * - Invalid limit values disable pagination entirely * - When both page and skip are provided, skip takes precedence for page calculation * - Negative or zero page values default to page 1 * - Fractional skip values are floored when calculating page numbers */ static normalizePagination<DataType = unknown>(options?: IResourceQueryOptions<DataType>): { page?: undefined; skip?: undefined; limit?: undefined; } | { page: number; skip: number; limit: number; }; /** * Normalizes orderBy parameters into a structured object format. * * This method takes various input formats for sorting criteria and converts them into a consistent * object where keys are valid nested property paths and values are sort directions ("asc" or "desc"). * It handles single strings, arrays of strings, and undefined inputs gracefully. * * @template T - The resource type to validate paths against * @param orderBy - The orderBy specification in various formats: * - `string`: Single field like "name" (ascending) or "-name" (descending) * - `string[]`: Multiple fields like ["name", "-age", "createdAt"] * - `undefined`: No sorting specified * @returns A partial record mapping valid nested paths to sort directions * * @example * ```typescript * interface User { * id: number; * name: string; * profile: { age: number; address: { city: string } }; * } * * // Single ascending field * normalizeOrderBy<User>("name") * // Returns: { "name": "asc" } * * // Single descending field * normalizeOrderBy<User>("-profile.age") * // Returns: { "profile.age": "desc" } * * // Multiple fields * normalizeOrderBy<User>(["name", "-profile.age", "profile.address.city"]) * // Returns: { "name": "asc", "profile.age": "desc", "profile.address.city": "asc" } * * // Invalid input (filtered out) * normalizeOrderBy<User>([null, "", "name", undefined]) * // Returns: { "name": "asc" } * ``` * * @remarks * - Fields starting with "-" are treated as descending order * - Invalid or empty strings are silently filtered out * - Duplicate fields will log a warning and use the last occurrence * - The method performs type assertion to `NestedPaths<T>` - ensure input paths are valid at call site */ static normalizeOrderBy<T = unknown>(orderBy?: IResourceQueryOrderBy<T>): Partial<Record<NestedPaths<T>, SortOrder>>; /*** * Determines if result can be paginated based on the provided query options. * It checks if the query options have a `limit` property of type number. * @template DataType The type of resource data. * @param {IResourceQueryOptions} queryOptions - The query options. * @returns {boolean} Whether the result can be paginated. * * @example * canPaginateResult({ limit: undefined }); //false * canPaginateResult({ limit: 10, skip: 20 }); // true * canPaginateResult({ page: 3, limit: 10 }); // true * canPaginateResult({ page: 3, skip: 20 }); // true */ static canPaginateResult<DataType = unknown>(queryOptions: IResourceQueryOptions<DataType>): boolean; static getPaginationMetaData<DataType = unknown>(count: number, queryOptions?: IResourceQueryOptions<DataType>): IResourcePaginationMetaData; /** * Paginates an array of data based on the provided query options and total count. * * This method applies pagination logic to slice the data array according to the specified * page size and current page, while also generating comprehensive pagination metadata. * It's designed to work seamlessly with database query results and API responses. * * **Pagination Logic:** * - Calculates the correct slice of data based on `page` and `limit` parameters * - Generates metadata including total pages, current page, navigation flags, etc. * - Handles edge cases like invalid page numbers or missing pagination parameters * * **Data Slicing:** * - Uses zero-based indexing for array slicing * - Ensures start/end indices are within array bounds * - Returns empty array if pagination parameters are invalid * * @template DataType - The type of individual data items in the array * @param {DataType[]} data - The complete array of data to be paginated * @param {number} count - The total number of records available (may be different from data.length for database queries) * @param {IResourceQueryOptions<DataType>} [options] - Pagination and query options * @returns {{ * data: DataType[], * total: number, * meta: IResourcePaginationMetaData * }} An object containing: * - `data`: The paginated slice of the original data array * - `total`: The total count of records (unchanged from input) * - `meta`: Comprehensive pagination metadata * * @example * ```typescript * // Basic pagination - page 1 with 10 items per page * const users = [{id: 1}, {id: 2}, ..., {id: 25}]; // 25 total users * const result = ResourcePaginationHelper.paginate(users, 25, { * page: 1, * limit: 10 * }); * // Result: * // { * // data: [{id: 1}, {id: 2}, ..., {id: 10}], // First 10 users * // total: 25, * // meta: { * // total: 25, * // currentPage: 1, * // pageSize: 10, * // totalPages: 3, * // hasNextPage: true, * // hasPreviousPage: false, * // nextPage: 2, * // previousPage: undefined, * // lastPage: 3 * // } * // } * ``` * * @example * ```typescript * // Middle page pagination * const result = ResourcePaginationHelper.paginate(users, 25, { * page: 2, * limit: 10 * }); * // Result: data contains items 11-20, meta shows navigation to pages 1 and 3 * ``` * * @example * ```typescript * // Last page with fewer items * const result = ResourcePaginationHelper.paginate(users, 25, { * page: 3, * limit: 10 * }); * // Result: data contains items 21-25 (5 items), meta shows no next page * ``` * * @example * ```typescript * // Using skip instead of page * const result = ResourcePaginationHelper.paginate(users, 25, { * skip: 15, * limit: 5 * }); * // Result: data contains items 16-20, meta shows currentPage: 4 * ``` * * @example * ```typescript * // No pagination options - returns all data * const result = ResourcePaginationHelper.paginate(users, 25); * // Result: data contains all 25 users, meta is minimal (only total) * ``` * * @example * ```typescript * // Invalid page number - clamps to valid range * const result = ResourcePaginationHelper.paginate(users, 25, { * page: 999, * limit: 10 * }); * // Result: data is empty array, meta shows page: 999 but no valid data slice * ``` * * @example * ```typescript * // Database scenario - data array smaller than total count * const dbResult = [{id: 101}, {id: 102}, {id: 103}]; // Only 3 records from DB * const result = ResourcePaginationHelper.paginate(dbResult, 150, { * page: 34, * limit: 3 * }); * // Result: data contains the 3 DB records, total: 150, meta shows page 34 of ~50 * ``` * * @example * ```typescript * // Empty data array * const result = ResourcePaginationHelper.paginate([], 0, { * page: 1, * limit: 10 * }); * // Result: data is empty array, total: 0, meta shows single empty page * ``` * * @example * ```typescript * // Large dataset pagination * const largeDataset = Array.from({length: 10000}, (_, i) => ({id: i + 1})); * const result = ResourcePaginationHelper.paginate(largeDataset, 10000, { * page: 500, * limit: 20 * }); * // Result: data contains items 9981-10000, meta shows page 500 of 500 * ``` */ static paginate<DataType = unknown>(data: DataType[], count: number, options?: IResourceQueryOptions<DataType>): { data: DataType[]; total: number; meta: IResourcePaginationMetaData; }; /** * Parses query options from HTTP request data, extracting and normalizing parameters from multiple sources. * * This method consolidates query parameters from URL query strings, request headers, route parameters, * and custom filters into a standardized `IResourceQueryOptions` object. It's designed for REST API * endpoints that need to handle complex query parameters for filtering, sorting, pagination, and more. * * **Parameter Sources (in precedence order):** * 1. URL query parameters (`req.url`) * 2. Route parameters (`req.params`) * 3. X-Filters header (`req.headers['x-filters']`) * 4. Custom filters (`req.filters`) * * **Supported Parameters:** * - **Pagination**: `limit`, `skip`, `page` * - **Sorting**: `orderBy` (string, array, or object) * - **Filtering**: `where` (object, array, or string conditions) * - **Relations**: `include`, `relations` (related data to load) * - **Caching**: `cache`, `cacheTTL` * - **Soft Deletes**: `includeDeleted` * - **Distinct**: `distinct` (unique results) * * @template T - The resource data type for type-safe query options * @param {Object} req - The request object containing query data from multiple sources * @param {string} req.url - The request URL containing query parameters * @param {Record<string, any>} req.headers - Request headers (may include 'x-filters') * @param {Record<string, any>} [req.params] - Route parameters from URL path * @param {Record<string, any>} [req.filters] - Custom filter object * @returns {IResourceQueryOptions<T> & {queryParams: Record<string, any>}} Normalized query options with: * - All standard `IResourceQueryOptions<T>` properties * - `queryParams`: Raw parsed query parameters for reference * * @example * ```typescript * // Basic pagination from URL query * const req = { * url: '/api/users?limit=10&page=2', * headers: {} * }; * const options = ResourcePaginationHelper.parseQueryOptions(req); * // Result: { limit: 10, page: 2, skip: 10, queryParams: { limit: '10', page: '2' } } * ``` * * @example * ```typescript * // Complex query with multiple sources * const req = { * url: '/api/users?limit=20&orderBy=name', * headers: { * 'x-filters': { * where: { active: true }, * include: ['profile', 'roles'] * } * }, * params: { userId: '123' }, * filters: { cache: true } * }; * const options = ResourcePaginationHelper.parseQueryOptions(req); * // Result includes all merged parameters with proper precedence * ``` * * @example * ```typescript * // Sorting with multiple fields * const req = { * url: '/api/users?orderBy[]=name&orderBy[]=-createdAt', * headers: {} * }; * const options = ResourcePaginationHelper.parseQueryOptions(req); * // Result: { orderBy: { name: 'asc', createdAt: 'desc' }, ... } * ``` * * @example * ```typescript * // Filtering with complex where conditions * const req = { * url: '/api/users', * headers: { * 'x-filters': { * where: { * age: { $gte: 18 }, * status: 'active', * $or: [{ role: 'admin' }, { role: 'moderator' }] * } * } * } * }; * const options = ResourcePaginationHelper.parseQueryOptions(req); * // Result includes complex where conditions for database queries * ``` * * @example * ```typescript * // Including related data * const req = { * url: '/api/posts?include[]=author&include[]=comments', * headers: { * 'x-filters': { * relations: ['tags', 'categories'] * } * } * }; * const options = ResourcePaginationHelper.parseQueryOptions(req); * // Result: { include: ['author', 'comments'], relations: ['tags', 'categories'], ... } * ``` * * @example * ```typescript * // Caching configuration * const req = { * url: '/api/users?cache=1&cacheTTL=300', * headers: {} * }; * const options = ResourcePaginationHelper.parseQueryOptions(req); * // Result: { cache: true, cacheTTL: 300, ... } * ``` * * @example * ```typescript * // Distinct results and soft deletes * const req = { * url: '/api/users?distinct=1&includeDeleted=true', * headers: {} * }; * const options = ResourcePaginationHelper.parseQueryOptions(req); * // Result: { distinct: true, includeDeleted: true, ... } * ``` * * @example * ```typescript * // Express.js style request object * const expressReq = { * url: '/api/users?limit=5&page=1&orderBy=name', * headers: { * 'x-filters': JSON.stringify({ where: { active: true } }) * }, * params: { tenantId: 'abc123' }, * query: { search: 'john' } // Additional query params * }; * const options = ResourcePaginationHelper.parseQueryOptions(expressReq); * // Handles all Express.js request formats automatically * ``` * * @example * ```typescript * // Minimal request - only URL parameters * const req = { * url: '/api/users', * headers: {} * }; * const options = ResourcePaginationHelper.parseQueryOptions(req); * // Result: { queryParams: {} } - minimal options, no pagination/sorting * ``` * * @example * ```typescript * // Mixed data types and type coercion * const req = { * url: '/api/users?limit=10&page=2&cache=true&includeDeleted=0', * headers: { * 'x-filters': { * distinct: '1', // String '1' becomes boolean true * cacheTTL: '600' // String '600' becomes number 600 * } * } * }; * const options = ResourcePaginationHelper.parseQueryOptions(req); * // Result: proper type coercion for all parameters * ``` */ static parseQueryOptions<T = unknown>(req: { url: string; headers: Record<string, any>; params?: Record<string, any>; filters?: Record<string, any>; }): IResourceQueryOptions<T> & { queryParams: Record<string, any>; }; } type SortOrder = "asc" | "desc"; export {};