From 27f6bde7758e393c476b1e5297b0ac11fe3ed09a Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 23 Sep 2021 00:16:04 +0800 Subject: [PATCH] feat: improve code --- package.json | 4 +- packages/api/bin/nocobase.js | 11 +- packages/api/src/app.ts | 58 ----- packages/api/src/index.ts | 73 ++++-- packages/api/src/migrate.ts | 15 -- packages/api/src/migrations/init.ts | 125 ---------- packages/api/src/migrations/sync.ts | 11 - .../api/src/migrations/ui-schema/index.ts | 3 - .../api/src/migrations/ui-schema/login.ts | 64 ------ packages/api/src/migrations/ui-schema/menu.ts | 15 -- .../api/src/migrations/ui-schema/register.ts | 100 -------- .../schema-fields/has-many-field.test.ts | 26 +++ .../schema-fields/has-one-field.test.ts | 67 ++++++ .../schema-fields/sort-field.test.ts | 70 ++++++ .../src/__tests__/update-associations.test.ts | 101 +++++++++ packages/collections/src/collection.ts | 3 + packages/collections/src/database.ts | 20 +- packages/collections/src/repository.ts | 23 ++ .../schema-fields/belongs-to-many-field.ts | 26 +-- .../src/schema-fields/date-field.ts | 8 + .../src/schema-fields/float-field.ts | 8 + .../src/schema-fields/has-many-field.ts | 1 + .../src/schema-fields/has-one-field.ts | 140 ++++++++++++ .../collections/src/schema-fields/index.ts | 2 + .../src/schema-fields/integer-field.ts | 8 + .../src/schema-fields/json-field.ts | 14 -- .../src/schema-fields/number-field.ts | 32 +++ .../src/schema-fields/schema-field.ts | 6 +- .../src/schema-fields/sort-field.ts | 25 ++ .../src/schema-fields/string-field.ts | 7 - .../src/schema-fields/text-field.ts | 8 + .../src/schema-fields/time-field.ts | 8 + .../src/schema-fields/virtual-field.ts | 8 + packages/collections/src/schema.ts | 5 +- .../collections/src/update-associations.ts | 192 ++++++++++++++++ packages/collections/src/utils.ts | 18 ++ packages/plugin-action-logs/src/server.ts | 23 +- packages/plugin-automations/src/server.ts | 1 + packages/plugin-china-region/src/server.ts | 31 +-- packages/plugin-collections/src/server.ts | 213 +++++++++--------- packages/plugin-export/src/server.ts | 49 ++-- packages/plugin-file-manager/src/server.ts | 74 +++--- packages/plugin-permissions/src/server.ts | 20 +- packages/plugin-system-settings/src/server.ts | 73 +++--- packages/plugin-ui-router/src/server.ts | 111 ++++----- packages/plugin-ui-schema/src/server.ts | 26 ++- packages/plugin-users/src/server.ts | 82 +++---- packages/server/src/__tests__/plugin.test.ts | 22 +- packages/server/src/application.ts | 40 ++-- packages/server/src/index.ts | 1 + packages/server/src/plugin.ts | 44 ++-- yarn.lock | 66 +++++- 52 files changed, 1324 insertions(+), 857 deletions(-) delete mode 100644 packages/api/src/app.ts delete mode 100644 packages/api/src/migrate.ts delete mode 100644 packages/api/src/migrations/init.ts delete mode 100644 packages/api/src/migrations/sync.ts delete mode 100644 packages/api/src/migrations/ui-schema/index.ts delete mode 100644 packages/api/src/migrations/ui-schema/login.ts delete mode 100644 packages/api/src/migrations/ui-schema/menu.ts delete mode 100644 packages/api/src/migrations/ui-schema/register.ts create mode 100644 packages/collections/src/__tests__/schema-fields/has-one-field.test.ts create mode 100644 packages/collections/src/__tests__/schema-fields/sort-field.test.ts create mode 100644 packages/collections/src/__tests__/update-associations.test.ts create mode 100644 packages/collections/src/repository.ts create mode 100644 packages/collections/src/schema-fields/date-field.ts create mode 100644 packages/collections/src/schema-fields/float-field.ts create mode 100644 packages/collections/src/schema-fields/integer-field.ts create mode 100644 packages/collections/src/schema-fields/number-field.ts create mode 100644 packages/collections/src/schema-fields/sort-field.ts create mode 100644 packages/collections/src/schema-fields/text-field.ts create mode 100644 packages/collections/src/schema-fields/time-field.ts create mode 100644 packages/collections/src/schema-fields/virtual-field.ts create mode 100644 packages/collections/src/update-associations.ts create mode 100644 packages/collections/src/utils.ts diff --git a/package.json b/package.json index 7e676b4179..9d92c2a2bb 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "examples": "ts-node-dev -r dotenv/config ./examples", "start": "cd packages/app && npm start", "start-client": "cd packages/app && npm run start-client", - "start-server": "nodemon", + "start-server": "ts-node-dev -r dotenv/config ./packages/api/src/index.ts", "start-docs": "dumi dev", "build-docs": "dumi build", "build2": "lerna run build", @@ -26,7 +26,7 @@ }, "devDependencies": { "@types/file-saver": "^2.0.3", - "@types/jest": "^24.0.18", + "@types/jest": "^27.0.1", "@types/koa": "^2.13.1", "@types/koa-mount": "^4.0.1", "@types/lodash": "^4.14.169", diff --git a/packages/api/bin/nocobase.js b/packages/api/bin/nocobase.js index f2ffb6626f..6304c46c2a 100755 --- a/packages/api/bin/nocobase.js +++ b/packages/api/bin/nocobase.js @@ -1,15 +1,6 @@ #!/usr/bin/env node const keys = process.argv; - -const key = keys.pop(); - const dotenv = require('dotenv'); - dotenv.config(); - -if (key === 'start') { - require('../lib/index'); -} else if (key === 'db-init') { - require('../lib/migrations/init'); -} +require('../lib/index'); diff --git a/packages/api/src/app.ts b/packages/api/src/app.ts deleted file mode 100644 index 40a28a6516..0000000000 --- a/packages/api/src/app.ts +++ /dev/null @@ -1,58 +0,0 @@ -import Server from '@nocobase/server'; - -// @ts-ignore -const sync = global.sync || { - force: false, - alter: { - drop: false, - }, -}; - -console.log('process.env.NOCOBASE_ENV', process.env.NOCOBASE_ENV); - -const api = new Server({ - database: { - username: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_DATABASE, - host: process.env.DB_HOST, - port: process.env.DB_PORT as any, - dialect: process.env.DB_DIALECT as any, - dialectOptions: { - charset: 'utf8mb4', - collate: 'utf8mb4_unicode_ci', - }, - pool: { - max: 5, - min: 0, - acquire: 60000, - idle: 10000, - }, - logging: process.env.DB_LOG_SQL === 'on' ? console.log : false, - define: {}, - sync, - }, - resourcer: { - prefix: '/api', - }, -}); - -const plugins = [ - '@nocobase/plugin-collections', - '@nocobase/plugin-ui-router', - '@nocobase/plugin-ui-schema', - '@nocobase/plugin-users', - '@nocobase/plugin-action-logs', - '@nocobase/plugin-file-manager', - '@nocobase/plugin-permissions', - '@nocobase/plugin-export', - '@nocobase/plugin-system-settings', - // // '@nocobase/plugin-automations', - '@nocobase/plugin-china-region', -]; - -for (const plugin of plugins) { - api.registerPlugin(plugin, [require(`${plugin}/${__filename.endsWith('.ts') ? 'src' : 'lib'}/server`).default]); -} - -export default api; diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 5702f7d9b9..9127440f27 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,30 +1,65 @@ -import api from './app'; -import { middlewares } from '@nocobase/server'; +import Server from '@nocobase/server'; + +const api = new Server({ + database: { + username: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE, + host: process.env.DB_HOST, + port: process.env.DB_PORT as any, + dialect: process.env.DB_DIALECT as any, + dialectOptions: { + charset: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }, + pool: { + max: 5, + min: 0, + acquire: 60000, + idle: 10000, + }, + logging: process.env.DB_LOG_SQL === 'on' ? console.log : false, + define: {}, + sync: { + force: false, + alter: { + drop: false, + }, + }, + }, + resourcer: { + prefix: '/api', + }, +}); + +const plugins = [ + '@nocobase/plugin-collections', + '@nocobase/plugin-ui-router', + '@nocobase/plugin-ui-schema', + '@nocobase/plugin-users', + '@nocobase/plugin-action-logs', + '@nocobase/plugin-file-manager', + '@nocobase/plugin-permissions', + '@nocobase/plugin-export', + '@nocobase/plugin-system-settings', + '@nocobase/plugin-china-region', +]; + +for (const plugin of plugins) { + api.plugin(require(`${plugin}/${__filename.endsWith('.ts') ? 'src' : 'lib'}/server`).default); +} (async () => { - api.resourcer.use(middlewares.actionParams()); - - api.on('plugins.afterLoad', async () => { - console.log('plugins.afterLoad') - if (process.env.NOCOBASE_ENV === 'demo') { - api.resourcer.use(middlewares.demoBlacklistedActions({ - emails: [process.env.ADMIN_EMAIL], - })); - } - api.use(middlewares.appDistServe({ - root: process.env.APP_DIST, - useStaticServer: !(process.env.APP_USE_STATIC_SERVER === 'false' || !process.env.APP_USE_STATIC_SERVER), - })); - }); - const start = Date.now(); if (process.argv.length < 3) { process.argv.push('start', '--port', process.env.API_PORT); } - await api.start(process.argv); + console.log(process.argv); + + await api.parse(process.argv); console.log(api.db.getTables().map(t => t.getName())); console.log(`Start-up time: ${(Date.now() - start) / 1000}s`); - console.log(`http://localhost:${process.env.API_PORT}/`); + // console.log(`http://localhost:${process.env.API_PORT}/`); })(); diff --git a/packages/api/src/migrate.ts b/packages/api/src/migrate.ts deleted file mode 100644 index 52371acae8..0000000000 --- a/packages/api/src/migrate.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-ignore -const keys = process.argv; - -// @ts-ignore -global.sync = { - force: false, - alter: { - drop: false, - }, -}; - -// @ts-ignore -const filename: string = keys.pop(); - -require(`./migrations/${filename}`); diff --git a/packages/api/src/migrations/init.ts b/packages/api/src/migrations/init.ts deleted file mode 100644 index 06a18aa547..0000000000 --- a/packages/api/src/migrations/init.ts +++ /dev/null @@ -1,125 +0,0 @@ -// @ts-ignore -global.sync = { - force: true, - alter: { - drop: true, - }, -}; - -import Database from '@nocobase/database'; -import api from '../app'; -import * as uiSchema from './ui-schema'; - -(async () => { - await api.loadPlugins(); - const database: Database = api.db; - await database.sync({ - // tables: ['collections', 'fields', 'actions', 'views', 'tabs'], - }); - - const config = - require('@nocobase/plugin-users/src/collections/users').default; - const Collection = database.getModel('collections'); - const collection = await Collection.create(config); - await collection.updateAssociations({ - generalFields: config.fields.filter((field) => field.state !== 0), - systemFields: config.fields.filter((field) => field.state === 0), - }); - await collection.migrate(); - - const Route = database.getModel('routes'); - - const data = [ - { - type: 'redirect', - from: '/', - to: '/admin', - exact: true, - }, - { - type: 'route', - path: '/admin/:name(.+)?', - component: 'AdminLayout', - title: `后台`, - uiSchema: uiSchema.menu, - }, - { - type: 'route', - component: 'AuthLayout', - children: [ - { - type: 'route', - path: '/login', - component: 'RouteSchemaRenderer', - title: `登录`, - uiSchema: uiSchema.login, - }, - { - type: 'route', - path: '/register', - component: 'RouteSchemaRenderer', - title: `注册`, - uiSchema: uiSchema.register, - }, - ], - }, - ]; - - for (const item of data) { - const route = await Route.create(item); - await route.updateAssociations(item); - } - - 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 ChinaRegion = database.getModel('china_regions'); - ChinaRegion && (await ChinaRegion.importData()); - - const SystemSetting = database.getModel('system_settings'); - if (SystemSetting) { - const setting = await SystemSetting.create({ - title: 'NocoBase', - showLogoOnly: true, - }); - await setting.updateAssociations({ - logo: { - title: 'nocobase-logo', - filename: '682e5ad037dd02a0fe4800a3e91c283b.png', - extname: '.png', - mimetype: 'image/png', - url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/682e5ad037dd02a0fe4800a3e91c283b.png', - storage_id: 2, - }, - }); - } - - const User = database.getModel('users'); - const user = await User.create({ - nickname: '超级管理员', - email: process.env.ADMIN_EMAIL, - password: process.env.ADMIN_PASSWORD, - }); - - await database.close(); -})(); - diff --git a/packages/api/src/migrations/sync.ts b/packages/api/src/migrations/sync.ts deleted file mode 100644 index 55ac5260c3..0000000000 --- a/packages/api/src/migrations/sync.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Database from '@nocobase/database'; -import api from '../app'; - -(async () => { - await api.loadPlugins(); - const database: Database = api.db; - await database.sync({ - // tables: ['collections', 'fields', 'actions', 'views', 'tabs'], - }); - await database.close(); -})(); diff --git a/packages/api/src/migrations/ui-schema/index.ts b/packages/api/src/migrations/ui-schema/index.ts deleted file mode 100644 index 6bef8bfe2d..0000000000 --- a/packages/api/src/migrations/ui-schema/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -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 deleted file mode 100644 index 2ccf1da4d5..0000000000 --- a/packages/api/src/migrations/ui-schema/login.ts +++ /dev/null @@ -1,64 +0,0 @@ -export const login = { - key: 'dtf9j0b8p9u', - type: 'object', - title: '登录', - 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 deleted file mode 100644 index 6a66606315..0000000000 --- a/packages/api/src/migrations/ui-schema/menu.ts +++ /dev/null @@ -1,15 +0,0 @@ -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 }}', - onRemove: '{{ onMenuItemRemove }}', - }, -}; diff --git a/packages/api/src/migrations/ui-schema/register.ts b/packages/api/src/migrations/ui-schema/register.ts deleted file mode 100644 index 1c183a3769..0000000000 --- a/packages/api/src/migrations/ui-schema/register.ts +++ /dev/null @@ -1,100 +0,0 @@ -export const register = { - key: '46qlxqam3xk', - type: 'object', - title: '注册', - 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: '密码', - checkStrength: true, - style: { - // width: 240, - }, - }, - 'x-reactions': [ - { - dependencies: ['.confirm_password'], - fulfill: { - state: { - errors: - '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}', - }, - }, - }, - ], - }, - confirm_password: { - type: 'string', - required: true, - 'x-decorator': 'FormItem', - 'x-component': 'Password', - 'x-component-props': { - placeholder: '确认密码', - checkStrength: true, - style: { - // width: 240, - }, - }, - 'x-reactions': [ - { - dependencies: ['.password'], - fulfill: { - state: { - errors: - '{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}', - }, - }, - }, - ], - }, - 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/collections/src/__tests__/schema-fields/has-many-field.test.ts b/packages/collections/src/__tests__/schema-fields/has-many-field.test.ts index 026043983b..bdd5595d3b 100644 --- a/packages/collections/src/__tests__/schema-fields/has-many-field.test.ts +++ b/packages/collections/src/__tests__/schema-fields/has-many-field.test.ts @@ -48,6 +48,32 @@ describe('has many field', () => { ]); }); + it.only('custom sourceKey', async () => { + const collection = db.collection({ + name: 'posts', + schema: [ + { type: 'string', name: 'key', unique: true }, + { + type: 'hasMany', + name: 'comments', + sourceKey: 'key', + // foreignKey: 'postKey', + }, + ], + }); + const comments = db.collection({ + name: 'comments', + schema: [], + }); + const association = collection.model.associations.comments; + expect(association).toBeDefined(); + expect(association.foreignKey).toBe('postKey'); + // @ts-ignore + expect(association.sourceKey).toBe('key'); + expect(comments.model.rawAttributes['postKey']).toBeDefined(); + await db.sync(); + }); + it('custom sourceKey and foreignKey', async () => { const collection = db.collection({ name: 'posts', diff --git a/packages/collections/src/__tests__/schema-fields/has-one-field.test.ts b/packages/collections/src/__tests__/schema-fields/has-one-field.test.ts new file mode 100644 index 0000000000..0bb1132ed5 --- /dev/null +++ b/packages/collections/src/__tests__/schema-fields/has-one-field.test.ts @@ -0,0 +1,67 @@ +import { Database } from '../../database'; +import { mockDatabase } from '../'; + +describe('has many field', () => { + let db: Database; + + beforeEach(() => { + db = mockDatabase(); + }); + + afterEach(async () => { + await db.close(); + }); + + it('association undefined', async () => { + const User = db.collection({ + name: 'users', + schema: [{ type: 'hasOne', name: 'profile' }], + }); + await db.sync(); + expect(User.model.associations.profile).toBeUndefined(); + }); + + it('association defined', async () => { + const User = db.collection({ + name: 'users', + schema: [{ type: 'hasOne', name: 'profile' }], + }); + expect(User.model.associations.phone).toBeUndefined(); + const Profile = db.collection({ + name: 'profiles', + schema: [{ type: 'string', name: 'content' }], + }); + const association = User.model.associations.profile; + expect(association).toBeDefined(); + expect(association.foreignKey).toBe('userId'); + // @ts-ignore + expect(association.sourceKey).toBe('id'); + expect(Profile.model.rawAttributes['userId']).toBeDefined(); + await db.sync(); + // const post = await model.create(); + // await post.createComment({ + // content: 'content111', + // }); + // const postComments = await post.getComments(); + // expect(postComments.map((comment) => comment.content)).toEqual([ + // 'content111', + // ]); + }); + + it('schema delete', async () => { + const User = db.collection({ + name: 'users', + schema: [{ type: 'hasOne', name: 'profile' }], + }); + const Profile = db.collection({ + name: 'profiles', + schema: [{ type: 'belongsTo', name: 'user' }], + }); + await db.sync(); + User.schema.delete('profile'); + expect(User.model.associations.profile).toBeUndefined(); + expect(Profile.model.rawAttributes.userId).toBeDefined(); + Profile.schema.delete('user'); + expect(Profile.model.rawAttributes.userId).toBeUndefined(); + }); +}); diff --git a/packages/collections/src/__tests__/schema-fields/sort-field.test.ts b/packages/collections/src/__tests__/schema-fields/sort-field.test.ts new file mode 100644 index 0000000000..e53fa543c2 --- /dev/null +++ b/packages/collections/src/__tests__/schema-fields/sort-field.test.ts @@ -0,0 +1,70 @@ +import { Database } from '../../database'; +import { mockDatabase } from '../'; +import { SortField } from '../../schema-fields'; + +describe('string field', () => { + let db: Database; + + beforeEach(() => { + db = mockDatabase(); + db.registerSchemaTypes({ + sort: SortField + }); + }); + + afterEach(async () => { + await db.close(); + }); + + it('sort', async () => { + const Test = db.collection({ + name: 'tests', + schema: [ + { type: 'sort', name: 'sort' }, + ], + }); + await db.sync(); + const test1 = await Test.model.create(); + expect(test1.sort).toBe(1); + const test2 = await Test.model.create(); + expect(test2.sort).toBe(2); + const test3 = await Test.model.create(); + expect(test3.sort).toBe(3); + }); + + it('skip if sort value not empty', async () => { + const Test = db.collection({ + name: 'tests', + schema: [ + { type: 'sort', name: 'sort' }, + ], + }); + await db.sync(); + const test1 = await Test.model.create({ sort: 3 }); + expect(test1.sort).toBe(3); + const test2 = await Test.model.create(); + expect(test2.sort).toBe(4); + const test3 = await Test.model.create(); + expect(test3.sort).toBe(5); + }); + + it('scopeKey', async () => { + const Test = db.collection({ + name: 'tests', + schema: [ + { type: 'sort', name: 'sort', scopeKey: 'status' }, + { type: 'string', name: 'status' }, + ], + }); + await db.sync(); + const t1 = await Test.model.create({ status: 'publish' }); + const t2 = await Test.model.create({ status: 'publish' }); + const t3 = await Test.model.create({ status: 'draft' }); + const t4 = await Test.model.create({ status: 'draft' }); + expect(t1.get('sort')).toBe(1); + expect(t2.get('sort')).toBe(2); + expect(t3.get('sort')).toBe(1); + expect(t4.get('sort')).toBe(2); + }); + +}); diff --git a/packages/collections/src/__tests__/update-associations.test.ts b/packages/collections/src/__tests__/update-associations.test.ts new file mode 100644 index 0000000000..9c3858822d --- /dev/null +++ b/packages/collections/src/__tests__/update-associations.test.ts @@ -0,0 +1,101 @@ +import { Database } from '../database'; +import { updateAssociation, updateAssociations } from '../update-associations'; +import { mockDatabase } from './'; + +describe('update associations', () => { + let db: Database; + + beforeEach(() => { + db = mockDatabase(); + }); + + afterEach(async () => { + await db.close(); + }); + + describe('hasMany', () => { + it.only('model', async () => { + const User = db.collection({ + name: 'users', + schema: [ + { type: 'string', name: 'name' }, + { type: 'hasMany', name: 'posts' }, + ], + }); + const Post = db.collection({ + name: 'posts', + schema: [ + { type: 'string', name: 'title' }, + ], + }); + await db.sync(); + const user = await User.model.create(); + const post1 = await Post.model.create(); + const post2 = await Post.model.create(); + const post3 = await Post.model.create(); + const post4 = await Post.model.create(); + await updateAssociations(user, { + posts: { + title: 'post0', + }, + }); + await updateAssociations(user, { + posts: post1, + }); + await updateAssociations(user, { + posts: post2.id, + }); + await updateAssociations(user, { + posts: [post3.id], + }); + await updateAssociations(user, { + posts: { + id: post4.id, + title: 'post4', + }, + }); + }); + }); + + it('nested', async () => { + const User = db.collection({ + name: 'users', + schema: [ + { type: 'string', name: 'name' }, + { type: 'hasMany', name: 'posts' }, + ], + }); + const Post = db.collection({ + name: 'posts', + schema: [ + { type: 'string', name: 'title' }, + { type: 'belongsTo', name: 'user' }, + { type: 'hasMany', name: 'comments' }, + ], + }); + const Comment = db.collection({ + name: 'comments', + schema: [ + { type: 'string', name: 'content' }, + { type: 'belongsTo', name: 'post' }, + ], + }); + await db.sync(); + const user = await User.model.create(); + await updateAssociations(user, { + posts: [ + { + title: 'post1', + // user: { + // name: 'user1', + // }, + comments: [ + { + content: 'content1', + }, + ], + }, + ], + }); + }); +}); diff --git a/packages/collections/src/collection.ts b/packages/collections/src/collection.ts index 0fd5d85765..e9e1998ee9 100644 --- a/packages/collections/src/collection.ts +++ b/packages/collections/src/collection.ts @@ -29,12 +29,15 @@ export class Collection { const { name, tableName } = options; this.model = class extends Model {}; const attributes = {}; + // TODO: 不能重复 model.init,如果有涉及 InitOptions 参数修改,需要另外处理。 this.model.init(attributes, { ..._.omit(options, ['name', 'schema']), sequelize: context.database.sequelize, modelName: name, tableName: tableName || name, }); + // schema 只针对字段,对应 Sequelize 的 Attributes + // 其他 InitOptions 参数放在 Collection 里,通过其他方法同步给 model this.schema = new Schema(options.schema, { ...context, collection: this, diff --git a/packages/collections/src/database.ts b/packages/collections/src/database.ts index 0d88b36fe7..80bd2b7296 100644 --- a/packages/collections/src/database.ts +++ b/packages/collections/src/database.ts @@ -4,6 +4,7 @@ import { Collection, CollectionOptions } from './collection'; import { RelationField, StringField, + HasOneField, HasManyField, BelongsToField, BelongsToManyField, @@ -21,6 +22,8 @@ export type DatabaseOptions = Options | Sequelize; export class Database extends EventEmitter { sequelize: Sequelize; schemaTypes = new Map(); + models = new Map(); + repositories = new Map(); collections: Map; pendingFields = new Map(); @@ -42,6 +45,7 @@ export class Database extends EventEmitter { string: StringField, json: JsonField, jsonb: JsonbField, + hasOne: HasOneField, hasMany: HasManyField, belongsTo: BelongsToField, belongsToMany: BelongsToManyField, @@ -72,9 +76,7 @@ export class Database extends EventEmitter { removePendingField(field: RelationField) { const items = this.pendingFields.get(field.target) || []; - const index = items.findIndex( - (item) => item && item.name === field.name, - ); + const index = items.indexOf(field); if (index !== -1) { delete items[index]; this.pendingFields.set(field.target, items); @@ -87,6 +89,18 @@ export class Database extends EventEmitter { } } + registerModels(models: any) { + for (const [type, schemaType] of Object.entries(models)) { + this.models.set(type, schemaType); + } + } + + registerRepositories(repositories: any) { + for (const [type, schemaType] of Object.entries(repositories)) { + this.repositories.set(type, schemaType); + } + } + buildSchemaField(options, context) { const { type } = options; const Field = this.schemaTypes.get(type); diff --git a/packages/collections/src/repository.ts b/packages/collections/src/repository.ts new file mode 100644 index 0000000000..ae605d0fb9 --- /dev/null +++ b/packages/collections/src/repository.ts @@ -0,0 +1,23 @@ +import { ModelCtor, Model } from 'sequelize'; + +export interface IRepository { + +} + +export class Repository implements IRepository { + model: ModelCtor; + + constructor(model: ModelCtor) { + this.model = model; + } + + findAll() {} + + findOne() {} + + create() {} + + update() {} + + destroy() {} +} diff --git a/packages/collections/src/schema-fields/belongs-to-many-field.ts b/packages/collections/src/schema-fields/belongs-to-many-field.ts index a5be4cb6ce..5a0cad0bca 100644 --- a/packages/collections/src/schema-fields/belongs-to-many-field.ts +++ b/packages/collections/src/schema-fields/belongs-to-many-field.ts @@ -3,10 +3,6 @@ import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize'; import { RelationField } from './relation-field'; export class BelongsToManyField extends RelationField { - get target() { - const { target, name } = this.options; - return target || name; - } get through() { return ( @@ -48,22 +44,10 @@ export class BelongsToManyField extends RelationField { } unbind() { - // const { database, collection } = this.context; - // // 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段 - // database.removePendingField(this); - // // 如果外键没有显式的创建,关系表也无反向关联字段,删除关系时,外键也删除掉 - // const tcoll = database.collections.get(this.target); - // const foreignKey = this.options.foreignKey; - // const field1 = collection.schema.get(foreignKey); - // const field2 = tcoll.schema.find((field) => { - // return field.type === 'hasMany' && field.foreignKey === foreignKey; - // }); - // if (!field1 && !field2) { - // collection.model.removeAttribute(foreignKey); - // } - // // 删掉 model 的关联字段 - // delete collection.model.associations[this.name]; - // // @ts-ignore - // collection.model.refreshAttributes(); + const { database, collection } = this.context; + // 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段 + database.removePendingField(this); + // 删掉 model 的关联字段 + delete collection.model.associations[this.name]; } } diff --git a/packages/collections/src/schema-fields/date-field.ts b/packages/collections/src/schema-fields/date-field.ts new file mode 100644 index 0000000000..76545666a7 --- /dev/null +++ b/packages/collections/src/schema-fields/date-field.ts @@ -0,0 +1,8 @@ +import { DataTypes } from 'sequelize'; +import { SchemaField } from './schema-field'; + +export class DateField extends SchemaField { + get dataType() { + return DataTypes.DATE; + } +} diff --git a/packages/collections/src/schema-fields/float-field.ts b/packages/collections/src/schema-fields/float-field.ts new file mode 100644 index 0000000000..c4849c1f90 --- /dev/null +++ b/packages/collections/src/schema-fields/float-field.ts @@ -0,0 +1,8 @@ +import { DataTypes } from 'sequelize'; +import { SchemaField } from './schema-field'; + +export class FloatField extends SchemaField { + get dataType() { + return DataTypes.FLOAT; + } +} diff --git a/packages/collections/src/schema-fields/has-many-field.ts b/packages/collections/src/schema-fields/has-many-field.ts index d7807820a0..5507426fb7 100644 --- a/packages/collections/src/schema-fields/has-many-field.ts +++ b/packages/collections/src/schema-fields/has-many-field.ts @@ -72,6 +72,7 @@ export interface HasManyFieldOptions extends HasManyOptions { } export class HasManyField extends RelationField { + bind() { const { database, collection } = this.context; const Target = this.TargetModel; diff --git a/packages/collections/src/schema-fields/has-one-field.ts b/packages/collections/src/schema-fields/has-one-field.ts index e69de29bb2..4863e76e59 100644 --- a/packages/collections/src/schema-fields/has-one-field.ts +++ b/packages/collections/src/schema-fields/has-one-field.ts @@ -0,0 +1,140 @@ +import { omit } from 'lodash'; +import { + Sequelize, + ModelCtor, + Model, + DataType, + AssociationScope, + ForeignKeyOptions, + HasOneOptions, + Utils, +} from 'sequelize'; +import { RelationField } from './relation-field'; + +export interface HasOneFieldOptions extends HasOneOptions { + /** + * The name of the field to use as the key for the association in the source table. Defaults to the primary + * key of the source table + */ + sourceKey?: string; + + /** + * A string or a data type to represent the identifier in the table + */ + keyType?: DataType; + + scope?: AssociationScope; + + /** + * The alias of this model, in singular form. See also the `name` option passed to `sequelize.define`. If + * you create multiple associations between the same tables, you should provide an alias to be able to + * distinguish between them. If you provide an alias when creating the assocition, you should provide the + * same alias when eager loading and when getting associated models. Defaults to the singularized name of + * target + */ + as?: string | { singular: string; plural: string }; + + /** + * The name of the foreign key in the target table or an object representing the type definition for the + * foreign column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property + * to set the name of the column. Defaults to the name of source + primary key of source + */ + foreignKey?: string | ForeignKeyOptions; + + /** + * What happens when delete occurs. + * + * Cascade if this is a n:m, and set null if it is a 1:m + * + * @default 'SET NULL' or 'CASCADE' + */ + onDelete?: string; + + /** + * What happens when update occurs + * + * @default 'CASCADE' + */ + onUpdate?: string; + + /** + * Should on update and on delete constraints be enabled on the foreign key. + */ + constraints?: boolean; + foreignKeyConstraint?: boolean; + + // scope?: AssociationScope; + + /** + * If `false` the applicable hooks will not be called. + * The default value depends on the context. + */ + hooks?: boolean; +} + +export class HasOneField extends RelationField { + + get target() { + const { target, name } = this.options; + return target || Utils.pluralize(name); + } + + get foreignKey() { + if (this.options.foreignKey) { + return this.options.foreignKey; + } + const { model } = this.context.collection; + return Utils.camelize( + [ + model.options.name.singular, + model.primaryKeyAttribute + ].join('_') + ); + } + + bind() { + const { database, collection } = this.context; + const Target = this.TargetModel; + if (!Target) { + database.addPendingField(this); + return false; + } + const association = collection.model.hasOne(Target, { + as: this.name, + foreignKey: this.foreignKey, + ...omit(this.options, ['name', 'type', 'target']), + }); + // 建立关系之后从 pending 列表中删除 + database.removePendingField(this); + if (!this.options.foreignKey) { + this.options.foreignKey = association.foreignKey; + } + if (!this.options.sourceKey) { + // @ts-ignore + this.options.sourceKey = association.sourceKey; + } + return true; + } + + unbind() { + const { database, collection } = this.context; + // 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段 + database.removePendingField(this); + // 如果关系表内没有显式的创建外键字段,删除关系时,外键也删除掉 + const tcoll = database.collections.get(this.target); + const foreignKey = this.options.foreignKey; + const field = tcoll.schema.find((field) => { + if (field.name === foreignKey) { + return true; + } + return field.type === 'belongsTo' && field.foreignKey === foreignKey; + }); + if (!field) { + tcoll.model.removeAttribute(foreignKey); + } + // 删掉 model 的关联字段 + delete collection.model.associations[this.name]; + // @ts-ignore + collection.model.refreshAttributes(); + } +} diff --git a/packages/collections/src/schema-fields/index.ts b/packages/collections/src/schema-fields/index.ts index 4246b10a6b..d9a398dc1e 100644 --- a/packages/collections/src/schema-fields/index.ts +++ b/packages/collections/src/schema-fields/index.ts @@ -3,5 +3,7 @@ export * from './string-field'; export * from './relation-field' export * from './belongs-to-field' export * from './belongs-to-many-field'; +export * from './has-one-field'; export * from './has-many-field'; export * from './json-field'; +export * from './sort-field'; diff --git a/packages/collections/src/schema-fields/integer-field.ts b/packages/collections/src/schema-fields/integer-field.ts new file mode 100644 index 0000000000..f87d1ecb7f --- /dev/null +++ b/packages/collections/src/schema-fields/integer-field.ts @@ -0,0 +1,8 @@ +import { DataTypes } from 'sequelize'; +import { SchemaField } from './schema-field'; + +export class IntegerField extends SchemaField { + get dataType() { + return DataTypes.INTEGER; + } +} diff --git a/packages/collections/src/schema-fields/json-field.ts b/packages/collections/src/schema-fields/json-field.ts index 568e8d8d21..a5a6e8efae 100644 --- a/packages/collections/src/schema-fields/json-field.ts +++ b/packages/collections/src/schema-fields/json-field.ts @@ -5,13 +5,6 @@ export class JsonField extends SchemaField { get dataType() { return DataTypes.JSON; } - - toSequelize() { - return { - ...this.options, - type: this.dataType, - }; - } } export class JsonbField extends SchemaField { @@ -22,11 +15,4 @@ export class JsonbField extends SchemaField { } return DataTypes.JSON; } - - toSequelize() { - return { - ...this.options, - type: this.dataType, - }; - } } diff --git a/packages/collections/src/schema-fields/number-field.ts b/packages/collections/src/schema-fields/number-field.ts new file mode 100644 index 0000000000..23b70ec8ad --- /dev/null +++ b/packages/collections/src/schema-fields/number-field.ts @@ -0,0 +1,32 @@ +import { DataTypes } from 'sequelize'; +import { SchemaField } from './schema-field'; + +export class IntegerField extends SchemaField { + get dataType() { + return DataTypes.INTEGER; + } +} + +export class FloatField extends SchemaField { + get dataType() { + return DataTypes.FLOAT; + } +} + +export class DoubleField extends SchemaField { + get dataType() { + return DataTypes.DOUBLE; + } +} + +export class RealField extends SchemaField { + get dataType() { + return DataTypes.REAL; + } +} + +export class DecimalField extends SchemaField { + get dataType() { + return DataTypes.DECIMAL; + } +} diff --git a/packages/collections/src/schema-fields/schema-field.ts b/packages/collections/src/schema-fields/schema-field.ts index c872be6de4..5905066c81 100644 --- a/packages/collections/src/schema-fields/schema-field.ts +++ b/packages/collections/src/schema-fields/schema-field.ts @@ -60,6 +60,10 @@ export abstract class SchemaField { } toSequelize(): any { - return _.omit(this.options, ['name']) + const opts = _.omit(this.options, ['name']); + if (this.dataType) { + Object.assign(opts, { type: this.dataType }); + } + return opts; } } diff --git a/packages/collections/src/schema-fields/sort-field.ts b/packages/collections/src/schema-fields/sort-field.ts new file mode 100644 index 0000000000..2d53dfc471 --- /dev/null +++ b/packages/collections/src/schema-fields/sort-field.ts @@ -0,0 +1,25 @@ +import { isNumber } from 'lodash'; +import { DataTypes } from 'sequelize'; +import { SchemaField } from './schema-field'; + +export class SortField extends SchemaField { + get dataType() { + return DataTypes.INTEGER; + } + + init() { + const { name, scopeKey } = this.options; + const { model } = this.context.collection; + model.beforeCreate(async (instance, options) => { + if (isNumber(instance.get(name))) { + return; + } + const where = {}; + if (scopeKey) { + where[scopeKey] = instance.get(scopeKey); + } + const max = await model.max(name, { ...options, where }); + instance.set(name, (max || 0) + 1); + }); + } +} diff --git a/packages/collections/src/schema-fields/string-field.ts b/packages/collections/src/schema-fields/string-field.ts index cc8e06d55e..9e495215a7 100644 --- a/packages/collections/src/schema-fields/string-field.ts +++ b/packages/collections/src/schema-fields/string-field.ts @@ -5,11 +5,4 @@ export class StringField extends SchemaField { get dataType() { return DataTypes.STRING; } - - toSequelize() { - return { - ...this.options, - type: this.dataType, - }; - } } diff --git a/packages/collections/src/schema-fields/text-field.ts b/packages/collections/src/schema-fields/text-field.ts new file mode 100644 index 0000000000..182d3e38b3 --- /dev/null +++ b/packages/collections/src/schema-fields/text-field.ts @@ -0,0 +1,8 @@ +import { DataTypes } from 'sequelize'; +import { SchemaField } from './schema-field'; + +export class TextField extends SchemaField { + get dataType() { + return DataTypes.TEXT; + } +} diff --git a/packages/collections/src/schema-fields/time-field.ts b/packages/collections/src/schema-fields/time-field.ts new file mode 100644 index 0000000000..ca57886a75 --- /dev/null +++ b/packages/collections/src/schema-fields/time-field.ts @@ -0,0 +1,8 @@ +import { DataTypes } from 'sequelize'; +import { SchemaField } from './schema-field'; + +export class TimeField extends SchemaField { + get dataType() { + return DataTypes.TIME; + } +} diff --git a/packages/collections/src/schema-fields/virtual-field.ts b/packages/collections/src/schema-fields/virtual-field.ts new file mode 100644 index 0000000000..231642be31 --- /dev/null +++ b/packages/collections/src/schema-fields/virtual-field.ts @@ -0,0 +1,8 @@ +import { DataTypes } from 'sequelize'; +import { SchemaField } from './schema-field'; + +export class VirtualField extends SchemaField { + get dataType() { + return DataTypes.VIRTUAL; + } +} diff --git a/packages/collections/src/schema.ts b/packages/collections/src/schema.ts index 2c5cf12501..833b7f4615 100644 --- a/packages/collections/src/schema.ts +++ b/packages/collections/src/schema.ts @@ -41,6 +41,7 @@ export class Schema extends EventEmitter { schema: this, model: this.context.collection.model, }); + // console.log('field', field); this.fields.set(name, field); this.emit('setted', field); } else if (Array.isArray(name)) { @@ -59,7 +60,9 @@ export class Schema extends EventEmitter { delete(name: string) { const field = this.fields.get(name); const bool = this.fields.delete(name); - this.emit('deleted', field); + if (bool) { + this.emit('deleted', field); + } return bool; } diff --git a/packages/collections/src/update-associations.ts b/packages/collections/src/update-associations.ts new file mode 100644 index 0000000000..9d3434374d --- /dev/null +++ b/packages/collections/src/update-associations.ts @@ -0,0 +1,192 @@ +import { + Sequelize, + ModelCtor, + Model, + DataTypes, + Utils, + Association, +} from 'sequelize'; + +function isUndefinedOrNull(value: any) { + return typeof value === 'undefined' || value === null; +} + +function isStringOrNumber(value: any) { + return typeof value === 'string' || typeof value === 'number'; +} + +export async function updateAssociations( + model: Model, + values: any, + options: any = {}, +) { + const { transaction = await model.sequelize.transaction() } = options; + // @ts-ignore + for (const key of Object.keys(model.constructor.associations)) { + // 如果 key 不存在才跳过 + if (!Object.keys(values).includes(key)) { + continue; + } + await updateAssociation(model, key, values[key], { + ...options, + transaction, + }); + } + if (!options.transaction) { + await transaction.commit(); + } +} + +export async function updateAssociation( + model: Model, + key: string, + value: any, + options: any = {}, +) { + // @ts-ignore + const association = model.constructor.associations[key] as Association; + if (!association) { + return false; + } + switch (association.associationType) { + case 'HasOne': + case 'BelongsTo': + return updateSingleAssociation(model, key, value, options); + case 'HasMany': + case 'BelongsToMany': + return updateMultipleAssociation(model, key, value, options); + } +} + +export async function updateSingleAssociation( + model: Model, + key: string, + value: any, + options: any = {}, +) { + // @ts-ignore + const association = model.constructor.associations[key] as Association; + if (!association) { + return false; + } + if (!['undefined', 'string', 'number', 'object'].includes(typeof value)) { + return false; + } + const { transaction = await model.sequelize.transaction() } = options; + try { + // @ts-ignore + const setAccessor = association.accessors.set; + if (isUndefinedOrNull(value)) { + return await model[setAccessor](null, { transaction }); + } + if (isStringOrNumber(value)) { + return await model[setAccessor](value, { transaction }); + } + // @ts-ignore + const createAccessor = association.accessors.create; + let key: string; + let M: ModelCtor; + if (association.associationType === 'BelongsTo') { + // @ts-ignore + key = association.targetKey; + M = association.target; + } else { + // @ts-ignore + key = association.sourceKey; + M = association.source; + } + if (isStringOrNumber(value)) { + let instance: any = await M.findOne({ + where: { + [key]: value[key], + }, + transaction, + }); + if (!instance) { + instance = await M.create(value, { transaction }); + } + await model[setAccessor](value[key]); + await updateAssociations(instance, value, { transaction, ...options }); + } else { + const instance = await model[createAccessor](value, { transaction }); + await updateAssociations(instance, value, { transaction, ...options }); + } + if (!options.transaction) { + await transaction.commit(); + } + } catch (error) { + if (!options.transaction) { + await transaction.rollback(); + } + throw error; + } +} + +export async function updateMultipleAssociation( + model: Model, + key: string, + value: any, + options: any = {}, +) { + // @ts-ignore + const association = model.constructor.associations[key] as Association; + if (!association) { + return false; + } + if (!['undefined', 'string', 'number', 'object'].includes(typeof value)) { + return false; + } + const { transaction = await model.sequelize.transaction() } = options; + try { + // @ts-ignore + const setAccessor = association.accessors.set; + // @ts-ignore + const createAccessor = association.accessors.create; + if (isUndefinedOrNull(value)) { + return await model[setAccessor](null, { transaction }); + } + if (isStringOrNumber(value)) { + return await model[setAccessor](value, { transaction }); + } + if (!Array.isArray(value)) { + value = [value]; + } + const list1 = []; // to be setted + const list2 = []; // to be added + for (const item of value) { + if (isUndefinedOrNull(item)) { + continue; + } + if (isStringOrNumber(item)) { + list1.push(item); + } else if (item instanceof Model) { + list1.push(item); + } else if (item.sequelize) { + list1.push(item); + } else if (typeof item === 'object') { + list2.push(item); + } + } + console.log('updateMultipleAssociation', list1, list2); + await model[setAccessor](list1, { transaction }); + for (const item of list2) { + const pk = association.target.primaryKeyAttribute; + if (isUndefinedOrNull(item[pk])) { + const instance = await model[createAccessor](item, { transaction }); + await updateAssociations(instance, item, { transaction, ...options }); + } else { + const instance = await association.target.findByPk(item[pk], { transaction }); + // @ts-ignore + const addAccessor = association.accessors.add; + await model[addAccessor](item[pk], { transaction }); + await updateAssociations(instance, item, { transaction, ...options }); + } + } + if (!options.transaction) { + await transaction.commit(); + } + } catch (error) { + await transaction.rollback(); + throw error; + } +} diff --git a/packages/collections/src/utils.ts b/packages/collections/src/utils.ts new file mode 100644 index 0000000000..ad5a964386 --- /dev/null +++ b/packages/collections/src/utils.ts @@ -0,0 +1,18 @@ +export default { + fiter: { + and: [ + { a: 'a' }, + { b: 'b' }, + { c: 'c' }, + { 'assoc.a': 'abc1' }, + { 'assoc.b': 'abc2' }, + { 'assoc.c': 'abc3' }, + { + and: [ + { 'assoc.a': 'abc1' }, + { 'assoc.b': 'abc2' }, + ], + }, + ], + }, +}; diff --git a/packages/plugin-action-logs/src/server.ts b/packages/plugin-action-logs/src/server.ts index 34b09c0528..f4d874567d 100644 --- a/packages/plugin-action-logs/src/server.ts +++ b/packages/plugin-action-logs/src/server.ts @@ -1,13 +1,16 @@ import path from 'path'; -import Application from '@nocobase/server'; +import { IPlugin } from '@nocobase/server'; import { afterCreate, afterUpdate, afterDestroy } from './hooks'; -export default async function (this: Application) { - const { database } = this; - database.import({ - directory: path.resolve(__dirname, 'collections'), - }); - database.on('afterCreate', afterCreate); - database.on('afterUpdate', afterUpdate); - database.on('afterDestroy', afterDestroy); -} +export default { + name: 'action-logs', + async load() { + const database = this.app.db; + database.import({ + directory: path.resolve(__dirname, 'collections'), + }); + database.on('afterCreate', afterCreate); + database.on('afterUpdate', afterUpdate); + database.on('afterDestroy', afterDestroy); + } +} as IPlugin; diff --git a/packages/plugin-automations/src/server.ts b/packages/plugin-automations/src/server.ts index 83b64f5087..43c43cb5c0 100644 --- a/packages/plugin-automations/src/server.ts +++ b/packages/plugin-automations/src/server.ts @@ -1,3 +1,4 @@ +// @ts-nocheck import Database, { registerModels } from '@nocobase/database'; import Resourcer from '@nocobase/resourcer'; import path from 'path'; diff --git a/packages/plugin-china-region/src/server.ts b/packages/plugin-china-region/src/server.ts index cfe363686c..85428ea2ae 100644 --- a/packages/plugin-china-region/src/server.ts +++ b/packages/plugin-china-region/src/server.ts @@ -1,19 +1,22 @@ import path from 'path'; -import Database, { registerModels } from '@nocobase/database'; +import { registerModels } from '@nocobase/database'; import { ChinaRegion } from './models/china-region'; -import Application from '@nocobase/server'; +import { Plugin } from '@nocobase/server'; registerModels({ ChinaRegion }); -export default async function (this: Application, options = {}) { - const { db } = this; - - db.import({ - directory: path.resolve(__dirname, 'collections'), - }); - - this.on('db.init', async () => { - const M = db.getModel('china_regions'); - await M.importData(); - }); -} +export default { + name: 'china-region', + async load(this: Plugin) { + const db = this.app.db; + + db.import({ + directory: path.resolve(__dirname, 'collections'), + }); + + this.app.on('db.init', async () => { + const M = db.getModel('china_regions'); + await M.importData(); + }); + } +}; diff --git a/packages/plugin-collections/src/server.ts b/packages/plugin-collections/src/server.ts index 44c01a91cd..9908c997f8 100644 --- a/packages/plugin-collections/src/server.ts +++ b/packages/plugin-collections/src/server.ts @@ -1,123 +1,132 @@ import path from 'path'; -import { Application } from '@nocobase/server'; +import { Plugin } from '@nocobase/server'; import { registerModels, Table, uid } from '@nocobase/database'; import * as models from './models'; import { createOrUpdate, findAll } from './actions'; import { create } from './actions/fields'; -export default async function (this: Application, options = {}) { - const database = this.db; +export default { + name: 'collections', + async load(this: Plugin) { + const database = this.app.db; - registerModels(models); + registerModels(models); - database.import({ - directory: path.resolve(__dirname, 'collections'), - }); - - this.on('afterLoadPlugins', async () => { - await database.getModel('collections').load(); - }); - - this.on('db.init', async () => { - const userTable = database.getTable('users'); - const config = userTable.getOptions(); - const Collection = database.getModel('collections'); - const collection = await Collection.create(config); - await collection.updateAssociations({ - generalFields: config.fields.filter((field) => field.state !== 0), - systemFields: config.fields.filter((field) => field.state === 0), + database.import({ + directory: path.resolve(__dirname, 'collections'), }); - await collection.migrate(); - }); - const [Collection, Field] = database.getModels(['collections', 'fields']); + this.app.on('beforeStart', async () => { + await database.getModel('collections').load(); + }); - database.on('fields.beforeCreate', async (model) => { - if (!model.get('name')) { - model.set('name', model.get('key')); - } - if (!model.get('collection_name') && model.get('parentKey')) { - const field = await Field.findByPk(model.get('parentKey')); - if (field) { - const { target } = field.get('options') || {}; - if (target) { - model.set('collection_name', target); + this.app.on('db.init', async () => { + const userTable = database.getTable('users'); + const config = userTable.getOptions(); + const Collection = database.getModel('collections'); + const collection = await Collection.create(config); + await collection.updateAssociations({ + generalFields: config.fields.filter((field) => field.state !== 0), + systemFields: config.fields.filter((field) => field.state === 0), + }); + await collection.migrate(); + }); + + const [Collection, Field] = database.getModels(['collections', 'fields']); + + database.on('fields.beforeCreate', async (model) => { + if (!model.get('name')) { + model.set('name', model.get('key')); + } + if (!model.get('collection_name') && model.get('parentKey')) { + const field = await Field.findByPk(model.get('parentKey')); + if (field) { + const { target } = field.get('options') || {}; + if (target) { + model.set('collection_name', target); + } } } - } - }); + }); - database.on('fields.beforeUpdate', async (model) => { - console.log('beforeUpdate', model.key); - if (!model.get('collection_name') && model.get('parentKey')) { - const field = await Field.findByPk(model.get('parentKey')); - if (field) { - const { target } = field.get('options') || {}; - if (target) { - model.set('collection_name', target); + database.on('fields.beforeUpdate', async (model) => { + console.log('beforeUpdate', model.key); + if (!model.get('collection_name') && model.get('parentKey')) { + const field = await Field.findByPk(model.get('parentKey')); + if (field) { + const { target } = field.get('options') || {}; + if (target) { + model.set('collection_name', target); + } } } - } - }); + }); - database.on('fields.afterCreate', async (model) => { - console.log('afterCreate', model.key, model.get('collection_name')); - if (model.get('interface') !== 'subTable') { - return; - } - const { target } = model.get('options') || {}; - // const uiSchemaKey = model.get('ui_schema_key'); - // console.log({ uiSchemaKey }) - try { - let collection = await Collection.findOne({ - where: { - name: target, - }, - }); - if (!collection) { - collection = await Collection.create({ - name: target, - // ui_schema_key: uiSchemaKey, - }); + database.on('fields.afterCreate', async (model) => { + console.log('afterCreate', model.key, model.get('collection_name')); + if (model.get('interface') !== 'subTable') { + return; } - // if (model.get('ui_schema_key')) { - // collection.set('ui_schema_key', model.get('ui_schema_key')); - // await collection.save({ hooks: false }); - // } - await collection.migrate(); - } catch (error) { - throw error; - } - }); - - database.on('fields.afterUpdate', async (model) => { - console.log('afterUpdate'); - if (model.get('interface') !== 'subTable') { - return; - } - const { target } = model.get('options') || {}; - try { - let collection = await Collection.findOne({ - where: { - name: target, - }, - }); - if (!collection) { - collection = await Collection.create({ - name: target, + const { target } = model.get('options') || {}; + // const uiSchemaKey = model.get('ui_schema_key'); + // console.log({ uiSchemaKey }) + try { + let collection = await Collection.findOne({ + where: { + name: target, + }, }); + if (!collection) { + collection = await Collection.create({ + name: target, + // ui_schema_key: uiSchemaKey, + }); + } + // if (model.get('ui_schema_key')) { + // collection.set('ui_schema_key', model.get('ui_schema_key')); + // await collection.save({ hooks: false }); + // } + await collection.migrate(); + } catch (error) { + throw error; } - // if (model.get('ui_schema_key')) { - // collection.set('ui_schema_key', model.get('ui_schema_key')); - // await collection.save({ hooks: false }); - // } - await collection.migrate(); - } catch (error) { - throw error; - } - }); + }); - this.resourcer.registerActionHandler('collections.fields:create', create); - this.resourcer.registerActionHandler('collections:findAll', findAll); - this.resourcer.registerActionHandler('collections:createOrUpdate', createOrUpdate); -} + database.on('fields.afterUpdate', async (model) => { + console.log('afterUpdate'); + if (model.get('interface') !== 'subTable') { + return; + } + const { target } = model.get('options') || {}; + try { + let collection = await Collection.findOne({ + where: { + name: target, + }, + }); + if (!collection) { + collection = await Collection.create({ + name: target, + }); + } + // if (model.get('ui_schema_key')) { + // collection.set('ui_schema_key', model.get('ui_schema_key')); + // await collection.save({ hooks: false }); + // } + await collection.migrate(); + } catch (error) { + throw error; + } + }); + + this.app.resourcer.registerActionHandler( + 'collections.fields:create', + create, + ); + this.app.resourcer.registerActionHandler('collections:findAll', findAll); + this.app.resourcer.registerActionHandler( + 'collections:createOrUpdate', + createOrUpdate, + ); + }, +}; diff --git a/packages/plugin-export/src/server.ts b/packages/plugin-export/src/server.ts index 5999094a1f..558f771d7d 100644 --- a/packages/plugin-export/src/server.ts +++ b/packages/plugin-export/src/server.ts @@ -1,28 +1,29 @@ -import Resourcer from '@nocobase/resourcer'; +import { PluginOptions } from '@nocobase/server'; import _export from './actions/export'; export const ACTION_NAME_EXPORT = 'export'; -export default async function (options = {}) { - const resourcer: Resourcer = this.resourcer; - - resourcer.registerActionHandler(ACTION_NAME_EXPORT, _export); - - // // TODO(temp): 继承 list 权限的临时写法 - // resourcer.use(async (ctx, next) => { - // if (ctx.action.params.actionName === ACTION_NAME_EXPORT) { - // ctx.action.mergeParams({ - // actionName: 'list' - // }); - - // console.log('action name in export has been rewritten to:', ctx.action.params.actionName); - - // const permissionPlugin = ctx.app.getPluginInstance('@nocobase/plugin-permissions'); - // if (permissionPlugin) { - // return permissionPlugin.middleware(ctx, next); - // } - // } - - // await next(); - // }); -} +export default { + name: 'export', + async load() { + const resourcer = this.app.resourcer; + resourcer.registerActionHandler(ACTION_NAME_EXPORT, _export); + // // TODO(temp): 继承 list 权限的临时写法 + // resourcer.use(async (ctx, next) => { + // if (ctx.action.params.actionName === ACTION_NAME_EXPORT) { + // ctx.action.mergeParams({ + // actionName: 'list' + // }); + + // console.log('action name in export has been rewritten to:', ctx.action.params.actionName); + + // const permissionPlugin = ctx.app.getPluginInstance('@nocobase/plugin-permissions'); + // if (permissionPlugin) { + // return permissionPlugin.middleware(ctx, next); + // } + // } + + // await next(); + // }); + } +} as PluginOptions; diff --git a/packages/plugin-file-manager/src/server.ts b/packages/plugin-file-manager/src/server.ts index 16748aee32..f8fcf94ab4 100644 --- a/packages/plugin-file-manager/src/server.ts +++ b/packages/plugin-file-manager/src/server.ts @@ -1,6 +1,7 @@ import path from 'path'; import Database from '@nocobase/database'; import Resourcer from '@nocobase/resourcer'; +import { PluginOptions, Plugin } from '@nocobase/server'; import { action as uploadAction, @@ -10,40 +11,43 @@ import { middleware as localMiddleware, } from './storages/local'; -export default async function () { - const database: Database = this.database; - const resourcer: Resourcer = this.resourcer; - - database.import({ - directory: path.resolve(__dirname, 'collections'), - }); - - // 暂时中间件只能通过 use 加进来 - resourcer.use(uploadMiddleware); - resourcer.registerActionHandler('upload', uploadAction); - localMiddleware(this); - - const Storage = database.getModel('storages'); - - this.on('db.init', async () => { - await Storage.create({ - title: '本地存储', - name: `local`, - type: 'local', - baseUrl: process.env.LOCAL_STORAGE_BASE_URL, - default: process.env.STORAGE_TYPE === 'local', +export default { + name: 'file-manager', + async load() { + const database: Database = this.app.db; + const resourcer: Resourcer = this.app.resourcer; + + database.import({ + directory: path.resolve(__dirname, 'collections'), }); - 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', + + // 暂时中间件只能通过 use 加进来 + resourcer.use(uploadMiddleware); + resourcer.registerActionHandler('upload', uploadAction); + localMiddleware(this.app); + + const Storage = database.getModel('storages'); + + this.app.on('db.init', async () => { + 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', + }); }); - }); -} + }, +} as PluginOptions; diff --git a/packages/plugin-permissions/src/server.ts b/packages/plugin-permissions/src/server.ts index f5270af55d..88752bad0a 100644 --- a/packages/plugin-permissions/src/server.ts +++ b/packages/plugin-permissions/src/server.ts @@ -1,12 +1,12 @@ import path from 'path'; -import Database from '@nocobase/database'; -import Resourcer from '@nocobase/resourcer'; +import { PluginOptions } from '@nocobase/server'; -export default async function () { - const database: Database = this.database; - const resourcer: Resourcer = this.resourcer; - - database.import({ - directory: path.resolve(__dirname, 'collections'), - }); -} +export default { + name: 'permissions', + async load() { + const database = this.app.db; + database.import({ + directory: path.resolve(__dirname, 'collections'), + }); + } +} as PluginOptions; diff --git a/packages/plugin-system-settings/src/server.ts b/packages/plugin-system-settings/src/server.ts index 4858d674a3..41acbba25d 100644 --- a/packages/plugin-system-settings/src/server.ts +++ b/packages/plugin-system-settings/src/server.ts @@ -1,42 +1,45 @@ import path from 'path'; -import { Application } from '@nocobase/server'; +import { PluginOptions } from '@nocobase/server'; -export default async function (this: Application, options = {}) { - const database = this.database; - const resourcer = this.resourcer; +export default { + name: 'system-settings', + async load() { + const database = this.app.db; + const resourcer = this.app.resourcer; - database.import({ - directory: path.resolve(__dirname, 'collections'), - }); + database.import({ + directory: path.resolve(__dirname, 'collections'), + }); - const SystemSetting = database.getModel('system_settings'); - - resourcer.use(async (ctx, next) => { - const { actionName, resourceName, resourceKey } = ctx.action.params; - if (resourceName === 'system_settings' && actionName === 'get') { - let model = await SystemSetting.findOne(); - if (!model) { - model = await SystemSetting.create(); + const SystemSetting = database.getModel('system_settings'); + + resourcer.use(async (ctx, next) => { + const { actionName, resourceName, resourceKey } = ctx.action.params; + if (resourceName === 'system_settings' && actionName === 'get') { + let model = await SystemSetting.findOne(); + if (!model) { + model = await SystemSetting.create(); + } + ctx.action.mergeParams({ + resourceKey: model.id, + }); } - ctx.action.mergeParams({ - resourceKey: model.id, + await next(); + }); + + this.app.on('db.init', async () => { + const setting = await SystemSetting.create({ + title: 'NocoBase', + }); + await setting.updateAssociations({ + logo: { + title: 'nocobase-logo', + filename: '682e5ad037dd02a0fe4800a3e91c283b.png', + extname: '.png', + mimetype: 'image/png', + url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/682e5ad037dd02a0fe4800a3e91c283b.png', + }, }); - } - await next(); - }); - - this.on('db.init', async () => { - const setting = await SystemSetting.create({ - title: 'NocoBase', }); - await setting.updateAssociations({ - logo: { - title: 'nocobase-logo', - filename: '682e5ad037dd02a0fe4800a3e91c283b.png', - extname: '.png', - mimetype: 'image/png', - url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/682e5ad037dd02a0fe4800a3e91c283b.png', - }, - }); - }); -} + } +} as PluginOptions; diff --git a/packages/plugin-ui-router/src/server.ts b/packages/plugin-ui-router/src/server.ts index e5d1475378..4d6c3de259 100644 --- a/packages/plugin-ui-router/src/server.ts +++ b/packages/plugin-ui-router/src/server.ts @@ -1,61 +1,64 @@ import path from 'path'; -import { Application } from '@nocobase/server'; +import { PluginOptions } from '@nocobase/server'; import { registerModels } from '@nocobase/database'; import * as models from './models'; import getAccessible from './actions/getAccessible'; import * as uiSchema from './ui-schema'; -export default async function (this: Application, options = {}) { - const database = this.database; - registerModels(models); +export default { + name: 'ui-router', + async load() { + const database = this.app.db; + registerModels(models); - database.import({ - directory: path.resolve(__dirname, 'collections'), - }); - - this.resourcer.registerActionHandler('routes:getAccessible', getAccessible); - - const Route = database.getModel('routes'); - - this.on('db.init', async () => { - const data = [ - { - type: 'redirect', - from: '/', - to: '/admin', - exact: true, - }, - { - type: 'route', - path: '/admin/:name(.+)?', - component: 'AdminLayout', - title: `后台`, - uiSchema: uiSchema.menu, - }, - { - type: 'route', - component: 'AuthLayout', - children: [ - { - type: 'route', - path: '/login', - component: 'RouteSchemaRenderer', - title: `登录`, - uiSchema: uiSchema.login, - }, - { - type: 'route', - path: '/register', - component: 'RouteSchemaRenderer', - title: `注册`, - uiSchema: uiSchema.register, - }, - ], - }, - ]; - for (const item of data) { - const route = await Route.create(item); - await route.updateAssociations(item); - } - }); -} + database.import({ + directory: path.resolve(__dirname, 'collections'), + }); + + this.app.resourcer.registerActionHandler('routes:getAccessible', getAccessible); + + const Route = database.getModel('routes'); + + this.app.on('db.init', async () => { + const data = [ + { + type: 'redirect', + from: '/', + to: '/admin', + exact: true, + }, + { + type: 'route', + path: '/admin/:name(.+)?', + component: 'AdminLayout', + title: `后台`, + uiSchema: uiSchema.menu, + }, + { + type: 'route', + component: 'AuthLayout', + children: [ + { + type: 'route', + path: '/login', + component: 'RouteSchemaRenderer', + title: `登录`, + uiSchema: uiSchema.login, + }, + { + type: 'route', + path: '/register', + component: 'RouteSchemaRenderer', + title: `注册`, + uiSchema: uiSchema.register, + }, + ], + }, + ]; + for (const item of data) { + const route = await Route.create(item); + await route.updateAssociations(item); + } + }); + } +} as PluginOptions; diff --git a/packages/plugin-ui-schema/src/server.ts b/packages/plugin-ui-schema/src/server.ts index a40247510a..eaa0a749cd 100644 --- a/packages/plugin-ui-schema/src/server.ts +++ b/packages/plugin-ui-schema/src/server.ts @@ -1,18 +1,22 @@ import path from 'path'; -import { Application } from '@nocobase/server'; +import { PluginOptions } from '@nocobase/server'; import { registerModels } from '@nocobase/database'; import * as models from './models'; import * as actions from './actions'; -export default async function (this: Application, options = {}) { - const database = this.database; - registerModels(models); +registerModels(models); - database.import({ - directory: path.resolve(__dirname, 'collections'), - }); - - for (const [name, action] of Object.entries(actions)) { - this.resourcer.registerActionHandler(`ui_schemas:${name}`, action); +export default { + name: 'ui-schema', + async load() { + const database = this.app.db; + + database.import({ + directory: path.resolve(__dirname, 'collections'), + }); + + for (const [name, action] of Object.entries(actions)) { + this.app.resourcer.registerActionHandler(`ui_schemas:${name}`, action); + } } -} +} as PluginOptions; diff --git a/packages/plugin-users/src/server.ts b/packages/plugin-users/src/server.ts index cf40d85833..10e5ece570 100644 --- a/packages/plugin-users/src/server.ts +++ b/packages/plugin-users/src/server.ts @@ -1,51 +1,53 @@ import path from 'path'; -import Database, { registerFields, Table } from '@nocobase/database'; -import Resourcer from '@nocobase/resourcer'; +import { registerFields, Table } from '@nocobase/database'; import * as fields from './fields'; import * as usersActions from './actions/users'; import * as middlewares from './middlewares'; -import Application from '@nocobase/server'; +import { PluginOptions } from '@nocobase/server'; -export default async function (this: Application, options = {}) { - const database: Database = this.database; - const resourcer: Resourcer = this.resourcer; +export default { + name: 'users', + async load() { + const database = this.app.db; + const resourcer = this.app.resourcer; - registerFields(fields); + registerFields(fields); - this.on('db.init', async () => { - const User = database.getModel('users'); - await User.create({ - nickname: '超级管理员', - email: process.env.ADMIN_EMAIL, - password: process.env.ADMIN_PASSWORD, + this.app.on('db.init', async () => { + const User = database.getModel('users'); + await User.create({ + nickname: '超级管理员', + email: process.env.ADMIN_EMAIL, + password: process.env.ADMIN_PASSWORD, + }); }); - }); - database.on('afterTableInit', (table: Table) => { - let { createdBy, updatedBy } = table.getOptions(); - if (createdBy !== false) { - table.addField({ - type: 'createdBy', - name: typeof createdBy === 'string' ? createdBy : 'createdBy', - target: 'users', - }); + database.on('afterTableInit', (table: Table) => { + let { createdBy, updatedBy } = table.getOptions(); + if (createdBy !== false) { + table.addField({ + type: 'createdBy', + name: typeof createdBy === 'string' ? createdBy : 'createdBy', + target: 'users', + }); + } + if (updatedBy !== false) { + table.addField({ + type: 'updatedBy', + name: typeof updatedBy === 'string' ? updatedBy : 'updatedBy', + target: 'users', + }); + } + }); + + database.import({ + directory: path.resolve(__dirname, 'collections'), + }); + + for (const [key, action] of Object.entries(usersActions)) { + resourcer.registerActionHandler(`users:${key}`, action); } - if (updatedBy !== false) { - table.addField({ - type: 'updatedBy', - name: typeof updatedBy === 'string' ? updatedBy : 'updatedBy', - target: 'users', - }); - } - }); - database.import({ - directory: path.resolve(__dirname, 'collections'), - }); - - for (const [key, action] of Object.entries(usersActions)) { - resourcer.registerActionHandler(`users:${key}`, action); - } - - resourcer.use(middlewares.parseToken(options)); -} + resourcer.use(middlewares.parseToken({})); + }, +} as PluginOptions; diff --git a/packages/server/src/__tests__/plugin.test.ts b/packages/server/src/__tests__/plugin.test.ts index 26f358a863..94f7526888 100644 --- a/packages/server/src/__tests__/plugin.test.ts +++ b/packages/server/src/__tests__/plugin.test.ts @@ -38,20 +38,25 @@ describe('plugin', () => { expect(plugin).toBeInstanceOf(Plugin); expect(plugin.getName()).toBe('abc'); }); + it('plugin name', async () => { - const plugin = app.plugin(function abc() {}, { - name: 'plugin-name2' + const plugin = app.plugin({ + name: 'plugin-name2', + async load() {}, }); expect(plugin).toBeInstanceOf(Plugin); expect(plugin.getName()).toBe('plugin-name2'); }); + it('plugin name', async () => { - const plugin = app.plugin(function () {}, { - name: 'plugin-name3' + const plugin = app.plugin({ + name: 'plugin-name3', + load: function () {}, }); expect(plugin).toBeInstanceOf(Plugin); expect(plugin.getName()).toBe('plugin-name3'); }); + it('plugin name', async () => { class MyPlugin extends Plugin {} const plugin = app.plugin(MyPlugin); @@ -59,6 +64,15 @@ describe('plugin', () => { expect(plugin.getName()).toBe('MyPlugin'); }); + it('plugin name', async () => { + class MyPlugin extends Plugin {} + const plugin = app.plugin({ + plugin: MyPlugin, + }); + expect(plugin).toBeInstanceOf(MyPlugin); + expect(plugin.getName()).toBe('MyPlugin'); + }); + it('plugin name', async () => { const plugin = app.plugin(path.resolve(__dirname, './plugins/plugin1')); expect(plugin).toBeInstanceOf(Plugin); diff --git a/packages/server/src/application.ts b/packages/server/src/application.ts index 22a0767468..d253e1b371 100644 --- a/packages/server/src/application.ts +++ b/packages/server/src/application.ts @@ -142,17 +142,16 @@ export class Application< console.log(args); const opts = cli.opts(); await this.load(); - await this.emitAsync('server.beforeStart'); + await this.emitAsync('beforeStart'); this.listen(opts.port || 3000); console.log(`http://localhost:${opts.port || 3000}/`); }); } - // @ts-ignore use( middleware: Koa.Middleware, options?: MiddlewareOptions, - ): Application { + ) { // @ts-ignore return super.use(middleware); } @@ -173,25 +172,34 @@ export class Application< return this.cli.command(nameAndArgs, opts); } - plugin(plugin: PluginType, options?: PluginOptions): Plugin { - if (typeof plugin === 'string') { - plugin = require(plugin).default; + plugin(options?: PluginType | PluginOptions): Plugin { + if (typeof options === 'string') { + return this.plugin(require(options).default); } let instance: Plugin; - try { - // @ts-ignore - const p = new plugin(); - if (p instanceof Plugin) { + if (typeof options === 'function') { + try { // @ts-ignore - instance = new plugin({}, { - ...options, + instance = new options({ + name: options.name, + app: this, + }); + if (!(instance instanceof Plugin)) { + throw new Error('plugin must be instanceof Plugin'); + } + } catch (err) { + // console.log(err); + instance = new Plugin({ + name: options.name, + // @ts-ignore + load: options, app: this, }); - } else { - throw new Error('plugin must be instanceof Plugin'); } - } catch (err) { - instance = new Plugin(plugin, { + } else if (typeof options === 'object') { + const plugin = options.plugin || Plugin; + instance = new plugin({ + name: options.plugin ? plugin.name : undefined, ...options, app: this, }); diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 83658ceacf..e19d8019d2 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,3 +1,4 @@ export * from './application'; export * as middlewares from './middlewares'; +export * from './plugin'; export { Application as default } from './application'; diff --git a/packages/server/src/plugin.ts b/packages/server/src/plugin.ts index 153e86a9af..97c411ca5f 100644 --- a/packages/server/src/plugin.ts +++ b/packages/server/src/plugin.ts @@ -1,47 +1,37 @@ import { uid } from '@nocobase/database'; import { Application } from './application'; - -export interface PluginOptions { - app?: Application; - name?: string; - activate?: boolean; - displayName?: string; - description?: string; - version?: string; -} +import _ from 'lodash'; export interface IPlugin { install?: (this: Plugin) => void; load?: (this: Plugin) => void; } +export interface PluginOptions { + name?: string; + activate?: boolean; + displayName?: string; + description?: string; + version?: string; + install?: (this: Plugin) => void; + load?: (this: Plugin) => void; + plugin?: typeof Plugin; + [key: string]: any; +} + export type PluginFn = (this: Plugin) => void; -export type PluginType = string | PluginFn | typeof Plugin | IPlugin; +export type PluginType = string | PluginFn | typeof Plugin; export class Plugin implements IPlugin { options: PluginOptions = {}; app: Application; callbacks: IPlugin = {}; - constructor(plugin?: PluginType, options?: PluginOptions) { + constructor(options?: PluginOptions & { app?: Application }) { this.app = options?.app; - this.options = options || {}; - if (typeof plugin === 'function') { - if (!this.options?.name && plugin.name) { - this.options.name = plugin.name; - } - this.callbacks.load = plugin as any; - } else if ( - typeof plugin === 'object' && - plugin.constructor === {}.constructor - ) { - this.callbacks = plugin; - } - const cName = this.constructor.name; - if (this.options && !this.options?.name && cName && cName !== 'Plugin') { - this.options.name = cName; - } + this.options = options; + this.callbacks = _.pick(options, ['load', 'activate']); } getName() { diff --git a/yarn.lock b/yarn.lock index 3835af8574..8510f1741f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2381,6 +2381,17 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@jest/types@^27.1.1": + version "27.1.1" + resolved "https://registry.npmjs.org/@jest/types/-/types-27.1.1.tgz#77a3fc014f906c65752d12123a0134359707c0ad" + integrity sha512-yqJPDDseb0mXgKqmNqypCsb85C22K1aY5+LUxh7syIM9n/b0AsaltxNy+o6tt29VcfGDpYEve175bm3uOhcehA== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^16.0.0" + chalk "^4.0.0" + "@juggle/resize-observer@^3.3.1": version "3.3.1" resolved "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" @@ -3758,12 +3769,13 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^24.0.18": - version "24.9.1" - resolved "https://registry.npmjs.org/@types/jest/-/jest-24.9.1.tgz#02baf9573c78f1b9974a5f36778b366aa77bd534" - integrity sha512-Fb38HkXSVA4L8fGKEZ6le5bB8r6MRWlOCZbVuWZcmOMSCd2wCYOwN1ibj8daIoV9naq7aaOZjrLCoCMptKU/4Q== +"@types/jest@^27.0.1": + version "27.0.1" + resolved "https://registry.npmjs.org/@types/jest/-/jest-27.0.1.tgz#fafcc997da0135865311bb1215ba16dba6bdf4ca" + integrity sha512-HTLpVXHrY69556ozYkcq47TtQJXpcWAWfkoqz+ZGz2JnmZhzlRjprCIyFnetSy8gpDWwTTGBcRVv1J1I1vBrHw== dependencies: - jest-diff "^24.3.0" + jest-diff "^27.0.0" + pretty-format "^27.0.0" "@types/js-cookie@^2.2.6": version "2.2.7" @@ -4103,6 +4115,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^16.0.0": + version "16.0.4" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" + integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== + dependencies: + "@types/yargs-parser" "*" + "@typescript-eslint/eslint-plugin@^4.9.1": version "4.29.1" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.1.tgz#808d206e2278e809292b5de752a91105da85860b" @@ -4932,6 +4951,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + ansi-wrap@0.1.0, ansi-wrap@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" @@ -7666,6 +7690,11 @@ diff-sequences@^26.6.2: resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== +diff-sequences@^27.0.6: + version "27.0.6" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723" + integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ== + diff@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -11153,7 +11182,7 @@ jest-config@^26.6.3: micromatch "^4.0.2" pretty-format "^26.6.2" -jest-diff@^24.3.0, jest-diff@^24.9.0: +jest-diff@^24.9.0: version "24.9.0" resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ== @@ -11173,6 +11202,16 @@ jest-diff@^26.6.2: jest-get-type "^26.3.0" pretty-format "^26.6.2" +jest-diff@^27.0.0: + version "27.2.0" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-27.2.0.tgz#bda761c360f751bab1e7a2fe2fc2b0a35ce8518c" + integrity sha512-QSO9WC6btFYWtRJ3Hac0sRrkspf7B01mGrrQEiCW6TobtViJ9RWL0EmOs/WnBsZDsI/Y2IoSHZA2x6offu0sYw== + dependencies: + chalk "^4.0.0" + diff-sequences "^27.0.6" + jest-get-type "^27.0.6" + pretty-format "^27.2.0" + jest-docblock@^24.3.0: version "24.9.0" resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2" @@ -11279,6 +11318,11 @@ jest-get-type@^26.3.0: resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== +jest-get-type@^27.0.6: + version "27.0.6" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz#0eb5c7f755854279ce9b68a9f1a4122f69047cfe" + integrity sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg== + jest-haste-map@^24.9.0: version "24.9.0" resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d" @@ -15401,6 +15445,16 @@ pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" +pretty-format@^27.0.0, pretty-format@^27.2.0: + version "27.2.0" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.2.0.tgz#ee37a94ce2a79765791a8649ae374d468c18ef19" + integrity sha512-KyJdmgBkMscLqo8A7K77omgLx5PWPiXJswtTtFV7XgVZv2+qPk6UivpXXO+5k6ZEbWIbLoKdx1pZ6ldINzbwTA== + dependencies: + "@jest/types" "^27.1.1" + ansi-regex "^5.0.0" + ansi-styles "^5.0.0" + react-is "^17.0.1" + printj@~1.1.0, printj@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"