Skip to content

Commit d59b6b5

Browse files
authored
ts: Add setField hook from feathers-authentication-hooks (#664)
* 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
1 parent 5898a56 commit d59b6b5

File tree

7 files changed

+284
-2
lines changed

7 files changed

+284
-2
lines changed

docs/hooks.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1681,6 +1681,99 @@ Prune values from related records. Calculate new values.
16811681
16821682
Works with <code>fastJoin</code> and <code>populate</code>.
16831683
1684+
## setField
1685+
1686+
The `setField` hook allows to set a field on the hook context based on the value of another field on the hook context.
1687+
1688+
|before|after|methods|multi|details|
1689+
|---|---|---|---|---|
1690+
|yes|yes|all|yes|[source](https://github.com/feathersjs-ecosystem/feathers-hooks-common/blob/master/src/hooks/set-field.ts)|
1691+
1692+
### Options
1693+
1694+
- `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'`).
1695+
- `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'`).
1696+
- `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.
1697+
1698+
> __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`).
1699+
1700+
### Examples
1701+
1702+
Limit all external access of the `users` service to the authenticated user:
1703+
1704+
> __Note:__ For MongoDB, Mongoose and NeDB `params.user.id` needs to be changed to `params.user._id`. For any other custom id accordingly.
1705+
1706+
```js
1707+
const { authenticate } = require('@feathersjs/authentication');
1708+
const { setField } = require('feathers-hooks-common');
1709+
1710+
app.service('users').hooks({
1711+
before: {
1712+
all: [
1713+
authenticate('jwt'),
1714+
setField({
1715+
from: 'params.user.id',
1716+
as: 'params.query.id'
1717+
})
1718+
]
1719+
}
1720+
})
1721+
```
1722+
1723+
Only allow access to invoices for the users organization:
1724+
1725+
```js
1726+
const { authenticate } = require('@feathersjs/authentication');
1727+
const { setField } = require('feathers-hooks-common');
1728+
1729+
app.service('invoices').hooks({
1730+
before: {
1731+
all: [
1732+
authenticate('jwt'),
1733+
setField({
1734+
from: 'params.user.organizationId',
1735+
as: 'params.query.organizationId'
1736+
})
1737+
]
1738+
}
1739+
})
1740+
```
1741+
1742+
Set the current user id as `userId` when creating a message and only allow users to edit and remove their own messages:
1743+
1744+
```js
1745+
const { authenticate } = require('@feathersjs/authentication');
1746+
const { setField } = require('feathers-hooks-common');
1747+
1748+
const setUserId = setField({
1749+
from: 'params.user.id',
1750+
as: 'data.userId'
1751+
});
1752+
const limitToUser = setField({
1753+
from: 'params.user.id',
1754+
as: 'params.query.userId'
1755+
});
1756+
1757+
app.service('messages').hooks({
1758+
before: {
1759+
all: [
1760+
authenticate('jwt')
1761+
],
1762+
create: [
1763+
setUserId
1764+
],
1765+
patch: [
1766+
limitToUser
1767+
],
1768+
update: [
1769+
limitToUser
1770+
]
1771+
remove: [
1772+
limitToUser
1773+
]
1774+
}
1775+
})
1776+
```
16841777
16851778
## setNow
16861779

docs/overview.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,10 @@ This documentation has several parts:
77
- [Hooks API](./hooks.md) - The API for the available hooks
88
- [Utilities API](./utilities.md) - The API for the available utility methods
99
- [Migrating](./migrating.md) - Information on how to migrate to the latest version of `feathers-hooks-common`
10-
- [Guides](./guides.md) - More in-depth guides for some of the available hooks
10+
- [Guides](./guides.md) - More in-depth guides for some of the available hooks
11+
12+
## Notable Changes
13+
14+
### 6.1.0
15+
16+
- **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)

src/hooks/set-field.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import _get from 'lodash/get';
2+
import _setWith from 'lodash/setWith';
3+
import _clone from 'lodash/clone';
4+
import _debug from 'debug';
5+
import { checkContext } from '../utils/check-context';
6+
import { Forbidden } from '@feathersjs/errors';
7+
import type { Hook } from '@feathersjs/feathers';
8+
import type { SetFieldOptions } from '../types';
9+
10+
const debug = _debug('feathers-hooks-common/setField');
11+
12+
/**
13+
* The `setField` hook allows to set a field on the hook context based on the value of another field on the hook context.
14+
* {@link https://hooks-common.feathersjs.com/hooks.html#setfield}
15+
*/
16+
export function setField (
17+
{ as, from, allowUndefined = false }: SetFieldOptions
18+
): Hook {
19+
if (!as || !from) {
20+
throw new Error('\'as\' and \'from\' options have to be set');
21+
}
22+
23+
return context => {
24+
const { params, app } = context;
25+
26+
if (app.version < '4.0.0') {
27+
throw new Error('The \'setField\' hook only works with Feathers 4 and the latest database adapters');
28+
}
29+
30+
checkContext(context, 'before', null, 'setField');
31+
32+
const value = _get(context, from);
33+
34+
if (value === undefined) {
35+
if (!params.provider || allowUndefined) {
36+
debug(`Skipping call with value ${from} not set`);
37+
return context;
38+
}
39+
40+
throw new Forbidden(`Expected field ${as} not available`);
41+
}
42+
43+
debug(`Setting value '${value}' from '${from}' as '${as}'`);
44+
45+
return _setWith(context, as, value, _clone);
46+
};
47+
}

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export { runHook } from './utils/run-hook';
2828
export { runParallel } from './hooks/run-parallel';
2929
export { sequelizeConvert } from './hooks/sequelize-convert';
3030
export { serialize } from './hooks/serialize';
31+
export { setField } from './hooks/set-field';
3132
export { setNow } from './hooks/set-now';
3233
export { setSlug } from './hooks/set-slug';
3334
export { sifter } from './hooks/sifter';
@@ -48,4 +49,4 @@ export { paramsForServer } from './utils/params-for-server';
4849
export { replaceItems } from './utils/replace-items';
4950
export { some } from './utils/some';
5051

