lots of updates

This commit is contained in:
chenos 2021-07-11 22:20:54 +08:00
parent d39a7d822d
commit af02a895c3
94 changed files with 2965 additions and 2043 deletions

View File

@ -3,6 +3,13 @@ networks:
nocobase: nocobase:
driver: bridge driver: bridge
services: services:
adminer:
image: adminer
restart: always
networks:
- nocobase
ports:
- ${ADMINER_PORT}:8080
verdaccio: verdaccio:
image: verdaccio/verdaccio image: verdaccio/verdaccio
networks: networks:

View File

@ -12,6 +12,7 @@
"build2": "lerna run build", "build2": "lerna run build",
"build": "npm run build-father-build && node packages/father-build/bin/father-build.js", "build": "npm run build-father-build && node packages/father-build/bin/father-build.js",
"build-father-build": "cd packages/father-build && npm run build", "build-father-build": "cd packages/father-build && npm run build",
"db-migrate": "ts-node -r dotenv/config ./packages/api/src/migrate.ts",
"lint": "eslint --ext .ts,.tsx,.js \"packages/*/src/**.@(ts|tsx|js)\" --fix", "lint": "eslint --ext .ts,.tsx,.js \"packages/*/src/**.@(ts|tsx|js)\" --fix",
"test": "npm run lint && jest" "test": "npm run lint && jest"
}, },

View File

@ -19,6 +19,8 @@
"@nocobase/plugin-file-manager": "^0.4.0-alpha.7", "@nocobase/plugin-file-manager": "^0.4.0-alpha.7",
"@nocobase/plugin-pages": "^0.4.0-alpha.7", "@nocobase/plugin-pages": "^0.4.0-alpha.7",
"@nocobase/plugin-permissions": "^0.4.0-alpha.7", "@nocobase/plugin-permissions": "^0.4.0-alpha.7",
"@nocobase/plugin-routes": "^0.4.0-alpha.7",
"@nocobase/plugin-ui-schema": "^0.4.0-alpha.7",
"@nocobase/plugin-users": "^0.4.0-alpha.7", "@nocobase/plugin-users": "^0.4.0-alpha.7",
"@nocobase/server": "^0.4.0-alpha.7", "@nocobase/server": "^0.4.0-alpha.7",
"koa-static": "^5.0.0" "koa-static": "^5.0.0"

View File

