graphql-framework-experiment: `subscriptionType` is not supported yet

subscriptionType which was supported by nexus is not supported by nexus-future yet.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 20
  • Comments: 18 (7 by maintainers)

Commits related to this issue

Most upvoted comments

@Manborough By migrate you mean add subscriptionField to the nexus schema api? Pending some discussion with @Weakky it looks technically trivial so could be very soon.

No update yet sorry. I will nominate this issue for next sprint right now though.

@jasonkuhrt @Weakky Any update on this? Would love to use nexus@next in an upcoming project, but that wonโ€™t work without subscriptions ๐Ÿ˜•

FYI, I have this currently working via a simple plugin and fork I made. Definitely not ideal, but works until either there is a way to pass in a custom HTTP server or until subscriptions are supported natively. I needed to get something working immediately and was trying to avoid returning to the apollo-server. Creating a plugin seemed to be the only way to get access to the generated schema, and the fork simply allows to pass in a shared httpServer.

NOTE: The nexus fork is based on the latest master.

package.json dependencies

    "nexus": "git://github.com/danielmahon/nexus.git#support-custom-http-server",
    "nexus-plugin-prisma": "0.8.0",
    "nexus-plugin-shield": "0.1.2",
    "nexus-plugin-subscriptions": "0.0.7",

example app.ts

import cors from 'cors';
import { PubSub } from 'graphql-subscriptions';
import * as HTTP from 'http';
import onExit from 'signal-exit';
import app, { log } from 'nexus';
import { prisma } from 'nexus-plugin-prisma';
import { PrismaClient } from 'nexus-plugin-prisma/client';
import { subscriptions } from 'nexus-plugin-subscriptions';
import { formatErrors } from './errors';
import { permissions } from './permissions';
import { createContext } from './context';

const db = new PrismaClient();
const pubsub = new PubSub();
const httpServer = HTTP.createServer();

// Nexus settings
app.settings.change({
  server: { httpServer, playground: true },
  // schema: { generateGraphQLSDLFile: 'schema.graphql' },
});

// Express plugins
app.server.express.use(cors());
app.server.express.use(formatErrors());

// Nexus context [NexusContext]
app.schema.addToContext(async (req) => {
  return await createContext(req.headers['authorization'], {
    req,
    pubsub,
  });
});


// Nexus plugins
app.use(prisma({ client: { instance: db } }));
app.use(permissions());
app.use(
  subscriptions({
    ws: { server: httpServer, path: '/subscriptions' },
    keepAlive: 10 * 1000,
    onConnect: (payload: Record<string, any>) => {
      log.info('client connected');
      return createContext(payload['authorization'], { pubsub, db });
    },
    onDisconnect: () => {
      log.info('client disconnected');
    },
  }),
);

// Handle process exit
// Still not sure on the right way to handle disconnect...
process.on('beforeExit', async (code) => {
  // Release prisma client connections
  log.info(`๐Ÿ‘‹ [process] disconnecting prisma client...`);
  await db.disconnect();
});

onExit(async (code, signal) => {
  log.info(`๐Ÿ‘‹ [process] exited`);
});

subscriptions.ts (extend Subscription type with your subscription fields)

import { SubscribeFieldConfig } from '@nexus/schema/dist/definitions/subscriptionField';
import { withFilter } from 'graphql-subscriptions';
import { schema } from 'nexus';
import { Tool, ToolWhereUniqueInput } from 'nexus-plugin-prisma/client';
import { MyContext } from '../context';

schema.extendType({
  type: 'Subscription',
  definition: (t) => {
    t.field('onToolUpdate', {
      type: 'Tool',
      args: { id: schema.idArg(), simple: schema.booleanArg() },
      subscribe: withFilter(
        (_parent, _args, ctx: MyContext) => {
          if (!ctx.pubsub) throw 'PubSub client is required!';
          return ctx.pubsub.asyncIterator('TOOL_UPDATE');
        },
        (payload: Tool['id'], args: ToolWhereUniqueInput) => {
          return args.id != null ? payload === args.id : true;
        },
      ),
      resolve: (payload: Tool['id'], args, ctx) => {
        return ctx.db.tool.findOne({
          where: { id: payload },
          include: { state: true, usersWatching: true },
        });
      },
    } as SubscribeFieldConfig<'Subscription', 'onToolUpdate'>);
  },
});

I didnโ€™t test it recently and donโ€™t have the time currently but this should be it:

Subscription.ts

import { subscriptionField } from "nexus"

export const RoundsSubscription = subscriptionField('SubscribeRounds', {
    type: 'Round',
    subscribe(root, args, ctx) {
      return ctx.pubsub.asyncIterator("ROUNDS")
    },
    resolve(payload) {
      return payload
    },
  })

Mutation.ts (excerpt)

t.field('createOneRound', {
  type: Round,
  resolve: async (parent, { }, ctx: Context) => {
    const round = await ctx.prisma.round.create(...)
    ctx.pubsub.publish('ROUNDS', round)
    return round
  },
})

main.ts (excerpt)

import { ApolloServer, makeExecutableSchema } from 'apollo-server'
import schema from './schema'
import { verify } from 'jsonwebtoken'
import { Token, APP_SECRET } from './utils'
import WebSocket from 'ws'
import { ConnectionContext } from 'subscriptions-transport-ws'
import { PrismaClient } from '@prisma/client'
import { PubSub } from 'apollo-server'
import { Request, Response } from 'apollo-server-env'
import { ExecutionParams } from 'subscriptions-transport-ws'
import { Context } from './context'

const prisma = new PrismaClient()
const pubsub = new PubSub()

interface ExpressContext {
  req: Request
  res: Response
  connection?: ExecutionParams<Context>
}

export function createContext(expressContext: ExpressContext): Context {
	// ...
    return {
      request: expressContext.req,
      prisma,
      pubsub,
      userId,
    }
}

const server = new ApolloServer({
  schema,
  context: createContext,
  playground: process.env.NODE_ENV === 'development',
  debug: process.env.NODE_ENV === 'development',
  subscriptions: {
    onConnect: (
      connectionParams: Object,
      websocket: WebSocket,
      context: ConnectionContext,
    ): Context => {
        // ...
        return {
          request: context.request,
          prisma,
          pubsub,
          userId: verifiedToken && verifiedToken.userId,
       }
    },
  },
})

server
  .listen(...)
  .then(({ url, subscriptionsUrl }) => {
    console.log(`๐Ÿš€ Server ready at ${url}`)
    console.log(`๐Ÿš€ Subscriptions ready at ${subscriptionsUrl}`)
  })