From 976b094f96743b91d2ba6a2c2d0368160d9f0c43 Mon Sep 17 00:00:00 2001 From: ChengLei Shao <chareice@live.com> Date: Mon, 13 Mar 2023 20:43:57 +0800 Subject: [PATCH] feat: multi-app-share-collection plugin (#1562) * feat: multi-app-share-collection plugin * chore: plugin name * fix: build * chore: pg only test * fix: test * Update package.json * Create --------- Co-authored-by: chenos <chenlinxh@gmail.com> --- .../src/plugins/multi-app-share-collection.ts | 1 + .../multi-app-share-collection/client.d.ts | 4 + .../multi-app-share-collection/client.js | 30 ++ .../multi-app-share-collection/package.json | 11 + .../multi-app-share-collection/server.d.ts | 4 + .../multi-app-share-collection/server.js | 30 ++ .../src/client/index.tsx | 39 ++ .../multi-app-share-collection/src/index.ts | 1 + .../server/__tests__/collection-sync.test.ts | 377 ++++++++++++++++++ .../src/server/__tests__/index.ts | 25 ++ .../src/server/collections/.gitkeep | 0 .../src/server/index.ts | 1 + .../src/server/plugin.ts | 283 +++++++++++++ packages/presets/nocobase/package.json | 1 + packages/presets/nocobase/src/index.ts | 1 + 15 files changed, 808 insertions(+) create mode 100644 packages/app/client/src/plugins/multi-app-share-collection.ts create mode 100755 packages/plugins/multi-app-share-collection/client.d.ts create mode 100755 packages/plugins/multi-app-share-collection/client.js create mode 100644 packages/plugins/multi-app-share-collection/package.json create mode 100755 packages/plugins/multi-app-share-collection/server.d.ts create mode 100755 packages/plugins/multi-app-share-collection/server.js create mode 100644 packages/plugins/multi-app-share-collection/src/client/index.tsx create mode 100644 packages/plugins/multi-app-share-collection/src/index.ts create mode 100644 packages/plugins/multi-app-share-collection/src/server/__tests__/collection-sync.test.ts create mode 100644 packages/plugins/multi-app-share-collection/src/server/__tests__/index.ts create mode 100644 packages/plugins/multi-app-share-collection/src/server/collections/.gitkeep create mode 100644 packages/plugins/multi-app-share-collection/src/server/index.ts create mode 100644 packages/plugins/multi-app-share-collection/src/server/plugin.ts diff --git a/packages/app/client/src/plugins/multi-app-share-collection.ts b/packages/app/client/src/plugins/multi-app-share-collection.ts new file mode 100644 index 000000000..2e6520e0c --- /dev/null +++ b/packages/app/client/src/plugins/multi-app-share-collection.ts @@ -0,0 +1 @@ +export { default } from '@nocobase/plugin-multi-app-share-collection/client'; diff --git a/packages/plugins/multi-app-share-collection/client.d.ts b/packages/plugins/multi-app-share-collection/client.d.ts new file mode 100755 index 000000000..765db9222 --- /dev/null +++ b/packages/plugins/multi-app-share-collection/client.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +export * from './lib/client'; +export { default } from './lib/client'; + diff --git a/packages/plugins/multi-app-share-collection/client.js b/packages/plugins/multi-app-share-collection/client.js new file mode 100755 index 000000000..238820257 --- /dev/null +++ b/packages/plugins/multi-app-share-collection/client.js @@ -0,0 +1,30 @@ +"use strict"; + +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } + +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } + +var _index = _interopRequireWildcard(require("./lib/client")); + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _exportNames = {}; +Object.defineProperty(exports, "default", { + enumerable: true, + get: function get() { + return _index.default; + } +}); + +Object.keys(_index).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _index[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _index[key]; + } + }); +}); diff --git a/packages/plugins/multi-app-share-collection/package.json b/packages/plugins/multi-app-share-collection/package.json new file mode 100644 index 000000000..6e47a4dee --- /dev/null +++ b/packages/plugins/multi-app-share-collection/package.json @@ -0,0 +1,11 @@ +{ + "name": "@nocobase/plugin-multi-app-share-collection", + "version": "0.9.1-alpha.2", + "main": "lib/server/index.js", + "devDependencies": { + "@nocobase/client": "0.9.1-alpha.2", + "@nocobase/server": "0.9.1-alpha.2", + "@nocobase/test": "0.9.1-alpha.2", + "@nocobase/plugin-multi-app-manager": "0.9.1-alpha.2" + } +} diff --git a/packages/plugins/multi-app-share-collection/server.d.ts b/packages/plugins/multi-app-share-collection/server.d.ts new file mode 100755 index 000000000..e70edb928 --- /dev/null +++ b/packages/plugins/multi-app-share-collection/server.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +export * from './lib/server'; +export { default } from './lib/server'; + diff --git a/packages/plugins/multi-app-share-collection/server.js b/packages/plugins/multi-app-share-collection/server.js new file mode 100755 index 000000000..d06a7eb92 --- /dev/null +++ b/packages/plugins/multi-app-share-collection/server.js @@ -0,0 +1,30 @@ +"use strict"; + +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } + +function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } + +var _index = _interopRequireWildcard(require("./lib/server")); + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _exportNames = {}; +Object.defineProperty(exports, "default", { + enumerable: true, + get: function get() { + return _index.default; + } +}); + +Object.keys(_index).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _index[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function get() { + return _index[key]; + } + }); +}); diff --git a/packages/plugins/multi-app-share-collection/src/client/index.tsx b/packages/plugins/multi-app-share-collection/src/client/index.tsx new file mode 100644 index 000000000..efa4c2d69 --- /dev/null +++ b/packages/plugins/multi-app-share-collection/src/client/index.tsx @@ -0,0 +1,39 @@ +import { collectionTemplates, Select, useRequest } from '@nocobase/client'; +import React from 'react'; + +const AppSelect = (props) => { + const { data, loading } = useRequest({ + resource: 'applications', + action: 'list', + params: { + paginate: false, + }, + }); + return ( + <Select + {...props} + mode={'multiple'} + fieldNames={{ value: 'name', label: 'displayName' }} + options={data?.data || []} + loading={loading} + /> + ); +}; + +collectionTemplates.calendar.configurableProperties.syncToApps = { + type: 'string', + title: '{{ t("Sync to apps") }}', + 'x-decorator': 'FormItem', + 'x-component': AppSelect, +}; + +collectionTemplates.general.configurableProperties.syncToApps = { + type: 'string', + title: '{{ t("Sync to apps") }}', + 'x-decorator': 'FormItem', + 'x-component': AppSelect, +}; + +export default (props) => { + return <>{props.children}</>; +}; diff --git a/packages/plugins/multi-app-share-collection/src/index.ts b/packages/plugins/multi-app-share-collection/src/index.ts new file mode 100644 index 000000000..7ddad5814 --- /dev/null +++ b/packages/plugins/multi-app-share-collection/src/index.ts @@ -0,0 +1 @@ +export { default } from './server'; diff --git a/packages/plugins/multi-app-share-collection/src/server/__tests__/collection-sync.test.ts b/packages/plugins/multi-app-share-collection/src/server/__tests__/collection-sync.test.ts new file mode 100644 index 000000000..01fd38f6b --- /dev/null +++ b/packages/plugins/multi-app-share-collection/src/server/__tests__/collection-sync.test.ts @@ -0,0 +1,377 @@ +import { Database } from '@nocobase/database'; +import { MockServer, mockServer } from '@nocobase/test'; +import Plugin from '..'; +const pgOnly = () => (process.env.DB_DIALECT == 'postgres' ? describe : describe.skip); + +pgOnly()('collection sync', () => { + let mainDb: Database; + let mainApp: MockServer; + + beforeEach(async () => { + const app = mockServer({ + acl: false, + plugins: ['nocobase'], + }); + + await app.load(); + + await app.db.sequelize.query(`DROP SCHEMA IF EXISTS sub1 CASCADE`); + await app.db.sequelize.query(`DROP SCHEMA IF EXISTS sub2 CASCADE`); + await app.install({ + clean: true, + }); + + await app.pm.enable('multi-app-manager'); + await app.pm.enable('multi-app-share-collection'); + + await app.start(); + + mainApp = app; + + mainDb = mainApp.db; + }); + + afterEach(async () => { + await mainApp.destroy(); + }); + + it('should set user role in sub app when user created at main app', async () => { + await mainApp.db.getRepository('applications').create({ + values: { + name: 'sub1', + }, + }); + + const sub1 = await mainApp.appManager.getApplication('sub1'); + + await mainApp.db.getRepository('users').create({ + values: { + email: 'test@qq.com', + password: 'test123', + }, + }); + + const defaultRole = await sub1.db.getRepository('roles').findOne({ + filter: { + default: true, + }, + }); + + const user = await sub1.db.getRepository('users').findOne({ + filter: { + email: 'test@qq.com', + }, + appends: ['roles'], + }); + + expect(user.get('roles').map((item) => item.name)).toEqual([defaultRole.name]); + + await mainApp.db.getRepository('applications').create({ + values: { + name: 'sub2', + }, + }); + + const sub2 = await mainApp.appManager.getApplication('sub2'); + + const defaultRoleInSub2 = await sub2.db.getRepository('roles').findOne({ + filter: { + default: true, + }, + }); + + const userInSub2 = await sub2.db.getRepository('users').findOne({ + filter: { + email: 'test@qq.com', + }, + appends: ['roles'], + }); + + expect(userInSub2.get('roles').map((item) => item.name)).toContain(defaultRoleInSub2.name); + }); + + it.skip('should sync plugin status between apps', async () => { + await mainApp.db.getRepository('applications').create({ + values: { + name: 'sub1', + }, + }); + + const sub1 = await mainApp.appManager.getApplication('sub1'); + + const getSub1MapRecord = async (app) => { + return await app.db.getRepository('applicationPlugins').findOne({ + filter: { + name: 'map', + }, + }); + }; + + expect((await getSub1MapRecord(sub1)).get('enabled')).toBeFalsy(); + await mainApp.pm.enable('map'); + + expect((await getSub1MapRecord(sub1)).get('enabled')).toBeTruthy(); + + await mainApp.db.getRepository('applications').create({ + values: { + name: 'sub2', + }, + }); + + const sub2 = await mainApp.appManager.getApplication('sub2'); + expect((await getSub1MapRecord(sub2)).get('enabled')).toBeTruthy(); + }); + + it('should not sync roles in sub app', async () => { + await mainDb.getRepository('applications').create({ + values: { + name: 'sub1', + }, + }); + + const subApp1 = await mainApp.appManager.getApplication('sub1'); + + await mainDb.getRepository('roles').create({ + values: { + name: 'role1', + }, + }); + + expect( + await subApp1.db.getRepository('roles').findOne({ + filter: { + name: 'role1', + }, + }), + ).toBeFalsy(); + }); + + it('should sync user collections', async () => { + const subApp1Record = await mainDb.getRepository('applications').create({ + values: { + name: 'sub1', + }, + }); + + const subApp1 = await mainApp.appManager.getApplication(subApp1Record.name); + + expect(subApp1.db.getCollection('users').options.schema).toBe(mainDb.options.schema || 'public'); + + const userCollectionRecord = await subApp1.db.getRepository('collections').findOne({ + filter: { + name: 'users', + }, + }); + expect(userCollectionRecord).toBeTruthy(); + await mainApp.db.getRepository('users').create({ + values: { + email: 'test@qq.com', + password: '123456', + }, + }); + const user = await subApp1.db.getRepository('users').find({ + filter: { + email: 'test@qq.com', + }, + }); + expect(user).toBeTruthy(); + }); + + it('should support syncToApps with wildcard value', async () => { + const subApp1Record = await mainDb.getRepository('applications').create({ + values: { + name: 'sub1', + }, + }); + const subApp1 = await mainApp.appManager.getApplication(subApp1Record.name); + + const subApp2Record = await mainDb.getRepository('applications').create({ + values: { + name: 'sub2', + }, + }); + const subApp2 = await mainApp.appManager.getApplication(subApp2Record.name); + + const mainCollection = await mainDb.getRepository('collections').create({ + values: { + name: 'mainCollection', + syncToApps: '*', + }, + context: {}, + }); + + const subApp1MainCollectionRecord = await subApp1.db.getRepository('collections').findOne({ + filter: { + name: 'mainCollection', + }, + }); + + expect(subApp1MainCollectionRecord).toBeTruthy(); + + const subApp2MainCollectionRecord = await subApp2.db.getRepository('collections').findOne({ + filter: { + name: 'mainCollection', + }, + }); + + expect(subApp2MainCollectionRecord).toBeTruthy(); + }); + + it('should sync collections in sub apps', async () => { + const subApp1Record = await mainDb.getRepository('applications').create({ + values: { + name: 'sub1', + }, + }); + + const subApp1 = await mainApp.appManager.getApplication(subApp1Record.name); + + const mainCollection = await mainDb.getRepository('collections').create({ + values: { + name: 'mainCollection', + fields: [ + { + name: 'field0', + type: 'string', + }, + ], + }, + context: {}, + }); + + const subAppMainCollectionRecord = await subApp1.db.getRepository('collections').findOne({ + filter: { + name: 'mainCollection', + }, + }); + + expect(subAppMainCollectionRecord).toBeTruthy(); + + const subAppMainCollection = subApp1.db.getCollection('mainCollection'); + + expect(subAppMainCollection).toBeTruthy(); + expect(subAppMainCollection.options.schema).toBe(mainCollection.options.schema || 'public'); + + await mainApp.db.getRepository('fields').create({ + values: { + collectionName: 'mainCollection', + name: 'title', + type: 'string', + uiSchema: { + type: 'string', + 'x-component': 'Input', + title: 'field1', + }, + }, + context: {}, + }); + + const mainCollectionTitleField = await subApp1.db.getRepository('fields').findOne({ + filter: { + collectionName: 'mainCollection', + name: 'title', + }, + }); + + expect(mainCollectionTitleField).toBeTruthy(); + + await mainApp.db.getRepository('mainCollection').create({ + values: { + title: 'test', + }, + }); + + const subRecord = await subApp1.db.getRepository('mainCollection').findOne({}); + expect(subRecord.get('title')).toBe('test'); + + await mainApp.db.getRepository('fields').update({ + values: { + unique: true, + }, + filter: { + collectionName: 'mainCollection', + name: 'title', + }, + }); + + const mainCollectionTitleField2 = await subApp1.db.getRepository('fields').findOne({ + filter: { + collectionName: 'mainCollection', + name: 'title', + }, + }); + + expect(mainCollectionTitleField2.get('unique')).toBe(true); + + expect(subAppMainCollection.getField('title')).toBeTruthy(); + + await mainApp.db.getRepository('fields').destroy({ + filter: { + collectionName: 'mainCollection', + name: 'title', + }, + }); + + expect( + await subApp1.db.getRepository('fields').findOne({ + filter: { + collectionName: 'mainCollection', + name: 'title', + }, + }), + ).toBeFalsy(); + + expect(subAppMainCollection.getField('title')).toBeFalsy(); + + await mainApp.db.getRepository('collections').destroy({ + filter: { + name: 'mainCollection', + }, + }); + + expect( + await subApp1.db.getRepository('collections').findOne({ + filter: { + name: 'mainCollection', + }, + }), + ).toBeFalsy(); + + expect(subApp1.db.getCollection('mainCollection')).toBeFalsy(); + }); + + it('should update syncToApp options', async () => { + await mainApp.db.getRepository('applications').create({ + values: { + name: 'sub1', + }, + }); + + const sub1 = await mainApp.appManager.getApplication('sub1'); + + await mainApp.db.getRepository('collections').create({ + values: { + name: 'testCollection', + fields: [ + { + type: 'string', + name: 'title', + }, + ], + }, + context: {}, + }); + + const sub1CollectionsRecord = await sub1.db.getRepository('collections').findOne({ + filter: { + name: 'testCollection', + }, + }); + + expect(sub1CollectionsRecord).toBeTruthy(); + + const mainTestCollection = await mainDb.getCollection('testCollection'); + const sub1TestCollection = await sub1.db.getCollection('testCollection'); + expect(sub1TestCollection.collectionSchema()).toEqual(mainTestCollection.collectionSchema()); + }); +}); diff --git a/packages/plugins/multi-app-share-collection/src/server/__tests__/index.ts b/packages/plugins/multi-app-share-collection/src/server/__tests__/index.ts new file mode 100644 index 000000000..11b31c280 --- /dev/null +++ b/packages/plugins/multi-app-share-collection/src/server/__tests__/index.ts @@ -0,0 +1,25 @@ +import { mockServer } from '@nocobase/test'; +import PluginCollectionManager from '@nocobase/plugin-collection-manager'; +import PluginMultiAppManager from '@nocobase/plugin-multi-app-manager'; +import PluginErrorHandler from '@nocobase/plugin-error-handler'; +import PluginUser from '@nocobase/plugin-users'; +import Plugin from '..'; + +export async function createApp(options = {}) { + const app = mockServer({ + acl: false, + ...options, + }); + + app.plugin(PluginUser, { name: 'users' }); + app.plugin(PluginErrorHandler, { name: 'error-handler' }); + app.plugin(PluginCollectionManager, { name: 'collection-manager' }); + await app.loadAndInstall({ clean: true }); + app.plugin(PluginMultiAppManager, { name: 'multi-app-manager' }); + app.plugin(Plugin, { name: 'multi-app-share-collection' }); + + await app.reload(); + await app.install(); + await app.start(); + return app; +} diff --git a/packages/plugins/multi-app-share-collection/src/server/collections/.gitkeep b/packages/plugins/multi-app-share-collection/src/server/collections/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugins/multi-app-share-collection/src/server/index.ts b/packages/plugins/multi-app-share-collection/src/server/index.ts new file mode 100644 index 000000000..b68aea57f --- /dev/null +++ b/packages/plugins/multi-app-share-collection/src/server/index.ts @@ -0,0 +1 @@ +export { default } from './plugin'; diff --git a/packages/plugins/multi-app-share-collection/src/server/plugin.ts b/packages/plugins/multi-app-share-collection/src/server/plugin.ts new file mode 100644 index 000000000..0dbcfc15f --- /dev/null +++ b/packages/plugins/multi-app-share-collection/src/server/plugin.ts @@ -0,0 +1,283 @@ +import PluginMultiAppManager from '@nocobase/plugin-multi-app-manager'; +import { Application, InstallOptions, Plugin } from '@nocobase/server'; + +const subAppFilteredPlugins = ['multi-app-share-collection', 'multi-app-manager']; + +class SubAppPlugin extends Plugin { + beforeLoad() { + const mainApp = this.options.mainApp; + const subApp = this.app; + + const sharedCollectionGroups = [ + 'audit-logs', + 'workflow', + 'charts', + 'collection-manager', + 'file-manager', + 'graph-collection-manager', + 'map', + 'sequence-field', + 'snapshot-field', + 'verification', + ]; + + const collectionGroups = mainApp.db.collectionGroupManager.getGroups(); + + const sharedCollectionGroupsCollections = []; + + for (const group of collectionGroups) { + if (sharedCollectionGroups.includes(group.namespace)) { + sharedCollectionGroupsCollections.push(...group.collections); + } + } + + const sharedCollections = [...sharedCollectionGroupsCollections.flat(), 'users', 'users_jobs']; + + subApp.db.on('beforeDefineCollection', (options) => { + const name = options.name; + + // 指向主应用的 系统schema + if (sharedCollections.includes(name)) { + options.schema = mainApp.db.options.schema || 'public'; + } + }); + + subApp.db.on('beforeUpdateCollection', (collection, newOptions) => { + if (collection.name === 'roles') { + newOptions.schema = subApp.db.options.schema; + } + }); + + this.app.resourcer.use(async (ctx, next) => { + const { actionName, resourceName } = ctx.action; + if (actionName === 'list' && resourceName === 'applicationPlugins') { + ctx.action.mergeParams({ + filter: { + 'name.$notIn': subAppFilteredPlugins, + }, + }); + } + + if (actionName === 'list' && resourceName === 'collections') { + const Collection = mainApp.db.getCollection('collections'); + const query = ` + select * from "${Collection.collectionSchema()}"."${Collection.model.tableName}" + where (options->'syncToApps')::jsonb ? '${subApp.name}' + `; + + const results = await mainApp.db.sequelize.query(query, { type: 'SELECT' }); + + ctx.action.mergeParams({ + filter: { + 'name.$in': [...results.map((item) => item['name']), 'users', 'roles'], + }, + }); + } + await next(); + }); + } +} + +export class MultiAppShareCollectionPlugin extends Plugin { + afterAdd() {} + + async beforeLoad() { + if (!this.db.inDialect('postgres')) { + throw new Error('multi-app-share-collection plugin only support postgres'); + } + + const traverseSubApps = async (callback: (subApp: Application) => void) => { + const subApps = [...this.app.appManager.applications.values()]; + + for (const subApp of subApps) { + await callback(subApp); + } + }; + + this.app.on('beforeSubAppLoad', async ({ subApp }: { subApp: Application }) => { + subApp.plugin(SubAppPlugin, { name: 'sub-app', mainApp: this.app }); + }); + + this.app.db.on('users.afterCreateWithAssociations', async (model, options) => { + await traverseSubApps(async (subApp) => { + const { transaction } = options; + const repository = subApp.db.getRepository('roles'); + const subAppModel = await subApp.db.getCollection('users').repository.findOne({ + filter: { + id: model.get('id'), + }, + transaction, + }); + const defaultRole = await repository.findOne({ + filter: { + default: true, + }, + transaction, + }); + + if (defaultRole && (await subAppModel.countRoles({ transaction })) == 0) { + await subAppModel.addRoles(defaultRole, { transaction }); + } + }); + }); + + this.app.db.on('collection:loaded', async ({ transaction, collection }) => { + await traverseSubApps(async (subApp) => { + const name = collection.name; + + const collectionRecord = await subApp.db.getRepository('collections').findOne({ + filter: { + name, + }, + transaction, + }); + + await collectionRecord.load({ transaction }); + }); + }); + + this.app.db.on('field:loaded', async ({ transaction, fieldKey }) => { + await traverseSubApps(async (subApp) => { + const fieldRecord = await subApp.db.getRepository('fields').findOne({ + filterByTk: fieldKey, + transaction, + }); + + if (fieldRecord) { + await fieldRecord.load({ transaction }); + } + }); + }); + + this.app.on('afterEnablePlugin', async (pluginName) => { + await traverseSubApps(async (subApp) => { + if (subAppFilteredPlugins.includes(pluginName)) return; + await subApp.pm.enable(pluginName); + }); + }); + + this.app.on('afterDisablePlugin', async (pluginName) => { + await traverseSubApps(async (subApp) => { + if (subAppFilteredPlugins.includes(pluginName)) return; + await subApp.pm.disable(pluginName); + }); + }); + + this.app.db.on('field.afterRemove', (removedField) => { + const subApps = [...this.app.appManager.applications.values()]; + for (const subApp of subApps) { + const collectionName = removedField.collection.name; + const collection = subApp.db.getCollection(collectionName); + if (!collection) { + subApp.log.warn(`collection ${collectionName} not found in ${subApp.name}`); + continue; + } + + collection.removeField(removedField.name); + } + }); + + this.app.db.on(`afterRemoveCollection`, (collection) => { + const subApps = [...this.app.appManager.applications.values()]; + for (const subApp of subApps) { + subApp.db.removeCollection(collection.name); + } + }); + } + + async load() { + const multiAppManager = this.app.getPlugin<any>('multi-app-manager'); + + if (!multiAppManager) { + this.app.log.warn('multi-app-share-collection plugin need multi-app-manager plugin enabled'); + return; + } + + // 应用加载完之后,需要将子应用全部载入到内存中 + this.app.on('afterLoad', async () => { + const subApplications = await this.db.getRepository('applications').find(); + for (const subApplication of subApplications) { + await this.app.appManager.getApplication(subApplication.name); + } + }); + + this.app.on('beforeSubAppInstall', async ({ subApp }) => { + const subAppPluginsCollection = subApp.db.getCollection('applicationPlugins'); + const mainAppPluginsCollection = this.app.db.getCollection('applicationPlugins'); + + // delete old collection + await subApp.db.sequelize.query(`TRUNCATE ${subAppPluginsCollection.quotedTableName()}`); + + await subApp.db.sequelize.query(` + INSERT INTO ${subAppPluginsCollection.quotedTableName()} + SELECT * + FROM ${mainAppPluginsCollection.quotedTableName()} + WHERE "name" not in ('multi-app-manager', 'multi-app-share-collection'); + `); + + const sequenceNameSql = `SELECT pg_get_serial_sequence('"${subAppPluginsCollection.collectionSchema()}"."${ + subAppPluginsCollection.model.tableName + }"', 'id')`; + + const sequenceName = await subApp.db.sequelize.query(sequenceNameSql, { type: 'SELECT' }); + await subApp.db.sequelize.query(` + SELECT setval('${ + sequenceName[0].pg_get_serial_sequence + }', (SELECT max("id") FROM ${subAppPluginsCollection.quotedTableName()})); + `); + }); + + // 子应用启动参数 + multiAppManager.setAppOptionsFactory((appName, mainApp) => { + const mainAppDbConfig = PluginMultiAppManager.getDatabaseConfig(mainApp); + + const databaseOptions = { + ...mainAppDbConfig, + schema: appName, + }; + + const plugins = [...mainApp.pm.getPlugins().keys()].filter( + (name) => name !== 'multi-app-manager' && name !== 'multi-app-share-collection', + ); + + return { + database: databaseOptions, + plugins: plugins.includes('nocobase') ? ['nocobase'] : plugins, + resourcer: { + prefix: '/api', + }, + logger: { + ...mainApp.options.logger, + requestWhitelist: [ + 'action', + 'header.x-role', + 'header.x-hostname', + 'header.x-timezone', + 'header.x-locale', + 'referer', + 'header.x-app', + ], + }, + // pmSock: resolve(process.cwd(), 'storage', `${appName}.sock`), + }; + }); + + // 子应用数据库创建 + multiAppManager.setAppDbCreator(async (app) => { + const schema = app.options.database.schema; + await this.app.db.sequelize.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`); + }); + } + + async install(options?: InstallOptions) {} + + async afterEnable() {} + + async afterDisable() { + // test + } + + async remove() {} +} + +export default MultiAppShareCollectionPlugin; diff --git a/packages/presets/nocobase/package.json b/packages/presets/nocobase/package.json index 7f475fdc5..c91dd3a84 100644 --- a/packages/presets/nocobase/package.json +++ b/packages/presets/nocobase/package.json @@ -23,6 +23,7 @@ "@nocobase/plugin-map": "0.9.1-alpha.2", "@nocobase/plugin-math-formula-field": "0.9.1-alpha.2", "@nocobase/plugin-multi-app-manager": "0.9.1-alpha.2", + "@nocobase/plugin-multi-app-share-collection": "0.9.1-alpha.2", "@nocobase/plugin-oidc": "0.9.1-alpha.2", "@nocobase/plugin-saml": "0.9.1-alpha.2", "@nocobase/plugin-sample-hello": "0.9.1-alpha.2", diff --git a/packages/presets/nocobase/src/index.ts b/packages/presets/nocobase/src/index.ts index f74d8fc83..d2400e45a 100644 --- a/packages/presets/nocobase/src/index.ts +++ b/packages/presets/nocobase/src/index.ts @@ -29,6 +29,7 @@ export class PresetNocoBase extends Plugin { localPlugins = [ 'sample-hello', 'multi-app-manager', + 'multi-app-share-collection', 'oidc', 'saml', 'map',