Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
at main 13 kB view raw
1import { describe, it, expect } from 'vitest'; 2import { OperationResult } from '../types'; 3import { queryOperation, subscriptionOperation } from '../test-utils'; 4import { makeResult, mergeResultPatch } from './result'; 5import { GraphQLError } from '@0no-co/graphql.web'; 6import { CombinedError } from './error'; 7 8describe('makeResult', () => { 9 it('adds extensions and errors correctly', () => { 10 const origResult = { 11 data: undefined, 12 errors: ['error message'], 13 extensions: { 14 extensionKey: 'extensionValue', 15 }, 16 }; 17 18 const result = makeResult(queryOperation, origResult); 19 20 expect(result.hasNext).toBe(false); 21 expect(result.operation).toBe(queryOperation); 22 expect(result.data).toBe(undefined); 23 expect(result.extensions).toEqual(origResult.extensions); 24 expect(result.error).toMatchInlineSnapshot( 25 `[CombinedError: [GraphQL] error message]` 26 ); 27 }); 28 29 it('default hasNext to true for subscriptions', () => { 30 const origResult = { 31 data: undefined, 32 errors: ['error message'], 33 extensions: { 34 extensionKey: 'extensionValue', 35 }, 36 }; 37 38 const result = makeResult(subscriptionOperation, origResult); 39 expect(result.hasNext).toBe(true); 40 }); 41}); 42 43describe('mergeResultPatch (defer/stream latest', () => { 44 it('should read pending and append the result', () => { 45 const pending = [{ id: '0', path: [] }]; 46 const prevResult: OperationResult = { 47 operation: queryOperation, 48 stale: false, 49 hasNext: true, 50 data: { 51 f2: { 52 a: 'a', 53 b: 'b', 54 c: { 55 d: 'd', 56 e: 'e', 57 f: { h: 'h', i: 'i' }, 58 }, 59 }, 60 }, 61 }; 62 63 const merged = mergeResultPatch( 64 prevResult, 65 { 66 incremental: [ 67 { id: '0', data: { MyFragment: 'Query' } }, 68 { id: '0', subPath: ['f2', 'c', 'f'], data: { j: 'j' } }, 69 ], 70 // TODO: not sure if we need this but it's part of the spec 71 // completed: [{ id: '0' }], 72 hasNext: false, 73 }, 74 undefined, 75 pending 76 ); 77 78 expect(merged.data).toEqual({ 79 MyFragment: 'Query', 80 f2: { 81 a: 'a', 82 b: 'b', 83 c: { 84 d: 'd', 85 e: 'e', 86 f: { h: 'h', i: 'i', j: 'j' }, 87 }, 88 }, 89 }); 90 }); 91 92 it('should read pending and append the result w/ overlapping fields', () => { 93 const pending = [ 94 { id: '0', path: [], label: 'D1' }, 95 { id: '1', path: ['f2', 'c', 'f'], label: 'D2' }, 96 ]; 97 const prevResult: OperationResult = { 98 operation: queryOperation, 99 stale: false, 100 hasNext: true, 101 data: { 102 f2: { 103 a: 'A', 104 b: 'B', 105 c: { 106 d: 'D', 107 e: 'E', 108 f: { 109 h: 'H', 110 i: 'I', 111 }, 112 }, 113 }, 114 }, 115 }; 116 117 const merged = mergeResultPatch( 118 prevResult, 119 { 120 incremental: [ 121 { id: '0', subPath: ['f2', 'c', 'f'], data: { j: 'J', k: 'K' } }, 122 ], 123 pending: [{ id: '1', path: ['f2', 'c', 'f'], label: 'D2' }], 124 hasNext: true, 125 }, 126 undefined, 127 pending 128 ); 129 130 const merged2 = mergeResultPatch( 131 merged, 132 { 133 incremental: [{ id: '1', data: { l: 'L', m: 'M' } }], 134 hasNext: false, 135 }, 136 undefined, 137 pending 138 ); 139 140 expect(merged2.data).toEqual({ 141 f2: { 142 a: 'A', 143 b: 'B', 144 c: { 145 d: 'D', 146 e: 'E', 147 f: { 148 h: 'H', 149 i: 'I', 150 j: 'J', 151 k: 'K', 152 l: 'L', 153 m: 'M', 154 }, 155 }, 156 }, 157 }); 158 }); 159}); 160 161describe('mergeResultPatch (defer/stream pre June-2023)', () => { 162 it('should default hasNext to true if the last result was set to true', () => { 163 const prevResult: OperationResult = { 164 operation: subscriptionOperation, 165 data: { 166 __typename: 'Subscription', 167 event: 1, 168 }, 169 stale: false, 170 hasNext: true, 171 }; 172 173 const merged = mergeResultPatch(prevResult, { 174 data: { 175 __typename: 'Subscription', 176 event: 2, 177 }, 178 }); 179 180 expect(merged.data).not.toBe(prevResult.data); 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 ); 238 expect(merged.hasNext).toBe(true); 239 }); 240 241 it('should ignore invalid patches', () => { 242 const prevResult: OperationResult = { 243 operation: queryOperation, 244 data: { 245 __typename: 'Query', 246 items: [ 247 { 248 __typename: 'Item', 249 id: 'id', 250 }, 251 ], 252 }, 253 stale: false, 254 hasNext: true, 255 }; 256 257 const merged = mergeResultPatch(prevResult, { 258 incremental: [ 259 { 260 data: undefined, 261 path: ['a'], 262 }, 263 { 264 items: null, 265 path: ['b'], 266 }, 267 ], 268 }); 269 270 expect(merged.data).toStrictEqual({ 271 __typename: 'Query', 272 items: [ 273 { 274 __typename: 'Item', 275 id: 'id', 276 }, 277 ], 278 }); 279 }); 280 281 it('should apply incremental defer patches', () => { 282 const prevResult: OperationResult = { 283 operation: queryOperation, 284 data: { 285 __typename: 'Query', 286 items: [ 287 { 288 __typename: 'Item', 289 id: 'id', 290 child: undefined, 291 }, 292 ], 293 }, 294 stale: false, 295 hasNext: true, 296 }; 297 298 const patch = { __typename: 'Child' }; 299 300 const merged = mergeResultPatch(prevResult, { 301 incremental: [ 302 { 303 data: patch, 304 path: ['items', 0, 'child'], 305 }, 306 ], 307 }); 308 309 expect(merged.data.items[0]).not.toBe(prevResult.data.items[0]); 310 expect(merged.data.items[0].child).toBe(patch); 311 expect(merged.data).toStrictEqual({ 312 __typename: 'Query', 313 items: [ 314 { 315 __typename: 'Item', 316 id: 'id', 317 child: patch, 318 }, 319 ], 320 }); 321 }); 322 323 it('should handle null incremental defer patches', () => { 324 const prevResult: OperationResult = { 325 operation: queryOperation, 326 data: { 327 __typename: 'Query', 328 item: undefined, 329 }, 330 stale: false, 331 hasNext: true, 332 }; 333 334 const merged = mergeResultPatch(prevResult, { 335 incremental: [ 336 { 337 data: null, 338 path: ['item'], 339 }, 340 ], 341 }); 342 343 expect(merged.data).not.toBe(prevResult.data); 344 expect(merged.data.item).toBe(null); 345 }); 346 347 it('should apply incremental stream patches', () => { 348 const prevResult: OperationResult = { 349 operation: queryOperation, 350 data: { 351 __typename: 'Query', 352 items: [{ __typename: 'Item' }], 353 }, 354 stale: false, 355 hasNext: true, 356 }; 357 358 const patch = { __typename: 'Item' }; 359 360 const merged = mergeResultPatch(prevResult, { 361 incremental: [ 362 { 363 items: [patch], 364 path: ['items', 1], 365 }, 366 ], 367 }); 368 369 expect(merged.data.items).not.toBe(prevResult.data.items); 370 expect(merged.data.items[0]).toBe(prevResult.data.items[0]); 371 expect(merged.data.items[1]).toBe(patch); 372 expect(merged.data).toStrictEqual({ 373 __typename: 'Query', 374 items: [{ __typename: 'Item' }, { __typename: 'Item' }], 375 }); 376 }); 377 378 it('should apply incremental stream patches deeply', () => { 379 const prevResult: OperationResult = { 380 operation: queryOperation, 381 data: { 382 __typename: 'Query', 383 test: [ 384 { 385 __typename: 'Test', 386 }, 387 ], 388 }, 389 stale: false, 390 hasNext: true, 391 }; 392 393 const patch = { name: 'Test' }; 394 395 const merged = mergeResultPatch(prevResult, { 396 incremental: [ 397 { 398 items: [patch], 399 path: ['test', 0], 400 }, 401 ], 402 }); 403 404 expect(merged.data).toStrictEqual({ 405 __typename: 'Query', 406 test: [ 407 { 408 __typename: 'Test', 409 name: 'Test', 410 }, 411 ], 412 }); 413 }); 414 415 it('should handle null incremental stream patches', () => { 416 const prevResult: OperationResult = { 417 operation: queryOperation, 418 data: { 419 __typename: 'Query', 420 items: [{ __typename: 'Item' }], 421 }, 422 stale: false, 423 hasNext: true, 424 }; 425 426 const merged = mergeResultPatch(prevResult, { 427 incremental: [ 428 { 429 items: null, 430 path: ['items', 1], 431 }, 432 ], 433 }); 434 435 expect(merged.data.items).not.toBe(prevResult.data.items); 436 expect(merged.data.items[0]).toBe(prevResult.data.items[0]); 437 expect(merged.data).toStrictEqual({ 438 __typename: 'Query', 439 items: [{ __typename: 'Item' }], 440 }); 441 }); 442 443 it('should handle root incremental stream patches', () => { 444 const prevResult: OperationResult = { 445 operation: queryOperation, 446 data: { 447 __typename: 'Query', 448 item: { 449 test: true, 450 }, 451 }, 452 stale: false, 453 hasNext: true, 454 }; 455 456 const merged = mergeResultPatch(prevResult, { 457 incremental: [ 458 { 459 data: { item: { test2: false } }, 460 path: [], 461 }, 462 ], 463 }); 464 465 expect(merged.data).toStrictEqual({ 466 __typename: 'Query', 467 item: { 468 test: true, 469 test2: false, 470 }, 471 }); 472 }); 473 474 it('should merge extensions from each patch', () => { 475 const prevResult: OperationResult = { 476 operation: queryOperation, 477 data: { 478 __typename: 'Query', 479 }, 480 extensions: { 481 base: true, 482 }, 483 stale: false, 484 hasNext: true, 485 }; 486 487 const merged = mergeResultPatch(prevResult, { 488 incremental: [ 489 { 490 data: null, 491 path: ['item'], 492 extensions: { 493 patch: true, 494 }, 495 }, 496 ], 497 }); 498 499 expect(merged.extensions).toStrictEqual({ 500 base: true, 501 patch: true, 502 }); 503 }); 504 505 it('should combine errors from each patch', () => { 506 const prevResult: OperationResult = makeResult(queryOperation, { 507 errors: ['base'], 508 }); 509 510 const merged = mergeResultPatch(prevResult, { 511 incremental: [ 512 { 513 data: null, 514 path: ['item'], 515 errors: ['patch'], 516 }, 517 ], 518 }); 519 520 expect(merged.error).toMatchInlineSnapshot(` 521 [CombinedError: [GraphQL] base 522 [GraphQL] patch] 523 `); 524 }); 525 526 it('should preserve all data for noop patches', () => { 527 const prevResult: OperationResult = { 528 operation: queryOperation, 529 data: { 530 __typename: 'Query', 531 }, 532 extensions: { 533 base: true, 534 }, 535 stale: false, 536 hasNext: true, 537 }; 538 539 const merged = mergeResultPatch(prevResult, { 540 hasNext: false, 541 }); 542 543 expect(merged.data).toStrictEqual({ 544 __typename: 'Query', 545 }); 546 }); 547 548 it('handles the old version of the incremental payload spec (DEPRECATED)', () => { 549 const prevResult: OperationResult = { 550 operation: queryOperation, 551 data: { 552 __typename: 'Query', 553 items: [ 554 { 555 __typename: 'Item', 556 id: 'id', 557 child: undefined, 558 }, 559 ], 560 }, 561 stale: false, 562 hasNext: true, 563 }; 564 565 const patch = { __typename: 'Child' }; 566 567 const merged = mergeResultPatch(prevResult, { 568 data: patch, 569 path: ['items', 0, 'child'], 570 } as any); 571 572 expect(merged.data.items[0]).not.toBe(prevResult.data.items[0]); 573 expect(merged.data.items[0].child).toBe(patch); 574 expect(merged.data).toStrictEqual({ 575 __typename: 'Query', 576 items: [ 577 { 578 __typename: 'Item', 579 id: 'id', 580 child: patch, 581 }, 582 ], 583 }); 584 }); 585});