redwood
redwood copied to clipboard
Explore REST API from GraphQL Schema using Sofa
See: https://www.sofa-api.com
Sofa takes your GraphQL Schema, looks for available queries, mutations and subscriptions and turns all of that into REST API.
Task is to explore how Redwood might offer a REST API for the entire schema (or subset) based on the GraphQL schema.
Ie - is it possible -- a proof of concept.
This feature has been requested by @BurnedChris and others.
Deliverable is a document or RFC for how one might implement it in the framework (is this a how to, or a setup command, etc).
Later:
- Also, consider how one might secure this API using an API Key rather than standard auth.
- How best to setup in framework and/or a project
Having updated to Yoga v3, configuration is rather straightforward.
Here is an example of have the api with Swagger interactive docs UI on the Test App.
Some to dos:
- Status codes. How to return 401 or 400s for auth or input request errors instead of 500.
- How to make a subgraph of a public api
- But, in short, I believe this should be documentation and an example app rather than added to the framerwork; However,, I could see an argument to "pass in a rest api schema" and do all the setup. Will discuss.
ah love this!!! do you have any kind of timeline on a guide/etc? thanks!!
This looks great! Also @burnsy -> @burnedchris
how to configure it?
import { useSofaWithSwaggerUI } from '@graphql-yoga/plugin-sofa'
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'
export const handler = createGraphQLHandler({
loggerConfig: { logger, options: {} },
directives,
sdls,
services,
extraPlugins: [
useSofaWithSwaggerUI({
basePath: '/rest',
swaggerUIEndpoint: '/swagger',
servers: [
{
url: '/', // Specify Server's URL.
description: 'Development server',
},
],
info: {
title: 'Example API',
version: '1.0.0',
},
}),
],
onException: () => {
// Disconnect from your database with an unhandled exception.
db.$disconnect()
},
})
Hi. In my test I elected to have a separate function just for the rest api as this simplified paths (at the moment).
I made a api/src/functions/rest.ts
function that is effectively another graphql handler/server
// api/src/functions/rest.ts
import { useSofaWithSwaggerUI } from '@graphql-yoga/plugin-sofa'
import { CurrencyDefinition, CurrencyResolver } from 'graphql-scalars'
import { authDecoder } from '@redwoodjs/auth-dbauth-api'
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
import { getCurrentUser } from 'src/lib/auth'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'
const restApi = () => {
return useSofaWithSwaggerUI({
basePath: '/rest/api',
swaggerUIEndpoint: '/rest/swagger',
servers: [
{
url: '/', // Specify Server's URL.
description: 'Development server',
},
],
info: {
title: 'RedwoodJS REST API',
version: '1.0.0',
},
})
}
export const handler = createGraphQLHandler({
authDecoder,
getCurrentUser,
loggerConfig: { logger, options: {} },
directives,
sdls,
services,
graphiQLEndpoint: '/rest',
extraPlugins: [restApi()],
schemaOptions: {
typeDefs: [CurrencyDefinition],
resolvers: { Currency: CurrencyResolver },
},
onException: () => {
// Disconnect from your database with an unhandled exception.
db.$disconnect()
},
})
and then I can access the Swagger UI via:
http://localhost:8911/rest/swagger
and do basic curl at
curl -X 'GET' \
'http://localhost:8911/rest/api/redwood' \
-H 'accept: application/json'
Also, you could let me pick and choose (eventually) which operations/resolvers are public and are part of the REST api rather then the app GraphQL api.
I'd be happy to help implement this! I think it could best be presented as a setup command (rw setup rest
), which could setup a new function similar to the one above. There are a few different enhancements discussed above, and I think providing helper functions through the framework would be the best way to provide those to the user.
Select queries/mutations to include (with an allowlist or blocklist)
From doing some research into the @graphql-yoga/plugin-sofa
package, it re-initializes SOFA each time the schema changes. This could be a good point to rebuild the schema by removing some queries/mutations, but I am not sure the best way to go about this.
Error Handling Right now every error is a 500. There is the ability to provide a custom error handler function to the SOFA instance, and it seems fairly straight forward to build a function that works with RedwoodErrors.
API Key Authentication
From my understanding of how this is all laid together, it should be fairly straightforward to just use a custom auth implementation (pass in a custom authDecoder
and getCurrentUser
) to protect these endpoints with API authentication instead of standard user authentication. This might be a good place to include a basic reference implementation on the documentation page, but leave the actual full implementation to the user so they can craft a solution that fits their requirements.
Overall, in my opinion it seems simplest to create a clone of the @graphql-yoga/plugin-sofa
package (which is a fairly basic plugin that connects SOFA to yoga) so that we can have direct control over the SOFA instance. I am happy to work on development for this, I just don't want to go down the wrong path.
Thanks @codekrafter These are great ideas.
Nudging @dthyresson
Not an issue, but a future task. Devs can use Sofa for a REST api currently. In future can explore a guide or setup command.
Hey all,
Just stumbled across this. it seems the @graphql-yoga/plugin-sofa plugin has changed a little bit and it's usage is meant to be something like this:
useSofa({
basePath: 'rest',
swaggerUI: {
endpoint: '/swagger'
},
})
So I dumped that into my graphql function file like this
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'
import { useSofa } from '@graphql-yoga/plugin-sofa'
export const handler = createGraphQLHandler({
loggerConfig: { logger, options: {} },
directives,
sdls,
services,
extraPlugins:[ useSofa({
basePath: 'rest',
swaggerUI: {
endpoint: '/swagger'
},
})],
onException: () => {
// Disconnect from your database with an unhandled exception.
db.$disconnect()?
},
})
But it does not seem to do anything.
Was there any other config when you were playing with it?
I also applied the same function the example that @dthyresson gave above with no luck
Hi @shansmith01 its been a little bit since I set that up and Yoga has a new way to configure yoga via a plugin: see https://the-guild.dev/graphql/yoga-server/docs/features/sofa-api
Some thing to note is the path and endpoint. Since if you deploy GraohQL as a function then it takes the function name plus and path config you make. This is why you could create a new function called “rest” and duplicate the GraphQL handler with other imports or settings.
if you deploy with the sever file the plugin is a bit easier to setup as have more control of the paths.
I’ll try an example a little later today and try again as share.
Hi @shansmith01 - I finally got around to testing today and here's how I configured my GraphQL handler:
Be sure to install the plugin:
yarn workspace api add @graphql-yoga/plugin-sofa
import { useSofa } from '@graphql-yoga/plugin-sofa'
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'
export const handler = createGraphQLHandler({
loggerConfig: { logger, options: {} },
directives,
sdls,
services,
extraPlugins: [
useSofa({
basePath: '/graphql',
swaggerUI: {
endpoint: '/swagger',
},
}),
],
onException: () => {
// Disconnect from your database with an unhandled exception.
db.$disconnect()
},
})
Then, visit http://localhost:8911/graphql/swagger
Note: I scaffolded a Country
model with 4 seeded countries.
And curl:
~ % curl -X 'GET' \
'http://localhost:8911/graphql/countries' \
-H 'accept: application/json'
[{"id":1,"name":"Sweden","code":"SE"},{"id":2,"name":"Finland","code":"FI"},{"id":3,"name":"USA","code":"US"},{"id":4,"name":"Canada","code":"CA"}]%
~ %
Note: I think there are some other ways to configure to get an endpoint you may prefer.
For example, you can leave your graphql.ts
function as is, and the duplicate it in functions as rest.ts
, and then:
import { useSofa } from '@graphql-yoga/plugin-sofa'
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'
export const handler = createGraphQLHandler({
loggerConfig: { logger, options: {} },
directives,
sdls,
services,
graphiQLEndpoint: 'rest',
extraPlugins: [
useSofa({
basePath: '/rest/api',
swaggerUI: {
endpoint: '/swagger',
},
}),
],
onException: () => {
// Disconnect from your database with an unhandled exception.
db.$disconnect()
},
})
```a
Then you can curl at:
```bash
~ % curl -X 'GET' \
'http://localhost:8911/rest/api/countries' \
-H 'accept: application/json'
[{"id":1,"name":"Sweden","code":"SE"},{"id":2,"name":"Finland","code":"FI"},{"id":3,"name":"USA","code":"US"},{"id":4,"name":"Canada","code":"CA"}]%
~ %
If you want status codes for auth, we haven't set those in RW by default for Authentication errors etc, but, in your service you can throw
throw new RedwoodGraphQLError('This is a custom auth message', {
http: {
status: 401,
},
})
Might just need some added extension info in https://github.com/redwoodjs/redwood/blob/e798075ca6e81655f8ae7869664004ccf94633d2/packages/graphql-server/src/errors.ts#L42 but that might break the ApolloClient error handling.
If perhaps you have dedicated services for REST endpoints, probably can just throw errors you need for 401 or 4xx as needed.
Hope this helps!
Thanks @dthyresson.
This is interesting stuff, had a bit of a mess around with the idea of making different parts of my schema available instead of the whole thing and had a quick play with error codes. If I explore further I will share some info on auth
Where this is super interesting for me is with RSC. If I am building an app where my product has an API that needs to be consumed I can use RSC to make the UI for my app and then use schema files to generate the API in rest/graphql to be consumed by my customers.