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

Add support for adding response to the context on custom Express server #506

Closed
robin-macpherson opened this issue Mar 17, 2020 · 28 comments · Fixed by #1295
Closed

Add support for adding response to the context on custom Express server #506

robin-macpherson opened this issue Mar 17, 2020 · 28 comments · Fixed by #1295
Labels
scope/schema-context scope/server Related to the server component type/feat Add a new capability or enhance an existing one

Comments

@robin-macpherson
Copy link

What

  • Currently, we cannot set up the express server so that we can access the response from inside of resolvers.

Why

  • It's important to support this so that users can do things like -- in my current use case -- access the cookie on the response inside of resolvers, which could be important for a lot of basic authentication setups, for example with Jason Web Tokens as I am doing.

How

  • We should add support so that we can pass the response as a second parameter in schema.addToContext along with the request, with an implementation that would look something like:
schema.addToContext((request, response) => ({
  request,
  response,
}))
@robin-macpherson robin-macpherson changed the title Add support for passing response to the context Add support for adding response to the context on custom Express server Mar 17, 2020
@Weakky Weakky added the type/feat Add a new capability or enhance an existing one label Mar 17, 2020
@jasonkuhrt
Copy link
Member

@robin-macpherson can you share a link to or snippet here of the use-case. I want to better understand if middleware should or should not play a role here for example.

@robin-macpherson
Copy link
Author

@jasonkuhrt absolutely.

Repo & relevant branch

Relevant files are:

  • backend/src/app.ts for server setup
  • backend/src/graphql.ts where you can look at the resolver inside the createUser mutation starting on line 64.

Does that give you enough context?

@jasonkuhrt
Copy link
Member

jasonkuhrt commented Mar 18, 2020

Thanks @robin-macpherson, yep (same thing I had checked out the other day IIUC).

So, I don't think addToContext api should get larger. I actually think it should get smaller #509.

The need is that, after a particular resolver runs, a header must be set on the response.

The API we design here cannot assume that Express server will be present. One plugin could contribute schema fields while another swaps the server (Fastify instead of Express).

We could add to core a setResponseHeader function to ctx. Or ctx.response.setHeader. Custom servers would then have to satisfy this interface (easy).

A simple setHeader api is pretty low level. cookie being one example where things get complex. That's why functional helpers exist to construct them. This leads me to think we're better with ctx.response namespace so we can add things like ctx.response.setCookie.

If we're going to have ctx.response then I think we should consider ctx.request. But I don't think request should be an API here, but rather just some data. Probably the same one available in addToContext and being sketched in #509.

Considering that we should abbreviate to ctx.req/ctx.res. Most users will write e.g. (root, args, ctx, info) not (root, arguments, context, info). Likewise I think most users will write addToContext(req => { .... Aligning the properties with how users call their arguments seems nice. Another example of abbreviation is we say ctx.db not ctx.database. And jwt not jsonWebToken.

Example

      resolve: async (parent, args, ctx) => {
        const Password = await bcrypt.hash(args.Password, 10)
        const user = await ctx.db.user.create({
          data: {
            Name: args.Name,
            Email: args.Email.toLowerCase(),
            Password,
          },
        })
        const token = jwt.sign({ userId: user.Id }, process.env.APP_SECRET!)
        ctx.res.cookie({
          name: 'token',
          value: token,
          httpOnly: true,
          maxAge: ONE_YEAR,
        })
        return user
      },
    })

And no longer needed:

-schema.addToContext((request: any) => ({
-  request,
-  response: request.response,
-}))

@robin-macpherson
Copy link
Author

robin-macpherson commented Mar 18, 2020

@jasonkuhrt that definitely seems like a fair point regarding the addToContext API getting too large, per your thoughts in #509 . I also agree that the ctx.response namespace would be better and less confusing. I do find it very confusing to have the response on the request.

