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

Added a New, Optional Apollo Link: HeadersLink #9

Merged
merged 12 commits into from
Dec 6, 2021
77 changes: 69 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ with REST apis and Apollo in a TypeScript application.

## Features

* A command line utiltity that takes a swagger file/url, and automatically
- A command line utiltity that takes a swagger file/url, and automatically
generates input and output types, and endpoint definitions that can be used
to make integration with `apollo-link-rest` much easier.
* Wrapper functions for common GraphQL operations that allow you to pass in
- Wrapper functions for common GraphQL operations that allow you to pass in
pure GraphQL, and enables the input variables and the result to be strongly
typed based on the swagger definition.
* Automatically checks your GraphQL at runtime and will throw exceptions if
- Automatically checks your GraphQL at runtime and will throw exceptions if
your GraphQL fields do not match the endpoint definition.
- Custom apollo links to cover REST API edge cases, such as using the
`headersLink` to retrieve data from REST response headers.

## Usage

Expand Down Expand Up @@ -69,18 +71,77 @@ To facilitate using this with multiple endpoints, you must specify an endpoint
id per endpoint. See
[https://www.apollographql.com/docs/react/api/link/apollo-link-rest/#multiple-endpoints](https://www.apollographql.com/docs/react/api/link/apollo-link-rest/#multiple-endpoints)

## Custom Links

### `HeadersLink`

This link allows you to access the REST API's response headers within the
`data` object that comes back from Apollo Client.

NOTE: Since GraphQL only accepts field names written in `camelCase`, the headers
should be requested in camel case format. I.e. if your REST API returns a
`total-count` header, you'll want to ask for it using `totalCount`.

#### `HeadersLink` Setup

```ts
import { RestLink } from 'apollo-link-rest';
import { HeadersLink } from 'apollo-rest-utils';

const headersLink = new HeadersLink();
const restLink = new RestLink({ ... });

new ApolloClient({
...
cache: new InMemoryCache({ ... }),
link: ApolloLink.from([headersLink, restLink]),
...
});
```

#### `HeadersLink` Usage

```ts
const { data } = wrapRestQuery<'something'>()(
gql`
query SomethingQuery($id: String!) {
something(id: $id) {
_id
fieldA
fieldB
fieldC
}
headers {
nextOffset
totalCount
totalPages
}
}
`,
{
endpoint: REST.GET.SOMETHING,
variables: {
id,
},
},
);

console.log(data?.something); // response data
console.log(data?.headers); // response headers
```

## Releasing

Checking in the dist folder is not necessary as it will be built upon
npm install by the downstream project.

After making any changes and merging them to main, please do the following:

* Create a new branch from main and run `npm run update:version`
* Verify the `CHANGELOG.md` generated changes
* Commit, push, and merge to main.
* Create a new
- Create a new branch from main and run `npm run update:version`
- Verify the `CHANGELOG.md` generated changes
- Commit, push, and merge to main.
- Create a new
[release](https://github.com/nrccua/apollo-rest-utils/releases/new) using
the tag generated in the previous steps
* Use the `Auto-generate release notes` button to generate the release notes,
- Use the `Auto-generate release notes` button to generate the release notes,
and add any context you may deem necessary.
29 changes: 29 additions & 0 deletions lib/HeadersLink/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ApolloLink, FetchResult, NextLink, Observable, Operation } from '@apollo/client';
import { camelCase, get } from 'lodash';

export class HeadersLink extends ApolloLink {
// eslint-disable-next-line class-methods-use-this
public request(operation: Operation, forward: NextLink): Observable<FetchResult> | null {
return forward(operation).map((response): Record<string, unknown> => {
const context = operation.getContext();

const headersMap = (get(context, 'restResponses[0].headers') as Map<string, unknown>) || new Map();
const headersObj: Record<string, unknown> = Object.fromEntries(headersMap);

const headersObjCamelCased: Record<string, unknown> = {};
Object.keys(headersObj).forEach((key): void => {
headersObjCamelCased[camelCase(key)] = headersObj[key];
});

return {
...response,
data: {
...response.data,
headers: headersObjCamelCased,
},
};
});
}
}

export default HeadersLink;
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './HeadersLink';
export * from './types';
export * from './useRestQuery';
12 changes: 11 additions & 1 deletion lib/useRestQuery/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
useQuery,
} from '@apollo/client';
import { DirectiveNode, FieldNode, OperationDefinitionNode } from 'graphql';
import { get } from 'lodash';

import { IEndpointOptions, getSchemaField, Input, InvalidQueryError, IRestEndpoint, NamedGQLResult } from '../types';

Expand All @@ -33,7 +34,16 @@ export function validateQueryAgainstEndpoint<TName extends string, TData = unkno
}

if (definition.selectionSet.selections.length !== 1) {
throw new InvalidQueryError('Query must contain exactly one selection', query, endpoint);
let selectionsIncludeHeaders = false;
definition.selectionSet.selections.forEach((selection): void => {
if (get(selection, 'name.value') === 'headers') {
selectionsIncludeHeaders = true;
}
});

if (!(selectionsIncludeHeaders && definition.selectionSet.selections.length === 2)) {
throw new InvalidQueryError('Query must contain exactly one selection, or one selection with headers (if using the HeadersLink)', query, endpoint);
}
}

const selection = definition.selectionSet.selections[0] as FieldNode;
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.