Skip to content

Commit 8ea4eed

Browse files
authored
Improve error messages when before/after hooks throw (#6239)
1 parent 79493fa commit 8ea4eed

5 files changed

Lines changed: 71 additions & 32 deletions

File tree

.changeset/olive-maps-serve.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@keystone-next/keystone': patch
3+
---
4+
5+
Added more details to before/after change/delete hook error messages.

packages/keystone/src/lib/core/graphql-errors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ export const validationFailureError = (messages: string[]) => {
77
return new ApolloError(`You provided invalid data for this operation.\n${s}`);
88
};
99

10+
export const extensionError = (extension: string, messages: string[]) => {
11+
const s = messages.map(m => ` - ${m}`).join('\n');
12+
return new ApolloError(`An error occured while running "${extension}".\n${s}`);
13+
};
14+
1015
// FIXME: In an upcoming PR we will use these args to construct a better
1116
// error message, so leaving the, here for now. - TL
1217
// eslint-disable-next-line @typescript-eslint/no-unused-vars

packages/keystone/src/lib/core/mutations/hooks.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { promiseAllRejectWithAllErrors } from '../utils';
1+
import { extensionError } from '../graphql-errors';
22

33
export async function runSideEffectOnlyHook<
44
HookName extends string,
@@ -14,6 +14,7 @@ export async function runSideEffectOnlyHook<
1414
hooks: {
1515
[Key in HookName]?: (args: any) => Promise<void> | void;
1616
};
17+
listKey: string;
1718
},
1819
Args extends Parameters<NonNullable<List['hooks'][HookName]>>[0]
1920
>(list: List, hookName: HookName, args: Args) {
@@ -30,14 +31,25 @@ export async function runSideEffectOnlyHook<
3031
}
3132

3233
// Field hooks
33-
await promiseAllRejectWithAllErrors(
34-
Object.entries(list.fields).map(async ([fieldKey, field]) => {
35-
if (shouldRunFieldLevelHook(fieldKey)) {
36-
await field.hooks[hookName]?.({ fieldPath: fieldKey, ...args });
34+
const fieldsErrors = [];
35+
for (const [fieldPath, field] of Object.entries(list.fields)) {
36+
if (shouldRunFieldLevelHook(fieldPath)) {
37+
try {
38+
// @ts-ignore
39+
await field.hooks[hookName]?.({ fieldPath, ...args });
40+
} catch (err) {
41+
fieldsErrors.push(`${list.listKey}.${fieldPath}: ${err.message}`);
3742
}
38-
})
39-
);
43+
}
44+
}
45+
if (fieldsErrors.length) {
46+
throw extensionError(hookName, fieldsErrors);
47+
}
4048

4149
// List hooks
42-
await list.hooks[hookName]?.(args);
50+
try {
51+
await list.hooks[hookName]?.(args);
52+
} catch (err) {
53+
throw extensionError(hookName, [`${list.listKey}: ${err.message}`]);
54+
}
4355
}

