Skip to content

Commit

Permalink
feat: added transformAndValidateObject
Browse files Browse the repository at this point in the history
- refactored @dereekb/model transform lib directory
  • Loading branch information
dereekb committed Apr 6, 2022
1 parent becdbdf commit 1f66094
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 75 deletions.
33 changes: 0 additions & 33 deletions packages/model/project.json

This file was deleted.

2 changes: 2 additions & 0 deletions packages/model/src/lib/transform/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './type';
export * from './type.annotation';
export * from './transform';
70 changes: 70 additions & 0 deletions packages/model/src/lib/transform/transform.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { transformAndValidateObjectResult, TransformAndValidateObjectResultFunction, TransformAndValidateObjectSuccessResultOutput, TransformAndValidateObjectErrorResultOutput, transformAndValidateObjectFactory } from './transform';
import { Expose } from 'class-transformer';
import { IsBoolean } from 'class-validator';

export class TestTransformAndValidateClass {

@Expose()
@IsBoolean()
valid?: boolean;

}

describe('transformAndValidateObjectFactory()', () => {

const errorValue = 0;
const factory = transformAndValidateObjectFactory({ onValidationError: async () => errorValue });

it('should create a transformAndValidateFunction', async () => {
const successValue = 100;

const fn = factory(TestTransformAndValidateClass, async () => successValue);
expect(fn).toBeDefined();
expect(typeof fn).toBe('function');
});

describe('function', () => {

it('should return the success value for valid input.', async () => {
const successValue = 100;
const fn = factory(TestTransformAndValidateClass, async () => successValue);

const result = await fn({ valid: true });
expect(result).toBe(successValue);
});

it('should handle the validation error', async () => {
const fn = factory(TestTransformAndValidateClass, async () => 0);

const result = await fn({ valid: true });
expect(result).toBe(errorValue);
});

});

});

describe('transformAndValidateObjectResult()', () => {

const transformResult: TransformAndValidateObjectResultFunction<object, { value: TestTransformAndValidateClass }> = transformAndValidateObjectResult(TestTransformAndValidateClass, async (value) => {
return { value };
});

it('should return success when the input is valid', async () => {
const result = await transformResult({ valid: true }) as TransformAndValidateObjectSuccessResultOutput<{ value: TestTransformAndValidateClass }>;

expect(result.success).toBe(true);
expect(result.result).toBeDefined();
expect(result.result.value).toBeDefined();
expect(result.result.value.valid).toBe(true);
});

it('should return validation errors when the input is invalid', async () => {
const result = await transformResult({ invalid: true }) as TransformAndValidateObjectErrorResultOutput;

expect(result.success).toBe(false);
expect(result.validationErrors.length > 0).toBe(true);
expect(result.validationErrors[0].property).toBe('valid'); // missing
});

});
127 changes: 86 additions & 41 deletions packages/model/src/lib/transform/transform.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,97 @@
import { MapStringFn, Maybe, splitCommaSeparatedString } from "@dereekb/util";
import { Transform, TransformFnParams } from "class-transformer";

