Services General

There are tons of questions about Services. And rightfully so. They’re Redwood’s most interesting abstraction. Interest only magnified after @mojombo talked about them on Frontend First, and it hasn’t waned since.

While we still don’t have all the answers, we thought we could at least fill you in on what we know right now. So here I’ll try to summarize everything I know about Services to date. Take everything with a grain of salt. This isn’t a spec, it’s just an attempt to explain.

Let’s start with a topic that I think will clear up most of the confusion around Services:

Services haven’t been fully introduced

If you’ve been poking around the graphql.js Function that comes with Redwood apps, you might’ve noticed that there’s this function called makeServices there:

// api/src/functions/graphql.js

import {
  createGraphQLHandler,
  makeMergedSchema,
  makeServices, // it's imported here
} from '@redwoodjs/api'

// ...

export const handler = createGraphQLHandler({
  schema: makeMergedSchema({
    schemas,
    services: makeServices({ services }), // and gets called here
  }),
  db,
})

Since it gets called in makeMergedSchema (above), you might think that it does the resolver-wrapping magic.

Actually, this is what makeServices does:

/**
 * This function is a stub for when we fully introduce the concept of
 * services.
 */
export const makeServices: MakeServices = ({ services }) => {
  return services
}

That’s right, it does nothing! Busted. (makeMergedSchema does the resolver-wrapping magic). But, as the comment says, this function is just a stub for when we fully introduce the concept of Services.

Fully introduce the concept of Services? They’re not fully introduced yet? That was a surprise to me. And, I think, is the major source of Services confusion: we all want to know “how do I use Services”, but it’s hard to explain how to use something that isn’t fully introduced.

We’re saying fully introduced because Services are indeed a thing. They’re there in your app; they each have their own directory in api/src/services; and if you name them just right, they get wrapped into your resolvers.

So it’s not like you can’t use them yet. Actually, far from it: you can already do pretty much anything you want with them. @peterp really gets at this in PR #334, where the auto import of db was removed:

We feel that having the import visible in the services will help people understand that services are a layer where you can connect your schema to a database, but can also be a request to an API, to Redis, or anything else… the possibilities are endless.

So I’d be doing you a disservice if I said that you have to wait for Services to be fully introduced before you can start grokking them. The fact that the possibilities are endless is what makes Services so hard. Granted, we should have a golden example that we can point you to that says “this is how you do services”. But we could argue that we actually kind of already do: it’s called Prisma Client! (Nevertheless, tutorial + cookbook recipes + docs will come.)

Anyway, we all want to know what Services might look like when they’re fully introduced, so:

What Services might look like when they’re fully introduced

Quoting @peterp:

Our vision for Services is to provide a run-time middleware layer that can run before or after a service is executed.

Note that he said this in May, two months ago. We’re in alpha, we have ideas; things change! But here’s the example he provided of what a Services file might look like with this run-time middleware layer:

// api/src/services/todos.js

import { debugToConsole, printExecutionTimeInMs } from '@redwoodjs/services-middleware-debug'
import { verifyUserRole } from '@redwoodjs/services-auth-middleware'

export const before = {
   deleteTodo: verifyUserRole('admin'),
}

export const after = {
    'all': [debugToConsole, printExecutionTimeInMs]
}

export const deleteTodo = ({ id } ) => {
   // delete from prisma
}

Again, this isn’t a spec; this is just an example. But hopefully it gets the idea across: like how Cells have before and after query lifecycle hooks, Services might have before and after execution lifecycle hooks.

How would this change auth?

Let’s look at how having a run-time middelware layer would change auth. We already kind of hinted at in the example above, but let’s make the before-after more explicit. Right now this is how we authenticate the api side:

export const createPost = ({ input }) => {
  requireAuth()
  return db.post.create({
    data: input,
  })
}

The check for authentication is part of the createPost logic. If we had a run-time middleware layer, we could do something like:

export const before = {
   createPost: verifyUserRole('admin'),
}

export const createPost = ({ input }) => {
  return db.post.create({
    data: input,
  })
}

Now our business logic is even more encapsulated!

Note that the Authentication section of the Tutorial kind of gets at this already, but about the GraphQL interface:

Note that we’re putting the authentication checks in the service and not checking in the GraphQL interface (in the SDL files).

Redwood created the concept of services as containers for your business logic which can be used by other parts of your application besides the GraphQL API. By putting authentication checks here you can be sure that any other code that tries to create/update/delete a post will fall under the same authentication checks. In fact, Apollo (the GraphQL library Redwood uses) agrees with us!

With the run-time middleware layer, we’d be walking it back even further: authentication checks would be in the run-time middleware layer instead of in the Services’ functions directly.

GraphQL and the Business Logic Layer

With Services, you’ll hear the words “Business Logic” a lot. When it comes to GraphQL, it’s a very important set of words. If you’ve read the GraphQL docs, you might already know what I mean. Take a look at this figure:

This is from the GraphQL docs, so not every colored rectangle here is relevant to Jamstack architecture. Here is a better version (sorry that they’re not the same width; ignore that detail):

rw-graphql

The point here is there shouldn’t be any business logic in your GraphQL. Redwood already makes this separation ridiculously easy.

But now the issue most of us are facing is how to think about Services as an API for our API. In other words, a lot is actually going on in that big blue rectangle:

And if we’re being accurate, we should include Prisma somewhere (here, the purple rectangles).

