refactor(server): server errors (#5741)

standardize the error raising in both GraphQL Resolvers and Controllers.

Now, All user aware errors should be throwed with `HttpException`'s variants, for example `NotFoundException`.

> Directly throwing `GraphQLError` are forbidden.
The GraphQL errorFormatter will handle it automatically and set `code`, `status` in error extensions.

At the same time, the frontend `GraphQLError` should be imported from `@affine/graphql`, which introduce a better error extensions type.

----
controller example:
```js
@Get('/docs/${id}')
doc() {
  // ...
  // imported from '@nestjs/common'
  throw new NotFoundException('Doc is not found.');
  // ...
}
```
the above will response as:
```
status: 404 Not Found
{
  "message": "Doc is not found.",
  "statusCode": 404,
  "error": "Not Found"
}
```

resolver example:
```js
@Mutation()
invite() {
  // ...
  throw new PayloadTooLargeException('Workspace seats is full.')
  // ...
}
```

the above will response as:
```
status: 200 Ok
{
  "data": null,
  "errors": [
    {
      "message": "Workspace seats is full.",
      "extensions": {
        "code": 404,
        "status": "Not Found"
      }
    }
  ]
}
```

for frontend GraphQLError user-friend, a helper function introduced:

```js
import { findGraphQLError } from '@affine/graphql'

fetch(query)
  .catch(errOrArr => {
    const e = findGraphQLError(errOrArr, e => e.extensions.code === 404)
    if (e) {
      // handle
    }
})
```
This commit is contained in:
liuyi
2024-01-31 08:43:03 +00:00
parent 72d9cc1e5b
commit 26db1d436d
22 changed files with 310 additions and 193 deletions

View File

@@ -1,4 +1,8 @@
import { HttpStatus } from '@nestjs/common';
import {
BadGatewayException,
ForbiddenException,
InternalServerErrorException,
} from '@nestjs/common';
import {
Args,
Context,
@@ -13,7 +17,6 @@ import {
Resolver,
} from '@nestjs/graphql';
import type { User, UserInvoice, UserSubscription } from '@prisma/client';
import { GraphQLError } from 'graphql';
import { groupBy } from 'lodash-es';
import { Auth, CurrentUser, Public } from '../../core/auth';
@@ -164,12 +167,9 @@ export class SubscriptionResolver {
);
if (!yearly || !monthly) {
throw new GraphQLError('The prices are not configured correctly', {
extensions: {
status: HttpStatus[HttpStatus.BAD_GATEWAY],
code: HttpStatus.BAD_GATEWAY,
},
});
throw new InternalServerErrorException(
'The prices are not configured correctly.'
);
}
return {
@@ -199,12 +199,7 @@ export class SubscriptionResolver {
});
if (!session.url) {
throw new GraphQLError('Failed to create checkout session', {
extensions: {
status: HttpStatus[HttpStatus.BAD_GATEWAY],
code: HttpStatus.BAD_GATEWAY,
},
});
throw new BadGatewayException('Failed to create checkout session.');
}
return session.url;
@@ -263,14 +258,8 @@ export class UserSubscriptionResolver {
) {
// allow admin to query other user's subscription
if (!ctx.isAdminQuery && me.id !== user.id) {
throw new GraphQLError(
'You are not allowed to access this subscription',
{
extensions: {
status: HttpStatus[HttpStatus.FORBIDDEN],
code: HttpStatus.FORBIDDEN,
},
}
throw new ForbiddenException(
'You are not allowed to access this subscription.'
);
}
@@ -310,12 +299,9 @@ export class UserSubscriptionResolver {
@Args('skip', { type: () => Int, nullable: true }) skip?: number
) {
if (me.id !== user.id) {
throw new GraphQLError('You are not allowed to access this invoices', {
extensions: {
status: HttpStatus[HttpStatus.FORBIDDEN],
code: HttpStatus.FORBIDDEN,
},
});
throw new ForbiddenException(
'You are not allowed to access this invoices'
);
}
return this.db.userInvoice.findMany({

View File

@@ -2,7 +2,7 @@ import { createAdapter } from '@socket.io/redis-adapter';
import { Redis } from 'ioredis';
import { Server, ServerOptions } from 'socket.io';
import { SocketIoAdapter } from '../../fundamentals';
import { SocketIoAdapter } from '../../fundamentals/websocket';
export function createSockerIoAdapterImpl(
redis: Redis