fix: align postgres default null sort with docs and improve postgres coverage

This commit is contained in:
ppetzold 2023-03-17 22:23:46 +01:00
parent 8b9c32247f
commit a6648e8d62
3 changed files with 75 additions and 62 deletions

View File

@ -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 }
}

View File

@ -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<CatEntity> = {
sortableColumns: ['id', 'name', 'color'],
searchableColumns: ['cutenessLevel'],
}
const query: PaginateQuery = {
path: '',
search: 'hi',
}
const result = await paginate<CatEntity>(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<CatEntity> = {
sortableColumns: ['id', 'name', 'color'],
searchableColumns: ['cutenessLevel'],
}
const query: PaginateQuery = {
path: '',
search: 'hi',
}
const result = await paginate<CatEntity>(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<CatEntity> = {
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<CatEntity> = {
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<CatEntity>(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<CatEntity> = {
sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'],

View File

@ -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<T extends ObjectLiteral>(
const sortBy = [] as SortBy<T>
const searchBy: Column<T>[] = []
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<T>)
}
}
}
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<T extends ObjectLiteral>(
}
}
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<T>)
}
}
}
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<T extends ObjectLiteral>(
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<T extends ObjectLiteral>(
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}` : ''