Skip to content

Commit 1ef566b

Browse files
Tim Streichercoronoro
Tim Streicher
authored andcommitted
feat: add guards
1 parent 11fec30 commit 1ef566b

5 files changed

+256
-3
lines changed

src/guards.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type {
2+
FilterSet,
3+
FilterSetAffix,
4+
FilterSetConfig,
5+
FilterSetExact,
6+
FilterSetIn,
7+
FilterSetRange,
8+
FilterSetValue,
9+
} from './types'
10+
import {DRFFilters} from './types'
11+
12+
export function isFilterSetValue<K>(config: FilterSetValue<K> | FilterSet<K> | Partial<Record<string, K>> | FilterSetConfig<K>): config is FilterSetValue<K> {
13+
return (config as FilterSetValue<K>).value !== undefined
14+
}
15+
16+
export function isFilterSetConfig<K>(config: FilterSetValue<K> | FilterSet<K> | FilterSetConfig<K>): config is FilterSetConfig<K> {
17+
const keys = Object.keys(config)
18+
const attributes = keys.filter(item => !DRFFilters.includes(item) && item !== 'value') // exclude all default drf Filters and value
19+
return attributes.length > 0
20+
}
21+
22+
export function isFilterSetRange<K>(config: FilterSetValue<K> | FilterSet<K> | FilterSetConfig<K>): config is FilterSetRange<K> {
23+
const keys = Object.keys(config)
24+
const rangeKeys = ['lt', 'gt', 'lte', 'gte']
25+
const attributes = keys.filter(item => rangeKeys.includes(item)) // exclude all default drf Filters
26+
return attributes.length > 0
27+
}
28+
29+
export function isFilterSetExact<K>(config: FilterSetValue<K> | FilterSet<K> | FilterSetConfig<K>): config is FilterSetExact<K> {
30+
return (config as FilterSetExact<K>).exact !== undefined
31+
}
32+
33+
export function isFilterSetIn<K>(config: FilterSetValue<K> | FilterSet<K> | FilterSetConfig<K>): config is FilterSetIn<K> {
34+
return (config as FilterSetIn<K>).in !== undefined
35+
}
36+
37+
export function isFilterSetAffix<K>(config: FilterSetValue<K> | FilterSet<K> | FilterSetConfig<K>): config is FilterSetAffix<K> {
38+
return (config as FilterSetAffix<K>).startswith !== undefined || (config as FilterSetAffix<K>).endswith !== undefined
39+
}

