urql-computed-exchange-plus 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +9 -0
- package/.eslintrc.js +58 -0
- package/.importsortrc +6 -0
- package/.prettierrc +12 -0
- package/CHANGELOG.md +50 -0
- package/LICENSE +24 -0
- package/README.md +367 -0
- package/jest.config.js +19 -0
- package/jest.integration.config.js +14 -0
- package/jest.performance.config.js +19 -0
- package/lib/async-computed.d.ts +16 -0
- package/lib/async-computed.js +68 -0
- package/lib/async-computed.js.map +1 -0
- package/lib/computed-exchange.d.ts +5 -0
- package/lib/computed-exchange.js +24 -0
- package/lib/computed-exchange.js.map +1 -0
- package/lib/create-entity.d.ts +2 -0
- package/lib/create-entity.js +7 -0
- package/lib/create-entity.js.map +1 -0
- package/lib/create-modern-entity.d.ts +2 -0
- package/lib/create-modern-entity.js +10 -0
- package/lib/create-modern-entity.js.map +1 -0
- package/lib/directive-utils.d.ts +6 -0
- package/lib/directive-utils.js +125 -0
- package/lib/directive-utils.js.map +1 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +23 -0
- package/lib/index.js.map +1 -0
- package/lib/merge-entities.d.ts +2 -0
- package/lib/merge-entities.js +35 -0
- package/lib/merge-entities.js.map +1 -0
- package/lib/resolve-data.d.ts +2 -0
- package/lib/resolve-data.js +152 -0
- package/lib/resolve-data.js.map +1 -0
- package/lib/set-utils.d.ts +5 -0
- package/lib/set-utils.js +27 -0
- package/lib/set-utils.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/lib/types/augmented-operation-result.d.ts +6 -0
- package/lib/types/augmented-operation-result.js +3 -0
- package/lib/types/augmented-operation-result.js.map +1 -0
- package/lib/types/augmented-operation.d.ts +6 -0
- package/lib/types/augmented-operation.js +3 -0
- package/lib/types/augmented-operation.js.map +1 -0
- package/lib/types/entity.d.ts +13 -0
- package/lib/types/entity.js +3 -0
- package/lib/types/entity.js.map +1 -0
- package/lib/types/index.d.ts +4 -0
- package/lib/types/index.js +21 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/node-with-directives.d.ts +2 -0
- package/lib/types/node-with-directives.js +3 -0
- package/lib/types/node-with-directives.js.map +1 -0
- package/lib/types.d.ts +68 -0
- package/lib/types.js +18 -0
- package/lib/types.js.map +1 -0
- package/package.json +77 -0
- package/test/integration/computed-exchange.test.ts +541 -0
- package/test/performance/large-dataset.test.ts +66 -0
- package/test/utils/index.ts +2 -0
- package/test/utils/run-query.ts +15 -0
- package/test/utils/simple-mock-fetch.ts +75 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +29 -0
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "urql-computed-exchange-plus",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "URQL exchange to allow computed properties in GraphQL queries. Maintained fork with updated dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"urql",
|
|
7
|
+
"graphql",
|
|
8
|
+
"computed",
|
|
9
|
+
"exchange",
|
|
10
|
+
"resolver"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"main": "lib/index.js",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/royalcala/urql-computed-exchange-plus"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"clean": "rimraf lib/",
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"build:watch": "tsc -w",
|
|
22
|
+
"test": "jest",
|
|
23
|
+
"test:watch": "jest --watch",
|
|
24
|
+
"test:integration": "jest --config ./jest.integration.config.js",
|
|
25
|
+
"test:integration:watch": "jest --config ./jest.integration.config.js --watch",
|
|
26
|
+
"test:performance": "jest --config ./jest.performance.config.js --testTimeout=15000",
|
|
27
|
+
"test:all": "npm test && npm run test:integration && npm run test:performance",
|
|
28
|
+
"lint": "eslint src --ext .ts",
|
|
29
|
+
"lint:fix": "eslint src --ext .ts --fix",
|
|
30
|
+
"release": "npm run test:all && npm run build && npm publish",
|
|
31
|
+
"release:patch": "npm version patch && npm run release",
|
|
32
|
+
"release:minor": "npm version minor && npm run release",
|
|
33
|
+
"release:major": "npm version major && npm run release",
|
|
34
|
+
"prepublishOnly": "npm run test:all && npm run build",
|
|
35
|
+
"prepare": "husky install"
|
|
36
|
+
},
|
|
37
|
+
"lint-staged": {
|
|
38
|
+
"*.{ts,tsx}": [
|
|
39
|
+
"eslint --ext ts,tsx --format node_modules/eslint-formatter-pretty -c .eslintrc.js --max-warnings=0",
|
|
40
|
+
"prettier --write"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"graphql": "^15.0.0 || ^16.0.0",
|
|
45
|
+
"urql": "^3.0.0 || ^4.0.0 || ^5.0.0",
|
|
46
|
+
"wonka": "^6.0.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/jest": "^30.0.0",
|
|
50
|
+
"@types/lodash": "^4.17.21",
|
|
51
|
+
"@types/react": "^18.0.0",
|
|
52
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
53
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
54
|
+
"eslint": "^9.0.0",
|
|
55
|
+
"eslint-formatter-pretty": "^7.0.0",
|
|
56
|
+
"eslint-plugin-jest": "^29.0.0",
|
|
57
|
+
"fraql": "^1.2.1",
|
|
58
|
+
"graphql": "^16.0.0",
|
|
59
|
+
"graphql-tag": "^2.12.6",
|
|
60
|
+
"husky": "^9.0.0",
|
|
61
|
+
"import-sort-style-module": "^6.0.0",
|
|
62
|
+
"jest": "^30.0.0",
|
|
63
|
+
"lint-staged": "^16.0.0",
|
|
64
|
+
"prettier": "^3.0.0",
|
|
65
|
+
"prettier-plugin-import-sort": "^0.0.7",
|
|
66
|
+
"react": "^18.0.0",
|
|
67
|
+
"rimraf": "^6.0.0",
|
|
68
|
+
"ts-jest": "^29.0.0",
|
|
69
|
+
"typescript": "^5.0.0",
|
|
70
|
+
"urql": "^5.0.0",
|
|
71
|
+
"wonka": "^6.0.0"
|
|
72
|
+
},
|
|
73
|
+
"dependencies": {
|
|
74
|
+
"fclone": "^1.0.11",
|
|
75
|
+
"graphql-anywhere": "^4.2.6"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import gql from 'fraql';
|
|
2
|
+
import { Client, cacheExchange, createClient, fetchExchange } from 'urql';
|
|
3
|
+
|
|
4
|
+
import { computedExchange, createEntity } from '../../src';
|
|
5
|
+
import { createMockFetch, runQuery } from '../utils';
|
|
6
|
+
|
|
7
|
+
describe('urql-computed-exchange', () => {
|
|
8
|
+
describe('computed-exchange', () => {
|
|
9
|
+
describe('computedExchange', () => {
|
|
10
|
+
let client: Client;
|
|
11
|
+
let entities: any;
|
|
12
|
+
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
entities = {
|
|
15
|
+
User: createEntity('User', {
|
|
16
|
+
fullName: {
|
|
17
|
+
dependencies: gql`
|
|
18
|
+
fragment _ on User {
|
|
19
|
+
firstName
|
|
20
|
+
lastName
|
|
21
|
+
}
|
|
22
|
+
`,
|
|
23
|
+
resolver: (user: any) => `${user.firstName} ${user.lastName}`,
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
client = createClient({
|
|
29
|
+
url: '/graphql',
|
|
30
|
+
fetch: createMockFetch()
|
|
31
|
+
.post('/graphql', {
|
|
32
|
+
status: 200,
|
|
33
|
+
json: async () => ({
|
|
34
|
+
data: {
|
|
35
|
+
user: {
|
|
36
|
+
id: 1,
|
|
37
|
+
firstName: 'Lorem',
|
|
38
|
+
lastName: 'Ipsum',
|
|
39
|
+
__typename: 'User',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
})
|
|
44
|
+
.build(),
|
|
45
|
+
exchanges: [cacheExchange, computedExchange({ entities }), fetchExchange],
|
|
46
|
+
preferGetMethod: false,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('runs queries without computed properties', async () => {
|
|
51
|
+
const query = gql`
|
|
52
|
+
query User {
|
|
53
|
+
user(id: "id") {
|
|
54
|
+
id
|
|
55
|
+
firstName
|
|
56
|
+
lastName
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
const result = await runQuery(client, query);
|
|
62
|
+
const { data } = result;
|
|
63
|
+
expect(data).toMatchObject({
|
|
64
|
+
user: {
|
|
65
|
+
id: 1,
|
|
66
|
+
firstName: 'Lorem',
|
|
67
|
+
lastName: 'Ipsum',
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('runs queries with computed properties', async () => {
|
|
73
|
+
const query = gql`
|
|
74
|
+
query User {
|
|
75
|
+
user(id: "id") {
|
|
76
|
+
id
|
|
77
|
+
firstName
|
|
78
|
+
lastName
|
|
79
|
+
fullName @computed(type: User)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
const { data } = await runQuery(client, query);
|
|
85
|
+
expect(data).toMatchObject({
|
|
86
|
+
user: {
|
|
87
|
+
id: 1,
|
|
88
|
+
firstName: 'Lorem',
|
|
89
|
+
lastName: 'Ipsum',
|
|
90
|
+
fullName: 'Lorem Ipsum',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('handles complex computed property chains', async () => {
|
|
96
|
+
const chainEntities = {
|
|
97
|
+
User: createEntity('User', {
|
|
98
|
+
fullName: {
|
|
99
|
+
dependencies: gql`
|
|
100
|
+
fragment _ on User {
|
|
101
|
+
firstName
|
|
102
|
+
lastName
|
|
103
|
+
}
|
|
104
|
+
`,
|
|
105
|
+
resolver: (user: any) => `${user.firstName} ${user.lastName}`,
|
|
106
|
+
},
|
|
107
|
+
displayName: {
|
|
108
|
+
dependencies: gql`
|
|
109
|
+
fragment _ on User {
|
|
110
|
+
fullName @computed(type: User)
|
|
111
|
+
title
|
|
112
|
+
}
|
|
113
|
+
`,
|
|
114
|
+
resolver: (user: any) => `${user.title} ${user.fullName}`,
|
|
115
|
+
},
|
|
116
|
+
signature: {
|
|
117
|
+
dependencies: gql`
|
|
118
|
+
fragment _ on User {
|
|
119
|
+
displayName @computed(type: User)
|
|
120
|
+
department
|
|
121
|
+
}
|
|
122
|
+
`,
|
|
123
|
+
resolver: (user: any) => `${user.displayName} - ${user.department}`,
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const chainClient = createClient({
|
|
129
|
+
url: '/graphql',
|
|
130
|
+
fetch: createMockFetch()
|
|
131
|
+
.post('/graphql', {
|
|
132
|
+
status: 200,
|
|
133
|
+
json: async () => ({
|
|
134
|
+
data: {
|
|
135
|
+
user: {
|
|
136
|
+
id: 1,
|
|
137
|
+
firstName: 'John',
|
|
138
|
+
lastName: 'Doe',
|
|
139
|
+
title: 'Dr.',
|
|
140
|
+
department: 'Engineering',
|
|
141
|
+
__typename: 'User',
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
}),
|
|
145
|
+
})
|
|
146
|
+
.build(),
|
|
147
|
+
exchanges: [cacheExchange, computedExchange({ entities: chainEntities }), fetchExchange],
|
|
148
|
+
preferGetMethod: false,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const query = gql`
|
|
152
|
+
query User {
|
|
153
|
+
user(id: "id") {
|
|
154
|
+
id
|
|
155
|
+
signature @computed(type: User)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
`;
|
|
159
|
+
|
|
160
|
+
const { data } = await runQuery(chainClient, query);
|
|
161
|
+
expect(data).toMatchObject({
|
|
162
|
+
user: {
|
|
163
|
+
id: 1,
|
|
164
|
+
signature: 'Dr. John Doe - Engineering',
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('handles multiple computed properties on same object', async () => {
|
|
170
|
+
const query = gql`
|
|
171
|
+
query User {
|
|
172
|
+
user(id: "id") {
|
|
173
|
+
id
|
|
174
|
+
fullName @computed(type: User)
|
|
175
|
+
fullName2: fullName @computed(type: User)
|
|
176
|
+
fullName3: fullName @computed(type: User)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
`;
|
|
180
|
+
|
|
181
|
+
const { data } = await runQuery(client, query);
|
|
182
|
+
expect(data).toMatchObject({
|
|
183
|
+
user: {
|
|
184
|
+
id: 1,
|
|
185
|
+
fullName: 'Lorem Ipsum',
|
|
186
|
+
fullName2: 'Lorem Ipsum',
|
|
187
|
+
fullName3: 'Lorem Ipsum',
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('handles arrays with computed properties', async () => {
|
|
193
|
+
const arrayClient = createClient({
|
|
194
|
+
url: '/graphql',
|
|
195
|
+
fetch: createMockFetch()
|
|
196
|
+
.post('/graphql', {
|
|
197
|
+
status: 200,
|
|
198
|
+
json: async () => ({
|
|
199
|
+
data: {
|
|
200
|
+
users: [
|
|
201
|
+
{
|
|
202
|
+
id: 1,
|
|
203
|
+
firstName: 'John',
|
|
204
|
+
lastName: 'Doe',
|
|
205
|
+
__typename: 'User',
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: 2,
|
|
209
|
+
firstName: 'Jane',
|
|
210
|
+
lastName: 'Smith',
|
|
211
|
+
__typename: 'User',
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
})
|
|
217
|
+
.build(),
|
|
218
|
+
exchanges: [cacheExchange, computedExchange({ entities }), fetchExchange],
|
|
219
|
+
preferGetMethod: false,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const query = gql`
|
|
223
|
+
query Users {
|
|
224
|
+
users {
|
|
225
|
+
id
|
|
226
|
+
fullName @computed(type: User)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
`;
|
|
230
|
+
|
|
231
|
+
const { data } = await runQuery(arrayClient, query);
|
|
232
|
+
expect(data).toMatchObject({
|
|
233
|
+
users: [
|
|
234
|
+
{ id: 1, fullName: 'John Doe' },
|
|
235
|
+
{ id: 2, fullName: 'Jane Smith' },
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('error handling', () => {
|
|
242
|
+
it.skip('handles circular dependencies gracefully', async () => {
|
|
243
|
+
// This test verifies that circular dependencies don't cause infinite loops
|
|
244
|
+
// and that the exchange fails gracefully rather than hanging
|
|
245
|
+
const circularEntities = {
|
|
246
|
+
User: createEntity('User', {
|
|
247
|
+
fieldA: {
|
|
248
|
+
dependencies: gql`
|
|
249
|
+
fragment _ on User {
|
|
250
|
+
fieldB @computed(type: User)
|
|
251
|
+
}
|
|
252
|
+
`,
|
|
253
|
+
resolver: (user: any) => `A-${user.fieldB}`,
|
|
254
|
+
},
|
|
255
|
+
fieldB: {
|
|
256
|
+
dependencies: gql`
|
|
257
|
+
fragment _ on User {
|
|
258
|
+
fieldA @computed(type: User)
|
|
259
|
+
}
|
|
260
|
+
`,
|
|
261
|
+
resolver: (user: any) => `B-${user.fieldA}`,
|
|
262
|
+
},
|
|
263
|
+
}),
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const circularClient = createClient({
|
|
267
|
+
url: '/graphql',
|
|
268
|
+
fetch: createMockFetch()
|
|
269
|
+
.post('/graphql', {
|
|
270
|
+
status: 200,
|
|
271
|
+
json: async () => ({
|
|
272
|
+
data: {
|
|
273
|
+
user: {
|
|
274
|
+
id: 1,
|
|
275
|
+
__typename: 'User',
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
}),
|
|
279
|
+
})
|
|
280
|
+
.build(),
|
|
281
|
+
exchanges: [cacheExchange, computedExchange({ entities: circularEntities }), fetchExchange],
|
|
282
|
+
preferGetMethod: false,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const query = gql`
|
|
286
|
+
query User {
|
|
287
|
+
user(id: "id") {
|
|
288
|
+
id
|
|
289
|
+
fieldA @computed(type: User)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
`;
|
|
293
|
+
|
|
294
|
+
// The test should complete within a reasonable time and not hang
|
|
295
|
+
// We expect either an error result or the query to fail gracefully
|
|
296
|
+
const startTime = Date.now();
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const result = await runQuery(circularClient, query);
|
|
300
|
+
const endTime = Date.now();
|
|
301
|
+
|
|
302
|
+
// Should complete quickly (within 5 seconds)
|
|
303
|
+
expect(endTime - startTime).toBeLessThan(5000);
|
|
304
|
+
|
|
305
|
+
// Should have an error due to circular dependency
|
|
306
|
+
expect(result.error).toBeDefined();
|
|
307
|
+
} catch (error: any) {
|
|
308
|
+
const endTime = Date.now();
|
|
309
|
+
|
|
310
|
+
// Should complete quickly (within 5 seconds)
|
|
311
|
+
expect(endTime - startTime).toBeLessThan(5000);
|
|
312
|
+
|
|
313
|
+
// Should be a circular dependency related error
|
|
314
|
+
expect(error.message).toMatch(/circular|dependency|iteration|irresoluble/i);
|
|
315
|
+
}
|
|
316
|
+
}, 6000); // 6 second timeout
|
|
317
|
+
|
|
318
|
+
it('handles missing dependencies gracefully', async () => {
|
|
319
|
+
const entitiesWithMissingDeps = {
|
|
320
|
+
User: createEntity('User', {
|
|
321
|
+
computedField: {
|
|
322
|
+
dependencies: gql`
|
|
323
|
+
fragment _ on User {
|
|
324
|
+
nonExistentField
|
|
325
|
+
}
|
|
326
|
+
`,
|
|
327
|
+
resolver: (user: any) => user.nonExistentField || 'fallback',
|
|
328
|
+
},
|
|
329
|
+
}),
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const missingDepsClient = createClient({
|
|
333
|
+
url: '/graphql',
|
|
334
|
+
fetch: createMockFetch()
|
|
335
|
+
.post('/graphql', {
|
|
336
|
+
status: 200,
|
|
337
|
+
json: async () => ({
|
|
338
|
+
data: {
|
|
339
|
+
user: {
|
|
340
|
+
id: 1,
|
|
341
|
+
firstName: 'John',
|
|
342
|
+
__typename: 'User',
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
}),
|
|
346
|
+
})
|
|
347
|
+
.build(),
|
|
348
|
+
exchanges: [cacheExchange, computedExchange({ entities: entitiesWithMissingDeps }), fetchExchange],
|
|
349
|
+
preferGetMethod: false,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const query = gql`
|
|
353
|
+
query User {
|
|
354
|
+
user(id: "id") {
|
|
355
|
+
id
|
|
356
|
+
computedField @computed(type: User)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
`;
|
|
360
|
+
|
|
361
|
+
const { data } = await runQuery(missingDepsClient, query);
|
|
362
|
+
expect(data).toMatchObject({
|
|
363
|
+
user: {
|
|
364
|
+
id: 1,
|
|
365
|
+
computedField: 'fallback',
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('handles resolver errors gracefully', async () => {
|
|
371
|
+
const entitiesWithErrorResolver = {
|
|
372
|
+
User: createEntity('User', {
|
|
373
|
+
errorField: {
|
|
374
|
+
dependencies: gql`
|
|
375
|
+
fragment _ on User {
|
|
376
|
+
firstName
|
|
377
|
+
}
|
|
378
|
+
`,
|
|
379
|
+
resolver: () => {
|
|
380
|
+
throw new Error('Resolver error');
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
}),
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const errorClient = createClient({
|
|
387
|
+
url: '/graphql',
|
|
388
|
+
fetch: createMockFetch()
|
|
389
|
+
.post('/graphql', {
|
|
390
|
+
status: 200,
|
|
391
|
+
json: async () => ({
|
|
392
|
+
data: {
|
|
393
|
+
user: {
|
|
394
|
+
id: 1,
|
|
395
|
+
firstName: 'John',
|
|
396
|
+
__typename: 'User',
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
}),
|
|
400
|
+
})
|
|
401
|
+
.build(),
|
|
402
|
+
exchanges: [cacheExchange, computedExchange({ entities: entitiesWithErrorResolver }), fetchExchange],
|
|
403
|
+
preferGetMethod: false,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const query = gql`
|
|
407
|
+
query User {
|
|
408
|
+
user(id: "id") {
|
|
409
|
+
id
|
|
410
|
+
errorField @computed(type: User)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
`;
|
|
414
|
+
|
|
415
|
+
// This should not crash the entire query
|
|
416
|
+
const result = await runQuery(errorClient, query);
|
|
417
|
+
// The resolver error should result in the field being omitted or undefined
|
|
418
|
+
expect(result.data).toMatchObject({
|
|
419
|
+
user: {
|
|
420
|
+
id: 1,
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
// The errorField should either be undefined or not present
|
|
424
|
+
expect(result.data?.user?.errorField).toBeUndefined();
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe('caching behavior', () => {
|
|
429
|
+
let entities: any;
|
|
430
|
+
|
|
431
|
+
beforeAll(() => {
|
|
432
|
+
entities = {
|
|
433
|
+
User: createEntity('User', {
|
|
434
|
+
fullName: {
|
|
435
|
+
dependencies: gql`
|
|
436
|
+
fragment _ on User {
|
|
437
|
+
firstName
|
|
438
|
+
lastName
|
|
439
|
+
}
|
|
440
|
+
`,
|
|
441
|
+
resolver: (user: any) => `${user.firstName} ${user.lastName}`,
|
|
442
|
+
},
|
|
443
|
+
}),
|
|
444
|
+
};
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('respects cache-first policy for computed properties', async () => {
|
|
448
|
+
let fetchCount = 0;
|
|
449
|
+
const cacheClient = createClient({
|
|
450
|
+
url: '/graphql',
|
|
451
|
+
fetch: createMockFetch()
|
|
452
|
+
.post('/graphql', {
|
|
453
|
+
status: 200,
|
|
454
|
+
json: async () => {
|
|
455
|
+
fetchCount++;
|
|
456
|
+
return {
|
|
457
|
+
data: {
|
|
458
|
+
user: {
|
|
459
|
+
id: 1,
|
|
460
|
+
firstName: 'John',
|
|
461
|
+
lastName: 'Doe',
|
|
462
|
+
__typename: 'User',
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
},
|
|
467
|
+
})
|
|
468
|
+
.build(),
|
|
469
|
+
exchanges: [cacheExchange, computedExchange({ entities }), fetchExchange],
|
|
470
|
+
preferGetMethod: false,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const query = gql`
|
|
474
|
+
query User {
|
|
475
|
+
user(id: "id") {
|
|
476
|
+
id
|
|
477
|
+
fullName @computed(type: User)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
`;
|
|
481
|
+
|
|
482
|
+
// First query
|
|
483
|
+
const result1 = await runQuery(cacheClient, query);
|
|
484
|
+
expect(result1.data).toMatchObject({
|
|
485
|
+
user: { id: 1, fullName: 'John Doe' },
|
|
486
|
+
});
|
|
487
|
+
expect(fetchCount).toBe(1);
|
|
488
|
+
|
|
489
|
+
// Second query should use cache
|
|
490
|
+
const result2 = await runQuery(cacheClient, query);
|
|
491
|
+
expect(result2.data).toMatchObject({
|
|
492
|
+
user: { id: 1, fullName: 'John Doe' },
|
|
493
|
+
});
|
|
494
|
+
expect(fetchCount).toBe(1); // Should still be 1 due to caching
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('works with different request policies', async () => {
|
|
498
|
+
let fetchCount = 0;
|
|
499
|
+
const networkClient = createClient({
|
|
500
|
+
url: '/graphql',
|
|
501
|
+
fetch: createMockFetch()
|
|
502
|
+
.post('/graphql', {
|
|
503
|
+
status: 200,
|
|
504
|
+
json: async () => {
|
|
505
|
+
fetchCount++;
|
|
506
|
+
return {
|
|
507
|
+
data: {
|
|
508
|
+
user: {
|
|
509
|
+
id: 1,
|
|
510
|
+
firstName: 'John',
|
|
511
|
+
lastName: 'Doe',
|
|
512
|
+
__typename: 'User',
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
},
|
|
517
|
+
})
|
|
518
|
+
.build(),
|
|
519
|
+
exchanges: [cacheExchange, computedExchange({ entities }), fetchExchange],
|
|
520
|
+
preferGetMethod: false,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const query = gql`
|
|
524
|
+
query User {
|
|
525
|
+
user(id: "id") {
|
|
526
|
+
id
|
|
527
|
+
fullName @computed(type: User)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
`;
|
|
531
|
+
|
|
532
|
+
// Test with network-only policy
|
|
533
|
+
const result = await runQuery(networkClient, query, {}, { requestPolicy: 'network-only' });
|
|
534
|
+
expect(result.data).toMatchObject({
|
|
535
|
+
user: { id: 1, fullName: 'John Doe' },
|
|
536
|
+
});
|
|
537
|
+
expect(fetchCount).toBe(1);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import gql from 'fraql';
|
|
2
|
+
import { createClient, cacheExchange, fetchExchange } from 'urql';
|
|
3
|
+
|
|
4
|
+
import { computedExchange, createEntity } from '../../src';
|
|
5
|
+
import { createMockFetch, runQuery } from '../utils';
|
|
6
|
+
|
|
7
|
+
describe('Performance Tests', () => {
|
|
8
|
+
it('handles large datasets efficiently', async () => {
|
|
9
|
+
const entities = {
|
|
10
|
+
User: createEntity('User', {
|
|
11
|
+
fullName: {
|
|
12
|
+
dependencies: gql`
|
|
13
|
+
fragment _ on User {
|
|
14
|
+
firstName
|
|
15
|
+
lastName
|
|
16
|
+
}
|
|
17
|
+
`,
|
|
18
|
+
resolver: (user: any) => `${user.firstName} ${user.lastName}`,
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Generate large dataset
|
|
24
|
+
const users = Array.from({ length: 1000 }, (_, i) => ({
|
|
25
|
+
id: i + 1,
|
|
26
|
+
firstName: `User${i}`,
|
|
27
|
+
lastName: `LastName${i}`,
|
|
28
|
+
__typename: 'User',
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const client = createClient({
|
|
32
|
+
url: '/graphql',
|
|
33
|
+
fetch: createMockFetch()
|
|
34
|
+
.post('/graphql', {
|
|
35
|
+
status: 200,
|
|
36
|
+
json: async () => ({
|
|
37
|
+
data: { users },
|
|
38
|
+
}),
|
|
39
|
+
})
|
|
40
|
+
.build(),
|
|
41
|
+
exchanges: [cacheExchange, computedExchange({ entities }), fetchExchange],
|
|
42
|
+
preferGetMethod: false,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const query = gql`
|
|
46
|
+
query Users {
|
|
47
|
+
users {
|
|
48
|
+
id
|
|
49
|
+
fullName @computed(type: User)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const startTime = Date.now();
|
|
55
|
+
const { data } = await runQuery(client, query);
|
|
56
|
+
const endTime = Date.now();
|
|
57
|
+
|
|
58
|
+
// Should complete within reasonable time
|
|
59
|
+
expect(endTime - startTime).toBeLessThan(5000);
|
|
60
|
+
|
|
61
|
+
// Verify data integrity
|
|
62
|
+
expect(data.users).toHaveLength(1000);
|
|
63
|
+
expect(data.users[0].fullName).toBe('User0 LastName0');
|
|
64
|
+
expect(data.users[999].fullName).toBe('User999 LastName999');
|
|
65
|
+
}, 10000);
|
|
66
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { DocumentNode } from 'graphql';
|
|
2
|
+
import { Client, OperationResult, createRequest } from 'urql';
|
|
3
|
+
import { pipe, subscribe } from 'wonka';
|
|
4
|
+
|
|
5
|
+
export function runQuery(client: Client, query: string | DocumentNode, variables: any = {}, context: any = {}) {
|
|
6
|
+
const request = createRequest(query, variables);
|
|
7
|
+
return new Promise<OperationResult>((resolve) => {
|
|
8
|
+
pipe(
|
|
9
|
+
client.executeQuery(request, context),
|
|
10
|
+
subscribe((res) => {
|
|
11
|
+
resolve(res);
|
|
12
|
+
}),
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
}
|