n1ru4l/graphql-live-query

example needed for adding GraphQLLiveDirective to schema using graphql-tools and using subscription in @n1ru4l/socket-io-graphql-server

goodnewso opened this issue · 11 comments

Screenshot 2021-03-29 125337

am using graphql-tools to build my schema with makeExcutableSchema but when GraphQLLiveDirective is added to the schema using the schema directives I get this error.

GraphQLLiveDirective Popup

this is a portion of my code

schema.ts

import { GraphQLSchema, specifiedDirectives } from 'graphql';
import { makeExecutableSchema, IResolvers } from 'apollo-server-express';
import { GraphQLLiveDirective } from '@n1ru4l/graphql-live-query';
import { typeDefs as scalarTypeDefs, resolvers as scalarResolvers } from 'graphql-scalars';
import { logger, SchemaData } from 'utils';
import * as data from './build';

const {
  typeDefs, Mutation, Query, Subscription, Directives, Resolvers,
  // @ts-ignore
} = Object.values(data as SchemaData)
  .reduce<SchemaData>((result: SchemaData, x: SchemaData): SchemaData => ({
    Mutation: { ...result.Mutation, ...x.Mutation },
    Query: { ...result.Query, ...x.Query },
    Subscription: { ...result.Subscription, ...x.Subscription },
    // @ts-ignore
    typeDefs: [...result.typeDefs, x.typeDefs],
    Directives: { ...result.Directives, ...x.Directives },
    Resolvers: { ...result.Resolvers, ...x.Resolvers },
  }),
{
  typeDefs: [...scalarTypeDefs],
  Mutation: {},
  Query: {},
  Subscription: {},
  Directives: {},
  Resolvers: { ...scalarResolvers },
} as SchemaData);

const schemaDirectives = {
  ...Directives,
};

const resolvers: IResolvers = {
  Mutation, Query, Subscription, ...Resolvers,
};

const schema: GraphQLSchema = makeExecutableSchema({
  logger: {
    log: e => {
      if (typeof e === 'string') {
        logger.info(e);
      }
      logger.error(e);
    },
  },
  allowUndefinedInResolve: false,
  resolverValidationOptions: {
    requireResolversForNonScalar: true,
    requireResolversForArgs: true,
    requireResolversForResolveType: true,
    // requireResolversForAllFields: false,
  },
  inheritResolversFromInterfaces: true,
  resolvers,
  typeDefs,
  schemaDirectives: [GraphQLLiveDirective]
});

export { schema };

and index.ts

import 'module-alias/register';
import { registerSocketIOGraphQLServer,  } from '@n1ru4l/socket-io-graphql-server';
import { Server as IOServer, Socket } from 'socket.io';
import express from 'express';
import http from 'http';
import { schema } from 'schema';
import { ApolloServer } from 'apollo-server-express';
import { checkJwt, pubClient, subClient, logger, mongoose } from 'utils';
import { InMemoryLiveQueryStore } from '@n1ru4l/in-memory-live-query-store';
import { createAdapter } from 'socket.io-redis';

const app = express();
const httpServer = http.createServer(app);

const socketServer = new IOServer(httpServer, {
  cors: {
    origin: ['http://localhost:3000'],
    methods: ['GET', 'POST'],
    allowedHeaders: ['client-name', 'Authorization'],
    credentials: true,
  },
  // path: '/socketio-graphql'
});

socketServer.adapter(createAdapter({ pubClient, subClient }));

const liveQueryStore = new InMemoryLiveQueryStore();

registerSocketIOGraphQLServer({
  socketServer,
  /* getParameter is invoked for each incoming operation and provides all values required for execution. */
  getParameter: async ({
    /* Socket.io instance of the client that executes the operation */
    socket,
  }) => ({
    execute: liveQueryStore.execute,

    /* The parameters used for the operation execution. */
    graphQLExecutionParameter: {
      /* GraphQL schema used for exection (required) */
      schema,
      /* root value for execution (optional) */
      rootValue: {},
      /* context value for execution (optional) */
      contextValue: await (async () => {
        const user = await checkJwt(socket.handshake?.auth?.token);
        return {
          user,
          socket,
          liveQueryStore,
          mongoose,
          logger
        }
      })(),
    },
  }),
});

