diff --git a/.changeset/little-ads-walk.md b/.changeset/little-ads-walk.md new file mode 100644 index 00000000000..da2bf549b03 --- /dev/null +++ b/.changeset/little-ads-walk.md @@ -0,0 +1,5 @@ +--- +'@keystone-next/keystone': patch +--- + +Made the original stacktraces for before/after hooks available on `error.extension.errors`. diff --git a/packages/keystone/src/lib/core/graphql-errors.ts b/packages/keystone/src/lib/core/graphql-errors.ts index cb760fd554d..33b58384f66 100644 --- a/packages/keystone/src/lib/core/graphql-errors.ts +++ b/packages/keystone/src/lib/core/graphql-errors.ts @@ -7,9 +7,18 @@ export const validationFailureError = (messages: string[]) => { return new ApolloError(`You provided invalid data for this operation.\n${s}`); }; -export const extensionError = (extension: string, messages: string[]) => { - const s = messages.map(m => ` - ${m}`).join('\n'); - return new ApolloError(`An error occured while running "${extension}".\n${s}`); +export const extensionError = (extension: string, things: { error: Error; tag: string }[]) => { + const s = things.map(t => ` - ${t.tag}: ${t.error.message}`).join('\n'); + return new ApolloError( + `An error occured while running "${extension}".\n${s}`, + 'INTERNAL_SERVER_ERROR', + // Make the original stack traces available in non-production modes. + // TODO: We need to have a way to make these stack traces available + // for logging in production mode. + process.env.NODE_ENV !== 'production' + ? { errors: things.map(t => ({ stacktrace: t.error.stack, message: t.error.message })) } + : undefined + ); }; // FIXME: In an upcoming PR we will use these args to construct a better diff --git a/packages/keystone/src/lib/core/mutations/hooks.ts b/packages/keystone/src/lib/core/mutations/hooks.ts index 38cff0a8d7c..eeac512823f 100644 --- a/packages/keystone/src/lib/core/mutations/hooks.ts +++ b/packages/keystone/src/lib/core/mutations/hooks.ts @@ -31,14 +31,13 @@ export async function runSideEffectOnlyHook< } // Field hooks - const fieldsErrors = []; + const fieldsErrors: { error: Error; tag: string }[] = []; for (const [fieldPath, field] of Object.entries(list.fields)) { if (shouldRunFieldLevelHook(fieldPath)) { try { - // @ts-ignore await field.hooks[hookName]?.({ fieldPath, ...args }); - } catch (err) { - fieldsErrors.push(`${list.listKey}.${fieldPath}: ${err.message}`); + } catch (error) { + fieldsErrors.push({ error, tag: `${list.listKey}.${fieldPath}` }); } } } @@ -49,7 +48,7 @@ export async function runSideEffectOnlyHook< // List hooks try { await list.hooks[hookName]?.(args); - } catch (err) { - throw extensionError(hookName, [`${list.listKey}: ${err.message}`]); + } catch (error) { + throw extensionError(hookName, [{ error, tag: list.listKey }]); } } diff --git a/tests/api-tests/hooks/hook-errors.test.ts b/tests/api-tests/hooks/hook-errors.test.ts index fd9938f5f64..7e6bdb74711 100644 --- a/tests/api-tests/hooks/hook-errors.test.ts +++ b/tests/api-tests/hooks/hook-errors.test.ts @@ -87,322 +87,470 @@ const runner = setupTestRunner({ }), }); -['before', 'after'].map(phase => { - describe(`List Hooks: ${phase}Change/${phase}Delete()`, () => { - test( - 'createOne', - runner(async ({ context }) => { - // Valid name should pass - await context.lists.User.createOne({ data: { name: 'good' } }); +(['dev', 'production'] as const).map(mode => + describe(`NODE_ENV=${mode}`, () => { + beforeAll(() => { + // @ts-ignore + process.env.NODE_ENV = mode; + }); + afterAll(() => { + // @ts-ignore + process.env.NODE_ENV = 'test'; + }); - // Trigger an error - const { data, errors } = await context.graphql.raw({ - query: `mutation ($data: UserCreateInput!) { createUser(data: $data) { id } }`, - variables: { data: { name: `trigger ${phase}` } }, - }); + ['before', 'after'].map(phase => { + describe(`List Hooks: ${phase}Change/${phase}Delete()`, () => { + test( + 'createOne', + runner(async ({ context }) => { + // Valid name should pass + await context.lists.User.createOne({ data: { name: 'good' } }); - // Returns null and throws an error - expect(data).toEqual({ createUser: null }); - expectExtensionError(errors, `${phase}Change`, [ - { path: ['createUser'], messages: [`User: Simulated error: ${phase}Change`] }, - ]); + // Trigger an error + const { data, errors } = await context.graphql.raw({ + query: `mutation ($data: UserCreateInput!) { createUser(data: $data) { id } }`, + variables: { data: { name: `trigger ${phase}` } }, + }); - // Only the original user should exist for 'before', both exist for 'after' - const _users = await context.lists.User.findMany({ query: 'id name' }); - expect(_users.map(({ name }) => name)).toEqual( - phase === 'before' ? ['good'] : ['good', 'trigger after'] + // Returns null and throws an error + expect(data).toEqual({ createUser: null }); + const message = `Simulated error: ${phase}Change`; + expectExtensionError(mode, errors, `${phase}Change`, [ + { + path: ['createUser'], + messages: [`User: Simulated error: ${phase}Change`], + errors: [ + { + message, + stacktrace: expect.stringMatching( + new RegExp(`Error: ${message}\n[^\n]*${phase}Change .${__filename}`) + ), + }, + ], + }, + ]); + + // Only the original user should exist for 'before', both exist for 'after' + const _users = await context.lists.User.findMany({ query: 'id name' }); + expect(_users.map(({ name }) => name)).toEqual( + phase === 'before' ? ['good'] : ['good', 'trigger after'] + ); + }) ); - }) - ); - test( - 'updateOne', - runner(async ({ context }) => { - // Valid name should pass - const user = await context.lists.User.createOne({ data: { name: 'good' } }); - await context.lists.User.updateOne({ where: { id: user.id }, data: { name: 'better' } }); + test( + 'updateOne', + runner(async ({ context }) => { + // Valid name should pass + const user = await context.lists.User.createOne({ data: { name: 'good' } }); + await context.lists.User.updateOne({ + where: { id: user.id }, + data: { name: 'better' }, + }); - // Invalid name - const { data, errors } = await context.graphql.raw({ - query: `mutation ($id: ID! $data: UserUpdateInput!) { updateUser(where: { id: $id }, data: $data) { id } }`, - variables: { id: user.id, data: { name: `trigger ${phase}` } }, - }); + // Invalid name + const { data, errors } = await context.graphql.raw({ + query: `mutation ($id: ID! $data: UserUpdateInput!) { updateUser(where: { id: $id }, data: $data) { id } }`, + variables: { id: user.id, data: { name: `trigger ${phase}` } }, + }); - // Returns null and throws an error - expect(data).toEqual({ updateUser: null }); - expectExtensionError(errors, `${phase}Change`, [ - { path: ['updateUser'], messages: [`User: Simulated error: ${phase}Change`] }, - ]); + // Returns null and throws an error + expect(data).toEqual({ updateUser: null }); + const message = `Simulated error: ${phase}Change`; + expectExtensionError(mode, errors, `${phase}Change`, [ + { + path: ['updateUser'], + messages: [`User: ${message}`], + errors: [ + { + message, + stacktrace: expect.stringMatching( + new RegExp(`Error: ${message}\n[^\n]*${phase}Change .${__filename}`) + ), + }, + ], + }, + ]); - // User should have its original name for 'before', and the new name for 'after'. - const _users = await context.lists.User.findMany({ query: 'id name' }); - expect(_users.map(({ name }) => name)).toEqual( - phase === 'before' ? ['better'] : ['trigger after'] + // User should have its original name for 'before', and the new name for 'after'. + const _users = await context.lists.User.findMany({ query: 'id name' }); + expect(_users.map(({ name }) => name)).toEqual( + phase === 'before' ? ['better'] : ['trigger after'] + ); + }) ); - }) - ); - test( - 'deleteOne', - runner(async ({ context }) => { - // Valid names should pass - const user1 = await context.lists.User.createOne({ data: { name: 'good' } }); - const user2 = await context.lists.User.createOne({ - data: { name: `trigger ${phase} delete` }, - }); - await context.lists.User.deleteOne({ where: { id: user1.id } }); + test( + 'deleteOne', + runner(async ({ context }) => { + // Valid names should pass + const user1 = await context.lists.User.createOne({ data: { name: 'good' } }); + const user2 = await context.lists.User.createOne({ + data: { name: `trigger ${phase} delete` }, + }); + await context.lists.User.deleteOne({ where: { id: user1.id } }); - // Invalid name - const { data, errors } = await context.graphql.raw({ - query: `mutation ($id: ID!) { deleteUser(where: { id: $id }) { id } }`, - variables: { id: user2.id }, - }); + // Invalid name + const { data, errors } = await context.graphql.raw({ + query: `mutation ($id: ID!) { deleteUser(where: { id: $id }) { id } }`, + variables: { id: user2.id }, + }); - // Returns null and throws an error - expect(data).toEqual({ deleteUser: null }); - expectExtensionError(errors, `${phase}Delete`, [ - { path: ['deleteUser'], messages: [`User: Simulated error: ${phase}Delete`] }, - ]); + // Returns null and throws an error + expect(data).toEqual({ deleteUser: null }); + const message = `Simulated error: ${phase}Delete`; + expectExtensionError(mode, errors, `${phase}Delete`, [ + { + path: ['deleteUser'], + messages: [`User: ${message}`], + errors: [ + { + message, + stacktrace: expect.stringMatching( + new RegExp(`Error: ${message}\n[^\n]*${phase}Delete .${__filename}`) + ), + }, + ], + }, + ]); - // Bad users should still be in the database for 'before', deleted for 'after'. - const _users = await context.lists.User.findMany({ query: 'id name' }); - expect(_users.map(({ name }) => name)).toEqual( - phase === 'before' ? ['trigger before delete'] : [] + // Bad users should still be in the database for 'before', deleted for 'after'. + const _users = await context.lists.User.findMany({ query: 'id name' }); + expect(_users.map(({ name }) => name)).toEqual( + phase === 'before' ? ['trigger before delete'] : [] + ); + }) ); - }) - ); - test( - 'createMany', - runner(async ({ context }) => { - // Mix of good and bad names - const { data, errors } = await context.graphql.raw({ - query: `mutation ($data: [UserCreateInput!]!) { createUsers(data: $data) { id name } }`, - variables: { - data: [ - { name: 'good 1' }, - { name: `trigger ${phase}` }, - { name: 'good 2' }, - { name: `trigger ${phase}` }, - { name: 'good 3' }, - ], - }, - }); + test( + 'createMany', + runner(async ({ context }) => { + // Mix of good and bad names + const { data, errors } = await context.graphql.raw({ + query: `mutation ($data: [UserCreateInput!]!) { createUsers(data: $data) { id name } }`, + variables: { + data: [ + { name: 'good 1' }, + { name: `trigger ${phase}` }, + { name: 'good 2' }, + { name: `trigger ${phase}` }, + { name: 'good 3' }, + ], + }, + }); - // Valid users are returned, invalid come back as null - expect(data).toEqual({ - createUsers: [ - { id: expect.any(String), name: 'good 1' }, - null, - { id: expect.any(String), name: 'good 2' }, - null, - { id: expect.any(String), name: 'good 3' }, - ], - }); - // The invalid creates should have errors which point to the nulls in their path - expectExtensionError(errors, `${phase}Change`, [ - { path: ['createUsers', 1], messages: [`User: Simulated error: ${phase}Change`] }, - { path: ['createUsers', 3], messages: [`User: Simulated error: ${phase}Change`] }, - ]); + // Valid users are returned, invalid come back as null + expect(data).toEqual({ + createUsers: [ + { id: expect.any(String), name: 'good 1' }, + null, + { id: expect.any(String), name: 'good 2' }, + null, + { id: expect.any(String), name: 'good 3' }, + ], + }); + // The invalid creates should have errors which point to the nulls in their path + const message = `Simulated error: ${phase}Change`; + expectExtensionError(mode, errors, `${phase}Change`, [ + { + path: ['createUsers', 1], + messages: [`User: ${message}`], + errors: [ + { + message, + stacktrace: expect.stringMatching( + new RegExp(`Error: ${message}\n[^\n]*${phase}Change .${__filename}`) + ), + }, + ], + }, + { + path: ['createUsers', 3], + messages: [`User: ${message}`], + errors: [ + { + message, + stacktrace: expect.stringMatching( + new RegExp(`Error: ${message}\n[^\n]*${phase}Change .${__filename}`) + ), + }, + ], + }, + ]); - // Three users should exist in the database for 'before,' five for 'after'. - const users = await context.lists.User.findMany({ - orderBy: { name: 'asc' }, - query: 'id name', - }); - expect(users.map(({ name }) => name)).toEqual( - phase === 'before' - ? ['good 1', 'good 2', 'good 3'] - : ['good 1', 'good 2', 'good 3', 'trigger after', 'trigger after'] + // Three users should exist in the database for 'before,' five for 'after'. + const users = await context.lists.User.findMany({ + orderBy: { name: 'asc' }, + query: 'id name', + }); + expect(users.map(({ name }) => name)).toEqual( + phase === 'before' + ? ['good 1', 'good 2', 'good 3'] + : ['good 1', 'good 2', 'good 3', 'trigger after', 'trigger after'] + ); + }) ); - }) - ); - test( - 'updateMany', - runner(async ({ context }) => { - // Start with some users - const users = await context.lists.User.createMany({ - data: [ - { name: 'good 1' }, - { name: 'good 2' }, - { name: 'good 3' }, - { name: 'good 4' }, - { name: 'good 5' }, - ], - query: 'id name', - }); + test( + 'updateMany', + runner(async ({ context }) => { + // Start with some users + const users = await context.lists.User.createMany({ + data: [ + { name: 'good 1' }, + { name: 'good 2' }, + { name: 'good 3' }, + { name: 'good 4' }, + { name: 'good 5' }, + ], + query: 'id name', + }); - // Mix of good and bad names - const { data, errors } = await context.graphql.raw({ - query: `mutation ($data: [UserUpdateArgs!]!) { updateUsers(data: $data) { id name } }`, - variables: { - data: [ - { where: { id: users[0].id }, data: { name: 'still good 1' } }, - { where: { id: users[1].id }, data: { name: `trigger ${phase}` } }, - { where: { id: users[2].id }, data: { name: 'still good 3' } }, - { where: { id: users[3].id }, data: { name: `trigger ${phase}` } }, - ], - }, - }); + // Mix of good and bad names + const { data, errors } = await context.graphql.raw({ + query: `mutation ($data: [UserUpdateArgs!]!) { updateUsers(data: $data) { id name } }`, + variables: { + data: [ + { where: { id: users[0].id }, data: { name: 'still good 1' } }, + { where: { id: users[1].id }, data: { name: `trigger ${phase}` } }, + { where: { id: users[2].id }, data: { name: 'still good 3' } }, + { where: { id: users[3].id }, data: { name: `trigger ${phase}` } }, + ], + }, + }); - // Valid users are returned, invalid come back as null - expect(data).toEqual({ - updateUsers: [ - { id: users[0].id, name: 'still good 1' }, - null, - { id: users[2].id, name: 'still good 3' }, - null, - ], - }); - // The invalid updates should have errors which point to the nulls in their path - expectExtensionError(errors, `${phase}Change`, [ - { path: ['updateUsers', 1], messages: [`User: Simulated error: ${phase}Change`] }, - { path: ['updateUsers', 3], messages: [`User: Simulated error: ${phase}Change`] }, - ]); + // Valid users are returned, invalid come back as null + expect(data).toEqual({ + updateUsers: [ + { id: users[0].id, name: 'still good 1' }, + null, + { id: users[2].id, name: 'still good 3' }, + null, + ], + }); + // The invalid updates should have errors which point to the nulls in their path + const message = `Simulated error: ${phase}Change`; + expectExtensionError(mode, errors, `${phase}Change`, [ + { + path: ['updateUsers', 1], + messages: [`User: ${message}`], + errors: [ + { + message, + stacktrace: expect.stringMatching( + new RegExp(`Error: ${message}\n[^\n]*${phase}Change .${__filename}`) + ), + }, + ], + }, + { + path: ['updateUsers', 3], + messages: [`User: ${message}`], + errors: [ + { + message, + stacktrace: expect.stringMatching( + new RegExp(`Error: ${message}\n[^\n]*${phase}Change .${__filename}`) + ), + }, + ], + }, + ]); - // All users should still exist in the database, un-changed for `before`, changed for `after`. - const _users = await context.lists.User.findMany({ - orderBy: { name: 'asc' }, - query: 'id name', - }); - expect(_users.map(({ name }) => name)).toEqual( - phase === 'before' - ? ['good 2', 'good 4', 'good 5', 'still good 1', 'still good 3'] - : ['good 5', 'still good 1', 'still good 3', 'trigger after', 'trigger after'] + // All users should still exist in the database, un-changed for `before`, changed for `after`. + const _users = await context.lists.User.findMany({ + orderBy: { name: 'asc' }, + query: 'id name', + }); + expect(_users.map(({ name }) => name)).toEqual( + phase === 'before' + ? ['good 2', 'good 4', 'good 5', 'still good 1', 'still good 3'] + : ['good 5', 'still good 1', 'still good 3', 'trigger after', 'trigger after'] + ); + }) ); - }) - ); - test( - 'deleteMany', - runner(async ({ context }) => { - // Start with some users - const users = await context.lists.User.createMany({ - data: [ - { name: 'good 1' }, - { name: `trigger ${phase} delete` }, - { name: 'good 3' }, - { name: `trigger ${phase} delete` }, - { name: 'good 5' }, - ], - query: 'id name', - }); + test( + 'deleteMany', + runner(async ({ context }) => { + // Start with some users + const users = await context.lists.User.createMany({ + data: [ + { name: 'good 1' }, + { name: `trigger ${phase} delete` }, + { name: 'good 3' }, + { name: `trigger ${phase} delete` }, + { name: 'good 5' }, + ], + query: 'id name', + }); - // Mix of good and bad names - const { data, errors } = await context.graphql.raw({ - query: `mutation ($where: [UserWhereUniqueInput!]!) { deleteUsers(where: $where) { id name } }`, - variables: { - where: [users[0].id, users[1].id, users[2].id, users[3].id].map(id => ({ id })), - }, - }); + // Mix of good and bad names + const { data, errors } = await context.graphql.raw({ + query: `mutation ($where: [UserWhereUniqueInput!]!) { deleteUsers(where: $where) { id name } }`, + variables: { + where: [users[0].id, users[1].id, users[2].id, users[3].id].map(id => ({ id })), + }, + }); - // Valid users are returned, invalid come back as null - expect(data).toEqual({ - deleteUsers: [ - { id: users[0].id, name: 'good 1' }, - null, - { id: users[2].id, name: 'good 3' }, - null, - ], - }); - // The invalid deletes should have errors which point to the nulls in their path - expectExtensionError(errors, `${phase}Delete`, [ - { path: ['deleteUsers', 1], messages: [`User: Simulated error: ${phase}Delete`] }, - { path: ['deleteUsers', 3], messages: [`User: Simulated error: ${phase}Delete`] }, - ]); + // Valid users are returned, invalid come back as null + expect(data).toEqual({ + deleteUsers: [ + { id: users[0].id, name: 'good 1' }, + null, + { id: users[2].id, name: 'good 3' }, + null, + ], + }); + // The invalid deletes should have errors which point to the nulls in their path + const message = `Simulated error: ${phase}Delete`; + expectExtensionError(mode, errors, `${phase}Delete`, [ + { + path: ['deleteUsers', 1], + messages: [`User: ${message}`], + errors: [ + { + message, + stacktrace: expect.stringMatching( + new RegExp(`Error: ${message}\n[^\n]*${phase}Delete .${__filename}`) + ), + }, + ], + }, + { + path: ['deleteUsers', 3], + messages: [`User: ${message}`], + errors: [ + { + message, + stacktrace: expect.stringMatching( + new RegExp(`Error: ${message}\n[^\n]*${phase}Delete .${__filename}`) + ), + }, + ], + }, + ]); - // Three users should still exist in the database for `before`, only 1 for `after`. - const _users = await context.lists.User.findMany({ - orderBy: { name: 'asc' }, - query: 'id name', - }); - expect(_users.map(({ name }) => name)).toEqual( - phase === 'before' - ? ['good 5', 'trigger before delete', 'trigger before delete'] - : ['good 5'] + // Three users should still exist in the database for `before`, only 1 for `after`. + const _users = await context.lists.User.findMany({ + orderBy: { name: 'asc' }, + query: 'id name', + }); + expect(_users.map(({ name }) => name)).toEqual( + phase === 'before' + ? ['good 5', 'trigger before delete', 'trigger before delete'] + : ['good 5'] + ); + }) ); - }) - ); - }); -}); + }); + }); -['before', 'after'].map(phase => { - describe(`Field Hooks: ${phase}Change/${phase}Delete()`, () => { - test( - 'update', - runner(async ({ context }) => { - const post = await context.lists.Post.createOne({ - data: { title: 'original title', content: 'original content' }, - }); + ['before', 'after'].map(phase => { + describe(`Field Hooks: ${phase}Change/${phase}Delete()`, () => { + test( + 'update', + runner(async ({ context }) => { + const post = await context.lists.Post.createOne({ + data: { title: 'original title', content: 'original content' }, + }); - const { data, errors } = await context.graphql.raw({ - query: `mutation ($id: ID! $data: PostUpdateInput!) { updatePost(where: { id: $id }, data: $data) { id } }`, - variables: { - id: post.id, - data: { title: `trigger ${phase}`, content: `trigger ${phase}` }, - }, - }); - expectExtensionError(errors, `${phase}Change`, [ - { - path: ['updatePost'], - messages: [ - `Post.title: Simulated error: title: ${phase}Change`, - `Post.content: Simulated error: content: ${phase}Change`, - ], - }, - ]); - expect(data).toEqual({ updatePost: null }); + const { data, errors } = await context.graphql.raw({ + query: `mutation ($id: ID! $data: PostUpdateInput!) { updatePost(where: { id: $id }, data: $data) { id } }`, + variables: { + id: post.id, + data: { title: `trigger ${phase}`, content: `trigger ${phase}` }, + }, + }); + const message1 = `Simulated error: title: ${phase}Change`; + const message2 = `Simulated error: content: ${phase}Change`; + expectExtensionError(mode, errors, `${phase}Change`, [ + { + path: ['updatePost'], + messages: [`Post.title: ${message1}`, `Post.content: ${message2}`], + errors: [ + { + message: message1, + stacktrace: expect.stringMatching( + new RegExp(`Error: ${message1}\n[^\n]*${phase}Change .${__filename}`) + ), + }, + { + message: message2, + stacktrace: expect.stringMatching( + new RegExp(`Error: ${message2}\n[^\n]*${phase}Change .${__filename}`) + ), + }, + ], + }, + ]); + expect(data).toEqual({ updatePost: null }); - // Post should have its original data for 'before', and the new data for 'after'. - const _post = await context.lists.Post.findOne({ - where: { id: post.id }, - query: 'title content', - }); - expect(_post).toEqual( - phase === 'before' - ? { title: 'original title', content: 'original content' } - : { title: 'trigger after', content: 'trigger after' } + // Post should have its original data for 'before', and the new data for 'after'. + const _post = await context.lists.Post.findOne({ + where: { id: post.id }, + query: 'title content', + }); + expect(_post).toEqual( + phase === 'before' + ? { title: 'original title', content: 'original content' } + : { title: 'trigger after', content: 'trigger after' } + ); + }) ); - }) - ); - test( - 'delete', - runner(async ({ context }) => { - const post = await context.lists.Post.createOne({ - data: { title: `trigger ${phase} delete`, content: `trigger ${phase} delete` }, - }); - const { data, errors } = await context.graphql.raw({ - query: `mutation ($id: ID!) { deletePost(where: { id: $id }) { id } }`, - variables: { id: post.id }, - }); - expectExtensionError(errors, `${phase}Delete`, [ - { - path: ['deletePost'], - messages: [ - `Post.title: Simulated error: title: ${phase}Delete`, - `Post.content: Simulated error: content: ${phase}Delete`, - ], - }, - ]); - expect(data).toEqual({ deletePost: null }); + test( + `delete`, + runner(async ({ context, graphQLRequest }) => { + const post = await context.lists.Post.createOne({ + data: { title: `trigger ${phase} delete`, content: `trigger ${phase} delete` }, + }); + const { body } = await graphQLRequest({ + query: `mutation ($id: ID!) { deletePost(where: { id: $id }) { id } }`, + variables: { id: post.id }, + }); + const { data, errors } = body; + const message1 = `Simulated error: title: ${phase}Delete`; + const message2 = `Simulated error: content: ${phase}Delete`; + expectExtensionError(mode, errors, `${phase}Delete`, [ + { + path: ['deletePost'], + messages: [`Post.title: ${message1}`, `Post.content: ${message2}`], + errors: [ + { + message: message1, + stacktrace: expect.stringMatching( + new RegExp(`Error: ${message1}\n[^\n]*${phase}Delete .${__filename}`) + ), + }, + { + message: message2, + stacktrace: expect.stringMatching( + new RegExp(`Error: ${message2}\n[^\n]*${phase}Delete .${__filename}`) + ), + }, + ], + }, + ]); + expect(data).toEqual({ deletePost: null }); - // Post should have its original data for 'before', and not exist for 'after'. - const result = await context.graphql.raw({ - query: `query ($id: ID!) { post(where: { id: $id }) { title content} }`, - variables: { id: post.id }, - }); - if (phase === 'before') { - expect(result.errors).toBe(undefined); - expect(result.data).toEqual({ - post: { title: 'trigger before delete', content: 'trigger before delete' }, - }); - } else { - expectAccessDenied(result.errors, [{ path: ['post'] }]); - expect(result.data).toEqual({ post: null }); - } - }) - ); - }); -}); + // Post should have its original data for 'before', and not exist for 'after'. + const result = await context.graphql.raw({ + query: `query ($id: ID!) { post(where: { id: $id }) { title content} }`, + variables: { id: post.id }, + }); + if (phase === 'before') { + expect(result.errors).toBe(undefined); + expect(result.data).toEqual({ + post: { title: 'trigger before delete', content: 'trigger before delete' }, + }); + } else { + expectAccessDenied(result.errors, [{ path: ['post'] }]); + expect(result.data).toEqual({ post: null }); + } + }) + ); + }); + }); + }) +); diff --git a/tests/api-tests/utils.ts b/tests/api-tests/utils.ts index 9b824b50bf2..22692db0a96 100644 --- a/tests/api-tests/utils.ts +++ b/tests/api-tests/utils.ts @@ -84,14 +84,15 @@ export const expectValidationError = ( }; export const expectExtensionError = ( + mode: 'dev' | 'production', errors: readonly any[] | undefined, extensionName: string, - args: { path: (string | number)[]; messages: string[] }[] + args: { path: (string | number)[]; messages: string[]; errors: any[] }[] ) => { const unpackedErrors = unpackErrors(errors); expect(unpackedErrors).toEqual( - args.map(({ path, messages }) => ({ - extensions: { code: undefined }, + args.map(({ path, messages, errors }) => ({ + extensions: { code: 'INTERNAL_SERVER_ERROR', ...(mode !== 'production' ? { errors } : {}) }, path, message: `An error occured while running "${extensionName}".\n${j(messages)}`, }))