diff --git a/src/chain/context-handler-impl.spec.ts b/src/chain/context-handler-impl.spec.ts index 6c2f41c1..80402f59 100644 --- a/src/chain/context-handler-impl.spec.ts +++ b/src/chain/context-handler-impl.spec.ts @@ -2,6 +2,7 @@ import { ContextBuilder } from '../context-builder'; import { ChainCondition, CustomCondition } from '../context-items'; import { check } from '../middlewares/check'; import { Bail } from '../context-items/bail'; +import { Rename } from '../context-items/rename'; import { ContextHandler, ContextHandlerImpl } from './'; let builder: ContextBuilder; @@ -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; } 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) { diff --git a/src/context-items/rename.spec.ts b/src/context-items/rename.spec.ts new file mode 100644 index 00000000..5c2be360 --- /dev/null +++ b/src/context-items/rename.spec.ts @@ -0,0 +1,58 @@ +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().setFields(['foo']).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(); +}); + +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(); +}); + +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..743479d1 --- /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.'); + } + + if (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(); + } +}