tests/api-tests/hooks/hook-errors.test.ts

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ const runner = setupTestRunner({
103103

104104
// Returns null and throws an error
105105
expect(data).toEqual({ createUser: null });
106-
expectExtensionError(errors, [
107-
{ path: ['createUser'], message: `Simulated error: ${phase}Change` },
106+
expectExtensionError(errors, `${phase}Change`, [
107+
{ path: ['createUser'], messages: [`User: Simulated error: ${phase}Change`] },
108108
]);
109109

110110
// Only the original user should exist for 'before', both exist for 'after'
@@ -130,8 +130,8 @@ const runner = setupTestRunner({
130130

131131
// Returns null and throws an error
132132
expect(data).toEqual({ updateUser: null });
133-
expectExtensionError(errors, [
134-
{ path: ['updateUser'], message: `Simulated error: ${phase}Change` },
133+
expectExtensionError(errors, `${phase}Change`, [
134+
{ path: ['updateUser'], messages: [`User: Simulated error: ${phase}Change`] },
135135
]);
136136

137137
// User should have its original name for 'before', and the new name for 'after'.
@@ -160,8 +160,8 @@ const runner = setupTestRunner({
160160

161161
// Returns null and throws an error
162162
expect(data).toEqual({ deleteUser: null });
163-
expectExtensionError(errors, [
164-
{ path: ['deleteUser'], message: `Simulated error: ${phase}Delete` },
163+
expectExtensionError(errors, `${phase}Delete`, [
164+
{ path: ['deleteUser'], messages: [`User: Simulated error: ${phase}Delete`] },
165165
]);
166166

167167
// Bad users should still be in the database for 'before', deleted for 'after'.
@@ -200,9 +200,9 @@ const runner = setupTestRunner({
200200
],
201201
});
202202
// The invalid creates should have errors which point to the nulls in their path
203-
expectExtensionError(errors, [
204-
{ path: ['createUsers', 1], message: `Simulated error: ${phase}Change` },
205-
{ path: ['createUsers', 3], message: `Simulated error: ${phase}Change` },
203+
expectExtensionError(errors, `${phase}Change`, [
204+
{ path: ['createUsers', 1], messages: [`User: Simulated error: ${phase}Change`] },
205+
{ path: ['createUsers', 3], messages: [`User: Simulated error: ${phase}Change`] },
206206
]);
207207

208208
// Three users should exist in the database for 'before,' five for 'after'.
@@ -256,9 +256,9 @@ const runner = setupTestRunner({
256256
],
257257
});
258258
// The invalid updates should have errors which point to the nulls in their path
259-
expectExtensionError(errors, [
260-
{ path: ['updateUsers', 1], message: `Simulated error: ${phase}Change` },
261-
{ path: ['updateUsers', 3], message: `Simulated error: ${phase}Change` },
259+
expectExtensionError(errors, `${phase}Change`, [
260+
{ path: ['updateUsers', 1], messages: [`User: Simulated error: ${phase}Change`] },
261+
{ path: ['updateUsers', 3], messages: [`User: Simulated error: ${phase}Change`] },
262262
]);
263263

264264
// All users should still exist in the database, un-changed for `before`, changed for `after`.
@@ -307,9 +307,9 @@ const runner = setupTestRunner({
307307
],
308308
});
309309
// The invalid deletes should have errors which point to the nulls in their path
310-
expectExtensionError(errors, [
311-
{ path: ['deleteUsers', 1], message: `Simulated error: ${phase}Delete` },
312-
{ path: ['deleteUsers', 3], message: `Simulated error: ${phase}Delete` },
310+
expectExtensionError(errors, `${phase}Delete`, [
311+
{ path: ['deleteUsers', 1], messages: [`User: Simulated error: ${phase}Delete`] },
312+
{ path: ['deleteUsers', 3], messages: [`User: Simulated error: ${phase}Delete`] },
313313
]);
314314

315315
// Three users should still exist in the database for `before`, only 1 for `after`.
@@ -343,8 +343,14 @@ const runner = setupTestRunner({
343343
data: { title: `trigger ${phase}`, content: `trigger ${phase}` },
344344
},
345345
});
346-
expectExtensionError(errors, [
347-
{ path: ['updatePost'], message: `Simulated error: title: ${phase}Change` },
346+
expectExtensionError(errors, `${phase}Change`, [
347+
{
348+
path: ['updatePost'],
349+
messages: [
350+
`Post.title: Simulated error: title: ${phase}Change`,
351+
`Post.content: Simulated error: content: ${phase}Change`,
352+
],
353+
},
348354
]);
349355
expect(data).toEqual({ updatePost: null });
350356

@@ -371,8 +377,14 @@ const runner = setupTestRunner({
371377
query: `mutation ($id: ID!) { deletePost(where: { id: $id }) { id } }`,
372378
variables: { id: post.id },
373379
});
374-
expectExtensionError(errors, [
375-
{ path: ['deletePost'], message: `Simulated error: title: ${phase}Delete` },
380+
expectExtensionError(errors, `${phase}Delete`, [
381+
{
382+
path: ['deletePost'],
383+
messages: [
384+
`Post.title: Simulated error: title: ${phase}Delete`,
385+
`Post.content: Simulated error: content: ${phase}Delete`,
386+
],
387+
},
376388
]);
377389
expect(data).toEqual({ deletePost: null });
378390

tests/api-tests/utils.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,17 @@ export const expectValidationError = (
8585

8686
export const expectExtensionError = (
8787
errors: readonly any[] | undefined,
88-
args: { path: (string | number)[]; message: string }[]
88+
extensionName: string,
89+
args: { path: (string | number)[]; messages: string[] }[]
8990
) => {
90-
const unpackedErrors = (errors || []).map(({ locations, ...unpacked }) => ({
91-
...unpacked,
92-
}));
93-
expect(unpackedErrors).toEqual(args.map(({ path, message }) => ({ path, message })));
91+
const unpackedErrors = unpackErrors(errors);
92+
expect(unpackedErrors).toEqual(
93+
args.map(({ path, messages }) => ({
94+
extensions: { code: undefined },
95+
path,
96+
message: `An error occured while running "${extensionName}".\n${j(messages)}`,
97+
}))
98+
);
9499
};
95100

96101
export const expectPrismaError = (

0 commit comments

Comments
 (0)