Skip to content

Commit

Permalink
Add executeExchange for using an executable schema (#474)
Browse files Browse the repository at this point in the history
* initial

* optimize andadd tests

* Update src/exchanges/execute.ts

* Remove unused vars

* Move execute exchange to it's own package

* Update getting started guide

* Remove extras

* Remove extras again

* Update exchanges/execute/src/execute.test.ts

* Update exchanges/execute/src/execute.ts

* Execute exchange: update package.json to align with new conventions & dependency versions.

* Execute exchange: fix failing test.

* Execute exchange: misc code cosmetics from PR review.

* Execute exchange: add empty CHANGELOG.md.

* Execute exchange: fix typo in README.md.

* Execute exchange: allow contextValue to be a function.

* Execute exchange: changes based on PR comments.

* Execute exchange: instead of exposing core's internal method, copy to exchange's package.

* Execute exchange: add test that execute and fetch exchanges both return same data for same inputs.

* Execute exchange: Add teardown support

* Execute exchange: Add to CodeSandbox CI

Co-authored-by: Amy Boyd <[email protected]>
  • Loading branch information
andyrichardson and amyboyd authored May 7, 2020
1 parent bde241f commit be2c8da
Show file tree
Hide file tree
Showing 9 changed files with 486 additions and 20 deletions.
7 changes: 3 additions & 4 deletions .codesandbox/ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Empty file added exchanges/execute/CHANGELOG.md
Empty file.
88 changes: 88 additions & 0 deletions exchanges/execute/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<h2 align="center">@urql/exchange-execute</h2>

<p align="center"><strong>An exchange for executing queries against a local schema in <code>urql</code></strong></p>

`@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.
67 changes: 67 additions & 0 deletions exchanges/execute/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
200 changes: 200 additions & 0 deletions exchanges/execute/src/execute.test.ts
Original file line number Diff line number Diff line change
@@ -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<OperationResult>,
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<any>();

const response = pipe(
source,
executeExchange({ schema })(exchangeArgs),
take(1),
toPromise
);

next(operation);
expect(await response).toEqual(operation);
});
});
Loading

0 comments on commit be2c8da

Please sign in to comment.