Plugin error handler (#222)
* feat: validation error with i18n * feat: plugin-error-handler * del: error-handle.test.ts * feat: field name with i18n * fix: yarn build
This commit is contained in:
parent
f9018cabda
commit
ad700c61b8
@ -1,41 +0,0 @@
|
||||
import { mockServer } from '@nocobase/test';
|
||||
|
||||
describe('create with exception', () => {
|
||||
it('should handle validationError', async () => {
|
||||
const app = mockServer();
|
||||
|
||||
app.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
allowNull: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await app.loadAndInstall();
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('users')
|
||||
.create({
|
||||
values: {
|
||||
title: 't1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toEqual(400);
|
||||
|
||||
expect(response.body).toEqual({
|
||||
errors: [
|
||||
{
|
||||
message: 'users.name cannot be null',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await app.destroy();
|
||||
});
|
||||
});
|
21
packages/plugin-error-handler/package.json
Normal file
21
packages/plugin-error-handler/package.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-error-handler",
|
||||
"version": "0.6.0-alpha.0",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm",
|
||||
"build:cjs": "tsc --project tsconfig.build.json",
|
||||
"build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nocobase/server": "^0.6.0-alpha.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nocobase/nocobase.git",
|
||||
"directory": "packages/plugin-error-handler"
|
||||
}
|
||||
}
|
120
packages/plugin-error-handler/src/__tests__/render-error.test.ts
Normal file
120
packages/plugin-error-handler/src/__tests__/render-error.test.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { MockServer, mockServer } from '@nocobase/test';
|
||||
import { PluginErrorHandler } from '../server';
|
||||
describe('create with exception', () => {
|
||||
let app: MockServer;
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
app.plugin(PluginErrorHandler);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should handle not null error', async () => {
|
||||
app.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
allowNull: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await app.loadAndInstall();
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('users')
|
||||
.create({
|
||||
values: {
|
||||
title: 't1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toEqual(400);
|
||||
|
||||
expect(response.body).toEqual({
|
||||
errors: [
|
||||
{
|
||||
message: 'name cannot be null',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle unique error', async () => {
|
||||
app.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
unique: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await app.loadAndInstall();
|
||||
|
||||
await app
|
||||
.agent()
|
||||
.resource('users')
|
||||
.create({
|
||||
values: {
|
||||
name: 'u1',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('users')
|
||||
.create({
|
||||
values: {
|
||||
name: 'u1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toEqual(400);
|
||||
|
||||
expect(response.body).toEqual({
|
||||
errors: [
|
||||
{
|
||||
message: 'name must be unique',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should render error with field title', async () => {
|
||||
app.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
allowNull: false,
|
||||
uiSchema: {
|
||||
title: '{{t("UserName")}}',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await app.loadAndInstall();
|
||||
|
||||
const response = await app.agent().resource('users').create({});
|
||||
|
||||
expect(response.statusCode).toEqual(400);
|
||||
|
||||
expect(response.body).toEqual({
|
||||
errors: [
|
||||
{
|
||||
message: 'UserName cannot be null',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
@ -1,10 +1,6 @@
|
||||
import Application from './application';
|
||||
|
||||
export class ErrorHandler {
|
||||
handlers = [];
|
||||
|
||||
constructor(app: Application) {}
|
||||
|
||||
register(guard: (err) => boolean, render: (err, ctx) => void) {
|
||||
this.handlers.push({
|
||||
guard,
|
4
packages/plugin-error-handler/src/locale/en_US.ts
Normal file
4
packages/plugin-error-handler/src/locale/en_US.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default {
|
||||
'unique violation': '{{field}} must be unique',
|
||||
'notNull Violation': '{{field}} cannot be null',
|
||||
};
|
4
packages/plugin-error-handler/src/locale/zh_CN.ts
Normal file
4
packages/plugin-error-handler/src/locale/zh_CN.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default {
|
||||
'unique violation': '{{field}} 字段值是唯一的',
|
||||
'notNull Violation': '{{field}} 字段不能为空',
|
||||
};
|
50
packages/plugin-error-handler/src/server.ts
Normal file
50
packages/plugin-error-handler/src/server.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { ErrorHandler } from './error-handler';
|
||||
import { BaseError } from 'sequelize';
|
||||
import zhCN from './locale/zh_CN';
|
||||
import enUS from './locale/en_US';
|
||||
import lodash from 'lodash';
|
||||
import { compile } from '@formily/json-schema/lib/compiler';
|
||||
|
||||
export class PluginErrorHandler extends Plugin {
|
||||
errorHandler: ErrorHandler = new ErrorHandler();
|
||||
i18nNs: string = 'error-handler';
|
||||
|
||||
beforeLoad() {
|
||||
this.registerSequelizeValidationErrorHandler();
|
||||
}
|
||||
|
||||
registerSequelizeValidationErrorHandler() {
|
||||
const findFieldTitle = (instance, path, tFunc) => {
|
||||
const model = instance.constructor;
|
||||
const collection = this.db.modelCollection.get(model);
|
||||
const field = collection.getField(path);
|
||||
const fieldOptions = compile(field.options, { t: tFunc });
|
||||
const title = lodash.get(fieldOptions, 'uiSchema.title', path);
|
||||
return title;
|
||||
};
|
||||
|
||||
this.errorHandler.register(
|
||||
(err) => err?.errors?.length && err instanceof BaseError,
|
||||
(err, ctx) => {
|
||||
ctx.body = {
|
||||
errors: err.errors.map((err) => {
|
||||
return {
|
||||
message: ctx.i18n.t(err.type, {
|
||||
ns: this.i18nNs,
|
||||
field: findFieldTitle(err.instance, err.path, ctx.i18n.t),
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
ctx.status = 400;
|
||||
},
|
||||
);
|
||||
}
|
||||
async load() {
|
||||
this.app.i18n.addResources('zh-CN', this.i18nNs, zhCN);
|
||||
this.app.i18n.addResources('en-US', this.i18nNs, enUS);
|
||||
|
||||
this.app.middleware.unshift(this.errorHandler.middleware());
|
||||
}
|
||||
}
|
9
packages/plugin-error-handler/tsconfig.build.json
Normal file
9
packages/plugin-error-handler/tsconfig.build.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib",
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"]
|
||||
}
|
5
packages/plugin-error-handler/tsconfig.json
Normal file
5
packages/plugin-error-handler/tsconfig.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"exclude": ["./esm/*", "./lib/*"]
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
import { mockServer } from '@nocobase/test';
|
||||
|
||||
describe('error handle', () => {
|
||||
it('should handle error with default handler', async () => {
|
||||
const app = mockServer();
|
||||
|
||||
app.use(async () => {
|
||||
throw new Error('some thing went wrong');
|
||||
});
|
||||
|
||||
const response = await app.agent().post('/');
|
||||
|
||||
expect(response.statusCode).toEqual(500);
|
||||
expect(response.body.errors[0].message).toEqual('some thing went wrong');
|
||||
});
|
||||
|
||||
it('should handle error by custom handler', async () => {
|
||||
class CustomError extends Error {
|
||||
constructor(message, errors) {
|
||||
super(message);
|
||||
this.name = 'CustomError';
|
||||
}
|
||||
}
|
||||
|
||||
const app = mockServer();
|
||||
|
||||
app.errorHandler.register(
|
||||
(err) => {
|
||||
return err.name == 'CustomError';
|
||||
},
|
||||
(err, ctx) => {
|
||||
ctx.body = {
|
||||
message: 'hello',
|
||||
};
|
||||
ctx.status = 422;
|
||||
},
|
||||
);
|
||||
|
||||
app.use(async () => {
|
||||
throw new CustomError('some thing went wrong', []);
|
||||
});
|
||||
|
||||
const response = await app.agent().post('/');
|
||||
|
||||
expect(response.statusCode).toEqual(422);
|
||||
expect(response.body).toEqual({ message: 'hello' });
|
||||
});
|
||||
});
|
@ -12,7 +12,6 @@ import { createACL } from './acl';
|
||||
import { createCli, createDatabase, createI18n, createResourcer, registerMiddlewares } from './helper';
|
||||
import { Plugin } from './plugin';
|
||||
import { PluginManager, InstallOptions } from './plugin-manager';
|
||||
import { ErrorHandler } from './error-handler';
|
||||
|
||||
export interface ResourcerOptions {
|
||||
prefix?: string;
|
||||
@ -85,8 +84,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
|
||||
public readonly acl: ACL;
|
||||
|
||||
public readonly errorHandler: ErrorHandler;
|
||||
|
||||
protected plugins = new Map<string, Plugin>();
|
||||
|
||||
public listenServer: Server;
|
||||
@ -104,8 +101,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
app: this,
|
||||
});
|
||||
|
||||
this.errorHandler = new ErrorHandler(this);
|
||||
|
||||
registerMiddlewares(this, options);
|
||||
if (options.registerActions !== false) {
|
||||
registerActions(this);
|
||||
|
@ -5,11 +5,20 @@ import { Command } from 'commander';
|
||||
import i18next from 'i18next';
|
||||
import { DefaultContext, DefaultState } from 'koa';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import { BaseError } from 'sequelize';
|
||||
import Application, { ApplicationOptions } from './application';
|
||||
import { dataWrapping } from './middlewares/data-wrapping';
|
||||
import { table2resource } from './middlewares/table2resource';
|
||||
|
||||
export function createI18n(options: ApplicationOptions) {
|
||||
const instance = i18next.createInstance();
|
||||
instance.init({
|
||||
lng: 'en-US',
|
||||
resources: {},
|
||||
...options.i18n,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
export function createDatabase(options: ApplicationOptions) {
|
||||
if (options.database instanceof Database) {
|
||||
return options.database;
|
||||
@ -22,16 +31,6 @@ export function createResourcer(options: ApplicationOptions) {
|
||||
return new Resourcer({ ...options.resourcer });
|
||||
}
|
||||
|
||||
export function createI18n(options: ApplicationOptions) {
|
||||
const instance = i18next.createInstance();
|
||||
instance.init({
|
||||
lng: 'en-US',
|
||||
resources: {},
|
||||
...options.i18n,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
export function createCli(app: Application, options: ApplicationOptions): Command {
|
||||
const cli = new Command();
|
||||
|
||||
@ -94,22 +93,7 @@ export function createCli(app: Application, options: ApplicationOptions): Comman
|
||||
return cli;
|
||||
}
|
||||
|
||||
function registerErrorHandler(app: Application) {
|
||||
app.errorHandler.register(
|
||||
(err) => err?.errors?.length && err instanceof BaseError,
|
||||
(err, ctx) => {
|
||||
ctx.body = {
|
||||
errors: err.errors.map((err) => ({ message: err.message })),
|
||||
};
|
||||
ctx.status = 400;
|
||||
},
|
||||
);
|
||||
app.use(app.errorHandler.middleware());
|
||||
}
|
||||
|
||||
export function registerMiddlewares(app: Application, options: ApplicationOptions) {
|
||||
registerErrorHandler(app);
|
||||
|
||||
if (options.bodyParser !== false) {
|
||||
app.use(
|
||||
bodyParser({
|
||||
|
Loading…
Reference in New Issue
Block a user