prisma-labs / graphql-framework-experiment

Code-First Type-Safe GraphQL Framework
https://nexusjs.org
MIT License
672 stars 66 forks source link

Plugin System #514

Open jasonkuhrt opened 4 years ago

jasonkuhrt commented 4 years ago

This issue is a WIP. The content here will be refined over time.

We've had feedback in the Nexus transition issue (1, 2), and committed to, keeping schema component usable on its own, and schema component plugins usable on their own, too.

This issue is a place to at least start design/discussion of the system we'll need to realize the commitment. Centralizing all the considerations into one place will help us design the system. It is very complex and tradeoffs are hard to analyze. It is easy to over-optimize a side at the expense of others, without noticing until it is too late, maybe a lot of work done that could have been avoided.

Capabilities

WIP - incomplete, sometimes ambiguous

Questions

Social

  1. How will plugins be discovered by users?
  2. How will plugins be evaluated by users?
  3. How will users get a uniform read on all the different things different plugins do? Some augment servers, some replace servers, some attach request data, some attach response headers, ...
  4. How will users get a uniform read on all the options a plugin has (aka. config)?
  5. How will plugin quality be measured so users can make informed choices?
  6. How will we make as much as possible cosuming 10 plugins from 10 different developers feel like it could be from the same developer.

Versioning

  1. How will plugin compatibility be tracked so users can see which plugins support the version of the framework they are using, or would like to use (e.g. they are evaluating doing upgrading their framework to a major version)
  2. How will a user find out that the version of the plugin they are using is incompatible with the framework version?
    Worst: runtime crash and burn only under a set of production circumstances; Best: clear up front feedback while in dev mode?
  3. Will plugins need to depend on one another? Should they? If so, how will a plugin express that it depends on another? Peer dependency? Will the package manager feedback for that be a good enough dx? Probably not. What if the pin range leads to something not compatible with the user's framework version? Feels like entering a kind of dependency hell is pretty easy here.

Schema Component vs Framework

  1. How will framework plugins be documented on the website?
  2. How will schema plugins be documented on the website?
  3. How will documentation for authoring framework plugins be on the website?
  4. How will documentation for authoring schema plugins be on the website?
  5. When authoring a framework plugin, how will the schema subset be easily made easily consumable by nexus schema component users?
  6. How will schema plugin npm packages be differentiated from framework plugin npm packages?
  7. Will a plugin that wants to offer schema and framework level support be done so in a single npm package and github repo or multiple npm packages and multiple github repos?
  8. If the answer is one package, what happens if the dependencies of the framework plugin are much greater than the schema one? This is the case for the Prisma plugin. Do schema component users accept the bloat in their workflow?
  9. If the answer is one package, what happens if the dependencies of the framework plugin include deps that are, at the schema level, intended to be peer? This is the case for the Prisma plugin.
  10. If the answer is multiple packages, how are they differentiated on npm? github repos?