51-
export * from "./types";
52+
export * from './types';

src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,9 @@ export interface ValidateSchemaOptions extends AjvOptions {
202202
export interface IffHook extends Hook {
203203
else(...hooks: Hook[]): Hook;
204204
}
205+
206+
export interface SetFieldOptions {
207+
as: string
208+
from: string
209+
allowUndefined?: boolean
210+
}

test/hooks/set-field.test.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import assert from 'assert';
2+
import feathers from '@feathersjs/feathers';
3+
import memory from 'feathers-memory';
4+
import { setField } from '../../src';
5+
6+
import type { Application } from '@feathersjs/feathers';
7+
8+
describe('setField', () => {
9+
const user = {
10+
id: 1,
11+
name: 'David'
12+
};
13+
14+
let app: Application;
15+
16+
beforeEach(async () => {
17+
app = feathers();
18+
app.use('/messages', memory());
19+
app.service('messages').hooks({
20+
before: {
21+
all: [setField({
22+
from: 'params.user.id',
23+
as: 'params.query.userId'
24+
})]
25+
}
26+
});
27+
await app.service('messages').create({
28+
id: 1,
29+
text: 'Message 1',
30+
userId: 1
31+
});
32+
await app.service('messages').create({
33+
id: 2,
34+
text: 'Message 2',
35+
userId: 2
36+
});
37+
});
38+
39+
it('errors when options not set', () => {
40+
assert.throws(() => app.service('messages').hooks({
41+
before: {
42+
// @ts-expect-error
43+
get: setField()
44+
}
45+
}));
46+
assert.throws(() => app.service('messages').hooks({
47+
before: {
48+
// @ts-expect-error
49+
get: setField({ as: 'me' })
50+
}
51+
}));
52+
assert.throws(() => app.service('messages').hooks({
53+
before: {
54+
// @ts-expect-error
55+
get: setField({ from: 'you' })
56+
}
57+
}));
58+
});
59+
60+
it('errors when used with wrong app version', async () => {
61+
app.version = '3.2.1';
62+
63+
await assert.rejects(async () => {
64+
await app.service('messages').get('testing');
65+
}, {
66+
message: 'The \'setField\' hook only works with Feathers 4 and the latest database adapters'
67+
});
68+
});
69+
70+
it('find queries with user information, does not modify original objects', async () => {
71+
const query = {};
72+
const results = await app.service('messages').find({ query, user });
73+
74+
assert.equal(results.length, 1);
75+
assert.deepEqual(query, {});
76+
});
77+
78+
it('adds user information to get, throws NotFound event if record exists', async () => {
79+
await assert.rejects(async () => {
80+
await app.service('messages').get(2, { user });
81+
}, {
82+
name: 'NotFound',
83+
message: 'No record found for id \'2\''
84+
});
85+
86+
const result = await app.service('messages').get(1, { user });
87+
88+
assert.deepEqual(result, {
89+
id: 1,
90+
text: 'Message 1',
91+
userId: 1
92+
});
93+
});
94+
95+
it('does nothing on internal calls if value does not exists', async () => {
96+
const results = await app.service('messages').find();
97+
98+
assert.equal(results.length, 2);
99+
});
100+
101+
it('errors on external calls if value does not exists', async () => {
102+
await assert.rejects(async () => {
103+
await app.service('messages').find({
104+
provider: 'rest'
105+
});
106+
}, {
107+
name: 'Forbidden',
108+
message: 'Expected field params.query.userId not available'
109+
});
110+
});
111+
112+
it('errors when not used as a before hook', async () => {
113+
app.service('messages').hooks({
114+
after: {
115+
get: setField({
116+
from: 'params.user.id',
117+
as: 'params.query.userId'
118+
})
119+
}
120+
});
121+
122+
await assert.rejects(async () => {
123+
await app.service('messages').get(1);
124+
}, {
125+
message: 'The \'setField\' hook can only be used as a \'before\' hook.'
126+
});
127+
});
128+
});

test/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const members = [
3939
'runParallel',
4040
'sequelizeConvert',
4141
'serialize',
42+
'setField',
4243
'setNow',
4344
'setSlug',
4445
'sifter',

0 commit comments

Comments
 (0)