Skip to content

feat: Implement client #3

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

Merged
merged 16 commits into from
Aug 4, 2022
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
259 changes: 259 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,267 @@ fastify.listen(4000);
console.log('Listening to port 4000');
```

#### Use the client

```js
import { createClient } from 'graphql-http';

const client = createClient({
url: 'http://localhost:4000/graphql',
});

(async () => {
let cancel = () => {
/* abort the request if it is in-flight */
};

const result = await new Promise((resolve, reject) => {
let result;
cancel = client.subscribe(
{
query: '{ hello }',
},
{
next: (data) => (result = data),
error: reject,
complete: () => resolve(result),
},
);
});

expect(result).toEqual({ hello: 'world' });
})();
```

## Recipes

<details id="promise">
<summary><a href="#promise">🔗</a> Client usage with Promise</summary>

```ts
import { ExecutionResult } from 'graphql';
import { createClient, RequestParams } from 'graphql-http';
import { getSession } from './my-auth';

const client = createClient({
url: 'http://hey.there:4000/graphql',
headers: async () => {
const session = await getSession();
if (session) {
return {
Authorization: `Bearer ${session.token}`,
};
}
},
});

function execute<Data, Extensions>(
params: RequestParams,
): [request: Promise<ExecutionResult<Data, Extensions>>, cancel: () => void] {
let cancel!: () => void;
const request = new Promise<ExecutionResult<Data, Extensions>>(
(resolve, reject) => {
let result: ExecutionResult<Data, Extensions>;
cancel = client.subscribe<Data, Extensions>(params, {
next: (data) => (result = data),
error: reject,
complete: () => resolve(result),
});
},
);
return [request, cancel];
}

(async () => {
const [request, cancel] = execute({
query: '{ hello }',
});

// just an example, not a real function
onUserLeavePage(() => {
cancel();
});

const result = await request;

expect(result).toBe({ data: { hello: 'world' } });
})();
```

</details>

</details>

<details id="observable">
<summary><a href="#observable">🔗</a> Client usage with <a href="https://github.com/tc39/proposal-observable">Observable</a></summary>

```js
import { Observable } from 'relay-runtime';
// or
import { Observable } from '@apollo/client/core';
// or
import { Observable } from 'rxjs';
// or
import Observable from 'zen-observable';
// or any other lib which implements Observables as per the ECMAScript proposal: https://github.com/tc39/proposal-observable
import { createClient } from 'graphql-http';
import { getSession } from './my-auth';

const client = createClient({
url: 'http://graphql.loves:4000/observables',
headers: async () => {
const session = await getSession();
if (session) {
return {
Authorization: `Bearer ${session.token}`,
};
}
},
});

const observable = new Observable((observer) =>
client.subscribe({ query: '{ hello }' }, observer),
);

const subscription = observable.subscribe({
next: (result) => {
expect(result).toBe({ data: { hello: 'world' } });
},
});

// unsubscribe will cancel the request if it is pending
subscription.unsubscribe();
```

</details>

<details id="relay">
<summary><a href="#relay">🔗</a> Client usage with <a href="https://relay.dev">Relay</a></summary>

```ts
import { GraphQLError } from 'graphql';
import {
Network,
Observable,
RequestParameters,
Variables,
} from 'relay-runtime';
import { createClient } from 'graphql-http';
import { getSession } from './my-auth';

const client = createClient({
url: 'http://i.love:4000/graphql',
headers: async () => {
const session = await getSession();
if (session) {
return {
Authorization: `Bearer ${session.token}`,
};
}
},
});

function fetch(operation: RequestParameters, variables: Variables) {
return Observable.create((sink) => {
if (!operation.text) {
return sink.error(new Error('Operation text cannot be empty'));
}
return client.subscribe(
{
operationName: operation.name,
query: operation.text,
variables,
},
sink,
);
});
}

