GithubHelp home page GithubHelp logo

dxcx / graphql-rxjs Goto Github PK

View Code? Open in Web Editor NEW
77.0 8.0 6.0 70 KB

fork of Graphql which adds Observable support

License: Other

JavaScript 59.48% TypeScript 38.60% Shell 1.92%
graphql graphql-js graphql-rxjs rxjs observables graphql-subscriptions

graphql-rxjs's Introduction

GraphQL-RxJs

fork of graphql-js which adds AsyncIterator & Observable support (RxJs).

npm version Build Status

Intro

This package adds Reactivity for GraphQLResolver, Which means you can now return:

  • Observables
  • AsyncIterator

This package also adds reactive directives support:

  • @defer
  • @live

The package is pretty small because it is importing the original graphql-js package, and then patches it to provide a reactive execution engine over it.

Examples

Documentation

There isn't much to document, all of GraphQL documentation is relevant. See the complete documentation at http://graphql.org/ and http://graphql.org/graphql-js/.

Versioning

I'll try to follow along with graphql-js versions, so basiclly, each graphql-js version should have a working graphql-rxjs package with it.

API

The library exports the following functions:

AsyncIterator support

export function graphqlReactive(
  schema: GraphQLSchema,
  requestString: string,
  rootValue?: any,
  contextValue?: any,
  variableValues?: {[key: string]: any},
  operationName?: string
  fieldResolver?: GraphQLFieldResolver<any, any>,
): AsyncIterator<ExecutionResult>;

export function executeReactive(
  schema: GraphQLSchema,
  document: DocumentNode,
  rootValue?: any,
  contextValue?: any,
  variableValues?: {[key: string]: any},
  operationName?: string
  fieldResolver?: GraphQLFieldResolver<any, any>,
): AsyncIterator<ExecutionResult>;

The signature is equal to GraphQL original implementation (graphql + execute), except it returns an asyncIterator instead of a promise. The asyncIterator will stream immutable results.

Observable support

export function graphqlRx(
  schema: GraphQLSchema,
  requestString: string,
  rootValue?: any,
  contextValue?: any,
  variableValues?: {[key: string]: any},
  operationName?: string
  fieldResolver?: GraphQLFieldResolver<any, any>,
): Observable<ExecutionResult>;

export function executeRx(
  schema: GraphQLSchema,
  document: DocumentNode,
  rootValue?: any,
  contextValue?: any,
  variableValues?: {[key: string]: any},
  operationName?: string
  fieldResolver?: GraphQLFieldResolver<any, any>,
): Observable<ExecutionResult>;

export function subscribeRx(
  schema: GraphQLSchema,
  document: DocumentNode,
  rootValue?: any,
  contextValue?: any,
  variableValues?: {[key: string]: any},
  operationName?: string,
  fieldResolver?: GraphQLFieldResolver<any, any>,
  subscribeFieldResolver?: GraphQLFieldResolver<any, any>
): Observable<ExecutionResult>;

The signature is equal to GraphQL original implementation (graphql + execute + subscribe), except it returns an observable instead of a promise. The observable will stream immutable results.

Preparing schema for GraphQL-RxJs

export function prepareSchema(
  schema: GraphQLSchema,
): prepareSchema;

This function is used to prepare schema for graphql-rxjs. wrapping resolvers, adding reactive directives support, etc.. At the moment, it will be automatically invoked when running GraphQL-RxJS.

NOTE: if you are using original graphql's validate, you will have to trigger prepareSchema manually and not count on auto-trigger.

if you don't want to call it directly, you can use the inner APIs seperately:

Reactive Directives
export function addReactiveDirectivesToSchema(
  schema: GraphQLSchema,
): void;

Calling this function on your existing GraphQLSchema object will enable reactive directives suppot for the schema. More information about reactive directives can be found below.

Observable support in resolvers
export function wrapResolvers(
  schema: GraphQLSchema,
): void;

Calling this function on your existing GraphQLSchema object will enable reactive directives suppot for the schema. More information about reactive directives can be found below.

Getting Started:

  1. The Data source

Let's start off by explaining our Observable, or Data-source that we are going to stream.

const clockSource = Observable.interval(1000).map(() => new Date()).publishReplay(1).refCount();

We can see that it is an Observable that emits i+1 every second:

let source = Observable.interval(1000);

Next we are going to map the value to a the current timestep:

source = source.map(() => new Date());

Finally, We are going to convert the observable into an hot observable, or multicast.

This means that there is a ref count on the observable, only once the first subscriber subscribe, the provider function will be triggered, and from then on, each subscriber will get the last value (without triggering the provider function) and then, both subscribers will get next values.

