Skip to content

Commit 68caf42

Browse files
committed
Initial work on error payloads
1 parent a0a8c39 commit 68caf42

File tree

8 files changed

+174
-6
lines changed

8 files changed

+174
-6
lines changed

src/__mocks__/userModel.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,18 @@ const UserSchema = new Schema(
110110
},
111111
},
112112

113+
// for error payloads tests
114+
valid: {
115+
type: String,
116+
required: false,
117+
validate: [
118+
() => {
119+
return false;
120+
},
121+
'this is a validate message',
122+
],
123+
},
124+
113125
// createdAt, created via option `timastamp: true` (see bottom)
114126
// updatedAt, created via option `timastamp: true` (see bottom)
115127
},

src/resolvers/__tests__/createOne-test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,27 @@ describe('createOne() ->', () => {
8989
expect(result.record.id).toBe(result.recordId);
9090
});
9191

92+
it('should return payload.errors', async () => {
93+
const result = await createOne(UserModel, UserTC).resolve({
94+
args: {
95+
record: { valid: 'AlwaysFails' },
96+
},
97+
});
98+
expect(result.errors).toEqual([
99+
{ messages: ['Path `n` is required.'], path: 'n' },
100+
{ messages: ['this is a validate message'], path: 'valid' },
101+
]);
102+
});
103+
104+
it('should return empty payload.errors', async () => {
105+
const result = await createOne(UserModel, UserTC).resolve({
106+
args: {
107+
record: { n: 'foo' },
108+
},
109+
});
110+
expect(result.errors).toEqual(null);
111+
});
112+
92113
it('should return mongoose document', async () => {
93114
const result = await createOne(UserModel, UserTC).resolve({
94115
args: { record: { name: 'NewUser' } },

src/resolvers/__tests__/updateById-test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,24 @@ describe('updateById() ->', () => {
9898
expect(result.recordId).toBe(user1.id);
9999
});
100100

101+
it('should return empty payload.errors', async () => {
102+
const result = await updateById(UserModel, UserTC).resolve({
103+
args: {
104+
record: { _id: user1.id, name: 'some name' },
105+
},
106+
});
107+
expect(result.errors).toEqual(null);
108+
});
109+
110+
it('should return payload.errors', async () => {
111+
const result = await updateById(UserModel, UserTC).resolve({
112+
args: {
113+
record: { _id: user1.id, name: 'some name', valid: 'AlwaysFails' },
114+
},
115+
});
116+
expect(result.errors).toEqual([{ messages: ['this is a validate message'], path: 'valid' }]);
117+
});
118+
101119
it('should change data via args.record in model', async () => {
102120
const result = await updateById(UserModel, UserTC).resolve({
103121
args: {

src/resolvers/__tests__/updateOne-test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,24 @@ describe('updateOne() ->', () => {
125125
expect(result.record.id).toBe(user1.id);
126126
});
127127

128+
it('should return empty payload.errors', async () => {
129+
const result = await updateOne(UserModel, UserTC).resolve({
130+
args: { filter: { _id: user1.id } },
131+
});
132+
expect(result.errors).toEqual(null);
133+
});
134+
135+
it('should return payload.errors', async () => {
136+
const result = await updateOne(UserModel, UserTC).resolve({
137+
args: {
138+
filter: { _id: user1.id },
139+
record: { valid: 'AlwaysFails' },
140+
},
141+
});
142+
143+
expect(result.errors).toEqual([{ messages: ['this is a validate message'], path: 'valid' }]);
144+
});
145+
128146
it('should skip records', async () => {
129147
const result1 = await updateOne(UserModel, UserTC).resolve({
130148
args: {

src/resolvers/createOne.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { Resolver, ObjectTypeComposer } from 'graphql-compose';
22
import type { Model, Document } from 'mongoose';
3+
import { getOrCreateErrorPayload } from '../utils/getOrCreateErrorPayload';
34
import { recordHelperArgs } from './helpers';
45
import type { ExtendedResolveParams, GenResolverOpts } from './index';
6+
import { MongoInstance } from 'mongodb-memory-server';
57

68
export default function createOne<TSource = Document, TContext = any>(
79
model: Model<any>,
@@ -29,6 +31,8 @@ export default function createOne<TSource = Document, TContext = any>(
2931
}
3032
}
3133

34+
getOrCreateErrorPayload(tc);
35+
3236
const outputTypeName = `CreateOne${tc.getTypeName()}Payload`;
3337
const outputType = tc.schemaComposer.getOrCreateOTC(outputTypeName, (t) => {
3438
t.addFields({
@@ -40,6 +44,10 @@ export default function createOne<TSource = Document, TContext = any>(
4044
type: tc,
4145
description: 'Created document',
4246
},
47+
errors: {
48+
type: '[ErrorPayload]',
49+
description: 'Errors that may occur, typically validations',
50+
},
4351
});
4452
});
4553

@@ -71,12 +79,33 @@ export default function createOne<TSource = Document, TContext = any>(
7179
doc = await resolveParams.beforeRecordMutate(doc, resolveParams);
7280
if (!doc) return null;
7381
}
74-
await doc.save();
7582

76-
return {
77-
record: doc,
78-
recordId: tc.getRecordIdFn()(doc),
79-
};
83+
const validationErrors = doc.validateSync();
84+
let errors: {
85+
path: string;
86+
messages: string[];
87+
}[];
88+
if (validationErrors && validationErrors.errors) {
89+
errors = [];
90+
Object.keys(validationErrors.errors).forEach((key) => {
91+
errors.push({
92+
path: key,
93+
messages: [validationErrors.errors[key].properties.message],
94+
});
95+
});
96+
return {
97+
record: null,
98+
recordId: null,
99+
errors,
100+
};
101+
} else {
102+
await doc.save();
103+
return {
104+
record: doc,
105+
recordId: tc.getRecordIdFn()(doc),
106+
errors: null,
107+
};
108+
}
80109
}) as any,
81110
});
82111

src/resolvers/updateById.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Resolver, ObjectTypeComposer } from 'graphql-compose';
22
import type { Model, Document } from 'mongoose';
3+
import { getOrCreateErrorPayload } from '../utils/getOrCreateErrorPayload';
34
import { recordHelperArgs } from './helpers/record';
45
import findById from './findById';
56

@@ -22,6 +23,8 @@ export default function updateById<TSource = Document, TContext = any>(
2223

2324
const findByIdResolver = findById(model, tc);
2425

26+
getOrCreateErrorPayload(tc);
27+
2528
const outputTypeName = `UpdateById${tc.getTypeName()}Payload`;
2629
const outputType = tc.schemaComposer.getOrCreateOTC(outputTypeName, (t) => {
2730
t.addFields({
@@ -33,6 +36,10 @@ export default function updateById<TSource = Document, TContext = any>(
3336
type: tc,
3437
description: 'Updated document',
3538
},
39+
errors: {
40+
type: '[ErrorPayload]',
41+
description: 'Errors that may occur, typically validations',
42+
},
3643
});
3744
});
3845

@@ -90,12 +97,34 @@ export default function updateById<TSource = Document, TContext = any>(
9097

9198
if (recordData) {
9299
doc.set(recordData);
100+
101+
const validationErrors = doc.validateSync();
102+
let errors: {
103+
path: string,
104+
messages: string[]
105+
}[];
106+
if (validationErrors && validationErrors.errors) {
107+
errors = [];
108+
Object.keys(validationErrors.errors).forEach((key) => {
109+
errors.push({
110+
path: key,
111+
messages: [validationErrors.errors[key].properties.message],
112+
});
113+
});
114+
return {
115+
record: null,
116+
recordId: null,
117+
errors,
118+
};
119+
}
120+
93121
await doc.save();
94122
}
95123

96124
return {
97125
record: doc,
98126
recordId: tc.getRecordIdFn()(doc),
127+
errors: null,
99128
};
100129
}) as any,
101130
});

src/resolvers/updateOne.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Resolver, ObjectTypeComposer } from 'graphql-compose';
22
import type { Model, Document } from 'mongoose';
33
import type { ExtendedResolveParams, GenResolverOpts } from './index';
4+
import { getOrCreateErrorPayload } from '../utils/getOrCreateErrorPayload';
45
import { skipHelperArgs, recordHelperArgs, filterHelperArgs, sortHelperArgs } from './helpers';
56
import findOne from './findOne';
67

@@ -20,6 +21,8 @@ export default function updateOne<TSource = Document, TContext = any>(
2021

2122
const findOneResolver = findOne(model, tc, opts);
2223

24+
getOrCreateErrorPayload(tc);
25+
2326
const outputTypeName = `UpdateOne${tc.getTypeName()}Payload`;
2427
const outputType = tc.schemaComposer.getOrCreateOTC(outputTypeName, (t) => {
2528
t.addFields({
@@ -31,6 +34,10 @@ export default function updateOne<TSource = Document, TContext = any>(
3134
type: tc,
3235
description: 'Updated document',
3336
},
37+
errors: {
38+
type: '[ErrorPayload]',
39+
description: 'Errors that may occur, typically validations',
40+
},
3441
});
3542
});
3643

@@ -73,7 +80,6 @@ export default function updateOne<TSource = Document, TContext = any>(
7380
)
7481
);
7582
}
76-
7783
// We should get all data for document, cause Mongoose model may have hooks/middlewares
7884
// which required some fields which not in graphql projection
7985
// So empty projection returns all fields.
@@ -87,13 +93,32 @@ export default function updateOne<TSource = Document, TContext = any>(
8793

8894
if (doc && recordData) {
8995
doc.set(recordData);
96+
97+
const validationErrors = doc.validateSync();
98+
let errors = null;
99+
if (validationErrors && validationErrors.errors) {
100+
errors = [];
101+
Object.keys(validationErrors.errors).forEach((key) => {
102+
errors.push({
103+
path: key,
104+
messages: [validationErrors.errors[key].properties.message],
105+
});
106+
});
107+
return {
108+
record: null,
109+
recordId: null,
110+
errors,
111+
};
112+
}
113+
90114
await doc.save();
91115
}
92116

93117
if (doc) {
94118
return {
95119
record: doc,
96120
recordId: tc.getRecordIdFn()(doc),
121+
errors: null,
97122
};
98123
}
99124

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { ObjectTypeComposer } from 'graphql-compose';
2+
3+
export function getOrCreateErrorPayload(tc: ObjectTypeComposer) {
4+
return tc.schemaComposer.getOrCreateOTC('ErrorPayload', (t) => {
5+
t.addFields({
6+
path: {
7+
type: 'String',
8+
description: 'Source of error, typically a model validation path',
9+
},
10+
messages: {
11+
type: '[String]',
12+
description: 'Error messages',
13+
},
14+
});
15+
});
16+
}

0 commit comments

Comments
 (0)