GithubHelp home page GithubHelp logo

apollographql / graphql-subscriptions Goto Github PK

View Code? Open in Web Editor NEW
1.6K 59.0 133.0 191 KB

:newspaper: A small module that implements GraphQL subscriptions for Node.js

License: MIT License

TypeScript 100.00%
graphql-subscriptions graphql real-time

graphql-subscriptions's Introduction

npm version GitHub license

graphql-subscriptions

GraphQL subscriptions is a simple npm package that lets you wire up GraphQL with a pubsub system (like Redis) to implement subscriptions in GraphQL.

You can use it with any GraphQL client and server (not only Apollo).

Installation

npm install graphql-subscriptions graphql or yarn add graphql-subscriptions graphql

This package should be used with a network transport, for example subscriptions-transport-ws.

TypeScript

If you are developing a project that uses this module with TypeScript:

  • ensure that your tsconfig.json lib definition includes "esnext.asynciterable"
  • npm install @types/graphql or yarn add @types/graphql

Getting started with your first subscription

To begin with GraphQL subscriptions, start by defining a GraphQL Subscription type in your schema:

type Subscription {
    somethingChanged: Result
}

type Result {
    id: String
}

Next, add the Subscription type to your schema definition:

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

Now, let's create a simple PubSub instance - it is a simple pubsub implementation, based on EventEmitter. Alternative EventEmitter implementations can be passed by an options object to the PubSub constructor.

import { PubSub } from 'graphql-subscriptions';

export const pubsub = new PubSub();

Now, implement your Subscriptions type resolver, using the pubsub.asyncIterator to map the event you need:

const SOMETHING_CHANGED_TOPIC = 'something_changed';

export const resolvers = {
  Subscription: {
    somethingChanged: {
      subscribe: () => pubsub.asyncIterator(SOMETHING_CHANGED_TOPIC),
    },
  },
}

Subscriptions resolvers are not a function, but an object with subscribe method, that returns AsyncIterable.

Now, the GraphQL engine knows that somethingChanged is a subscription, and every time we use pubsub.publish over this topic - it will publish it using the transport we use:

pubsub.publish(SOMETHING_CHANGED_TOPIC, { somethingChanged: { id: "123" }});

Note that the default PubSub implementation is intended for demo purposes. It only works if you have a single instance of your server and doesn't scale beyond a couple of connections. For production usage you'll want to use one of the PubSub implementations backed by an external store. (e.g. Redis)

Filters

When publishing data to subscribers, we need to make sure that each subscriber gets only the data it needs.

To do so, we can use withFilter helper from this package, which wraps AsyncIterator with a filter function, and lets you control each publication for each user.

withFilter API:

  • asyncIteratorFn: (rootValue, args, context, info) => AsyncIterator<any> : A function that returns AsyncIterator you got from your pubsub.asyncIterator.
  • filterFn: (payload, variables, context, info) => boolean | Promise<boolean> - A filter function, executed with the payload (the published value), variables, context and operation info, must return boolean or Promise<boolean> indicating if the payload should pass to the subscriber.

For example, if somethingChanged would also accept a variable with the ID that is relevant, we can use the following code to filter according to it:

import { withFilter } from 'graphql-subscriptions';

const SOMETHING_CHANGED_TOPIC = 'something_changed';

export const resolvers = {
  Subscription: {
    somethingChanged: {
      subscribe: withFilter(() => pubsub.asyncIterator(SOMETHING_CHANGED_TOPIC), (payload, variables) => {
        return payload.somethingChanged.id === variables.relevantId;
      }),
    },
  },
}

Note that when using withFilter, you don't need to wrap your return value with a function.

Channels Mapping

You can map multiple channels into the same subscription, for example when there are multiple events that trigger the same subscription in the GraphQL engine.

const SOMETHING_UPDATED = 'something_updated';
const SOMETHING_CREATED = 'something_created';
const SOMETHING_REMOVED = 'something_removed';

export const resolvers = {
  Subscription: {
    somethingChanged: {
      subscribe: () => pubsub.asyncIterator([ SOMETHING_UPDATED, SOMETHING_CREATED, SOMETHING_REMOVED ]),
    },
  },
}

Payload Manipulation

You can also manipulate the published payload, by adding resolve methods to your subscription:

const SOMETHING_UPDATED = 'something_updated';

export const resolvers = {
  Subscription: {
    somethingChanged: {
      resolve: (payload, args, context, info) => {
        // Manipulate and return the new value
        return payload.somethingChanged;
      },
      subscribe: () => pubsub.asyncIterator(SOMETHING_UPDATED),
    },
  },
}

Note that resolve methods execute after subscribe, so if the code in subscribe depends on a manipulated payload field, you will need to factor out the manipulation and call it from both subscribe and resolve.

