From 1ef566b61ce563cad7fe65de08a1edf416179d6c Mon Sep 17 00:00:00 2001 From: Tim Streicher Date: Thu, 16 Nov 2023 11:52:01 +0100 Subject: [PATCH] feat: add guards --- src/guards.ts | 39 +++++++++ src/test/guards-basic.test.ts | 133 +++++++++++++++++++++++++++++ src/test/guards-key-config.test.ts | 58 +++++++++++++ src/test/type-guard.test.ts | 20 +++++ src/types.ts | 9 +- 5 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 src/guards.ts create mode 100644 src/test/guards-basic.test.ts create mode 100644 src/test/guards-key-config.test.ts create mode 100644 src/test/type-guard.test.ts diff --git a/src/guards.ts b/src/guards.ts new file mode 100644 index 0000000..12c2f90 --- /dev/null +++ b/src/guards.ts @@ -0,0 +1,39 @@ +import type { + FilterSet, + FilterSetAffix, + FilterSetConfig, + FilterSetExact, + FilterSetIn, + FilterSetRange, + FilterSetValue, +} from './types' +import {DRFFilters} from './types' + +export function isFilterSetValue(config: FilterSetValue | FilterSet | Partial> | FilterSetConfig): config is FilterSetValue { + return (config as FilterSetValue).value !== undefined +} + +export function isFilterSetConfig(config: FilterSetValue | FilterSet | FilterSetConfig): config is FilterSetConfig { + 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(config: FilterSetValue | FilterSet | FilterSetConfig): config is FilterSetRange { + 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(config: FilterSetValue | FilterSet | FilterSetConfig): config is FilterSetExact { + return (config as FilterSetExact).exact !== undefined +} + +export function isFilterSetIn(config: FilterSetValue | FilterSet | FilterSetConfig): config is FilterSetIn { + return (config as FilterSetIn).in !== undefined +} + +export function isFilterSetAffix(config: FilterSetValue | FilterSet | FilterSetConfig): config is FilterSetAffix { + return (config as FilterSetAffix).startswith !== undefined || (config as FilterSetAffix).endswith !== undefined +} diff --git a/src/test/guards-basic.test.ts b/src/test/guards-basic.test.ts new file mode 100644 index 0000000..8ada1e1 --- /dev/null +++ b/src/test/guards-basic.test.ts @@ -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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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}) +}) + diff --git a/src/test/guards-key-config.test.ts b/src/test/guards-key-config.test.ts new file mode 100644 index 0000000..41d4bc7 --- /dev/null +++ b/src/test/guards-key-config.test.ts @@ -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 = { + 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 = { + 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 = { + 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 = { + 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}) +}) diff --git a/src/test/type-guard.test.ts b/src/test/type-guard.test.ts new file mode 100644 index 0000000..3eb57d3 --- /dev/null +++ b/src/test/type-guard.test.ts @@ -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 = { + 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]}) +}) diff --git a/src/types.ts b/src/types.ts index 608b522..1858584 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,7 +36,7 @@ export interface FilterSetAffix extends NotExact, NotIn { endswith?: F } -type FilterSetRange = +export type FilterSetRange = (FilterSetRangeLT & FilterSetRangeGTE) | (FilterSetRangeLT & FilterSetRangeGT) | (FilterSetRangeLTE & FilterSetRangeGTE) | (FilterSetRangeLTE & FilterSetRangeGT) | (FilterSetRangeGT & FilterSetRangeLTE) | (FilterSetRangeGT & FilterSetRangeLT) @@ -66,7 +66,7 @@ export interface FilterSetRangeGTE extends FilterSetAffix { /** * all FilterSets */ -type FilterSet = FilterSetRange | FilterSetExact | FilterSetAffix | FilterSetIn +export type FilterSet = FilterSetRange | FilterSetExact | FilterSetAffix | FilterSetIn // Config to exclude certain filters and enable custom filters // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -116,10 +116,13 @@ type CheckConfigKeys, key extends keyof D, C extends ) : FilterSet // no config for the key so we take the default combinations +// type for plain values unaffected by any filters +export type FilterSetValue = Record<'value', K> + // eslint-disable-next-line @typescript-eslint/no-explicit-any export type FilterSetConfig, K extends FSKeyConfig | null = null, C extends CustomKeyConfig | null = null> = { [key in keyof D]: - {value: D[key]} // no filters apply + FilterSetValue // no filters apply | ( K extends null ? // check if we have a config FilterSet // no config so we take the default combinations for each key