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

feat!: accept DocumentNode input #183

Merged
merged 5 commits into from
Aug 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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",
jasonkuhrt marked this conversation as resolved.
Show resolved Hide resolved
"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