Usage with callback listeners

Your database might have callback-based listeners for changes, for example something like this:

const listenToNewMessages = (callback) => {
  return db.table('messages').listen(newMessage => callback(newMessage));
}

// Kick off the listener
listenToNewMessages(message => {
  console.log(message);
})

The callback function would be called every time a new message is saved in the database. Unfortunately, that doesn't play very well with async iterators out of the box because callbacks are push-based, where async iterators are pull-based.

We recommend using the callback-to-async-iterator module to convert your callback-based listener into an async iterator:

import asyncify from 'callback-to-async-iterator';

export const resolvers = {
  Subscription: {
    somethingChanged: {
      subscribe: () => asyncify(listenToNewMessages),
    },
  },
}

Custom AsyncIterator Wrappers

The value you should return from your subscribe resolver must be an AsyncIterator.

You can use this value and wrap it with another AsyncIterator to implement custom logic over your subscriptions.

For example, the following implementation manipulates the payload by adding some static fields:

import { $$asyncIterator } from 'iterall';

export const withStaticFields = (asyncIterator: AsyncIterator<any>, staticFields: Object): Function => {
  return (rootValue: any, args: any, context: any, info: any): AsyncIterator<any> => {

    return {
      next() {
        return asyncIterator.next().then(({ value, done }) => {
          return {
            value: {
              ...value,
              ...staticFields,
            },
            done,
          };
        });
      },
      return() {
        return Promise.resolve({ value: undefined, done: true });
      },
      throw(error) {
        return Promise.reject(error);
      },
      [$$asyncIterator]() {
        return this;
      },
    };
  };
};

You can also take a look at withFilter for inspiration.

For more information about AsyncIterator:

PubSub Implementations

It can be easily replaced with some other implementations of PubSubEngine abstract class. Here are a few of them:

You can also implement a PubSub of your own, by using the exported abstract class PubSubEngine from this package. By using extends PubSubEngine you use the default asyncIterator method implementation; by using implements PubSubEngine you must implement your own AsyncIterator.

SubscriptionManager @deprecated

SubscriptionManager is the previous alternative for using graphql-js subscriptions directly, and it's now deprecated.

If you are looking for its API docs, refer to a previous commit of the repository

graphql-subscriptions's People

Contributors

ancashoria avatar davidyaha avatar dnalborczyk avatar dotansimha avatar freiksenet avatar glasser avatar gluck avatar grantwwu avatar greenkeeper[bot] avatar helfer avatar hofmannz avatar hwillson avatar jedwards1211 avatar kouak avatar maktouch avatar mxstbr avatar neophi avatar ozyman42 avatar phryneas avatar quintstoffers avatar scf4 avatar siyfion avatar svc-secops avatar tladd avatar tomasalabes avatar urigo avatar urossmolnik avatar valdestron avatar wtgtybhertgeghgtwtg avatar wtrocki avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

graphql-subscriptions's Issues

Add 3rd argument to PubSub subscribe - options

This little change will enable PubSub to access query itself.
Seems to be the necessary step if we want to implement things like subscribing to RethinkDB changefeeds which itself can trigger publish events.
Without this information we can only hardcode some changefeeds without arguments.

