diff --git a/docs/hooks.md b/docs/hooks.md index 83a8c2da..b9ec5fc9 100644 --- a/docs/hooks.md +++ b/docs/hooks.md @@ -277,6 +277,38 @@ Persistent, least-recently-used record cache for services. | `item` | `Object` | | The record. | | `clonedItem` | `Object` | | A clone of `item`. | +## capitalize + +Converts the first character of string to upper case and the remaining to lower case. + +|before|after|methods|multi|details| +|---|---|---|---|---| +|yes||create, update, patch|yes|[source](https://github.com/feathersjs-ecosystem/feathers-hooks-common/blob/master/src/hooks/capitalize.ts)| +||yes|all||| + + +- Arguments + - `{Array < String >} fieldNames` + +|Name|Type|Description| +|---|---|---| +|fieldNames|dot notation|The fields in the record(s) whose values are converted to lower case.| + +- **Example** + + ```js + const { capitalize } = require('feathers-hooks-common') + + module.exports = { + before: { + create: capitalize('email', 'username', 'div.dept') + } + } + ``` + +- **Details** + + Update either `context.data` (before hook) or `context.result[.data]` (after hook). ## debug @@ -2065,6 +2097,38 @@ Transform fields & objects in place in the record(s) using a recursive walk. Pow > [substack/js-traverse](https://github.com/substack/js-traverse) documents the extensive methods and context available to the transformer function. +## trim + +Removes leading and trailing whitespace or specified characters from string. + +|before|after|methods|multi|details| +|---|---|---|---|---| +|yes||create, update, patch|yes|[source](https://github.com/feathersjs-ecosystem/feathers-hooks-common/blob/master/src/hooks/trim.ts)| +||yes|all||| + + +- Arguments + - `{Array < String >} fieldNames` + +|Name|Type|Description| +|---|---|---| +|fieldNames|dot notation|The fields in the record(s) whose values are converted to lower case.| + +- **Example** + + ```js + const { trim } = require('feathers-hooks-common') + + module.exports = { + before: { + create: trim('email', 'username', 'div.dept') + } + } + ``` + +- **Details** + + Update either `context.data` (before hook) or `context.result[.data]` (after hook). ## unless diff --git a/src/common/transform-items.ts b/src/common/transform-items.ts index f537caf1..a814d4a5 100755 --- a/src/common/transform-items.ts +++ b/src/common/transform-items.ts @@ -7,6 +7,11 @@ export function transformItems > ( ): void { (Array.isArray(items) ? items : [items]).forEach(item => { fieldNames.forEach((fieldName: any) => { + const value = _get(item, fieldName); + if (value === undefined) { + return + } + transformer(item, fieldName, _get(item, fieldName)); }); }); diff --git a/src/hooks/capitalize.ts b/src/hooks/capitalize.ts new file mode 100755 index 00000000..4a6f4ca8 --- /dev/null +++ b/src/hooks/capitalize.ts @@ -0,0 +1,28 @@ +import _set from 'lodash/set'; +import _capitalize from 'lodash/capitalize'; +import { BadRequest } from '@feathersjs/errors'; + +import { transformItems } from '../common'; +import { checkContextIf } from './check-context-if'; +import { getItems } from '../utils/get-items'; +import type { Hook } from '@feathersjs/feathers'; + +/** + * Converts the first character of string to upper case and the remaining to lower case. + * {@link https://hooks-common.feathersjs.com/hooks.html#capitalize} + */ +export function capitalize (...fieldNames: string[]): Hook { + return (context: any) => { + checkContextIf(context, 'before', ['create', 'update', 'patch'], 'lowercase'); + + transformItems(getItems(context), fieldNames, (item: any, fieldName: any, value: any) => { + if (typeof value !== 'string' && value !== null) { + throw new BadRequest(`Expected string data. (lowercase ${fieldName})`); + } + + _set(item, fieldName, value ? _capitalize(value) : value); + }); + + return context; + }; +} diff --git a/src/hooks/lower-case.ts b/src/hooks/lower-case.ts index dce5820d..fb88c838 100755 --- a/src/hooks/lower-case.ts +++ b/src/hooks/lower-case.ts @@ -15,13 +15,11 @@ export function lowerCase (...fieldNames: string[]): Hook { checkContextIf(context, 'before', ['create', 'update', 'patch'], 'lowercase'); transformItems(getItems(context), fieldNames, (item: any, fieldName: any, value: any) => { - if (value !== undefined) { - if (typeof value !== 'string' && value !== null) { - throw new BadRequest(`Expected string data. (lowercase ${fieldName})`); - } - - _set(item, fieldName, value ? value.toLowerCase() : value); + if (typeof value !== 'string' && value !== null) { + throw new BadRequest(`Expected string data. (lowercase ${fieldName})`); } + + _set(item, fieldName, value ? value.toLowerCase() : value); }); return context; diff --git a/src/hooks/trim.ts b/src/hooks/trim.ts new file mode 100755 index 00000000..c93e9d21 --- /dev/null +++ b/src/hooks/trim.ts @@ -0,0 +1,27 @@ +import _set from 'lodash/set'; +import { BadRequest } from '@feathersjs/errors'; + +import { transformItems } from '../common'; +import { checkContextIf } from './check-context-if'; +import { getItems } from '../utils/get-items'; +import type { Hook } from '@feathersjs/feathers'; + +/** + * Removes leading and trailing whitespace or specified characters from string. + * {@link https://hooks-common.feathersjs.com/hooks.html#trim} + */ +export function trim (...fieldNames: string[]): Hook { + return (context: any) => { + checkContextIf(context, 'before', ['create', 'update', 'patch'], 'trim'); + + transformItems(getItems(context), fieldNames, (item: any, fieldName: any, value: any) => { + if (typeof value !== 'string' && value !== null) { + throw new BadRequest(`Expected string data. (lowercase ${fieldName})`); + } + + _set(item, fieldName, value ? value.trim() : value); + }); + + return context; + }; +} diff --git a/src/index.ts b/src/index.ts index 363a6bbc..f88063d7 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export { actOnDefault, actOnDispatch } from './hooks/act-on-dispatch'; export { alterItems } from './hooks/alter-items'; export { cache } from './hooks/cache'; +export { capitalize } from './hooks/capitalize'; export { checkContextIf } from './hooks/check-context-if'; export { debug } from './hooks/debug'; export { dePopulate } from './hooks/de-populate'; @@ -35,6 +36,7 @@ export { sifter } from './hooks/sifter'; export { softDelete } from './hooks/soft-delete'; export { stashBefore } from './hooks/stash-before'; export { traverse } from './hooks/traverse'; +export { trim } from './hooks/trim'; export { unless } from './hooks/unless' export { validate } from './hooks/validate'; export { validateSchema } from './hooks/validate-schema'; diff --git a/test/hooks/capitalize.test.ts b/test/hooks/capitalize.test.ts new file mode 100755 index 00000000..dc73c4a4 --- /dev/null +++ b/test/hooks/capitalize.test.ts @@ -0,0 +1,130 @@ + +import { assert } from 'chai'; +import { capitalize } from '../../src'; + +let hookBefore: any; +let hookAfter: any; +let hookFindPaginate: any; +let hookFind: any; + +describe('services capitalize', () => { + describe('updates data', () => { + beforeEach(() => { + hookBefore = { type: 'before', method: 'create', data: { first: 'John', last: 'DOE' } }; + hookAfter = { type: 'after', method: 'create', result: { first: 'jane', last: 'doE' } }; + hookFindPaginate = { + type: 'after', + method: 'find', + result: { + total: 2, + data: [ + { first: 'John', last: 'DOE' }, + { first: 'jane', last: 'doE' } + ] + } + }; + hookFind = { + type: 'after', + method: 'find', + result: [ + { first: 'John', last: 'DOE' }, + { first: 'jane', last: 'doE' } + ] + }; + }); + + it('updates hook before::create', () => { + capitalize('first', 'last')(hookBefore); + assert.deepEqual(hookBefore.data, { first: 'John', last: 'Doe' }); + }); + + it('updates hook after::find with pagination', () => { + capitalize('first', 'last')(hookFindPaginate); + assert.deepEqual(hookFindPaginate.result.data, [ + { first: 'John', last: 'Doe' }, + { first: 'Jane', last: 'Doe' } + ]); + }); + + it('updates hook after::find with no pagination', () => { + capitalize('first', 'last')(hookFind); + assert.deepEqual(hookFind.result, [ + { first: 'John', last: 'Doe' }, + { first: 'Jane', last: 'Doe' } + ]); + }); + + it('updates hook after', () => { + capitalize('first', 'last')(hookAfter); + assert.deepEqual(hookAfter.result, { first: 'Jane', last: 'Doe' }); + }); + + it('does not throw if field is missing', () => { + const hook: any = { type: 'before', method: 'create', data: { last: 'Doe' } }; + capitalize('first', 'last')(hook); + assert.deepEqual(hook.data, { last: 'Doe' }); + }); + + it('does not throw if field is undefined', () => { + const hook: any = { type: 'before', method: 'create', data: { first: undefined, last: 'doe' } }; + capitalize('first', 'last')(hook); + assert.deepEqual(hook.data, { first: undefined, last: 'Doe' }); + }); + + it('does not throw if field is null', () => { + const hook: any = { type: 'before', method: 'create', data: { first: null, last: 'doe' } }; + capitalize('first', 'last')(hook); + assert.deepEqual(hook.data, { first: null, last: 'Doe' }); + }); + + it('throws if field is not a string', () => { + const hook: any = { type: 'before', method: 'create', data: { first: 1, last: 'doe' } }; + assert.throws(() => { capitalize('first', 'last')(hook); }); + }); + }); + + describe('handles dot notation', () => { + beforeEach(() => { + hookBefore = { + type: 'before', + method: 'create', + data: { empl: { name: { first: 'john', last: 'doE' }, status: 'aa' }, dept: 'ACCT' } + }; + }); + + it('prop with no dots', () => { + capitalize('dept')(hookBefore); + assert.deepEqual(hookBefore.data, + { empl: { name: { first: 'john', last: 'doE' }, status: 'aa' }, dept: 'Acct' } + ); + }); + + it('prop with 1 dot', () => { + capitalize('empl.status')(hookBefore); + assert.deepEqual(hookBefore.data, + { empl: { name: { first: 'john', last: 'doE' }, status: 'Aa' }, dept: 'ACCT' } + ); + }); + + it('prop with 2 dots', () => { + capitalize('empl.name.first')(hookBefore); + assert.deepEqual(hookBefore.data, + { empl: { name: { first: 'John', last: 'doE' }, status: 'aa' }, dept: 'ACCT' } + ); + }); + + it('ignores bad or missing paths', () => { + capitalize('empl.xx.first')(hookBefore); + assert.deepEqual(hookBefore.data, + { empl: { name: { first: 'john', last: 'doE' }, status: 'aa' }, dept: 'ACCT' } + ); + }); + + it('ignores bad or missing no dot path', () => { + capitalize('xx')(hookBefore); + assert.deepEqual(hookBefore.data, + { empl: { name: { first: 'john', last: 'doE' }, status: 'aa' }, dept: 'ACCT' } + ); + }); + }); +}); diff --git a/test/hooks/trim.test.ts b/test/hooks/trim.test.ts new file mode 100755 index 00000000..4521d78a --- /dev/null +++ b/test/hooks/trim.test.ts @@ -0,0 +1,130 @@ + +import { assert } from 'chai'; +import { trim } from '../../src'; + +let hookBefore: any; +let hookAfter: any; +let hookFindPaginate: any; +let hookFind: any; + +describe('services trim', () => { + describe('updates data', () => { + beforeEach(() => { + hookBefore = { type: 'before', method: 'create', data: { first: ' John', last: 'Doe ' } }; + hookAfter = { type: 'after', method: 'create', result: { first: ' Jane', last: ' Doe' } }; + hookFindPaginate = { + type: 'after', + method: 'find', + result: { + total: 2, + data: [ + { first: ' John', last: 'Doe ' }, + { first: ' Jane ', last: ' Doe' } + ] + } + }; + hookFind = { + type: 'after', + method: 'find', + result: [ + { first: ' John', last: 'Doe ' }, + { first: ' Jane ', last: ' Doe' } + ] + }; + }); + + it('updates hook before::create', () => { + trim('first', 'last')(hookBefore); + assert.deepEqual(hookBefore.data, { first: 'John', last: 'Doe' }); + }); + + it('updates hook after::find with pagination', () => { + trim('first', 'last')(hookFindPaginate); + assert.deepEqual(hookFindPaginate.result.data, [ + { first: 'John', last: 'Doe' }, + { first: 'Jane', last: 'Doe' } + ]); + }); + + it('updates hook after::find with no pagination', () => { + trim('first', 'last')(hookFind); + assert.deepEqual(hookFind.result, [ + { first: 'John', last: 'Doe' }, + { first: 'Jane', last: 'Doe' } + ]); + }); + + it('updates hook after', () => { + trim('first', 'last')(hookAfter); + assert.deepEqual(hookAfter.result, { first: 'Jane', last: 'Doe' }); + }); + + it('does not throw if field is missing', () => { + const hook: any = { type: 'before', method: 'create', data: { last: 'Doe ' } }; + trim('first', 'last')(hook); + assert.deepEqual(hook.data, { last: 'Doe' }); + }); + + it('does not throw if field is undefined', () => { + const hook: any = { type: 'before', method: 'create', data: { first: undefined, last: 'Doe ' } }; + trim('first', 'last')(hook); + assert.deepEqual(hook.data, { first: undefined, last: 'Doe' }); + }); + + it('does not throw if field is null', () => { + const hook: any = { type: 'before', method: 'create', data: { first: null, last: ' Doe ' } }; + trim('first', 'last')(hook); + assert.deepEqual(hook.data, { first: null, last: 'Doe' }); + }); + + it('throws if field is not a string', () => { + const hook: any = { type: 'before', method: 'create', data: { first: 1, last: 'Doe ' } }; + assert.throws(() => { trim('first', 'last')(hook); }); + }); + }); + + describe('handles dot notation', () => { + beforeEach(() => { + hookBefore = { + type: 'before', + method: 'create', + data: { empl: { name: { first: ' John', last: 'Doe ' }, status: 'Aa ' }, dept: ' Acct ' } + }; + }); + + it('prop with no dots', () => { + trim('dept')(hookBefore); + assert.deepEqual(hookBefore.data, + { empl: { name: { first: ' John', last: 'Doe ' }, status: 'Aa ' }, dept: 'Acct' } + ); + }); + + it('prop with 1 dot', () => { + trim('empl.status')(hookBefore); + assert.deepEqual(hookBefore.data, + { empl: { name: { first: ' John', last: 'Doe ' }, status: 'Aa' }, dept: ' Acct ' } + ); + }); + + it('prop with 2 dots', () => { + trim('empl.name.first')(hookBefore); + assert.deepEqual(hookBefore.data, + { empl: { name: { first: 'John', last: 'Doe ' }, status: 'Aa ' }, dept: ' Acct ' } + ); + }); + + it('ignores bad or missing paths', () => { + trim('empl.xx.first')(hookBefore); + assert.deepEqual(hookBefore.data, + { empl: { name: { first: ' John', last: 'Doe ' }, status: 'Aa ' }, dept: ' Acct ' } + ); + }); + + it('ignores bad or missing no dot path', () => { + trim('xx')(hookBefore); + assert.deepEqual(hookBefore.data, + { empl: { name: { first: ' John', last: 'Doe ' }, status: 'Aa ' }, dept: ' Acct ' } + ); + }); + }); +}); diff --git a/test/index.test.ts b/test/index.test.ts index d8224de6..0de5d71a 100755 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -9,6 +9,7 @@ const members = [ 'cache', 'callingParams', 'callingParamsDefaults', + 'capitalize', 'checkContext', 'checkContextIf', 'combine', @@ -46,6 +47,7 @@ const members = [ 'softDelete', 'stashBefore', 'traverse', + 'trim', 'validate', 'validateSchema', 'iffElse',