apollographql / graphql-subscriptions

:newspaper: A small module that implements GraphQL subscriptions for Node.js
MIT License
1.58k stars 133 forks source link

Using directives with subscriptions #258

Closed moritz157 closed 2 years ago

moritz157 commented 2 years ago

I want to use a directive to authorize subscription requests since I already use it to authorize my regular queries and mutations. The directive is apparently getting called (before the subscription) but when I add session information to the context, I cannot access it using the context provided when using withFilter. It appears that the context accessible in the directive transformer and the context in withFilter are different from one another.

Why is that or am I doing something wrong? Is there an easy way around this? Is it even possible to properly use directives with subscriptions?

My code is basically:

// directive transformer
return mapSchema(schema, {
[MapperKind.TYPE]: type => {
  const authDirective = getDirective(schema, type, 'auth')?.[0]
  if (authDirective) {
    typeDirectiveArgumentMaps[type.name] = authDirective
  }
  return undefined
},
[MapperKind.OBJECT_FIELD]: (fieldConfig, _fieldName, typeName) => {
  const authDirective: Record<string, string> = getDirective(schema, fieldConfig, 'auth')?.[0] ?? typeDirectiveArgumentMaps[typeName]
  if (authDirective) {
    const { requires, needsCompany } = authDirective
    if (requires) {
      const { resolve = defaultFieldResolver } = fieldConfig
      fieldConfig.resolve = async function (source: unknown, args: { [argName: string]: unknown }, context: { connectionParams?: { token: string }, myInfo?: any }, info: GraphQLResolveInfo): Promise<unknown> {
        // ... do things
       context.myInfo = 'some interesting information';

        return resolve(source , args, context, info);
      }
      return fieldConfig
      }
    }
  }
 }
)

// Subscription
//...
subscribe: withFilter(
  () => pubsub.asyncIterator('MY_SUBSCRIPTION_EVENT'),
  (payload: { eventData: any }, variables: { someEventFilterVariable: string }, context: { myInfo?: any }, info: any) => {

      // context does not have myInfo here

      return context.myInfo && context.myInfo === 'some interesting information';
  }
)
//...
moritz157 commented 2 years ago

It seems I finally solved it (some testing still needed though)🚀

The main problem were that I needed to create different mappings for subscriptions when mapping the schema and that the way you have to map subscriptions is different to mapping for example queries or mutations.

Your directive transformer has to look something like this:

function myDirectiveTransformer(schema: GraphQLSchema): GraphQLSchema {
    const typeDirectiveArgumentMaps: Record<string, Record<string, string>> = {};

    return mapSchema(schema, {
        [MapperKind.SUBSCRIPTION]: type => {
          const myDirective = getDirective(schema, type, 'myDirective')?.[0]
          if (myDirective) {
            typeDirectiveArgumentMaps[type.name] = myDirective
          }
          return undefined
        },
        [MapperKind.SUBSCRIPTION_ROOT_FIELD]: (fieldConfig, _fieldName, typeName) => {
          const myDirective: Record<string, string> = getDirective(schema, fieldConfig, 'myDirective')?.[0] ?? typeDirectiveArgumentMaps[typeName]
          if (myDirective) {
            const { myDirectiveParameter } = myDirective

              const { resolve = defaultFieldResolver } = fieldConfig;
              const defaultFieldSubscription = fieldConfig.subscribe;

              fieldConfig.subscribe = async (rootValue: any, args: { [argName: string]: any }, context: { [argName: string]: unknown }, info: GraphQLResolveInfo): Promise<any> => {
                try {
                  // Manipulate context here
                  context.someContextVariable = 'hello there';

                  // If you want to throw an error (for example because authentication failed), just throw it here (or in any functions called here)
                } catch(error) {
                  context.error = error;

                  const emptyIterator = {
                      done: false,
                      next: (): Promise<IteratorResult<any>> => {
                        if(emptyIterator.done) return Promise.resolve({ value: undefined, done: true }) ;
                        else {
                          emptyIterator.done = true;
                          return Promise.resolve({ value: undefined, done: false });
                        }},
                      return: () => Promise.resolve({ value: undefined, done: true }),
                      throw: (err) => Promise.reject(err),
                      [$$asyncIterator] (): any { return emptyIterator; } 
                  };

                  return emptyIterator;
                }

                return defaultFieldSubscription(rootValue, args, context, info);
              }

              fieldConfig.resolve = async (source, args: { [argsName: string]: any }, context: { error?: unknown }, info: GraphQLResolveInfo): Promise<unknown> => {
                if(context.error) throw context.error;

                return resolve(source, args, context, info);
              }
              return fieldConfig

          }
        },
    })
}

Perhaps we should add documentation for it somewhere because I couldn't find any about this, which was extremely frustating. I am not really sure where though, especially since this repo seems to be more or less abandonded.