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.

feat(core): Update supported "Incremental Delivery" format/spec (#3007)

authored by

Phil Pluckthun and committed by
GitHub
842cbaa5 63f26427

+490 -224
+5
.changeset/curly-avocados-buy.md
··· 1 + --- 2 + '@urql/core': minor 3 + --- 4 + 5 + Update support for the "Incremental Delivery" payload specification, accepting the new `incremental` property on execution results, as per the specification. This will expand support for newer APIs implementing the more up-to-date specification.
+5
.changeset/nine-dancers-film.md
··· 1 + --- 2 + '@urql/core': minor 3 + --- 4 + 5 + Update default `Accept` header to include `multipart/mixed` and `application/graphql-response+json`. The former seems to now be a defactor standard-accepted indication for support of the "Incremental Delivery" GraphQL over HTTP spec addition/RFC, and the latter is an updated form of the older `Content-Type` of GraphQL responses, so both the old and new one should now be included.
+15 -12
examples/with-defer-stream-directives/package.json
··· 3 3 "version": "0.0.0", 4 4 "private": true, 5 5 "scripts": { 6 - "start": "concurrently -k \"vite\" \"node server/index.js\"" 6 + "server:apollo": "node server/apollo-server.js", 7 + "server:yoga": "node server/graphql-yoga.js", 8 + "client": "vite", 9 + "start": "run-p client server:yoga" 7 10 }, 8 11 "dependencies": { 9 - "@urql/core": "^2.3.0", 10 - "@urql/exchange-graphcache": "^4.3.0", 11 - "graphql": "15.4.0-experimental-stream-defer.1", 12 + "@apollo/server": "^4.4.1", 13 + "@graphql-yoga/plugin-defer-stream": "^1.7.1", 14 + "@urql/core": "^3.1.1", 15 + "@urql/exchange-graphcache": "^5.0.9", 16 + "graphql": "17.0.0-alpha.2", 17 + "graphql-yoga": "^3.7.1", 12 18 "react": "^17.0.2", 13 19 "react-dom": "^17.0.2", 14 - "urql": "^2.0.2" 20 + "urql": "^3.0.3" 15 21 }, 16 22 "devDependencies": { 17 - "@polka/parse": "^1.0.0-next.0", 18 - "@vitejs/plugin-react-refresh": "^1.3.3", 19 - "concurrently": "^6.2.1", 20 - "cors": "^2.8.5", 21 - "graphql-helix": "^1.7.0", 22 - "polka": "^0.5.2", 23 - "vite": "^2.2.4" 23 + "@vitejs/plugin-react-refresh": "^1.3.6", 24 + "graphql-helix": "^1.13.0", 25 + "npm-run-all": "^4.1.5", 26 + "vite": "^2.9.15" 24 27 } 25 28 }
+14
examples/with-defer-stream-directives/server/apollo-server.js
··· 1 + // NOTE: This currently fails because responses for @defer/@stream are not sent 2 + // as multipart responses, but the request fails silently with an empty JSON response payload 3 + 4 + const { ApolloServer } = require('@apollo/server'); 5 + const { startStandaloneServer } = require('@apollo/server/standalone'); 6 + const { schema } = require('./schema'); 7 + 8 + const server = new ApolloServer({ schema }); 9 + 10 + startStandaloneServer(server, { 11 + listen: { 12 + port: 3004, 13 + }, 14 + });
+13
examples/with-defer-stream-directives/server/graphql-yoga.js
··· 1 + const { createYoga } = require('graphql-yoga'); 2 + const { useDeferStream } = require('@graphql-yoga/plugin-defer-stream'); 3 + const { createServer } = require('http'); 4 + const { schema } = require('./schema'); 5 + 6 + const yoga = createYoga({ 7 + schema, 8 + plugins: [useDeferStream()], 9 + }); 10 + 11 + const server = createServer(yoga); 12 + 13 + server.listen(3004);
-126
examples/with-defer-stream-directives/server/index.js
··· 1 - // Credits to https://github.com/maraisr/meros/blob/main/examples/relay-with-helix/server.js 2 - 3 - /* eslint-disable @typescript-eslint/no-var-requires, es5/no-generators, no-console */ 4 - const polka = require('polka'); 5 - const { json } = require('@polka/parse'); 6 - const cors = require('cors')(); 7 - const { getGraphQLParameters, processRequest } = require('graphql-helix'); 8 - const { 9 - GraphQLList, 10 - GraphQLObjectType, 11 - GraphQLSchema, 12 - GraphQLString, 13 - } = require('graphql'); 14 - 15 - const schema = new GraphQLSchema({ 16 - query: new GraphQLObjectType({ 17 - name: 'Query', 18 - fields: () => ({ 19 - alphabet: { 20 - type: new GraphQLList( 21 - new GraphQLObjectType({ 22 - name: 'Alphabet', 23 - fields: { 24 - char: { 25 - type: GraphQLString, 26 - }, 27 - }, 28 - }) 29 - ), 30 - resolve: async function* () { 31 - for (let letter = 65; letter <= 90; letter++) { 32 - await new Promise(resolve => setTimeout(resolve, 500)); 33 - yield { char: String.fromCharCode(letter) }; 34 - } 35 - }, 36 - }, 37 - song: { 38 - type: new GraphQLObjectType({ 39 - name: 'Song', 40 - fields: () => ({ 41 - firstVerse: { 42 - type: GraphQLString, 43 - resolve: () => "Now I know my ABC's.", 44 - }, 45 - secondVerse: { 46 - type: GraphQLString, 47 - resolve: () => 48 - new Promise(resolve => 49 - setTimeout( 50 - () => resolve("Next time won't you sing with me?"), 51 - 5000 52 - ) 53 - ), 54 - }, 55 - }), 56 - }), 57 - resolve: () => 58 - new Promise(resolve => setTimeout(() => resolve('goodbye'), 1000)), 59 - }, 60 - }), 61 - }), 62 - }); 63 - 64 - polka() 65 - .use(cors, json()) 66 - .use('/graphql', async (req, res) => { 67 - const request = { 68 - body: req.body, 69 - headers: req.headers, 70 - method: req.method, 71 - query: req.query, 72 - }; 73 - 74 - let { operationName, query, variables } = getGraphQLParameters(request); 75 - 76 - const result = await processRequest({ 77 - operationName, 78 - query, 79 - variables, 80 - request, 81 - schema, 82 - }); 83 - 84 - if (result.type === 'RESPONSE') { 85 - result.headers.forEach(({ name, value }) => res.setHeader(name, value)); 86 - res.writeHead(result.status, { 87 - 'Content-Type': 'application/json', 88 - }); 89 - res.end(JSON.stringify(result.payload)); 90 - } else if (result.type === 'MULTIPART_RESPONSE') { 91 - res.writeHead(200, { 92 - Connection: 'keep-alive', 93 - 'Content-Type': 'multipart/mixed; boundary="-"', 94 - 'Transfer-Encoding': 'chunked', 95 - }); 96 - 97 - req.on('close', () => { 98 - result.unsubscribe(); 99 - }); 100 - 101 - res.write('---'); 102 - 103 - await result.subscribe(result => { 104 - const chunk = Buffer.from(JSON.stringify(result), 'utf8'); 105 - const data = [ 106 - '', 107 - 'Content-Type: application/json; charset=utf-8', 108 - '', 109 - chunk, 110 - ]; 111 - 112 - if (result.hasNext) { 113 - data.push('---'); 114 - } 115 - 116 - res.write(data.join('\r\n')); 117 - }); 118 - 119 - res.write('\r\n-----\r\n'); 120 - res.end(); 121 - } 122 - }) 123 - .listen(3004, err => { 124 - if (err) throw err; 125 - console.log(`> Running on localhost:3004`); 126 - });
+57
examples/with-defer-stream-directives/server/schema.js
··· 1 + const { 2 + GraphQLList, 3 + GraphQLObjectType, 4 + GraphQLSchema, 5 + GraphQLString, 6 + } = require('graphql'); 7 + 8 + const schema = new GraphQLSchema({ 9 + query: new GraphQLObjectType({ 10 + name: 'Query', 11 + fields: () => ({ 12 + alphabet: { 13 + type: new GraphQLList( 14 + new GraphQLObjectType({ 15 + name: 'Alphabet', 16 + fields: { 17 + char: { 18 + type: GraphQLString, 19 + }, 20 + }, 21 + }) 22 + ), 23 + resolve: async function* () { 24 + for (let letter = 65; letter <= 90; letter++) { 25 + await new Promise(resolve => setTimeout(resolve, 500)); 26 + yield { char: String.fromCharCode(letter) }; 27 + } 28 + }, 29 + }, 30 + song: { 31 + type: new GraphQLObjectType({ 32 + name: 'Song', 33 + fields: () => ({ 34 + firstVerse: { 35 + type: GraphQLString, 36 + resolve: () => "Now I know my ABC's.", 37 + }, 38 + secondVerse: { 39 + type: GraphQLString, 40 + resolve: () => 41 + new Promise(resolve => 42 + setTimeout( 43 + () => resolve("Next time won't you sing with me?"), 44 + 5000 45 + ) 46 + ), 47 + }, 48 + }), 49 + }), 50 + resolve: () => 51 + new Promise(resolve => setTimeout(() => resolve('goodbye'), 1000)), 52 + }, 53 + }), 54 + }), 55 + }); 56 + 57 + module.exports = { schema };
+1 -1
examples/with-defer-stream-directives/src/Songs.jsx
··· 13 13 firstVerse 14 14 ...secondVerseFields @defer 15 15 } 16 - alphabet @stream(initial_count: 3) { 16 + alphabet @stream(initialCount: 3) { 17 17 char 18 18 } 19 19 }
+3 -15
exchanges/multipart-fetch/src/__snapshots__/multipartFetchExchange.test.ts.snap
··· 1 - // Vitest Snapshot v1 1 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 2 3 3 exports[`on error > returns error data 1`] = ` 4 4 { ··· 590 590 } 591 591 `; 592 592 593 - exports[`on success > uses a file when given 2`] = ` 594 - { 595 - "accept": "application/graphql+json, application/json", 596 - } 597 - `; 598 - 599 - exports[`on success > uses a file when given 3`] = `FormData {}`; 593 + exports[`on success > uses a file when given 2`] = `FormData {}`; 600 594 601 595 exports[`on success > uses multiple files when given 1`] = ` 602 596 { ··· 737 731 } 738 732 `; 739 733 740 - exports[`on success > uses multiple files when given 2`] = ` 741 - { 742 - "accept": "application/graphql+json, application/json", 743 - } 744 - `; 745 - 746 - exports[`on success > uses multiple files when given 3`] = `FormData {}`; 734 + exports[`on success > uses multiple files when given 2`] = `FormData {}`;
-2
exchanges/multipart-fetch/src/multipartFetchExchange.test.ts
··· 82 82 83 83 expect(data).toMatchSnapshot(); 84 84 expect(fetchOptions).toHaveBeenCalled(); 85 - expect(fetch.mock.calls[0][1].headers).toMatchSnapshot(); 86 85 expect(fetch.mock.calls[0][1].body).toMatchSnapshot(); 87 86 }); 88 87 ··· 103 102 104 103 expect(data).toMatchSnapshot(); 105 104 expect(fetchOptions).toHaveBeenCalled(); 106 - expect(fetch.mock.calls[0][1].headers).toMatchSnapshot(); 107 105 expect(fetch.mock.calls[0][1].body).toMatchSnapshot(); 108 106 }); 109 107
+1 -1
packages/core/src/__snapshots__/client.test.ts.snap
··· 1 - // Vitest Snapshot v1 1 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 2 3 3 exports[`createClient / Client > passes snapshot 1`] = ` 4 4 Client2 {
+1 -1
packages/core/src/exchanges/__snapshots__/fetch.test.ts.snap
··· 1 - // Vitest Snapshot v1 1 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 2 3 3 exports[`on error > returns error data 1`] = ` 4 4 {
+1 -1
packages/core/src/exchanges/__snapshots__/subscription.test.ts.snap
··· 1 - // Vitest Snapshot v1 1 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 2 3 3 exports[`should return response data from forwardSubscription observable 1`] = ` 4 4 {
+1 -1
packages/core/src/internal/__snapshots__/fetchSource.test.ts.snap
··· 1 - // Vitest Snapshot v1 1 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 2 3 3 exports[`on error > ignores the error when a result is available 1`] = ` 4 4 {
+2 -1
packages/core/src/internal/fetchOptions.ts
··· 57 57 const useGETMethod = 58 58 operation.kind === 'query' && !!operation.context.preferGetMethod; 59 59 const headers: HeadersInit = { 60 - accept: 'application/graphql+json, application/json', 60 + accept: 61 + 'multipart/mixed, application/graphql-response+json, application/graphql+json, application/json', 61 62 }; 62 63 if (!useGETMethod) headers['content-type'] = 'application/json'; 63 64 const extraOptions =
+18 -6
packages/core/src/internal/fetchSource.test.ts
··· 236 236 done: false, 237 237 value: Buffer.from( 238 238 wrap({ 239 - path: ['author', 'todos', 1], 240 - data: { id: '2', text: 'defer', __typename: 'Todo' }, 239 + incremental: [ 240 + { 241 + path: ['author', 'todos', 1], 242 + data: { id: '2', text: 'defer', __typename: 'Todo' }, 243 + }, 244 + ], 241 245 hasNext: true, 242 246 }) 243 247 ), ··· 360 364 done: false, 361 365 value: Buffer.from( 362 366 wrap({ 363 - path: ['author'], 364 - data: { name: 'Steve' }, 367 + incremental: [ 368 + { 369 + path: ['author'], 370 + data: { name: 'Steve' }, 371 + }, 372 + ], 365 373 hasNext: true, 366 374 }) 367 375 ), ··· 483 491 done: false, 484 492 value: Buffer.from( 485 493 wrap({ 486 - path: ['author', 'address'], 487 - data: { street: 'home' }, 494 + incremental: [ 495 + { 496 + path: ['author', 'address'], 497 + data: { street: 'home' }, 498 + }, 499 + ], 488 500 hasNext: true, 489 501 }) 490 502 ),
+19 -17
packages/core/src/types.ts
··· 18 18 __apiType?: (variables: Variables) => Result; 19 19 } 20 20 21 - export type ExecutionResult = 22 - | { 23 - errors?: 24 - | Array<Partial<GraphQLError> | string | Error> 25 - | readonly GraphQLError[]; 26 - data?: null | Record<string, any>; 27 - extensions?: Record<string, any>; 28 - hasNext?: boolean; 29 - } 30 - | { 31 - errors?: 32 - | Array<Partial<GraphQLError> | string | Error> 33 - | readonly GraphQLError[]; 34 - data: any; 35 - path: (string | number)[]; 36 - hasNext?: boolean; 37 - }; 21 + type ErrorLike = Partial<GraphQLError> | Error; 22 + type Extensions = Record<string, any>; 23 + 24 + export interface IncrementalPayload { 25 + label?: string | null; 26 + path: readonly (string | number)[]; 27 + data?: Record<string, unknown> | null; 28 + items?: readonly unknown[] | null; 29 + errors?: ErrorLike[] | readonly ErrorLike[]; 30 + extensions?: Extensions; 31 + } 32 + 33 + export interface ExecutionResult { 34 + incremental?: IncrementalPayload[]; 35 + data?: null | Record<string, any>; 36 + errors?: ErrorLike[] | readonly ErrorLike[]; 37 + extensions?: Extensions; 38 + hasNext?: boolean; 39 + } 38 40 39 41 export type PromisifiedSource<T = any> = Source<T> & { 40 42 toPromise: () => Promise<T>;
+1 -1
packages/core/src/utils/__snapshots__/error.test.ts.snap
··· 1 - // Vitest Snapshot v1 1 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 2 3 3 exports[`CombinedError > behaves like a normal Error 1`] = `"[Network] test"`;
-7
packages/core/src/utils/error.test.ts
··· 60 60 expect(err.graphQLErrors).toEqual(graphQLErrors); 61 61 }); 62 62 63 - it('passes graphQLErrors through as a last resort', () => { 64 - const graphQLErrors = [{ x: 'y' }] as any; 65 - const err = new CombinedError({ graphQLErrors }); 66 - 67 - expect(err.graphQLErrors).toEqual(graphQLErrors); 68 - }); 69 - 70 63 it('accepts a response that is attached to the resulting error', () => { 71 64 const response = {}; 72 65 const err = new CombinedError({
+3 -3
packages/core/src/utils/error.ts
··· 16 16 }; 17 17 18 18 const rehydrateGraphQlError = (error: any): GraphQLError => { 19 - if (typeof error === 'string') { 20 - return new GraphQLError(error); 19 + if (error instanceof GraphQLError) { 20 + return error; 21 21 } else if (typeof error === 'object' && error.message) { 22 22 return new GraphQLError( 23 23 error.message, ··· 29 29 error.extensions || {} 30 30 ); 31 31 } else { 32 - return error as any; 32 + return new GraphQLError(error as any); 33 33 } 34 34 }; 35 35
+265 -1
packages/core/src/utils/result.test.ts
··· 1 1 import { describe, it, expect } from 'vitest'; 2 + import { OperationResult } from '../types'; 2 3 import { queryOperation } from '../test-utils'; 3 - import { makeResult } from './result'; 4 + import { makeResult, mergeResultPatch } from './result'; 4 5 5 6 describe('makeResult', () => { 6 7 it('adds extensions and errors correctly', () => { ··· 23 24 ); 24 25 }); 25 26 }); 27 + 28 + describe('mergeResultPatch', () => { 29 + it('should ignore invalid patches', () => { 30 + const prevResult: OperationResult = { 31 + operation: queryOperation, 32 + data: { 33 + __typename: 'Query', 34 + items: [ 35 + { 36 + __typename: 'Item', 37 + id: 'id', 38 + }, 39 + ], 40 + }, 41 + }; 42 + 43 + const merged = mergeResultPatch(prevResult, { 44 + incremental: [ 45 + { 46 + data: undefined, 47 + path: ['a'], 48 + }, 49 + { 50 + items: null, 51 + path: ['b'], 52 + }, 53 + ], 54 + }); 55 + 56 + expect(merged.data).toStrictEqual({ 57 + __typename: 'Query', 58 + items: [ 59 + { 60 + __typename: 'Item', 61 + id: 'id', 62 + }, 63 + ], 64 + }); 65 + }); 66 + 67 + it('should apply incremental defer patches', () => { 68 + const prevResult: OperationResult = { 69 + operation: queryOperation, 70 + data: { 71 + __typename: 'Query', 72 + items: [ 73 + { 74 + __typename: 'Item', 75 + id: 'id', 76 + child: undefined, 77 + }, 78 + ], 79 + }, 80 + }; 81 + 82 + const patch = { __typename: 'Child' }; 83 + 84 + const merged = mergeResultPatch(prevResult, { 85 + incremental: [ 86 + { 87 + data: patch, 88 + path: ['items', 0, 'child'], 89 + }, 90 + ], 91 + }); 92 + 93 + expect(merged.data.items[0]).not.toBe(prevResult.data.items[0]); 94 + expect(merged.data.items[0].child).toBe(patch); 95 + expect(merged.data).toStrictEqual({ 96 + __typename: 'Query', 97 + items: [ 98 + { 99 + __typename: 'Item', 100 + id: 'id', 101 + child: patch, 102 + }, 103 + ], 104 + }); 105 + }); 106 + 107 + it('should handle null incremental defer patches', () => { 108 + const prevResult: OperationResult = { 109 + operation: queryOperation, 110 + data: { 111 + __typename: 'Query', 112 + item: undefined, 113 + }, 114 + }; 115 + 116 + const merged = mergeResultPatch(prevResult, { 117 + incremental: [ 118 + { 119 + data: null, 120 + path: ['item'], 121 + }, 122 + ], 123 + }); 124 + 125 + expect(merged.data).not.toBe(prevResult.data); 126 + expect(merged.data.item).toBe(null); 127 + }); 128 + 129 + it('should apply incremental stream patches', () => { 130 + const prevResult: OperationResult = { 131 + operation: queryOperation, 132 + data: { 133 + __typename: 'Query', 134 + items: [{ __typename: 'Item' }], 135 + }, 136 + }; 137 + 138 + const patch = { __typename: 'Item' }; 139 + 140 + const merged = mergeResultPatch(prevResult, { 141 + incremental: [ 142 + { 143 + items: [patch], 144 + path: ['items', 1], 145 + }, 146 + ], 147 + }); 148 + 149 + expect(merged.data.items).not.toBe(prevResult.data.items); 150 + expect(merged.data.items[0]).toBe(prevResult.data.items[0]); 151 + expect(merged.data.items[1]).toBe(patch); 152 + expect(merged.data).toStrictEqual({ 153 + __typename: 'Query', 154 + items: [{ __typename: 'Item' }, { __typename: 'Item' }], 155 + }); 156 + }); 157 + 158 + it('should handle null incremental stream patches', () => { 159 + const prevResult: OperationResult = { 160 + operation: queryOperation, 161 + data: { 162 + __typename: 'Query', 163 + items: [{ __typename: 'Item' }], 164 + }, 165 + }; 166 + 167 + const merged = mergeResultPatch(prevResult, { 168 + incremental: [ 169 + { 170 + items: null, 171 + path: ['items', 1], 172 + }, 173 + ], 174 + }); 175 + 176 + expect(merged.data.items).not.toBe(prevResult.data.items); 177 + expect(merged.data.items[0]).toBe(prevResult.data.items[0]); 178 + expect(merged.data).toStrictEqual({ 179 + __typename: 'Query', 180 + items: [{ __typename: 'Item' }], 181 + }); 182 + }); 183 + 184 + it('should merge extensions from each patch', () => { 185 + const prevResult: OperationResult = { 186 + operation: queryOperation, 187 + data: { 188 + __typename: 'Query', 189 + }, 190 + extensions: { 191 + base: true, 192 + }, 193 + }; 194 + 195 + const merged = mergeResultPatch(prevResult, { 196 + incremental: [ 197 + { 198 + data: null, 199 + path: ['item'], 200 + extensions: { 201 + patch: true, 202 + }, 203 + }, 204 + ], 205 + }); 206 + 207 + expect(merged.extensions).toStrictEqual({ 208 + base: true, 209 + patch: true, 210 + }); 211 + }); 212 + 213 + it('should combine errors from each patch', () => { 214 + const prevResult: OperationResult = makeResult(queryOperation, { 215 + errors: ['base'], 216 + }); 217 + 218 + const merged = mergeResultPatch(prevResult, { 219 + incremental: [ 220 + { 221 + data: null, 222 + path: ['item'], 223 + errors: ['patch'], 224 + }, 225 + ], 226 + }); 227 + 228 + expect(merged.error).toMatchInlineSnapshot(` 229 + [CombinedError: [GraphQL] base 230 + [GraphQL] patch] 231 + `); 232 + }); 233 + 234 + it('should preserve all data for noop patches', () => { 235 + const prevResult: OperationResult = { 236 + operation: queryOperation, 237 + data: { 238 + __typename: 'Query', 239 + }, 240 + extensions: { 241 + base: true, 242 + }, 243 + }; 244 + 245 + const merged = mergeResultPatch(prevResult, { 246 + hasNext: false, 247 + }); 248 + 249 + expect(merged.data).toStrictEqual({ 250 + __typename: 'Query', 251 + }); 252 + }); 253 + 254 + it('handles the old version of the incremental payload spec (DEPRECATED)', () => { 255 + const prevResult: OperationResult = { 256 + operation: queryOperation, 257 + data: { 258 + __typename: 'Query', 259 + items: [ 260 + { 261 + __typename: 'Item', 262 + id: 'id', 263 + child: undefined, 264 + }, 265 + ], 266 + }, 267 + }; 268 + 269 + const patch = { __typename: 'Child' }; 270 + 271 + const merged = mergeResultPatch(prevResult, { 272 + data: patch, 273 + path: ['items', 0, 'child'], 274 + } as any); 275 + 276 + expect(merged.data.items[0]).not.toBe(prevResult.data.items[0]); 277 + expect(merged.data.items[0].child).toBe(patch); 278 + expect(merged.data).toStrictEqual({ 279 + __typename: 'Query', 280 + items: [ 281 + { 282 + __typename: 'Item', 283 + id: 'id', 284 + child: patch, 285 + }, 286 + ], 287 + }); 288 + }); 289 + });
+65 -28
packages/core/src/utils/result.ts
··· 1 - import { ExecutionResult, Operation, OperationResult } from '../types'; 1 + import { 2 + ExecutionResult, 3 + Operation, 4 + OperationResult, 5 + IncrementalPayload, 6 + } from '../types'; 2 7 import { CombinedError } from './error'; 3 8 4 9 export const makeResult = ( ··· 6 11 result: ExecutionResult, 7 12 response?: any 8 13 ): OperationResult => { 9 - if ((!('data' in result) && !('errors' in result)) || 'path' in result) { 14 + if ( 15 + (!('data' in result) && !('errors' in result)) || 16 + 'incremental' in result 17 + ) { 10 18 throw new Error('No Content'); 11 19 } 12 20 ··· 27 35 28 36 export const mergeResultPatch = ( 29 37 prevResult: OperationResult, 30 - patch: ExecutionResult, 38 + nextResult: ExecutionResult, 31 39 response?: any 32 40 ): OperationResult => { 33 - const result = { ...prevResult }; 34 - result.hasNext = !!patch.hasNext; 41 + let data: ExecutionResult['data']; 42 + let hasExtensions = !!prevResult.extensions || !!nextResult.extensions; 43 + const extensions = { ...prevResult.extensions, ...nextResult.extensions }; 44 + const errors = prevResult.error ? prevResult.error.graphQLErrors : []; 35 45 36 - if (!('path' in patch)) { 37 - if ('data' in patch) result.data = patch.data; 38 - return result; 46 + let incremental = nextResult.incremental; 47 + 48 + // NOTE: We handle the old version of the incremental delivery payloads as well 49 + if ('path' in nextResult) { 50 + incremental = [ 51 + { 52 + data: nextResult.data, 53 + path: nextResult.path, 54 + } as IncrementalPayload, 55 + ]; 39 56 } 40 57 41 - if (Array.isArray(patch.errors)) { 42 - result.error = new CombinedError({ 43 - graphQLErrors: result.error 44 - ? [...result.error.graphQLErrors, ...patch.errors] 45 - : patch.errors, 46 - response, 47 - }); 48 - } 58 + if (incremental) { 59 + data = { ...prevResult.data }; 60 + for (const patch of incremental) { 61 + if (Array.isArray(patch.errors)) { 62 + errors.push(...(patch.errors as any)); 63 + } 64 + 65 + if (patch.extensions) { 66 + Object.assign(extensions, patch.extensions); 67 + hasExtensions = true; 68 + } 49 69 50 - let part: Record<string, any> | Array<any> = (result.data = { 51 - ...result.data, 52 - }); 70 + let prop: string | number = patch.path[0]; 71 + let part: Record<string, any> | Array<any> = data as object; 72 + for (let i = 1, l = patch.path.length; i < l; prop = patch.path[i++]) { 73 + part = part[prop] = Array.isArray(part[prop]) 74 + ? [...part[prop]] 75 + : { ...part[prop] }; 76 + } 53 77 54 - let i = 0; 55 - let prop: string | number; 56 - while (i < patch.path.length) { 57 - prop = patch.path[i++]; 58 - part = part[prop] = Array.isArray(part[prop]) 59 - ? [...part[prop]] 60 - : { ...part[prop] }; 78 + if (Array.isArray(patch.items)) { 79 + const startIndex = +prop >= 0 ? (prop as number) : 0; 80 + for (let i = 0, l = patch.items.length; i < l; i++) 81 + part[startIndex + i] = patch.items[i]; 82 + } else if (patch.data !== undefined) { 83 + part[prop] = 84 + part[prop] && patch.data 85 + ? { ...part[prop], ...patch.data } 86 + : patch.data; 87 + } 88 + } 89 + } else { 90 + data = nextResult.data || prevResult.data; 61 91 } 62 92 63 - Object.assign(part, patch.data); 64 - return result; 93 + return { 94 + operation: prevResult.operation, 95 + data, 96 + error: errors.length 97 + ? new CombinedError({ graphQLErrors: errors, response }) 98 + : undefined, 99 + extensions: hasExtensions ? extensions : undefined, 100 + hasNext: !!nextResult.hasNext, 101 + }; 65 102 }; 66 103 67 104 export const makeErrorResult = (