-
-
Notifications
You must be signed in to change notification settings - Fork 454
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add executeExchange for using an executable schema (#474)
* 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
1 parent
bde241f
commit be2c8da
Showing
9 changed files
with
486 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
Oops, something went wrong.