Skip to content

Commit

Permalink
Add Subscription query types to v1
Browse files Browse the repository at this point in the history
  • Loading branch information
blainekasten committed Dec 28, 2018
1 parent d877e5f commit f524c78
Show file tree
Hide file tree
Showing 16 changed files with 781 additions and 63 deletions.
44 changes: 43 additions & 1 deletion example/src/app/home.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
/* tslint:disable */

import * as React from 'react';
import { Connect, createQuery, createMutation } from '../../../src/';
import {
Connect,
createQuery,
createMutation,
createSubscription,
} from '../../../src/';
import TodoList from './todo-list';
import TodoForm from './todo-form';

Expand All @@ -17,13 +22,33 @@ export interface ITodoMutations {
removeTodo: (input: { id: string }) => void;
}

const handleSubscription = (type, data, todo) => {
switch (type) {
case 'todoAdded':
if (data.todos.find(t => t.id === todo.id) === undefined) {
data.todos.push(todo);
}
break;
case 'todoRemoved':
data.todos.splice(data.todos.findIndex(t => t.id === todo.id), 1);
break;
}

return data;
};

const Home: React.SFC<{}> = () => (
<Connect
query={createQuery(TodoQuery)}
mutations={{
addTodo: createMutation(AddTodo),
removeTodo: createMutation(RemoveTodo),
}}
subscriptions={[
createSubscription(TodoAdded),
createSubscription(TodoRemoved),
]}
updateSubscription={handleSubscription}
children={({ data, error, mutations, fetching, refetch }) => {
const content = fetching ? (
<Loading />
Expand Down Expand Up @@ -82,4 +107,21 @@ query {
}
`;

const TodoAdded = `
subscription todoAdded {
todoAdded {
id
text
}
}
`;

const TodoRemoved = `
subscription todoRemoved {
todoRemoved {
id
}
}
`;

export default Home;
33 changes: 31 additions & 2 deletions example/src/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,40 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';

import { Provider, createClient } from '../../../src/index';
import {
Provider,
createClient,
dedupeExchange,
fetchExchange,
cacheExchange,
createSubscriptionExchange,
} from '../../../src/index';
import Home from './home';

import { SubscriptionClient } from 'subscriptions-transport-ws';
const subscriptionClient = new SubscriptionClient(
'ws://localhost:3001/graphql',
{}
);

const subscriptionExchange = createSubscriptionExchange((operation, observer) =>
subscriptionClient.request(operation).subscribe({
next(data) {
observer.next({ operation, data, error: null });
},
error(error) {
observer.error({ operation, data: null, error });
},
})
);

const client = createClient({
url: 'http://localhost:3001/graphql',
exchanges: [
subscriptionExchange,
dedupeExchange,
cacheExchange,
fetchExchange,
],
});

export const App: React.SFC<{}> = () => (
Expand Down
46 changes: 30 additions & 16 deletions example/src/server/index.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
const graphqlHttp = require('express-graphql');
const { ApolloServer, graphiqlExpress } = require('apollo-server-express');
const { createServer } = require('http');
const { SubscriptionServer } = require('subscriptions-transport-ws');
const expressPlayground = require('graphql-playground-middleware-express')
.default;
const express = require('express');
const app = express();
const cors = require('cors');

app.use(cors());

const { schema, context } = require('./schema');
const { typeDefs, resolvers } = require('./schema');

const PORT = 3001;

const initializedGraphQLMiddleware = graphqlHttp({
// GraphQL’s data schema
schema: schema,
// Pretty Print the JSON response
pretty: true,
// Enable GraphiQL dev tool
graphiql: true,
// A function that returns extra data available to every resolver
context: context,
const server = new ApolloServer({
typeDefs,
resolvers,
});

app.use(initializedGraphQLMiddleware);
server.applyMiddleware({ app });

const webServer = createServer(app);
server.installSubscriptionHandlers(webServer);

const graphqlEndpoint = `http://localhost:${PORT}${server.graphqlPath}`;
const subscriptionEndpoint = `ws://localhost:${PORT}${
server.subscriptionsPath
}`;

app.use(cors());
app.get(
'/',
expressPlayground({
endpoint: graphqlEndpoint,
subscriptionEndpoint: subscriptionEndpoint,
})
);

app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}`);
webServer.listen(PORT, () => {
console.log(`🚀 Server ready at ${graphqlEndpoint}`);
console.log(`🚀 Subscriptions ready at ${subscriptionEndpoint}`);
});
36 changes: 29 additions & 7 deletions example/src/server/schema.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const fetch = require('isomorphic-fetch');
const { makeExecutableSchema } = require('graphql-tools');
const uuid = require('uuid/v4');
const { PubSub } = require('graphql-subscriptions');

const pubsub = new PubSub();

const store = {
todos: [
Expand Down Expand Up @@ -34,6 +36,11 @@ const typeDefs = `
removeTodo(id: ID!): Todo
editTodo(id: ID!, text: String!): Todo
}
type Subscription {
todoAdded: Todo
todoRemoved: Todo
todoUpdated: Todo
}
type Todo {
id: ID,
text: String,
Expand All @@ -60,32 +67,47 @@ const resolvers = {
addTodo: (root, args, context) => {
const id = uuid();
const { text } = args;
store.todos.push({ id, text });
return { id, text };
const todo = { id, text };

store.todos.push(todo);
pubsub.publish('todoAdded', { todoAdded: todo });

return todo;
},
removeTodo: (root, args, context) => {
const { id } = args;
let todo = store.todos.find(todo => todo.id === id);
store.todos.splice(store.todos.indexOf(todo), 1);
pubsub.publish('todoRemoved', { todoRemoved: todo });
return { id };
},
editTodo: (root, args, context) => {
const { id, text } = args;
let todo = store.todos.some(todo => todo.id === id);
pubsub.publish('todoUpdated', { todoUpdated: todo });
todo.text = text;
return {
text,
id,
};
},
},
Subscription: {
todoAdded: {
subscribe: () => pubsub.asyncIterator('todoAdded'),
},
todoRemoved: {
subscribe: () => pubsub.asyncIterator('todoRemoved'),
},
todoUpdated: {
subscribe: () => pubsub.asyncIterator('todoUpdated'),
},
},
};

module.exports = {
schema: makeExecutableSchema({
typeDefs,
resolvers,
}),
typeDefs,
resolvers,
context: (headers, secrets) => {
return {
headers,
Expand Down
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@
"collectCoverageFrom": [
"<rootDir>/src/**/*.{ts,tsx}"
],
"coveragePathIgnorePatterns": ["<rootDir>/src/test-utils", "<rootDir>/src.*/index.ts"]
"coveragePathIgnorePatterns": [
"<rootDir>/src/test-utils",
"<rootDir>/src.*/index.ts"
]
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,md}": [
Expand All @@ -66,6 +69,7 @@
"@types/react": "^16.0.34",
"@types/react-dom": "^16.0.3",
"@types/uuid": "^3.4.3",
"apollo-server-express": "^2.3.1",
"awesome-typescript-loader": "^5.2.1",
"builder": "^3.2.3",
"bundlesize": "^0.17.0",
Expand All @@ -74,8 +78,8 @@
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.7.0",
"express": "^4.16.2",
"express-graphql": "^0.7.1",
"graphql-tools": "^4.0.3",
"graphql-playground-middleware-express": "^1.7.8",
"graphql-subscriptions": "^1.0.0",
"husky": "^1.2.0",
"isomorphic-fetch": "^2.2.1",
"jest": "^23.6.0",
Expand All @@ -89,6 +93,7 @@
"react-test-renderer": "^16.2.0",
"regenerator-runtime": "^0.11.1",
"rimraf": "^2.6.2",
"subscriptions-transport-ws": "^0.9.15",
"ts-jest": "^23.10.5",
"tslint": "^5.11.0",
"tslint-config-prettier": "^1.16.0",
Expand Down
31 changes: 30 additions & 1 deletion src/components/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
Mutation,
Query,
StreamUpdate,
Subscription,
SubscriptionStreamUpdate,
} from '../types';

export interface ClientState<MutationDeclarations> {
Expand All @@ -22,6 +24,12 @@ export interface ClientProps<MutationDeclarations> {
children: (obj: ChildArgs<MutationDeclarations>) => ReactNode;
query?: Query;
mutations?: { [type in keyof MutationDeclarations]: Mutation };
subscriptions?: Subscription[];
updateSubscription?: (
type: string,
prev: object | null,
next: object | null
) => object | null;
}

/** Component responsible for implementing the [Client]{@link Client} utility in React. */
Expand All @@ -35,6 +43,7 @@ export class UrqlClient<MutationDeclarations> extends Component<
this.state = {
client: props.client.createInstance({
onChange: result => this.onStreamUpdate(result),
onSubscriptionUpdate: result => this.onSubscriptionStreamUpdate(result),
}),
data: undefined,
error: undefined,
Expand All @@ -44,7 +53,14 @@ export class UrqlClient<MutationDeclarations> extends Component<
}

public componentDidMount() {
this.state.client.executeQuery(this.props.query);
const s = this.state.client.executeQuery(this.props.query);

if (Array.isArray(this.props.subscriptions)) {
this.props.subscriptions.forEach(subscription => {
// TODO: Unsubscribe on unmount
this.state.client.executeSubscription(subscription);
});
}
}

public componentDidUpdate(prevProps: ClientProps<MutationDeclarations>) {
Expand All @@ -58,6 +74,8 @@ export class UrqlClient<MutationDeclarations> extends Component<
if (JSON.stringify(prevProps.query) !== JSON.stringify(this.props.query)) {
this.state.client.executeQuery(this.props.query);
}

// TODO: unsubuscribe and ressub to new subscriptions if the prop changes
}

public componentWillUnmount() {
Expand Down Expand Up @@ -106,4 +124,15 @@ export class UrqlClient<MutationDeclarations> extends Component<
data: updated.data,
});
}

private onSubscriptionStreamUpdate(updated: SubscriptionStreamUpdate) {
const [type, data] = Object.entries(updated.data)[0];

this.setState({
error: updated.error,
data: this.props.updateSubscription
? this.props.updateSubscription(type, this.state.data, data)
: this.state.data,
});
}
}
12 changes: 11 additions & 1 deletion src/components/connect.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { ReactNode } from 'react';
import { ChildArgs, Client, Mutation, Query } from '../types';
import { ChildArgs, Client, Mutation, Query, Subscription } from '../types';
import { UrqlClient } from './client';
import { ContextConsumer } from './context';

Expand All @@ -11,6 +11,14 @@ export interface ConnectProps<T> {
query?: Query;
/** A collection of GrahpQL mutation queries */
mutations?: { [type in keyof T]: Mutation };
/** An array of GrahpQL subscription queries */
subscriptions?: Subscription[];
/** An updator function for merging subscription responses */
updateSubscription?: (
type: string,
prev: object | null,
next: object | null
) => object | null;
}

/** Component for connecting to the urql client for executing queries, mutations and returning the result to child components. */
Expand All @@ -23,6 +31,8 @@ export const Connect = function<T>(props: ConnectProps<T>) {
children={props.children}
query={props.query}
mutations={props.mutations}
subscriptions={props.subscriptions}
updateSubscription={props.updateSubscription}
/>
)}
</ContextConsumer>
Expand Down
Loading

0 comments on commit f524c78

Please sign in to comment.