feat: added operators for postgres array column type (#826)

This commit is contained in:
Jonathan Chapman 2023-12-04 02:32:05 -07:00 committed by GitHub
parent 88cd95295d
commit ee86e8bdf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 126 additions and 15 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ coverage/
npm-debug.log
.history
.idea/
.env

3
jest.setup.ts Normal file
View File

@ -0,0 +1,3 @@
import * as dotenv from 'dotenv'
dotenv.config({ path: '.env' })

16
package-lock.json generated
View File

@ -21,6 +21,7 @@
"@types/node": "^20.10.1",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"dotenv": "^16.3.1",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
@ -3484,12 +3485,15 @@
}
},
"node_modules/dotenv": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/motdotla/dotenv?sponsor=1"
}
},
"node_modules/ee-first": {
@ -10950,9 +10954,9 @@
}
},
"dotenv": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
"version": "16.3.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
"dev": true
},
"ee-first": {

View File

@ -8,7 +8,7 @@
"files": [
"lib/**/*"
],
"description": "Pagination and filtering helper method for TypeORM repostiories or query builders using Nest.js framework.",
"description": "Pagination and filtering helper method for TypeORM repositories or query builders using Nest.js framework.",
"keywords": [
"nestjs",
"typeorm",
@ -32,15 +32,16 @@
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
},
"devDependencies": {
"@nestjs/testing": "^10.2.10",
"@nestjs/platform-express": "^10.2.10",
"@nestjs/common": "^10.2.10",
"@nestjs/platform-express": "^10.2.10",
"@nestjs/testing": "^10.2.10",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.10",
"@types/lodash": "^4.14.201",
"@types/node": "^20.10.1",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"dotenv": "^16.3.1",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
@ -78,7 +79,8 @@
"^.+\\.(t|j)s$": "ts-jest"
},
"coverageDirectory": "../coverage",
"testEnvironment": "node"
"testEnvironment": "node",
"setupFiles": ["<rootDir>/../jest.setup.ts"]
},
"repository": {
"type": "git",

View File

@ -0,0 +1,16 @@
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
@Entity()
export class CatHairEntity {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@Column({ type: 'text', array: true, default: '{}' })
colors: string[]
@CreateDateColumn()
createdAt: string
}

View File

@ -18,6 +18,7 @@ import {
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
import { PaginateQuery } from './decorator'
import {
checkIsArray,
checkIsEmbedded,
checkIsRelation,
extractVirtualProperty,
@ -162,6 +163,8 @@ export function addWhereCondition<T>(qb: SelectQueryBuilder<T>, column: string,
const { isVirtualProperty, query: virtualQuery } = extractVirtualProperty(qb, columnProperties)
const isRelation = checkIsRelation(qb, columnProperties.propertyPath)
const isEmbedded = checkIsEmbedded(qb, columnProperties.propertyPath)
const isArray = checkIsArray(qb, columnProperties.propertyName)
const alias = fixColumnAlias(columnProperties, qb.alias, isRelation, isVirtualProperty, isEmbedded, virtualQuery)
filter[column].forEach((columnFilter: Filter, index: number) => {
const columnNamePerIteration = `${columnProperties.column}${index}`
@ -175,6 +178,9 @@ export function addWhereCondition<T>(qb: SelectQueryBuilder<T>, column: string,
const parameters = fixQueryParam(alias, columnNamePerIteration, columnFilter, condition, {
[columnNamePerIteration]: columnFilter.findOperator.value,
})
if (isArray && condition.parameters?.length && !['not', 'isNull'].includes(condition.operator)) {
condition.parameters[0] = `cardinality(${condition.parameters[0]})`
}
if (columnFilter.comparator === FilterComparator.OR) {
qb.orWhere(qb['createWhereConditionExpression'](condition), parameters)
} else {

View File

@ -126,6 +126,13 @@ export function checkIsEmbedded(qb: SelectQueryBuilder<unknown>, propertyPath: s
return !!qb?.expressionMap?.mainAlias?.metadata?.hasEmbeddedWithPropertyPath(propertyPath)
}
export function checkIsArray(qb: SelectQueryBuilder<unknown>, propertyName: string): boolean {
if (!qb || !propertyName) {
return false
}
return !!qb?.expressionMap?.mainAlias?.metadata.findColumnWithPropertyName(propertyName)?.isArray
}
// This function is used to fix the column alias when using relation, embedded or virtual properties
export function fixColumnAlias(
properties: ColumnProperties,

View File

@ -18,6 +18,8 @@ import {
} from './filter'
import { ToyShopEntity } from './__tests__/toy-shop.entity'
import { ToyShopAddressEntity } from './__tests__/toy-shop-address.entity'
import * as process from 'process'
import { CatHairEntity } from './__tests__/cat-hair.entity'
const isoStringToDate = (isoString) => new Date(isoString)
@ -25,6 +27,7 @@ describe('paginate', () => {
let dataSource: DataSource
let catRepo: Repository<CatEntity>
let catToyRepo: Repository<CatToyEntity>
let catHairRepo: Repository<CatHairEntity>
let toyShopRepo: Repository<ToyShopEntity>
let toyShopAddressRepository: Repository<ToyShopAddressEntity>
let catHomeRepo: Repository<CatHomeEntity>
@ -36,17 +39,18 @@ describe('paginate', () => {
let toysShops: ToyShopEntity[]
let catHomes: CatHomeEntity[]
let catHomePillows: CatHomePillowEntity[]
let catHairs: CatHairEntity[] = []
beforeAll(async () => {
dataSource = new DataSource({
...(process.env.DB === 'postgres'
? {
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'root',
password: 'pass',
database: 'test',
host: process.env.DB_HOST || 'localhost',
port: +process.env.DB_PORT || 5432,
username: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || 'pass',
database: process.env.DB_DATABASE || 'test',
}
: {
type: 'sqlite',
@ -61,6 +65,7 @@ describe('paginate', () => {
CatHomeEntity,
CatHomePillowEntity,
ToyShopEntity,
process.env.DB === 'postgres' ? CatHairEntity : undefined,
],
})
await dataSource.initialize()
@ -163,6 +168,18 @@ describe('paginate', () => {
// add friends to Milo
await catRepo.save({ ...cats[0], friends: cats.slice(1) })
catHairs = []
if (process.env.DB === 'postgres') {
catHairRepo = dataSource.getRepository(CatHairEntity)
catHairs = await catHairRepo.save([
catHairRepo.create({ name: 'short', colors: ['white', 'brown', 'black'] }),
catHairRepo.create({ name: 'long', colors: ['white', 'brown'] }),
catHairRepo.create({ name: 'buzzed', colors: ['white'] }),
catHairRepo.create({ name: 'none' }),
])
}
})
if (process.env.DB === 'postgres') {
@ -2813,6 +2830,61 @@ describe('paginate', () => {
})
})
if (process.env.DB === 'postgres') {
describe('should return results for an array column', () => {
it.each`
operator | data | expectedIndexes
${'$not:$null'} | ${undefined} | ${[0, 1, 2, 3]}
${'$lt'} | ${2} | ${[2, 3]}
${'$lte'} | ${2} | ${[1, 2, 3]}
${'$btw'} | ${'1,2'} | ${[1, 2]}
${'$gte'} | ${2} | ${[0, 1]}
${'$gt'} | ${2} | ${[0]}
`('with $operator operator', async ({ operator, data, expectedIndexes }) => {
const config: PaginateConfig<CatHairEntity> = {
sortableColumns: ['id'],
filterableColumns: {
colors: true,
},
}
const queryFilter = `${operator}${data ? `:${data}` : ''}`
const query: PaginateQuery = {
path: '',
filter: {
colors: queryFilter,
},
}
const result = await paginate<CatHairEntity>(query, catHairRepo, config)
expect(result.meta.filter).toStrictEqual({
colors: queryFilter,
})
expect(result.data).toStrictEqual(expectedIndexes.map((index) => catHairs[index]))
expect(result.links.current).toBe(`?page=1&limit=20&sortBy=id:ASC&filter.colors=${queryFilter}`)
})
it('should work with search', async () => {
const config: PaginateConfig<CatHairEntity> = {
sortableColumns: ['id'],
searchableColumns: ['colors'],
}
const query: PaginateQuery = {
path: '',
search: 'brown',
}
const result = await paginate<CatHairEntity>(query, catHairRepo, config)
expect(result.meta.search).toStrictEqual('brown')
expect(result.data).toStrictEqual([catHairs[0], catHairs[1]])
expect(result.links.current).toBe(`?page=1&limit=20&sortBy=id:ASC&search=brown`)
})
})
}
if (process.env.DB !== 'postgres') {
describe('should return result based on virtual column', () => {
it('should return result sorted and filter by a virtual column in main entity', async () => {