Skip to content

Commit

Permalink
feat!: accept DocumentNode input (#183)
Browse files Browse the repository at this point in the history
closes #176

Example:

```ts
import gql from 'graphql-tag'

await request('https://foo.bar/graphql', gql`...`)
```

If you don't actually care about using DocumentNode but just
want the tooling support for gql template tag like IDE syntax
coloring and prettier autoformat then note you can use the
passthrough gql tag shipped with graphql-request to save a bit
of performance and not have to install another dep into your project.

```ts
import { gql } from 'graphql-request'

await request('https://foo.bar/graphql', gql`...`)
```

BREAKING CHANGE:

`graphql-request` now requires `graphql` version 14.x or 15.x as a peer dependency.

`graphql-request` uses a TypeScript type from the `graphql` package such that if you are using TypeScript to build your project and you are using `graphql-request` but don't have `graphql` installed TypeScript build will fail. Details [here](#183 (comment)). If you are a JS user then you do not technically need to install `graphql`. However if you use an IDE that picks up TS types even for JS (like VSCode) then its still in your interest to install `graphql` so that you can benefit from enhanced type safety during development.
  • Loading branch information
Jason Kuhrt authored Aug 3, 2020
1 parent 3f51dab commit e757927
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 49 deletions.
59 changes: 35 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,26 @@ Minimal GraphQL client supporting Node and browsers for scripts or simple apps
## Install

```sh
npm add graphql-request
npm add graphql-request graphql
```

## Quickstart

Send a GraphQL query with a single line of code. ▶️ [Try it out](https://runkit.com/593130bdfad7120012472003/593130bdfad7120012472004).

```js
import { request } from 'graphql-request'

const query = `{
Movie(title: "Inception") {
releaseDate
actors {
name
import { request, gql } from 'graphql-request'

const query = gql`
{
Movie(title: "Inception") {
releaseDate
actors {
name
}
}
}
}`
`

request('https://api.graph.cool/simple/v1/movies', query).then((data) => console.log(data))
```
Expand All @@ -54,7 +56,7 @@ client.request(query, variables).then((data) => console.log(data))
### Authentication via HTTP header

```js
import { GraphQLClient } from 'graphql-request'
import { GraphQLClient, gql } from 'graphql-request'

async function main() {
const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr'
Expand All @@ -65,7 +67,7 @@ async function main() {
},
})

const query = /* GraphQL */ `
const query = gql`
{
Movie(title: "Inception") {
releaseDate
Expand All @@ -86,6 +88,7 @@ main().catch((error) => console.error(error))
[TypeScript Source](examples/authentication-via-http-header.ts)

#### Dynamically setting headers

If you want to set headers after the GraphQLClient has been initialised, you can use the `setHeader()` or `setHeaders()` functions.

```js
Expand All @@ -106,7 +109,7 @@ client.setHeaders({
### Passing more options to fetch

```js
import { GraphQLClient } from 'graphql-request'
import { GraphQLClient, gql } from 'graphql-request'

async function main() {
const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr'
Expand All @@ -116,7 +119,7 @@ async function main() {
mode: 'cors',
})

const query = /* GraphQL */ `
const query = gql`
{
Movie(title: "Inception") {
releaseDate
Expand All @@ -139,12 +142,12 @@ main().catch((error) => console.error(error))
### Using variables

```js
import { request } from 'graphql-request'
import { request, gql } from 'graphql-request'

async function main() {
const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr'

const query = /* GraphQL */ `
const query = gql`
query getMovie($title: String!) {
Movie(title: $title) {
releaseDate
Expand All @@ -171,12 +174,12 @@ main().catch((error) => console.error(error))
### Error handling

```js
import { request } from 'graphql-request'
import { request, gql } from 'graphql-request'

async function main() {
const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr'

const query = /* GraphQL */ `
const query = gql`
{
Movie(title: "Inception") {
releaseDate
Expand Down Expand Up @@ -204,12 +207,12 @@ main().catch((error) => console.error(error))
### Using `require` instead of `import`

```js
const { request } = require('graphql-request')
const { request, gql } = require('graphql-request')

async function main() {
const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr'

const query = /* GraphQL */ `
const query = gql`
{
Movie(title: "Inception") {
releaseDate
Expand All @@ -236,7 +239,7 @@ npm install fetch-cookie
```js
require('fetch-cookie/node-fetch')(require('node-fetch'))

import { GraphQLClient } from 'graphql-request'
import { GraphQLClient, gql } from 'graphql-request'

async function main() {
const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr'
Expand All @@ -247,7 +250,7 @@ async function main() {
},
})

const query = /* GraphQL */ `
const query = gql`
{
Movie(title: "Inception") {
releaseDate
Expand All @@ -273,12 +276,12 @@ The `request` method will return the `data` or `errors` key from the response.
If you need to access the `extensions` key you can use the `rawRequest` method:

```js
import { rawRequest } from 'graphql-request'
import { rawRequest, gql } from 'graphql-request'

async function main() {
const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr'

const query = /* GraphQL */ `
const query = gql`
{
Movie(title: "Inception") {
releaseDate
Expand All @@ -305,7 +308,15 @@ main().catch((error) => console.error(error))

## FAQ

### What's the difference between `graphql-request`, Apollo and Relay?
#### Why do I have to install `graphql`?

`graphql-request` uses a TypeScript type from the `graphql` package such that if you are using TypeScript to build your project and you are using `graphql-request` but don't have `graphql` installed TypeScript build will fail. Details [here](https://github.com/prisma-labs/graphql-request/pull/183#discussion_r464453076). If you are a JS user then you do not technically need to install `graphql`. However if you use an IDE that picks up TS types even for JS (like VSCode) then its still in your interest to install `graphql` so that you can benefit from enhanced type safety during development.

#### Do I need to wrap my GraphQL documents inside the `gql` template exported by `graphql-request`?

No. It is there for convenience so that you can get the tooling support like prettier formatting and IDE syntax highlighting. You can use `gql` from `graphql-tag` if you need it for some reason too.

#### What's the difference between `graphql-request`, Apollo and Relay?

`graphql-request` is the most minimal and simplest to use GraphQL client. It's perfect for small scripts or simple apps.

Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
"dependencies": {
"cross-fetch": "^3.0.4"
},
"peerDependencies": {
"graphql": "14.x || 15.x"
},
"devDependencies": {
"@prisma-labs/prettier-config": "^0.1.0",
"@types/body-parser": "^1.19.0",
Expand All @@ -57,6 +60,8 @@
"dripip": "^0.9.0",
"express": "^4.17.1",
"fetch-cookie": "0.7.2",
"graphql": "^15.3.0",
"graphql-tag": "^2.11.0",
"jest": "^26.0.1",
"prettier": "^2.0.5",
"ts-jest": "^26.0.0",
Expand Down
98 changes: 89 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import fetch from 'cross-fetch'
import { ClientError, GraphQLError, Variables } from './types'
import { print } from 'graphql/language/printer'
import { ClientError, GraphQLError, RequestDocument, Variables } from './types'
import { RequestInit, Response } from './types.dom'

export { ClientError } from './types'

/**
* todo
*/
export class GraphQLClient {
private url: string
private options: RequestInit
Expand Down Expand Up @@ -45,11 +49,15 @@ export class GraphQLClient {
}
}

async request<T = any, V = Variables>(query: string, variables?: V): Promise<T> {
/**
* todo
*/
async request<T = any, V = Variables>(document: RequestDocument, variables?: V): Promise<T> {
const { headers, ...others } = this.options
const resolvedDoc = resolveRequestDocument(document)

const body = JSON.stringify({
query,
query: resolvedDoc,
variables: variables ? variables : undefined,
})

Expand All @@ -66,13 +74,12 @@ export class GraphQLClient {
return result.data
} else {
const errorResult = typeof result === 'string' ? { error: result } : result
throw new ClientError({ ...errorResult, status: response.status }, { query, variables })
throw new ClientError({ ...errorResult, status: response.status }, { query: resolvedDoc, variables })
}
}

setHeaders(headers: Response['headers']): GraphQLClient {
this.options.headers = headers

return this
}

Expand All @@ -86,28 +93,71 @@ export class GraphQLClient {
} else {
this.options.headers = { [key]: value }
}

return this
}
}

/**
* todo
*/
export async function rawRequest<T = any, V = Variables>(
url: string,
query: string,
variables?: V
): Promise<{ data?: T; extensions?: any; headers: Headers; status: number; errors?: GraphQLError[] }> {
const client = new GraphQLClient(url)

return client.rawRequest<T, V>(query, variables)
}

export async function request<T = any, V = Variables>(url: string, query: string, variables?: V): Promise<T> {
/**
* Send a GraphQL Document to the GraphQL server for exectuion.
*
* @example
*
* ```ts
* // You can pass a raw string
*
* await request('https://foo.bar/graphql', `
* {
* query {
* users
* }
* }
* `)
*
* // You can also pass a GraphQL DocumentNode. Convenient if you
* // are using graphql-tag package.
*
* import gql from 'graphql-tag'
*
* await request('https://foo.bar/graphql', gql`...`)
*
* // If you don't actually care about using DocumentNode but just
* // want the tooling support for gql template tag like IDE syntax
* // coloring and prettier autoformat then note you can use the
* // passthrough gql tag shipped with graphql-request to save a bit
* // of performance and not have to install another dep into your project.
*
* import { gql } from 'graphql-request'
*
* await request('https://foo.bar/graphql', gql`...`)
* ```
*/
export async function request<T = any, V = Variables>(
url: string,
document: RequestDocument,
variables?: V
): Promise<T> {
const client = new GraphQLClient(url)

return client.request<T, V>(query, variables)
return client.request<T, V>(document, variables)
}

export default request

/**
* todo
*/
function getResult(response: Response): Promise<any> {
const contentType = response.headers.get('Content-Type')
if (contentType && contentType.startsWith('application/json')) {
Expand All @@ -116,3 +166,33 @@ function getResult(response: Response): Promise<any> {
return response.text()
}
}

/**
* helpers
*/

function resolveRequestDocument(document: RequestDocument): string {
if (typeof document === 'string') return document

return print(document)
}

/**
* Convenience passthrough template tag to get the benefits of tooling for the gql template tag. This does not actually parse the input into a GraphQL DocumentNode like graphql-tag package does. It just returns the string with any variables given interpolated. Can save you a bit of performance and having to install another package.
*
* @example
*
* import { gql } from 'graphql-request'
*
* await request('https://foo.bar/graphql', gql`...`)
*
* @remarks
*
* Several tools in the Node GraphQL ecosystem are hardcoded to specially treat any template tag named "gql". For example see this prettier issue: https://github.com/prettier/prettier/issues/4360. Using this template tag has no runtime effect beyond variable interpolation.
*/
export function gql(chunks: TemplateStringsArray, ...variables: any[]): string {
return chunks.reduce(
(accumulator, chunk, index) => `${accumulator}${chunk}${index in variables ? variables[index] : ''}`,
''
)
}
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { DocumentNode } from 'graphql/language/ast'

export type Variables = { [key: string]: any }

export interface GraphQLError {
Expand Down Expand Up @@ -50,3 +52,5 @@ export class ClientError extends Error {
}
}
}

export type RequestDocument = string | DocumentNode
Loading

0 comments on commit e757927

Please sign in to comment.