Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

(execute) - Subscription support (#2061)

* add support for subscriptions in execute exchange

authored by

Gustav Tiger and committed by
GitHub
25584258 c2b9eb5a

+99 -29
+5
.changeset/hungry-monkeys-fry.md
··· 1 + --- 2 + '@urql/exchange-execute': minor 3 + --- 4 + 5 + Add subscription support
+67 -19
exchanges/execute/src/execute.test.ts
··· 6 6 ...graphql, 7 7 print: jest.fn(() => '{ placeholder }'), 8 8 execute: jest.fn(() => ({ key: 'value' })), 9 + subscribe: jest.fn(), 9 10 }; 10 11 }); 11 12 12 13 import { fetchExchange } from 'urql'; 13 14 import { executeExchange } from './execute'; 14 - import { execute, print } from 'graphql'; 15 + import { execute, print, subscribe } from 'graphql'; 15 16 import { 16 17 pipe, 17 18 fromValue, ··· 21 22 empty, 22 23 Source, 23 24 } from 'wonka'; 24 - import { queryOperation } from '@urql/core/test-utils'; 25 + import { queryOperation, subscriptionOperation } from '@urql/core/test-utils'; 25 26 import { 26 27 makeErrorResult, 27 28 makeOperation, ··· 38 39 client: {}, 39 40 } as any; 40 41 41 - const expectedOperationName = getOperationName(queryOperation.query); 42 + const expectedQueryOperationName = getOperationName(queryOperation.query); 43 + const expectedSubscribeOperationName = getOperationName( 44 + subscriptionOperation.query 45 + ); 42 46 43 47 const fetchMock = (global as any).fetch as jest.Mock; 44 48 const mockHttpResponseData = { key: 'value' }; ··· 47 51 jest.clearAllMocks(); 48 52 mocked(print).mockImplementation(a => a as any); 49 53 mocked(execute).mockResolvedValue({ data: mockHttpResponseData }); 54 + mocked(subscribe).mockImplementation(async function* x(this: any) { 55 + yield { data: { key: 'value1' } }; 56 + yield { data: { key: 'value2' } }; 57 + yield { data: { key: 'value3' } }; 58 + }); 50 59 }); 51 60 52 61 afterEach(() => { ··· 65 74 ); 66 75 67 76 expect(mocked(execute)).toBeCalledTimes(1); 68 - expect(mocked(execute)).toBeCalledWith( 77 + expect(mocked(execute)).toBeCalledWith({ 69 78 schema, 70 - queryOperation.query, 71 - undefined, 72 - context, 73 - queryOperation.variables, 74 - expectedOperationName, 75 - undefined, 76 - undefined 79 + document: queryOperation.query, 80 + rootValue: undefined, 81 + contextValue: context, 82 + variableValues: queryOperation.variables, 83 + operationName: expectedQueryOperationName, 84 + fieldResolver: undefined, 85 + typeResolver: undefined, 86 + subscribeFieldResolver: undefined, 87 + }); 88 + }); 89 + 90 + it('calls subscribe with args', async () => { 91 + const context = 'USER_ID=123'; 92 + 93 + await pipe( 94 + fromValue(subscriptionOperation), 95 + executeExchange({ schema, context })(exchangeArgs), 96 + take(3), 97 + toPromise 77 98 ); 99 + 100 + expect(mocked(subscribe)).toBeCalledTimes(1); 101 + expect(mocked(subscribe)).toBeCalledWith({ 102 + schema, 103 + document: subscriptionOperation.query, 104 + rootValue: undefined, 105 + contextValue: context, 106 + variableValues: subscriptionOperation.variables, 107 + operationName: expectedSubscribeOperationName, 108 + fieldResolver: undefined, 109 + typeResolver: undefined, 110 + subscribeFieldResolver: undefined, 111 + }); 78 112 }); 79 113 80 114 it('calls execute after executing context as a function', async () => { ··· 91 125 ); 92 126 93 127 expect(mocked(execute)).toBeCalledTimes(1); 94 - expect(mocked(execute)).toBeCalledWith( 128 + expect(mocked(execute)).toBeCalledWith({ 95 129 schema, 96 - queryOperation.query, 97 - undefined, 98 - 'CALCULATED_USER_ID=80', 99 - queryOperation.variables, 100 - expectedOperationName, 101 - undefined, 102 - undefined 130 + document: queryOperation.query, 131 + rootValue: undefined, 132 + contextValue: 'CALCULATED_USER_ID=80', 133 + variableValues: queryOperation.variables, 134 + operationName: expectedQueryOperationName, 135 + fieldResolver: undefined, 136 + typeResolver: undefined, 137 + subscribeFieldResolver: undefined, 138 + }); 139 + }); 140 + 141 + it('should return data from subscribe', async () => { 142 + const context = 'USER_ID=123'; 143 + 144 + const responseFromExecuteExchange = await pipe( 145 + fromValue(subscriptionOperation), 146 + executeExchange({ schema, context })(exchangeArgs), 147 + take(3), 148 + toPromise 103 149 ); 150 + 151 + expect(responseFromExecuteExchange.data).toEqual({ key: 'value3' }); 104 152 }); 105 153 106 154 it('should return the same data as the fetch exchange', async () => {
+26 -10
exchanges/execute/src/execute.ts
··· 14 14 GraphQLFieldResolver, 15 15 GraphQLTypeResolver, 16 16 execute, 17 + subscribe, 18 + ExecutionArgs, 19 + SubscriptionArgs, 17 20 } from 'graphql'; 18 21 19 22 import { ··· 33 36 context?: ((op: Operation) => void) | any; 34 37 fieldResolver?: GraphQLFieldResolver<any, any>; 35 38 typeResolver?: GraphQLTypeResolver<any, any>; 39 + subscribeFieldResolver?: GraphQLFieldResolver<any, any>; 36 40 } 37 41 38 - type ExecuteParams = Parameters<typeof execute>; 42 + type ExecuteParams = ExecutionArgs | SubscriptionArgs; 39 43 40 44 const asyncIterator = 41 45 typeof Symbol !== 'undefined' ? Symbol.asyncIterator : null; ··· 50 54 Promise.resolve() 51 55 .then(() => { 52 56 if (ended) return; 53 - return execute(...args) as any; 57 + if (operation.kind === 'subscription') { 58 + return subscribe(args) as any; 59 + } 60 + return execute(args) as any; 54 61 }) 55 62 .then((result: ExecutionResult | AsyncIterable<ExecutionResult>) => { 56 63 if (ended || !result) { ··· 82 89 83 90 if (!done && !ended) { 84 91 return iterator.next().then(next); 92 + } 93 + if (ended) { 94 + iterator.return && iterator.return(); 85 95 } 86 96 } 87 97 ··· 108 118 context, 109 119 fieldResolver, 110 120 typeResolver, 121 + subscribeFieldResolver, 111 122 }: ExecuteExchangeArgs): Exchange => ({ forward }) => { 112 123 return ops$ => { 113 124 const sharedOps$ = share(ops$); ··· 115 126 const executedOps$ = pipe( 116 127 sharedOps$, 117 128 filter((operation: Operation) => { 118 - return operation.kind === 'query' || operation.kind === 'mutation'; 129 + return ( 130 + operation.kind === 'query' || 131 + operation.kind === 'mutation' || 132 + operation.kind === 'subscription' 133 + ); 119 134 }), 120 135 mergeMap((operation: Operation) => { 121 136 const { key } = operation; ··· 124 139 filter(op => op.kind === 'teardown' && op.key === key) 125 140 ); 126 141 127 - const calculatedContext = 142 + const contextValue = 128 143 typeof context === 'function' ? context(operation) : context; 129 144 return pipe( 130 - makeExecuteSource(operation, [ 145 + makeExecuteSource(operation, { 131 146 schema, 132 - operation.query, 147 + document: operation.query, 133 148 rootValue, 134 - calculatedContext, 135 - operation.variables, 136 - getOperationName(operation.query), 149 + contextValue, 150 + variableValues: operation.variables, 151 + operationName: getOperationName(operation.query), 137 152 fieldResolver, 138 153 typeResolver, 139 - ]), 154 + subscribeFieldResolver, 155 + }), 140 156 takeUntil(teardown$) 141 157 ); 142 158 })
+1
scripts/eslint/common.js
··· 60 60 '*.spec.tsx', 61 61 ], 62 62 rules: { 63 + 'es5/no-generators': 'off', 63 64 'es5/no-es6-methods': 'off', 64 65 'es5/no-es6-static-methods': 'off', 65 66