Skip to content

Commit ee86e8b

Browse files
authored
feat: added operators for postgres array column type (#826)
1 parent 88cd952 commit ee86e8b

File tree

8 files changed

+126
-15
lines changed

8 files changed

+126
-15
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ coverage/
55
npm-debug.log
66
.history
77
.idea/
8+
.env

jest.setup.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import * as dotenv from 'dotenv'
2+
3+
dotenv.config({ path: '.env' })

package-lock.json

Lines changed: 10 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"files": [
99
"lib/**/*"
1010
],
11-
"description": "Pagination and filtering helper method for TypeORM repostiories or query builders using Nest.js framework.",
11+
"description": "Pagination and filtering helper method for TypeORM repositories or query builders using Nest.js framework.",
1212
"keywords": [
1313
"nestjs",
1414
"typeorm",
@@ -32,15 +32,16 @@
3232
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
3333
},
3434
"devDependencies": {
35-
"@nestjs/testing": "^10.2.10",
36-
"@nestjs/platform-express": "^10.2.10",
3735
"@nestjs/common": "^10.2.10",
36+
"@nestjs/platform-express": "^10.2.10",
37+
"@nestjs/testing": "^10.2.10",
3838
"@types/express": "^4.17.21",
3939
"@types/jest": "^29.5.10",
4040
"@types/lodash": "^4.14.201",
4141
"@types/node": "^20.10.1",
4242
"@typescript-eslint/eslint-plugin": "^6.11.0",
4343
"@typescript-eslint/parser": "^6.11.0",
44+
"dotenv": "^16.3.1",
4445
"eslint": "^8.54.0",
4546
"eslint-config-prettier": "^9.0.0",
4647
"eslint-plugin-prettier": "^5.0.1",
@@ -78,7 +79,8 @@
7879
"^.+\\.(t|j)s$": "ts-jest"
7980
},
8081
"coverageDirectory": "../coverage",
81-
"testEnvironment": "node"
82+
"testEnvironment": "node",
83+
"setupFiles": ["<rootDir>/../jest.setup.ts"]
8284
},
8385
"repository": {
8486
"type": "git",

src/__tests__/cat-hair.entity.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'
2+
3+
@Entity()
4+
export class CatHairEntity {
5+
@PrimaryGeneratedColumn()
6+
id: number
7+
8+
@Column()
9+
name: string
10+
11+
@Column({ type: 'text', array: true, default: '{}' })
12+
colors: string[]
13+
14+
@CreateDateColumn()
15+
createdAt: string
16+
}

src/filter.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { WherePredicateOperator } from 'typeorm/query-builder/WhereClause'
1919
import { PaginateQuery } from './decorator'
2020
import {
21+
checkIsArray,
2122
checkIsEmbedded,
2223
checkIsRelation,
2324
extractVirtualProperty,
@@ -162,6 +163,8 @@ export function addWhereCondition<T>(qb: SelectQueryBuilder<T>, column: string,
162163
const { isVirtualProperty, query: virtualQuery } = extractVirtualProperty(qb, columnProperties)
163164
const isRelation = checkIsRelation(qb, columnProperties.propertyPath)
164165
const isEmbedded = checkIsEmbedded(qb, columnProperties.propertyPath)
166+
const isArray = checkIsArray(qb, columnProperties.propertyName)
167+
165168
const alias = fixColumnAlias(columnProperties, qb.alias, isRelation, isVirtualProperty, isEmbedded, virtualQuery)
166169
filter[column].forEach((columnFilter: Filter, index: number) => {
167170
const columnNamePerIteration = `${columnProperties.column}${index}`
@@ -175,6 +178,9 @@ export function addWhereCondition<T>(qb: SelectQueryBuilder<T>, column: string,
175178
const parameters = fixQueryParam(alias, columnNamePerIteration, columnFilter, condition, {
176179
[columnNamePerIteration]: columnFilter.findOperator.value,
177180
})
181+
if (isArray && condition.parameters?.length && !['not', 'isNull'].includes(condition.operator)) {
182+
condition.parameters[0] = `cardinality(${condition.parameters[0]})`
183+
}
178184
if (columnFilter.comparator === FilterComparator.OR) {
179185
qb.orWhere(qb['createWhereConditionExpression'](condition), parameters)
180186
} else {

src/helper.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ export function checkIsEmbedded(qb: SelectQueryBuilder<unknown>, propertyPath: s
126126
return !!qb?.expressionMap?.mainAlias?.metadata?.hasEmbeddedWithPropertyPath(propertyPath)
127127
}
128128

129+
export function checkIsArray(qb: SelectQueryBuilder<unknown>, propertyName: string): boolean {
130+
if (!qb || !propertyName) {
131+
return false
132+
}
133+
return !!qb?.expressionMap?.mainAlias?.metadata.findColumnWithPropertyName(propertyName)?.isArray
134+
}
135+
129136
// This function is used to fix the column alias when using relation, embedded or virtual properties
130137
export function fixColumnAlias(
131138
properties: ColumnProperties,

src/paginate.spec.ts

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ import {
1818
} from './filter'
1919
import { ToyShopEntity } from './__tests__/toy-shop.entity'
2020
import { ToyShopAddressEntity } from './__tests__/toy-shop-address.entity'
21+
import * as process from 'process'
22+
import { CatHairEntity } from './__tests__/cat-hair.entity'
2123

2224
const isoStringToDate = (isoString) => new Date(isoString)
2325

2426
describe('paginate', () => {
2527
let dataSource: DataSource
2628
let catRepo: Repository<CatEntity>
2729
let catToyRepo: Repository<CatToyEntity>
30+
let catHairRepo: Repository<CatHairEntity>
2831
let toyShopRepo: Repository<ToyShopEntity>
2932
let toyShopAddressRepository: Repository<ToyShopAddressEntity>
3033
let catHomeRepo: Repository<CatHomeEntity>
@@ -36,17 +39,18 @@ describe('paginate', () => {
3639
let toysShops: ToyShopEntity[]
3740
let catHomes: CatHomeEntity[]
3841
let catHomePillows: CatHomePillowEntity[]
42+
let catHairs: CatHairEntity[] = []
3943

4044
beforeAll(async () => {
4145
dataSource = new DataSource({
4246
...(process.env.DB === 'postgres'
4347
? {
4448
type: 'postgres',
45-
host: 'localhost',
46-
port: 5432,
47-
username: 'root',
48-
password: 'pass',
49-
database: 'test',
49+
host: process.env.DB_HOST || 'localhost',
50+
port: +process.env.DB_PORT || 5432,
51+
username: process.env.DB_USERNAME || 'root',
52+
password: process.env.DB_PASSWORD || 'pass',
53+
database: process.env.DB_DATABASE || 'test',
5054
}
5155
: {
5256
type: 'sqlite',
@@ -61,6 +65,7 @@ describe('paginate', () => {
6165
CatHomeEntity,
6266
CatHomePillowEntity,
6367
ToyShopEntity,
68+
process.env.DB === 'postgres' ? CatHairEntity : undefined,
6469
],
6570
})
6671
await dataSource.initialize()
@@ -163,6 +168,18 @@ describe('paginate', () => {
163168

164169
// add friends to Milo
165170
await catRepo.save({ ...cats[0], friends: cats.slice(1) })
171+
172+
catHairs = []
173+
174+
if (process.env.DB === 'postgres') {
175+
catHairRepo = dataSource.getRepository(CatHairEntity)
176+
catHairs = await catHairRepo.save([
177+
catHairRepo.create({ name: 'short', colors: ['white', 'brown', 'black'] }),
178+
catHairRepo.create({ name: 'long', colors: ['white', 'brown'] }),
179+
catHairRepo.create({ name: 'buzzed', colors: ['white'] }),
180+
catHairRepo.create({ name: 'none' }),
181+
])
182+
}
166183
})
167184

168185
if (process.env.DB === 'postgres') {
@@ -2813,6 +2830,61 @@ describe('paginate', () => {
28132830
})
28142831
})
28152832

2833+
if (process.env.DB === 'postgres') {
2834+
describe('should return results for an array column', () => {
2835+
it.each`
2836+
operator | data | expectedIndexes
2837+
${'$not:$null'} | ${undefined} | ${[0, 1, 2, 3]}
2838+
${'$lt'} | ${2} | ${[2, 3]}
2839+
${'$lte'} | ${2} | ${[1, 2, 3]}
2840+
${'$btw'} | ${'1,2'} | ${[1, 2]}
2841+
${'$gte'} | ${2} | ${[0, 1]}
2842+
${'$gt'} | ${2} | ${[0]}
2843+
`('with $operator operator', async ({ operator, data, expectedIndexes }) => {
2844+
const config: PaginateConfig<CatHairEntity> = {
2845+
sortableColumns: ['id'],
2846+
filterableColumns: {
2847+
colors: true,
2848+
},
2849+
}
2850+
2851+
const queryFilter = `${operator}${data ? `:${data}` : ''}`
2852+
const query: PaginateQuery = {
2853+
path: '',
2854+
filter: {
2855+
colors: queryFilter,
2856+
},
2857+
}
2858+
2859+
const result = await paginate<CatHairEntity>(query, catHairRepo, config)
2860+
2861+
expect(result.meta.filter).toStrictEqual({
2862+
colors: queryFilter,
2863+
})
2864+
expect(result.data).toStrictEqual(expectedIndexes.map((index) => catHairs[index]))
2865+
expect(result.links.current).toBe(`?page=1&limit=20&sortBy=id:ASC&filter.colors=${queryFilter}`)
2866+
})
2867+
2868+
it('should work with search', async () => {
2869+
const config: PaginateConfig<CatHairEntity> = {
2870+
sortableColumns: ['id'],
2871+
searchableColumns: ['colors'],
2872+
}
2873+
2874+
const query: PaginateQuery = {
2875+
path: '',
2876+
search: 'brown',
2877+
}
2878+
2879+
const result = await paginate<CatHairEntity>(query, catHairRepo, config)
2880+
2881+
expect(result.meta.search).toStrictEqual('brown')
2882+
expect(result.data).toStrictEqual([catHairs[0], catHairs[1]])
2883+
expect(result.links.current).toBe(`?page=1&limit=20&sortBy=id:ASC&search=brown`)
2884+
})
2885+
})
2886+
}
2887+
28162888
if (process.env.DB !== 'postgres') {
28172889
describe('should return result based on virtual column', () => {
28182890
it('should return result sorted and filter by a virtual column in main entity', async () => {

0 commit comments

Comments
 (0)