Just little change in SubscriptionManager, I am TS disabled, so no PR :-(

// 3. subscribe and keep the subscription id
            const subsPromise = this.pubsub.subscribe(triggerName, handler, options);
            subsPromise.then(id => this.subscriptions[externalSubscriptionId].push(id));

Publish events

Hi guys,

In file README, currently we have the demo code to publish events:

pubsub.publish(SOMETHING_CHANGED_TOPIC, { somethingChanged: { id: "123" }});

I tried to use the template above but in clients, I've got nothing. All fields are set to null. But if I modify it a little bit like this:

pubsub.publish(SOMETHING_CHANGED_TOPIC, {  id: "123" });

It works as expected. So is there any thing wrong with the documentation?

Warning: Resolve function missing

In Readme - Payload Manipulation writing You can also manipulate the published payload.

But if I not set resolve method. There is a warning show me Resolve function missing for "Subscription.commentAdded". To disable this warning check https://github.com/apollostack/graphql-tools/issues/131

To resolve this I must set a passthrough function.

    commentAdded: {
      resolve: root => root,
      subscribe: () => pubsub.asyncIterator('commentAdded')
    }

Is this a bug or change readme to You must manipulate the published payload, by adding resovle methods to your subscription

withFilter not working

Im using withFilter in my code

watchDevice: {
      resolve: (payload) => payload,
      subscribe: withFilter(() => pubsub.asyncIterator('updateDeviceLocation'), (payload, args) => {
        console.log('---------P', payload)
        console.log('---------A', args)
        return payload.id === args.id;
      }),
    },

but the filter function is not being called, any idea?

How to update all subscribed clients except the one making the changes

I tried to integrate graphql-subscriptions and it worked out pretty good, But it broke the app (which i can fix easily.. but i don't think that's the right decision)

Subscriptions sends the updates for everything i am subscribed to, But when i create a new item, The store is already updated from the mutation i just ran, but Subscription kicks in and runs the logic which updates the store again.. And i get the duplicate key warning.

I can and have fixed this by checking if a record already exists and then handling the update., But this means that the updates are still sent to all the subscribed clients who want to be notified of the changes.

Is there any way to send updates to all the subscribed clients, Except the one making the changes/operations (CRUD), without writing lot of codes?

Ofcourse i can use setupFunctions filter but if the same user has opened the app in different tabs, this will not work out.

What do you recommend, How should i move forward with this.

Memory leak: event handlers are not removed when a subscription ends

Versions used :

  • graphql-subscriptions 0.4.2
  • subscription-transport-ws 0.7.1

When a subscription is created, event handlers are created and registered through the underlying EventEmitter class.

However, these event handlers are never removed leading to a memory leak.

Now, I don't know whether the bug lies in this repo or in subscriptions-transport-ws as I'm not familiar enough with this codebase yet.

However, I managed to reproduce the issue in the Githunt-API example implementation.

Steps to reproduce

My only change is to console.log registered event handlers in the underlying EventEmitter object every 5 seconds.

  • Setup the project as described in the README
  • Start the server
  • Start a subscription in GraphiQL
  • Watch the event handler registered in console output
  • Close GraphiQL tab, reopen, and register a new subscription
  • Watch both event handlers in console output
  • Rince & repeat until OOM :)

Have to publish with subscription name in the object

I was following the tutorial in the README file. I noticed if I use

// publish to the channel
pubsub.publish('newCommentsChannel', {
  id: 123,
  content: 'Test',
  repoFullName: 'apollostack/GitHunt-API',
  posted_by: 'helfer',
});

I will get

{
  data: {
     commentAdded: null
   }
}

But if I publish with the subscription name in the object

// publish to the channel
pubsub.publish('newCommentsChannel', {
  commentAdded: {
    id: 123,
    content: 'Test',
    repoFullName: 'apollostack/GitHunt-API',
    posted_by: 'helfer',
  }
});

Then I will get the result as in the README.
Am I missing something from the document?

Cannot find name 'AsyncIterator' error in Typescript compilation process.

I have found a difficulty compiling my Typescript code and it turns out that inside many files in graphql-subscriptions and subscriptions-transport-ws, the AsyncIterator iterator type interface is treated as defined, but the Typescript compiler doesn't agree with that. here is the list of errors:

node_modules/graphql-subscriptions/dist/pubsub-engine.d.ts(5,52): error TS2304: Cannot find name 'AsyncIterator'.
node_modules/graphql-subscriptions/dist/pubsub.d.ts(12,52): error TS2304: Cannot find name 'AsyncIterator'.
node_modules/graphql-subscriptions/dist/with-filter.d.ts(2,94): error TS2304: Cannot find name 'AsyncIterator'.
node_modules/graphql-subscriptions/dist/with-filter.d.ts(3,58): error TS2304: Cannot find name 'AsyncIterator'.
node_modules/subscriptions-transport-ws/dist/server.d.ts(5,41): error TS2304: Cannot find name 'AsyncIterator'.
node_modules/subscriptions-transport-ws/dist/server.d.ts(29,58): error TS2304: Cannot find name 'AsyncIterator'.
node_modules/subscriptions-transport-ws/dist/server.d.ts(32,31): error TS2304: Cannot find name 'AsyncIterator'.

screenshot 22

I had found a temporary solution to bypass the errors by defining AsyncIterator type interface in every file involved in node_modules, here is the definition:

interface AsyncIterator<T> {
  next(value?: any): Promise<IteratorResult<T>>;
  return?(value?: any): Promise<IteratorResult<T>>;
  throw?(e?: any): Promise<IteratorResult<T>>;
}

screenshot 23

discussion: livequery integration

Let's discuss how a possible Meteor livequery integration could look like.

Advantages
Livequery integration would allow the full expressiveness of the mongo query syntax (limited by the minimongo limitations). Especially the ability to sort & limit queries seems interesting to me. This makes subscriptions possible like: give me updates on the top 3 voted posts. This would be hard to implement in a traditional pubsub system.

Disadvantages
Hard to extend to other DB's beyond mongo (but maybe postgres and rethink).
Only as scalable as livequery is (although no merge box required?)

Possible approach with the current system
The current system doesn't seem particularly well fit, but one could imagine something like:

