Skip to content

Commit ff9a04f

Browse files
authored
Merge pull request #213 from aave/cesare/aave-3193-sdk-fix-issue-with-gql-errors-blocking-transaction-hooks
fix: issue where GQL error blocks React transaction hooks
2 parents da5a59b + 0966b94 commit ff9a04f

File tree

12 files changed

+158
-87
lines changed

12 files changed

+158
-87
lines changed

.changeset/hip-views-cough.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@aave/client": patch
3+
"@aave/react": patch
4+
"@aave/core": patch
5+
---
6+
7+
**fix:** issue with GQL errors suchas Bad User Input or Bad Request blocking indefintely transaction hooks.

packages/client/src/testing.ts

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference path="../../../vite-env.d.ts" />
22

3-
import { GraphQLErrorCode, UnexpectedError } from '@aave/core';
3+
import { UnexpectedError } from '@aave/core';
44
import { encodeHubId, encodeSpokeId } from '@aave/graphql';
55
import {
66
type BigDecimal,
@@ -236,30 +236,6 @@ export function fundErc20Address(
236236
);
237237
}
238238

239-
const messages: Record<GraphQLErrorCode, string> = {
240-
[GraphQLErrorCode.UNAUTHENTICATED]:
241-
"Unauthenticated - Authentication is required to access '<operation>'",
242-
[GraphQLErrorCode.FORBIDDEN]:
243-
"Forbidden - You are not authorized to access '<operation>'",
244-
[GraphQLErrorCode.INTERNAL_SERVER_ERROR]:
245-
'Internal server error - Please try again later',
246-
[GraphQLErrorCode.BAD_USER_INPUT]:
247-
'Bad user input - Please check the input and try again',
248-
[GraphQLErrorCode.BAD_REQUEST]:
249-
'Bad request - Please check the request and try again',
250-
};
251-
252-
export function createGraphQLErrorObject(code: GraphQLErrorCode) {
253-
return {
254-
message: messages[code],
255-
locations: [],
256-
path: [],
257-
extensions: {
258-
code: code,
259-
},
260-
};
261-
}
262-
263239
export function wait(ms: number) {
264240
return new Promise((resolve) => setTimeout(resolve, ms));
265241
}

packages/core/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
".": {
1616
"import": "./dist/index.js",
1717
"require": "./dist/index.cjs"
18+
},
19+
"./testing": {
20+
"default": "./dist/testing.js"
1821
}
1922
},
2023
"typesVersions": {
@@ -24,6 +27,9 @@
2427
],
2528
"require": [
2629
"./dist/index.d.cts"
30+
],
31+
"testing": [
32+
"./dist/testing.d.ts"
2733
]
2834
}
2935
},

packages/core/src/GqlClient.test.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { assertOk, ResultAsync } from '@aave/types';
1+
import { assertErr, assertOk, ResultAsync } from '@aave/types';
22
import { gql, stringifyDocument } from '@urql/core';
33
import * as msw from 'msw';
44
import { setupServer } from 'msw/node';
@@ -11,9 +11,10 @@ import {
1111
expect,
1212
it,
1313
} from 'vitest';
14-
1514
import type { Context } from './context';
15+
import { GraphQLErrorCode } from './errors';
1616
import { GqlClient } from './GqlClient';
17+
import { createGraphQLErrorObject } from './testing';
1718
import { delay } from './utils';
1819

1920
// see: https://mswjs.io/docs/graphql/mocking-responses/query-batching
@@ -153,6 +154,26 @@ describe(`Given an instance of the ${GqlClient.name}`, () => {
153154
});
154155
});
155156