socketServer.on('connect', (socket: Socket) => console.log({ id: socket.id, token: socket.handshake.auth}));

const apolloServer = new ApolloServer({
  schema,
  context: async ({ req }) => ({
    user: req?.user,
    mongoose,
    logger,
    req,
  }),
  introspection: true,
});

apolloServer.applyMiddleware({ app })

httpServer.listen(4000, () => console.log('listening'));

secondly @n1ru4l/socket-io-graphql-server does not export pubsub. so is there an example guy cause the todo app does not have an example of subscription. but in the readme it says
A layer for serving a GraphQL schema via a socket.io server. Supports Queries, Mutations, Subscriptions and Live Queries.
so how is the subscription suppose to work without pubsub over socket.io.

am I missing something. please help

Regarding the makeExecutableSchema API:

The schemaDirectives option is dedicated to the legacy schema directives. https://www.graphql-tools.com/docs/legacy-schema-directives/

What you will have to do is to declare the directive in SDL:

import { makeExecutableSchema } from "graphql-tools";

const schema = makeExecutableSchema({
  typeDefs: /* GraphQL */ `
    type Query {
      hi: String!
    }

    # the directive must be declared manually via SDL
    # as there is no way to provide the GraphQLLIveDirective exported
    # from @n1ru4l/graphql-live-query with makeExecutableSchema
    directive @live(if: Boolean) on QUERY
  `,
  resolvers: {
    Query: {
      hi: () => "hello"
    }
  }
});

You don't need to provide any directive handler as the InMemoryLiveQueryStore is already capable of handling the directive.

This should probably be better documented as graphql-tools is a popular library. I will try to do that soon. 😅

Regarding the statement: "@n1ru4l/socket-io-graphql-server does not export pubsub"

PubSub is not something the GraphQL network layer should provide. This might be confusing at first for people coming from apollo which is trying to address most use-cases with its bloated apollo-server.

Subscriptions can but must not be implemented with PubSub. Subscriptions need a source which could be anything that can be wrapped in an AsyncIterable. The graphql subscription engine then subscribes to the AsyncIterable.

The most popular implementation is probably https://github.com/apollographql/graphql-subscriptions which is basically a PubSub wrapped in an AsyncIterable.

If you only have one GraphQL server instance, you might not even need it: apollographql/graphql-subscriptions#240 (comment)

https://github.com/n1ru4l/graphql-bleeding-edge-playground has an example of setting up a PubSub based on Node.js EventEmitter.


Please let me know whether this answered your questions and feel free to ask any further questions or point out stuff that should be better documented.

Hey @goodnewso, could you provide some feedback on whether the information above could solve your issues?

Hey @goodnewso, could you provide some feedback on whether the information above could solve your issues?

I will give an update on that soon. please

The help was ok

I will try my best to highlight the possible issues am having. but first I will want to say thank you for the assistance. I had some serious issues with my system just got it fixed.

  1. am using multiple instances of Graphql server because I could not find any documentation or example on how to enable introspection or playground or graphiql in the graphql socketio server.
  2. Comming from apollo-server. apollo documentation states that default pubsub which uses event emitter should not be used in production. so my question is the pubsub you created in dungeon reveavler. is it production-ready, can it be scale using Redis or anything?
  3. in your example Lazy Socket Authentication You only give an example if the client would send token or authentication through socket but not through graphql mutation. I think an example for both client and server is needed. for clarity

I think some simple examples and documentation for both client and servers will ease the ground query, mutation, subscription and, live query. but thanks for the help so far.

am using multiple instances of Graphql server because I could not find any documentation or example on how to enable introspection or playground or graphiql in the graphql socketio server.

