Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/neat-pandas-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@urql/exchange-throw-on-error": minor
---

Initial release
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 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"
]
}
66 changes: 66 additions & 0 deletions exchanges/throw-on-error/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"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",
JoviDeCroock marked this conversation as resolved.
Show resolved Hide resolved
"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