From 882a5dcb58ecd5cb57a7e4f5326b9e6a528a1194 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Fri, 21 Oct 2022 16:36:17 +0200 Subject: [PATCH] feat: add filter validate option --- src/parameter/fields/type.ts | 6 +-- src/parameter/filters/build.ts | 4 +- src/parameter/filters/index.ts | 1 + src/parameter/filters/parse.ts | 42 ++++++++++------- src/parameter/filters/type.ts | 27 ++++++----- src/parameter/filters/utils/operator.ts | 4 +- src/parameter/filters/utils/value.ts | 8 ++-- src/parameter/relations/parse.ts | 4 +- src/parameter/sort/parse.ts | 4 +- src/parameter/sort/type.ts | 12 ++--- src/parameter/type.ts | 23 ++++++---- src/parameter/utils/parse/options-allowed.ts | 11 +++-- src/type.ts | 45 ++++++++++-------- test/unit/filters.spec.ts | 48 ++++++++++++++++++-- test/unit/utils.spec.ts | 30 ++++++------ 15 files changed, 168 insertions(+), 101 deletions(-) diff --git a/src/parameter/fields/type.ts b/src/parameter/fields/type.ts index c4908c1e..07a0e17e 100644 --- a/src/parameter/fields/type.ts +++ b/src/parameter/fields/type.ts @@ -10,7 +10,7 @@ import { } from '../../type'; import { RelationsParseOutput } from '../relations'; import { - ParseOptionsAllowed, + ParseAllowedKeys, } from '../type'; import { FieldOperator } from './constants'; @@ -48,8 +48,8 @@ export type FieldsParseOptions< T extends Record = Record, > = { mapping?: Record, - allowed?: ParseOptionsAllowed, - default?: ParseOptionsAllowed, + allowed?: ParseAllowedKeys, + default?: ParseAllowedKeys, defaultPath?: string, relations?: RelationsParseOutput, }; diff --git a/src/parameter/filters/build.ts b/src/parameter/filters/build.ts index 5574711e..b15ec825 100644 --- a/src/parameter/filters/build.ts +++ b/src/parameter/filters/build.ts @@ -9,7 +9,7 @@ import { merge } from 'smob'; import { ObjectLiteral } from '../../type'; import { FiltersBuildInput } from './type'; import { FilterOperator } from './constants'; -import { isFilterOperatorConfig } from './utils'; +import { isFilterValueConfig } from './utils'; import { flattenNestedObject } from '../../utils'; const OperatorWeight = { @@ -44,7 +44,7 @@ export function buildQueryFilters( return true; } - if (isFilterOperatorConfig(input)) { + if (isFilterValueConfig(input)) { if (typeof input.value === 'undefined') { input.value = null; } diff --git a/src/parameter/filters/index.ts b/src/parameter/filters/index.ts index b42fc100..8e3d4eab 100644 --- a/src/parameter/filters/index.ts +++ b/src/parameter/filters/index.ts @@ -9,3 +9,4 @@ export * from './constants'; export * from './build'; export * from './parse'; export * from './type'; +export * from './utils'; diff --git a/src/parameter/filters/parse.ts b/src/parameter/filters/parse.ts index 988cca2a..2d964294 100644 --- a/src/parameter/filters/parse.ts +++ b/src/parameter/filters/parse.ts @@ -5,7 +5,7 @@ * view the LICENSE file that was distributed with this source code. */ -import { ObjectLiteral } from '../../type'; +import { NestedKeys, ObjectLiteral } from '../../type'; import { FieldDetails, applyMapping, @@ -14,7 +14,7 @@ import { getFieldDetails, hasOwnProperty, isFieldNonRelational, isFieldPathAllowedByRelations, } from '../../utils'; -import { isPathCoveredByParseOptionsAllowed } from '../utils'; +import { isPathCoveredByParseAllowed } from '../utils'; import { FiltersParseOptions, FiltersParseOutput, FiltersParseOutputElement } from './type'; import { determineFilterOperatorLabelsByValue, transformFilterValue } from './utils'; @@ -95,6 +95,7 @@ function buildDefaultFiltersParseOutput } else if (options.defaultPath) { path = options.defaultPath; } + output.push(transformFiltersParseOutputElement({ ...(path ? { path } : {}), key: fieldDetails.name, @@ -139,7 +140,7 @@ export function parseQueryFilters( ); } - const temp : Record = {}; + const items : Record = {}; // transform to appreciate data format & validate input const keys = Object.keys(data); @@ -186,26 +187,33 @@ export function parseQueryFilters( if ( options.allowed && - !isPathCoveredByParseOptionsAllowed(options.allowed, [keys[i], fullKey]) + !isPathCoveredByParseAllowed(options.allowed, [keys[i], fullKey]) ) { continue; } - let path : string | undefined; - if (fieldDetails.path) { - path = fieldDetails.path; - } else if (options.defaultPath) { - path = options.defaultPath; - } - - temp[fullKey] = { - ...(path ? { path } : {}), + const filter = transformFiltersParseOutputElement({ key: fieldDetails.name, value: value as string | boolean | number, - }; + }); + + if (options.validate) { + if (Array.isArray(filter.value)) { + filter.value = (filter.value as any[]).filter((item: unknown) => options.validate(filter.key as NestedKeys, item)); + if (filter.value.length === 0) { + continue; + } + } else if (!options.validate(filter.key as NestedKeys, filter.value)) { + continue; + } + } + + if (fieldDetails.path || options.defaultPath) { + filter.path = fieldDetails.path || options.defaultPath; + } + + items[fullKey] = filter; } - return transformFiltersParseOutput( - buildDefaultFiltersParseOutput(options, temp), - ); + return buildDefaultFiltersParseOutput(options, items); } diff --git a/src/parameter/filters/type.ts b/src/parameter/filters/type.ts index 9871c2ee..82cc7b25 100644 --- a/src/parameter/filters/type.ts +++ b/src/parameter/filters/type.ts @@ -7,11 +7,11 @@ import { Flatten, - NestedKeys, OnlyObject, OnlyScalar, TypeFromNestedKeyPath, + NestedKeys, ObjectLiteral, OnlyObject, OnlyScalar, TypeFromNestedKeyPath, } from '../../type'; import { RelationsParseOutput } from '../relations'; import { - ParseOptionsAllowed, + ParseAllowedKeys, } from '../type'; import { FilterOperator, FilterOperatorLabel } from './constants'; @@ -20,16 +20,16 @@ import { FilterOperator, FilterOperatorLabel } from './constants'; type FilterValueInputPrimitive = boolean | number | string; type FilterValueInput = FilterValueInputPrimitive | null | undefined; -export type FilterValueSimple = V extends FilterValueInputPrimitive ? (V | V[]) : V; +export type FilterValueSimple = V extends string | number ? (V | V[]) : V; export type FilterValueWithOperator = V extends string | number ? `!${V}` | `!~${V}` | `~${V}` | `${V}~` | `~${V}~` | `<${V}` | `<=${V}` | `>${V}` | `>=${V}` | null | '!null' : V extends boolean ? null | '!null' : never; -export type FilterValue = V extends FilterValueInputPrimitive ? +export type FilterValue = V extends string | number ? (FilterValueSimple | FilterValueWithOperator | Array>) : V; -export type FilterOperatorConfig = { +export type FilterValueConfig = { operator: `${FilterOperator}` | (`${FilterOperator}`)[]; value: FilterValueSimple }; @@ -39,7 +39,7 @@ export type FilterOperatorConfig // ----------------------------------------------------------- export type FiltersBuildInputValue = T extends OnlyScalar ? - T | FilterValue | FilterOperatorConfig : + T | FilterValue | FilterValueConfig : never; export type FiltersBuildInput> = { @@ -54,23 +54,26 @@ export type FiltersBuildInput> = { // Parse // ----------------------------------------------------------- -export type FiltersParseOptionsDefault> = { +export type FiltersDefaultKeys> = { [K in keyof T]?: Flatten extends OnlyObject ? - FiltersParseOptionsDefault> : + FiltersDefaultKeys> : (K extends string ? FilterValue> : never) } | { [K in NestedKeys]?: FilterValue> }; +export type FiltersValidator = (key: K, value: unknown) => boolean; + export type FiltersParseOptions< - T extends Record = Record, + T extends ObjectLiteral = ObjectLiteral, > = { mapping?: Record, - allowed?: ParseOptionsAllowed, - default?: FiltersParseOptionsDefault, + allowed?: ParseAllowedKeys, + default?: FiltersDefaultKeys, defaultByElement?: boolean, defaultPath?: string, - relations?: RelationsParseOutput + relations?: RelationsParseOutput, + validate?: FiltersValidator> }; export type FiltersParseOutputElement = { diff --git a/src/parameter/filters/utils/operator.ts b/src/parameter/filters/utils/operator.ts index 0813405d..158e79ea 100644 --- a/src/parameter/filters/utils/operator.ts +++ b/src/parameter/filters/utils/operator.ts @@ -6,7 +6,7 @@ */ import { - FilterOperatorConfig, + FilterValueConfig, } from '../type'; import { hasOwnProperty, isSimpleValue } from '../../../utils'; import { FilterOperator, FilterOperatorLabel } from '../constants'; @@ -67,7 +67,7 @@ export function determineFilterOperatorLabelsByValue(input: string) : { }; } -export function isFilterOperatorConfig(data: unknown) : data is FilterOperatorConfig { +export function isFilterValueConfig(data: unknown) : data is FilterValueConfig { if (typeof data !== 'object' || data === null) { return false; } diff --git a/src/parameter/filters/utils/value.ts b/src/parameter/filters/utils/value.ts index acf87683..8b06f196 100644 --- a/src/parameter/filters/utils/value.ts +++ b/src/parameter/filters/utils/value.ts @@ -5,16 +5,16 @@ * view the LICENSE file that was distributed with this source code. */ -import { FilterValueSimple } from '../type'; +import { FilterValue } from '../type'; -export function transformFilterValue(input: FilterValueSimple) : FilterValueSimple { +export function transformFilterValue(input: FilterValue) : FilterValue { if (Array.isArray(input)) { for (let i = 0; i < input.length; i++) { - input[i] = transformFilterValue(input[i]) as string | number | boolean; + input[i] = transformFilterValue(input[i]) as string | number; } return (input as unknown[]) - .filter((n) => n === 0 || !!n) as FilterValueSimple; + .filter((n) => n === 0 || !!n) as FilterValue; } if (typeof input === 'undefined' || input === null) { diff --git a/src/parameter/relations/parse.ts b/src/parameter/relations/parse.ts index 1385c3d7..f9e705e0 100644 --- a/src/parameter/relations/parse.ts +++ b/src/parameter/relations/parse.ts @@ -8,7 +8,7 @@ import minimatch from 'minimatch'; import { ObjectLiteral } from '../../type'; import { applyMapping, hasOwnProperty } from '../../utils'; -import { isPathCoveredByParseOptionsAllowed } from '../utils'; +import { isPathCoveredByParseAllowed } from '../utils'; import { RelationsParseOptions, RelationsParseOutput } from './type'; import { includeParents } from './utils'; @@ -60,7 +60,7 @@ export function parseQueryRelations( } if (options.allowed) { - items = items.filter((item) => isPathCoveredByParseOptionsAllowed(options.allowed, item)); + items = items.filter((item) => isPathCoveredByParseAllowed(options.allowed, item)); } if (options.includeParents) { diff --git a/src/parameter/sort/parse.ts b/src/parameter/sort/parse.ts index 95726bd6..acbb9540 100644 --- a/src/parameter/sort/parse.ts +++ b/src/parameter/sort/parse.ts @@ -13,7 +13,7 @@ import { hasOwnProperty, isFieldNonRelational, isFieldPathAllowedByRelations, } from '../../utils'; -import { flattenParseOptionsAllowed, isPathCoveredByParseOptionsAllowed } from '../utils'; +import { flattenParseOptionsAllowed, isPathCoveredByParseAllowed } from '../utils'; import { SortParseOptions, @@ -149,7 +149,7 @@ export function parseQuerySort( if ( typeof options.allowed !== 'undefined' && !isMultiDimensionalArray(options.allowed) && - !isPathCoveredByParseOptionsAllowed(options.allowed, [key, keyWithAlias]) + !isPathCoveredByParseAllowed(options.allowed, [key, keyWithAlias]) ) { continue; } diff --git a/src/parameter/sort/type.ts b/src/parameter/sort/type.ts index 776b8075..8b630cb1 100644 --- a/src/parameter/sort/type.ts +++ b/src/parameter/sort/type.ts @@ -10,7 +10,7 @@ import { } from '../../type'; import { RelationsParseOutput } from '../relations'; import { - ParseOptionsAllowed, + ParseAllowedKeys, } from '../type'; export enum SortDirection { @@ -31,14 +31,14 @@ export type SortBuildInput> = `${SortDirection}` } | - [ - SortWithOperator>[], + ( + SortWithOperator>[] | { [K in keyof T]?: Flatten extends OnlyObject ? SortBuildInput> : `${SortDirection}` - }, - ] + } + )[] | SortWithOperator>[] | SortWithOperator>; @@ -58,7 +58,7 @@ export type SortParseOptionsDefault> = { export type SortParseOptions< T extends Record = Record, > = { - allowed?: ParseOptionsAllowed | ParseOptionsAllowed[], + allowed?: ParseAllowedKeys, mapping?: Record, default?: SortParseOptionsDefault, defaultPath?: string, diff --git a/src/parameter/type.ts b/src/parameter/type.ts index 321227e3..29881af0 100644 --- a/src/parameter/type.ts +++ b/src/parameter/type.ts @@ -6,21 +6,24 @@ */ import { - Flatten, NestedKeys, OnlyObject, SimpleKeys, + Flatten, NestedKeys, ObjectLiteral, OnlyObject, SimpleKeys, } from '../type'; // ----------------------------------------------------------- -export type ParseOptionsAllowedObject> = { +type ParseAllowedObject = { [K in keyof T]?: T[K] extends OnlyObject ? - ParseOptionsAllowed> : + ParseAllowedKeys> : never }; -export type ParseOptionsAllowed> = ParseOptionsAllowedObject | -[ - SimpleKeys[], - ParseOptionsAllowedObject, -] -| -NestedKeys[]; +export type ParseAllowedKeys = T extends ObjectLiteral ? + ( + ParseAllowedObject | + ( + SimpleKeys[] | + ParseAllowedObject + )[] + | + NestedKeys[] + ) : string[]; diff --git a/src/parameter/utils/parse/options-allowed.ts b/src/parameter/utils/parse/options-allowed.ts index 35adef85..f645c252 100644 --- a/src/parameter/utils/parse/options-allowed.ts +++ b/src/parameter/utils/parse/options-allowed.ts @@ -5,17 +5,20 @@ * view the LICENSE file that was distributed with this source code. */ +import { NestedKeys, NestedResourceKeys } from '../../../type'; import { flattenToKeyPathArray } from '../../../utils'; -import { ParseOptionsAllowed } from '../../type'; +import { ParseAllowedKeys } from '../../type'; export function flattenParseOptionsAllowed( - input: ParseOptionsAllowed, + input: ParseAllowedKeys, ) : string[] { return flattenToKeyPathArray(input); } -export function isPathCoveredByParseOptionsAllowed( - input: ParseOptionsAllowed, +export function isPathCoveredByParseAllowed( + input: ParseAllowedKeys | + NestedKeys[] | + NestedResourceKeys[], path: string | string[], ) : boolean { const paths = Array.isArray(path) ? path : [path]; diff --git a/src/type.ts b/src/type.ts index 60712fa3..d7a3e447 100644 --- a/src/type.ts +++ b/src/type.ts @@ -14,28 +14,37 @@ export type KeyWithOptionalPrefix = T extends string ? (`${ type PrevIndex = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; -export type SimpleKeys> = - {[Key in keyof T & (string | number)]: Flatten extends Record - ? (Flatten extends Date ? `${Key}` : never) - : `${Key}` - }[keyof T & (string | number)]; +export type SimpleKeys = + T extends ObjectLiteral ? + ( + {[Key in keyof T & (string | number)]: Flatten extends Record + ? (Flatten extends Date ? `${Key}` : never) + : `${Key}` + }[keyof T & (string | number)] + ) : string; -export type NestedKeys, Depth extends number = 4> = - [Depth] extends [0] ? never : - {[Key in keyof T & (string | number)]: Flatten extends Record - ? (Flatten extends Date ? `${Key}` : `${Key}.${NestedKeys, PrevIndex[Depth]>}`) - : `${Key}` - }[keyof T & (string | number)]; +export type NestedKeys = + T extends ObjectLiteral ? + ( + [Depth] extends [0] ? never : + {[Key in keyof T & (string | number)]: Flatten extends Record + ? (Flatten extends Date ? `${Key}` : `${Key}.${NestedKeys, PrevIndex[Depth]>}`) + : `${Key}` + }[keyof T & (string | number)] + ) : string; -export type NestedResourceKeys, Depth extends number = 4> = - [Depth] extends [0] ? never : - {[Key in keyof T & (string | number)]: Flatten extends Record - ? Key | `${Key}.${NestedResourceKeys, PrevIndex[Depth]>}` - : never - }[keyof T & (string | number)]; +export type NestedResourceKeys = + T extends ObjectLiteral ? + ( + [Depth] extends [0] ? never : + {[Key in keyof T & (string | number)]: Flatten extends Record + ? Key | `${Key}.${NestedResourceKeys, PrevIndex[Depth]>}` + : never + }[keyof T & (string | number)] + ) : string; export type TypeFromNestedKeyPath< - T extends Record, + T extends ObjectLiteral, Path extends string, > = { [K in Path]: K extends keyof T diff --git a/test/unit/filters.spec.ts b/test/unit/filters.spec.ts index 68571e0d..1d8a250f 100644 --- a/test/unit/filters.spec.ts +++ b/test/unit/filters.spec.ts @@ -6,13 +6,13 @@ */ import { + isFilterValueConfig, FilterOperatorLabel, FiltersParseOptions, FiltersParseOutput, parseQueryFilters, parseQueryRelations, } from '../../src'; -import {isFilterOperatorConfig} from "../../src/parameter/filters/utils"; describe('src/filter/index.ts', () => { it('should transform request filters', () => { @@ -182,6 +182,46 @@ describe('src/filter/index.ts', () => { ] as FiltersParseOutput); }); + it('should transform filters with validator', () => { + let data = parseQueryFilters( + { id: '1' }, + { + allowed: ['id'], + validate: (key, value) => { + if(key === 'id') { + return typeof value === 'number'; + } + + return false; + } + } + ); + expect(data).toEqual([ + { + key: 'id', + value: 1, + }, + ] as FiltersParseOutput); + + data = parseQueryFilters( + { id: '1,2,3' }, + { + allowed: ['id'], + validate: (key, value) => { + if(key === 'id') { + return typeof value === 'number' && + value > 1; + } + + return false; + } + } + ); + expect(data).toEqual([ + { key: 'id', value: [2, 3], operator: { in: true }}, + ] as FiltersParseOutput); + }) + it('should transform filters with different operators', () => { // equal operator let data = parseQueryFilters({ id: '1' }, { allowed: ['id'] }); @@ -357,21 +397,21 @@ describe('src/filter/index.ts', () => { }); it('should determine filter operator config', () => { - let data = isFilterOperatorConfig({ + let data = isFilterValueConfig({ value: 1, operator: '<' }); expect(data).toBeTruthy(); - data = isFilterOperatorConfig({ + data = isFilterValueConfig({ value: 1, operator: {} }) expect(data).toBeFalsy(); - data = isFilterOperatorConfig({ + data = isFilterValueConfig({ value: {}, operator: '<' }); diff --git a/test/unit/utils.spec.ts b/test/unit/utils.spec.ts index 8ab44fb2..7fcc1ce4 100644 --- a/test/unit/utils.spec.ts +++ b/test/unit/utils.spec.ts @@ -5,7 +5,7 @@ * view the LICENSE file that was distributed with this source code. */ -import { flattenParseOptionsAllowed, isPathCoveredByParseOptionsAllowed} from "../../src"; +import { flattenParseOptionsAllowed, isPathCoveredByParseAllowed} from "../../src"; import {applyMapping} from "../../src/utils"; import { User } from "../data"; @@ -46,56 +46,56 @@ describe('src/utils/*.ts', () => { }); it('should verify if path is covered by parse options', () => { - let result = isPathCoveredByParseOptionsAllowed(['id', 'name'], 'id'); + let result = isPathCoveredByParseAllowed(['id', 'name'], 'id'); expect(result).toBeTruthy(); - result = isPathCoveredByParseOptionsAllowed(['id', 'name'], 'description'); + result = isPathCoveredByParseAllowed(['id', 'name'], 'description'); expect(result).toBeFalsy(); // ----------------------------------------------------------- - result = isPathCoveredByParseOptionsAllowed({realm: ['id', 'name']}, 'realm.id'); + result = isPathCoveredByParseAllowed({realm: ['id', 'name']}, 'realm.id'); expect(result).toBeTruthy(); - result = isPathCoveredByParseOptionsAllowed({realm: ['id', 'name']}, 'realm.description'); + result = isPathCoveredByParseAllowed({realm: ['id', 'name']}, 'realm.description'); expect(result).toBeFalsy(); // ----------------------------------------------------------- - result = isPathCoveredByParseOptionsAllowed(['realm.id', 'realm.name'], 'realm.id'); + result = isPathCoveredByParseAllowed(['realm.id', 'realm.name'], 'realm.id'); expect(result).toBeTruthy(); - result = isPathCoveredByParseOptionsAllowed(['realm.id', 'realm.name'], 'realm.description'); + result = isPathCoveredByParseAllowed(['realm.id', 'realm.name'], 'realm.description'); expect(result).toBeFalsy(); // ----------------------------------------------------------- - result = isPathCoveredByParseOptionsAllowed([['name'], { realm: ['id'] }], 'name'); + result = isPathCoveredByParseAllowed([['name'], { realm: ['id'] }], 'name'); expect(result).toBeTruthy(); - result = isPathCoveredByParseOptionsAllowed([['name'], { realm: ['id'] }], 'id'); + result = isPathCoveredByParseAllowed([['name'], { realm: ['id'] }], 'id'); expect(result).toBeFalsy(); - result = isPathCoveredByParseOptionsAllowed([['name'], { realm: ['id'] }], 'realm.id'); + result = isPathCoveredByParseAllowed([['name'], { realm: ['id'] }], 'realm.id'); expect(result).toBeTruthy(); - result = isPathCoveredByParseOptionsAllowed([['name'], { realm: ['id'] }], 'realm.name'); + result = isPathCoveredByParseAllowed([['name'], { realm: ['id'] }], 'realm.name'); expect(result).toBeFalsy(); // ----------------------------------------------------------- - result = isPathCoveredByParseOptionsAllowed({items: ['realm.id']}, 'items.realm.id'); + result = isPathCoveredByParseAllowed({items: ['realm.id']}, 'items.realm.id'); expect(result).toBeTruthy(); - result = isPathCoveredByParseOptionsAllowed({items: ['realm.name']}, 'items.realm.id'); + result = isPathCoveredByParseAllowed({items: ['realm.name']}, 'items.realm.id'); expect(result).toBeFalsy(); // ----------------------------------------------------------- - result = isPathCoveredByParseOptionsAllowed({items: {realm: ['id']}}, 'items.realm.id'); + result = isPathCoveredByParseAllowed({items: {realm: ['id']}}, 'items.realm.id'); expect(result).toBeTruthy(); - result = isPathCoveredByParseOptionsAllowed({items: {realm: ['name']}}, 'items.realm.id'); + result = isPathCoveredByParseAllowed({items: {realm: ['name']}}, 'items.realm.id'); expect(result).toBeFalsy(); }) })