Introspection is not necessarily tied to the GraphQL server, but rather to the execution engine the server is using. "Introspection" is just another GraphQL query that is executed against the GraphQL schema. See https://github.com/graphql/graphql-js/blob/7f40198a080ab3ea5ff3e1ecc9bf7ade130ec1bf/src/utilities/getIntrospectionQuery.js#L25

So GraphiQL or GraphQL Playground are simply executing an introspection query against the configured endpoint schema.

In GraphiQL you can pass a fetcher function into the GraphiQL component. You can find an example fetcher function for the socket-io transport over here: https://github.com/n1ru4l/graphql-bleeding-edge-playground/blob/90c1d1c089b95578fd5d45a1564ff32747fcb977/src/dev/GraphiQL.tsx#L22-L26

You can simply pass it to your GraphiQL instance https://github.com/n1ru4l/graphql-bleeding-edge-playground/blob/90c1d1c089b95578fd5d45a1564ff32747fcb977/src/dev/GraphiQL.tsx#L210-L212

In general, https://github.com/n1ru4l/graphql-bleeding-edge-playground shows how you can set up GraphiQL as a development-only route within create-react-app (and it should be applicable to any other setup).

Comming from apollo-server. apollo documentation states that default pubsub which uses event emitter should not be used in production. so my question is the pubsub you created in dungeon reveavler. is it production-ready, can it be scale using Redis or anything?

It depends on your use case. If you don't need to scale horizontally and only have one instance of your GraphQL server running you don't need something like Redis. dungeon-revealer for example is an isolated service that uses SQLite and does not need any external services/databases/scaling with minimum traffic. So in that case having an in-memory PubSub is perfectly fine. The highest load I ever had on an instance at the same time is 10 users. You might re-evaluate based on your concurrent user count/load.

However, nobody is stopping you from using https://github.com/apollographql/graphql-subscriptions or https://github.com/davidyaha/graphql-redis-subscriptions TBH I think their naming is very misleading as the packages CAN be used with any other library that can benefit from pub subs wrapped in AsyncIterables.

in your example Lazy Socket Authentication You only give an example if the client would send token or authentication through socket but not through graphql mutation. I think an example for both client and server is needed. for clarity

It is possible to do authentication via a mutation. For that, you would however have to not use the lazy socket authentication at all.

The implementation could look similar to this:

// we use a weak map to avoid memory leaks
// so once a socket disconnects the value is purged from the map here
const authenticatedSockets = new WeakMap();

registerSocketIOGraphQLServer({
  socketServer,
  getParameter: ({ socket }) => ({
    graphQLExecutionParameter: {
      schema,
      rootValue,
      contextValue: {
        // this is the socket that executes the operation
        socket,
        // a global lookup map of the authenticated sockets
        authenticatedSockets,
        // some auth service that is used for getting the user data for given credentials
        authService
      }
    }
  })
});

And the given resolvers (I picked a resolver map graphql-tools style), you can use whatever approach you prefer.

const Mutation = {
  authenticate: (_, args, context) => {
    const userInfo = context.authService(args.credentials);
    if (userInfo) {
      authenticatedSockets.set(context.socket, userInfo);
    }
  },
  doSomething: (_, args, context) => {
    const userInfo = context.authenticatedSockets.get(context.socket);
    if (!userInfo) {
      throw new Error("Not authenticated :(");
    }
    if (!userInfo.isAdmin) {
      throw new Error("Insufficient permissions :(");
    }
  }
};

Wonderful

If I may suggest, one more tool should also be documented which is Graphql Code Generator.

Why should it be documented? GraphQL code generator is a tool unrelated to graphql-live-query.

@n1ru4l

Why should it be documented? GraphQL code generator is a tool unrelated to graphql-live-query.

sorry, i was referring to @n1ru4l/socket-io-graphql-server not graphql-live-query.

@n1ru4l/socket-io-graphql-server does not use graphql-codegen. I think all open questions here are answered, thus I will close this issue. If you have additional questions please open a new discussion.