Skip to content

Commit f583dfc

Browse files
authored
Merge pull request #3057 from SeedCompany/edgedb/convert-not-found-errors
2 parents d712d2f + 75559ba commit f583dfc

File tree

3 files changed

+104
-38
lines changed

3 files changed

+104
-38
lines changed
Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
import { ArgumentsHost } from '@nestjs/common';
2-
import {
3-
GqlContextType as ContextKey,
4-
GqlExecutionContext,
5-
} from '@nestjs/graphql';
62
import { lowerCase, upperFirst } from 'lodash';
73
import type { ExclusivityViolationError } from '~/core/edgedb';
84
import { InputException } from './input.exception';
@@ -27,38 +23,14 @@ export class DuplicateException extends InputException {
2723

2824
// Attempt to add path prefix automatically to the property name, based
2925
// on given GQL input.
30-
if (context && context.getType<ContextKey>() === 'graphql') {
31-
let gqlArgs = GqlExecutionContext.create(context as any).getArgs();
32-
33-
// unwind single `input` argument, based on our own conventions
34-
if (Object.keys(gqlArgs).length === 1 && 'input' in gqlArgs) {
35-
gqlArgs = gqlArgs.input;
36-
}
37-
38-
const flattened = flattenObject(gqlArgs);
39-
// Guess the correct path based on property name.
40-
// This kinda assumes the property name will be unique amongst all the input.
41-
const guessedPath = Object.keys(flattened).find(
42-
(path) => property === path || path.endsWith('.' + property),
43-
);
44-
property = guessedPath ?? property;
45-
}
26+
// This kinda assumes the property name will be unique amongst all the input.
27+
const guessedPath = Object.keys(
28+
InputException.getFlattenInput(context),
29+
).find((path) => property === path || path.endsWith('.' + property));
30+
property = guessedPath ?? property;
4631

4732
const ex = new DuplicateException(property, message, exception);
4833
ex.stack = exception.stack;
4934
return ex;
5035
}
5136
}
52-
53-
const flattenObject = (obj: object, prefix = '') => {
54-
const result: Record<string, any> = {};
55-
for (const [key, value] of Object.entries(obj)) {
56-
if (value && typeof value === 'object' && !Array.isArray(value)) {
57-
const nestedObj = flattenObject(value, prefix + key + '.');
58-
Object.assign(result, nestedObj);
59-
} else {
60-
result[prefix + key] = value;
61-
}
62-
}
63-
return result;
64-
};

src/common/exceptions/input.exception.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import { ArgumentsHost } from '@nestjs/common';
2+
import {
3+
GqlContextType as ContextKey,
4+
GqlExecutionContext,
5+
} from '@nestjs/graphql';
16
import { ClientException } from './exception';
27

38
export type InputExceptionArgs =
@@ -104,4 +109,35 @@ export class InputException extends ClientException {
104109
}
105110
return [message, field, previous] as const;
106111
}
112+
113+
static getFlattenInput(context?: ArgumentsHost) {
114+
if (!context || context.getType<ContextKey>() !== 'graphql') {
115+
return {};
116+
}
117+
const gqlContext =
118+
context instanceof GqlExecutionContext
119+
? context
120+
: GqlExecutionContext.create(context as any);
121+
let gqlArgs = gqlContext.getArgs();
122+
123+
// unwind single `input` argument, based on our own conventions
124+
if (Object.keys(gqlArgs).length === 1 && 'input' in gqlArgs) {
125+
gqlArgs = gqlArgs.input;
126+
}
127+
128+
return flattenObject(gqlArgs);
129+
}
107130
}
131+
132+
const flattenObject = (obj: object, prefix = '') => {
133+
const result: Record<string, any> = {};
134+
for (const [key, value] of Object.entries(obj)) {
135+
if (value && typeof value === 'object' && !Array.isArray(value)) {
136+
const nestedObj = flattenObject(value, prefix + key + '.');
137+
Object.assign(result, nestedObj);
138+
} else {
139+
result[prefix + key] = value;
140+
}
141+
}
142+
return result;
143+
};