157+
describe('When executing a query that fails with GraphQL errors', () => {
158+
beforeAll(() => {
159+
server.use(
160+
api.query(TestQuery, () => {
161+
return msw.HttpResponse.json({
162+
errors: [createGraphQLErrorObject(GraphQLErrorCode.BAD_REQUEST)],
163+
});
164+
}),
165+
);
166+
});
167+
168+
it('Then it should fail with an `UnexpectedError`', async () => {
169+
const client = new GqlClient(context);
170+
171+
const result = await client.query(TestQuery, { id: 1 });
172+
173+
assertErr(result);
174+
});
175+
});
176+
156177
describe('When batching concurrent queries', () => {
157178
it('Then it should limit batching to a maximum of 10 queries', async () => {
158179
const client = new GqlClient(context);

packages/core/src/GqlClient.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
extractOperationName,
2222
isActiveQueryOperation,
2323
isTeardownOperation,
24-
takeValue,
2524
} from './utils';
2625

2726
/**
@@ -99,7 +98,7 @@ export class GqlClient {
9998
const query = this.resolver.replaceFrom(document);
10099
return this.resultFrom(
101100
this.urql.query(query, variables, { batch, requestPolicy }),
102-
).map(takeValue);
101+
);
103102
}
104103

105104
/**
@@ -113,9 +112,7 @@ export class GqlClient {
113112
document: TypedDocumentNode<StandardData<TValue>, TVariables>,
114113
variables: TVariables,
115114
): ResultAsync<TValue, UnexpectedError> {
116-
return this.resultFrom(this.urql.mutation(document, variables)).map(
117-
takeValue,
118-
);
115+
return this.resultFrom(this.urql.mutation(document, variables));
119116
}
120117

121118
protected async refreshWhere(
@@ -199,17 +196,24 @@ export class GqlClient {
199196
);
200197
}
201198

202-
private resultFrom<TData, TVariables extends AnyVariables>(
203-
source: OperationResultSource<OperationResult<TData, TVariables>>,
204-
): ResultAsync<OperationResult<TData, TVariables>, UnexpectedError> {
199+
private resultFrom<TValue, TVariables extends AnyVariables>(
200+
source: OperationResultSource<
201+
OperationResult<StandardData<TValue>, TVariables>
202+
>,
203+
): ResultAsync<TValue, UnexpectedError> {
205204
return ResultAsync.fromPromise(source.toPromise(), (err: unknown) => {
206205
this.logger.error(err);
207206
return UnexpectedError.from(err);
208207
}).andThen((result) => {
209208
if (result.error?.networkError) {
210209
return errAsync(UnexpectedError.from(result.error.networkError));
211210
}
212-
return okAsync(result);
211+
212+
if (result.data) {
213+
return okAsync(result.data.value);
214+
}
215+
216+
return errAsync(UnexpectedError.from(result.error));
213217
});
214218
}
215219
}

packages/core/src/testing.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { GraphQLErrorCode } from './errors';
2+
3+
const messages: Record<GraphQLErrorCode, string> = {
4+
[GraphQLErrorCode.UNAUTHENTICATED]:
5+
"Unauthenticated - Authentication is required to access '<operation>'",
6+
[GraphQLErrorCode.FORBIDDEN]:
7+
"Forbidden - You are not authorized to access '<operation>'",
8+
[GraphQLErrorCode.INTERNAL_SERVER_ERROR]:
9+
'Internal server error - Please try again later',
10+
[GraphQLErrorCode.BAD_USER_INPUT]:
11+
'Bad user input - Please check the input and try again',
12+
[GraphQLErrorCode.BAD_REQUEST]:
13+
'Bad request - Please check the request and try again',
14+
};
15+
16+
export function createGraphQLErrorObject(code: GraphQLErrorCode) {
17+
return {
18+
message: messages[code],
19+
locations: [],
20+
path: [],
21+
extensions: {
22+
code: code,
23+
},
24+
};
25+
}

packages/core/src/utils.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { invariant } from '@aave/types';
2-
import type { AnyVariables, Operation, OperationResult } from '@urql/core';
1+
import type { Operation } from '@urql/core';
32
import { Kind, type OperationDefinitionNode } from 'graphql';
4-
import type { StandardData } from './types';
53

64
/**
75
* @internal
@@ -10,17 +8,6 @@ export function delay(ms: number): Promise<void> {
108
return new Promise((resolve) => setTimeout(resolve, ms));
119
}
1210

13-
/**
14-
* @internal
15-
*/
16-
export function takeValue<T>({
17-
data,
18-
error,
19-
}: OperationResult<StandardData<T> | undefined, AnyVariables>): T {
20-
invariant(data, `Expected a value, got: ${error?.message}`);
21-
return data.value;
22-
}
23-
2411
/**
2512
* @internal
2613
*/

packages/core/tsup.config.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,34 @@
11
/* eslint-disable import/no-default-export */
22
import { defineConfig } from 'tsup';
33

4-
export default defineConfig(() => ({
5-
entry: ['src/index.ts'],
6-
outDir: 'dist',
7-
splitting: false,
8-
sourcemap: true,
9-
treeshake: true,
10-
clean: true,
11-
tsconfig: 'tsconfig.build.json',
12-
bundle: true,
13-
minify: true,
14-
dts: true,
15-
platform: 'neutral',
16-
format: ['esm', 'cjs'],
17-
}));
4+
export default defineConfig(() => [
5+
{
6+
entry: ['src/index.ts'],
7+
outDir: 'dist',
8+
splitting: false,
9+
sourcemap: true,
10+
treeshake: true,
11+
clean: true,
12+
tsconfig: 'tsconfig.build.json',
13+
bundle: true,
14+
minify: true,
15+
dts: true,
16+
platform: 'neutral',
17+
format: ['esm', 'cjs'],
18+
},
19+
// ESM only
20+
{
21+
entry: ['src/testing.ts'],
22+
outDir: 'dist',
23+
splitting: false,
24+
sourcemap: true,
25+
treeshake: true,
26+
clean: true,
27+
tsconfig: 'tsconfig.build.json',
28+
bundle: true,
29+
minify: true,
30+
dts: true,
31+
platform: 'neutral',
32+
format: ['esm'],
33+
},
34+
]);

packages/react/src/helpers/reads.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { UnexpectedError } from '@aave/client';
1+
import { GraphQLErrorCode, UnexpectedError } from '@aave/client';
2+
import { createGraphQLErrorObject } from '@aave/core/testing';
23
import { act } from '@testing-library/react';
34
import { graphql, HttpResponse } from 'msw';
45
import { setupServer } from 'msw/node';
@@ -66,9 +67,7 @@ describe(`Given the '${useSuspendableQuery.name}' hook`, () => {
6667
server.use(
6768
graphql.query(AnyQuery, () => {
6869
return HttpResponse.json({
69-
errors: [
70-
{ message: 'Test error', extensions: { code: 'TEST_ERROR' } },
71-
],
70+
errors: [createGraphQLErrorObject(GraphQLErrorCode.BAD_REQUEST)],
7271
});
7372
}),
7473
);

packages/react/src/helpers/tasks.test.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,27 @@ describe(`Given the '${useAsyncTask.name}' hook`, () => {
115115
});
116116
});
117117

118+
describe('When the task throws an error synchronously', () => {
119+
it('Then it should revert to the previous state and rethrow the error', async () => {
120+
const { result } = renderHook(() =>
121+
useAsyncTask((_: string) => {
122+
throw new Error('test error');
123+
}, []),
124+
);
125+
const state = result.current[1];
126+
127+
await act(async () => {
128+
await expect(async () => {
129+
await result.current[0]('test');
130+
}).rejects.toThrow(Error);
131+
});
132+
133+
expect(result.current[1]).toMatchObject(state);
134+
});
135+
});
136+
118137
describe('When the task fails', () => {
119-
it('Then it should return the state in line with type of `AsyncTaskFailed`', async () => {
138+
it('Then it should return the state in line with type of `AsyncTaskError`', async () => {
120139
const { result } = renderHook(() =>
121140
useAsyncTask((_: string) => errAsync(new Error('test error')), []),
122141
);
@@ -150,7 +169,7 @@ describe(`Given the '${useAsyncTask.name}' hook`, () => {
150169
});
151170
});
152171

153-
describe('And the hook is executed once with an error', () => {
172+
describe('And a previous execution failed', () => {
154173
describe('When the hook is executed again', () => {
155174
it('Then it should return the state in line with type of `AsyncTaskLoading`', async () => {
156175
const { result } = renderHook(() =>
@@ -235,7 +254,7 @@ describe(`Given the '${useAsyncTask.name}' hook`, () => {
235254
});
236255

237256
describe('When the task fails', () => {
238-
it('Then it should return the state in line with type of `AsyncTaskFailed`', async () => {
257+
it('Then it should return the state in line with type of `AsyncTaskError`', async () => {
239258
const { result } = renderHook(() =>
240259
useAsyncTask((input: string) => {
241260
if (input === 'one') return okAsync(input);

0 commit comments

Comments
 (0)