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

feat(core): Support Apollo Federation subscriptions multipart payloads (#3499)

authored by

Jovi De Croock and committed by
GitHub
e8e16bba f06ef349

+75 -4
+5
.changeset/small-cheetahs-jam.md
··· 1 + --- 2 + '@urql/core': minor 3 + --- 4 + 5 + Support [Apollo Federation's format](https://www.apollographql.com/docs/router/executing-operations/subscription-multipart-protocol/) for subscription results in `multipart/mixed` responses (result properties essentially are namespaced on a `payload` key)
+1
packages/core/src/types.ts
··· 223 223 * @see {@link https://github.com/graphql/graphql-wg/blob/main/rfcs/DeferStream.md#payload-format} for the DeferStream spec 224 224 */ 225 225 hasNext?: boolean; 226 + payload?: Omit<ExecutionResult, 'payload'>; 226 227 } 227 228 228 229 /** A source of {@link OperationResult | OperationResults}, convertable to a promise, subscribable, or Wonka Source.
+58
packages/core/src/utils/result.test.ts
··· 2 2 import { OperationResult } from '../types'; 3 3 import { queryOperation, subscriptionOperation } from '../test-utils'; 4 4 import { makeResult, mergeResultPatch } from './result'; 5 + import { GraphQLError } from '@0no-co/graphql.web'; 6 + import { CombinedError } from './error'; 5 7 6 8 describe('makeResult', () => { 7 9 it('adds extensions and errors correctly', () => { ··· 177 179 178 180 expect(merged.data).not.toBe(prevResult.data); 179 181 expect(merged.data.event).toBe(2); 182 + expect(merged.hasNext).toBe(true); 183 + }); 184 + 185 + it('should work with the payload property', () => { 186 + const prevResult: OperationResult = { 187 + operation: subscriptionOperation, 188 + data: { 189 + __typename: 'Subscription', 190 + event: 1, 191 + }, 192 + stale: false, 193 + hasNext: true, 194 + }; 195 + 196 + const merged = mergeResultPatch(prevResult, { 197 + payload: { 198 + data: { 199 + __typename: 'Subscription', 200 + event: 2, 201 + }, 202 + }, 203 + }); 204 + 205 + expect(merged.data).not.toBe(prevResult.data); 206 + expect(merged.data.event).toBe(2); 207 + expect(merged.hasNext).toBe(true); 208 + }); 209 + 210 + it('should work with the payload property and errors', () => { 211 + const prevResult: OperationResult = { 212 + operation: subscriptionOperation, 213 + data: { 214 + __typename: 'Subscription', 215 + event: 1, 216 + }, 217 + stale: false, 218 + hasNext: true, 219 + }; 220 + 221 + const merged = mergeResultPatch(prevResult, { 222 + payload: { 223 + data: { 224 + __typename: 'Subscription', 225 + event: 2, 226 + }, 227 + }, 228 + errors: [new GraphQLError('Something went horribly wrong')], 229 + }); 230 + 231 + expect(merged.data).not.toBe(prevResult.data); 232 + expect(merged.data.event).toBe(2); 233 + expect(merged.error).toEqual( 234 + new CombinedError({ 235 + graphQLErrors: [new GraphQLError('Something went horribly wrong')], 236 + }) 237 + ); 180 238 expect(merged.hasNext).toBe(true); 181 239 }); 182 240
+11 -4
packages/core/src/utils/result.ts
··· 92 92 pending?: ExecutionResult['pending'] 93 93 ): OperationResult => { 94 94 let errors = prevResult.error ? prevResult.error.graphQLErrors : []; 95 - let hasExtensions = !!prevResult.extensions || !!nextResult.extensions; 96 - const extensions = { ...prevResult.extensions, ...nextResult.extensions }; 95 + let hasExtensions = 96 + !!prevResult.extensions || !!(nextResult.payload || nextResult).extensions; 97 + const extensions = { 98 + ...prevResult.extensions, 99 + ...(nextResult.payload || nextResult).extensions, 100 + }; 97 101 98 102 let incremental = nextResult.incremental; 99 103 ··· 146 150 } 147 151 } 148 152 } else { 149 - withData.data = nextResult.data || prevResult.data; 150 - errors = (nextResult.errors as any[]) || errors; 153 + withData.data = (nextResult.payload || nextResult).data || prevResult.data; 154 + errors = 155 + (nextResult.errors as any[]) || 156 + (nextResult.payload && nextResult.payload.errors) || 157 + errors; 151 158 } 152 159 153 160 return {