From 7a1f079d3297ae897c007735b3a90b6873a492fd Mon Sep 17 00:00:00 2001 From: Federico Ciardi Date: Fri, 1 Jan 2021 18:23:06 +0100 Subject: [PATCH 1/5] feat: add 'Rename' context-item --- src/context-items/rename.spec.ts | 67 ++++++++++++++++++++++++++++++++ src/context-items/rename.ts | 30 ++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 src/context-items/rename.spec.ts create mode 100644 src/context-items/rename.ts diff --git a/src/context-items/rename.spec.ts b/src/context-items/rename.spec.ts new file mode 100644 index 00000000..6c8f3fdf --- /dev/null +++ b/src/context-items/rename.spec.ts @@ -0,0 +1,67 @@ +import { ContextBuilder } from '../context-builder'; +import { Meta } from '../base'; +import { Rename } from './rename'; + +it('the new path is identical to the old one', () => { + const context = new ContextBuilder().build(); + const meta: Meta = { req: {}, location: 'body', path: 'foo' }; + + expect(new Rename('foo').run(context, 'value', meta)).resolves; +}); + +it('throws an error if trying to rename more than one field', async () => { + const context = new ContextBuilder().setFields(['foo', 'bar']).build(); + const meta: Meta = { req: {}, location: 'body', path: 'foo' }; + + await expect(new Rename('_foo').run(context, 'value', meta)).rejects.toThrow(); +}); + +describe('throws an error if using wildcards', () => { + it('in context fields', async () => { + const context = new ContextBuilder().setFields(['foo.*']).build(); + const meta: Meta = { req: {}, location: 'body', path: 'foo' }; + + await expect(new Rename('_foo').run(context, 'value', meta)).rejects.toThrow(); + }); + + it('in new path', async () => { + const context = new ContextBuilder().setFields(['foo']).build(); + const meta: Meta = { req: {}, location: 'body', path: 'foo' }; + + await expect(new Rename('foo.*').run(context, 'value', meta)).rejects.toThrow(); + }); +}); + +it('throws an error if the new path is already assigned to a property', async () => { + const context = new ContextBuilder().setFields(['foo']).build(); + const meta: Meta = { + req: { + body: { + foo: 'value', + _foo: 'bar', + }, + }, + location: 'body', + path: 'foo', + }; + + await expect(new Rename('_foo').run(context, 'value', meta)).rejects.toThrow(); +}); + +it('throws an error if trying to rename to an existing property', () => { + const context = new ContextBuilder().setFields(['foo']).build(); + const meta: Meta = { + req: { + body: { + foo: 'value', + }, + }, + location: 'body', + path: 'foo', + }; + + expect(new Rename('_foo').run(context, 'value', meta)).resolves; + expect(meta.req.body).toEqual({ + _foo: 'value', + }); +}); diff --git a/src/context-items/rename.ts b/src/context-items/rename.ts new file mode 100644 index 00000000..010e0993 --- /dev/null +++ b/src/context-items/rename.ts @@ -0,0 +1,30 @@ +import * as _ from 'lodash'; +import { Meta } from '../base'; +import { Context } from '../context'; +import { ContextItem } from './context-item'; + +export class Rename implements ContextItem { + constructor(readonly newPath: string) {} + + async run(context: Context, value: any, meta: Meta) { + const { req, location, path } = meta; + + if (path !== this.newPath) { + if (context.fields.length !== 1) { + throw new Error('Cannot rename multiple fields.'); + } + const field = context.fields[0]; + if (field.includes('*') || this.newPath.includes('*')) { + throw new Error('Cannot use rename() with wildcards.'); + } + + if (_.get(req[location], this.newPath) !== undefined) { + throw new Error(`Cannot rename to req.${location}.${path} as it already exists.`); + } + + _.set(req[location], this.newPath, value); + _.unset(req[location], path); + } + return Promise.resolve(); + } +} From a663a4b20f6890aacde4d7e83e3232ed490683b5 Mon Sep 17 00:00:00 2001 From: Federico Ciardi Date: Fri, 1 Jan 2021 18:40:21 +0100 Subject: [PATCH 2/5] feat: add 'rename' to context-handler --- src/chain/context-handler-impl.spec.ts | 8 ++++++++ src/chain/context-handler-impl.ts | 6 ++++++ src/chain/context-handler.ts | 1 + 3 files changed, 15 insertions(+) diff --git a/src/chain/context-handler-impl.spec.ts b/src/chain/context-handler-impl.spec.ts index 6c2f41c1..5509e9ab 100644 --- a/src/chain/context-handler-impl.spec.ts +++ b/src/chain/context-handler-impl.spec.ts @@ -3,6 +3,7 @@ import { ChainCondition, CustomCondition } from '../context-items'; import { check } from '../middlewares/check'; import { Bail } from '../context-items/bail'; import { ContextHandler, ContextHandlerImpl } from './'; +import { Rename } from '../context-items/rename'; let builder: ContextBuilder; let contextHandler: ContextHandler; @@ -74,3 +75,10 @@ describe('#optional()', () => { expect(builder.setOptional).toHaveBeenNthCalledWith(3, false); }); }); + +describe('#rename()', () => { + it('adds a Rename item', () => { + contextHandler.rename('foo'); + expect(builder.addItem).toHaveBeenCalledWith(new Rename('foo')); + }); +}); diff --git a/src/chain/context-handler-impl.ts b/src/chain/context-handler-impl.ts index 320777c9..acede1b2 100644 --- a/src/chain/context-handler-impl.ts +++ b/src/chain/context-handler-impl.ts @@ -3,6 +3,7 @@ import { Optional } from '../context'; import { ChainCondition, CustomCondition } from '../context-items'; import { CustomValidator } from '../base'; import { Bail } from '../context-items/bail'; +import { Rename } from '../context-items/rename'; import { ContextHandler } from './context-handler'; import { ValidationChain } from './validation-chain'; @@ -37,4 +38,9 @@ export class ContextHandlerImpl implements ContextHandler { return this.chain; } + + rename(newPath: string) { + this.builder.addItem(new Rename(newPath)); + return this.chain; + } } diff --git a/src/chain/context-handler.ts b/src/chain/context-handler.ts index 8d6aac11..0627ac59 100644 --- a/src/chain/context-handler.ts +++ b/src/chain/context-handler.ts @@ -6,4 +6,5 @@ export interface ContextHandler { bail(): Chain; if(condition: CustomValidator | ValidationChain): Chain; optional(options?: Partial | true): Chain; + rename(newPath: string): Chain; } From d19d8d34d77202f0264a0520819d93c2ce6a3b37 Mon Sep 17 00:00:00 2001 From: Federico Ciardi Date: Sat, 2 Jan 2021 14:47:11 +0100 Subject: [PATCH 3/5] feat(context-runner): update instance.path on rename --- src/chain/context-runner-impl.spec.ts | 21 +++++++++++++++++++++ src/chain/context-runner-impl.ts | 20 +++++++++++++------- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/chain/context-runner-impl.spec.ts b/src/chain/context-runner-impl.spec.ts index f97b5730..f3cfc93e 100644 --- a/src/chain/context-runner-impl.spec.ts +++ b/src/chain/context-runner-impl.spec.ts @@ -3,6 +3,7 @@ import { FieldInstance, InternalRequest, ValidationHalt, contextsKey } from '../ import { ContextBuilder } from '../context-builder'; import { ContextItem } from '../context-items'; import { Result } from '../validation-result'; +import { Rename } from '../context-items/rename'; import { ContextRunnerImpl } from './context-runner-impl'; let builder: ContextBuilder; @@ -206,3 +207,23 @@ describe('with dryRun: true option', () => { expect(req.query).toHaveProperty('bar', 456); }); }); + +it('contextItem is instance of Rename', async () => { + const rename = new Rename('bar'); + const mockItem = { run: jest.fn() }; + builder.setFields(['foo']).addItem(rename).addItem(mockItem); + selectFields.mockReturnValue([ + { location: 'query', path: 'foo', originalPath: 'foo', value: 123, originalValue: 123 }, + ]); + + const req = { query: { foo: 123 } }; + await contextRunner.run(req); + expect(req.query).toEqual({ bar: 123 }); + expect(mockItem.run).toHaveBeenCalledWith(expect.any(Context), 123, { + req: expect.objectContaining({ + query: { bar: 123 }, + }), + location: 'query', + path: 'bar', + }); +}); diff --git a/src/chain/context-runner-impl.ts b/src/chain/context-runner-impl.ts index 0d68c2ec..711228a5 100644 --- a/src/chain/context-runner-impl.ts +++ b/src/chain/context-runner-impl.ts @@ -2,6 +2,7 @@ import * as _ from 'lodash'; import { InternalRequest, Request, ValidationHalt, contextsKey } from '../base'; import { Context, ReadonlyContext } from '../context'; import { ContextBuilder } from '../context-builder'; +import { Rename } from '../context-items/rename'; import { SelectFields, selectFields as baseSelectFields } from '../select-fields'; import { Result } from '../validation-result'; import { ContextRunner } from './context-runner'; @@ -43,14 +44,19 @@ export class ContextRunnerImpl implements ContextRunner { path, }); - // An instance is mutable, so if an item changed its value, there's no need to call getData again - const newValue = instance.value; + if (contextItem instanceof Rename) { + // change instance path + instance.path = contextItem.newPath; + } else { + // An instance is mutable, so if an item changed its value, there's no need to call getData again + const newValue = instance.value; - // Checks whether the value changed. - // Avoids e.g. undefined values being set on the request if it didn't have the key initially. - const reqValue = path !== '' ? _.get(req[location], path) : req[location]; - if (!options.dryRun && reqValue !== instance.value) { - path !== '' ? _.set(req[location], path, newValue) : _.set(req, location, newValue); + // Checks whether the value changed. + // Avoids e.g. undefined values being set on the request if it didn't have the key initially. + const reqValue = path !== '' ? _.get(req[location], path) : req[location]; + if (!options.dryRun && reqValue !== instance.value) { + path !== '' ? _.set(req[location], path, newValue) : _.set(req, location, newValue); + } } } catch (e) { if (e instanceof ValidationHalt) { From 55097d459a0bd3f4c8b2b4f6a8db6bfc7e29bffa Mon Sep 17 00:00:00 2001 From: Federico Ciardi Date: Sat, 2 Jan 2021 14:50:17 +0100 Subject: [PATCH 4/5] style: run eslint --- src/chain/context-handler-impl.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chain/context-handler-impl.spec.ts b/src/chain/context-handler-impl.spec.ts index 5509e9ab..80402f59 100644 --- a/src/chain/context-handler-impl.spec.ts +++ b/src/chain/context-handler-impl.spec.ts @@ -2,8 +2,8 @@ import { ContextBuilder } from '../context-builder'; import { ChainCondition, CustomCondition } from '../context-items'; import { check } from '../middlewares/check'; import { Bail } from '../context-items/bail'; -import { ContextHandler, ContextHandlerImpl } from './'; import { Rename } from '../context-items/rename'; +import { ContextHandler, ContextHandlerImpl } from './'; let builder: ContextBuilder; let contextHandler: ContextHandler; From db52eb49bf6920fe9d016fca1729321f886682ea Mon Sep 17 00:00:00 2001 From: Federico Ciardi Date: Sat, 16 Jan 2021 18:45:59 +0100 Subject: [PATCH 5/5] feat: assume no wildcards in old path --- src/context-items/rename.spec.ts | 19 +++++-------------- src/context-items/rename.ts | 4 ++-- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/context-items/rename.spec.ts b/src/context-items/rename.spec.ts index 6c8f3fdf..5c2be360 100644 --- a/src/context-items/rename.spec.ts +++ b/src/context-items/rename.spec.ts @@ -3,7 +3,7 @@ import { Meta } from '../base'; import { Rename } from './rename'; it('the new path is identical to the old one', () => { - const context = new ContextBuilder().build(); + const context = new ContextBuilder().setFields(['foo']).build(); const meta: Meta = { req: {}, location: 'body', path: 'foo' }; expect(new Rename('foo').run(context, 'value', meta)).resolves; @@ -16,20 +16,11 @@ it('throws an error if trying to rename more than one field', async () => { await expect(new Rename('_foo').run(context, 'value', meta)).rejects.toThrow(); }); -describe('throws an error if using wildcards', () => { - it('in context fields', async () => { - const context = new ContextBuilder().setFields(['foo.*']).build(); - const meta: Meta = { req: {}, location: 'body', path: 'foo' }; - - await expect(new Rename('_foo').run(context, 'value', meta)).rejects.toThrow(); - }); - - it('in new path', async () => { - const context = new ContextBuilder().setFields(['foo']).build(); - const meta: Meta = { req: {}, location: 'body', path: 'foo' }; +it('throws an error if using wildcards in new path', async () => { + const context = new ContextBuilder().setFields(['foo']).build(); + const meta: Meta = { req: {}, location: 'body', path: 'foo' }; - await expect(new Rename('foo.*').run(context, 'value', meta)).rejects.toThrow(); - }); + await expect(new Rename('foo.*').run(context, 'value', meta)).rejects.toThrow(); }); it('throws an error if the new path is already assigned to a property', async () => { diff --git a/src/context-items/rename.ts b/src/context-items/rename.ts index 010e0993..743479d1 100644 --- a/src/context-items/rename.ts +++ b/src/context-items/rename.ts @@ -13,8 +13,8 @@ export class Rename implements ContextItem { if (context.fields.length !== 1) { throw new Error('Cannot rename multiple fields.'); } - const field = context.fields[0]; - if (field.includes('*') || this.newPath.includes('*')) { + + if (this.newPath.includes('*')) { throw new Error('Cannot use rename() with wildcards.'); }