fix: search on camelcase fields for postgres (#544)

This commit is contained in:
Philipp 2023-03-16 20:54:09 +01:00 committed by GitHub
parent 8b6fd702b0
commit a6384eb984
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 376 additions and 10 deletions

View File

@ -14,6 +14,20 @@ jobs:
matrix:
node-version: [14.x, 16.x, 18.x]
services:
postgres:
image: postgres:latest
env:
POSTGRES_USER: root
POSTGRES_PASSWORD: pass
POSTGRES_DB: test
ports:
- 5432:5432
options: --health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
@ -24,6 +38,10 @@ jobs:
- run: npm run format:ci
- run: npm run lint
- run: npm run build
# TODO: run postgres and sqlite in parallel
- run: DB=postgres npm run test
- run: npm run test:cov
- run: bash <(curl -s https://codecov.io/bash)
- name: Semantic Release

12
docker-compose.yml Normal file
View File

@ -0,0 +1,12 @@
version: '3.5'
services:
postgres:
container_name: postgres_nestjs_paginate
image: postgres:latest
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: pass
POSTGRES_DB: test
ports:
- "5432:5432"

252
package-lock.json generated
View File

@ -24,6 +24,7 @@
"eslint-plugin-prettier": "^4.2.1",
"fastify": "^4.14.1",
"jest": "^29.5.0",
"pg": "^8.10.0",
"prettier": "^2.8.4",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.0",
@ -2358,6 +2359,15 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"node_modules/buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -5575,6 +5585,12 @@
"node": ">=6"
}
},
"node_modules/packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==",
"dev": true
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -5683,6 +5699,87 @@
"node": ">=8"
}
},
"node_modules/pg": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.10.0.tgz",
"integrity": "sha512-ke7o7qSTMb47iwzOSaZMfeR7xToFdkE71ifIipOAAaLIM0DYzfOAXlgFFmYUIE2BcJtvnVlGCID84ZzCegE8CQ==",
"dev": true,
"dependencies": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.5.0",
"pg-pool": "^3.6.0",
"pg-protocol": "^1.6.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
"engines": {
"node": ">= 8.0.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-connection-string": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
"integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==",
"dev": true
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"dev": true,
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.0.tgz",
"integrity": "sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==",
"dev": true,
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz",
"integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==",
"dev": true
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dev": true,
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dev": true,
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -5775,6 +5872,45 @@
"node": ">=8"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dev": true,
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -7354,6 +7490,15 @@
"node": ">=4.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true,
"engines": {
"node": ">=0.4"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@ -9227,6 +9372,12 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
"dev": true
},
"bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -11696,6 +11847,12 @@
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
"packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==",
"dev": true
},
"parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -11782,6 +11939,68 @@
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true
},
"pg": {
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.10.0.tgz",
"integrity": "sha512-ke7o7qSTMb47iwzOSaZMfeR7xToFdkE71ifIipOAAaLIM0DYzfOAXlgFFmYUIE2BcJtvnVlGCID84ZzCegE8CQ==",
"dev": true,
"requires": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.5.0",
"pg-pool": "^3.6.0",
"pg-protocol": "^1.6.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
}
},
"pg-connection-string": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
"integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==",
"dev": true
},
"pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"dev": true
},
"pg-pool": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.0.tgz",
"integrity": "sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==",
"dev": true,
"requires": {}
},
"pg-protocol": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz",
"integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==",
"dev": true
},
"pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dev": true,
"requires": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
}
},
"pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dev": true,
"requires": {
"split2": "^4.1.0"
}
},
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -11858,6 +12077,33 @@
"find-up": "^4.0.0"
}
},
"postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"dev": true
},
"postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"dev": true
},
"postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"dev": true
},
"postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dev": true,
"requires": {
"xtend": "^4.0.0"
}
},
"prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -12944,6 +13190,12 @@
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"dev": true
},
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true
},
"y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -44,6 +44,7 @@
"eslint-plugin-prettier": "^4.2.1",
"fastify": "^4.14.1",
"jest": "^29.5.0",
"pg": "^8.10.0",
"prettier": "^2.8.4",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.0",

View File