src/core/exception/exception.normalizer.ts

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import {
66
GqlContextType as ContextKey,
77
GqlExecutionContext,
88
} from '@nestjs/graphql';
9-
import { isNotFalsy, setHas, setOf, simpleSwitch } from '@seedcompany/common';
9+
import {
10+
entries,
11+
isNotFalsy,
12+
setHas,
13+
setOf,
14+
simpleSwitch,
15+
} from '@seedcompany/common';
1016
import * as Edge from 'edgedb';
1117
import * as EdgeDBTags from 'edgedb/dist/errors/tags.js';
1218
import { GraphQLError, GraphQLResolveInfo } from 'graphql';
@@ -17,11 +23,14 @@ import {
1723
Exception,
1824
getParentTypes,
1925
getPreviousList,
26+
InputException,
2027
JsonSet,
28+
NotFoundException,
2129
} from '~/common';
2230
import type { ConfigService } from '~/core';
2331
import * as Neo from '../database/errors';
2432
import { ExclusivityViolationError } from '../edgedb/exclusivity-violation.error';
33+
import { ResourcesHost } from '../resources/resources.host';
2534
import { isSrcFrame } from './is-src-frame';
2635
import { normalizeFramePath } from './normalize-frame-path';
2736

@@ -35,7 +44,10 @@ export interface ExceptionJson {
3544

3645
@Injectable()
3746
export class ExceptionNormalizer {
38-
constructor(@Inject('CONFIG') private readonly config?: ConfigService) {}
47+
constructor(
48+
@Inject('CONFIG') private readonly config?: ConfigService,
49+
private readonly resources?: ResourcesHost,
50+
) {}
3951

4052
normalize(ex: Error, context?: ArgumentsHost): ExceptionJson {
4153
const {
@@ -114,13 +126,22 @@ export class ExceptionNormalizer {
114126
};
115127
}
116128

129+
const gqlContext =
130+
context &&
131+
context.getType<ContextKey>() === 'graphql' &&
132+
// schema input validation errors don't create an execution context correctly
133+
!(ex instanceof GraphQLError)
134+
? GqlExecutionContext.create(context as any)
135+
: undefined;
136+
137+
ex = this.wrapIDNotFoundError(ex, gqlContext);
138+
117139
if (ex instanceof ExclusivityViolationError) {
118-
ex = DuplicateException.fromDB(ex, context);
140+
ex = DuplicateException.fromDB(ex, gqlContext);
119141
} else if (ex instanceof Edge.EdgeDBError) {
120142
// Mask actual DB error with a nicer user error message.
121143
let message = 'Failed';
122-
if (context && context.getType<ContextKey>() === 'graphql') {
123-
const gqlContext = GqlExecutionContext.create(context as any);
144+
if (gqlContext) {
124145
const info = gqlContext.getInfo<GraphQLResolveInfo>();
125146
if (info.operation.operation === 'mutation') {
126147
message += ` to ${lowerCase(info.fieldName)}`;
@@ -161,6 +182,43 @@ export class ExceptionNormalizer {
161182
return { codes: ['Server'] };
162183
}
163184

185+
/**
186+
* Convert ID not found database errors from user input
187+
* to user input NotFound error with that input path.
188+
*/
189+
private wrapIDNotFoundError(
190+
ex: Error,
191+
gqlContext: GqlExecutionContext | undefined,
192+
) {
193+
if (!(ex instanceof Edge.CardinalityViolationError)) {
194+
return ex;
195+
}
196+
197+
const matched = ex.message.match(/'(.+)' with id '(.+)' does not exist/);
198+
if (!matched) {
199+
return ex;
200+
}
201+
const [_, type, id] = matched;
202+
203+
const inputPath = entries(InputException.getFlattenInput(gqlContext)).find(
204+
([_, value]) => value === id,
205+
)?.[0];
206+
if (!inputPath) {
207+
return ex;
208+
}
209+
210+
const typeName = this.resources
211+
? this.resources.getByEdgeDB(type).name
212+
: type;
213+
const wrapped = new NotFoundException(
214+
`${typeName} could not be found`,
215+
inputPath,
216+
ex,
217+
);
218+
wrapped.stack = ex.stack;
219+
return wrapped;
220+
}
221+
164222
private httpException(ex: Nest.HttpException) {
165223
const res = ex.getResponse();
166224
const {

0 commit comments

Comments
 (0)