Skip to content

Commit 3c2a294

Browse files
committed
chore: Initial work on error payloads
1 parent 3ced48d commit 3c2a294

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
@@ -93,6 +93,27 @@ describe('createOne() ->', () => {
9393
expect(result.record.id).toBe(result.recordId);
9494
});
9595

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

src/resolvers/__tests__/updateById-test.ts

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

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

src/resolvers/__tests__/updateOne-test.ts

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

137+
it('should return empty payload.errors', async () => {
138+
const result = await updateOne(UserModel, UserTC).resolve({
139+
args: { filter: { _id: user1.id } },
140+
});
141+
expect(result.errors).toEqual(null);
142+
});
143+
144+
it('should return payload.errors', async () => {
145+
const result = await updateOne(UserModel, UserTC).resolve({
146+
args: {
147+
filter: { _id: user1.id },
148+
record: { valid: 'AlwaysFails' },
149+
},
150+
});
151+
152+
expect(result.errors).toEqual([{ messages: ['this is a validate message'], path: 'valid' }]);
153+
});
154+
137155
it('should skip records', async () => {
138156
const result1 = await updateOne(UserModel, UserTC).resolve({
139157
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

@@ -72,12 +80,33 @@ export default function createOne<TSource = Document, TContext = any>(
7280
doc = await resolveParams.beforeRecordMutate(doc, resolveParams);
7381
if (!doc) return null;
7482
}
75-
await doc.save();
7683

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

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

@@ -91,12 +98,34 @@ export default function updateById<TSource = Document, TContext = any>(
9198

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

97125
return {
98126
record: doc,
99127
recordId: tc.getRecordIdFn()(doc),
128+
errors: null,
100129
};
101130
}) as any,
102131
});

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

@@ -75,7 +82,6 @@ export default function updateOne<TSource = Document, TContext = any>(
7582
)
7683
);
7784
}
78-
7985
// We should get all data for document, cause Mongoose model may have hooks/middlewares
8086
// which required some fields which not in graphql projection
8187
// So empty projection returns all fields.
@@ -89,13 +95,32 @@ export default function updateOne<TSource = Document, TContext = any>(
8995

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

95119
if (doc) {
96120
return {
97121
record: doc,
98122
recordId: tc.getRecordIdFn()(doc),
123+
errors: null,
99124
};
100125
}
101126

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)