prisma-labs / graphql-framework-experiment

Code-First Type-Safe GraphQL Framework
https://nexusjs.org
MIT License
673 stars 66 forks source link

Serverless mode #782

Open jasonkuhrt opened 4 years ago

jasonkuhrt commented 4 years ago

Perceived Problem

Ideas / Proposed Solution(s)

References

jasonkuhrt commented 4 years ago

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


kristoferma commented 4 years ago

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() },
    })
    )
  2. 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.

jasonkuhrt commented 4 years ago

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 commented 4 years ago

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

kristoferma commented 4 years ago

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)
}
jasonkuhrt commented 4 years ago

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 commented 4 years ago

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.

kristoferma commented 4 years ago

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

jasonkuhrt commented 4 years ago

@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.

toddpla commented 4 years ago

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

jasonkuhrt commented 4 years ago

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.

zapbr commented 4 years ago

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?

kristoferma commented 4 years ago

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

zapbr commented 4 years ago

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.

jasonkuhrt commented 4 years ago

@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.

kldzj commented 4 years ago

@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).

jasonkuhrt commented 4 years ago

@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.

zapbr commented 4 years ago

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

AaronBuxbaum commented 4 years ago

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!

kldzj commented 4 years ago

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

jasonkuhrt commented 4 years ago

@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?

AaronBuxbaum commented 4 years ago

@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

kldzj commented 4 years ago

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

jasonkuhrt commented 4 years ago

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

sfratini commented 4 years ago

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.

heibel commented 4 years ago

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

hienlh commented 4 years ago

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

sfratini commented 4 years ago

@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

heibel commented 4 years ago

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

sfratini commented 4 years ago

@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  57 ✕ nexus There were errors loading 1 or more of your plugins.

 

 

ben-walker commented 4 years ago

I've been having a blast playing around with Nexus + Serverless for the last few days, thought I'd throw my solution out there in case it helps anyone. Still a WIP, but I'm pretty happy with what I've got so far (Prisma client operational on AWS Lambda, doesn't require webpack or any request hacking).

api/app.ts

import app, { use } from "nexus";
import { prisma } from "nexus-plugin-prisma";
import serverless from "serverless-http";
import "./graphql"; // Force injection of schema into app bundle (this is just an index.ts in the GraphQL module)

app.assemble();

export const graphqlFunc = serverless(app.server.express); // Make sure to use express rather than the graphql handler

prisma/schema.prisma

...

generator client {
  provider      = "prisma-client-js"
  // Add "rhel-openssl-1.0.x" as a binary target (AWS needs this)
  binaryTargets = ["native", "rhel-openssl-1.0.x"]
}

serverless.yaml

...

# We only include the .nexus/ directory, which keeps the size within Lambda limits
package:
  exclude: ["**"]
  include: [.nexus/**]

functions:
  graphql:
    handler: .nexus/build/api/app.graphqlFunc
    events:
      - http:
          path: /{proxy+}
          method: any

I'm still trying to wrap my head around injecting a different DATABASE_URL in different environments, and running migrations/seeds post-deployment, but I think the broad strokes are there!

sfratini commented 4 years ago

I've been having a blast playing around with Nexus + Serverless for the last few days, thought I'd throw my solution out there in case it helps anyone. Still a WIP, but I'm pretty happy with what I've got so far (Prisma client operational on AWS Lambda, doesn't require webpack or any request hacking).

api/app.ts

import app, { use } from "nexus";
import { prisma } from "nexus-plugin-prisma";
import serverless from "serverless-http";
import "./graphql"; // Force injection of schema into app bundle (this is just an index.ts in the GraphQL module)

app.assemble();

export const graphqlFunc = serverless(app.server.express); // Make sure to use express rather than the graphql handler

prisma/schema.prisma

...

generator client {
  provider      = "prisma-client-js"
  // Add "rhel-openssl-1.0.x" as a binary target (AWS needs this)
  binaryTargets = ["native", "rhel-openssl-1.0.x"]
}

serverless.yaml

...

# We only include the .nexus/ directory, which keeps the size within Lambda limits
package:
  exclude: ["**"]
  include: [.nexus/**]

functions:
  graphql:
    handler: .nexus/build/api/app.graphqlFunc
    events:
      - http:
          path: /{proxy+}
          method: any

I'm still trying to wrap my head around injecting a different DATABASE_URL in different environments, and running migrations/seeds post-deployment, but I think the broad strokes are there!

This is very interesting. As for the URL, I would use Lambda environment variables or WKS. I never understood why the env variables in lambda cannot be protected a little more. Anyone with read access to the function will see those.

So every dependency that nexus needs is in there? What about prisma client? resolvers? The nexus build actually works as webpack and generates an isolated folder?

ben-walker commented 4 years ago

Yup, everything nexus needs should be in that .nexus/build/ directory. My deploys to AWS were failing due to size constraints, and I noticed node_modules at the root of the Lambda, and node_modules in .nexus/build/; turns out you only need one set!

The Prisma Client should also be in .nexus/build/node_modules, under .prisma. As long as you run npm run build before your serverless deploy, the resolvers/prisma client/node_modules should all be there as required.

Yeah my solution for the DATABASE_URL was as follows, seems ok but it's in plaintext in the AWS Console so still not ideal :(

With the above everything is working (and also allows for migrations in the GitHub deploy action). I can access the GraphQL playground and query the DB in AWS. Eventually should maybe use AWS SSM for the DB URL though. Hope that helps!

frankyveras2 commented 4 years ago

Hello, I've been playing with Nexus + Prisma + Serverless for a few days as well, I want to share my working solution

graphql.ts

` import { settings, use } from "nexus"; import { prisma } from "nexus-plugin-prisma"; import { PrismaClient } from "@prisma/client"; import { join } from "path";

// Enable nexus prisma plugin with crud features use( prisma({ migrations: true, features: { crud: true }, client: { instance: new PrismaClient() }, }) );

settings.change({ schema: { connections: { default: { includeNodesField: true, cursorFromNode: (node, args, ctx, info, { index, nodes }) => { return node.id; }, }, }, generateGraphQLSDLFile: join(process.cwd(), "/generated/schema.graphql"), }, });`

app.ts

` import app, { use, settings, server } from "nexus"; import serverless from "serverless-http"; import * as bodyParser from "body-parser-graphql"; import raw from "raw-body"; import inflate from "inflation";

require(./pathToObjectTypes) // Here import all your object types schema

settings.change({ logger: { pretty: true, }, server: { playground: true, graphql: { introspection: true }, cors: true, }, });

app.assemble();

export const graphql = serverless(server.handlers.graphql, { async request(request: any, event: any, context: any) { const { body } = request; request.context = event.requestContext; // this is required because it's giving a weird error when trying to parse the body of the request, check by yourself without it on the playground if (request.headers["content-type"] === "application/json") { const str = await raw(inflate(request), { encoding: "utf8" }); request.body = JSON.parse(str.toString()); } return request; }, }); `

Serverless.yml

` package: exclude:

  • ./**
  • '!.nexus/build/**'

functions: graphql: handler: .nexus/build/src/app.graphql environment: GOOGLE_API_KEY: XXXXXXXXXXXXXXXX events:

  • http: path: / method: post cors: true integration: lambda-proxy

  • http: path: / method: get cors: true integration: lambda-proxy `