Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/subscriptions #1

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/pages/docs/guides/document-fields.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ export const componentBlocks = {

There are a variety of fields you can configure.

{/* TBC: ### Child Fields */}
{/_ TBC: ### Child Fields _/}

#### Form Fields

Expand Down Expand Up @@ -637,7 +637,7 @@ fields.conditional(fields.checkbox({ label: 'Show Call to action' }), {

> You might find `fields.empty()` useful which stores and renders nothing if you want to have a field in one case and nothing anything in another

{/* TBC: ### Preview Props */}
{/_ TBC: ### Preview Props _/}

### Chromeless

Expand Down
5 changes: 5 additions & 0 deletions examples/subscriptions/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# @keystone-next/example-subscriptions

## 1.0.0

Initial commit
22 changes: 22 additions & 0 deletions examples/subscriptions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## Feature Example - Authentication

This project demonstrates how to use GraphQL Subscriptions with KeystoneJS.

## Instructions

To run this project, clone the Keystone repository locally then navigate to this directory and run:

```shell
yarn dev
```

This will start the Admin UI at [localhost:3000](http://localhost:3000).
You can use the Admin UI to create items in your database.

You can also access a Apollo Sandbox at [localhost:3000/api/graphql](http://localhost:3000/api/graphql), which allows you to directly run GraphQL queries and mutations.

From this sandbox, you may listen for subscription updates `onTick` and `onTaskUpdated`, then try to update a task in the Admin UI and see new realtime update in Apollo!

## Features

This example is based on the `with-auth` one. The `keystone.ts` file has all the needed logic to make subscriptions working. The Task list has been updated to add a hook.
133 changes: 133 additions & 0 deletions examples/subscriptions/keystone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { config, graphQLSchemaExtension } from '@keystone-next/keystone';
import { statelessSessions } from '@keystone-next/keystone/session';
import { createAuth } from '@keystone-next/auth';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import { execute, subscribe } from 'graphql';
import { PubSub, withFilter } from 'graphql-subscriptions';
import { lists } from './schema';

// creating pubsub client, see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#the-pubsub-class
export const pubsub = new PubSub();

// defining subscription signatures
const typeDefs = `
type Subscription {
onTick: String!
taskUpdated(isComplete: Boolean! = false): Task!
}
`;

// creating resolvers
const resolvers = {
Subscription: {
// simple tick example
onTick: {
subscribe: () => pubsub.asyncIterator(['TICK']),
},
// complex Task update example, see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#filtering-events
taskUpdated: {
subscribe: withFilter(
() => pubsub.asyncIterator('TASK'),
(payload, variables) => {
// Only push an update if the comment is on
// the correct repository for this operation
return (payload.taskUpdated.isComplete === variables.isComplete);
},
),

}
},
};

// extending the schema to add subscriptions handlers
// please note the the Resolvers interface from Keystone has been modified to accept subscribe methods
const extendGraphqlSchema = graphQLSchemaExtension({ typeDefs, resolvers });

// the function that will be passed to keystone config to compute ApolloConfig object
// please note that the apolloConfig entry has been modified inside KS6 to accept a function instead of a plain object
// this function will take the generated KS schema and the HTTP Server (added to KS) as param
// and these params will be used to generate the Subsriptions server! According to https://www.apollographql.com/docs/apollo-server/data/subscriptions/#enabling-subscriptions
const getApolloConfig = (schema, httpServer) => {
const subscriptionServer = SubscriptionServer.create({
// This is the `schema` generated by KS.
schema,
// These are imported from `graphql`.
execute,
subscribe,
// Providing `onConnect` is the `SubscriptionServer` equivalent to the
// `context` function in `ApolloServer`. Please [see the docs](https://github.com/apollographql/subscriptions-transport-ws#constructoroptions-socketoptions--socketserver)
// for more information on this hook.
async onConnect(
connectionParams: { token: string },
// webSocket: WebSocket,
// context: any
) {
// suppose a token header is sent with the subscription request, we'll try to get the session of the user = logging in
// we could tweak the session functions to get avoid the workaround using any there
const item = await session.get({
req: { headers: { authorization: connectionParams.token } } as any
} as any);
if (item) {
// will set the context to subscribers
return item;
} else {
// you may return false or throw an exception to reject the WS connection
// https://www.apollographql.com/docs/apollo-server/data/subscriptions/#onconnect-and-ondisconnect
}
}
}, {
// This is the `httpServer` now created by KS.
server: httpServer,
// Same path as the GQL entry path
path: '/api/graphql',
});

// registering the plugin, see Apollo Server subscriptions doc
return {
plugins: [{
async serverWillStart() {
return {
async drainServer() {
subscriptionServer.close();
}
};
}
}],
};
};

// tick each second (simulating event)
setInterval(() => {
pubsub.publish('TICK', { onTick: new Date().toISOString() });
}, 1000);

// the rest bellow is regular KS with-auth example
const { withAuth } = createAuth({
listKey: 'Person',
identityField: 'email',
secretField: 'password',
initFirstItem: {
fields: ['name', 'email', 'password'],
},
});

const session = statelessSessions({
secret: '-- EXAMPLE COOKIE SECRET; CHANGE ME --',
});

export default withAuth(
config({
db: {
provider: 'sqlite',
url: process.env.DATABASE_URL || 'file:./keystone-example.db',
},
lists,
session,
// except these 4 lines bellow
extendGraphqlSchema,
graphql: {
apolloConfig: getApolloConfig
}
})
);

25 changes: 25 additions & 0 deletions examples/subscriptions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@keystone-next/example-subscriptions",
"version": "1.0.0",
"private": true,
"license": "MIT",
"scripts": {
"dev": "keystone-next dev",
"start": "keystone-next start",
"build": "keystone-next build"
},
"dependencies": {
"@keystone-next/auth": "file:./../../packages/auth",
"@keystone-next/keystone": "file:./../../packages/keystone",
"graphql": "^15.6.1",
"graphql-subscriptions": "^1.2.1",
"subscriptions-transport-ws": "^0.9.19"
},
"devDependencies": {
"typescript": "^4.4.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
},
"repository": "https://github.com/keystonejs/keystone/tree/main/examples/with-auth"
}
Loading