diff --git a/docker-compose.yml b/docker-compose.yml index 1e05f039b..1276247ce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,13 @@ networks: nocobase: driver: bridge services: + adminer: + image: adminer + restart: always + networks: + - nocobase + ports: + - ${ADMINER_PORT}:8080 verdaccio: image: verdaccio/verdaccio networks: diff --git a/package.json b/package.json index 39b280ce8..a52ebf86a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "build2": "lerna run build", "build": "npm run build-father-build && node packages/father-build/bin/father-build.js", "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", "test": "npm run lint && jest" }, diff --git a/packages/api/package.json b/packages/api/package.json index 504f48d4c..4fbaa1919 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -19,6 +19,8 @@ "@nocobase/plugin-file-manager": "^0.4.0-alpha.7", "@nocobase/plugin-pages": "^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/server": "^0.4.0-alpha.7", "koa-static": "^5.0.0" diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts index e56d5f4d6..53adaaa33 100644 --- a/packages/api/src/app.ts +++ b/packages/api/src/app.ts @@ -39,13 +39,15 @@ const api = Api.create({ const plugins = [ '@nocobase/plugin-collections', - '@nocobase/plugin-action-logs', - '@nocobase/plugin-pages', - '@nocobase/plugin-users', - '@nocobase/plugin-file-manager', - '@nocobase/plugin-permissions', - '@nocobase/plugin-automations', - '@nocobase/plugin-china-region', + '@nocobase/plugin-routes', + '@nocobase/plugin-ui-schema', + // '@nocobase/plugin-action-logs', + // '@nocobase/plugin-pages', + // '@nocobase/plugin-users', + // '@nocobase/plugin-file-manager', + // '@nocobase/plugin-permissions', + // '@nocobase/plugin-automations', + // '@nocobase/plugin-china-region', ]; for (const plugin of plugins) { diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 855a5354e..26305231a 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -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({ root: process.env.APP_DIST, useStaticServer: !(process.env.APP_USE_STATIC_SERVER === 'false' || !process.env.APP_USE_STATIC_SERVER), diff --git a/packages/api/src/migrations/init.ts b/packages/api/src/migrations/init.ts index c2250fa36..6b28fe18f 100644 --- a/packages/api/src/migrations/init.ts +++ b/packages/api/src/migrations/init.ts @@ -8,57 +8,7 @@ global.sync = { import Database from '@nocobase/database'; import api from '../app'; - -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, - }, -]; +import * as uiSchema from './ui-schema'; (async () => { await api.loadPlugins(); @@ -66,196 +16,47 @@ const data = [ await database.sync({ // 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({ - 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 Route = database.getModel('routes'); - const Action = database.getModel('actions'); - // 全局 - await Action.bulkCreate([ - ]); - - // 导入地域数据 - const ChinaRegion = database.getModel('china_regions'); - ChinaRegion && await ChinaRegion.importData(); - - const Menu = database.getModel('menus'); - - const menus = [ + const data = [ { - title: '仪表盘', - icon: 'DashboardOutlined', - type: 'group', + type: 'redirect', + from: '/', + to: '/admin', + exact: true, + }, + { + path: '/admin/:name(.+)?', + component: 'AdminLayout', + title: `后台`, + uiSchema: uiSchema.menu, + }, + { + component: 'AuthLayout', children: [ { - title: '欢迎光临', - icon: 'DatabaseOutlined', - type: 'page', - views: [], - name: 'welcome', - }, - ], - }, - { - title: '数据', - icon: 'DatabaseOutlined', - type: 'group', - children: [], - }, - { - 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: 'login', + path: '/login', + component: 'DefaultPage', + title: `登录`, + uiSchema: uiSchema.login, }, { - 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, + name: 'register', + path: '/register', + component: 'DefaultPage', + title: `注册`, + uiSchema: uiSchema.register, }, ], }, ]; - for (const item of menus) { - const menu = await Menu.create(item); - await menu.updateAssociations(item); + for (const item of data) { + const route = await Route.create(item); + await route.updateAssociations(item); } + await database.close(); })(); diff --git a/packages/api/src/migrations/select.ts b/packages/api/src/migrations/select.ts deleted file mode 100644 index e9265f59c..000000000 --- a/packages/api/src/migrations/select.ts +++ /dev/null @@ -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); - } - } -})(); diff --git a/packages/api/src/migrations/sync.ts b/packages/api/src/migrations/sync.ts deleted file mode 100644 index 90dba33aa..000000000 --- a/packages/api/src/migrations/sync.ts +++ /dev/null @@ -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 }); - } -})(); diff --git a/packages/api/src/migrations/ui-schema/index.ts b/packages/api/src/migrations/ui-schema/index.ts new file mode 100644 index 000000000..6bef8bfe2 --- /dev/null +++ b/packages/api/src/migrations/ui-schema/index.ts @@ -0,0 +1,3 @@ +export * from './login'; +export * from './menu'; +export * from './register'; diff --git a/packages/api/src/migrations/ui-schema/login.ts b/packages/api/src/migrations/ui-schema/login.ts new file mode 100644 index 000000000..d06e4c780 --- /dev/null +++ b/packages/api/src/migrations/ui-schema/login.ts @@ -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: '注册账号', + }, + }, + }, + }, +}; diff --git a/packages/api/src/migrations/ui-schema/menu.ts b/packages/api/src/migrations/ui-schema/menu.ts new file mode 100644 index 000000000..7a5382a4f --- /dev/null +++ b/packages/api/src/migrations/ui-schema/menu.ts @@ -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 }}', + }, +}; diff --git a/packages/api/src/migrations/ui-schema/register.ts b/packages/api/src/migrations/ui-schema/register.ts new file mode 100644 index 000000000..a129f5e3a --- /dev/null +++ b/packages/api/src/migrations/ui-schema/register.ts @@ -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: '使用已有账号登录', + }, + }, + }, + }, +} diff --git a/packages/client/src/demos/api/index.ts b/packages/client/src/demos/api/index.ts index b4b0d6a45..c73f53e15 100644 --- a/packages/client/src/demos/api/index.ts +++ b/packages/client/src/demos/api/index.ts @@ -1,18 +1,20 @@ const api = { '/api/routes:getAccessible': require('./routes-getAccessible').default, - '/api/blocks:getSchema/login': require('./blocks-getSchema/login').default, - '/api/blocks:getSchema/register': require('./blocks-getSchema/register').default, - '/api/blocks:getSchema/item1': require('./blocks-getSchema/item1').default, - '/api/blocks:getSchema/item2': require('./blocks-getSchema/item2').default, - '/api/blocks:getSchema/item22': require('./blocks-getSchema/item22').default, - '/api/blocks:getSchema/item3': require('./blocks-getSchema/item3').default, - '/api/blocks:getSchema/item4': require('./blocks-getSchema/item4').default, - '/api/blocks:getSchema/item5': require('./blocks-getSchema/item5').default, - '/api/blocks:getSchema/menu': require('./blocks-getSchema/menu').default, + // '/api/blocks:getSchema/login': require('./blocks-getSchema/login').default, + // '/api/blocks:getSchema/register': require('./blocks-getSchema/register').default, + // '/api/blocks:getSchema/item1': require('./blocks-getSchema/item1').default, + // '/api/blocks:getSchema/item2': require('./blocks-getSchema/item2').default, + // '/api/blocks:getSchema/item22': require('./blocks-getSchema/item22').default, + // '/api/blocks:getSchema/item3': require('./blocks-getSchema/item3').default, + // '/api/blocks:getSchema/item4': require('./blocks-getSchema/item4').default, + // '/api/blocks:getSchema/item5': require('./blocks-getSchema/item5').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) { - console.log({ service }) let url = null; if (typeof service === 'string') { url = service; @@ -20,6 +22,8 @@ export function request(service) { url = service.url; } + console.log('request.url', url) + return new Promise((resolve, reject) => { setTimeout(() => { if (api[url]) { diff --git a/packages/client/src/demos/api/routes-getAccessible.ts b/packages/client/src/demos/api/routes-getAccessible.ts index a0c18ec5f..ac4f51b42 100644 --- a/packages/client/src/demos/api/routes-getAccessible.ts +++ b/packages/client/src/demos/api/routes-getAccessible.ts @@ -17,7 +17,7 @@ export default [ path: '/admin/:name(.+)?', component: 'AdminLayout', title: `后台 - ${Mock.mock('@string')}`, - blockId: 'menu', + schemaName: 'menu', }, { component: 'AuthLayout', @@ -27,14 +27,14 @@ export default [ path: '/login', component: 'DefaultPage', title: `登录 - ${Mock.mock('@string')}`, - blockId: 'login', + schemaName: 'login', }, { name: 'register', path: '/register', component: 'DefaultPage', title: `注册 - ${Mock.mock('@string')}`, - blockId: 'register', + schemaName: 'register', }, ], }, diff --git a/packages/client/src/demos/api/blocks-getSchema/item1.ts b/packages/client/src/demos/api/ui-schemas-getTree/item1.ts similarity index 100% rename from packages/client/src/demos/api/blocks-getSchema/item1.ts rename to packages/client/src/demos/api/ui-schemas-getTree/item1.ts diff --git a/packages/client/src/demos/api/blocks-getSchema/item2.ts b/packages/client/src/demos/api/ui-schemas-getTree/item2.ts similarity index 100% rename from packages/client/src/demos/api/blocks-getSchema/item2.ts rename to packages/client/src/demos/api/ui-schemas-getTree/item2.ts diff --git a/packages/client/src/demos/api/blocks-getSchema/item22.ts b/packages/client/src/demos/api/ui-schemas-getTree/item22.ts similarity index 100% rename from packages/client/src/demos/api/blocks-getSchema/item22.ts rename to packages/client/src/demos/api/ui-schemas-getTree/item22.ts diff --git a/packages/client/src/demos/api/blocks-getSchema/item3.ts b/packages/client/src/demos/api/ui-schemas-getTree/item3.ts similarity index 100% rename from packages/client/src/demos/api/blocks-getSchema/item3.ts rename to packages/client/src/demos/api/ui-schemas-getTree/item3.ts diff --git a/packages/client/src/demos/api/blocks-getSchema/item4.ts b/packages/client/src/demos/api/ui-schemas-getTree/item4.ts similarity index 100% rename from packages/client/src/demos/api/blocks-getSchema/item4.ts rename to packages/client/src/demos/api/ui-schemas-getTree/item4.ts diff --git a/packages/client/src/demos/api/blocks-getSchema/item5.ts b/packages/client/src/demos/api/ui-schemas-getTree/item5.ts similarity index 100% rename from packages/client/src/demos/api/blocks-getSchema/item5.ts rename to packages/client/src/demos/api/ui-schemas-getTree/item5.ts diff --git a/packages/client/src/demos/api/blocks-getSchema/login.ts b/packages/client/src/demos/api/ui-schemas-getTree/login.ts similarity index 100% rename from packages/client/src/demos/api/blocks-getSchema/login.ts rename to packages/client/src/demos/api/ui-schemas-getTree/login.ts diff --git a/packages/client/src/demos/api/blocks-getSchema/menu.ts b/packages/client/src/demos/api/ui-schemas-getTree/menu.ts similarity index 100% rename from packages/client/src/demos/api/blocks-getSchema/menu.ts rename to packages/client/src/demos/api/ui-schemas-getTree/menu.ts diff --git a/packages/client/src/demos/api/blocks-getSchema/register.ts b/packages/client/src/demos/api/ui-schemas-getTree/register.ts similarity index 100% rename from packages/client/src/demos/api/blocks-getSchema/register.ts rename to packages/client/src/demos/api/ui-schemas-getTree/register.ts diff --git a/packages/client/src/schemas/action/index.tsx b/packages/client/src/schemas/action/index.tsx index 5e44180ac..d249397f2 100644 --- a/packages/client/src/schemas/action/index.tsx +++ b/packages/client/src/schemas/action/index.tsx @@ -212,6 +212,7 @@ Action.Popover = observer((props) => { const schema = useFieldSchema(); return ( { setVisible(visible); diff --git a/packages/client/src/schemas/table/demos/demo1.tsx b/packages/client/src/schemas/table/demos/demo1.tsx index a29b10d17..c69f6ef7c 100644 --- a/packages/client/src/schemas/table/demos/demo1.tsx +++ b/packages/client/src/schemas/table/demos/demo1.tsx @@ -58,10 +58,46 @@ const schema = { 'x-component': 'Form', properties: { filter: { - type: 'string', - title: '字段1', - 'x-decorator': 'FormItem', - 'x-component': 'Input', + name: 'filter', + type: 'object', + 'x-component': 'Filter', + properties: { + column1: { + type: 'void', + title: '字段1', + 'x-component': 'Filter.Column', + 'x-component-props': { + operations: [ + { label: '等于', value: 'eq' }, + { label: '不等于', value: 'ne' }, + ], + }, + properties: { + field1: { + type: 'string', + '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: { type: 'void', diff --git a/packages/client/src/templates/admin-layout/index.tsx b/packages/client/src/templates/admin-layout/index.tsx index 85cd507a0..a3602ed02 100644 --- a/packages/client/src/templates/admin-layout/index.tsx +++ b/packages/client/src/templates/admin-layout/index.tsx @@ -195,7 +195,7 @@ function LayoutWithMenu({ schema }) { function Content({ activeKey }) { const { data = {}, loading } = useRequest( - `/api/blocks:getSchema/${activeKey}`, + `/api/ui-schemas:getTree/${activeKey}?filter[parentId]=${activeKey}`, { refreshDeps: [activeKey], formatResult: (result) => result?.data, @@ -212,12 +212,13 @@ function Content({ activeKey }) { export function AdminLayout({ route, children }: any) { const { data = {}, loading } = useRequest( - `/api/blocks:getSchema/${route.blockId}`, + `/api/ui-schemas:getTree/${route.schemaName}`, { refreshDeps: [route], formatResult: (result) => result?.data, }, ); + if (loading) { return ; } diff --git a/packages/client/src/templates/default-page/index.tsx b/packages/client/src/templates/default-page/index.tsx index f8713a8c0..a03faf6f7 100644 --- a/packages/client/src/templates/default-page/index.tsx +++ b/packages/client/src/templates/default-page/index.tsx @@ -6,7 +6,7 @@ import { SchemaRenderer } from '../../schemas'; export function DefaultPage({ route }) { const { data = {}, loading } = useRequest( - `/api/blocks:getSchema/${route.blockId}`, + `/api/ui-schemas:getTree/${route.schemaName}`, { refreshDeps: [route], formatResult: (result) => result?.data, diff --git a/packages/database/src/__tests__/model/updateAssociations2.test.ts b/packages/database/src/__tests__/model/updateAssociations2.test.ts new file mode 100644 index 000000000..4294bd49c --- /dev/null +++ b/packages/database/src/__tests__/model/updateAssociations2.test.ts @@ -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(); + }); +}); diff --git a/packages/database/src/fields/field-types.ts b/packages/database/src/fields/field-types.ts index 3583292e3..8e54f459f 100644 --- a/packages/database/src/fields/field-types.ts +++ b/packages/database/src/fields/field-types.ts @@ -18,7 +18,7 @@ import { getDataTypeKey } from '.'; import Table from '../table'; import Database from '../database'; import Model, { ModelCtor } from '../model'; -import { whereCompare, isNumber } from '../utils'; +import { whereCompare, isNumber, uid } from '../utils'; export interface IField { @@ -344,7 +344,7 @@ export class ARRAY extends Column { export class JSON extends Column { public getDataType() { - return DataTypes.JSONB; + return DataTypes.JSON; } } @@ -352,6 +352,33 @@ export class JSONB 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 { diff --git a/packages/database/src/model.ts b/packages/database/src/model.ts index ac4308522..0e194a6de 100644 --- a/packages/database/src/model.ts +++ b/packages/database/src/model.ts @@ -310,17 +310,22 @@ export abstract class Model extends SequelizeModel { ? association.options.targetKey : association.options.sourceKey; if (data[targetAttribute]) { - await this[accessors.set](data[targetAttribute], opts); - if (Object.keys(data).length > 1) { + if (Object.keys(data).length > 0) { const target = await Target.findOne({ where: { [targetAttribute]: data[targetAttribute], }, transaction }); - await target.update(data, opts); - // @ts-ignore - await target.updateAssociations(data, opts); + if (target) { + await this[accessors.set](data[targetAttribute], opts); + await target.update(data, opts); + // @ts-ignore + await target.updateAssociations(data, opts); + } else { + const t = await this[accessors.create](data, opts); + await t.updateAssociations(data, opts); + } } } else { const t = await this[accessors.create](data, opts); @@ -513,8 +518,8 @@ export abstract class Model extends SequelizeModel { */ async updateAssociations(data: any, options: UpdateAssociationOptions = {}) { const { transaction = await this.sequelize.transaction() } = options; - const table = this.database.getTable(this.constructor.name); - for (const key of table.getAssociations().keys()) { + // @ts-ignore 判断 Model.associations 更准确 + for (const key of Object.keys(this.constructor.associations)) { // 如果 key 不存在才跳过 if (!Object.keys(data).includes(key)) { continue; diff --git a/packages/database/src/utils.ts b/packages/database/src/utils.ts index 537763874..17adda060 100644 --- a/packages/database/src/utils.ts +++ b/packages/database/src/utils.ts @@ -336,3 +336,14 @@ export function isNumber(num) { } 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 +} diff --git a/packages/plugin-collections-v04/.npmignore b/packages/plugin-collections-v04/.npmignore new file mode 100644 index 000000000..461574b2f --- /dev/null +++ b/packages/plugin-collections-v04/.npmignore @@ -0,0 +1,7 @@ +node_modules +*.log +docs +__tests__ +tsconfig.json +src +.fatherrc.ts \ No newline at end of file diff --git a/packages/plugin-collections-v04/package.json b/packages/plugin-collections-v04/package.json new file mode 100644 index 000000000..87d791eda --- /dev/null +++ b/packages/plugin-collections-v04/package.json @@ -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" +} diff --git a/packages/plugin-collections/src/__tests__/actions/collections.test.ts b/packages/plugin-collections-v04/src/__tests__/actions/collections.test.ts similarity index 100% rename from packages/plugin-collections/src/__tests__/actions/collections.test.ts rename to packages/plugin-collections-v04/src/__tests__/actions/collections.test.ts diff --git a/packages/plugin-collections-v04/src/__tests__/index.ts b/packages/plugin-collections-v04/src/__tests__/index.ts new file mode 100644 index 000000000..e0574424b --- /dev/null +++ b/packages/plugin-collections-v04/src/__tests__/index.ts @@ -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; + list: (params?: ActionParams) => Promise; + create: (params?: ActionParams) => Promise; + update: (params?: ActionParams) => Promise; + destroy: (params?: ActionParams) => Promise; + [name: string]: (params?: ActionParams) => Promise; +} + +export interface Agent { + resource: (name: string) => Handler; +} + +export function getAgent(app: Application): 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}`; + } + } + }); +}; diff --git a/packages/plugin-collections/src/__tests__/models/base.test.ts b/packages/plugin-collections-v04/src/__tests__/models/base.test.ts similarity index 100% rename from packages/plugin-collections/src/__tests__/models/base.test.ts rename to packages/plugin-collections-v04/src/__tests__/models/base.test.ts diff --git a/packages/plugin-collections/src/__tests__/models/collection.test.ts b/packages/plugin-collections-v04/src/__tests__/models/collection.test.ts similarity index 100% rename from packages/plugin-collections/src/__tests__/models/collection.test.ts rename to packages/plugin-collections-v04/src/__tests__/models/collection.test.ts diff --git a/packages/plugin-collections/src/__tests__/models/field.test.ts b/packages/plugin-collections-v04/src/__tests__/models/field.test.ts similarity index 100% rename from packages/plugin-collections/src/__tests__/models/field.test.ts rename to packages/plugin-collections-v04/src/__tests__/models/field.test.ts diff --git a/packages/plugin-collections/src/actions/getCollection.ts b/packages/plugin-collections-v04/src/actions/getCollection.ts similarity index 100% rename from packages/plugin-collections/src/actions/getCollection.ts rename to packages/plugin-collections-v04/src/actions/getCollection.ts diff --git a/packages/plugin-collections/src/actions/getView.ts b/packages/plugin-collections-v04/src/actions/getView.ts similarity index 100% rename from packages/plugin-collections/src/actions/getView.ts rename to packages/plugin-collections-v04/src/actions/getView.ts diff --git a/packages/plugin-collections/src/collections/actions.ts b/packages/plugin-collections-v04/src/collections/actions.ts similarity index 100% rename from packages/plugin-collections/src/collections/actions.ts rename to packages/plugin-collections-v04/src/collections/actions.ts diff --git a/packages/plugin-collections-v04/src/collections/collections.ts b/packages/plugin-collections-v04/src/collections/collections.ts new file mode 100644 index 000000000..23dbe2148 --- /dev/null +++ b/packages/plugin-collections-v04/src/collections/collections.ts @@ -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; diff --git a/packages/plugin-collections-v04/src/collections/fields.ts b/packages/plugin-collections-v04/src/collections/fields.ts new file mode 100644 index 000000000..81be1ed77 --- /dev/null +++ b/packages/plugin-collections-v04/src/collections/fields.ts @@ -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; diff --git a/packages/plugin-collections/src/collections/scopes.ts b/packages/plugin-collections-v04/src/collections/scopes.ts similarity index 100% rename from packages/plugin-collections/src/collections/scopes.ts rename to packages/plugin-collections-v04/src/collections/scopes.ts diff --git a/packages/plugin-collections/src/collections/tabs.ts b/packages/plugin-collections-v04/src/collections/tabs.ts similarity index 100% rename from packages/plugin-collections/src/collections/tabs.ts rename to packages/plugin-collections-v04/src/collections/tabs.ts diff --git a/packages/plugin-collections/src/collections/views.ts b/packages/plugin-collections-v04/src/collections/views.ts similarity index 100% rename from packages/plugin-collections/src/collections/views.ts rename to packages/plugin-collections-v04/src/collections/views.ts diff --git a/packages/plugin-collections/src/hooks/collections-after-create.ts b/packages/plugin-collections-v04/src/hooks/collections-after-create.ts similarity index 100% rename from packages/plugin-collections/src/hooks/collections-after-create.ts rename to packages/plugin-collections-v04/src/hooks/collections-after-create.ts diff --git a/packages/plugin-collections/src/hooks/collections-after-update.ts b/packages/plugin-collections-v04/src/hooks/collections-after-update.ts similarity index 100% rename from packages/plugin-collections/src/hooks/collections-after-update.ts rename to packages/plugin-collections-v04/src/hooks/collections-after-update.ts diff --git a/packages/plugin-collections/src/hooks/collections-before-validate.ts b/packages/plugin-collections-v04/src/hooks/collections-before-validate.ts similarity index 100% rename from packages/plugin-collections/src/hooks/collections-before-validate.ts rename to packages/plugin-collections-v04/src/hooks/collections-before-validate.ts diff --git a/packages/plugin-collections/src/hooks/fields-after-bulk-update.ts b/packages/plugin-collections-v04/src/hooks/fields-after-bulk-update.ts similarity index 100% rename from packages/plugin-collections/src/hooks/fields-after-bulk-update.ts rename to packages/plugin-collections-v04/src/hooks/fields-after-bulk-update.ts diff --git a/packages/plugin-collections/src/hooks/fields-after-create.ts b/packages/plugin-collections-v04/src/hooks/fields-after-create.ts similarity index 100% rename from packages/plugin-collections/src/hooks/fields-after-create.ts rename to packages/plugin-collections-v04/src/hooks/fields-after-create.ts diff --git a/packages/plugin-collections/src/hooks/fields-after-update.ts b/packages/plugin-collections-v04/src/hooks/fields-after-update.ts similarity index 100% rename from packages/plugin-collections/src/hooks/fields-after-update.ts rename to packages/plugin-collections-v04/src/hooks/fields-after-update.ts diff --git a/packages/plugin-collections/src/hooks/fields-before-validate.ts b/packages/plugin-collections-v04/src/hooks/fields-before-validate.ts similarity index 100% rename from packages/plugin-collections/src/hooks/fields-before-validate.ts rename to packages/plugin-collections-v04/src/hooks/fields-before-validate.ts diff --git a/packages/plugin-collections/src/hooks/generateName.ts b/packages/plugin-collections-v04/src/hooks/generateName.ts similarity index 100% rename from packages/plugin-collections/src/hooks/generateName.ts rename to packages/plugin-collections-v04/src/hooks/generateName.ts diff --git a/packages/plugin-collections/src/hooks/index.ts b/packages/plugin-collections-v04/src/hooks/index.ts similarity index 100% rename from packages/plugin-collections/src/hooks/index.ts rename to packages/plugin-collections-v04/src/hooks/index.ts diff --git a/packages/plugin-collections/src/interfaces/index.ts b/packages/plugin-collections-v04/src/interfaces/index.ts similarity index 100% rename from packages/plugin-collections/src/interfaces/index.ts rename to packages/plugin-collections-v04/src/interfaces/index.ts diff --git a/packages/plugin-collections/src/interfaces/types.ts b/packages/plugin-collections-v04/src/interfaces/types.ts similarity index 100% rename from packages/plugin-collections/src/interfaces/types.ts rename to packages/plugin-collections-v04/src/interfaces/types.ts diff --git a/packages/plugin-collections/src/models/action.ts b/packages/plugin-collections-v04/src/models/action.ts similarity index 100% rename from packages/plugin-collections/src/models/action.ts rename to packages/plugin-collections-v04/src/models/action.ts diff --git a/packages/plugin-collections/src/models/base.ts b/packages/plugin-collections-v04/src/models/base.ts similarity index 100% rename from packages/plugin-collections/src/models/base.ts rename to packages/plugin-collections-v04/src/models/base.ts diff --git a/packages/plugin-collections-v04/src/models/collection.ts b/packages/plugin-collections-v04/src/models/collection.ts new file mode 100644 index 000000000..ec692dffa --- /dev/null +++ b/packages/plugin-collections-v04/src/models/collection.ts @@ -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 { + 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 { + 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; diff --git a/packages/plugin-collections-v04/src/models/field.ts b/packages/plugin-collections-v04/src/models/field.ts new file mode 100644 index 000000000..dc4271b46 --- /dev/null +++ b/packages/plugin-collections-v04/src/models/field.ts @@ -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 { + 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 { + 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; diff --git a/packages/plugin-collections-v04/src/models/index.ts b/packages/plugin-collections-v04/src/models/index.ts new file mode 100644 index 000000000..91a4aef21 --- /dev/null +++ b/packages/plugin-collections-v04/src/models/index.ts @@ -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'; diff --git a/packages/plugin-collections/src/models/page.ts b/packages/plugin-collections-v04/src/models/page.ts similarity index 100% rename from packages/plugin-collections/src/models/page.ts rename to packages/plugin-collections-v04/src/models/page.ts diff --git a/packages/plugin-collections/src/models/tab.ts b/packages/plugin-collections-v04/src/models/tab.ts similarity index 100% rename from packages/plugin-collections/src/models/tab.ts rename to packages/plugin-collections-v04/src/models/tab.ts diff --git a/packages/plugin-collections/src/models/view.ts b/packages/plugin-collections-v04/src/models/view.ts similarity index 100% rename from packages/plugin-collections/src/models/view.ts rename to packages/plugin-collections-v04/src/models/view.ts diff --git a/packages/plugin-collections-v04/src/server.ts b/packages/plugin-collections-v04/src/server.ts new file mode 100644 index 000000000..82e94e24f --- /dev/null +++ b/packages/plugin-collections-v04/src/server.ts @@ -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(); + } + }); +} diff --git a/packages/plugin-collections-v04/src/utils.ts b/packages/plugin-collections-v04/src/utils.ts new file mode 100644 index 000000000..e58cbe491 --- /dev/null +++ b/packages/plugin-collections-v04/src/utils.ts @@ -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, + }); +} diff --git a/packages/plugin-collections/src/__tests__/collections.test.ts b/packages/plugin-collections/src/__tests__/collections.test.ts new file mode 100644 index 000000000..4cdc87f57 --- /dev/null +++ b/packages/plugin-collections/src/__tests__/collections.test.ts @@ -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', + }, + ], + }); + }); +}); diff --git a/packages/plugin-collections/src/collections/collections.ts b/packages/plugin-collections/src/collections/collections.ts index 23dbe2148..893476c64 100644 --- a/packages/plugin-collections/src/collections/collections.ts +++ b/packages/plugin-collections/src/collections/collections.ts @@ -3,377 +3,28 @@ import { TableOptions } from '@nocobase/database'; export default { name: 'collections', title: '数据表配置', - internal: true, - sortable: true, - draggable: true, - model: 'CollectionModel', - developerMode: true, - createdAt: 'createdTime', - updatedAt: 'updatedTime', + model: 'Collection', 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', 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; diff --git a/packages/plugin-collections/src/collections/fields.ts b/packages/plugin-collections/src/collections/fields.ts index 81be1ed77..36513cd39 100644 --- a/packages/plugin-collections/src/collections/fields.ts +++ b/packages/plugin-collections/src/collections/fields.ts @@ -1,690 +1,55 @@ import { TableOptions } from '@nocobase/database'; -import { types, getOptions } from '../interfaces'; +import { DataTypes } from 'sequelize'; export default { name: 'fields', title: '字段配置', - internal: true, - draggable: true, - model: 'FieldModel', - developerMode: true, + model: 'Field', fields: [ { - interface: 'sort', - type: 'sort', - name: 'sort', - scope: ['collection'], - title: '排序', - component: { - type: 'sort', - className: 'drag-visible', - width: 60, - showInTable: true, - }, + type: 'uid', + name: 'key', + primaryKey: true, + prefix: 'f_', + // autoIncrement: false, + // defaultValue: DataTypes.UUIDV4, }, { - 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, - }, + name: 'dataType', }, { - 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, - }, + sourceKey: 'key', + foreignKey: 'parentKey', }, { 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: '前端组件', + type: 'belongsTo', + name: 'uiSchema', + target: 'ui_schemas', 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; diff --git a/packages/plugin-collections/src/models/collection.ts b/packages/plugin-collections/src/models/collection.ts index ec692dffa..83aadfbb6 100644 --- a/packages/plugin-collections/src/models/collection.ts +++ b/packages/plugin-collections/src/models/collection.ts @@ -1,241 +1,6 @@ import _ from 'lodash'; -import BaseModel from './base'; -import Field from './field'; -import { TableOptions } from '@nocobase/database'; -import { SaveOptions, Op } from 'sequelize'; +import { Model } from '@nocobase/database'; + +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 { - 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 { - 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; diff --git a/packages/plugin-collections/src/models/field.ts b/packages/plugin-collections/src/models/field.ts index dc4271b46..418c1b847 100644 --- a/packages/plugin-collections/src/models/field.ts +++ b/packages/plugin-collections/src/models/field.ts @@ -1,263 +1,6 @@ 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'; +import { Model } from '@nocobase/database'; + +export class Field extends Model { -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 { - 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 { - 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; diff --git a/packages/plugin-collections/src/models/index.ts b/packages/plugin-collections/src/models/index.ts index 91a4aef21..730a3dd28 100644 --- a/packages/plugin-collections/src/models/index.ts +++ b/packages/plugin-collections/src/models/index.ts @@ -1,7 +1,2 @@ -export * from './base'; -export * from './action'; export * from './collection'; export * from './field'; -export * from './tab'; -export * from './view'; -export * from './page'; diff --git a/packages/plugin-collections/src/server.ts b/packages/plugin-collections/src/server.ts index 82e94e24f..9519b62ad 100644 --- a/packages/plugin-collections/src/server.ts +++ b/packages/plugin-collections/src/server.ts @@ -1,77 +1,19 @@ 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(); + database.getModel('fields').beforeCreate((model) => { + if (!model.get('name')) { + model.set('name', model.get('key')); } }); } diff --git a/packages/plugin-routes/.npmignore b/packages/plugin-routes/.npmignore new file mode 100644 index 000000000..461574b2f --- /dev/null +++ b/packages/plugin-routes/.npmignore @@ -0,0 +1,7 @@ +node_modules +*.log +docs +__tests__ +tsconfig.json +src +.fatherrc.ts \ No newline at end of file diff --git a/packages/plugin-routes/package.json b/packages/plugin-routes/package.json new file mode 100644 index 000000000..6fc081878 --- /dev/null +++ b/packages/plugin-routes/package.json @@ -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" +} diff --git a/packages/plugin-routes/src/__tests__/index.ts b/packages/plugin-routes/src/__tests__/index.ts new file mode 100644 index 000000000..e0574424b --- /dev/null +++ b/packages/plugin-routes/src/__tests__/index.ts @@ -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; + list: (params?: ActionParams) => Promise; + create: (params?: ActionParams) => Promise; + update: (params?: ActionParams) => Promise; + destroy: (params?: ActionParams) => Promise; + [name: string]: (params?: ActionParams) => Promise; +} + +export interface Agent { + resource: (name: string) => Handler; +} + +export function getAgent(app: Application): 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}`; + } + } + }); +}; diff --git a/packages/plugin-routes/src/__tests__/routes.test.ts b/packages/plugin-routes/src/__tests__/routes.test.ts new file mode 100644 index 000000000..6b46d3fec --- /dev/null +++ b/packages/plugin-routes/src/__tests__/routes.test.ts @@ -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); + } + }); +}); diff --git a/packages/plugin-routes/src/collections/routes.ts b/packages/plugin-routes/src/collections/routes.ts new file mode 100644 index 000000000..66ade6d26 --- /dev/null +++ b/packages/plugin-routes/src/collections/routes.ts @@ -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; diff --git a/packages/plugin-routes/src/models/index.ts b/packages/plugin-routes/src/models/index.ts new file mode 100644 index 000000000..d99221922 --- /dev/null +++ b/packages/plugin-routes/src/models/index.ts @@ -0,0 +1 @@ +export * from './route'; diff --git a/packages/plugin-routes/src/models/route.ts b/packages/plugin-routes/src/models/route.ts new file mode 100644 index 000000000..443913eb7 --- /dev/null +++ b/packages/plugin-routes/src/models/route.ts @@ -0,0 +1,6 @@ +import _ from 'lodash'; +import { Model } from '@nocobase/database'; + +export class Route extends Model { + +} diff --git a/packages/plugin-routes/src/server.ts b/packages/plugin-routes/src/server.ts new file mode 100644 index 000000000..0a1bdeee6 --- /dev/null +++ b/packages/plugin-routes/src/server.ts @@ -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'), + }); +} diff --git a/packages/plugin-routes/src/utils.ts b/packages/plugin-routes/src/utils.ts new file mode 100644 index 000000000..e58cbe491 --- /dev/null +++ b/packages/plugin-routes/src/utils.ts @@ -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, + }); +} diff --git a/packages/plugin-ui-schema/.npmignore b/packages/plugin-ui-schema/.npmignore new file mode 100644 index 000000000..461574b2f --- /dev/null +++ b/packages/plugin-ui-schema/.npmignore @@ -0,0 +1,7 @@ +node_modules +*.log +docs +__tests__ +tsconfig.json +src +.fatherrc.ts \ No newline at end of file diff --git a/packages/plugin-ui-schema/package.json b/packages/plugin-ui-schema/package.json new file mode 100644 index 000000000..96b75f8de --- /dev/null +++ b/packages/plugin-ui-schema/package.json @@ -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" +} diff --git a/packages/plugin-ui-schema/src/__tests__/index.ts b/packages/plugin-ui-schema/src/__tests__/index.ts new file mode 100644 index 000000000..e0574424b --- /dev/null +++ b/packages/plugin-ui-schema/src/__tests__/index.ts @@ -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; + list: (params?: ActionParams) => Promise; + create: (params?: ActionParams) => Promise; + update: (params?: ActionParams) => Promise; + destroy: (params?: ActionParams) => Promise; + [name: string]: (params?: ActionParams) => Promise; +} + +export interface Agent { + resource: (name: string) => Handler; +} + +export function getAgent(app: Application): 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}`; + } + } + }); +}; diff --git a/packages/plugin-ui-schema/src/__tests__/ui_schemas.test.ts b/packages/plugin-ui-schema/src/__tests__/ui_schemas.test.ts new file mode 100644 index 000000000..57afe8109 --- /dev/null +++ b/packages/plugin-ui-schema/src/__tests__/ui_schemas.test.ts @@ -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)); + }); +}); diff --git a/packages/plugin-ui-schema/src/collections/ui_schemas.ts b/packages/plugin-ui-schema/src/collections/ui_schemas.ts new file mode 100644 index 000000000..4cce6c1bb --- /dev/null +++ b/packages/plugin-ui-schema/src/collections/ui_schemas.ts @@ -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; diff --git a/packages/plugin-ui-schema/src/models/index.ts b/packages/plugin-ui-schema/src/models/index.ts new file mode 100644 index 000000000..349209b7c --- /dev/null +++ b/packages/plugin-ui-schema/src/models/index.ts @@ -0,0 +1 @@ +export * from './ui-schema'; diff --git a/packages/plugin-ui-schema/src/models/ui-schema.ts b/packages/plugin-ui-schema/src/models/ui-schema.ts new file mode 100644 index 000000000..8c54fef1d --- /dev/null +++ b/packages/plugin-ui-schema/src/models/ui-schema.ts @@ -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 { + // 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; + } +} diff --git a/packages/plugin-ui-schema/src/server.ts b/packages/plugin-ui-schema/src/server.ts new file mode 100644 index 000000000..0146073cc --- /dev/null +++ b/packages/plugin-ui-schema/src/server.ts @@ -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')); + } + }); +} diff --git a/packages/plugin-ui-schema/src/utils.ts b/packages/plugin-ui-schema/src/utils.ts new file mode 100644 index 000000000..e58cbe491 --- /dev/null +++ b/packages/plugin-ui-schema/src/utils.ts @@ -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, + }); +} diff --git a/uid.js b/uid.js new file mode 100644 index 000000000..522d07070 --- /dev/null +++ b/uid.js @@ -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()); diff --git a/yarn.lock b/yarn.lock index 166c22c6f..a8a79366d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4136,7 +4136,7 @@ "@umijs/deps" "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" resolved "https://registry.npmjs.org/@umijs/test/-/test-3.4.25.tgz#9df95dc25cecaee9f45cdb62a227634defb5ae87" integrity sha512-VfaQyvF3vkHkHROS3LYQjRvx3dwk1Lv56gfgDIHGCX2n2ARSGyaFTbXsv/QafrKENDTDp3FTcTIUG6oY6mHH2g== @@ -4319,14 +4319,6 @@ agentkeepalive@^3.4.1: dependencies: 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: version "2.10.6" 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" 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" resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" 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" 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: version "2.2.1" resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" @@ -5891,21 +5878,6 @@ cli-cursor@^2.1.0: dependencies: 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: version "2.2.1" 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" 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: version "1.0.1" 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: 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" resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== @@ -7513,7 +7480,7 @@ enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.1: memory-fs "^0.5.0" tapable "^1.0.0" -enquirer@^2.3.5, enquirer@^2.3.6: +enquirer@^2.3.5: version "2.3.6" resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -8056,7 +8023,7 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^4.0.0, execa@^4.1.0: +execa@^4.0.0: version "4.1.0" resolved "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== @@ -10224,11 +10191,6 @@ is-unc-path@^1.0.0: dependencies: 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: version "1.2.4" 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" 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: version "1.1.0" 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" 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: version "0.1.1" 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" 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: version "1.2.0" 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" 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: version "0.1.2" 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" 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: version "0.1.15" 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: aproba "^1.1.1" -rxjs@^6.4.0, rxjs@^6.6.7: +rxjs@^6.4.0: version "6.6.7" resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" 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" 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: version "2.1.0" 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" 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: version "4.0.0" 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" 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: version "0.2.1" resolved "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97" @@ -18021,7 +17890,7 @@ through2@^4.0.0: dependencies: 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" resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" 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" 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: version "0.7.28" 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" slash2 "2.0.0" -umi@^3.0.0, umi@^3.4.25: +umi@^3.0.0: version "3.4.25" resolved "https://registry.npmjs.org/umi/-/umi-3.4.25.tgz#e1d53378ef8fa89011a8f54e7c5ff6984875b2de" integrity sha512-67GMhP40Tz2sbyAzB4at7IUjXEIlG4j3zADfRhepLRWYbhUa55ZHnWyvuws3/dtUfifpraz4ckVcJJESoJ7Xaw== @@ -19284,15 +19148,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.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: version "1.0.2" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"