diff --git a/exchanges/context/CHANGELOG.md b/exchanges/context/CHANGELOG.md new file mode 100644 index 0000000000..8c30f86a21 --- /dev/null +++ b/exchanges/context/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## v0.1.0 + +**Initial Release** diff --git a/exchanges/context/README.md b/exchanges/context/README.md new file mode 100644 index 0000000000..ba8a23aa6c --- /dev/null +++ b/exchanges/context/README.md @@ -0,0 +1,41 @@ +

@urql/exchange-context

+ +

An exchange for setting operation context in urql

+ +`@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. diff --git a/exchanges/context/package.json b/exchanges/context/package.json new file mode 100644 index 0000000000..31d279a4d3 --- /dev/null +++ b/exchanges/context/package.json @@ -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" + } +} diff --git a/exchanges/context/src/context.test.ts b/exchanges/context/src/context.test.ts new file mode 100644 index 0000000000..6ab48bc53a --- /dev/null +++ b/exchanges/context/src/context.test.ts @@ -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()); +}); + +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); +}); diff --git a/exchanges/context/src/context.ts b/exchanges/context/src/context.ts new file mode 100644 index 0000000000..1adde2e2ce --- /dev/null +++ b/exchanges/context/src/context.ts @@ -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; +} + +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 + ); + }; +}; diff --git a/exchanges/context/src/index.ts b/exchanges/context/src/index.ts new file mode 100644 index 0000000000..d9379222b2 --- /dev/null +++ b/exchanges/context/src/index.ts @@ -0,0 +1 @@ +export { contextExchange, ContextExchangeArgs } from './context'; diff --git a/exchanges/context/tsconfig.json b/exchanges/context/tsconfig.json new file mode 100644 index 0000000000..5797ce6168 --- /dev/null +++ b/exchanges/context/tsconfig.json @@ -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"] + } + } +}