Above is an example API with a Billing Service and a User Service. Note that the Billing Service subsumes more than one table (the ellipsis should be filled in, but I just didn’t know with what). Read this figure from top to bottom; for example,

  • The GraphQL Function receives a POST; say it’s a mutation, to bill a user
  • It says to the Billing Service “bill user x”
  • The Billing Service talks to the User Service, maybe saying something like “I need user x” (This detail is important, the Billing Service is just talking to the User Service, not the User Table)
  • Both Services make the necessary changes to their underlying data tables (via the Prisma Client)

I glossed over infinitely many details there, but I didn’t think having all the details should stop me from giving a high-level overview. Hope this was helpful + feedback is greatly appreciated!

6 Likes

Great overview @dom, as always! Excited for what’s to come in this area :slight_smile:

1 Like

Really well done introduction, @dom! I’ll see if I can help make this more visible to more people! And also a good one for the next newsletter.

Thanks so much :rocket:

1 Like

I very much like the idea of “before”-type hooks when authorizing whether or not the currentUser can execute a service … action?

When I wrote up https://github.com/redwoodjs/redwood/issues/806 , I used something similar to the the sample:

export const createPost = ({ input }) => {
  requireAuth()
  return db.post.create({
    data: input,
  })
}

and thought that requireAuth() could also perform the role check.

What like more about the before is that I can see what’s going on easily; I don’t have to read every service method to get a handle trying to find and interpret each requireAuth() on who can do what and when. IE – the auth isn’t being obscured.

That’s something that often pains me with security: it’s so complex and spread out to setup and maintain that I can’t get a handle on what’s actually enforced (hello AWS :wink: ) so I end up being lax in security because it is easier for me to manage. Not a great rationale I know, but it’s the truth.

Better yet - if the rules are broken out and encapsulated, they can be scaffolded and thus encourage security from the start of a project/app and maintenance in future.

I think what you’ve just said is very important - middleware is detached and applied like layers in a call chain. So at a glance, it’s near impossible to understand what’s going to process your function before and after it executes without mentally parsing the middleware layer.

This before and after structure also has that problem, but since it’s in the same file as the service it’s less obscured, but more than I would like it to be.

That’s why services are not fully-fledged ideas yet, because we want to make sure that keep the simplicity of just calling a function.

I think that decorators could work nicely instead of a before/ after data structure, and if people needed something like middleware we could figure out something that’s easy to mentally parse.

2 Likes

So true – in fact as I was writing the prior post, I couldn’t help think about Rails ActiveRecord callbacks – and how practically everyone recognized them as not the best practice – The Problem with Rails Callbacks.

In the author’s example, instead of using an after_create callback to notify about a completed order, create a new object that’s responsible for that – an OrderNotifier and then call that in the normal flow of creating an order.

More like the requireAuth() but not exactly.

Maybe there’s some sort or OrderPolicy or UserPolicy that accepts the current function and checks if authorized via some opinionated and enforced convention.

I would bet (maybe) that 8 times out of 10, the permissions on the roles are likely CRUD or some other <verb>:<model> … which is kind of what a service method does, no? editUser -> edit:user, createUser -> create:user, publishNewsletter- > publish:newsletter, deletePost -> delete:post.

When one reads Auth0’s benefits of RBAC they define:

  • create systematic, repeatable assignment of permissions
  • easily audit user privileges and correct identified issues
  • quickly add and change roles, as well as implement them across APIs
  • cut down on the potential for error when assigning user permissions

Auditing here is a big one. I do not know how many times I have sat in workshops at the beginning (or more often near the end) and it’s Google Sheet time to make the “security matrix™”. Roles in column A and permissions across the top. Then hand over to the development team and the they have to go find each spot to put “some sort of check” … and then hand to QA and say, “check my work” ;). Joking … kinda.

I always thought, if I have the matrix, why can’t that just be the security policy? Why can’t role security like that be baked in (yes there’s a pun there) – and all one has to do is read that matrix (the policy/ies) and adjust/map the roles to their organization and what they think they can do. Just define the roles to the service actions and it might “just work”. And you audit the role list and you’re done.

That let’s you

  • quickly add and change roles, as well as implement them across APIs

In any case, I completely agree that " it’s near impossible to understand what’s going to process your function before and after it executes" and if you cannot understand security, you won’t implement it … or at least not properly or effectively.

So, should aim to make it much much much easier, effective, and used. Security is often tacked on at the end of an app/project – why not promote it or better yet, include it, from the start. It would a nice goal.

2 Likes

Ran across this example yesterday:

exports.handler = async (event, context) => {
+   const { type } = JSON.parse(event.body);
+   const { user } = context.clientContext;
+   const roles = user ? user.app_metadata.roles : false;
+   const { allowedRoles } = content[type];
+
+   if (!roles || !roles.some(role => allowedRoles.includes(role))) {
+     return {
+       statusCode: 402,
+       body: JSON.stringify({
+         src: 'https://res.cloudinary.com/jlengstorf/image/upload/q_auto,f_auto/v1592618179/stripe-subscription/subscription-required.jpg',
+         alt: 'corgi in a crossed circle with the text “subscription required”',
+         credit: 'Jason Lengstorf',
+         creditLink: 'https://dribbble.com/jlengstorf',
+         message: `This content requires a ${type} subscription.`,
+       }),
+     };
+   }

    return {
      statusCode: 200,
      body: JSON.stringify(content[type]),
    };
  };

where

 (!roles || !roles.some(role => allowedRoles.includes(role)))

is likely a common way of role checking – and no doubt works – but perhaps is an example of what RedwoodJS aims to “pattern”-ize for the lack of a better term.

1 Like