NestJs: How can I add options for my Custom Validation Pipe? - javascript

I created a custom validation pipe where I want to add some custom logic.I want to know how can I extend it so that I can call it with the below options
Edited:
I have referred the below link of the github file and have developed my own pipe but there still seems to be something missing as its not validating as per the DTO
https://github.com/nestjs/nest/blob/1f6fca5f55e9e51705fa326654760736b254f4e5/packages/common/pipes/validation.pipe.ts#L48

I was able to solve it by creating my own pipe.
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException, Logger, Optional } from '#nestjs/common';
import { ValidatorOptions, ValidationError, isObject, validate } from 'class-validator';
import { plainToClass, classToPlain, ClassTransformOptions } from 'class-transformer';
import { iterate } from 'iterare';
const isUndefined = (obj: any): obj is undefined =>
typeof obj === 'undefined';
const isNil = (val: any): val is null | undefined =>
isUndefined(val) || val === null;
interface ValidationPipeOptions extends ValidatorOptions {
transform?: boolean;
transformOptions?: ClassTransformOptions;
validateCustomDecorators?: boolean;
}
#Injectable()
export class ValidationPipe implements PipeTransform<any> {
private readonly logger = new Logger(ValidationPipe.name);
protected isTransformEnabled: boolean;
protected transformOptions: ClassTransformOptions;
protected validatorOptions: ValidatorOptions;
protected validateCustomDecorators: boolean;
constructor(#Optional() options?: ValidationPipeOptions) {
options = options || {};
const {
transform,
transformOptions,
validateCustomDecorators,
...validatorOptions
} = options;
this.isTransformEnabled = !!transform;
this.transformOptions = transformOptions;
this.validatorOptions = validatorOptions;
this.validateCustomDecorators = validateCustomDecorators || false;
}
async transform(value: any, metadata: ArgumentMetadata) {
const metatype = metadata.metatype;
if (!metatype || !this.toValidate(metadata)) {
return this.isTransformEnabled
? this.transformPrimitive(value, metadata)
: value;
}
const originalValue = value;
value = this.toEmptyIfNil(value);
const isNil = value !== originalValue;
const isPrimitive = this.isPrimitive(value);
this.stripProtoKeys(value);
let object = plainToClass(metatype, value, this.transformOptions);
const originalEntity = object;
const isCtorNotEqual = object.constructor !== metatype;
if (isCtorNotEqual && !isPrimitive) {
object.constructor = metatype;
} else if (isCtorNotEqual) {
// when "entity" is a primitive value, we have to temporarily
// replace the entity to perform the validation against the original
// metatype defined inside the handler
object = { constructor: metatype };
}
const errors = await this.validate(object, this.validatorOptions);
if (errors.length > 0) {
for (let error of errors) {
for (let key in error.constraints) {
this.logger.error(`${error.target.constructor.name}:${error.constraints[key]}`);
}
}
throw new BadRequestException('Invalid Input Parameters');
}
if (isPrimitive) {
// if the value is a primitive value and the validation process has been successfully completed
// we have to revert the original value passed through the pipe
object = originalEntity;
}
if (this.isTransformEnabled) {
return object;
}
if (isNil) {
// if the value was originally undefined or null, revert it back
return originalValue;
}
return Object.keys(this.validatorOptions).length > 0
? classToPlain(object, this.transformOptions)
: value;
}
protected stripProtoKeys(value: Record<string, any>) {
delete value.__proto__;
const keys = Object.keys(value);
iterate(keys)
.filter(key => isObject(value[key]) && value[key])
.forEach(key => this.stripProtoKeys(value[key]));
}
protected isPrimitive(value: unknown): boolean {
return ['number', 'boolean', 'string'].includes(typeof value);
}
protected transformPrimitive(value: any, metadata: ArgumentMetadata) {
if (!metadata.data) {
// leave top-level query/param objects unmodified
return value;
}
const { type, metatype } = metadata;
if (type !== 'param' && type !== 'query') {
return value;
}
if (metatype === Boolean) {
return value === true || value === 'true';
}
if (metatype === Number) {
return +value;
}
return value;
}
protected toEmptyIfNil<T = any, R = any>(value: T): R | {} {
return isNil(value) ? {} : value;
}
private toValidate(metadata: ArgumentMetadata): boolean {
const { metatype, type } = metadata;
if (type === 'custom' && !this.validateCustomDecorators) {
return false;
}
const types = [String, Boolean, Number, Array, Object, Buffer];
return !types.some(t => metatype === t) && !isNil(metatype);
}
protected validate(
object: object,
validatorOptions?: ValidatorOptions,
): Promise<ValidationError[]> | ValidationError[] {
return validate(object, validatorOptions);
}
}

