Open jasonkuhrt opened 4 years ago
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.
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
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
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
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() },
})
)
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.
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.
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"? ...
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.
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)
}
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
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.
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 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.
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
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.
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
?
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 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 installThis 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.
@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 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 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.
Hallo @jasonkuhrt, thank you for your quick response. So we build a little demo with the issue:
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 great find, but seems very ugly when you have a lot of models. We decided to just migrate back to @nexus/schema.
@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 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
@jasonkuhrt wanting to use federation was another reason, but mainly for ease of serverless deployment.
@AaronBuxbaum If you can contribute an example or recipe that would be great. It seems that your technique is similar to our nextjs recipe.
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.
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
I tried all of code above but I can not run my serverless. Does Nexus support serverless?
@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 I forgot to mention I deploy to/with Vercel. Which works with the above code. Maybe you can share your code/lambda errors?
@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. |
---|
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!
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?
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 :(
sls deploy
serverless.yaml
, add environment: DATABASE_URL: ${env:DATABASE_URL}
, which will read from the GitHub secret during the deployWith 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!
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 `
Perceived Problem
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.
...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:
server.platformHandle
orserver.handle.fromPlatform
but it is unclear why we need the indirection, why we need both. It seems right now that we should just makeserver.handle
be that.The request handler should be exported for use by the host platform. Example with nextjs (with vercel):
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
server.start
server.stop
APIs should only appear in serverful modeserver
jsDoc should reflect the current server modeserver.express
API should not appear in serverless mode. Express should not even be in the final built bundle.port
andhost
should only appear in serverful modepath
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 thepath
configuration makes sense in serverless mode, however it stops making sense once a user needs to manually configure path in another system.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?playground.path
has the same issues as described forpath
. 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
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