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