graphql-nexus / nexus

Code-First, Type-Safe, GraphQL Schema Construction
https://nexusjs.org
MIT License
3.4k stars 274 forks source link

typegenAutoConfig support for modular context types #323

Open jasonkuhrt opened 4 years ago

jasonkuhrt commented 4 years ago

We have a use-case for something like this:

typegenAutoConfig: {
  contextType: 'ContextA.Context & ContextB.Context & ContextC.Context',
  sources: [
    { alias: 'ContextA', source: '...a.ts', onlyTypes: [] },
    { alias: 'ContextB', source: '...b.ts', onlyTypes: [] },
    { alias: 'ContextC', source: '...c.ts', onlyTypes: [] },
  ]
}

It is possible by using manual typegenConfig. But going manual ~gives up~ (https://github.com/prisma-labs/nexus/issues/323#issuecomment-554016318) a non-trivial amount of feature-set.

So, got thinking, is our use-case generic enough a pattern to warrant inclusion into typegenAutoConfig?

The pattern is modular context sources. It doesn’t work right now, at least in part (haven’t digested the whole module logic yet) because there is this regex assumption:

    const forceImports = new Set(
      objValues(backingTypeMap)
        .concat(contextType || "")
        .map((t) => {
          const match = t.match(/^(\w+)\./);
          return match ? match[1] : null;
        })
        .filter((f) => f)
    );
'A.A & B.B & C.C'.match(/^(\w+)\./)
[ 'A.', 
  'A', 
  index: 0, 
  input: 'A.A & B.B & C.C', 
  groups: undefined ] 

meaning B C will not be forced imports, meaning those modular context contributions will be TS type reference errors in the typegen file because they are never imported.

~It would be relatively trivial to to add support for TS & construct into the above regex. Here's a working draft (^(\w+)\.|\s+&\s+(\w+)\.).~

~It might be a bit magical, but of course we would document it, and it seems intuitive.~

-- edit see later proposals below.

jasonkuhrt commented 4 years ago

Revising part of the idea. Another way to make the type ref would be:

contextType: 'MergedContext'

Given an additional header is added like:

interface MergedContext extends ContextA.Context, ContextB.Context, ContextC.Context

And doing this has material impact on the TS IDE intellisense DX.

So we need a more general solution than my regex tweak suggestion.

I think the simplest thing we can for this feature for this API is add a new contextType option to sources.

If a source is marked as being a forContext then it forces it being an import.

typegenAutoConfig: {
  contextType: 'ContextA.Context & ContextB.Context & ContextC.Context',
  sources: [
    { alias: 'ContextA', source: '...a.ts', forContext: true },
    { alias: 'ContextB', source: '...b.ts', forContext: true },
    { alias: 'ContextC', source: '...c.ts', forContext: true },
  ]
}
jasonkuhrt commented 4 years ago

This issue should probably be considered in conjunction with what we do in #265.

jasonkuhrt commented 4 years ago

So its not accurate to say:

But going manual gives up a non-trivial amount of feature-set.

We can wrap e.g.:

      nexusConfig.typegenConfig = async (schema, outputPath) => {
        const configurator = await typegenAutoConfig(autoConfig)
        const config = await configurator(schema, outputPath)

It reduces some of my motivation for this issue. However I still think having an easy way to express multiple contexts with the autoConfig API could make sense.

tgriesser commented 4 years ago

Neat idea - I'm initially curious about how/where you're generating these programmatically, mostly for my own reference.

I've also wanted a way to declare context typings dynamically as something that a plugin can modify / add typings for if there's a place it needs to tack local state for the request, e.g. a DataLoader plugin which automatically creates DataLoaders for types/fields and builds them into an object hanging off the request context.

I agree that anything we do here should be part of #265 - I think that an object | array would be fine here, and multiple contexts would just be unioned automatically?

jasonkuhrt commented 4 years ago

Hey @tgriesser

I'm initially curious about how/where you're generating these programmatically, mostly for my own reference.

The use-case is that the prototype we're building allows something as follows:

app.use(plugin1).use(plugin2).use(plugin3)

Each plugin may optionally contribute toward the app's graphql resolver context.

Make sense?

I've also wanted a way to declare context typings dynamically as something that a plugin can modify / add typings for if there's a place it needs to tack local state for the request, e.g. a DataLoader plugin which automatically creates DataLoaders for types/fields and builds them into an object hanging off the request context.

👍

I wonder if this is related: Currently we have e.g. plugin1 write to disk something like:

      import { Photon } from '${GENERATED_PHOTON_OUTPUT_PATH}'

      export type Context = {
        photon: Photon
      }

      export const context: Context = {
        photon: new Photon(),
      }

Our prototype then has makeSchema reference that file. This works, but of cousre sub-optimal (end-user experiences perf loss b/c IO, plugin author works harder to implement correctly, etc.).

If Nexus plugins could provide context types as strings rather than (in addition to) file paths then we could stay completely in memory I think.

So does this fall under your point or on this second point are we talking about two different ideas?