Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Allow source fields validation #193

Open
wants to merge 10 commits into
base: next
Choose a base branch
from
66 changes: 33 additions & 33 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,41 +56,41 @@
],
"dependencies": {},
"devDependencies": {
"@babel/core": "7.8.7",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
"@babel/preset-env": "7.8.7",
"@babel/preset-typescript": "^7.8.3",
"@hapi/joi": "^16.1.7",
"@types/hapi__joi": "^16.0.1",
"@types/jest": "25.1.4",
"@types/node": "^13.1.0",
"@types/validator": "^10.11.3",
"@types/webpack": "^4.4.27",
"@types/webpack-bundle-analyzer": "^2.13.3",
"babel-loader": "^8.0.5",
"fork-ts-checker-webpack-plugin": "^4.0.1",
"jest": "24.7.1",
"lint-staged": "^10.0.3",
"nodemon": "^2.0.0",
"nodemon-webpack-plugin": "^4.2.2",
"@babel/core": "7.10.5",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-proposal-object-rest-spread": "^7.10.4",
"@babel/preset-env": "7.10.4",
"@babel/preset-typescript": "^7.10.4",
"@hapi/joi": "^17.1.1",
"@types/hapi__joi": "^17.1.4",
"@types/jest": "26.0.7",
"@types/node": "^14.0.26",
"@types/validator": "^13.1.0",
"@types/webpack": "^4.41.21",
"@types/webpack-bundle-analyzer": "^3.8.0",
"babel-loader": "^8.1.0",
"fork-ts-checker-webpack-plugin": "^5.0.11",
"jest": "26.1.0",
"lint-staged": "^10.2.11",
"nodemon": "^2.0.4",
"nodemon-webpack-plugin": "^4.3.2",
"now": "^17.0.0",
"npm-run-all": "^4.1.5",
"prettier": "1.19.1",
"semantic-release": "^17.0.0",
"source-map-loader": "^0.2.4",
"ts-node": "^8.0.3",
"tslint": "^5.15.0",
"tslint-loader": "^3.6.0",
"typedoc": "^0.16.0",
"typedoc-plugin-external-module-name": "^3.0.0",
"typedoc-plugin-internal-external": "^2.0.1",
"typescript": "^3.7.2",
"validator": "^11.1.0",
"webpack": "4.42.0",
"webpack-bundle-analyzer": "^3.5.2",
"webpack-cli": "^3.3.0"
"prettier": "2.0.5",
"semantic-release": "^17.1.1",
"source-map-loader": "^1.0.1",
"ts-node": "^8.10.2",
"tslint": "^6.1.2",
"tslint-loader": "^3.5.4",
"typedoc": "^0.17.8",
"typedoc-plugin-external-module-name": "^4.0.3",
"typedoc-plugin-internal-external": "^2.2.0",
"typescript": "^3.9.7",
"validator": "^13.1.1",
"webpack": "4.44.0",
"webpack-bundle-analyzer": "^3.8.0",
"webpack-cli": "^3.3.12"
},
"eslintConfig": {
"extends": "xo-space",
Expand Down
22 changes: 15 additions & 7 deletions src/MorphismTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export interface SchemaOptions<Target = any> {
*/
class?: {
/**
* Specify wether ES6 Class fields should be automapped if names on source and target match
* Specify wether ES6 Class fields should be automatically mapped if names on source and target match
* @default true
* @type {boolean}
*/
Expand Down Expand Up @@ -255,19 +255,27 @@ export class MorphismSchemaTree<Target, Source> {
// Action<Object>: a path and a function: [ destination : { path: 'source', fn:(fieldValue, items) }]
return ({ object, items, objectToCompute }) => {
let targetValue: any;
let sourceValue: any;
if (action.path) {
if (Array.isArray(action.path)) {
targetValue = aggregator(action.path, object);
sourceValue = aggregator(action.path, object);
} else if (isString(action.path)) {
targetValue = get(object, action.path);
sourceValue = get(object, action.path);
}
targetValue = sourceValue;
} else {
targetValue = object;
sourceValue = object;
}

if (action.validation?.source) {
const validationResult = action.validation.source({ value: sourceValue });

this.processValidationResult(validationResult, targetProperty, objectToCompute);
sourceValue = validationResult.value;
}
if (action.fn) {
try {
targetValue = action.fn.call(undefined, targetValue, object, items, objectToCompute);
targetValue = action.fn.call(undefined, sourceValue, object, items, objectToCompute);
} catch (e) {
e.message = `Unable to set target property [${targetProperty}].
\n An error occured when applying [${action.fn.name}] on property [${action.path}]
Expand All @@ -276,8 +284,8 @@ export class MorphismSchemaTree<Target, Source> {
}
}

if (action.validation) {
const validationResult = action.validation({ value: targetValue });
if (action.validation?.target) {
const validationResult = action.validation.target({ value: targetValue });

this.processValidationResult(validationResult, targetProperty, objectToCompute);
targetValue = validationResult.value;
Expand Down
2 changes: 1 addition & 1 deletion src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function isValidAction(action: any) {

export const aggregator = (paths: string[], object: any) => {
return paths.reduce((delta, path) => {
set(delta, path, get(object, path)); // TODO: ensure set will return the mutated object
set(delta, path, get(object, path));
return delta;
}, {});
};
Expand Down
8 changes: 4 additions & 4 deletions src/morphism.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ describe('Morphism', () => {
t1: string;
}
const schema = createSchema<Target>({
t1: { path: 's1', validation: Validation.string() },
t1: { path: 's1', validation: { target: Validation.string() } },
});
const result = morphism(schema, { s1: 1234 });
const errors = reporter.report(result);
Expand All @@ -380,7 +380,7 @@ describe('Morphism', () => {
t1: string;
}
const schema = createSchema<Target>({
t1: { fn: value => value.s1, validation: Validation.string() },
t1: { fn: value => value.s1, validation: { target: Validation.string() } },
});
const result = morphism(schema, { s1: 1234 });
const errors = reporter.report(result);
Expand Down Expand Up @@ -572,8 +572,8 @@ describe('Morphism', () => {
}
const schema = createSchema<Target, Source>(
{
t1: { fn: value => value.s1, validation: Validation.string() },
t2: { fn: value => value.s1, validation: Validation.string() },
t1: { fn: value => value.s1, validation: { target: Validation.string() } },
t2: { fn: value => value.s1, validation: { target: Validation.string() } },
},
{ validation: { throw: true } }
);
Expand Down
6 changes: 5 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,12 @@ export type ActionAggregator<T extends unknown = unknown> = T extends object ? (
*/
export interface ActionSelector<Source = object, Target = any, TargetProperty extends keyof Target = any> {
path?: ActionString<Source> | ActionAggregator<Source>;
// TODO: fieldValue should be of type Target if path is not defined. Try to infer fieldValue if path is defined.
fn?: (fieldValue: any, object: Source, items: Source, objectToCompute: Target) => Target[TargetProperty];
validation?: ValidateFunction;
validation?: {
source?: ValidateFunction;
target?: ValidateFunction;
};
}

export interface ValidatorValidateResult {
Expand Down
69 changes: 57 additions & 12 deletions src/validation/Validation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('Validation', () => {
const length = 2;
const source: S = { s1: ['a', 'b', 'c'] };
const schema = createSchema<T, S>({
t1: { path: 's1', validation: Validation.test().max(length) },
t1: { path: 's1', validation: { target: Validation.test().max(length) } },
});

const result = morphism(schema, source);
Expand Down Expand Up @@ -97,23 +97,68 @@ describe('Validation', () => {
const schema = createSchema<T, S>({
t1: {
path: 's1',
validation: input => {
if (isEmail(input.value)) {
return { value: input.value };
} else {
return {
value: input.value,
error: new ValidatorError({
validation: {
target: input => {
if (isEmail(input.value)) {
return { value: input.value };
} else {
return {
value: input.value,
expect: `Expected value to be an <email> but received <${input.value}>`,
}),
};
}
error: new ValidatorError({
value: input.value,
expect: `Expected value to be an <email> but received <${input.value}>`,
}),
};
}
},
},
},
});
const expected: T = { t1: source.s1 };
const result = morphism(schema, source);
expect(result).toEqual(expected);
});

it('should throw on source field validation', () => {
interface S {
s1: number;
}
interface T {
t1: number;
}

const source: S = { s1: 14 };
const schema = createSchema<T, S>(
{
t1: {
path: 's1',
fn: () => 20,
validation: { source: Validation.number().min(18) },
},
},
{ validation: { throw: true } }
);
const error = new ValidationError({
targetProperty: 't1',
innerError: new ValidatorError({
expect: `value to be greater or equal than 18`,
value: source.s1,
}),
});
const message = defaultFormatter(error);

const fn = () => morphism(schema, source);
expect(fn).toThrow(message);
});

it('should report when the source is not valid', () => {
type Source = { value: number };
type Target = { value: string };
const schema = createSchema<Target, Source>({
value: ({ value }) => {
return '' + value;
},
});
expect(true).toBe(false); // TODO:
});
});
Loading