Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Derived GraphQL Contexts #159

Open
captbaritone opened this issue Dec 13, 2024 · 2 comments
Open

[RFC] Derived GraphQL Contexts #159

captbaritone opened this issue Dec 13, 2024 · 2 comments

Comments

@captbaritone
Copy link
Owner

TL;DR: I’m proposing a scheme for defining scoped context values to allow large/modular client-side executors to only pay for bundle size/setup cost of the context values they actually use.

Problem Statement

When executing GraphQL on the client JavaScript bundle size must be managed. This means that in a given JavaScript bundle you only want to include code that you actually use. “Pay for what you use”.

In the case or GraphQL resolvers this is possible (though not yet widely adopted). Relay Resolvers achieve this by inlining resolvers into it’s query/fragment generated artifacts, Grats has a path toward achieving this via deriving per-query sub-schemas.

GraphQL context is harder. Context, as used in graphql-js is a global object which is the same for all consumers. This is doubly problematic because the things you consume off of context are generally quite large/expensive: Data stores, databases, etc.

Proposed Solution

Implementation-first GraphQL tools, like Grats, could introduce the notion of a “derived context” which is defined as a function of the global context return an explicitly typed derived context value. For any resolvers which specify an argument of this type, Grats would then be able to import in this function and invoke it, passing the result to the resolver.

Let’s look at an example:

First we have some global code

// - GlobalCode.ts

// We still define a global context object, which 
// the executor must be configured to provide

/** @gqlContext */
export type GlobalContext = {};

/** @gqlType */
expprt type Query = unknown;

Then we have a module which defines a derived context

// - SqliteDb.ts
import {DB, connect} from "some-sqlite-library";
import {GlobalContext} from "./GlobalCode.ts"

// By defining this annotated function we are telling
// Grats that `DB` type is a context value, and that this is
// the function to call if a component wants it.

/** @gqlContext */
export function getSqlite(_ctx: GlobalContext): DB {
  return connect({source: "in-memory"});
}

Finally we have some normal looking Grats resolver code

// - models.ts

/** @gqlType */
type User = {
  /** @gqlField */
  name: string;
}

// Here we have a function which consumes the derived context value `DB`.
// Grats can tell that this is a reference to the derived context type and
// pull in, and execute, the code to get that value.

/** @gqlField */
export function me(_: Query, db: DB): User {
  const row = DB("SELECT * FROM users WHERE id = '4';").get();
  return {name: row.name};
}

Output

Now if we imagine a query like this: query { me { name } }, Grats could produce a sub-schema resolver map like this:

import {getSqlite} from "./SqliteDb";
import {me} from "./models";

export function getResolverMap() {
  return {
    query: {
      me(source, _args, context) {
        // Grats pull in this context creation function only
        // because it's used by `Query.me`.
        return me(source, getSqlite(context));
      }
    }
    // User.name is omitted because it can use the default resolver
  }
} 

Memoization

Generally you don’t want to produce a new context value for each resolver. Instead you want a shared DB connection/logger/whatever. I’m not sure if Grats should apply per-context WeakMap memoization to ensure only one derived context is created per context, or if that should be left up to the implementor of the derived context function. Perhaps there are use cases for non-memoized derived contexts?

Other tools

This approach could work for Grats, but also eventually for Relay which is also exploring and implementation-first Grats-style syntax for Relay Resolvers.

@captbaritone
Copy link
Owner Author

I suppose you could also just implement this as a pattern rather than as a Grats feature.

If you define your own weak-map-memorized derived context function, you could’ve just import it into your resolver’s module and call it yourself. This have the advantage of being less magics (you can see the function that will get invoked) but is slightly more tedious, especially if these context values are consumed in many many resolvers.

Making this a feature rather than a pattern also helps codify which functions are expected to be used in this way.

captbaritone added a commit that referenced this issue Dec 13, 2024
Summary: A sketch of derived contexts as described in #159

