1import {
2 Source,
3 pipe,
4 fromValue,
5 fromArray,
6 toPromise,
7 delay,
8 take,
9 tap,
10 map,
11} from 'wonka';
12
13import { Client, Operation, OperationResult, CombinedError } from '@urql/core';
14
15import { vi, expect, it } from 'vitest';
16import {
17 queryResponse,
18 queryOperation,
19} from '../../../packages/core/src/test-utils';
20import { persistedExchange } from './persistedExchange';
21
22const makeExchangeArgs = () => {
23 const operations: Operation[] = [];
24
25 const result = vi.fn(
26 (operation: Operation): OperationResult => ({ ...queryResponse, operation })
27 );
28
29 return {
30 operations,
31 result,
32 exchangeArgs: {
33 forward: (op$: Source<Operation>) =>
34 pipe(
35 op$,
36 tap(op => operations.push(op)),
37 map(result)
38 ),
39 client: new Client({ url: '/api', exchanges: [] }),
40 } as any,
41 };
42};
43
44it('adds the APQ extensions correctly', async () => {
45 const { exchangeArgs } = makeExchangeArgs();
46
47 const res = await pipe(
48 fromValue(queryOperation),
49 persistedExchange()(exchangeArgs),
50 take(1),
51 toPromise
52 );
53
54 expect(res.operation.context.persistAttempt).toBe(true);
55 expect(res.operation.extensions).toEqual({
56 persistedQuery: {
57 version: 1,
58 sha256Hash: expect.any(String),
59 miss: undefined,
60 },
61 });
62});
63
64it('retries query when persisted query resulted in miss', async () => {
65 const { result, operations, exchangeArgs } = makeExchangeArgs();
66
67 result.mockImplementationOnce(operation => ({
68 ...queryResponse,
69 operation,
70 error: new CombinedError({
71 graphQLErrors: [{ message: 'PersistedQueryNotFound' }],
72 }),
73 }));
74
75 const res = await pipe(
76 fromValue(queryOperation),
77 persistedExchange()(exchangeArgs),
78 take(1),
79 toPromise
80 );
81
82 expect(res.operation.context.persistAttempt).toBe(true);
83 expect(operations.length).toBe(2);
84
85 expect(operations[1].extensions).toEqual({
86 persistedQuery: {
87 version: 1,
88 sha256Hash: expect.any(String),
89 miss: true,
90 },
91 });
92});
93
94it('retries query persisted query resulted in unsupported', async () => {
95 const { result, operations, exchangeArgs } = makeExchangeArgs();
96
97 result.mockImplementationOnce(operation => ({
98 ...queryResponse,
99 operation,
100 error: new CombinedError({
101 graphQLErrors: [{ message: 'PersistedQueryNotSupported' }],
102 }),
103 }));
104
105 await pipe(
106 fromArray([queryOperation, queryOperation]),
107 delay(0),
108 persistedExchange()(exchangeArgs),
109 take(2),
110 toPromise
111 );
112
113 expect(operations.length).toBe(3);
114
115 expect(operations[1].extensions).toEqual({
116 persistedQuery: undefined,
117 });
118
119 expect(operations[2].extensions).toEqual(undefined);
120});
121
122it('fails gracefully when an invalid result with `PersistedQueryNotFound` is always delivered', async () => {
123 const { result, operations, exchangeArgs } = makeExchangeArgs();
124
125 result.mockImplementation(operation => ({
126 ...queryResponse,
127 operation,
128 error: new CombinedError({
129 graphQLErrors: [{ message: 'PersistedQueryNotFound' }],
130 }),
131 }));
132
133 const res = await pipe(
134 fromValue(queryOperation),
135 persistedExchange()(exchangeArgs),
136 take(1),
137 toPromise
138 );
139
140 expect(res.operation.context.persistAttempt).toBe(true);
141 expect(operations.length).toBe(2);
142
143 expect(operations[1].extensions).toEqual({
144 persistedQuery: {
145 version: 1,
146 sha256Hash: expect.any(String),
147 miss: true,
148 },
149 });
150
151 expect(console.warn).toHaveBeenLastCalledWith(
152 expect.stringMatching(/two misses/i)
153 );
154});
155
156it('skips operation when generateHash returns a nullish value', async () => {
157 const { result, operations, exchangeArgs } = makeExchangeArgs();
158
159 result.mockImplementationOnce(operation => ({
160 ...queryResponse,
161 operation,
162 data: null,
163 }));
164
165 const res = await pipe(
166 fromValue(queryOperation),
167 persistedExchange({ generateHash: async () => null })(exchangeArgs),
168 take(1),
169 toPromise
170 );
171
172 expect(res.operation.context.persistAttempt).toBe(true);
173 expect(operations.length).toBe(1);
174 expect(operations[0]).not.toHaveProperty('extensions.persistedQuery');
175});
176
177it.each([true, false, 'force', 'within-url-limit'] as const)(
178 'sets `context.preferGetMethod` to %s when `options.preferGetForPersistedQueries` is %s',
179 async preferGetMethodValue => {
180 const { exchangeArgs } = makeExchangeArgs();
181
182 const res = await pipe(
183 fromValue(queryOperation),
184 persistedExchange({ preferGetForPersistedQueries: preferGetMethodValue })(
185 exchangeArgs
186 ),
187 take(1),
188 toPromise
189 );
190
191 expect(res.operation.context.preferGetMethod).toBe(preferGetMethodValue);
192 }
193);