// MARK: String
export function transformStringToBoolean(defaultValue?: boolean | undefined): (params: TransformFnParams) => Maybe<boolean> {
return (params: TransformFnParams) => {
if (params.value) {
switch (params.value.toLowerCase()) {
case 't':
case 'true':
return true;
case 'f':
case 'false':
return false;
default:
return defaultValue;
}
import { ClassType } from "@dereekb/util";
import { plainToClass } from "class-transformer";
import { validate, ValidationError } from "class-validator";

// MARK: Transform and Validate
export type TransformAndValidateObjectFunction<I, O> = (input: I) => Promise<O>;
export type TransformAndValidateObjectHandleValidate<O = any> = (validationErrors: ValidationError[]) => Promise<O>;

/**
* transformAndValidateObject() configuration that also provides error handling.
*/
export interface TransformAndValidateObject<T extends object, O> {
readonly classType: ClassType<T>;
readonly fn: (parsed: T) => Promise<O>;
readonly onValidationError: TransformAndValidateObjectHandleValidate<O>;
}

export function transformAndValidateObject<T extends object, O, I = any>(config: TransformAndValidateObject<T, O>): TransformAndValidateObjectFunction<I, O> {
const transformToResult = transformAndValidateObjectResult(config.classType, config.fn);
const { onValidationError } = config;

return (input) => transformToResult(input).then((x) => {
if (x.success) {
return x.result;
} else {
return defaultValue;
return onValidationError(x.validationErrors);
}
}
});
}

// MARK: Comma Separated Values
export function transformCommaSeparatedValueToArray<T>(mapFn: MapStringFn<T>): (params: TransformFnParams) => Maybe<T[]> {
return (params: TransformFnParams) => {
let result: Maybe<T[]>;

if (params.value) {
if (Array.isArray(params.value)) {
result = params.value;
} else {
result = splitCommaSeparatedString(params.value, mapFn);
}
}
// MARK: Transform and Validate Factory
/**
* Configuration for the transformAndValidateObject function from transformAndValidateObjectFactory().
*/
export interface TransformAndValidateObjectFactoryDefaults {
readonly onValidationError: TransformAndValidateObjectHandleValidate<any>;
}

/**
* Factory for generating TransformAndValidateObjectFunction functions.
*/
export type TransformAndValidateObjectFactory = <T extends object, O, I = any>(classType: ClassType<T>, fn: (parsed: T) => Promise<O>, onValidationError?: TransformAndValidateObjectHandleValidate<any>) => TransformAndValidateObjectFunction<I, O>;

export function transformAndValidateObjectFactory(defaults: TransformAndValidateObjectFactoryDefaults): TransformAndValidateObjectFactory {
const { onValidationError: defaultOnValidationError } = defaults;

return result;
}
return <T extends object, O>(classType: ClassType<T>, fn: (parsed: T) => Promise<O>, onValidationError?: TransformAndValidateObjectHandleValidate<any>) => {
const config: TransformAndValidateObject<T, O> = {
classType,
fn,
onValidationError: onValidationError ?? defaultOnValidationError
};

return transformAndValidateObject(config);
};
}

export const transformCommaSeparatedNumberValueToArray = transformCommaSeparatedValueToArray((x) => Number(x));
export const transformCommaSeparatedStringValueToArray = transformCommaSeparatedValueToArray((x) => x);
// MARK: Transform And Validate Result
export type TransformAndValidateObjectResultFunction<I, O> = (input: I) => Promise<TransformAndValidateObjectResultOutput<O>>;

export type TransformAndValidateObjectResultOutput<O> = TransformAndValidateObjectSuccessResultOutput<O> | TransformAndValidateObjectErrorResultOutput;

// MARK: Transform Annotations
export function TransformCommaSeparatedValueToArray<T>(mapFn: MapStringFn<T>) {
return Transform(transformCommaSeparatedValueToArray(mapFn));
export interface TransformAndValidateObjectSuccessResultOutput<O> {
readonly success: true;
readonly result: O;
}

export const TransformCommaSeparatedStringValueToArray = () => Transform(transformCommaSeparatedStringValueToArray);
export const TransformCommaSeparatedNumberValueToArray = () => Transform(transformCommaSeparatedNumberValueToArray);
export interface TransformAndValidateObjectErrorResultOutput {
readonly success: false;
readonly validationErrors: ValidationError[];
}

/**
* Factory function that wraps the input class type and handler function to first transform the input object to a the given class, and then validate it.
*
* @param classType
* @param fn
* @returns
*/
export function transformAndValidateObjectResult<T extends object, O, I = any>(classType: ClassType<T>, fn: (parsed: T) => Promise<O>): TransformAndValidateObjectResultFunction<I, O> {
return async (input: I) => {

const object: T = plainToClass(classType, input, {
// Note: Each variable on the target class must be marked with the @Expose() annotation.
excludeExtraneousValues: true
});

export const TransformStringValueToBoolean = () => Transform(transformStringToBoolean());
const validationErrors: ValidationError[] = await validate(object);

if (validationErrors.length) {
return { validationErrors, success: false };
} else {
const result = await fn(object);
return { result, success: true };
}
};
}
13 changes: 13 additions & 0 deletions packages/model/src/lib/transform/type.annotation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MapStringFn } from "@dereekb/util";
import { Transform } from "class-transformer";
import { transformCommaSeparatedValueToArray, transformCommaSeparatedStringValueToArray, transformCommaSeparatedNumberValueToArray, transformStringToBoolean } from "./type";

// MARK: Transform Annotations
export function TransformCommaSeparatedValueToArray<T>(mapFn: MapStringFn<T>) {
return Transform(transformCommaSeparatedValueToArray(mapFn));
}

export const TransformCommaSeparatedStringValueToArray = () => Transform(transformCommaSeparatedStringValueToArray);
export const TransformCommaSeparatedNumberValueToArray = () => Transform(transformCommaSeparatedNumberValueToArray);

export const TransformStringValueToBoolean = () => Transform(transformStringToBoolean());
42 changes: 42 additions & 0 deletions packages/model/src/lib/transform/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { MapStringFn, Maybe, splitCommaSeparatedString } from "@dereekb/util";
import { TransformFnParams } from "class-transformer";

// MARK: String
export function transformStringToBoolean(defaultValue?: boolean | undefined): (params: TransformFnParams) => Maybe<boolean> {
return (params: TransformFnParams) => {
if (params.value) {
switch (params.value.toLowerCase()) {
case 't':
case 'true':
return true;
case 'f':
case 'false':
return false;
default:
return defaultValue;
}
} else {
return defaultValue;
}
}
}

// MARK: Comma Separated Values
export function transformCommaSeparatedValueToArray<T>(mapFn: MapStringFn<T>): (params: TransformFnParams) => Maybe<T[]> {
return (params: TransformFnParams) => {
let result: Maybe<T[]>;

if (params.value) {
if (Array.isArray(params.value)) {
result = params.value;
} else {
result = splitCommaSeparatedString(params.value, mapFn);
}
}

return result;
}
}

export const transformCommaSeparatedNumberValueToArray = transformCommaSeparatedValueToArray((x) => Number(x));
export const transformCommaSeparatedStringValueToArray = transformCommaSeparatedValueToArray((x) => x);
51 changes: 50 additions & 1 deletion workspace.json
Original file line number Diff line number Diff line change
Expand Up @@ -955,7 +955,56 @@
},
"tags": []
},
"model": "packages/model",
"model": {
"root": "packages/model",
"sourceRoot": "packages/model/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nrwl/workspace:run-commands",
"options": {
"command": "npx nx run model:build-base",
"color": true
}
},
"build-base": {
"executor": "@nrwl/node:package",
"outputs": ["{options.outputPath}"],
"dependsOn": [
{
"target": "build",
"projects": "dependencies"
}
],
"options": {
"outputPath": "dist/packages/model",
"tsConfig": "packages/model/tsconfig.lib.json",
"packageJson": "packages/model/package.json",
"main": "packages/model/src/index.ts",
"assets": ["packages/model/*.md"]
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["packages/model/**/*.ts"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": [
"coverage/packages/model",
"./.reports/jest/model.junit.xml"
],
"options": {
"jestConfig": "packages/model/jest.config.js",
"passWithNoTests": true
}
}
},
"tags": []
},
"rxjs": {
"root": "packages/rxjs",
"sourceRoot": "packages/rxjs/src",
Expand Down

0 comments on commit 1f66094

Please sign in to comment.