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}`
|
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) })
|
await catRepo.save({ ...cats[0], friends: cats.slice(1) })
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Make all tests pass postgres driver.
|
|
||||||
if (process.env.DB === 'postgres') {
|
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 () => {
|
afterAll(async () => {
|
||||||
const entities = dataSource.entityMetadatas
|
const entities = dataSource.entityMetadatas
|
||||||
const tableNames = entities.map((entity) => `"${entity.tableName}"`).join(', ')
|
const tableNames = entities.map((entity) => `"${entity.tableName}"`).join(', ')
|
||||||
|
|
||||||
await dataSource.query(`TRUNCATE ${tableNames} CASCADE;`)
|
await dataSource.query(`TRUNCATE ${tableNames} CASCADE;`)
|
||||||
})
|
})
|
||||||
|
|
||||||
// We end postgres coverage here. See TODO above.
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should return an instance of Paginated', async () => {
|
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')
|
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 () => {
|
it('should not result in a sql syntax error when attempting a sql injection', async () => {
|
||||||
const config: PaginateConfig<CatEntity> = {
|
const config: PaginateConfig<CatEntity> = {
|
||||||
sortableColumns: ['id', 'name', 'color'],
|
sortableColumns: ['id', 'name', 'color'],
|
||||||
@ -537,12 +533,16 @@ describe('paginate', () => {
|
|||||||
it('should return result based on search term on one-to-many relation', async () => {
|
it('should return result based on search term on one-to-many relation', async () => {
|
||||||
const config: PaginateConfig<CatEntity> = {
|
const config: PaginateConfig<CatEntity> = {
|
||||||
relations: ['toys'],
|
relations: ['toys'],
|
||||||
sortableColumns: ['id', 'name'],
|
sortableColumns: ['id', 'toys.id'],
|
||||||
searchableColumns: ['name', 'toys.name'],
|
searchableColumns: ['name', 'toys.name'],
|
||||||
}
|
}
|
||||||
const query: PaginateQuery = {
|
const query: PaginateQuery = {
|
||||||
path: '',
|
path: '',
|
||||||
search: 'Mouse',
|
search: 'Mouse',
|
||||||
|
sortBy: [
|
||||||
|
['id', 'ASC'],
|
||||||
|
['toys.id', 'DESC'],
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await paginate<CatEntity>(query, catRepo, config)
|
const result = await paginate<CatEntity>(query, catRepo, config)
|
||||||
@ -554,7 +554,7 @@ describe('paginate', () => {
|
|||||||
delete toy2.cat
|
delete toy2.cat
|
||||||
|
|
||||||
expect(result.data).toStrictEqual([Object.assign(clone(cats[0]), { toys: [toy2, toy] })])
|
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 () => {
|
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')
|
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 () => {
|
it('should return result based on sort on embedded entity when other relations loaded', async () => {
|
||||||
const config: PaginateConfig<CatEntity> = {
|
const config: PaginateConfig<CatEntity> = {
|
||||||
sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'],
|
sortableColumns: ['id', 'name', 'size.height', 'size.length', 'size.width'],
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
hasColumnWithPropertyPath,
|
hasColumnWithPropertyPath,
|
||||||
includesAllPrimaryKeyColumns,
|
includesAllPrimaryKeyColumns,
|
||||||
isEntityKey,
|
isEntityKey,
|
||||||
|
getQueryUrlComponents,
|
||||||
} from './helper'
|
} from './helper'
|
||||||
import { addFilter, FilterOperator, FilterSuffix } from './filter'
|
import { addFilter, FilterOperator, FilterSuffix } from './filter'
|
||||||
|
|
||||||
@ -94,48 +95,13 @@ export async function paginate<T extends ObjectLiteral>(
|
|||||||
|
|
||||||
const sortBy = [] as SortBy<T>
|
const sortBy = [] as SortBy<T>
|
||||||
const searchBy: Column<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]
|
let [items, totalItems]: [T[], number] = [[], 0]
|
||||||
|
|
||||||
const queryBuilder = repo instanceof Repository ? repo.createQueryBuilder('__root') : repo
|
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 (repo instanceof Repository && !config.relations && config.loadEagerRelations === true) {
|
||||||
if (!config.relations) {
|
if (!config.relations) {
|
||||||
FindOptionsUtils.joinEagerRelations(queryBuilder, queryBuilder.alias, repo.metadata)
|
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) {
|
if (config.nullSort) {
|
||||||
nullSort = config.nullSort === 'last' ? 'NULLS LAST' : 'NULLS FIRST'
|
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) {
|
for (const order of sortBy) {
|
||||||
const columnProperties = getPropertiesByColumnName(order[0])
|
const columnProperties = getPropertiesByColumnName(order[0])
|
||||||
const { isVirtualProperty } = extractVirtualProperty(queryBuilder, columnProperties)
|
const { isVirtualProperty } = extractVirtualProperty(queryBuilder, columnProperties)
|
||||||
@ -252,7 +235,7 @@ export async function paginate<T extends ObjectLiteral>(
|
|||||||
parameters: [alias, `:${property.column}`],
|
parameters: [alias, `:${property.column}`],
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['postgres', 'cockroachdb'].includes(queryBuilder.connection.options.type)) {
|
if (isPostgres) {
|
||||||
condition.parameters[0] = `CAST(${condition.parameters[0]} AS text)`
|
condition.parameters[0] = `CAST(${condition.parameters[0]} AS text)`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,6 +257,16 @@ export async function paginate<T extends ObjectLiteral>(
|
|||||||
items = await queryBuilder.getMany()
|
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 sortByQuery = sortBy.map((order) => `&sortBy=${order.join(':')}`).join('')
|
||||||
const searchQuery = query.search ? `&search=${query.search}` : ''
|
const searchQuery = query.search ? `&search=${query.search}` : ''
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user