@ -15,6 +15,12 @@ import { CatToyEntity } from './cat-toy.entity'
import { CatHomeEntity } from './cat-home.entity'
import { SizeEmbed } from './size.embed'
export enum CutenessLevel {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
}
@Entity({ name: 'cat' })
export class CatEntity {
@PrimaryGeneratedColumn()
@ -29,6 +35,12 @@ export class CatEntity {
@Column({ nullable: true })
age: number | null
@Column({
type: 'simple-enum',
enum: CutenessLevel,
})
cutenessLevel: CutenessLevel
@Column(() => SizeEmbed)
size: SizeEmbed

View File

@ -2,7 +2,7 @@ import { Repository, In, DataSource, TypeORMError } from 'typeorm'
import { Paginated, paginate, PaginateConfig, NO_PAGINATION } from './paginate'
import { PaginateQuery } from './decorator'
import { HttpException } from '@nestjs/common'
import { CatEntity } from './__tests__/cat.entity'
import { CatEntity, CutenessLevel } from './__tests__/cat.entity'
import { CatToyEntity } from './__tests__/cat-toy.entity'
import { CatHomeEntity } from './__tests__/cat-home.entity'
import { CatHomePillowEntity } from './__tests__/cat-home-pillow.entity'
@ -30,8 +30,19 @@ describe('paginate', () => {
beforeAll(async () => {
dataSource = new DataSource({
type: 'sqlite',
database: ':memory:',
...(process.env.DB === 'postgres'
? {
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'root',
password: 'pass',
database: 'test',
}
: {
type: 'sqlite',
database: ':memory:',
}),
synchronize: true,
logging: false,
entities: [CatEntity, CatToyEntity, CatHomeEntity, CatHomePillowEntity],
@ -43,11 +54,41 @@ describe('paginate', () => {
catHomePillowRepo = dataSource.getRepository(CatHomePillowEntity)
cats = await catRepo.save([
catRepo.create({ name: 'Milo', color: 'brown', age: 6, size: { height: 25, width: 10, length: 40 } }),
catRepo.create({ name: 'Garfield', color: 'ginger', age: 5, size: { height: 30, width: 15, length: 45 } }),
catRepo.create({ name: 'Shadow', color: 'black', age: 4, size: { height: 25, width: 10, length: 50 } }),
catRepo.create({ name: 'George', color: 'white', age: 3, size: { height: 35, width: 12, length: 40 } }),
catRepo.create({ name: 'Leche', color: 'white', age: null, size: { height: 10, width: 5, length: 15 } }),
catRepo.create({
name: 'Milo',
color: 'brown',
age: 6,
cutenessLevel: CutenessLevel.HIGH,
size: { height: 25, width: 10, length: 40 },
}),
catRepo.create({
name: 'Garfield',
color: 'ginger',
age: 5,
cutenessLevel: CutenessLevel.MEDIUM,
size: { height: 30, width: 15, length: 45 },
}),
catRepo.create({
name: 'Shadow',
color: 'black',
age: 4,
cutenessLevel: CutenessLevel.HIGH,
size: { height: 25, width: 10, length: 50 },
}),
catRepo.create({
name: 'George',
color: 'white',
age: 3,
cutenessLevel: CutenessLevel.LOW,
size: { height: 35, width: 12, length: 40 },
}),
catRepo.create({
name: 'Leche',
color: 'white',
age: null,
cutenessLevel: CutenessLevel.HIGH,
size: { height: 10, width: 5, length: 15 },
}),
])
catToys = await catToyRepo.save([
catToyRepo.create({ name: 'Fuzzy Thing', cat: cats[0], size: { height: 10, width: 10, length: 10 } }),
@ -69,9 +110,39 @@ describe('paginate', () => {
])
// add friends to Milo
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') {
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 () => {
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'],

View File

@ -271,7 +271,7 @@ export async function paginate<T extends ObjectLiteral>(
}
if (['postgres', 'cockroachdb'].includes(queryBuilder.connection.options.type)) {
condition.parameters[0] += '::text'
condition.parameters[0] = `CAST(${condition.parameters[0]} AS text)`
}
qb.orWhere(qb['createWhereConditionExpression'](condition), {