Skip to content

Commit

Permalink
fix(LocalGraphQLClient): handle middleware and responseReducer (#1206)
Browse files Browse the repository at this point in the history
  • Loading branch information
G100g authored Jul 22, 2024
1 parent 4aa3b12 commit 95219cd
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 34 deletions.
3 changes: 3 additions & 0 deletions examples/create-react-app/test/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
if (typeof global.Response === 'undefined') {
global.Response = function () {}
}
3 changes: 2 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ const projects = [
'\\.[jt]sx?$': 'babel-jest'
},
displayName: 'cra-example',
testEnvironment: 'jsdom'
testEnvironment: 'jsdom',
setupFiles: ['<rootDir>/examples/create-react-app/test/setup.js']
}
}
]
Expand Down
21 changes: 16 additions & 5 deletions packages/graphql-hooks/src/GraphQLClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,11 +348,11 @@ class GraphQLClient {
return response.json().then(({ errors, data }) => {
return this.generateResult<ResponseData, TGraphQLError>({
graphQLErrors: errors,
data:
// enrich data with responseReducer if defined
typeof options.responseReducer === 'function'
? options.responseReducer(data, response)
: data,
data: applyResponseReducer(
options.responseReducer,
data,
response
),
headers: response.headers
})
})
Expand Down Expand Up @@ -466,4 +466,15 @@ function isGraphQLWsClient(value: any): value is GraphQLWsClient {
return typeof value.subscribe === 'function'
}

export function applyResponseReducer(
responseReducer: RequestOptions['responseReducer'],
data,
response: Response
) {
// enrich data with responseReducer if defined
return typeof responseReducer === 'function'
? responseReducer(data, response)
: data
}

export default GraphQLClient
62 changes: 38 additions & 24 deletions packages/graphql-hooks/src/LocalGraphQLClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import GraphQLClient from './GraphQLClient'
import GraphQLClient, { applyResponseReducer } from './GraphQLClient'
import LocalGraphQLError from './LocalGraphQLError'
import { LocalClientOptions, LocalQueries, Result } from './types/common-types'
import {
LocalClientOptions,
LocalQueries,
Operation,
RequestOptions,
Result
} from './types/common-types'

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

request<ResponseData = any, TGraphQLError = object, TVariables = object>(
operation
): Promise<Result<any, TGraphQLError>> {
if (!this.localQueries[operation.query]) {
throw new Error(
`LocalGraphQLClient: no query match for: ${operation.query}`
)
}
return timeoutPromise(this.requestDelayMs)
.then(() =>
Promise.resolve(
this.localQueries[operation.query](
operation.variables,
operation.operationName
)
requestViaHttp<ResponseData, TGraphQLError = object, TVariables = object>(
operation: Operation<TVariables>,
options: RequestOptions = {}
): Promise<Result<ResponseData, TGraphQLError>> {
return timeoutPromise(this.requestDelayMs).then(() => {
if (!operation.query || !this.localQueries[operation.query]) {
throw new Error(
`LocalGraphQLClient: no query match for: ${operation.query}`
)
}

const data = this.localQueries[operation.query](
operation.variables,
operation.operationName
)

return applyResponseReducer(options.responseReducer, data, new Response())
})
}

request<ResponseData, TGraphQLError = object, TVariables = object>(
operation: Operation<TVariables>,
options?: RequestOptions
): Promise<Result<any, TGraphQLError>> {
return super
.request<ResponseData, TGraphQLError, TVariables>(operation, options)
.then(result => {
if (result instanceof LocalGraphQLError) {
return { error: result }
}
const { data, errors } = collectErrorsFromObject(result)
if (errors.length > 0) {
const { data, errors } = collectErrors(result)
if (errors && errors.length > 0) {
return {
data,
error: new LocalGraphQLError({
Expand All @@ -76,7 +91,6 @@ class LocalGraphQLClient extends GraphQLClient {
})
}
}

function timeoutPromise(delayInMs) {
return new Promise(resolve => {
setTimeout(resolve, delayInMs)
Expand All @@ -95,7 +109,7 @@ function collectErrorsFromObject(objectIn: object): {
const errors: Error[] = []

for (const [key, value] of Object.entries(objectIn)) {
const child = collectErrorsFromChild(value)
const child = collectErrors(value)
data[key] = child.data
if (child.errors != null) {
errors.push(...child.errors)
Expand All @@ -113,7 +127,7 @@ function collectErrorsFromArray(arrayIn: object[]): {
const errors: Error[] = []

for (const [idx, entry] of arrayIn.entries()) {
const child = collectErrorsFromChild(entry)
const child = collectErrors(entry)
data[idx] = child.data
if (child.errors != null) {
errors.push(...child.errors)
Expand All @@ -123,7 +137,7 @@ function collectErrorsFromArray(arrayIn: object[]): {
return { data, errors }
}

function collectErrorsFromChild(entry: object) {
function collectErrors(entry: object) {
if (entry instanceof Error) {
return { data: null, errors: [entry] }
} else if (Array.isArray(entry)) {
Expand Down
70 changes: 66 additions & 4 deletions packages/graphql-hooks/test-jsdom/unit/LocalGraphQLClient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ const QUERY_PARTIAL_ERROR_WITH_ARRAY = {
query: 'PartialErrorQueryWithArray'
}

const QUERY_ARRAY = {
query: 'ArrayQuery'
}

const HooksTestQuery = `
query {
testQuery {
Expand All @@ -54,15 +58,19 @@ const localQueries = {
PartialErrorQuery: () => ({
property1: 'Hello World',
property2: new Error('failed to resolve property 2'),
nested: {property3: new Error('failed to resolve nested property 3'), property4: 'Hello again'}
nested: {
property3: new Error('failed to resolve nested property 3'),
property4: 'Hello again'
}
}),
PartialErrorQueryWithArray: () => ({
property1: 'Hello World',
arrayProperty: [
{item: 'Hello item'},
{ item: 'Hello item' },
new Error('failed to resolve child of array')
]
}),
ArrayQuery: () => [{ item: 'Hello item' }],
[HooksTestQuery]: () => ({
testQuery: {
value: 2
Expand Down Expand Up @@ -123,7 +131,9 @@ describe('LocalGraphQLClient', () => {
expect(result.error.graphQLErrors).toEqual(
expect.arrayContaining([
expect.objectContaining({ message: 'failed to resolve property 2' }),
expect.objectContaining({ message: 'failed to resolve nested property 3' })
expect.objectContaining({
message: 'failed to resolve nested property 3'
})
])
)
})
Expand All @@ -138,10 +148,18 @@ describe('LocalGraphQLClient', () => {
expect(result.error).toBeDefined()
expect(result.error.graphQLErrors).toEqual(
expect.arrayContaining([
expect.objectContaining({ message: 'failed to resolve child of array' }),
expect.objectContaining({
message: 'failed to resolve child of array'
})
])
)
})
it('should handle array result', async () => {
const result = await client.request(QUERY_ARRAY)
expect(result.data).toBeInstanceOf(Array)
expect(result.data).toHaveLength(1)
expect(result.data[0]).toHaveProperty('item', 'Hello item')
})
})
describe('integration with hooks', () => {
let client, wrapper
Expand All @@ -159,4 +177,48 @@ describe('LocalGraphQLClient', () => {
expect(dataNode.textContent).toBe('2')
})
})
describe('middleware', () => {
let client: LocalGraphQLClient
const middlewareSpy = jest.fn()
const addResponseHookSpy = jest.fn()

beforeEach(() => {
client = new LocalGraphQLClient({
localQueries,
middleware: [
({ addResponseHook }, next) => {
addResponseHook(response => {
addResponseHookSpy()
return response
})
middlewareSpy()
next()
}
]
})
})
it('should run middleware', async () => {
const result = await client.request(QUERY_BASIC)

expect(result.data.hello).toBe('Hello world')
expect(middlewareSpy).toHaveBeenCalledTimes(1)
expect(addResponseHookSpy).toHaveBeenCalledTimes(1)
})
})
describe('responseReducer option', () => {
let client: LocalGraphQLClient

beforeEach(() => {
client = new LocalGraphQLClient({
localQueries
})
})
it('should return responseReducer result', async () => {
const result = await client.request<string[]>(QUERY_ARRAY, {
responseReducer: fetchedData => [...fetchedData, 'foo']
})

expect(result.data).toStrictEqual([{ item: 'Hello item' }, 'foo'])
})
})
})

0 comments on commit 95219cd

Please sign in to comment.