diff --git a/package-lock.json b/package-lock.json index c27ebb14..429baf8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.8.1", "license": "MIT", "dependencies": { + "ebec": "^1.1.0", "smob": "^1.4.0" }, "devDependencies": { @@ -5171,6 +5172,14 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ebec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ebec/-/ebec-1.1.0.tgz", + "integrity": "sha512-BUSQN/hPnUUo+v/psEMYGruVXyI+ACEOJBnG+EXz7PKcDNEQfCLjzyifUGu91nX/cEWuSF3Yrad4LLF8F3MvsA==", + "dependencies": { + "smob": "^1.4.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.413", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.413.tgz", diff --git a/package.json b/package.json index ed6c8e89..d6addd67 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ }, "homepage": "https://github.com/Tada5hi/rapiq#readme", "dependencies": { + "ebec": "^1.1.0", "smob": "^1.4.0" }, "devDependencies": { diff --git a/src/errors/base.ts b/src/errors/base.ts new file mode 100644 index 00000000..49d4a6b3 --- /dev/null +++ b/src/errors/base.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { BaseError as Base } from 'ebec'; +import { ErrorCode } from './code'; + +export class BaseError extends Base { + get code() : `${ErrorCode}` { + return this.getOption('code') as `${ErrorCode}` || ErrorCode.NONE; + } +} diff --git a/src/errors/build.ts b/src/errors/build.ts new file mode 100644 index 00000000..a0ce0a39 --- /dev/null +++ b/src/errors/build.ts @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import type { Options } from 'ebec'; +import { isObject } from '../utils'; +import { BaseError } from './base'; + +export class BuildError extends BaseError { + constructor(message?: string | Options) { + if (isObject(message)) { + message.message = 'A building error has occurred.'; + } + + super(message || 'A building error has occurred.'); + } +} diff --git a/src/errors/code.ts b/src/errors/code.ts new file mode 100644 index 00000000..8e6c6a23 --- /dev/null +++ b/src/errors/code.ts @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +export enum ErrorCode { + NONE = 'none', + + INPUT_INVALID = 'inputInvalid', + + KEY_INVALID = 'keyInvalid', + + KEY_PATH_INVALID = 'keyPathInvalid', + + KEY_NOT_ALLOWED = 'keyNotAllowed', + + KEY_VALUE_INVALID = 'keyValueInvalid', +} diff --git a/src/errors/index.ts b/src/errors/index.ts new file mode 100644 index 00000000..91b4bc7c --- /dev/null +++ b/src/errors/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +export * from './base'; +export * from './build'; +export * from './code'; +export * from './parse'; diff --git a/src/errors/parse.ts b/src/errors/parse.ts new file mode 100644 index 00000000..a8bf260f --- /dev/null +++ b/src/errors/parse.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import type { Options } from 'ebec'; +import { isObject } from '../utils'; +import { BaseError } from './base'; +import { ErrorCode } from './code'; + +export class ParseError extends BaseError { + constructor(message?: string | Options) { + if (isObject(message)) { + message.message = message.message || 'A parsing error has occurred.'; + } + + super(message || 'A parsing error has occurred.'); + } + + static inputInvalid() { + return new this({ + message: 'The shape of the input is not valid.', + code: ErrorCode.INPUT_INVALID, + }); + } + + static keyNotAllowed(name: string) { + return new this({ + message: `The key ${name} is not covered by allowed/default options.`, + code: ErrorCode.KEY_NOT_ALLOWED, + }); + } + + static keyInvalid(key: string) { + return new this({ + message: `The key ${key} is invalid.`, + code: ErrorCode.KEY_INVALID, + }); + } + + static keyPathInvalid(key: string) { + return new this({ + message: `The key path ${key} is invalid.`, + code: ErrorCode.KEY_PATH_INVALID, + }); + } + + static keyValueInvalid(key: string) { + return new this({ + message: `The value of the key ${key} is invalid.`, + code: ErrorCode.KEY_VALUE_INVALID, + }); + } +} diff --git a/src/index.ts b/src/index.ts index d164a726..51176e99 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ */ export * from './build'; +export * from './errors'; export * from './parameter'; export * from './parse'; export * from './constants'; diff --git a/src/parameter/fields/errors/build.ts b/src/parameter/fields/errors/build.ts new file mode 100644 index 00000000..8c419b4f --- /dev/null +++ b/src/parameter/fields/errors/build.ts @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { BuildError } from '../../../errors'; + +export class FieldsBuildError extends BuildError { + +} diff --git a/src/parameter/fields/errors/index.ts b/src/parameter/fields/errors/index.ts new file mode 100644 index 00000000..e28a8539 --- /dev/null +++ b/src/parameter/fields/errors/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +export * from './build'; +export * from './parse'; diff --git a/src/parameter/fields/errors/parse.ts b/src/parameter/fields/errors/parse.ts new file mode 100644 index 00000000..06a44a33 --- /dev/null +++ b/src/parameter/fields/errors/parse.ts @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { ParseError } from '../../../errors'; + +export class FieldsParseError extends ParseError { + +} diff --git a/src/parameter/fields/index.ts b/src/parameter/fields/index.ts index 96390a18..c091bdb9 100644 --- a/src/parameter/fields/index.ts +++ b/src/parameter/fields/index.ts @@ -7,6 +7,7 @@ export * from './build'; export * from './constants'; +export * from './errors'; export * from './parse'; export * from './type'; export * from './utils'; diff --git a/src/parameter/fields/parse.ts b/src/parameter/fields/parse.ts index a53f5918..1bf293cb 100644 --- a/src/parameter/fields/parse.ts +++ b/src/parameter/fields/parse.ts @@ -5,21 +5,17 @@ * view the LICENSE file that was distributed with this source code. */ -import { isObject } from 'smob'; +import { distinctArray, isObject } from 'smob'; +import { DEFAULT_ID } from '../../constants'; import type { ObjectLiteral } from '../../type'; import { - applyMapping, buildFieldWithPath, groupArrayByKeyPath, hasOwnProperty, isFieldPathAllowedByRelations, merge, + applyMapping, groupArrayByKeyPath, hasOwnProperty, isPathAllowedByRelations, merge, } from '../../utils'; import { flattenParseAllowedOption } from '../utils'; -import type { - FieldsInputTransformed, FieldsParseOptions, FieldsParseOutput, -} from './type'; -import { - isValidFieldName, - parseFieldsInput, removeFieldInputOperator, - transformFieldsInput, -} from './utils'; -import { DEFAULT_ID } from '../../constants'; +import { FieldOperator } from './constants'; +import { FieldsParseError } from './errors'; +import type { FieldsInputTransformed, FieldsParseOptions, FieldsParseOutput } from './type'; +import { isValidFieldName, parseFieldsInput } from './utils'; // -------------------------------------------------- @@ -59,10 +55,7 @@ export function parseQueryFields( // If it is an empty array nothing is allowed if ( - ( - typeof options.default !== 'undefined' || - typeof options.allowed !== 'undefined' - ) && + (typeof options.default !== 'undefined' || typeof options.allowed !== 'undefined') && keys.length === 0 ) { return []; @@ -74,17 +67,24 @@ export function parseQueryFields( if (isObject(input)) { data = input; - } else if (typeof input === 'string') { - data = { [DEFAULT_ID]: input }; - } else if (Array.isArray(input)) { + } else if (typeof input === 'string' || Array.isArray(input)) { data = { [DEFAULT_ID]: input }; + } else if (options.throwOnFailure) { + throw FieldsParseError.inputInvalid(); } options.mapping = options.mapping || {}; const reverseMapping = buildReverseRecord(options.mapping); - if (keys.length === 0) { - keys = Object.keys(data); + if ( + keys.length > 0 && + hasOwnProperty(data, DEFAULT_ID) + ) { + data = { + [keys[0]]: data[DEFAULT_ID], + }; + } else { + keys = distinctArray([...keys, ...Object.keys(data)]); } const output : FieldsParseOutput = []; @@ -93,9 +93,13 @@ export function parseQueryFields( const path = keys[i]; if ( - !isFieldPathAllowedByRelations({ path }, options.relations) && - path !== DEFAULT_ID + path !== DEFAULT_ID && + !isPathAllowedByRelations(path, options.relations) ) { + if (options.throwOnFailure) { + throw FieldsParseError.keyPathInvalid(path); + } + continue; } @@ -110,7 +114,7 @@ export function parseQueryFields( fields = parseFieldsInput(data[reverseMapping[path]]); } - let transformed : FieldsInputTransformed = { + const transformed : FieldsInputTransformed = { default: [], included: [], excluded: [], @@ -118,24 +122,45 @@ export function parseQueryFields( if (fields.length > 0) { for (let j = 0; j < fields.length; j++) { - fields[j] = applyMapping( - buildFieldWithPath({ name: fields[j], path }), - options.mapping, - true, - ); - } + let operator: FieldOperator | undefined; - if (hasOwnProperty(domainFields, path)) { - fields = fields.filter((field) => domainFields[path].indexOf( - removeFieldInputOperator(field), - ) !== -1); - } else { - fields = fields.filter((field) => isValidFieldName(removeFieldInputOperator(field))); - } + const character = fields[j].substring(0, 1); - transformed = transformFieldsInput( - fields, - ); + if (character === FieldOperator.INCLUDE) { + operator = FieldOperator.INCLUDE; + } else if (character === FieldOperator.EXCLUDE) { + operator = FieldOperator.EXCLUDE; + } + + if (operator) { + fields[j] = fields[j].substring(1); + } + + fields[j] = applyMapping(fields[j], options.mapping, true); + + let isValid : boolean; + if (hasOwnProperty(domainFields, path)) { + isValid = domainFields[path].indexOf(fields[j]) !== -1; + } else { + isValid = isValidFieldName(fields[j]); + } + + if (!isValid) { + if (options.throwOnError) { + throw FieldsParseError.keyNotAllowed(fields[j]); + } + + continue; + } + + if (operator === FieldOperator.INCLUDE) { + transformed.included.push(fields[j]); + } else if (operator === FieldOperator.EXCLUDE) { + transformed.excluded.push(fields[j]); + } else { + transformed.default.push(fields[j]); + } + } } if ( diff --git a/src/parameter/fields/type.ts b/src/parameter/fields/type.ts index 50d0a710..8766c12a 100644 --- a/src/parameter/fields/type.ts +++ b/src/parameter/fields/type.ts @@ -51,6 +51,7 @@ export type FieldsParseOptions< allowed?: ParseAllowedOption, default?: ParseAllowedOption, defaultPath?: string, + throwOnFailure?: boolean, relations?: RelationsParseOutput, }; diff --git a/src/parameter/fields/utils/input.ts b/src/parameter/fields/utils/input.ts index 35486187..b2943bc4 100644 --- a/src/parameter/fields/utils/input.ts +++ b/src/parameter/fields/utils/input.ts @@ -5,59 +5,6 @@ * view the LICENSE file that was distributed with this source code. */ -import type { FieldsInputTransformed } from '../type'; -import { FieldOperator } from '../constants'; - -export function removeFieldInputOperator(field: string) { - const firstCharacter = field.substring(0, 1); - - return firstCharacter === FieldOperator.INCLUDE || - firstCharacter === FieldOperator.EXCLUDE ? - field.substring(1) : - field; -} - -export function transformFieldsInput( - fields: string[], -): FieldsInputTransformed { - const output: FieldsInputTransformed = { - default: [], - included: [], - excluded: [], - }; - - for (let i = 0; i < fields.length; i++) { - let operator: FieldOperator | undefined; - - const character = fields[i].substring(0, 1); - - if (character === FieldOperator.INCLUDE) { - operator = FieldOperator.INCLUDE; - } else if (character === FieldOperator.EXCLUDE) { - operator = FieldOperator.EXCLUDE; - } - - if (operator) { - fields[i] = fields[i].substring(1); - - switch (operator) { - case FieldOperator.INCLUDE: { - output.included.push(fields[i]); - break; - } - case FieldOperator.EXCLUDE: { - output.excluded.push(fields[i]); - break; - } - } - } else { - output.default.push(fields[i]); - } - } - - return output; -} - export function parseFieldsInput(input: unknown): string[] { let output: string[] = []; diff --git a/src/parameter/filters/errors/build.ts b/src/parameter/filters/errors/build.ts new file mode 100644 index 00000000..0213adc9 --- /dev/null +++ b/src/parameter/filters/errors/build.ts @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { BuildError } from '../../../errors'; + +export class FiltersBuildError extends BuildError { + +} diff --git a/src/parameter/filters/errors/index.ts b/src/parameter/filters/errors/index.ts new file mode 100644 index 00000000..e28a8539 --- /dev/null +++ b/src/parameter/filters/errors/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +export * from './build'; +export * from './parse'; diff --git a/src/parameter/filters/errors/parse.ts b/src/parameter/filters/errors/parse.ts new file mode 100644 index 00000000..f9cdde65 --- /dev/null +++ b/src/parameter/filters/errors/parse.ts @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { ParseError } from '../../../errors'; + +export class FiltersParseError extends ParseError { + +} diff --git a/src/parameter/filters/index.ts b/src/parameter/filters/index.ts index 8e3d4eab..3cafa56b 100644 --- a/src/parameter/filters/index.ts +++ b/src/parameter/filters/index.ts @@ -7,6 +7,7 @@ export * from './constants'; export * from './build'; +export * from './errors'; export * from './parse'; export * from './type'; export * from './utils'; diff --git a/src/parameter/filters/parse.ts b/src/parameter/filters/parse.ts index 191fb24e..fba7991c 100644 --- a/src/parameter/filters/parse.ts +++ b/src/parameter/filters/parse.ts @@ -6,23 +6,26 @@ */ import type { NestedKeys, ObjectLiteral } from '../../type'; -import type { FieldDetails } from '../../utils'; +import type { KeyDetails } from '../../utils'; import { applyMapping, - buildFieldWithPath, + buildKeyWithPath, flattenNestedObject, - getFieldDetails, - hasOwnProperty, isFieldNonRelational, isFieldPathAllowedByRelations, + hasOwnProperty, + isObject, + isPathAllowedByRelations, + parseKey, } from '../../utils'; import { isValidFieldName } from '../fields'; import type { ParseAllowedOption } from '../type'; import { flattenParseAllowedOption, isPathCoveredByParseAllowedOption } from '../utils'; import { FilterComparisonOperator } from './constants'; +import { FiltersParseError } from './errors'; import type { FiltersParseOptions, FiltersParseOutput, FiltersParseOutputElement } from './type'; import { parseFilterValue, transformFilterValue } from './utils'; // -------------------------------------------------- - +// ^([0-9]+(?:\.[0-9]+)*){0,1}([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)*){0,1}$ function transformFiltersParseOutputElement(element: FiltersParseOutputElement) : FiltersParseOutputElement { if ( hasOwnProperty(element, 'path') && @@ -69,29 +72,29 @@ function buildDefaultFiltersParseOutput const output : FiltersParseOutput = []; for (let i = 0; i < keys.length; i++) { - const fieldDetails = getFieldDetails(keys[i]); + const keyDetails = parseKey(keys[i]); if ( options.defaultByElement && inputKeys.length > 0 ) { - const fieldWithAlias = buildFieldWithPath(fieldDetails); - if (hasOwnProperty(input, fieldWithAlias)) { + const keyWithPath = buildKeyWithPath(keyDetails); + if (hasOwnProperty(input, keyWithPath)) { continue; } } if (options.defaultByElement || inputKeys.length === 0) { let path : string | undefined; - if (fieldDetails.path) { - path = fieldDetails.path; + if (keyDetails.path) { + path = keyDetails.path; } else if (options.defaultPath) { path = options.defaultPath; } output.push(transformFiltersParseOutputElement({ ...(path ? { path } : {}), - key: fieldDetails.name, + key: keyDetails.name, value: flatten[keys[i]], })); } @@ -120,7 +123,11 @@ export function parseQueryFilters( } /* istanbul ignore next */ - if (typeof data !== 'object' || data === null) { + if (!isObject(data)) { + if (options.throwOnFailure) { + throw FiltersParseError.inputInvalid(); + } + return buildDefaultFiltersParseOutput(options); } @@ -142,12 +149,6 @@ export function parseQueryFilters( // transform to appreciate data format & validate input const keys = Object.keys(data); for (let i = 0; i < keys.length; i++) { - /* istanbul ignore next */ - if (!hasOwnProperty(data, keys[i])) { - // eslint-disable-next-line no-continue - continue; - } - const value : unknown = data[keys[i]]; if ( @@ -158,33 +159,46 @@ export function parseQueryFilters( value !== null && !Array.isArray(value) ) { + if (options.throwOnFailure) { + throw FiltersParseError.keyValueInvalid(keys[i]); + } continue; } keys[i] = applyMapping(keys[i], options.mapping); - const fieldDetails : FieldDetails = getFieldDetails(keys[i]); + const fieldDetails : KeyDetails = parseKey(keys[i]); if ( typeof options.allowed === 'undefined' && !isValidFieldName(fieldDetails.name) ) { + if (options.throwOnFailure) { + throw FiltersParseError.keyInvalid(fieldDetails.name); + } continue; } if ( - !isFieldPathAllowedByRelations(fieldDetails, options.relations) && - !isFieldNonRelational(fieldDetails) + typeof fieldDetails.path !== 'undefined' && + !isPathAllowedByRelations(fieldDetails.path, options.relations) ) { + if (options.throwOnFailure) { + throw FiltersParseError.keyPathInvalid(fieldDetails.path); + } continue; } - const fullKey : string = buildFieldWithPath(fieldDetails); + const fullKey : string = buildKeyWithPath(fieldDetails); if ( options.allowed && !isPathCoveredByParseAllowedOption(options.allowed, [keys[i], fullKey]) ) { + if (options.throwOnFailure) { + throw FiltersParseError.keyInvalid(fieldDetails.name); + } + continue; } @@ -199,6 +213,8 @@ export function parseQueryFilters( for (let j = 0; j < filter.value.length; j++) { if (options.validate(filter.key as NestedKeys, filter.value[j])) { output.push(filter.value[j]); + } else if (options.throwOnError) { + throw FiltersParseError.keyValueInvalid(fieldDetails.name); } } @@ -207,6 +223,10 @@ export function parseQueryFilters( continue; } } else if (!options.validate(filter.key as NestedKeys, filter.value)) { + if (options.throwOnFailure) { + throw FiltersParseError.keyValueInvalid(fieldDetails.name); + } + continue; } } @@ -215,6 +235,10 @@ export function parseQueryFilters( typeof filter.value === 'string' && filter.value.length === 0 ) { + if (options.throwOnFailure) { + throw FiltersParseError.keyValueInvalid(fieldDetails.name); + } + continue; } @@ -222,6 +246,10 @@ export function parseQueryFilters( Array.isArray(filter.value) && filter.value.length === 0 ) { + if (options.throwOnFailure) { + throw FiltersParseError.keyValueInvalid(fieldDetails.name); + } + continue; } diff --git a/src/parameter/filters/type.ts b/src/parameter/filters/type.ts index 25dc3f98..781d0c16 100644 --- a/src/parameter/filters/type.ts +++ b/src/parameter/filters/type.ts @@ -65,6 +65,7 @@ export type FiltersParseOptions< default?: FiltersParseDefaultOption, defaultByElement?: boolean, defaultPath?: string, + throwOnFailure?: boolean, relations?: RelationsParseOutput, validate?: FiltersValidatorOption> }; diff --git a/src/parameter/pagination/errors/build.ts b/src/parameter/pagination/errors/build.ts new file mode 100644 index 00000000..674ae17c --- /dev/null +++ b/src/parameter/pagination/errors/build.ts @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { BuildError } from '../../../errors'; + +export class PaginationBuildError extends BuildError { + +} diff --git a/src/parameter/pagination/errors/index.ts b/src/parameter/pagination/errors/index.ts new file mode 100644 index 00000000..e28a8539 --- /dev/null +++ b/src/parameter/pagination/errors/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +export * from './build'; +export * from './parse'; diff --git a/src/parameter/pagination/errors/parse.ts b/src/parameter/pagination/errors/parse.ts new file mode 100644 index 00000000..e69638f9 --- /dev/null +++ b/src/parameter/pagination/errors/parse.ts @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { ParseError } from '../../../errors'; + +export class PaginationParseError extends ParseError { + static limitExceeded(limit: number) { + return new this({ + message: `The pagination limit must not exceed the value of ${limit}.`, + }); + } +} diff --git a/src/parameter/pagination/index.ts b/src/parameter/pagination/index.ts index f63fb93e..f05d360f 100644 --- a/src/parameter/pagination/index.ts +++ b/src/parameter/pagination/index.ts @@ -6,5 +6,6 @@ */ export * from './build'; +export * from './errors'; export * from './parse'; export * from './type'; diff --git a/src/parameter/pagination/parse.ts b/src/parameter/pagination/parse.ts index e9f1e117..acacdfa2 100644 --- a/src/parameter/pagination/parse.ts +++ b/src/parameter/pagination/parse.ts @@ -6,6 +6,7 @@ */ import { isObject } from 'smob'; +import { PaginationParseError } from './errors'; import type { PaginationParseOptions, PaginationParseOutput } from './type'; // -------------------------------------------------- @@ -19,6 +20,10 @@ function finalizePagination( typeof data.limit === 'undefined' || data.limit > options.maxLimit ) { + if (options.throwOnFailure) { + throw PaginationParseError.limitExceeded(options.maxLimit); + } + data.limit = options.maxLimit; } } @@ -48,6 +53,10 @@ export function parseQueryPagination( const pagination : PaginationParseOutput = {}; if (!isObject(data)) { + if (options.throwOnFailure) { + throw PaginationParseError.inputInvalid(); + } + return finalizePagination(pagination, options); } @@ -58,6 +67,8 @@ export function parseQueryPagination( if (!Number.isNaN(limit) && limit > 0) { pagination.limit = limit; + } else if (options.throwOnFailure) { + throw PaginationParseError.keyValueInvalid('limit'); } } @@ -66,6 +77,8 @@ export function parseQueryPagination( if (!Number.isNaN(offset) && offset >= 0) { pagination.offset = offset; + } else if (options.throwOnFailure) { + throw PaginationParseError.keyValueInvalid('offset'); } } diff --git a/src/parameter/pagination/type.ts b/src/parameter/pagination/type.ts index 2d232133..700ea9ba 100644 --- a/src/parameter/pagination/type.ts +++ b/src/parameter/pagination/type.ts @@ -20,6 +20,7 @@ export type PaginationBuildInput = { export type PaginationParseOptions = { maxLimit?: number + throwOnFailure?: boolean }; export type PaginationParseOutput = { diff --git a/src/parameter/relations/errors/build.ts b/src/parameter/relations/errors/build.ts new file mode 100644 index 00000000..ab2d9fa9 --- /dev/null +++ b/src/parameter/relations/errors/build.ts @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { BuildError } from '../../../errors'; + +export class RelationsBuildError extends BuildError { + +} diff --git a/src/parameter/relations/errors/index.ts b/src/parameter/relations/errors/index.ts new file mode 100644 index 00000000..e28a8539 --- /dev/null +++ b/src/parameter/relations/errors/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +export * from './build'; +export * from './parse'; diff --git a/src/parameter/relations/errors/parse.ts b/src/parameter/relations/errors/parse.ts new file mode 100644 index 00000000..14d1e10d --- /dev/null +++ b/src/parameter/relations/errors/parse.ts @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { ParseError } from '../../../errors'; + +export class RelationsParseError extends ParseError { + +} diff --git a/src/parameter/relations/index.ts b/src/parameter/relations/index.ts index f63fb93e..f05d360f 100644 --- a/src/parameter/relations/index.ts +++ b/src/parameter/relations/index.ts @@ -6,5 +6,6 @@ */ export * from './build'; +export * from './errors'; export * from './parse'; export * from './type'; diff --git a/src/parameter/relations/parse.ts b/src/parameter/relations/parse.ts index 20060e45..395586cc 100644 --- a/src/parameter/relations/parse.ts +++ b/src/parameter/relations/parse.ts @@ -8,6 +8,7 @@ import type { ObjectLiteral } from '../../type'; import { applyMapping, hasOwnProperty } from '../../utils'; import { isPathCoveredByParseAllowedOption } from '../utils'; +import { RelationsParseError } from './errors'; import type { RelationsParseOptions, RelationsParseOutput } from './type'; import { includeParents, isValidRelationPath } from './utils'; @@ -42,8 +43,12 @@ export function parseQueryRelations( for (let i = 0; i < input.length; i++) { if (typeof input[i] === 'string') { items.push(input[i]); + } else { + throw RelationsParseError.inputInvalid(); } } + } else if (options.throwOnFailure) { + throw RelationsParseError.inputInvalid(); } if (items.length === 0) { @@ -57,10 +62,21 @@ export function parseQueryRelations( } } - if (options.allowed) { - items = items.filter((item) => isPathCoveredByParseAllowedOption(options.allowed as string[], item)); - } else { - items = items.filter((item) => isValidRelationPath(item)); + for (let j = items.length - 1; j >= 0; j--) { + let isValid : boolean; + if (options.allowed) { + isValid = isPathCoveredByParseAllowedOption(options.allowed as string[], items[j]); + } else { + isValid = isValidRelationPath(items[j]); + } + + if (!isValid) { + if (options.throwOnFailure) { + throw RelationsParseError.keyInvalid(items[j]); + } + + items.splice(j, 1); + } } if (options.includeParents) { diff --git a/src/parameter/relations/type.ts b/src/parameter/relations/type.ts index 3e7f8f0e..35207a92 100644 --- a/src/parameter/relations/type.ts +++ b/src/parameter/relations/type.ts @@ -29,7 +29,8 @@ export type RelationsParseOptions< mapping?: Record, // set alternate value for relation key. pathMapping?: Record, - includeParents?: boolean | string[] | string + includeParents?: boolean | string[] | string, + throwOnFailure?: boolean }; export type RelationsParseOutputElement = { diff --git a/src/parameter/sort/errors/build.ts b/src/parameter/sort/errors/build.ts new file mode 100644 index 00000000..c996d0c2 --- /dev/null +++ b/src/parameter/sort/errors/build.ts @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { BuildError } from '../../../errors'; + +export class SortBuildError extends BuildError { + +} diff --git a/src/parameter/sort/errors/index.ts b/src/parameter/sort/errors/index.ts new file mode 100644 index 00000000..e28a8539 --- /dev/null +++ b/src/parameter/sort/errors/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +export * from './build'; +export * from './parse'; diff --git a/src/parameter/sort/errors/parse.ts b/src/parameter/sort/errors/parse.ts new file mode 100644 index 00000000..2af6c1e0 --- /dev/null +++ b/src/parameter/sort/errors/parse.ts @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2023. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { ParseError } from '../../../errors'; + +export class SortParseError extends ParseError { + +} diff --git a/src/parameter/sort/index.ts b/src/parameter/sort/index.ts index f63fb93e..f05d360f 100644 --- a/src/parameter/sort/index.ts +++ b/src/parameter/sort/index.ts @@ -6,5 +6,6 @@ */ export * from './build'; +export * from './errors'; export * from './parse'; export * from './type'; diff --git a/src/parameter/sort/parse.ts b/src/parameter/sort/parse.ts index 5e4606e2..99fb5e6e 100644 --- a/src/parameter/sort/parse.ts +++ b/src/parameter/sort/parse.ts @@ -9,14 +9,17 @@ import { isObject } from 'smob'; import type { ObjectLiteral } from '../../type'; import { applyMapping, - buildFieldWithPath, buildKeyPath, flattenNestedObject, - getFieldDetails, - hasOwnProperty, isFieldNonRelational, - isFieldPathAllowedByRelations, + buildKeyPath, + buildKeyWithPath, + flattenNestedObject, + hasOwnProperty, + isPathAllowedByRelations, + parseKey, } from '../../utils'; import { isValidFieldName } from '../fields'; import type { ParseAllowedOption } from '../type'; import { flattenParseAllowedOption, isPathCoveredByParseAllowedOption } from '../utils'; +import { SortParseError } from './errors'; import type { SortParseOptions, @@ -45,7 +48,7 @@ function buildDefaultSortParseOutput( const keys = Object.keys(flatten); for (let i = 0; i < keys.length; i++) { - const fieldDetails = getFieldDetails(keys[i]); + const fieldDetails = parseKey(keys[i]); let path : string | undefined; if (fieldDetails.path) { @@ -94,6 +97,10 @@ export function parseQuerySort( !Array.isArray(data) && !isObject(data) ) { + if (options.throwOnFailure) { + throw SortParseError.inputInvalid(); + } + return buildDefaultSortParseOutput(options); } @@ -115,10 +122,7 @@ export function parseQuerySort( parts = data.filter((item) => typeof item === 'string'); } - if ( - typeof data === 'object' && - data !== null - ) { + if (isObject(data)) { const keys = Object.keys(data); for (let i = 0; i < keys.length; i++) { /* istanbul ignore next */ @@ -126,7 +130,13 @@ export function parseQuerySort( !hasOwnProperty(data, keys[i]) || typeof keys[i] !== 'string' || typeof data[keys[i]] !== 'string' - ) continue; + ) { + if (options.throwOnFailure) { + throw SortParseError.keyValueInvalid(keys[i]); + } + + continue; + } const fieldPrefix = (data[keys[i]] as string) .toLowerCase() === 'desc' ? '-' : ''; @@ -145,29 +155,40 @@ export function parseQuerySort( const key: string = applyMapping(parts[i], options.mapping); - const fieldDetails = getFieldDetails(key); + const fieldDetails = parseKey(key); if ( typeof options.allowed === 'undefined' && !isValidFieldName(fieldDetails.name) ) { + if (options.throwOnFailure) { + throw SortParseError.keyInvalid(fieldDetails.name); + } + continue; } if ( - !isFieldPathAllowedByRelations(fieldDetails, options.relations) && - !isFieldNonRelational(fieldDetails) + !isPathAllowedByRelations(fieldDetails.path, options.relations) && + typeof fieldDetails.path !== 'undefined' ) { + if (options.throwOnFailure) { + throw SortParseError.keyPathInvalid(fieldDetails.path); + } + continue; } - const keyWithAlias : string = buildFieldWithPath(fieldDetails); - + const keyWithAlias = buildKeyWithPath(fieldDetails); if ( typeof options.allowed !== 'undefined' && !isMultiDimensionalArray(options.allowed) && !isPathCoveredByParseAllowedOption(options.allowed, [key, keyWithAlias]) ) { + if (options.throwOnFailure) { + throw SortParseError.keyNotAllowed(fieldDetails.name); + } + continue; } diff --git a/src/parameter/sort/type.ts b/src/parameter/sort/type.ts index 0bc02338..e66e7f66 100644 --- a/src/parameter/sort/type.ts +++ b/src/parameter/sort/type.ts @@ -62,6 +62,7 @@ export type SortParseOptions< mapping?: Record, default?: SortParseDefaultOption, defaultPath?: string, + throwOnFailure?: boolean, relations?: RelationsParseOutput, }; export type SortParseOutputElement = { diff --git a/src/parse/module.ts b/src/parse/module.ts index e3295e5f..6122faa3 100644 --- a/src/parse/module.ts +++ b/src/parse/module.ts @@ -23,11 +23,18 @@ export function parseQuery( ) : ParseOutput { options = options || {}; - const mergeWithGlobalOptions = (data?: T) : T => { + const mergeWithGlobalOptions = (data?: T) : T => { if (typeof data !== 'undefined') { - if (options.defaultPath) { + if (typeof data.defaultPath === 'undefined') { data.defaultPath = options.defaultPath; } + + if (typeof data.throwOnError === 'undefined') { + data.throwOnError = options.throwOnFailure; + } } return data || {} as T; diff --git a/src/parse/type.ts b/src/parse/type.ts index b9a1aa08..25f538ea 100644 --- a/src/parse/type.ts +++ b/src/parse/type.ts @@ -24,7 +24,8 @@ export type ParseOptions = { */ [P in `${Parameter}`]?: boolean | ParseParameterOptions } & { - defaultPath?: string + defaultPath?: string, + throwOnFailure?: boolean }; //------------------------------------------------ diff --git a/src/utils/field.ts b/src/utils/field.ts deleted file mode 100644 index 39dde5c2..00000000 --- a/src/utils/field.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (c) 2022. - * Author Peter Placzek (tada5hi) - * For the full copyright and license information, - * view the LICENSE file that was distributed with this source code. - */ - -import type { FieldDetails } from './type'; - -export function getFieldDetails(field: string) : FieldDetails { - const parts : string[] = field.split('.'); - - return { - name: parts.pop() as string, - path: parts.length > 0 ? parts.join('.') : undefined, - }; -} diff --git a/src/utils/index.ts b/src/utils/index.ts index 1a4800fa..4ea5834c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,11 +7,9 @@ export * from './array'; export * from './mapping'; -export * from './field'; export * from './key'; export * from './merge'; export * from './relation'; export * from './object'; -export * from './simple'; export * from './type'; export * from './url'; diff --git a/src/utils/key.ts b/src/utils/key.ts index 71092b11..064f9162 100644 --- a/src/utils/key.ts +++ b/src/utils/key.ts @@ -1,7 +1,21 @@ -export function buildKeyWithPrefix(name: string, prefix?: string) { - if (prefix) { - return `${prefix}.${name}`; - } +/* + * Copyright (c) 2022. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ - return name; +import type { KeyDetails } from './type'; + +export function parseKey( + field: string, +) : KeyDetails { + const parts : string[] = field.split('.'); + + const name = parts.pop() as string; + + return { + name, + path: parts.length > 0 ? parts.join('.') : undefined, + }; } diff --git a/src/utils/mapping.ts b/src/utils/mapping.ts index ab2f1391..6935b414 100644 --- a/src/utils/mapping.ts +++ b/src/utils/mapping.ts @@ -16,6 +16,11 @@ export function applyMapping( return name; } + const keys = Object.keys(map); + if (keys.length === 0) { + return name; + } + let parts = name.split('.'); const output = []; diff --git a/src/utils/object.ts b/src/utils/object.ts index 0e917b49..2d571a76 100644 --- a/src/utils/object.ts +++ b/src/utils/object.ts @@ -5,6 +5,13 @@ * view the LICENSE file that was distributed with this source code. */ +export function isObject(item: unknown) : item is Record { + return ( + !!item && + typeof item === 'object' && + !Array.isArray(item) + ); +} export function hasOwnProperty< X extends Record, Y extends PropertyKey>(obj: X, prop: Y): obj is X & Record { diff --git a/src/utils/relation.ts b/src/utils/relation.ts index 121c3ee0..619e4a96 100644 --- a/src/utils/relation.ts +++ b/src/utils/relation.ts @@ -5,48 +5,38 @@ * view the LICENSE file that was distributed with this source code. */ +import { isObject } from 'smob'; import type { RelationsParseOutput } from '../parameter'; -import { getFieldDetails } from './field'; -import type { FieldDetails } from './type'; +import type { KeyDetails } from './type'; -export function isFieldNonRelational(field: string | FieldDetails) { - const details = typeof field === 'string' ? - getFieldDetails(field) : - field; - - return typeof details.path === 'undefined'; -} - -export function isFieldPathAllowedByRelations( - field: string | Pick, +export function isPathAllowedByRelations( + path?: string, includes?: RelationsParseOutput, ) : boolean { - if (typeof includes === 'undefined') { - return true; - } - - const details : Pick = typeof field === 'string' ? - getFieldDetails(field) : - field; - - if ( - typeof details.path === 'undefined' - ) { + if (typeof path === 'undefined' || typeof includes === 'undefined') { return true; } return includes.some( - (include) => include.key === details.path, + (include) => include.key === path, ); } -export function buildFieldWithPath( - field: string | FieldDetails, +export function buildKeyWithPath(input: KeyDetails) : string; +export function buildKeyWithPath(key: string, path: string): string; +export function buildKeyWithPath( + name: string | KeyDetails, path?: string, ) : string { - const details = typeof field === 'string' ? - getFieldDetails(field) : - field; + let details : KeyDetails; + if (isObject(name)) { + details = name; + } else { + details = { + name, + path, + }; + } return details.path || path ? `${details.path || path}.${details.name}` : diff --git a/src/utils/simple.ts b/src/utils/simple.ts deleted file mode 100644 index 60f790d0..00000000 --- a/src/utils/simple.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2022-2022. - * Author Peter Placzek (tada5hi) - * For the full copyright and license information, - * view the LICENSE file that was distributed with this source code. - */ - -export function isSimpleValue(value: unknown, options?: { - withNull?: boolean, - withUndefined?: boolean -}) { - if ( - typeof value === 'string' || - typeof value === 'boolean' || - typeof value === 'number' - ) { - return true; - } - - options = options || {}; - if (options.withNull) { - if (value === null) { - return true; - } - } - - if (options.withUndefined) { - if (typeof value === 'undefined') { - return true; - } - } - - return false; -} diff --git a/src/utils/type.ts b/src/utils/type.ts index 2e3c6d04..337e382c 100644 --- a/src/utils/type.ts +++ b/src/utils/type.ts @@ -5,7 +5,7 @@ * view the LICENSE file that was distributed with this source code. */ -export type FieldDetails = { +export type KeyDetails = { name: string, path?: string }; diff --git a/test/unit/fields.spec.ts b/test/unit/fields.spec.ts index 0429dbb5..d434f1e1 100644 --- a/test/unit/fields.spec.ts +++ b/test/unit/fields.spec.ts @@ -8,6 +8,7 @@ import { buildFieldDomainRecords, DEFAULT_ID, + FieldsParseError, FieldsParseOptions, FieldsParseOutput, parseQueryFields, @@ -200,22 +201,32 @@ describe('src/fields/index.ts', () => { // field with invalid value data = parseQueryFields({ id: null }, options); expect(data).toEqual([{ key: 'id' }, { key: 'name' }] as FieldsParseOutput); + }); + it('should parse with single allowed domain', () => { // if only one domain is given, try to parse request field to single domain. - data = parseQueryFields(['id'], { allowed: { domain: ['id'] } }); + const data = parseQueryFields(['id'], { allowed: { domain: ['id'] } }); expect(data).toEqual([{ path: 'domain', key: 'id' }] as FieldsParseOutput); + }); + it('should parse with multiple allowed domains', () => { // if multiple possibilities are available for request field, use allowed - data = parseQueryFields(['id'], { allowed: { domain: ['id', 'name'], domain2: ['id', 'name'] } }); + const data = parseQueryFields(['id'], { + allowed: { + domain: ['id', 'name'], + domain2: ['id', 'name'] + } + }); expect(data).toEqual([ { path: 'domain', key: 'id' }, - { path: 'domain', key: 'name' }, { path: 'domain2', key: 'id' }, { path: 'domain2', key: 'name' }, ] as FieldsParseOutput); + }); + it('should use default fields if default & allowed are set', () => { // if multiple possibilities are available for request field, use default - data = parseQueryFields>(['id'], { + const data = parseQueryFields>(['id'], { allowed: { domain: ['id', 'name'], domain2: ['id', 'name'] }, default: { domain: ['id'], domain2: ['name']} }); @@ -223,9 +234,9 @@ describe('src/fields/index.ts', () => { { path: 'domain', key: 'id' }, { path: 'domain2', key: 'name' }, ] as FieldsParseOutput); - }); + }) - it('should transform fields with defaults', () => { + it('should parse with defaults', () => { let data = parseQueryFields([], { default: ['id', 'name'] }); expect(data).toEqual([{ key: 'id' }, { key: 'name'}] as FieldsParseOutput); @@ -247,17 +258,18 @@ describe('src/fields/index.ts', () => { it('should transform fields with aliasMapping', () => { let data = parseQueryFields('+alias', { - allowed: ['id'], + allowed: ['id', 'name'], mapping: { path: 'id' } }); expect(data).toEqual([ - {key: 'id'} + {key: 'id'}, + {key: 'name'} ] as FieldsParseOutput); data = parseQueryFields('+alias', { - allowed: ['id'], + allowed: ['id', 'name'], mapping: { alias: 'id' } @@ -267,7 +279,7 @@ describe('src/fields/index.ts', () => { ] as FieldsParseOutput); }) - it('should transform fields with includes', () => { + it('should parse with includes', () => { const includes = parseQueryRelations(['profile', 'roles'], { allowed: ['user', 'profile'] }); // simple domain match @@ -278,4 +290,58 @@ describe('src/fields/index.ts', () => { data = parseQueryFields({ profile: ['id'], permissions: ['id'] }, { allowed: { profile: ['id'], permissions: ['id'] }, relations: includes }); expect(data).toEqual([{ path: 'profile', key: 'id' }] as FieldsParseOutput); }); + + it('should throw on invalid input shape', () => { + let options : FieldsParseOptions = { + throwOnFailure: true + } + + let error = FieldsParseError.inputInvalid(); + let evaluate = () => { + parseQueryFields(false, options); + } + expect(evaluate).toThrowError(error); + }); + + it('should throw on non allowed relation', () => { + let options : FieldsParseOptions = { + throwOnFailure: true, + allowed: ['user.foo'], + relations: [ + { + key: 'user', + value: 'user' + } + ] + } + + let error = FieldsParseError.keyPathInvalid('bar'); + let evaluate = () => { + parseQueryFields({ + 'bar': ['bar'] + }, options); + } + expect(evaluate).toThrowError(error); + }); + + it('should throw on invalid key', () => { + let options : FieldsParseOptions = { + throwOnFailure: true + }; + + let t = () => { + return parseQueryFields(['!.bar'], options) + } + + expect(t).toThrow(FieldsParseError); + + options.allowed = ['id', 'name', 'email']; + options.defaultPath = 'user'; + + t = () => { + return parseQueryFields(['baz'], options) + } + + expect(t).toThrow(FieldsParseError); + }) }); diff --git a/test/unit/filters.spec.ts b/test/unit/filters.spec.ts index 80ad4314..1a546035 100644 --- a/test/unit/filters.spec.ts +++ b/test/unit/filters.spec.ts @@ -9,7 +9,7 @@ import { FiltersParseOptions, FiltersParseOutput, parseQueryFilters, - parseQueryRelations, FilterComparisonOperator, + parseQueryRelations, FilterComparisonOperator, FiltersParseError, } from '../../src'; describe('src/filter/index.ts', () => { @@ -437,4 +437,101 @@ describe('src/filter/index.ts', () => { }, ] as FiltersParseOutput); }); + + it('should throw on invalid input shape', () => { + let options : FiltersParseOptions = { + throwOnFailure: true + } + + let error = FiltersParseError.inputInvalid(); + let evaluate = () => { + parseQueryFilters('foo', options); + } + expect(evaluate).toThrowError(error); + }); + + it('should throw on invalid key value', () => { + let options : FiltersParseOptions = { + throwOnFailure: true + } + + let error = FiltersParseError.keyValueInvalid('foo'); + let evaluate = () => { + parseQueryFilters({ + foo: new Buffer('foo'), + }, options); + } + expect(evaluate).toThrowError(error); + }); + + it('should throw on invalid key', () => { + let options : FiltersParseOptions = { + throwOnFailure: true + } + + let error = FiltersParseError.keyInvalid('1foo'); + let evaluate = () => { + parseQueryFilters({ + '1foo': 1 + }, options); + } + expect(evaluate).toThrowError(error); + }); + + it('should throw on non allowed relation', () => { + let options : FiltersParseOptions = { + throwOnFailure: true, + allowed: ['user.foo'], + relations: [ + { + key: 'user', + value: 'user' + } + ] + } + + let error = FiltersParseError.keyPathInvalid('bar'); + let evaluate = () => { + parseQueryFilters({ + 'bar.bar': 1 + }, options); + } + expect(evaluate).toThrowError(error); + }); + + it('should throw on non allowed key which is not covered by a relation', () => { + let options : FiltersParseOptions = { + throwOnFailure: true, + allowed: ['user.foo'], + relations: [ + { + key: 'user', + value: 'user' + } + ] + } + + let error = FiltersParseError.keyInvalid('bar'); + let evaluate = () => { + parseQueryFilters({ + 'user.bar': 1 + }, options); + } + expect(evaluate).toThrowError(error); + }); + + it('should throw on non allowed key', () => { + let options : FiltersParseOptions = { + throwOnFailure: true, + allowed: ['foo'] + } + + let error = FiltersParseError.keyInvalid('bar'); + let evaluate = () => { + parseQueryFilters({ + bar: 1 + }, options); + } + expect(evaluate).toThrowError(error); + }); }); diff --git a/test/unit/pagination.spec.ts b/test/unit/pagination.spec.ts index 8c619630..f6e87a64 100644 --- a/test/unit/pagination.spec.ts +++ b/test/unit/pagination.spec.ts @@ -5,7 +5,7 @@ * view the LICENSE file that was distributed with this source code. */ -import { parseQueryPagination } from '../../src'; +import {PaginationParseError, PaginationParseOptions, parseQueryPagination} from '../../src'; describe('src/pagination/index.ts', () => { it('should transform pagination', () => { @@ -24,4 +24,57 @@ describe('src/pagination/index.ts', () => { pagination = parseQueryPagination({ offset: 20, limit: 20 }, { maxLimit: 50 }); expect(pagination).toEqual({ offset: 20, limit: 20 }); }); + + it('should throw on exceeded limit', () => { + let options : PaginationParseOptions = { + throwOnFailure: true, + maxLimit: 50 + }; + + let evaluate = () => { + parseQueryPagination({limit: 100}, options); + } + + const error = PaginationParseError.limitExceeded(50); + expect(evaluate).toThrowError(error); + }) + + it('should throw on invalid input', () => { + let options : PaginationParseOptions = { + throwOnFailure: true + }; + + let evaluate = () => { + parseQueryPagination(false, options); + } + + const error = PaginationParseError.inputInvalid(); + expect(evaluate).toThrowError(error); + }); + + it('should throw on invalid limit', () => { + let options : PaginationParseOptions = { + throwOnFailure: true + }; + + let evaluate = () => { + parseQueryPagination({limit: false}, options); + } + + const error = PaginationParseError.keyValueInvalid('limit'); + expect(evaluate).toThrowError(error); + }) + + it('should throw on invalid offset', () => { + let options : PaginationParseOptions = { + throwOnFailure: true + }; + + let evaluate = () => { + parseQueryPagination({offset: false}, options); + } + + const error = PaginationParseError.keyValueInvalid('offset'); + expect(evaluate).toThrowError(error); + }) }); diff --git a/test/unit/relations.spec.ts b/test/unit/relations.spec.ts index 16aa5916..a2306519 100644 --- a/test/unit/relations.spec.ts +++ b/test/unit/relations.spec.ts @@ -5,88 +5,142 @@ * view the LICENSE file that was distributed with this source code. */ -import { RelationsParseOutput, parseQueryRelations } from '../../src'; +import {RelationsParseOutput, parseQueryRelations, RelationsParseOptions, RelationsParseError} from '../../src'; describe('src/relations/index.ts', () => { - it('should transform request relations', () => { - // single data matching - let allowed = parseQueryRelations('profile', { allowed: ['profile'] }); - expect(allowed).toEqual([ - { key: 'profile', value: 'profile' }, + it('should parse simple relations', () => { + let output = parseQueryRelations('profile', {allowed: ['profile']}); + expect(output).toEqual([ + {key: 'profile', value: 'profile'}, ] as RelationsParseOutput); - allowed = parseQueryRelations([], { allowed: ['profile'] }); - expect(allowed).toEqual([]); + output = parseQueryRelations([], {allowed: ['profile']}); + expect(output).toEqual([]); + + }) + it('should parse with invalid path', () => { // invalid path - allowed = parseQueryRelations(['profile!']); - expect(allowed).toEqual([]); + let output = parseQueryRelations(['profile!']); + expect(output).toEqual([]); + + }) + it('should parse ignore path pattern, if permitted by allowed key', () => { // ignore path pattern, if permitted by allowed key - allowed = parseQueryRelations(['profile!'], {allowed: ['profile!']}); - expect(allowed).toEqual([ - { key: 'profile!', value: 'profile!'} + let output = parseQueryRelations(['profile!'], {allowed: ['profile!']}); + expect(output).toEqual([ + {key: 'profile!', value: 'profile!'} ] as RelationsParseOutput); + }); + it('should parse with alias', () => { // with alias - allowed = parseQueryRelations('pro', { mapping: { pro: 'profile' }, allowed: ['profile'] }); - expect(allowed).toEqual([ - { key: 'profile', value: 'profile' }, + let output = parseQueryRelations('pro', {mapping: {pro: 'profile'}, allowed: ['profile']}); + expect(output).toEqual([ + {key: 'profile', value: 'profile'}, ]); + }); + it('should parse with nested alias', () => { // with nested alias - allowed = parseQueryRelations(['abc.photos'], { + let output = parseQueryRelations(['abc.photos'], { allowed: ['profile.photos'], mapping: { 'abc.photos': 'profile.photos' }, }); - expect(allowed).toEqual([ + expect(output).toEqual([ { key: 'profile', value: 'profile' }, { key: 'profile.photos', value: 'photos' }, ] as RelationsParseOutput); // with nested alias & includeParents - allowed = parseQueryRelations(['abc.photos'], { + output = parseQueryRelations(['abc.photos'], { allowed: ['profile.photos'], mapping: { 'abc.photos': 'profile.photos' }, includeParents: false, }); - expect(allowed).toEqual([ + expect(output).toEqual([ { key: 'profile.photos', value: 'photos' }, ] as RelationsParseOutput); // with nested alias & limited includeParents ( no user_roles rel) - allowed = parseQueryRelations(['abc.photos', 'user_roles.role'], { + output = parseQueryRelations(['abc.photos', 'user_roles.role'], { allowed: ['profile.photos', 'user_roles.role'], mapping: { 'abc.photos': 'profile.photos' }, includeParents: ['profile'], }); - expect(allowed).toEqual([ + expect(output).toEqual([ { key: 'profile', value: 'profile' }, { key: 'profile.photos', value: 'photos' }, { key: 'user_roles.role', value: 'role' }, ] as RelationsParseOutput); // multiple data matching - allowed = parseQueryRelations(['profile', 'abc'], { allowed: ['profile'] }); - expect(allowed).toEqual([{ key: 'profile', value: 'profile' }] as RelationsParseOutput); + output = parseQueryRelations(['profile', 'abc'], { allowed: ['profile'] }); + expect(output).toEqual([{ key: 'profile', value: 'profile' }] as RelationsParseOutput); // no allowed - allowed = parseQueryRelations(['profile'], { allowed: [] }); - expect(allowed).toEqual([] as RelationsParseOutput); + output = parseQueryRelations(['profile'], { allowed: [] }); + expect(output).toEqual([] as RelationsParseOutput); // non array, permit everything - allowed = parseQueryRelations(['profile'], { allowed: undefined }); - expect(allowed).toEqual([{key: 'profile', value: 'profile'}] as RelationsParseOutput); + output = parseQueryRelations(['profile'], { allowed: undefined }); + expect(output).toEqual([{key: 'profile', value: 'profile'}] as RelationsParseOutput); // nested data with alias - allowed = parseQueryRelations(['profile.photos', 'profile.photos.abc', 'profile.abc'], { allowed: ['profile.photos'] }); - expect(allowed).toEqual([ + output = parseQueryRelations(['profile.photos', 'profile.photos.abc', 'profile.abc'], { allowed: ['profile.photos'] }); + expect(output).toEqual([ { key: 'profile', value: 'profile' }, { key: 'profile.photos', value: 'photos' }, ] as RelationsParseOutput); // null data - allowed = parseQueryRelations(null); - expect(allowed).toEqual([]); + output = parseQueryRelations(null); + expect(output).toEqual([]); + }); + + it('should throw on invalid input', () => { + let options : RelationsParseOptions = { + throwOnFailure: true + }; + + let error = RelationsParseError.inputInvalid(); + + let evaluate = () => { + parseQueryRelations(['foo', true], options); + } + expect(evaluate).toThrowError(error); + + evaluate = () => { + parseQueryRelations(false, options); + } + expect(evaluate).toThrowError(error); }); + + it('should throw on non allowed key', () => { + let options : RelationsParseOptions = { + throwOnFailure: true, + allowed: ['foo'] + }; + + let error = RelationsParseError.keyInvalid('bar'); + + let evaluate = () => { + parseQueryRelations(['foo', 'bar'], options); + } + expect(evaluate).toThrowError(error); + }); + + it('should throw on invalid key', () => { + let options : RelationsParseOptions = { + throwOnFailure: true + }; + + let error = RelationsParseError.keyInvalid(',foo'); + + let evaluate = () => { + parseQueryRelations([',foo'], options); + } + expect(evaluate).toThrowError(error); + }) }); diff --git a/test/unit/sort.spec.ts b/test/unit/sort.spec.ts index 09c17d4a..70517c54 100644 --- a/test/unit/sort.spec.ts +++ b/test/unit/sort.spec.ts @@ -12,6 +12,7 @@ import { parseQueryRelations, parseQuerySort, FieldsParseOutput, + SortParseError, } from '../../src'; import {User} from "../data"; @@ -177,4 +178,107 @@ describe('src/sort/index.ts', () => { { path: 'user_roles.role', key: 'id', value: SortDirection.ASC }, ] as SortParseOutput); }); + + it('should throw on invalid input', () => { + let options : SortParseOptions = { + throwOnFailure: true, + }; + + let evaluate = () => { + parseQuerySort(false, options); + } + + let error = SortParseError.inputInvalid(); + expect(evaluate).toThrowError(error); + }) + + it('should throw on invalid key', () => { + let options : SortParseOptions = { + throwOnFailure: true + } + + let evaluate = () => { + parseQuerySort({ + '1foo': 'desc' + }, options); + } + let error = SortParseError.keyInvalid('1foo'); + expect(evaluate).toThrowError(error); + }); + + it('should throw on non allowed relation', () => { + let options : SortParseOptions = { + throwOnFailure: true, + allowed: ['user.foo'], + relations: [ + { + key: 'user', + value: 'user' + } + ] + } + + let evaluate = () => { + parseQuerySort({ + 'bar.bar': 'desc' + }, options); + } + + let error = SortParseError.keyPathInvalid('bar'); + expect(evaluate).toThrowError(error); + }); + + it('should throw on non allowed key which is not covered by a relation', () => { + let options : SortParseOptions = { + throwOnFailure: true, + allowed: ['user.foo'], + relations: [ + { + key: 'user', + value: 'user' + } + ] + } + + let evaluate = () => { + parseQuerySort({ + 'user.bar': 'desc' + }, options); + } + + let error = SortParseError.keyNotAllowed('bar'); + expect(evaluate).toThrowError(error); + }); + + it('should throw on invalid key value', () => { + let options : SortParseOptions = { + throwOnFailure: true, + allowed: ['foo'] + } + + let evaluate = () => { + parseQuerySort({ + bar: 1 + }, options); + } + + let error = SortParseError.keyValueInvalid('bar'); + expect(evaluate).toThrowError(error); + }); + + it('should throw on non allowed key', () => { + let options : SortParseOptions = { + throwOnFailure: true, + allowed: ['foo'] + } + + let evaluate = () => { + parseQuerySort({ + bar: 'desc' + }, options); + } + + let error = SortParseError.keyNotAllowed('bar'); + expect(evaluate).toThrowError(error); + }); });