Regarding abbreviations, can the base property name still be the unabbreviated version which could then be aliased to the abbreviated names that you described, so that we can support both? Perhaps my mixed feelings about making the property names themselves abbreviated just comes from my weird style of keeping things super clear and explicit so I often actually do write context and request, response, etc.

It just feels like it would be nice to support both but perhaps this just makes the whole thing more convoluted. Curious to know your thoughts.

@jasonkuhrt
Copy link
Member

jasonkuhrt commented Mar 18, 2020

Regarding abbreviations, can the base property name still be the unabbreviated version which could then be aliased to the abbreviated names that you described, so that we can support both? Perhaps my mixed feelings about making the property names themselves abbreviated just comes from my weird style of keeping things super clear and explicit so I often actually do write context and request, response, etc.

👌 Good to know!

It just feels like it would be nice to support both but perhaps this just makes the whole thing more convoluted. Curious to know your thoughts.

Minimizing the property footprint is a pretty effective way to maximize the leverage of intellisense I feel.

Users could do the following fwiw but I don't see much value in it in practice (in the sense that, I don't think many users will actually want to do this).

(_root, args, { response: res, request: req }) => {
  // ...
}

@jasonkuhrt
Copy link
Member

jasonkuhrt commented Mar 18, 2020

Had a longish discussion with @Weakky about this and #509. It involved things like:

  • context is a graphql concept
  • but the request lifecycle that we need context scoping for is a server concept
  • how to model this in the framework API
  • framework level server middleware
  • we don't want server.custom to become the thing used in most cases
  • we probably don't want addToContext to become an unofficial way to write server middleware! (e.g. write auth logic, throw errors)
  • be careful about making users re-learn everything
  • the express middleware system is pretty good, koa is neat, anything else?
  • yes req/res should probably be exposed on the ctx by default––but what exact view of those is unclear still

We'll continue to mull. Needs a bit more attention to get this right. The framework server abstraction is a bit too weak right now to fully address the issue. Meaning we may need to iterate on what the framework's server system is generally. Then return to this issue concretely.

@robin-macpherson
Copy link
Author

@jasonkuhrt Good to know! That makes sense to me regarding maximizing leverage of intellisense, then.

Thanks for sharing those notes from the discussion with @Weakky . I agree that addToContext shouldn't become an unofficial way of writing server middleware, with it is starting to feel like in the current state for me. As a developer using the framework every day now, I feel myself wanting to just know what is "the way" to add my custom server configuration, if I should need to do that at all, and what is "the way" to add my middleware, and again, what do I actually need to implement myself.

I definitely appreciate being a part of the discussion around this 😊

@jasonkuhrt
Copy link
Member

I feel myself wanting to just know what is "the way" to add my custom server configuration, if I should need to do that at all, and what is "the way" to add my middleware, and again, what do I actually need to implement myself.

I think its fair to say that @Weakky and I have directionally aligned that Nexus will ship with a server middleware system. A dedicated issue for this will be created soon.

@emroot
Copy link

emroot commented Apr 16, 2020

Hey @jasonkuhrt, thanks for the through explanation above. I was curious if you had any update regarding adding response to the context variable. I'm itching to use Nexus next, but am blocked as I use the graphql mutation to set my user's cookie once logged in. Or maybe you have a recommendation in the meantime on how to achieve this in the meantime?

@robin-macpherson
Copy link
Author

Hi @emroot , in the meantime we found that you can access the response by grabbing it off of the request, since it lives at request.response.

So for example, if you wanted to add it to the context you could do something like:

schema.addToContext((request) => ({
  request,
  response: request.response,
}))

And then you could use it inside of your resolvers for the mutations where you are setting the user's cookie. Something like:

const token = jwt.sign({ userId: user.id }, <yourappsecret>)
ctx.response.cookie('token', token, {
  httpOnly: true,
  maxAge: ONE_YEAR,
})

@emroot
Copy link

emroot commented Apr 17, 2020

oh smart! Duh! Thanks for sharing!

@robin-macpherson
Copy link
Author