Integration

  1. It should be practically impossible to have silently incompatible plugins. This means a case where framework user installs two plugins and their app stops working because of an integration problem caused by the combination of the two plugins. How?

  2. We want the api to be built on top of the same internals that plugins are. This means anything the api let's users do is something that plugins can do, automate. That means a strong separation needs to exist in the API between escape hatches and official API surface that plugins rely on. This means for example that if a plugin A does (effectively, psudeo code):

    schema.addToContext(req => {
      req.foobar()
    })

    and plugin B does (effectively, psudeo code):

    server.custom(createSomeCustomServer)

    And req.foobar() is broken by createSomeCustomServer, then, bad.

    The features in the non-escape-hatch API need to be guaranteed compatible. Any features in schema.addToContext that rely on server need to be abstracted from the particulars of the actual server, and createSomeCustomServer must be required to fulfill all the framework features that are touching the server.

    This is one example but the principal is far reaching, general.

  3. Any plugin can augment the CLI. This means any CLI Invocation begins by loading the plugins that tap into it. How will we do that?

    1. Plugins will be configurable by users. This means:

      1. Read user config of plugins
      2. Read plugins, configure them, apply them
      3. Run cli
    2. User could configure cli plugins in dedicate config file

      1. some plugins, like prisma, augment runtime and worktime
      2. this means user would configure plugin in two places, some config file, and inside their app
      3. Furthermore, if not a single plugin, still for their app, some plugins would be configued one place, while others another
      4. This is a bad experience, so why invest in this direction?
    3. We could have the singular system, configuration in the app

      1. This would mean running app modules shallowly to acquire settings
      2. Shallowly means we don't need to run through everything, like building the schema or starting the server
      3. Settings is an API that can live anywhere in the app
      4. This means running all app modules
      5. This means requiring the app to not run substantial side-effects in modules
      6. This is ALREADY THE CASE with typegen
      7. So embrace it
      8. But what about added latency CLI invocation?
        1. Answer depends on what % of total time it accounts for
        2. 500ms added to 10s ~does not matter
        3. 500ms added to 1s ~does
    4. Should users have to enable the plugins explicitly after installing them?

      1. Why?
      2. What % of cases are there where a plugin will be installed but not used?
      3. Why not auto-use and allow opting-out? Doesn't that optimize for what most people are mostly doing?
      4. If plugins are not auto-used then that means, for plugins with cli augmentation, possibly only cli augmentation, user needs to add entry into their code in order for the cli to use it?
    5. Should plugins be imported or appear configurable via typegen?

      • if import-based, we more or less force ourselves down the must-be-explicitly-enabled-after-install design path
      • if typegen-based, we can go either way
      // user has to import some new concept
      import { use } from 'nexus-future'
      
      // user has to think, however little, about all the variances
      import * as Foo from 'foo'
      import foo from 'foo'
      import { createFoo } from 'foo'
      
      // user has to use it
      use(foo({ /* ... */ }))
      import { settings } from 'nexus-future'
      
      settings.change({
      plugins: {
        foo: {
          enabled: true | false // we can play with the defualt
          /* ... */
        }
      }
      })
    6. If we go the auto-use way, how does that square with schema level plugins?

      1. Do we enforce an npm package name for schema plugins so that they can be auto-used too?
        1. pattern of nexus-schema-plugin-*?
      2. Do we ignore them and say "publish a framework variant that wraps it"
        1. This seems to go against the first paragraph of this issue?
      3. Do we add a new API like:

        import { schema } from 'nexus-future'
        import someSchemaPlugin from 'some-schema-plugin'
        
        schema.use(someSchemaPlugin({ /* ... */ }))
      4. If we had such an API, would one not expect too then (this is compatible with the above framework use above, for framework):

        import { schema, server, logger } from 'nexus-future'
        import someSchemaPlugin from 'some-schema-plugin'
        import someServerPlugin from 'some-server-plugin'
        import someLoggerPlugin from 'some-logger-plugin'
        
        schema.use(someSchemaPlugin)
        server.use(someServerPlugin)
        logger.use(someLoggerPlugin)
jasonkuhrt commented 4 years ago

About the relationship between @nexus/schema and plugins.

The past/status-quo

With the framework, the game changes

What can we do about it?

Here are some options

  1. Generally, simply: Make Nexus framework plugins supply all the peer dependencies that their deps expect.

    Ramifications:

    • Multiple instances of some deps in the dep tree can lead to issues? For example graphql ... ?
  2. Give schema plugins what they will need via args, so that schema plugins do not have to import their deps.

    Ramifications:

    • Plugin code arguably gets more complicated, because all code needs to live within closures, no module-level logic.
    • Plugin needs to rely on type imports to get autocomplete etc. (@nexus/schema becomes a devDep)

      import { Plugin } from '@nexus/schema'
      
      const plugin: Plugin = () => {}
      
      export default plugin
jasonkuhrt commented 4 years ago

@tgriesser Any thoughts you have about the above comment are welcome!

jasonkuhrt commented 4 years ago

Took a deep dive with @Weakky about https://github.com/graphql-nexus/nexus-future/issues/514#issuecomment-604668904, and got aligned;

Solution 1 - Dynamically pass core components to plugins

This is what we think we should do.

Major Wins

Schema

App

{
  "dependencies": {
    "@nexus/schema": "...",
    "graphql": "...",
    "nexus-schema-plugin-foo": "..."
  }
}
import { makeSchema } from '@nexus/schema'
import nexusSchemaPluginFoo from 'nexus-schema-plugin-foo'

const schema = makeSchema({
  plugins: [
    nexusSchemaPluginFoo()
  ],
  // ...
})

// ...

Plugin

{
  "name": "nexus-schema-plugin-foo",
  "devDependencies": {
    "@nexus/schema": "...",
  }
}
import type { Plugin } from '@nexus/schema'

