Skip to content

Commit fb2cb65

Browse files
committed
refactor: improve createOne resolver which throws an error on top level if client does not requested errors in payload
1 parent a6124df commit fb2cb65

File tree

4 files changed

+180
-4
lines changed

4 files changed

+180
-4
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/* eslint-disable no-await-in-loop */
2+
3+
import mongoose from 'mongoose';
4+
import { MongoMemoryServer } from 'mongodb-memory-server';
5+
import { schemaComposer, graphql } from 'graphql-compose';
6+
import { composeWithMongoose } from '../../index';
7+
8+
let mongoServer: MongoMemoryServer;
9+
beforeAll(async () => {
10+
mongoServer = new MongoMemoryServer();
11+
const mongoUri = await mongoServer.getConnectionString();
12+
await mongoose.connect(mongoUri, { useNewUrlParser: true, useUnifiedTopology: true });
13+
// mongoose.set('debug', true);
14+
});
15+
16+
afterAll(() => {
17+
mongoose.disconnect();
18+
mongoServer.stop();
19+
});
20+
21+
// May require additional time for downloading MongoDB binaries
22+
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
23+
24+
describe("issue #248 - payloads' errors", () => {
25+
const UserSchema = new mongoose.Schema({
26+
name: String,
27+
someStrangeField: {
28+
type: String,
29+
validate: [
30+
() => {
31+
return false;
32+
},
33+
'this is a validate message',
34+
],
35+
},
36+
});
37+
38+
const User = mongoose.model('User', UserSchema);
39+
const UserTC = composeWithMongoose(User);
40+
schemaComposer.Query.addFields({
41+
findMany: UserTC.getResolver('findMany'),
42+
});
43+
schemaComposer.Mutation.addFields({
44+
createOne: UserTC.getResolver('createOne'),
45+
});
46+
const schema = schemaComposer.buildSchema();
47+
48+
it('check errors in payload if errors field requested', async () => {
49+
const res = await graphql.graphql({
50+
schema,
51+
source: `
52+
mutation {
53+
createOne(record: { name: "John", someStrangeField: "Test" }) {
54+
record {
55+
name
56+
}
57+
errors {
58+
__typename
59+
message
60+
... on ValidationError {
61+
path
62+
value
63+
}
64+
}
65+
}
66+
}
67+
`,
68+
});
69+
70+
expect(res).toEqual({
71+
data: {
72+
createOne: {
73+
record: null,
74+
errors: [
75+
{
76+
__typename: 'ValidationError',
77+
message: 'this is a validate message',
78+
path: 'someStrangeField',
79+
value: 'Test',
80+
},
81+
],
82+
},
83+
},
84+
});
85+
});
86+
87+
it('check errors on top-level if errors field is not requested in payload', async () => {
88+
const res = await graphql.graphql({
89+
schema,
90+
source: `
91+
mutation {
92+
createOne(record: { name: "John", someStrangeField: "Test" }) {
93+
record {
94+
name
95+
}
96+
}
97+
}
98+
`,
99+
});
100+
101+
(res as any).errors[0] = convertToSimpleObject((res as any).errors[0]);
102+
103+
expect(res).toEqual({
104+
data: {
105+
createOne: null,
106+
},
107+
errors: [
108+
expect.objectContaining({
109+
message: 'User validation failed: someStrangeField: this is a validate message',
110+
extensions: {
111+
validationErrors: {
112+
someStrangeField: {
113+
message: 'this is a validate message',
114+
path: 'someStrangeField',
115+
value: 'Test',
116+
},
117+
},
118+
},
119+
path: ['createOne'],
120+
}),
121+
],
122+
});
123+
});
124+
});
125+
126+
function convertToSimpleObject(theClass: Error): Record<string, any> {
127+
const keys = Object.getOwnPropertyNames(theClass);
128+
return keys.reduce((classAsObj, key) => {
129+
classAsObj[key] = (theClass as any)[key];
130+
return classAsObj;
131+
}, {} as Record<string, any>);
132+
}

src/resolvers/__tests__/createOne-test.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,28 @@ describe('createOne() ->', () => {
9898
args: {
9999
record: { valid: 'AlwaysFails', contacts: { email: 'mail' } },
100100
},
101+
projection: {
102+
errors: true,
103+
},
101104
});
102105
expect(result.errors).toEqual([
103-
{ message: 'Path `n` is required.', path: 'n' },
104-
{ message: 'this is a validate message', path: 'valid' },
106+
{ message: 'Path `n` is required.', path: 'n', value: undefined },
107+
{ message: 'this is a validate message', path: 'valid', value: 'AlwaysFails' },
105108
]);
106109
});
107110

111+
it('should throw GraphQLError if client does not request errors field in payload', async () => {
112+
await expect(
113+
createOne(UserModel, UserTC).resolve({
114+
args: {
115+
record: { valid: 'AlwaysFails', contacts: { email: 'mail' } },
116+
},
117+
})
118+
).rejects.toThrowError(
119+
'User validation failed: n: Path `n` is required., valid: this is a validate message'
120+
);
121+
});
122+
108123
it('should return empty payload.errors', async () => {
109124
const result = await createOne(UserModel, UserTC).resolve({
110125
args: {

src/resolvers/createOne.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import type { Resolver, ObjectTypeComposer } from 'graphql-compose';
1+
import { Resolver, ObjectTypeComposer, mapEachKey } from 'graphql-compose';
22
import type { Model, Document } from 'mongoose';
33
import { getOrCreateErrorInterface } from '../utils/getOrCreateErrorInterface';
44
import { recordHelperArgs } from './helpers';
55
import type { ExtendedResolveParams, GenResolverOpts } from './index';
6+
import { GraphQLError } from 'graphql';
67

78
export default function createOne<TSource = Document, TContext = any>(
89
model: Model<any>,
@@ -84,12 +85,36 @@ export default function createOne<TSource = Document, TContext = any>(
8485
const errors: {
8586
path: string;
8687
message: string;
88+
value: any;
8789
}[] = [];
90+
8891
if (validationErrors && validationErrors.errors) {
92+
if (!resolveParams?.projection?.errors) {
93+
// if client does not request `errors` field we throw Exception on to level
94+
throw new GraphQLError(
95+
validationErrors.message,
96+
undefined,
97+
undefined,
98+
undefined,
99+
undefined,
100+
undefined,
101+
{
102+
validationErrors: mapEachKey(validationErrors.errors, (e: any) => {
103+
return {
104+
path: e.path,
105+
message: e.message,
106+
value: e.value,
107+
};
108+
}),
109+
}
110+
);
111+
}
89112
Object.keys(validationErrors.errors).forEach((key) => {
113+
const { message, value } = validationErrors.errors[key];
90114
errors.push({
91115
path: key,
92-
message: validationErrors.errors[key].message,
116+
message,
117+
value,
93118
});
94119
});
95120
return {

src/utils/getOrCreateErrorInterface.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export function getOrCreateErrorInterface(tc: ObjectTypeComposer): InterfaceType
2525
description: 'Source of the validation error from the model path',
2626
type: 'String',
2727
},
28+
value: {
29+
description: 'Field value which occurs the validation error',
30+
type: 'JSON',
31+
},
2832
});
2933
otc.addInterface(errorInterface);
3034
}

0 commit comments

Comments
 (0)