diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json
index a99f2eb4c7..27063483c6 100644
--- a/.codesandbox/ci.json
+++ b/.codesandbox/ci.json
@@ -8,11 +8,10 @@
"exchanges/multipart-fetch",
"exchanges/persisted-fetch",
"exchanges/retry",
- "exchanges/suspense"
- ],
- "sandboxes": [
- "urql-issue-template-client-iui0o"
+ "exchanges/suspense",
+ "exchanges/execute"
],
+ "sandboxes": ["urql-issue-template-client-iui0o"],
"buildCommand": "build",
"silent": true
}
diff --git a/exchanges/execute/CHANGELOG.md b/exchanges/execute/CHANGELOG.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/exchanges/execute/README.md b/exchanges/execute/README.md
new file mode 100644
index 0000000000..a37c64c2be
--- /dev/null
+++ b/exchanges/execute/README.md
@@ -0,0 +1,88 @@
+
@urql/exchange-execute
+
+An exchange for executing queries against a local schema in urql
+
+`@urql/exchange-execute` is an exchange for the [`urql`](https://github.com/FormidableLabs/urql) GraphQL client which executes queries against a local schema.
+This is a replacement for the default _fetchExchange_ which sends queries over HTTP/S to be executed remotely.
+
+## Quick Start Guide
+
+First install `@urql/exchange-execute` alongside `urql`:
+
+```sh
+yarn add @urql/exchange-execute
+# or
+npm install --save @urql/exchange-execute
+```
+
+You'll then need to add the `executeExchange`, that this package exposes, to your `urql` Client,
+by replacing the default fetch exchange with it:
+
+```js
+import { createClient, dedupExchange, cacheExchange } from 'urql';
+import { executeExchange } from '@urql/exchange-execute';
+
+const client = createClient({
+ url: 'http://localhost:1234/graphql',
+ exchanges: [
+ dedupExchange,
+ cacheExchange,
+ // Replace the default fetchExchange with the new one.
+ executeExchange({
+ /* config */
+ }),
+ ],
+});
+```
+
+## Usage
+
+The exchange takes the same arguments as the [_execute_ function](https://graphql.org/graphql-js/execution/#execute) provided by graphql-js.
+
+Here's a brief example of how it might be used:
+
+```js
+import { buildSchema } from 'graphql';
+
+// Create local schema
+const schema = buildSchema(`
+ type Todo {
+ id: ID!
+ text: String!
+ }
+
+ type Query {
+ todos: [Todo]!
+ }
+
+ type Mutation {
+ addTodo(text: String!): Todo!
+ }
+`);
+
+// Create local state
+let todos = [];
+
+// Create root value with resolvers
+const rootValue = {
+ todos: () => todos,
+ addTodo: (_, args) => {
+ const todo = { id: todos.length.toString(), ...args };
+ todos = [...todos, todo];
+ return todo;
+ }
+}
+
+// ...
+
+// Pass schema and root value to executeExchange
+executeExchange({
+ schema,
+ rootValue,
+}),
+// ...
+```
+
+## 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/execute/package.json b/exchanges/execute/package.json
new file mode 100644
index 0000000000..2158d7771d
--- /dev/null
+++ b/exchanges/execute/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "@urql/exchange-execute",
+ "version": "1.0.0",
+ "description": "An exchange for executing queries against a local schema 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/execute"
+ },
+ "keywords": [
+ "urql",
+ "exchange",
+ "execute",
+ "executable schema",
+ "formidablelabs",
+ "exchanges"
+ ],
+ "main": "dist/urql-exchange-execute",
+ "module": "dist/urql-exchange-execute.mjs",
+ "types": "dist/types/index.d.ts",
+ "source": "src/index.ts",
+ "exports": {
+ ".": {
+ "import": "./dist/urql-exchange-execute.mjs",
+ "require": "./dist/urql-exchange-execute.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": ">=1.11.7",
+ "wonka": "^4.0.10"
+ },
+ "peerDependencies": {
+ "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
+ },
+ "devDependencies": {
+ "graphql": "^15.0.0",
+ "graphql-tag": "^2.10.1"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/exchanges/execute/src/execute.test.ts b/exchanges/execute/src/execute.test.ts
new file mode 100644
index 0000000000..f4f46396ad
--- /dev/null
+++ b/exchanges/execute/src/execute.test.ts
@@ -0,0 +1,200 @@
+jest.mock('graphql');
+
+import { fetchExchange } from 'urql';
+import { executeExchange, getOperationName } from './execute';
+import { execute, print } from 'graphql';
+import {
+ pipe,
+ fromValue,
+ toPromise,
+ take,
+ makeSubject,
+ empty,
+ Source,
+} from 'wonka';
+import { mocked } from 'ts-jest/utils';
+import { queryOperation } from '@urql/core/test-utils';
+import { makeErrorResult } from '@urql/core';
+import { Client } from '@urql/core/client';
+import { OperationResult } from '@urql/core/types';
+
+const schema = 'STUB_SCHEMA' as any;
+const exchangeArgs = {
+ forward: a => a,
+ client: {},
+} as any;
+
+const expectedOperationName = getOperationName(queryOperation.query);
+
+const fetchMock = (global as any).fetch as jest.Mock;
+afterEach(() => {
+ fetchMock.mockClear();
+});
+
+const mockHttpResponseData = { key: 'value' };
+
+beforeEach(jest.clearAllMocks);
+
+beforeEach(() => {
+ mocked(print).mockImplementation(a => a as any);
+ mocked(execute).mockResolvedValue({ data: mockHttpResponseData });
+});
+
+describe('on operation', () => {
+ it('calls execute with args', async () => {
+ const context = 'USER_ID=123';
+
+ await pipe(
+ fromValue(queryOperation),
+ executeExchange({ schema, context })(exchangeArgs),
+ take(1),
+ toPromise
+ );
+
+ expect(mocked(execute)).toBeCalledTimes(1);
+ expect(mocked(execute)).toBeCalledWith(
+ schema,
+ queryOperation.query,
+ undefined,
+ context,
+ queryOperation.variables,
+ expectedOperationName,
+ undefined,
+ undefined
+ );
+ });
+
+ it('calls execute after executing context as a function', async () => {
+ const context = operation => {
+ expect(operation).toBe(queryOperation);
+ return 'CALCULATED_USER_ID=' + 8 * 10;
+ };
+
+ await pipe(
+ fromValue(queryOperation),
+ executeExchange({ schema, context })(exchangeArgs),
+ take(1),
+ toPromise
+ );
+
+ expect(mocked(execute)).toBeCalledTimes(1);
+ expect(mocked(execute)).toBeCalledWith(
+ schema,
+ queryOperation.query,
+ undefined,
+ 'CALCULATED_USER_ID=80',
+ queryOperation.variables,
+ expectedOperationName,
+ undefined,
+ undefined
+ );
+ });
+
+ it('should return the same data as the fetch exchange', async () => {
+ const context = 'USER_ID=123';
+
+ const responseFromExecuteExchange = await pipe(
+ fromValue(queryOperation),
+ executeExchange({ schema, context })(exchangeArgs),
+ take(1),
+ toPromise
+ );
+
+ fetchMock.mockResolvedValue({
+ status: 200,
+ json: jest.fn().mockResolvedValue({ data: mockHttpResponseData }),
+ });
+
+ const responseFromFetchExchange = await pipe(
+ fromValue(queryOperation),
+ fetchExchange({
+ dispatchDebug: jest.fn(),
+ forward: () => empty as Source,
+ client: {} as Client,
+ }),
+ toPromise
+ );
+
+ expect(responseFromExecuteExchange.data).toEqual(
+ responseFromFetchExchange.data
+ );
+ expect(mocked(execute)).toBeCalledTimes(1);
+ expect(fetchMock).toBeCalledTimes(1);
+ });
+});
+
+describe('on success response', () => {
+ it('returns operation result', async () => {
+ const response = await pipe(
+ fromValue(queryOperation),
+ executeExchange({ schema })(exchangeArgs),
+ take(1),
+ toPromise
+ );
+
+ expect(response).toEqual({
+ operation: queryOperation,
+ data: mockHttpResponseData,
+ });
+ });
+});
+
+describe('on error response', () => {
+ const errors = ['error'] as any;
+
+ beforeEach(() => {
+ mocked(execute).mockResolvedValue({ errors });
+ });
+
+ it('returns operation result', async () => {
+ const response = await pipe(
+ fromValue(queryOperation),
+ executeExchange({ schema })(exchangeArgs),
+ take(1),
+ toPromise
+ );
+
+ expect(response).toHaveProperty('operation', queryOperation);
+ expect(response).toHaveProperty('error');
+ });
+});
+
+describe('on thrown error', () => {
+ const errors = ['error'] as any;
+
+ beforeEach(() => {
+ mocked(execute).mockRejectedValue({ errors });
+ });
+
+ it('returns operation result', async () => {
+ const response = await pipe(
+ fromValue(queryOperation),
+ executeExchange({ schema })(exchangeArgs),
+ take(1),
+ toPromise
+ );
+
+ expect(response).toMatchObject(makeErrorResult(queryOperation, errors));
+ });
+});
+
+describe('on unsupported operation', () => {
+ const operation = {
+ ...queryOperation,
+ operationName: 'teardown',
+ } as const;
+
+ it('returns operation result', async () => {
+ const { source, next } = makeSubject();
+
+ const response = pipe(
+ source,
+ executeExchange({ schema })(exchangeArgs),
+ take(1),
+ toPromise
+ );
+
+ next(operation);
+ expect(await response).toEqual(operation);
+ });
+});
diff --git a/exchanges/execute/src/execute.ts b/exchanges/execute/src/execute.ts
new file mode 100644
index 0000000000..f146b54348
--- /dev/null
+++ b/exchanges/execute/src/execute.ts
@@ -0,0 +1,112 @@
+import {
+ pipe,
+ share,
+ filter,
+ fromPromise,
+ takeUntil,
+ onEnd,
+ mergeMap,
+ merge,
+} from 'wonka';
+
+import {
+ DocumentNode,
+ Kind,
+ GraphQLSchema,
+ GraphQLFieldResolver,
+ GraphQLTypeResolver,
+ execute,
+} from 'graphql';
+
+import { Exchange, makeResult, makeErrorResult, Operation } from '@urql/core';
+
+export const getOperationName = (query: DocumentNode): string | undefined => {
+ for (let i = 0, l = query.definitions.length; i < l; i++) {
+ const node = query.definitions[i];
+ if (node.kind === Kind.OPERATION_DEFINITION && node.name) {
+ return node.name.value;
+ }
+ }
+};
+
+interface ExecuteExchangeArgs {
+ schema: GraphQLSchema;
+ rootValue?: any;
+ context?: ((op: Operation) => void) | any;
+ fieldResolver?: GraphQLFieldResolver;
+ typeResolver?: GraphQLTypeResolver;
+}
+
+/** Exchange for executing queries locally on a schema using graphql-js. */
+export const executeExchange = ({
+ schema,
+ rootValue,
+ context,
+ fieldResolver,
+ typeResolver,
+}: ExecuteExchangeArgs): Exchange => ({ forward }) => {
+ return ops$ => {
+ const sharedOps$ = share(ops$);
+
+ const executedOps$ = pipe(
+ sharedOps$,
+ filter((operation: Operation) => {
+ return (
+ operation.operationName === 'query' ||
+ operation.operationName === 'mutation'
+ );
+ }),
+ mergeMap((operation: Operation) => {
+ const { key } = operation;
+ const teardown$ = pipe(
+ sharedOps$,
+ filter(op => op.operationName === 'teardown' && op.key === key)
+ );
+
+ const calculatedContext =
+ typeof context === 'function' ? context(operation) : context;
+
+ let ended = false;
+
+ const result = Promise.resolve()
+ .then(() => {
+ if (ended) return;
+
+ return execute(
+ schema,
+ operation.query,
+ rootValue,
+ calculatedContext,
+ operation.variables,
+ getOperationName(operation.query),
+ fieldResolver,
+ typeResolver
+ );
+ })
+ .then(result => makeResult(operation, result))
+ .catch(err => makeErrorResult(operation, err));
+
+ return pipe(
+ fromPromise(result),
+ onEnd(() => {
+ ended = true;
+ }),
+ takeUntil(teardown$)
+ );
+ })
+ );
+
+ const forwardedOps$ = pipe(
+ sharedOps$,
+ filter((operation: Operation) => {
+ return (
+ operation.operationName !== 'query' &&
+ operation.operationName !== 'mutation'
+ );
+ }),
+ forward
+ );
+
+ return merge([executedOps$, forwardedOps$]);
+ };
+};
diff --git a/exchanges/execute/src/index.ts b/exchanges/execute/src/index.ts
new file mode 100644
index 0000000000..cf7bbd94ec
--- /dev/null
+++ b/exchanges/execute/src/index.ts
@@ -0,0 +1 @@
+export { executeExchange } from './execute';
diff --git a/exchanges/execute/tsconfig.json b/exchanges/execute/tsconfig.json
new file mode 100644
index 0000000000..5797ce6168
--- /dev/null
+++ b/exchanges/execute/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"]
+ }
+ }
+}
diff --git a/exchanges/graphcache/README.md b/exchanges/graphcache/README.md
index cbf0fcdea8..79549ce54d 100644
--- a/exchanges/graphcache/README.md
+++ b/exchanges/graphcache/README.md
@@ -1,20 +1,6 @@
@urql/exchange-graphcache
-
-An exchange for normalized caching support in urql
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+An exchange for normalized caching support in urql
`@urql/exchange-graphcache` is a normalized cache exchange for the [`urql`](https://github.com/FormidableLabs/urql) GraphQL client.
This is a drop-in replacement for the default `cacheExchange` that, instead of document