interface Options {
  // ...
}

const plugin: Plugin = (options: Options) => ({ lens, graphql, nexusSchema }) => {
  // ...
};

export { Options }
export default plugin

Framework

App

{
  "dependencies": {
    "nexus": "...",
    "nexus-plugin-foo": "..."
  }
}

Plugin

Notes
Minor Problems
{
  "name": "nexus-plugin-foo",
  "dependencies": {
    "nexus-schema-plugin-foo": "...",
  },
  "devDependencies": {
    "nexus": "...",
  }
}
import type { Plugin } from 'nexus/plugins'

interface Options {
  // ...
}

const plugin: Plugin = (options: Options) => ({ lens, graphql, nexusSchema }) => {
  // ...
};

export { Options }
export default plugin

Solution 2 - Apps depend upon graphql & @nexus/schema

Schema

...

Framework

Major Problems
  1. User is burdened with doing compat upkeep between framework and its components. Evaluating upgrade of a component or framework requires thinking about compat. This sucks.
  2. Plugins relying on hoisting, which will probably work now, but still a danerous hack
  3. In an SDK scenario, this gets even uglier, these deps always need to stay top level, never transient, so its not really possible to abstract/embed nexus cleanly, user's app always gets polluted with these deps

App

{
  "dependencies": {
    "nexus": "...",
    "@nexus/schema": "...",
    "graphql": "...",
    "nexus-plugin-foo": "..."
  }
}

Plugin

{
  "name": "nexus-plugin-foo",
  "dependencies": {
    "nexus-schema-plugin-foo": "...",
  },
  "devDependencies": {
    "nexus": "...",
  }
}
import { createPlugin } from 'nexus/plugins'
import graphql from 'graphql'
import nexusSchema from '@nexus/schema'

export interface Options {
  // ...
}

export default createPlugin((options: Options) => ({ lens }) => {
  // ...
})

Solution 3 - Framework plugins depend upon graphql & @nexus/schema

Major Problems
jasonkuhrt commented 4 years ago

Separate topic from previous comment but in that same deep dive with @Weakky.

Design around how the framework plugins can express their dep on the framework.

The following is not exact API but the gist of what we want to do.

Framework plugins will use their devDep pin on the framework to supply at runtime their dep. fwiw this is akin to how the Fastify plugin system works. This is needed in lieu of not pinning in the dep graph https://github.com/graphql-nexus/nexus-future/issues/514#issuecomment-605105455 but also because it permits to provide a lot better feedback anyways for the user about the incompatibility––compared to if we relied solely on the package manager feedback (e.g. peer dep warnings are largely ignored by users etc.).

import package from "../package.json";

// ...
// devDependencies: {
//   "nexus": "^1.2.3",
//   ...
// }
// ...

const plugin = () => {
  // ...
  return {
    supports: package.devDependencies["@nexus/schema"]
  };
};

If this system works out well, we'll go further and automate this so plugin authors 1) cannot screw it up 2) don't have to do it, think about it.

jasonkuhrt commented 4 years ago

Notes from call today with @Weakky