Once a subscriber unsubscribe, the refCount will decrease, until the counter reachs zero, and only then the unsubscribe function will be triggered.

const clockSource = source.publishReplay(1).refCount();

This approach will let us manage resources much more efficiently.

  1. The Scheme

Next, let's look at our scheme

# Root Query
type Query {
  someInt: Int
}

# Root Subscription
type Subscription {
  clock: String
}

Query type exposes someInt, just because it cannot be empty, let's ignore that.

Subscription type expoes clock which is basiclly periodic timestamp strings.

  1. Wrapping them togather

So, here is a basic code to demonstrate the concept:

import { Observable } from 'rxjs';
import { makeExecutableSchema } from 'graphql-tools';
import { prepareSchema, graphqlRx } from 'graphql-rxjs';

const clockSource = Observable.interval(1000).map(() => new Date()).publishReplay(1).refCount();

const typeDefs = `
# Root Query
type Query {
  someInt: Int
}

# Root Subscription
type Subscription {
  clock: String
}
`;

const resolvers = {
    Subscription: {
        clock(root, args, ctx) {
              return ctx.clockSource;
        },
    },
};

// Compose togather resolver and typeDefs.
const scheme = makeExecutableSchema({typeDefs: typeDefs, resolvers: resolvers});
prepareSchema(schema);

// subscribe the clock
const query = `
  subscription {
	clock
  }
`

// Calling the reactive version of graphql
graphqlRx(scheme, query, null, { clockSource })
.subscribe(console.log.bind(console), console.error.bind(console));

The following response will emit in console:

{"data":{"clock":"Fri Feb 02 2017 20:28:01 GMT+0200 (IST)"}}
{"data":{"clock":"Fri Feb 02 2017 20:28:02 GMT+0200 (IST)"}}
{"data":{"clock":"Fri Feb 02 2017 20:28:03 GMT+0200 (IST)"}}
...

Reactive Directives

This library also implements reactive directives, those are supported at the moment:

  1. GraphQLDeferDirective (@defer)
  • This directive does not require any arguments.

  • This directive instructs the executor to not resolve this field immedaitly, but instead returning the response without the deferred field, once the field is deferred, it will emit a corrected result.

  • executor will ignore this directive if resolver for this value is not async.

  • can be applied on:

    • specific field
    • spread fragment
    • named fragment
  • Example:

    import { Observable } from 'rxjs';
    import { makeExecutableSchema } from 'graphql-tools';
    import { prepareSchema, graphqlReactive } from 'graphql-rxjs';
    
    const remoteString = new Promise((resolve, reject) => {
      setTimeout(() => resolve('Hello World!'), 5000);
    });
    
    const typeDefs = `
    # Root Query
    type Query {
      remoteString: String
    }
    `;
    
    const resolvers = {
        Query: {
          remoteString: (root, args, ctx) => ctx.remoteString,
        },
    };
    
    const scheme = makeExecutableSchema({typeDefs, resolvers});
    prepareSchema(scheme);
    
    const query = `
      query {
        remoteString @defer
      }
    `;
    
    const log = (result) => console.log("[" + (new Date).toLocaleTimeString() + "] " + JSON.stringify(result));
    
    graphqlReactive(scheme, query, null, { remoteString })
    .subscribe(log, console.error.bind(console));

    The following response will emit in console:

    [8:58:05 PM] {"data":{}}
    [8:58:10 PM] {"data":{"remoteString":"Hello World!"}}
    
  1. GraphQLLiveDirective (@live)
  • This directive does not require any arguments.

  • This directive instructs the executor that the value should be monitored live which means that once updated, it will emit the updated respose.

  • executor will ignore this directive if field is not resolved with an observable (or at least have a parent observable).

  • can be applied on:

    • specific field
    • spread fragment
    • named fragment
    • fragment definition - (Live Fragment)
  • Example:

    import { Observable } from 'rxjs';
    import { makeExecutableSchema } from 'graphql-tools';
    import { prepareSchema, graphqlReactive } from '..';
    
    const clockSource = Observable.interval(1000).map(() => new Date()).publishReplay(1).refCount();
    
    const typeDefs = `
    # Root Query
    type Query {
      clock: String
    }
    `;
    
    const resolvers = {
        Query: {
          clock: (root, args, ctx) => ctx.clockSource,
        },
    };
    
    const scheme = makeExecutableSchema({typeDefs, resolvers});
    prepareSchema(scheme);
    
    const query = `
      query {
        clock
      }
    `;
    
    const liveQuery = `
      query {
        clock @live
      }
    `;
    
    graphqlReactive(scheme, query, null, { clockSource })
    .subscribe(console.log.bind(console, "standard: "), console.error.bind(console));
    
    graphqlReactive(scheme, liveQuery, null, { clockSource })
    .subscribe(console.log.bind(console, "live: "), console.error.bind(console));

    The following response will emit in console:

    standard:  { data: { clock: 'Sun Apr 16 2017 21:04:57 GMT+0300 (EEST)' } }
    live:  { data: { clock: 'Sun Apr 16 2017 21:04:57 GMT+0300 (EEST)' } }
    live:  { data: { clock: 'Sun Apr 16 2017 21:04:58 GMT+0300 (EEST)' } }
    live:  { data: { clock: 'Sun Apr 16 2017 21:04:59 GMT+0300 (EEST)' } }
    live:  { data: { clock: 'Sun Apr 16 2017 21:05:00 GMT+0300 (EEST)' } }
    ...
    