const subscriptionManager = new SubscriptionManager({
  schema,
  pubsub,
  setupFunctions: {
    topComments: (options, args) => ({
      someName: {
        channelOptions: {
          cursor: Comments.find({reponame: args.repoFullName},{sort:{votes:-1},limit:3}),
          added: true,
          changed: true,
          removed: true
        }
      },
    }),
  },
});

The pubsub system could then add an observer on the cursor and trigger the changes the user indicated it wants to listen to (added, changed and/or `removed).

Problems with this approach
It's quite obvious that it's quite hacky, the current system doesn't seem well fit. E.g. the channel name (someName) has no meaning.

It's also not really a pubsub system. We're only subscribing, but can't publish.

[object Object] Result when doing pubsub.publish

The problem is when publishing a value it will return null on the subscribe method

export const subscriptions = model => ({
    [model]: {
        resolve(payload) {
            console.log({...payload[model]})
            return {...payload[model]}  
        },
        subscribe: withFilter(() => pubsub.asyncIterator(
            ['CREATED','UPDATED', 'DELETED']
            .map(mutation => `${model.toUpperCase()}_${mutation}`)),
            (payload,variables) => {
                return payload.id === variables.id
            })
    }
})

And publishing it like this

    [`update${model}`]: async(_, { id, ...args }, { models }, info) => {
        let result = await models[model].findById(id)
        await result.update(args)
        pubsub.publish(`${model.toUpperCase()}_UPDATED`, {
            [model]: args
        })
        
        return result
    },

this is what i get received from the Subscription resolve

[nodemon] starting `babel-node src`
Server started at 0.0.0.0:8080
express deprecated req.host: Use req.hostname instead src/server.js:97:88
{ name: 'testerssas',
  description: 'test',
  slug: 'testerssas',
  fee: 0 }
POST /graphql 200 59.101 ms - -

and getting this error

image

Noticed that [object Object] is an error

Merge all queries on each specific subscription

Hi,

Actually, if I'm not mistaken, for each subscription, when there is new data available the schema of each listener query is executed against the GraphQL Schema. Am I right ?

This seems rather inefficient.
I think it would be doable to merge all listeners query into one, execute it against the schema and then dispatch only the queried data for each listener.

What do you think ?

Need to see schema for example

Maybe I missed it, but it would be nice to see all the relevant pieces of the example, namely what the schema looks like.

Subscription must return Async Iterable when using withFilter

I was trying to use filtering explained in the official documentation but it fails with the "Subscription must return Async Iterable" error.

Here is my example for my Subscription resolver:

This doesn't work:

Subscription: {
    errorChanged: {
        subscribe: () => withFilter(
            () => pubsub.asyncIterator("errorChanged"),
            (payload, variables) => {
                return true;
            },
        ),
    },
}

This works:

Subscription: {
    errorChanged: {
       subscribe: () => pubsub.asyncIterator("errorChanged")
    },
}

Am I doing something wrong?

Using version: 0.4.4

Can not resolve field that is not part of the published object

What I am using:

react-native: 0.42.3
react-apollo: 1.0.0-rc.3
apollo-client: 1.0.0-rc.6
subscriptions-transport-ws: 0.5.5-alpha.0
graphql-subscriptions: ^0.3.1

I have the following schema:

type Episode {
  id: Int!
  number: String!
  title: String!
  cover: String!
}

type CastQueueItem {
  itemId: Int!
  episode: Episode!
}

type CastStatus {
  queueItems: [CastQueueItem]!
}

type Subscription {
  castStatus: CastStatus!
}

schema {
  query: Query # not included
  mutation: Mutation # not included
  subscription: Subscription
}

This is my Subscription to which I subscribe in my react-native application:

subscription castStatus {
  castStatus { 
    queueItems {
      itemId
      episode {
        id
        title
        cover
      }
    }
  }
}

Somewhere in my javascript I publish the CastStatus:

const castStatus = {
  queueItems: [
    { itemId: 1, episode: { id: 1, title: '1', },
    { itemId: 2, episode: { id: 2, title: '2', },
  ],
}
pubsub.publish(`castStatus`,  { castStatus })

The cover field has a resolver that exists on the schema I passed to the SubscriptionManager constructor.

I am expecting the cover field to be resolved trough my schema - but it seems not to work. I am getting no updates from the server as long as I the cover field is listed in my subscription. Also, I am not getting any error message :(

I wanted to ask if this behavior is on purpose and if so why SubscriptionManager does not resolve field via the schema.

Subscription using Typescript - Property 'Subscription' is incompatible with index signature.

Hello,

I got this error when I tried to execute this code. Do you have any idea?

src/index.ts(40,49): error TS2345: Argument of type '{ typeDefs: string; resolvers: { Query: { addCell: (: any, { type }: { type: any; }) => Prom...' is not assignable to parameter of type 'IExecutableSchemaDefinition'.
Types of property 'resolvers' are incompatible.
Type '{ Query: { addCell: (
: any, { type }: { type: any; }) => Promise; }; Subscription: ...' is not assignable to type 'IResolvers'.
Property 'Subscription' is incompatible with index signature.
Type '{ newCell: { subscribe: (: any, { type }: { type: any; }) => AsyncIterator<{}>; }; }' is not assignable to type 'GraphQLScalarType | IResolverObject | (() => any)'.
Type '{ newCell: { subscribe: (
: any, { type }: { type: any; }) => AsyncIterator<{}>; }; }' is not assignable to type '() => any'.
Type '{ newCell: { subscribe: (_: any, { type }: { type: any; }) => AsyncIterator<{}>; }; }' provides no match for the signature '(): any'.

import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();

export const typeDefs = `

type Subscription {
  newCell: Cell
}

type Query {
  addCell(type: CellType!): Cell
}

schema {
  query: Query
  subscription: Subscription
}
`

export const resolvers = {
  Query: {
    addCell: async (_, { type }) => {
      let cell = new Cell();
      cell.type = type;
      pubsub.publish('newCell', { newCell: cell });
      return cell;
    },
  },

  Subscription: {
    newCell: {
      subscribe: () =>
        pubsub.asyncIterator('newCell')
    }
  }
};

Infinite loop after last unsubscribe in withFilter function.

I use withFilter to validate payload and got an infinite loop when there are no subscribers left (payload is resolved immediately with {value: undefined, done: true}).

const getNextPromise = () => {
        return asyncIterator
          .next()
          .then(payload => Promise.all([
            payload,
            Promise.resolve(filterFn(payload.value, args, context, info)).catch(() => false),
          ]))
          .then(([payload, filterResult]) => {
            if (filterResult === true) {
              return payload;
            }

            // Skip the current value and wait for the next one
            return getNextPromise();
          });
      };

This function recursively calls itself even if payload.done is true and it leads to infinite call. Adding check for payload.done before return getNextPromise(); fixes this problem or am I missing something?

Dynamic channel names

newCommentsChannel: {
   filter: comment => comment.repository_name === args.repoFullName,
},

Hi, I was wondering about the snippet above, would it scale to a large number of subscriptions? Since each subscription is doing a filter check, that would be o(n). Alternatively, is there a way to declare that variable upfront, for example newComments_${postId} as the channel name. Currently, the setupFunctions method seems to be mapping each distinct channel. A way to use a pattern would be helpful. Would love to understand more about optimizing subscriptions. Cheers :)

Authorizing subscriptions

I've got a foobarUpdated subscription, but I need to authorize the users who subscribe to it as well as validate the variable (if the given id is not a valid foobar id, subscription doesn't make sense).

Is there a way to deny/cancel a subscription? I'm using withFilter but that only works for filtering after the subscription has been initiated.

Can't catch exception

Hi, I have warnings like
backend | [1] bk-app-dev | (node:2449) Warning: a promise was created in a handler at var/www/backend/node_modules/graphql-subscriptions/src/pubsub.ts:190:19 but was not returned from it, see http://goo.gl/rRqMUw, but I can't figure out where throwed error.

Schema not provided?

I have followed the Readme and tried to follow along with this Medium post and the GitHunt API example.

Unfortunately my pubsub doesn't seem to be publishing my updates. Interestingly when I try to subscribe directly through my node server I am getting the Error: Must provide schema, which I do not understand. Not sure if you can see anything wrong with this setup:

// subscribe.js
import { PubSub, SubscriptionManager } from 'graphql-subscriptions';
import schema from './schema';

const pubsub = new PubSub();
const subManger = new SubscriptionManager({
	schema,
	pubsub,
	setupFunctions: {
		todoAdded: (options, args) => ({
			todoAdded: (todo) => {
				return true;
			},
		}),
	},
});

subManger.subscribe({
	query: `subscription todos($completed:Boolean) {
				todoAdded(completed:$completed){
					id
					title
					completed
				}
			}`,
	variables: {
		completed: null
	},
	context: {},
	callback: (err, data) => console.log(data)
});

pubsub.publish('todoAdded', {
	id: 123,
	title: 'testing graphql subscriptions',
	completed: false
});

export { subManger, pubsub };
// schema.js
import Resolvers from './resolvers';
import Schema from './schema.graphql';
import { makeExecutableSchema } from 'graphql-tools';

const executableSchema = makeExecutableSchema({
	typeDefs: Schema,
	resolvers: Resolvers,
	allowUndefinedInResolve: true,
	resolverValidationOptions: {
		requireResolversForNonScalar: false
	},
	printErrors: process.env.NODE_ENV !== 'production',
});

export default executableSchema;
// schema.graphql
type Todo {
	id: ID
	createdAt: String
	updatedAt: String
	title: String
	completed: Boolean
}

type Query {
	todos(completed: Boolean): [Todo]
	todoAdded(completed: Boolean): Todo
}

type Mutation {
	addTodo(title: String!): Todo
	editTodo(id: Int!, title: String, completed: Boolean): Todo
	deleteTodo(id: Int!): String
}

type Subscription {
	todoAdded(completed: Boolean): Todo
}

schema {
	query: Query
	mutation: Mutation
	subscription: Subscription
}

Consider handling the errors thrown in setupFunction and make it promisable.

On file pubsub.ts on line 162, the setupFunction should be handling the errors thrown in the function. That is important because we can decline and authorize subscriptions directly in the setupFunction itself. And by having this in mind you should make the setupFunction promisable for security async operations, like the onSubscribe method.

should handle as promise

In my case this is important because I have different authorization methods, and one can be invalid depending on the subscription.

Here's an example of how I would be able to use it.

example usage

How to set context for subscriptions?

It seems that my resolver when responding to a subscription doesn't have access to the context variable, that are typically available with a non-subscription request. In my case I'm trying to push as many request that I can through a data loader.

Heres my resolver:

  resolve: (source, args, { loaders }) => {
    if (source.user.length === 0) {
      return {};
    }
    return loaders.users.load(source.user);
  },

Currently, I have something like this setup for standard graphql requests

  router.post('/graphql', graphqlKoa({
    schema,
    formatParams(params) {
      return params;
    },
    context: { loaders: {
      users: Loader(r.table('users'), 'id')
    } },
  }));

How might I set something like this with subscriptions?

Default GraphQL arguments are not passed into setupFunctions

Hello,

Versions used

  • master@341482dc982983f4904b7fee51e672820f376830

Setup

When defining setup functions like this :

  setupFunctions: {
    commentAdded: (options, args) => ({
      newCommentsChannel: {
        filter: comment => comment.repository_name === args.repoFullName,
      },
    }),
  },

I might be wrong here, since I'm not really familiar with graphql-subscriptions but I assume args represents the graphql arguments passed into the subscription query.

Considering this query :

subscription CommentAdded {
  commentAdded(feed: 1234) {
    ...
   }
}

args will be properly set to the expected value :

{ feed: 1234 }

However, when default arguments are defined in the schema like this :

type RootSubscription {
  commentAdded(
    feed: Int = 10
  ): Comment!
}

args will not be set to { feed: 10 } as one would expect.

Actual outcome

args is set to

{}

Expected outcome

args set to

{ feed: 10 }

Failing test

I forked and created a failing test case demonstrating the issue. See here : kouak@820101a

Let me know if you want me to submit a PR with this failing test case.

Using an external PubSub engine

Hello,

I would like to know if it is possible to use graphql-mqtt-subscriptions along with this package. More precisely, I'd like someone to explain how to wrap a pubsub with an AsyncIterator since the apollo client needs one for the subscription to be effective.

My final goal is to use a MQTT broker to handle subscriptions instead of the in-memory pubsub engine provided by your package.

Thank you for your help.

Help: Easy subscriptions with rethinkdb changefeeds

Hello guys!

Sorry for the level on this. But I havnt found any better place to ask this, and i'm new to nodejs and graphql. I have been looking for an easy way to make use of rethinkdb changefeeds in my subscription, and just found out of this solution and was wondering what you guys think of it:

Subscription: {
    changefeedTest: {
      subscribe: () => {
        const feedUuid = uuid()
        r.table('people').filter({ maybeSomethingUniqueHereForEachUser }).changes().run().then((feed) => {
          feed.each((error, change) => {
            console.log('publishing data to user')
            pubsub.publish(feedUuid, { changefeedTest: change.new_val })
          })
        })

        return pubsub.asyncIterator(feedUuid)
      }
    }
  }

Any thoughts of this approach? With this code I skip the generic pubsub topics and create a unique one for each subscription query against the DB. This way I think i'm certain that each user will just get the data it should have since they will only get the unique topic.

Only problem I have right now, is that the changefeed keeps running after the user disconnects. So after a while testing I have X (one more each time) "publishing data to user" as soon as i open a new connection. I am thinking of saving a reference to the feed to an array, and close them in the onDisconnect hook on subscriptionserver. I would however either save them somehow on a session specific variable (i dont know how to do this in node), or save it in a big array grouped on session id.

feedsToDisconnect[uniqueSessionId].push(feed)

and then inside the onDisconnect:

feedsToDisconnect[uniqueSessionId].map((feed) => { feed.close() }) and kill of the group.

Subscription leak

SubscriptionManager::subscriptions maps is never cleaned up from subscription tracking and leaks their references.
I assume it should be cleaned up on unsubscribe ?

Async subscribe function

I want to validate the subscription params with an async function. If I return a promise from the subscription resolver on the server the client errors with Subscription must return Async Iterable.

Can I submit a PR to allow an async subscribe function? And where should I look to make those changes?

pass variables to pub sub?

I've hacked your code so it does

// 3. subscribe and keep the subscription id
            const subsPromise = this.pubsub.subscribe(triggerName, onMessage, channelOptions, options.variables);

rather than..

// 3. subscribe and keep the subscription id
            const subsPromise = this.pubsub.subscribe(triggerName, onMessage, channelOptions);

this means I can implement pubsub so it subscribes to an external event source (dont worry about details):

const valueSubscriptions = {};

class MyPubSub extends PubSub {

  subscribe(trigger, onMessage, channelOptions, variables){
    const ret = super.subscribe(trigger, onMessage);
    if (trigger === 'value') {
      ret.then(f => valueSubscriptions[f] = opcObserver(variables.id).subscribe(v => pubsub.publish('value', v)));
    }
    return ret;
  }
  unsubscribe(subid) {
    if (valueSubscriptions[subid]) {
      valueSubscriptions[subid].unsubscribe();
      delete valueSubscriptions[subid];
    }
    return super.unsubscribe(subid);
  }

}

would this make sense for you to incorporate?

JWT auth + refresh token issue

I use JWT auth for authentication and no problems with apollo client authentication because http://dev.apollodata.com/core/network.html#networkInterfaceMiddleware even when token refreshes, but I have the issue with graphql subscriptions authentication. According to https://github.com/apollographql/graphql-subscriptions/blob/master/.designs/authorization.md and https://github.com/apollographql/tools-docs/blob/master/tools/graphql-subscriptions/authentication.md token sets once only (connectionParams: {token: 'some string'} and lazy: true) on initialisation SubscriptionClient and it works fine, but not when token refreshes.

Technologies/libs which I use in my project:

  • vue
  • vue-auth
  • vue-apollo
  • expressjs
  • graphql-subscriptions
  • subscriptions-transport-ws
  • graphql

Trouble subscribing to the publisher using async iterator

I have a graphql server and I'm attempting to trigger an event whenever the status of an entity is updated. I have a mutation for updating the entity, which uses graphql-subscriptions to publish the update for the subscriber to listen to. However, pubsub.subscribe(EVENT_NAME) picked up the change, but when I integrate that into the subscription resolver, it doesn't trigger the update.

I've redacted parts of the code not relevant to the issue for brevity.

My resolver:

Mutation: {
  updateStatus(_, { id, status, hash }) {
    return repository.updateStatus(id, status, hash);
  }
},
Subscription: {
  updatedStatus: {
    subscribe: () => pubsub.asyncIterator('updatedStatus'),
  }
},

My repository:

export const updateStatus = async function(id: String, status: String, hash: String) {
  try {
    const res = await fetch(`${baseURL}/article/${id}/status?publish_hash=${hash}`, {
      headers,
      method: 'PATCH',
      body: JSON.stringify({ status })
    });

    const article = await fetchSingleArticle(id, 'XML', headers.channel, false);

    await pubsub.publish('updatedStatus', article);
    return article;
  } catch (e) {
    console.log(e.message);
  }
}

My schema:

type Mutation {
  updateStatus(id: ID!, status: PublishingStatus = PUBLISHING, hash: String): Article
}

type Subscription {
  updatedStatus(id: ID!): Article
}

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

To test this, I'm just using graphiql, which is picking up the websocket connection fine.

I've attempted the publish using both article (as shown) and { updatedStatus: article }, neither seem to get picked up.

As mentioned previously pubsub.subscribe('updatedStatus') does pick up the change. So the issue seems to be related to the Subscription resolver.

I'm going off the following docs: http://dev.apollodata.com/tools/graphql-subscriptions/setup.html#setup

pattern for getting initial subscription payload

I was wondering what a good pattern would be for receiving an initial subscription payload?

let me try to explain with the help of some contrived examples:

if we run the code from the docs ...

// hooking up subscription resolvers etc., simplified
const resolvers = {
  Subscription: {
    getReport: {
      subscribe: () => ....
// ...

// then run ...
pubsub.publish( 'somethingChanged', { getReport: { id: "1" }});

then the subscription query would not receive any payload:

subscription {
   getReport {
      id
   }
}

... unless we are triggering publish, e.g. through a Mutation.

function publish() {
   pubsub.publish( 'somethingChanged', { getReport: { id: "1" }});
}

const resolvers = {
  Mutation: {
    triggerReport: () => publish()

e.g. we could also make it work this way - just to have a (bad) example:

setInterval( () =>
   pubsub.publish( 'somethingChanged', { getReport: { id: "1" }})
), 1000 );

My understanding is that one would usually use a Mutation in order to trigger publish.
I was wondering how could one receive the initial payload without a trigger?

Unless subscriptions are only meant to be published through some sort of trigger?

Or would one get the initial payload through an ordinary graphql query?

query {
   getReport {
      id
   }
}

Thanks in advance for any advice!

Can't subscribe when field is of another type

I'm trying to create a Subscription to tell when a new post is created. My issue is that the type Post have an author: User! and I don't know how to pass the User to the Post on the mutation. I can pass the id, title, and description without issues but not the User.

Subscription:
https://github.com/KadoBOT/musical-memory/blob/master/src/components/Posts/index.js

Schema and Resolver:
https://github.com/KadoBOT/musical-memory/tree/master/server/schema

Can someone help me?

Move `graphql` to be peer dependency instead of dependency

Unable to work with latest Node, now when adding graphql and graphql-subscriptions as dependencies - I get duplicate of graphql:

node_modules
     graphql
     graphql-subscriptions
           node_modules
                 graphql

Temporary workaround is to remove graphql dependency, and then NPM flatten the need for graphql and install it under the root node_modules.

Publish API should be removed (me thinks)

How one want to publish shouldn't matter to the SubscriptionManager, as the correct way to publish messages is through the onMessage callback.
I think the publish methods on PubSub and SubscriptionManager are confusing, as you can implement a correct (for now) server without them.

E.g. I implemented a PubSub based on Observables:

    pubsub: {
      subscribe(triggerName, onMessage, channelOptions) {
        return Promise.resolve(channelOptions.resolve$.subscribe(onMessage, console.error))
      },
      unsubscribe(subscription) {
        subscription.unsubscribe()
      }
    },

No need for publish (and I wouldn't know how to implement it if I wanted to).

It's fine for the default in-memory-event-based-pubsub-implementation to have a publish method, but it shouldn't be in the interface or wrapped in SubscriptionManager.

Thx.

Besides filter, add a property to format rootValue

You should have another property besides filter for trigger map which could clean the rootValue before executing on graphql and submitting to the client, a suggestion would be a promisable (!) format or even a resolve property.

On file pubsub.ts on line 197,

before execute

In my case this is important because I need to remove private values on an object type when the client authorized is not eligible to see it.

The following is an example of how I would be able to use it.

example usage

Subscriptions fails with untraceable error

I've been trying to trace a subscription_failed error for hours without any success. The data sent to the client on failure is {"type":"subscription_fail","id":0,"payload":{"errors":[{"message":"Cannot read property 'forEach' of undefined"}]}}.

There is nothing logged to the console of the server. I've also checked the application code and there is no use of forEach so I suspect it's rooted in graphql-subscriptions or subscriptions-transport-ws. Both the client and server are using the latest releases of the Apollo stack.

Subscriptions were working flawlessly up until I resumed development today. I'm going to check out the master branch and see if it the error is still happening. I'll also attempt a clean install.

I couldn't find an issue similar to this one so I'm opening this in hope of some direction. Any help is much appreciated.

The terminology is confusing (at least for me)

I've struggled for a couple hours last night to get this but I couldn't.


Snippet 1

subscription($repoName: String!) {
  commentAdded(repoName: $repoName) { # <-- `commentAdded` is the subscription name
    id
    content
  }
}

Snippet 2

const subscriptionManager = new SubscriptionManager({
  schema,
  pubsub,
  setupFunctions: {
    commentAdded: (options, args) => ({
      commentAdded: {
        filter: comment => comment.repository_name === args.repoFullName,
      },
    }),
  },
});

Snippet 3

pubsub.publish('commentAdded', payload);

I have a couple questions

  1. In the 1st snippet commentAdded is the subscription name and in the 3rd snippet commentAdded is the event name. Are those the same thing?

  2. Why commentAdded appears twice in setupFunctions of the 2nd snippet? Wouldn't this be enough?

    const subscriptionManager = new SubscriptionManager({
      schema,
      pubsub,
      setupFunctions: {
        commentAdded: (options, args) =>
          payload => payload.repository_name === args.repoFullName,
      },
    });
  3. The example from the main readme of this repo confused me even more. The subscription name is commentAdded but the event name is newCommentsChannel. How is the event name constructed? Do you simply append Channel to the end of the operation name (newComments in this case)? Isn't the operation name optional?

    subscription newComments($repoName: String!){
      commentAdded(repoName: $repoName) { # <-- this is the subscription name
        id
        content
        createdBy {
          username
        }
      }
    }

How to store all clients?

Hi, I have a task to display all connected clients on client side.
Ex:

Name1 (email)
Name2 (email)
...
NameN (email)

I know that I have to store some information on server and should use onConnect, but how?
With SocketIO I can use rooms, but how I can realize the same with graphql-subscriptions?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.