@ -39,13 +39,15 @@ const api = Api.create({
const plugins = [ const plugins = [
'@nocobase/plugin-collections', '@nocobase/plugin-collections',
'@nocobase/plugin-action-logs', '@nocobase/plugin-routes',
'@nocobase/plugin-pages', '@nocobase/plugin-ui-schema',
'@nocobase/plugin-users', // '@nocobase/plugin-action-logs',
'@nocobase/plugin-file-manager', // '@nocobase/plugin-pages',
'@nocobase/plugin-permissions', // '@nocobase/plugin-users',
'@nocobase/plugin-automations', // '@nocobase/plugin-file-manager',
'@nocobase/plugin-china-region', // '@nocobase/plugin-permissions',
// '@nocobase/plugin-automations',
// '@nocobase/plugin-china-region',
]; ];
for (const plugin of plugins) { for (const plugin of plugins) {

View File

@ -12,12 +12,6 @@ import { middlewares } from '@nocobase/server';
})); }));
} }
await api.database.getModel('collections').load({skipExisting: true});
await api.database.getModel('collections').load({where: {
name: 'users',
}});
await api.database.getModel('automations').load();
api.use(middlewares.appDistServe({ api.use(middlewares.appDistServe({
root: process.env.APP_DIST, root: process.env.APP_DIST,
useStaticServer: !(process.env.APP_USE_STATIC_SERVER === 'false' || !process.env.APP_USE_STATIC_SERVER), useStaticServer: !(process.env.APP_USE_STATIC_SERVER === 'false' || !process.env.APP_USE_STATIC_SERVER),

View File

@ -8,57 +8,7 @@ global.sync = {
import Database from '@nocobase/database'; import Database from '@nocobase/database';
import api from '../app'; import api from '../app';
import * as uiSchema from './ui-schema';
const data = [
{
title: '后台应用',
path: '/',
type: 'layout',
template: 'TopMenuLayout',
sort: 10,
redirect: '/admin',
},
{
title: '后台',
path: '/admin',
type: 'page',
inherit: false,
template: 'AdminLoader',
order: 230,
},
{
title: '登录页面',
path: '/login',
type: 'page',
inherit: false,
template: 'login',
order: 120,
},
{
title: '注册页面',
path: '/register',
type: 'page',
inherit: false,
template: 'register',
order: 130,
},
{
title: '忘记密码',
path: '/lostpassword',
type: 'page',
inherit: false,
template: 'lostpassword',
order: 140,
},
{
title: '重置密码',
path: '/resetpassword',
type: 'page',
inherit: false,
template: 'resetpassword',
order: 150,
},
];
(async () => { (async () => {
await api.loadPlugins(); await api.loadPlugins();
@ -66,196 +16,47 @@ const data = [
await database.sync({ await database.sync({
// tables: ['collections', 'fields', 'actions', 'views', 'tabs'], // tables: ['collections', 'fields', 'actions', 'views', 'tabs'],
}); });
const [Collection, Page, User] = database.getModels(['collections', 'pages', 'users']);
const tables = database.getTables([]);
for (let table of tables) {
// console.log(table.getName());
if (table.getName() === 'roles') {
// console.log('roles', table.getOptions())
}
await Collection.import(table.getOptions(), { update: true, migrate: false });
}
await Page.import(data);
const user = await User.create({ const Route = database.getModel('routes');
email: process.env.ADMIN_EMAIL,
password: process.env.ADMIN_PASSWORD,
});
const Storage = database.getModel('storages');
await Storage.create({
title: '本地存储',
name: `local`,
type: 'local',
baseUrl: process.env.LOCAL_STORAGE_BASE_URL,
default: process.env.STORAGE_TYPE === 'local',
});
await Storage.create({
name: `ali-oss`,
type: 'ali-oss',
baseUrl: process.env.ALI_OSS_STORAGE_BASE_URL,
options: {
region: process.env.ALI_OSS_REGION,
accessKeyId: process.env.ALI_OSS_ACCESS_KEY_ID,
accessKeySecret: process.env.ALI_OSS_ACCESS_KEY_SECRET,
bucket: process.env.ALI_OSS_BUCKET,
},
default: process.env.STORAGE_TYPE === 'ali-oss',
});
const Role = database.getModel('roles');
if (Role) {
const roles = await Role.bulkCreate([
{ title: '系统开发组', type: -1 },
// { title: '匿名用户组', type: 0 },
{ title: '普通用户组', default: true },
]);
await roles[0].updateAssociations({
users: user
});
}
const Action = database.getModel('actions'); const data = [
// 全局
await Action.bulkCreate([
]);
// 导入地域数据
const ChinaRegion = database.getModel('china_regions');
ChinaRegion && await ChinaRegion.importData();
const Menu = database.getModel('menus');
const menus = [
{ {
title: '仪表盘', type: 'redirect',
icon: 'DashboardOutlined', from: '/',
type: 'group', to: '/admin',
exact: true,
},
{
path: '/admin/:name(.+)?',
component: 'AdminLayout',
title: `后台`,
uiSchema: uiSchema.menu,
},
{
component: 'AuthLayout',
children: [ children: [
{ {
title: '欢迎光临', name: 'login',
icon: 'DatabaseOutlined', path: '/login',
type: 'page', component: 'DefaultPage',
views: [], title: `登录`,
name: 'welcome', uiSchema: uiSchema.login,
},
],
}, },
{ {
title: '数据', name: 'register',
icon: 'DatabaseOutlined', path: '/register',
type: 'group', component: 'DefaultPage',
children: [], title: `注册`,
}, uiSchema: uiSchema.register,
{
title: '用户',
icon: 'TeamOutlined',
type: 'group',
children: [
{
title: '用户管理',
icon: 'DatabaseOutlined',
type: 'page',
views: ['users.table'],
name: 'users',
},
],
},
{
title: '日志',
icon: 'NotificationOutlined',
type: 'group',
developerMode: true,
children: [
{
title: '操作记录',
icon: 'DatabaseOutlined',
type: 'group',
developerMode: true,
children: [
{
title: '全部数据',
type: 'page',
views: ['action_logs.table'],
developerMode: true,
name: 'auditing',
},
{
title: '新增数据',
type: 'page',
views: ['action_logs.create'],
developerMode: true,
name: 'create-auditing',
},
{
title: '更新数据',
type: 'page',
views: ['action_logs.update'],
developerMode: true,
name: 'update-auditing',
},
{
title: '删除数据',
type: 'page',
views: ['action_logs.destroy'],
developerMode: true,
name: 'destroy-auditing',
},
],
},
],
},
{
title: '配置',
icon: 'SettingOutlined',
type: 'group',
developerMode: true,
children: [
{
name: 'menus',
title: '菜单和页面配置',
icon: 'MenuOutlined',
type: 'page',
views: ['menus.table'],
developerMode: true,
},
{
name: 'collections',
title: '数据表配置',
icon: 'DatabaseOutlined',
type: 'page',
views: ['collections.table'],
developerMode: true,
},
{
name: 'permissions',
title: '权限配置',
icon: 'MenuOutlined',
type: 'page',
views: ['roles.table'],
developerMode: true,
},
{
name: 'automations',
title: '自动化配置',
icon: 'MenuOutlined',
type: 'page',
views: ['automations.table'],
developerMode: true,
},
{
name: 'system_settings',
title: '系统配置',
icon: 'DatabaseOutlined',
type: 'page',
views: ['system_settings.descriptions'],
developerMode: true,
}, },
], ],
}, },
]; ];
for (const item of menus) { for (const item of data) {
const menu = await Menu.create(item); const route = await Route.create(item);
await menu.updateAssociations(item); await route.updateAssociations(item);
} }
await database.close(); await database.close();
})(); })();

View File

@ -1,32 +0,0 @@
import api from '../app';
import Database from '@nocobase/database';
(async () => {
await api.loadPlugins();
await api.database.getModel('collections').load({ skipExisting: true });
const database: Database = api.database;
const [Field] = database.getModels(['fields']);
const fields = await Field.findAll({
where: {
interface: 'multipleSelect',
},
});
for (const field of fields) {
const M = database.getModel(field.collection_name);
const models = await M.findAll();
for (const model of models) {
let value = model.get(field.name);
if (!value) {
continue;
}
if (!Array.isArray(value)) {
value = [value];
}
model.set(field.name, value);
await model.save();
console.log(field.name, value);
}
}
})();

View File

@ -1,18 +0,0 @@
import api from '../app';
import Database from '@nocobase/database';
(async () => {
await api.loadPlugins();
const database: Database = api.database;
await api.database.sync({
});
const [Collection] = database.getModels(['collections']);
const tables = database.getTables();
for (let table of tables) {
console.log(table.getName());
await Collection.import(table.getOptions(), { migrate: false });
}
})();

View File

@ -0,0 +1,3 @@
export * from './login';
export * from './menu';
export * from './register';

View File

@ -0,0 +1,66 @@
import { ISchema } from '@formily/react';
export const login: ISchema = {
key: 'dtf9j0b8p9u',
name: 'dtf9j0b8p9u',
type: 'object',
properties: {
email: {
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '邮箱或用户名',
style: {
// width: 240,
},
},
},
password: {
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Password',
'x-component-props': {
placeholder: '密码',
style: {
// width: 240,
},
},
},
actions: {
type: 'void',
'x-component': 'Div',
properties: {
submit: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
block: true,
type: 'primary',
useAction: '{{ useLogin }}',
style: {
width: '100%',
},
},
title: '登录',
},
},
},
registerlink: {
type: 'void',
'x-component': 'Div',
properties: {
link: {
type: 'void',
'x-component': 'Action.Link',
'x-component-props': {
to: '/register',
},
title: '注册账号',
},
},
},
},
};

View File

@ -0,0 +1,14 @@
export const menu = {
key: 'qqzzjakwkwl',
name: 'qqzzjakwkwl',
type: 'void',
'x-component': 'Menu',
'x-designable-bar': 'Menu.DesignableBar',
'x-component-props': {
mode: 'mix',
theme: 'dark',
defaultSelectedKeys: '{{ selectedKeys }}',
sideMenuRef: '{{ sideMenuRef }}',
onSelect: '{{ onSelect }}',
},
};

View File

@ -0,0 +1,78 @@
export const register = {
key: '46qlxqam3xk',
name: '46qlxqam3xk',
type: 'object',
properties: {
username: {
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '用户名',
style: {
// width: 240,
},
},
},
pwd1: {
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Password',
'x-component-props': {
placeholder: '密码',
checkStrength: true,
style: {
// width: 240,
},
},
},
pwd2: {
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Password',
'x-component-props': {
placeholder: '密码',
checkStrength: true,
style: {
// width: 240,
},
},
},
actions: {
type: 'void',
'x-component': 'Div',
properties: {
submit: {
type: 'void',
title: '注册',
'x-component': 'Action',
'x-component-props': {
block: true,
type: 'primary',
useAction: '{{ useRegister }}',
style: {
width: '100%',
},
},
},
},
},
registerlink: {
type: 'void',
'x-component': 'Div',
properties: {
link: {
type: 'void',
'x-component': 'Action.Link',
'x-component-props': {
to: '/login',
},
title: '使用已有账号登录',
},
},
},
},
}

View File

@ -1,18 +1,20 @@
const api = { const api = {
'/api/routes:getAccessible': require('./routes-getAccessible').default, '/api/routes:getAccessible': require('./routes-getAccessible').default,
'/api/blocks:getSchema/login': require('./blocks-getSchema/login').default, // '/api/blocks:getSchema/login': require('./blocks-getSchema/login').default,
'/api/blocks:getSchema/register': require('./blocks-getSchema/register').default, // '/api/blocks:getSchema/register': require('./blocks-getSchema/register').default,
'/api/blocks:getSchema/item1': require('./blocks-getSchema/item1').default, // '/api/blocks:getSchema/item1': require('./blocks-getSchema/item1').default,
'/api/blocks:getSchema/item2': require('./blocks-getSchema/item2').default, // '/api/blocks:getSchema/item2': require('./blocks-getSchema/item2').default,
'/api/blocks:getSchema/item22': require('./blocks-getSchema/item22').default, // '/api/blocks:getSchema/item22': require('./blocks-getSchema/item22').default,
'/api/blocks:getSchema/item3': require('./blocks-getSchema/item3').default, // '/api/blocks:getSchema/item3': require('./blocks-getSchema/item3').default,
'/api/blocks:getSchema/item4': require('./blocks-getSchema/item4').default, // '/api/blocks:getSchema/item4': require('./blocks-getSchema/item4').default,
'/api/blocks:getSchema/item5': require('./blocks-getSchema/item5').default, // '/api/blocks:getSchema/item5': require('./blocks-getSchema/item5').default,
'/api/blocks:getSchema/menu': require('./blocks-getSchema/menu').default, // '/api/blocks:getSchema/menu': require('./blocks-getSchema/menu').default,
'/api/ui-schemas:getTree/login': require('./ui-schemas-getTree/login').default,
'/api/ui-schemas:getTree/register': require('./ui-schemas-getTree/register').default,
'/api/ui-schemas:getTree/menu': require('./ui-schemas-getTree/menu').default,
} }
export function request(service) { export function request(service) {
console.log({ service })
let url = null; let url = null;
if (typeof service === 'string') { if (typeof service === 'string') {
url = service; url = service;
@ -20,6 +22,8 @@ export function request(service) {
url = service.url; url = service.url;
} }
console.log('request.url', url)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
if (api[url]) { if (api[url]) {

View File

@ -17,7 +17,7 @@ export default [
path: '/admin/:name(.+)?', path: '/admin/:name(.+)?',
component: 'AdminLayout', component: 'AdminLayout',
title: `后台 - ${Mock.mock('@string')}`, title: `后台 - ${Mock.mock('@string')}`,
blockId: 'menu', schemaName: 'menu',
}, },
{ {
component: 'AuthLayout', component: 'AuthLayout',
@ -27,14 +27,14 @@ export default [
path: '/login', path: '/login',
component: 'DefaultPage', component: 'DefaultPage',
title: `登录 - ${Mock.mock('@string')}`, title: `登录 - ${Mock.mock('@string')}`,
blockId: 'login', schemaName: 'login',
}, },
{ {
name: 'register', name: 'register',
path: '/register', path: '/register',
component: 'DefaultPage', component: 'DefaultPage',
title: `注册 - ${Mock.mock('@string')}`, title: `注册 - ${Mock.mock('@string')}`,
blockId: 'register', schemaName: 'register',
}, },
], ],
}, },

View File

@ -212,6 +212,7 @@ Action.Popover = observer((props) => {
const schema = useFieldSchema(); const schema = useFieldSchema();
return ( return (
<Popover <Popover
placement={'bottom'}
visible={visible} visible={visible}
onVisibleChange={(visible) => { onVisibleChange={(visible) => {
setVisible(visible); setVisible(visible);

View File

@ -58,11 +58,47 @@ const schema = {
'x-component': 'Form', 'x-component': 'Form',
properties: { properties: {
filter: { filter: {
type: 'string', name: 'filter',
type: 'object',
'x-component': 'Filter',
properties: {
column1: {
type: 'void',
title: '字段1', title: '字段1',
'x-decorator': 'FormItem', 'x-component': 'Filter.Column',
'x-component-props': {
operations: [
{ label: '等于', value: 'eq' },
{ label: '不等于', value: 'ne' },
],
},
properties: {
field1: {
type: 'string',
'x-component': 'Input', 'x-component': 'Input',
}, },
},
},
column2: {
type: 'void',
title: '字段2',
'x-component': 'Filter.Column',
'x-component-props': {
operations: [
{ label: '大于', value: 'gt' },
{ label: '小于', value: 'lt' },
{ label: '非空', value: 'notNull', noValue: true },
],
},
properties: {
field1: {
type: 'number',
'x-component': 'InputNumber',
},
},
},
}
},
action: { action: {
type: 'void', type: 'void',
title: '提交', title: '提交',

View File

@ -195,7 +195,7 @@ function LayoutWithMenu({ schema }) {
function Content({ activeKey }) { function Content({ activeKey }) {
const { data = {}, loading } = useRequest( const { data = {}, loading } = useRequest(
`/api/blocks:getSchema/${activeKey}`, `/api/ui-schemas:getTree/${activeKey}?filter[parentId]=${activeKey}`,
{ {
refreshDeps: [activeKey], refreshDeps: [activeKey],
formatResult: (result) => result?.data, formatResult: (result) => result?.data,
@ -212,12 +212,13 @@ function Content({ activeKey }) {
export function AdminLayout({ route, children }: any) { export function AdminLayout({ route, children }: any) {
const { data = {}, loading } = useRequest( const { data = {}, loading } = useRequest(
`/api/blocks:getSchema/${route.blockId}`, `/api/ui-schemas:getTree/${route.schemaName}`,
{ {
refreshDeps: [route], refreshDeps: [route],
formatResult: (result) => result?.data, formatResult: (result) => result?.data,
}, },
); );
if (loading) { if (loading) {
return <Spin />; return <Spin />;
} }

View File

@ -6,7 +6,7 @@ import { SchemaRenderer } from '../../schemas';
export function DefaultPage({ route }) { export function DefaultPage({ route }) {
const { data = {}, loading } = useRequest( const { data = {}, loading } = useRequest(
`/api/blocks:getSchema/${route.blockId}`, `/api/ui-schemas:getTree/${route.schemaName}`,
{ {
refreshDeps: [route], refreshDeps: [route],
formatResult: (result) => result?.data, formatResult: (result) => result?.data,

View File

@ -0,0 +1,58 @@
import { getDatabase } from '..';
import Database from '../..';
let db: Database;
beforeEach(async () => {
db = getDatabase();
db.table({
name: 'routes',
fields: [
{
type: 'uid',
name: 'key',
prefix: 'r_',
primaryKey: true,
},
{
type: 'belongsTo',
name: 'uiSchema',
target: 'ui_schemas',
},
],
});
db.table({
name: 'ui_schemas',
fields: [
{
type: 'uid',
name: 'key',
primaryKey: true,
},
{
type: 'string',
name: 'name',
},
],
});
await db.sync();
});
afterEach(async () => {
await db.close();
});
describe('updateAssociations', () => {
it('belongsTo', async () => {
const [Route, Schema] = db.getModels(['routes', 'ui_schemas']);
const route = await Route.create();
await route.updateAssociations({
uiSchema: {
key: '6kyo0t1jnpw',
// name: '6kyo0t1jnpw'
},
});
const schema = await Schema.findByPk('6kyo0t1jnpw');
expect(schema).toBeDefined();
});
});

View File

@ -18,7 +18,7 @@ import { getDataTypeKey } from '.';
import Table from '../table'; import Table from '../table';
import Database from '../database'; import Database from '../database';
import Model, { ModelCtor } from '../model'; import Model, { ModelCtor } from '../model';
import { whereCompare, isNumber } from '../utils'; import { whereCompare, isNumber, uid } from '../utils';
export interface IField { export interface IField {
@ -344,7 +344,7 @@ export class ARRAY extends Column {
export class JSON extends Column { export class JSON extends Column {
public getDataType() { public getDataType() {
return DataTypes.JSONB; return DataTypes.JSON;
} }
} }
@ -352,6 +352,33 @@ export class JSONB extends Column {
} }
export class UUID extends Column { export class UUID extends Column {
public getDataType() {
return DataTypes.UUID;
}
}
export class UID extends Column {
constructor(options: Options.StringOptions, context: FieldContext) {
super(options, context);
const { name, prefix = '' } = options;
const Model = context.sourceTable.getModel();
Model.addHook('beforeCreate', (model) => {
if (!model.get(name)) {
model.set(name, `${prefix}${uid()}`);
}
});
}
public getDataType() {
return DataTypes.STRING;
}
}
export class UUIDV4 extends Column {
public getDataType() {
return DataTypes.UUIDV4;
}
} }
export interface HasOneAccessors { export interface HasOneAccessors {

View File

@ -310,17 +310,22 @@ export abstract class Model extends SequelizeModel {
? association.options.targetKey ? association.options.targetKey
: association.options.sourceKey; : association.options.sourceKey;
if (data[targetAttribute]) { if (data[targetAttribute]) {
await this[accessors.set](data[targetAttribute], opts); if (Object.keys(data).length > 0) {
if (Object.keys(data).length > 1) {
const target = await Target.findOne({ const target = await Target.findOne({
where: { where: {
[targetAttribute]: data[targetAttribute], [targetAttribute]: data[targetAttribute],
}, },
transaction transaction
}); });
if (target) {
await this[accessors.set](data[targetAttribute], opts);
await target.update(data, opts); await target.update(data, opts);
// @ts-ignore // @ts-ignore
await target.updateAssociations(data, opts); await target.updateAssociations(data, opts);
} else {
const t = await this[accessors.create](data, opts);
await t.updateAssociations(data, opts);
}
} }
} else { } else {
const t = await this[accessors.create](data, opts); const t = await this[accessors.create](data, opts);
@ -513,8 +518,8 @@ export abstract class Model extends SequelizeModel {
*/ */
async updateAssociations(data: any, options: UpdateAssociationOptions = {}) { async updateAssociations(data: any, options: UpdateAssociationOptions = {}) {
const { transaction = await this.sequelize.transaction() } = options; const { transaction = await this.sequelize.transaction() } = options;
const table = this.database.getTable(this.constructor.name); // @ts-ignore 判断 Model.associations 更准确
for (const key of table.getAssociations().keys()) { for (const key of Object.keys(this.constructor.associations)) {
// 如果 key 不存在才跳过 // 如果 key 不存在才跳过
if (!Object.keys(data).includes(key)) { if (!Object.keys(data).includes(key)) {
continue; continue;

View File

@ -336,3 +336,14 @@ export function isNumber(num) {
} }
return false; return false;
}; };
let IDX = 36,
HEX = ''
while (IDX--) HEX += IDX.toString(36)
export function uid(len?: number) {
let str = '',
num = len || 11
while (num--) str += HEX[(Math.random() * 36) | 0]
return str
}

View File

@ -0,0 +1,7 @@
node_modules
*.log
docs
__tests__
tsconfig.json
src
.fatherrc.ts

View File

@ -0,0 +1,16 @@
{
"name": "@nocobase/plugin-collections-v04",
"version": "0.4.0-alpha.7",
"main": "lib/index.js",
"license": "MIT",
"dependencies": {
"@nocobase/database": "^0.4.0-alpha.7",
"@nocobase/resourcer": "^0.4.0-alpha.7",
"@nocobase/server": "^0.4.0-alpha.7",
"deepmerge": "^4.2.2"
},
"devDependencies": {
"@nocobase/actions": "^0.4.0-alpha.7"
},
"gitHead": "f0b335ac30f29f25c95d7d137655fa64d8d67f1e"
}

View File

@ -0,0 +1,144 @@
import qs from 'qs';
import plugin from '../server';
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, middleware } from '@nocobase/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,
define: {
hooks: {
beforeCreate(model, options) {
},
},
},
logging: false,
sync: {
force: true,
alter: {
drop: true,
},
},
};
export async function getApp() {
const app = new Application({
database: {
...config,
hooks: {
beforeDefine(columns, model) {
model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`;
}
},
},
resourcer: {
prefix: '/api',
},
});
app.resourcer.use(middlewares.associated);
app.resourcer.registerActionHandlers({ ...actions.associate, ...actions.common });
app.registerPlugin('collections', [plugin]);
await app.loadPlugins();
await app.database.sync();
// 表配置信息存到数据库里
// const tables = app.database.getTables([]);
// for (const table of tables) {
// const Collection = app.database.getModel('collections');
// await Collection.import(table.getOptions(), { hooks: false });
// }
app.use(async (ctx, next) => {
ctx.db = app.database;
await next();
});
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<supertest.Response>;
list: (params?: ActionParams) => Promise<supertest.Response>;
create: (params?: ActionParams) => Promise<supertest.Response>;
update: (params?: ActionParams) => Promise<supertest.Response>;
destroy: (params?: ActionParams) => Promise<supertest.Response>;
[name: string]: (params?: ActionParams) => Promise<supertest.Response>;
}
export interface Agent {
resource: (name: string) => Handler;
}
export function getAgent(app: Application): Agent {
const agent = supertest.agent(app.callback());
return {
resource(name: string): any {
return new Proxy({}, {
get(target, method, receiver) {
return (params: ActionParams = {}) => {
const { associatedKey, resourceKey, values = {}, ...restParams } = params;
let url = `/api/${name}`;
if (associatedKey) {
url = `/api/${name.split('.').join(`/${associatedKey}/`)}`;
}
url += `:${method as string}`;
if (resourceKey) {
url += `/${resourceKey}`;
}
console.log(url);
if (['list', 'get'].indexOf(method as string) !== -1) {
return agent.get(`${url}?${qs.stringify(restParams)}`);
} else {
return agent.post(`${url}?${qs.stringify(restParams)}`).send(values);
}
}
}
});
}
};
}
export function getDatabase() {
return new Database({
...config,
hooks: {
beforeDefine(columns, model) {
model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`;
}
}
});
};

View File

@ -0,0 +1,379 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'collections',
title: '数据表配置',
internal: true,
sortable: true,
draggable: true,
model: 'CollectionModel',
developerMode: true,
createdAt: 'createdTime',
updatedAt: 'updatedTime',
fields: [
// {
// interface: 'sort',
// type: 'sort',
// name: 'sort',
// title: '排序',
// component: {
// type: 'sort',
// className: 'drag-visible',
// width: 60,
// showInTable: true,
// },
// },
{
interface: 'string',
type: 'string',
name: 'title',
title: '数据表名称',
required: true,
component: {
type: 'string',
},
},
{
interface: 'string',
type: 'string',
name: 'name',
createOnly: true,
title: '标识',
unique: true,
required: true,
developerMode: true,
component: {
type: 'string',
},
},
{
interface: 'textarea',
type: 'text',
name: 'description',
title: '数据表描述',
component: {
type: 'textarea',
},
},
// {
// interface: 'boolean',
// type: 'virtual',
// name: 'createdAt',
// title: '记录创建时间',
// developerMode: true,
// defaultValue: true,
// component: {
// type: 'checkbox',
// default: true,
// showInForm: true,
// },
// },
// {
// interface: 'boolean',
// type: 'virtual',
// name: 'updatedAt',
// title: '记录修改时间',
// developerMode: true,
// defaultValue: true,
// component: {
// type: 'checkbox',
// default: true,
// showInForm: true,
// },
// },
{
interface: 'boolean',
type: 'virtual',
name: 'createdBy',
title: '记录创建人信息',
developerMode: true,
component: {
type: 'checkbox',
default: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'updatedBy',
title: '记录修改人信息',
developerMode: true,
component: {
type: 'checkbox',
default: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
developerMode: true,
defaultValue: false,
component: {
type: 'boolean',
},
},
{
interface: 'json',
type: 'json',
name: 'options',
title: '配置信息',
defaultValue: {},
component: {
type: 'hidden',
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'internal',
title: '系统内置',
defaultValue: false,
developerMode: true,
component: {
type: 'boolean',
},
},
{
interface: 'linkTo',
type: 'hasMany',
name: 'fields',
title: '字段',
sourceKey: 'name',
draggable: true,
actions: {
list: {
sort: 'sort',
},
get: {
fields: {
appends: ['children'],
},
},
},
component: {
type: 'drawerSelect',
},
},
{
interface: 'linkTo',
type: 'hasMany',
name: 'actions',
title: '动作',
sourceKey: 'name',
draggable: true,
actions: {
list: {
sort: 'sort',
},
},
component: {
type: 'drawerSelect',
},
},
{
interface: 'linkTo',
type: 'hasMany',
name: 'views_v2',
target: 'views_v2',
title: '视图',
sourceKey: 'name',
draggable: true,
// actions: {
// list: {
// sort: 'sort',
// },
// destroy: {
// filter: {
// default: false
// }
// }
// },
component: {
type: 'drawerSelect',
},
},
{
interface: 'linkTo',
type: 'hasMany',
name: 'scopes',
target: 'scopes',
title: '数据范围',
sourceKey: 'name',
actions: {
list: {
sort: 'id',
},
update: {
filter: {
locked: false
}
},
destroy: {
filter: {
locked: false
}
}
},
component: {
type: 'drawerSelect',
},
},
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
},
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '全部数据',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: ['title', 'description'],
detailsOpenMode: 'window', // window
details: ['descriptions', 'fields', 'views'],
sort: ['id'],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: ['title', 'description'],
},
{
developerMode: true,
type: 'descriptions',
name: 'descriptions',
title: '详情',
fields: ['title', 'description'],
actions: [
{
name: 'update',
type: 'update',
title: '编辑',
viewName: 'form',
},
],
},
{
developerMode: true,
type: 'table',
name: 'permissions_table',
title: '权限表格',
labelField: 'title',
actions: [],
fields: ['title'],
detailsOpenMode: 'drawer', // window
details: ['permissions_form'],
sort: ['id'],
},
{
developerMode: true,
type: 'form',
name: 'permissions_form',
title: '权限表单',
fields: [
{
interface: 'json',
type: 'json',
title: '数据操作权限',
name: 'actions',
component: {
"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": []
}
},
},
{
interface: 'json',
type: 'json',
title: '字段权限',
name: 'fields',
component: {
"type": "permissions.fields",
"x-linkages": [{
"type": "value:schema",
"target": "fields",
"schema": {
"x-component-props": {
"resourceKey": "{{ $form.values && $form.values.resourceKey }}"
}
}
}],
"x-component-props": {
"dataSource": []
}
},
},
],
},
{
developerMode: true,
type: 'table',
dataSourceType: 'association',
name: 'fields',
title: '字段',
targetViewName: 'table2',
targetFieldName: 'fields',
},
{
developerMode: true,
type: 'table',
dataSourceType: 'association',
name: 'views',
title: '视图',
targetViewName: 'table',
targetFieldName: 'views_v2',
},
],
} as TableOptions;

View File

@ -0,0 +1,690 @@
import { TableOptions } from '@nocobase/database';
import { types, getOptions } from '../interfaces';
export default {
name: 'fields',
title: '字段配置',
internal: true,
draggable: true,
model: 'FieldModel',
developerMode: true,
fields: [
{
interface: 'sort',
type: 'sort',
name: 'sort',
scope: ['collection'],
title: '排序',
component: {
type: 'sort',
className: 'drag-visible',
width: 60,
showInTable: true,
},
},
{
interface: 'string',
type: 'string',
name: 'title',
title: '字段名称',
required: true,
component: {
type: 'string',
className: 'drag-visible',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'string',
type: 'string',
name: 'name',
title: '标识',
required: true,
createOnly: true,
developerMode: true,
component: {
type: 'string',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'select',
type: 'string',
name: 'interface',
title: '字段类型',
required: true,
dataSource: getOptions(),
createOnly: true,
component: {
type: 'select',
showInTable: true,
showInDetail: true,
showInForm: true,
"x-linkages": [
// TODO(draft): 统一解决字段类型和配置参数联动的一种方式
// {
// type: 'value:schema',
// target: 'options',
// schema: {
// 'x-component-props': {
// fields: '{{ $self.values[1].fields || [] }}'
// }
// },
// condition: '{{ !!$self.value }}'
// },
{
"type": "value:visible",
"target": "precision",
"condition": "{{ ['number', 'percent'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "dataSource",
"condition": "{{ ['select', 'multipleSelect', 'radio', 'checkboxes'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "dateFormat",
"condition": "{{ ['datetime', 'createdAt', 'updatedAt'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "showTime",
"condition": "{{ ['datetime', 'createdAt', 'updatedAt'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "timeFormat",
"condition": "{{ ['time'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "multiple",
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "target",
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
},
// {
// "type": "value:visible",
// "target": "labelField",
// "condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
// },
{
"type": "value:visible",
"target": "createable",
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "children",
"condition": "{{ ['subTable'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "component.showInTable",
"condition": "{{ ['subTable', 'description'].indexOf($self.value) === -1 }}"
},
{
"type": "value:visible",
"target": "component.showInForm",
"condition": "{{ ['createdAt', 'updatedAt', 'createdBy', 'updatedBy'].indexOf($self.value) === -1 }}"
},
{
"type": "value:visible",
"target": "required",
"condition": "{{ ['createdAt', 'updatedAt', 'createdBy', 'updatedBy'].indexOf($self.value) === -1 }}"
},
{
"type": "value:visible",
"target": "maxLevel",
"condition": "{{ ['chinaRegion'].includes($self.value) }}"
},
{
"type": "value:visible",
"target": "incompletely",
"condition": "{{ ['chinaRegion'].includes($self.value) }}"
},
],
},
},
// TODO(draft): 将 options 作为集合字段开放出来,可以动态的解决字段参数的配置表单联动问题
// {
// interface: 'json',
// type: 'json',
// name: 'options',
// title: '配置信息',
// defaultValue: {},
// component: {
// type: 'subFields',
// showInForm: true,
// },
// },
{
interface: 'subTable',
type: 'virtual',
name: 'dataSource',
title: '可选项',
component: {
type: 'table',
default: [{}],
// showInTable: true,
// showInDetail: true,
showInForm: true,
items: {
type: 'object',
properties: {
value: {
type: "string",
title: "值",
required: true
},
label: {
type: "string",
title: "选项",
required: true
},
},
},
},
},
{
interface: 'string',
type: 'string',
name: 'type',
title: '数据类型',
developerMode: true,
component: {
type: 'string',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'number',
type: 'integer',
name: 'parent_id',
title: '所属分组',
component: {
type: 'number',
},
},
{
interface: 'select',
type: 'virtual',
name: 'precision',
title: '精度',
dataSource: [
{ value: 0, label: '1' },
{ value: 1, label: '1.0' },
{ value: 2, label: '1.00' },
{ value: 3, label: '1.000' },
{ value: 4, label: '1.0000' },
],
component: {
type: 'number',
showInForm: true,
default: 0,
},
},
{
interface: 'select',
type: 'virtual',
name: 'dateFormat',
title: '日期格式',
dataSource: [
{ value: 'YYYY/MM/DD', label: '年/月/日' },
{ value: 'YYYY-MM-DD', label: '年-月-日' },
{ value: 'DD/MM/YYYY', label: '日/月/年' },
],
component: {
type: 'string',
showInForm: true,
default: 'YYYY-MM-DD',
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'showTime',
title: '显示时间',
component: {
type: 'boolean',
showInForm: true,
default: false,
"x-linkages": [
{
"type": "value:visible",
"target": "timeFormat",
"condition": "{{ ($form.values && $form.values.interface === 'time') || $self.value === true }}"
},
],
},
},
{
interface: 'select',
type: 'virtual',
name: 'timeFormat',
title: '时间格式',
dataSource: [
{ value: 'HH:mm:ss', label: '24小时制' },
{ value: 'hh:mm:ss a', label: '12小时制' },
],
component: {
type: 'string',
showInForm: true,
default: 'HH:mm:ss',
},
},
// TODO(refactor): 此部分类型相关的参数,后期应拆分出去
{
name: 'maxLevel',
title: '可选层级',
interface: 'radio',
type: 'virtual',
dataSource: [
{ value: 1, label: '省' },
{ value: 2, label: '市' },
{ value: 3, label: '区/县' },
{ value: 4, label: '乡镇/街道' },
{ value: 5, label: '村/居委会' },
],
component: {
showInForm: true,
default: 3
}
},
{
name: 'incompletely',
title: '可部分选择',
interface: 'boolean',
type: 'virtual',
component: {
showInForm: true,
}
},
{
interface: 'linkTo',
multiple: false,
type: 'belongsTo',
name: 'parent',
title: '所属分组',
target: 'fields',
foreignKey: 'parent_id',
targetKey: 'id',
component: {
type: 'drawerSelect',
},
},
{
interface: 'string',
type: 'virtual',
name: 'target',
title: '要关联的数据表',
required: true,
createOnly: true,
component: {
type: 'remoteSelect',
showInDetail: true,
showInForm: true,
'x-component-props': {
mode: 'simple',
resourceName: 'collections',
labelField: 'title',
valueField: 'name',
},
"x-linkages": [
{
type: "value:state",
target: "labelField",
condition: "{{ $self.inputed }}",
state: {
value: null,
}
},
{
"type": "value:visible",
"target": "labelField",
"condition": "{{ !!$self.value }}"
},
{
type: "value:schema",
target: "labelField",
// condition: "{{ $self.value }}",
schema: {
"x-component-props": {
"associatedKey": "{{ $self.value }}"
},
},
},
{
type: 'value:visible',
target: 'component.x-component-props.filter',
condition: '{{ !!$self.value }}'
},
{
type: "value:schema",
target: "component.x-component-props.filter",
schema: {
"x-component-props": {
"associatedKey": "{{ $self.value }}"
},
},
},
],
},
},
{
interface: 'string',
type: 'virtual',
name: 'labelField',
title: '要显示的字段',
required: true,
component: {
type: 'remoteSelect',
'x-component-props': {
mode: 'simple',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
},
showInDetail: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'multiple',
title: '允许添加多条记录',
component: {
type: 'checkbox',
showInDetail: true,
showInForm: true,
default: true,
},
},
{
name: 'component.x-component-props.filter',
interface: 'json',
type: 'virtual',
title: '数据范围',
component: {
type: 'filter',
'x-component-props': {
resourceName: 'collections.fields',
},
showInForm: true,
}
},
{
interface: 'boolean',
type: 'virtual',
name: 'createable',
title: '允许直接在关联的数据表内新建数据',
component: {
type: 'checkbox',
showInDetail: true,
showInForm: true,
},
},
{
interface: 'subTable',
type: 'hasMany',
name: 'children',
target: 'fields',
sourceKey: 'id',
foreignKey: 'parent_id',
title: '子表格字段',
viewName: 'table2',
// visible: true,
component: {
type: 'subTable',
default: [],
// showInTable: true,
// showInDetail: true,
showInForm: true,
'x-linkages': [
{
type: 'value:schema',
target: 'children',
schema: {
'x-component-props': {
associatedKey: "{{ $form.values && $form.values.id }}"
},
},
},
],
},
},
// {
// interface: 'linkTo',
// multiple: true,
// type: 'hasMany',
// name: 'children',
// title: '子字段',
// target: 'fields',
// foreignKey: 'parent_id',
// sourceKey: 'id',
// component: {
// type: 'drawerSelect',
// },
// },
{
interface: 'textarea',
type: 'virtual',
name: 'component.tooltip',
title: '提示信息',
component: {
type: 'textarea',
showInDetail: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'required',
title: '必填项',
component: {
type: 'checkbox',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'component.showInTable',
title: '显示在表格中',
component: {
type: 'checkbox',
tooltip: '若勾选,该字段将作为一列显示在数据表里',
showInTable: true,
showInDetail: true,
showInForm: true,
default: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'component.showInForm',
title: '显示在表单中',
component: {
type: 'checkbox',
tooltip: '若勾选,该字段将出现在表单中',
showInTable: true,
showInDetail: true,
showInForm: true,
default: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'component.showInDetail',
title: '显示在详情中',
component: {
type: 'checkbox',
tooltip: '若勾选,该字段将出现在详情中',
showInTable: true,
showInDetail: true,
showInForm: true,
default: true,
},
},
{
interface: 'linkTo',
type: 'belongsTo',
name: 'collection',
title: '所属数据表',
target: 'collections',
targetKey: 'name',
labelField: 'title',
component: {
type: 'drawerSelect',
// showInTable: true,
'x-component-props': {
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
},
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
defaultValue: false,
component: {
type: 'boolean',
},
},
{
interface: 'json',
type: 'json',
name: 'component',
title: '前端组件',
defaultValue: {},
component: {
type: 'hidden',
},
},
{
interface: 'json',
type: 'json',
name: 'options',
title: '配置信息',
defaultValue: {},
component: {
type: 'hidden',
},
},
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
},
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '关联的字段',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: ['title', 'interface'],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['sort'],
},
{
developerMode: true,
type: 'table',
name: 'table2',
title: '表格',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: ['sort', 'title', 'interface'],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['sort'],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: [
'title',
'interface',
'dataSource',
'precision',
'dateFormat',
'showTime',
'timeFormat',
'maxLevel',
'incompletely',
'target',
'labelField',
'children',
'multiple',
// 'required',
],
},
],
} as TableOptions;

View File

@ -0,0 +1,241 @@
import _ from 'lodash';
import BaseModel from './base';
import Field from './field';
import { TableOptions } from '@nocobase/database';
import { SaveOptions, Op } from 'sequelize';
/**
*
*
* 使 3+2
* 1. id
* 2.
* 3.
* 4.
* 5.
*
* @param title
*/
export function generateCollectionName(title?: string): string {
return `t_${Date.now().toString(36)}_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
}
export interface LoadOptions {
reset?: boolean;
where?: any;
skipExisting?: boolean;
[key: string]: any;
}
export interface MigrateOptions {
[key: string]: any;
}
export class CollectionModel extends BaseModel {
generateName() {
this.set('name', generateCollectionName());
}
/**
* name collection
*
* @param name
*/
static async findByName(name: string) {
return this.findOne({ where: { name } });
}
/**
* DOTO
* - database.table
* -
*
* @param opts
*/
async loadTableOptions(opts: any = {}) {
const options = await this.getOptions();
// const prevTable = this.database.getTable(this.get('name'));
// const prevOptions = prevTable ? prevTable.getOptions() : {};
// table 是初始化和重新初始化
const table = this.database.table(options);
// console.log({options, actions: table.getOptions()['actions']})
// 如果关系表未加载,一起处理
// const associationTableNames = [];
// for (const [key, association] of table.getAssociations()) {
// // TODO是否需要考虑重载的情况暂时是跳过处理
// if (!this.database.isDefined(association.options.target)) {
// continue;
// }
// associationTableNames.push(association.options.target);
// }
// console.log({associationTableNames});
// if (associationTableNames.length) {
// await CollectionModel.load({
// ...opts,
// where: {
// name: {
// [Op.in]: associationTableNames,
// }
// }
// });
// }
return table;
}
/**
*
*/
async migrate(options: MigrateOptions = {}) {
const { isNewRecord } = options;
const table = await this.loadTableOptions(options);
// 如果不是新增数据force 必须为 false
if (!isNewRecord) {
return await table.sync({
force: false,
alter: {
drop: false,
}
});
}
// TODO: 暂时加了个 collectionSync 解决 collection.create 的数据不清空问题
// @ts-ignore
const sync = this.sequelize.options.collectionSync;
return await table.sync(sync || {
force: false,
alter: {
drop: false,
}
});
}
async getFieldsOptions() {
const fieldsOptions = [];
const fields = await this.getFields();
for (const field of fields) {
fieldsOptions.push(await field.getOptions());
}
return fieldsOptions;
}
async getOptions(): Promise<TableOptions> {
const options: any = {
...this.get(),
actions: await this.getActions(),
fields: await this.getFieldsOptions(),
}
// @ts-ignore
// console.log(this.constructor.associations);
// @ts-ignore
if (this.constructor.hasAlias('views_v2')) {
options.views_v2 = await this.getViews_v2();
}
return options;
}
/**
* TODO
*
* @param options
*/
static async load(options: LoadOptions = {}) {
const { skipExisting = false, reset = false, where = {}, transaction } = options;
const collections = await this.findAll({
transaction,
where,
});
for (const collection of collections) {
if (skipExisting && this.database.isDefined(collection.get('name'))) {
continue;
}
await collection.loadTableOptions({
transaction,
reset,
});
}
}
static async import(data: TableOptions, options: SaveOptions = {}): Promise<CollectionModel> {
data = _.cloneDeep(data);
// @ts-ignore
const { update } = options;
let collection: CollectionModel;
if (data.name) {
collection = await this.findOne({
...options,
where: {
name: data.name,
},
});
}
if (collection) {
// @ts-ignore
await collection.update(data, options);
}
if (!collection) {
// @ts-ignore
collection = await this.create(data, options);
}
const associations = ['fields', 'actions', 'views_v2'];
for (const key of associations) {
if (!Array.isArray(data[key])) {
continue;
}
const Model = this.database.getModel(key);
if (!Model) {
continue;
}
let ids = [];
for (const index in data[key]) {
if (key === 'fields') {
ids = await Model.import(data[key], {
...options,
collectionName: collection.name,
});
continue;
}
let model;
const item = data[key][index];
if (item.name) {
model = await Model.findOne({
...options,
where: {
collection_name: collection.name,
name: item.name,
},
});
}
if (model) {
await model.update({
...item,
// sort: index+1
}, options);
}
if (!model) {
model = await Model.create(
{
...item,
// sort: index+1,
collection_name: collection.name,
},
// @ts-ignore
options
);
}
if (model) {
ids.push(model.id);
}
}
if (ids.length && collection.get('internal')) {
await collection.updateAssociations({
[key]: ids,
});
}
}
return collection;
}
}
export default CollectionModel;

View File

@ -0,0 +1,263 @@
import _ from 'lodash';
import BaseModel from './base';
import { FieldOptions, BELONGSTO, BELONGSTOMANY, HASMANY } from '@nocobase/database';
import * as types from '../interfaces/types';
import { merge } from '../utils';
import { BuildOptions } from 'sequelize';
import { SaveOptions, Utils } from 'sequelize';
import { generateCollectionName } from './collection';
interface FieldImportOptions extends SaveOptions {
parentId?: number;
collectionName?: string;
}
export function generateValueName(title?: string): string {
return `${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
}
export function generateFieldName(title?: string): string {
return `f_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
}
export class FieldModel extends BaseModel {
constructor(values: any = {}, options: any = {}) {
let data = {
...(values.options || {}),
...values,
// ..._.omit(values, 'options'),
};
const interfaceType = data.interface;
if (interfaceType) {
const { options } = types[interfaceType];
let args = [options, data];
// @ts-ignore
data = merge(...args);
if (['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(data.type)) {
// 关系字段如果没有 name相关参数都随机生成
if (!data.name) {
data.name = generateFieldName();
data.paired = true;
// 通用,关系表
if (!data.target) {
data.target = generateCollectionName();
}
// 通用,外键
if (!data.foreignKey) {
data.foreignKey = generateFieldName();
}
if (data.type !== 'belongsTo' && !data.sourceKey) {
data.sourceKey = 'id';
}
if (['belongsTo', 'belongsToMany'].includes(data.type) && !data.targetKey) {
data.targetKey = 'id';
}
// 多对多关联
if (data.type === 'belongsToMany') {
if (!data.through) {
data.through = generateCollectionName();
}
if (!data.otherKey) {
data.otherKey = generateFieldName();
}
}
}
// 有 name但是没有 target
if (!data.target) {
data.target = ['hasOne', 'belongsTo'].includes(data.type) ? Utils.pluralize(data.name) : data.name;
}
}
if (!data.name) {
data.name = generateFieldName();
}
}
// @ts-ignore
super(data, options);
}
generateName() {
this.set('name', generateFieldName());
}
async generatePairField(options) {
const { interface: control, paired, type, target, sourceKey, targetKey, foreignKey, otherKey, through, collection_name } = this.get();
if (control !== 'linkTo' || type !== 'belongsToMany' || !collection_name || !paired) {
return;
}
if (!this.database.isDefined(target)) {
return;
}
const targetTable = this.database.getTable(target);
const Field = FieldModel;
let labelField = 'id';
const targetField = await Field.findOne({
...options,
where: {
type: 'string',
collection_name: target,
},
order: [['sort', 'asc']],
});
if (targetField) {
labelField = targetField.get('name');
}
const collection = await this.getCollection(options);
let targetOptions: any = {
...types.linkTo.options,
interface: 'linkTo',
title: collection.get('title'),
collection_name: target,
options: {
paired: true,
target: collection_name,
labelField,
},
component: {
showInTable: true,
showInForm: true,
showInDetail: true,
},
};
// 暂时不处理 hasone
switch (type) {
case 'hasMany':
targetOptions.type = 'belongsTo';
targetOptions.options.targetKey = sourceKey;
targetOptions.options.foreignKey = foreignKey;
break;
case 'belongsTo':
targetOptions.type = 'hasMany';
targetOptions.options.sourceKey = targetKey;
targetOptions.options.foreignKey = foreignKey;
break;
case 'belongsToMany':
targetOptions.type = 'belongsToMany';
targetOptions.options.sourceKey = targetKey;
targetOptions.options.foreignKey = otherKey;
targetOptions.options.targetKey = sourceKey;
targetOptions.options.otherKey = foreignKey;
targetOptions.options.through = through;
break;
}
const associations = targetTable.getAssociations();
// console.log(associations);
for (const association of associations.values()) {
if (association instanceof BELONGSTOMANY) {
if (
association.options.foreignKey === otherKey
&& association.options.sourceKey === targetKey
&& association.options.otherKey === foreignKey
&& association.options.targetKey === sourceKey
&& association.options.through === through
) {
return;
}
}
// if (association instanceof BELONGSTO) {
// continue;
// }
// if (association instanceof HASMANY) {
// continue;
// }
}
const f = await Field.create(targetOptions, options);
// console.log({targetOptions}, f.get('options'));
}
setInterface(value) {
const { options } = types[value];
let args = [];
// 如果是新数据或 interface 不相等interface options 放后
if (this.isNewRecord || this.get('interface') !== value) {
args = [this.get(), options];
} else {
// 已存在的数据更新不相等interface options 放前面
args = [options, this.get()];
}
// @ts-ignore
const values = merge(...args);
this.set(values);
}
async getOptions(): Promise<FieldOptions> {
return this.get();
}
async migrate(options: any = {}) {
const collectionName = this.get('collection_name');
if (!collectionName) {
return false;
}
if (!this.database.isDefined(collectionName)) {
throw new Error(`${collectionName} is not defined`);
}
const table = this.database.getTable(collectionName);
table.addField(await this.getOptions());
await table.sync({
force: false,
alter: {
drop: false,
}
});
}
static async import(items: any, options: FieldImportOptions = {}): Promise<any> {
const { parentId, collectionName } = options;
if (!Array.isArray(items)) {
items = [items];
}
const ids = [];
for (const index in items) {
const item = items[index];
let model;
const where: any = {};
if (parentId) {
where.parent_id = parentId
} else {
where.collection_name = collectionName;
}
if (item.name) {
model = await this.findOne({
...options,
where: {
...where,
name: item.name,
},
});
}
if (!model) {
const tmp: any = {};
if (parentId) {
tmp.parent_id = parentId
} else {
tmp.collection_name = collectionName;
}
model = await this.create(
{
...item,
...tmp,
},
//@ts-ignore
options
);
} else {
//@ts-ignore
await model.update(item, options);
}
if (Array.isArray(item.children)) {
const childrenIds = await this.import(item.children, {
...options,
parentId: model.id,
collectionName,
});
await model.updateAssociations({
children: childrenIds,
}, options);
}
}
return ids;
}
}
export default FieldModel;

View File

@ -0,0 +1,7 @@
export * from './base';
export * from './action';
export * from './collection';
export * from './field';
export * from './tab';
export * from './view';
export * from './page';

View File

@ -0,0 +1,77 @@
import path from 'path';
import { Application } from '@nocobase/server';
import hooks from './hooks';
import { registerModels, Table } from '@nocobase/database';
import * as models from './models';
export default async function (this: Application, options = {}) {
const database = this.database;
const resourcer = this.resourcer;
// 提供全局的 models 注册机制
registerModels(models);
database.import({
directory: path.resolve(__dirname, 'collections'),
});
database.addHook('afterUpdateAssociations', async function (model, options) {
if (model instanceof models.FieldModel) {
if (model.get('interface') === 'subTable') {
const { migrate = true } = options;
const Collection = model.database.getModel('collections');
await Collection.load({ ...options, where: { name: model.get('collection_name') } });
migrate && await model.migrate(options);
}
}
});
Object.keys(hooks).forEach(modelName => {
const Model = database.getModel(modelName);
Object.keys(hooks[modelName]).forEach(hookKey => {
// TODO(types): 多层 map 映射类型定义较为复杂,暂时忽略
// @ts-ignore
Model.addHook(hookKey, hooks[modelName][hookKey]);
});
});
const Collection = database.getModel('collections');
Collection.addHook('afterCreate', async (model: any, options) => {
if (model.get('developerMode')) {
return;
}
if (model.get('statusable') === false) {
return;
}
console.log("model.get('developerMode')", model.get('name'));
const { transaction = await model.sequelize.transaction() } = options;
await model.createField({
interface: 'radio',
name: 'status',
type: 'string',
filterable: true,
title: '状态',
// index: true,
dataSource: [
{
label: '已发布',
value: 'publish',
},
{
label: '草稿',
value: 'draft',
}
],
component: {
type: 'radio',
},
}, { transaction });
if (!options.transaction) {
await transaction.commit();
}
});
}

View File

@ -0,0 +1,9 @@
import deepmerge from 'deepmerge';
const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray
export function merge(obj1: any, obj2: any) {
return deepmerge(obj1, obj2, {
arrayMerge: overwriteMerge,
});
}

View File

@ -0,0 +1,40 @@
import { Agent, getAgent, getApp } from '.';
import { Application } from '@nocobase/server';
import Database from '@nocobase/database';
describe('collection hooks', () => {
let app: Application;
let agent: Agent;
let db: Database;
beforeEach(async () => {
app = await getApp();
db = app.database;
agent = getAgent(app);
});
afterEach(() => app.database.close());
it('create table', async () => {
const Collection = db.getModel('collections');
const collection = await Collection.create({
title: 'tests',
// name: 'tests',
});
await collection.updateAssociations({
fields: [
{
name: 'title',
dataType: 'string',
},
{
name: 'content',
dataType: 'text',
},
{
dataType: 'integer',
},
],
});
});
});

View File

@ -3,377 +3,28 @@ import { TableOptions } from '@nocobase/database';
export default { export default {
name: 'collections', name: 'collections',
title: '数据表配置', title: '数据表配置',
internal: true, model: 'Collection',
sortable: true,
draggable: true,
model: 'CollectionModel',
developerMode: true,
createdAt: 'createdTime',
updatedAt: 'updatedTime',
fields: [ fields: [
// {
// interface: 'sort',
// type: 'sort',
// name: 'sort',
// title: '排序',
// component: {
// type: 'sort',
// className: 'drag-visible',
// width: 60,
// showInTable: true,
// },
// },
{ {
interface: 'string', type: 'uid',
name: 'name',
primaryKey: true,
prefix: 't_',
},
{
type: 'string', type: 'string',
name: 'title', name: 'title',
title: '数据表名称',
required: true, required: true,
component: {
type: 'string',
},
}, },
{ {
interface: 'string',
type: 'string',
name: 'name',
createOnly: true,
title: '标识',
unique: true,
required: true,
developerMode: true,
component: {
type: 'string',
},
},
{
interface: 'textarea',
type: 'text',
name: 'description',
title: '数据表描述',
component: {
type: 'textarea',
},
},
// {
// interface: 'boolean',
// type: 'virtual',
// name: 'createdAt',
// title: '记录创建时间',
// developerMode: true,
// defaultValue: true,
// component: {
// type: 'checkbox',
// default: true,
// showInForm: true,
// },
// },
// {
// interface: 'boolean',
// type: 'virtual',
// name: 'updatedAt',
// title: '记录修改时间',
// developerMode: true,
// defaultValue: true,
// component: {
// type: 'checkbox',
// default: true,
// showInForm: true,
// },
// },
{
interface: 'boolean',
type: 'virtual',
name: 'createdBy',
title: '记录创建人信息',
developerMode: true,
component: {
type: 'checkbox',
default: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'updatedBy',
title: '记录修改人信息',
developerMode: true,
component: {
type: 'checkbox',
default: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'developerMode',
title: '开发者模式',
developerMode: true,
defaultValue: false,
component: {
type: 'boolean',
},
},
{
interface: 'json',
type: 'json', type: 'json',
name: 'options', name: 'options',
title: '配置信息',
defaultValue: {}, defaultValue: {},
component: {
type: 'hidden',
},
}, },
{ {
interface: 'boolean',
type: 'boolean',
name: 'internal',
title: '系统内置',
defaultValue: false,
developerMode: true,
component: {
type: 'boolean',
},
},
{
interface: 'linkTo',
type: 'hasMany', type: 'hasMany',
name: 'fields', name: 'fields',
title: '字段',
sourceKey: 'name', sourceKey: 'name',
draggable: true,
actions: {
list: {
sort: 'sort',
},
get: {
fields: {
appends: ['children'],
},
},
},
component: {
type: 'drawerSelect',
},
},
{
interface: 'linkTo',
type: 'hasMany',
name: 'actions',
title: '动作',
sourceKey: 'name',
draggable: true,
actions: {
list: {
sort: 'sort',
},
},
component: {
type: 'drawerSelect',
},
},
{
interface: 'linkTo',
type: 'hasMany',
name: 'views_v2',
target: 'views_v2',
title: '视图',
sourceKey: 'name',
draggable: true,
// actions: {
// list: {
// sort: 'sort',
// },
// destroy: {
// filter: {
// default: false
// }
// }
// },
component: {
type: 'drawerSelect',
},
},
{
interface: 'linkTo',
type: 'hasMany',
name: 'scopes',
target: 'scopes',
title: '数据范围',
sourceKey: 'name',
actions: {
list: {
sort: 'id',
},
update: {
filter: {
locked: false
}
},
destroy: {
filter: {
locked: false
}
}
},
component: {
type: 'drawerSelect',
},
},
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
},
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '全部数据',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: ['title', 'description'],
detailsOpenMode: 'window', // window
details: ['descriptions', 'fields', 'views'],
sort: ['id'],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: ['title', 'description'],
},
{
developerMode: true,
type: 'descriptions',
name: 'descriptions',
title: '详情',
fields: ['title', 'description'],
actions: [
{
name: 'update',
type: 'update',
title: '编辑',
viewName: 'form',
},
],
},
{
developerMode: true,
type: 'table',
name: 'permissions_table',
title: '权限表格',
labelField: 'title',
actions: [],
fields: ['title'],
detailsOpenMode: 'drawer', // window
details: ['permissions_form'],
sort: ['id'],
},
{
developerMode: true,
type: 'form',
name: 'permissions_form',
title: '权限表单',
fields: [
{
interface: 'json',
type: 'json',
title: '数据操作权限',
name: 'actions',
component: {
"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": []
}
},
},
{
interface: 'json',
type: 'json',
title: '字段权限',
name: 'fields',
component: {
"type": "permissions.fields",
"x-linkages": [{
"type": "value:schema",
"target": "fields",
"schema": {
"x-component-props": {
"resourceKey": "{{ $form.values && $form.values.resourceKey }}"
}
}
}],
"x-component-props": {
"dataSource": []
}
},
},
],
},
{
developerMode: true,
type: 'table',
dataSourceType: 'association',
name: 'fields',
title: '字段',
targetViewName: 'table2',
targetFieldName: 'fields',
},
{
developerMode: true,
type: 'table',
dataSourceType: 'association',
name: 'views',
title: '视图',
targetViewName: 'table',
targetFieldName: 'views_v2',
}, },
], ],
} as TableOptions; } as TableOptions;

View File

@ -1,690 +1,55 @@
import { TableOptions } from '@nocobase/database'; import { TableOptions } from '@nocobase/database';
import { types, getOptions } from '../interfaces'; import { DataTypes } from 'sequelize';
export default { export default {
name: 'fields', name: 'fields',
title: '字段配置', title: '字段配置',
internal: true, model: 'Field',
draggable: true,
model: 'FieldModel',
developerMode: true,
fields: [ fields: [
{ {
interface: 'sort', type: 'uid',
type: 'sort', name: 'key',
name: 'sort', primaryKey: true,
scope: ['collection'], prefix: 'f_',
title: '排序', // autoIncrement: false,
component: { // defaultValue: DataTypes.UUIDV4,
type: 'sort',
className: 'drag-visible',
width: 60,
showInTable: true,
},
}, },
{ {
interface: 'string',
type: 'string',
name: 'title',
title: '字段名称',
required: true,
component: {
type: 'string',
className: 'drag-visible',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'string',
type: 'string', type: 'string',
name: 'name', name: 'name',
title: '标识',
required: true,
createOnly: true,
developerMode: true,
component: {
type: 'string',
showInTable: true,
showInDetail: true,
showInForm: true,
},
}, },
{ {
interface: 'select',
type: 'string', type: 'string',
name: 'interface', name: 'interface',
title: '字段类型',
required: true,
dataSource: getOptions(),
createOnly: true,
component: {
type: 'select',
showInTable: true,
showInDetail: true,
showInForm: true,
"x-linkages": [
// TODO(draft): 统一解决字段类型和配置参数联动的一种方式
// {
// type: 'value:schema',
// target: 'options',
// schema: {
// 'x-component-props': {
// fields: '{{ $self.values[1].fields || [] }}'
// }
// },
// condition: '{{ !!$self.value }}'
// },
{
"type": "value:visible",
"target": "precision",
"condition": "{{ ['number', 'percent'].indexOf($self.value) !== -1 }}"
}, },
{ {
"type": "value:visible",
"target": "dataSource",
"condition": "{{ ['select', 'multipleSelect', 'radio', 'checkboxes'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "dateFormat",
"condition": "{{ ['datetime', 'createdAt', 'updatedAt'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "showTime",
"condition": "{{ ['datetime', 'createdAt', 'updatedAt'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "timeFormat",
"condition": "{{ ['time'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "multiple",
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "target",
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
},
// {
// "type": "value:visible",
// "target": "labelField",
// "condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
// },
{
"type": "value:visible",
"target": "createable",
"condition": "{{ ['linkTo'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "children",
"condition": "{{ ['subTable'].indexOf($self.value) !== -1 }}"
},
{
"type": "value:visible",
"target": "component.showInTable",
"condition": "{{ ['subTable', 'description'].indexOf($self.value) === -1 }}"
},
{
"type": "value:visible",
"target": "component.showInForm",
"condition": "{{ ['createdAt', 'updatedAt', 'createdBy', 'updatedBy'].indexOf($self.value) === -1 }}"
},
{
"type": "value:visible",
"target": "required",
"condition": "{{ ['createdAt', 'updatedAt', 'createdBy', 'updatedBy'].indexOf($self.value) === -1 }}"
},
{
"type": "value:visible",
"target": "maxLevel",
"condition": "{{ ['chinaRegion'].includes($self.value) }}"
},
{
"type": "value:visible",
"target": "incompletely",
"condition": "{{ ['chinaRegion'].includes($self.value) }}"
},
],
},
},
// TODO(draft): 将 options 作为集合字段开放出来,可以动态的解决字段参数的配置表单联动问题
// {
// interface: 'json',
// type: 'json',
// name: 'options',
// title: '配置信息',
// defaultValue: {},
// component: {
// type: 'subFields',
// showInForm: true,
// },
// },
{
interface: 'subTable',
type: 'virtual',
name: 'dataSource',
title: '可选项',
component: {
type: 'table',
default: [{}],
// showInTable: true,
// showInDetail: true,
showInForm: true,
items: {
type: 'object',
properties: {
value: {
type: "string",
title: "值",
required: true
},
label: {
type: "string",
title: "选项",
required: true
},
},
},
},
},
{
interface: 'string',
type: 'string', type: 'string',
name: 'type', name: 'dataType',
title: '数据类型',
developerMode: true,
component: {
type: 'string',
showInTable: true,
showInDetail: true,
showInForm: true,
},
}, },
{ {
interface: 'number',
type: 'integer',
name: 'parent_id',
title: '所属分组',
component: {
type: 'number',
},
},
{
interface: 'select',
type: 'virtual',
name: 'precision',
title: '精度',
dataSource: [
{ value: 0, label: '1' },
{ value: 1, label: '1.0' },
{ value: 2, label: '1.00' },
{ value: 3, label: '1.000' },
{ value: 4, label: '1.0000' },
],
component: {
type: 'number',
showInForm: true,
default: 0,
},
},
{
interface: 'select',
type: 'virtual',
name: 'dateFormat',
title: '日期格式',
dataSource: [
{ value: 'YYYY/MM/DD', label: '年/月/日' },
{ value: 'YYYY-MM-DD', label: '年-月-日' },
{ value: 'DD/MM/YYYY', label: '日/月/年' },
],
component: {
type: 'string',
showInForm: true,
default: 'YYYY-MM-DD',
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'showTime',
title: '显示时间',
component: {
type: 'boolean',
showInForm: true,
default: false,
"x-linkages": [
{
"type": "value:visible",
"target": "timeFormat",
"condition": "{{ ($form.values && $form.values.interface === 'time') || $self.value === true }}"
},
],
},
},
{
interface: 'select',
type: 'virtual',
name: 'timeFormat',
title: '时间格式',
dataSource: [
{ value: 'HH:mm:ss', label: '24小时制' },
{ value: 'hh:mm:ss a', label: '12小时制' },
],
component: {
type: 'string',
showInForm: true,
default: 'HH:mm:ss',
},
},
// TODO(refactor): 此部分类型相关的参数,后期应拆分出去
{
name: 'maxLevel',
title: '可选层级',
interface: 'radio',
type: 'virtual',
dataSource: [
{ value: 1, label: '省' },
{ value: 2, label: '市' },
{ value: 3, label: '区/县' },
{ value: 4, label: '乡镇/街道' },
{ value: 5, label: '村/居委会' },
],
component: {
showInForm: true,
default: 3
}
},
{
name: 'incompletely',
title: '可部分选择',
interface: 'boolean',
type: 'virtual',
component: {
showInForm: true,
}
},
{
interface: 'linkTo',
multiple: false,
type: 'belongsTo',
name: 'parent',
title: '所属分组',
target: 'fields',
foreignKey: 'parent_id',
targetKey: 'id',
component: {
type: 'drawerSelect',
},
},
{
interface: 'string',
type: 'virtual',
name: 'target',
title: '要关联的数据表',
required: true,
createOnly: true,
component: {
type: 'remoteSelect',
showInDetail: true,
showInForm: true,
'x-component-props': {
mode: 'simple',
resourceName: 'collections',
labelField: 'title',
valueField: 'name',
},
"x-linkages": [
{
type: "value:state",
target: "labelField",
condition: "{{ $self.inputed }}",
state: {
value: null,
}
},
{
"type": "value:visible",
"target": "labelField",
"condition": "{{ !!$self.value }}"
},
{
type: "value:schema",
target: "labelField",
// condition: "{{ $self.value }}",
schema: {
"x-component-props": {
"associatedKey": "{{ $self.value }}"
},
},
},
{
type: 'value:visible',
target: 'component.x-component-props.filter',
condition: '{{ !!$self.value }}'
},
{
type: "value:schema",
target: "component.x-component-props.filter",
schema: {
"x-component-props": {
"associatedKey": "{{ $self.value }}"
},
},
},
],
},
},
{
interface: 'string',
type: 'virtual',
name: 'labelField',
title: '要显示的字段',
required: true,
component: {
type: 'remoteSelect',
'x-component-props': {
mode: 'simple',
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
},
showInDetail: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'multiple',
title: '允许添加多条记录',
component: {
type: 'checkbox',
showInDetail: true,
showInForm: true,
default: true,
},
},
{
name: 'component.x-component-props.filter',
interface: 'json',
type: 'virtual',
title: '数据范围',
component: {
type: 'filter',
'x-component-props': {
resourceName: 'collections.fields',
},
showInForm: true,
}
},
{
interface: 'boolean',
type: 'virtual',
name: 'createable',
title: '允许直接在关联的数据表内新建数据',
component: {
type: 'checkbox',
showInDetail: true,
showInForm: true,
},
},
{
interface: 'subTable',
type: 'hasMany', type: 'hasMany',
name: 'children', name: 'children',
target: 'fields', target: 'fields',
sourceKey: 'id', sourceKey: 'key',
foreignKey: 'parent_id', foreignKey: 'parentKey',
title: '子表格字段',
viewName: 'table2',
// visible: true,
component: {
type: 'subTable',
default: [],
// showInTable: true,
// showInDetail: true,
showInForm: true,
'x-linkages': [
{
type: 'value:schema',
target: 'children',
schema: {
'x-component-props': {
associatedKey: "{{ $form.values && $form.values.id }}"
},
},
},
],
},
},
// {
// interface: 'linkTo',
// multiple: true,
// type: 'hasMany',
// name: 'children',
// title: '子字段',
// target: 'fields',
// foreignKey: 'parent_id',
// sourceKey: 'id',
// component: {
// type: 'drawerSelect',
// },
// },
{
interface: 'textarea',
type: 'virtual',
name: 'component.tooltip',
title: '提示信息',
component: {
type: 'textarea',
showInDetail: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'boolean',
name: 'required',
title: '必填项',
component: {
type: 'checkbox',
showInTable: true,
showInDetail: true,
showInForm: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'component.showInTable',
title: '显示在表格中',
component: {
type: 'checkbox',
tooltip: '若勾选,该字段将作为一列显示在数据表里',
showInTable: true,
showInDetail: true,
showInForm: true,
default: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'component.showInForm',
title: '显示在表单中',
component: {
type: 'checkbox',
tooltip: '若勾选,该字段将出现在表单中',
showInTable: true,
showInDetail: true,
showInForm: true,
default: true,
},
},
{
interface: 'boolean',
type: 'virtual',
name: 'component.showInDetail',
title: '显示在详情中',
component: {
type: 'checkbox',
tooltip: '若勾选,该字段将出现在详情中',
showInTable: true,
showInDetail: true,
showInForm: true,
default: true,
},
}, },
{ {
interface: 'linkTo', interface: 'linkTo',
type: 'belongsTo', type: 'belongsTo',
name: 'collection', name: 'collection',
title: '所属数据表',
target: 'collections', target: 'collections',
targetKey: 'name', targetKey: 'name',
labelField: 'title',
component: {
type: 'drawerSelect',
// showInTable: true,
'x-component-props': {
resourceName: 'collections.fields',
labelField: 'title',
valueField: 'name',
},
},
}, },
{ {
interface: 'boolean', type: 'belongsTo',
type: 'boolean', name: 'uiSchema',
name: 'developerMode', target: 'ui_schemas',
title: '开发者模式',
defaultValue: false,
component: {
type: 'boolean',
},
},
{
interface: 'json',
type: 'json',
name: 'component',
title: '前端组件',
defaultValue: {}, defaultValue: {},
component: {
type: 'hidden',
},
}, },
{ {
interface: 'json',
type: 'json', type: 'json',
name: 'options', name: 'options',
title: '配置信息',
defaultValue: {}, defaultValue: {},
component: {
type: 'hidden',
},
},
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
},
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '关联的字段',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: ['title', 'interface'],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['sort'],
},
{
developerMode: true,
type: 'table',
name: 'table2',
title: '表格',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: ['sort', 'title', 'interface'],
detailsOpenMode: 'drawer', // window
details: ['form'],
sort: ['sort'],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: [
'title',
'interface',
'dataSource',
'precision',
'dateFormat',
'showTime',
'timeFormat',
'maxLevel',
'incompletely',
'target',
'labelField',
'children',
'multiple',
// 'required',
],
}, },
], ],
} as TableOptions; } as TableOptions;

View File

@ -1,241 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import BaseModel from './base'; import { Model } from '@nocobase/database';
import Field from './field';
import { TableOptions } from '@nocobase/database';
import { SaveOptions, Op } from 'sequelize';
/** export class Collection extends Model {
*
*
* 使 3+2
* 1. id
* 2.
* 3.
* 4.
* 5.
*
* @param title
*/
export function generateCollectionName(title?: string): string {
return `t_${Date.now().toString(36)}_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
}
export interface LoadOptions {
reset?: boolean;
where?: any;
skipExisting?: boolean;
[key: string]: any;
} }
export interface MigrateOptions {
[key: string]: any;
}
export class CollectionModel extends BaseModel {
generateName() {
this.set('name', generateCollectionName());
}
/**
* name collection
*
* @param name
*/
static async findByName(name: string) {
return this.findOne({ where: { name } });
}
/**
* DOTO
* - database.table
* -
*
* @param opts
*/
async loadTableOptions(opts: any = {}) {
const options = await this.getOptions();
// const prevTable = this.database.getTable(this.get('name'));
// const prevOptions = prevTable ? prevTable.getOptions() : {};
// table 是初始化和重新初始化
const table = this.database.table(options);
// console.log({options, actions: table.getOptions()['actions']})
// 如果关系表未加载,一起处理
// const associationTableNames = [];
// for (const [key, association] of table.getAssociations()) {
// // TODO是否需要考虑重载的情况暂时是跳过处理
// if (!this.database.isDefined(association.options.target)) {
// continue;
// }
// associationTableNames.push(association.options.target);
// }
// console.log({associationTableNames});
// if (associationTableNames.length) {
// await CollectionModel.load({
// ...opts,
// where: {
// name: {
// [Op.in]: associationTableNames,
// }
// }
// });
// }
return table;
}
/**
*
*/
async migrate(options: MigrateOptions = {}) {
const { isNewRecord } = options;
const table = await this.loadTableOptions(options);
// 如果不是新增数据force 必须为 false
if (!isNewRecord) {
return await table.sync({
force: false,
alter: {
drop: false,
}
});
}
// TODO: 暂时加了个 collectionSync 解决 collection.create 的数据不清空问题
// @ts-ignore
const sync = this.sequelize.options.collectionSync;
return await table.sync(sync || {
force: false,
alter: {
drop: false,
}
});
}
async getFieldsOptions() {
const fieldsOptions = [];
const fields = await this.getFields();
for (const field of fields) {
fieldsOptions.push(await field.getOptions());
}
return fieldsOptions;
}
async getOptions(): Promise<TableOptions> {
const options: any = {
...this.get(),
actions: await this.getActions(),
fields: await this.getFieldsOptions(),
}
// @ts-ignore
// console.log(this.constructor.associations);
// @ts-ignore
if (this.constructor.hasAlias('views_v2')) {
options.views_v2 = await this.getViews_v2();
}
return options;
}
/**
* TODO
*
* @param options
*/
static async load(options: LoadOptions = {}) {
const { skipExisting = false, reset = false, where = {}, transaction } = options;
const collections = await this.findAll({
transaction,
where,
});
for (const collection of collections) {
if (skipExisting && this.database.isDefined(collection.get('name'))) {
continue;
}
await collection.loadTableOptions({
transaction,
reset,
});
}
}
static async import(data: TableOptions, options: SaveOptions = {}): Promise<CollectionModel> {
data = _.cloneDeep(data);
// @ts-ignore
const { update } = options;
let collection: CollectionModel;
if (data.name) {
collection = await this.findOne({
...options,
where: {
name: data.name,
},
});
}
if (collection) {
// @ts-ignore
await collection.update(data, options);
}
if (!collection) {
// @ts-ignore
collection = await this.create(data, options);
}
const associations = ['fields', 'actions', 'views_v2'];
for (const key of associations) {
if (!Array.isArray(data[key])) {
continue;
}
const Model = this.database.getModel(key);
if (!Model) {
continue;
}
let ids = [];
for (const index in data[key]) {
if (key === 'fields') {
ids = await Model.import(data[key], {
...options,
collectionName: collection.name,
});
continue;
}
let model;
const item = data[key][index];
if (item.name) {
model = await Model.findOne({
...options,
where: {
collection_name: collection.name,
name: item.name,
},
});
}
if (model) {
await model.update({
...item,
// sort: index+1
}, options);
}
if (!model) {
model = await Model.create(
{
...item,
// sort: index+1,
collection_name: collection.name,
},
// @ts-ignore
options
);
}
if (model) {
ids.push(model.id);
}
}
if (ids.length && collection.get('internal')) {
await collection.updateAssociations({
[key]: ids,
});
}
}
return collection;
}
}
export default CollectionModel;

View File

@ -1,263 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import BaseModel from './base'; import { Model } from '@nocobase/database';
import { FieldOptions, BELONGSTO, BELONGSTOMANY, HASMANY } from '@nocobase/database';
import * as types from '../interfaces/types';
import { merge } from '../utils';
import { BuildOptions } from 'sequelize';
import { SaveOptions, Utils } from 'sequelize';
import { generateCollectionName } from './collection';
interface FieldImportOptions extends SaveOptions { export class Field extends Model {
parentId?: number;
collectionName?: string;
}
export function generateValueName(title?: string): string {
return `${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
} }
export function generateFieldName(title?: string): string {
return `f_${Math.random().toString(36).replace('0.', '').slice(-4).padStart(4, '0')}`;
}
export class FieldModel extends BaseModel {
constructor(values: any = {}, options: any = {}) {
let data = {
...(values.options || {}),
...values,
// ..._.omit(values, 'options'),
};
const interfaceType = data.interface;
if (interfaceType) {
const { options } = types[interfaceType];
let args = [options, data];
// @ts-ignore
data = merge(...args);
if (['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(data.type)) {
// 关系字段如果没有 name相关参数都随机生成
if (!data.name) {
data.name = generateFieldName();
data.paired = true;
// 通用,关系表
if (!data.target) {
data.target = generateCollectionName();
}
// 通用,外键
if (!data.foreignKey) {
data.foreignKey = generateFieldName();
}
if (data.type !== 'belongsTo' && !data.sourceKey) {
data.sourceKey = 'id';
}
if (['belongsTo', 'belongsToMany'].includes(data.type) && !data.targetKey) {
data.targetKey = 'id';
}
// 多对多关联
if (data.type === 'belongsToMany') {
if (!data.through) {
data.through = generateCollectionName();
}
if (!data.otherKey) {
data.otherKey = generateFieldName();
}
}
}
// 有 name但是没有 target
if (!data.target) {
data.target = ['hasOne', 'belongsTo'].includes(data.type) ? Utils.pluralize(data.name) : data.name;
}
}
if (!data.name) {
data.name = generateFieldName();
}
}
// @ts-ignore
super(data, options);
}
generateName() {
this.set('name', generateFieldName());
}
async generatePairField(options) {
const { interface: control, paired, type, target, sourceKey, targetKey, foreignKey, otherKey, through, collection_name } = this.get();
if (control !== 'linkTo' || type !== 'belongsToMany' || !collection_name || !paired) {
return;
}
if (!this.database.isDefined(target)) {
return;
}
const targetTable = this.database.getTable(target);
const Field = FieldModel;
let labelField = 'id';
const targetField = await Field.findOne({
...options,
where: {
type: 'string',
collection_name: target,
},
order: [['sort', 'asc']],
});
if (targetField) {
labelField = targetField.get('name');
}
const collection = await this.getCollection(options);
let targetOptions: any = {
...types.linkTo.options,
interface: 'linkTo',
title: collection.get('title'),
collection_name: target,
options: {
paired: true,
target: collection_name,
labelField,
},
component: {
showInTable: true,
showInForm: true,
showInDetail: true,
},
};
// 暂时不处理 hasone
switch (type) {
case 'hasMany':
targetOptions.type = 'belongsTo';
targetOptions.options.targetKey = sourceKey;
targetOptions.options.foreignKey = foreignKey;
break;
case 'belongsTo':
targetOptions.type = 'hasMany';
targetOptions.options.sourceKey = targetKey;
targetOptions.options.foreignKey = foreignKey;
break;
case 'belongsToMany':
targetOptions.type = 'belongsToMany';
targetOptions.options.sourceKey = targetKey;
targetOptions.options.foreignKey = otherKey;
targetOptions.options.targetKey = sourceKey;
targetOptions.options.otherKey = foreignKey;
targetOptions.options.through = through;
break;
}
const associations = targetTable.getAssociations();
// console.log(associations);
for (const association of associations.values()) {
if (association instanceof BELONGSTOMANY) {
if (
association.options.foreignKey === otherKey
&& association.options.sourceKey === targetKey
&& association.options.otherKey === foreignKey
&& association.options.targetKey === sourceKey
&& association.options.through === through
) {
return;
}
}
// if (association instanceof BELONGSTO) {
// continue;
// }
// if (association instanceof HASMANY) {
// continue;
// }
}
const f = await Field.create(targetOptions, options);
// console.log({targetOptions}, f.get('options'));
}
setInterface(value) {
const { options } = types[value];
let args = [];
// 如果是新数据或 interface 不相等interface options 放后
if (this.isNewRecord || this.get('interface') !== value) {
args = [this.get(), options];
} else {
// 已存在的数据更新不相等interface options 放前面
args = [options, this.get()];
}
// @ts-ignore
const values = merge(...args);
this.set(values);
}
async getOptions(): Promise<FieldOptions> {
return this.get();
}
async migrate(options: any = {}) {
const collectionName = this.get('collection_name');
if (!collectionName) {
return false;
}
if (!this.database.isDefined(collectionName)) {
throw new Error(`${collectionName} is not defined`);
}
const table = this.database.getTable(collectionName);
table.addField(await this.getOptions());
await table.sync({
force: false,
alter: {
drop: false,
}
});
}
static async import(items: any, options: FieldImportOptions = {}): Promise<any> {
const { parentId, collectionName } = options;
if (!Array.isArray(items)) {
items = [items];
}
const ids = [];
for (const index in items) {
const item = items[index];
let model;
const where: any = {};
if (parentId) {
where.parent_id = parentId
} else {
where.collection_name = collectionName;
}
if (item.name) {
model = await this.findOne({
...options,
where: {
...where,
name: item.name,
},
});
}
if (!model) {
const tmp: any = {};
if (parentId) {
tmp.parent_id = parentId
} else {
tmp.collection_name = collectionName;
}
model = await this.create(
{
...item,
...tmp,
},
//@ts-ignore
options
);
} else {
//@ts-ignore
await model.update(item, options);
}
if (Array.isArray(item.children)) {
const childrenIds = await this.import(item.children, {
...options,
parentId: model.id,
collectionName,
});
await model.updateAssociations({
children: childrenIds,
}, options);
}
}
return ids;
}
}
export default FieldModel;

View File

@ -1,7 +1,2 @@
export * from './base';
export * from './action';
export * from './collection'; export * from './collection';
export * from './field'; export * from './field';
export * from './tab';
export * from './view';
export * from './page';

View File

@ -1,77 +1,19 @@
import path from 'path'; import path from 'path';
import { Application } from '@nocobase/server'; import { Application } from '@nocobase/server';
import hooks from './hooks';
import { registerModels, Table } from '@nocobase/database'; import { registerModels, Table } from '@nocobase/database';
import * as models from './models'; import * as models from './models';
export default async function (this: Application, options = {}) { export default async function (this: Application, options = {}) {
const database = this.database; const database = this.database;
const resourcer = this.resourcer;
// 提供全局的 models 注册机制
registerModels(models); registerModels(models);
database.import({ database.import({
directory: path.resolve(__dirname, 'collections'), directory: path.resolve(__dirname, 'collections'),
}); });
database.addHook('afterUpdateAssociations', async function (model, options) { database.getModel('fields').beforeCreate((model) => {
if (model instanceof models.FieldModel) { if (!model.get('name')) {
if (model.get('interface') === 'subTable') { model.set('name', model.get('key'));
const { migrate = true } = options;
const Collection = model.database.getModel('collections');
await Collection.load({ ...options, where: { name: model.get('collection_name') } });
migrate && await model.migrate(options);
}
}
});
Object.keys(hooks).forEach(modelName => {
const Model = database.getModel(modelName);
Object.keys(hooks[modelName]).forEach(hookKey => {
// TODO(types): 多层 map 映射类型定义较为复杂,暂时忽略
// @ts-ignore
Model.addHook(hookKey, hooks[modelName][hookKey]);
});
});
const Collection = database.getModel('collections');
Collection.addHook('afterCreate', async (model: any, options) => {
if (model.get('developerMode')) {
return;
}
if (model.get('statusable') === false) {
return;
}
console.log("model.get('developerMode')", model.get('name'));
const { transaction = await model.sequelize.transaction() } = options;
await model.createField({
interface: 'radio',
name: 'status',
type: 'string',
filterable: true,
title: '状态',
// index: true,
dataSource: [
{
label: '已发布',
value: 'publish',
},
{
label: '草稿',
value: 'draft',
}
],
component: {
type: 'radio',
},
}, { transaction });
if (!options.transaction) {
await transaction.commit();
} }
}); });
} }

View File

@ -0,0 +1,7 @@
node_modules
*.log
docs
__tests__
tsconfig.json
src
.fatherrc.ts

View File

@ -0,0 +1,16 @@
{
"name": "@nocobase/plugin-routes",
"version": "0.4.0-alpha.7",
"main": "lib/index.js",
"license": "MIT",
"dependencies": {
"@nocobase/database": "^0.4.0-alpha.7",
"@nocobase/resourcer": "^0.4.0-alpha.7",
"@nocobase/server": "^0.4.0-alpha.7",
"deepmerge": "^4.2.2"
},
"devDependencies": {
"@nocobase/actions": "^0.4.0-alpha.7"
},
"gitHead": "f0b335ac30f29f25c95d7d137655fa64d8d67f1e"
}

View File

@ -0,0 +1,144 @@
import qs from 'qs';
import plugin from '../server';
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, middleware } from '@nocobase/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,
define: {
hooks: {
beforeCreate(model, options) {
},
},
},
logging: false,
sync: {
force: true,
alter: {
drop: true,
},
},
};
export async function getApp() {
const app = new Application({
database: {
...config,
hooks: {
beforeDefine(columns, model) {
model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`;
}
},
},
resourcer: {
prefix: '/api',
},
});
app.resourcer.use(middlewares.associated);
app.resourcer.registerActionHandlers({ ...actions.associate, ...actions.common });
app.registerPlugin('collections', [plugin]);
await app.loadPlugins();
await app.database.sync();
// 表配置信息存到数据库里
// const tables = app.database.getTables([]);
// for (const table of tables) {
// const Collection = app.database.getModel('collections');
// await Collection.import(table.getOptions(), { hooks: false });
// }
app.use(async (ctx, next) => {
ctx.db = app.database;
await next();
});
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<supertest.Response>;
list: (params?: ActionParams) => Promise<supertest.Response>;
create: (params?: ActionParams) => Promise<supertest.Response>;
update: (params?: ActionParams) => Promise<supertest.Response>;
destroy: (params?: ActionParams) => Promise<supertest.Response>;
[name: string]: (params?: ActionParams) => Promise<supertest.Response>;
}
export interface Agent {
resource: (name: string) => Handler;
}
export function getAgent(app: Application): Agent {
const agent = supertest.agent(app.callback());
return {
resource(name: string): any {
return new Proxy({}, {
get(target, method, receiver) {
return (params: ActionParams = {}) => {
const { associatedKey, resourceKey, values = {}, ...restParams } = params;
let url = `/api/${name}`;
if (associatedKey) {
url = `/api/${name.split('.').join(`/${associatedKey}/`)}`;
}
url += `:${method as string}`;
if (resourceKey) {
url += `/${resourceKey}`;
}
console.log(url);
if (['list', 'get'].indexOf(method as string) !== -1) {
return agent.get(`${url}?${qs.stringify(restParams)}`);
} else {
return agent.post(`${url}?${qs.stringify(restParams)}`).send(values);
}
}
}
});
}
};
}
export function getDatabase() {
return new Database({
...config,
hooks: {
beforeDefine(columns, model) {
model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`;
}
}
});
};

View File

@ -0,0 +1,82 @@
import { Agent, getAgent, getApp } from '.';
import { Application } from '@nocobase/server';
import Database from '@nocobase/database';
describe('routes', () => {
let app: Application;
let agent: Agent;
let db: Database;
beforeEach(async () => {
app = await getApp();
db = app.database;
agent = getAgent(app);
});
afterEach(() => app.database.close());
it.only('create route', async () => {
const Route = db.getModel('routes');
const item = {
path: '/admin/:name(.+)?',
component: 'AdminLayout',
title: `后台`,
uiSchema: {
name: 'menu',
},
};
console.log(Route.associations);
const route = await Route.create(item);
await route.updateAssociations(item);
});
it('create route', async () => {
const Route = db.getModel('routes');
const data = [
{
type: 'redirect',
from: '/',
to: '/admin',
exact: true,
},
{
path: '/admin/:name(.+)?',
component: 'AdminLayout',
title: `后台`,
uiSchema: {
key: 'qqzzjakwkwl',
name: 'qqzzjakwkwl',
},
},
{
component: 'AuthLayout',
children: [
{
name: 'login',
path: '/login',
component: 'DefaultPage',
title: `登录`,
uiSchema: {
key: 'dtf9j0b8p9u',
name: 'dtf9j0b8p9u',
},
},
{
name: 'register',
path: '/register',
component: 'DefaultPage',
title: `注册`,
uiSchema: {
key: '46qlxqam3xk',
name: '46qlxqam3xk',
},
},
],
},
];
for (const item of data) {
const route = await Route.create(item);
await route.updateAssociations(item);
}
});
});

View File

@ -0,0 +1,35 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'routes',
title: '路由表',
fields: [
{
type: 'uid',
name: 'key',
prefix: 'r_',
primaryKey: true,
},
{
type: 'string',
name: 'type',
},
{
type: 'hasMany',
name: 'children',
target: 'routes',
sourceKey: 'key',
foreignKey: 'parentKey',
},
{
type: 'belongsTo',
name: 'uiSchema',
target: 'ui_schemas',
},
{
type: 'json',
name: 'options',
defaultValue: {},
},
],
} as TableOptions;

View File

@ -0,0 +1 @@
export * from './route';

View File

@ -0,0 +1,6 @@
import _ from 'lodash';
import { Model } from '@nocobase/database';
export class Route extends Model {
}

View File

@ -0,0 +1,13 @@
import path from 'path';
import { Application } from '@nocobase/server';
import { registerModels } from '@nocobase/database';
import * as models from './models';
export default async function (this: Application, options = {}) {
const database = this.database;
registerModels(models);
database.import({
directory: path.resolve(__dirname, 'collections'),
});
}

View File

@ -0,0 +1,9 @@
import deepmerge from 'deepmerge';
const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray
export function merge(obj1: any, obj2: any) {
return deepmerge(obj1, obj2, {
arrayMerge: overwriteMerge,
});
}

View File

@ -0,0 +1,7 @@
node_modules
*.log
docs
__tests__
tsconfig.json
src
.fatherrc.ts

View File

@ -0,0 +1,16 @@
{
"name": "@nocobase/plugin-ui-schema",
"version": "0.4.0-alpha.7",
"main": "lib/index.js",
"license": "MIT",
"dependencies": {
"@nocobase/database": "^0.4.0-alpha.7",
"@nocobase/resourcer": "^0.4.0-alpha.7",
"@nocobase/server": "^0.4.0-alpha.7",
"deepmerge": "^4.2.2"
},
"devDependencies": {
"@nocobase/actions": "^0.4.0-alpha.7"
},
"gitHead": "f0b335ac30f29f25c95d7d137655fa64d8d67f1e"
}

View File

@ -0,0 +1,144 @@
import qs from 'qs';
import plugin from '../server';
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, middleware } from '@nocobase/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,
define: {
hooks: {
beforeCreate(model, options) {
},
},
},
logging: false,
sync: {
force: true,
alter: {
drop: true,
},
},
};
export async function getApp() {
const app = new Application({
database: {
...config,
hooks: {
beforeDefine(columns, model) {
model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`;
}
},
},
resourcer: {
prefix: '/api',
},
});
app.resourcer.use(middlewares.associated);
app.resourcer.registerActionHandlers({ ...actions.associate, ...actions.common });
app.registerPlugin('collections', [plugin]);
await app.loadPlugins();
await app.database.sync();
// 表配置信息存到数据库里
// const tables = app.database.getTables([]);
// for (const table of tables) {
// const Collection = app.database.getModel('collections');
// await Collection.import(table.getOptions(), { hooks: false });
// }
app.use(async (ctx, next) => {
ctx.db = app.database;
await next();
});
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<supertest.Response>;
list: (params?: ActionParams) => Promise<supertest.Response>;
create: (params?: ActionParams) => Promise<supertest.Response>;
update: (params?: ActionParams) => Promise<supertest.Response>;
destroy: (params?: ActionParams) => Promise<supertest.Response>;
[name: string]: (params?: ActionParams) => Promise<supertest.Response>;
}
export interface Agent {
resource: (name: string) => Handler;
}
export function getAgent(app: Application): Agent {
const agent = supertest.agent(app.callback());
return {
resource(name: string): any {
return new Proxy({}, {
get(target, method, receiver) {
return (params: ActionParams = {}) => {
const { associatedKey, resourceKey, values = {}, ...restParams } = params;
let url = `/api/${name}`;
if (associatedKey) {
url = `/api/${name.split('.').join(`/${associatedKey}/`)}`;
}
url += `:${method as string}`;
if (resourceKey) {
url += `/${resourceKey}`;
}
console.log(url);
if (['list', 'get'].indexOf(method as string) !== -1) {
return agent.get(`${url}?${qs.stringify(restParams)}`);
} else {
return agent.post(`${url}?${qs.stringify(restParams)}`).send(values);
}
}
}
});
}
};
}
export function getDatabase() {
return new Database({
...config,
hooks: {
beforeDefine(columns, model) {
model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`;
}
}
});
};

View File

@ -0,0 +1,50 @@
import { Agent, getAgent, getApp } from '.';
import { Application } from '@nocobase/server';
import Database from '@nocobase/database';
describe('ui_schemas', () => {
let app: Application;
let agent: Agent;
let db: Database;
beforeEach(async () => {
app = await getApp();
db = app.database;
agent = getAgent(app);
});
afterEach(() => app.database.close());
it('create ui_schemas', async () => {
const UISchema = db.getModel('ui_schemas');
const schema = await UISchema.create({
type: 'void',
name: 'abc',
properties: {
field1: {
type: 'string',
'x-component': 'Input',
},
field2: {
type: 'string',
'x-component': 'Input',
},
field3: {
type: 'array',
properties: {
field11: {
type: 'string',
'x-component': 'Input',
},
field12: {
type: 'string',
'x-component': 'Input',
},
},
},
},
});
const properties = await schema.getProperties();
console.log(JSON.stringify(properties, null, 2));
});
});

View File

@ -0,0 +1,38 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'ui_schemas',
title: '字段配置',
model: 'UISchema',
fields: [
{
type: 'uid',
name: 'key',
primaryKey: true,
},
{
type: 'string',
name: 'name',
},
{
type: 'string',
name: 'title',
},
{
type: 'string',
name: 'type',
},
{
type: 'json',
name: 'options',
defaultValue: {},
},
{
type: 'hasMany',
name: 'children',
target: 'ui_schemas',
sourceKey: 'key',
foreignKey: 'parentKey',
},
],
} as TableOptions;

View File

@ -0,0 +1 @@
export * from './ui-schema';

View File

@ -0,0 +1,62 @@
import _ from 'lodash';
import { Model } from '@nocobase/database';
export class UISchema extends Model {
static async create(value?: any, options?: any): Promise<any> {
// console.log({ value });
const attributes = this.toAttributes(value);
// @ts-ignore
const model: Model = await super.create(attributes, options);
if (!attributes.children) {
attributes.children = this.properties2children(attributes.properties);
await model.updateAssociation('children', attributes.children, options);
}
return model;
}
static toAttributes(value = {}): any {
const data = _.cloneDeep(value);
const keys = [
'properties',
...Object.keys(this.rawAttributes),
...Object.keys(this.associations),
];
const attrs = _.pick(data, keys);
const options = _.omit(data, keys);
return { ...attrs, options };
}
static properties2children(properties = []) {
const children = [];
for (const [name, property] of Object.entries(properties)) {
if (property.properties) {
property.children = this.properties2children(property.properties);
}
children.push({
name,
...property,
});
}
return children;
}
toProperty() {
const options = this.get('options') || {};
const data = _.omit(this.toJSON(), ['created_at', 'updated_at', 'options', 'parentKey']);
return { ...data, ...options };
}
async getProperties() {
const properties = {};
const children: UISchema[] = await this.getChildren();
for (const child of children) {
const property = child.toProperty();
const childProperties = await child.getProperties();
if (Object.keys(childProperties).length) {
property['properties'] = childProperties;
}
properties[child.name] = property;
}
return properties;
}
}

View File

@ -0,0 +1,19 @@
import path from 'path';
import { Application } from '@nocobase/server';
import { registerModels, Table } from '@nocobase/database';
import * as models from './models';
export default async function (this: Application, options = {}) {
const database = this.database;
registerModels(models);
database.import({
directory: path.resolve(__dirname, 'collections'),
});
database.getModel('ui_schemas').beforeCreate((model) => {
if (!model.get('name')) {
model.set('name', model.get('key'));
}
});
}

View File

@ -0,0 +1,9 @@
import deepmerge from 'deepmerge';
const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray
export function merge(obj1: any, obj2: any) {
return deepmerge(obj1, obj2, {
arrayMerge: overwriteMerge,
});
}

13
uid.js Normal file
View File

@ -0,0 +1,13 @@
let IDX = 36,
HEX = ''
while (IDX--) HEX += IDX.toString(36)
function uid(len = 11) {
let str = '',
num = len || 11
while (num--) str += HEX[(Math.random() * 36) | 0]
return str
}
console.log(uid());

161
yarn.lock
View File

@ -4136,7 +4136,7 @@
"@umijs/deps" "3.4.25" "@umijs/deps" "3.4.25"
"@umijs/utils" "3.4.25" "@umijs/utils" "3.4.25"
"@umijs/test@^3.0.7", "@umijs/test@^3.4.15", "@umijs/test@^3.4.25": "@umijs/test@^3.0.7", "@umijs/test@^3.4.15":
version "3.4.25" version "3.4.25"
resolved "https://registry.npmjs.org/@umijs/test/-/test-3.4.25.tgz#9df95dc25cecaee9f45cdb62a227634defb5ae87" resolved "https://registry.npmjs.org/@umijs/test/-/test-3.4.25.tgz#9df95dc25cecaee9f45cdb62a227634defb5ae87"
integrity sha512-VfaQyvF3vkHkHROS3LYQjRvx3dwk1Lv56gfgDIHGCX2n2ARSGyaFTbXsv/QafrKENDTDp3FTcTIUG6oY6mHH2g== integrity sha512-VfaQyvF3vkHkHROS3LYQjRvx3dwk1Lv56gfgDIHGCX2n2ARSGyaFTbXsv/QafrKENDTDp3FTcTIUG6oY6mHH2g==
@ -4319,14 +4319,6 @@ agentkeepalive@^3.4.1:
dependencies: dependencies:
humanize-ms "^1.2.1" humanize-ms "^1.2.1"
aggregate-error@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==
dependencies:
clean-stack "^2.0.0"
indent-string "^4.0.0"
ahooks@^2.10.2: ahooks@^2.10.2:
version "2.10.6" version "2.10.6"
resolved "https://registry.npmjs.org/ahooks/-/ahooks-2.10.6.tgz#05fad82f77a308886c174a61b7e1232cb16516ac" resolved "https://registry.npmjs.org/ahooks/-/ahooks-2.10.6.tgz#05fad82f77a308886c174a61b7e1232cb16516ac"
@ -4479,7 +4471,7 @@ ansi-escapes@^3.0.0, ansi-escapes@^3.2.0:
resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: ansi-escapes@^4.2.1:
version "4.3.2" version "4.3.2"
resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
@ -5874,11 +5866,6 @@ classnames@2.x, classnames@^2.2.0, classnames@^2.2.1, classnames@^2.2.3, classna
resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
clean-stack@^2.0.0:
version "2.2.0"
resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
cli-boxes@^2.2.0: cli-boxes@^2.2.0:
version "2.2.1" version "2.2.1"
resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
@ -5891,21 +5878,6 @@ cli-cursor@^2.1.0:
dependencies: dependencies:
restore-cursor "^2.0.0" restore-cursor "^2.0.0"
cli-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
dependencies:
restore-cursor "^3.1.0"
cli-truncate@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==
dependencies:
slice-ansi "^3.0.0"
string-width "^4.2.0"
cli-width@^2.0.0: cli-width@^2.0.0:
version "2.2.1" version "2.2.1"
resolved "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" resolved "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
@ -6128,11 +6100,6 @@ commander@^2.19.0, commander@^2.20.0:
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^6.2.0:
version "6.2.1"
resolved "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
commondir@^1.0.1: commondir@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@ -6918,7 +6885,7 @@ debug@3.X, debug@^3.1.0, debug@^3.2.7:
dependencies: dependencies:
ms "^2.1.1" ms "^2.1.1"
debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1: debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
version "4.3.1" version "4.3.1"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
@ -7513,7 +7480,7 @@ enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.1:
memory-fs "^0.5.0" memory-fs "^0.5.0"
tapable "^1.0.0" tapable "^1.0.0"
enquirer@^2.3.5, enquirer@^2.3.6: enquirer@^2.3.5:
version "2.3.6" version "2.3.6"
resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
@ -8056,7 +8023,7 @@ execa@^1.0.0:
signal-exit "^3.0.0" signal-exit "^3.0.0"
strip-eof "^1.0.0" strip-eof "^1.0.0"
execa@^4.0.0, execa@^4.1.0: execa@^4.0.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" resolved "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a"
integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==
@ -10224,11 +10191,6 @@ is-unc-path@^1.0.0:
dependencies: dependencies:
unc-path-regex "^0.1.2" unc-path-regex "^0.1.2"
is-unicode-supported@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
is-url@1.2.4: is-url@1.2.4:
version "1.2.4" version "1.2.4"
resolved "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" resolved "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
@ -11660,40 +11622,6 @@ lines-and-columns@^1.1.6:
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
lint-staged@^10.0.7:
version "10.5.4"
resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-10.5.4.tgz#cd153b5f0987d2371fc1d2847a409a2fe705b665"
integrity sha512-EechC3DdFic/TdOPgj/RB3FicqE6932LTHCUm0Y2fsD9KGlLB+RwJl2q1IYBIvEsKzDOgn0D4gll+YxG5RsrKg==
dependencies:
chalk "^4.1.0"
cli-truncate "^2.1.0"
commander "^6.2.0"
cosmiconfig "^7.0.0"
debug "^4.2.0"
dedent "^0.7.0"
enquirer "^2.3.6"
execa "^4.1.0"
listr2 "^3.2.2"
log-symbols "^4.0.0"
micromatch "^4.0.2"
normalize-path "^3.0.0"
please-upgrade-node "^3.2.0"
string-argv "0.3.1"
stringify-object "^3.3.0"
listr2@^3.2.2:
version "3.10.0"
resolved "https://registry.npmjs.org/listr2/-/listr2-3.10.0.tgz#58105a53ed7fa1430d1b738c6055ef7bb006160f"
integrity sha512-eP40ZHihu70sSmqFNbNy2NL1YwImmlMmPh9WO5sLmPDleurMHt3n+SwEWNu2kzKScexZnkyFtc1VI0z/TGlmpw==
dependencies:
cli-truncate "^2.1.0"
colorette "^1.2.2"
log-update "^4.0.0"
p-map "^4.0.0"
rxjs "^6.6.7"
through "^2.3.8"
wrap-ansi "^7.0.0"
load-json-file@^1.0.0: load-json-file@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@ -11897,24 +11825,6 @@ lodash@4.x, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.15, lodash@^4.17.19,
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log-symbols@^4.0.0:
version "4.1.0"
resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
dependencies:
chalk "^4.1.0"
is-unicode-supported "^0.1.0"
log-update@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==
dependencies:
ansi-escapes "^4.3.0"
cli-cursor "^3.1.0"
slice-ansi "^4.0.0"
wrap-ansi "^6.2.0"
long-timeout@0.1.1: long-timeout@0.1.1:
version "0.1.1" version "0.1.1"
resolved "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" resolved "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514"
@ -13467,13 +13377,6 @@ p-map@^2.1.0:
resolved "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" resolved "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
p-map@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==
dependencies:
aggregate-error "^3.0.0"
p-pipe@^1.2.0: p-pipe@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz#4b1a11399a11520a67790ee5a0c1d5881d6befe9" resolved "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz#4b1a11399a11520a67790ee5a0c1d5881d6befe9"
@ -13965,13 +13868,6 @@ platform@^1.3.1:
resolved "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" resolved "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7"
integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==
please-upgrade-node@^3.2.0:
version "3.2.0"
resolved "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"
integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==
dependencies:
semver-compare "^1.0.0"
plugin-error@^0.1.2: plugin-error@^0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz#3b9bb3335ccf00f425e07437e19276967da47ace" resolved "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz#3b9bb3335ccf00f425e07437e19276967da47ace"
@ -16383,14 +16279,6 @@ restore-cursor@^2.0.0:
onetime "^2.0.0" onetime "^2.0.0"
signal-exit "^3.0.2" signal-exit "^3.0.2"
restore-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
dependencies:
onetime "^5.1.0"
signal-exit "^3.0.2"
ret@~0.1.10: ret@~0.1.10:
version "0.1.15" version "0.1.15"
resolved "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" resolved "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
@ -16754,7 +16642,7 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies: dependencies:
aproba "^1.1.1" aproba "^1.1.1"
rxjs@^6.4.0, rxjs@^6.6.7: rxjs@^6.4.0:
version "6.6.7" version "6.6.7"
resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
@ -16876,11 +16764,6 @@ select@^1.1.2:
resolved "https://registry.npmjs.org/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" resolved "https://registry.npmjs.org/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
semver-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
semver-diff@^2.0.0: semver-diff@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" resolved "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
@ -17095,15 +16978,6 @@ slash@^3.0.0:
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
slice-ansi@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787"
integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==
dependencies:
ansi-styles "^4.0.0"
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
slice-ansi@^4.0.0: slice-ansi@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
@ -17459,11 +17333,6 @@ strict-uri-encode@^2.0.0:
resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
string-argv@0.3.1:
version "0.3.1"
resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==
string-convert@^0.2.0: string-convert@^0.2.0:
version "0.2.1" version "0.2.1"
resolved "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97" resolved "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97"
@ -18021,7 +17890,7 @@ through2@^4.0.0:
dependencies: dependencies:
readable-stream "3" readable-stream "3"
through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3: through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@~2.3:
version "2.3.8" version "2.3.8"
resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
@ -18398,11 +18267,6 @@ typescript@^3.7.2:
resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8"
integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==
typescript@^4.1.2:
version "4.3.4"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.4.tgz#3f85b986945bcf31071decdd96cf8bfa65f9dcbc"
integrity sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==
ua-parser-js@^0.7.18: ua-parser-js@^0.7.18:
version "0.7.28" version "0.7.28"
resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
@ -18513,7 +18377,7 @@ umi-utils@1.7.2:
prettier "1.15.3" prettier "1.15.3"
slash2 "2.0.0" slash2 "2.0.0"
umi@^3.0.0, umi@^3.4.25: umi@^3.0.0:
version "3.4.25" version "3.4.25"
resolved "https://registry.npmjs.org/umi/-/umi-3.4.25.tgz#e1d53378ef8fa89011a8f54e7c5ff6984875b2de" resolved "https://registry.npmjs.org/umi/-/umi-3.4.25.tgz#e1d53378ef8fa89011a8f54e7c5ff6984875b2de"
integrity sha512-67GMhP40Tz2sbyAzB4at7IUjXEIlG4j3zADfRhepLRWYbhUa55ZHnWyvuws3/dtUfifpraz4ckVcJJESoJ7Xaw== integrity sha512-67GMhP40Tz2sbyAzB4at7IUjXEIlG4j3zADfRhepLRWYbhUa55ZHnWyvuws3/dtUfifpraz4ckVcJJESoJ7Xaw==
@ -19284,15 +19148,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0" string-width "^4.1.0"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrappy@1: wrappy@1:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"