gramps-graphql / gramps--legacy

The core data source combination engine of GrAMPS.
https://gramps.js.org
MIT License
197 stars 13 forks source link

Integration issues #62

Open kbrandwijk opened 6 years ago

kbrandwijk commented 6 years ago

After having a good look at the getting started and the sources, I'd like to share a few integration issues that I've come across. Basically the biggest issue is the fact that the output from gramps() goes directly into the apollo-server middleware. I thought I could get around it by accessing the individual properties of that object (for example, the schema), but that's also not possible, because the output is actually a function of req. Of course, I can work around that as well, by running gramps()(null).schema to get to the schema, but that doesn't really feel right.

This means that I end up with an all or nothing solution, which is not really what I would like. I'd like to have the ability to composition these components into my own server.

To be able to do this I need the following:

This might be as easy as:

const gramps = gramps()
const schema = gramps.schema

// do my own stuff with my schemas

app.use('/graphql', gramps.setContext())

// do my own stuff in the context

app.use('/graphql', graphqlExpress({ schema: finalSchema, context: req => req, /* additional options */ })

Now, if I didn't need all of that, I would still be able to do:

const gramps = gramps()
app.use('/graphql', express.json(), graphqlExpress(gramps.serverOptions));

So it wouldn't impact the getting-started experience much, but would make it a lot easier to either use it in a more mixed environment, or integrate it with other tools (like graphql-yoga and my upcoming supergraph framework).

jlengstorf commented 6 years ago

I think we could make this work, yeah.

For default cases, I think GrAMPS working standalone makes sense, so I don't think I'd want to change that API (especially because breaking changes this early on seem ill-advised).

However, I think we could easily abstract out the core parts of GrAMPS into named exports, so you could do something like this:

import { generateExecutableSchema } from 'gramps';

const gramps = generateExecutableSchema();

const schema = gramps.schema;

// do whatever you want

app.use('/graphql', graphqlExpress({
  schema: finalSchema,
  context: req => ({
    req,
    gramps: gramps.context(req),
  }),
  // additional options...
}));

Were you thinking the setContext() method in your example would be middleware to attach the context to req? Or something else?

jlengstorf commented 6 years ago

^^ to clarify, generateExecutableSchema would effectively return the same GraphQLOptions object as the default GrAMPS export, but wouldn't wrap it with the req => /* ... */ function.

kbrandwijk commented 6 years ago

And where would I pass in my GrAMPS datasources in this example?

Also, as the function does not return just the executable schema, maybe you can call it prepare()?

kbrandwijk commented 6 years ago

Regarding the context, that depends on where you 'need' it to be. I see in your example you pass it in on the top level, so you would have (in apollo) context.req and context.gramps. I usually pass in the entire req object as context, so if I add in to req in an Express middleware, it would still end up in context.gramps for apollo, which is good.

Maybe you could expose an actual middleware function too, so I don't have to do:

app.use((req, res, next) => { 
   req.gramps = gramps.context(req)
   next()
}

But could instead do:

app.use(gramps.contextMiddleware)
jlengstorf commented 6 years ago

prepare() makes sense, yeah.

It would have the same API as gramps(), I imagine, with the limitation that it doesn't have access to req for extraContext.

We could refactor under the hood so that gramps() calls prepare() to make sure we keep things DRY.

Data sources scope context in resolvers to their own namespace + the content of extraContext, so we'd have to think through that. Basically anything that a GrAMPS data source needs to access will need to be attached via:

const gramps = prepare({
  dataSources: [/* ... */],
  extraContext: {
    // any extra context needed by GrAMPS data sources goes here
  },
});
kbrandwijk commented 6 years ago

See my proposal (very initial version, no linting, tests, or even running). I don't think there's an issue with extraContext.

jlengstorf commented 6 years ago

At first glance, this looks like it's the right direction. @ecwyne, anything seem out of place with the API in #63?

kbrandwijk commented 6 years ago

So, to recap, that would mean you could alternatively do (keeping the existing API 100% intact):

import { prepare } from 'gramps'

const gramps = prepare({ /* all gramps() parameters here */ })
const schema = gramps.schema

// do my own stuff with my schemas

app.use('/graphql', gramps.contextMiddleware)

// do my own stuff in the context

app.use('/graphql', graphqlExpress({ schema: finalSchema, context: req => req, /* additional options */ })

Looking at this code sample, app.use('/graphql', gramps.context) or app.use('/graphql', gramps.addContext()) might be easier to read.

jlengstorf commented 6 years ago

This seems most descriptive to my eye:

app.use('/graphql', gramps.addContext);

As long as the existing API stays intact, I think this is a solid addition for advanced use cases.

kbrandwijk commented 6 years ago

As add is a verb, I would go for gramps.addContext(), not gramps.addContext, but that's just semantics.

kbrandwijk commented 6 years ago

I threw together a quick boilerplate, for use with graphql-cli. Run the following command to create a server:

graphql create server -b https://github.com/supergraphql/gramps-boilerplate.

As you can see in the resulting index file, this now integrates perfectly with the existing (best practice) server boilerplates and the other tools.

jlengstorf commented 6 years ago

This is awesome. Thank you!

ecwyne commented 6 years ago

This is a totally different take on a solution, but let me know what you think @kbrandwijk. If what you need is the resulting schema and the getContext function created by GrAMPS, what if we just add those as properties to the middleware function returned by GrAMPS.

// gramps.js
const middleware = req => ({
    schema,
    context: getContext(req),
    ...apolloOptions.graphqlExpress,
  });
  middleware.schema = schema;
  middleware.getContext = getContext;
  return middleware;

Then you can use them however you'd like.

import gramps from 'gramps'

const middleware = gramps({ /* all gramps() parameters here */ });
const { schema, getContext } = middleware;

// do my own stuff with my schemas

app.use('/graphql', middleware)

// do my own stuff in the context

app.use('/graphql', graphqlExpress({ schema: finalSchema, context: req => req, /* additional options */ })
kbrandwijk commented 6 years ago

@ecwyne Although technically allowed in JS, the signature of what gramps() returns is a bit awful like this. I don't like functions with additional properties.

Also, for type safety in TS, it would result in a typing declaration that uses declaration merging in order to work, which I also try to avoid usually.

function middleware(req: express.Request): ApolloServerOptions { }
namespace middleware {
    // schema and getContext here;
}
jlengstorf commented 6 years ago

I'm inclined to agree: adding props to functions has always looked hacky to me, and I think it increases the cognitive overhead of understanding what's going on.