From 3fa9e59093b1cd67de98156511764bf2561b7c45 Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 9 Sep 2021 13:05:33 +0800 Subject: [PATCH] refactor: improve server application --- .../server/src/__tests__/application.test.ts | 123 ++++++++++++++++ .../server/src/__tests__/dataWrapping.test.ts | 57 ++++++++ .../server/src/__tests__/middleware.test.ts | 105 -------------- packages/server/src/application.ts | 88 +++++++----- packages/server/src/middleware.ts | 135 ------------------ .../server/src/middlewares/data-wrapping.ts | 30 ++++ .../src/middlewares/db-resource-router.ts | 133 ----------------- packages/server/src/middlewares/index.ts | 3 +- .../server/src/middlewares/table2resource.ts | 72 ++++++++++ 9 files changed, 335 insertions(+), 411 deletions(-) create mode 100644 packages/server/src/__tests__/application.test.ts create mode 100644 packages/server/src/__tests__/dataWrapping.test.ts delete mode 100644 packages/server/src/__tests__/middleware.test.ts delete mode 100644 packages/server/src/middleware.ts create mode 100644 packages/server/src/middlewares/data-wrapping.ts delete mode 100644 packages/server/src/middlewares/db-resource-router.ts create mode 100644 packages/server/src/middlewares/table2resource.ts diff --git a/packages/server/src/__tests__/application.test.ts b/packages/server/src/__tests__/application.test.ts new file mode 100644 index 000000000..79734facc --- /dev/null +++ b/packages/server/src/__tests__/application.test.ts @@ -0,0 +1,123 @@ +import supertest from 'supertest'; +import { Application } from '../application'; + +describe('application', () => { + let app: Application; + let agent; + + beforeEach(() => { + app = new Application({ + database: { + username: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE, + host: process.env.DB_HOST, + port: process.env.DB_PORT as any, + dialect: process.env.DB_DIALECT as any, + dialectOptions: { + charset: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }, + }, + resourcer: { + prefix: '/api', + }, + dataWrapping: false, + }); + app.resourcer.registerActionHandlers({ + list: async (ctx, next) => { + ctx.body = [1, 2]; + await next(); + }, + get: async (ctx, next) => { + ctx.body = [3, 4]; + await next(); + }, + 'foo2s.bar2s:list': async (ctx, next) => { + ctx.body = [5, 6]; + await next(); + }, + }); + agent = supertest.agent(app.callback()); + }); + + afterEach(async () => { + return app.database.close(); + }); + + it('resourcer.define', async () => { + app.resourcer.define({ + name: 'test', + }); + const response = await agent.get('/api/test'); + expect(response.body).toEqual([1, 2]); + }); + + it('resourcer.define', async () => { + app.resourcer.define({ + type: 'hasMany', + name: 'test.abc', + }); + const response = await agent.get('/api/test/1/abc'); + expect(response.body).toEqual([1, 2]); + }); + + it('db.table', async () => { + app.database.table({ + name: 'tests', + }); + const response = await agent.get('/api/tests'); + expect(response.body).toEqual([1, 2]); + }); + + it('db.association', async () => { + app.database.table({ + name: 'bars', + }); + app.database.table({ + name: 'foos', + fields: [ + { + type: 'hasMany', + name: 'bars', + } + ], + }); + const response = await agent.get('/api/foos/1/bars'); + expect(response.body).toEqual([1, 2]); + }); + + it('db.middleware', async () => { + const index = app.middleware.findIndex(m => m.name === 'table2resource'); + app.middleware.splice(index, 0, async (ctx, next) => { + app.database.table({ + name: 'tests', + }); + await next(); + }); + const response = await agent.get('/api/tests'); + expect(response.body).toEqual([1, 2]); + }); + + it('db.middleware', async () => { + const index = app.middleware.findIndex(m => m.name === 'table2resource'); + app.middleware.splice(index, 0, async (ctx, next) => { + app.database.table({ + name: 'bars', + }); + app.database.table({ + name: 'foos', + fields: [ + { + type: 'hasMany', + name: 'bars', + } + ], + }); + await next(); + }); + console.log(app.middleware); + const response = await agent.get('/api/foos/1/bars'); + expect(response.body).toEqual([1, 2]); + }); +}); diff --git a/packages/server/src/__tests__/dataWrapping.test.ts b/packages/server/src/__tests__/dataWrapping.test.ts new file mode 100644 index 000000000..82ab9b0be --- /dev/null +++ b/packages/server/src/__tests__/dataWrapping.test.ts @@ -0,0 +1,57 @@ +import supertest from 'supertest'; +import { Application } from '../application'; + +describe('application', () => { + let app: Application; + let agent; + + beforeEach(() => { + app = new Application({ + database: { + username: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE, + host: process.env.DB_HOST, + port: process.env.DB_PORT as any, + dialect: process.env.DB_DIALECT as any, + dialectOptions: { + charset: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }, + }, + resourcer: { + prefix: '/api', + }, + dataWrapping: true, + }); + app.resourcer.registerActionHandlers({ + list: async (ctx, next) => { + ctx.body = [1, 2]; + await next(); + }, + get: async (ctx, next) => { + ctx.body = [3, 4]; + await next(); + }, + 'foo2s.bar2s:list': async (ctx, next) => { + ctx.body = [5, 6]; + await next(); + }, + }); + agent = supertest.agent(app.callback()); + }); + + afterEach(async () => { + return app.database.close(); + }); + + it('resourcer.define', async () => { + app.resourcer.define({ + name: 'test', + }); + const response = await agent.get('/api/test'); + expect(response.body).toEqual({ + data: [1, 2], + }); + }); +}); diff --git a/packages/server/src/__tests__/middleware.test.ts b/packages/server/src/__tests__/middleware.test.ts deleted file mode 100644 index 8947cbf56..000000000 --- a/packages/server/src/__tests__/middleware.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import Koa from 'koa'; -import supertest from 'supertest'; -import http from 'http'; -import Resourcer from '@nocobase/resourcer'; -import Database from '@nocobase/database'; -import middleware from '../middleware'; - -describe('middleware', () => { - let app: Koa; - let resourcer: Resourcer; - let database: Database; - let agent; - - beforeAll(() => { - app = new Koa(); - resourcer = new Resourcer(); - resourcer.registerActionHandlers({ - list: async (ctx, next) => { - ctx.body = [1, 2]; - await next(); - }, - get: async (ctx, next) => { - ctx.body = [3, 4]; - await next(); - }, - 'foo2s.bar2s:list': async (ctx, next) => { - ctx.body = [5, 6]; - await next(); - }, - }); - database = new Database({ - username: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_DATABASE, - host: process.env.DB_HOST, - port: process.env.DB_PORT as any, - dialect: process.env.DB_DIALECT as any, - dialectOptions: { - charset: 'utf8mb4', - collate: 'utf8mb4_unicode_ci', - }, - }); - agent = supertest.agent(app.callback()); - app.use(middleware({ - prefix: '/api', - database, - resourcer, - })); - }); - it('shound work', async () => { - database.table({ - name: 'tests', - }); - const response = await agent.get('/api/tests'); - expect(response.body).toEqual([1, 2]); - }); - it('shound work', async () => { - database.table({ - name: 'foos', - fields: [ - { - type: 'hasmany', - name: 'bars', - } - ] - }); - database.table({ - name: 'bars', - fields: [ - { - type: 'belongsTo', - name: 'foo', - }, - ], - }); - let response = await agent.get('/api/foos/1/bars'); - expect(response.body).toEqual([1, 2]); - response = await agent.get('/api/bars/1/foo'); - expect(response.body).toEqual([3, 4]); - }); - it('shound work', async () => { - database.table({ - name: 'foo2s', - fields: [ - { - type: 'belongsToMany', - name: 'bar2s', - } - ] - }); - database.table({ - name: 'bar2s', - fields: [ - { - type: 'belongsToMany', - name: 'foo2s', - }, - ], - }); - let response = await agent.get('/api/foo2s/1/bar2s'); - expect(response.body).toEqual([5, 6]); - response = await agent.get('/api/bar2s/1/foo2s'); - expect(response.body).toEqual([1, 2]); - }); -}); diff --git a/packages/server/src/application.ts b/packages/server/src/application.ts index 37bfcbcc3..adb83dc55 100644 --- a/packages/server/src/application.ts +++ b/packages/server/src/application.ts @@ -1,19 +1,24 @@ import Koa from 'koa'; +import cors from '@koa/cors'; +import bodyParser from 'koa-bodyparser'; +import { Command } from 'commander'; import Database, { DatabaseOptions } from '@nocobase/database'; import Resourcer from '@nocobase/resourcer'; -import { Command } from 'commander'; -import { actions, middlewares as m } from '@nocobase/actions'; -import cors from '@koa/cors'; -import { dbResourceRouter } from './middlewares'; -import bodyParser from 'koa-bodyparser'; +import { dataWrapping, table2resource } from './middlewares'; + +export interface ResourcerOptions { + prefix?: string; +} export interface ApplicationOptions { - database: DatabaseOptions; - resourcer?: any; + database?: DatabaseOptions; + resourcer?: ResourcerOptions; + bodyParser?: any; + cors?: any; + dataWrapping?: boolean; } export class Application extends Koa { - public readonly database: Database; public readonly resourcer: Resourcer; @@ -24,31 +29,37 @@ export class Application extends Koa { constructor(options: ApplicationOptions) { super(); + this.database = new Database(options.database); - this.resourcer = new Resourcer(); + this.resourcer = new Resourcer({ ...options.resourcer }); this.cli = new Command(); - this.use(bodyParser()); - this.use(cors({ - exposeHeaders: ['content-disposition'], - })); + this.use( + bodyParser({ + ...options.bodyParser, + }), + ); - this.resourcer.registerActionHandlers({ ...actions.common, ...actions.associate }); + this.use( + cors({ + exposeHeaders: ['content-disposition'], + ...options.cors, + }), + ); this.use(async (ctx, next) => { ctx.db = this.database; ctx.database = this.database; + ctx.resourcer = this.resourcer; await next(); }); - this.resourcer.use(m.associated); - this.use(m.dataWrapping); + if (options.dataWrapping !== false) { + this.use(dataWrapping); + } - this.use(dbResourceRouter({ - database: this.database, - resourcer: this.resourcer, - ...(options.resourcer || {}), - })); + this.use(table2resource); + this.use(this.resourcer.restApiMiddleware()); this.cli .command('db sync') @@ -65,7 +76,7 @@ export class Application extends Koa { .action(async (...args) => { const cli = args.pop(); await this.emitAsync('db.init'); - await this.database.close(); + await this.destroy(); }); this.cli @@ -110,12 +121,7 @@ export class Application extends Koa { cb = cb.apply(this, args); } - if ( - cb && ( - cb instanceof Promise || - typeof cb.then === 'function' - ) - ) { + if (cb && (cb instanceof Promise || typeof cb.then === 'function')) { return cb; } @@ -128,7 +134,9 @@ export class Application extends Koa { callbacks = callbacks.slice().filter(Boolean); await callbacks.reduce((prev, next) => { return prev.then((res) => { - return run(next).then((result) => Promise.resolve(res.concat(result))); + return run(next).then((result) => + Promise.resolve(res.concat(result)), + ); }); }, Promise.resolve([])); } @@ -153,11 +161,6 @@ export class Application extends Koa { } } - getPluginInstance(key: string) { - const plugin = this.plugins.get(key); - return plugin && plugin.instance; - } - async loadPlugins() { await this.emitAsync('plugins.beforeLoad'); const allPlugins = this.plugins.values(); @@ -171,15 +174,26 @@ export class Application extends Koa { return this.cli.parseAsync(argv); } - protected async loadPlugin({ entry, options = {} }: { entry: string | Function, options: any }) { + async destroy() { + await this.database.close() + } + + protected async loadPlugin({ + entry, + options = {}, + }: { + entry: string | Function; + options: any; + }) { let main: any; if (typeof entry === 'function') { main = entry; } else if (typeof entry === 'string') { - const pathname = `${entry}/${__filename.endsWith('.ts') ? 'src' : 'lib'}/server`; + const pathname = `${entry}/${__filename.endsWith('.ts') ? 'src' : 'lib' + }/server`; main = require(pathname).default; } - return main && await main.call(this, options); + return main && (await main.call(this, options)); } } diff --git a/packages/server/src/middleware.ts b/packages/server/src/middleware.ts deleted file mode 100644 index d088b81f9..000000000 --- a/packages/server/src/middleware.ts +++ /dev/null @@ -1,135 +0,0 @@ -import compose from 'koa-compose'; -import pathToRegexp from 'path-to-regexp'; -import Resourcer, { getNameByParams, KoaMiddlewareOptions, parseRequest, parseQuery, ResourcerContext, ResourceType } from '@nocobase/resourcer'; -import Database, { BELONGSTO, BELONGSTOMANY, HASMANY, HASONE } from '@nocobase/database'; - -interface MiddlewareOptions extends KoaMiddlewareOptions { - resourcer?: Resourcer; - database?: Database; -} -/** - * database + resourcer 结合的中间件(暂时不知道起什么名好) - * - * @param options - */ -export function middleware(options: MiddlewareOptions = {}) { - const { - prefix, - database, - resourcer, - accessors, - paramsKey = 'params', - nameRule = getNameByParams, - } = options; - return async (ctx: ResourcerContext, next: () => Promise) => { - ctx.resourcer = resourcer; - let params = parseRequest({ - path: ctx.request.path, - method: ctx.request.method, - }, { - prefix, - accessors, - }); - if (!params) { - return next(); - } - try { - const resourceName = nameRule(params); - // 如果资源名称未被定义 - if (!resourcer.isDefined(resourceName)) { - const [tableName, fieldName] = resourceName.split('.'); - const Collection = database.getModel('collections'); - // 检查资源对应的表名是否已经定义 - if (!database.isDefined(tableName) && Collection) { - // 未定义则尝试通过 collection 表来加载 - await Collection.load({ - where: { - name: tableName, - }, - }); - } - // 如果经过加载后是已经定义的表 - if (database.isDefined(tableName)) { - const table = database.getTable(tableName); - const field = table.getField(fieldName) as BELONGSTO | HASMANY | BELONGSTOMANY | HASONE; - if (!fieldName || field) { - let resourceType: ResourceType = 'single'; - let actions = {}; - if (field) { - if (field instanceof HASONE) { - resourceType = 'hasOne'; - } else if (field instanceof HASMANY) { - resourceType = 'hasMany'; - } else if (field instanceof BELONGSTO) { - resourceType = 'belongsTo'; - } else if (field instanceof BELONGSTOMANY) { - resourceType = 'belongsToMany'; - } - if (field.options.actions) { - actions = field.options.actions; - } - } else { - const items = table.getOptions('actions') || []; - for (const item of (items as any[])) { - actions[item.name] = item; - } - } - resourcer.define({ - type: resourceType, - name: resourceName, - actions, - }); - } - } - } - const resource = resourcer.getResource(resourceName); - // 为关系资源时,暂时需要再执行一遍 parseRequest - if (resource.options.type !== 'single') { - params = parseRequest({ - path: ctx.request.path, - method: ctx.request.method, - type: resource.options.type, - }, { - prefix, - accessors, - }); - if (!params) { - return next(); - } - } - // console.log(resource); - // action 需要 clone 之后再赋给 ctx - ctx.action = resourcer.getAction(resourceName, params.actionName).clone(); - ctx.action.setContext(ctx); - // 自带 query 处理的不太给力,需要用 qs 转一下 - const query = parseQuery(ctx.request.querystring); - // 兼容 ctx.params 的处理,之后的版本里会去掉 - ctx[paramsKey] = { - table: params.resourceName, - tableKey: params.resourceKey, - relatedTable: params.associatedName, - relatedKey: params.resourceKey, - action: params.actionName, - }; - if (pathToRegexp('/resourcer/{:associatedName.}?:resourceName{\\::actionName}').test(ctx.request.path)) { - await ctx.action.mergeParams({ - ...query, - ...params, - ...ctx.request.body, - }); - } else { - await ctx.action.mergeParams({ - ...query, - ...params, - values: ctx.request.body, - }); - } - return compose(ctx.action.getHandlers())(ctx, next); - } catch (error) { - console.log(error); - return next(); - } - } -} - -export default middleware; diff --git a/packages/server/src/middlewares/data-wrapping.ts b/packages/server/src/middlewares/data-wrapping.ts new file mode 100644 index 000000000..8f6253e9e --- /dev/null +++ b/packages/server/src/middlewares/data-wrapping.ts @@ -0,0 +1,30 @@ +import { Action } from '@nocobase/resourcer'; + +export async function dataWrapping(ctx, next) { + await next(); + if (ctx.withoutDataWrapping) { + return; + } + if (!ctx?.action?.params) { + return; + } + if (ctx.body instanceof Buffer) { + return; + } + if (!ctx.body) { + ctx.body = {}; + } + const { rows, ...meta } = ctx.body; + if (rows) { + ctx.body = { + data: rows, + meta, + }; + } else { + ctx.body = { + data: ctx.body, + }; + } +} + +export default dataWrapping; diff --git a/packages/server/src/middlewares/db-resource-router.ts b/packages/server/src/middlewares/db-resource-router.ts deleted file mode 100644 index 762894076..000000000 --- a/packages/server/src/middlewares/db-resource-router.ts +++ /dev/null @@ -1,133 +0,0 @@ -import compose from 'koa-compose'; -import pathToRegexp from 'path-to-regexp'; -import Resourcer, { getNameByParams, KoaMiddlewareOptions, parseRequest, parseQuery, ResourcerContext, ResourceType } from '@nocobase/resourcer'; -import Database, { BELONGSTO, BELONGSTOMANY, HASMANY, HASONE } from '@nocobase/database'; - -interface MiddlewareOptions extends KoaMiddlewareOptions { - resourcer?: Resourcer; - database?: Database; -} -/** - * database + resourcer 结合的中间件(暂时不知道起什么名好) - * - * @param options - */ -export function dbResourceRouter(options: MiddlewareOptions = {}) { - const { - prefix, - database, - resourcer, - accessors, - paramsKey = 'params', - nameRule = getNameByParams, - } = options; - return async (ctx: ResourcerContext, next: () => Promise) => { - ctx.resourcer = resourcer; - let params = parseRequest({ - path: ctx.request.path, - method: ctx.request.method, - }, { - prefix, - accessors, - }); - if (!params) { - return next(); - } - try { - const resourceName = nameRule(params); - // 如果资源名称未被定义 - if (!resourcer.isDefined(resourceName)) { - const [tableName, fieldName] = resourceName.split('.'); - const Collection = database.getModel('collections'); - // 检查资源对应的表名是否已经定义 - if (!database.isDefined(tableName) && Collection) { - // 未定义则尝试通过 collection 表来加载 - await Collection.load({ - where: { - name: tableName, - }, - }); - } - // 如果经过加载后是已经定义的表 - if (database.isDefined(tableName)) { - const table = database.getTable(tableName); - const field = table.getField(fieldName) as BELONGSTO | HASMANY | BELONGSTOMANY | HASONE; - if (!fieldName || field) { - let resourceType: ResourceType = 'single'; - let actions = {}; - if (field) { - if (field instanceof HASONE) { - resourceType = 'hasOne'; - } else if (field instanceof HASMANY) { - resourceType = 'hasMany'; - } else if (field instanceof BELONGSTO) { - resourceType = 'belongsTo'; - } else if (field instanceof BELONGSTOMANY) { - resourceType = 'belongsToMany'; - } - if (field.options.actions) { - actions = field.options.actions; - } - } else { - const items = table.getOptions('actions') || []; - for (const item of (items as any[])) { - actions[item.name] = item; - } - } - resourcer.define({ - type: resourceType, - name: resourceName, - actions, - }); - } - } - } - const resource = resourcer.getResource(resourceName); - // 为关系资源时,暂时需要再执行一遍 parseRequest - if (resource.options.type !== 'single') { - params = parseRequest({ - path: ctx.request.path, - method: ctx.request.method, - type: resource.options.type, - }, { - prefix, - accessors, - }); - if (!params) { - return next(); - } - } - // console.log(resource); - // action 需要 clone 之后再赋给 ctx - ctx.action = resourcer.getAction(resourceName, params.actionName).clone(); - ctx.action.setContext(ctx); - // 自带 query 处理的不太给力,需要用 qs 转一下 - const query = parseQuery(ctx.request.querystring); - // 兼容 ctx.params 的处理,之后的版本里会去掉 - ctx[paramsKey] = { - table: params.resourceName, - tableKey: params.resourceKey, - relatedTable: params.associatedName, - relatedKey: params.resourceKey, - action: params.actionName, - }; - if (pathToRegexp('/resourcer/{:associatedName.}?:resourceName{\\::actionName}').test(ctx.request.path)) { - await ctx.action.mergeParams({ - ...query, - ...params, - ...ctx.request.body, - }); - } else { - await ctx.action.mergeParams({ - ...query, - ...params, - values: ctx.request.body, - }); - } - return compose(ctx.action.getHandlers())(ctx, next); - } catch (error) { - console.log(error); - return next(); - } - } -} diff --git a/packages/server/src/middlewares/index.ts b/packages/server/src/middlewares/index.ts index 34578155e..499f85c85 100644 --- a/packages/server/src/middlewares/index.ts +++ b/packages/server/src/middlewares/index.ts @@ -1,4 +1,5 @@ export * from './action-params'; export * from './app-dist-serve'; -export * from './db-resource-router'; export * from './demo-blacklisted-actions'; +export * from './table2resource'; +export * from './data-wrapping'; diff --git a/packages/server/src/middlewares/table2resource.ts b/packages/server/src/middlewares/table2resource.ts new file mode 100644 index 000000000..7706b1331 --- /dev/null +++ b/packages/server/src/middlewares/table2resource.ts @@ -0,0 +1,72 @@ +import { + getNameByParams, + parseRequest, + ResourcerContext, + ResourceType, +} from '@nocobase/resourcer'; +import { BELONGSTO, BELONGSTOMANY, HASMANY, HASONE } from '@nocobase/database'; + +export async function table2resource( + ctx: ResourcerContext, + next: () => Promise, +) { + const resourcer = ctx.resourcer; + const database = ctx.database; + let params = parseRequest( + { + path: ctx.request.path, + method: ctx.request.method, + }, + { + prefix: resourcer.options.prefix, + accessors: resourcer.options.accessors, + }, + ); + if (!params) { + return next(); + } + const resourceName = getNameByParams(params); + // 如果资源名称未被定义 + if (resourcer.isDefined(resourceName)) { + return next(); + } + const [tableName, fieldName] = resourceName.split('.'); + // 如果经过加载后是已经定义的表 + if (!database.isDefined(tableName)) { + return next(); + } + const table = database.getTable(tableName); + const field = table.getField(fieldName) as + | BELONGSTO + | HASMANY + | BELONGSTOMANY + | HASONE; + if (!fieldName || field) { + let resourceType: ResourceType = 'single'; + let actions = {}; + if (field) { + if (field instanceof HASONE) { + resourceType = 'hasOne'; + } else if (field instanceof HASMANY) { + resourceType = 'hasMany'; + } else if (field instanceof BELONGSTO) { + resourceType = 'belongsTo'; + } else if (field instanceof BELONGSTOMANY) { + resourceType = 'belongsToMany'; + } + if (field.options.actions) { + actions = field.options.actions || {}; + } + } else { + actions = table.getOptions('actions') || {}; + } + resourcer.define({ + type: resourceType, + name: resourceName, + actions, + }); + } + return next(); +} + +export default table2resource;