src/test/guards-basic.test.ts

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import type {FilterSetConfig} from '../types'
2+
import {isFilterSetAffix, isFilterSetConfig, isFilterSetIn, isFilterSetRange, isFilterSetValue} from '../guards'
3+
import {convertFilterSetConfig} from '../middleware'
4+
5+
interface Data {
6+
number: number
7+
}
8+
9+
test('it should be possible to set a value by using a guard', () => {
10+
const config: FilterSetConfig<Data> = {
11+
number: {value: 123},
12+
}
13+
if (isFilterSetValue(config.number))
14+
config.number.value = 3
15+
16+
const converted = convertFilterSetConfig(config)
17+
expect(converted).toEqual({number: 3})
18+
})
19+
20+
test('if its not a FilterSetValue it should not be editable', () => {
21+
const config: FilterSetConfig<Data> = {
22+
number: {lt: 123},
23+
}
24+
if (isFilterSetValue(config.number))
25+
config.number.value = 3
26+
27+
const converted = convertFilterSetConfig(config)
28+
// eslint-disable-next-line camelcase
29+
expect(converted).toEqual({number__lt: 123})
30+
})
31+
32+
test('it should be possible to set a range by using a guard', () => {
33+
const config: FilterSetConfig<Data> = {
34+
number: {gt: 123},
35+
}
36+
if (isFilterSetRange(config.number))
37+
config.number.gt = 3
38+
39+
const converted = convertFilterSetConfig(config)
40+
// eslint-disable-next-line camelcase
41+
expect(converted).toEqual({number__gt: 3})
42+
})
43+
44+
test('if its not a isFilterSetRange it should not be editable', () => {
45+
const config: FilterSetConfig<Data> = {
46+
number: {value: 123},
47+
}
48+
if (isFilterSetRange(config.number))
49+
config.number.gt = 3
50+
51+
const converted = convertFilterSetConfig(config)
52+
expect(converted).toEqual({number: 123})
53+
})
54+
55+
test('it should be possible to set a in filter by using a guard', () => {
56+
const numberList = [1, 2, 3]
57+
const config: FilterSetConfig<Data> = {
58+
number: {in: numberList},
59+
}
60+
if (isFilterSetIn(config.number))
61+
config.number.in = [4]
62+
63+
const converted = convertFilterSetConfig(config)
64+
// eslint-disable-next-line camelcase
65+
expect(converted).toEqual({number__in: [4]})
66+
})
67+
68+
test('if its not a isFilterSetIn it should not be editable', () => {
69+
const config: FilterSetConfig<Data> = {
70+
number: {value: 123},
71+
}
72+
if (isFilterSetIn(config.number))
73+
config.number.in = [3]
74+
75+
const converted = convertFilterSetConfig(config)
76+
expect(converted).toEqual({number: 123})
77+
})
78+
79+
test('it should be possible to set a in affix by using a guard', () => {
80+
const config: FilterSetConfig<Data> = {
81+
number: {startswith: 123},
82+
}
83+
if (isFilterSetAffix(config.number))
84+
config.number.startswith = 4
85+
86+
const converted = convertFilterSetConfig(config)
87+
// eslint-disable-next-line camelcase
88+
expect(converted).toEqual({number__startswith: 4})
89+
})
90+
91+
test('if its not a isFilterSetAffix it should not be editable', () => {
92+
const config: FilterSetConfig<Data> = {
93+
number: {value: 123},
94+
}
95+
if (isFilterSetAffix(config.number))
96+
config.number.startswith = 4
97+
98+
const converted = convertFilterSetConfig(config)
99+
expect(converted).toEqual({number: 123})
100+
})
101+
102+
interface Complex {
103+
id: number
104+
}
105+
106+
interface ComplexData {
107+
complex: Complex
108+
}
109+
110+
test('it should be possible to set a range by using a guard', () => {
111+
const config: FilterSetConfig<ComplexData> = {
112+
complex: {id: {value: 123}},
113+
}
114+
if (isFilterSetConfig(config.complex) && isFilterSetValue(config.complex.id))
115+
config.complex.id.value = 3
116+
117+
const converted = convertFilterSetConfig(config)
118+
// eslint-disable-next-line camelcase
119+
expect(converted).toEqual({complex__id: 3})
120+
})
121+
122+
test('if its not a isFilterSetConfig it should not be editable', () => {
123+
const complexData = {id: 123}
124+
const config: FilterSetConfig<ComplexData> = {
125+
complex: {value: complexData},
126+
}
127+
if (isFilterSetConfig(config.complex) && isFilterSetValue(config.complex.id))
128+
config.complex.id.value = 3
129+
130+
const converted = convertFilterSetConfig(config)
131+
expect(converted).toEqual({complex: complexData})
132+
})
133+

