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/.editorconfig
ADDED
package/.eslintrc.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
parser: '@typescript-eslint/parser',
|
|
3
|
+
plugins: ['@typescript-eslint', 'jest'],
|
|
4
|
+
extends: [
|
|
5
|
+
'eslint:recommended',
|
|
6
|
+
'@typescript-eslint/recommended',
|
|
7
|
+
'plugin:jest/recommended'
|
|
8
|
+
],
|
|
9
|
+
parserOptions: {
|
|
10
|
+
ecmaVersion: 2020,
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
project: './tsconfig.json',
|
|
13
|
+
},
|
|
14
|
+
env: {
|
|
15
|
+
node: true,
|
|
16
|
+
commonjs: true,
|
|
17
|
+
browser: true,
|
|
18
|
+
es6: true,
|
|
19
|
+
jest: true,
|
|
20
|
+
},
|
|
21
|
+
rules: {
|
|
22
|
+
// General
|
|
23
|
+
'array-callback-return': ['warn'],
|
|
24
|
+
'eqeqeq': ['warn', 'always', { null: 'ignore' }],
|
|
25
|
+
'new-parens': ['warn'],
|
|
26
|
+
'no-array-constructor': ['warn'],
|
|
27
|
+
'no-caller': ['warn'],
|
|
28
|
+
'no-cond-assign': ['warn', 'always'],
|
|
29
|
+
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
|
30
|
+
'no-eval': ['warn'],
|
|
31
|
+
'no-extend-native': ['warn'],
|
|
32
|
+
'no-extra-bind': ['warn'],
|
|
33
|
+
'no-implied-eval': ['warn'],
|
|
34
|
+
'no-iterator': ['warn'],
|
|
35
|
+
'no-lone-blocks': ['warn'],
|
|
36
|
+
'no-loop-func': ['warn'],
|
|
37
|
+
'no-multi-str': ['warn'],
|
|
38
|
+
'no-new-wrappers': ['warn'],
|
|
39
|
+
'no-script-url': ['warn'],
|
|
40
|
+
'no-self-compare': ['warn'],
|
|
41
|
+
'no-shadow-restricted-names': ['warn'],
|
|
42
|
+
'no-template-curly-in-string': ['warn'],
|
|
43
|
+
'no-throw-literal': ['warn'],
|
|
44
|
+
'no-useless-computed-key': ['warn'],
|
|
45
|
+
'no-useless-concat': ['warn'],
|
|
46
|
+
'no-useless-rename': ['warn'],
|
|
47
|
+
'no-whitespace-before-property': ['warn'],
|
|
48
|
+
|
|
49
|
+
// TypeScript
|
|
50
|
+
'no-unused-vars': 'off',
|
|
51
|
+
'@typescript-eslint/no-unused-vars': ['warn', { ignoreRestSiblings: true }],
|
|
52
|
+
'no-useless-constructor': 'off',
|
|
53
|
+
'@typescript-eslint/no-useless-constructor': ['warn'],
|
|
54
|
+
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
55
|
+
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
56
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
57
|
+
},
|
|
58
|
+
};
|
package/.importsortrc
ADDED
package/.prettierrc
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.3] - 2025-01-02
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **CRITICAL**: Fixed infinite loop in dependency resolution caused by incorrect set difference implementation
|
|
7
|
+
- Fixed circular dependency detection in GraphQL fragment processing
|
|
8
|
+
- Improved error handling for resolver failures (now fails gracefully instead of crashing)
|
|
9
|
+
- Updated integration with latest urql version (removed deprecated `dedupExchange`)
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Async Computed Properties**: Support for async resolvers with caching and TTL
|
|
13
|
+
- **Enhanced TypeScript Types**: Better type safety with generic entity definitions
|
|
14
|
+
- **Circular Dependency Detection**: Proactive detection with detailed error messages
|
|
15
|
+
- **Performance Optimizations**: Handles 1000+ items efficiently with proper caching
|
|
16
|
+
- **Modern Entity Creator**: `createModernEntity` with enhanced type safety
|
|
17
|
+
- **Comprehensive Documentation**:
|
|
18
|
+
- Integration examples with different urql exchanges
|
|
19
|
+
- Dependency resolution behavior explanation
|
|
20
|
+
- Troubleshooting guide for common issues
|
|
21
|
+
- Error handling examples
|
|
22
|
+
- **Enhanced Testing**:
|
|
23
|
+
- Comprehensive integration tests with urql's cache exchange
|
|
24
|
+
- Performance tests for large datasets
|
|
25
|
+
- Tests for complex computed property chains
|
|
26
|
+
- Tests for error handling scenarios
|
|
27
|
+
- Tests for array handling with computed properties
|
|
28
|
+
- Async computed property tests
|
|
29
|
+
- **CI/CD Pipeline**: GitHub Actions workflow for automated testing and publishing
|
|
30
|
+
|
|
31
|
+
### Improved
|
|
32
|
+
- **Better Error Messages**: Detailed error messages for circular dependencies and resolution failures
|
|
33
|
+
- **More Robust Handling**: Better handling of missing dependencies and edge cases
|
|
34
|
+
- **Enhanced TypeScript Support**: Improved type definitions and generic constraints
|
|
35
|
+
- **Documentation**: Comprehensive README with examples and troubleshooting
|
|
36
|
+
- **Test Coverage**: 45+ tests covering unit, integration, and performance scenarios
|
|
37
|
+
|
|
38
|
+
### Technical Improvements
|
|
39
|
+
- Maximum iteration limit to prevent infinite loops (100 iterations)
|
|
40
|
+
- Proactive circular dependency detection using graph analysis
|
|
41
|
+
- Enhanced resolver error handling with detailed logging
|
|
42
|
+
- Support for complex dependency chains with proper resolution order
|
|
43
|
+
- Async computed properties with caching and TTL support
|
|
44
|
+
- Modern TypeScript features and better type safety
|
|
45
|
+
|
|
46
|
+
## [1.0.2] - Previous
|
|
47
|
+
- Maintained fork with updated dependencies
|
|
48
|
+
|
|
49
|
+
## [1.0.1] - Previous
|
|
50
|
+
- Initial fork from original urql-computed-exchange
|
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org>
|
package/README.md
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# URQL Computed Exchange Plus
|
|
2
|
+
|
|
3
|
+
An [URQL](https://github.com/FormidableLabs/urql) exchange to compute data using resolvers and entities.
|
|
4
|
+
|
|
5
|
+
**This is a maintained fork of the original `urql-computed-exchange` with updated dependencies and modern tooling.**
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
$ npm i urql-computed-exchange-plus
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
First, create your entities and their resolvers:
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
// entities.js
|
|
19
|
+
import { createEntity, mergeEntities } from 'urql-computed-exchange-plus';
|
|
20
|
+
|
|
21
|
+
const Pokemon = createEntity('Pokemon', {
|
|
22
|
+
numberOfEvolutions: {
|
|
23
|
+
dependencies: gql`
|
|
24
|
+
fragment _ on Pokemon {
|
|
25
|
+
evolutions {
|
|
26
|
+
id
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
`,
|
|
30
|
+
resolver: (pokemon) => {
|
|
31
|
+
return (pokemon.evolutions && pokemon.evolutions.length) ?? 0;
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export default mergeEntities(Pokemon);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then, add it to the list of exchanges in URQL when setting up the client:
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
// client.js
|
|
43
|
+
|
|
44
|
+
import { computedExchange } from 'urql-computed-exchange-plus';
|
|
45
|
+
import {
|
|
46
|
+
createClient,
|
|
47
|
+
cacheExchange,
|
|
48
|
+
fetchExchange,
|
|
49
|
+
} from 'urql';
|
|
50
|
+
|
|
51
|
+
import entities from './entities';
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
const client = createClient({
|
|
55
|
+
url: 'https://graphql-pokemon.now.sh/',
|
|
56
|
+
exchanges: [
|
|
57
|
+
cacheExchange,
|
|
58
|
+
computedExchange({ entities }),
|
|
59
|
+
fetchExchange,
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export default client;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Finally, use the `@computed` directive when declaring your GraphQL queries. Don't forget to indicate the corresponding `type`:
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
// App.js
|
|
70
|
+
|
|
71
|
+
import React from 'react';
|
|
72
|
+
import { useQuery } from 'urql';
|
|
73
|
+
import gql from 'graphql-tag';
|
|
74
|
+
|
|
75
|
+
const PokemonQuery = gql`
|
|
76
|
+
query PokemonQuery {
|
|
77
|
+
pokemon(name: "charmander") {
|
|
78
|
+
id
|
|
79
|
+
name
|
|
80
|
+
numberOfEvolutions @computed(type: Pokemon)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
const App = () => {
|
|
86
|
+
const [ res ] = useQuery({
|
|
87
|
+
query: PokemonQuery,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (res.fetching) {
|
|
91
|
+
return 'Loading...';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<pre>
|
|
96
|
+
{JSON.stringify(res.data, null, 2)}
|
|
97
|
+
</pre>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export default App;
|
|
102
|
+
```
|
|
103
|
+
## Error Handling
|
|
104
|
+
|
|
105
|
+
The exchange handles various error scenarios gracefully:
|
|
106
|
+
|
|
107
|
+
### Circular Dependencies
|
|
108
|
+
```typescript
|
|
109
|
+
// This will throw an error instead of hanging
|
|
110
|
+
const entities = {
|
|
111
|
+
User: createEntity('User', {
|
|
112
|
+
fieldA: {
|
|
113
|
+
dependencies: gql`fragment _ on User { fieldB @computed(type: User) }`,
|
|
114
|
+
resolver: (user) => `A-${user.fieldB}`,
|
|
115
|
+
},
|
|
116
|
+
fieldB: {
|
|
117
|
+
dependencies: gql`fragment _ on User { fieldA @computed(type: User) }`,
|
|
118
|
+
resolver: (user) => `B-${user.fieldA}`,
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Resolver Errors
|
|
125
|
+
```typescript
|
|
126
|
+
const entities = {
|
|
127
|
+
User: createEntity('User', {
|
|
128
|
+
riskyField: {
|
|
129
|
+
dependencies: gql`fragment _ on User { someField }`,
|
|
130
|
+
resolver: (user) => {
|
|
131
|
+
if (!user.someField) {
|
|
132
|
+
throw new Error('Missing required field');
|
|
133
|
+
}
|
|
134
|
+
return user.someField.toUpperCase();
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
};
|
|
139
|
+
// If resolver throws, the field will be undefined instead of crashing the query
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Missing Dependencies
|
|
143
|
+
```typescript
|
|
144
|
+
const entities = {
|
|
145
|
+
User: createEntity('User', {
|
|
146
|
+
safeField: {
|
|
147
|
+
dependencies: gql`fragment _ on User { nonExistentField }`,
|
|
148
|
+
resolver: (user) => user.nonExistentField || 'fallback value',
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
};
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Integration with urql Exchanges
|
|
155
|
+
|
|
156
|
+
The computed exchange works seamlessly with urql's built-in exchanges:
|
|
157
|
+
|
|
158
|
+
### Basic Setup
|
|
159
|
+
```typescript
|
|
160
|
+
import { createClient, cacheExchange, fetchExchange } from 'urql';
|
|
161
|
+
import { computedExchange } from 'urql-computed-exchange-plus';
|
|
162
|
+
|
|
163
|
+
const client = createClient({
|
|
164
|
+
url: 'https://api.example.com/graphql',
|
|
165
|
+
exchanges: [
|
|
166
|
+
cacheExchange, // Works with caching
|
|
167
|
+
computedExchange({ entities }),
|
|
168
|
+
fetchExchange,
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### With Authentication Exchange
|
|
174
|
+
```typescript
|
|
175
|
+
import { authExchange } from '@urql/exchange-auth';
|
|
176
|
+
|
|
177
|
+
const client = createClient({
|
|
178
|
+
url: 'https://api.example.com/graphql',
|
|
179
|
+
exchanges: [
|
|
180
|
+
cacheExchange,
|
|
181
|
+
authExchange({
|
|
182
|
+
// auth config
|
|
183
|
+
}),
|
|
184
|
+
computedExchange({ entities }),
|
|
185
|
+
fetchExchange,
|
|
186
|
+
],
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### With Retry Exchange
|
|
191
|
+
```typescript
|
|
192
|
+
import { retryExchange } from '@urql/exchange-retry';
|
|
193
|
+
|
|
194
|
+
const client = createClient({
|
|
195
|
+
url: 'https://api.example.com/graphql',
|
|
196
|
+
exchanges: [
|
|
197
|
+
cacheExchange,
|
|
198
|
+
retryExchange({
|
|
199
|
+
initialDelayMs: 1000,
|
|
200
|
+
maxDelayMs: 15000,
|
|
201
|
+
randomDelay: true,
|
|
202
|
+
maxNumberAttempts: 2,
|
|
203
|
+
}),
|
|
204
|
+
computedExchange({ entities }),
|
|
205
|
+
fetchExchange,
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Dependency Resolution Behavior
|
|
211
|
+
|
|
212
|
+
The computed exchange resolves dependencies in the following order:
|
|
213
|
+
|
|
214
|
+
1. **Parse Query**: Identifies all `@computed` directives and their types
|
|
215
|
+
2. **Collect Dependencies**: Gathers all required fields from entity definitions
|
|
216
|
+
3. **Resolve Chain**: Processes computed properties in dependency order
|
|
217
|
+
4. **Circular Detection**: Prevents infinite loops with maximum iteration limits
|
|
218
|
+
5. **Error Handling**: Gracefully handles resolver failures
|
|
219
|
+
|
|
220
|
+
### Dependency Chain Example
|
|
221
|
+
```typescript
|
|
222
|
+
const entities = {
|
|
223
|
+
User: createEntity('User', {
|
|
224
|
+
// Level 1: Direct field access
|
|
225
|
+
fullName: {
|
|
226
|
+
dependencies: gql`fragment _ on User { firstName lastName }`,
|
|
227
|
+
resolver: (user) => `${user.firstName} ${user.lastName}`,
|
|
228
|
+
},
|
|
229
|
+
// Level 2: Depends on Level 1
|
|
230
|
+
displayName: {
|
|
231
|
+
dependencies: gql`fragment _ on User { fullName @computed(type: User) title }`,
|
|
232
|
+
resolver: (user) => `${user.title} ${user.fullName}`,
|
|
233
|
+
},
|
|
234
|
+
// Level 3: Depends on Level 2
|
|
235
|
+
greeting: {
|
|
236
|
+
dependencies: gql`fragment _ on User { displayName @computed(type: User) }`,
|
|
237
|
+
resolver: (user) => `Hello, ${user.displayName}!`,
|
|
238
|
+
},
|
|
239
|
+
}),
|
|
240
|
+
};
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Troubleshooting
|
|
244
|
+
|
|
245
|
+
### Common Issues
|
|
246
|
+
|
|
247
|
+
#### Issue: "Maximum iterations reached" Error
|
|
248
|
+
**Cause**: Circular dependencies in computed properties
|
|
249
|
+
**Solution**: Check your entity definitions for circular references
|
|
250
|
+
```typescript
|
|
251
|
+
// ❌ Bad: Circular dependency
|
|
252
|
+
const entities = {
|
|
253
|
+
User: createEntity('User', {
|
|
254
|
+
fieldA: {
|
|
255
|
+
dependencies: gql`fragment _ on User { fieldB @computed(type: User) }`,
|
|
256
|
+
resolver: (user) => user.fieldB,
|
|
257
|
+
},
|
|
258
|
+
fieldB: {
|
|
259
|
+
dependencies: gql`fragment _ on User { fieldA @computed(type: User) }`,
|
|
260
|
+
resolver: (user) => user.fieldA,
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// ✅ Good: Linear dependency chain
|
|
266
|
+
const entities = {
|
|
267
|
+
User: createEntity('User', {
|
|
268
|
+
fullName: {
|
|
269
|
+
dependencies: gql`fragment _ on User { firstName lastName }`,
|
|
270
|
+
resolver: (user) => `${user.firstName} ${user.lastName}`,
|
|
271
|
+
},
|
|
272
|
+
displayName: {
|
|
273
|
+
dependencies: gql`fragment _ on User { fullName @computed(type: User) title }`,
|
|
274
|
+
resolver: (user) => `${user.title} ${user.fullName}`,
|
|
275
|
+
},
|
|
276
|
+
}),
|
|
277
|
+
};
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### Issue: Computed Fields Return `undefined`
|
|
281
|
+
**Cause**: Missing dependencies or resolver errors
|
|
282
|
+
**Solution**: Check that all required fields are available and resolvers handle edge cases
|
|
283
|
+
```typescript
|
|
284
|
+
// ✅ Good: Handle missing data gracefully
|
|
285
|
+
const entities = {
|
|
286
|
+
User: createEntity('User', {
|
|
287
|
+
safeField: {
|
|
288
|
+
dependencies: gql`fragment _ on User { optionalField }`,
|
|
289
|
+
resolver: (user) => {
|
|
290
|
+
if (!user.optionalField) {
|
|
291
|
+
return 'Default Value';
|
|
292
|
+
}
|
|
293
|
+
return user.optionalField.toUpperCase();
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
}),
|
|
297
|
+
};
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
#### Issue: Performance Issues with Large Datasets
|
|
301
|
+
**Cause**: Complex computed property chains on many items
|
|
302
|
+
**Solution**: Optimize resolvers and consider caching
|
|
303
|
+
```typescript
|
|
304
|
+
// ✅ Good: Efficient resolver
|
|
305
|
+
const entities = {
|
|
306
|
+
User: createEntity('User', {
|
|
307
|
+
expensiveComputation: {
|
|
308
|
+
dependencies: gql`fragment _ on User { data }`,
|
|
309
|
+
resolver: (user) => {
|
|
310
|
+
// Cache expensive operations
|
|
311
|
+
if (user._cachedResult) return user._cachedResult;
|
|
312
|
+
const result = performExpensiveOperation(user.data);
|
|
313
|
+
user._cachedResult = result;
|
|
314
|
+
return result;
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
}),
|
|
318
|
+
};
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
#### Issue: TypeScript Type Errors
|
|
322
|
+
**Cause**: Incorrect type annotations or missing type definitions
|
|
323
|
+
**Solution**: Ensure proper typing for entities and resolvers
|
|
324
|
+
```typescript
|
|
325
|
+
interface User {
|
|
326
|
+
id: string;
|
|
327
|
+
firstName: string;
|
|
328
|
+
lastName: string;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const entities = {
|
|
332
|
+
User: createEntity('User', {
|
|
333
|
+
fullName: {
|
|
334
|
+
dependencies: gql`fragment _ on User { firstName lastName }`,
|
|
335
|
+
resolver: (user: User) => `${user.firstName} ${user.lastName}`,
|
|
336
|
+
},
|
|
337
|
+
}),
|
|
338
|
+
};
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Testing
|
|
342
|
+
|
|
343
|
+
The library includes comprehensive test coverage:
|
|
344
|
+
- Unit tests for core functionality
|
|
345
|
+
- Integration tests with urql's cache exchange
|
|
346
|
+
- Error handling and edge case testing
|
|
347
|
+
- Performance and circular dependency protection
|
|
348
|
+
|
|
349
|
+
Run tests:
|
|
350
|
+
```bash
|
|
351
|
+
npm test # Unit tests
|
|
352
|
+
npm run test:integration # Integration tests
|
|
353
|
+
npm run test:performance # Performance tests
|
|
354
|
+
npm run test:all # All tests
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Changelog
|
|
358
|
+
|
|
359
|
+
See [CHANGELOG.md](./CHANGELOG.md) for version history and breaking changes.
|
|
360
|
+
|
|
361
|
+
## Contributing
|
|
362
|
+
|
|
363
|
+
This is a maintained fork. Contributions are welcome! Please ensure tests pass and add tests for new features.
|
|
364
|
+
|
|
365
|
+
## License
|
|
366
|
+
|
|
367
|
+
MIT
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
preset: 'ts-jest',
|
|
5
|
+
verbose: false,
|
|
6
|
+
testEnvironment: 'node',
|
|
7
|
+
rootDir: path.resolve(__dirname, 'src'),
|
|
8
|
+
coverageDirectory: './coverage',
|
|
9
|
+
moduleFileExtensions: ['js', 'json', 'ts'],
|
|
10
|
+
testRegex: '\\.test\\.ts$',
|
|
11
|
+
transform: {
|
|
12
|
+
'^.+\\.(t|j)s$': 'ts-jest',
|
|
13
|
+
},
|
|
14
|
+
collectCoverageFrom: [
|
|
15
|
+
'**/*.(t|j)s',
|
|
16
|
+
'!**/*.test.(t|j)s',
|
|
17
|
+
'!**/node_modules/**',
|
|
18
|
+
],
|
|
19
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
preset: 'ts-jest',
|
|
5
|
+
verbose: false,
|
|
6
|
+
testEnvironment: 'node',
|
|
7
|
+
rootDir: path.resolve(__dirname, 'test'),
|
|
8
|
+
coverageDirectory: './coverage',
|
|
9
|
+
moduleFileExtensions: ['js', 'json', 'ts'],
|
|
10
|
+
testRegex: '\\.test\\.ts$',
|
|
11
|
+
transform: {
|
|
12
|
+
'^.+\\.(t|j)s$': 'ts-jest',
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
preset: 'ts-jest',
|
|
5
|
+
verbose: false,
|
|
6
|
+
testEnvironment: 'node',
|
|
7
|
+
rootDir: path.resolve(__dirname),
|
|
8
|
+
coverageDirectory: './coverage',
|
|
9
|
+
moduleFileExtensions: ['js', 'json', 'ts'],
|
|
10
|
+
testRegex: 'test/performance/.*\\.test\\.ts$',
|
|
11
|
+
transform: {
|
|
12
|
+
'^.+\\.(t|j)s$': 'ts-jest',
|
|
13
|
+
},
|
|
14
|
+
collectCoverageFrom: [
|
|
15
|
+
'src/**/*.(t|j)s',
|
|
16
|
+
'!src/**/*.test.(t|j)s',
|
|
17
|
+
'!**/node_modules/**',
|
|
18
|
+
],
|
|
19
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DocumentNode } from 'graphql';
|
|
2
|
+
import { GraphQLObject } from './types';
|
|
3
|
+
export type AsyncComputedResolver<T extends GraphQLObject = GraphQLObject, R = any> = (data: T) => Promise<R>;
|
|
4
|
+
export interface AsyncComputedProperty<T extends GraphQLObject = GraphQLObject, R = any> {
|
|
5
|
+
dependencies: DocumentNode;
|
|
6
|
+
resolver: AsyncComputedResolver<T, R>;
|
|
7
|
+
cacheKey?: (data: T) => string;
|
|
8
|
+
ttl?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveAsyncComputedProperties<T extends GraphQLObject>(data: T, properties: Record<string, AsyncComputedProperty<T>>): Promise<Partial<T>>;
|
|
11
|
+
export declare function createAsyncEntity<T extends GraphQLObject = GraphQLObject>(typeName: string, properties: Record<string, AsyncComputedProperty<T>>): {
|
|
12
|
+
__typename: string;
|
|
13
|
+
asyncComputedProperties: Record<string, AsyncComputedProperty<T, any>>;
|
|
14
|
+
resolve: (data: T) => Promise<Partial<T>>;
|
|
15
|
+
};
|
|
16
|
+
export declare function clearAsyncComputedCache(): void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveAsyncComputedProperties = resolveAsyncComputedProperties;
|
|
4
|
+
exports.createAsyncEntity = createAsyncEntity;
|
|
5
|
+
exports.clearAsyncComputedCache = clearAsyncComputedCache;
|
|
6
|
+
class AsyncComputedCache {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.cache = new Map();
|
|
9
|
+
}
|
|
10
|
+
set(key, value, ttl = 5 * 60 * 1000) {
|
|
11
|
+
this.cache.set(key, {
|
|
12
|
+
value,
|
|
13
|
+
timestamp: Date.now(),
|
|
14
|
+
ttl,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
get(key) {
|
|
18
|
+
const entry = this.cache.get(key);
|
|
19
|
+
if (!entry)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (Date.now() - entry.timestamp > entry.ttl) {
|
|
22
|
+
this.cache.delete(key);
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
return entry.value;
|
|
26
|
+
}
|
|
27
|
+
clear() {
|
|
28
|
+
this.cache.clear();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const asyncCache = new AsyncComputedCache();
|
|
32
|
+
async function resolveAsyncComputedProperties(data, properties) {
|
|
33
|
+
const results = {};
|
|
34
|
+
const promises = Object.entries(properties).map(async ([key, property]) => {
|
|
35
|
+
try {
|
|
36
|
+
const cacheKey = property.cacheKey
|
|
37
|
+
? `${data.__typename}:${key}:${property.cacheKey(data)}`
|
|
38
|
+
: `${data.__typename}:${key}:${JSON.stringify(data)}`;
|
|
39
|
+
const cached = asyncCache.get(cacheKey);
|
|
40
|
+
if (cached !== undefined) {
|
|
41
|
+
return [key, cached];
|
|
42
|
+
}
|
|
43
|
+
const result = await property.resolver(data);
|
|
44
|
+
asyncCache.set(cacheKey, result, property.ttl);
|
|
45
|
+
return [key, result];
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.warn(`Async computed property "${key}" failed:`, error);
|
|
49
|
+
return [key, undefined];
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const resolvedEntries = await Promise.all(promises);
|
|
53
|
+
for (const [key, value] of resolvedEntries) {
|
|
54
|
+
results[key] = value;
|
|
55
|
+
}
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
function createAsyncEntity(typeName, properties) {
|
|
59
|
+
return {
|
|
60
|
+
__typename: typeName,
|
|
61
|
+
asyncComputedProperties: properties,
|
|
62
|
+
resolve: (data) => resolveAsyncComputedProperties(data, properties),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function clearAsyncComputedCache() {
|
|
66
|
+
asyncCache.clear();
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=async-computed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-computed.js","sourceRoot":"","sources":["../src/async-computed.ts"],"names":[],"mappings":";;AAwDA,wEAuCC;AAKD,8CASC;AAKD,0DAEC;AA7FD,MAAM,kBAAkB;IAAxB;QACU,UAAK,GAAG,IAAI,GAAG,EAA0D,CAAC;IAyBpF,CAAC;IAvBC,GAAG,CAAC,GAAW,EAAE,KAAU,EAAE,MAAc,CAAC,GAAG,EAAE,GAAG,IAAI;QACtD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE;YAClB,KAAK;YACL,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,GAAG;SACJ,CAAC,CAAC;IACL,CAAC;IAED,GAAG,CAAC,GAAW;QACb,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAE7B,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC;YAC7C,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,OAAO,KAAK,CAAC,KAAK,CAAC;IACrB,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;CACF;AAED,MAAM,UAAU,GAAG,IAAI,kBAAkB,EAAE,CAAC;AAKrC,KAAK,UAAU,8BAA8B,CAClD,IAAO,EACP,UAAoD;IAEpD,MAAM,OAAO,GAAe,EAAE,CAAC;IAE/B,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,EAAE;QACxE,IAAI,CAAC;YAEH,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ;gBAChC,CAAC,CAAC,GAAG,IAAI,CAAC,UAAU,IAAI,GAAG,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;gBACxD,CAAC,CAAC,GAAG,IAAI,CAAC,UAAU,IAAI,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;YAGxD,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACxC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YACvB,CAAC;YAGD,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YAG7C,UAAU,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC;YAE/C,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACvB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,4BAA4B,GAAG,WAAW,EAAE,KAAK,CAAC,CAAC;YAChE,OAAO,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,eAAe,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAEpD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,eAAe,EAAE,CAAC;QAC1C,OAAe,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IAChC,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAKD,SAAgB,iBAAiB,CAC/B,QAAgB,EAChB,UAAoD;IAEpD,OAAO;QACL,UAAU,EAAE,QAAQ;QACpB,uBAAuB,EAAE,UAAU;QACnC,OAAO,EAAE,CAAC,IAAO,EAAE,EAAE,CAAC,8BAA8B,CAAC,IAAI,EAAE,UAAU,CAAC;KACvE,CAAC;AACJ,CAAC;AAKD,SAAgB,uBAAuB;IACrC,UAAU,CAAC,KAAK,EAAE,CAAC;AACrB,CAAC"}
|