See Raw Live Share Session Sketches ``` // // plugin configuration // // Centralized Settings // (Package.json enabled plugins) const packageJson = { dependencies: { 'nexus-plugin-something': '', 'nexus-schema-plugin-something': '', }, } /* Foo plugin settings a: boolean b: string c: { c1: 'c1' | 'c2' } */ import { foo } from '@bloomberg/foo' import { plugin } from '@nexus/schema' import { settings, use } from 'nexus' /* # typegen based, but how does it really work? src/ settings.ts // export type Settings = { } runtime.ts testtime.ts worktime.ts interface RuntimePlugin (options) => (project) => interface RuntimePlugin (options) => (project) => interface RuntimePlugin interface Options {...} // not this import {Settings} from './settings' // probably this? (options: Settings) => (project) => */ // nexus-typegen-plugin-settings/index.d.ts /** * import { Settings as SomethingSettings } from 'nexus-plugin-something/dist/settings' * * declare global { * export interface PluginSettings { * something: SomethingSettings * } * } */ // Programatic API /** use(plugin, {}) use(plugin()) */ /** * package.json * { * main: 'dist/runtime.js' * } */ import { prisma } from 'nexus-plugin-prisma' // runtime settings.change({ plugins: { something: { // configuration goes here }, }, }) // opt-in auto-load // settings.ts // hand-made centralized configuration interface PluginSettings { prisma: PrismaSettings otherPlugin: OtherSettings } export const pluginSettings: PluginSettings = { prisma: {}, otherPlugin: {}, } use(prisma(pluginSettings.prisma)) use(other(pluginSettings.other)) use(foo({ a: process.env.NEXUS_DEV === 'true' })) use(foo, { a: process.env.NEXUS_DEV === 'true' }) use(bar, { a: process.env.NEXUS_DEV === 'true' }) use(janine, { a: process.env.NEXUS_DEV === 'true' }) /* // package.json "nexus": { // false "auto-import": true // short hand "auto-import": { "prefix": "nexus-plugin.*" }, "auto-import": { "prefix": [ "nexus-plugin.*", '@bloomberf/nexus-.* ] } } */ /* NEXUS_PLUGIN_FOO_A=false node node_modules/.build NEXUS_CONFIG='{ plugins: { foo: { a: true } } }' node node_modules/.build ... */ interface Plugin { runtime?: string testtime?: string worktime?: string } export const plugin: Plugin = { runtime: require.resolve('./runtime'), worktime: require.resolve('./worktime'), testime: require.resolve('./testime'), } interface RuntimePlugin { plugin: Plugin /** * Optional path to your worktime entrypoint in case you want it to leave * elsewhere than next to your runtime */ worktime?: string // require.resolve('./worktime') testtime?: string // require.resolve('./testtime') } use.inline({ type: 'inline', runtime() {}, testtime() {}, worktime() {}, // worktime: string | () => {}, // testtime: string | () => {} }) // app.ts // object OR runtime use( plugin({ runtime() {}, testtime() {}, worktime() {}, }) ) // runtime shorthand? use(plugin(project => {})) // dimension shorthands? use(plugin.runtime(project => {})) use(plugin.testime(project => {})) use(plugin.worktime(project => {})) use( plugin({ runtime: require.resolve('./runtime'), worktime: require.resolve('./worktime'), }) ) // ------------------- use(plugin.treeShakableWorktime(require.resolve('../plugin/worktime'))) use(plugin.worktime(() => {})) // I care about tree shaking // I want inline // I do not want a package // plugin/worktime.ts // plugin/testtime.ts // api? // will not work b/c cannot import dev deps into runtime plugin.runtime(() => {}) plugin.worktime(() => {}) plugin.testtime(() => {}) // folder based? /* plugin/ runtime testtime worktime */ // plugin // plugin // // Questions/Tradeoffs to think about // /** 1. Centralized settings vs distributed settings */ // // Features We Want To Have? // /** 1. Configuration Cascade - code - environment variables - cli flags 2. Guaranteed standard plugin interface 3. Drop down to logcal plugins "on the fly" 4. Different plugin patterns/prefixes */ // // Future Ideas // /** 1. Standard systesm for users togglging *time of plugins */ ```


Building Packaged Plugins

The heart of the plugin interface could be a manifest that provides some basic metadata and location information about where modules can be found for each respective dimension.

The reason for the module paths is that the runtime dimension of a plugin must be kept in separate from the others to facilitate tree shaking production builds.

type Plugin = <Settings>(settings: Settings) => {
  name: string
  frameworkVersion: string,   // valid npm version expression
  settings: Settings
  settingsType?: {
    module: string            // a path to TS module
    export: string            // a name of an export in the TS module for the Settings type
  },
  runtime?:  {
    module: string            // a path to a compiled JS module
    export: string            // a name of an export in the JS module for the plugin dimension
  },
  worktime?: {
    module: string            // ^
    export: string            // ^
  },
  testtime?: {
    module: string            // ^
    export: string            // ^

  },
}

The signatures of the various dimensions are thus:

ThisPluginSettings => RuntimeLens => RuntimeContributions
ThisPluginSettings => WorktimeLens => WorktimeContributions
ThisPluginSettings => TesttimeLens => TesttimeContributions
About Settings and Settings Module

Note that the dimensions share a common settings data. A plugin feature might require collaboration between runtime and worktime, or testtime and worktime, etc. From a plugin consumer perspective, its one feature, they don't want to manage plugin settings across different dimensions. We think this is a hard usability requirement. The consequence of this is that the type of ThisPluginSettings is not owned by any one dimension. Consequently it needs its own peer module. A plugin author will import the settings type into their respective dimension modules for type safety. The framework will look at this module to provide typesafety/autocomplete for users of the plugin when they configure it.

