Skip to content

Commit

Permalink
Update resolveInput error handling (#6316)
Browse files Browse the repository at this point in the history
  • Loading branch information
timleslie authored Aug 13, 2021
1 parent 399561b commit 78dac76
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/grumpy-jobs-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/keystone': patch
---

Updated handling of errors in `resolveInput` hooks to provide developers with appropriate debug information.
41 changes: 28 additions & 13 deletions packages/keystone/src/lib/core/mutations/create-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
runWithPrisma,
} from '../utils';
import { resolveUniqueWhereInput, UniqueInputFilter } from '../where-inputs';
import { extensionError } from '../graphql-errors';
import {
resolveRelateToManyForCreateInput,
resolveRelateToManyForUpdateInput,
Expand Down Expand Up @@ -228,23 +229,37 @@ async function getResolvedData(
);

// Resolve input hooks
resolvedData = Object.fromEntries(
await promiseAllRejectWithAllErrors(
Object.entries(list.fields).map(async ([fieldKey, field]) => {
if (field.hooks.resolveInput === undefined) {
return [fieldKey, resolvedData[fieldKey]];
}
const value = await field.hooks.resolveInput({
const hookName = 'resolveInput';
// Field hooks
let _resolvedData: Record<string, any> = {};
const fieldsErrors: { error: Error; tag: string }[] = [];
for (const [fieldPath, field] of Object.entries(list.fields)) {
if (field.hooks.resolveInput === undefined) {
_resolvedData[fieldPath] = resolvedData[fieldPath];
} else {
try {
_resolvedData[fieldPath] = await field.hooks.resolveInput({
...hookArgs,
resolvedData,
fieldPath: fieldKey,
fieldPath,
});
return [fieldKey, value];
})
)
);
} catch (error) {
fieldsErrors.push({ error, tag: `${list.listKey}.${fieldPath}` });
}
}
}
if (fieldsErrors.length) {
throw extensionError(hookName, fieldsErrors);
}
resolvedData = _resolvedData;

// List hooks
if (list.hooks.resolveInput) {
resolvedData = (await list.hooks.resolveInput({ ...hookArgs, resolvedData })) as any;
try {
resolvedData = (await list.hooks.resolveInput({ ...hookArgs, resolvedData })) as any;
} catch (error) {
throw extensionError(hookName, [{ error, tag: list.listKey }]);
}
}

return resolvedData;
Expand Down
73 changes: 66 additions & 7 deletions tests/api-tests/hooks/list-hooks.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { text } from '@keystone-next/fields';
import { createSchema, list } from '@keystone-next/keystone/schema';
import { setupTestRunner } from '@keystone-next/testing';
import { apiTestConfig } from '../utils';
import { apiTestConfig, expectExtensionError } from '../utils';

const runner = setupTestRunner({
config: apiTestConfig({
Expand All @@ -11,13 +11,20 @@ const runner = setupTestRunner({
name: text({
hooks: {
resolveInput: ({ resolvedData }) => {
if (resolvedData.name === 'trigger field error') {
throw new Error('Field error triggered');
}

return `${resolvedData.name}-field`;
},
},
}),
},
hooks: {
resolveInput: ({ resolvedData }) => {
if (resolvedData.name === 'trigger list error-field') {
throw new Error('List error triggered');
}
return {
name: `${resolvedData.name}-list`,
};
Expand All @@ -29,18 +36,70 @@ const runner = setupTestRunner({
});

describe('List Hooks: #resolveInput()', () => {
it(
test(
'resolves fields first, then passes them to the list',
runner(async ({ context }) => {
const user = await context.lists.User.createOne({
data: { name: 'jess' },
query: 'name',
});

const user = await context.lists.User.createOne({ data: { name: 'jess' }, query: 'name' });
// Field should be executed first, appending `-field`, then the list
// should be executed which appends `-list`, and finally that total
// result should be stored.
expect(user.name).toBe('jess-field-list');
})
);

test(
'List error',
runner(async ({ context }) => {
// Trigger an error
const { data, errors } = await context.graphql.raw({
query: `mutation ($data: UserCreateInput!) { createUser(data: $data) { id } }`,
variables: { data: { name: `trigger list error` } },
});
// Returns null and throws an error
expect(data).toEqual({ createUser: null });
const message = `List error triggered`;
expectExtensionError('dev', false, undefined, errors, `resolveInput`, [
{
path: ['createUser'],
messages: [`User: ${message}`],
debug: [
{
message,
stacktrace: expect.stringMatching(
new RegExp(`Error: ${message}\n[^\n]*resolveInput .${__filename}`)
),
},
],
},
]);
})
);

test(
'Field error',
runner(async ({ context }) => {
// Trigger an error
const { data, errors } = await context.graphql.raw({
query: `mutation ($data: UserCreateInput!) { createUser(data: $data) { id } }`,
variables: { data: { name: `trigger field error` } },
});
// Returns null and throws an error
expect(data).toEqual({ createUser: null });
const message = `Field error triggered`;
expectExtensionError('dev', false, undefined, errors, `resolveInput`, [
{
path: ['createUser'],
messages: [`User.name: ${message}`],
debug: [
{
message,
stacktrace: expect.stringMatching(
new RegExp(`Error: ${message}\n[^\n]*resolveInput .${__filename}`)
),
},
],
},
]);
})
);
});

0 comments on commit 78dac76

Please sign in to comment.