Haha, no worries! I was stuck on this for a while, too 🙂

@orjellingsen
Copy link

Hey @robin-macpherson, I have the same use-case as you describe, and I tried to grab response from request.response, but it is undefined. Did you run into this as well? Which version of nexus are you using?

@jasonkuhrt
Copy link
Member

Talked with @Weakky today about where we want to go with this. Once we have the concept of NexusRequest and NexusResponse (graphql-nexus/nexus#523) then we can consider automatically exposing req/res on the context.

Until then, @robin-macpherson would be great if you can contribute a recipe for your solution to the docs!

@robin-macpherson
Copy link
Author

@jasonkuhrt will do. I'll write to you directly to co-ordinate and @orjellingsen I'm going to update this thread and tag you when the recipe is ready. Hang tight! 💪🏼

@emroot
Copy link

emroot commented May 13, 2020

@robin-macpherson any update by chance on your recipe?
Still can't access request.response, getting a typescript error.

@robin-macpherson
Copy link
Author

Hi @emroot , sorry been completely slammed. In the meantime perhaps you can tell me what error you are getting and I might be able to help quickly unblock you.

@emroot
Copy link

emroot commented May 13, 2020

@robin-macpherson yeah super simple. When I try to use request.response I get a typescript error, and when I inspect request it doesn't have a response object, it's null

@decosvaldo
Copy link

@robin-macpherson yeah super simple. When I try to use request.response I get a typescript error, and when I inspect request it doesn't have a response object, it's null

@emroot, please try with this sintax:

schema.addToContext((req) => ({
  req,
  res: req.res
}))

ctx.res.cookie('token', token, {
            httpOnly: true,
            maxAge: 18000,
})

Working here so, hope it helps.

@mjyoung
Copy link

mjyoung commented Jun 1, 2020

@decosvaldo Thanks for the above example. It works well, but one issue I'm running into is typescript telling me Property 'res' does not exist on type 'Request'.

Any way around this? Trying to ignore that specific line using tslint:disable-line isn't working for me

@nvsd
Copy link

nvsd commented Jun 7, 2020

@robin-macpherson yeah super simple. When I try to use request.response I get a typescript error, and when I inspect request it doesn't have a response object, it's null

Yeah, I don't see res or response on the request object either.

@emroot
Copy link

emroot commented Jun 25, 2020

@decosvaldo this works thanks. There's a typescript error but I can easily override that.

@jcheese1
Copy link

@robin-macpherson having the same problem as @emroot , trying to access req from res results in undefined. any updates?
these are the versions im in:

    "nexus": "^0.25.0",
    "nexus-plugin-prisma": "^0.16.1",

@victoriris
Copy link

@robin-macpherson having the same problem as @emroot , trying to access req from res results in undefined. any updates?
these are the versions im in:

    "nexus": "^0.25.0",
    "nexus-plugin-prisma": "^0.16.1",

@yuyao17 The code mentioned above works for me! Are you having issues with typescript? Just set the type of the context argument as "any". If "res" is not correct try "response"

@jcheese1
Copy link

@victormidp I think I found the root cause. res exists when sending requests through the graphql playground via nexus dev, but its undefined when used as an api route in nextjs

@robin-macpherson
Copy link
Author

@yuyao17 I don't believe that is the case because we are now using Next's API routes and everything is working fine. I'm super busy today running behind on a few things so I'm not able to look into each person's case but please feel free to take a look at the Journaly repo and see exactly how we're doing things and let me know if you have any questions or if I can point you to something in particular:

https://github.com/Journaly/journaly/tree/master/frontend/nexus
https://github.com/Journaly/journaly/blob/master/frontend/server/index.js

@robin-macpherson
Copy link
Author

@emroot did you get all sorted out with these issues?

@emroot
Copy link

emroot commented Jul 27, 2020

yeah works for me. just a typescript error that I was able to override.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
scope/schema-context scope/server Related to the server component type/feat Add a new capability or enhance an existing one
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants