Skip to content

Commit

Permalink
ts: Add setField hook from feathers-authentication-hooks (#664)
Browse files Browse the repository at this point in the history
* feat(set-field): new hook 'set-field'

* chore: use assert instead of assert/strict

* docs: copy 'setField' docs from feathers-authentication-hooks

* docs: add notable changes section on overview

* docs: add jsdoc for setField
  • Loading branch information
fratzinger committed May 22, 2022
1 parent 5898a56 commit d59b6b5
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 2 deletions.
93 changes: 93 additions & 0 deletions docs/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -1681,6 +1681,99 @@ Prune values from related records. Calculate new values.
Works with <code>fastJoin</code> and <code>populate</code>.
## setField
The `setField` hook allows to set a field on the hook context based on the value of another field on the hook context.
|before|after|methods|multi|details|
|---|---|---|---|---|
|yes|yes|all|yes|[source](https://github.com/feathersjs-ecosystem/feathers-hooks-common/blob/master/src/hooks/set-field.ts)|
### Options
- `from` *required* - The property on the hook context to use. Can be an array (e.g. `[ 'params', 'user', 'id' ]`) or a dot separated string (e.g. `'params.user.id'`).
- `as` *required* - The property on the hook context to set. Can be an array (e.g. `[ 'params', 'query', 'userId' ]`) or a dot separated string (e.g. `'params.query.userId'`).
- `allowUndefined` (default: `false`) - If set to `false`, an error will be thrown if the value of `from` is `undefined` in an external request (`params.provider` is set). On internal calls (or if set to true `true` for external calls) the hook will do nothing.
> __Important:__ This hook should be used after the [authenticate hook](https://docs.feathersjs.com/api/authentication/hook.html#authenticate-options) when accessing user fields (from `params.user`).
### Examples
Limit all external access of the `users` service to the authenticated user:
> __Note:__ For MongoDB, Mongoose and NeDB `params.user.id` needs to be changed to `params.user._id`. For any other custom id accordingly.
```js
const { authenticate } = require('@feathersjs/authentication');
const { setField } = require('feathers-hooks-common');

app.service('users').hooks({
before: {
all: [
authenticate('jwt'),
setField({
from: 'params.user.id',
as: 'params.query.id'
})
]
}
})
```
Only allow access to invoices for the users organization:
```js
const { authenticate } = require('@feathersjs/authentication');
const { setField } = require('feathers-hooks-common');

app.service('invoices').hooks({
before: {
all: [
authenticate('jwt'),
setField({
from: 'params.user.organizationId',
as: 'params.query.organizationId'
})
]
}
})
```
Set the current user id as `userId` when creating a message and only allow users to edit and remove their own messages:
```js
const { authenticate } = require('@feathersjs/authentication');
const { setField } = require('feathers-hooks-common');

const setUserId = setField({
from: 'params.user.id',
as: 'data.userId'
});
const limitToUser = setField({
from: 'params.user.id',
as: 'params.query.userId'
});

app.service('messages').hooks({
before: {
all: [
authenticate('jwt')
],
create: [
setUserId
],
patch: [
limitToUser
],
update: [
limitToUser
]
remove: [
limitToUser
]
}
})
```
## setNow
Expand Down
8 changes: 7 additions & 1 deletion docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,10 @@ This documentation has several parts:
- [Hooks API](./hooks.md) - The API for the available hooks
- [Utilities API](./utilities.md) - The API for the available utility methods
- [Migrating](./migrating.md) - Information on how to migrate to the latest version of `feathers-hooks-common`
- [Guides](./guides.md) - More in-depth guides for some of the available hooks
- [Guides](./guides.md) - More in-depth guides for some of the available hooks

## Notable Changes

### 6.1.0

- **new hook `setField`**: The `setField` hook allows to set a field on the hook context based on the value of another field on the hook context. [see docs](./hooks.md#setfield)
47 changes: 47 additions & 0 deletions src/hooks/set-field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import _get from 'lodash/get';
import _setWith from 'lodash/setWith';
import _clone from 'lodash/clone';
import _debug from 'debug';
import { checkContext } from '../utils/check-context';
import { Forbidden } from '@feathersjs/errors';
import type { Hook } from '@feathersjs/feathers';
import type { SetFieldOptions } from '../types';

const debug = _debug('feathers-hooks-common/setField');

/**
* The `setField` hook allows to set a field on the hook context based on the value of another field on the hook context.
* {@link https://hooks-common.feathersjs.com/hooks.html#setfield}
*/
export function setField (
{ as, from, allowUndefined = false }: SetFieldOptions
): Hook {
if (!as || !from) {
throw new Error('\'as\' and \'from\' options have to be set');
}

return context => {
const { params, app } = context;

if (app.version < '4.0.0') {
throw new Error('The \'setField\' hook only works with Feathers 4 and the latest database adapters');
}

checkContext(context, 'before', null, 'setField');

const value = _get(context, from);

if (value === undefined) {
if (!params.provider || allowUndefined) {
debug(`Skipping call with value ${from} not set`);
return context;
}

throw new Forbidden(`Expected field ${as} not available`);
}

debug(`Setting value '${value}' from '${from}' as '${as}'`);

return _setWith(context, as, value, _clone);
};
}
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export { runHook } from './utils/run-hook';
export { runParallel } from './hooks/run-parallel';
export { sequelizeConvert } from './hooks/sequelize-convert';
export { serialize } from './hooks/serialize';
export { setField } from './hooks/set-field';
export { setNow } from './hooks/set-now';
export { setSlug } from './hooks/set-slug';
export { sifter } from './hooks/sifter';
Expand All @@ -48,4 +49,4 @@ export { paramsForServer } from './utils/params-for-server';
export { replaceItems } from './utils/replace-items';
export { some } from './utils/some';

export * from "./types";
export * from './types';
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,9 @@ export interface ValidateSchemaOptions extends AjvOptions {
export interface IffHook extends Hook {
else(...hooks: Hook[]): Hook;
}

export interface SetFieldOptions {
as: string
from: string
allowUndefined?: boolean
}
128 changes: 128 additions & 0 deletions test/hooks/set-field.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import assert from 'assert';
import feathers from '@feathersjs/feathers';
import memory from 'feathers-memory';
import { setField } from '../../src';

import type { Application } from '@feathersjs/feathers';

describe('setField', () => {
const user = {
id: 1,
name: 'David'
};

let app: Application;

beforeEach(async () => {
app = feathers();
app.use('/messages', memory());
app.service('messages').hooks({
before: {
all: [setField({
from: 'params.user.id',
as: 'params.query.userId'
})]
}
});
await app.service('messages').create({
id: 1,
text: 'Message 1',
userId: 1
});
await app.service('messages').create({
id: 2,
text: 'Message 2',
userId: 2
});
});

it('errors when options not set', () => {
assert.throws(() => app.service('messages').hooks({
before: {
// @ts-expect-error
get: setField()
}
}));
assert.throws(() => app.service('messages').hooks({
before: {
// @ts-expect-error
get: setField({ as: 'me' })
}
}));
assert.throws(() => app.service('messages').hooks({
before: {
// @ts-expect-error
get: setField({ from: 'you' })
}
}));
});

it('errors when used with wrong app version', async () => {
app.version = '3.2.1';

await assert.rejects(async () => {
await app.service('messages').get('testing');
}, {
message: 'The \'setField\' hook only works with Feathers 4 and the latest database adapters'
});
});

it('find queries with user information, does not modify original objects', async () => {
const query = {};
const results = await app.service('messages').find({ query, user });

assert.equal(results.length, 1);
assert.deepEqual(query, {});
});

it('adds user information to get, throws NotFound event if record exists', async () => {
await assert.rejects(async () => {
await app.service('messages').get(2, { user });
}, {
name: 'NotFound',
message: 'No record found for id \'2\''
});

const result = await app.service('messages').get(1, { user });

assert.deepEqual(result, {
id: 1,
text: 'Message 1',
userId: 1
});
});

it('does nothing on internal calls if value does not exists', async () => {
const results = await app.service('messages').find();

assert.equal(results.length, 2);
});

it('errors on external calls if value does not exists', async () => {
await assert.rejects(async () => {
await app.service('messages').find({
provider: 'rest'
});
}, {
name: 'Forbidden',
message: 'Expected field params.query.userId not available'
});
});

it('errors when not used as a before hook', async () => {
app.service('messages').hooks({
after: {
get: setField({
from: 'params.user.id',
as: 'params.query.userId'
})
}
});

await assert.rejects(async () => {
await app.service('messages').get(1);
}, {
message: 'The \'setField\' hook can only be used as a \'before\' hook.'
});
});
});
1 change: 1 addition & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const members = [
'runParallel',
'sequelizeConvert',
'serialize',
'setField',
'setNow',
'setSlug',
'sifter',
Expand Down

0 comments on commit d59b6b5

Please sign in to comment.