graphql-framework-experiment icon indicating copy to clipboard operation
graphql-framework-experiment copied to clipboard

Serverless mode

Open jasonkuhrt opened this issue 5 years ago • 34 comments

Perceived Problem

  • serverless is a popular kind of deployment; nextjs, vercel, lambda, serverless frmework, google cloud functions, kubernetes functions, edge compute, apollo-micro, ...
  • nextjs integration #648 needs serverless

Ideas / Proposed Solution(s)

  • Nexus needs to expose a "request event" handler. This is what the host server platform will call.

  • Many Node http server APIs have request handlers with request and response parameters. However it seems a lot more intuitive to have request as the input and response as asynchronous output.

    interface NexusServer {
      handle: (req: NexusServerRequest) => Promise<NexusServerResponse>
    }
    
  • ...But what does a user do when they want to code against their deployment platform of choice. For example from a comment in the nextjs integration issue:

    / pages/api/graphql.ts
    mport Cors from 'cors'
    mport { server, use } from 'nexus'
    mport { nextjs } from 'nexus-plugin-nextjs'
    
    se(nextjs())
    
    xport default server.handler
    
    / Initializing the cors middleware
    onst cors = Cors({
     methods: ['GET', 'HEAD'],
    )
    
    / Helper method to wait for a middleware to execute before continuing
    / And to throw an error when an error happens in a middleware
    unction runMiddleware(req, res, fn) {
     return new Promise((resolve, reject) => {
       fn(req, res, result => {
         if (result instanceof Error) {
           return reject(result)
         }
    
         return resolve(result)
       })
     })
    
    
    sync function handler(req, res) {
     // Run the middleware
     await runMiddleware(req, res, cors)
    
     // Rest of the API logic
     server.handle(req, res)  // <---- nexus handler here
    
    
    xport default handler
    
    • https://nextjs.org/docs/api-routes/api-middlewares
    • We could introduce server.platformHandle or server.handle.fromPlatform but it is unclear why we need the indirection, why we need both. It seems right now that we should just make server.handle be that.
  • The request handler should be exported for use by the host platform. Example with nextjs (with vercel):

    import { server } from 'nexus'
    const handleRequest = server.handle
    export default handleRequest
    

    The inability to export an import directly seems bad. But one could say either way is boilerplate and wht's really bad is doing any of that in the first place.

  • several things should change in nexus between serverful and serverless modes

    1. server.start server.stop APIs should only appear in serverful mode
    2. server jsDoc should reflect the current server mode
    3. Nexus dev mode; In serverful mode it should run the app server (what it does today). In serverless mode it should emulate a serverless platform and call the app's request handler
    4. server.express API should not appear in serverless mode. Express should not even be in the final built bundle.
    5. server settings;
      • port and host should only appear in serverful mode
      • path is for configuring the graphql endpoint path. The concept of path control applies to both serverful and serverless systems but its configuration/mental model is very different. In serverless the path may be configuration within terraform, yaml, cloud formation, file name/location (nextjs), etc. To the extend that Nexus can automate this the path configuration makes sense in serverless mode, however it stops making sense once a user needs to manually configure path in another system.
      • In serverless mode, the port used by dev mode serverless platform emulation should probably be configurable by the user. How should that work? Re-purpose port? settings.server.platform.port? settings.dev.server.platform.port? Conceptually, configuring the platform that runs the app in the app makes no sense, so... package.json#nexus.serverless.port? Or the conceptual nonsense is acceptable for the simplicity it gives for dev and transition between serverless/serverful?
      • In serverless mode, playground.path has the same issues as described for path. Additionally, if users expect to deploy playground in serverless mode, it raises the question of multiple endpoints, which in serverless, each constitutes its own app.
  • Some things should not change between serverful and serverless modes

    1. Server middleware #523 should remain abstracted from the host platform so that plugins can provide server middleware without concern for the server in use.

      This is non-trivial because certain http features are simply not possible on certain serverless platforms. How can a plugin author design their plugin for maximum portability, needing some server features, but not knowing if the plugin will be used in an environment that has that capability?

      The design problems here are not unlike prisma which abstracts different databases.

      Maybe some kind of capability modelling/matching system will need to be designed.

      This is a whole topic unto itself.

  • Some of the points above (settings.server.path, server middleware, ...) make pretty clear that a serverless mode is not enough, but a sererless platform mode is needed, wherein the particulars of a server platform are taken into account, adapted for.

References

  • https://github.com/graphql-nexus/nexus/issues/648#issuecomment-620205954
  • https://github.com/graphql-nexus/nexus/issues/295
  • https://nextjs.org/docs/api-routes/api-middlewares

jasonkuhrt avatar May 01 '20 14:05 jasonkuhrt

Some additional thoughts came up with @Weakky today while sprint planning.

  • Going between serverless and serverful should not be mutually exclusive. This is a significant change in our thinking.

  • How do we talk about this? Is there one app with zero or more lambdas and zero or one server? Or is there one project with zero or more serverless apps and zero or one serverful app? Does a project have an app or does app/project mean the same thing? Is it that a project has multiple apps or that an app has multiple entrypoints? "Entrypoints" instead of "apps"? ...

  • In dev mode, a serverless platform emulator would run the lambda entrypoints, and the serverful entrypoint. But reflection on each could lead to different typegen. Typegen is global. There would be conflicts.

    @Weakky maybe we can look into tsconfig paths to find way to have each lambda consume only its respective typegen...

  • Build would needs to run per entrypoint. And if we cannot find solution to global typegen (see above point), then serially.

  • Nexus will need to create a bundle per lambda. This does overlap with what e.g. Next.js / Vercel provide, but we think that's ok. Just like we wanted a bundle/tree-shake step #119 but again Next.JS also provides that. The point is integrations will always have some feature overlap with pure Nexus, and Nexus should just be as much as possible flexible enough to turn off the overlapping parts.

  • If there is a convention for serverless then we probably need a convention for serverful, e.g. server.ts becomes the serverful entrypoint for the app. For example in the following there would be three entrypoints in the app.

    erver.ts         <-- serverful entrypoint
    ambdas/
    graphql.ts     <-- serverless entrypoint 1
    playground.ts  <-- serverless entrypoint 2
    
  • Re bundles, given the above example, the build result would be e.g.:

    ode_modules/
    .build/
    	server.js          <-- (self-contained bundle)
    	lambdas/
    		graphql.js     <-- (self-contained bundle)
    		playground.js  <-- (self-contained bundle)
    
  • Plugins could tailor further to platform needs, e.g. nexus-plugin-aws-lambda could zip the lambdas etc.

  • Lambdas would use file paths to infer URL paths

  • All the different entrypoints, if they constitute their own apps, will need their own app state. Changing the schema or changing the settings should apply to a specific entrypoint, not all. Of course the classic way to do this is build up state at the module level from stateless imports, then export/import everywhere to build up a graph of shared module deps where needed, etc. But that's really against the Nexus way (today). How does this problem look to Nexus.

  • I think ☝️ is one of the biggest fundamental issues, so here's a point just to point to that point 🤦 😛


  • Multiple serverless entrypoints in an app look an aweful lot like multiple serverful apps in a monorepo. ...? It feels like we can generalize the system further to accept zero-or-more serverless AND serverful entrypoints. And it feels like doing so would simplify things.

                          serverful
    
    server.ts        <-- 1 serverful entrypoint
    
    server/          <-- 1 serverful entrypoint (dir style)
      index.ts
    
    servers/         <-- OR N serverful entrypoints
      a.ts
      b.ts
    
    servers/         <-- OR N serverful entrypoints (dir style)
      a/
        index.ts
      b/
        index.ts
    
                          serverless
    
    
    lambda.ts        <-- 1 serverless entrypoint
    
    lambda/          <-- 1 serverless entrypoint (dir style)
      index.ts
    
    lambdas/         <-- OR N serverful entrypoints
      a.ts
      b.ts
    
    lambdas/         <-- OR N serverful entrypoints (dir style)
      a/
        index.ts
      b/
        index.ts
    
  • The simplicity comes from the fact that there are fewer rules to learn. There is a dimension of server kind and there is a dimension of number. No exceptions.

  • Naming is hard

    • serverless things: lambda / serverless / func / function

    • serverful things: server / process

    • ambiguous things: service

    • other things: app / project

    • I somehow like func/funcs more than lambda/lambdas...

      • easier to spell
      • easier to write
      • easier to say (feels like one less syllabol)
      func.ts        <-- 1 serverless entrypoint
      
      func/          <-- 1 serverless entrypoint (dir style)
        index.ts
      
      funcs/         <-- OR N serverful entrypoints
        a.ts
        b.ts
      
      funcs/         <-- OR N serverful entrypoints (dir style)
        a/
          index.ts
        b/
          index.ts
      

jasonkuhrt avatar May 06 '20 20:05 jasonkuhrt

I am currently using nexus-schema with nexus-schema-plugin-prisma with next.js and it works very well. I tried to "upgrade" to nexus and nexus-plugin-prisma with the 0.21.1 update but ran into few issues

  1. use(prisma()) gave me the Error: You most likely forgot to initialize the Prisma Client. error. I bypassed that by doing
use(
  prisma({
    client: { instance: new PrismaClient() },
  })
)
  1. That gave me the following error
TypeError: Converting circular structure to JSON
       |            --> starting at object with constructor 'Object'
       |            |     property 'fields' -> object with constructor 'Array'
       |            |     index 4 -> object with constructor 'Object'
       |            |     ...
       |            |     property 'outputType' -> object with constructor 'Object'
       |            --- property 'type' closes the circle

from this location: nexus/dist/lib/reflection/reflect.js:36:46

Please let me know if I can do anything to help solve this. You folks are doing a great job and I look forward to see further work on next.js integration.

kristoferma avatar May 06 '20 22:05 kristoferma

That gave me the following error

Hey @kristoferma, thanks for the kind words. Ah, I think I know why yes. We'll fix that soon, maybe tomorrow. If you file a bug issue for it that would be great too.

@Weakky We'll need to not assume that all plugin settings are JSON serializable. I kind of saw this coming but now here we are.

jasonkuhrt avatar May 07 '20 03:05 jasonkuhrt

Some additional thoughts came up with @Weakky today while sprint planning.

  • Going between serverless and serverful should not be mutually exclusive. This is a significant change in our thinking.

  • How do we talk about this? Is there one app with zero or more lambdas and zero or one server? Or is there one project with zero or more serverless apps and zero or one serverful app? Does a project have an app or does app/project mean the same thing? Is it that a project has multiple apps or that an app has multiple entrypoints? "Entrypoints" instead of "apps"? ...

    • ...Thinking about this more considering that Nexus calls its default import app it seems that we should consider every entrypoint its own app and the larger context be a project.
  • In dev mode, a serverless platform emulator would run the lambda entrypoints, and the serverful entrypoint. But reflection on each could lead to different typegen. Typegen is global. There would be conflicts.

    @Weakky maybe we can look into tsconfig paths to find way to have each lambda consume only its respective typegen...

  • Build would needs to run per entrypoint. And if we cannot find solution to global typegen (see above point), then serially.

  • Nexus will need to create a bundle per lambda. This does overlap with what e.g. Next.js / Vercel provide, but we think that's ok. Just like we wanted a bundle/tree-shake step #119 but again Next.JS also provides that. The point is integrations will always have some feature overlap with pure Nexus, and Nexus should just be as much as possible flexible enough to turn off the overlapping parts.

  • If there is a convention for serverless then we probably need a convention for serverful, e.g. server.ts becomes the serverful entrypoint for the app. For example in the following there would be three entrypoints in the app.

    	```
    	server.ts         <-- serverful entrypoint
    	lambdas/
    		graphql.ts     <-- serverless entrypoint 1
    		playground.ts  <-- serverless entrypoint 2
    	```
    
  • Re bundles, given the above example, the build result would be e.g.:

    	```
    	node_modules/
    		.build/
    			server.js          <-- (self-contained bundle)
    			lambdas/
    				graphql.js     <-- (self-contained bundle)
    				playground.js  <-- (self-contained bundle)
    	```
    
  • Plugins could tailor further to platform needs, e.g. nexus-plugin-aws-lambda could zip the lambdas etc.

  • ~Lambdas would use file paths to infer URL paths. Config overrides etc. would probably be available somehow.~ See points way below.

  • All the different entrypoints, if they constitute their own apps, will need their own app state. Changing the schema or changing the settings should apply to a specific entrypoint, not all. ~Of course the classic way to do this is build up state at the module level from stateless imports, then export/import everywhere to build up a graph of shared module deps where needed, etc. But that's really against the Nexus way (today). How does this problem look to Nexus.~ To achieve this dev mode will need to run each entrypoint in its own sub process. A very large app of dozens of entrypoints could lead to serious performance problems. But we're not sure about that. And maybe most apps will only have a few entrypoints. Anyways being limited here could be an acceptable way to start out.

    If we don't run each entrypoint in its own process we'll get the kind of problems we're seeing in nextjs dev.

  • Multiple serverless entrypoints in an app look an aweful lot like multiple serverful apps in a monorepo. ...? It feels like we can generalize the system further to accept zero-or-more serverless AND serverful entrypoints. And it feels like doing so would simplify things.

                           serverful
    
    server.ts        <-- 1 serverful app
    
    server/          <-- 1 serverful app (dir style)
      index.ts
    
    servers/         <-- N serverful app
      a.ts
      b.ts
    
    servers/         <-- N serverful app (dir style)
      a/
        index.ts
      b/
        index.ts
    
                           serverless
    
    lambda.ts        <-- 1 serverless app
    
    lambda/          <-- 1 serverless app (dir style)
      index.ts
    
    lambdas/         <-- N serverless apps
      a.ts
      b.ts
    
    lambdas/         <-- N serverless apps (dir style)
      a/
        index.ts
      b/
        index.ts
    
  • The simplicity comes from the fact that there are fewer rules to learn. There is a dimension of server kind and there is a dimension of number. No exceptions.

  • Naming is hard

    • serverless things: lambda / serverless / func / function

    • serverful things: server / process

    • ambiguous things: service

    • other things: app / project

    • I somehow like func/funcs more than lambda/lambdas...

      • easier to spell
      • easier to write
      • easier to say (feels like one less syllabol)
      func.ts        <-- 1 serverless app
      
      func/          <-- 1 serverless app (dir style)
        index.ts
      
      funcs/         <-- N serverful apps
        a.ts
        b.ts
      
      funcs/         <-- N serverful apps (dir style)
        a/
          index.ts
        b/
          index.ts
      
  • We'll need to revise the schema module auto-import system given multiple entrypoints, since, how can we know which entrypoint a schema module is associated with?

    To solve this we can use the disk as a scoping mechanism like as follows:

    server/
      index.ts        <-- app A
      graphql.ts      <-- only visible to app A
    funcs/
      b/
        index.ts      <-- app B
        graphql.ts    <-- only visible to app B
      c/
        index.ts      <-- app C
        graphql.ts    <-- only visible to app C
    common/
      graphql.ts      <-- visible to apps A B C
    

    I'm also realizing that this makes possible for a user to do something like this inside the common modules:

    import { server } from 'nexus'
    
    server.middleware(...) // all lambdas will get this!
    

    However if you want e.g. all modules - 1 exception then you're out of luck... unless we introduced a new apps selection api... e.g.:

    import apps from 'nexus/apps'
    
    apps('b', 'c').server.middleware(...) // app A will not get this!
    
    apps('a','b').schema.objectType(...) // app C will not get this!
    

    But this feels odd to me right now... probably we can drop this train of thought for now.

  • Thinking more about the conventional naming strategies. We haven't explored prefixes/suffixes.

    server.ts        <-- 1 serverful app
    
    server/          <-- 1 serverful app (dir style)
      index.ts
    
    a.server.ts      <-- OR N serverful apps
    b.server.ts
    
    a.server/        <-- OR N serverful apps (dir style)
      index.ts
    b.server/
      index.ts
    
    func.ts        <-- 1 serverless app
    
    func/          <-- 1 serverless app (dir style)
      index.ts
    
    a.func.ts      <-- N serverful apps
    b.func.ts
    
    a.func/        <-- N serverful apps (dir style)
      index.ts
    b.func/
      index.ts
    

    In order to support better grouping in file trees etc. we might want prefix instead:

    server.ts        <-- 1 serverful app
    
    server/          <-- 1 serverful app (dir style)
      index.ts
    
    server.a.ts      <-- OR N serverful apps
    server.b.ts
    
    server.a/        <-- OR N serverful apps (dir style)
      index.ts
    server.b/
      index.ts
    
    func.ts        <-- 1 serverless app
    
    func/          <-- 1 serverless app (dir style)
      index.ts
    
    func.a.ts      <-- N serverful apps
    func.b.ts
    
    func.a/        <-- N serverful apps (dir style)
      index.ts
    func.b/
      index.ts
    

    Removing a [forced] level of nesting in project layout is great. However, for serverless funcs, has the issue of complicating the file-systems-as-url.

    Before

    funcs/
      foo/
        qux/
          bar.ts   <-- POST /foo/qux/a
    

    After??

    func.a/
      foo/
        qux/
          index.ts   <-- POST /foo/qux/a ????
    

    Ultimately I'm not sure the file-system-as-url-path is a great idea, though. It is very rigid. For example maybe a handler wants to be available at multiple paths or methods... Maybe this is where Nexus draws the line and we stick to APIs, e.g.:

    // func.a.ts
    import { func } from "nexus";
    
    func.routes.add({
      method: "POST",
      path: "/a/b/c",
    });
    
    func.routes.add({
      method: "GET",
      path: "/z/y/x",
    });
    

    or e.g.

    // func.a.ts
    import { settings } from 'nexus'
    
    settings.change({
      func: {
        routes: {
          method: 'POST',
          path: '/a/b/c',
        }, {
          method: 'GET',
          path: '/z/y/x'
        }
      }
    })
    

    or e.g. maybe Nexus has a centralized routing table system:

    // imagine this module is in some common/top-level place in the project
    // e.g. routing.ts
    
    import { routing } from 'nexus/funcs'
    
    routing.a.add({ methods: ['POST', 'GET'], path: '/a/b/c' })
    routing.b.add({ method: 'POST', path: '/a/b/z' })
    routing.c.add({ method: 'DELETE', path: '/a/b/foo' })
    //      ^----- app names, typegen
    

    Then Nexus would use reflection to extract data and generate the config actually needed in the e.g. serverless.yml etc. file.

    In fact, even for a NextJS integration, maybe Nexus could generate modules into /api !?

    Unlike working with the file-system an API approach is type safe, works with IDE workflows, including refactoring and so on. It seems to scale much better.

    @Weakky IIUC this was more or less your position on the call today right? I think I'm onboard now.

jasonkuhrt avatar May 07 '20 03:05 jasonkuhrt

Next.js encourages users to fetch initial props for apps via getServerSideProps method of each page component. They encourage users not to use api routes but to "write server side code directly" docs. This would require a new way of interfacing with nexus without launching a server.

The easiest way to do this is to expose a method to execute graphql queries without starting a server. Something like this:

import { execute } from 'nexus'

export const getServerSideProps = async ({ params }) => {
  const response = execute(params.query, params.variables)
  return JSON.stringify(response)
}

kristoferma avatar May 13 '20 15:05 kristoferma

Hey @kristoferma, yeah we're aware, from the docs:

That means you can write code such as direct database queries without them being sent to browsers. You should not fetch an API route from getStaticProps — instead, you can write the server-side code directly in getStaticProps.

This means that one should be using, for example, and generally, Prisma Client directly, not an API at all.

Of course, that's the general idea. A user in should do what they need to in their scenario.

interfacing with nexus without launching a server.

Using Nexus without a server is already possible https://www.nexusjs.org/#/guides/serverless

jasonkuhrt avatar May 16 '20 15:05 jasonkuhrt

Hey @kristoferma sorry missed your post before, about it:

use(prisma()) gave me the Error: You most likely forgot to initialize the Prisma Client. error. I bypassed that by doing

This was a bug, its fixed now in latest canary.

That gave me the following error

Plugin settings reflection was enhanced to support non-serializable settings data. So again, latest canary this is fixed.

jasonkuhrt avatar May 16 '20 15:05 jasonkuhrt

Plugin settings reflection was enhanced to support non-serializable settings data. So again, latest canary this is fixed.

Thanks, this fixed the issue for me

This means that one should be using, for example, and generally, Prisma Client directly, not an API at all.

I should have explained my use case better. I am using Relay with Next.js and I want to server side render my page and pass on the Relay Store caching from the server to the client. To do this I must fetch the data with relay on the server, stringify it as json and pass it on as props via GetServerSideProps. If I use Prisma Client directly, I lose the ability to pass the Relay store from the server to the client.

Currently I fetch the data from the graphql endpoint and it works fine, but it adds an extra step that could be bypassed by executing the graphql query from the GetServerSideProps

kristoferma avatar May 16 '20 21:05 kristoferma

@kristoferma Ah my mistake, you were talking about getServerSideProps and I replied about getStaticProps.

Currently I fetch the data from the graphql endpoint and it works fine, but it adds an extra step that could be bypassed by executing the graphql query from the GetServerSideProps

It seems like what you need is the layer right below the handler. You might be able to hack a solution right now with Nexus' current serverless support.

Your use-case is interesting. If you can create a new issue for that'd be great.

jasonkuhrt avatar May 18 '20 01:05 jasonkuhrt

Hello @jasonkuhrt, I am enjoying getting to grips with nexus and would like to deploy with lambda. If this is already possible then it would be useful to reference an example lambda handler function. Perhaps I am jumping ahead slightly though. Thanks

toddpla avatar May 24 '20 23:05 toddpla

Perhaps I am jumping ahead slightly though

Hey, yeah slightly. You're kind of on your own right now. I actually think it might be possible now with the primitives we've shipped but, not sure. Check out https://www.nexusjs.org/#/guides/serverless.

Now that we have an integrated bundle step, it means you should be able to zip the build and ship to AWS Lambda. But again we haven't explicitly made this a feature/goal yet, its on the roadmap.

jasonkuhrt avatar May 25 '20 00:05 jasonkuhrt

I'm trying to use with Serverless AWS but I've problem with the export handlers because the graphQL handler cant load all the schema files.

▲ nexus:schema Your GraphQL schema is empty. This is normal if you have not defined any GraphQL types yet. If you did however, check that your files are contained in the same directory specified in the `rootDir` property of your tsconfig.json file.

If we access the playground using the yarn start it works.

In serverless it works too, but It cant find any of the schema files. The playground works but empty.

Manually changing the deployed files and moving the require files from the index.js to app.js solves the issue.

There is a copy of the app.ts

// app.ts
import app, { use, server } from 'nexus'
import { prisma } from 'nexus-plugin-prisma'
import serverless from 'serverless-http'

use(prisma())

app.assemble()

export const graphql = serverless(server.handlers.graphql, {
    request(request: any, event: any, context: any) {
        const { body } = request as any

        request.context = event.requestContext;
        request.body = JSON.parse(body.toString()) // parsing body bc body is some weird buffer thing

        return request;
    }
})

export const playground = serverless(server.handlers.playground)

Is it somehow possible to bundle the schema files into the built app.js?

zapbr avatar Jun 03 '20 14:06 zapbr

This happens for me because there are two instances of Prisma, one in nexus-plugin-prisma node_modules and another one in the root project node_modules.

I fix this by deleting node_modules/nexus-plugin-prisma/node_modules/.prisma after npm install

This is probably a bug but I have not submitted an issue on this

kristoferma avatar Jun 03 '20 14:06 kristoferma

This happens for me because there are two instances of Prisma, one in nexus-plugin-prisma node_modules and another one in the root project node_modules.

I fix this by deleting node_modules/nexus-plugin-prisma/node_modules/.prisma after npm install

This is probably a bug but I have not submitted an issue on this

This not solve my issue, sadly.

In my environment I don't have this duplicated Prisma.

Pretty sure is because we are missing the schema required files on the handler call.

In the non-index.js entrypoint we have to use in order to export the handlers.

zapbr avatar Jun 03 '20 15:06 zapbr

@kristoferma

and another one in the root project node_modules.

Nexus Prisma apps depending on prisma deps directly is not supported.

@zapbr

Is it somehow possible to bundle the schema files into the built app.js?

That's what nexus build effectively does, but it preserves the file tree.

jasonkuhrt avatar Jun 03 '20 22:06 jasonkuhrt

@jasonkuhrt the necessary schema files are bundled into index.js, which does not contain any of the exported handlers from app.ts. So in order to use nexus with the serverless framework, currently my understanding is that we need to copy all the require statements into the built app.js (manual retouching of built files).

kldzj avatar Jun 04 '20 07:06 kldzj

@kldzj will have to look into it. Like I mentioned to toddpla you're a bit ahead of us. If you want to contribute a serverless example to examples repo that might be a good place to explore the issue more precisely.

jasonkuhrt avatar Jun 04 '20 14:06 jasonkuhrt

Hallo @jasonkuhrt, thank you for your quick response. So we build a little demo with the issue:

https://github.com/zapbr/nexus-prisma-aws-serverless-demo

zapbr avatar Jun 04 '20 15:06 zapbr

This is a hack, but if you just import your schema files, you'll force it into the bundle.

import app, { server, use } from "nexus";
import { prisma } from "nexus-plugin-prisma";
import * as serverless from "serverless-http";
import "./graphql/User"; // hack to resolve https://github.com/graphql-nexus/nexus/issues/782

use(prisma({ features: { crud: true } }));

app.assemble();

Hoping for a better solution soon!

AaronBuxbaum avatar Jul 25 '20 04:07 AaronBuxbaum

@AaronBuxbaum great find, but seems very ugly when you have a lot of models. We decided to just migrate back to @nexus/schema.

kldzj avatar Jul 25 '20 08:07 kldzj

@AaronBuxbaum that's with nextjs correct?

@kldzj I'm assuming there was another reason than that since you'll have to import modules just the same with @nexus/schema. What was the reason in your case?

jasonkuhrt avatar Jul 27 '20 04:07 jasonkuhrt

@AaronBuxbaum that's with nextjs correct?

Currently, no (I'm playing with serverless -> AWS Lambda), but I suspect it would be easy to put this on NextJS if desired

AaronBuxbaum avatar Jul 27 '20 16:07 AaronBuxbaum

@jasonkuhrt wanting to use federation was another reason, but mainly for ease of serverless deployment.

kldzj avatar Jul 27 '20 16:07 kldzj

@AaronBuxbaum If you can contribute an example or recipe that would be great. It seems that your technique is similar to our nextjs recipe.

jasonkuhrt avatar Jul 27 '20 23:07 jasonkuhrt

I made it work with Webpack + Serverless + Lambda but I am not sure if the solution is feasible. Basically, like the #109 issue says, you get a lot of warnings if you use sls and webpack, because of the usage of require.resolve(). However, if you wrap it on eval() those are executed at runtime and they seem to work just fine (it took me like 50 webpack google searches to get there). So, I manually modified the files with warnings and I got it to work locally with sls offline and I also was able to deploy to Lambda, however you get the error from the require right now, because the code does not include the fix. I'll try to bundle manually and see if I can confirm this, however, here are some snippets in case it helps:

Note: I could not make the typescript serverless plugin to work so I run tsc, prisma generate and nexus build before deploy.

//app.ts
import app, { use, settings, server} from 'nexus'
import { prisma } from 'nexus-plugin-prisma'
import serverless = require('serverless-http')
import './graphql/graphql'; //Here is my Nexus schema definitions

settings.change({
    logger: {
        pretty: true
    },

    server: {
        startMessage: (info) => {
        settings.original.server.startMessage(info)
        },
    },

    schema: {
        generateGraphQLSDLFile: './graphql/schema.graphql'
    }

})

use(
    prisma(
        {
            migrations: false,
            features: {
                crud: true
            }
        }
    )
)

app.assemble()

export const graphql = serverless(server.handlers.graphql, {
    request(request: any, event: any, context: any) {
        const { body } = request as any

        request.context = event.requestContext;
        request.body = JSON.parse(body.toString()) // parsing body bc body is some weird buffer thing

        return request;
    }
})

export const playground = serverless(server.handlers.playground)
//serverless.yml
service: graphql-lambda

plugins:
#  - serverless-plugin-typescript  
  - serverless-webpack
  - serverless-offline

custom:
  prune:
    automatic: true
    number: 5
  serverless-offline:
    port: 1337
  webpack:
    webpackConfig: 'webpack.config.js'   # Name of webpack configuration file
    includeModules: true # Node modules configuration for packaging #I dont think this is needed anymore
    packager: 'yarn'   # Packager that will be used to package your external modules

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev # Set the default stage used. Default is dev
  region: us-east-1 # Overwrite the default region used. Default is us-east-1
  profile: production # The default profile to use with this service
  memorySize: 512 # Overwrite the default memory size. Default is 1024
  deploymentBucket:
    name: com.serverless.${self:provider.region}.deploys # Overwrite the default deployment bucket
    serverSideEncryption: AES256 # when using server-side encryption
    tags: # Tags that will be added to each of the deployment resources
      environment: ${self:provider.stage}
      service: serverless
  deploymentPrefix: serverless # Overwrite the default S3 prefix under which deployed artifacts should be stored. Default is serverless
  versionFunctions: true #false # Optional function versioning

functions:
  playground:
    handler: app.playground
    events:
        - http:
              path: playground
              method: get
              cors: true
  graphql:
    handler: app.graphql
    events:
    - http:
        path: graphql
        method: get
        cors: true
    - http:
        path: graphql
        method: post
        cors: true
//webpack.config.js
const slsw = require('serverless-webpack');
const CopyPlugin = require('copy-webpack-plugin')
const path = require('path');

module.exports = {
    entry: slsw.lib.entries,
    target: 'node',
    mode: slsw.lib.webpack.isLocal ? "development": "production",
    output: {
      libraryTarget: 'commonjs',
      path: path.join(__dirname, '.webpack'),
      filename: '[name].js',
      pathinfo: false,
    },
    optimization: {
      minimize: false
    },
    plugins: [
      new CopyPlugin({
        patterns: [
          { from: './prisma/schema.prisma' },
          { from: './.nexus', to: './.nexus'},
          { from: './node_modules/.prisma', to: './node_modules/.prisma'},
          { from: './node_modules/nexus-plugin-prisma', to: './node_modules/nexus-plugin-prisma'},
        ]
      })
    ]
  };

Example that I had to change in typegenAutoConfig.js

resolvedPath = eval(`require.resolve("${pathOrModule}", {
                    paths: [${process.cwd()}],
                })`);

I also had to make similar changes on files like manifest.js, linkable.js, couple of utils.js, import.js, etc.

After this, it will complain that it cannot find the core, so I also had to change the import order on @nexus/schema/dist-esm/index.js, so this line is at the end, after the imports:

export { core, blocks, ext };

After this, sls offline will work just fine and I can query the database normally. Also, my deployed function on Lambda is about 21mb which is completely fine.

As I said, this does not work just yet on deployment. It will complain about the plugin not being able to find because I have to assume sls is just doing yarn and getting the npm module which does not have the fix. I'll try to work on that next.

sfratini avatar Jul 30 '20 01:07 sfratini

Dropping by to say I have a blast combining NextJS, Prisma and Nexus with the experimental serverless API. My /api/graphql endpoint looks something like this:

// pages/api/graphql.ts

import app, { settings } from "nexus";
import nextConnect from "next-connect";

import { sessionMiddleware, randomMiddleware } from "../../lib/middleware";

require("../../graphql/schema");

settings.change({
  server: {
    path: "/api/graphql",  // for playground
  },
});

app.assemble();

export default nextConnect()
  .use(sessionMiddleware)
  .use(randomMiddleware)
  .use(app.server.handlers.graphql);

Thank you for your hard and much appreciated work on Nexus

heibel avatar Aug 06 '20 13:08 heibel

I tried all of code above but I can not run my serverless. Does Nexus support serverless?

hienlh avatar Aug 09 '20 14:08 hienlh

@heibel Could you please share the rest of your setup? I am still trying to deploy to lambda and while it works locally, it is because of the manual changes that I did to the code and I could not make the setup to work effortlessly. Thanks

sfratini avatar Aug 17 '20 07:08 sfratini

@sfratini I forgot to mention I deploy to/with Vercel. Which works with the above code. Maybe you can share your code/lambda errors?

heibel avatar Aug 17 '20 08:08 heibel

@sfratini I forgot to mention I deploy to/with Vercel. Which works with the above code. Maybe you can share your code/lambda errors?

Sure, so basically it is not finding the prisma plugin. Now, the plugins are loaded using require.resolve which webpack does not like. I "solved" that locally by replacing all the places nexus uses that, with eval() calls which are executed at runtime (you can see my changes a couple of comments above). The issue however, is that serverless does an install which, obviously installs the version without the change so on the cloud, the plugins won't work. (with webpack)

I need to use webpack (or any other packager/compression tool), because lambda has a 250MB limit.

  2020-08-17T09:35:48.525+02:00 [90m 57 [39m[31m✕ [39m[31mnexus[39m There were errors loading 1 or more of your plugins.
 

 

sfratini avatar Aug 17 '20 15:08 sfratini