Open dthyresson opened 2 years ago
@nnennajohn You would mind elaborating on your original question and maybe describe the use case?
Then @dac09 and I will have a think as see what might be possible and check back with you.
Thanks for setting this up @dthyresson .
The main use case for this is to leverage existing apps to add functionality to your SAAS applications, without having to build from scratch. A lot of large organizations use a hosted CMS because it's really easier for marketers, designers, content-editors etc to make updates to pages, without investing in building a full blown CMS that does essentially what several existing offerings already do. Why build a full in-house CMS if your content-editors can use say Ghost, WP, GraphCMS, Contentful, Content-Stack, etc. Same goes for E-commerce.
Now, most of these apps already offer graphql endpoints. In building an app that consumes data from them, you'd either have to resort to good-old rest/redux approach, a federated graph, or perhaps my hacky approach of nested providers and webhooks. This assumes you have a more complex app, where content-management is a minor subset of your eng needs.
If Redwood provides a means to stitch schemas, it becomes more appealing to larger-scale apps that have needs beyonds a simple data model with prisma. The prisma models can still be used for all integral parts of the app, and minor parts can be offloaded to the externals schema. In my case, Shopify for subscriptions(still stripe under the hood), as I want to have access to the rest of Shopify's ecosystem around social marketing, analytics, etc. And for the site blog, some external CMS. For the actual app's functionality, its all prisma models.
Hope that all makes sense.
I spent quite some time trying to get it to work yesterday, by recreating the createGraphQLHandler
function. Copied from the repo into my local functions/graphql.ts
and updated schema to be a stichedSchema. I think I almost got it working, but it choked on something to do with aws-lambda. When I stitch the schemas outside of redwood and run with a simple express/apollo-server, it works. So I think maybe it's attempting to run the entire schema a certain way. Not really sure. Gave up after a few hours :)
Thanks again for opening the conversation of this.
The general idea is below. Quoting my previous comment:
import { introspectSchema } from '@graphql-tools/wrap'
import { fetch } from 'cross-fetch'
import { print } from 'graphql'
import { FilterRootFields, RenameTypes, FilterTypes } from '@graphql-tools/wrap'
import { stitchSchemas } from '@graphql-tools/stitch'
async function shopifyRemoteExecutor({ document, variables }) {
const query = print(document)
const fetchResult = await fetch(
process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_API_ENDPOINT,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Storefront-Access-Token':
process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN,
},
body: JSON.stringify({ query, variables }),
}
)
return fetchResult.json()
}
const shopifySchema = {
schema: await introspectSchema(shopifyRemoteExecutor),
executor: shopifyRemoteExecutor,
// https://www.graphql-tools.com/docs/schema-wrapping
transforms: [
new RenameTypes((name) => `Shop${name}`),
],
}
async function graphCMSRemoteExecutor({ document, variables }) {
const query = print(document)
const fetchResult = await fetch(process.env.NEXT_PUBLIC_GRAPH_CMS_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.NEXT_PUBLIC_GRAPH_CMS_API_ACCESS_TOKEN}`,
},
body: JSON.stringify({ query, variables }),
})
return fetchResult.json()
}
const graphCMSSchema = {
schema: await introspectSchema(graphCMSRemoteExecutor),
executor: graphCMSRemoteExecutor,
// https://www.graphql-tools.com/docs/schema-wrapping
transforms: [
new FilterRootFields((operation, rootField) => rootField !== 'collections'),
new RenameRootFields((_operation, name) => `Cms${name}`),
new RenameTypes((name) => `Cms${name}`),
],
}
export const gatewaySchema = stitchSchemas({
subschemas: [
shopifySchema,
graphCMSSchema,
// redwoodSchema?
],
// mergeTypes: true, // << default in v7
// typeMergingOptions: {}
})
The Redwood makeMergedSchema takes sdl
and services
as options. But perhaps it could take additional schemas. And instead of returning the result of addResolversToSchema, can switch to returning a call to stitchSchemas
if additionalSchemas exist, passing the result of addResolversToSchema into the array passed to stitchSchemas with the addtionalSchemas.
Ok. That was long. Some faux code:
Inside the makeMergedSchema func:
const redwoodSchema = addResolversToSchema({
schema,
resolvers,
resolverValidationOptions,
inheritResolversFromInterfaces,
})
if(additionalSchema) {
return stitchSchemas({
subschemas: [
redwoodSchema,
...additionalSchema,
],
...additionalSchemaOptions // like typeMergingOptions
})
}
return redwoodSchema
Really would love to get this to work, as I love everything else about Redwood and its convention over configuration approach.
@nnennajohn I have a question - if you were able to combine the Shopify and GraphCMS schemas with your Redwood api's, how would you write your resolvers and services?
Would you have a "shop" service and then implement the queries and mutations using the shopifyRemoteExecutor
as a client?
Another option is GraphQL Mesh: https://www.graphql-mesh.com
And the SDK implementation: https://www.graphql-mesh.com/docs/recipes/as-sdk instead of the Gateway (which runs a separate server).
I experimented with this over a year ago, but this might be more aligned with what you want to do -- since you actually want to call out toe Shopify and GraphCMS with an authenticated client.
Hi @dthyresson
Thanks for following up on this. Actually, you don't need a separate shop service. If you stitch schema, it intelligently delegate to the underlying executor, and you still end up with one server.
Below is an example of what I spun up, just to try to get it working. In the below instance, localSchema would be replaced with redwoodSchema. The localSchema I have below is built with prisma and nexus-prisma which I use to write the local resolvers. I end up with one schema, which I stitch with shop and cms and pass to apollo server.
Below is an example. Including files, localSchema, externalSchema, serverFile and end-result in Apollo. Used nexus mostly for brevity. I've never used it in prod and don't think it is ready. Just a heads up.
LocalSchema:
ExternalSchemas:
FinalServer: Simplified for brevity.
End Result: Able to deploy one graph and query them all. Similar to well - One Graph
example
query is localSchema, cmsCategories
is from graphcms and products
is from shopify.
Now, if I could use this in Redwood, life would be great! :)
This is obviously simplified. But since this is all coming from one server, you can use auth and role based access control on all fields including the externalSchema using something like graphql shield, extra layer of security or whatever middleware you want on the server. Also, one typegen, single source of truth,
You could do this with StepZen pretty simply (it's where I work, so I'm of course biased). I already have a bunch of example projects out there showing how to do it:
You end up with a schema and a few directives like so:
type Product {
id: ID!
handle: String
title: String
}
type Query {
products: [Product]
@rest(
resultroot: "products[]"
endpoint: "https://$api_key:$api_password@$store_name.myshopify.com/admin/api/2020-01/products.json"
configuration: "shopify_config"
)
}
type PostItems {
items: [PostItem]
}
type PostItem {
name: String
content: Content
}
type Content {
title: String
intro: String
}
type Query {
PostItems: PostItems
@graphql(
endpoint: "https://gapi.storyblok.com/v1/api"
headers: [
{ name:"Token" value:"$token" }
]
configuration: "storyblok_config"
)
}
Then on the Redwood side you can call an external API that StepZen deploys for you and you send regular GraphQL queries in your services.
import { GraphQLClient } from 'graphql-request'
export const request = async (query = {}) => {
const endpoint = process.env.API_ENDPOINT
const graphQLClient = new GraphQLClient(endpoint, {
headers: {
authorization: 'apikey ' + process.env.API_KEY
},
})
try {
return await graphQLClient.request(query)
} catch (err) {
console.log(err)
return err
}
}
import { request } from 'src/lib/client'
import { gql } from 'graphql-request'
export const products = async () => {
const GET_PRODUCTS_QUERY = gql`
query getProducts {
products {
title
id
handle
}
}
`
const data = await request(GET_PRODUCTS_QUERY)
return data['products']
}
Hi @ajcwebdev ,
This is quite different from your setup with stepzen, which requires you writing the typedef for each item, then running a separate stepzen server, and manually telling your sdl items which endpoint to resolve to.
In the case of a stitched schema, the only additional configuration is the RemoteExecutor func, and you get access to all shopify api. Also, no additional servers.
I want to be able to pass in an array of additional schemas to the createGraphQLHandler, with a configuration for type conflict or just nest like the One Graph setup, and everything should just work.
In the case of a stitched schema, the only additional configuration is the RemoteExecutor func, and you get access to all shopify api. Also, no additional servers.
Useful to know, thanks @nnennajohn! Also thanks for sharing your implementation - ~I wonder if this is a ApolloServer specific feature. Something to look into~ Came across this through graphql-tools, so a lot to explore https://github.com/gmac/schema-stitching-handbook
Once we have time to evaluate this, I suspect we won't be using the same createGraphqlHandler
function, given the specific usecase (and potentially the other configurations that may be needed), we probably want to have a different builder function!
The challenge for me with all of this graphql stuff is that pretty much everything is just called a "schema". What does that even mean? Just the schema file, or does it mean this special remote schema, or does it mean DocumentNode - you never know. 🙄
@dac09 Thanks for sharing that repo. Looks like a great resource to figure this out. Diving in.
More input to bump this concept. Schema stitching, or something like this, would be very helpful for my use case. We have an existing internal company application where we have built a client/app/roles user management system. Our framework allows us to have many client organizations which may have multiple, and possibly distinct apps, available to them. Also within each org, each user has differential app availability and roles.
We use a postgres DB with a graphql api. We have the user/client/app/roles tables in one schema, and in the same database we have a second schema for the app data. I am going in circles trying to figure out how to connect to our user management system with prisma/RW. I do not want to mess with the existing table structure, so I really want to leverage our existing graphql api and keep the rw data in a separate table or db.
I have seen the prisma issue for supporting querying across multiple schemas. DT actually commented with a solution that he found works for this (https://github.com/prisma/prisma/issues/1122#issuecomment-812076938). I am not well versed, so I have not yet figured out how to implement his solution... but, this issue is currently a WIP on the Prisma roadmap, so maybe it will be available relatively soon. This would probably close this issue for me, as long as RW supports this feature when it comes out.
However, after thinking about broader use cases, like @nnennajohn's case or the schema-stitching handbook samples, this seems like a pretty common and very useful feature. I want to keep writing, but my understanding of the terminology and concepts involved will confuse you. I know ajc has some stepzen cookbooks that illustrate this functionality using stepzen. However, I like @nnennajohn's idea in reply to ajc, of having a stitched schema with no extra servers, all written within my RW app.
Would be curious if there is any plan for schema stitching, or remote joins like Hasura does. Currently, I have shopify api in a separate package and using urql to access it on the pages I need. There's probably a better way to handle it in Redwood.
Originally posted by @nnennajohn in https://github.com/redwoodjs/redwood/issues/3627#issuecomment-951911624
See: Combinging schemas in GraphQL tools.
Perhaps Redwood can make it easier to combine third party schemas, like some Shopify apis