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 {
|
export class ErrorHandler {
|
||||||
handlers = [];
|
handlers = [];
|
||||||
|
|
||||||
constructor(app: Application) {}
|
|
||||||
|
|
||||||
register(guard: (err) => boolean, render: (err, ctx) => void) {
|
register(guard: (err) => boolean, render: (err, ctx) => void) {
|
||||||
this.handlers.push({
|
this.handlers.push({
|
||||||
guard,
|
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 { createCli, createDatabase, createI18n, createResourcer, registerMiddlewares } from './helper';
|
||||||
import { Plugin } from './plugin';
|
import { Plugin } from './plugin';
|
||||||
import { PluginManager, InstallOptions } from './plugin-manager';
|
import { PluginManager, InstallOptions } from './plugin-manager';
|
||||||
import { ErrorHandler } from './error-handler';
|
|
||||||
|
|
||||||
export interface ResourcerOptions {
|
export interface ResourcerOptions {
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
@ -85,8 +84,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
|
|
||||||
public readonly acl: ACL;
|
public readonly acl: ACL;
|
||||||
|
|
||||||
public readonly errorHandler: ErrorHandler;
|
|
||||||
|
|
||||||
protected plugins = new Map<string, Plugin>();
|
protected plugins = new Map<string, Plugin>();
|
||||||
|
|
||||||
public listenServer: Server;
|
public listenServer: Server;
|
||||||
@ -104,8 +101,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
app: this,
|
app: this,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.errorHandler = new ErrorHandler(this);
|
|
||||||
|
|
||||||
registerMiddlewares(this, options);
|
registerMiddlewares(this, options);
|
||||||
if (options.registerActions !== false) {
|
if (options.registerActions !== false) {
|
||||||
registerActions(this);
|
registerActions(this);
|
||||||
|
@ -5,11 +5,20 @@ import { Command } from 'commander';
|
|||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import { DefaultContext, DefaultState } from 'koa';
|
import { DefaultContext, DefaultState } from 'koa';
|
||||||
import bodyParser from 'koa-bodyparser';
|
import bodyParser from 'koa-bodyparser';
|
||||||
import { BaseError } from 'sequelize';
|
|
||||||
import Application, { ApplicationOptions } from './application';
|
import Application, { ApplicationOptions } from './application';
|
||||||
import { dataWrapping } from './middlewares/data-wrapping';
|
import { dataWrapping } from './middlewares/data-wrapping';
|
||||||
import { table2resource } from './middlewares/table2resource';
|
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) {
|
export function createDatabase(options: ApplicationOptions) {
|
||||||
if (options.database instanceof Database) {
|
if (options.database instanceof Database) {
|
||||||
return options.database;
|
return options.database;
|
||||||
@ -22,16 +31,6 @@ export function createResourcer(options: ApplicationOptions) {
|
|||||||
return new Resourcer({ ...options.resourcer });
|
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 {
|
export function createCli(app: Application, options: ApplicationOptions): Command {
|
||||||
const cli = new Command();
|
const cli = new Command();
|
||||||
|
|
||||||
@ -94,22 +93,7 @@ export function createCli(app: Application, options: ApplicationOptions): Comman
|
|||||||
return cli;
|
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) {
|
export function registerMiddlewares(app: Application, options: ApplicationOptions) {
|
||||||
registerErrorHandler(app);
|
|
||||||
|
|
||||||
if (options.bodyParser !== false) {
|
if (options.bodyParser !== false) {
|
||||||
app.use(
|
app.use(
|
||||||
bodyParser({
|
bodyParser({
|
||||||
|
Loading…
Reference in New Issue
Block a user