diff --git a/src/helper.ts b/src/helper.ts index 0f0c294..8b2069c 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -141,3 +141,17 @@ export function fixColumnAlias( return `${alias}.${properties.propertyName}` } } + +export function getQueryUrlComponents(path: string): { queryOrigin: string; queryPath: string } { + 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 } +} diff --git a/src/paginate.spec.ts b/src/paginate.spec.ts index a273957..f45ea0c 100644 --- a/src/paginate.spec.ts +++ b/src/paginate.spec.ts @@ -113,34 +113,13 @@ describe('paginate', () => { await catRepo.save({ ...cats[0], friends: cats.slice(1) }) }) - // TODO: Make all tests pass postgres driver. if (process.env.DB === 'postgres') { - it('should return result based on search term including a camelcase named column', async () => { - const config: PaginateConfig = { - sortableColumns: ['id', 'name', 'color'], - searchableColumns: ['cutenessLevel'], - } - const query: PaginateQuery = { - path: '', - search: 'hi', - } - - const result = await paginate(query, catRepo, config) - - expect(result.meta.search).toStrictEqual('hi') - expect(result.data).toStrictEqual([cats[0], cats[2], cats[4]]) - expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=hi') - }) - afterAll(async () => { const entities = dataSource.entityMetadatas const tableNames = entities.map((entity) => `"${entity.tableName}"`).join(', ') await dataSource.query(`TRUNCATE ${tableNames} CASCADE;`) }) - - // We end postgres coverage here. See TODO above. - return } it('should return an instance of Paginated', async () => { @@ -501,6 +480,23 @@ describe('paginate', () => { expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=i') }) + it('should return result based on search term on a camelcase named column', async () => { + const config: PaginateConfig = { + sortableColumns: ['id', 'name', 'color'], + searchableColumns: ['cutenessLevel'], + } + const query: PaginateQuery = { + path: '', + search: 'hi', + } + + const result = await paginate(query, catRepo, config) + + expect(result.meta.search).toStrictEqual('hi') + expect(result.data).toStrictEqual([cats[0], cats[2], cats[4]]) + expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=hi') + }) + it('should not result in a sql syntax error when attempting a sql injection', async () => { const config: PaginateConfig = { sortableColumns: ['id', 'name', 'color'], @@ -537,12 +533,16 @@ describe('paginate', () => { it('should return result based on search term on one-to-many relation', async () => { const config: PaginateConfig = { relations: ['toys'], - sortableColumns: ['id', 'name'], + sortableColumns: ['id', 'toys.id'], searchableColumns: ['name', 'toys.name'], } const query: PaginateQuery = { path: '', search: 'Mouse', + sortBy: [ + ['id', 'ASC'], + ['toys.id', 'DESC'], + ], } const result = await paginate(query, catRepo, config) @@ -554,7 +554,7 @@ describe('paginate', () => { delete toy2.cat expect(result.data).toStrictEqual([Object.assign(clone(cats[0]), { toys: [toy2, toy] })]) - expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&search=Mouse') + expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&sortBy=toys.id:DESC&search=Mouse') }) it('should return result based on search term on one-to-one relation', async () => { @@ -904,6 +904,12 @@ describe('paginate', () => { expect(result.links.current).toBe('?page=1&limit=20&sortBy=size.height:ASC&sortBy=size.length:ASC') }) + // TODO: Make all tests pass postgres driver. + if (process.env.DB === 'postgres') { + // We end postgres coverage here. See TODO above. + return + } + it('should return result based on sort on embedded entity when other relations loaded', async () => { const config: PaginateConfig = { sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'], diff --git a/src/paginate.ts b/src/paginate.ts index 42a7eb5..80e95c5 100644 --- a/src/paginate.ts +++ b/src/paginate.ts @@ -26,6 +26,7 @@ import { hasColumnWithPropertyPath, includesAllPrimaryKeyColumns, isEntityKey, + getQueryUrlComponents, } from './helper' import { addFilter, FilterOperator, FilterSuffix } from './filter' @@ -94,48 +95,13 @@ export async function paginate( const sortBy = [] as SortBy const searchBy: Column[] = [] - let path: string - - const r = new RegExp('^(?:[a-z+]+:)?//', 'i') - let queryOrigin = '' - let queryPath = '' - if (r.test(query.path)) { - const url = new URL(query.path) - queryOrigin = url.origin - queryPath = url.pathname - } else { - queryPath = query.path - } - - if (config.relativePath) { - path = queryPath - } else if (config.origin) { - path = config.origin + queryPath - } else { - path = queryOrigin + queryPath - } - - if (config.sortableColumns.length < 1) { - logger.debug("Missing required 'sortableColumns' config.") - throw new ServiceUnavailableException() - } - - if (query.sortBy) { - for (const order of query.sortBy) { - if (isEntityKey(config.sortableColumns, order[0]) && ['ASC', 'DESC'].includes(order[1])) { - sortBy.push(order as Order) - } - } - } - - if (!sortBy.length) { - sortBy.push(...(config.defaultSortBy || [[config.sortableColumns[0], 'ASC']])) - } let [items, totalItems]: [T[], number] = [[], 0] const queryBuilder = repo instanceof Repository ? repo.createQueryBuilder('__root') : repo + const isPostgres = ['postgres', 'cockroachdb'].includes(queryBuilder.connection.options.type) + if (repo instanceof Repository && !config.relations && config.loadEagerRelations === true) { if (!config.relations) { FindOptionsUtils.joinEagerRelations(queryBuilder, queryBuilder.alias, repo.metadata) @@ -177,11 +143,28 @@ export async function paginate( } } - let nullSort: 'NULLS LAST' | 'NULLS FIRST' | undefined = undefined + let nullSort: 'NULLS LAST' | 'NULLS FIRST' | undefined = isPostgres ? 'NULLS FIRST' : undefined if (config.nullSort) { nullSort = config.nullSort === 'last' ? 'NULLS LAST' : 'NULLS FIRST' } + if (config.sortableColumns.length < 1) { + logger.debug("Missing required 'sortableColumns' config.") + throw new ServiceUnavailableException() + } + + if (query.sortBy) { + for (const order of query.sortBy) { + if (isEntityKey(config.sortableColumns, order[0]) && ['ASC', 'DESC'].includes(order[1])) { + sortBy.push(order as Order) + } + } + } + + if (!sortBy.length) { + sortBy.push(...(config.defaultSortBy || [[config.sortableColumns[0], 'ASC']])) + } + for (const order of sortBy) { const columnProperties = getPropertiesByColumnName(order[0]) const { isVirtualProperty } = extractVirtualProperty(queryBuilder, columnProperties) @@ -252,7 +235,7 @@ export async function paginate( parameters: [alias, `:${property.column}`], } - if (['postgres', 'cockroachdb'].includes(queryBuilder.connection.options.type)) { + if (isPostgres) { condition.parameters[0] = `CAST(${condition.parameters[0]} AS text)` } @@ -274,6 +257,16 @@ export async function paginate( items = await queryBuilder.getMany() } + let path: string + const { queryOrigin, queryPath } = getQueryUrlComponents(query.path) + if (config.relativePath) { + path = queryPath + } else if (config.origin) { + path = config.origin + queryPath + } else { + path = queryOrigin + queryPath + } + const sortByQuery = sortBy.map((order) => `&sortBy=${order.join(':')}`).join('') const searchQuery = query.search ? `&search=${query.search}` : ''