About Settings Being Returned

The settings must be returned to the framework. This may seem odd. The reason is that the framework takes responsibility for feeding settings to plugins. The reason for that is settings, better known as "configuration management" can get complex in real-world deployment scenarios; different stages of deployment will require different settings, and getting those settings actually set will have different ideal mechanisms, such as config files or environment variables. We want the framework to support a configuration cascade where users can count on e.g. being able to set via an env var any plugin option in a consistent and safely parsed way. There should be zero effort to "rig" this up by users and zero effort for plugin authors too. By everyone just following a few minimal conventions, things will Just Work (tm).

Another reason we need settings returned is that at worktime and testtime we need to read the settings in from the user's source code. Having plugins return the settings is our only way to do that, assuming we want settings to be entered directly into plugin functions (e.g. we reject this: use(prisma, {...})).

About Tooling

There is very little value for most plugin authors to manage the above manifest manually. Instead the Nexus CLI should have a build step that provides the following benefits:

  1. entrypoint is generated
    • name is extracted from the package name
    • version range is extracted from the package dev dep on nexus
    • settings passthrough is generated
    • settingsType / runtime / worktime / testtime config is inferred by looking for conventional module names
  2. package metadata is generated and stored in package.json
    • property: { nexusPlugin: { hasRuntime: boolean, hasTesttime: boolean, hasWorktime: boolean, ... } }
    • tags too maybe, to support npm search
    • this package metadata will be scrapped by the Nexus website and displayed graphically in the plugin list
    • we could go even further and use static analysis to track which plugin features are being used. This would then allow the user to scan the plugin list and see at a glance which plugins use which hooks etc.
    • this static analysis would also be the basis for e.g. giving great deprecation warnings to authors, and allowing users to hide from view in the plugin list those plugins that are using outdated APIs etc.
  3. automtic ES module support for app-level tree shaking (tsdx does something similar to this for reference/example)
  4. linting for idiomatic plugin style
  5. automated smoke test against the stated framework version being supported (e.g. the plugin can be loaded, doesn't break dev mode, build, etc.)

The conventional layout could be:

  plugin/
    settings.ts
    runtime.ts
    worktime.ts
    testtime.ts

The conventional export name would be plugin and for settings type Settings

If the plugin author insists on using another layout for some reason, we could support that via config in the package json, e.g.:

nexusPlugin: {
  layout: {
    runtimeModule: {
      module: "..."
    }
  }
}

Note that this starts to look like the API all over again but the important difference is that it is for generating API boilerplate. And the important thing about having an API is that we don't lock out other ideas in the future or other tooling ideas from other users, etc. API has benefits of being an... API!

Consuming Packaged Plugins

  1. Using and configuring a plugin programatically at its base could look like this:

    import { use } from 'nexus'
    import { prisma } from 'nexus-plugin-prisma'
    
    use(prisma({
      // ...
    }))
  2. The framework could support auto-discovery of plugins that follow conventions. It would allow the above to be refactored to this:

    import { use } from 'nexus'
    
    use.prisma({
      // ...
    })

    The cool thing about this is that a user can type use.<tab> to get autocomplete for all the plugins they have installed.

    Furthermore, we could enhance the autocomplete to have a list of all published plugins. Upon selecting one it would be auto-installed into their project. Codelens could sit above this lines of code to offer install/uninstall option too. This would bring plugin discoverability, consumption, and usage to an absurdly convenient level.

    use.<tab>  -- ✔ prisma
                  ⬇ script-hooks
                  ⬇ react-admin
                  ⬇ query-complexity
                  ⬇ knex
                  ⬇ now
                  ⬇ netlify
  3. If centralized configuration is important for a user, they could enable centralize mode, allowing the above to be refactored like so:

    import { settings } from 'nexus'
    
    settings.change({
      framework: {
        centralizedPluginSettings: true
      },
      plugins: {
        prisma: {
          enabled: true
          // ...
        }
      }
    })
  4. If the user is bothered by having to manually use every plugin they install, they could enable auto-use mode, which would make plugins opt-out instead of opt-in. In a proper dev mode ui (terminal, web, desktop, whatever) they would get all the feedback they need about what is going on, and more.

  5. Users will be able to use a configuration cascade to flexible control their app's settings, including all the plugins it uses. Example (rough, sketch):

    NEXUS_CONFIG='{ "plugins": { "prisma": { "foo": true }}}' node node_modules/.build

Consuming Inline Plugins

Being able to write a plugin inline inside an app is important. The constraints are different than with packages plugins:

  1. We don't have to care as much about tree shaking because the producer/consumer are the same person here. We don't need to think about how to create a scalable guaranteed social contract etc.
  2. we don't have to care about framework version compat
  3. We don't have to care about name collisions. User can
  4. We don't have to care about settings

First of all, a user can of course just do this:

import { use } from 'nexus'

use({
  name: "...",
  frameworkVersion: "..."
  // ...
})

But this is pretty verbose and the paths part quickly turns this into a nightmare that is too taxing for the quick "I'm curious" moments.

A simpler API should be made availble:

import { inlinePlugin } from 'nexus/plugins'

use(inlinePlugin({
  runtime(lens) { /*...*/ },
  worktimetime(lens) { /*...*/ },
  testtime(lens) { /*...*/ },
}))

This pattern must never ever be used by packaged plugins. The framework stance should be that measures, at any time, will be taken to actively block packaged plugins from using this pattern. There are various static and runtime tricks we could deploy to enforce it. Ideally though the benefits of following the conventions and general ecosystem cohesion will be good enough to make this a non-issue. None the less it should be clear that any packaged plugin trying to export itself using inline can expect to be broken by the framework at some point.

Sometimes a user might want to go a bit futher with their plugin, building what is effectively a packaged plugin yet also still local to their app.

The farmework can support this via a special convention:

plugins/
  <name>/
    settings.ts
    runtime.ts
    testtime.ts
    worktime.ts

With this convention then, imagine this setup and example:

api/
  app.ts
  plugins/
    foo/
      settings.ts
      runtime.ts
//app.ts
import { use } from 'nexus'
import { localPlugin } from 'nexus/plugins'

use(localPlugin('foo', {
  // ..
}))

Maybe we could use proxies instead:

//app.ts
import { use } from 'nexus'
import { localPlugin } from 'nexus/plugins'

use(localPlugin.foo({
  // ..
}))

But actually, it seems the auto-discovery feature would be almost the same then, and already exists. Name collisions are possible (between their local and packaged plugins) but the user can always change the name of their local easily if needed.

//app.ts
import { use } from 'nexus'

use.foo({
  // ..
})

Consuming Component Plugins

components that are pluggable could accept plugins over a use API

import { schema, log, server } from 'nexus'
import { superCoolScalar } from 'qux'
import { nyanCatTheme } from 'foo'
import { cors } from 'bar'

schema.use(superCoolScalar({
  // ...
}))

log.use(nyanCatTheme({
  // ...
}))

server.use(cors({
  // ...
}))

Component plugins lose the following benefits over framework plugins:

jasonkuhrt commented 4 years ago

Tree Shaking Plugins

Because the plugin entrypoint is a set of paths using a custom/dynamic loader system, it means tree-shaking will not work by default. Specifically, it will think that the only thing a used by the app is the plugin manifest.

// user's app.ts
import { prisma } from 'nexus-plugin-prisma'
import { use } from 'nexus'

use(prisma())

What tree shaking would think it needs to bring in (roughly):

// node_modules/nexus-plugin-prisma/dist/index.js
export const prisma = {
  name: '...',
  // ...
}

To solve this the nexus start module could do this:

import 'nexus-plugin-prisma/dist/runtime'

This information will come from plugin.runtime.module

jasonkuhrt commented 4 years ago

https://github.com/graphql-nexus/nexus-plugin-prisma/runs/571935272

This error occurred b/c of mismatch of nexus versions between plugin and framework.

I still don't understand the actual issue (config protected class blah blah) I think it has to do with the mix of nominally different classes, "same class" but different packages.

We're lucky we can fix this as we own the framework and plugin, but this is a high bar to climb from a plugin ecosystem perspective.

jasonkuhrt commented 4 years ago

https://github.com/graphql-nexus/nexus/issues/603#issuecomment-611609336

jasonkuhrt commented 4 years ago

Noticed in #633 that another Nexus Schema plugin api part aside from plugin wrapper is NexusSchema.plugin.completeValue. As stated in https://github.com/graphql-nexus/nexus/issues/514#issuecomment-605105455 we need to get away from static Nexus Schema term level access in Nexus Schema plugins.

jasonkuhrt commented 4 years ago

Udpate