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

Add executeExchange for using an executable schema #474

Merged
merged 21 commits into from
May 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0224eb5
initial
andyrichardson Nov 21, 2019
855dc01
optimize andadd tests
andyrichardson Nov 21, 2019
342a0bd
Update src/exchanges/execute.ts
andyrichardson Feb 19, 2020
b6325ef
Remove unused vars
andyrichardson Feb 19, 2020
068abc7
Move execute exchange to it's own package
andyrichardson Mar 18, 2020
9ea23cb
Update getting started guide
andyrichardson Mar 18, 2020
5ce0bfe
Remove extras
andyrichardson Mar 23, 2020
b84208c
Remove extras again
andyrichardson Mar 23, 2020
0979877
Update exchanges/execute/src/execute.test.ts
andyrichardson Apr 17, 2020
9f421e8
Update exchanges/execute/src/execute.ts
andyrichardson Apr 17, 2020
a2b1a95
Execute exchange: update package.json to align with new conventions &…
amyboyd May 7, 2020
a9feb34
Execute exchange: fix failing test.
amyboyd May 7, 2020
4dc118e
Execute exchange: misc code cosmetics from PR review.
amyboyd May 7, 2020
c2d33cf
Execute exchange: add empty CHANGELOG.md.
amyboyd May 7, 2020
298e7be
Execute exchange: fix typo in README.md.
amyboyd May 7, 2020
4ed0c80
Execute exchange: allow contextValue to be a function.
amyboyd May 7, 2020
f4e67d8
Execute exchange: changes based on PR comments.
amyboyd May 7, 2020
6df92cb
Execute exchange: instead of exposing core's internal method, copy to…
amyboyd May 7, 2020
5af6677
Execute exchange: add test that execute and fetch exchanges both retu…
amyboyd May 7, 2020
c1aec75
Execute exchange: Add teardown support
kitten May 7, 2020
91100da
Execute exchange: Add to CodeSandbox CI
kitten May 7, 2020
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
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