Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test case for filtering null and not null on relation #539

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,22 @@ const paginateConfig: PaginateConfig<CatEntity> {
* Description: Prevent `select` query param from limiting selection further. Partial selection will depend upon `select` config option only
*/
ignoreSelectInQueryParam: true,

/**
* Required: false
* Type: 'leftJoinAndSelect' | 'innerJoinAndSelect'
* Default: 'leftJoinAndSelect'
* Description: Relationships will be joined with either LEFT JOIN or INNER JOIN, and their columns selected. Can be specified per column with `joinMethods` configuration.
*/
defaultJoinMethod: 'leftJoinAndSelect'

/**
* Required: false
* Type: MappedColumns<T, JoinMethod>
* Default: false
* Description: Overrides the join method per relationship.
*/
joinMethods: {age: 'innerJoinAndSelect', size: 'leftJoinAndSelect'}
}
```

Expand Down
13 changes: 13 additions & 0 deletions src/__tests__/cat-home-pillow-brand.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'

@Entity()
export class CatHomePillowBrandEntity {
@PrimaryGeneratedColumn()
id: number

@Column()
name: string

@Column({ nullable: true })
quality: string
}
4 changes: 4 additions & 0 deletions src/__tests__/cat-home-pillow.entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
import { CatHomeEntity } from './cat-home.entity'
import { CatHomePillowBrandEntity } from './cat-home-pillow-brand.entity'

@Entity()
export class CatHomePillowEntity {
Expand All @@ -12,6 +13,9 @@ export class CatHomePillowEntity {
@Column()
color: string

@ManyToOne(() => CatHomePillowBrandEntity)
brand: CatHomePillowBrandEntity

@CreateDateColumn()
createdAt: string
}
17 changes: 16 additions & 1 deletion src/__tests__/cat-home.entity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { Column, CreateDateColumn, Entity, OneToMany, OneToOne, PrimaryGeneratedColumn, VirtualColumn } from 'typeorm'
import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
VirtualColumn,
} from 'typeorm'
import { CatHomePillowEntity } from './cat-home-pillow.entity'
import { CatEntity } from './cat.entity'

Expand All @@ -10,12 +19,18 @@ export class CatHomeEntity {
@Column()
name: string

@Column({ nullable: true })
street: string | null

@OneToOne(() => CatEntity, (cat) => cat.home)
cat: CatEntity

@OneToMany(() => CatHomePillowEntity, (pillow) => pillow.home)
pillows: CatHomePillowEntity[]

@ManyToOne(() => CatHomePillowEntity, { nullable: true })
naptimePillow: CatHomePillowEntity | null

@CreateDateColumn()
createdAt: string

Expand Down
27 changes: 21 additions & 6 deletions src/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import { PaginateQuery } from './decorator'
import {
checkIsArray,
checkIsEmbedded,
checkIsNestedRelation,
checkIsRelation,
extractVirtualProperty,
fixColumnAlias,
getPropertiesByColumnName,
isISODate,
JoinMethod,
} from './helper'

export enum FilterOperator {
Expand Down Expand Up @@ -81,7 +83,8 @@ export const OperatorSymbolToFunction = new Map<
])

type Filter = { comparator: FilterComparator; findOperator: FindOperator<string> }
type ColumnsFilters = { [columnName: string]: Filter[] }
type ColumnFilters = { [columnName: string]: Filter[] }
type ColumnJoinMethods = { [columnName: string]: JoinMethod }

export interface FilterToken {
comparator: FilterComparator
Expand Down Expand Up @@ -158,7 +161,7 @@ export function generatePredicateCondition(
) as WherePredicateOperator
}

export function addWhereCondition<T>(qb: SelectQueryBuilder<T>, column: string, filter: ColumnsFilters) {
export function addWhereCondition<T>(qb: SelectQueryBuilder<T>, column: string, filter: ColumnFilters) {
const columnProperties = getPropertiesByColumnName(column)
const { isVirtualProperty, query: virtualQuery } = extractVirtualProperty(qb, columnProperties)
const isRelation = checkIsRelation(qb, columnProperties.propertyPath)
Expand Down Expand Up @@ -239,8 +242,8 @@ export function parseFilterToken(raw?: string): FilterToken | null {
export function parseFilter(
query: PaginateQuery,
filterableColumns?: { [column: string]: (FilterOperator | FilterSuffix)[] | true }
): ColumnsFilters {
const filter: ColumnsFilters = {}
): ColumnFilters {
const filter: ColumnFilters = {}
if (!filterableColumns || !query.filter) {
return {}
}
Expand Down Expand Up @@ -322,7 +325,7 @@ export function addFilter<T>(
qb: SelectQueryBuilder<T>,
query: PaginateQuery,
filterableColumns?: { [column: string]: (FilterOperator | FilterSuffix)[] | true }
): SelectQueryBuilder<T> {
): ColumnJoinMethods {
const filter = parseFilter(query, filterableColumns)

const filterEntries = Object.entries(filter)
Expand All @@ -345,5 +348,17 @@ export function addFilter<T>(
)
}

return qb
// Set the join type of every relationship used in a filter to `innerJoinAndSelect`
// so that records without that relationships don't show up in filters on their columns.
return Object.fromEntries(
filterEntries
.map(([key]) => [key, getPropertiesByColumnName(key)] as const)
.filter(([, properties]) => properties.propertyPath)
.flatMap(([, properties]) => {
const nesting = properties.column.split('.')
return Array.from({ length: nesting.length - 1 }, (_, i) => nesting.slice(0, i + 1).join('.'))
.filter((relation) => checkIsNestedRelation(qb, relation))
.map((relation) => [relation, 'innerJoinAndSelect'] as const)
})
)
}
40 changes: 39 additions & 1 deletion src/helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { FindOperator, Repository, SelectQueryBuilder } from 'typeorm'
import {
FindOperator,
FindOptionsRelationByString,
FindOptionsRelations,
Repository,
SelectQueryBuilder,
} from 'typeorm'
import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata'
import { OrmUtils } from 'typeorm/util/OrmUtils'
import { mergeWith } from 'lodash'

/**
* Joins 2 keys as `K`, `K.P`, `K.(P` or `K.P)`
Expand Down Expand Up @@ -80,6 +88,13 @@ export type RelationColumn<T> = Extract<
export type Order<T> = [Column<T>, 'ASC' | 'DESC']
export type SortBy<T> = Order<T>[]

// eslint-disable-next-line @typescript-eslint/ban-types
export type MappedColumns<T, S> = { [key in Column<T> | (string & {})]: S }
export type JoinMethod = 'leftJoinAndSelect' | 'innerJoinAndSelect'
export type RelationSchemaInput<T = any> = FindOptionsRelations<T> | RelationColumn<T>[] | FindOptionsRelationByString
// eslint-disable-next-line @typescript-eslint/ban-types
export type RelationSchema<T = any> = { [relation in Column<T> | (string & {})]: true }

export function isEntityKey<T>(entityColumns: Column<T>[], column: string): column is Column<T> {
return !!entityColumns.find((c) => c === column)
}
Expand Down Expand Up @@ -155,6 +170,18 @@ export function checkIsRelation(qb: SelectQueryBuilder<unknown>, propertyPath: s
return !!qb?.expressionMap?.mainAlias?.metadata?.hasRelationWithPropertyPath(propertyPath)
}

export function checkIsNestedRelation(qb: SelectQueryBuilder<unknown>, propertyPath: string): boolean {
let metadata = qb?.expressionMap?.mainAlias?.metadata
for (const relationName of propertyPath.split('.')) {
const relation = metadata?.relations.find((relation) => relation.propertyPath === relationName)
if (!relation) {
return false
}
metadata = relation.inverseEntityMetadata
}
return true
}

export function checkIsEmbedded(qb: SelectQueryBuilder<unknown>, propertyPath: string): boolean {
if (!qb || !propertyPath) {
return false
Expand Down Expand Up @@ -247,3 +274,14 @@ export function isFindOperator<T>(value: unknown | FindOperator<T>): value is Fi
return false
}
}

export function createRelationSchema<T>(configurationRelations: RelationSchemaInput<T>): RelationSchema<T> {
return Array.isArray(configurationRelations)
? OrmUtils.propertyPathsToTruthyObject(configurationRelations)
: configurationRelations
}

export function mergeRelationSchema(...schemas: RelationSchema[]) {
const noTrueOverride = (obj, source) => (source === true && obj !== undefined ? obj : undefined)
return mergeWith({}, ...schemas, noTrueOverride)
}
Loading