diff --git a/package.json b/package.json
index 4af0ac464b..6a6031f199 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"db:start": "docker-compose up -d",
"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",
"release": "npm run build && lerna publish --yes --registry https://npm.pkg.github.com"
},
"devDependencies": {
diff --git a/packages/actions/src/__tests__/actions/create1.ts b/packages/actions/src/__tests__/actions/create1.ts
index e5b132cef4..2ed6af4e51 100644
--- a/packages/actions/src/__tests__/actions/create1.ts
+++ b/packages/actions/src/__tests__/actions/create1.ts
@@ -2,7 +2,7 @@ import { ActionOptions } from '@nocobase/resourcer';
import { create } from '../../actions/common';
export default {
- defaultValues: {
+ values: {
meta: {
location: 'Kunming'
}
@@ -13,4 +13,4 @@ export default {
},
handler: create
-} as ActionOptions;
+} as unknown as ActionOptions;
diff --git a/packages/actions/src/__tests__/actions/create2.ts b/packages/actions/src/__tests__/actions/create2.ts
index ba2a113522..43a40300b6 100644
--- a/packages/actions/src/__tests__/actions/create2.ts
+++ b/packages/actions/src/__tests__/actions/create2.ts
@@ -7,4 +7,4 @@ export default {
},
handler: create
-} as ActionOptions;
+} as unknown as ActionOptions;
diff --git a/packages/actions/src/__tests__/actions/list1.ts b/packages/actions/src/__tests__/actions/list1.ts
index a38e75d9df..a5d283b286 100644
--- a/packages/actions/src/__tests__/actions/list1.ts
+++ b/packages/actions/src/__tests__/actions/list1.ts
@@ -14,4 +14,4 @@ export default {
},
handler: list
-} as ActionOptions;
+} as unknown as ActionOptions;
diff --git a/packages/actions/src/__tests__/actions/update1.ts b/packages/actions/src/__tests__/actions/update1.ts
index 5c3d1ac898..449978ee15 100644
--- a/packages/actions/src/__tests__/actions/update1.ts
+++ b/packages/actions/src/__tests__/actions/update1.ts
@@ -2,7 +2,7 @@ import { ActionOptions } from '@nocobase/resourcer';
import { update } from '../../actions/common';
export default {
- defaultValues: {
+ values: {
meta: {
location: 'Kunming'
}
@@ -13,4 +13,4 @@ export default {
},
handler: update
-} as ActionOptions;
+} as unknown as ActionOptions;
diff --git a/packages/actions/src/__tests__/actions/update2.ts b/packages/actions/src/__tests__/actions/update2.ts
index 7c499408bc..6f3d708cca 100644
--- a/packages/actions/src/__tests__/actions/update2.ts
+++ b/packages/actions/src/__tests__/actions/update2.ts
@@ -7,4 +7,4 @@ export default {
},
handler: update
-} as ActionOptions;
+} as unknown as ActionOptions;
diff --git a/packages/actions/src/__tests__/index.ts b/packages/actions/src/__tests__/index.ts
index 06422e4ea1..3b02a37033 100644
--- a/packages/actions/src/__tests__/index.ts
+++ b/packages/actions/src/__tests__/index.ts
@@ -60,7 +60,7 @@ const tableFiles = glob.sync(`${resolve(__dirname, './tables')}/*.ts`);
// resourcer 在内存中是单例,需要谨慎使用
export const resourcer = new Resourcer();
resourcer.use(associated);
-resourcer.registerActionHandlers({...actions.associate, ...actions.common });
+resourcer.registerActionHandlers({ ...actions.associate, ...actions.common });
resourcer.define({
name: 'posts',
actions: {
diff --git a/packages/actions/src/middlewares/associated.ts b/packages/actions/src/middlewares/associated.ts
index e081fcc305..c14896da66 100644
--- a/packages/actions/src/middlewares/associated.ts
+++ b/packages/actions/src/middlewares/associated.ts
@@ -41,8 +41,7 @@ export async function associated(ctx: Context, next: Next) {
}
});
if (model) {
- ctx.action.setParam('associated', model);
- ctx.action.setParam('resourceField', field);
+ ctx.action.mergeParams({ associated: model, resourceField: field });
}
}
diff --git a/packages/app/src/api/app.ts b/packages/app/src/api/app.ts
index 4490b842d6..07baacd0d2 100644
--- a/packages/app/src/api/app.ts
+++ b/packages/app/src/api/app.ts
@@ -3,12 +3,12 @@ import dotenv from 'dotenv';
import path from 'path';
import actions from '../../../actions/src';
import associated from '../../../actions/src/middlewares/associated';
-import { Op } from 'sequelize';
-const sync = {
- force: false,
+// @ts-ignore
+const sync = global.sync || {
+ force: true,
alter: {
- drop: false,
+ drop: true,
},
};
@@ -40,60 +40,31 @@ const api = Api.create({
api.resourcer.use(associated);
api.resourcer.registerActionHandlers({...actions.common, ...actions.associate});
-api.resourcer.use(async (ctx: actions.Context, next) => {
- const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, '');
- // console.log('user check', ctx.action.params.actionName);
- // const { actionName } = ctx.action.params;
- if (!token) {
- return next();
- }
- const User = ctx.db.getModel('users');
- const user = await User.findOne({
- where: {
- token,
- },
- });
- if (!user) {
- return next();
- }
- ctx.state.currentUser = user;
- // console.log('ctx.state.currentUser', ctx.state.currentUser);
- await next();
-});
-
-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.setParam('filter.developerMode', ctx.state.developerMode);
- }
- if (table && ['get', 'list'].includes(actionName)) {
- const except = fields.except || [];
- const appends = fields.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);
- }
- }
- if (except.length) {
- ctx.action.setParam('fields.except', except);
- }
- if (appends.length) {
- ctx.action.setParam('fields.appends', appends);
- }
- console.log('ctx.action.params.fields', ctx.action.params.fields, except, appends);
- }
- await next();
-});
+// api.resourcer.use(async (ctx: actions.Context, next) => {
+// const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, '');
+// // console.log('user check', ctx.action.params.actionName);
+// // const { actionName } = ctx.action.params;
+// if (!token) {
+// return next();
+// }
+// const User = ctx.db.getModel('users');
+// const user = await User.findOne({
+// where: {
+// token,
+// },
+// });
+// if (!user) {
+// return next();
+// }
+// ctx.state.currentUser = user;
+// // console.log('ctx.state.currentUser', ctx.state.currentUser);
+// await next();
+// });
api.registerPlugin('plugin-collections', [path.resolve(__dirname, '../../../plugin-collections'), {}]);
api.registerPlugin('plugin-pages', [path.resolve(__dirname, '../../../plugin-pages'), {}]);
api.registerPlugin('plugin-users', [path.resolve(__dirname, '../../../plugin-users'), {}]);
api.registerPlugin('plugin-file-manager', [path.resolve(__dirname, '../../../plugin-file-manager'), {}]);
+api.registerPlugin('plugin-permissions', [path.resolve(__dirname, '../../../plugin-permissions'), {}]);
export default api;
diff --git a/packages/app/src/api/index.ts b/packages/app/src/api/index.ts
index c7d26d5603..abbb89f2e8 100644
--- a/packages/app/src/api/index.ts
+++ b/packages/app/src/api/index.ts
@@ -1,65 +1,5 @@
-import Api from '../../../server/src';
-import dotenv from 'dotenv';
-import path from 'path';
+import api from './app';
import actions from '../../../actions/src';
-import associated from '../../../actions/src/middlewares/associated';
-import { Op } from 'sequelize';
-
-const sync = {
- force: false,
- alter: {
- drop: false,
- },
-};
-
-console.log('process.env.NOCOBASE_ENV', process.env.NOCOBASE_ENV);
-
-dotenv.config();
-
-const api = Api.create({
- 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,
- dialect: process.env.DB_DIALECT,
- dialectOptions: {
- charset: 'utf8mb4',
- collate: 'utf8mb4_unicode_ci',
- },
- logging: false,
- define: {},
- sync,
- },
- resourcer: {
- prefix: '/api',
- },
-});
-
-api.resourcer.use(associated);
-api.resourcer.registerActionHandlers({...actions.common, ...actions.associate});
-
-api.resourcer.use(async (ctx: actions.Context, next) => {
- const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, '');
- // console.log('user check', ctx.action.params.actionName);
- // const { actionName } = ctx.action.params;
- if (!token) {
- return next();
- }
- const User = ctx.db.getModel('users');
- const user = await User.findOne({
- where: {
- token,
- },
- });
- if (!user) {
- return next();
- }
- ctx.state.currentUser = user;
- // console.log('ctx.state.currentUser', ctx.state.currentUser);
- await next();
-});
api.resourcer.use(async (ctx: actions.Context, next) => {
const { actionName, resourceField, resourceName, fields = {} } = ctx.action.params;
@@ -67,11 +7,11 @@ api.resourcer.use(async (ctx: actions.Context, next) => {
// ctx.state.developerMode = {[Op.not]: null};
ctx.state.developerMode = false;
if (table && table.hasField('developerMode') && ctx.state.developerMode === false) {
- ctx.action.setParam('filter.developerMode.$isFalsy', true);
+ ctx.action.mergeParams({ filter: { developerMode: ctx.state.developerMode } }, { filter: 'and' });
}
if (table && ['get', 'list'].includes(actionName)) {
- const except = fields.except || [];
- // const appends = fields.appends || [];
+ const except = [];
+ const appends = [];
for (const [name, field] of table.getFields()) {
if (field.options.hidden) {
except.push(field.options.name);
@@ -80,12 +20,10 @@ api.resourcer.use(async (ctx: actions.Context, next) => {
// appends.push(field.options.name);
// }
}
- if (except.length) {
- ctx.action.setParam('fields.except', except);
- }
- // if (appends.length) {
- // ctx.action.setParam('fields.appends', appends);
- // }
+ ctx.action.mergeParams({ fields: {
+ except,
+ appends
+ } }, { fields: 'append' });
// console.log('ctx.action.params.fields', ctx.action.params.fields, except, appends);
}
await next();
@@ -112,11 +50,6 @@ api.resourcer.use(async (ctx: actions.Context, next) => {
// await next();
// });
-api.registerPlugin('plugin-collections', [path.resolve(__dirname, '../../../plugin-collections'), {}]);
-api.registerPlugin('plugin-pages', [path.resolve(__dirname, '../../../plugin-pages'), {}]);
-api.registerPlugin('plugin-users', [path.resolve(__dirname, '../../../plugin-users'), {}]);
-api.registerPlugin('plugin-file-manager', [path.resolve(__dirname, '../../../plugin-file-manager'), {}]);
-
(async () => {
await api.loadPlugins();
diff --git a/packages/app/src/api/migrate.ts b/packages/app/src/api/migrate.ts
index 5dedebadab..19a55a998f 100644
--- a/packages/app/src/api/migrate.ts
+++ b/packages/app/src/api/migrate.ts
@@ -1,5 +1,14 @@
const args = process.argv;
+
+// @ts-ignore
+global.sync = {
+ force: true,
+ alter: {
+ drop: true,
+ },
+};
+
const fileName: string = args.pop();
require(`./migrations/${fileName}`);
diff --git a/packages/app/src/api/collections/authors.ts b/packages/app/src/api/migrations/collections/authors.ts
similarity index 100%
rename from packages/app/src/api/collections/authors.ts
rename to packages/app/src/api/migrations/collections/authors.ts
diff --git a/packages/app/src/api/collections/books.ts b/packages/app/src/api/migrations/collections/books.ts
similarity index 100%
rename from packages/app/src/api/collections/books.ts
rename to packages/app/src/api/migrations/collections/books.ts
diff --git a/packages/app/src/api/collections/example.ts b/packages/app/src/api/migrations/collections/example.ts
similarity index 100%
rename from packages/app/src/api/collections/example.ts
rename to packages/app/src/api/migrations/collections/example.ts
diff --git a/packages/app/src/api/migrations/init.ts b/packages/app/src/api/migrations/init.ts
index b6592c382d..ffd087ee10 100644
--- a/packages/app/src/api/migrations/init.ts
+++ b/packages/app/src/api/migrations/init.ts
@@ -1,46 +1,5 @@
-import Api from '../../../../server/src';
-import dotenv from 'dotenv';
-import path from 'path';
-import Database, { Model } from '@nocobase/database';
-import actions from '../../../../actions/src';
-import associated from '../../../../actions/src/middlewares/associated';
-
-console.log(process.argv);
-
-const clean = true;
-
-const sync = {
- force: clean,
- alter: {
- drop: clean,
- },
-};
-
-dotenv.config();
-
-const api = Api.create({
- 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,
- dialect: process.env.DB_DIALECT,
- dialectOptions: {
- charset: 'utf8mb4',
- collate: 'utf8mb4_unicode_ci',
- },
- logging: false,
- define: {},
- sync,
- },
- resourcer: {
- prefix: '/api',
- },
-});
-
-api.resourcer.use(associated);
-api.resourcer.registerActionHandlers({...actions.common, ...actions.associate});
+import api from '../app';
+import Database from '@nocobase/database';
const data = [
{
@@ -135,6 +94,15 @@ const data = [
sort: 110,
showInMenu: true,
},
+ {
+ title: '权限组配置',
+ type: 'collection',
+ collection: 'roles',
+ path: '/settings/roles',
+ icon: 'TableOutlined',
+ sort: 120,
+ showInMenu: true,
+ },
]
},
],
@@ -157,11 +125,6 @@ const data = [
}
];
-api.registerPlugin('plugin-collections', [path.resolve(__dirname, '../../../plugin-collections'), {}]);
-api.registerPlugin('plugin-pages', [path.resolve(__dirname, '../../../plugin-pages'), {}]);
-api.registerPlugin('plugin-users', [path.resolve(__dirname, '../../../plugin-users'), {}]);
-api.registerPlugin('plugin-file-manager', [path.resolve(__dirname, '../../../plugin-file-manager'), {}]);
-
(async () => {
await api.loadPlugins();
const database: Database = api.database;
@@ -171,6 +134,7 @@ api.registerPlugin('plugin-file-manager', [path.resolve(__dirname, '../../../plu
const [Collection, Page, User] = database.getModels(['collections', 'pages', 'users']);
const tables = database.getTables([]);
for (let table of tables) {
+ console.log(table.getName());
await Collection.import(table.getOptions(), { update: true, migrate: false });
}
await Page.import(data);
@@ -195,18 +159,32 @@ api.registerPlugin('plugin-file-manager', [path.resolve(__dirname, '../../../plu
// baseUrl: process.env.LOCAL_STORAGE_BASE_URL,
// default: true
// });
- await Storage.create({
- name: `ali-oss`,
- type: 'ali-oss',
- baseUrl: process.env.STORAGE_BASE_URL,
- options: {
- region: process.env.ALIYUN_OSS_REGION,// 'oss-cn-beijing',
- accessKeyId: process.env.ALIYUN_OSS_ACCESS_KEY_ID,// 'LTAI4GEGDJsdGantisvSaz47',
- accessKeySecret: process.env.ALIYUN_OSS_ACCESS_KEY_SECRET,//'cDFaOUwigps7ohRmsfBFXGDxNm8uIq',
- bucket: process.env.ALIYUN_OSS_BUCKET, //'nocobase'
+ const storage = await Storage.findOne({
+ where: {
+ name: "ali-oss",
},
- default: true
});
+ if (!storage) {
+ await Storage.create({
+ name: `ali-oss`,
+ type: 'ali-oss',
+ baseUrl: process.env.STORAGE_BASE_URL,
+ options: {
+ region: process.env.ALIYUN_OSS_REGION,// 'oss-cn-beijing',
+ accessKeyId: process.env.ALIYUN_OSS_ACCESS_KEY_ID,// 'LTAI4GEGDJsdGantisvSaz47',
+ accessKeySecret: process.env.ALIYUN_OSS_ACCESS_KEY_SECRET,//'cDFaOUwigps7ohRmsfBFXGDxNm8uIq',
+ bucket: process.env.ALIYUN_OSS_BUCKET, //'nocobase'
+ },
+ default: true
+ });
+ }
+ const Role = database.getModel('roles');
+ const roles = await Role.bulkCreate([
+ { title: '系统开发组' },
+ { title: '数据管理组' },
+ { title: '普通用户组' },
+ { title: '未登录用户组' },
+ ]);
await database.getModel('collections').import(require('./collections/example').default);
await database.getModel('collections').import(require('./collections/authors').default);
await database.getModel('collections').import(require('./collections/books').default);
diff --git a/packages/app/src/components/form.fields/permissions/Scope.tsx b/packages/app/src/components/form.fields/permissions/Scope.tsx
new file mode 100644
index 0000000000..b2cb08a919
--- /dev/null
+++ b/packages/app/src/components/form.fields/permissions/Scope.tsx
@@ -0,0 +1,115 @@
+import React, { useState } from 'react'
+import { connect } from '@formily/react-schema-renderer'
+import { Select, Drawer, Button, Space } from 'antd'
+import {
+ mapStyledProps,
+ mapTextComponent,
+ compose,
+ isStr,
+ isArr
+} from '../shared'
+import ViewFactory from '@/components/views'
+
+function transform({value, multiple, labelField, valueField = 'id'}) {
+ let selectedKeys = [];
+ let selectedValue = [];
+ const values = Array.isArray(value) ? value : [];
+ selectedKeys = values.map(item => item[valueField]);
+ selectedValue = values.map(item => {
+ return {
+ value: item[valueField],
+ label: item[labelField],
+ }
+ });
+ if (!multiple) {
+ return [selectedKeys.shift(), selectedValue.shift()];
+ }
+ return [selectedKeys, selectedValue];
+}
+
+export function Scope(props) {
+ const { target, multiple, associatedName, labelField, valueField = 'id', value, onChange } = props;
+ const [selectedKeys, selectedValue] = transform({value, multiple, labelField, valueField });
+ const [visible, setVisible] = useState(false);
+ const [selectedRowKeys, setSelectedRowKeys] = useState(multiple ? selectedKeys : [selectedKeys]);
+ const [selectedRows, setSelectedRows] = useState(selectedValue);
+ const [options, setOptions] = useState(selectedValue);
+ // console.log('valuevaluevaluevaluevaluevalue', value);
+ return (
+ <>
+
+ {
+ setVisible(false);
+ }}
+ footer={[
+
+
+
+
+
+
+
+ ]}
+ >
+ {
+ // 需要返回的是 array
+ const [selectedKeys, selectedValue] = transform({value: values, multiple: true, labelField, valueField });
+ setSelectedRows(selectedValue);
+ setSelectedRowKeys(selectedKeys);
+ // console.log('valuevaluevaluevaluevaluevalue', {values, selectedKeys, selectedValue});
+ }}
+ // associatedKey={}
+ // associatedName={associatedName}
+ viewName={'table'}
+ />
+
+ >
+ );
+}
diff --git a/packages/app/src/components/form.fields/permissions/index.tsx b/packages/app/src/components/form.fields/permissions/index.tsx
new file mode 100644
index 0000000000..ca78b74074
--- /dev/null
+++ b/packages/app/src/components/form.fields/permissions/index.tsx
@@ -0,0 +1,227 @@
+import { connect } from '@formily/react-schema-renderer'
+import React, { useEffect, useState } from 'react';
+import { Input as AntdInput, Table, Checkbox, Select } from 'antd'
+import { acceptEnum, mapStyledProps, mapTextComponent } from '../shared'
+import api from '@/api-client';
+import { useRequest } from 'umi';
+import { useDynamicList } from 'ahooks';
+import findIndex from 'lodash/findIndex';
+import get from 'lodash/get';
+import { Scope } from './Scope';
+
+export const Permissions = {} as {Actions: any, Fields: any, Tabs: any};
+
+Permissions.Actions = connect({
+ getProps: mapStyledProps,
+ getComponent: mapTextComponent
+})(({onChange, value = [], resourceKey, ...restProps}) => {
+ const { data = [], loading = true } = useRequest(() => {
+ return api.resource('collections.actions').list({
+ associatedKey: resourceKey,
+ });
+ }, {
+ refreshDeps: [resourceKey]
+ });
+
+ return
{
+ const values = [...value||[]];
+ const index = findIndex(values, (item: any) => item && item.name === `${resourceKey}:${record.name}`);
+ console.log(values);
+ return (
+ = 0} onChange={(e) => {
+ // const index = findIndex(values, (item: any) => item && item.name === `${resourceKey}:${record.name}`);
+ if (index >= 0) {
+ if (!e.target.checked) {
+ values.splice(index, 1);
+ }
+ } else {
+ values.push({
+ name: `${resourceKey}:${record.name}`,
+ });
+ }
+ onChange(values);
+ }}/>
+ )
+ }
+ },
+ {
+ title: '可操作的数据范围',
+ dataIndex: ['scope'],
+ render: (value, record) =>
+ },
+ ]} loading={loading}/>
+})
+
+Permissions.Fields = connect<'TextArea'>({
+ getProps: mapStyledProps,
+ getComponent: mapTextComponent
+})(({onChange, value = [], resourceKey, ...restProps}) => {
+
+ const actions = {};
+ value.forEach(item => {
+ actions[item.field_id] = item.actions;
+ });
+
+ // console.log(actions);
+
+ const [fields, setFields] = useState(value||[]);
+
+ const { data = [], loading = true } = useRequest(() => {
+ return api.resource('collections.fields').list({
+ associatedKey: resourceKey,
+ });
+ }, {
+ refreshDeps: [resourceKey]
+ });
+ console.log({resourceKey, data});
+
+ const columns = [
+ {
+ title: '字段名称',
+ dataIndex: ['title'],
+ }
+ ].concat([
+ {
+ title: '查看',
+ action: `${resourceKey}:list`,
+ },
+ {
+ title: '编辑',
+ action: `${resourceKey}:update`,
+ },
+ {
+ title: '新增',
+ action: `${resourceKey}:create`,
+ },
+ ].map(({title, action}) => {
+ let checked = value.filter(({ actions = [] }) => actions.indexOf(action) !== -1).length === data.length;
+ return {
+ title: <> {
+ const values = data.map(field => {
+ const items = actions[field.id] || [];
+ const index = items.indexOf(action);
+ if (index > -1) {
+ if (!e.target.checked) {
+ items.splice(index, 1);
+ }
+ } else {
+ if (e.target.checked) {
+ items.push(action);
+ }
+ }
+ return {
+ field_id: field.id,
+ actions: items,
+ }
+ });
+ // console.log(values);
+ setFields([...values]);
+ onChange([...values]);
+ }}/> {title}>,
+ dataIndex: ['id'],
+ render: (val, record) => {
+ const items = actions[record.id]||[]
+ // console.log({items}, items.indexOf(action));
+ return (
+ {
+ const values = [...value];
+ const index = findIndex(values, ({field_id, actions = []}) => {
+ return field_id === record.id;
+ });
+ if (e.target.checked && index === -1) {
+ values.push({
+ field_id: record.id,
+ actions: [action],
+ });
+ } else {
+ const items = values[index].actions || [];
+ const actionIndex = items.indexOf(action);
+ if (!e.target.checked && actionIndex > -1) {
+ items.splice(actionIndex, 1);
+ // values[index].actions = items;
+ } else if (e.target.checked && actionIndex === -1) {
+ items.push(action);
+ }
+ }
+ onChange(values);
+ setFields(values);
+ }}/>
+ )
+ }
+ }
+ }) as any)
+
+ return
+})
+
+Permissions.Tabs = connect<'TextArea'>({
+ getProps: mapStyledProps,
+ getComponent: mapTextComponent
+})(({onChange, value = [], resourceKey, ...restProps}) => {
+
+ const { data = [], loading = true, mutate } = useRequest(() => {
+ return api.resource('collections.tabs').list({
+ associatedKey: resourceKey,
+ });
+ }, {
+ refreshDeps: [resourceKey]
+ });
+
+ // const [checked, setChecked] = useState(false);
+
+ // console.log(checked);
+
+ // useEffect(() => {
+ // setChecked(data.length === value.length);
+ // console.log({resourceKey, data, value}, data.length === value.lengh);
+ // }, [
+ // data,
+ // ]);
+
+ return
+ {
+ onChange(e.target.checked ? data.map(record => record.id) : []);
+ }}/> 查看
+ >
+ ),
+ dataIndex: ['id'],
+ render: (val, record) => {
+ const values = [...value];
+ return (
+ {
+ const index = values.indexOf(record.id);
+ if (index !== -1) {
+ if (!e.target.checked) {
+ values.splice(index, 1);
+ }
+ } else {
+ values.push(record.id);
+ }
+ onChange(values);
+ }}/>
+ )
+ }
+ },
+ ]} loading={loading}/>
+});
diff --git a/packages/app/src/components/form.fields/registry.ts b/packages/app/src/components/form.fields/registry.ts
index 42c77cdb02..8e0af0b6af 100644
--- a/packages/app/src/components/form.fields/registry.ts
+++ b/packages/app/src/components/form.fields/registry.ts
@@ -19,6 +19,7 @@ import { DrawerSelect } from './drawer-select'
import { SubTable } from './sub-table'
import { Icon } from './icons'
import { ColorSelect } from './color-select'
+import { Permissions } from './permissions'
export const setup = () => {
registerFormFields({
@@ -52,5 +53,8 @@ export const setup = () => {
drawerSelect: DrawerSelect,
colorSelect: ColorSelect,
subTable: SubTable,
+ 'permissions.actions': Permissions.Actions,
+ 'permissions.fields': Permissions.Fields,
+ 'permissions.tabs': Permissions.Tabs,
})
}
diff --git a/packages/app/src/components/pages/CollectionLoader/CollectionTabPane.tsx b/packages/app/src/components/pages/CollectionLoader/CollectionTabPane.tsx
index 573745c70a..7d3fa62479 100644
--- a/packages/app/src/components/pages/CollectionLoader/CollectionTabPane.tsx
+++ b/packages/app/src/components/pages/CollectionLoader/CollectionTabPane.tsx
@@ -20,6 +20,8 @@ export function CollectionTabPane(props) {
params['resourceKey'] = item.itemId;
}
+ console.log({params});
+
if (loading) {
return ;
}
diff --git a/packages/app/src/components/views/Details.tsx b/packages/app/src/components/views/Details.tsx
index 4fe6f16176..83038f8b75 100644
--- a/packages/app/src/components/views/Details.tsx
+++ b/packages/app/src/components/views/Details.tsx
@@ -65,7 +65,7 @@ export function Details(props: any) {
{fields.map((field: any) => {
return (
-
+
)
})}
diff --git a/packages/app/src/components/views/Field/index.tsx b/packages/app/src/components/views/Field/index.tsx
index 0e2512d199..f0890cabae 100644
--- a/packages/app/src/components/views/Field/index.tsx
+++ b/packages/app/src/components/views/Field/index.tsx
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import moment from 'moment';
-import { Tag, Popover, Table, Drawer, Modal } from 'antd';
+import { Tag, Popover, Table, Drawer, Modal, Checkbox, message } from 'antd';
import Icon from '@/components/icons';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
@@ -14,6 +14,8 @@ import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
import marked from 'marked';
import Lightbox from 'react-image-lightbox';
import 'react-image-lightbox/style.css';
+import api from '@/api-client';
+import { useRequest } from 'umi';
marked.setOptions({
gfm: true,
@@ -87,9 +89,28 @@ export function TextareaField(props: any) {
<>{value}>
);
}
-
+// const { data = [], loading = true } = useRequest(() => {
+// return api.resource('collections.actions').list({
+// associatedKey: resourceKey,
+// });
+// }, {
+// refreshDeps: [resourceKey]
+// });
export function BooleanField(props: any) {
- const { value } = props;
+ const { data = {}, value, schema: { name, editable, resource } } = props;
+ if (editable) {
+ return {
+ await api.resource(resource).update({
+ associatedKey: data.id,
+ values: {
+ [name]: e.target.checked,
+ },
+ });
+ message.success('保存成功');
+ // console.log(props);
+ }}/>
+ }
+ console.log(props);
return (
<>{value ? : }>
);
@@ -166,19 +187,19 @@ export function DataSourceField(props: any) {
if (Array.isArray(value)) {
return value.map(val => {
const item = items.find(item => item.value === val);
- return (
+ return item ? (
{item ? item.label : val}
- )
+ ) : {val};
});
}
const item = items.find(item => item.value === value);
- return (
+ return item ? (
{item ? item.label : value}
- )
+ ) : {value};
}
export function RealtionField(props: any) {
diff --git a/packages/app/src/components/views/Form/DrawerForm.tsx b/packages/app/src/components/views/Form/DrawerForm.tsx
index 7f9d9f9e2f..ab12f03cd6 100644
--- a/packages/app/src/components/views/Form/DrawerForm.tsx
+++ b/packages/app/src/components/views/Form/DrawerForm.tsx
@@ -69,7 +69,7 @@ export const DrawerForm = forwardRef((props: any, ref) => {
onClose={() => {
actions.getFormState(state => {
const values = cleanDeep(state.values);
- const others = Object.keys(data).length ? cleanDeep({...data, associatedKey}) : cleanDeep(state.initialValues);
+ const others = Object.keys(data).length ? cleanDeep({...data, associatedKey, resourceKey}) : cleanDeep(state.initialValues);
if (isEqual(values, others)) {
setVisible(false);
return;
@@ -132,7 +132,7 @@ export const DrawerForm = forwardRef((props: any, ref) => {
colon={true}
layout={'vertical'}
// 暂时先这么处理,如果有 associatedKey 注入表单里
- initialValues={{associatedKey, ...data}}
+ initialValues={{associatedKey, resourceKey, ...data}}
actions={actions}
schema={{
type: 'object',
diff --git a/packages/app/src/components/views/SortableTable/index.tsx b/packages/app/src/components/views/SortableTable/index.tsx
index 859e80adcd..477efa26cd 100644
--- a/packages/app/src/components/views/SortableTable/index.tsx
+++ b/packages/app/src/components/views/SortableTable/index.tsx
@@ -61,7 +61,7 @@ export const components = ({data = {}, rowKey, mutate, onMoved, isFieldComponent
export function fields2columns(fields) {
const columns: any[] = fields.map(item => {
const field = cloneDeep(item);
- field.render = (value) => field.interface === 'sort' ? : ;
+ field.render = (value, record) => field.interface === 'sort' ? : ;
field.className = `${field.className||''} noco-field-${field.interface}`;
return {
...field,
diff --git a/packages/app/src/components/views/Table.tsx b/packages/app/src/components/views/Table.tsx
index 15390937a3..b6895f0db9 100644
--- a/packages/app/src/components/views/Table.tsx
+++ b/packages/app/src/components/views/Table.tsx
@@ -42,6 +42,7 @@ export function Table(props: TableProps) {
actions = [],
paginated = true,
defaultPerPage = 10,
+ disableRowClick,
} = schema;
// const { data, mutate } = useRequest(() => api.resource(name).list({
// associatedKey,
@@ -162,7 +163,7 @@ export function Table(props: TableProps) {
data,
mutate,
rowKey,
- isFieldComponent,
+ isFieldComponent: disableRowClick || isFieldComponent,
onMoved: async ({resourceKey, target}) => {
await api.resource(name).sort({
associatedKey,
@@ -178,7 +179,7 @@ export function Table(props: TableProps) {
})}
onRow={(data) => ({
onClick: () => {
- if (isFieldComponent) {
+ if (disableRowClick || isFieldComponent) {
return;
}
if (mode === 'simple') {
diff --git a/packages/client/.fatherrc.ts b/packages/client/.fatherrc.ts
new file mode 100644
index 0000000000..86bce87c0e
--- /dev/null
+++ b/packages/client/.fatherrc.ts
@@ -0,0 +1,5 @@
+export default {
+ target: 'node',
+ cjs: { type: 'babel', lazy: true },
+ disableTypeCheck: true,
+};
diff --git a/packages/database/src/database.ts b/packages/database/src/database.ts
index d4b28e355f..77ef72e620 100644
--- a/packages/database/src/database.ts
+++ b/packages/database/src/database.ts
@@ -30,6 +30,9 @@ export interface ImportOptions {
extensions?: string[];
}
+export interface DatabaseOptions extends Options {
+}
+
export type HookType = 'beforeTableInit' | 'afterTableInit' | 'beforeAddField' | 'afterAddField';
export default class Database {
@@ -48,11 +51,11 @@ export default class Database {
protected tables = new Map();
- protected options: Options;
+ protected options: DatabaseOptions;
protected hooks = {};
- constructor(options: Options) {
+ constructor(options: DatabaseOptions) {
this.options = options;
this.sequelize = new Sequelize(options);
}
diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts
index 4555b98af8..a1300edbf5 100644
--- a/packages/database/src/index.ts
+++ b/packages/database/src/index.ts
@@ -5,5 +5,6 @@ export * from './model';
export * from './table';
export * from './fields';
export * from './utils';
+export { default as Operator } from './op';
export default Database;
diff --git a/packages/database/src/op.ts b/packages/database/src/op.ts
index 99a1a89f04..580f9607fe 100644
--- a/packages/database/src/op.ts
+++ b/packages/database/src/op.ts
@@ -141,4 +141,16 @@ op.set('$notMatch', (values: any[], options) => {
// return Sequelize.literal(`(not (${sql})) AND ${column} IS NULL`);
});
-export default op;
+export default class Operator {
+ public static get(key: string) {
+ return op.get(key);
+ }
+
+ public static has(key: string) {
+ return op.has(key);
+ }
+
+ public static register(key: string, fn: Function) {
+ op.set(key, fn);
+ }
+};
diff --git a/packages/database/src/utils.ts b/packages/database/src/utils.ts
index f315effcac..ce5f62e7fd 100644
--- a/packages/database/src/utils.ts
+++ b/packages/database/src/utils.ts
@@ -54,7 +54,8 @@ export function toWhere(options: any, context: ToWhereContext = {}) {
switch (typeof opKey) {
case 'function':
const name = model ? model.name : '';
- const result = opKey(items[key], {
+ const result = opKey(items[key], {
+ ctx,
model,
database,
fieldPath: name ? `${name}.${prefix}` : prefix,
diff --git a/packages/plugin-collections/src/collections/collections.ts b/packages/plugin-collections/src/collections/collections.ts
index 49dae45e3c..e258890afe 100644
--- a/packages/plugin-collections/src/collections/collections.ts
+++ b/packages/plugin-collections/src/collections/collections.ts
@@ -336,15 +336,23 @@ export default {
actionNames: ['update'],
developerMode: true,
},
- // {
- // type: 'table',
- // name: 'simple',
- // title: '简易模式',
- // template: 'SimpleTable',
- // actionNames: ['create', 'destroy'],
- // detailsViewName: 'details',
- // updateViewName: 'form',
- // },
+ {
+ type: 'table',
+ name: 'permissionTable',
+ title: '权限设置表格',
+ mode: 'simple',
+ template: 'SimpleTable',
+ // actionNames: ['create', 'destroy'],
+ detailsViewName: 'details',
+ updateViewName: 'permissionForm',
+ },
+ {
+ type: 'form',
+ name: 'permissionForm',
+ title: '权限设置表单',
+ mode: 'simple',
+ template: 'DrawerForm',
+ },
{
type: 'table',
name: 'table',
diff --git a/packages/plugin-file-manager/src/__tests__/index.ts b/packages/plugin-file-manager/src/__tests__/index.ts
index 3e29fd7284..4cee60d2e3 100644
--- a/packages/plugin-file-manager/src/__tests__/index.ts
+++ b/packages/plugin-file-manager/src/__tests__/index.ts
@@ -4,8 +4,8 @@ import supertest from 'supertest';
import bodyParser from 'koa-bodyparser';
import { Dialect } from 'sequelize';
import Database from '@nocobase/database';
-import { actions, middlewares } from '@nocobase/actions';
-import { Application } from '@nocobase/server';
+import { actions, middlewares } from '@nocobase/actions/src';
+import { Application } from '@nocobase/server/src';
import middleware from '@nocobase/server/src/middleware'
import plugin from '../server';
import { FILE_FIELD_NAME } from '../constants';
@@ -62,7 +62,7 @@ export async function getApp() {
});
app.resourcer.use(middlewares.associated);
app.resourcer.registerActionHandlers({...actions.associate, ...actions.common});
- app.registerPlugins({
+ app.registerPlugin({
'collections': [path.resolve(__dirname, '../../../plugin-collections')],
'file-manager': [plugin]
});
diff --git a/packages/plugin-pages/src/actions/getCollection.ts b/packages/plugin-pages/src/actions/getCollection.ts
index e821785234..6fc0226684 100644
--- a/packages/plugin-pages/src/actions/getCollection.ts
+++ b/packages/plugin-pages/src/actions/getCollection.ts
@@ -17,9 +17,11 @@ export default async (ctx, next) => {
});
collection.setDataValue('defaultViewName', defaultView.get('name'));
const options = Tab.parseApiJson({
- filter: {
+ filter: ctx.state.developerMode ? {
enabled: true,
- developerMode: ctx.state.developerMode ? {'$isTruly': true} : {'$isFalsy': true},
+ } : {
+ enabled: true,
+ developerMode: {'$isFalsy': true},
},
fields: {
appends: ['associationField'],
diff --git a/packages/plugin-pages/src/actions/getRoutes.ts b/packages/plugin-pages/src/actions/getRoutes.ts
index 65547fa563..258a66224d 100644
--- a/packages/plugin-pages/src/actions/getRoutes.ts
+++ b/packages/plugin-pages/src/actions/getRoutes.ts
@@ -40,9 +40,13 @@ export default async function getRoutes(ctx, next) {
const database: Database = ctx.database;
const Page = database.getModel('pages');
const Collection = database.getModel('collections');
- let pages = await Page.findAll(Page.parseApiJson({
+ let pages = await Page.findAll(Page.parseApiJson(ctx.state.developerMode ? {
filter: {
- developerMode: ctx.state.developerMode ? {'$isTruly': true} : {'$isFalsy': true},
+ },
+ sort: ['sort'],
+ } : {
+ filter: {
+ developerMode: {'$isFalsy': true},
},
sort: ['sort'],
}));
@@ -50,9 +54,14 @@ export default async function getRoutes(ctx, next) {
for (const page of pages) {
items.push(page.toJSON());
if (page.get('path') === '/collections') {
- const collections = await Collection.findAll(Collection.parseApiJson({
+ const collections = await Collection.findAll(Collection.parseApiJson(ctx.state.developerMode ? {
filter: {
- developerMode: ctx.state.developerMode ? {'$isTruly': true} : {'$isFalsy': true},
+ showInDataMenu: true,
+ },
+ sort: ['sort'],
+ }: {
+ filter: {
+ developerMode: {'$isFalsy': true},
showInDataMenu: true,
},
sort: ['sort'],
diff --git a/packages/plugin-pages/src/actions/getView.ts b/packages/plugin-pages/src/actions/getView.ts
index 75eb0474d0..19e3fd6dfc 100644
--- a/packages/plugin-pages/src/actions/getView.ts
+++ b/packages/plugin-pages/src/actions/getView.ts
@@ -38,10 +38,15 @@ const transforms = {
}
if (field.get('name') === 'filter' && field.get('collection_name') === 'views') {
const { values } = ctx.action.params;
- const options = Field.parseApiJson({
+ const options = Field.parseApiJson(ctx.state.developerMode ? {
filter: {
collection_name: get(values, 'associatedKey'),
- developerMode: ctx.state.developerMode ? {'$isTruly': true} : {'$isFalsy': true},
+ },
+ sort: 'sort',
+ } : {
+ filter: {
+ collection_name: get(values, 'associatedKey'),
+ developerMode: {'$isFalsy': true},
},
sort: 'sort',
});
@@ -200,8 +205,9 @@ export default async (ctx, next) => {
// const where: any = {
// developerMode: ctx.state.developerMode,
// }
- const filter: any = {
- developerMode: ctx.state.developerMode ? {'$isTruly': true} : {'$isFalsy': true},
+ const filter: any = {}
+ if (!ctx.state.developerMode) {
+ filter.developerMode = {'$isFalsy': true}
}
if (!view.get('draggable')) {
filter.type = {
@@ -221,15 +227,18 @@ export default async (ctx, next) => {
}
return true;
})
- const options = Action.parseApiJson({
+ const options = Action.parseApiJson(ctx.state.developerMode ? {
+ filter: {},
+ sort: 'sort',
+ } : {
filter: {
- developerMode: ctx.state.developerMode ? {'$isTruly': true} : {'$isFalsy': true},
+ developerMode: {'$isFalsy': true},
},
sort: 'sort',
});
const actions = await collection.getActions(options);
let actionNames = view.get('actionNames') || [];
- if (actionNames.length === 0) {
+ if (actionNames.length === 0 && resourceKey !== 'permissionTable') {
actionNames = ['filter', 'create', 'destroy'];
}
const defaultTabs = await collection.getTabs({
@@ -301,17 +310,156 @@ export default async (ctx, next) => {
}
}
actionDefaultParams['fields[appends]'] = appends.join(',');
- ctx.body = {
- ...view.get(),
- title,
- actionDefaultParams,
- original: fields,
- fields: await (transforms[view.type]||transforms.table)(fields, ctx),
- actions: actions.filter(action => actionNames.includes(action.name)).map(action => ({
- ...action.toJSON(),
- ...action.options,
- // viewCollectionName: action.collection_name,
- })),
- };
+ if (resourceFieldName === 'pages' && resourceKey === 'permissionTable') {
+ ctx.body = {
+ ...view.get(),
+ title,
+ actionDefaultParams,
+ original: fields,
+ disableRowClick: true,
+ fields: [
+ {
+ "title": "页面",
+ "name": "title",
+ "interface": "string",
+ "type": "string",
+ "parent_id": null,
+ "required": true,
+ "developerMode": false,
+ "component": {
+ "type": "string",
+ "className": "drag-visible",
+ "showInForm": true,
+ "showInTable": true,
+ "showInDetail": true
+ },
+ "dataIndex": ["title"]
+ },
+ {
+ "title": "访问权限",
+ "name": "accessable",
+ "interface": "boolean",
+ "type": "boolean",
+ "parent_id": null,
+ "required": true,
+ "editable": true,
+ "resource": 'roles.pages',
+ "developerMode": false,
+ "component": {
+ "type": "boolean",
+ "showInTable": true,
+ },
+ "dataIndex": ["accessable"]
+ }
+ ],
+ };
+ } else if (resourceFieldName === 'collections' && resourceKey === 'permissionTable') {
+ ctx.body = {
+ ...view.get(),
+ title,
+ actionDefaultParams,
+ original: fields,
+ rowKey: 'name',
+ fields: [
+ {
+ "title": "数据表名称",
+ "name": "title",
+ "interface": "string",
+ "type": "string",
+ "parent_id": null,
+ "required": true,
+ "developerMode": false,
+ "component": {
+ "type": "string",
+ "className": "drag-visible",
+ "showInForm": true,
+ "showInTable": true,
+ "showInDetail": true
+ },
+ "dataIndex": ["title"]
+ }
+ ],
+ };
+ } else if (resourceFieldName === 'collections' && resourceKey === 'permissionForm') {
+ ctx.body = {
+ ...view.get(),
+ title,
+ actionDefaultParams,
+ original: fields,
+ fields: {
+ actions: {
+ type: 'permissions.actions',
+ title: '数据操作权限',
+ 'x-linkages': [
+ {
+ type: "value:schema",
+ target: "actions",
+ schema: {
+ "x-component-props": {
+ resourceKey: "{{ $form.values && $form.values.resourceKey }}"
+ },
+ },
+ },
+ ],
+ 'x-component-props': {
+ dataSource: [],
+ },
+ },
+ fields: {
+ type: 'permissions.fields',
+ title: '字段权限',
+ 'x-linkages': [
+ {
+ type: "value:schema",
+ target: "fields",
+ schema: {
+ "x-component-props": {
+ resourceKey: "{{ $form.values && $form.values.resourceKey }}"
+ },
+ },
+ },
+ ],
+ 'x-component-props': {
+ dataSource: [],
+ }
+ },
+ tabs: {
+ type: 'permissions.tabs',
+ title: '标签页权限',
+ 'x-linkages': [
+ {
+ type: "value:schema",
+ target: "tabs",
+ schema: {
+ "x-component-props": {
+ resourceKey: "{{ $form.values && $form.values.resourceKey }}"
+ },
+ },
+ },
+ ],
+ 'x-component-props': {
+ dataSource: [],
+ }
+ },
+ description: {
+ type: 'textarea',
+ title: '权限描述',
+ },
+ },
+ };
+ } else {
+ ctx.body = {
+ ...view.get(),
+ title,
+ actionDefaultParams,
+ original: fields,
+ fields: await (transforms[view.type]||transforms.table)(fields, ctx),
+ actions: actions.filter(action => actionNames.includes(action.name)).map(action => ({
+ ...action.toJSON(),
+ ...action.options,
+ // viewCollectionName: action.collection_name,
+ })),
+ };
+ }
await next();
};
diff --git a/packages/plugin-pages/src/collections/pages.ts b/packages/plugin-pages/src/collections/pages.ts
index bcf8b26b61..0bced642ab 100644
--- a/packages/plugin-pages/src/collections/pages.ts
+++ b/packages/plugin-pages/src/collections/pages.ts
@@ -74,7 +74,7 @@ export default {
type: 'string',
name: 'type',
title: '类型',
- options: [
+ dataSource: [
{
label: '页面',
value: 'page',
@@ -118,7 +118,7 @@ export default {
type: 'string',
name: 'template',
title: '模板',
- options: [
+ dataSource: [
{
label: '顶部菜单布局',
value: 'TopMenuLayout',
@@ -241,11 +241,12 @@ export default {
actionNames: ['update'],
},
{
- type: 'simple',
+ type: 'table',
name: 'simple',
title: '简易模式',
template: 'SimpleTable',
default: true,
+ mode: 'simple',
actionNames: ['create', 'destroy'],
detailsViewName: 'details',
updateViewName: 'form',
@@ -258,5 +259,15 @@ export default {
template: 'Table',
actionNames: ['create', 'destroy'],
},
+ {
+ type: 'table',
+ name: 'permissionTable',
+ title: '菜单权限',
+ template: 'Table',
+ mode: 'simple',
+ detailsViewName: 'details',
+ updateViewName: 'form',
+ paginated: false,
+ },
],
} as TableOptions;
diff --git a/packages/plugin-permissions/package.json b/packages/plugin-permissions/package.json
index 59bf79cf53..66ebd4b11f 100644
--- a/packages/plugin-permissions/package.json
+++ b/packages/plugin-permissions/package.json
@@ -4,7 +4,9 @@
"main": "lib/index.js",
"license": "MIT",
"dependencies": {
+ "@nocobase/actions": "^0.3.0-alpha.0",
"@nocobase/database": "^0.3.0-alpha.0",
- "@nocobase/resourcer": "^0.3.0-alpha.0"
+ "@nocobase/resourcer": "^0.3.0-alpha.0",
+ "@nocobase/server": "^0.3.0-alpha.0"
}
}
diff --git a/packages/plugin-permissions/src/__tests__/index.ts b/packages/plugin-permissions/src/__tests__/index.ts
new file mode 100644
index 0000000000..d369cab092
--- /dev/null
+++ b/packages/plugin-permissions/src/__tests__/index.ts
@@ -0,0 +1,145 @@
+import path from 'path';
+import qs from 'qs';
+import supertest from 'supertest';
+import bodyParser from 'koa-bodyparser';
+import { Dialect } from 'sequelize';
+import Database from '@nocobase/database';
+import { actions, middlewares } from '@nocobase/actions';
+import { Application } from '@nocobase/server';
+import middleware from '@nocobase/server/src/middleware';
+import plugin from '../server';
+
+function getTestKey() {
+ const { id } = require.main;
+ const key = id
+ .replace(`${process.env.PWD}/packages`, '')
+ .replace(/src\/__tests__/g, '')
+ .replace('.test.ts', '')
+ .replace(/[^\w]/g, '_')
+ .replace(/_+/g, '_');
+ return key
+}
+
+const config = {
+ username: process.env.DB_USER,
+ password: process.env.DB_PASSWORD,
+ database: process.env.DB_DATABASE,
+ host: process.env.DB_HOST,
+ port: Number.parseInt(process.env.DB_PORT, 10),
+ dialect: process.env.DB_DIALECT as Dialect,
+ logging: process.env.DB_LOG_SQL === 'on',
+ sync: {
+ force: true,
+ alter: {
+ drop: true,
+ },
+ },
+ hooks: {
+ beforeDefine(columns, model) {
+ model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`;
+ }
+ },
+};
+
+export function getDatabase() {
+ return new Database(config);
+};
+
+export async function getApp() {
+ const app = new Application({
+ database: config,
+ resourcer: {
+ prefix: '/api',
+ },
+ });
+ app.resourcer.use(middlewares.associated);
+ app.resourcer.registerActionHandlers({...actions.associate, ...actions.common});
+ app.registerPlugin({
+ collections: path.resolve(__dirname, '../../../plugin-collections'),
+ users: path.resolve(__dirname, '../../../plugin-users'),
+ permissions: plugin
+ });
+ await app.loadPlugins();
+ const testTables = app.database.import({
+ directory: path.resolve(__dirname, './tables')
+ });
+ try {
+ await app.database.sync();
+ }catch(err) {
+ console.error(err);
+ }
+
+ for (const table of testTables.values()) {
+ // TODO(bug): 由于每个用例结束后不会清理用于测试的数据表,导致再次创建和更新
+ // 创建和更新里面仍会再次创建 fields,导致创建相关的数据重复,数据库报错。
+ await app.database.getModel('collections').import(table.getOptions(), { update: true, migrate: false });
+ }
+ app.context.db = app.database;
+ app.use(bodyParser());
+ app.use(middleware({
+ prefix: '/api',
+ resourcer: app.resourcer,
+ database: app.database,
+ }));
+ return app;
+}
+
+interface ActionParams {
+ resourceKey?: string | number;
+ // resourceName?: string;
+ // associatedName?: string;
+ associatedKey?: string | number;
+ fields?: any;
+ filter?: any;
+ values?: any;
+ [key: string]: any;
+}
+
+interface Handler {
+ get: (params?: ActionParams) => Promise;
+ list: (params?: ActionParams) => Promise;
+ create: (params?: ActionParams) => Promise;
+ update: (params?: ActionParams) => Promise;
+ destroy: (params?: ActionParams) => Promise;
+ [name: string]: (params?: ActionParams) => Promise;
+}
+
+export interface Agent {
+ resource: (name: string) => Handler;
+}
+
+export function getAgent(app: Application) {
+ return supertest.agent(app.callback());
+}
+
+export function getAPI(app: Application) {
+ const agent = getAgent(app);
+ return {
+ resource(name: string): any {
+ return new Proxy({}, {
+ get(target, method: string, receiver) {
+ return (params: ActionParams = {}) => {
+ const { associatedKey, resourceKey, values = {}, filePath, ...restParams } = params;
+ let url = `/api/${name}`;
+ if (associatedKey) {
+ url = `/api/${name.split('.').join(`/${associatedKey}/`)}`;
+ }
+ url += `:${method as string}`;
+ if (resourceKey) {
+ url += `/${resourceKey}`;
+ }
+
+ switch (method) {
+ case 'list':
+ case 'get':
+ return agent.get(`${url}?${qs.stringify(restParams)}`);
+
+ default:
+ return agent.post(`${url}?${qs.stringify(restParams)}`).send(values);
+ }
+ }
+ }
+ });
+ }
+ };
+}
diff --git a/packages/plugin-permissions/src/__tests__/list.test.ts b/packages/plugin-permissions/src/__tests__/list.test.ts
new file mode 100644
index 0000000000..4ffc2d3d54
--- /dev/null
+++ b/packages/plugin-permissions/src/__tests__/list.test.ts
@@ -0,0 +1,189 @@
+import { getApp, getAgent, getAPI } from '.';
+
+describe.skip('list', () => {
+ let app;
+ let agent;
+ let api;
+ let db;
+ let userAgents;
+
+ beforeEach(async () => {
+ app = await getApp();
+ agent = getAgent(app);
+ db = app.database;
+
+ const User = db.getModel('users');
+ const users = await User.bulkCreate([
+ { username: 'user1', token: 'token1' },
+ { username: 'user2', token: 'token2' },
+ { username: 'user3', token: 'token3' },
+ { username: 'user4', token: 'token4' },
+ ]);
+
+ userAgents = users.map(user => {
+ const userAgent = getAgent(app);
+ userAgent.set('Authorization', `Bearer ${users[0].token}`);
+ return userAgent;
+ });
+
+ const Role = db.getModel('roles');
+ const roles = await Role.bulkCreate([
+ { title: '匿名用户', type: 0 },
+ { title: '普通用户' },
+ { title: '编辑' },
+ { title: '管理员', type: -1 },
+ ]);
+
+ const Field = db.getModel('fields');
+ const postTitleField = await Field.findOne({
+ where: {
+ name: 'title',
+ collection_name: 'posts'
+ }
+ });
+ const postStatusField = await Field.findOne({
+ where: {
+ name: 'status',
+ collection_name: 'posts'
+ }
+ });
+ const postCategoryField = await Field.findOne({
+ where: {
+ name: 'category',
+ collection_name: 'posts'
+ }
+ });
+
+ // 匿名用户
+ await roles[0].updateAssociations({
+ permissions: [
+ {
+ collection_name: 'posts',
+ actions_permissions: [
+ {
+ name: 'list',
+ scope: { filter: { status: 'published' }, collection_name: 'posts' },
+ }
+ ],
+ fields: [
+ {
+ id: postTitleField.id,
+ fields_permissions: { actions: ['posts:list'] }
+ }
+ ]
+ },
+ {
+ collection_name: 'categories',
+ actions_permissions: [
+ { name: 'list' }
+ ]
+ }
+ ]
+ });
+
+ // 普通用户
+ await roles[1].updateAssociations({
+ users: [users[0], users[3]],
+ permissions: [
+ {
+ collection_name: 'posts',
+ actions_permissions: [
+ {
+ name: 'list',
+ // TODO(bug): 字段应使用 'created_by' 名称,通过程序映射成外键
+ scope: { filter: { status: 'draft', 'created_by_id.$currentUser': true }, collection_name: 'posts' },
+ },
+ {
+ name: 'update',
+ scope: { filter: { status: 'draft', 'created_by_id.$currentUser': true }, collection_name: 'posts' },
+ }
+ ],
+ fields: [
+ {
+ id: postTitleField.id,
+ fields_permissions: { actions: ['posts:list', 'posts:create', 'posts:update'] }
+ },
+ {
+ id: postStatusField.id,
+ fields_permissions: { actions: ['posts:list'] }
+ },
+ {
+ id: postCategoryField.id,
+ fields_permissions: { actions: ['posts:list'] }
+ },
+ ]
+ }
+ ]
+ });
+
+ // 编辑
+ await roles[2].updateAssociations({
+ users: [users[1], users[3]],
+ permissions: [
+ {
+ collection_name: 'posts',
+ actions_permissions: [
+ {
+ name: 'update'
+ }
+ ],
+ fields: [
+ {
+ id: postTitleField.id,
+ fields_permissions: { actions: ['posts:update'] }
+ },
+ {
+ id: postStatusField.id,
+ fields_permissions: { actions: ['posts:update'] }
+ },
+ {
+ id: postCategoryField.id,
+ fields_permissions: { actions: ['posts:update'] }
+ },
+ ]
+ },
+ {
+ collection_name: 'categories',
+ actions_permissions: [
+ { name: 'create' },
+ { name: 'update' },
+ { name: 'destroy' },
+ ]
+ }
+ ]
+ });
+
+ // 管理员
+
+ const Post = db.getModel('posts');
+ await Post.bulkCreate([
+ { title: 'title1', created_by_id: users[0].id },
+ { title: 'title2', created_by_id: users[0].id },
+ { title: 'title3', created_by_id: users[1].id },
+ { title: 'title4', created_by_id: users[3].id },
+ ]);
+ });
+
+ afterEach(() => db.close());
+
+ describe('anonymous', () => {
+ it('fetch all drafts only get empty list', async () => {
+ const response = await agent.get('/api/posts');
+ expect(response.status).toBe(200);
+ expect(response.body.count).toBe(0);
+ });
+ });
+
+ // TODO(bug): 单独执行可以通过。
+ // 由于 app.database.getModel('collections').import() 有 bug,无法正确的完成数据构造。
+ describe('normal user', () => {
+ it('user could get posts created by self and limited fields', async () => {
+ const response = await userAgents[0].get('/api/posts?sort=title');
+ expect(response.body.count).toBe(2);
+ expect(response.body.rows).toEqual([
+ { title: 'title1', status: 'draft', category: null },
+ { title: 'title2', status: 'draft', category: null }
+ ]);
+ });
+ });
+});
diff --git a/packages/plugin-permissions/src/__tests__/tables/categories.ts b/packages/plugin-permissions/src/__tests__/tables/categories.ts
new file mode 100644
index 0000000000..7d1e0f501d
--- /dev/null
+++ b/packages/plugin-permissions/src/__tests__/tables/categories.ts
@@ -0,0 +1,15 @@
+import { TableOptions } from "@nocobase/database";
+
+export default {
+ name: 'categories',
+ fields: [
+ {
+ type: 'string',
+ name: 'title',
+ },
+ {
+ type: 'hasMany',
+ name: 'posts',
+ },
+ ]
+} as TableOptions;
diff --git a/packages/plugin-permissions/src/__tests__/tables/comments.ts b/packages/plugin-permissions/src/__tests__/tables/comments.ts
new file mode 100644
index 0000000000..1253e4da4a
--- /dev/null
+++ b/packages/plugin-permissions/src/__tests__/tables/comments.ts
@@ -0,0 +1,19 @@
+import { TableOptions } from "@nocobase/database";
+
+export default {
+ name: 'comments',
+ fields: [
+ {
+ type: 'string',
+ name: 'content',
+ },
+ {
+ type: 'belongsTo',
+ name: 'post',
+ },
+ {
+ type: 'belongsTo',
+ name: 'user',
+ },
+ ]
+} as TableOptions;
diff --git a/packages/plugin-permissions/src/__tests__/tables/posts.ts b/packages/plugin-permissions/src/__tests__/tables/posts.ts
new file mode 100644
index 0000000000..a4588ede1a
--- /dev/null
+++ b/packages/plugin-permissions/src/__tests__/tables/posts.ts
@@ -0,0 +1,26 @@
+import { TableOptions } from "@nocobase/database";
+
+export default {
+ name: 'posts',
+ // 目前默认就带了
+ // createdBy: true,
+ fields: [
+ {
+ type: 'string',
+ name: 'title',
+ },
+ {
+ type: 'string',
+ name: 'status',
+ defaultValue: 'draft',
+ },
+ {
+ type: 'belongsTo',
+ name: 'category',
+ },
+ {
+ type: 'hasMany',
+ name: 'comments',
+ },
+ ]
+} as TableOptions;
diff --git a/packages/plugin-permissions/src/actions/roles.collections.ts b/packages/plugin-permissions/src/actions/roles.collections.ts
new file mode 100644
index 0000000000..f8ff08476c
--- /dev/null
+++ b/packages/plugin-permissions/src/actions/roles.collections.ts
@@ -0,0 +1,85 @@
+import { actions } from '@nocobase/actions';
+
+export async function list(ctx: actions.Context, next: actions.Next) {
+ // TODO: 暂时 action 中间件就这么写了
+ ctx.action.mergeParams({associated: null});
+ return actions.common.list(ctx, next);
+}
+
+export async function get(ctx: actions.Context, next: actions.Next) {
+ const {
+ resourceKey,
+ associated
+ } = ctx.action.params;
+
+ const [permission] = await associated.getPermissions({
+ where: {
+ collection_name: resourceKey
+ },
+ include: [
+ {
+ association: 'actions_permissions',
+ // 对 hasMany 关系可以进行拆分查询,避免联表过多标识符超过 PG 的 64 字符限制
+ separate: true,
+ include: [
+ {
+ association: 'scope',
+ attribute: ['filter']
+ },
+ ]
+ },
+ {
+ association: 'fields_permissions',
+ separate: true,
+ }
+ ],
+ limit: 1
+ });
+
+ const result = permission
+ ? {
+ actions: permission.actions_permissions || [],
+ fields: permission.fields_permissions || [],
+ tabs: permission.tabs_permissions || [],
+ }
+ : {
+ actions: [],
+ fields: [],
+ tabs: []
+ };
+
+ ctx.body = result;
+
+ await next();
+}
+
+export async function update(ctx: actions.Context, next: actions.Next) {
+ const {
+ resourceKey,
+ associated,
+ values
+ } = ctx.action.params;
+
+ let [permission] = await associated.getPermissions({
+ where: {
+ collection_name: resourceKey
+ }
+ });
+ if (!permission) {
+ permission = await associated.createPermission({
+ collection_name: resourceKey,
+ description: values.description
+ });
+ } else {
+ await permission.update({ description: values.description });
+ }
+
+ await permission.updateAssociations({
+ actions_permissions: values.actions,
+ fields_permissions: values.fields,
+ tabs: values.tabs
+ });
+
+ ctx.body = permission;
+ await next();
+}
diff --git a/packages/plugin-permissions/src/actions/roles.pages.ts b/packages/plugin-permissions/src/actions/roles.pages.ts
new file mode 100644
index 0000000000..24f3bf201b
--- /dev/null
+++ b/packages/plugin-permissions/src/actions/roles.pages.ts
@@ -0,0 +1,12 @@
+import { actions } from '@nocobase/actions';
+
+export async function list(ctx: actions.Context, next: actions.Next) {
+ // TODO: 暂时 action 中间件就这么写了
+ ctx.action.mergeParams({associated: null});
+ return actions.common.list(ctx, next);
+}
+
+export async function update(ctx: actions.Context, next: actions.Next) {
+ ctx.body = {};
+ await next();
+}
diff --git a/packages/plugin-permissions/src/collections/actions_premissions.ts b/packages/plugin-permissions/src/collections/actions_premissions.ts
new file mode 100644
index 0000000000..88f2f486a1
--- /dev/null
+++ b/packages/plugin-permissions/src/collections/actions_premissions.ts
@@ -0,0 +1,25 @@
+import { TableOptions } from '@nocobase/database';
+
+export default {
+ name: 'actions_permissions',
+ title: '表操作权限',
+ developerMode: true,
+ internal: true,
+ fields: [
+ {
+ comment: '程序操作名称("list", "create" 等)',
+ type: 'string',
+ name: 'name',
+ },
+ {
+ comment: '操作范围',
+ type: 'belongsTo',
+ name: 'scope',
+ target: 'actions_scopes'
+ },
+ {
+ type: 'belongsTo',
+ name: 'permission',
+ },
+ ],
+} as TableOptions;
diff --git a/packages/plugin-permissions/src/collections/actions_scopes.ts b/packages/plugin-permissions/src/collections/actions_scopes.ts
new file mode 100644
index 0000000000..2aafdab604
--- /dev/null
+++ b/packages/plugin-permissions/src/collections/actions_scopes.ts
@@ -0,0 +1,39 @@
+import { TableOptions } from '@nocobase/database';
+
+export default {
+ name: 'actions_scopes',
+ title: '表操作范围',
+ developerMode: true,
+ internal: true,
+ fields: [
+ {
+ comment: '范围名称',
+ type: 'string',
+ name: 'title',
+ title: '名称',
+ component: {
+ type: 'filter',
+ showInTable: true,
+ showInForm: true,
+ },
+ },
+ {
+ interface: 'json',
+ type: 'jsonb',
+ name: 'filter',
+ title: '条件',
+ developerMode: false,
+ mode: 'replace',
+ defaultValue: {},
+ component: {
+ type: 'filter',
+ showInForm: true,
+ },
+ },
+ {
+ type: 'belongsTo',
+ name: 'collection',
+ targetKey: 'name'
+ }
+ ],
+} as TableOptions;
diff --git a/packages/plugin-permissions/src/collections/fields_permissions.ts b/packages/plugin-permissions/src/collections/fields_permissions.ts
new file mode 100644
index 0000000000..724dc527de
--- /dev/null
+++ b/packages/plugin-permissions/src/collections/fields_permissions.ts
@@ -0,0 +1,22 @@
+import { TableOptions } from '@nocobase/database';
+
+export default {
+ name: 'fields_permissions',
+ title: '列操作权限',
+ developerMode: true,
+ internal: true,
+ fields: [
+ {
+ type: 'belongsTo',
+ name: 'permission',
+ },
+ {
+ type: 'belongsTo',
+ name: 'field'
+ },
+ {
+ type: 'jsonb',
+ name: 'actions'
+ }
+ ],
+} as TableOptions;
diff --git a/packages/plugin-permissions/src/collections/permissions.ts b/packages/plugin-permissions/src/collections/permissions.ts
new file mode 100644
index 0000000000..f4446ff088
--- /dev/null
+++ b/packages/plugin-permissions/src/collections/permissions.ts
@@ -0,0 +1,107 @@
+import { TableOptions } from '@nocobase/database';
+
+export default {
+ name: 'permissions',
+ title: '权限集',
+ developerMode: true,
+ internal: true,
+ fields: [
+ // TODO(feature): 黑白名单控制
+ // {
+ // comment: '白名单或黑名单',
+ // type: 'boolean',
+ // name: 'mode',
+ // defaultValue: false
+ // },
+ {
+ type: 'integer',
+ name: 'id',
+ primaryKey: true,
+ autoIncrement: true
+ },
+ {
+ type: 'string',
+ name: 'desctiption',
+ },
+ {
+ comment: '关联的角色',
+ type: 'belongsTo',
+ name: 'role',
+ },
+ {
+ comment: '关联的表名',
+ type: 'belongsTo',
+ name: 'collection',
+ targetKey: 'name'
+ },
+ {
+ comment: '允许的操作集',
+ type: 'hasMany',
+ name: 'actions_permissions',
+ },
+ {
+ type: 'belongsToMany',
+ name: 'fields',
+ through: 'fields_permissions',
+ },
+ {
+ type: 'hasMany',
+ name: 'fields_permissions'
+ },
+ // {
+ // comment: '允许的 views 列表',
+ // type: 'belongsToMany',
+ // name: 'views',
+ // through: 'views_permissions'
+ // },
+ // {
+ // comment: '视图集(方便访问)',
+ // type: 'hasMany',
+ // name: 'views_permissions',
+ // },
+ // {
+ // comment: '允许的 tabs 列表',
+ // type: 'belongsToMany',
+ // name: 'tabs',
+ // through: 'tabs_permissions'
+ // },
+ // {
+ // comment: '标签页集(方便访问)',
+ // type: 'hasMany',
+ // name: 'tabs_permissions',
+ // },
+ ],
+ views: [
+ {
+ type: 'form',
+ name: 'form',
+ title: '表单',
+ template: 'DrawerForm',
+ },
+ {
+ type: 'details',
+ name: 'details',
+ title: '详情',
+ template: 'Details',
+ actionNames: ['update'],
+ },
+ {
+ type: 'simple',
+ name: 'simple',
+ title: '简易模式',
+ mode: 'simple',
+ template: 'SimpleTable',
+ actionNames: ['create', 'destroy'],
+ detailsViewName: 'details',
+ updateViewName: 'form',
+ },
+ {
+ type: 'table',
+ name: 'table',
+ title: '列表',
+ template: 'Table',
+ actionNames: ['create', 'destroy'],
+ default: true,
+ },
+ ],
+} as TableOptions;
diff --git a/packages/plugin-permissions/src/collections/roles.ts b/packages/plugin-permissions/src/collections/roles.ts
index ad61000904..65506aef60 100644
--- a/packages/plugin-permissions/src/collections/roles.ts
+++ b/packages/plugin-permissions/src/collections/roles.ts
@@ -2,43 +2,59 @@ import { TableOptions } from '@nocobase/database';
export default {
name: 'roles',
- title: '权限配置',
+ title: '权限组配置',
+ developerMode: true,
+ internal: true,
fields: [
{
+ interface: 'string',
+ title: '权限组名称',
+ comment: '角色名称',
type: 'string',
name: 'title',
+ component: {
+ showInTable: true,
+ showInForm: true,
+ showInDetail: true,
+ },
},
{
- type: 'string',
- name: 'name',
+ interface: 'boolean',
+ comment: '支持匿名用户',
+ type: 'boolean',
+ name: 'anonymous',
+ defaultValue: false
+ },
+ // TODO(feature): 用户组后续考虑
+ // TODO(feature): 用户表应通过插件配置关联,考虑到今后会有多账户系统的情况
+ {
+ interface: 'linkTo',
+ title: '用户',
+ comment: '关联的用户表',
+ type: 'belongsToMany',
+ name: 'users',
+ target: 'users',
+ through: 'users_roles'
},
{
- type: 'json',
- name: 'options',
- },
- ],
- actions: [
- {
- type: 'list',
- name: 'list',
- title: '查看',
+ interface: 'linkTo',
+ title: '数据表',
+ comment: '包含的以数据表分组的权限集',
+ type: 'belongsToMany',
+ name: 'collections',
+ through: 'permissions',
+ targetKey: 'name'
},
{
- type: 'create',
- name: 'create',
- title: '新增',
- viewName: 'form',
+ interface: 'linkTo',
+ title: '页面',
+ type: 'belongsToMany',
+ name: 'pages',
},
{
- type: 'update',
- name: 'update',
- title: '编辑',
- viewName: 'form',
- },
- {
- type: 'destroy',
- name: 'destroy',
- title: '删除',
+ comment: '权限集(方便访问)',
+ type: 'hasMany',
+ name: 'permissions'
},
],
views: [
@@ -48,12 +64,6 @@ export default {
title: '表单',
template: 'DrawerForm',
},
- {
- type: 'form',
- name: 'permission_form',
- title: '数据表配置',
- template: 'PermissionForm',
- },
{
type: 'details',
name: 'details',
@@ -62,22 +72,14 @@ export default {
actionNames: ['update'],
},
{
- type: 'simple',
+ type: 'table',
name: 'simple',
title: '简易模式',
+ mode: 'simple',
template: 'SimpleTable',
actionNames: ['create', 'destroy'],
detailsViewName: 'details',
- updateViewName: 'permission_form',
- },
- {
- type: 'simple',
- name: 'simple2',
- title: '简易模式',
- template: 'SimpleTable',
- detailsViewName: 'details',
- updateViewName: 'permission_form',
- paginated: false,
+ updateViewName: 'form',
},
{
type: 'table',
@@ -96,10 +98,31 @@ export default {
viewName: 'details',
default: true,
},
+ // {
+ // type: 'details',
+ // name: 'collections',
+ // title: '数据表权限',
+ // viewName: 'simple',
+ // },
{
- type: 'details',
+ type: 'association',
name: 'collections',
title: '数据表权限',
+ association: 'collections',
+ viewName: 'permissionTable',
+ },
+ {
+ type: 'association',
+ name: 'pages',
+ title: '页面权限',
+ association: 'pages',
+ viewName: 'permissionTable',
+ },
+ {
+ type: 'association',
+ name: 'users',
+ title: '当前组用户',
+ association: 'users',
viewName: 'simple',
},
],
diff --git a/packages/plugin-permissions/src/server.ts b/packages/plugin-permissions/src/server.ts
index 16e788eff7..13ba544686 100644
--- a/packages/plugin-permissions/src/server.ts
+++ b/packages/plugin-permissions/src/server.ts
@@ -1,12 +1,184 @@
import path from 'path';
-import Database from '@nocobase/database';
+import { Op } from 'sequelize';
+import { Application } from '@nocobase/server';
+import Database, { Operator } from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
+import * as rolesCollectionsActions from './actions/roles.collections';
+import * as rolesPagesActions from './actions/roles.pages';
+
+// API
+// const permissions = ctx.app.getPluginInstance('permissions');
+// const result: boolean = permissions.check(ctx);
+
+class Permissions {
+ readonly app: Application;
+ readonly options: any;
+
+ static check(roles) {
+ return Boolean(this.getActionPermissions(roles).length);
+ }
+
+ static getActionPermissions(roles) {
+ const permissions = roles.reduce((permissions, role) => permissions.concat(role.get('permissions')), []);
+ return permissions.reduce((actions, permission) => actions.concat(permission.get('actions_permissions')), []);
+ }
+
+ static getFieldPermissions(roles) {
+ const permissions = roles.reduce((permissions, role) => permissions.concat(role.get('permissions')), []);
+ return permissions.reduce((fields, permission) => fields.concat(permission.get('fields_permissions')), []);
+ }
+
+ constructor(app: Application, options) {
+ this.app = app;
+ this.options = options;
+
+ const database: Database = app.database;
+ const resourcer: Resourcer = app.resourcer;
+
+ database.import({
+ directory: path.resolve(__dirname, 'collections'),
+ });
+
+ Object.keys(rolesCollectionsActions).forEach(actionName => {
+ resourcer.registerActionHandler(`roles.collections:${actionName}`, rolesCollectionsActions[actionName]);
+ });
+
+ Object.keys(rolesPagesActions).forEach(actionName => {
+ resourcer.registerActionHandler(`roles.pages:${actionName}`, rolesPagesActions[actionName]);
+ });
+
+ // TODO(optimize): 临时处理,相关逻辑还需要更严格
+ const usersTable = database.getTable('users');
+ if (!usersTable.hasField('roles')) {
+ usersTable.addField({
+ type: 'belongsToMany',
+ name: 'roles',
+ through: 'users_roles'
+ });
+ }
+
+ // 针对“自己创建的” scope 添加特殊的操作符以生成查询条件
+ if (!Operator.has('$currentUser')) {
+ Operator.register('$currentUser', (value, { ctx }) => {
+ const user = this.getCurrentUser(ctx);
+ return { [Op.eq]: user[user.constructor.primaryKeyAttribute] };
+ });
+ }
+
+ // resourcer.use(this.middleware());
+ }
+
+ middleware() {
+ return async (ctx, next) => {
+ const {
+ resourceName,
+ actionName
+ } = ctx.action.params;
+
+ const roles = await this.getRolesWithPermissions(ctx);
+ const actionPermissions = Permissions.getActionPermissions(roles);
+
+ if (!actionPermissions.length) {
+ return ctx.throw(404);
+ }
+
+ const filters = actionPermissions
+ .filter(item => Boolean(item.scope) && Object.keys(item.scope.filter).length)
+ .map(item => item.scope.filter);
+
+ const fields = new Set();
+ const fieldPermissions = Permissions.getFieldPermissions(roles);
+ fieldPermissions.forEach(item => {
+ const actions = item.get('actions');
+ if (actions && actions.includes(`${resourceName}:${actionName}`)) {
+ fields.add(item.get('field').get('name'));
+ }
+ });
+
+ ctx.action.mergeParams({
+ ...(filters.length
+ ? { filter: filters.length > 1 ? { or: filters } : filters[0] }
+ : {}),
+ fields: Array.from(fields)
+ });
+
+ return next();
+ };
+ }
+
+ getCurrentUser(ctx) {
+ // TODO: 获取当前用户应调用当前依赖用户插件的相关方法
+ return ctx.state.currentUser;
+ }
+
+ async getRolesWithPermissions(ctx) {
+ // TODO: 还未定义关联数据的权限如何表达
+ const {
+ resourceName,
+ associated,
+ associatedName,
+ associatedKey,
+ actionName
+ } = ctx.action.params;
+
+ const Role = ctx.db.getModel('roles');
+ const permissionInclusion = {
+ association: 'permissions',
+ where: {
+ collection_name: resourceName
+ },
+ required: true,
+ include: [
+ {
+ association: 'actions_permissions',
+ where: {
+ name: actionName
+ },
+ required: true,
+ // 对 hasMany 关系可以进行拆分查询,避免联表过多标识符超过 PG 的 64 字符限制
+ separate: true,
+ include: [
+ {
+ association: 'scope',
+ attribute: ['filter']
+ }
+ ]
+ },
+ {
+ association: 'fields_permissions',
+ include: [
+ {
+ association: 'field',
+ attributes: ['name']
+ }
+ ],
+ separate: true,
+ }
+ ],
+ };
+ // 获取匿名用户的角色及权限
+ const anonymousRoles = await Role.findAll({
+ where: {
+ anonymous: true
+ },
+ include: [
+ permissionInclusion
+ ]
+ });
+ // 获取登入用户的角色及权限
+ const currentUser = this.getCurrentUser(ctx);
+ const userRoles = currentUser ? await currentUser.getRoles({
+ include: [
+ permissionInclusion
+ ]
+ }) : [];
+
+ return [...anonymousRoles, ...userRoles];
+ }
+}
export default async function (options = {}) {
- const database: Database = this.database;
- const resourcer: Resourcer = this.resourcer;
+ const instance = new Permissions(this, options);
- database.import({
- directory: path.resolve(__dirname, 'collections'),
- });
+ return instance;
}
diff --git a/packages/plugin-users/package.json b/packages/plugin-users/package.json
index a5eae06717..b77b337a45 100644
--- a/packages/plugin-users/package.json
+++ b/packages/plugin-users/package.json
@@ -3,22 +3,13 @@
"version": "0.3.0-alpha.0",
"main": "lib/index.js",
"license": "MIT",
- "resolutions": {
- "@types/react": "16.14.0"
- },
- "peerDependencies": {
- "umi": "^3.2.23"
- },
"devDependencies": {
"@nocobase/actions": "^0.3.0-alpha.0",
"@nocobase/server": "^0.3.0-alpha.0",
- "crypto-random-string": "^3.3.0",
- "umi": "^3.2.23"
+ "crypto-random-string": "^3.3.0"
},
"dependencies": {
- "@nocobase/client": "^0.3.0-alpha.0",
"@nocobase/database": "^0.3.0-alpha.0",
- "@nocobase/resourcer": "^0.3.0-alpha.0",
- "@types/react": "16.14.0"
+ "@nocobase/resourcer": "^0.3.0-alpha.0"
}
}
diff --git a/packages/plugin-users/src/__tests__/index.ts b/packages/plugin-users/src/__tests__/index.ts
index 032bc19afc..0fd51b1651 100644
--- a/packages/plugin-users/src/__tests__/index.ts
+++ b/packages/plugin-users/src/__tests__/index.ts
@@ -59,7 +59,7 @@ export async function getApp() {
app.resourcer.use(middlewares.associated);
app.resourcer.registerActionHandlers({...actions.associate, ...actions.common});
app.registerPlugin('collections', [path.resolve(__dirname, '../../../plugin-collections')]);
- app.registerPlugin('file-manager', [plugin]);
+ app.registerPlugin('users', [plugin]);
await app.loadPlugins();
await app.database.sync();
app.use(async (ctx, next) => {
diff --git a/packages/plugin-users/src/collections/users.ts b/packages/plugin-users/src/collections/users.ts
index cbf2b779a8..e0e8bddbe2 100644
--- a/packages/plugin-users/src/collections/users.ts
+++ b/packages/plugin-users/src/collections/users.ts
@@ -122,10 +122,11 @@ export default {
developerMode: true,
},
{
- type: 'simple',
+ type: 'table',
name: 'simple',
title: '简易模式',
- template: 'SimpleTable',
+ mode: 'simple',
+ template: 'Table',
actionNames: ['create', 'destroy'],
detailsViewName: 'details',
updateViewName: 'form',
diff --git a/packages/plugin-users/src/index.ts b/packages/plugin-users/src/index.ts
index 65fb02d8e5..e69de29bb2 100644
--- a/packages/plugin-users/src/index.ts
+++ b/packages/plugin-users/src/index.ts
@@ -1,35 +0,0 @@
-// @ts-ignore
-import { history, request } from 'umi';
-
-export async function getInitialState() {
- const { pathname, search } = location;
- console.log(location);
- let redirect = '';
- // if (href.includes('?')) {
- redirect = `?redirect=${pathname}${search}`;
- // }
-
- if (pathname !== '/login' && pathname !== '/register') {
- try {
- const { data = {} } = await request('/users:check', {
- method: 'post',
- });
-
- if (!data.data) {
- history.push('/login' + redirect);
- return {
- currentUser: {},
- };
- }
-
- return {
- currentUser: data.data,
- };
- } catch (error) {
- console.log(error)
- history.push('/login' + redirect);
- }
- }
-
- return {};
-}
\ No newline at end of file
diff --git a/packages/plugin-users/src/middlewares/check.ts b/packages/plugin-users/src/middlewares/check.ts
new file mode 100644
index 0000000000..f60923a5c9
--- /dev/null
+++ b/packages/plugin-users/src/middlewares/check.ts
@@ -0,0 +1,11 @@
+// TODO(usage): 拦截用户的处理暂时作为一个中间件导出,应用需要的时候可以直接使用这个中间件
+export default function (options) {
+ return async (ctx, next) => {
+ const { currentUser } = ctx.state;
+ if (!currentUser) {
+ return ctx.throw(401, 'Unauthorized');
+ }
+
+ return next();
+ };
+}
diff --git a/packages/plugin-users/src/middlewares/index.ts b/packages/plugin-users/src/middlewares/index.ts
new file mode 100644
index 0000000000..d6faff0fe1
--- /dev/null
+++ b/packages/plugin-users/src/middlewares/index.ts
@@ -0,0 +1,2 @@
+export { default as parseToken } from './parseToken';
+export { default as check } from './check';
diff --git a/packages/plugin-users/src/middlewares/parseToken.ts b/packages/plugin-users/src/middlewares/parseToken.ts
new file mode 100644
index 0000000000..0352a1d1cb
--- /dev/null
+++ b/packages/plugin-users/src/middlewares/parseToken.ts
@@ -0,0 +1,19 @@
+// TODO(feature): 表名应在 options 中配置
+// 中间件默认只解决解析 token 和附加对应 user 的工作,不解决是否提前报 401 退出。
+// 因为是否提供匿名访问资源是应用决定的,不是使用插件就一定不能匿名访问。
+export default function (options) {
+ return async (ctx, next) => {
+ const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, '');
+ const User = ctx.db.getModel('users');
+ const user = await User.findOne({
+ where: {
+ token,
+ },
+ });
+ if (user) {
+ ctx.state.currentUser = user;
+ }
+
+ return next();
+ };
+}
diff --git a/packages/plugin-users/src/resources/users.ts b/packages/plugin-users/src/resources/users.ts
deleted file mode 100644
index 70160325c7..0000000000
--- a/packages/plugin-users/src/resources/users.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { ResourceOptions } from "@nocobase/resourcer";
-
-export default {
- name: 'users',
- middlewares: [
- {
- // only: ['check'],
- handler: async (ctx, next) => {
- const token = ctx.get('Authorization').replace(/^Bearer\s+/gi, '');
- console.log(ctx.action.params.actionName);
- const { actionName } = ctx.action.params;
- if (actionName !== 'check') {
- return next();
- }
- if (!token) {
- ctx.throw(401, 'Unauthorized');
- }
- const User = ctx.db.getModel('users');
- const user = await User.findOne({
- where: {
- token,
- },
- });
- if (!user) {
- ctx.throw(401, 'Unauthorized');
- }
- ctx.state.currentUser = user;
- await next();
- },
- },
- ],
-} as ResourceOptions;
diff --git a/packages/plugin-users/src/server.ts b/packages/plugin-users/src/server.ts
index 97cc3f9634..a19e539c64 100644
--- a/packages/plugin-users/src/server.ts
+++ b/packages/plugin-users/src/server.ts
@@ -10,6 +10,7 @@ import register from './actions/register';
import logout from './actions/logout';
import check from './actions/check';
import { makeOptions } from './hooks/collection-after-create';
+import * as middlewares from './middlewares';
export default async function (options = {}) {
const database: Database = this.database;
@@ -38,8 +39,6 @@ export default async function (options = {}) {
.map(type => table.addField(makeOptions(type, fieldsToMake[type])));
});
- // hooks.call(this);
-
resourcer.registerActionHandlers({
'users:login': login,
'users:register': register,
@@ -47,7 +46,5 @@ export default async function (options = {}) {
'users:check': check,
});
- // resourcer.import({
- // directory: path.resolve(__dirname, 'resources'),
- // });
+ resourcer.use(middlewares.parseToken(options));
}
diff --git a/packages/resourcer/src/__tests__/koa.test.ts b/packages/resourcer/src/__tests__/koa.test.ts
index 382b0ea8fd..38fc152dd8 100644
--- a/packages/resourcer/src/__tests__/koa.test.ts
+++ b/packages/resourcer/src/__tests__/koa.test.ts
@@ -1,16 +1,13 @@
import Koa from 'koa';
-import Router from '@koa/router';
-import request from 'supertest';
-import http from 'http';
+import supertest from 'supertest';
import Resourcer from '../resourcer';
-import qs from 'qs';
import bodyParser from 'koa-bodyparser';
describe('koa middleware', () => {
it('shound work', async () => {
const app = new Koa();
-
const resourcer = new Resourcer();
+ const agent = supertest.agent(app.callback());
resourcer.define({
name: 'test',
@@ -39,14 +36,14 @@ describe('koa middleware', () => {
}
});
- const response = await request(http.createServer(app.callback())).get('/api/test');
+ const response = await agent.get('/api/test');
expect(response.body.arr).toEqual([5,3,4,6]);
});
it('shound work', async () => {
const app = new Koa();
-
const resourcer = new Resourcer();
+ const agent = supertest.agent(app.callback());
resourcer.registerActionHandlers({
async index(ctx, next) {
@@ -79,22 +76,23 @@ describe('koa middleware', () => {
}
});
- const response = await request(http.createServer(app.callback())).get('/api/test');
+ const response = await agent.get('/api/test');
expect(response.body.arr).toEqual([5,3,4,6]);
});
it('shound be 404', async () => {
const app = new Koa();
const resourcer = new Resourcer();
+ const agent = supertest.agent(app.callback());
app.use(resourcer.middleware());
- const response = await request(http.createServer(app.callback())).get('/test');
+ const response = await agent.get('/test');
expect(response.status).toBe(404);
});
it('shound work', async () => {
const app = new Koa();
-
const resourcer = new Resourcer();
+ const agent = supertest.agent(app.callback());
resourcer.define({
name: 'test',
@@ -126,13 +124,14 @@ describe('koa middleware', () => {
}
});
- const response = await request(http.createServer(app.callback())).get('/api/test');
+ const response = await agent.get('/api/test');
expect(response.body.arr).toEqual([5,3,4,6]);
});
it('shound work', async () => {
const app = new Koa();
const resourcer = new Resourcer();
+ const agent = supertest.agent(app.callback());
resourcer.define({
name: 'tables.fields',
@@ -149,13 +148,14 @@ describe('koa middleware', () => {
app.use(resourcer.middleware());
- const response = await request(http.createServer(app.callback())).get('/tables/demos/fields');
+ const response = await agent.get('/tables/demos/fields');
expect(response.body.arr).toEqual([3,4]);
});
it('shound work', async () => {
const app = new Koa();
const resourcer = new Resourcer();
+ const agent = supertest.agent(app.callback());
resourcer.define({
name: 'tables#fields',
@@ -174,13 +174,14 @@ describe('koa middleware', () => {
nameRule: ({resourceName, associatedName}) => associatedName ? `${associatedName}#${resourceName}` : resourceName,
}));
- const response = await request(http.createServer(app.callback())).get('/tables/demos/fields');
+ const response = await agent.get('/tables/demos/fields');
expect(response.body.arr).toEqual([3,4]);
});
describe('action options', () => {
- let app: Koa;
let resourcer: Resourcer;
+ let app: Koa;
+ let agent;
beforeAll(() => {
app = new Koa();
resourcer = new Resourcer();
@@ -200,6 +201,7 @@ describe('koa middleware', () => {
});
app.use(bodyParser());
app.use(resourcer.middleware());
+ agent = supertest.agent(app.callback());
});
it('options1', async () => {
resourcer.define({
@@ -215,7 +217,7 @@ describe('koa middleware', () => {
},
},
});
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.get('/tests')
.query({
filter: {
@@ -242,13 +244,13 @@ describe('koa middleware', () => {
name: 'tests',
actions: {
create: {
- defaultValues: {
+ values: {
col1: 'val1',
},
},
},
});
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.post('/tests')
.send({'aa': 'aa'});
expect(response.body).toEqual({
@@ -262,13 +264,13 @@ describe('koa middleware', () => {
name: 'tests',
actions: {
create: {
- defaultValues: {
+ values: {
col1: 'val1',
},
},
},
});
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.post('/resourcer/tests:create')
.send({
values: {'aa': 'aa'}
@@ -284,13 +286,13 @@ describe('koa middleware', () => {
name: 'tests',
actions: {
update: {
- defaultValues: {
+ values: {
col1: 'val1',
},
},
},
});
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.post('/resourcer/tests:update')
.send({
resourceKey: 1,
@@ -311,7 +313,7 @@ describe('koa middleware', () => {
});
});
it('get', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.get('/users/1/settings');
expect(response.body).toEqual({
associatedName: 'users',
@@ -321,7 +323,7 @@ describe('koa middleware', () => {
});
});
it('update', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.post('/users/1/settings');
expect(response.body).toEqual({
associatedName: 'users',
@@ -331,7 +333,7 @@ describe('koa middleware', () => {
});
});
it('destroy', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.delete('/users/1/settings');
expect(response.body).toEqual({
associatedName: 'users',
@@ -349,7 +351,7 @@ describe('koa middleware', () => {
});
});
it('list', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.get('/users/1/posts');
expect(response.body).toEqual({
associatedName: 'users',
@@ -359,7 +361,7 @@ describe('koa middleware', () => {
});
});
it('get', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.get('/users/1/posts/1');
expect(response.body).toEqual({
associatedName: 'users',
@@ -370,7 +372,7 @@ describe('koa middleware', () => {
});
});
it('create', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.post('/users/1/posts');
expect(response.body).toEqual({
associatedName: 'users',
@@ -380,7 +382,7 @@ describe('koa middleware', () => {
});
});
it('update', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.put('/users/1/posts/1');
expect(response.body).toEqual({
associatedName: 'users',
@@ -391,7 +393,7 @@ describe('koa middleware', () => {
});
});
it('destroy', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.delete('/users/1/posts/1');
expect(response.body).toEqual({
associatedName: 'users',
@@ -410,7 +412,7 @@ describe('koa middleware', () => {
});
});
it('get', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.get('/posts/1/user');
expect(response.body).toEqual({
associatedName: 'posts',
@@ -420,7 +422,7 @@ describe('koa middleware', () => {
});
});
it('set', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.post('/posts/1/user/1');
expect(response.body).toEqual({
associatedName: 'posts',
@@ -431,7 +433,7 @@ describe('koa middleware', () => {
});
});
it('remove', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.delete('/posts/1/user');
expect(response.body).toEqual({
associatedName: 'posts',
@@ -449,7 +451,7 @@ describe('koa middleware', () => {
});
});
it('list', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.get('/posts/1/tags');
expect(response.body).toEqual({
associatedName: 'posts',
@@ -459,7 +461,7 @@ describe('koa middleware', () => {
});
});
it('get', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.get('/posts/1/tags/1');
expect(response.body).toEqual({
associatedName: 'posts',
@@ -470,7 +472,7 @@ describe('koa middleware', () => {
});
});
it('set', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.post('/posts/1/tags');
expect(response.body).toEqual({
associatedName: 'posts',
@@ -480,7 +482,7 @@ describe('koa middleware', () => {
});
});
it('add', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.post('/posts/1/tags/1');
expect(response.body).toEqual({
associatedName: 'posts',
@@ -491,7 +493,7 @@ describe('koa middleware', () => {
});
});
it('update', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.put('/posts/1/tags/1');
expect(response.body).toEqual({
associatedName: 'posts',
@@ -502,7 +504,7 @@ describe('koa middleware', () => {
});
});
it('remove', async () => {
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.delete('/posts/1/tags/1');
expect(response.body).toEqual({
associatedName: 'posts',
@@ -520,7 +522,7 @@ describe('koa middleware', () => {
list: {},
},
});
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.get('/test1')
.query({
fields: ['id', 'col1'],
@@ -540,7 +542,7 @@ describe('koa middleware', () => {
},
},
});
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.get('/test1')
.query({
fields: ['id', 'col1'],
@@ -562,7 +564,7 @@ describe('koa middleware', () => {
},
},
});
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.get('/test1')
.query({
fields: ['id', 'col1', 'password'],
@@ -585,7 +587,7 @@ describe('koa middleware', () => {
},
},
});
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.get('/test1')
.query({
fields: ['id', 'col1', 'password', 'col2'],
@@ -608,7 +610,7 @@ describe('koa middleware', () => {
},
},
});
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.get('/test1').query({
fields: {
appends: ['relation1'],
@@ -623,9 +625,11 @@ describe('koa middleware', () => {
it('fields6', async () => {
resourcer.define({
name: 'test1',
- list: {},
+ actions: {
+ list: {}
+ },
});
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.get('/test1').query({
'fields[appends]': 'rel1,rel2',
});
@@ -638,9 +642,11 @@ describe('koa middleware', () => {
it('fields7', async () => {
resourcer.define({
name: 'users.posts',
- list: {},
+ actions: {
+ list: {}
+ },
});
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.get('/users/name/posts').query({
'fields[appends]': 'rel1,rel2',
});
@@ -658,13 +664,13 @@ describe('koa middleware', () => {
actions: {
list: {
async middleware(ctx, next) {
- ctx.action.setParam('filter.user_name', ctx.action.params.associatedKey);
+ ctx.action.mergeParams({ filter: { user_name: ctx.action.params.associatedKey } });
await next();
},
},
}
});
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.get('/users/name/posts');
expect(response.body).toEqual({
associatedName: 'users',
@@ -680,13 +686,13 @@ describe('koa middleware', () => {
actions: {
list: {
async middleware(ctx, next) {
- ctx.action.setParam('fields.only[]', ctx.action.params.associatedKey);
+ ctx.action.mergeParams({ fields: { only: [ctx.action.params.associatedKey] } }, { fields: 'append' });
await next();
},
},
}
});
- const response = await request(http.createServer(app.callback()))
+ const response = await agent
.get('/users/name/posts');
expect(response.body).toEqual({
associatedName: 'users',
diff --git a/packages/resourcer/src/action.ts b/packages/resourcer/src/action.ts
index d908223eb5..999da3b34a 100644
--- a/packages/resourcer/src/action.ts
+++ b/packages/resourcer/src/action.ts
@@ -1,9 +1,10 @@
-import _, { isArray } from 'lodash';
+import _ from 'lodash';
import compose from 'koa-compose';
import Resource from './resource';
-import { requireModule, mergeFields } from './utils';
+import { requireModule } from './utils';
import { HandlerType } from './resourcer';
import Middleware, { MiddlewareType } from './middleware';
+import { ActionParameterTypes, ActionParameter, UnknownParameter, PageParameter } from './parameter';
export type ActionType = string | HandlerType | ActionOptions;
@@ -43,7 +44,7 @@ export interface ActionOptions {
/**
* 默认数据
*/
- defaultValues?: any;
+ values?: any;
/**
* 字段
*
@@ -171,9 +172,9 @@ export interface ActionParams {
[key: string]: any;
}
-export const DEFAULT_PAGE = 1;
-export const DEFAULT_PER_PAGE = 20;
-export const MAX_PER_PAGE = 100;
+export const DEFAULT_PAGE = PageParameter.DEFAULT_PAGE;
+export const DEFAULT_PER_PAGE = PageParameter.DEFAULT_PER_PAGE;
+export const MAX_PER_PAGE = PageParameter.MAX_PER_PAGE;
export class Action {
@@ -185,7 +186,7 @@ export class Action {
protected options: ActionOptions;
- protected parameters: ActionParams = {};
+ protected parameters: Map = new Map();
protected context: ActionContext = {};
@@ -196,14 +197,24 @@ export class Action {
if (typeof options === 'function') {
options = { handler: options };
}
- const { middleware, middlewares = [], handler } = options;
+ const {
+ middleware,
+ middlewares = [],
+ handler,
+ ...params
+ } = options;
this.middlewares = Middleware.toInstanceArray(middleware || middlewares);
this.handler = handler;
this.options = options;
+ this.mergeParams(params, {});
}
get params(): ActionParams {
- return this.parameters;
+ const result = this.parameters.get('_').get();
+ for (const paramType of this.parameters.values()) {
+ Object.assign(result, paramType.get());
+ }
+ return result;
}
clone() {
@@ -221,81 +232,36 @@ export class Action {
this.context = context;
}
- setParam(key: string, value: any) {
- if (/\[\]$/.test(key)) {
- key = key.substr(0, key.length - 2);
- let values = _.get(this.parameters, key);
- if (_.isArray(values)) {
- values.push(value);
- } else {
- values = [];
- values.push(value);
+ async mergeParams(params, strategies = {}) {
+ const typeKeys = new Set();
+ Object.keys(params).forEach(key => {
+ for (const typeKey of ActionParameterTypes.getAllKeys()) {
+ if (typeof params[key] !== 'undefined' && key in ActionParameterTypes.get(typeKey).picking) {
+ typeKeys.add(typeKey);
+ }
}
- value = values;
+ });
+ let type;
+ for (const key of typeKeys) {
+ const strategy = strategies[key];
+ type = this.parameters.get(key);
+ if (!type) {
+ const Type = ActionParameterTypes.get(key);
+ if (!Type) {
+ throw new Error(`parameter type ${key} is unregistered`);
+ }
+ // @ts-ignore
+ type = new Type(params);
+ this.parameters.set(key, type);
+ }
+ type.merge(params, strategy);
}
- _.set(this.parameters, key, value);
- }
-
- async mergeParams(params: ActionParams) {
- const {
- filter,
- fields,
- values,
- page: paramPage,
- perPage: paramPerPage,
- per_page,
- ...restPrams
- } = params;
- const {
- filter: optionsFilter,
- fields: optionsFields,
- page = DEFAULT_PAGE,
- perPage = DEFAULT_PER_PAGE,
- maxPerPage = MAX_PER_PAGE
- } = this.options;
- const options = _.omit(this.options, [
- 'defaultValues',
- 'filter',
- 'fields',
- 'maxPerPage',
- 'page',
- 'perPage',
- 'handler',
- 'middlewares',
- 'middleware',
- ]);
- const data: ActionParams = {
- ...options,
- ...restPrams,
- };
- if (!_.isEmpty(this.options.defaultValues) || !_.isEmpty(values)) {
- data.values = _.merge(_.cloneDeep(this.options.defaultValues), values);
+ type = this.parameters.get('_');
+ if (!type) {
+ type = new UnknownParameter(params);
+ this.parameters.set('_', type);
}
- // TODO: to be unified by style funciton
- if (per_page || paramPerPage) {
- data.perPage = per_page || paramPerPage;
- }
- if (paramPage || data.perPage) {
- data.page = paramPage || page;
- data.perPage = data.perPage == -1 ? maxPerPage : Math.min(data.perPage || perPage, maxPerPage);
- }
- // if (typeof optionsFilter === 'function') {
- // this.parameters = _.cloneDeep(data);
- // optionsFilter = await optionsFilter(this.context);
- // }
- if (!_.isEmpty(optionsFilter) || !_.isEmpty(filter)) {
- const filterOptions = [_.cloneDeep(optionsFilter), filter].filter(Boolean);
- // TODO(feature): change 'and' to symbol
- data.filter = filterOptions.length > 1 ? { and: filterOptions } : filterOptions[0];
- }
- // if (typeof optionsFields === 'function') {
- // this.parameters = _.cloneDeep(data);
- // optionsFields = await optionsFields(this.context);
- // }
- if (!_.isEmpty(optionsFields) || !_.isEmpty(fields)) {
- data.fields = mergeFields(optionsFields, fields);
- }
- this.parameters = _.cloneDeep(data);
+ type.merge(params, strategies['_']);
}
setResource(resource: Resource) {
diff --git a/packages/resourcer/src/parameter.ts b/packages/resourcer/src/parameter.ts
new file mode 100644
index 0000000000..9b0e86c366
--- /dev/null
+++ b/packages/resourcer/src/parameter.ts
@@ -0,0 +1,297 @@
+import _ from 'lodash';
+
+
+
+const types = new Map();
+
+export class ActionParameterTypes {
+ static register(type: Exclude, Type: typeof ActionParameter) {
+ types.set(type, Type);
+ }
+
+ static get(type: string): typeof ActionParameter {
+ return types.get(type);
+ }
+
+ static getAllKeys(): Iterable {
+ return types.keys();
+ }
+
+ static getAllTypes(): Iterable {
+ return types.values();
+ }
+}
+
+
+
+// TODO(optimize) 当前要兼容 resourcer 和 action 已有的参数形式
+// 更好的情况可以定义参数从 ctx 对象中的来源路径,适应性可能更好
+export abstract class ActionParameter {
+ // TODO(optimize): 设计稍微有点奇怪
+ // 选取未来传入参数集中的哪些键,同时值为默认值
+ static picking: { [key: string]: any }
+
+ // 默认逻辑是直接选取传入对象中的对应键
+ static pick(params: { [key: string]: any }) {
+ const result: { [key: string]: any } = {};
+ Object.keys(this.picking).forEach(key => {
+ if (typeof this.picking[key] !== 'undefined') {
+ result[key] = this.picking[key];
+ }
+ if (typeof params[key] !== 'undefined') {
+ result[key] = params[key];
+ }
+ });
+
+ return result;
+ }
+
+ // 暂时以对象加一层包装,以便类似 pager 这样有多个参数的情况
+ params: { [key: string]: any } = {};
+
+ constructor(params) {
+ this.merge(params);
+ }
+
+ get() {
+ return _.cloneDeep(this.params);
+ }
+
+ merge(params, strategy?: string): void {
+ this.params = ActionParameter.pick(params);
+ }
+}
+
+
+
+export class FieldsParameter extends ActionParameter {
+ static picking = { fields: {} };
+
+ static parse(fields: any) {
+ if (!fields) {
+ return {}
+ }
+ if (typeof fields === 'string') {
+ fields = fields.split(',').map(field => field.trim());
+ }
+ if (Array.isArray(fields)) {
+ const onlyFields = [];
+ const output: any = {};
+ fields.forEach(item => {
+ if (typeof item === 'string') {
+ onlyFields.push(item);
+ } else if (typeof item === 'object') {
+ if (item.only) {
+ onlyFields.push(...item.only.toString().split(','));
+ }
+ Object.assign(output, this.parse(item));
+ }
+ });
+ if (onlyFields.length) {
+ output.only = onlyFields;
+ }
+ return output;
+ }
+ if (fields.only && typeof fields.only === 'string') {
+ fields.only = fields.only.split(',').map(field => field.trim());
+ }
+ if (fields.except && typeof fields.except === 'string') {
+ fields.except = fields.except.split(',').map(field => field.trim());
+ }
+ if (fields.appends && typeof fields.appends === 'string') {
+ fields.appends = fields.appends.split(',').map(field => field.trim());
+ }
+ return fields;
+ }
+
+ static intersect(defaults: any, inputs: any) {
+ let fields: any = {};
+ defaults = this.parse(defaults);
+ inputs = this.parse(inputs);
+ if (inputs.only) {
+ // 前端提供 only,后端提供 only
+ if (defaults.only) {
+ fields.only = defaults.only.filter(field => inputs.only.includes(field));
+ }
+ // 前端提供 only,后端提供 except,输出 only 排除 except
+ else if (defaults.except) {
+ fields.only = inputs.only.filter(field => !defaults.except.includes(field));
+ }
+ // 前端提供 only,后端没有提供 only 或 except
+ else {
+ fields.only = inputs.only;
+ }
+ } else if (inputs.except) {
+ // 前端提供 except,后端提供 only,只输出 only 里排除 except 的字段
+ if (defaults.only) {
+ fields.only = defaults.only.filter(field => !inputs.except.includes(field));
+ }
+ // 前端提供 except,后端提供 except 或不提供,合并 except
+ else {
+ fields.except = _.uniq([...inputs.except, ...(defaults.except||[])]);
+ }
+ }
+ // 前端没提供 only 或 except
+ else {
+ fields = defaults;
+ }
+ // 如果前端提供了 appends
+ if (!_.isEmpty(inputs.appends)) {
+ fields.appends = _.uniq([...inputs.appends, ...(defaults.appends||[])]);
+ }
+ if (!fields.appends) {
+ fields.appends = [];
+ }
+ return fields;
+ }
+
+ static append(defaults, inputs) {
+ let fields: any = {};
+ defaults = this.parse(defaults);
+ inputs = this.parse(inputs);
+
+ ['only', 'except', 'appends'].forEach(key => {
+ if (defaults[key] && defaults[key].length || inputs[key] && inputs[key].length) {
+ fields[key] = _.uniq([...(defaults[key] || []), ...(inputs[key] || [])]);
+ }
+ });
+ return fields;
+ }
+
+ params = { fields: {} };
+
+ merge(params, strategy: 'intersect' | 'append' | 'replace' = 'intersect') {
+ switch (strategy) {
+ case 'intersect':
+ this.params = {
+ fields: FieldsParameter.intersect(this.params.fields, FieldsParameter.pick(params).fields)
+ };
+ break;
+ case 'append':
+ this.params = {
+ fields: FieldsParameter.append(this.params.fields, FieldsParameter.pick(params).fields)
+ };
+ break;
+ default:
+ throw new Error('not implemented yet');
+ }
+ }
+}
+
+
+
+export class FilterParameter extends ActionParameter {
+ static picking = { filter: {} };
+
+ params = { filter: {} };
+
+ merge(params, strategy: 'and' | 'or' = 'and') {
+ const { filter } = FilterParameter.pick(params);
+ if (!filter || _.isEmpty(filter)) {
+ return;
+ }
+ if (_.isEmpty(this.params.filter)) {
+ this.params.filter = filter;
+ } else {
+ this.params.filter = {
+ [strategy]: [this.params.filter, filter]
+ };
+ }
+ }
+}
+
+
+
+export class PageParameter extends ActionParameter {
+ static picking = { page: undefined, perPage: undefined, per_page: undefined };
+
+ static DEFAULT_PAGE = 1;
+ static DEFAULT_PER_PAGE = 20;
+ static MAX_PER_PAGE = 100;
+
+ params = { page: undefined, perPage: undefined };
+
+ maxPerPage = PageParameter.MAX_PER_PAGE;
+
+ constructor(options = { maxPerPage: PageParameter.MAX_PER_PAGE }) {
+ super(options);
+
+ if (typeof options.maxPerPage !== 'undefined') {
+ this.maxPerPage = options.maxPerPage;
+ }
+ }
+
+ merge(params) {
+ const data = PageParameter.pick(params);
+ if (typeof params.per_page !== 'undefined') {
+ data.perPage = params.per_page;
+ }
+
+ if (data.page || data.perPage) {
+ data.page = data.page || this.params.page || PageParameter.DEFAULT_PAGE;
+ data.perPage = data.perPage == -1 ? this.maxPerPage : Math.min(data.perPage || this.params.perPage || PageParameter.DEFAULT_PER_PAGE, this.maxPerPage);
+
+ Object.assign(this.params, data);
+ }
+ }
+}
+
+
+
+export class PayloadParameter extends ActionParameter {
+ static picking = { values: undefined };
+
+ merge(params, strategy: 'replace' | 'merge' | 'intersect' = 'merge') {
+ const data = PayloadParameter.pick(params);
+ switch (strategy) {
+ case 'replace':
+ this.params.values = data.values;
+ break;
+ case 'merge':
+ _.merge(this.params, _.cloneDeep(data));
+ break;
+ default:
+ throw new Error('not implemented yet');
+ }
+ }
+}
+
+
+
+export class UnknownParameter {
+ params: { [key: string]: any } = {};
+
+ constructor(options) {
+ this.merge(options);
+ }
+
+ get() {
+ return _.cloneDeep(this.params);
+ }
+
+ merge(params, strategy: 'replace' | 'merge' | 'intersect' = 'merge') {
+ const knownKeys = new Set();
+ for (const key of ActionParameterTypes.getAllKeys()) {
+ Object.keys(ActionParameterTypes.get(key).picking).forEach(key => knownKeys.add(key));
+ }
+ const data = _.omit(params, Array.from(knownKeys));
+
+ switch (strategy) {
+ case 'merge':
+ _.merge(this.params, _.cloneDeep(data));
+ break;
+ case 'replace':
+ this.params = data;
+ break;
+ default:
+ throw new Error('not implemented yet');
+ }
+ }
+}
+
+
+
+ActionParameterTypes.register('fields', FieldsParameter);
+ActionParameterTypes.register('filter', FilterParameter);
+ActionParameterTypes.register('page', PageParameter);
+ActionParameterTypes.register('payload', PayloadParameter);
diff --git a/packages/resourcer/src/resourcer.ts b/packages/resourcer/src/resourcer.ts
index c5f8ff2160..d875a94e23 100644
--- a/packages/resourcer/src/resourcer.ts
+++ b/packages/resourcer/src/resourcer.ts
@@ -5,6 +5,7 @@ import Action, { ActionName } from './action';
import Resource, { ResourceOptions } from './resource';
import { parseRequest, getNameByParams, ParsedParams, requireModule, parseQuery } from './utils';
import { pathToRegexp } from 'path-to-regexp';
+import _ from 'lodash';
export interface ResourcerContext {
resourcer?: Resourcer;
@@ -279,7 +280,7 @@ export class Resourcer {
await ctx.action.mergeParams({
...query,
...params,
- values: ctx.request.body,
+ ...(_.isEmpty(ctx.request.body) ? {} : { values: ctx.request.body }),
});
}
return compose(ctx.action.getHandlers())(ctx, next);
diff --git a/packages/server/src/application.ts b/packages/server/src/application.ts
index 39e85ba525..183ef5ff3a 100644
--- a/packages/server/src/application.ts
+++ b/packages/server/src/application.ts
@@ -1,50 +1,10 @@
import Koa from 'koa';
-import Database from '@nocobase/database';
+import Database, { DatabaseOptions } from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
export interface ApplicationOptions {
- database: any;
- resourcer: any;
-}
-
-export class PluginManager {
-
- protected application: Application;
-
- protected plugins = new Map();
-
- constructor(application: Application) {
- this.application = application;
- }
-
- register(key: string | object, plugin?: any) {
- if (typeof key === 'object') {
- Object.keys(key).forEach((k) => {
- this.register(k, key[k]);
- });
- } else {
- this.plugins.set(key, plugin);
- }
- }
-
- async load() {
- for (const pluginOptions of this.plugins.values()) {
- if (Array.isArray(pluginOptions)) {
- const [entry, options = {}] = pluginOptions;
- await this.call(entry, options);
- } else {
- await this.call(pluginOptions);
- }
- }
- }
-
- async call(entry: string | Function, options: any = {}) {
- const main = typeof entry === 'function'
- ? entry
- : require(`${entry}/${__filename.endsWith('.ts') ? 'src' : 'lib'}/server`).default;
-
- await main.call(this.application, options);
- }
+ database: DatabaseOptions;
+ resourcer?: any;
}
export class Application extends Koa {
@@ -53,26 +13,49 @@ export class Application extends Koa {
public readonly resourcer: Resourcer;
- public readonly pluginManager: PluginManager;
+ protected plugins = new Map();
constructor(options: ApplicationOptions) {
super();
this.database = new Database(options.database);
this.resourcer = new Resourcer();
- this.pluginManager = new PluginManager(this);
// this.runHook('afterInit');
}
- registerPlugin(key: string, plugin: any) {
- this.pluginManager.register(key, plugin);
+ registerPlugin(key: string | object, plugin?: any) {
+ if (typeof key === 'object') {
+ Object.keys(key).forEach((k) => {
+ this.registerPlugin(k, key[k]);
+ });
+ } else {
+ const config = {};
+ if (Array.isArray(plugin)) {
+ const [entry, options = {}] = plugin;
+ Object.assign(config, { entry, options });
+ } else {
+ Object.assign(config, { entry: plugin, options: {} });
+ }
+ this.plugins.set(key, config);
+ }
}
- registerPlugins(plugins: object) {
- this.pluginManager.register(plugins);
+ getPluginInstance(key: string) {
+ const plugin = this.plugins.get(key);
+ return plugin && plugin.instance;
}
async loadPlugins() {
- return this.pluginManager.load();
+ for (const plugin of this.plugins.values()) {
+ plugin.instance = await this.loadPlugin(plugin);
+ }
+ }
+
+ protected async loadPlugin({ entry, options = {} }: { entry: string | Function, options: any }) {
+ const main = typeof entry === 'function'
+ ? entry
+ : require(`${entry}/${__filename.endsWith('.ts') ? 'src' : 'lib'}/server`).default;
+
+ return await main.call(this, options);
}
}