├── .gitignore ├── .graphqlconfig.yml ├── README.md ├── database ├── datamodel.graphql └── prisma.yml ├── package.json ├── src ├── generated │ └── prisma.graphql ├── index.js └── schema.graphql └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .env* 2 | dist 3 | package-lock.json 4 | node_modules 5 | .idea 6 | .graphcoolrc 7 | .vscode 8 | *.log -------------------------------------------------------------------------------- /.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | app: 3 | schemaPath: src/schema.graphql 4 | extensions: 5 | endpoints: 6 | default: http://localhost:4000 7 | database: 8 | schemaPath: src/generated/prisma.graphql 9 | extensions: 10 | prisma: database/prisma.yml 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Subscriptions 2 | 3 | This repository contains the code for the [GraphQL subscriptions tutorial](https://medium.com/@graphcool/tutorial-building-a-realtime-graphql-server-with-subscriptions-2758cfc6d427) on the Graphcool blog. 4 | 5 | ## Get started 6 | 7 | ### 1. Download the example & Install dependencies 8 | 9 | Clone the repository: 10 | 11 | ```sh 12 | git clone git@github.com:nikolasburk/subscriptions.git 13 | ``` 14 | 15 | Next, navigate into the downloaded folder and install the NPM dependencies: 16 | 17 | ```sh 18 | cd subscriptions 19 | yarn install 20 | ``` 21 | 22 | ### 2. Deploy the Prisma database API 23 | 24 | You can now [deploy](https://www.prismagraphql.com/docs/reference/cli-command-reference/database-service/prisma-deploy-kee1iedaov) the Prisma database API: 25 | 26 | ```sh 27 | yarn prisma deploy 28 | ``` 29 | 30 | When prompted by the CLI how you want to deploy your Prisma API, select either of the Prisma Sandbox options: `sandbox-eu1` or `sandbox-us1`. Then provide a _name_ for your API and the _stage_ or simply hit **Enter** to select the suggested values. 31 | 32 | > **Note**: `prisma` is listed as a _development dependency_ and _script_ in this project's [`package.json`](./package.json). This means you can invoke the Prisma CLI without having it globally installed on your machine (by prefixing it with `yarn`), e.g. `yarn prisma deploy` or `yarn prisma playground`. If you have the Prisma CLI installed globally (which you can do with `npm install -g prisma`), you can omit the `yarn` prefix. 33 | 34 | ### 3. Set the Prisma service endpoint 35 | 36 | From the output of the previous command, copy the `HTTP` endpoint and paste it into `src/index.js` where it's used to instantiate the `Prisma` binding. You need to replace the current placeholder `__PRISMA_ENDPOINT__`: 37 | 38 | ```js 39 | const server = new GraphQLServer({ 40 | typeDefs: './src/schema.graphql', 41 | resolvers, 42 | context: req => ({ 43 | ...req, 44 | db: new Prisma({ 45 | typeDefs: 'src/generated/prisma.graphql', 46 | endpoint: "__PRISMA_ENDPOINT__", 47 | secret: 'mysecret123', 48 | }), 49 | }), 50 | }) 51 | ``` 52 | 53 | For example: 54 | 55 | ```js 56 | const server = new GraphQLServer({ 57 | typeDefs: './src/schema.graphql', 58 | resolvers, 59 | context: req => ({ 60 | ...req, 61 | db: new Prisma({ 62 | typeDefs: 'src/generated/prisma.graphql', 63 | endpoint: "https://eu1.prisma.sh/public-hillcloak-flier-942261/hackernews-graphql-js/dev", 64 | secret: 'mysecret123', 65 | }), 66 | }), 67 | }) 68 | ``` 69 | 70 | Note that the part `public-hillcloak-flier-952361` of the URL is unique to _your_ service. 71 | 72 | ### 4. Start the GraphQL server & Open a Playground 73 | 74 | You can now test the API with the following command: 75 | 76 | ```sh 77 | yarn dev 78 | ``` 79 | 80 | The server is now running on [http://localhost:4000](http://localhost:4000) and a [GraphQL Playground](https://github.com/graphcool/graphql-playground) opens automatically so you can start sending requests to your GraphQL API. 81 | 82 | ## Learn more 83 | 84 | To learn more about this example, check out the corresponding [tutorial](https://medium.com/@graphcool/tutorial-building-a-realtime-graphql-server-with-subscriptions-2758cfc6d427). 85 | -------------------------------------------------------------------------------- /database/datamodel.graphql: -------------------------------------------------------------------------------- 1 | type Post { 2 | id: ID! @unique 3 | title: String! 4 | } -------------------------------------------------------------------------------- /database/prisma.yml: -------------------------------------------------------------------------------- 1 | # Points to the file containing your data model. 2 | datamodel: datamodel.graphql 3 | 4 | # The secret is used to generate JWTs which allow to authenticate 5 | # against your Prisma service. You can use the `prisma token` command from the CLI 6 | # to generate a JWT based on the secret. When using the `prisma-binding` package, 7 | # you don't need to generate the JWTs manually as the library is doing that for you. 8 | secret: mysecret123 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prisma-subscriptions", 3 | "scripts": { 4 | "start": "node src/index.js", 5 | "playground": "graphql playground", 6 | "dev": "npm-run-all --parallel start playground", 7 | "prisma": "prisma" 8 | }, 9 | "dependencies": { 10 | "graphql-yoga": "1.16.7", 11 | "prisma-binding": "2.1.6" 12 | }, 13 | "devDependencies": { 14 | "graphql-cli": "2.17.0", 15 | "npm-run-all": "4.1.3", 16 | "prisma": "1.20.6" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/generated/prisma.graphql: -------------------------------------------------------------------------------- 1 | # THIS FILE HAS BEEN AUTO-GENERATED BY "PRISMA DEPLOY" 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | 4 | # 5 | # Model Types 6 | # 7 | 8 | type Post implements Node { 9 | id: ID! 10 | title: String! 11 | } 12 | 13 | 14 | # 15 | # Other Types 16 | # 17 | 18 | type AggregatePost { 19 | count: Int! 20 | } 21 | 22 | type BatchPayload { 23 | """ 24 | The number of nodes that have been affected by the Batch operation. 25 | """ 26 | count: Long! 27 | } 28 | 29 | """ 30 | The `Long` scalar type represents non-fractional signed whole numeric values. 31 | Long can represent values between -(2^63) and 2^63 - 1. 32 | """ 33 | scalar Long 34 | 35 | type Mutation { 36 | createPost(data: PostCreateInput!): Post! 37 | updatePost(data: PostUpdateInput!, where: PostWhereUniqueInput!): Post 38 | deletePost(where: PostWhereUniqueInput!): Post 39 | upsertPost(where: PostWhereUniqueInput!, create: PostCreateInput!, update: PostUpdateInput!): Post! 40 | updateManyPosts(data: PostUpdateInput!, where: PostWhereInput!): BatchPayload! 41 | deleteManyPosts(where: PostWhereInput!): BatchPayload! 42 | } 43 | 44 | enum MutationType { 45 | CREATED 46 | UPDATED 47 | DELETED 48 | } 49 | 50 | """ 51 | An object with an ID 52 | """ 53 | interface Node { 54 | """ 55 | The id of the object. 56 | """ 57 | id: ID! 58 | } 59 | 60 | """ 61 | Information about pagination in a connection. 62 | """ 63 | type PageInfo { 64 | """ 65 | When paginating forwards, are there more items? 66 | """ 67 | hasNextPage: Boolean! 68 | """ 69 | When paginating backwards, are there more items? 70 | """ 71 | hasPreviousPage: Boolean! 72 | """ 73 | When paginating backwards, the cursor to continue. 74 | """ 75 | startCursor: String 76 | """ 77 | When paginating forwards, the cursor to continue. 78 | """ 79 | endCursor: String 80 | } 81 | 82 | """ 83 | A connection to a list of items. 84 | """ 85 | type PostConnection { 86 | """ 87 | Information to aid in pagination. 88 | """ 89 | pageInfo: PageInfo! 90 | """ 91 | A list of edges. 92 | """ 93 | edges: [PostEdge]! 94 | aggregate: AggregatePost! 95 | } 96 | 97 | input PostCreateInput { 98 | title: String! 99 | } 100 | 101 | """ 102 | An edge in a connection. 103 | """ 104 | type PostEdge { 105 | """ 106 | The item at the end of the edge. 107 | """ 108 | node: Post! 109 | """ 110 | A cursor for use in pagination. 111 | """ 112 | cursor: String! 113 | } 114 | 115 | enum PostOrderByInput { 116 | id_ASC 117 | id_DESC 118 | title_ASC 119 | title_DESC 120 | updatedAt_ASC 121 | updatedAt_DESC 122 | createdAt_ASC 123 | createdAt_DESC 124 | } 125 | 126 | type PostPreviousValues { 127 | id: ID! 128 | title: String! 129 | } 130 | 131 | type PostSubscriptionPayload { 132 | mutation: MutationType! 133 | node: Post 134 | updatedFields: [String!] 135 | previousValues: PostPreviousValues 136 | } 137 | 138 | input PostSubscriptionWhereInput { 139 | """ 140 | Logical AND on all given filters. 141 | """ 142 | AND: [PostSubscriptionWhereInput!] 143 | """ 144 | Logical OR on all given filters. 145 | """ 146 | OR: [PostSubscriptionWhereInput!] 147 | """ 148 | The subscription event gets dispatched when it's listed in mutation_in 149 | """ 150 | mutation_in: [MutationType!] 151 | """ 152 | The subscription event gets only dispatched when one of the updated fields names is included in this list 153 | """ 154 | updatedFields_contains: String 155 | """ 156 | The subscription event gets only dispatched when all of the field names included in this list have been updated 157 | """ 158 | updatedFields_contains_every: [String!] 159 | """ 160 | The subscription event gets only dispatched when some of the field names included in this list have been updated 161 | """ 162 | updatedFields_contains_some: [String!] 163 | node: PostWhereInput 164 | } 165 | 166 | input PostUpdateInput { 167 | title: String 168 | } 169 | 170 | input PostWhereInput { 171 | """ 172 | Logical AND on all given filters. 173 | """ 174 | AND: [PostWhereInput!] 175 | """ 176 | Logical OR on all given filters. 177 | """ 178 | OR: [PostWhereInput!] 179 | id: ID 180 | """ 181 | All values that are not equal to given value. 182 | """ 183 | id_not: ID 184 | """ 185 | All values that are contained in given list. 186 | """ 187 | id_in: [ID!] 188 | """ 189 | All values that are not contained in given list. 190 | """ 191 | id_not_in: [ID!] 192 | """ 193 | All values less than the given value. 194 | """ 195 | id_lt: ID 196 | """ 197 | All values less than or equal the given value. 198 | """ 199 | id_lte: ID 200 | """ 201 | All values greater than the given value. 202 | """ 203 | id_gt: ID 204 | """ 205 | All values greater than or equal the given value. 206 | """ 207 | id_gte: ID 208 | """ 209 | All values containing the given string. 210 | """ 211 | id_contains: ID 212 | """ 213 | All values not containing the given string. 214 | """ 215 | id_not_contains: ID 216 | """ 217 | All values starting with the given string. 218 | """ 219 | id_starts_with: ID 220 | """ 221 | All values not starting with the given string. 222 | """ 223 | id_not_starts_with: ID 224 | """ 225 | All values ending with the given string. 226 | """ 227 | id_ends_with: ID 228 | """ 229 | All values not ending with the given string. 230 | """ 231 | id_not_ends_with: ID 232 | title: String 233 | """ 234 | All values that are not equal to given value. 235 | """ 236 | title_not: String 237 | """ 238 | All values that are contained in given list. 239 | """ 240 | title_in: [String!] 241 | """ 242 | All values that are not contained in given list. 243 | """ 244 | title_not_in: [String!] 245 | """ 246 | All values less than the given value. 247 | """ 248 | title_lt: String 249 | """ 250 | All values less than or equal the given value. 251 | """ 252 | title_lte: String 253 | """ 254 | All values greater than the given value. 255 | """ 256 | title_gt: String 257 | """ 258 | All values greater than or equal the given value. 259 | """ 260 | title_gte: String 261 | """ 262 | All values containing the given string. 263 | """ 264 | title_contains: String 265 | """ 266 | All values not containing the given string. 267 | """ 268 | title_not_contains: String 269 | """ 270 | All values starting with the given string. 271 | """ 272 | title_starts_with: String 273 | """ 274 | All values not starting with the given string. 275 | """ 276 | title_not_starts_with: String 277 | """ 278 | All values ending with the given string. 279 | """ 280 | title_ends_with: String 281 | """ 282 | All values not ending with the given string. 283 | """ 284 | title_not_ends_with: String 285 | } 286 | 287 | input PostWhereUniqueInput { 288 | id: ID 289 | } 290 | 291 | type Query { 292 | posts(where: PostWhereInput, orderBy: PostOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Post]! 293 | post(where: PostWhereUniqueInput!): Post 294 | postsConnection(where: PostWhereInput, orderBy: PostOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): PostConnection! 295 | """ 296 | Fetches an object given its ID 297 | """ 298 | node(""" 299 | The ID of an object 300 | """ 301 | id: ID!): Node 302 | } 303 | 304 | type Subscription { 305 | post(where: PostSubscriptionWhereInput): PostSubscriptionPayload 306 | } 307 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { GraphQLServer } = require('graphql-yoga') 2 | const { Prisma } = require('prisma-binding') 3 | 4 | const resolvers = { 5 | Query: { 6 | feed(parent, args, ctx, info) { 7 | return ctx.db.query.posts({}, info) 8 | }, 9 | }, 10 | Mutation: { 11 | writePost(parent, { title }, ctx, info) { 12 | return ctx.db.mutation.createPost( 13 | { 14 | data: { 15 | title, 16 | }, 17 | }, 18 | info, 19 | ) 20 | }, 21 | updateTitle(parent, { id, newTitle }, ctx, info) { 22 | return ctx.db.mutation.updatePost( 23 | { 24 | where: { 25 | id, 26 | }, 27 | data: { 28 | title: newTitle, 29 | }, 30 | }, 31 | info, 32 | ) 33 | }, 34 | deletePost(parent, { id }, ctx, info) { 35 | return ctx.db.mutation.deletePost( 36 | { 37 | where: { 38 | id, 39 | }, 40 | }, 41 | info, 42 | ) 43 | }, 44 | }, 45 | Subscription: { 46 | publications: { 47 | subscribe: (parent, args, ctx, info) => { 48 | return ctx.db.subscription.post( 49 | { 50 | where: { 51 | mutation_in: ['CREATED', 'UPDATED'], 52 | }, 53 | }, 54 | info, 55 | ) 56 | }, 57 | }, 58 | postDeleted: { 59 | subscribe: (parent, args, ctx, info) => { 60 | const selectionSet = `{ previousValues { id title } }` 61 | return ctx.db.subscription.post( 62 | { 63 | where: { 64 | mutation_in: ['DELETED'], 65 | }, 66 | }, 67 | selectionSet, 68 | ) 69 | }, 70 | resolve: (payload, args, context, info) => { 71 | return payload ? payload.post.previousValues : payload // sanity check 72 | }, 73 | }, 74 | }, 75 | } 76 | 77 | const server = new GraphQLServer({ 78 | typeDefs: './src/schema.graphql', 79 | resolvers, 80 | context: req => ({ 81 | ...req, 82 | db: new Prisma({ 83 | typeDefs: 'src/generated/prisma.graphql', 84 | endpoint: '__PRISMA_ENDPOINT__', 85 | secret: 'mysecret123', 86 | debug: true, 87 | }), 88 | }), 89 | }) 90 | 91 | server.start(() => console.log(`Server is running on http://localhost:4000`)) 92 | -------------------------------------------------------------------------------- /src/schema.graphql: -------------------------------------------------------------------------------- 1 | # import Post, PostSubscriptionPayload from "./generated/prisma.graphql" 2 | 3 | type Query { 4 | feed: [Post!]! 5 | } 6 | 7 | type Mutation { 8 | writePost(title: String!): Post 9 | updateTitle(id: ID!, newTitle: String!): Post 10 | deletePost(id: ID): Post 11 | } 12 | 13 | type Subscription { 14 | publications: PostSubscriptionPayload 15 | postDeleted: Post 16 | } 17 | --------------------------------------------------------------------------------