api icon indicating copy to clipboard operation
api copied to clipboard

Document actions

Open mingard opened this issue 6 years ago • 17 comments

Observation

Options for document-related actions that don't inject a history state are limited.

Example actions that would not require a payload-driven update to a document

  • Refund an order in an orders collection
  • Reindex a document (think elasticsearch)
  • Resend an invitation email

In these examples, the operation requires the contents of a document, but doesn't necessarily require an update. These are document actions.

Current options

  • Create a hook that fires on GET, but only when a specific parameter (which must be removed before saving if it isn't in the schema) exists in the payload.

Example payload

{
  "refund": true
}
  • Create a custom endpoint that accepts the document _id and in turn retrieves the document from API, before delivering the required fields to the payment processor.

Concept

Similar to hooks, introduce actions

const smtp = require('smtp')

const Action = function (documents) {
  document.forEach(doc => {
    smtp.send({
      email: doc.email,
      message: doc.message
    })
  })
}

** Attach actions to collections settings block**

{
  "actions": {
    "refund":  {
      "title": "Refund Order"
    }
  }
}

Usage in Publish

Without any extra configuration, Publish could render all action buttons at the bottom of each document.

These could be extended with parameters based on the document state e.g.

{
  "actions": {
    "refund":  {
      "title": "Refund Order",
      "requires": {
        "paid": true,
        "refunded": false
      }
    }
  }
}

mingard avatar Oct 09 '17 18:10 mingard

Your examples make perfect sense and I can see other use cases where this might be useful.

I also agree that hooks are the wrong answer to this, because hooks are meant to be used for custom logic that complements the document endpoints, so they're meant to be used as part of normal CRUD operations. Something like resending an email does not necessarily involve a CRUD operation, so hooks wouldn't make sense.

However, as I mentioned earlier in private, I think custom endpoints are a perfect fit for this. I think the code for your send email action is a bit over-simplified in comparison to what actions would need to contain, because you'd need to be able to describe the behaviour for different HTTP verbs (i.e. some actions would just receive GET requests, others could need POST and/or PUT). So we're really talking about building custom controllers that would run arbitrary logic for each of the HTTP verbs you're interested in. Which is exactly what custom endpoints do.

From docs on custom endpoints:

module.exports.get = function (req, res, next) {

}

module.exports.post = function (req, res, next) {

}

From what I can see, the only difference between what you're proposing and the existing custom endpoints is:

  1. The URL for actions would sit within the structure currently used by normal document endpoints (e.g. /1.0/database/users/1234/send-email instead of /1.0/send-email?user=1234)
  2. Actions would have immediate access to the associated documents rather than having to query them manually using the document model

But I don't think these reasons justify the trouble of adding a new feature because:

  1. You can customise the URL of custom endpoints to your liking, including the ability to define URL parameters to avoid relying on query string parameters.

    module.exports.config = function () {
      return {
        route: '/1.0/send-email/:userId'
      }
    }
    

    With this, you could essentially construct the URLs you'd get with actions.

  2. We have plans for making the document model more friendly and easier to work with (see #177). We could even look at modifying API wrapper to directly interact with the model rather than making an external request.

Also, I see some disadvantages with introducing this feature. We're introducing yet another method for running custom logic in API, in addition to hooks and custom endpoints. This means:

  • The burden of testing, documenting and maintaining all these different methods
  • Offering people different, disconnected methods for achieving the same thing. I think this makes it harder for us to evangelise the product
  • It also makes our job of debugging client projects a bit easier if we don't have people doing things in lots of different ways

Now, if you really think reasons 1 and 2 above are a deal-breaker and justify introducing a new concept, I would propose that we make actions just aliases of custom endpoints, with the ability to automatically make documents available to them.

For example, hitting GET /1.0/database/users/1234/send-email (an action) would retrieve the document with {_id: 1234}, find a custom endpoint under 1.0 called send-email with a GET handler, and call it with the document retrieved.

This would solve the problem of the URL structure and the ease of accessing the document (although I personally think it's not necessary).

eduardoboucas avatar Oct 10 '17 10:10 eduardoboucas

I agree with those points - custom endpoints were designed for adding additional logic to API and should be able to handle all these use cases.

The onus is really on the individual developer implementing API to ensure that appropriate tests and documentation are included.

jimlambie avatar Oct 10 '17 10:10 jimlambie

I don't agree. With that logic, hooks need not exist. They could easily be custom endpoint.

The point here is simplicity - as with hooks, delivering the payload to the module without the addition of a model query makes actions easier to build. Their ability to easily extend the collection endpoint further simplifies the process of creating actions.

A further point is that these could also be used to handle core actions, such as search indexing.

Regarding issues with tests, yes that's the cost of any new feature.

Regarding issues with debugging client projects, endpoints are notoriously difficult to debug. By nature they are wide open to issues, being sparse with restraints.

mingard avatar Oct 10 '17 11:10 mingard

With that logic, hooks need not exist. They could easily be custom endpoint.

That's not true. Hooks run in conjunction with the normal document endpoints logic, not instead of. Also, they run at specific (and optionally multiple) points in the lifecycle of a document. To achieve this with custom endpoints you'd need to rebuild the entire model + controller logic handled by core.

eduardoboucas avatar Oct 10 '17 12:10 eduardoboucas

Hook logic could be part of an endpoint which receives the payload and mutates before CRUD. Hooks like slugify used to exist in custom endpoints.

mingard avatar Oct 10 '17 12:10 mingard

Your example of an endpoints http verb methods covers the logic required by hooks too.

mingard avatar Oct 10 '17 12:10 mingard

But to do that you have to replicate a lot of the core functionality. In API, you either call a custom endpoint, or you call a collection endpoint. It's either one or the other.

I've seen endpoints that replicate a lot of the core CRUD functionality. I can't say I like them!

jimlambie avatar Oct 10 '17 12:10 jimlambie

Hook logic is not covered solely by endpoints - a hook receives the current set of data that is being operated on, whereas an endpoint only receives whatever is manually passed to it. Endpoints are called from a consumer application; hooks are part of the CRUD lifecycle of regular collections.

jimlambie avatar Oct 10 '17 12:10 jimlambie

That’s right, you would need to reuse some core functionality. In addition to endpoint calls and collection endpoint calls, collection action endpoints calls would be introduced.

The benefits remain:

  • reusable actions
  • simple to add to a collection
  • no requirement to build model queries into actions
  • interpretable by Publish
  • simple to introduce into core
  • useful with inbuilt operations like search indexing

mingard avatar Oct 10 '17 12:10 mingard

Hook logic is not covered solely by endpoints

Very true, an endpoint would need to query the model to get the current document(s). This is exactly what I’m trying to avoid with document actions. It’s an incredibly useful and time saving feature.

mingard avatar Oct 10 '17 12:10 mingard

As per request on Slack, I'll build a prototype.

mingard avatar Oct 10 '17 12:10 mingard

@mingard did you get any further with the construction of a prototype? Will close the issue for now, but please reopen when ready with a prototype, if still required!

jimlambie avatar Mar 14 '18 11:03 jimlambie

@jimlambie I didn't get any further with this. At the point of writing this there were a number of changes affecting API so I decided to wait for V3. We're there now, but I don't have the time. It's still definitely something I'd like to see, and I don't want to lose this ticket in the closed backlog. I'll keep it open for now, and try and find time to prototype.

mingard avatar Mar 14 '18 12:03 mingard

Any chance we can add it to Trello in the Icebox instead/

jimlambie avatar Mar 14 '18 12:03 jimlambie

@jimlambie 🍦

mingard avatar Mar 14 '18 12:03 mingard

Make that sugar free, please.

eduardoboucas avatar Mar 14 '18 12:03 eduardoboucas

Adding some further examples:

  • Resend of email
  • Reprint of label
  • 'Call a customer' where the action triggers a voip phone system

mingard avatar Dec 10 '19 10:12 mingard