Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom methods the Feathers way #1976

Closed
daffl opened this issue Jun 2, 2020 · 15 comments · Fixed by #2270
Closed

Custom methods the Feathers way #1976

daffl opened this issue Jun 2, 2020 · 15 comments · Fixed by #2270

Comments

@daffl
Copy link
Member

daffl commented Jun 2, 2020

Custom methods are one of the most commonly requested features for Feathers. As the FAQ points out, it is possible to handle 90% of all use cases with the normal CRUD service interface and hooks for workflows. It does however still leave some edge cases that would be good if a service could expose additional methods externally. One reason for this limitation was that it is difficult to secure arbitrary methods properly. The new https://github.com/feathersjs/hooks general purpose hooks make this much easier.

This is why starting at v5, in addition to the existing service methods, it will be possible to register your own custom methods. A custom method has a name and fixed parameters of (data, params) where data is the payload and params is the usual service method parameters (including things like provider, authenticated user, query etc.). Method information can be passed as options to app.use (app.use(path, service, options)). This will also allow to more easily disable existing service methods (or their events) externally.

Example

class MyMessageService {
  async create (data, params) {}

  async findOne (data, params) {}

  async resendNotification (data, params) {}
}

app.use('/messages', new MyMessageService(), {
  methods: [ 'create', 'findOne', 'resendNotification' ],
  events: [ 'my-custom-event' ]
})

The available options are:

  • methods: A list of all publicly exposed methods. Will default to the standard service methods. When passed those will have to be added explicitly.
  • events: The custom service events (previous in service.events)

Calling via transports

  • Via REST a custom method can be called using the POST method and the X-Service-Method header. data will be the body payload.
  • Via websockets the method will be available as socket.emit(methodName, 'messages', { username: 'dave' }, query)
  • Feathers does not have an introspection mechanism, so when using the Feathers client, custom methods will have to be defined again explicitly on the client
  • In GraphQL the method will be exposed as a mutation under a unique name
@daffl daffl added this to the v5 (Dove) milestone Jun 2, 2020
@J3m5
Copy link

J3m5 commented Jun 2, 2020

I think that this is a great addition that will allow more flexibility for building APIs and help keeping the hooks lean and simple.

@webafra
Copy link

webafra commented Jun 3, 2020

hello .

thanks to public this option.

where route findOne in rest api ?
/message/:id ?

@bertho-zero
Copy link
Collaborator

I think that a custom method must also be able to have as arguments id, data, params or id, params.
To determine if there is id or not, we could apply this mechanism also on /messages/:id.


There is currently a way to expose custom methods to the desired routes with service.methods & httpMethod: #924 #945
The downside is for client-side use, the routes are just not predefined.

const { rest } = require('@feathersjs/express')

class MyMessageService {
  methods = {
    findOne: ['data', 'params'],
    resendNotification: ['data', 'params']
  }

  async create (data, params) {}

  @rest.httpMethod('POST', ':__feathersId/find-one')
  async findOne (data, params) {}

  @rest.httpMethod('POST', ':__feathersId/resend-notification')
  async resendNotification (data, params) {}
}

app.use('/messages', new MyMessageService())

We could do without service.methods and trust the parameters named by @feathersjs/hooks but it is no longer possible to know the name of the arguments before executing the function since feathersjs/hooks#37.

@daffl
Copy link
Member Author

daffl commented Jun 4, 2020

Agreed, supporting an id does make sense. I'd however like to avoid any route mapping that only works for a specific transport. The HTTP specification is also fairly clear on that a URI should only specify the resource location (service + resource id) and not contain any actions (verbs).

@J3m5
Copy link

J3m5 commented Jun 4, 2020

I also prefer keeping the service options as a separate object.

@bertho-zero
Copy link
Collaborator

@daffl I agree, the only thing that bothers me is the constraint of fixed parameters.

To determine if there is id or not, we could apply this mechanism also on /messages/:id.
To determine if there is data or not, we could use the verb GET rather than POST.
Params is always there as the last argument.

@bertho-zero
Copy link
Collaborator

I didn't understand what I was saying, surely fatigue. 😅

When the client makes his request, we do not have the url or the arguments that the function expects.

All we know is that this is not a basic method and that there are X arguments.

In my opinion, it's easier if the Feathers client always does a POST, and gives the array of arguments in body. Whether it's [id, data, query], [id, query], [data, query] or just [query].

It is only once the server receives the name of the method via the header and the array of arguments that it can look at the service and determine what each argument corresponds to in the array thanks to the signature of the function.

It will therefore be necessary to do a qs.parse(qs.stringify()) on the query on the server side to keep consistency with the other methods. The id usually coming from the url, it may be necessary to do the same thing.

@avimar
Copy link

avimar commented Jan 5, 2021

Calling via transports

  • Via REST a custom method can be called using the POST method and the X-Method-Override header. data will be the body payload.

I'm assuming the feathers rest client would handle this automatically?

Sounds very nice! I've had either methods outside the normal CRUD or wanted an additional custom function and it seemed appropriate to include it inside the same service.

@bwgjoseph
Copy link
Contributor

Hi, do you have to declare the default methods (create, patch, update, remove) as well when initializing the service? or just need to declare custom methods?

@daffl
Copy link
Member Author

daffl commented Mar 27, 2021

Currently all methods need to be defined. That way it is possible to completely disable a default method for external access (instead of having to rely on a hook that still runs the method).

@bwgjoseph
Copy link
Contributor

This way is to completely disable the method (e.g patch), but if I want to disable for certain transport (e.g REST), I would still need to register the method, and disable via hooks right?

@daffl
Copy link
Member Author

daffl commented Mar 30, 2021

@bwgjoseph I don't think I had a case where I wanted to disable for one transport but not another but yes, that would be correct.

@unconfident
Copy link

@bertho-zero is what you described in your comment above (#1976 (comment)) documented anywhere besides that comment? The use of rest.httpMethod decorator I mean.

I stumbled upon the Symbol and httpMethod method declarations while reading the code, googled it and that's how I got here. But this issue and those two pull requests you mentioned in your comment are the only things that show up when I search "httpMethod" within this repository or on the FeathersJS documentation site.

Relevant section of the FAQ page in the docs still suggests we define a separate service if we want a custom method at a custom path or do this using the hooks somehow as described in the answer for findOrCreate.

Just trying to pick the framework for the first time and migrate exiting API to it. Ability to define custom handlers was a corner stone for me and I would assume I'm not the only one who faced this. Would be nice if it was easier to discover

@daffl
Copy link
Member Author

daffl commented May 5, 2021

That decorator will no longer be available in v5 and later (but a compatibility wrapper is available mentioned in #2270 (comment)).

In general, Feathers aims at letting you implement your API without having to worry about the transport (and especially HTTP) specifics, instead providing set of best practises and design patterns that work for any transport protocol. The API outlined here, in #2270 and in the prerelease documentation is the recommended way going forward for custom functionality and works for both, HTTP and websockets.

@unconfident
Copy link

Oh, that's good to know. Thank you so much for the heads up!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants