Skip to content

Commit

Permalink
Merge pull request #9 from nrccua/feature/headersLink
Browse files Browse the repository at this point in the history
Added a New, Optional Apollo Link: HeadersLink
  • Loading branch information
TheCleric authored Dec 6, 2021
2 parents eea1539 + ebc7c39 commit 239ebd2
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 26 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

## [1.2.0](https://github.com/nrccua/apollo-rest-utils/compare/1.1.1...1.2.0) (2021-12-06)


### Changes

* [E4E-0]: Flesh out unit tests ([5668f6b](https://github.com/nrccua/apollo-rest-utils/commit/5668f6b24a25e394865225ac3f8e0dd013a3007f))
* Merge branch 'main' into feature/headersLink ([991c60f](https://github.com/nrccua/apollo-rest-utils/commit/991c60f103428a30d1c24690a537fe0bc46b6dab))
* Merge pull request #10 from nrccua/fix/swagger_2_wordpress_fixes ([eea1539](https://github.com/nrccua/apollo-rest-utils/commit/eea1539cb6684a543a5a12b4143405f445b7fe29)), closes [#10](https://github.com/nrccua/apollo-rest-utils/issues/10)
* [E4E-0]: 1.1.1 ([81b1e1d](https://github.com/nrccua/apollo-rest-utils/commit/81b1e1d9c262314f91501f920e2ed3e24f7da125))
* [E4E-14]: Add eslint as a devDependency ([66613e4](https://github.com/nrccua/apollo-rest-utils/commit/66613e40b67d7151d414f9123333b1529af4c65b))

### [1.1.1](https://github.com/nrccua/apollo-rest-utils/compare/1.1.0...1.1.1) (2021-12-03)


Expand Down
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.
46 changes: 46 additions & 0 deletions lib/HeadersLink/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { NextLink, Operation } from '@apollo/client';
import HeadersLink from '.';

describe('HeadersLink', () => {
const mockForward = jest.fn();
const mockGetContext = jest.fn();
const mockOperation = {
getContext: mockGetContext,
} as unknown as Operation;

beforeEach(() => {
jest.resetAllMocks();
mockForward.mockReturnValue([{}]);
mockGetContext.mockReturnValue({
restResponses: [{}],
});
});

afterAll(() => {
jest.restoreAllMocks();
});

it('can handle requests without headers', () => {
const headersLink = new HeadersLink();

const result = headersLink.request(mockOperation, mockForward as unknown as NextLink);

expect(result).toEqual([{ data: { headers: {} } }]);
});

it('can handle requests with headers', () => {
mockGetContext.mockReturnValue({
restResponses: [
{
headers: [['x-api-key', '12345']],
},
],
});

const headersLink = new HeadersLink();

const result = headersLink.request(mockOperation, mockForward as unknown as NextLink);

expect(result).toEqual([{ data: { headers: { xApiKey: '12345' } } }]);
});
});
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';
13 changes: 13 additions & 0 deletions lib/useRestQuery/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,19 @@ describe('validateQueryAgainstEndpoint', () => {
expect(() => validateQueryAgainstEndpoint(query, dummyEndpoint)).not.toThrowError();
});

it('should not throw an error for a valid query with headers', () => {
const query = gql`
query TestQuery($input: input) {
refreshToken(input: $input) {
sessionToken
}
headers
}
`;

expect(() => validateQueryAgainstEndpoint(query, dummyEndpoint)).not.toThrowError();
});

it('should throw an error for a query with no definitions', () => {
const query = { definitions: [], kind: 'Document' } as DocumentNode;

Expand Down
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
31 changes: 15 additions & 16 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@types/react": "17.0.35",
"babel-plugin-root-import": "5.1.0",
"babel-plugin-transform-imports": "2.0.0",
"eslint": "8.3.0",
"eslint-import-resolver-babel-plugin-root-import": "1.1.1",
"eslint-plugin-react": "7.27.1",
"husky": "7.0.4",
Expand Down Expand Up @@ -67,5 +68,5 @@
"update:version:major": "standard-version --release-as major",
"update:version:minor": "standard-version --release-as minor"
},
"version": "1.1.1"
"version": "1.2.0"
}

0 comments on commit 239ebd2

Please sign in to comment.