Related

How to write a generic function that creates an HTMLElement using typescript

I have a problem. I need to write a common function (class) that can both accept HTMLElements and give them using typescript
An example of attempts
class Create {
protected element: HTMLElement;
constructor(parent: ParentNode, tag: string, classNames: string, value?: string, attr?: Record<string, unknown>) {
this.element = document.createElement(tag);
this.element.classList.add(...classNames.split(' '));
if (typeof value === 'string') {
this.element.innerHTML = value;
}
if ('appendChild' in parent) {
parent.appendChild(this.element);
}
if (attr) {
for (const key in attr) {
this.element.setAttribute(key, <string>attr[key]);
}
}
}
append(element: ElementNode) {
this.element.append(element);
}
}
This function will be auxiliary to create elements on the page
example of available elements
const wrapper = new Create(this.container, 'div', styles.wrapper);
const headerWrapper = document.createElement('div') as HTMLDivElement;
const headerNav = document.createElement('nav');
const headerList = document.createElement('ul');
const headerLogo = document.createElement('div');
const headerLogoLink = document.createElement('a');
const headerLogoTitle = document.createElement('h2');
headerLogo.classList.add(styles.headerWrapperLogo);
headerLogoLink.classList.add(styles.headerWrapperLogoLink);
headerLogoLink.href = '/';
headerLogoTitle.classList.add(styles.headerWrapperLogoTitle);
headerLogoTitle.innerHTML = 'Manga Store';
headerLogo.append(headerLogoLink);
headerLogoLink.append(headerLogoTitle);
headerWrapper.append(headerLogo);
headerWrapper.classList.add(styles.headerWrapper);
headerNav.classList.add(styles.headerWrapperNav);
headerList.classList.add(styles.headerWrapperNavList);
btnHeader.forEach((btns) => {
const headerListItem = document.createElement('li');
const headerListItemLink = document.createElement('a') as HTMLAnchorElement;
headerListItem.classList.add(styles.headerWrapperNavListItem);
headerListItemLink.classList.add(styles.headerWrapperNavListItemLink);
headerListItemLink.href = `#${btns.id}`;
headerListItemLink.innerText = btns.content;
wrapper.append(headerWrapper);
headerWrapper.append(headerNav);
headerNav.append(headerList);
headerList.append(headerListItem);
headerListItem.append(headerListItemLink);
Or tell me if it is possible to rewrite the "Abstract" class (see example) to get the desired result
abstract class Template {
protected container: HTMLElement;
static content = {};
protected constructor(id: string, classNames?: string) {
this.container = document.createElement('main');
if (typeof classNames === 'string') {
this.container.classList.add(classNames);
}
this.container.id = id;
}
protected header(content: string) {
const title = document.createElement('h1');
title.innerText = content;
return title;
}
render() {
return this.container;
}
}
When I tried this code:
const wrapper = new Create(document.body, 'div', '1')
const subWrap = new Create(wrapper, 'div', '2') // ???
I got this error:
If you want Create's append method to support being passed a ParentNode or an instance of Create, then you just need to type that argument with a union.
For example:
constructor(
parent: ParentNode | Create, // changed this type
tag: string,
classNames: string,
value?: string,
attr?: Record<string, unknown>
) {
//...
}
See playground
export type Values = string | null | unknown;
export type ParentElem = ParentNode | Create | null;
class Create {
element: Element;
constructor(tag: string, classNames: string, parent?: ParentElem, value?: Values, attr?: Record<string, unknown>) {
this.element = document.createElement(tag);
this.element.classList.add(...classNames.split(' '));
if (typeof value === 'string') {
this.element.innerHTML = value;
}
parent ? (!(parent instanceof Create) ? parent.appendChild(this.element) : null) : undefined;
if (attr) {
for (const key in attr) {
this.element.setAttribute(key, <string>attr[key]);
}
}
}
append(element: Create | Node | string) {
return element ? (!(element instanceof Create) ? this.element.append(element) : null) : undefined;
}
remove(element = this.element) {
return element ? (!(element instanceof Create) ? this.element.remove() : null) : undefined;
}
}
ternary operators helped me, and a few checks, to protect against surprises in the form of undefind and null, I also created two additional methods for adding and removing elements, maybe it will come in handy for someone)

Typescript instanceof issue

I have the following code that works, but the moment I change the if to p instanceof Address the condition is never met.
p is an object but I want to check it against its type that has already been defined:
export default class Address {
public addressLine1: string = ''
public addressLine2: string | null = null
public town: string = ''
public postcode: string = ''
}
const displayResults = (list: any) => {
const slicedList = list.slice(0, 3)
const html =
slicedList.map((p: string | Address) => {
console.log('p: ', p instanceof Address)
if (typeof p === 'object') {
return <span key={`id-${uniqid()}`}>{addressToString(p)}</span>
}
return <span key={`id-${uniqid()}`}>{p}</span>
})
if (list.length > 3) {
html.push(elipsis)
}
return html
}

Why can not I re-use SessionStorage when updating its values?

I have a modal that is a word filter and every user digit in the input it traverses the values ​​in the storage, bringing me the results.
When the user clicks on this link, my algorithm removes the given item from the array and re-inserts the updated values ​​into the storage, but it starts to give error when clicking on the other items in the list found, as it complains about the position of the item removed in the vector ..
What should I do to get around this problem?
VM6332:1 Uncaught SyntaxError: Unexpected token . in JSON at position 0
at JSON.parse (<anonymous>)
at StorageController.get [as itemToJSON] (WebStorage.ts:31)
at StorageController.removeItemByIndexOf (WebStorage.ts:59)
at HTMLLIElement.eval (FilterData.ts:86)
WEBSTORAGE
export class StorageController {
protected readonly __typeStorage: string;
protected readonly __key: string;
public constructor(readonly typeStorage: string, readonly key: string) {
this.__typeStorage = typeStorage;
this.__key = key;
}
protected get storage(): Storage {
switch(this.__typeStorage) {
case 'local':
return localStorage;
case 'session':
return sessionStorage;
}
}
protected get item(): string {
return this.storage.getItem(this.__key);
}
protected get itemToJSON(): JSON {
return JSON.parse(this.item).sort();
}
protected get hasStorageData(): boolean {
return this.storage ? true : false;
}
protected createItem(_data: any): void {
this.storage.setItem(this.__key, _data);
}
protected filterItem(_data: string): Object {
let _arrItem = (<any>this.itemToJSON);
return _arrItem.filter((_value: string) => {
return _value.search(_data.toUpperCase()) !== -1;
});
}
protected filterItemByIndexOf(_data: string): string {
let _arrItem = (<any>this.itemToJSON),
itemIndexOf = (<any>this.itemToJSON).indexOf(_data.toUpperCase());
if (itemIndexOf !== -1) {
return _arrItem[itemIndexOf];
}
}
protected removeItemByIndexOf(_data: string): void {
let _arrItem = (<any>this.itemToJSON),
itemIndexOf = _arrItem.indexOf(_data.toUpperCase());
if (itemIndexOf !== -1) {
_arrItem.splice(itemIndexOf, 1);
this.createItem(_arrItem);
}
}
}
SELECT CONTROLLER
class SelectController {
// ATTRIBUTES OF THEIR CLASS
private _filteredData: any;
private _selectedData: any;
protected readonly __input: HTMLInputElement;
protected readonly __listbox: HTMLElement;
// UTILIZATION OF THE PROCEDURES MADE IN THE MAIN CLASS
private readonly _storageController: any;
private readonly _selectedController: any;
constructor(protected readonly input: string, protected readonly listbox: string, protected readonly storageController: Object, protected readonly selectedController: Object) {
this.__input = <HTMLInputElement>document.getElementById(input);
this.__listbox = document.getElementById(listbox);
this._storageController = storageController;
this._selectedController = selectedController;
}
protected set filteredData(_data: any) {
this._filteredData = _data;
}
protected get filteredData(): any {
return this._filteredData;
}
protected set selectedData(_data: string) {
this._selectedData = _data;
}
protected get selectedData(): string {
return this._selectedData;
}
protected get hasFilteredData(): boolean {
return this.filteredData && this.filteredData.length !== 0 && this.filteredData !== undefined && this.filteredData !== null ? true : false;
}
protected init(): void {
this.search();
this.renderList();
this.selectItem();
}
protected search(): void {
this.__input.addEventListener('keyup', (_event: KeyboardEvent) => {
let typexTextValue = (<any>_event.target).value,
typedTextSize = typexTextValue.length;
if (typedTextSize === 2) {
this.filteredData = this._storageController.filterItemByIndexOf(typexTextValue);
} else if (typedTextSize > 2) {
this.filteredData = this._storageController.filterItem(typexTextValue);
}
});
}
protected renderList(): void {
this.__input.addEventListener('keyup', () => {
HelpersMethods.cleanHTML(this.__listbox);
const HAS_LISTBOX_DATA = this.__listbox.hasChildNodes(),
HAS_FILTERED_DATA = this.hasFilteredData;
if (!HAS_LISTBOX_DATA && HAS_FILTERED_DATA) {
const FILTERED_DATA_IS_ARRAY = Array.isArray(this.filteredData);
FILTERED_DATA_IS_ARRAY ? this.filteredData.forEach((_value: string) => this.createHTML(_value)) : this.createHTML(this.filteredData);
}
});
}
protected createHTML(_value: string): void {
const CONVERTED_VALUE = HelpersMethods.convertID(_value);
const HTML_STRUCTURE =
`<li role="option" id="list-unselected-${CONVERTED_VALUE}" class="list-group-item">
<div class="d-flex justify-content-between">
<div class="flex-fill">
<label class="form-check-label" for="checkbox-${CONVERTED_VALUE}">${_value.toUpperCase()}</label>
</div>
<div>
<input type="checkbox" id="checkbox-${CONVERTED_VALUE}" class="form-check-input" value="${_value.toUpperCase()}" data-target="list-unselected-${CONVERTED_VALUE}">
</div>
</div>
</li>`;
this.__listbox.insertAdjacentHTML('beforeend', HTML_STRUCTURE);
}
protected selectItem(): void {
this.__listbox.addEventListener('click', () => {
this.__listbox.querySelectorAll('li').forEach((_li: HTMLElement) => {
_li.addEventListener('click', (_event: MouseEvent) => {
let elementTarget = (<any>_event).target,
elementTargetType = elementTarget.type,
elementTargetValue = elementTarget.value;
if(elementTargetType === 'checkbox') {
let elementTargetListID = elementTarget.getAttribute('data-target');
this.selectedData = elementTargetValue;
this._storageController.removeItemByIndexOf(elementTargetValue);
this._selectedController.renderList();
HelpersMethods.hideElement(elementTargetListID);
}
});
});
});
}
}
When you write back the data to storage, you don't stringify it:
this.createItem(_arrItem);
...which does:
this.storage.setItem(this.__key, _data);
You should do:
this.createItem(JSON.stringify(_arrItem));

Typescript limitation workaround

I have a code, that is totaly OK from the human perspective. But it looks like typescript type system have hard time to understand it. Is there a smart way to hint compiler that everything is fine in that line?
const isMustToRun: boolean = isFunc(condition) ? condition() : condition;
code:
export const noop = function() {
};
export const isFunc = function(obj: any): boolean {
return typeof obj === 'function';
};
/**
*
* #param func funtion to run
* #param condition condition check
* #param args
*/
export const runIf = function f(condition: (Function | boolean), func: Function, ...args: any[]) {
return () => {
const isMustToRun: boolean = isFunc(condition) ? condition() : condition;
return isMustToRun ? func(...args) : noop();
};
};
If I wrote
typeof condition === 'function'
instead of "isFunc" call, then it works. But I don't want to repeat the code..
Change the return type of
export const isFunc = function(obj: any): boolean {
return typeof obj === 'function';
};
to be is Function
export const isFunc = function(obj: any): obj is Function {
return typeof obj === 'function';
};
More
This is called a user defined type guard 🌹
You need to transform isFunc in a custom type guard and your code will work as expected.:
export const isFunc = function(obj: any): obj is Function {
return typeof obj === 'function';
};
I would recommend not using Function though as it is not really very type safe, you can tighten up the function type with a function signature, and use the Extract conditional type to preserve the actual type of the passed in function to the isFunc type-guard:
export const isFunc = function<T>(obj: T): obj is Extract<T, Function> {
return typeof obj === 'function';
};
export const runIf = function f(condition: ((()=> boolean) | boolean), func: Function, ...args: any[]) {
return () => {
const isMustToRun: boolean = isFunc(condition) ? condition() : condition;
return isMustToRun ? func(...args) : noop();
};
};
Or a fully type safe version of runIf that checks the args agains the func parameters:
export const runIf = function f<T extends (...a: any)=>any>(condition: ((()=> boolean) | boolean), func: T, ...args: Parameters<T>): (()=>ReturnType<T>) {
return () => {
const isMustToRun: boolean = isFunc(condition) ? condition() : condition;
return isMustToRun ? func(...args as any[]) : noop();
};
};
function testFn(n: string) : number { return +n;}
runIf(true, testFn, "0"); //ok
runIf(true, testFn, 0); //err

Typescript, serialize class objects

How do I serialize these classes to JSON?
As you can see from the example below JSON.stringify() does not serialize the list of Cache_Backend_LocalStorage_Tag inside the Cache_Backend_LocalStorage_TagThree object.
What am I missing?
interface Cache_Backend_LocalStorage_Tag_Interface {
_tag : string;
_keys : string[];
}
class Cache_Backend_LocalStorage_Tag implements Cache_Backend_LocalStorage_Tag_Interface {
public _tag : string;
public _keys : string[];
constructor(tag : string) {
this._tag = tag;
this._keys = [];
}
public add(key : string) : void {
this._keys.push(key);
}
public remove(key : string): boolean {
// Get the index of the key
var index = this._keys.indexOf(key, 0);
// Check if we found the keys index
if (index != undefined) {
this._keys.splice(index, 1);
return true;
}
return false;
}
public get tag(): string {
return this._tag;
}
public get keys(): string[] {
return this._keys;
}
}
interface Cache_Backend_LocalStorage_TagThree_Interface {
_tags : Cache_Backend_LocalStorage_Tag[];
}
class Cache_Backend_LocalStorage_TagThree implements Cache_Backend_LocalStorage_TagThree_Interface {
public _tags : Cache_Backend_LocalStorage_Tag[];
constructor(tags : Cache_Backend_LocalStorage_Tag[] = []) {
this._tags = tags;
}
public add(tag : Cache_Backend_LocalStorage_Tag) : void {
this.tags[tag.tag] = tag;
}
public get tags(): Cache_Backend_LocalStorage_Tag[] {
return this._tags;
}
public get(tagKey : string): Cache_Backend_LocalStorage_Tag {
return this.tags[tagKey];
}
public addKeyToTag(tagKey, key) {
this.tags[tagKey].add(key);
}
public removeKeyFromTag(tagKey, key) {
// Get the tag
var tag = this._tags[tagKey];
// Check if we found the tag index
if (tag != undefined) {
return tag.remove(key);
}
return false;
}
public clear(tagKey : string): void {
delete this._tags[tagKey];
}
public static fromObject(object): Cache_Backend_LocalStorage_TagThree {
return new Cache_Backend_LocalStorage_TagThree(object._tags);
}
}
Issue:
var tagThree = new Cache_Backend_LocalStorage_TagThree();
tagThree.add(new Cache_Backend_LocalStorage_Tag("stores"));
tagThree.addKeyToTag("stores", "store5");
tagThree.removeKeyFromTag("stores", "store5");
// {"_tags":[]}
console.log(JSON.stringify(tagThree));
// { _tags: [ stores: { _tag: 'stores', _keys: [Object] } ] }
console.log(tagThree);
Reason
It's because you're assigning properties to an array and array properties won't be included in JSON serialization. For example:
var a = [];
a["test"] = "some value";
JSON.stringify(a); // returns: []
You need to use a plain object:
var o = {};
o["test"] = "some value";
JSON.stringify(o); // returns: {"test":"some value"}
Solution
Change your Cache_Backend_LocalStorage_TagThree_Interface interface to use a dictionary like object:
interface Cache_Backend_LocalStorage_TagThree_Interface {
_tags : { [tag: string] : Cache_Backend_LocalStorage_Tag; };
}
Then update all areas of the code that will now have a compile error to use the same type. For example, change:
constructor(tags : Cache_Backend_LocalStorage_Tag[] = []) {
To:
constructor(tags : { [tag: string] : Cache_Backend_LocalStorage_Tag; } = {}) {
Just for fun - Changing default serialization behaviour (Not recommended)
If you really want to make this work with an array with your current setup (I'm not sure why), you can override how serialization is done. To do this, add a toJSON method to the _tags array in Cache_Backend_LocalStorage_TagThree. This allows you to control how the object is serialized when JSON.stringify is called on it. For example:
this._tags.toJSON = function() {
var values = [];
for (var v in this) {
if (this[v] instanceof Cache_Backend_LocalStorage_Tag) {
values.push(this[v]);
}
}
return JSON.stringify(values);
};

Categories