Skip to content

Commit

Permalink
feat(toe): Add @urql/exchange-throw-on-error
Browse files Browse the repository at this point in the history
  • Loading branch information
XiNiHa committed Sep 19, 2024
1 parent 6408474 commit 2169190
Show file tree
Hide file tree
Showing 9 changed files with 552 additions and 133 deletions.
15 changes: 15 additions & 0 deletions exchanges/throw-on-error/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# @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 was 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 changes: 15 additions & 0 deletions exchanges/throw-on-error/jsr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@urql/exchange-throw-on-error",
"version": "0.0.0",
"exports": {
".": "./src/index.ts"
},
"exclude": [
"node_modules",
"cypress",
"**/*.test.*",
"**/*.spec.*",
"**/*.test.*.snap",
"**/*.spec.*.snap"
]
}
67 changes: 67 additions & 0 deletions exchanges/throw-on-error/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"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.9.0",
"vitest": "^2.1.1"
},
"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 change: 1 addition & 0 deletions exchanges/throw-on-error/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { throwOnErrorExchange } from './throwOnErrorExchange';
259 changes: 259 additions & 0 deletions exchanges/throw-on-error/src/throwOnErrorExchange.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
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 changes: 19 additions & 0 deletions exchanges/throw-on-error/src/throwOnErrorExchange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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 changes: 4 additions & 0 deletions exchanges/throw-on-error/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["src"]
}
4 changes: 4 additions & 0 deletions exchanges/throw-on-error/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { mergeConfig } from 'vitest/config';
import baseConfig from '../../vitest.config';

export default mergeConfig(baseConfig, {});
Loading

0 comments on commit 2169190

Please sign in to comment.