fix: align postgres default null sort with docs and improve postgres coverage
This commit is contained in:
parent
8b9c32247f
commit
a6648e8d62
@ -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 }
|
||||
}
|
||||
|
@ -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'],
|
||||
|
@ -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}` : ''
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user