Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.

feat(toe): Add @urql/exchange-throw-on-error (#3677)

Co-authored-by: Jovi De Croock <decroockjovi@gmail.com>

authored by

Iha Shin (신의하)
Jovi De Croock
and committed by
GitHub
7d9302bb b971fd00

+409
+5
.changeset/neat-pandas-punch.md
··· 1 + --- 2 + "@urql/exchange-throw-on-error": minor 3 + --- 4 + 5 + Initial release
+15
exchanges/throw-on-error/README.md
··· 1 + # @urql/exchange-throw-on-error (Exchange factory) 2 + 3 + `@urql/exchange-throw-on-error` is an exchange for the [`urql`](../../README.md) GraphQL client that makes field access to data throw an error if the field errored. 4 + 5 + It is built on top of the [`graphql-toe`](https://github.com/graphile/graphql-toe) package. 6 + 7 + ## Quick Start Guide 8 + 9 + First install `@urql/exchange-throw-on-error` alongside `urql`: 10 + 11 + ```sh 12 + yarn add @urql/exchange-throw-on-error 13 + # or 14 + npm install --save @urql/exchange-throw-on-error 15 + ```
+15
exchanges/throw-on-error/jsr.json
··· 1 + { 2 + "name": "@urql/exchange-throw-on-error", 3 + "version": "0.0.0", 4 + "exports": { 5 + ".": "./src/index.ts" 6 + }, 7 + "exclude": [ 8 + "node_modules", 9 + "cypress", 10 + "**/*.test.*", 11 + "**/*.spec.*", 12 + "**/*.test.*.snap", 13 + "**/*.spec.*.snap" 14 + ] 15 + }
+66
exchanges/throw-on-error/package.json
··· 1 + { 2 + "name": "@urql/exchange-throw-on-error", 3 + "version": "0.0.0", 4 + "description": "An exchange for throw-on-error support in urql", 5 + "sideEffects": false, 6 + "homepage": "https://formidable.com/open-source/urql/docs/", 7 + "bugs": "https://github.com/urql-graphql/urql/issues", 8 + "license": "MIT", 9 + "author": "urql GraphQL Contributors", 10 + "repository": { 11 + "type": "git", 12 + "url": "https://github.com/urql-graphql/urql.git", 13 + "directory": "exchanges/throw-on-error" 14 + }, 15 + "keywords": [ 16 + "urql", 17 + "graphql client", 18 + "graphql", 19 + "exchanges", 20 + "throw on error" 21 + ], 22 + "main": "dist/urql-exchange-throw-on-error", 23 + "module": "dist/urql-exchange-throw-on-error.mjs", 24 + "types": "dist/urql-exchange-throw-on-error.d.ts", 25 + "source": "src/index.ts", 26 + "exports": { 27 + ".": { 28 + "types": "./dist/urql-exchange-throw-on-error.d.ts", 29 + "import": "./dist/urql-exchange-throw-on-error.mjs", 30 + "require": "./dist/urql-exchange-throw-on-error.js", 31 + "source": "./src/index.ts" 32 + }, 33 + "./package.json": "./package.json" 34 + }, 35 + "files": [ 36 + "LICENSE", 37 + "CHANGELOG.md", 38 + "README.md", 39 + "dist/" 40 + ], 41 + "scripts": { 42 + "test": "vitest", 43 + "clean": "rimraf dist", 44 + "check": "tsc --noEmit", 45 + "lint": "eslint --ext=js,jsx,ts,tsx .", 46 + "build": "rollup -c ../../scripts/rollup/config.mjs", 47 + "prepare": "node ../../scripts/prepare/index.js", 48 + "prepublishOnly": "run-s clean build" 49 + }, 50 + "devDependencies": { 51 + "@urql/core": "workspace:*", 52 + "graphql": "^16.0.0" 53 + }, 54 + "peerDependencies": { 55 + "@urql/core": "^5.0.0" 56 + }, 57 + "dependencies": { 58 + "@urql/core": "^5.0.0", 59 + "graphql-toe": "0.1.2", 60 + "wonka": "^6.3.2" 61 + }, 62 + "publishConfig": { 63 + "access": "public", 64 + "provenance": true 65 + } 66 + }
+1
exchanges/throw-on-error/src/index.ts
··· 1 + export { throwOnErrorExchange } from './throwOnErrorExchange';
+259
exchanges/throw-on-error/src/throwOnErrorExchange.test.ts
··· 1 + import { pipe, map, fromValue, toPromise, take } from 'wonka'; 2 + import { vi, expect, it, beforeEach } from 'vitest'; 3 + import { GraphQLError } from 'graphql'; 4 + 5 + import { 6 + gql, 7 + createClient, 8 + Operation, 9 + ExchangeIO, 10 + Client, 11 + CombinedError, 12 + } from '@urql/core'; 13 + 14 + import { throwOnErrorExchange } from './throwOnErrorExchange'; 15 + 16 + const dispatchDebug = vi.fn(); 17 + 18 + const query = gql` 19 + { 20 + topLevel 21 + topLevelList 22 + object { 23 + inner 24 + } 25 + objectList { 26 + inner 27 + } 28 + } 29 + `; 30 + const mockData = { 31 + topLevel: 'topLevel', 32 + topLevelList: ['topLevelList'], 33 + object: { inner: 'inner' }, 34 + objectList: [{ inner: 'inner' }], 35 + }; 36 + 37 + let client: Client, op: Operation; 38 + beforeEach(() => { 39 + client = createClient({ 40 + url: 'http://0.0.0.0', 41 + exchanges: [], 42 + }); 43 + op = client.createRequestOperation('query', { key: 1, query, variables: {} }); 44 + }); 45 + 46 + it('throws on top level field error', async () => { 47 + const forward: ExchangeIO = ops$ => 48 + pipe( 49 + ops$, 50 + map( 51 + operation => 52 + ({ 53 + operation, 54 + data: { 55 + ...mockData, 56 + topLevel: null, 57 + }, 58 + error: new CombinedError({ 59 + graphQLErrors: [ 60 + new GraphQLError('top level error', { path: ['topLevel'] }), 61 + ], 62 + }), 63 + }) as any 64 + ) 65 + ); 66 + 67 + const res = await pipe( 68 + fromValue(op), 69 + throwOnErrorExchange()({ forward, client, dispatchDebug }), 70 + take(1), 71 + toPromise 72 + ); 73 + 74 + expect(() => res.data?.topLevel).toThrow('top level error'); 75 + expect(() => res.data).not.toThrow(); 76 + expect(() => res.data?.topLevelList[0]).not.toThrow(); 77 + }); 78 + 79 + it('throws on top level list element error', async () => { 80 + const forward: ExchangeIO = ops$ => 81 + pipe( 82 + ops$, 83 + map( 84 + operation => 85 + ({ 86 + operation, 87 + data: { 88 + ...mockData, 89 + topLevelList: ['topLevelList', null], 90 + }, 91 + error: new CombinedError({ 92 + graphQLErrors: [ 93 + new GraphQLError('top level list error', { 94 + path: ['topLevelList', 1], 95 + }), 96 + ], 97 + }), 98 + }) as any 99 + ) 100 + ); 101 + 102 + const res = await pipe( 103 + fromValue(op), 104 + throwOnErrorExchange()({ forward, client, dispatchDebug }), 105 + take(1), 106 + toPromise 107 + ); 108 + 109 + expect(() => res.data?.topLevelList[1]).toThrow('top level list error'); 110 + expect(() => res.data).not.toThrow(); 111 + expect(() => res.data?.topLevelList[0]).not.toThrow(); 112 + }); 113 + 114 + it('throws on object field error', async () => { 115 + const forward: ExchangeIO = ops$ => 116 + pipe( 117 + ops$, 118 + map( 119 + operation => 120 + ({ 121 + operation, 122 + data: { 123 + ...mockData, 124 + object: null, 125 + }, 126 + error: new CombinedError({ 127 + graphQLErrors: [ 128 + new GraphQLError('object field error', { path: ['object'] }), 129 + ], 130 + }), 131 + }) as any 132 + ) 133 + ); 134 + 135 + const res = await pipe( 136 + fromValue(op), 137 + throwOnErrorExchange()({ forward, client, dispatchDebug }), 138 + take(1), 139 + toPromise 140 + ); 141 + 142 + expect(() => res.data?.object).toThrow('object field error'); 143 + expect(() => res.data?.object.inner).toThrow('object field error'); 144 + expect(() => res.data).not.toThrow(); 145 + expect(() => res.data?.topLevel).not.toThrow(); 146 + }); 147 + 148 + it('throws on object inner field error', async () => { 149 + const forward: ExchangeIO = ops$ => 150 + pipe( 151 + ops$, 152 + map( 153 + operation => 154 + ({ 155 + operation, 156 + data: { 157 + ...mockData, 158 + object: { 159 + inner: null, 160 + }, 161 + }, 162 + error: new CombinedError({ 163 + graphQLErrors: [ 164 + new GraphQLError('object inner field error', { 165 + path: ['object', 'inner'], 166 + }), 167 + ], 168 + }), 169 + }) as any 170 + ) 171 + ); 172 + 173 + const res = await pipe( 174 + fromValue(op), 175 + throwOnErrorExchange()({ forward, client, dispatchDebug }), 176 + take(1), 177 + toPromise 178 + ); 179 + 180 + expect(() => res.data?.object.inner).toThrow('object inner field error'); 181 + expect(() => res.data).not.toThrow(); 182 + expect(() => res.data?.object).not.toThrow(); 183 + }); 184 + 185 + it('throws on object list field error', async () => { 186 + const forward: ExchangeIO = ops$ => 187 + pipe( 188 + ops$, 189 + map( 190 + operation => 191 + ({ 192 + operation, 193 + data: { 194 + ...mockData, 195 + objectList: null, 196 + }, 197 + error: new CombinedError({ 198 + graphQLErrors: [ 199 + new GraphQLError('object list field error', { 200 + path: ['objectList'], 201 + }), 202 + ], 203 + }), 204 + }) as any 205 + ) 206 + ); 207 + 208 + const res = await pipe( 209 + fromValue(op), 210 + throwOnErrorExchange()({ forward, client, dispatchDebug }), 211 + take(1), 212 + toPromise 213 + ); 214 + 215 + expect(() => res.data?.objectList).toThrow('object list field error'); 216 + expect(() => res.data?.objectList[0]).toThrow('object list field error'); 217 + expect(() => res.data?.objectList[0].inner).toThrow( 218 + 'object list field error' 219 + ); 220 + expect(() => res.data).not.toThrow(); 221 + expect(() => res.data?.topLevel).not.toThrow(); 222 + }); 223 + 224 + it('throws on object inner field error', async () => { 225 + const forward: ExchangeIO = ops$ => 226 + pipe( 227 + ops$, 228 + map( 229 + operation => 230 + ({ 231 + operation, 232 + data: { 233 + ...mockData, 234 + objectList: [{ inner: 'inner' }, { inner: null }], 235 + }, 236 + error: new CombinedError({ 237 + graphQLErrors: [ 238 + new GraphQLError('object list inner field error', { 239 + path: ['objectList', 1, 'inner'], 240 + }), 241 + ], 242 + }), 243 + }) as any 244 + ) 245 + ); 246 + 247 + const res = await pipe( 248 + fromValue(op), 249 + throwOnErrorExchange()({ forward, client, dispatchDebug }), 250 + take(1), 251 + toPromise 252 + ); 253 + 254 + expect(() => res.data?.objectList[1].inner).toThrow( 255 + 'object list inner field error' 256 + ); 257 + expect(() => res.data).not.toThrow(); 258 + expect(() => res.data?.objectList[0].inner).not.toThrow(); 259 + });
+19
exchanges/throw-on-error/src/throwOnErrorExchange.ts
··· 1 + import type { Exchange } from '@urql/core'; 2 + import { mapExchange } from '@urql/core'; 3 + import { toe } from 'graphql-toe'; 4 + 5 + /** Exchange factory that maps the fields of the data to throw an error on access if the field was errored. 6 + * 7 + * @returns the created throw-on-error {@link Exchange}. 8 + */ 9 + export const throwOnErrorExchange = (): Exchange => { 10 + return mapExchange({ 11 + onResult(result) { 12 + if (result.data) { 13 + const errors = result.error && result.error.graphQLErrors; 14 + result.data = toe({ data: result.data, errors }); 15 + } 16 + return result; 17 + }, 18 + }); 19 + };
+4
exchanges/throw-on-error/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json", 3 + "include": ["src"] 4 + }
+4
exchanges/throw-on-error/vitest.config.ts
··· 1 + import { mergeConfig } from 'vitest/config'; 2 + import baseConfig from '../../vitest.config'; 3 + 4 + export default mergeConfig(baseConfig, {});
+21
pnpm-lock.yaml
··· 321 321 specifier: ^16.6.0 322 322 version: 16.6.0 323 323 324 + exchanges/throw-on-error: 325 + dependencies: 326 + '@urql/core': 327 + specifier: ^5.0.0 328 + version: 5.0.6(graphql@16.6.0) 329 + graphql-toe: 330 + specifier: 0.1.2 331 + version: 0.1.2 332 + wonka: 333 + specifier: ^6.3.2 334 + version: 6.3.2 335 + devDependencies: 336 + graphql: 337 + specifier: ^16.6.0 338 + version: 16.6.0 339 + 324 340 packages/core: 325 341 dependencies: 326 342 '@0no-co/graphql.web': ··· 5366 5382 5367 5383 graphemer@1.4.0: 5368 5384 resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} 5385 + 5386 + graphql-toe@0.1.2: 5387 + resolution: {integrity: sha512-XK04wXEHbLY33YHoPAnLMIafRKSOn7FTWzTCob23GC6o8DnO4ibkA8Aje+Udee8QdXx46TV6m6LQM9iU8C9vwQ==} 5369 5388 5370 5389 graphql@16.6.0: 5371 5390 resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==} ··· 17176 17195 graceful-fs@4.2.11: {} 17177 17196 17178 17197 graphemer@1.4.0: {} 17198 + 17199 + graphql-toe@0.1.2: {} 17179 17200 17180 17201 graphql@16.6.0: {} 17181 17202