src/test/guards-key-config.test.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type {FilterSetConfig} from '../types'
2+
import {isFilterSetExact, isFilterSetRange, isFilterSetValue} from '../guards'
3+
import {convertFilterSetConfig} from '../middleware'
4+
5+
interface Data {
6+
number: number
7+
}
8+
9+
interface FilterSetKeyConfig {
10+
number: 'exact' | 'lte' | 'lt' | 'gt'
11+
}
12+
13+
test('it should be possible to set a value by using a guard with a key config', () => {
14+
const config: FilterSetConfig<Data, FilterSetKeyConfig> = {
15+
number: {exact: 123},
16+
}
17+
if (isFilterSetExact(config.number))
18+
config.number.exact = 3
19+
20+
const converted = convertFilterSetConfig(config)
21+
// eslint-disable-next-line camelcase
22+
expect(converted).toEqual({number__exact: 3})
23+
})
24+
25+
test('if the config is not a FilterSetValue it should not be editable with a key config', () => {
26+
const config: FilterSetConfig<Data> = {
27+
number: {lt: 123},
28+
}
29+
if (isFilterSetValue(config.number))
30+
config.number.value = 3
31+
32+
const converted = convertFilterSetConfig(config)
33+
// eslint-disable-next-line camelcase
34+
expect(converted).toEqual({number__lt: 123})
35+
})
36+
37+
test('it should be possible to set a value by using a guard with a key config', () => {
38+
const config: FilterSetConfig<Data, FilterSetKeyConfig> = {
39+
number: {value: 123},
40+
}
41+
if (isFilterSetValue(config.number))
42+
config.number.value = 3
43+
44+
const converted = convertFilterSetConfig(config)
45+
expect(converted).toEqual({number: 3})
46+
})
47+
48+
test('it should be possible to set range by using a guard with a key config', () => {
49+
const config: FilterSetConfig<Data, FilterSetKeyConfig> = {
50+
number: {lt: 123},
51+
}
52+
if (isFilterSetRange(config.number))
53+
config.number.lt = 3
54+
55+
const converted = convertFilterSetConfig(config)
56+
// eslint-disable-next-line camelcase
57+
expect(converted).toEqual({number__lt: 3})
58+
})

src/test/type-guard.test.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type {FilterSetConfig} from '../types'
2+
import {isFilterSetRange} from '../guards'
3+
import {convertFilterSetConfig} from '../middleware'
4+
5+
interface Data {
6+
number: number
7+
}
8+
9+
test('it should not be possible to set a in Filter by using a range guard', () => {
10+
const config: FilterSetConfig<Data> = {
11+
number: {gt: 123},
12+
}
13+
if (isFilterSetRange(config.number))
14+
// @ts-expect-error in has type never
15+
config.number.in = [3]
16+
17+
const converted = convertFilterSetConfig(config)
18+
// eslint-disable-next-line camelcase
19+
expect(converted).toEqual({number__gt: 123, number__in: [3]})
20+
})

src/types.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export interface FilterSetAffix<F> extends NotExact, NotIn {
3636
endswith?: F
3737
}
3838

39-
type FilterSetRange<T> =
39+
export type FilterSetRange<T> =
4040
(FilterSetRangeLT<T> & FilterSetRangeGTE<T>) | (FilterSetRangeLT<T> & FilterSetRangeGT<T>)
4141
| (FilterSetRangeLTE<T> & FilterSetRangeGTE<T>) | (FilterSetRangeLTE<T> & FilterSetRangeGT<T>)
4242
| (FilterSetRangeGT<T> & FilterSetRangeLTE<T>) | (FilterSetRangeGT<T> & FilterSetRangeLT<T>)
@@ -66,7 +66,7 @@ export interface FilterSetRangeGTE<T> extends FilterSetAffix<T> {
6666
/**
6767
* all FilterSets
6868
*/
69-
type FilterSet<T> = FilterSetRange<T> | FilterSetExact<T> | FilterSetAffix<T> | FilterSetIn<T>
69+
export type FilterSet<T> = FilterSetRange<T> | FilterSetExact<T> | FilterSetAffix<T> | FilterSetIn<T>
7070

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

119+
// type for plain values unaffected by any filters
120+
export type FilterSetValue<K> = Record<'value', K>
121+
119122
// eslint-disable-next-line @typescript-eslint/no-explicit-any
120123
export type FilterSetConfig<D = Record<any, any>, K extends FSKeyConfig<D> | null = null, C extends CustomKeyConfig | null = null> = {
121124
[key in keyof D]:
122-
{value: D[key]} // no filters apply
125+
FilterSetValue<D[key]> // no filters apply
123126
| (
124127
K extends null ? // check if we have a config
125128
FilterSet<D[key]> // no config so we take the default combinations for each key

0 commit comments

Comments
 (0)