UNPKG

nestjs-paginate

Version:

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

439 lines 20.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JSON_COLUMN_TYPES = exports.positiveNumberOrDefault = void 0; exports.isEntityKey = isEntityKey; exports.getPropertiesByColumnName = getPropertiesByColumnName; exports.extractVirtualProperty = extractVirtualProperty; exports.includesAllPrimaryKeyColumns = includesAllPrimaryKeyColumns; exports.getPrimaryKeyColumns = getPrimaryKeyColumns; exports.getMissingPrimaryKeyColumns = getMissingPrimaryKeyColumns; exports.hasColumnWithPropertyPath = hasColumnWithPropertyPath; exports.checkIsRelation = checkIsRelation; exports.checkIsNestedRelation = checkIsNestedRelation; exports.checkIsOneOfNestedPrimaryColumns = checkIsOneOfNestedPrimaryColumns; exports.checkIsEmbedded = checkIsEmbedded; exports.checkIsArray = checkIsArray; exports.checkIsJsonb = checkIsJsonb; exports.resolveJsonbPath = resolveJsonbPath; exports.fixColumnAlias = fixColumnAlias; exports.getQueryUrlComponents = getQueryUrlComponents; exports.isISODate = isISODate; exports.isRepository = isRepository; exports.isFindOperator = isFindOperator; exports.createRelationSchema = createRelationSchema; exports.mergeRelationSchema = mergeRelationSchema; exports.getPaddedExpr = getPaddedExpr; exports.isDateColumnType = isDateColumnType; exports.quoteColumn = quoteColumn; exports.isNil = isNil; exports.isNotNil = isNotNil; exports.andWhereNoneExist = andWhereNoneExist; exports.andWhereAllExist = andWhereAllExist; exports.buildOptimizedCountQuery = buildOptimizedCountQuery; const lodash_1 = require("lodash"); const typeorm_1 = require("typeorm"); const OrmUtils_1 = require("typeorm/util/OrmUtils"); function isEntityKey(entityColumns, column) { return !!entityColumns.find((c) => c === column); } const positiveNumberOrDefault = (value, defaultValue, minValue = 0) => value === undefined || value < minValue ? defaultValue : value; exports.positiveNumberOrDefault = positiveNumberOrDefault; function getPropertiesByColumnName(column) { const propertyPath = column.split('.'); if (propertyPath.length > 1) { const propertyNamePath = propertyPath.slice(1); let isNested = false, propertyName = propertyNamePath.join('.'); if (!propertyName.startsWith('(') && propertyNamePath.length > 1) { isNested = true; } propertyName = propertyName.replace('(', '').replace(')', ''); return { propertyPath: propertyPath[0], propertyName, // the join is in case of an embedded entity isNested, column: `${propertyPath[0]}.${propertyName}`, }; } else { return { propertyName: propertyPath[0], isNested: false, column: propertyPath[0] }; } } function extractVirtualProperty(qb, columnProperties) { var _a, _b, _c, _d, _e, _f, _g, _h; const metadata = columnProperties.propertyPath ? (_e = (_d = (_c = (_b = (_a = qb === null || qb === void 0 ? void 0 : qb.expressionMap) === null || _a === void 0 ? void 0 : _a.mainAlias) === null || _b === void 0 ? void 0 : _b.metadata) === null || _c === void 0 ? void 0 : _c.findColumnWithPropertyPath(columnProperties.propertyPath)) === null || _d === void 0 ? void 0 : _d.referencedColumn) === null || _e === void 0 ? void 0 : _e.entityMetadata // on relation : (_g = (_f = qb === null || qb === void 0 ? void 0 : qb.expressionMap) === null || _f === void 0 ? void 0 : _f.mainAlias) === null || _g === void 0 ? void 0 : _g.metadata; return (((_h = metadata === null || metadata === void 0 ? void 0 : metadata.columns) === null || _h === void 0 ? void 0 : _h.find((column) => column.propertyName === columnProperties.propertyName)) || { isVirtualProperty: false, query: undefined, }); } function includesAllPrimaryKeyColumns(qb, propertyPath) { var _a, _b; if (!qb || !propertyPath) { return false; } return (_b = (_a = qb.expressionMap.mainAlias) === null || _a === void 0 ? void 0 : _a.metadata) === null || _b === void 0 ? void 0 : _b.primaryColumns.map((column) => column.propertyPath).every((column) => propertyPath.includes(column)); } function getPrimaryKeyColumns(qb, entityName) { var _a, _b; return (_b = (_a = qb.expressionMap.mainAlias) === null || _a === void 0 ? void 0 : _a.metadata) === null || _b === void 0 ? void 0 : _b.primaryColumns.map((column) => entityName ? `${entityName}.${column.propertyName}` : column.propertyName); } function getMissingPrimaryKeyColumns(qb, transformedCols) { if (!transformedCols || transformedCols.length === 0) return []; const mainEntityPrimaryKeys = getPrimaryKeyColumns(qb); const missingPrimaryKeys = []; for (const pk of mainEntityPrimaryKeys) { const columnProperties = getPropertiesByColumnName(pk); const pkAlias = fixColumnAlias(columnProperties, qb.alias, false, false, false, undefined, qb); if (!transformedCols.includes(pkAlias)) { missingPrimaryKeys.push(pkAlias); } } return missingPrimaryKeys; } function hasColumnWithPropertyPath(qb, columnProperties) { var _a, _b; if (!qb || !columnProperties) { return false; } return !!((_b = (_a = qb.expressionMap.mainAlias) === null || _a === void 0 ? void 0 : _a.metadata) === null || _b === void 0 ? void 0 : _b.hasColumnWithPropertyPath(columnProperties.propertyName)); } function checkIsRelation(qb, propertyPath) { var _a, _b, _c; if (!qb || !propertyPath) { return false; } return !!((_c = (_b = (_a = qb === null || qb === void 0 ? void 0 : qb.expressionMap) === null || _a === void 0 ? void 0 : _a.mainAlias) === null || _b === void 0 ? void 0 : _b.metadata) === null || _c === void 0 ? void 0 : _c.hasRelationWithPropertyPath(propertyPath)); } function checkIsNestedRelation(qb, propertyPath) { var _a, _b; let metadata = (_b = (_a = qb === null || qb === void 0 ? void 0 : qb.expressionMap) === null || _a === void 0 ? void 0 : _a.mainAlias) === null || _b === void 0 ? void 0 : _b.metadata; for (const relationName of propertyPath.split('.')) { const relation = metadata === null || metadata === void 0 ? void 0 : metadata.relations.find((relation) => relation.propertyPath === relationName); if (!relation) { return false; } metadata = relation.inverseEntityMetadata; } return true; } function checkIsOneOfNestedPrimaryColumns(qb, propertyPath) { var _a, _b; let metadata = (_b = (_a = qb === null || qb === void 0 ? void 0 : qb.expressionMap) === null || _a === void 0 ? void 0 : _a.mainAlias) === null || _b === void 0 ? void 0 : _b.metadata; const [deepestProperty, ...subRelations] = propertyPath.split('.').reverse(); for (const relationName of subRelations.reverse()) { const relation = metadata === null || metadata === void 0 ? void 0 : metadata.relations.find((relation) => relation.propertyPath === relationName); if (!relation) { return false; } metadata = relation.inverseEntityMetadata; } return !!metadata.primaryColumns.find((col) => col.propertyName === deepestProperty); } function checkIsEmbedded(qb, propertyPath) { var _a, _b, _c; if (!qb || !propertyPath) { return false; } return !!((_c = (_b = (_a = qb === null || qb === void 0 ? void 0 : qb.expressionMap) === null || _a === void 0 ? void 0 : _a.mainAlias) === null || _b === void 0 ? void 0 : _b.metadata) === null || _c === void 0 ? void 0 : _c.hasEmbeddedWithPropertyPath(propertyPath)); } function checkIsArray(qb, propertyName) { var _a, _b, _c; if (!qb || !propertyName) { return false; } return !!((_c = (_b = (_a = qb === null || qb === void 0 ? void 0 : qb.expressionMap) === null || _a === void 0 ? void 0 : _a.mainAlias) === null || _b === void 0 ? void 0 : _b.metadata.findColumnWithPropertyName(propertyName)) === null || _c === void 0 ? void 0 : _c.isArray); } function checkIsJsonb(qb, propertyName) { if (!qb || !propertyName) { return false; } const resolution = resolveJsonbPath(qb, propertyName); return resolution.isJsonb; } /** * Column data types treated as JSON. Both `jsonb` and plain `json` are supported: * `#>>` path extraction works on both, and TypeORM's `JsonContains` ($eq/$in/$contains) * emits `<column> ::jsonb @> :value`, which casts a `json` column to `jsonb` for free. */ exports.JSON_COLUMN_TYPES = ['jsonb', 'json']; /** * Walks the dot-separated `column` path through TypeORM entity metadata to determine * whether the path terminates in a JSONB column and, if so, where the relation chain * ends and the JSON key path begins. * * Algorithm: * For each segment, check whether the current entity metadata has a relation * with that name. If yes, follow the relation and continue. If no, check * whether it is a JSONB column on the current entity. If yes, all remaining * segments are JSON key path. Otherwise, the path is not JSONB. */ function resolveJsonbPath(qb, column) { var _a, _b, _c, _d, _e; const notJsonb = { isJsonb: false, relationPath: [], jsonbColumn: '', jsonPath: [] }; if (!qb || !column) { return notJsonb; } const parts = column.split('.'); // A plain column name without dots is not a JSONB path — callers use checkIsJsonb directly. if (parts.length < 2) { return notJsonb; } let metadata = (_b = (_a = qb === null || qb === void 0 ? void 0 : qb.expressionMap) === null || _a === void 0 ? void 0 : _a.mainAlias) === null || _b === void 0 ? void 0 : _b.metadata; const relationPath = []; for (let i = 0; i < parts.length - 1; i++) { const segment = parts[i]; const relation = (_c = metadata === null || metadata === void 0 ? void 0 : metadata.relations) === null || _c === void 0 ? void 0 : _c.find((r) => r.propertyPath === segment); if (relation) { relationPath.push(segment); metadata = relation.inverseEntityMetadata; } else { // Not a relation — check whether it is a JSON(B) column const columnType = (_d = metadata === null || metadata === void 0 ? void 0 : metadata.findColumnWithPropertyName(segment)) === null || _d === void 0 ? void 0 : _d.type; if (!exports.JSON_COLUMN_TYPES.includes(columnType)) { return notJsonb; } return { isJsonb: true, relationPath, jsonbColumn: segment, jsonPath: parts.slice(i + 1), }; } } // All segments except the last were relations; the last segment must be a JSON(B) column. const lastSegment = parts[parts.length - 1]; const lastColumnType = (_e = metadata === null || metadata === void 0 ? void 0 : metadata.findColumnWithPropertyName(lastSegment)) === null || _e === void 0 ? void 0 : _e.type; if (exports.JSON_COLUMN_TYPES.includes(lastColumnType)) { return { isJsonb: true, relationPath, jsonbColumn: lastSegment, jsonPath: [], }; } return notJsonb; } // This function is used to fix the column alias when using relation, embedded or virtual properties function fixColumnAlias(properties, alias, isRelation = false, isVirtualProperty = false, isEmbedded = false, query, qb) { let jsonbResolution; if (qb) { jsonbResolution = resolveJsonbPath(qb, properties.column); } if (jsonbResolution && jsonbResolution.isJsonb) { const baseColumnProperties = getPropertiesByColumnName([...jsonbResolution.relationPath, jsonbResolution.jsonbColumn].join('.')); const baseAlias = fixColumnAlias(baseColumnProperties, alias, jsonbResolution.relationPath.length > 0, isVirtualProperty, isEmbedded, query); if (jsonbResolution.jsonPath.length === 0) { return baseAlias; } const dbType = qb.connection.options.type; if (dbType === 'postgres' || dbType === 'cockroachdb') { const pathLiteral = jsonbResolution.jsonPath.join(','); return `${baseAlias} #>> '{${pathLiteral}}'`; } else if (dbType === 'mysql' || dbType === 'mariadb') { const mysqlPath = jsonbResolution.jsonPath.map((p) => `"${p}"`).join('.'); return `JSON_UNQUOTE(JSON_EXTRACT(${baseAlias}, '$.${mysqlPath}'))`; } else { const sqlitePath = jsonbResolution.jsonPath.map((p) => `"${p}"`).join('.'); return `json_extract(${baseAlias}, '$.${sqlitePath}')`; } } if (isRelation) { if (isVirtualProperty && query) { return `(${query(`${alias}_${properties.propertyPath}_rel`)})`; // () is needed to avoid parameter conflict } else if ((isVirtualProperty && !query) || properties.isNested) { if (properties.propertyName.includes('.')) { const propertyPath = properties.propertyName.split('.'); const nestedRelations = propertyPath .slice(0, -1) .map((v) => `${v}_rel`) .join('_'); const nestedCol = propertyPath[propertyPath.length - 1]; return `${alias}_${properties.propertyPath}_rel_${nestedRelations}.${nestedCol}`; } else { return `${alias}_${properties.propertyPath}_rel_${properties.propertyName}`; } } else { return `${alias}_${properties.propertyPath}_rel.${properties.propertyName}`; } } else if (isVirtualProperty) { return query ? `(${query(`${alias}`)})` : `${alias}_${properties.propertyName}`; } else if (isEmbedded) { return `${alias}.${properties.propertyPath}.${properties.propertyName}`; } else { return `${alias}.${properties.propertyName}`; } } function getQueryUrlComponents(path) { const r = new RegExp('^(?:[a-z+]+:)?//', 'i'); let queryOrigin = ''; let queryPath = ''; if (r.test(path)) { const url = new URL(path); queryOrigin = url.origin; queryPath = url.pathname; } else { queryPath = path; } return { queryOrigin, queryPath }; } const isoDateRegExp = new RegExp(/^((\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)))$/); function isISODate(str) { return isoDateRegExp.test(str); } function isRepository(repo) { if (repo instanceof typeorm_1.Repository) return true; try { if (Object.getPrototypeOf(repo).constructor.name === 'Repository') return true; return typeof repo === 'object' && !('connection' in repo) && 'manager' in repo; } catch (_a) { return false; } } function isFindOperator(value) { if (value instanceof typeorm_1.FindOperator) return true; try { if (Object.getPrototypeOf(value).constructor.name === 'FindOperator') return true; return typeof value === 'object' && '_type' in value && '_value' in value; } catch (_a) { return false; } } function createRelationSchema(configurationRelations) { return Array.isArray(configurationRelations) ? OrmUtils_1.OrmUtils.propertyPathsToTruthyObject(configurationRelations) : configurationRelations; } function mergeRelationSchema(...schemas) { const noTrueOverride = (obj, source) => (source === true && obj !== undefined ? obj : undefined); return (0, lodash_1.mergeWith)({}, ...schemas, noTrueOverride); } function getPaddedExpr(valueExpr, length, dbType) { const lengthStr = String(length); if (dbType === 'postgres' || dbType === 'cockroachdb') { return `LPAD((${valueExpr})::bigint::text, ${lengthStr}, '0')`; } else if (dbType === 'mysql' || dbType === 'mariadb') { return `LPAD(${valueExpr}, ${lengthStr}, '0')`; } else { // sqlite const padding = '0'.repeat(length); return `SUBSTR('${padding}' || CAST(${valueExpr} AS INTEGER), -${lengthStr}, ${lengthStr})`; } } function isDateColumnType(type) { const dateTypes = [ Date, // JavaScript Date class 'datetime', 'timestamp', 'timestamptz', ]; return dateTypes.includes(type); } function quoteColumn(columnName, isMySqlOrMariaDb) { return isMySqlOrMariaDb ? `\`${columnName}\`` : `"${columnName}"`; } function isNil(v) { return v === null || v === undefined; } function isNotNil(v) { return !isNil(v); } function andWhereNoneExist(qb, existsQb) { const [query, params] = qb['getExistsCondition'](existsQb); return qb.andWhere(`NOT ${query}`, params); } /** * Adds a condition to the query builder that ensures all related entities match the given filter criteria. * * This method combines two conditions: * 1. EXISTS(X) - There must be at least one related entity matching the criteria * 2. NOT EXISTS(NOT X) - There must not be any related entities that don't match the criteria * * Together, these conditions ensure that all related entities match the filter criteria X. * For example, when filtering pillows in a cat home, this could find homes where ALL pillows are red. * * If you need to include cases where there are either 0 or all entities match, use $none:$not:X instead. * * @param {SelectQueryBuilder<any>} qb The main query builder instance to add the condition to. * @param {SelectQueryBuilder<any>} existsQb The subquery builder containing the filter criteria. * @return {SelectQueryBuilder<any>} The modified query builder with the combined EXISTS conditions. */ function andWhereAllExist(qb, existsQb) { qb = qb.andWhereExists(existsQb); const [query, params] = qb['getExistsCondition'](existsQb); // The getExistsCondition clears anything that comes after WHERE, and our joining logic does not contain WHERE, // so it should be safe to replace the first WHERE with WHERE NOT (...) and get a correct query. const existsWhereNot = query.replace('WHERE', 'WHERE NOT (') + ')'; return qb.andWhere(`NOT ${existsWhereNot}`, params); } /** * Strips the parts of a fully-built paginate query that do not affect how many root * entities match, so the count query stays cheap even when many relations are joined * for hydration. * * Pruning rules: * - INNER joins are always kept: they restrict the result set even when unreferenced. * - LEFT joins are kept only when the WHERE clause references their alias. * - Parent joins of any kept join are kept, so nested relation chains stay intact. * - ORDER BY is cleared, since ordering does not change the count. * * Used by `paginate` when `PaginateConfig.optimizedCount` is enabled. It can also be * composed inside a custom `PaginateConfig.buildCountQuery`. * * @param {SelectQueryBuilder<T>} qb A clone of the fully-built query builder. * @return {SelectQueryBuilder<T>} The same builder with count-irrelevant joins removed. */ function buildOptimizedCountQuery(qb) { var _a; qb.orderBy(); // Protected TypeORM API that renders only the WHERE clause. Slicing getQuery() at // its first WHERE instead would false-match subqueries rendered into the SELECT // clause, such as virtual columns. const whereSql = qb['createWhereExpression'](); const joins = qb.expressionMap.joinAttributes; const rootAlias = (_a = qb.expressionMap.mainAlias) === null || _a === void 0 ? void 0 : _a.name; const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const isReferenced = (alias) => whereSql.includes(`"${alias}".`) || new RegExp(`(?<![\\w"])${escapeRegExp(alias)}\\.`).test(whereSql); const kept = new Set(); for (const join of joins) { if (join.direction === 'INNER' || isReferenced(join.alias.name)) { kept.add(join.alias.name); } } let added = true; while (added) { added = false; for (const join of joins) { if (!kept.has(join.alias.name)) continue; const parent = join.parentAlias; if (parent && parent !== rootAlias && !kept.has(parent)) { kept.add(parent); added = true; } } } qb.expressionMap.joinAttributes = joins.filter((join) => kept.has(join.alias.name)); return qb; } //# sourceMappingURL=helper.js.map