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>

+5
.changeset/neat-pandas-punch.md
···
+
---
+
"@urql/exchange-throw-on-error": minor
+
---
+
+
Initial release
+15
exchanges/throw-on-error/README.md
···
+
# @urql/exchange-throw-on-error (Exchange factory)
+
+
`@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.
+
+
It is built on top of the [`graphql-toe`](https://github.com/graphile/graphql-toe) package.
+
+
## Quick Start Guide
+
+
First install `@urql/exchange-throw-on-error` alongside `urql`:
+
+
```sh
+
yarn add @urql/exchange-throw-on-error
+
# or
+
npm install --save @urql/exchange-throw-on-error
+
```
+15
exchanges/throw-on-error/jsr.json
···
+
{
+
"name": "@urql/exchange-throw-on-error",
+
"version": "0.0.0",
+
"exports": {
+
".": "./src/index.ts"
+
},
+
"exclude": [
+
"node_modules",
+
"cypress",
+
"**/*.test.*",
+
"**/*.spec.*",
+
"**/*.test.*.snap",
+
"**/*.spec.*.snap"
+
]
+
}
+66
exchanges/throw-on-error/package.json
···
+
{
+
"name": "@urql/exchange-throw-on-error",
+
"version": "0.0.0",
+
"description": "An exchange for throw-on-error support in urql",
+
"sideEffects": false,
+
"homepage": "https://formidable.com/open-source/urql/docs/",
+
"bugs": "https://github.com/urql-graphql/urql/issues",
+
"license": "MIT",
+
"author": "urql GraphQL Contributors",
+
"repository": {
+
"type": "git",
+
"url": "https://github.com/urql-graphql/urql.git",
+
"directory": "exchanges/throw-on-error"
+
},
+
"keywords": [
+
"urql",
+
"graphql client",
+
"graphql",
+
"exchanges",
+
"throw on error"
+
],
+
"main": "dist/urql-exchange-throw-on-error",
+
"module": "dist/urql-exchange-throw-on-error.mjs",
+
"types": "dist/urql-exchange-throw-on-error.d.ts",
+
"source": "src/index.ts",
+
"exports": {
+
".": {
+
"types": "./dist/urql-exchange-throw-on-error.d.ts",
+
"import": "./dist/urql-exchange-throw-on-error.mjs",
+
"require": "./dist/urql-exchange-throw-on-error.js",
+
"source": "./src/index.ts"
+
},
+
"./package.json": "./package.json"
+
},
+
"files": [
+
"LICENSE",
+
"CHANGELOG.md",
+
"README.md",
+
"dist/"
+
],
+
"scripts": {
+
"test": "vitest",
+
"clean": "rimraf dist",
+
"check": "tsc --noEmit",
+
"lint": "eslint --ext=js,jsx,ts,tsx .",
+
"build": "rollup -c ../../scripts/rollup/config.mjs",
+
"prepare": "node ../../scripts/prepare/index.js",
+
"prepublishOnly": "run-s clean build"
+
},
+
"devDependencies": {
+
"@urql/core": "workspace:*",
+
"graphql": "^16.0.0"
+
},
+
"peerDependencies": {
+
"@urql/core": "^5.0.0"
+
},
+
"dependencies": {
+
"@urql/core": "^5.0.0",
+
"graphql-toe": "0.1.2",
+
"wonka": "^6.3.2"
+
},
+
"publishConfig": {
+
"access": "public",
+
"provenance": true
+
}
+
}
+1
exchanges/throw-on-error/src/index.ts
···
+
export { throwOnErrorExchange } from './throwOnErrorExchange';
+259
exchanges/throw-on-error/src/throwOnErrorExchange.test.ts
···
+
import { pipe, map, fromValue, toPromise, take } from 'wonka';
+
import { vi, expect, it, beforeEach } from 'vitest';
+
import { GraphQLError } from 'graphql';
+
+
import {
+
gql,
+
createClient,
+
Operation,
+
ExchangeIO,
+
Client,
+
CombinedError,
+
} from '@urql/core';
+
+
import { throwOnErrorExchange } from './throwOnErrorExchange';
+
+
const dispatchDebug = vi.fn();
+
+
const query = gql`
+
{
+
topLevel
+
topLevelList
+
object {
+
inner
+
}
+
objectList {
+
inner
+
}
+
}
+
`;
+
const mockData = {
+
topLevel: 'topLevel',
+
topLevelList: ['topLevelList'],
+
object: { inner: 'inner' },
+
objectList: [{ inner: 'inner' }],
+
};
+
+
let client: Client, op: Operation;
+
beforeEach(() => {
+
client = createClient({
+
url: 'http://0.0.0.0',
+
exchanges: [],
+
});
+
op = client.createRequestOperation('query', { key: 1, query, variables: {} });
+
});
+
+
it('throws on top level field error', async () => {
+
const forward: ExchangeIO = ops$ =>
+
pipe(
+
ops$,
+
map(
+
operation =>
+
({
+
operation,
+
data: {
+
...mockData,
+
topLevel: null,
+
},
+
error: new CombinedError({
+
graphQLErrors: [
+
new GraphQLError('top level error', { path: ['topLevel'] }),
+
],
+
}),
+
}) as any
+
)
+
);
+
+
const res = await pipe(
+
fromValue(op),
+
throwOnErrorExchange()({ forward, client, dispatchDebug }),
+
take(1),
+
toPromise
+
);
+
+
expect(() => res.data?.topLevel).toThrow('top level error');
+
expect(() => res.data).not.toThrow();
+
expect(() => res.data?.topLevelList[0]).not.toThrow();
+
});
+
+
it('throws on top level list element error', async () => {
+
const forward: ExchangeIO = ops$ =>
+
pipe(
+
ops$,
+
map(
+
operation =>
+
({
+
operation,
+
data: {
+
...mockData,
+
topLevelList: ['topLevelList', null],
+
},
+
error: new CombinedError({
+
graphQLErrors: [
+
new GraphQLError('top level list error', {
+
path: ['topLevelList', 1],
+
}),
+
],
+
}),
+
}) as any
+
)
+
);
+
+
const res = await pipe(
+
fromValue(op),
+
throwOnErrorExchange()({ forward, client, dispatchDebug }),
+
take(1),
+
toPromise
+
);
+
+
expect(() => res.data?.topLevelList[1]).toThrow('top level list error');
+
expect(() => res.data).not.toThrow();
+
expect(() => res.data?.topLevelList[0]).not.toThrow();
+
});
+
+
it('throws on object field error', async () => {
+
const forward: ExchangeIO = ops$ =>
+
pipe(
+
ops$,
+
map(
+
operation =>
+
({
+
operation,
+
data: {
+
...mockData,
+
object: null,
+
},
+
error: new CombinedError({
+
graphQLErrors: [
+
new GraphQLError('object field error', { path: ['object'] }),
+
],
+
}),
+
}) as any
+
)
+
);
+
+
const res = await pipe(
+
fromValue(op),
+
throwOnErrorExchange()({ forward, client, dispatchDebug }),
+
take(1),
+
toPromise
+
);
+
+
expect(() => res.data?.object).toThrow('object field error');
+
expect(() => res.data?.object.inner).toThrow('object field error');
+
expect(() => res.data).not.toThrow();
+
expect(() => res.data?.topLevel).not.toThrow();
+
});
+
+
it('throws on object inner field error', async () => {
+
const forward: ExchangeIO = ops$ =>
+
pipe(
+
ops$,
+
map(
+
operation =>
+
({
+
operation,
+
data: {
+
...mockData,
+
object: {
+
inner: null,
+
},
+
},
+
error: new CombinedError({
+
graphQLErrors: [
+
new GraphQLError('object inner field error', {
+
path: ['object', 'inner'],
+
}),
+
],
+
}),
+
}) as any
+
)
+
);
+
+
const res = await pipe(
+
fromValue(op),
+
throwOnErrorExchange()({ forward, client, dispatchDebug }),
+
take(1),
+
toPromise
+
);
+
+
expect(() => res.data?.object.inner).toThrow('object inner field error');
+
expect(() => res.data).not.toThrow();
+
expect(() => res.data?.object).not.toThrow();
+
});
+
+
it('throws on object list field error', async () => {
+
const forward: ExchangeIO = ops$ =>
+
pipe(
+
ops$,
+
map(
+
operation =>
+
({
+
operation,
+
data: {
+
...mockData,
+
objectList: null,
+
},
+
error: new CombinedError({
+
graphQLErrors: [
+
new GraphQLError('object list field error', {
+
path: ['objectList'],
+
}),
+
],
+
}),
+
}) as any
+
)
+
);
+
+
const res = await pipe(
+
fromValue(op),
+
throwOnErrorExchange()({ forward, client, dispatchDebug }),
+
take(1),
+
toPromise
+
);
+
+
expect(() => res.data?.objectList).toThrow('object list field error');
+
expect(() => res.data?.objectList[0]).toThrow('object list field error');
+
expect(() => res.data?.objectList[0].inner).toThrow(
+
'object list field error'
+
);
+
expect(() => res.data).not.toThrow();
+
expect(() => res.data?.topLevel).not.toThrow();
+
});
+
+
it('throws on object inner field error', async () => {
+
const forward: ExchangeIO = ops$ =>
+
pipe(
+
ops$,
+
map(
+
operation =>
+
({
+
operation,
+
data: {
+
...mockData,
+
objectList: [{ inner: 'inner' }, { inner: null }],
+
},
+
error: new CombinedError({
+
graphQLErrors: [
+
new GraphQLError('object list inner field error', {
+
path: ['objectList', 1, 'inner'],
+
}),
+
],
+
}),
+
}) as any
+
)
+
);
+
+
const res = await pipe(
+
fromValue(op),
+
throwOnErrorExchange()({ forward, client, dispatchDebug }),
+
take(1),
+
toPromise
+
);
+
+
expect(() => res.data?.objectList[1].inner).toThrow(
+
'object list inner field error'
+
);
+
expect(() => res.data).not.toThrow();
+
expect(() => res.data?.objectList[0].inner).not.toThrow();
+
});
+19
exchanges/throw-on-error/src/throwOnErrorExchange.ts
···
+
import type { Exchange } from '@urql/core';
+
import { mapExchange } from '@urql/core';
+
import { toe } from 'graphql-toe';
+
+
/** Exchange factory that maps the fields of the data to throw an error on access if the field was errored.
+
*
+
* @returns the created throw-on-error {@link Exchange}.
+
*/
+
export const throwOnErrorExchange = (): Exchange => {
+
return mapExchange({
+
onResult(result) {
+
if (result.data) {
+
const errors = result.error && result.error.graphQLErrors;
+
result.data = toe({ data: result.data, errors });
+
}
+
return result;
+
},
+
});
+
};
+4
exchanges/throw-on-error/tsconfig.json
···
+
{
+
"extends": "../../tsconfig.json",
+
"include": ["src"]
+
}
+4
exchanges/throw-on-error/vitest.config.ts
···
+
import { mergeConfig } from 'vitest/config';
+
import baseConfig from '../../vitest.config';
+
+
export default mergeConfig(baseConfig, {});
+21
pnpm-lock.yaml
···
specifier: ^16.6.0
version: 16.6.0
+
exchanges/throw-on-error:
+
dependencies:
+
'@urql/core':
+
specifier: ^5.0.0
+
version: 5.0.6(graphql@16.6.0)
+
graphql-toe:
+
specifier: 0.1.2
+
version: 0.1.2
+
wonka:
+
specifier: ^6.3.2
+
version: 6.3.2
+
devDependencies:
+
graphql:
+
specifier: ^16.6.0
+
version: 16.6.0
+
packages/core:
dependencies:
'@0no-co/graphql.web':
···
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+
+
graphql-toe@0.1.2:
+
resolution: {integrity: sha512-XK04wXEHbLY33YHoPAnLMIafRKSOn7FTWzTCob23GC6o8DnO4ibkA8Aje+Udee8QdXx46TV6m6LQM9iU8C9vwQ==}
graphql@16.6.0:
resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==}
···
graceful-fs@4.2.11: {}
graphemer@1.4.0: {}
+
+
graphql-toe@0.1.2: {}
graphql@16.6.0: {}