diff --git a/README.md b/README.md index 999a3fc7..face9daa 100644 --- a/README.md +++ b/README.md @@ -7,52 +7,35 @@ Minimal GraphQL client supporting Node and browsers for scripts or simple apps -- [Features](#features) +- [Highlights](#highlights) - [Install](#install) - [Quick Start](#quick-start) -- [Usage](#usage) -- [Node Version Support](#node-version-support) - [Examples](#examples) - - [Authentication via HTTP header](#authentication-via-http-header) - - [Incrementally setting headers](#incrementally-setting-headers) - - [Set endpoint](#set-endpoint) - - [passing-headers-in-each-request](#passing-headers-in-each-request) - - [Passing dynamic headers to the client](#passing-dynamic-headers-to-the-client) - - [Passing more options to `fetch`](#passing-more-options-to-fetch) - - [Custom JSON serializer](#custom-json-serializer) - - [Using GraphQL Document variables](#using-graphql-document-variables) - - [Making a GET request](#making-a-get-request) - - [GraphQL Mutations](#graphql-mutations) - - [Error handling](#error-handling) - - [Using `require` instead of `import`](#using-require-instead-of-import) - - [Cookie support for `node`](#cookie-support-for-node) - - [Using a custom `fetch` method](#using-a-custom-fetch-method) - - [Receiving a raw response](#receiving-a-raw-response) - - [Batching](#batching) - - [Cancellation](#cancellation) - - [Middleware](#middleware) - - [ErrorPolicy](#errorpolicy) - - [None (default)](#none-default) - - [Ignore](#ignore) - - [All](#all) +- [Node Version Support](#node-version-support) +- [Reference](#reference) + - [Configuration](#configuration) + - [ErrorPolicy](#errorpolicy) + - [None (default)](#none-default) + - [Ignore](#ignore) + - [All](#all) - [Knowledge Base](#knowledge-base) - [Why was the file upload feature taken away? Will it return?](#why-was-the-file-upload-feature-taken-away-will-it-return) - [Why do I have to install `graphql`?](#why-do-i-have-to-install-graphql) - [Do I need to wrap my GraphQL documents inside the `gql` template exported by `graphql-request`?](#do-i-need-to-wrap-my-graphql-documents-inside-the-gql-template-exported-by-graphql-request) - - [What's the difference between `graphql-request`, Apollo and Relay?](#whats-the-difference-between-graphql-request-apollo-and-relay) + - [What sets `graphql-request` apart from other clients like Apollo, Relay, etc.?](#what-sets-graphql-request-apart-from-other-clients-like-apollo-relay-etc) - [Why is the package `main` field missing?](#why-is-the-package-main-field-missing) - [How do I work around React Native + Metro's lack of `exports` support?](#how-do-i-work-around-react-native--metros-lack-of-exports-support) - - [Get typed GraphQL Queries with GraphQL Code Generator](#get-typed-graphql-queries-with-graphql-code-generator) -## Features +## Highlights - Most **simple & lightweight** GraphQL client - Promise-based API (works with `async` / `await`) - ESM native package (CJS build is included for now as well, but will eventually be removed) -- TypeScript support -- Isomorphic (works with Node / browsers) +- First class TypeScript support + - Including `TypedDocumentNode` +- Isomorphic (works in both Nodejs and Browsers) ## Install @@ -62,626 +45,96 @@ npm add graphql-request graphql ## Quick Start -Send a GraphQL query with a single line of code. ▶️ [Try it out](https://runkit.com/593130bdfad7120012472003/593130bdfad7120012472004). +Send a GraphQL document using a static request function: ```js import { request, gql } from 'graphql-request' -const query = gql` +const document = gql` { company { ceo } - roadster { - apoapsis_au - } } ` - -request('https://api.spacex.land/graphql/', query).then((data) => console.log(data)) -``` - -## Usage - -```js -import { request, GraphQLClient } from 'graphql-request' - -// Run GraphQL queries/mutations using a static function -request(endpoint, query, variables).then((data) => console.log(data)) - -// ... or create a GraphQL client instance to send requests -const client = new GraphQLClient(endpoint, { headers: {} }) -client.request(query, variables).then((data) => console.log(data)) -``` - -You can also use the single argument function variant: - -```js -request({ - url: endpoint, - document: query, - variables: variables, - requestHeaders: headers, -}).then((data) => console.log(data)) +await request('https://api.spacex.land/graphql/', document) ``` -## Node Version Support - -We only officially support [LTS Node versions](https://github.com/nodejs/Release#release-schedule). We also make an effort to support two additional versions: - -1. The latest even Node version if it is not LTS already. -2. The odd Node version directly following the latest even version. - -You are free to try using other versions of Node (e.g. `13.x`) with `graphql-request` but at your own risk. - -## Examples - -### Authentication via HTTP header - -```js -import { GraphQLClient, gql } from 'graphql-request' - -async function main() { - const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr' - - const graphQLClient = new GraphQLClient(endpoint, { - headers: { - authorization: 'Bearer MY_TOKEN', - }, - }) - - const query = gql` - { - Movie(title: "Inception") { - releaseDate - actors { - name - } - } - } - ` - - const data = await graphQLClient.request(query) - console.log(JSON.stringify(data, undefined, 2)) -} - -main().catch((error) => console.error(error)) -``` - -[TypeScript Source](examples/authentication-via-http-header.ts) - -#### Incrementally setting headers +The function can be passed a configuration object for more complex cases: -If you want to set headers after the GraphQLClient has been initialised, you can use the `setHeader()` or `setHeaders()` functions. - -```js -import { GraphQLClient } from 'graphql-request' - -const client = new GraphQLClient(endpoint) - -// Set a single header -client.setHeader('authorization', 'Bearer MY_TOKEN') - -// Override all existing headers -client.setHeaders({ - authorization: 'Bearer MY_TOKEN', - anotherheader: 'header_value', +```ts +await request({ + url, + document, + variables, + requestHeaders, }) ``` -#### Set endpoint - -If you want to change the endpoint after the GraphQLClient has been initialised, you can use the `setEndpoint()` function. - -```js -import { GraphQLClient } from 'graphql-request' - -const client = new GraphQLClient(endpoint) - -client.setEndpoint(newEndpoint) -``` - -#### passing-headers-in-each-request - -It is possible to pass custom headers for each request. `request()` and `rawRequest()` accept a header object as the third parameter - -```js -import { GraphQLClient } from 'graphql-request' - -const client = new GraphQLClient(endpoint) - -const query = gql` - query getMovie($title: String!) { - Movie(title: $title) { - releaseDate - actors { - name - } - } - } -` - -const variables = { - title: 'Inception', -} - -const requestHeaders = { - authorization: 'Bearer MY_TOKEN', -} - -// Overrides the clients headers with the passed values -const data = await client.request(query, variables, requestHeaders) -``` - -#### Passing dynamic headers to the client - -It's possible to recalculate the global client headers dynamically before each request. -To do that, pass a function that returns the headers to the `headers` property when creating a new `GraphQLClient`. +A class is available for constructing your own instances: ```js import { GraphQLClient } from 'graphql-request' -const client = new GraphQLClient(endpoint, { - headers: () => ({ 'X-Sent-At-Time': Date.now().toString() }), -}) - -const query = gql` - query getCars { - cars { - name - } - } -` -// Function saved in the client runs and calculates fresh headers before each request -const data = await client.request(query) -``` - -### Passing more options to `fetch` - -```js -import { GraphQLClient, gql } from 'graphql-request' - -async function main() { - const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr' - - const graphQLClient = new GraphQLClient(endpoint, { - credentials: 'include', - mode: 'cors', - }) - - const query = gql` - { - Movie(title: "Inception") { - releaseDate - actors { - name - } - } - } - ` - - const data = await graphQLClient.request(query) - console.log(JSON.stringify(data, undefined, 2)) -} - -main().catch((error) => console.error(error)) -``` - -[TypeScript Source](examples/passing-more-options-to-fetch.ts) - -### Custom JSON serializer - -If you want to use non-standard JSON types, you can use your own JSON serializer to replace `JSON.parse`/`JSON.stringify` used by the `GraphQLClient`. - -An original use case for this feature is `BigInt` support: - -```js -import JSONbig from 'json-bigint' -import { GraphQLClient, gql } from 'graphql-request' - -async function main() { - const jsonSerializer = JSONbig({ useNativeBigInt: true }) - const graphQLClient = new GraphQLClient(endpoint, { jsonSerializer }) - const data = await graphQLClient.request( - gql` - { - someBigInt - } - ` - ) - console.log(typeof data.someBigInt) // if >MAX_SAFE_INTEGER then 'bigint' else 'number' -} -``` - -### Using GraphQL Document variables - -```js -import { request, gql } from 'graphql-request' - -async function main() { - const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr' - - const query = gql` - query getMovie($title: String!) { - Movie(title: $title) { - releaseDate - actors { - name - } - } - } - ` - - const variables = { - title: 'Inception', - } - - const data = await request(endpoint, query, variables) - console.log(JSON.stringify(data, undefined, 2)) -} - -main().catch((error) => console.error(error)) -``` - -### Making a GET request - -Queries can be sent as an HTTP GET request: - -```js -import { GraphQLClient, gql } from 'graphql-request' - -async function main() { - const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr' - - const graphQLClient = new GraphQLClient(endpoint, { - method: 'GET', - jsonSerializer: { - parse: JSON.parse, - stringify: JSON.stringify, - }, - }) - - const query = gql` - query getMovie($title: String!) { - Movie(title: $title) { - releaseDate - actors { - name - } - } - } - ` - - const variables = { - title: 'Inception', - } - - const data = await graphQLClient.request(query, variables) - console.log(JSON.stringify(data, undefined, 2)) -} - -main().catch((error) => console.error(error)) -``` - -### GraphQL Mutations - -```js -import { GraphQLClient, gql } from 'graphql-request' - -async function main() { - const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr' - - const graphQLClient = new GraphQLClient(endpoint, { - headers: { - authorization: 'Bearer MY_TOKEN', - }, - }) - - const mutation = gql` - mutation AddMovie($title: String!, $releaseDate: Int!) { - insert_movies_one(object: { title: $title, releaseDate: $releaseDate }) { - title - releaseDate - } - } - ` - - const variables = { - title: 'Inception', - releaseDate: 2010, - } - const data = await graphQLClient.request(mutation, variables) - - console.log(JSON.stringify(data, undefined, 2)) -} - -main().catch((error) => console.error(error)) -``` - -[TypeScript Source](examples/using-variables.ts) - -### Error handling - -```js -import { request, gql } from 'graphql-request' - -async function main() { - const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr' - - const query = gql` - { - Movie(title: "Inception") { - releaseDate - actors { - fullname # "Cannot query field 'fullname' on type 'Actor'. Did you mean 'name'?" - } - } - } - ` - - try { - const data = await request(endpoint, query) - console.log(JSON.stringify(data, undefined, 2)) - } catch (error) { - console.error(JSON.stringify(error, undefined, 2)) - process.exit(1) - } -} - -main().catch((error) => console.error(error)) -``` - -[TypeScript Source](examples/error-handling.ts) - -### Using `require` instead of `import` - -```js -const { request, gql } = require('graphql-request') - -async function main() { - const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr' - - const query = gql` - { - Movie(title: "Inception") { - releaseDate - actors { - name - } - } - } - ` - - const data = await request(endpoint, query) - console.log(JSON.stringify(data, undefined, 2)) -} - -main().catch((error) => console.error(error)) -``` - -### Cookie support for `node` - -```sh -npm install fetch-cookie -``` - -```js -require('fetch-cookie/node-fetch')(require('node-fetch')) - -import { GraphQLClient, gql } from 'graphql-request' - -async function main() { - const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr' - - const graphQLClient = new GraphQLClient(endpoint, { - headers: { - authorization: 'Bearer MY_TOKEN', - }, - }) - - const query = gql` - { - Movie(title: "Inception") { - releaseDate - actors { - name - } - } - } - ` - - const data = await graphQLClient.rawRequest(query) - console.log(JSON.stringify(data, undefined, 2)) -} - -main().catch((error) => console.error(error)) -``` - -[TypeScript Source](examples/cookie-support-for-node.ts) - -### Using a custom `fetch` method - -```sh -npm install fetch-cookie -``` - -```js -import { GraphQLClient, gql } from 'graphql-request' -import crossFetch from 'cross-fetch' - -async function main() { - const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr' - - // a cookie jar scoped to the client object - const fetch = require('fetch-cookie')(crossFetch) - const graphQLClient = new GraphQLClient(endpoint, { fetch }) - - const query = gql` - { - Movie(title: "Inception") { - releaseDate - actors { - name - } - } - } - ` - - const data = await graphQLClient.rawRequest(query) - console.log(JSON.stringify(data, undefined, 2)) -} - -main().catch((error) => console.error(error)) -``` - -### Receiving a raw response - -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, gql } from 'graphql-request' - -async function main() { - const endpoint = 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr' - - const query = gql` - { - Movie(title: "Inception") { - releaseDate - actors { - name - } - } - } - ` - - const { data, errors, extensions, headers, status } = await rawRequest(endpoint, query) - console.log(JSON.stringify({ data, errors, extensions, headers, status }, undefined, 2)) -} - -main().catch((error) => console.error(error)) -``` - -### Batching - -It is possible with `graphql-request` to use [batching](https://github.com/graphql/graphql-over-http/blob/main/rfcs/Batching.md) via the `batchRequests()` function. Example available at [examples/batching-requests.ts](examples/batching-requests.ts) - -```ts -import { batchRequests, gql } from 'graphql-request' - -const endpoint = 'https://api.spacex.land/graphql/' - -const query1 = gql` - query ($id: ID!) { - capsule(id: $id) { - id - landings - } - } -` - -const query2 = gql` +const document = gql` { - rockets(limit: 10) { - active + company { + ceo } } ` - -const data = await batchRequests(endpoint, [ - { document: query1, variables: { id: 'C105' } }, - { document: query2 }, -]) - -console.log(JSON.stringify(data, undefined, 2)) -``` - -### Cancellation - -It is possible to cancel a request using an `AbortController` signal. - -You can define the `signal` in the `GraphQLClient` constructor: - -```ts -const abortController = new AbortController() - -const client = new GraphQLClient(endpoint, { signal: abortController.signal }) -client.request(query) - -abortController.abort() -``` - -You can also set the signal per request (this will override an existing GraphQLClient signal): - -```ts -const abortController = new AbortController() - const client = new GraphQLClient(endpoint) -client.request({ document: query, signal: abortController.signal }) - -abortController.abort() -``` - -In Node environment, `AbortController` is supported since version v14.17.0. -For Node.js v12 you can use [abort-controller](https://github.com/mysticatea/abort-controller) polyfill. - +await request('https://api.spacex.land/graphql/', document) ``` - import 'abort-controller/polyfill' - const abortController = new AbortController() -``` +## Examples -### Middleware +- Request: + - [Authentication via HTTP header](./examples/request-authentication-via-http-header.ts) + - [Method GET](./examples/request-method-get.ts) + - [Cancellation](./examples/request-cancellation.ts) + - [Headers Per Request (static)](./examples/request-headers-static-per-request.ts) + - [Headers Per Request (dynamic)](./examples/request-headers-dynamic-per-request.ts) + - [Cookie support for Nodejs](./examples/request-cookie-support-for-node.ts) + - [Handle Raw Response](./examples/request-handle-raw-response.ts) +- GraphQL: + - [Document Variables](./examples/graphql-document-variables.ts) + - [Mutation](./examples/graphql-mutations.ts) + - [Batching Requests](./examples/graphql-batching-requests.ts) +- Configuration: + - [Fetch: Passing Options](./examples/configuration-fetch-options.ts) + - [Fetch: Use custom function](./examples/configuration-fetch-custom-function.ts) + - [Custom JSON Serializer](./examples/configuration-request-json-serializer.ts) + - [Incremental: Set Endpoint](./examples/configuration-incremental-endpoint.ts) + - [Incremental: Set Request Headers](./examples/configuration-incremental-request-headers.ts) +- Community + - [GraphQL Code Generator for typed GraphQL Queries](./examples/community-graphql-code-generator.ts) +- TypeScript + - [Use `TypedDocumentNode`](./examples/typescript-typed-document-node.ts.ts) +- Other: + - [Middleware](./examples/other-middleware.ts) + - [Error Handling](./examples/other-error-handling.ts) + - [OCommonJS Support](./examples/other-package-commonjs.ts) -It's possible to use a middleware to pre-process any request or handle raw response. +## Node Version Support -Request middleware example (set actual auth token to each request): +We only (officially) support [versions of Nodejs](https://github.com/nodejs/Release#release-schedule) of the following status: -```ts -function middleware(request: RequestInit) { - const token = getToken() - return { - ...request, - headers: { ...request.headers, 'x-auth-token': token }, - } -} +- Current +- LTS +- Maintenance _and end of life not yet reached_ -const client = new GraphQLClient(endpoint, { requestMiddleware: middleware }) -``` +So for example on May 1 2023 that would mean these versions: 16, 18, 19. -It's also possible to use an async function as a request middleware. The resolved data will be passed to the request: +Any issue that exists solely for an unsupported version of Nodejs will be rejected (not worked on). -```ts -async function middleware(request: RequestInit) { - const token = await getToken() - return { - ...request, - headers: { ...request.headers, 'x-auth-token': token }, - } -} - -const client = new GraphQLClient(endpoint, { requestMiddleware: middleware }) -``` +## Reference -Response middleware example (log request trace id if error caused): - -```ts -function middleware(response: Response) { - if (response.errors) { - const traceId = response.headers.get('x-b3-traceid') || 'unknown' - console.error( - `[${traceId}] Request error: - status ${response.status} - details: ${response.errors}` - ) - } -} +⚠️ This reference is incomplete. Check out the [examples](./examples/) for more reference material. -const client = new GraphQLClient(endpoint, { responseMiddleware: middleware }) -``` +### Configuration -### ErrorPolicy +#### ErrorPolicy By default GraphQLClient will throw when an error is received. However, sometimes you still want to resolve the (partial) data you received. You can define `errorPolicy` in the `GraphQLClient` constructor. @@ -690,15 +143,15 @@ You can define `errorPolicy` in the `GraphQLClient` constructor. const client = new GraphQLClient(endpoint, { errorPolicy: 'all' }) ``` -#### None (default) +##### None (default) Allow no errors at all. If you receive a GraphQL error the client will throw. -#### Ignore +##### Ignore Ignore incoming errors and resolve like no errors occurred -#### All +##### All Return both the errors and data, only works with `rawRequest`. @@ -716,7 +169,7 @@ In [this issue](https://github.com/jasonkuhrt/graphql-request/issues/500) we dec 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? +#### What sets `graphql-request` apart from other clients like Apollo, Relay, etc.? `graphql-request` is the most minimal and simplest to use GraphQL client. It's perfect for small scripts or simple apps. @@ -752,38 +205,3 @@ resolver: { ``` After doing this change, clear Metro's cache and restart your application. - -#### Get typed GraphQL Queries with GraphQL Code Generator - -`graphql-request@^5` supports `TypedDocumentNode`, the typed counterpart of `graphql`'s `DocumentNode`. - -Installing and configuring [GraphQL Code Generator](https://www.the-guild.dev/graphql/codegen) requires a few steps in order to get end-to-end typed GraphQL operations using the provided `graphql()` helper: - -```ts -import request from 'graphql-request' -import { graphql } from './gql/gql' - -const getMovieQueryDocument = graphql(gql` - query getMovie($title: String!) { - Movie(title: $title) { - releaseDate - actors { - name - } - } - } -`) - -const data = await request( - 'https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr', - getMovieQueryDocument, - // variables are type-checked! - { title: 'Inception' } -) - -// `data.Movie` is typed! -``` - -[_The complete example is available in the GraphQL Code Generator repository_](https://github.com/dotansimha/graphql-code-generator/tree/master/examples/front-end/react/graphql-request) - -Visit GraphQL Code Generator's dedicated guide to get started: https://www.the-guild.dev/graphql/codegen/docs/guides/react-vue. diff --git a/examples/community-graphql-code-generator.ts b/examples/community-graphql-code-generator.ts new file mode 100644 index 00000000..b3769c7f --- /dev/null +++ b/examples/community-graphql-code-generator.ts @@ -0,0 +1,38 @@ +/** + * `graphql-request@^5` supports `TypedDocumentNode`, the typed counterpart of `graphql`'s `DocumentNode`. + * + * Installing and configuring GraphQL Code Generator requires a few steps in order to get end-to-end typed GraphQL operations using the provided `graphql()`. + * + * The complete example is available in the GraphQL Code Generator repository: + * + * @see https://github.com/dotansimha/graphql-code-generator/tree/master/examples/front-end/react/graphql-request + * + * Visit GraphQL Code Generator's dedicated guide to get started: + * + * @see https://www.the-guild.dev/graphql/codegen/docs/guides/react-vue. + */ + +import request, { gql } from '../src/index.js' +// @ts-expect-error todo make this actually work +import { graphql } from './gql/gql' + +const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` + +// todo fixme +// eslint-disable-next-line +const document = graphql(gql` + query getMovie($title: String!) { + Movie(title: $title) { + releaseDate + actors { + name + } + } + } +`) + +// Variables are typed! +const data = await request(endpoint, document, { title: `Inception` }) + +// @ts-expect-error todo make this actually work +console.log(data.Movie) // typed! diff --git a/examples/custom-fetch.ts b/examples/configuration-fetch-custom-function.ts similarity index 60% rename from examples/custom-fetch.ts rename to examples/configuration-fetch-custom-function.ts index 2acf1c4e..8e6e72f0 100644 --- a/examples/custom-fetch.ts +++ b/examples/configuration-fetch-custom-function.ts @@ -1,9 +1,15 @@ import { gql, GraphQLClient } from '../src/index.js' -import fetch from 'cross-fetch' +import crossFetch from 'cross-fetch' +import fetchCookie from 'fetch-cookie' const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` -const graphQLClient = new GraphQLClient(endpoint, { fetch: fetch }) +/** + * Fetch with a cookie jar scoped to the client object. + */ +const fetch = fetchCookie(crossFetch) + +const graphQLClient = new GraphQLClient(endpoint, { fetch }) const query = gql` { @@ -21,4 +27,4 @@ interface TData { } const data = await graphQLClient.rawRequest(query) -console.log(JSON.stringify(data, undefined, 2)) +console.log(data) diff --git a/examples/passing-more-options-to-fetch.ts b/examples/configuration-fetch-options.ts similarity index 91% rename from examples/passing-more-options-to-fetch.ts rename to examples/configuration-fetch-options.ts index 7362fa14..2cdb8ed9 100644 --- a/examples/passing-more-options-to-fetch.ts +++ b/examples/configuration-fetch-options.ts @@ -23,4 +23,4 @@ interface TData { } const data = await graphQLClient.request(query) -console.log(JSON.stringify(data, undefined, 2)) +console.log(data) diff --git a/examples/configuration-incremental-endpoint.ts b/examples/configuration-incremental-endpoint.ts new file mode 100644 index 00000000..1830b8e7 --- /dev/null +++ b/examples/configuration-incremental-endpoint.ts @@ -0,0 +1,9 @@ +/** + * If you want to change the endpoint after the GraphQLClient has been initialized, you can use the `setEndpoint()` function. + */ + +import { GraphQLClient } from '../src/index.js' + +const client = new GraphQLClient(`https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr`) + +client.setEndpoint(`https://api.graph.cool/simple/v2/cixos23120m0n0173veiiwrjr`) diff --git a/examples/configuration-incremental-request-headers.ts b/examples/configuration-incremental-request-headers.ts new file mode 100644 index 00000000..0a423850 --- /dev/null +++ b/examples/configuration-incremental-request-headers.ts @@ -0,0 +1,16 @@ +/** + * If you want to set headers after the GraphQLClient has been initialized, you can use the `setHeader()` or `setHeaders()` functions. + */ + +import { GraphQLClient } from '../src/index.js' + +const client = new GraphQLClient(`https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr`) + +// Set a single header +client.setHeader(`authorization`, `Bearer MY_TOKEN`) + +// Override all existing headers +client.setHeaders({ + authorization: `Bearer MY_TOKEN`, + 'x-another-header': `header_value`, +}) diff --git a/examples/configuration-request-json-serializer.ts b/examples/configuration-request-json-serializer.ts new file mode 100644 index 00000000..92de9a4c --- /dev/null +++ b/examples/configuration-request-json-serializer.ts @@ -0,0 +1,18 @@ +/** + * If you want to use non-standard JSON types, you can use your own JSON serializer to replace `JSON.parse`/`JSON.stringify` used by the `GraphQLClient`. + * An original use case for this feature is `BigInt` support: + */ + +import { gql, GraphQLClient } from '../src/index.js' +import JSONbig from 'json-bigint' + +const jsonSerializer = JSONbig({ useNativeBigInt: true }) +const graphQLClient = new GraphQLClient(`https://some-api`, { jsonSerializer }) +const data = await graphQLClient.request<{ someBigInt: bigint }>( + gql` + { + someBigInt + } + ` +) +console.log(typeof data.someBigInt) // if >MAX_SAFE_INTEGER then 'bigint' else 'number' diff --git a/examples/batching-requests.ts b/examples/graphql-batching-requests.ts similarity index 81% rename from examples/batching-requests.ts rename to examples/graphql-batching-requests.ts index 7156ae70..fd083ba0 100644 --- a/examples/batching-requests.ts +++ b/examples/graphql-batching-requests.ts @@ -1,3 +1,7 @@ +/** + * It is possible with `graphql-request` to use batching via the `batchRequests()` function. + * @see https://github.com/graphql/graphql-over-http/blob/main/rfcs/Batching.md + */ import { batchRequests, gql } from '../src/index.js' const endpoint = `https://api.spacex.land/graphql/` @@ -53,4 +57,4 @@ const data = await batchRequests<[TData1, TData2, TData3]>(endpoint, [ { document: query2 }, { document: query3, variables: variables3 }, ]) -console.log(JSON.stringify(data, undefined, 2)) +console.log(data) diff --git a/examples/using-variables.ts b/examples/graphql-document-variables.ts similarity index 90% rename from examples/using-variables.ts rename to examples/graphql-document-variables.ts index 2f97d1d1..261ac960 100644 --- a/examples/using-variables.ts +++ b/examples/graphql-document-variables.ts @@ -22,4 +22,4 @@ interface TData { } const data = await request(endpoint, query, variables) -console.log(JSON.stringify(data, undefined, 2)) +console.log(data) diff --git a/examples/graphql-mutations.ts b/examples/graphql-mutations.ts new file mode 100644 index 00000000..fe7c5216 --- /dev/null +++ b/examples/graphql-mutations.ts @@ -0,0 +1,26 @@ +import { gql, GraphQLClient } from '../src/index.js' + +const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` + +const graphQLClient = new GraphQLClient(endpoint, { + headers: { + authorization: `Bearer MY_TOKEN`, + }, +}) + +const mutation = gql` + mutation AddMovie($title: String!, $releaseDate: Int!) { + insert_movies_one(object: { title: $title, releaseDate: $releaseDate }) { + title + releaseDate + } + } +` + +const variables = { + title: `Inception`, + releaseDate: 2010, +} + +const data = await graphQLClient.request(mutation, variables) +console.log(data) diff --git a/examples/error-handling.ts b/examples/other-error-handling.ts similarity index 83% rename from examples/error-handling.ts rename to examples/other-error-handling.ts index 7fb08a6c..0d0e8e0f 100644 --- a/examples/error-handling.ts +++ b/examples/other-error-handling.ts @@ -19,8 +19,8 @@ interface TData { try { const data = await request(endpoint, query) - console.log(JSON.stringify(data, undefined, 2)) + console.log(data) } catch (error) { - console.error(JSON.stringify(error, undefined, 2)) + console.error(error) process.exit(1) } diff --git a/examples/other-middleware.ts b/examples/other-middleware.ts new file mode 100644 index 00000000..faa2dd5b --- /dev/null +++ b/examples/other-middleware.ts @@ -0,0 +1,62 @@ +/* eslint-disable */ + +/** + * It's possible to use a middleware to pre-process any request or handle raw response. + */ + +import { GraphQLClient } from '../src/index.js' +import type { RequestMiddleware } from '../src/types.js' + +const endpoint = `https://api.spacex.land/graphql/` + +const getAccessToken = () => Promise.resolve(`some special token here`) + +{ + /** + * Request middleware example (set actual auth token to each request): + */ + + const requestMiddleware: RequestMiddleware = async (request) => { + const token = await getAccessToken() + return { + ...request, + headers: { ...request.headers, 'x-auth-token': token }, + } + } + + const _client = new GraphQLClient(endpoint, { requestMiddleware }) +} +{ + /** + * It's also possible to use an async function as a request middleware. The resolved data will be passed to the request: + */ + + const requestMiddleware: RequestMiddleware = async (request) => { + const token = await getAccessToken() + return { + ...request, + headers: { ...request.headers, 'x-auth-token': token }, + } + } + + const _client = new GraphQLClient(endpoint, { requestMiddleware }) +} +{ + /** + * Response middleware example (log request trace id if error caused): + */ + + // @ts-expect-error TODO export a response middleware type + const responseMiddleware = (response: Response) => { + if (response.errors) { + const traceId = response.headers.get(`x-b3-trace-id`) || `unknown` + console.error( + `[${traceId}] Request error: + status ${response.status} + details: ${response.errors}` + ) + } + } + + const _client = new GraphQLClient(endpoint, { responseMiddleware }) +} diff --git a/examples/other-package-commonjs.ts b/examples/other-package-commonjs.ts new file mode 100644 index 00000000..ceaea508 --- /dev/null +++ b/examples/other-package-commonjs.ts @@ -0,0 +1,23 @@ +/* eslint-disable */ + +const { request, gql } = require(`graphql-request`) + +main() + +async function main() { + const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` + + const query = gql` + { + Movie(title: "Inception") { + releaseDate + actors { + name + } + } + } + ` + + const data = await request(endpoint, query) + console.log(data) +} diff --git a/examples/authentication-via-http-header.ts b/examples/request-authentication-via-http-header.ts similarity index 91% rename from examples/authentication-via-http-header.ts rename to examples/request-authentication-via-http-header.ts index 6589bae4..3ac64ec4 100644 --- a/examples/authentication-via-http-header.ts +++ b/examples/request-authentication-via-http-header.ts @@ -24,4 +24,4 @@ interface TData { } const data = await graphQLClient.request(query) -console.log(JSON.stringify(data, undefined, 2)) +console.log(data) diff --git a/examples/request-cancellation.ts b/examples/request-cancellation.ts new file mode 100644 index 00000000..b9581b4c --- /dev/null +++ b/examples/request-cancellation.ts @@ -0,0 +1,59 @@ +/** + * It is possible to cancel a request using an `AbortController` signal. + */ + +import { gql, GraphQLClient } from '../src/index.js' + +const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` + +/** + * You can define the `signal` in the `GraphQLClient` constructor: + */ + +{ + const abortController = new AbortController() + + const client = new GraphQLClient(endpoint, { + signal: abortController.signal, + }) + + // todo + // eslint-disable-next-line + client.request(gql` + { + Movie(title: "Inception") { + releaseDate + actors { + name + } + } + } + `) + + abortController.abort() +} + +/** + * You can also set the signal per request (this will override an existing GraphQLClient signal): + */ + +{ + const abortController = new AbortController() + + const client = new GraphQLClient(endpoint) + const document = gql` + { + Movie(title: "Inception") { + releaseDate + actors { + name + } + } + } + ` + const _requesting = client.request({ + document, + signal: abortController.signal, + }) + abortController.abort() +} diff --git a/examples/cookie-support-for-node.ts b/examples/request-cookie-support-for-node.ts similarity index 64% rename from examples/cookie-support-for-node.ts rename to examples/request-cookie-support-for-node.ts index 8c230a0b..d1d1d0dc 100644 --- a/examples/cookie-support-for-node.ts +++ b/examples/request-cookie-support-for-node.ts @@ -2,9 +2,7 @@ import { gql, GraphQLClient } from '../src/index.js' -const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` - -const graphQLClient = new GraphQLClient(endpoint, { +const client = new GraphQLClient(`https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr`, { headers: { authorization: `Bearer MY_TOKEN`, }, @@ -25,5 +23,5 @@ interface TData { Movie: { releaseDate: string; actors: Array<{ name: string }> } } -const data = await graphQLClient.rawRequest(query) -console.log(JSON.stringify(data, undefined, 2)) +const data = await client.rawRequest(query) +console.log(data) diff --git a/examples/receiving-a-raw-response.ts b/examples/request-handle-raw-response.ts similarity index 65% rename from examples/receiving-a-raw-response.ts rename to examples/request-handle-raw-response.ts index 9eeb6bb2..77daf468 100644 --- a/examples/receiving-a-raw-response.ts +++ b/examples/request-handle-raw-response.ts @@ -1,3 +1,8 @@ +/** + * 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: + */ + import { gql, rawRequest } from '../src/index.js' const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` @@ -18,4 +23,4 @@ interface TData { } const { data, errors, extensions, headers, status } = await rawRequest(endpoint, query) -console.log(JSON.stringify({ data, errors, extensions, headers, status }, undefined, 2)) +console.log({ data, errors, extensions, headers, status }) diff --git a/examples/request-headers-dynamic-per-request.ts b/examples/request-headers-dynamic-per-request.ts new file mode 100644 index 00000000..e31562a4 --- /dev/null +++ b/examples/request-headers-dynamic-per-request.ts @@ -0,0 +1,21 @@ +/** + * It's possible to recalculate the global client headers dynamically before each request. + * To do that, pass a function that returns the headers to the `headers` property when creating a new `GraphQLClient`. + */ + +import { gql, GraphQLClient } from '../src/index.js' + +const client = new GraphQLClient(`https://some-api`, { + headers: () => ({ 'X-Sent-At-Time': Date.now().toString() }), +}) + +const query = gql` + query getCars { + cars { + name + } + } +` +// Function saved in the client runs and calculates fresh headers before each request +const data = await client.request(query) +console.log(data) diff --git a/examples/passing-custom-header-per-request.ts b/examples/request-headers-static-per-request.ts similarity index 78% rename from examples/passing-custom-header-per-request.ts rename to examples/request-headers-static-per-request.ts index 2c7de90e..6e811fb9 100644 --- a/examples/passing-custom-header-per-request.ts +++ b/examples/request-headers-static-per-request.ts @@ -1,3 +1,7 @@ +/** + * It is possible to pass custom headers for each request. `request()` and `rawRequest()` accept a header object as the third parameter + */ + import { gql, GraphQLClient } from '../src/index.js' const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` @@ -29,4 +33,4 @@ interface TData { } const data = await client.request(query, {}, requestHeaders) -console.log(JSON.stringify(data, undefined, 2)) +console.log(data) diff --git a/examples/request-method-get.ts b/examples/request-method-get.ts new file mode 100644 index 00000000..33a0062c --- /dev/null +++ b/examples/request-method-get.ts @@ -0,0 +1,32 @@ +/** + * Queries can be sent as an HTTP GET request: + */ +import { gql, GraphQLClient } from '../src/index.js' + +const endpoint = `https://api.graph.cool/simple/v1/cixos23120m0n0173veiiwrjr` + +const graphQLClient = new GraphQLClient(endpoint, { + method: `GET`, + jsonSerializer: { + parse: JSON.parse, + stringify: JSON.stringify, + }, +}) + +const query = gql` + query getMovie($title: String!) { + Movie(title: $title) { + releaseDate + actors { + name + } + } + } +` + +const variables = { + title: `Inception`, +} + +const data = await graphQLClient.request(query, variables) +console.log(data) diff --git a/examples/typed-document-node.ts b/examples/typescript-typed-document-node.ts similarity index 100% rename from examples/typed-document-node.ts rename to examples/typescript-typed-document-node.ts diff --git a/package.json b/package.json index 07c29be7..0d0b97c5 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@tsconfig/node16": "^1.0.3", "@types/body-parser": "^1.19.2", "@types/express": "^4.17.17", + "@types/json-bigint": "^1.0.1", "@types/node": "^18.15.11", "@types/ws": "^8.5.4", "@typescript-eslint/eslint-plugin": "^5.58.0", @@ -97,6 +98,7 @@ "graphql-tag": "^2.12.6", "graphql-ws": "^5.12.1", "happy-dom": "^9.7.1", + "json-bigint": "^1.0.0", "prettier": "^2.8.7", "type-fest": "^3.8.0", "typescript": "^5.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab3f7e71..875766f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,9 @@ devDependencies: '@types/express': specifier: ^4.17.17 version: 4.17.17 + '@types/json-bigint': + specifier: ^1.0.1 + version: 1.0.1 '@types/node': specifier: ^18.15.11 version: 18.15.11 @@ -96,6 +99,9 @@ devDependencies: happy-dom: specifier: ^9.7.1 version: 9.7.1 + json-bigint: + specifier: ^1.0.0 + version: 1.0.0 prettier: specifier: ^2.8.7 version: 2.8.7 @@ -1063,6 +1069,10 @@ packages: resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} dev: true + /@types/json-bigint@1.0.1: + resolution: {integrity: sha512-zpchZLNsNuzJHi6v64UBoFWAvQlPhch7XAi36FkH6tL1bbbmimIF+cS7vwkzY4u5RaSWMoflQfu+TshMPPw8uw==} + dev: true + /@types/json-schema@7.0.11: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} dev: true @@ -1567,6 +1577,10 @@ packages: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} dev: true + /bignumber.js@9.1.1: + resolution: {integrity: sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==} + dev: true + /blueimp-md5@2.19.0: resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} dev: true @@ -2879,6 +2893,12 @@ packages: resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} dev: true + /json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + dependencies: + bignumber.js: 9.1.1 + dev: true + /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} dev: true