Not sure this is how the implemenetaiton should work. Was just focusing on getting things working end to end.

If we go this way, I'd want to focus a bit more on internal architecture as well as error handling.

Test Plan:

[ghstack-poisoned]
captbaritone added a commit that referenced this issue Dec 13, 2024
Summary: A sketch of derived contexts as described in #159

Not sure this is how the implemenetaiton should work. Was just focusing on getting things working end to end.

If we go this way, I'd want to focus a bit more on internal architecture as well as error handling.

Test Plan:

ghstack-source-id: 602bcb6bde2a9f1044c46b94c1c38890ee50983e
Pull Request resolved: #161
captbaritone added a commit that referenced this issue Dec 13, 2024
Summary: A sketch of derived contexts as described in #159

Not sure this is how the implemenetaiton should work. Was just focusing on getting things working end to end.

If we go this way, I'd want to focus a bit more on internal architecture as well as error handling.

Test Plan:

[ghstack-poisoned]
captbaritone added a commit that referenced this issue Dec 13, 2024
Summary: A sketch of derived contexts as described in #159

Not sure this is how the implemenetaiton should work. Was just focusing on getting things working end to end.

If we go this way, I'd want to focus a bit more on internal architecture as well as error handling.

Test Plan:

ghstack-source-id: fee290ab069aae504627486ca59254f165e78620
Pull Request resolved: #161
@captbaritone
Copy link
Owner Author

I've done a sketch of how this might work here: #161

captbaritone added a commit that referenced this issue Dec 13, 2024
Summary: A sketch of derived contexts as described in #159

Not sure this is how the implemenetaiton should work. Was just focusing on getting things working end to end.

If we go this way, I'd want to focus a bit more on internal architecture as well as error handling.

Test Plan:

[ghstack-poisoned]
captbaritone added a commit that referenced this issue Dec 13, 2024
Summary: A sketch of derived contexts as described in #159

Not sure this is how the implemenetaiton should work. Was just focusing on getting things working end to end.

If we go this way, I'd want to focus a bit more on internal architecture as well as error handling.

Test Plan:

ghstack-source-id: 3ebc208dcf7538ab0e48347beffc67189e557a5d
Pull Request resolved: #161
captbaritone added a commit that referenced this issue Dec 14, 2024
Summary: A sketch of derived contexts as described in #159

Not sure this is how the implemenetaiton should work. Was just focusing on getting things working end to end.

If we go this way, I'd want to focus a bit more on internal architecture as well as error handling.

Test Plan:

[ghstack-poisoned]
captbaritone added a commit that referenced this issue Dec 14, 2024
Summary: A sketch of derived contexts as described in #159

Not sure this is how the implemenetaiton should work. Was just focusing on getting things working end to end.

If we go this way, I'd want to focus a bit more on internal architecture as well as error handling.

Test Plan:

ghstack-source-id: 0513dda4e4937a4b1ca9b09e1dd17e68557cd2e1
Pull Request resolved: #161
captbaritone added a commit that referenced this issue Dec 16, 2024
Summary: A sketch of derived contexts as described in #159

Not sure this is how the implemenetaiton should work. Was just focusing on getting things working end to end.

If we go this way, I'd want to focus a bit more on internal architecture as well as error handling.

Test Plan:

ghstack-source-id: 0513dda4e4937a4b1ca9b09e1dd17e68557cd2e1
Pull Request resolved: #161
captbaritone added a commit that referenced this issue Jan 15, 2025
* [WIP] Sketch of derived context

Summary: A sketch of derived contexts as described in #159

Not sure this is how the implemenetaiton should work. Was just focusing on getting things working end to end.

If we go this way, I'd want to focus a bit more on internal architecture as well as error handling.

Test Plan:

ghstack-source-id: 0513dda4e4937a4b1ca9b09e1dd17e68557cd2e1
Pull Request resolved: #161

* Document derived context

* Cleanup
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant