Skip to content

Commit

Permalink
feat: add guards
Browse files Browse the repository at this point in the history
  • Loading branch information
Tim Streicher authored and coronoro committed Nov 16, 2023
1 parent 11fec30 commit 1ef566b
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 3 deletions.
39 changes: 39 additions & 0 deletions src/guards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type {
FilterSet,
FilterSetAffix,
FilterSetConfig,
FilterSetExact,
FilterSetIn,
FilterSetRange,
FilterSetValue,
} from './types'
import {DRFFilters} from './types'

export function isFilterSetValue<K>(config: FilterSetValue<K> | FilterSet<K> | Partial<Record<string, K>> | FilterSetConfig<K>): config is FilterSetValue<K> {
return (config as FilterSetValue<K>).value !== undefined
}

export function isFilterSetConfig<K>(config: FilterSetValue<K> | FilterSet<K> | FilterSetConfig<K>): config is FilterSetConfig<K> {
const keys = Object.keys(config)
const attributes = keys.filter(item => !DRFFilters.includes(item) && item !== 'value') // exclude all default drf Filters and value
return attributes.length > 0
}

export function isFilterSetRange<K>(config: FilterSetValue<K> | FilterSet<K> | FilterSetConfig<K>): config is FilterSetRange<K> {
const keys = Object.keys(config)
const rangeKeys = ['lt', 'gt', 'lte', 'gte']
const attributes = keys.filter(item => rangeKeys.includes(item)) // exclude all default drf Filters
return attributes.length > 0
}

export function isFilterSetExact<K>(config: FilterSetValue<K> | FilterSet<K> | FilterSetConfig<K>): config is FilterSetExact<K> {
return (config as FilterSetExact<K>).exact !== undefined
}

export function isFilterSetIn<K>(config: FilterSetValue<K> | FilterSet<K> | FilterSetConfig<K>): config is FilterSetIn<K> {
return (config as FilterSetIn<K>).in !== undefined
}

export function isFilterSetAffix<K>(config: FilterSetValue<K> | FilterSet<K> | FilterSetConfig<K>): config is FilterSetAffix<K> {
return (config as FilterSetAffix<K>).startswith !== undefined || (config as FilterSetAffix<K>).endswith !== undefined
}
133 changes: 133 additions & 0 deletions src/test/guards-basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import type {FilterSetConfig} from '../types'
import {isFilterSetAffix, isFilterSetConfig, isFilterSetIn, isFilterSetRange, isFilterSetValue} from '../guards'
import {convertFilterSetConfig} from '../middleware'

interface Data {
number: number
}

test('it should be possible to set a value by using a guard', () => {
const config: FilterSetConfig<Data> = {
number: {value: 123},
}
if (isFilterSetValue(config.number))
config.number.value = 3

const converted = convertFilterSetConfig(config)
expect(converted).toEqual({number: 3})
})

test('if its not a FilterSetValue it should not be editable', () => {
const config: FilterSetConfig<Data> = {
number: {lt: 123},
}
if (isFilterSetValue(config.number))
config.number.value = 3

const converted = convertFilterSetConfig(config)
// eslint-disable-next-line camelcase
expect(converted).toEqual({number__lt: 123})
})

test('it should be possible to set a range by using a guard', () => {
const config: FilterSetConfig<Data> = {
number: {gt: 123},
}
if (isFilterSetRange(config.number))
config.number.gt = 3

const converted = convertFilterSetConfig(config)
// eslint-disable-next-line camelcase
expect(converted).toEqual({number__gt: 3})
})

test('if its not a isFilterSetRange it should not be editable', () => {
const config: FilterSetConfig<Data> = {
number: {value: 123},
}
if (isFilterSetRange(config.number))
config.number.gt = 3

const converted = convertFilterSetConfig(config)
expect(converted).toEqual({number: 123})
})

test('it should be possible to set a in filter by using a guard', () => {
const numberList = [1, 2, 3]
const config: FilterSetConfig<Data> = {
number: {in: numberList},
}
if (isFilterSetIn(config.number))
config.number.in = [4]

const converted = convertFilterSetConfig(config)
// eslint-disable-next-line camelcase
expect(converted).toEqual({number__in: [4]})
})

test('if its not a isFilterSetIn it should not be editable', () => {
const config: FilterSetConfig<Data> = {
number: {value: 123},
}
if (isFilterSetIn(config.number))
config.number.in = [3]

const converted = convertFilterSetConfig(config)
expect(converted).toEqual({number: 123})
})

test('it should be possible to set a in affix by using a guard', () => {
const config: FilterSetConfig<Data> = {
number: {startswith: 123},
}
if (isFilterSetAffix(config.number))
config.number.startswith = 4

const converted = convertFilterSetConfig(config)
// eslint-disable-next-line camelcase
expect(converted).toEqual({number__startswith: 4})
})

test('if its not a isFilterSetAffix it should not be editable', () => {
const config: FilterSetConfig<Data> = {
number: {value: 123},
}
if (isFilterSetAffix(config.number))
config.number.startswith = 4

const converted = convertFilterSetConfig(config)
expect(converted).toEqual({number: 123})
})

interface Complex {
id: number
}

