├── .babelrc.js ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── codecov.yml ├── package.json ├── renovate.json ├── src └── index.ts ├── test ├── subscriptionWithClientId.test.ts └── tsconfig.json ├── tsconfig.json └── yarn.lock /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@4c', 5 | { 6 | target: 'node', 7 | }, 8 | ], 9 | '@babel/preset-typescript', 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | !.babelrc.js 2 | !.eslintrc.js 3 | 4 | **/coverage/ 5 | **/lib/ 6 | **/es/ 7 | **/node_modules/ 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['4catalyzer', '4catalyzer-typescript', 'prettier'], 3 | plugins: ['prettier'], 4 | env: { 5 | node: true, 6 | }, 7 | rules: { 8 | 'prettier/prettier': 'error', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Transpiled code 40 | /lib 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | - lts/* 5 | 6 | cache: 7 | yarn: true 8 | npm: true 9 | 10 | after_script: 11 | - node_modules/.bin/codecov 12 | 13 | branches: 14 | only: 15 | - master 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Jimmy Jia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-relay-subscription [![Travis][build-badge]][build] [![npm][npm-badge]][npm] 2 | 3 | [Relay](http://facebook.github.io/relay/) subscription helper for [GraphQL.js](https://github.com/graphql/graphql-js). 4 | 5 | [![Codecov][codecov-badge]][codecov] 6 | [![Discord][discord-badge]][discord] 7 | 8 | ## Usage 9 | 10 | As with `mutationWithClientId` in graphql-relay-js, `subscriptionWithClientId` creates subscriptions with single inputs and client subscription IDs. 11 | 12 | ```js 13 | import { parse, subscribe } from 'graphql'; 14 | import { subscriptionWithClientId } from 'graphql-relay-subscription'; 15 | 16 | /* ... */ 17 | 18 | const UpdateWidgetSubscription = subscriptionWithClientId({ 19 | name: 'UpdateWidgetSubscription', 20 | inputFields: { 21 | widgetId: { type: GraphQLString }, 22 | }, 23 | outputFields: { 24 | widget: Widget, 25 | }, 26 | subscribe: ({ widgetId }) => 27 | createSubscription(`widgets:${widgetId}:updated`), 28 | }); 29 | 30 | const subscription = await subscribe( 31 | schema, 32 | parse(` 33 | subscription ($input_0: UpdateWidgetSubscriptionInput!) { 34 | updateWidget(input: $input_0) { 35 | widget { 36 | name 37 | } 38 | clientSubscriptionId 39 | } 40 | } 41 | `), 42 | null, 43 | null, 44 | { 45 | input_0: { 46 | widgetId: 'foo', 47 | clientSubscriptionId: '0', 48 | }, 49 | }, 50 | ); 51 | ``` 52 | 53 | [build-badge]: https://img.shields.io/travis/taion/graphql-relay-subscription/master.svg 54 | [build]: https://travis-ci.org/taion/graphql-relay-subscription 55 | [npm-badge]: https://img.shields.io/npm/v/graphql-relay-subscription.svg 56 | [npm]: https://www.npmjs.org/package/graphql-relay-subscription 57 | [codecov-badge]: https://img.shields.io/codecov/c/github/taion/graphql-relay-subscription/master.svg 58 | [codecov]: https://codecov.io/gh/taion/graphql-relay-subscription 59 | [discord-badge]: https://img.shields.io/badge/Discord-join%20chat%20%E2%86%92-738bd7.svg 60 | [discord]: https://discord.gg/0ZcbPKXt5bX40xsQ 61 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-relay-subscription", 3 | "version": "1.0.0", 4 | "description": "Relay subscription helpers for GraphQL.js", 5 | "keywords": [ 6 | "graphql", 7 | "relay", 8 | "subscriptions" 9 | ], 10 | "homepage": "https://github.com/taion/graphql-relay-subscription#readme", 11 | "bugs": { 12 | "url": "https://github.com/taion/graphql-relay-subscription/issues" 13 | }, 14 | "license": "MIT", 15 | "author": "Jimmy Jia", 16 | "files": [ 17 | "lib" 18 | ], 19 | "main": "lib/index.js", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/taion/graphql-relay-subscription.git" 23 | }, 24 | "scripts": { 25 | "build": "4c build src", 26 | "format": "4c format --prettier-ignore .eslintignore .", 27 | "lint": "4c lint --prettier-ignore .eslintignore .", 28 | "prepublish": "npm run build", 29 | "tdd": "jest --watch", 30 | "test": "npm run lint && npm run typecheck && npm run testonly -- --coverage", 31 | "testonly": "jest --runInBand --verbose", 32 | "typecheck": "tsc --noEmit && tsc --noEmit -p test" 33 | }, 34 | "husky": { 35 | "hooks": { 36 | "pre-commit": "lint-staged" 37 | } 38 | }, 39 | "lint-staged": { 40 | "*": "yarn 4c lint --fix --prettier-ignore .eslintignore" 41 | }, 42 | "prettier": "@4c/prettier-config", 43 | "jest": { 44 | "preset": "@4c/jest-preset" 45 | }, 46 | "devDependencies": { 47 | "@4c/babel-preset": "^7.4.1", 48 | "@4c/cli": "^2.2.0", 49 | "@4c/jest-preset": "^1.5.4", 50 | "@4c/prettier-config": "^1.1.0", 51 | "@4c/tsconfig": "^0.3.1", 52 | "@babel/core": "^7.12.13", 53 | "@babel/preset-typescript": "^7.12.13", 54 | "@types/jest": "^26.0.20", 55 | "@typescript-eslint/eslint-plugin": "^4.14.2", 56 | "@typescript-eslint/parser": "^4.14.2", 57 | "babel-jest": "^26.6.3", 58 | "codecov": "^3.8.1", 59 | "eslint-config-4catalyzer": "^1.1.5", 60 | "eslint-config-4catalyzer-jest": "^2.0.10", 61 | "eslint-config-4catalyzer-typescript": "^2.0.4", 62 | "eslint-config-prettier": "^6.15.0", 63 | "eslint-plugin-import": "^2.22.1", 64 | "eslint-plugin-jest": "^23.20.0", 65 | "eslint-plugin-prettier": "^3.3.1", 66 | "graphql": "^16.2.0", 67 | "husky": "^4.3.8", 68 | "jest": "^26.6.3", 69 | "lint-staged": "^10.5.4", 70 | "prettier": "^2.2.1", 71 | "typescript": "^4.1.3" 72 | }, 73 | "peerDependencies": { 74 | "graphql": ">=16.0.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>4Catalyzer/renovate-config:library", ":automergeMinor"] 3 | } 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLInputFieldConfig, 3 | GraphQLInputObjectType, 4 | GraphQLNonNull, 5 | GraphQLObjectType, 6 | GraphQLString, 7 | resolveObjMapThunk, 8 | } from 'graphql'; 9 | import type { 10 | GraphQLFieldConfig, 11 | GraphQLResolveInfo, 12 | ThunkObjMap, 13 | } from 'graphql'; 14 | 15 | export interface InputArgs { 16 | input: TInput & { clientSubscriptionId?: string | null | undefined }; 17 | } 18 | 19 | export interface SubscriptionConfig 20 | extends Omit< 21 | GraphQLFieldConfig>, 22 | 'type' | 'args' | 'subscribe' | 'resolve' 23 | > { 24 | name: string; 25 | inputFields?: ThunkObjMap; 26 | outputFields?: ThunkObjMap>; 27 | subscribe?: ( 28 | input: TInput, 29 | context: TContext, 30 | info: GraphQLResolveInfo, 31 | ) => any; 32 | getPayload?: ( 33 | obj: TSource, 34 | input: TInput, 35 | context: TContext, 36 | info: GraphQLResolveInfo, 37 | ) => Promise | any; 38 | } 39 | 40 | function defaultGetPayload(obj: TSource) { 41 | return obj; 42 | } 43 | 44 | export function subscriptionWithClientId< 45 | TSource = any, 46 | TContext = any, 47 | TInput = { [inputName: string]: any } 48 | >({ 49 | name, 50 | inputFields, 51 | outputFields, 52 | subscribe, 53 | getPayload = defaultGetPayload, 54 | ...config 55 | }: SubscriptionConfig): GraphQLFieldConfig< 56 | TSource, 57 | TContext, 58 | InputArgs 59 | > { 60 | const inputType = new GraphQLInputObjectType({ 61 | name: `${name}Input`, 62 | fields: () => ({ 63 | ...resolveObjMapThunk(inputFields || {}), 64 | clientSubscriptionId: { type: GraphQLString }, 65 | }), 66 | }); 67 | 68 | const outputType = new GraphQLObjectType({ 69 | name: `${name}Payload`, 70 | fields: () => ({ 71 | ...resolveObjMapThunk(outputFields || {}), 72 | clientSubscriptionId: { type: GraphQLString }, 73 | }), 74 | }); 75 | 76 | return { 77 | ...config, 78 | type: outputType, 79 | args: { 80 | input: { type: new GraphQLNonNull(inputType) }, 81 | }, 82 | subscribe: 83 | subscribe && 84 | (( 85 | _obj: TSource, 86 | { input }: InputArgs, 87 | context: TContext, 88 | info: GraphQLResolveInfo, 89 | ) => subscribe(input, context, info)), 90 | resolve: (obj, { input }, context, info) => 91 | Promise.resolve(getPayload(obj, input, context, info)).then( 92 | (payload) => ({ 93 | ...payload, 94 | clientSubscriptionId: input.clientSubscriptionId, 95 | }), 96 | ), 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /test/subscriptionWithClientId.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExecutionResult, 3 | GraphQLObjectType, 4 | GraphQLSchema, 5 | GraphQLString, 6 | parse, 7 | subscribe, 8 | } from 'graphql'; 9 | 10 | import { subscriptionWithClientId } from '../src'; 11 | 12 | describe('default resolution', () => { 13 | let schema: GraphQLSchema; 14 | 15 | beforeEach(() => { 16 | schema = new GraphQLSchema({ 17 | query: new GraphQLObjectType({ 18 | name: 'Query', 19 | fields: { 20 | dummy: { type: GraphQLString }, 21 | }, 22 | }), 23 | subscription: new GraphQLObjectType({ 24 | name: 'Subscription', 25 | fields: { 26 | foo: subscriptionWithClientId({ 27 | name: 'FooSubscription', 28 | outputFields: { 29 | value: { type: GraphQLString }, 30 | }, 31 | async *subscribe() { 32 | yield { value: 'bar' }; 33 | }, 34 | }), 35 | bar: subscriptionWithClientId({ 36 | name: 'BarSubscription', 37 | outputFields: { 38 | value: { type: GraphQLString }, 39 | }, 40 | }), 41 | }, 42 | }), 43 | }); 44 | }); 45 | 46 | it('should subscribe', async () => { 47 | const subscription = await subscribe( 48 | schema, 49 | parse(` 50 | subscription { 51 | foo(input: {}) { 52 | value 53 | } 54 | } 55 | `), 56 | ); 57 | 58 | expect( 59 | await (subscription as AsyncIterableIterator).next(), 60 | ).toEqual({ 61 | value: { 62 | data: { 63 | foo: { 64 | value: 'bar', 65 | }, 66 | }, 67 | }, 68 | done: false, 69 | }); 70 | 71 | expect( 72 | await (subscription as AsyncIterableIterator).next(), 73 | ).toEqual({ 74 | done: true, 75 | }); 76 | }); 77 | 78 | it('should default-subscribe', async () => { 79 | async function* generator() { 80 | yield { value: 'foo' }; 81 | } 82 | 83 | const subscription = await subscribe( 84 | schema, 85 | parse(` 86 | subscription { 87 | bar(input: {}) { 88 | value 89 | } 90 | } 91 | `), 92 | { 93 | bar: generator(), 94 | }, 95 | ); 96 | 97 | expect( 98 | await (subscription as AsyncIterableIterator).next(), 99 | ).toEqual({ 100 | value: { 101 | data: { 102 | bar: { 103 | value: 'foo', 104 | }, 105 | }, 106 | }, 107 | done: false, 108 | }); 109 | 110 | expect( 111 | await (subscription as AsyncIterableIterator).next(), 112 | ).toEqual({ 113 | done: true, 114 | }); 115 | }); 116 | }); 117 | 118 | describe('custom resolution', () => { 119 | let schema: GraphQLSchema; 120 | 121 | beforeEach(() => { 122 | schema = new GraphQLSchema({ 123 | query: new GraphQLObjectType({ 124 | name: 'Query', 125 | fields: { 126 | dummy: { type: GraphQLString }, 127 | }, 128 | }), 129 | subscription: new GraphQLObjectType({ 130 | name: 'Subscription', 131 | fields: { 132 | foo: subscriptionWithClientId({ 133 | name: 'FooSubscription', 134 | inputFields: () => ({ 135 | arg: { type: GraphQLString }, 136 | }), 137 | outputFields: () => ({ 138 | value: { type: GraphQLString }, 139 | arg: { type: GraphQLString }, 140 | }), 141 | async *subscribe({ arg }) { 142 | yield { value: `subscribed:${arg}` }; 143 | yield { value: 'bar' }; 144 | }, 145 | // eslint-disable-next-line require-await 146 | getPayload: async ({ value }, { arg }) => ({ value, arg }), 147 | }), 148 | }, 149 | }), 150 | }); 151 | }); 152 | 153 | it('should subscribe and get payload', async () => { 154 | const subscription = await subscribe( 155 | schema, 156 | parse(` 157 | subscription ($input: FooSubscriptionInput!) { 158 | foo(input: $input) { 159 | value 160 | arg 161 | clientSubscriptionId 162 | } 163 | } 164 | `), 165 | null, 166 | null, 167 | { 168 | input: { 169 | arg: 'foo', 170 | clientSubscriptionId: '3', 171 | }, 172 | }, 173 | ); 174 | 175 | expect( 176 | await (subscription as AsyncIterableIterator).next(), 177 | ).toEqual({ 178 | value: { 179 | data: { 180 | foo: { 181 | value: 'subscribed:foo', 182 | arg: 'foo', 183 | clientSubscriptionId: '3', 184 | }, 185 | }, 186 | }, 187 | done: false, 188 | }); 189 | 190 | expect( 191 | await (subscription as AsyncIterableIterator).next(), 192 | ).toEqual({ 193 | value: { 194 | data: { 195 | foo: { 196 | value: 'bar', 197 | arg: 'foo', 198 | clientSubscriptionId: '3', 199 | }, 200 | }, 201 | }, 202 | done: false, 203 | }); 204 | 205 | expect( 206 | await (subscription as AsyncIterableIterator).next(), 207 | ).toEqual({ 208 | done: true, 209 | }); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@4c/tsconfig/node", 3 | "compilerOptions": { 4 | "types": ["jest"] 5 | }, 6 | "include": [".", "../src"] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@4c/tsconfig/node", 3 | "compilerOptions": { 4 | "rootDir": "src" 5 | }, 6 | "include": ["src"] 7 | } 8 | --------------------------------------------------------------------------------