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(set-context): create set context exchange #2610

Merged
merged 4 commits into from
Aug 19, 2022
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 exchanges/context/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## v0.1.0

**Initial Release**
41 changes: 41 additions & 0 deletions exchanges/context/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<h2 align="center">@urql/exchange-context</h2>

<p align="center"><strong>An exchange for setting operation context in <code>urql</code></strong></p>

`@urql/exchange-context` is an exchange for the [`urql`](https://github.com/FormidableLabs/urql) GraphQL client which can set the operation context both synchronously as well as asynchronously

## Quick Start Guide

First install `@urql/exchange-context` alongside `urql`:

```sh
yarn add @urql/exchange-context
# or
npm install --save @urql/exchange-context
```

You'll then need to add the `contextExchange`, that this package exposes, to your `urql` Client, the positioning of this exchange depends on whether you set an async setter or not. If you set an async context-setter it's best placed after all the synchronous exchanges (in front of the fetchExchange).

```js
import { createClient, dedupExchange, cacheExchange, fetchExchange } from 'urql';
import { contextExchange } from '@urql/exchange-context';

const client = createClient({
url: 'http://localhost:1234/graphql',
exchanges: [
dedupExchange,
cacheExchange,
contextExchange({
getContext: async (operation) => {
const token = await getToken();
return { ...operation.context, headers: { authorization: token } }
},
}),
fetchExchange,
],
});
```

## Maintenance Status

**Active:** Formidable is actively working on this project, and we expect to continue for work for the foreseeable future. Bug reports, feature requests and pull requests are welcome.
65 changes: 65 additions & 0 deletions exchanges/context/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"name": "@urql/exchange-context",
"version": "0.1.0",
"description": "An exchange for setting (a)synchronous operation-context in urql",
"sideEffects": false,
"homepage": "https://formidable.com/open-source/urql/docs/",
"bugs": "https://github.com/FormidableLabs/urql/issues",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/FormidableLabs/urql.git",
"directory": "exchanges/context"
},
"keywords": [
"urql",
"exchange",
"context",
"formidablelabs",
"exchanges"
],
"main": "dist/urql-exchange-context",
"module": "dist/urql-exchange-context.mjs",
"types": "dist/types/index.d.ts",
"source": "src/index.ts",
"exports": {
".": {
"import": "./dist/urql-exchange-context.mjs",
"require": "./dist/urql-exchange-context.js",
"types": "./dist/types/index.d.ts",
"source": "./src/index.ts"
},
"./package.json": "./package.json"
},
"files": [
"LICENSE",
"CHANGELOG.md",
"README.md",
"dist/"
],
"scripts": {
"test": "jest",
"clean": "rimraf dist extras",
"check": "tsc --noEmit",
"lint": "eslint --ext=js,jsx,ts,tsx .",
"build": "rollup -c ../../scripts/rollup/config.js",
"prepare": "node ../../scripts/prepare/index.js",
"prepublishOnly": "run-s clean build"
},
"jest": {
"preset": "../../scripts/jest/preset"
},
"dependencies": {
"@urql/core": ">=2.3.6",
"wonka": "^6.0.0"
},
"peerDependencies": {
"graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
},
"devDependencies": {
"graphql": "^16.0.0"
},
"publishConfig": {
"access": "public"
}
}
117 changes: 117 additions & 0 deletions exchanges/context/src/context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { pipe, map, makeSubject, publish, tap } from 'wonka';

import {
gql,
createClient,
Operation,
OperationResult,
ExchangeIO,
} from '@urql/core';

import { contextExchange } from './context';

const queryOne = gql`
{
author {
id
name
}
}
`;

const queryOneData = {
__typename: 'Query',
author: {
__typename: 'Author',
id: '123',
name: 'Author',
},
};

const dispatchDebug = jest.fn();
let client, op, ops$, next;
beforeEach(() => {
client = createClient({ url: 'http://0.0.0.0' });
op = client.createRequestOperation('query', {
key: 1,
query: queryOne,
});

({ source: ops$, next } = makeSubject<Operation>());
});

it(`calls getContext`, () => {
const response = jest.fn(
(forwardOp: Operation): OperationResult => {
return {
operation: forwardOp,
data: queryOneData,
};
}
);

const result = jest.fn();
const forward: ExchangeIO = ops$ => {
return pipe(ops$, map(response));
};

const headers = { hello: 'world' };
pipe(
contextExchange({
getContext: op => ({ ...op.context, headers }),
})({
forward,
client,
dispatchDebug,
})(ops$),
tap(result),
publish
);

next(op);

expect(response).toHaveBeenCalledTimes(1);
expect(response.mock.calls[0][0].context.headers).toEqual(headers);
expect(result).toHaveBeenCalledTimes(1);
});

it(`calls getContext async`, done => {
const response = jest.fn(
(forwardOp: Operation): OperationResult => {
return {
operation: forwardOp,
data: queryOneData,
};
}
);

const result = jest.fn();
const forward: ExchangeIO = ops$ => {
return pipe(ops$, map(response));
};

const headers = { hello: 'world' };
pipe(
contextExchange({
getContext: async op => {
await Promise.resolve();
return { ...op.context, headers };
},
})({
forward,
client,
dispatchDebug,
})(ops$),
tap(result),
publish
);

next(op);

setTimeout(() => {
expect(response).toHaveBeenCalledTimes(1);
expect(response.mock.calls[0][0].context.headers).toEqual(headers);
expect(result).toHaveBeenCalledTimes(1);
done();
}, 10);
});
39 changes: 39 additions & 0 deletions exchanges/context/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
Exchange,
makeOperation,
Operation,
OperationContext,
} from '@urql/core';
import { fromPromise, fromValue, mergeMap, pipe } from 'wonka';

export interface ContextExchangeArgs {
getContext: (
operation: Operation
) => OperationContext | Promise<OperationContext>;
}

export const contextExchange = ({
getContext,
}: ContextExchangeArgs): Exchange => ({ forward }) => {
return ops$ => {
return pipe(
ops$,
mergeMap(operation => {
const result = getContext(operation);
const isPromise = 'then' in result;
if (isPromise) {
return fromPromise(
result.then((ctx: OperationContext) =>
makeOperation(operation.kind, operation, ctx)
)
);
} else {
return fromValue(
makeOperation(operation.kind, operation, result as OperationContext)
);
}
}),
forward
);
};
};
1 change: 1 addition & 0 deletions exchanges/context/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { contextExchange, ContextExchangeArgs } from './context';
13 changes: 13 additions & 0 deletions exchanges/context/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"include": ["src"],
"compilerOptions": {
"baseUrl": "./",
"paths": {
"urql": ["../../node_modules/urql/src"],
"*-urql": ["../../node_modules/*-urql/src"],
"@urql/core/*": ["../../node_modules/@urql/core/src/*"],
"@urql/*": ["../../node_modules/@urql/*/src"]
}
}
}