Typescript support

Just install @types/graphql for initial GraphQL support, then the package will automatically add typings for the new functions it provides.

Benchmarks

      * graphqlOri x 7,496 ops/sec ±9.20% (78 runs sampled)
      * graphqlRx x 6,790 ops/sec ±3.48% (75 runs sampled)
      => Fastest is graphqlOri
    ✓ compare performance for simple query (11088ms)

      * graphqlOri x 3,040 ops/sec ±9.81% (70 runs sampled)
      * graphqlRx x 1,303 ops/sec ±4.19% (74 runs sampled)
      => Fastest is graphqlOri
    ✓ compare performance for deep query (17515ms)
    
      * graphqlOri x 5,360 ops/sec ±20.55% (12 runs sampled)
      * graphqlRx x 3,067 ops/sec ±32.41% (10 runs sampled)
      => Fastest is graphqlOri
    ✓ compare performance for serial mutation (27004ms)

as results shows above, the reactive engine is abit slower then the original one, however that was expected, i have yet to put the effort on optimizations, and i am planing to optimize on the future.

Issues

If you found an issue or have an idea, you are welcome to open a new ticket in Issues Page

Support

Using this approach, feels much more intuative then the other approaches to stream results so far. Because of that I tried to push it into upstream graphql-js but got rejected, if you want to support the project you can follow/thumbs up the following:

  1. Issue on graphql-js
  2. PR on graphql-js
  3. Draft design for apollo subscriptions

Contributing

All pull requests are welcome, if you think something needs to be done, just open an issue or a PR :)

graphql-rxjs's People

Contributors

dxcx avatar ohadgk 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

graphql-rxjs's Issues

Send chunks(continues) responses to client

I am trying something like below, but getting null as response.

const resolvers = {
  Query: {
    someLiveInt(root, args, ctx) {
      return Observable.interval(100);
    },
    simpleInt(root, args, ctx) {
      return 10;
    },
  },
  **Subscription: {
    clock: function clock(root, args, ctx) {
      Observable.from([1,2,3,4,5]).subscribe(x => {return x});
    }
  }**
};

My expectation is to send 5 responses to client with one number in each.
response1 { "data": {"clock": 1}}
response2 { "data": {"clock": 2}}
response3 { "data": {"clock": 3}}
response4 { "data": {"clock": 4}}
response5 { "data": {"clock": 5}}

Can you suggest what is missing here?

TypeError: Cannot read property 'next' of undefined

I tried the example in document,
which failed with error:

Error: Cannot find module 'graphql/validation/rules/ArgumentsOfCorrectType'
    at Function.Module._resolveFilename (module.js:542:15)
    at Function.Module._load (module.js:472:25)
    at Module.require (module.js:585:17)
    at require (internal/module.js:11:18)
    at Object.<anonymous> (/node_modules/graphql-rxjs/dist/bundle.js:1:27561)
    at Module._compile (module.js:641:30)
    at Object.Module._extensions..js (module.js:652:10)
    at Module.load (module.js:560:32)
    at tryModuleLoad (module.js:503:12)
    at Function.Module._load (module.js:495:3)

And it's plain javascript, no typescript or flow.
This is the code I wrote:

const { Observable } = require("rxjs");
const { makeExecutableSchema } = require("graphql-tools");
const { prepareSchema, graphqlRx } = require("graphql-rxjs");

const clockSource = Observable.interval(1000)
    .map(() => new Date())
    .publishReplay(1)
    .refCount();

const typeDefs = `
# Root Query
type Query {
  someInt: Int
}
 
# Root Subscription
type Subscription {
  clock: String
}
`;

const resolvers = {
    Subscription: {
        clock(root, args, ctx) {
            return ctx.clockSource;
        }
    }
};

