Skip to content

Commit 693828a

Browse files
authored
Merge pull request #3181 from SeedCompany/exception-tracing
2 parents caa7829 + 68a9b24 commit 693828a

22 files changed

+363
-290
lines changed

src/common/exceptions/duplicate.exception.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ export class DuplicateException extends InputException {
2929
).find((path) => property === path || path.endsWith('.' + property));
3030
property = guessedPath ?? property;
3131

32-
const ex = new DuplicateException(property, message, exception);
33-
ex.stack = exception.stack;
34-
return ex;
32+
return new DuplicateException(property, message, exception);
3533
}
3634
}

src/common/exceptions/exception.ts

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,23 @@
33
* Don't throw this, but rather a sub-class instead.
44
*/
55
export abstract class Exception extends Error {
6-
/**
7-
* Basically just to group exceptions into client/server groups.
8-
* This may be removed later.
9-
*/
10-
abstract readonly status: number;
6+
declare cause?: Error;
117

12-
constructor(message: string, readonly previous?: Error) {
13-
super(message);
8+
constructor(message: string, cause?: Error) {
9+
super(message, { cause });
1410
this.name = this.constructor.name;
1511
}
1612
}
1713

18-
export class ServerException extends Exception {
19-
readonly status: number = 500;
20-
}
21-
22-
export class ClientException extends Exception {
23-
readonly status: number = 400;
24-
}
14+
export class ServerException extends Exception {}
2515

26-
export const hasPrevious = (e: Error): e is Error & { previous: Error } =>
27-
'previous' in e && e.previous instanceof Error;
16+
export class ClientException extends Exception {}
2817

29-
export function getPreviousList(ex: Error, includeSelf: boolean) {
18+
export function getCauseList(ex: Error, includeSelf = true): readonly Error[] {
3019
const previous: Error[] = includeSelf ? [ex] : [];
3120
let current = ex;
32-
while (current.cause instanceof Error || hasPrevious(current)) {
33-
current = hasPrevious(current)
34-
? current.previous
35-
: (current.cause as Error);
21+
while (current.cause instanceof Error) {
22+
current = current.cause;
3623
previous.push(current);
3724
}
3825
return previous;

src/common/exceptions/unauthorized.exception.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { HttpStatus } from '@nestjs/common';
21
import { lowerCase } from 'lodash';
32
import pluralize from 'plur';
4-
import { Writable as Mutable } from 'type-fest';
53
import { EnhancedResource } from '../resource.dto';
64
import { InputException, InputExceptionArgs } from './input.exception';
75

@@ -67,7 +65,6 @@ export class UnauthorizedException extends InputException {
6765

6866
constructor(...args: InputExceptionArgs) {
6967
super(...InputException.parseArgs(`Insufficient permission`, args));
70-
(this as Mutable<this>).status = HttpStatus.FORBIDDEN;
7168
}
7269

7370
static fromPrivileges(

src/core/database/transaction.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Connection } from 'cypher-query-builder';
22
import { Duration, DurationLike } from 'luxon';
33
import { Transaction as NeoTransaction } from 'neo4j-driver';
4-
import { getPreviousList, ServerException } from '../../common';
4+
import { getCauseList, ServerException } from '../../common';
55
import { ILogger } from '../logger';
66
import { PatchedConnection } from './cypher.factory';
77
import { isNeo4jError } from './errors';
@@ -120,9 +120,7 @@ Connection.prototype.runInTransaction = async function withTransaction<R>(
120120
// throw that here and save the original.
121121
// This allows neo4j to check if the error is retry-able.
122122
// If it is, then this error doesn't matter; otherwise we'll unwrap below.
123-
const maybeRetryableError = getPreviousList(error, true).find(
124-
isNeo4jError,
125-
);
123+
const maybeRetryableError = getCauseList(error).find(isNeo4jError);
126124
if (maybeRetryableError) {
127125
errorMap.set(maybeRetryableError, error);
128126
throw maybeRetryableError;

src/core/edgedb/edgedb.service.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/* eslint-disable @typescript-eslint/unified-signatures */
22
import { Injectable, Optional } from '@nestjs/common';
3-
import { $, Executor } from 'edgedb';
3+
import { $, ConstraintViolationError, EdgeDBError, Executor } from 'edgedb';
44
import { QueryArgs } from 'edgedb/dist/ifaces';
55
import { retry, RetryOptions } from '~/common/retry';
6+
import { jestSkipFileInExceptionSource } from '../exception';
67
import { TypedEdgeQL } from './edgeql';
7-
import { ExclusivityViolationError } from './exclusivity-violation.error';
8+
import { enhanceConstraintError } from './errors';
89
import { InlineQueryRuntimeMap } from './generated-client/inline-queries';
910
import { ApplyOptions, OptionsContext } from './options.context';
1011
import { Client } from './reexports';
@@ -131,10 +132,22 @@ export class EdgeDB {
131132
}
132133
} catch (e) {
133134
// Ignore this call in stack trace. This puts the actual query as the first.
134-
e.stack = e.stack!.replace(/^\s+at EdgeDB\.run.+\n/m, '');
135+
e.stack = e.stack!.replace(/^\s+at(?: async)? EdgeDB\.run.+$\n/m, '');
136+
137+
// Don't present abstract repositories as the src block in jest reports
138+
// for DB execution errors.
139+
// There shouldn't be anything specific to there to be helpful.
140+
// This is a bit of a broad assumption though, so only do for jest and
141+
// keep the frame for actual use from users/devs.
142+
if (e instanceof EdgeDBError) {
143+
jestSkipFileInExceptionSource(
144+
e,
145+
/^\s+at .+src[/|\\]core[/|\\]edgedb[/|\\].+\.repository\..+$\n/gm,
146+
);
147+
}
135148

136-
if (ExclusivityViolationError.is(e)) {
137-
throw ExclusivityViolationError.cast(e);
149+
if (e instanceof ConstraintViolationError) {
150+
throw enhanceConstraintError(e);
138151
}
139152
throw e;
140153
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { ConstraintViolationError } from 'edgedb';
2+
import { LiteralUnion } from 'type-fest';
3+
import type { AllResourceDBNames } from '~/core/resources';
4+
import { ExclusivityViolationError } from './exclusivity-violation.error';
5+
6+
declare module 'edgedb' {
7+
interface ConstraintViolationError {
8+
readonly objectFQN: AllResourceDBNames;
9+
readonly property: string;
10+
readonly constraint: LiteralUnion<'std::exclusive' | 'std::regexp', string>;
11+
}
12+
}
13+
14+
export const enhanceConstraintError = (e: ConstraintViolationError) => {
15+
// @ts-expect-error it is a private field
16+
const attrs: Map<number, Uint8Array> = e._attrs;
17+
18+
const detail = new TextDecoder('utf8').decode(attrs.get(2 /* details */));
19+
const matches = detail.match(
20+
/^violated constraint '(?<constraint>.+)' on property '(?<property>.+)' of object type '(?<fqn>.+)'$/,
21+
);
22+
if (!matches) {
23+
throw new Error(`Could not parse constraint violation error`, { cause: e });
24+
}
25+
const { fqn, property, constraint } = matches.groups!;
26+
27+
if (constraint === 'std::exclusive') {
28+
e = ExclusivityViolationError.cast(e);
29+
}
30+
31+
return Object.assign(e, {
32+
objectFQN: fqn,
33+
property: property,
34+
constraint: constraint,
35+
});
36+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ConstraintViolationError } from 'edgedb';
2+
3+
export class ExclusivityViolationError extends ConstraintViolationError {
4+
static cast(e: ConstraintViolationError) {
5+
// @ts-expect-error it is a private field
6+
const message: string = e._message;
7+
// @ts-expect-error it is a private field
8+
const query: string = e._query;
9+
// @ts-expect-error it is a private field
10+
const attrs: Map<number, Uint8Array> = e._attrs;
11+
12+
const ex = new ExclusivityViolationError(message);
13+
14+
ex.stack = e.stack!.replace(
15+
/^ConstraintViolationError:/,
16+
'ExclusivityViolationError:',
17+
);
18+
// @ts-expect-error it's a private field
19+
ex._query = query;
20+
// @ts-expect-error it's a private field
21+
ex._attrs = attrs;
22+
23+
return ex;
24+
}
25+
}

src/core/edgedb/errors/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './constraint-violation.error';
2+
export * from './exclusivity-violation.error';

src/core/edgedb/exclusivity-violation.error.ts

Lines changed: 0 additions & 59 deletions
This file was deleted.

src/core/edgedb/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export * from './options';
44
export type { OptionsFn } from './options.context';
55
export * from './edgedb.service';
66
export * from './withScope';
7-
export * from './exclusivity-violation.error';
7+
export * from './errors/exclusivity-violation.error';
88
export * from './common.repository';
99
export * from './dto.repository';
1010
export * from './query-util/disable-access-policies.option';

0 commit comments

Comments
 (0)