interface ComplexData {
complex: Complex
}

test('it should be possible to set a range by using a guard', () => {
const config: FilterSetConfig<ComplexData> = {
complex: {id: {value: 123}},
}
if (isFilterSetConfig(config.complex) && isFilterSetValue(config.complex.id))
config.complex.id.value = 3

const converted = convertFilterSetConfig(config)
// eslint-disable-next-line camelcase
expect(converted).toEqual({complex__id: 3})
})

test('if its not a isFilterSetConfig it should not be editable', () => {
const complexData = {id: 123}
const config: FilterSetConfig<ComplexData> = {
complex: {value: complexData},
}
if (isFilterSetConfig(config.complex) && isFilterSetValue(config.complex.id))
config.complex.id.value = 3

const converted = convertFilterSetConfig(config)
expect(converted).toEqual({complex: complexData})
})

58 changes: 58 additions & 0 deletions src/test/guards-key-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type {FilterSetConfig} from '../types'
import {isFilterSetExact, isFilterSetRange, isFilterSetValue} from '../guards'
import {convertFilterSetConfig} from '../middleware'

interface Data {
number: number
}

interface FilterSetKeyConfig {
number: 'exact' | 'lte' | 'lt' | 'gt'
}

test('it should be possible to set a value by using a guard with a key config', () => {
const config: FilterSetConfig<Data, FilterSetKeyConfig> = {
number: {exact: 123},
}
if (isFilterSetExact(config.number))
config.number.exact = 3

const converted = convertFilterSetConfig(config)
// eslint-disable-next-line camelcase
expect(converted).toEqual({number__exact: 3})
})

test('if the config is not a FilterSetValue it should not be editable with a key config', () => {
const config: FilterSetConfig<Data> = {
number: {lt: 123},
}
if (isFilterSetValue(config.number))
config.number.value = 3

const converted = convertFilterSetConfig(config)
// eslint-disable-next-line camelcase
expect(converted).toEqual({number__lt: 123})
})

test('it should be possible to set a value by using a guard with a key config', () => {
const config: FilterSetConfig<Data, FilterSetKeyConfig> = {
number: {value: 123},
}
if (isFilterSetValue(config.number))
config.number.value = 3

const converted = convertFilterSetConfig(config)
expect(converted).toEqual({number: 3})
})

test('it should be possible to set range by using a guard with a key config', () => {
const config: FilterSetConfig<Data, FilterSetKeyConfig> = {
number: {lt: 123},
}
if (isFilterSetRange(config.number))
config.number.lt = 3

const converted = convertFilterSetConfig(config)
// eslint-disable-next-line camelcase
expect(converted).toEqual({number__lt: 3})
})
20 changes: 20 additions & 0 deletions src/test/type-guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type {FilterSetConfig} from '../types'
import {isFilterSetRange} from '../guards'
import {convertFilterSetConfig} from '../middleware'

interface Data {
number: number
}

test('it should not be possible to set a in Filter by using a range guard', () => {
const config: FilterSetConfig<Data> = {
number: {gt: 123},
}
if (isFilterSetRange(config.number))
// @ts-expect-error in has type never
config.number.in = [3]

const converted = convertFilterSetConfig(config)
// eslint-disable-next-line camelcase
expect(converted).toEqual({number__gt: 123, number__in: [3]})
})
9 changes: 6 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export interface FilterSetAffix<F> extends NotExact, NotIn {
endswith?: F
}

type FilterSetRange<T> =
export type FilterSetRange<T> =
(FilterSetRangeLT<T> & FilterSetRangeGTE<T>) | (FilterSetRangeLT<T> & FilterSetRangeGT<T>)
| (FilterSetRangeLTE<T> & FilterSetRangeGTE<T>) | (FilterSetRangeLTE<T> & FilterSetRangeGT<T>)
| (FilterSetRangeGT<T> & FilterSetRangeLTE<T>) | (FilterSetRangeGT<T> & FilterSetRangeLT<T>)
Expand Down Expand Up @@ -66,7 +66,7 @@ export interface FilterSetRangeGTE<T> extends FilterSetAffix<T> {
/**
* all FilterSets
*/
type FilterSet<T> = FilterSetRange<T> | FilterSetExact<T> | FilterSetAffix<T> | FilterSetIn<T>
export type FilterSet<T> = FilterSetRange<T> | FilterSetExact<T> | FilterSetAffix<T> | FilterSetIn<T>

// Config to exclude certain filters and enable custom filters
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -116,10 +116,13 @@ type CheckConfigKeys<D, K extends FSKeyConfig<D>, key extends keyof D, C extends
)
: FilterSet<D[key]> // no config for the key so we take the default combinations

// type for plain values unaffected by any filters
export type FilterSetValue<K> = Record<'value', K>

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type FilterSetConfig<D = Record<any, any>, K extends FSKeyConfig<D> | null = null, C extends CustomKeyConfig | null = null> = {
[key in keyof D]:
{value: D[key]} // no filters apply
FilterSetValue<D[key]> // no filters apply
| (
K extends null ? // check if we have a config
FilterSet<D[key]> // no config so we take the default combinations for each key
Expand Down

0 comments on commit 1ef566b

Please sign in to comment.