Skip to content

Commit

Permalink
feat: allow throwing error on invalid parsing input (#312)
Browse files Browse the repository at this point in the history
* feat: allow throwing error on invalid parsing input

* feat: rename option throwOnError to throwOnFailure
  • Loading branch information
tada5hi committed Jul 4, 2023
1 parent 6b6826f commit 3afd7f2
Show file tree
Hide file tree
Showing 54 changed files with 976 additions and 269 deletions.
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
},
"homepage": "https://github.com/Tada5hi/rapiq#readme",
"dependencies": {
"ebec": "^1.1.0",
"smob": "^1.4.0"
},
"devDependencies": {
Expand Down
15 changes: 15 additions & 0 deletions src/errors/base.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
20 changes: 20 additions & 0 deletions src/errors/build.ts
Original file line number Diff line number Diff line change
@@ -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.');
}
}
20 changes: 20 additions & 0 deletions src/errors/code.ts
Original file line number Diff line number Diff line change
@@ -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',
}
11 changes: 11 additions & 0 deletions src/errors/index.ts
Original file line number Diff line number Diff line change
@@ -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';
56 changes: 56 additions & 0 deletions src/errors/parse.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

export * from './build';
export * from './errors';
export * from './parameter';
export * from './parse';
export * from './constants';
Expand Down
12 changes: 12 additions & 0 deletions src/parameter/fields/errors/build.ts
Original file line number Diff line number Diff line change
@@ -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 {

}
9 changes: 9 additions & 0 deletions src/parameter/fields/errors/index.ts
Original file line number Diff line number Diff line change
@@ -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';
12 changes: 12 additions & 0 deletions src/parameter/fields/errors/parse.ts
Original file line number Diff line number Diff line change
@@ -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 {

}
1 change: 1 addition & 0 deletions src/parameter/fields/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

export * from './build';
export * from './constants';
export * from './errors';
export * from './parse';
export * from './type';
export * from './utils';
103 changes: 64 additions & 39 deletions src/parameter/fields/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

// --------------------------------------------------

Expand Down Expand Up @@ -59,10 +55,7 @@ export function parseQueryFields<T extends ObjectLiteral = ObjectLiteral>(

// 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 [];
Expand All @@ -74,17 +67,24 @@ export function parseQueryFields<T extends ObjectLiteral = ObjectLiteral>(

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 = [];
Expand All @@ -93,9 +93,13 @@ export function parseQueryFields<T extends ObjectLiteral = ObjectLiteral>(
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;
}

Expand All @@ -110,32 +114,53 @@ export function parseQueryFields<T extends ObjectLiteral = ObjectLiteral>(
fields = parseFieldsInput(data[reverseMapping[path]]);
}

let transformed : FieldsInputTransformed = {
const transformed : FieldsInputTransformed = {
default: [],
included: [],
excluded: [],
};

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 (
Expand Down
1 change: 1 addition & 0 deletions src/parameter/fields/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export type FieldsParseOptions<
allowed?: ParseAllowedOption<T>,
default?: ParseAllowedOption<T>,
defaultPath?: string,
throwOnFailure?: boolean,
relations?: RelationsParseOutput,
};

Expand Down
Loading

0 comments on commit 3afd7f2

Please sign in to comment.