Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update resolveInput error handling #6316

Merged
merged 1 commit into from
Aug 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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}`)
),
},
],
},
]);
})
);
});