nexus icon indicating copy to clipboard operation
nexus copied to clipboard

How to do schema stitching with Nexus?

Open otrebu opened this issue 5 years ago • 24 comments

Hi there!

This is a question not an issue or a bug 😄

What is it the best way to do schema stitching with nexus? The only thing I can think about is using the converter to convert the schemas to code first, but it doesn’t sound ideal and I am not sure if it is possible to use fragment with it.

Thanks for the amazing work!

otrebu avatar Mar 05 '19 09:03 otrebu

I think we can just use graphql-tools for this.

Since the output of nexus makeSchema function is a graphql js schema, one can easily use packages from the existing ecosystem like graphql-shield and many others.

import {
  makeExecutableSchema,
  addMockFunctionsToSchema,
  mergeSchemas,
} from 'graphql-tools';
import { makeSchema } from 'nexus';


const nexusSchema = makeSchema({
  types: [Account, Node, Query, StatusEnum],
  // or types: { Account, Node, Query }
  // or types: [Account, [Node], { Query }]
});

const authorSchema = makeExecutableSchema({
  typeDefs: `
    type User {
      id: ID!
      email: String
    }

    type Query {
      userById(id: ID!): User
    }
  `
});

addMockFunctionsToSchema({ schema: authorSchema });

export const schema = mergeSchemas({
  schemas: [
    authorSchema,
    nexusSchema
  ],
});

pantharshit00 avatar Mar 06 '19 07:03 pantharshit00

@pantharshit00 oh that is great thank you!

Just to clarify a couple of things:

addMockFunctionsToSchema({ schema: authorSchema }); this is optional and just for mocking/testing right?

export const schema = mergeSchemas({
  schemas: [
    chirpSchema,
    nexusSchema
  ],
});

Did you mean authorSchema rather than chirpSchema?

😄

Also do you know if it would be possible to also extend the merged schema with nexus, rather than with graphql tool mergeSchema? Like this:

