UNPKG

nestjs-paginate

Version:

Pagination and filtering helper method for TypeORM repositories or query builders using Nest.js framework.

793 lines 41.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PaginationLimit = exports.PaginationType = exports.Paginated = exports.FilterSuffix = exports.FilterOperator = void 0; exports.paginate = paginate; exports.addRelationsFromSchema = addRelationsFromSchema; const common_1 = require("@nestjs/common"); const lodash_1 = require("lodash"); const querystring_1 = require("querystring"); const typeorm_1 = require("typeorm"); const filter_1 = require("./filter"); Object.defineProperty(exports, "FilterOperator", { enumerable: true, get: function () { return filter_1.FilterOperator; } }); Object.defineProperty(exports, "FilterSuffix", { enumerable: true, get: function () { return filter_1.FilterSuffix; } }); const helper_1 = require("./helper"); const logger = new common_1.Logger('nestjs-paginate'); class Paginated { } exports.Paginated = Paginated; var PaginationType; (function (PaginationType) { PaginationType["LIMIT_AND_OFFSET"] = "limit"; PaginationType["TAKE_AND_SKIP"] = "take"; PaginationType["CURSOR"] = "cursor"; })(PaginationType || (exports.PaginationType = PaginationType = {})); var PaginationLimit; (function (PaginationLimit) { PaginationLimit[PaginationLimit["NO_PAGINATION"] = -1] = "NO_PAGINATION"; PaginationLimit[PaginationLimit["COUNTER_ONLY"] = 0] = "COUNTER_ONLY"; PaginationLimit[PaginationLimit["DEFAULT_LIMIT"] = 20] = "DEFAULT_LIMIT"; PaginationLimit[PaginationLimit["DEFAULT_MAX_LIMIT"] = 100] = "DEFAULT_MAX_LIMIT"; })(PaginationLimit || (exports.PaginationLimit = PaginationLimit = {})); function generateWhereStatement(queryBuilder, obj) { const toTransform = Array.isArray(obj) ? obj : [obj]; return toTransform.map((item) => flattenWhereAndTransform(queryBuilder, item).join(' AND ')).join(' OR '); } function flattenWhereAndTransform(queryBuilder, obj, separator = '.', parentKey = '') { return Object.entries(obj).flatMap(([key, value]) => { if (obj.hasOwnProperty(key)) { const joinedKey = parentKey ? `${parentKey}${separator}${key}` : key; if (typeof value === 'object' && value !== null && !(0, helper_1.isFindOperator)(value)) { return flattenWhereAndTransform(queryBuilder, value, separator, joinedKey); } else { const property = (0, helper_1.getPropertiesByColumnName)(joinedKey); const { isVirtualProperty, query: virtualQuery } = (0, helper_1.extractVirtualProperty)(queryBuilder, property); const isRelation = (0, helper_1.checkIsRelation)(queryBuilder, property.propertyPath); const isEmbedded = (0, helper_1.checkIsEmbedded)(queryBuilder, property.propertyPath); const alias = (0, helper_1.fixColumnAlias)(property, queryBuilder.alias, isRelation, isVirtualProperty, isEmbedded, virtualQuery); const whereClause = queryBuilder['createWhereConditionExpression'](queryBuilder['getWherePredicateCondition'](alias, value)); const allJoinedTables = queryBuilder.expressionMap.joinAttributes.reduce((acc, attr) => { acc[attr.alias.name] = true; return acc; }, {}); const allTablesInPath = property.column.split('.').slice(0, -1); const tablesToJoin = allTablesInPath.map((table, idx) => { if (idx === 0) { return table; } return [...allTablesInPath.slice(0, idx), table].join('.'); }); tablesToJoin.forEach((table) => { const pathSplit = table.split('.'); const fullPath = pathSplit.length === 1 ? '' : `_${pathSplit .slice(0, -1) .map((p) => p + '_rel') .join('_')}`; const tableName = pathSplit[pathSplit.length - 1]; const tableAliasWithProperty = `${queryBuilder.alias}${fullPath}.${tableName}`; const joinTableAlias = `${queryBuilder.alias}${fullPath}_${tableName}_rel`; const baseTableAlias = allJoinedTables[joinTableAlias]; if (baseTableAlias) { return; } else { queryBuilder.leftJoin(tableAliasWithProperty, joinTableAlias); } }); return whereClause; } } }); } function fixCursorValue(value) { if ((0, helper_1.isISODate)(value)) { return new Date(value); } return value; } async function paginate(query, repo, config) { var _a, _b, _c; const dbType = ((0, helper_1.isRepository)(repo) ? repo.manager : repo).connection.options.type; const isMySqlOrMariaDb = ['mysql', 'mariadb'].includes(dbType); const metadata = (0, helper_1.isRepository)(repo) ? repo.metadata : repo.expressionMap.mainAlias.metadata; const page = (0, helper_1.positiveNumberOrDefault)(query.page, 1, 1); const defaultLimit = config.defaultLimit || PaginationLimit.DEFAULT_LIMIT; const maxLimit = config.maxLimit || PaginationLimit.DEFAULT_MAX_LIMIT; const isPaginated = !(query.limit === PaginationLimit.COUNTER_ONLY || (query.limit === PaginationLimit.NO_PAGINATION && maxLimit === PaginationLimit.NO_PAGINATION)); const limit = query.limit === PaginationLimit.COUNTER_ONLY ? PaginationLimit.COUNTER_ONLY : isPaginated === true ? maxLimit === PaginationLimit.NO_PAGINATION ? (_a = query.limit) !== null && _a !== void 0 ? _a : defaultLimit : query.limit === PaginationLimit.NO_PAGINATION ? defaultLimit : Math.min((_b = query.limit) !== null && _b !== void 0 ? _b : defaultLimit, maxLimit) : defaultLimit; const generateNullCursor = () => { return 'A' + '0'.repeat(15); // null values ​​should be looked up last, so use the smallest prefix }; const generateDateCursor = (value, direction) => { if (direction === 'ASC' && value === 0) { return 'X' + '0'.repeat(15); } const finalValue = direction === 'ASC' ? Math.pow(10, 15) - value : value; return 'V' + String(finalValue).padStart(15, '0'); }; const generateNumberCursor = (value, direction) => { const integerLength = 11; const decimalLength = 4; // sorting is not possible if the decimal point exceeds 4 digits const maxIntegerDigit = Math.pow(10, integerLength); const fixedScale = Math.pow(10, decimalLength); const absValue = Math.abs(value); const scaledValue = Math.round(absValue * fixedScale); const integerPart = Math.floor(scaledValue / fixedScale); const decimalPart = scaledValue % fixedScale; let integerPrefix; let decimalPrefix; let finalInteger; let finalDecimal; if (direction === 'ASC') { if (value < 0) { integerPrefix = 'Y'; decimalPrefix = 'V'; finalInteger = integerPart; finalDecimal = decimalPart; } else if (value === 0) { integerPrefix = 'X'; decimalPrefix = 'X'; finalInteger = 0; finalDecimal = 0; } else { integerPrefix = integerPart === 0 ? 'X' : 'V'; // X > V decimalPrefix = decimalPart === 0 ? 'X' : 'V'; // X > V finalInteger = integerPart === 0 ? 0 : maxIntegerDigit - integerPart; finalDecimal = decimalPart === 0 ? 0 : fixedScale - decimalPart; } } else { // DESC if (value < 0) { integerPrefix = integerPart === 0 ? 'N' : 'M'; // N > M decimalPrefix = decimalPart === 0 ? 'X' : 'V'; // X > V finalInteger = integerPart === 0 ? 0 : maxIntegerDigit - integerPart; finalDecimal = decimalPart === 0 ? 0 : fixedScale - decimalPart; } else if (value === 0) { integerPrefix = 'N'; decimalPrefix = 'X'; finalInteger = 0; finalDecimal = 0; } else { integerPrefix = 'V'; decimalPrefix = 'V'; finalInteger = integerPart; finalDecimal = decimalPart; } } return (integerPrefix + String(finalInteger).padStart(integerLength, '0') + decimalPrefix + String(finalDecimal).padStart(decimalLength, '0')); }; const generateCursor = (item, sortBy, linkType = 'next') => { return sortBy .map(([column, direction]) => { const columnProperties = (0, helper_1.getPropertiesByColumnName)(String(column)); let propertyPath = []; if (columnProperties.isNested) { if (columnProperties.propertyPath) { propertyPath.push(columnProperties.propertyPath); } propertyPath = propertyPath.concat(columnProperties.propertyName.split('.')); } else if (columnProperties.propertyPath) { propertyPath = [columnProperties.propertyPath, columnProperties.propertyName]; } else { propertyPath = [columnProperties.propertyName]; } // Extract value from nested object let value = item; for (let i = 0; i < propertyPath.length; i++) { const key = propertyPath[i]; if (value === null || value === undefined) { value = null; break; } // Handle case where value is an array if (Array.isArray(value[key])) { const arrayValues = value[key] .map((item) => { let nestedValue = item; for (let j = i + 1; j < propertyPath.length; j++) { if (nestedValue === null || nestedValue === undefined) { return null; } // Handle embedded properties if (propertyPath[j].includes('.')) { const nestedProperties = propertyPath[j].split('.'); for (const nestedProperty of nestedProperties) { nestedValue = nestedValue[nestedProperty]; } } else { nestedValue = nestedValue[propertyPath[j]]; } } return nestedValue; }) .filter((v) => v !== null && v !== undefined); if (arrayValues.length === 0) { value = null; } else { // Select min or max value based on sort direction and linkType (XOR) value = ((direction === 'ASC') !== (linkType === 'previous') ? Math.min(...arrayValues) : Math.max(...arrayValues)); } break; } else { value = value[key]; } } value = fixCursorValue(value); // Find column metadata let columnMeta = null; if (propertyPath.length === 1) { // For regular column columnMeta = metadata.columns.find((col) => col.propertyName === columnProperties.propertyName); } else { // For relation column let currentMetadata = metadata; let currentPath = ''; // Traverse the relation path except for the last part for (let i = 0; i < propertyPath.length - 1; i++) { const relationName = propertyPath[i]; currentPath = currentPath ? `${currentPath}.${relationName}` : relationName; const relation = currentMetadata.findRelationWithPropertyPath(relationName); if (relation) { currentMetadata = relation.inverseEntityMetadata; } else { break; } } // Find column by the last property name const propertyName = propertyPath[propertyPath.length - 1]; columnMeta = currentMetadata.columns.find((col) => col.propertyName === propertyName); } const isDateColumn = columnMeta && (0, helper_1.isDateColumnType)(columnMeta.type); if (value === null || value === undefined) { return generateNullCursor(); } if (isDateColumn) { return generateDateCursor(value.getTime(), direction); } else { const numericValue = Number(value); return generateNumberCursor(numericValue, direction); } }) .join(''); }; const getDateColumnExpression = (alias, dbType) => { switch (dbType) { case 'mysql': case 'mariadb': return `UNIX_TIMESTAMP(${alias}) * 1000`; case 'postgres': return `EXTRACT(EPOCH FROM ${alias}) * 1000`; case 'sqlite': return `(STRFTIME('%s', ${alias}) + (STRFTIME('%f', ${alias}) - STRFTIME('%S', ${alias}))) * 1000`; default: return alias; } }; const logAndThrowException = (msg) => { logger.debug(msg); throw new common_1.ServiceUnavailableException(msg); }; if (config.sortableColumns.length < 1) { logAndThrowException("Missing required 'sortableColumns' config."); } const sortBy = []; if (query.sortBy) { for (const order of query.sortBy) { if ((0, helper_1.isEntityKey)(config.sortableColumns, order[0]) && ['ASC', 'DESC'].includes(order[1])) { sortBy.push(order); } } } if (!sortBy.length) { sortBy.push(...(config.defaultSortBy || [[config.sortableColumns[0], 'ASC']])); } const searchBy = []; let [items, totalItems] = [[], 0]; const queryBuilder = (0, helper_1.isRepository)(repo) ? repo.createQueryBuilder('__root') : repo; if ((0, helper_1.isRepository)(repo) && !config.relations && config.loadEagerRelations === true) { if (!config.relations) { typeorm_1.FindOptionsUtils.joinEagerRelations(queryBuilder, queryBuilder.alias, repo.metadata); } } if (isPaginated) { config.paginationType = config.paginationType || PaginationType.TAKE_AND_SKIP; // Allow user to choose between limit/offset and take/skip, or cursor-based pagination. // However, using limit/offset can cause problems when joining one-to-many etc. if (config.paginationType === PaginationType.LIMIT_AND_OFFSET) { queryBuilder.limit(limit).offset((page - 1) * limit); } else if (config.paginationType === PaginationType.TAKE_AND_SKIP) { queryBuilder.take(limit).skip((page - 1) * limit); } else if (config.paginationType === PaginationType.CURSOR) { queryBuilder.take(limit); const padLength = 15; const integerLength = 11; const decimalLength = 4; const fixedScale = Math.pow(10, 4); const maxIntegerDigit = Math.pow(10, 11); const concat = (parts) => isMySqlOrMariaDb ? `CONCAT(${parts.join(', ')})` : parts.join(' || '); const generateNullCursorExpr = () => { const zeroPaddedExpr = (0, helper_1.getPaddedExpr)('0', padLength, dbType); const prefix = 'A'; return isMySqlOrMariaDb ? `CONCAT('${prefix}', ${zeroPaddedExpr})` : `'${prefix}' || ${zeroPaddedExpr}`; }; const generateDateCursorExpr = (columnExpr, direction) => { const safeExpr = `COALESCE(${columnExpr}, 0)`; const sqlExpr = direction === 'ASC' ? `POW(10, ${padLength}) - ${safeExpr}` : safeExpr; const paddedExpr = (0, helper_1.getPaddedExpr)(sqlExpr, padLength, dbType); const zeroPaddedExpr = (0, helper_1.getPaddedExpr)('0', padLength, dbType); const prefixNull = "'A'"; const prefixValue = "'V'"; const prefixZero = "'X'"; if (direction === 'ASC') { return `CASE WHEN ${columnExpr} IS NULL THEN ${concat([prefixNull, zeroPaddedExpr])} WHEN ${columnExpr} = 0 THEN ${concat([prefixZero, zeroPaddedExpr])} ELSE ${concat([prefixValue, paddedExpr])} END`; } else { return `CASE WHEN ${columnExpr} IS NULL THEN ${concat([prefixNull, zeroPaddedExpr])} ELSE ${concat([prefixValue, paddedExpr])} END`; } }; const generateNumberCursorExpr = (columnExpr, direction) => { const safeExpr = `COALESCE(${columnExpr}, 0)`; const absSafeExpr = `ABS(${safeExpr})`; const scaledExpr = `ROUND(${absSafeExpr} * ${fixedScale}, 0)`; const intExpr = `FLOOR(${scaledExpr} / ${fixedScale})`; const decExpr = `(${scaledExpr} % ${fixedScale})`; const reversedIntExpr = `(${maxIntegerDigit} - ${intExpr})`; const reversedDecExpr = `(${fixedScale} - ${decExpr})`; const paddedIntExpr = (0, helper_1.getPaddedExpr)(intExpr, integerLength, dbType); const paddedDecExpr = (0, helper_1.getPaddedExpr)(decExpr, decimalLength, dbType); const reversedIntPaddedExpr = (0, helper_1.getPaddedExpr)(reversedIntExpr, integerLength, dbType); const reversedDecPaddedExpr = (0, helper_1.getPaddedExpr)(reversedDecExpr, decimalLength, dbType); const zeroPaddedIntExpr = (0, helper_1.getPaddedExpr)('0', integerLength, dbType); const zeroPaddedDecExpr = (0, helper_1.getPaddedExpr)('0', decimalLength, dbType); if (direction === 'ASC') { return `CASE WHEN ${columnExpr} IS NULL THEN ${generateNullCursorExpr()} WHEN ${columnExpr} < 0 THEN ${concat(["'Y'", paddedIntExpr, "'V'", paddedDecExpr])} WHEN ${columnExpr} = 0 THEN ${concat(["'X'", zeroPaddedIntExpr, "'X'", zeroPaddedDecExpr])} WHEN ${columnExpr} > 0 AND ${intExpr} = 0 AND ${decExpr} > 0 THEN ${concat([ "'X'", zeroPaddedIntExpr, "'V'", reversedDecPaddedExpr, ])} WHEN ${columnExpr} > 0 AND ${intExpr} > 0 AND ${decExpr} = 0 THEN ${concat([ "'V'", reversedIntPaddedExpr, "'X'", zeroPaddedDecExpr, ])} WHEN ${columnExpr} > 0 AND ${intExpr} > 0 AND ${decExpr} > 0 THEN ${concat([ "'V'", reversedIntPaddedExpr, "'V'", reversedDecPaddedExpr, ])} END`; } else { return `CASE WHEN ${columnExpr} IS NULL THEN ${generateNullCursorExpr()} WHEN ${columnExpr} < 0 AND ${intExpr} > 0 AND ${decExpr} > 0 THEN ${concat([ "'M'", reversedIntPaddedExpr, "'V'", reversedDecPaddedExpr, ])} WHEN ${columnExpr} < 0 AND ${intExpr} > 0 AND ${decExpr} = 0 THEN ${concat([ "'M'", reversedIntPaddedExpr, "'X'", zeroPaddedDecExpr, ])} WHEN ${columnExpr} < 0 AND ${intExpr} = 0 AND ${decExpr} > 0 THEN ${concat([ "'N'", zeroPaddedIntExpr, "'V'", reversedDecPaddedExpr, ])} WHEN ${columnExpr} = 0 THEN ${concat(["'N'", zeroPaddedIntExpr, "'X'", zeroPaddedDecExpr])} WHEN ${columnExpr} > 0 THEN ${concat(["'V'", paddedIntExpr, "'V'", paddedDecExpr])} END`; } }; const cursorExpressions = sortBy.map(([column, direction]) => { const columnProperties = (0, helper_1.getPropertiesByColumnName)(column); const { isVirtualProperty, query: virtualQuery } = (0, helper_1.extractVirtualProperty)(queryBuilder, columnProperties); const isRelation = (0, helper_1.checkIsRelation)(queryBuilder, columnProperties.propertyPath); const isEmbedded = (0, helper_1.checkIsEmbedded)(queryBuilder, columnProperties.propertyPath); const alias = (0, helper_1.fixColumnAlias)(columnProperties, queryBuilder.alias, isRelation, isVirtualProperty, isEmbedded, virtualQuery); // Find column metadata to determine type for proper cursor handling let columnMeta = metadata.columns.find((col) => col.propertyName === columnProperties.propertyName); // If it's a relation column, we need to find the target column metadata if (isRelation) { // Find the relation by path and get the target entity metadata const relationPath = columnProperties.column.split('.'); // The base entity is the starting point let currentMetadata = metadata; // Traverse the relation path to find the target metadata for (let i = 0; i < relationPath.length - 1; i++) { const relationName = relationPath[i]; const relation = currentMetadata.findRelationWithPropertyPath(relationName); if (relation) { // Update the metadata to the target entity metadata for the next iteration currentMetadata = relation.inverseEntityMetadata; } else { break; } } // Now get the property from the target entity const propertyName = relationPath[relationPath.length - 1]; columnMeta = currentMetadata.columns.find((col) => col.propertyName === propertyName); } // Determine whether it's a date column const isDateColumn = columnMeta && (0, helper_1.isDateColumnType)(columnMeta.type); const columnExpr = isDateColumn ? getDateColumnExpression(alias, dbType) : alias; return isDateColumn ? generateDateCursorExpr(columnExpr, direction) : generateNumberCursorExpr(columnExpr, direction); }); const cursorExpression = cursorExpressions.length > 1 ? isMySqlOrMariaDb ? `CONCAT(${cursorExpressions.join(', ')})` : cursorExpressions.join(' || ') : cursorExpressions[0]; queryBuilder.addSelect(cursorExpression, 'cursor'); if (query.cursor) { queryBuilder.andWhere(`${cursorExpression} < :cursor`, { cursor: query.cursor }); } isMySqlOrMariaDb ? queryBuilder.orderBy('`cursor`', 'DESC') : queryBuilder.orderBy('cursor', 'DESC'); // since cursor is a reserved word in mysql, wrap it in backticks to recognize it as an alias } } if (config.withDeleted) { queryBuilder.withDeleted(); } let filterJoinMethods = {}; if (query.filter) { filterJoinMethods = (0, filter_1.addFilter)(queryBuilder, query, config.filterableColumns); } const joinMethods = Object.assign(Object.assign({}, filterJoinMethods), config.joinMethods); // Add the relations specified by the config, or used in the currently // filtered filterable columns. if (config.relations || Object.keys(filterJoinMethods).length) { const relationsSchema = (0, helper_1.mergeRelationSchema)((0, helper_1.createRelationSchema)(config.relations), (0, helper_1.createRelationSchema)(Object.keys(joinMethods))); addRelationsFromSchema(queryBuilder, relationsSchema, config, joinMethods); } if (config.paginationType !== PaginationType.CURSOR) { let nullSort; if (config.nullSort) { if (isMySqlOrMariaDb) { nullSort = config.nullSort === 'last' ? 'IS NULL' : 'IS NOT NULL'; } else { nullSort = config.nullSort === 'last' ? 'NULLS LAST' : 'NULLS FIRST'; } } for (const order of sortBy) { const columnProperties = (0, helper_1.getPropertiesByColumnName)(order[0]); const { isVirtualProperty } = (0, helper_1.extractVirtualProperty)(queryBuilder, columnProperties); const isRelation = (0, helper_1.checkIsRelation)(queryBuilder, columnProperties.propertyPath); const isEmbedded = (0, helper_1.checkIsEmbedded)(queryBuilder, columnProperties.propertyPath); let alias = (0, helper_1.fixColumnAlias)(columnProperties, queryBuilder.alias, isRelation, isVirtualProperty, isEmbedded); if (isVirtualProperty) { alias = (0, helper_1.quoteVirtualColumn)(alias, isMySqlOrMariaDb); } if (isMySqlOrMariaDb) { if (nullSort) { const selectionAliasName = `${alias.replace(/\./g, '_')}IsNull`; queryBuilder.addSelect(`${alias} ${nullSort}`, selectionAliasName); queryBuilder.addOrderBy(selectionAliasName); } queryBuilder.addOrderBy(alias, order[1]); } else { queryBuilder.addOrderBy(alias, order[1], nullSort); } } } /** * Expands select parameters containing wildcards (*) into actual column lists * * @returns Array of expanded column names */ const expandWildcardSelect = (selectParams, queryBuilder) => { const expandedParams = []; const mainAlias = queryBuilder.expressionMap.mainAlias; const mainMetadata = mainAlias.metadata; /** * Internal function to expand wildcards * * @returns Array of expanded column names */ const _expandWidcard = (entityPath, metadata) => { const expanded = []; // Add all columns from the relation entity expanded.push(...metadata.columns .filter((col) => !metadata.embeddeds .map((embedded) => embedded.columns.map((embeddedCol) => embeddedCol.propertyName)) .flat() .includes(col.propertyName)) .map((col) => (entityPath ? `${entityPath}.${col.propertyName}` : col.propertyName))); // Add columns from embedded entities in the relation metadata.embeddeds.forEach((embedded) => { expanded.push(...embedded.columns.map((col) => `${entityPath}.(${embedded.propertyName}.${col.propertyName})`)); }); return expanded; }; for (const param of selectParams) { if (param === '*') { expandedParams.push(..._expandWidcard('', mainMetadata)); } else if (param.endsWith('.*')) { // Handle relation entity wildcards (e.g. 'user.*', 'user.profile.*') const parts = param.slice(0, -2).split('.'); let currentPath = ''; let currentMetadata = mainMetadata; for (let i = 0; i < parts.length; i++) { const part = parts[i]; currentPath = currentPath ? `${currentPath}.${part}` : part; const relation = currentMetadata.findRelationWithPropertyPath(part); if (relation) { currentMetadata = relation.inverseEntityMetadata; if (i === parts.length - 1) { // Expand wildcard at the last part expandedParams.push(..._expandWidcard(currentPath, currentMetadata)); } } else { break; } } } else { // Add regular columns as is expandedParams.push(param); } } // Remove duplicates while preserving order return [...new Set(expandedParams)]; }; const selectParams = (() => { // Expand wildcards in config.select if it exists const expandedConfigSelect = config.select ? expandWildcardSelect(config.select, queryBuilder) : undefined; // Expand wildcards in query.select if it exists const expandedQuerySelect = query.select ? expandWildcardSelect(query.select, queryBuilder) : undefined; // Filter config.select with expanded query.select if both exist and ignoreSelectInQueryParam is false if (expandedConfigSelect && expandedQuerySelect && !config.ignoreSelectInQueryParam) { return expandedConfigSelect.filter((column) => expandedQuerySelect.includes(column)); } return expandedConfigSelect; })(); if ((selectParams === null || selectParams === void 0 ? void 0 : selectParams.length) > 0) { let cols = selectParams.reduce((cols, currentCol) => { const columnProperties = (0, helper_1.getPropertiesByColumnName)(currentCol); const isRelation = (0, helper_1.checkIsRelation)(queryBuilder, columnProperties.propertyPath); cols.push((0, helper_1.fixColumnAlias)(columnProperties, queryBuilder.alias, isRelation)); return cols; }, []); const missingPrimaryKeys = (0, helper_1.getMissingPrimaryKeyColumns)(queryBuilder, cols); if (missingPrimaryKeys.length > 0) { cols = cols.concat(missingPrimaryKeys); } queryBuilder.select(cols); } if (config.where && (0, helper_1.isRepository)(repo)) { const baseWhereStr = generateWhereStatement(queryBuilder, config.where); queryBuilder.andWhere(`(${baseWhereStr})`); } if (config.searchableColumns) { if (query.searchBy && !config.ignoreSearchByInQueryParam) { for (const column of query.searchBy) { if ((0, helper_1.isEntityKey)(config.searchableColumns, column)) { searchBy.push(column); } } } else { searchBy.push(...config.searchableColumns); } } if (query.search && searchBy.length) { queryBuilder.andWhere(new typeorm_1.Brackets((qb) => { var _a; // Explicitly handle the default case - multiWordSearch defaults to false const useMultiWordSearch = (_a = config.multiWordSearch) !== null && _a !== void 0 ? _a : false; if (!useMultiWordSearch) { // Strict search mode (default behavior) for (const column of searchBy) { const property = (0, helper_1.getPropertiesByColumnName)(column); const { isVirtualProperty, query: virtualQuery } = (0, helper_1.extractVirtualProperty)(qb, property); const isRelation = (0, helper_1.checkIsRelation)(qb, property.propertyPath); const isEmbedded = (0, helper_1.checkIsEmbedded)(qb, property.propertyPath); const alias = (0, helper_1.fixColumnAlias)(property, qb.alias, isRelation, isVirtualProperty, isEmbedded, virtualQuery); const condition = { operator: 'ilike', parameters: [alias, `:${property.column}`], }; if (['postgres', 'cockroachdb'].includes(queryBuilder.connection.options.type)) { condition.parameters[0] = `CAST(${condition.parameters[0]} AS text)`; } qb.orWhere(qb['createWhereConditionExpression'](condition), { [property.column]: `%${query.search}%`, }); } } else { // Multi-word search mode const searchWords = query.search.split(' ').filter((word) => word.length > 0); searchWords.forEach((searchWord, index) => { qb.andWhere(new typeorm_1.Brackets((subQb) => { for (const column of searchBy) { const property = (0, helper_1.getPropertiesByColumnName)(column); const { isVirtualProperty, query: virtualQuery } = (0, helper_1.extractVirtualProperty)(subQb, property); const isRelation = (0, helper_1.checkIsRelation)(subQb, property.propertyPath); const isEmbedded = (0, helper_1.checkIsEmbedded)(subQb, property.propertyPath); const alias = (0, helper_1.fixColumnAlias)(property, subQb.alias, isRelation, isVirtualProperty, isEmbedded, virtualQuery); const condition = { operator: 'ilike', parameters: [alias, `:${property.column}_${index}`], }; if (['postgres', 'cockroachdb'].includes(queryBuilder.connection.options.type)) { condition.parameters[0] = `CAST(${condition.parameters[0]} AS text)`; } subQb.orWhere(subQb['createWhereConditionExpression'](condition), { [`${property.column}_${index}`]: `%${searchWord}%`, }); } })); }); } })); } if (query.limit === PaginationLimit.COUNTER_ONLY) { totalItems = await queryBuilder.getCount(); } else if (isPaginated && config.paginationType !== PaginationType.CURSOR) { if (config.buildCountQuery) { items = await queryBuilder.getMany(); totalItems = await config.buildCountQuery(queryBuilder.clone()).getCount(); } else { ; [items, totalItems] = await queryBuilder.getManyAndCount(); } } else { items = await queryBuilder.getMany(); } const sortByQuery = sortBy.map((order) => `&sortBy=${order.join(':')}`).join(''); const searchQuery = query.search ? `&search=${query.search}` : ''; const searchByQuery = query.searchBy && searchBy.length && !config.ignoreSearchByInQueryParam ? searchBy.map((column) => `&searchBy=${column}`).join('') : ''; // Only expose select in meta data if query select differs from config select const isQuerySelected = (selectParams === null || selectParams === void 0 ? void 0 : selectParams.length) !== ((_c = config.select) === null || _c === void 0 ? void 0 : _c.length); const selectQuery = isQuerySelected ? `&select=${selectParams.join(',')}` : ''; const filterQuery = query.filter ? '&' + (0, querystring_1.stringify)((0, lodash_1.mapKeys)(query.filter, (_param, name) => 'filter.' + name), '&', '=', { encodeURIComponent: (str) => str }) : ''; const options = `&limit=${limit}${sortByQuery}${searchQuery}${searchByQuery}${selectQuery}${filterQuery}`; let path = null; if (query.path !== null) { // `query.path` does not exist in RPC/WS requests and is set to null then. const { queryOrigin, queryPath } = (0, helper_1.getQueryUrlComponents)(query.path); if (config.relativePath) { path = queryPath; } else if (config.origin) { path = config.origin + queryPath; } else { path = queryOrigin + queryPath; } } const buildLink = (p) => path + '?page=' + p + options; const reversedSortBy = sortBy.map(([col, dir]) => [col, dir === 'ASC' ? 'DESC' : 'ASC']); const buildLinkForCursor = (cursor, isReversed = false) => { let adjustedOptions = options; if (isReversed && sortBy.length > 0) { adjustedOptions = `&limit=${limit}${reversedSortBy .map((order) => `&sortBy=${order.join(':')}`) .join('')}${searchQuery}${searchByQuery}${selectQuery}${filterQuery}`; } return path + adjustedOptions.replace(/^./, '?') + (cursor ? `&cursor=${cursor}` : ''); }; const itemsPerPage = limit === PaginationLimit.COUNTER_ONLY ? totalItems : isPaginated ? limit : items.length; const totalItemsForMeta = limit === PaginationLimit.COUNTER_ONLY || isPaginated ? totalItems : items.length; const totalPages = isPaginated ? Math.ceil(totalItems / limit) : 1; const results = { data: items, meta: { itemsPerPage: config.paginationType === PaginationType.CURSOR ? items.length : itemsPerPage, totalItems: config.paginationType === PaginationType.CURSOR ? undefined : totalItemsForMeta, currentPage: config.paginationType === PaginationType.CURSOR ? undefined : page, totalPages: config.paginationType === PaginationType.CURSOR ? undefined : totalPages, sortBy, search: query.search, searchBy: query.search ? searchBy : undefined, select: isQuerySelected ? selectParams : undefined, filter: query.filter, cursor: config.paginationType === PaginationType.CURSOR ? query.cursor : undefined, }, // If there is no `path`, don't build links. links: path !== null ? config.paginationType === PaginationType.CURSOR ? { previous: items.length ? buildLinkForCursor(generateCursor(items[0], reversedSortBy, 'previous'), true) : undefined, current: buildLinkForCursor(query.cursor), next: items.length ? buildLinkForCursor(generateCursor(items[items.length - 1], sortBy)) : undefined, } : { first: page == 1 ? undefined : buildLink(1), previous: page - 1 < 1 ? undefined : buildLink(page - 1), current: buildLink(page), next: page + 1 > totalPages ? undefined : buildLink(page + 1), last: page == totalPages || !totalItems ? undefined : buildLink(totalPages), } : {}, }; return Object.assign(new Paginated(), results); } function addRelationsFromSchema(queryBuilder, schema, config, joinMethods) { var _a; const defaultJoinMethod = (_a = config.defaultJoinMethod) !== null && _a !== void 0 ? _a : 'leftJoinAndSelect'; const createQueryBuilderRelations = (prefix, relations, alias, parentRelation) => { Object.keys(relations).forEach((relationName) => { var _a; const joinMethod = (_a = joinMethods[parentRelation ? `${parentRelation}.${relationName}` : relationName]) !== null && _a !== void 0 ? _a : defaultJoinMethod; queryBuilder[joinMethod](`${alias !== null && alias !== void 0 ? alias : prefix}.${relationName}`, `${alias !== null && alias !== void 0 ? alias : prefix}_${relationName}_rel`); // Check whether this is a non-terminal node with a relation schema to load const relationSchema = relations[relationName]; if (typeof relationSchema === 'object' && relationSchema !== null && Object.keys(relationSchema).length > 0) { createQueryBuilderRelations(relationName, relationSchema, `${alias !== null && alias !== void 0 ? alias : prefix}_${relationName}_rel`, parentRelation ? `${parentRelation}.${relationName}` : relationName); } }); }; createQueryBuilderRelations(queryBuilder.alias, schema); } //# sourceMappingURL=paginate.js.map