export const network = Network.create(fetch);
```

</details>

<details id="apollo-client">
<summary><a href="#apollo-client">🔗</a> Client usage with <a href="https://www.apollographql.com">Apollo</a></summary>

```ts
import {
ApolloLink,
Operation,
FetchResult,
Observable,
} from '@apollo/client/core';
import { print, GraphQLError } from 'graphql';
import { createClient, ClientOptions, Client } from 'graphql-http';
import { getSession } from './my-auth';

class HTTPLink extends ApolloLink {
private client: Client;

constructor(options: ClientOptions) {
super();
this.client = createClient(options);
}

public request(operation: Operation): Observable<FetchResult> {
return new Observable((sink) => {
return this.client.subscribe<FetchResult>(
{ ...operation, query: print(operation.query) },
{
next: sink.next.bind(sink),
complete: sink.complete.bind(sink),
error: sink.error.bind(sink),
},
);
});
}
}

const link = new HTTPLink({
url: 'http://where.is:4000/graphql',
headers: async () => {
const session = await getSession();
if (session) {
return {
Authorization: `Bearer ${session.token}`,
};
}
},
});
```

</details>

<details id="request-retries">
<summary><a href="#request-retries">🔗</a> Client usage with request retries</summary>

```ts
import { createClient, NetworkError } from 'graphql-http';

const client = createClient({
url: 'http://unstable.service:4000/graphql',
shouldRetry: async (err: NetworkError, retries: number) => {
if (retries > 3) {
// max 3 retries and then report service down
return false;
}

// try again when service unavailable, could be temporary
if (err.response?.status === 503) {
// wait one second (you can alternatively time the promise resolution to your preference)
await new Promise((resolve) => setTimeout(resolve, 1000));
return true;
}

// otherwise report error immediately
return false;
},
});
```

</details>

<details id="auth">
<summary><a href="#auth">🔗</a> Server handler usage with authentication</summary>

Expand Down
26 changes: 26 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ graphql-http

## Table of contents

### Classes

- [NetworkError](classes/NetworkError.md)

### Interfaces

- [Client](interfaces/Client.md)
- [ClientOptions](interfaces/ClientOptions.md)
- [HandlerOptions](interfaces/HandlerOptions.md)
- [Headers](interfaces/Headers.md)
- [Request](interfaces/Request.md)
Expand All @@ -22,9 +28,29 @@ graphql-http

### Functions

- [createClient](README.md#createclient)
- [createHandler](README.md#createhandler)
- [isResponse](README.md#isresponse)

## Client

### createClient

▸ **createClient**(`options`): [`Client`](interfaces/Client.md)

Creates a disposable GraphQL over HTTP client to transmit
GraphQL operation results.

#### Parameters

| Name | Type |
| :------ | :------ |
| `options` | [`ClientOptions`](interfaces/ClientOptions.md) |

#### Returns

[`Client`](interfaces/Client.md)

## Common

### Response
Expand Down
63 changes: 63 additions & 0 deletions docs/classes/NetworkError.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
[graphql-http](../README.md) / NetworkError

# Class: NetworkError<Response\>

A network error caused by the client or an unexpected response from the server.

To avoid bundling DOM typings (because the client can run in Node env too),
you should supply the `Response` generic depending on your Fetch implementation.

## Type parameters

| Name | Type |
| :------ | :------ |
| `Response` | extends `ResponseLike` = `ResponseLike` |

## Hierarchy

- `Error`

↳ **`NetworkError`**

## Table of contents

### Constructors

- [constructor](NetworkError.md#constructor)

### Properties

- [response](NetworkError.md#response)

## Constructors

### constructor

• **new NetworkError**<`Response`\>(`msgOrErrOrResponse`)

#### Type parameters

| Name | Type |
| :------ | :------ |
| `Response` | extends `ResponseLike` = `ResponseLike` |

#### Parameters

| Name | Type |
| :------ | :------ |
| `msgOrErrOrResponse` | `string` \| `Response` \| `Error` |

#### Overrides

Error.constructor

## Properties

### response

• **response**: `undefined` \| `Response`

The underlyig response thats considered an error.

Will be undefined when no response is received,
instead an unexpected network error.
Loading