refactor: app middlewares

This commit is contained in:
chenos 2021-04-07 17:10:52 +08:00
parent 6906ccadb0
commit 17362a8444
19 changed files with 310 additions and 152 deletions

View File

@ -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=

View File

@ -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"
}

View File

@ -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",

View File

@ -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",

View File

@ -2,9 +2,9 @@ import Api from '@nocobase/server';
// @ts-ignore
const sync = global.sync || {
force: true,
force: false,
alter: {
drop: true,
drop: false,
},
};

View File

@ -1,8 +0,0 @@
import path from 'path';
import dotenv from 'dotenv';
dotenv.config({
path: path.resolve(__dirname, '../../../.env'),
});
import './index';

View File

@ -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}/`);
});
}
})();

View File

@ -1,4 +1,3 @@
// @ts-ignore
const keys = process.argv;

View File

@ -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([

View File

@ -1,6 +0,0 @@
{
"watch": ["../"],
"ignore": ["../app"],
"ext": "ts",
"exec": "ts-node ../api/src/dev.ts"
}

View File

@ -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}'",

View File

@ -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))));
}

View File

@ -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",

View File

@ -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||{}),

View File

@ -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();
}
}

View File

@ -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 });
}
}
}

View File

@ -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<any>) => {
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();
}
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,4 @@
export * from './action-params';
export * from './app-dist-serve';
export * from './db-resource-router';
export * from './demo-blacklisted-actions';