From cc0b577c2997bf07b75db9dfbe006c9b7c4d3c90 Mon Sep 17 00:00:00 2001 From: Aaron Magid Date: Sat, 12 Feb 2022 22:41:25 -0600 Subject: [PATCH 1/4] Add condition to errors, no tests yet --- src/base.ts | 3 +++ src/context-items/custom-validation.ts | 9 +++++++-- src/context-items/standard-validation.ts | 2 +- src/context.ts | 21 +++++++++++++++------ 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/base.ts b/src/base.ts index 3ded9e36..3abe3db3 100644 --- a/src/base.ts +++ b/src/base.ts @@ -1,4 +1,5 @@ import { ReadonlyContext } from './context'; +import { ContextItem } from './context-items'; export interface Request { [k: string]: any; @@ -38,6 +39,7 @@ export type ValidationError = // These are optional so places don't need to define them, but can reference them location?: undefined; value?: undefined; + condition?: ContextItem; } | { location: Location; @@ -46,6 +48,7 @@ export type ValidationError = msg: any; // This is optional so places don't need to define it, but can reference it nestedErrors?: unknown[]; + condition?: ContextItem; }; // Not using Symbol because of #813 diff --git a/src/context-items/custom-validation.ts b/src/context-items/custom-validation.ts index d3e421ae..c872103a 100644 --- a/src/context-items/custom-validation.ts +++ b/src/context-items/custom-validation.ts @@ -17,14 +17,19 @@ export class CustomValidation implements ContextItem { // A promise that was resolved only adds an error if negated. // Otherwise it always suceeds if ((!isPromise && failed) || (isPromise && this.negated)) { - context.addError(this.message, value, meta); + context.addError(this.message, value, meta, this); } } catch (err) { if (this.negated) { return; } - context.addError(this.message || (err instanceof Error ? err.message : err), value, meta); + context.addError( + this.message || (err instanceof Error ? err.message : err), + value, + meta, + this, + ); } } } diff --git a/src/context-items/standard-validation.ts b/src/context-items/standard-validation.ts index 90c495d9..8f4efcf9 100644 --- a/src/context-items/standard-validation.ts +++ b/src/context-items/standard-validation.ts @@ -15,7 +15,7 @@ export class StandardValidation implements ContextItem { async run(context: Context, value: any, meta: Meta) { const result = this.validator(toString(value), ...this.options); if (this.negated ? result : !result) { - context.addError(this.message, value, meta); + context.addError(this.message, value, meta, this); } } } diff --git a/src/context.ts b/src/context.ts index 710fd26b..9d1ec895 100644 --- a/src/context.ts +++ b/src/context.ts @@ -72,23 +72,32 @@ export class Context { instance.value = value; } - addError(message: any, value: any, meta: Meta): void; + addError(message: any, value: any, meta: Meta, condition: ContextItem): void; addError(message: any, nestedErrors: ValidationError[]): void; - addError(message: any, valueOrNestedErrors: any, meta?: Meta) { + addError(message: any, valueOrNestedErrors: any, meta?: Meta, condition?: ContextItem) { const msg = message || this.message || 'Invalid value'; + let obj: ValidationError; if (meta) { - this._errors.push({ + obj = { value: valueOrNestedErrors, msg: typeof msg === 'function' ? msg(valueOrNestedErrors, meta) : msg, param: meta.path, location: meta.location, - }); + }; + if (condition) { + Object.defineProperty(obj, 'condition', { value: condition }); + } + this._errors.push(obj); } else { - this._errors.push({ + obj = { msg, param: '_error', nestedErrors: valueOrNestedErrors, - }); + }; + if (condition) { + Object.defineProperty(obj, 'condition', { value: condition }); + } + this._errors.push(obj); } } } From afd078b0d4e039216c274aec63c0be4c04865b12 Mon Sep 17 00:00:00 2001 From: Aaron Magid Date: Sun, 13 Feb 2022 11:08:05 -0600 Subject: [PATCH 2/4] Make condition always optional on addError --- src/context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/context.ts b/src/context.ts index 9d1ec895..667610dd 100644 --- a/src/context.ts +++ b/src/context.ts @@ -72,7 +72,7 @@ export class Context { instance.value = value; } - addError(message: any, value: any, meta: Meta, condition: ContextItem): void; + addError(message: any, value: any, meta: Meta, condition?: ContextItem): void; addError(message: any, nestedErrors: ValidationError[]): void; addError(message: any, valueOrNestedErrors: any, meta?: Meta, condition?: ContextItem) { const msg = message || this.message || 'Invalid value'; From 7c36c7e3cf70b603395bb12da4e72997bd9d5ccb Mon Sep 17 00:00:00 2001 From: Aaron Magid Date: Sun, 13 Feb 2022 11:08:20 -0600 Subject: [PATCH 3/4] Update existing addError tests to pass condition --- src/context-items/custom-validation.spec.ts | 16 +++++----------- src/context-items/standard-validation.spec.ts | 2 +- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/context-items/custom-validation.spec.ts b/src/context-items/custom-validation.spec.ts index 69edf219..e6c4b42b 100644 --- a/src/context-items/custom-validation.spec.ts +++ b/src/context-items/custom-validation.spec.ts @@ -25,7 +25,7 @@ const createSyncTest = (options: { returnValue: any; addsError: boolean }) => as validator.mockReturnValue(options.returnValue); await validation.run(context, 'bar', meta); if (options.addsError) { - expect(context.addError).toHaveBeenCalledWith(validation.message, 'bar', meta); + expect(context.addError).toHaveBeenCalledWith(validation.message, 'bar', meta, validation); } else { expect(context.addError).not.toHaveBeenCalled(); } @@ -53,13 +53,7 @@ describe('when not negated', () => { throw new Error('boom'); }); await validation.run(context, 'bar', meta); - expect(context.addError).toHaveBeenCalledWith('nope', 'bar', meta); - }); - - it('adds error with validation message if validator returns a promise that rejects', async () => { - validator.mockRejectedValue('a bomb'); - await validation.run(context, 'bar', meta); - expect(context.addError).toHaveBeenCalledWith('nope', 'bar', meta); + expect(context.addError).toHaveBeenCalledWith('nope', 'bar', meta, validation); }); }); @@ -73,13 +67,13 @@ describe('when not negated', () => { throw new Error('boom'); }); await validation.run(context, 'bar', meta); - expect(context.addError).toHaveBeenCalledWith('boom', 'bar', meta); + expect(context.addError).toHaveBeenCalledWith('boom', 'bar', meta, validation); }); it('adds error with rejection message if validator returns a promise that rejects', async () => { validator.mockRejectedValue('a bomb'); await validation.run(context, 'bar', meta); - expect(context.addError).toHaveBeenCalledWith('a bomb', 'bar', meta); + expect(context.addError).toHaveBeenCalledWith('a bomb', 'bar', meta, validation); }); }); @@ -123,6 +117,6 @@ describe('when negated', () => { it('adds error with validation message if validator returns a promise that resolves', async () => { validator.mockResolvedValue(true); await validation.run(context, 'bar', meta); - expect(context.addError).toHaveBeenCalledWith('nope', 'bar', meta); + expect(context.addError).toHaveBeenCalledWith('nope', 'bar', meta, validation); }); }); diff --git a/src/context-items/standard-validation.spec.ts b/src/context-items/standard-validation.spec.ts index 5ee7f106..f3ed8964 100644 --- a/src/context-items/standard-validation.spec.ts +++ b/src/context-items/standard-validation.spec.ts @@ -25,7 +25,7 @@ const createTest = (options: { returnValue: any; addsError: boolean }) => async validator.mockReturnValue(options.returnValue); await validation.run(context, 'bar', meta); if (options.addsError) { - expect(context.addError).toHaveBeenCalledWith(validation.message, 'bar', meta); + expect(context.addError).toHaveBeenCalledWith(validation.message, 'bar', meta, validation); } else { expect(context.addError).not.toHaveBeenCalled(); } From d04bc59dfb4bb8ee4cee5f77ad29d99ba45c81db Mon Sep 17 00:00:00 2001 From: Aaron Magid Date: Sun, 13 Feb 2022 11:08:44 -0600 Subject: [PATCH 4/4] Add new test for condition field --- src/context.spec.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/context.spec.ts b/src/context.spec.ts index 94b74c35..7711a2ce 100644 --- a/src/context.spec.ts +++ b/src/context.spec.ts @@ -1,6 +1,7 @@ import { Context } from './context'; import { ContextBuilder } from './context-builder'; import { FieldInstance, Meta } from './base'; +import { StandardValidation } from './context-items'; let context: Context; let data: FieldInstance[]; @@ -93,6 +94,25 @@ describe('#addError()', () => { location: 'headers', }); }); + + it('includes failed validation condition on error result as nonenumerable property', () => { + const meta: Meta = { + path: 'bar', + location: 'headers', + req: {}, + }; + let validator: jest.Mock; + validator = jest.fn(); + let validation = new StandardValidation(validator, false); + validation.message = 'nope'; + context.addError('bar', 'foo', meta, validation); + let serializedError = JSON.parse(JSON.stringify(context.errors[0])); + + expect(context.errors).toHaveLength(1); + expect(context.errors[0]).toHaveProperty('condition', validation); + // Nonenumerable property addition will not affect existing express responses + expect(serializedError).not.toHaveProperty('condition'); + }); }); describe('#addFieldInstance()', () => {