feathersjs-ecosystem / feathers-hooks-common Goto Github PK
View Code? Open in Web Editor NEWUseful hooks for use with FeathersJS services.
Home Page: https://hooks-common.feathersjs.com
License: MIT License
Useful hooks for use with FeathersJS services.
Home Page: https://hooks-common.feathersjs.com
License: MIT License
What do you guys think about a $count
special query param? Where it literally just returns the number in place of hook.result
. The reason I ask is knowing to set $limit: 0
is a bit esoteric and feels like a hack. Plus with the new populate hook if you wanted to just get a count of the related items you can do
function(hook) {
if(hook.params.query.$count) {
hook.params.query.$limit = 0;
delete hook.params.query.$count;
}
}
I still think it can be implemented with a fairly simple hook for all cases by setting hook.id:
function slugId(name, idField = '_id') {
const methods = [ 'get', 'update', 'patch', 'remove' ];
return function(hook) {
if(methods.indexOf(hook.method) !== -1) {
const query = {
[name]: hook.id
};
return hook.service.find({
query,
paginate: false
}).then(result => {
if(result.length === 1) {
hook.id = result[0][idField];
}
return hook;
});
}
}
}
This can then be initialized for all services with
app.hooks({
before: slugId('slug')
});
Should we have every hook that uses hook.result first normalize it?
// If it's a mongoose model then
if (typeof obj.toObject === 'function') {
obj = obj.toObject();
}
// If it's a Sequelize model
else if (typeof obj.toJSON === 'function') {
obj = obj.toJSON();
}
I believe there is an app wide call to do this after the CRUD operation. But it seems we keep getting questions on Slack from people who are new to Feathers.
Run CI with Node 4, 6 and latest (the Node versions that Feathers will officially support starting next year)
Consider if something should be done re feathersjs/feathers#450 (comment)
Waiting till a Node-based one is generated.
feathersjs/feathers#433
either
https://github.com/rafinskipg/git-changelog, or
https://github.com/conventional-changelog/conventional-changelog-cli
We just released a new plugin generator at https://github.com/feathersjs/generator-feathers-plugin that includes some changes that all other repositories should be updated to as well so that they all share the same infrastructure. Changes for this repository would include:
_spec.js
files to .test.js
(for consistency with the other repositories)@eddyystop I can create a pull request with those changes if they are ok for you.
When we drop support for Node v4 in 2018 we can test func's directly instead of their signatures
Hooks should not return arrow functions because the context (this
) is the surrounding context, not the service (which is being set on normal functions).
From documentation:
Sync functions return either an error object or null.
The following validation function do nothing.
function messageValidator(values) {
return new Error('Error message');
}
Promise functions should throw on an error.
Throw on an error or reject with an error object?
Idea for a new hook from bede
"Has anyone here had to develop a pattern for ‘debouncing’ (not quite debouncing) requests using feathers-client? e.g. I have two calls to the same find, but only care about the result of one:"
app('projects').find(…);
// While request is happening request is made again
app('projects').find(…);
// Should just return promise from first request
If we have a system of categories and subcategories, we have:
/*
Category (parent)
v
Category (child)
*/
{
service: 'category',
nameAs: 'subCategories',
parentField: 'category_id',
childField: 'id',
include: [ ... ],
}
/*
Category (child)
v
Category (parent)
*/
{
service: 'category',
nameAs: 'parentCategories',
parentField: 'parent_id', // but this field is on the child in this case
childField: 'id', // but this field is on the parent in this case
include: [ ... ],
}
I think that as for relational databases, like mySQL, you should rename parentField to localField and childField to foreignField.
Which will give:
/*
Category (parent)
v
Category (child)
*/
{
service: 'category',
nameAs: 'subCategories',
localField: 'category_id',
foreignField: 'id',
include: [ ... ],
}
/*
Category (child)
v
Category (parent)
*/
{
service: 'category',
nameAs: 'parentCategories',
localField: 'parent_id',
foreignField: 'id',
include: [ ... ],
}
Code here means to disable when the result is false, not to disable when the result is true.
if (typeof realm === 'function') {
return function (hook) {
const result = realm(hook);
const update = check => {
if (!check) {
throw new errors.MethodNotAllowed(`Calling '${hook.method}' not allowed. (disable)`);
}
};
}
But in docs here it seems to be opposite logic:
// Disable the remove hook if the user is not an admin
remove: hooks.disable(function(hook) {
return !hook.params.user.isAdmin
})
Either the doc or the code is wrong. I guest it's the code which is wrong.
An attempt at a design to start discussion.
I'm hoping for
const populates = {
// a straight forward set of populates taken from MichaelErmer/feathers-populate-hook
message: {
user: { // what @ekryski proposed. Temp renames to localField & foreignField for clarity.
service: 'users', localField: '_id', foreignField: 'userId',
on: 'result', multiple: false, query: {}
},
comments: {
service: 'comments', localField: '_id', foreignField: 'commentIds' /* is array */,
on: 'result', multiple: true, query: {}
},
categories: {},
creator: {},
},
// populate a room with all its messages, further populating each message.
room: {
creator: {},
messages: {
service: 'messages', localField: '_id', foreignField: 'messageIds',
on: 'result', multiple: true, query: {},
expandBy: populates.message, // expand every message with populates.message
},
owners: {},
},
// populate an organization with its rooms, partially further populating each room
organization: {
owners: {},
rooms: {
service: 'rooms', localField: '_id', foreignField: 'roomIds',
on: 'result', multiple: true, query: {},
expandBy: [populates.room, 'messages', 'owners'], // don't expand using populates.room.creator
}
},
};
const organizations = app.service('organizations');
organizations.after({
find: [
// the fancy populating
hook.population(populates.organization),
// You can continue to populate an item the same way as with the current populate
hook.population({
creator: {
service: 'users', localField: '_id', foreignField: 'creatorId',
on: 'result', multiple: false, query: {}
}
}),
]
});
Not just remove & find.
I found many of hooks in this package has been added to feathers-hooks out-of-box?
/*
This thread concerns integrating the new feathers-permissions with the new populate++, serialize, etc.
Here are my thoughts.
(1) Populate: Let's take Purchase Requests as an example. Accounting UIs want to show things like invoices and general ledger numbers along with the PR. Purchasing UIs want to show things like receiving slips along with the PR. I would argue that the app decides what type of populate it needs and the hooks have almost no decisions to make.
We can't of course allow a curl command to get whatever it wants, even if another curl was first used to authenticate to a random user. So perhaps populate++ can ensure an Accounting PR populate is at least requested by a user/API/IoT that has Accounting permissions.
So maybe we can have
const populations = {
favorites: {
// alternative syntax
permissions: 'acct',
permissions: ['aaa', 'bbb'],
permissions: (hook) => true,
include: {
Do we allow permissions
just at the base record, or do we allow them on every include
'd table? I would like populate structures to be composable though I'm not sure how effective that would be in practice. permissions
at every table would be needed.
(2) Serialize: Let's take Payroll as an example, and that we have a populated Employee item. The Payroll Manager is allowed to see the salaries of other department heads, while Payroll Clerks are not. The situation can grow into something more complicated.
I think the server has to decide on the proper serialization and call hook.serialize(serializers.employee_manager)
with the correct serialization schema from
const serializers = {
employee_manager: { ... },
employee_clerk: { ... },
We can standardize this with something like
const serializerPermissions = {
employee : [
{ permissions: ['payroll-mgr'], serializer: serializers.employee_manager },
{ permissions: 'payroll-clerk', serializer: serializers.employee_clerk },
]
}
hooks.serializeWithPermissions((hook) => serializers.employee_manager)
// or
hooks.serializeWithPermissions(serializerPermissions.employee)
The serializer decision occurs only at the base record.
(3) If populate and serialize do have different permission needs, then @ekryski 's proposal to separate the hooks was even more smart.
(4) Concerns
or
s on the permissions.I'd like to do some async validation and use app
and some services
to do so, but the default validation
hook is only useful for very basic stuff that can be done locally with the data provided.
I started wrapping my validation in a higher order function to pass the app
, but hooks/index.js that feathers generates are straight up functions.
Convert to these where possible https://github.com/feathersjs/generator-feathers-plugin/tree/master/app/templates
Code Climate complains about duplicate code even if its in different modules. I found that did not work well for me with unit tests as I want each test to be as obvious as possible. I'll look into how to ignore test files.
(First please check that this issue is not already solved as described
here)
common.iff(
custom.isSuperAdmin(),
common.pluck('username', 'firstName', 'lastName', 'money'),
common.pluck('username', 'firstName', 'lastName'),
)
Allow an else
condition on iff
hook. Above is not possible. Adding a third parameter for the else
condition. In some cases, this might give the possibility to chain iff
hook.
Above code is not possible.
Tell us about the applicable parts of your setup.
Module versions (especially the part that's not working):
NodeJS version: 7.2.0
Operating System: macOS 10.12
I just noticed that in the published npm code, the softDelete is compiled from
return this.patch(hook.id, hook.data, hook.params).then(data => {
to
return undefined.patch(hook.id, hook.data, hook.params).then(function (data) {
Don't think that's correct. I'll dig around more tomorrow if I get a minute.
This hook (https://www.npmjs.com/package/feathers-populate-hook) has come up in the community and does some great things but also I think overcomplicates things a bit.
A few things that we've seen a need for through experience or what people have mentioned:
The ability to specify a location on the hook object to put the populated object. (ie. params
, data
, result
). We frequently will populate an entity into params so that fields can be referenced for later but they are not the entity that is actively being modified. An example, would be:
After a payment we need to email a receipt. So we need to have the products bought, the user to send to, and the company they belong to in order to get the shipping address. The
order
is the entity being modified by acreate
but we need to populate both the user and the business to hook.params for reference when setting up the email payload.
Being able to select fields from a populated entity. (this could just be a second hook that plucks those attributes, and likely should be)
Being able to populate multiple entities (ie. user and business). I think this can be resolved by simply registering the populate hook twice.
Being able to populate multiple of the same entity (ie. all comments on a post)
Being able to allow the client to specify what to populate and which fields via $populate
. This would be very useful but starts to tread into GraphQL territory and might become unnecessarily complex or an expensive operation.
Now this may be more of a meta issue to discuss the possibility and viability of supporting these requirements. Not all of the requirements are valid in my mind or can be easily solved as I had mentioned above.
commonHooks.populate({
user: { // destination key
service: 'users', // can be a string or an actual service object
field: 'adminId', // field to use for query, only required if different from destination key
on: 'result' // where to put on the hook object. could be 'data', 'params', or 'result'
multiple: true // defaults to false and returns one,
query: { // defaults to {service.id: this.field} but can be any standard Feathers query object
$select: ['name', 'email']
}
}
})
const { iff, isProvider, do } from 'feathers-hooks-common';
service.before({
create: iff(isProvider('external'), do([hook1, hook2, hook3]),
});
iff()
avoided for the same condition.feathers-hook-common
creates a promise chain out of the array using code from feathers-hooks
. feathers-hooks
uses that chain without changes to itself.Comments @daffl ?
It would be nice to have a hook that deeply inspects hook.data
and removes whitespace. Node already has the String.trim
function, so this would be a fairly straightforward hook to implement.
e.g, before:
hook.data = {
foo: ' not trimmed ',
bar: {
baz: ' needs trimming\t\n '
},
bam: 'trimmed'
};
and after:
hook.data = {
foo: 'not trimmed',
bar: {
baz: 'needs trimming'
},
bam: 'trimmed'
}
Some DBs can specify relationships in their method calls. This would be significantly faster than using the populate hook. So is there a hook that could aid this for such DBs?
Potential approaches:
(1) https://github.com/BestBuy/api-playground/blob/master/src/services/products/hooks/index.js
(2) https://github.com/lionelrudaz/api.wellnow.ch
https://github.com/seyz/jsonapi-serializer
(3) From j214e on slack channel
let _app;
/* usage:
setModelRelations(app, {
categories:{
hasMany: {
products: {foreignKey: 'belongsToCategory'}
}
}
})
*/
export function setModelRelations(app, relations) {
_app = app;
for (let service in relations) {
if (relations.hasOwnProperty(service)) {
if (relations[service].hasMany) {
const targets = relations[service].hasMany;
for(let targetService in targets){
if(targets.hasOwnProperty(targetService)){
_hasMany(service, targetService, targets[targetService]);
}
}
}
if (relations[service].belongsTo) {
const targets = relations[service].belongsTo;
for(let targetService in targets){
if(targets.hasOwnProperty(targetService)){
_belongsTo(service, targetService, targets[targetService]);
}
}
}
}
}
}
function _model(service){
return _app.service(service).Model
}
function _belongsTo(service, targetService, options){
_model(service).belongsTo(_model(targetService),options)
}
function _hasMany(service, targetService, options){
_model(service).hasMany(_model(targetService),options)
}
Idea for a new hook:
I still think it can be implemented with a fairly simple hook for all cases by setting hook.id:
function slugId(name, idField = '_id') {
const methods = [ 'get', 'update', 'patch', 'remove' ];
return function(hook) {
if(methods.indexOf(hook.method) !== -1) {
const query = {
[name]: hook.id
};
return hook.service.find({
query,
paginate: false
}).then(result => {
if(result.length === 1) {
hook.id = result[0][idField];
}
return hook;
});
}
}
}
This can then be initialized for all services with
app.hooks({
before: slugId('slug')
});
unless allows a boolean, Promise or a function as the predicate.
The new features originally described here have been removed. An array of hooks can be created using JS and then
service.before({
create: hookArray
});
I'll add this to the docs.
JS can continue to use common.iff()
as that's cleaner for async than writing JS code.
Seems utils
can be imported from feathers-hooks-common/lib/utils
, rather than feathers-hooks-common/utils
as per README.
Not sure if this is an issue with the readme or the implementation, or my system could be just screwing up - but when I look for a util function e.g. checkContext
, I need to look for as per above, rather than what's documented.
exports.after = {
all: [],
find: [function (hook) {
hook.app.response.statusCode = 418
}],
...
While the new populate hook supports getByDot
with the parent and child fields, it does not permit a dot string in the nameAs
field. This would also be helpful to have so that all fields can be populated directly on their parentField
.
Wait until separate subpages for hook categories are merged into AUK release docs.
hook.result = { users: [
{ password...},
{ password...},
{ password...},
] };
What other hooks could use this? removeQuery, pluck, populate, ...
Idea for new hook arising from a comment by someone
"This may be a total noob question. How to restrict a complete hierarchy of services to a user.
I have a budget that has items in it, each item has events
how to assure that all items and events can only be viewed / edited by their owner ?
I thought it would be auth.restrictToOwner() but that does not work on find()"
Just installed latest version and it's giving me 2.0.2 - looks like 2.0.3 hasn't been published onto NPM yet?
// service.get(someOtherField, params)
app.service('myservice').hooks({
before: {
get(hook) {
return this.find({
query: { somOtherField: hook.id }
}).then(page => {
const { data } = page;
if(data.length === 0) {
throw new Error('Not found');
}
hook.result = data[0];
return hook
});
}
}
});
fnPromisifyCallback is documented. The other fn* funcs are not.
Decide if we want the others to be available or not.
Essentially it would take a hook function, and run it outside of the rest of the hooks - so that if an error occurs, it doesn't break the request, or if it will potentially take a long time it won't cause lags. Where I've used something like this is for analytics or low priority notifications.
Obviously you could just bake the functionality directly into the hook, but if you're consuming 3rd party hooks, or have a hook you sometimes want to depend on, that wouldn't work.
Is this common enough to be bundled into hooks-common
/ is there a pattern like this already for feathers?
I'm thinking something like this:
function defer(handler) {
return function(hook) {
setImmediate(() => handler.call(this, hook));
return hook;
}
}
app.service('users').after({
create: [ defer(notify('User Created')) ]
});
Happy to make a PR
function betterSetByDot(item, key, value, ifDelete) {
if (!key.contains(',')) {
item[key] = value;
if (value === undefined && ifDelete) {
delete item[key];
}
return;
}
setByDot(item, key, value, ifDelete);
}
and then update serialize
node 7.1.0
"dependencies": {
"feathers": "^2.0.2",
"feathers-hooks": "^1.6.1",
"feathers-hooks-common": "^2.0.1",
"feathers-rest": "^1.5.2"
}
const feathers = require('feathers');
const rest = require('feathers-rest');
const hooks = require('feathers-hooks');
const commonHooks = require('feathers-hooks-common');
const app = feathers()
.configure(rest())
.configure(hooks());
const data = [{
text: 'one'
}, {
text: 'two',
deleted: true
}];
const messageService = app.service('/messages', {
find(params) {
return Promise.resolve(data);
}
});
messageService.before({
find: commonHooks.softDelete()
});
app.listen(3000);
TypeError: Cannot read property 'hasOwnProperty' of undefined
at /Volumes/Data/development/dev/two/node_modules/feathers-hooks-common/lib/utils.js:49:16
at Array.reduce (native)
at setByDot (/Volumes/Data/development/dev/two/node_modules/feathers-hooks-common/lib/utils.js:47:16)
at Object.<anonymous> (/Volumes/Data/development/dev/two/node_modules/feathers-hooks-common/lib/new.js:31:27)
at process._tickCallback (internal/process/next_tick.js:103:7)
Recently I've been wondering if the best way to maintain this growing number of hooks would be to migrate each one into their own repository/module (potentially in a separate feathers-hooks
organization):
feathers-hooks-common
could be because of an API change of any of the bundled hooks. People will have to go through all the upgrading steps just to find out that they weren't even using that hook.Alternative: Another option would be a monorepo structure like Babel but I am not too familiar with that (it seems you will have to automate everything in a very similar way).
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.