return mergeSchemas({
        schemas: [
            bookingSchema,
            customerSchema
        ],
        resolvers: {
            Booking: {
                customer: {
                    fragment: `... on Booking { id }`,
                    resolve(booking, args, context, info) {
                        return info.mergeInfo.delegateToSchema({
                            schema: customerSchema,
                            operation: "query",
                            fieldName: "customers",
                            args: {
                                where: { id: booking.customerId }
                            },
                            context,
                            info
                        });
                    }
                },

To be fair, probably it is no point to try to do it differently.

otrebu avatar Mar 06 '19 09:03 otrebu

@otrebu there are some good solutions for doing schema extension and composition with other GraphQL schemas in Nexus that I need to document / add a few new options for.

Unfortunately don't have time to fully go into them right this minute, but I'll leave this ticket open and report back once I have some time to document/add some examples for how to do this.

Would you be able to share the general structure of how your schema is split up and how you'd expect a tool that allowed schema stitching with nexus to work? Would most types be written in nexus and only some would be stitched in?

Also, have you seen the extendType api for building a schema from multiple places in the codebase? There's also a mutationField and I'll soon add a similar queryField api for this use case.

tgriesser avatar Mar 06 '19 14:03 tgriesser

doing schema stitching using mergeSchemas from graphql-tools, working great so far.

using extendType is problematic if the schema is create from a remote schema, because the typechecking have no idea the type to extends exists

kandros avatar Mar 08 '19 11:03 kandros

@tgriesser thank you for reply, and thank you @kandros for your comment. It would be great to see more examples, as I didn't try to implement the stitching yet.

I would need to create another example, as what I am working on it is for a client. It might take me a while before I can find the time to do this, but I can try.

I need to look into extendType, as I didn't see it yet.

otrebu avatar Mar 08 '19 15:03 otrebu

@otrebu most of the example are using features not yet documented, so it's a great place to look into

https://github.com/prisma/nexus/blob/18401c465a745254fb8dbd3c5244bdc1f02c1d11/examples/kitchen-sink/src/kitchen-sink-definitions.ts

kandros avatar Mar 08 '19 17:03 kandros

Hi @tgriesser do you have any update about the documentation?

I started to implement stitching alongside nexus like @pantharshit00 kindly showed me. For something simple works as a treat. Happy days.

But then I found myself in the scenario (or similar at least) @kandros mentioned!

I would like to add query and mutations that return types that are defined in remote schemas (in my scenario I have full control on those). Nexus is not aware of those types because are stitched after. I would be cool to stitch "before"/ within nexus, so that it is aware of this other types and they become usable.

Otherwise I should merge these schemas, use the converter. It is not optimal I would say.

otrebu avatar Mar 15 '19 12:03 otrebu

It's possible, from an introspected schema we could generate nexus code but pretty hard to accomplish, it needs lot of tooling work but looks doable.

I suggest you take a look at nexus codebase to get a grasp of how typings generation is applied, what happens is crazy but easy to follow along 😁

kandros avatar Mar 15 '19 13:03 kandros

@kandros I horribly done that before, I was hoping to be a temporary solution though. But in the scenario below I didn't want mutations and queries.

I have done something like this(horrible):

import * as fs from "fs";
import { parse, DocumentNode, buildASTSchema, printSchema } from "graphql";
import { convertSDL } from "nexus";

const sdl = fs.readFileSync(__dirname + "/unified-graphql/schema.graphql", "utf-8");

const parsedSchema = parse(sdl);

const newDefinitions = parsedSchema.definitions
    .filter(
        (d: any) =>
            !d.name.value.includes("Query") &&
            !d.name.value.includes("Mutation") &&
            !d.name.value.includes("Subscription") &&
            !d.name.value.includes("Edge") &&
            !d.name.value.includes("Aggregate") &&
            !d.name.value.includes("Connection")
    );

const filteredSchema: DocumentNode = {
    kind: "Document",
    definitions: newDefinitions,
    loc: parsedSchema.loc
};

const newSchema = buildASTSchema(filteredSchema);

const codeFirstString = convertSDL(printSchema(newSchema));

fs.writeFileSync(__dirname + "/someTypesFromPrisma.ts", codeFirstString, {
    encoding: "utf-8"
});

otrebu avatar Mar 15 '19 14:03 otrebu

Hi @tgriesser, sorry to tag you again.

It would be good to have your view. I see this topic being related to this one on the nexus-prisma project plugin.

It would be good to be part of nexus, or a plugin like nexus-prisma the ability to stitch. As like in my case I would like to be able to add a query and mutations of types I stitched in.

otrebu avatar Mar 18 '19 16:03 otrebu

@tgriesser to answer your question 😄

Would you be able to share the general structure of how your schema is split up and how you'd expect a tool that allowed schema stitching with nexus to work? Would most types be written in nexus and only some would be stitched in?

At the moment I am doing something like this:

const stitchSchemas = async () => {
    const organisationSchema = await getRemoteSchema(
        prismaEndpoints.organisations
    );
    const customerSchema = await getRemoteSchema(prismaEndpoints.customers);

    return mergeSchemas({
        schemas: [
            organisationSchema,
            customerSchema,
            nexusSchema,
            linkTypeDefs // where I extend some types from organisationSchema and customerSchema
        ],

        resolvers: {
            /// resolvers to delegate with the extended schema
        }
    })};

nexusSchema is not aware of the types in organisationSchema and customerSchema.

So if I create a mutation using nexus:

const Mutation = mutationType({
        definition(t) {
            t.field("createCustomerFromPlainPassword", {
                type: "Customer", // I can't do this 😢 
                args: {
                    input: arg({ type: createCustomerFromPlainPasswordInput })
                },
                resolve: async (
                    root,
                    { input },
                    { customerService },
                    info
                ) => {
                    
                    const newCustomer = await customerService.createCustomer(
                        input
                    );
    
                    return newCustomer;
                }
            });
        }
    });

I can't do that, Customer type is defined in my customerSchema.

So when you ask me what I would really like to be able to do, it is to stitch from nexus so that it is aware of my other types.

const nexusSchema = makeSchema({
        externalSchemasToStitchIn:{
            schemas: [customerSchema, organisationSchema],
            excludeTypes: ["Role"]
        }
        types: [CreateCustomerFromPlainPasswordInput, Query, Mutation],
        shouldGenerateArtifacts: true,
        outputs: {
            schema: path.join(__dirname, "./generated/schema.graphql"),
            typegen: path.join(__dirname, "./generated/types.ts")
        },
        typegenAutoConfig: {
            contextType: "ctx.IContext",
            sources: [
                {
                    alias: "ctx",
                    source: path.join(__dirname, "../context.ts")
                }
            ]
        }
    });

So that when I extend my Mutation type I can return my Customer type. Of which shape I am sure will respect the schema.

otrebu avatar Mar 19 '19 12:03 otrebu

Thanks for the info - I need to think a little more about how this will work and come up with some recommended patterns here. I'm considering incorporating something similar to the tools in Prisma to handle schema delegation - but it require a decent bit of work, so no ETA on it at the moment.

By the way, have you tried doing it the other way around, feeding the types generated from the merged schemas into Nexus? Haven't tried it, but something like:

const mergedSchemas = mergeSchemas({
  schemas: [
    organisationSchema,
    customerSchema,
  ],

  resolvers: {
    /// resolvers to delegate with the extended schema
  }
})};

const addedMutationField = mutationField("createCustomerFromPlainPassword", {
  type: "Customer", // should be able to do this now.
  args: {
      input: arg({ type: createCustomerFromPlainPasswordInput })
  },
  resolve: async (
      root,
      { input },
      { customerService },
      info
  ) => {
    const newCustomer = await customerService.createCustomer(
        input
    );
    return newCustomer;
  }
});

const finalSchema = makeSchema({
    types: [schema.getTypeMap(), addedMutationField],
    shouldGenerateArtifacts: true,
    outputs: {
        schema: path.join(__dirname, "./generated/schema.graphql"),
        typegen: path.join(__dirname, "./generated/types.ts")
    },
    typegenAutoConfig: {
        contextType: "ctx.IContext",
        sources: [
            {
                alias: "ctx",
                source: path.join(__dirname, "../context.ts")
            }
        ]
    }
});

tgriesser avatar Mar 19 '19 16:03 tgriesser

By the way, have you tried doing it the other way around

Thought about this more after making the previous comment and this approach won't quite work as I mentioned - though it's how it should work eventually once the appropriate changes are made to support this. Will work on some approaches for this and keep you posted once there's something to try out here.

tgriesser avatar Mar 20 '19 00:03 tgriesser

@tgriesser thanks!

I see this topic being related to this from the nexus-prisma plugin: https://github.com/prisma/nexus-prisma/issues/129

What are your thoughts on that?

Just to give you an idea, this is the infrastructure I am working on: There are 4 different prisma services, which we then stitch together. This stitched graphql server which contains all the CRUD operations from these 4 different prisma servers also does some extensions. Adding some queries, mutations and types. At the moment I achieve this by using the converter. Convert the stitched schema into code first, so that I have the type I need to create my nexus schema. Then stitching again, including the nexus schema. This is not ideal, but it seems working.

Then we have another 3 graphql servers in front of the unified graph server. On these we will do the auth, maybe extending/hiding some queries/mutations/types. If there were a generalised nexus-prisma it would be great to use that approach to achieve the above. This last paragraph only partially concerning you, but at least you have a picture of a scenario, that doesn't seem so strange in a world of microservices.

If you could help me to help you and help @Weakky . Cheers!

otrebu avatar Mar 20 '19 09:03 otrebu

Any update on this given the recent transition to Nexus Framework?

I would love to be able to combine several nexus microservices using a nexus gateway in some way that allows me to reuse my types from the microservices in the gateway code.

OliverEvans96 avatar Apr 29 '20 13:04 OliverEvans96

Same here. I got an apollo server with the old nexus schema (and sequelize) and building a new service with nexus framework (and prisma) and want to gradually move over the entire system, but its tightly coupled so as soon as I move one objectType over, it has fields that references other ObjectTypes and I keep moving down a rabbithole, so need schema stitching or federtion or something to allow all fields that aren't yet implemented to default back to the old service

cyrus-za avatar Sep 08 '20 19:09 cyrus-za

Same here as well. It'd be nice to have a gradual migration path from SDL or graphql-js types. It seems like one option is to generate stub Nexus types for everything and merge it with graphql-tools, but that's a rather brittle process.

swac avatar Dec 09 '20 21:12 swac

Not sure if will address everything above, but see https://github.com/graphql-nexus/nexus/issues/148#issuecomment-747998067 for plans related to stitching code first schemas

yaacovCR avatar Dec 18 '20 10:12 yaacovCR

I created a repo with a simple example here, hope it helps someone looking to get a feel of how it is done: https://github.com/nayaabkhan/nexus-stitching-example

nayaabkhan avatar Apr 11 '21 09:04 nayaabkhan

Have needed to do this in a real world situation so I'm planning on adding first class support for better support of properly merging in types from an external schema.

tgriesser avatar Sep 03 '21 13:09 tgriesser

See #983 which will offer better support for interop with an existing schema

tgriesser avatar Sep 05 '21 16:09 tgriesser

As follow up, contrast, https://github.com/gmac/schema-stitching-handbook/tree/master/subservice-languages/javascript shows a schema stitching way of doing this where you annotate your nexus schema with directives to teach the gateway how to merge your types

Key difference is that the nexus schema is a “remote” schema in this example, ie set up as a separate micro service. I am not advocating for this; in my opinion, it is always better when possible to manage your schema as a monolith, except when you can’t. graphql-tools schema stitching is there for you when you can’t.

otherwise, you are probably best off with schema merging. graphql tools has utility functions for that, too, but definitely makes sense to take advantage of any local framework capabilities like described above now in nexus

yaacovCR avatar Sep 05 '21 17:09 yaacovCR

Check out 1.2.0-next.14 / #983 for better support here, idea being that the local Nexus constructed schema definitions can consume & merge with an external schema, with configuration options for merging specific types, and/or omitting individual types / fields /args.

If have multiple schemas, you'll need to merge into a single schema before consuming using something like graphql-tools wrap / merge, and even if you don't have multiple schemas you'll probably want to use delegation to issue queries against the remote schema.

Would love to get any feedback on it, I'm planning on using it for a real world use case with Cypress and will continue to refine the API as needed, so consider it potentially experimental/subject to change.

tgriesser avatar Sep 06 '21 14:09 tgriesser

are there any docs ? I would like to test it out!

m1cl avatar Sep 27 '21 13:09 m1cl