aws-lambda-graphql icon indicating copy to clipboard operation
aws-lambda-graphql copied to clipboard

Subscriptions based on the userId from authorizer

Open tkohout opened this issue 5 years ago • 9 comments

Hello, thanks for this library! I've been trying to setup simple subscriptions based on your example server app.

I managed to setup the authorizer on websocket, and retrieve user in the context function. With something like this:

context: async (context) => {
   // When called from DynamoDB event stream
    if (context.event?.requestContext == undefined) {
      return {
        pubSub
      }
    }

    let principalId = context.event.requestContext.authorizer!.principalId
    
    const user = await User.findByPk(principalId)

    if (!user) {
      throw new AuthenticationError('User not authorized');
      return
    }

    return {
      me: user,
      pubSub
    };
  }

And the subscription would be something like this:

hasUnseenViewNotifications: {
            resolve: (rootValue: ViewNotificationSeenChanged) => {
              return rootValue.unseen;
            },
            subscribe: (rootValue, args, context, info) => {
                const result = (context.pubSub as PubSub).subscribe(Subscriptions.viewNotificationSeenChanged.toString())
                
                return withFilter(result, (rootValue: ViewNotificationSeenChanged, args) => {
                    return rootValue.userId == context.me.id
                })(rootValue, args, context, info)
            }
        },

I assume the subscribe function is called from the dynamodb event stream so I am not getting requestContext and the userId with it. How to get it inside of the subscribe function?

I came upon this issue: https://github.com/michalkvasnicak/aws-lambda-graphql/pull/70 which might be what I am looking for, but I am confused how this would go all together.

Could you point me in the right direction?

Thanks Tomas

tkohout avatar Feb 25 '20 10:02 tkohout

@tkohout Sorry for late response. Basically if you want to store something in context, you should use onConnect event, that returns an object that can be JSON serialised. Then this data are stored on your connection.data so they should be available in the GraphQL context as ctx.foo.

See:

https://github.com/michalkvasnicak/aws-lambda-graphql/blob/master/packages/aws-lambda-graphql/src/tests/Server.test.ts#L396

https://github.com/michalkvasnicak/aws-lambda-graphql/blob/master/packages/aws-lambda-graphql/src/tests/Server.test.ts#L443

michalkvasnicak avatar Feb 26 '20 11:02 michalkvasnicak

Please, let me know if my advice helped you.

michalkvasnicak avatar Feb 26 '20 11:02 michalkvasnicak

Thanks @michalkvasnicak, yes, that set me in the right direction. I got my usecase working now. For anybody who would find this useful, I did:

subscriptions: {
    onConnect: async (messagePayload, connection, event, context) => {

      let principalId: string
      //serverless-offline does not pass around authorizer info for websockets
      if (process.env.IS_OFFLINE) {
        principalId = "default-user-id"
      } else {
        principalId = event.requestContext?.authorizer?.principalId
      }

      return {
        userId: principalId
      }
    }

and then access it from resolver directly on context:

unseenViewNotificationsCount: {
            resolve: (rootValue: ViewNotificationSeenChanged) => {
              return rootValue.count;
            },
            subscribe: (rootValue, args, context, info) => {
                const result = (context.pubSub as PubSub).subscribe(Subscription.viewNotificationSeenChanged.toString())
                
                return withFilter(result, (rootValue: ViewNotificationSeenChanged, args) => {
                    return rootValue.userId == context.userId
                })(rootValue, args, context, info)
            }
        }

One gotcha would be that if you want to pass around pubSub using context (as per docs) you have to return it for the dynamodb stream as well:

context: async (context) => {
    //This is how I check it's a dynamodb stream, maybe there's a better way?
    if (context.event?.requestContext == undefined) {
      return {
        pubSub
      }
    }

    //If you don't return early this will fail and subscriptions won't work 
    const user = await userForRequest(context.event.requestContext.authorizer!.principalId)

    return {
      me: user,
      pubSub
    };
}

Perhaps authorization could be part of the example? It's a bit hard to setup and I think most of the time the websocket will need to be protected.

tkohout avatar Feb 26 '20 17:02 tkohout

@tkohout thank you very much for this valuable info. An example is really good idea, would you have some free time to prepare some really simple solution in a PR?

Also about

One gotcha would be that if you want to pass around pubSub using context (as per docs) you have to return it for the dynamodb stream as well:

Yeah this one maybe should be a part of example too.

michalkvasnicak avatar Mar 08 '20 10:03 michalkvasnicak

@michalkvasnicak @tkohout thanks for this info it's really helpful and would be nice to add this to the readme or the code example

IslamWahid avatar Apr 06 '20 18:04 IslamWahid

@michalkvasnicak @tkohout can you guys please explain the benefits of passing the pubSub through the context instead of just creating a new object and use it when you need it?

IslamWahid avatar Jul 02 '20 09:07 IslamWahid

@IslamWahid for me GraphQL context can serve as a Dependency injection container that contains all the services, etc. So instead of importing the pubSub or other services that you need, you can expose them in a context and access them anywhere.

michalkvasnicak avatar Jul 02 '20 09:07 michalkvasnicak

@michalkvasnicak I'm thinking about it performance-wise as it gives me more flexibility to just import it whenever and wherever I need to publish an event instead of injecting it, but just wanted to ask if you're doing this for performance reasons.

IslamWahid avatar Jul 02 '20 10:07 IslamWahid

@IslamWahid for me it's just about convenience. It's easier to change type declaration for context and add/remove few things than add/remove multiple import statements from N files.

michalkvasnicak avatar Jul 02 '20 10:07 michalkvasnicak