Skip to content

Can't access context / injector inside schema directives #2514

Open
@fridaystreet

Description

@fridaystreet

Describe the bug

Seems to be related to this issue #2460

If I setup a module application running through yoga the context in the schema directives doesn't contain the injector. Various hacks to fix this then cause any additonal data added to the context from the directive to not be populated into all the providers. (See the issue linked above).

While the workaround outlined in the above issue used to work, it no longer appears to work with latest version of graphql-modules and in fact just breaks the providers that are trying to use the injector.

The workaround above found that the context was being pulled from cache, so in the event that the context was being updated after a module had it's context, it would not be available in that modules providers. The workaround attempted to just repopulate every modules context with updates.

The current workaround is to force the injector into the context using the context: () =>.. function of the createYoga function. And use the controlled lifecycle to destroy after request. (not entirely sure this is the right approach, but only way could get the injector into the directive context)

const getContext = async (context) => {
  const controller = App.createOperationController({
    context: {
      ...context,
    },
    autoDestroy: false,
  })

  return {
     injector: controller.injector,
     destroy: controller.destroy,
      ...context
   }
}

const YogaApp = createYoga({
  plugins: [
    useGraphQLModules(App),
    {
      async onResponse({ serverContext }: any) {
        try {
          serverContext.destroy()
        } catch(e) {
          console.log('cleanup App controller failed', e)
        }
      }
    }
  ],
  context: getContext
 })

The probalem here is that the same problem as mentioned in the other issue is then present. Not all the modules/providers context get the updated context (for some currently unknown reason).

Now in the context of the directive there is a Symbol(GRAPHQL_MODULES) key which does in deed contain an injector property. So I thought, maybe this is just a dependancy injection problem and I need to reference this in the directive function properties like so

const auth = async function (source, args, context: GraphQLModules.GlobalContext, info) {

But that doesn't do anything either, so I tried the follwoing, before realising that the 'injector' in this Symbol referenced part of the context is not an injector but a reference to the modules App. See screenshots of the context object passed to the directive.

Screenshot 2024-11-29 at 7 54 32 am
Screenshot 2024-11-29 at 9 32 24 am

Here is the directive, trying to pull the injector out of there. This works but feels very hacky.

const fieldMapper = (schema, name, mapperKind) => (fieldConfig, _fieldName, typeName) => {

  const IsAuthenticatedDirective =
    getDirective(schema, fieldConfig, name)?.[0] ??
    typeDirectiveArgumentMaps[typeName]

  if (IsAuthenticatedDirective) {
    const { resolve = defaultFieldResolver, subscribe = defaultFieldResolver} = fieldConfig

    const next = mapperKind === 'SUBSCRIPTION_ROOT_FIELD' ? subscribe : resolve
    const auth = async function (source, args, context, info) {

      const contextSymbol = Object.getOwnPropertySymbols(context).find(
        (s) => s.toString() === Symbol.for("GRAPHQL_MODULES").toString()
      )

      if (!contextSymbol) {
        throw new Error('Context Symbol is not available')
      }

      try {
        const result = await context[contextSymbol].injector.get(JWT).processAuthorizationHeader(context)
        context.user = result?.user
        context[contextSymbol].context.user = result?.user
      } catch(e) {
        throw e;
      }

      if (!context.user) {
        throw new AuthenticationError('You are not authenticated!')
      }

      return next(source, args, context, info)
    }

    fieldConfig.resolve = fieldConfig.resolve ? auth : fieldConfig.resolve;
    fieldConfig.subscribe = fieldConfig.subscribe ? auth : fieldConfig.subscribe;
    return fieldConfig
  }
}

Expected behavior
I would expect that the injector is available in the directive context, I would also expect that amnything I add to the context in the directive is avilable to all modules/providers for the rest of the request execution

If anyone could give some advice on what we're doing wrong here, more than happy to approach it a different way and would be very grateful

BTW the middlewar context of all the providers seems to contain the proper context. And I'm half expecting someone to say, use the middleware, but I don't think that's ana acceptable solution. Directives should work and I don't want to have to go and manually define somewhere outsiode the schema that a particular query/mutation is auth or not

Environment:

  • OS: Mac
    graphql": "^16.9.0",
    "graphql-modules": "^2.4.0",
    "graphql-yoga": "^5.10.1",
  • NodeJS: 20

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions