Skip to content

Commit f83881d

Browse files
[RK] Transform request & response fix, add documentation (#53)
1 parent 2605834 commit f83881d

File tree

7 files changed

+114
-18
lines changed

7 files changed

+114
-18
lines changed

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,45 @@ api.mount('GetUser').withOptions({
331331
});
332332
```
333333

334+
### Request & Response Transformations
335+
336+
GraphQL Rest Router allows the developer to add transformations on incoming requests or outgoing responses. By default, the regular axios transformers are used.
337+
338+
Say the shape of data coming from GraphQL is not what your consuming application needs. Instead of putting transformational logic into your consuming components, you can encapsulate it into the REST layer.
339+
340+
```js
341+
import GraphQLRestRouter from 'graphql-rest-router';
342+
343+
const api = new GraphQLRestRouter('http://localhost:1227', schema);
344+
345+
api.mount('GetImages').withOption('transformResponse', (response => {
346+
const { data, errors } = response;
347+
348+
return {
349+
data: {
350+
// Turn images array into image URL map
351+
images: data.images?.reduce((acc, img) => {
352+
acc[img.url] = img;
353+
}, {}),
354+
errors,
355+
}
356+
};
357+
}));
358+
```
359+
360+
You can also modify the outgoing request. These transforms should return a string request, but also allow you to modify the request headers.
361+
362+
```js
363+
import GraphQLRestRouter from 'graphql-rest-router';
364+
365+
const api = new GraphQLRestRouter('http://localhost:1227', schema);
366+
367+
api.mount('GetImages').at('/images').withOption('transformRequest', (request, headers) => {
368+
headers['X-My-Header'] = 'MyValue';
369+
return request;
370+
});
371+
```
372+
334373
### Logging
335374
336375
GraphQL Rest Router is capable of logging incoming requests and errors. When creating your router, you may use a logger of your own choice. GraphQL Rest Router allows you to configure log levels. The logger parameter must implement [ILogger](https://github.com/Econify/graphql-rest-router/blob/29cc328f23b8dd579a6f4af242266460e95e7d69/src/types.ts#L101-L107), and is compatible with most standard logging libraries.

example-consuming-client/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example-consuming-client/src/api.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ api.mount('GetCharacters').at('/characters/').withOptions({
1919
api.mount('GetCharacterById').at('/characters/:id');
2020

2121
api.mount('GetLocations').at('/locations').withOption('transformResponse', (response) => {
22-
const json = JSON.parse(response);
23-
const { data, errors } = json;
22+
const { data, errors } = response;
2423

2524
return {
2625
data: {

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"typescript": "4.0.3"
5151
},
5252
"dependencies": {
53-
"axios": "^0.21.1",
53+
"axios": "^0.21.4",
5454
"express": "^4.17.1",
5555
"graphql": "^14.0.2",
5656
"redis": "^3.1.2",

src/Route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
OperationDefinitionNode, ListTypeNode, VariableDefinitionNode,
44
parse, print, getOperationAST, NonNullTypeNode, DocumentNode,
55
} from 'graphql';
6-
import { AxiosTransformer, AxiosInstance, AxiosRequestConfig } from 'axios';
6+
import axios, { AxiosTransformer, AxiosInstance, AxiosRequestConfig } from 'axios';
77
import * as express from 'express';
88

99
import { createHash } from 'crypto';
@@ -124,8 +124,8 @@ export default class Route implements IMountableItem {
124124
private schema!: DocumentNode;
125125
private logger!: Logger;
126126

127-
private transformRequestFn: AxiosTransformer[] = [];
128-
private transformResponseFn: AxiosTransformer[] = [];
127+
private transformRequestFn = axios.defaults.transformRequest as AxiosTransformer[];
128+
private transformResponseFn = axios.defaults.transformResponse as AxiosTransformer[];
129129

130130
private staticVariables: Record<string, unknown> = {};
131131
private defaultVariables: Record<string, unknown> = {};

test/Route.test.js

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ describe('Route', () => {
5656
route = new Route({ schema, operationName: 'GetUserById', logger: console, logLevel: 3 });
5757
});
5858

59-
it('should allow you to change logging level with .setLogLevel()', () => {
59+
it('should allow you to change logging level with .withOption()', () => {
6060
const newLogLevel = -1;
6161

62-
route.setLogLevel(newLogLevel);
62+
route.withOption('logLevel', newLogLevel);
6363

6464
assert.equal(route.logger.logLevel, newLogLevel);
6565
})
@@ -98,6 +98,65 @@ describe('Route', () => {
9898
});
9999
});
100100

101+
describe('#transformResponse', () => {
102+
const operationName = 'GetUserById';
103+
let route;
104+
105+
beforeEach(() => {
106+
route = new Route({ schema, operationName });
107+
});
108+
109+
it('should include the default axios transformResponse', () => {
110+
assert.equal(route.transformResponseFn.length, 1)
111+
});
112+
113+
it('should append additional transforms when called', () => {
114+
route.withOption('transformResponse', (data) => 'testing transform')
115+
assert.equal(route.transformResponseFn.length, 2);
116+
});
117+
118+
it('should return data as JSON if response is stringified JSON', async () => {
119+
const stringifiedJSON = "{\"data\":{\"users\":[{\"id\":1,\"name\":\"Charles Barkley\"}]}}";
120+
const transformResponse = route.transformResponseFn[0];
121+
const transitional = {
122+
silentJSONParsing: true,
123+
forcedJSONParsing: true,
124+
clarifyTimeoutError: false
125+
};
126+
// HACK: transitional default not bound to axios function by default. This is fixed in
127+
// later versions of axios (> 0.24.0), please remove after upgrade.
128+
const data = await transformResponse.bind({ transitional })(stringifiedJSON);
129+
130+
assert.strictEqual(typeof data, 'object');
131+
});
132+
});
133+
134+
describe('#transformRequest', () => {
135+
const operationName = 'GetUserById';
136+
let route;
137+
138+
beforeEach(() => {
139+
route = new Route({ schema, operationName });
140+
});
141+
142+
it('should include the default axios transformRequest', () => {
143+
assert.equal(route.transformRequestFn.length, 1)
144+
});
145+
146+
it('should append additional transforms when called', () => {
147+
route.withOption('transformRequest', (data) => 'testing transform')
148+
assert.equal(route.transformRequestFn.length, 2);
149+
});
150+
151+
it('should return request', async () => {
152+
const stringifiedRequest = `{"query":"{users}","operationName":"${operationName}"}`;
153+
const transformRequest = route.transformRequestFn[0];
154+
const data = await transformRequest(stringifiedRequest);
155+
156+
assert.strictEqual(data, stringifiedRequest);
157+
});
158+
});
159+
101160
describe('private#setOperationName', () => {
102161
it('throws an error if operation name does not exist in the schema', () => {
103162
assert.throws(() => {
@@ -141,7 +200,6 @@ describe('Route', () => {
141200
const operationAsPath = `/${operationName}`;
142201

143202
const route = new Route({ schema, operationName });
144-
145203
assert.equal(route.operationName, operationName);
146204
assert.equal(route.path, operationAsPath);
147205
});

0 commit comments

Comments
 (0)