// Compose togather resolver and typeDefs.
const schema = makeExecutableSchema({
    typeDefs: typeDefs,
    resolvers: resolvers
});
prepareSchema(schema);

// subscribe the clock
const query = `
  subscription {
    clock
  }
`;
// Calling the reactive version of graphql
graphqlRx(schema, query, null, { clockSource }).subscribe(
    console.log.bind(console),
    console.error.bind(console)
);

And my package.json

{
  "name": "learngraphql",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "body-parser": "^1.18.2",
    "express": "^4.16.2",
    "express-graphql": "^0.6.11",
    "graphql": "^0.12.3",
    "graphql-rxjs": "^0.11.7-0",
    "graphql-tools": "^2.14.1",
    "rxjs": "^5.5.5"
  },
  "devDependencies": {
    "eslint": "^4.13.1"
  }
}

External schema generation and Promises for Observables

I'm using an external library to generate part of my schema. The library support async resolvers but isn't aware they are allowed to be AsyncIterators/Observables, and as such I end up with strings saying [object Object] if I try to use Observables in the partially generated resolvers. As far as I can tell, this is because the library wraps the result in a Promise, resulting in an Observable wrapped in a Promise which is just interpreted as being a Promise resolving to an object.

I will probably end up solving this by fixing this myself on the library, but I was thinking this might be a broader problem that might be generally solvable by considering Promises for Observables to be a valid use case. Curious about your thoughts. 😃

mutation resolver to return asynciterator

I have a case where I need to run a train of actions with a mutation call and update the client on each step completion.

For example

  1. client requested to create and start an action (mutation call)
  2. mutation calls a service and the service should update the client when each action completes. So I think asynciterator is the best fit. Do you think graphql-rxjs fits the case? What do you suggest would be a better way to do it.

Below is just base line for what i want to achieve.

public async updateAction(request): AsyncIterator<IAction> {
cons action = await this.writeToDatabase(request). 
// udpate the client that request is persisted and action is in pending
const createAction = // some async action to create task
// update client that action is created
const startAction = // some async action to start task
// update client that action is started. 
finally return

I know this can be achieved with subscription, but I feel we don't have use subscription since the data feed would only go to one client during the series of these action.

Make compatible with Apollo Link

I'm trying to create a custom Apollo Link to use with Apollo Client. However, I'm running into an issue that I believe has to do with the differences between RxJS and zen-observable (used by apollo). Here's my Apollo Link:

import { ApolloLink, Operation, FetchResult, Observable } from 'apollo-link'
import { graphqlRx } from 'graphql-rxjs'
import { print } from 'graphql/language/printer'

export class LocalLink extends ApolloLink {
  constructor({ schema, rootValue, context }) {
    super()

    this.schema = schema
    this.rootValue = rootValue
    this.context = context
  }

  request(operation) {
    const request = {
      ...operation,
      query: print(operation.query)
    }

    return Observable.from(graphqlRx(
      this.schema,
      request.query,
      this.rootValue,
      this.context,
      request.variables,
      request.operationName
    ))
  }
}

export default LocalLink

However, I'm getting the following error:

Error: Network error: Observable.Observable.fromPromise(...).flatMap is not a function

Can you please provide complete example

Can you please provide end to end example, in current example query is hard coded and output is written on console. Do you have example where I can pass query from client(similar to graphiQL) and send response back to client only?

We use below for graphql to start server at 4000 and inbuilt client(graphiql).
`const app = express();

app.use(graphQLHTTP({
schema,
graphiql:true,
}));

app.listen(4000);`

pubsub withFilter and resolve

How can I use @live with pubsub withFilter and resolve?
here is my code:

score: {
          resolve: (payload, args, context, info) => {
            this.log('resolve', payload)
            return payload.score || 0.01
          },
          subscribe: (_) => {
            this.log('subscribe', 'score')
            return withFilter(
              // Subscribe
              () => this.pubsub.asyncIterator('score.updated'),
              // Filter Events
              async (payload) => {
                this.log('filter', payload, payload.score)
                return false
              }
            )
          }
        },

But at the end, only resolve function was called, without any filtering and subscribing. what is my problem?

automatically update

could you add a web hook which will automatically build test and release graphql-rxjs (by CI) every time graphql.js release a new version. Here is some references http://blog.npmjs.org/post/145260155635/introducing-hooks-get-notifications-of-npm

This way you could keep this package up to date with graphql.js with less maintenance effort. I think that you should rewrite the package with typescript so you can have automatically update typing file too.

I have a question that why you not just include graphql.js as a peer dependency? Why do you have to keep a fork of graphql.js when you don't modify any thing at all?

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.