Skip to content

Commit 5a23dd3

Browse files
committed
Automatically convert exclusivity violations to duplicate exceptions on the way out
There's only a little bit of magic here to convert the EdgeDB property name to the "field path". We look at the gql input args to try to determine this.
1 parent ca85db1 commit 5a23dd3

File tree

3 files changed

+66
-5
lines changed

3 files changed

+66
-5
lines changed

src/common/exceptions/duplicate.exception.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
import { ArgumentsHost } from '@nestjs/common';
2+
import {
3+
GqlContextType as ContextKey,
4+
GqlExecutionContext,
5+
} from '@nestjs/graphql';
6+
import { lowerCase, upperFirst } from 'lodash';
7+
import type { ExclusivityViolationError } from '~/core/edgedb';
18
import { InputException } from './input.exception';
29

310
/**
@@ -11,4 +18,47 @@ export class DuplicateException extends InputException {
1118
previous,
1219
);
1320
}
21+
22+
static fromDB(exception: ExclusivityViolationError, context?: ArgumentsHost) {
23+
let property = exception.property;
24+
const message = `${upperFirst(
25+
lowerCase(property),
26+
)} already exists and needs to be unique`;
27+
28+
// Attempt to add path prefix automatically to the property name, based
29+
// 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+
}
46+
47+
const ex = new DuplicateException(property, message, exception);
48+
ex.stack = exception.stack;
49+
return ex;
50+
}
1451
}
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/core/exception/exception.filter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class ExceptionFilter implements GqlExceptionFilter {
3232

3333
let normalized: ExceptionJson;
3434
try {
35-
normalized = this.normalizer.normalize(exception);
35+
normalized = this.normalizer.normalize(exception, args);
3636
} catch (e) {
3737
throw exception;
3838
}

src/core/exception/exception.normalizer.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import { ApolloServerErrorCode as ApolloCode } from '@apollo/server/errors';
2-
import { Inject, Injectable } from '@nestjs/common';
2+
import { ArgumentsHost, Inject, Injectable } from '@nestjs/common';
33
// eslint-disable-next-line no-restricted-imports
44
import * as Nest from '@nestjs/common';
55
import { isNotFalsy, setHas, setOf, simpleSwitch } from '@seedcompany/common';
66
import { GraphQLError } from 'graphql';
77
import { uniq } from 'lodash';
88
import {
99
AbstractClassType,
10+
DuplicateException,
1011
Exception,
1112
getParentTypes,
1213
getPreviousList,
1314
JsonSet,
1415
} from '~/common';
1516
import type { ConfigService } from '~/core';
1617
import * as Neo from '../database/errors';
18+
import { ExclusivityViolationError } from '../edgedb/exclusivity-violation.error';
1719
import { isSrcFrame } from './is-src-frame';
1820
import { normalizeFramePath } from './normalize-frame-path';
1921

@@ -29,14 +31,14 @@ export interface ExceptionJson {
2931
export class ExceptionNormalizer {
3032
constructor(@Inject('CONFIG') private readonly config?: ConfigService) {}
3133

32-
normalize(ex: Error): ExceptionJson {
34+
normalize(ex: Error, context?: ArgumentsHost): ExceptionJson {
3335
const {
3436
message = ex.message,
3537
stack = ex.stack,
3638
code: _,
3739
codes,
3840
...extensions
39-
} = this.gatherExtraInfo(ex);
41+
} = this.gatherExtraInfo(ex, context);
4042
return {
4143
message,
4244
code: codes[0],
@@ -52,7 +54,10 @@ export class ExceptionNormalizer {
5254
};
5355
}
5456

55-
private gatherExtraInfo(ex: Error): Record<string, any> {
57+
private gatherExtraInfo(
58+
ex: Error,
59+
context?: ArgumentsHost,
60+
): Record<string, any> {
5661
if (ex instanceof Nest.HttpException) {
5762
return this.httpException(ex);
5863
}
@@ -87,10 +92,16 @@ export class ExceptionNormalizer {
8792
};
8893
}
8994

95+
if (ex instanceof ExclusivityViolationError) {
96+
ex = DuplicateException.fromDB(ex, context);
97+
}
98+
9099
if (ex instanceof Exception) {
91100
const { name, message, stack, previous, ...rest } = ex;
92101
return {
102+
message,
93103
codes: this.errorToCodes(ex),
104+
stack,
94105
...rest,
95106
};
96107
}

0 commit comments

Comments
 (0)