From 17362a844439e5510f254195fa135b6335866ef3 Mon Sep 17 00:00:00 2001 From: chenos Date: Wed, 7 Apr 2021 17:10:52 +0800 Subject: [PATCH] refactor: app middlewares --- .env.example | 52 ++++--- nodemon.json | 4 +- package.json | 3 +- packages/api/package.json | 5 +- packages/api/src/app.ts | 4 +- packages/api/src/dev.ts | 8 -- packages/api/src/index.ts | 82 +++-------- packages/api/src/migrate.ts | 1 - packages/api/src/migrations/init.ts | 60 +++----- packages/app/nodemon.json | 6 - packages/app/package.json | 3 +- .../plugin-file-manager/src/storages/local.ts | 2 +- packages/server/package.json | 1 + packages/server/src/index.ts | 11 +- .../server/src/middlewares/action-params.ts | 31 ++++ .../server/src/middlewares/app-dist-serve.ts | 25 ++++ .../src/middlewares/db-resource-router.ts | 133 ++++++++++++++++++ .../middlewares/demo-blacklisted-actions.ts | 27 ++++ packages/server/src/middlewares/index.ts | 4 + 19 files changed, 310 insertions(+), 152 deletions(-) delete mode 100644 packages/api/src/dev.ts delete mode 100644 packages/app/nodemon.json create mode 100644 packages/server/src/middlewares/action-params.ts create mode 100644 packages/server/src/middlewares/app-dist-serve.ts create mode 100644 packages/server/src/middlewares/db-resource-router.ts create mode 100644 packages/server/src/middlewares/demo-blacklisted-actions.ts create mode 100644 packages/server/src/middlewares/index.ts diff --git a/.env.example b/.env.example index 92670215e..3479d5cc4 100644 --- a/.env.example +++ b/.env.example @@ -1,29 +1,47 @@ +########## DOCKER COMPOSE ENV ########## + +DB_MYSQL_PORT=3306 +DB_POSTGRES_PORT=5432 +API_PORT=23000 +VERDACCIO_PORT=4873 + +########## NOCOBASE ENV ########## + +# DATABASE + DB_DIALECT=postgres DB_HOST=localhost DB_PORT=5432 DB_DATABASE=nocobase -DB_USER=test -DB_PASSWORD=test - +DB_USER=nocobase +DB_PASSWORD=nocobase # set to 'on' to enable log DB_LOG_SQL= -# DB_DIALECT=mysql -# DB_PORT=3306 - -DB_MYSQL_PORT=3306 -DB_POSTGRES_PORT=5432 +# API & APP +NOCOBASE_ENV= API_PORT=23000 +APP_USE_STATIC_SERVER=true +APP_DIST=packages/app/dist -VERDACCIO_PORT=4873 +# ADMIN USER (Initialization only) -APP_DIST= -USE_STATIC_SERVER=true -LOCAL_STORAGE_BASE_URL=http://localhost:23000/uploads -STORAGE_BASE_URL=http://localhost:23000/uploads +ADMIN_EMAIL=admin@nocobase.com +ADMIN_PASSWORD=admin -ALIYUN_OSS_REGION=oss-cn-beijing -ALIYUN_OSS_ACCESS_KEY_ID= -ALIYUN_OSS_ACCESS_KEY_SECRET= -ALIYUN_OSS_BUCKET= +# STORAGE (Initialization only) + +# local or ali-oss +STORAGE_TYPE=local + +# LOCAL STORAGE +LOCAL_STORAGE_USE_STATIC_SERVER=true +LOCAL_STORAGE_BASE_URL=http://localhost:23000 + +# ALI OSS STORAGE +ALI_OSS_STORAGE_BASE_URL= +ALI_OSS_REGION=oss-cn-beijing +ALI_OSS_ACCESS_KEY_ID= +ALI_OSS_ACCESS_KEY_SECRET= +ALI_OSS_BUCKET= diff --git a/nodemon.json b/nodemon.json index a03552072..51695bfc2 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,6 +1,6 @@ { - "watch": ["packages/"], + "watch": ["packages/", ".env"], "ignore": ["packages/app"], "ext": "ts", - "exec": "ts-node ./packages/api/src/dev.ts" + "exec": "ts-node -r dotenv/config ./packages/api/src/index.ts" } \ No newline at end of file diff --git a/package.json b/package.json index 8f4a83230..16bdf90f8 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,7 @@ "bootstrap": "lerna bootstrap --no-ci", "build": "father-build", "clean": "lerna clean", - "db-migrate": "cd packages/api && npm run db-migrate", - "db:start": "docker-compose up -d", + "db-migrate": "ts-node -r dotenv/config ./packages/api/src/migrate.ts", "lint": "eslint --ext .ts,.tsx,.js \"packages/*/src/**.@(ts|tsx|js)\" --fix", "test": "npm run lint && jest", "debug": "node --inspect-brk node_modules/.bin/jest --runInBand", diff --git a/packages/api/package.json b/packages/api/package.json index 59cb1de6a..9b74643cc 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -4,10 +4,7 @@ "main": "./lib/index.js", "types": "./lib/index.d.ts", "license": "MIT", - "scripts": { - "start": "nodemon", - "db-migrate": "ts-node ./src/migrate.ts" - }, + "scripts": {}, "dependencies": { "@nocobase/actions": "^0.4.0-alpha.1", "@nocobase/client": "^0.4.0-alpha.1", diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts index cd2e92bf8..e56d5f4d6 100644 --- a/packages/api/src/app.ts +++ b/packages/api/src/app.ts @@ -2,9 +2,9 @@ import Api from '@nocobase/server'; // @ts-ignore const sync = global.sync || { - force: true, + force: false, alter: { - drop: true, + drop: false, }, }; diff --git a/packages/api/src/dev.ts b/packages/api/src/dev.ts deleted file mode 100644 index 668a4b9f9..000000000 --- a/packages/api/src/dev.ts +++ /dev/null @@ -1,8 +0,0 @@ -import path from 'path'; -import dotenv from 'dotenv'; - -dotenv.config({ - path: path.resolve(__dirname, '../../../.env'), -}); - -import './index'; diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 8523a6407..f62c11272 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,61 +1,16 @@ import api from './app'; -import actions from '@nocobase/actions'; -import send from 'koa-send'; -import serve from 'koa-static'; - -api.resourcer.use(async (ctx: actions.Context, next) => { - const { actionName, resourceField, resourceName, fields = {} } = ctx.action.params; - const table = ctx.db.getTable(resourceField ? resourceField.options.target : resourceName); - // ctx.state.developerMode = {[Op.not]: null}; - ctx.state.developerMode = false; - if (table && table.hasField('developerMode') && ctx.state.developerMode === false) { - ctx.action.mergeParams({ filter: { "developerMode.$isFalsy": true } }, { filter: 'and' }); - } - if (table && ['get', 'list'].includes(actionName)) { - const except = []; - const appends = []; - for (const [name, field] of table.getFields()) { - if (field.options.hidden) { - except.push(field.options.name); - } - // if (field.options.appends) { - // appends.push(field.options.name); - // } - } - ctx.action.mergeParams({ fields: { - except, - appends - } }, { fields: 'append' }); - // console.log('ctx.action.params.fields', ctx.action.params.fields, except, appends); - } - await next(); -}); +import { middlewares } from '@nocobase/server'; (async () => { + api.resourcer.use(middlewares.actionParams()); + await api.loadPlugins(); - api.database.getModel('users').addHook('beforeUpdate', function (model) { - console.log('users.beforeUpdate', model.get(), model.changed('password' as any)); - }); - - api.resourcer.use(async (ctx, next) => { - if (process.env.NOCOBASE_ENV !== 'demo') { - return next(); - } - const currentUser = ctx.state.currentUser; - if (currentUser && currentUser.username === 'admin') { - return next(); - } - const { actionName } = ctx.action.params; - if (['create', 'update', 'destroy', 'sort', 'upload'].includes(actionName)) { - ctx.body = { - data: {}, - meta: {}, - }; - return; - } - await next(); - }); + if (process.env.NOCOBASE_ENV === 'demo') { + api.resourcer.use(middlewares.demoBlacklistedActions({ + emails: [process.env.ADMIN_EMAIL], + })); + } await api.database.getModel('collections').load({skipExisting: true}); await api.database.getModel('collections').load({where: { @@ -63,17 +18,14 @@ api.resourcer.use(async (ctx: actions.Context, next) => { }}); await api.database.getModel('automations').load(); - api.use(async (ctx, next) => { - if (!process.env.APP_DIST || !process.env.USE_STATIC_SERVER || process.env.USE_STATIC_SERVER === 'false') { - return next(); - } - await serve(process.env.APP_DIST)(ctx, next); - if (ctx.status == 404) { - return send(ctx, 'index.html', { root: process.env.APP_DIST }); - } - }); + api.use(middlewares.appDistServe({ + root: process.env.APP_DIST, + useStaticServer: !(process.env.APP_USE_STATIC_SERVER === 'false' || !process.env.APP_USE_STATIC_SERVER), + })); - api.listen(process.env.API_PORT, () => { - console.log(`http://localhost:${process.env.API_PORT}/`); - }); + if (require.main === module) { + api.listen(process.env.API_PORT, () => { + console.log(`http://localhost:${process.env.API_PORT}/`); + }); + } })(); diff --git a/packages/api/src/migrate.ts b/packages/api/src/migrate.ts index 5711e6c0d..52371acae 100644 --- a/packages/api/src/migrate.ts +++ b/packages/api/src/migrate.ts @@ -1,4 +1,3 @@ - // @ts-ignore const keys = process.argv; diff --git a/packages/api/src/migrations/init.ts b/packages/api/src/migrations/init.ts index d930cb02c..c2250fa36 100644 --- a/packages/api/src/migrations/init.ts +++ b/packages/api/src/migrations/init.ts @@ -76,47 +76,31 @@ const data = [ await Collection.import(table.getOptions(), { update: true, migrate: false }); } await Page.import(data); - let user = await User.findOne({ - where: { - username: "admin", - }, + + const user = await User.create({ + email: process.env.ADMIN_EMAIL, + password: process.env.ADMIN_PASSWORD, }); - if (!user) { - user = await User.create({ - nickname: "超级管理员", - password: "admin", - username: "admin", - email: 'dev@nocobase.com', - token: "38979f07e1fca68fb3d2", - }); - } const Storage = database.getModel('storages'); - // await Storage.create({ - // title: '本地存储', - // name: `local`, - // type: 'local', - // baseUrl: process.env.LOCAL_STORAGE_BASE_URL, - // default: true - // }); - const storage = await Storage.findOne({ - where: { - name: "ali-oss", - }, + await Storage.create({ + title: '本地存储', + name: `local`, + type: 'local', + baseUrl: process.env.LOCAL_STORAGE_BASE_URL, + default: process.env.STORAGE_TYPE === 'local', + }); + await Storage.create({ + name: `ali-oss`, + type: 'ali-oss', + baseUrl: process.env.ALI_OSS_STORAGE_BASE_URL, + options: { + region: process.env.ALI_OSS_REGION, + accessKeyId: process.env.ALI_OSS_ACCESS_KEY_ID, + accessKeySecret: process.env.ALI_OSS_ACCESS_KEY_SECRET, + bucket: process.env.ALI_OSS_BUCKET, + }, + default: process.env.STORAGE_TYPE === 'ali-oss', }); - if (!storage) { - await Storage.create({ - name: `ali-oss`, - type: 'ali-oss', - baseUrl: process.env.ALIYUN_STORAGE_BASE_URL, - options: { - region: process.env.ALIYUN_OSS_REGION, - accessKeyId: process.env.ALIYUN_OSS_ACCESS_KEY_ID, - accessKeySecret: process.env.ALIYUN_OSS_ACCESS_KEY_SECRET, - bucket: process.env.ALIYUN_OSS_BUCKET, - }, - default: true - }); - } const Role = database.getModel('roles'); if (Role) { const roles = await Role.bulkCreate([ diff --git a/packages/app/nodemon.json b/packages/app/nodemon.json deleted file mode 100644 index 379d42969..000000000 --- a/packages/app/nodemon.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "watch": ["../"], - "ignore": ["../app"], - "ext": "ts", - "exec": "ts-node ../api/src/dev.ts" -} \ No newline at end of file diff --git a/packages/app/package.json b/packages/app/package.json index b19a06498..bbdf2706e 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -2,7 +2,8 @@ "name": "@nocobase/app", "version": "0.4.0-alpha.1", "scripts": { - "start": "concurrently \"nodemon\" \"umi dev\"", + "start": "concurrently \"cd ../../ && nodemon\" \"umi dev\"", + "dev": "umi dev", "build": "umi build", "postinstall": "umi generate tmp", "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", diff --git a/packages/plugin-file-manager/src/storages/local.ts b/packages/plugin-file-manager/src/storages/local.ts index 384d9706f..ada0b85de 100644 --- a/packages/plugin-file-manager/src/storages/local.ts +++ b/packages/plugin-file-manager/src/storages/local.ts @@ -54,7 +54,7 @@ export function middleware(app) { // 否则都忽略,交给其他 server 来提供(如 nginx/cdn 等) // TODO(bug): https、端口 80 默认值和其他本地 ip/hostname 的情况未考虑 // TODO 实际应该用 NOCOBASE_ENV 来判断,或者抛给 env 处理 - if (process.env.USE_STATIC_SERVER) { + if (process.env.LOCAL_STORAGE_USE_STATIC_SERVER) { const basePath = url.pathname.startsWith('/') ? url.pathname : `/${url.pathname}`; app.use(mount(basePath, serve(getDocumentRoot(storage)))); } diff --git a/packages/server/package.json b/packages/server/package.json index 4ca507c20..29740894c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -14,6 +14,7 @@ "dotenv": "^8.2.0", "koa": "^2.13.0", "koa-bodyparser": "^4.3.0", + "koa-static": "^5.0.0", "mockjs": "^1.1.0", "mysql2": "^2.1.0", "pg": "^8.3.3", diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 0f88c23bd..74b6dc460 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,11 +1,12 @@ -import { actions, middlewares } from '@nocobase/actions'; +import { actions, middlewares as m } from '@nocobase/actions'; import Application from './application'; import bodyParser from 'koa-bodyparser'; import cors from '@koa/cors'; -import middleware from './middleware'; +import { dbResourceRouter } from './middlewares'; export * from './application'; export * from './middleware'; +export * as middlewares from './middlewares'; export default { /** @@ -29,10 +30,10 @@ export default { await next(); }); - app.resourcer.use(middlewares.associated); - app.use(middlewares.dataWrapping); + app.resourcer.use(m.associated); + app.use(m.dataWrapping); - app.use(middleware({ + app.use(dbResourceRouter({ database: app.database, resourcer: app.resourcer, ...(options.resourcer||{}), diff --git a/packages/server/src/middlewares/action-params.ts b/packages/server/src/middlewares/action-params.ts new file mode 100644 index 000000000..2d780af9c --- /dev/null +++ b/packages/server/src/middlewares/action-params.ts @@ -0,0 +1,31 @@ +import { actions } from '@nocobase/actions'; + +export function actionParams(options: any = {}) { + return async (ctx: actions.Context, next: actions.Next) => { + const { actionName, resourceField, resourceName, fields = {} } = ctx.action.params; + const table = ctx.db.getTable(resourceField ? resourceField.options.target : resourceName); + // ctx.state.developerMode = {[Op.not]: null}; + ctx.state.developerMode = false; + if (table && table.hasField('developerMode') && ctx.state.developerMode === false) { + ctx.action.mergeParams({ filter: { "developerMode.$isFalsy": true } }, { filter: 'and' }); + } + if (table && ['get', 'list'].includes(actionName)) { + const except = []; + const appends = []; + for (const [name, field] of table.getFields()) { + if (field.options.hidden) { + except.push(field.options.name); + } + // if (field.options.appends) { + // appends.push(field.options.name); + // } + } + ctx.action.mergeParams({ fields: { + except, + appends + } }, { fields: 'append' }); + // console.log('ctx.action.params.fields', ctx.action.params.fields, except, appends); + } + await next(); + } +} diff --git a/packages/server/src/middlewares/app-dist-serve.ts b/packages/server/src/middlewares/app-dist-serve.ts new file mode 100644 index 000000000..da8db08ff --- /dev/null +++ b/packages/server/src/middlewares/app-dist-serve.ts @@ -0,0 +1,25 @@ +import path from 'path'; +import send from 'koa-send'; +import serve from 'koa-static'; +import actions from '@nocobase/actions'; + +export interface AppDistServeOptions { + root?: string; + useStaticServer?: boolean; +} + +export function appDistServe(options: AppDistServeOptions = {}) { + return async (ctx: actions.Context, next: actions.Next) => { + if (!options.root || !options.useStaticServer) { + return next(); + } + let root = options.root; + if (!root.startsWith('/')) { + root = path.resolve(process.cwd(), root); + } + await serve(root)(ctx, next); + if (ctx.status == 404) { + return send(ctx, 'index.html', { root }); + } + } +} diff --git a/packages/server/src/middlewares/db-resource-router.ts b/packages/server/src/middlewares/db-resource-router.ts new file mode 100644 index 000000000..1256998f4 --- /dev/null +++ b/packages/server/src/middlewares/db-resource-router.ts @@ -0,0 +1,133 @@ +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/demo-blacklisted-actions.ts b/packages/server/src/middlewares/demo-blacklisted-actions.ts new file mode 100644 index 000000000..253b21bfc --- /dev/null +++ b/packages/server/src/middlewares/demo-blacklisted-actions.ts @@ -0,0 +1,27 @@ +import { actions } from '@nocobase/actions'; + +export interface DemoBlackListedActionsOptions { + emails?: string[]; + blacklist?: string[]; +} + +const defaultBlacklist = ['create', 'update', 'destroy', 'sort', 'upload']; + +export function demoBlacklistedActions(options: DemoBlackListedActionsOptions = {}) { + const { emails, blacklist = defaultBlacklist } = options; + return async (ctx: actions.Context, next: actions.Next) => { + const currentUser = ctx.state.currentUser; + if (currentUser && emails.includes(currentUser.email)) { + return next(); + } + const { actionName } = ctx.action.params; + if (blacklist.includes(actionName)) { + ctx.body = { + data: {}, + meta: {}, + }; + return; + } + await next(); + } +} diff --git a/packages/server/src/middlewares/index.ts b/packages/server/src/middlewares/index.ts new file mode 100644 index 000000000..34578155e --- /dev/null +++ b/packages/server/src/middlewares/index.ts @@ -0,0 +1,4 @@ +export * from './action-params'; +export * from './app-dist-serve'; +export * from './db-resource-router'; +export * from './demo-blacklisted-actions';