Skip to content

Commit 95219cd

Browse files
authored
fix(LocalGraphQLClient): handle middleware and responseReducer (#1206)
1 parent 4aa3b12 commit 95219cd

File tree

5 files changed

+125
-34
lines changed

5 files changed

+125
-34
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
if (typeof global.Response === 'undefined') {
2+
global.Response = function () {}
3+
}

jest.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ const projects = [
4545
'\\.[jt]sx?$': 'babel-jest'
4646
},
4747
displayName: 'cra-example',
48-
testEnvironment: 'jsdom'
48+
testEnvironment: 'jsdom',
49+
setupFiles: ['<rootDir>/examples/create-react-app/test/setup.js']
4950
}
5051
}
5152
]

packages/graphql-hooks/src/GraphQLClient.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -348,11 +348,11 @@ class GraphQLClient {
348348
return response.json().then(({ errors, data }) => {
349349
return this.generateResult<ResponseData, TGraphQLError>({
350350
graphQLErrors: errors,
351-
data:
352-
// enrich data with responseReducer if defined
353-
typeof options.responseReducer === 'function'
354-
? options.responseReducer(data, response)
355-
: data,
351+
data: applyResponseReducer(
352+
options.responseReducer,
353+
data,
354+
response
355+
),
356356
headers: response.headers
357357
})
358358
})
@@ -466,4 +466,15 @@ function isGraphQLWsClient(value: any): value is GraphQLWsClient {
466466
return typeof value.subscribe === 'function'
467467
}
468468

469+
export function applyResponseReducer(
470+
responseReducer: RequestOptions['responseReducer'],
471+
data,
472+
response: Response
473+
) {
474+
// enrich data with responseReducer if defined
475+
return typeof responseReducer === 'function'
476+
? responseReducer(data, response)
477+
: data
478+
}
479+
469480
export default GraphQLClient

packages/graphql-hooks/src/LocalGraphQLClient.ts

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import GraphQLClient from './GraphQLClient'
1+
import GraphQLClient, { applyResponseReducer } from './GraphQLClient'
22
import LocalGraphQLError from './LocalGraphQLError'
3-
import { LocalClientOptions, LocalQueries, Result } from './types/common-types'
3+
import {
4+
LocalClientOptions,
5+
LocalQueries,
6+
Operation,
7+
RequestOptions,
8+
Result
9+
} from './types/common-types'
410

511
/** Local version of the GraphQLClient which only returns specified queries
612
* Meant to be used as a way to easily mock and test queries during development. This client never contacts any actual server.
@@ -27,7 +33,7 @@ class LocalGraphQLClient extends GraphQLClient {
2733
// Delay before sending responses in miliseconds for simulating latency
2834
requestDelayMs: number
2935
constructor(config: LocalClientOptions) {
30-
super({ url: '', ...config })
36+
super({ url: 'http://localhost', ...config })
3137
this.localQueries = config.localQueries
3238
this.requestDelayMs = config.requestDelayMs || 0
3339
if (!this.localQueries) {
@@ -41,29 +47,38 @@ class LocalGraphQLClient extends GraphQLClient {
4147
// Skips all config verification from the parent class because we're mocking the client
4248
}
4349

44-
request<ResponseData = any, TGraphQLError = object, TVariables = object>(
45-
operation
46-
): Promise<Result<any, TGraphQLError>> {
47-
if (!this.localQueries[operation.query]) {
48-
throw new Error(
49-
`LocalGraphQLClient: no query match for: ${operation.query}`
50-
)
51-
}
52-
return timeoutPromise(this.requestDelayMs)
53-
.then(() =>
54-
Promise.resolve(
55-
this.localQueries[operation.query](
56-
operation.variables,
57-
operation.operationName
58-
)
50+
requestViaHttp<ResponseData, TGraphQLError = object, TVariables = object>(
51+
operation: Operation<TVariables>,
52+
options: RequestOptions = {}
53+
): Promise<Result<ResponseData, TGraphQLError>> {
54+
return timeoutPromise(this.requestDelayMs).then(() => {
55+
if (!operation.query || !this.localQueries[operation.query]) {
56+
throw new Error(
57+
`LocalGraphQLClient: no query match for: ${operation.query}`
5958
)
59+
}
60+
61+
const data = this.localQueries[operation.query](
62+
operation.variables,
63+
operation.operationName
6064
)
65+
66+
return applyResponseReducer(options.responseReducer, data, new Response())
67+
})
68+
}
69+
70+
request<ResponseData, TGraphQLError = object, TVariables = object>(
71+
operation: Operation<TVariables>,
72+
options?: RequestOptions
73+
): Promise<Result<any, TGraphQLError>> {
74+
return super
75+
.request<ResponseData, TGraphQLError, TVariables>(operation, options)
6176
.then(result => {
6277
if (result instanceof LocalGraphQLError) {
6378
return { error: result }
6479
}
65-
const { data, errors } = collectErrorsFromObject(result)
66-
if (errors.length > 0) {
80+
const { data, errors } = collectErrors(result)
81+
if (errors && errors.length > 0) {
6782
return {
6883
data,
6984
error: new LocalGraphQLError({
@@ -76,7 +91,6 @@ class LocalGraphQLClient extends GraphQLClient {
7691
})
7792
}
7893
}
79-
8094
function timeoutPromise(delayInMs) {
8195
return new Promise(resolve => {
8296
setTimeout(resolve, delayInMs)
@@ -95,7 +109,7 @@ function collectErrorsFromObject(objectIn: object): {
95109
const errors: Error[] = []
96110

97111
for (const [key, value] of Object.entries(objectIn)) {
98-
const child = collectErrorsFromChild(value)
112+
const child = collectErrors(value)
99113
data[key] = child.data
100114
if (child.errors != null) {
101115
errors.push(...child.errors)
@@ -113,7 +127,7 @@ function collectErrorsFromArray(arrayIn: object[]): {
113127
const errors: Error[] = []
114128

115129
for (const [idx, entry] of arrayIn.entries()) {
116-
const child = collectErrorsFromChild(entry)
130+
const child = collectErrors(entry)
117131
data[idx] = child.data
118132
if (child.errors != null) {
119133
errors.push(...child.errors)
@@ -123,7 +137,7 @@ function collectErrorsFromArray(arrayIn: object[]): {
123137
return { data, errors }
124138
}
125139

126-
function collectErrorsFromChild(entry: object) {
140+
function collectErrors(entry: object) {
127141
if (entry instanceof Error) {
128142
return { data: null, errors: [entry] }
129143
} else if (Array.isArray(entry)) {

packages/graphql-hooks/test-jsdom/unit/LocalGraphQLClient.test.tsx

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ const QUERY_PARTIAL_ERROR_WITH_ARRAY = {
2929
query: 'PartialErrorQueryWithArray'
3030
}
3131

32+
const QUERY_ARRAY = {
33+
query: 'ArrayQuery'
34+
}
35+
3236
const HooksTestQuery = `
3337
query {
3438
testQuery {
@@ -54,15 +58,19 @@ const localQueries = {
5458
PartialErrorQuery: () => ({
5559
property1: 'Hello World',
5660
property2: new Error('failed to resolve property 2'),
57-
nested: {property3: new Error('failed to resolve nested property 3'), property4: 'Hello again'}
61+
nested: {
62+
property3: new Error('failed to resolve nested property 3'),
63+
property4: 'Hello again'
64+
}
5865
}),
5966
PartialErrorQueryWithArray: () => ({
6067
property1: 'Hello World',
6168
arrayProperty: [
62-
{item: 'Hello item'},
69+
{ item: 'Hello item' },
6370
new Error('failed to resolve child of array')
6471
]
6572
}),
73+
ArrayQuery: () => [{ item: 'Hello item' }],
6674
[HooksTestQuery]: () => ({
6775
testQuery: {
6876
value: 2
@@ -123,7 +131,9 @@ describe('LocalGraphQLClient', () => {
123131
expect(result.error.graphQLErrors).toEqual(
124132
expect.arrayContaining([
125133
expect.objectContaining({ message: 'failed to resolve property 2' }),
126-
expect.objectContaining({ message: 'failed to resolve nested property 3' })
134+
expect.objectContaining({
135+
message: 'failed to resolve nested property 3'
136+
})
127137
])
128138
)
129139
})
@@ -138,10 +148,18 @@ describe('LocalGraphQLClient', () => {
138148
expect(result.error).toBeDefined()
139149
expect(result.error.graphQLErrors).toEqual(
140150
expect.arrayContaining([
141-
expect.objectContaining({ message: 'failed to resolve child of array' }),
151+
expect.objectContaining({
152+
message: 'failed to resolve child of array'
153+
})
142154
])
143155
)
144156
})
157+
it('should handle array result', async () => {
158+
const result = await client.request(QUERY_ARRAY)
159+
expect(result.data).toBeInstanceOf(Array)
160+
expect(result.data).toHaveLength(1)
161+
expect(result.data[0]).toHaveProperty('item', 'Hello item')
162+
})
145163
})
146164
describe('integration with hooks', () => {
147165
let client, wrapper
@@ -159,4 +177,48 @@ describe('LocalGraphQLClient', () => {
159177
expect(dataNode.textContent).toBe('2')
160178
})
161179
})
180+
describe('middleware', () => {
181+
let client: LocalGraphQLClient
182+
const middlewareSpy = jest.fn()
183+
const addResponseHookSpy = jest.fn()
184+
185+
beforeEach(() => {
186+
client = new LocalGraphQLClient({
187+
localQueries,
188+
middleware: [
189+
({ addResponseHook }, next) => {
190+
addResponseHook(response => {
191+
addResponseHookSpy()
192+
return response
193+
})
194+
middlewareSpy()
195+
next()
196+
}
197+
]
198+
})
199+
})
200+
it('should run middleware', async () => {
201+
const result = await client.request(QUERY_BASIC)
202+
203+
expect(result.data.hello).toBe('Hello world')
204+
expect(middlewareSpy).toHaveBeenCalledTimes(1)
205+
expect(addResponseHookSpy).toHaveBeenCalledTimes(1)
206+
})
207+
})
208+
describe('responseReducer option', () => {
209+
let client: LocalGraphQLClient
210+
211+
beforeEach(() => {
212+
client = new LocalGraphQLClient({
213+
localQueries
214+
})
215+
})
216+
it('should return responseReducer result', async () => {
217+
const result = await client.request<string[]>(QUERY_ARRAY, {
218+
responseReducer: fetchedData => [...fetchedData, 'foo']
219+
})
220+
221+
expect(result.data).toStrictEqual([{ item: 'Hello item' }, 'foo'])
222+
})
223+
})
162224
})

0 commit comments

Comments
 (0)