feat: sync collection using sync manager (#4920)

* chore: sync collection message

* chore: sync acl

* fix: typo

* chore: sync data source

* chore: remove collection

* fix: typo

* fix: test

* chore: sync sub app event

* chore: sync collection test

* chore: collection test

* chore: test

* chore: data source sync message

* chore: sync multi app

* chore: test

* chore: test

* chore: test

* chore: test

* chore: test
This commit is contained in:
ChengLei Shao 2024-08-02 22:51:57 +08:00 committed by mytharcher
parent aa41ae4cc9
commit 28cda22a79
18 changed files with 693 additions and 68 deletions

View File

@ -110,7 +110,7 @@ export interface SchemaInitializerOptions<P1 = ButtonProps, P2 = {}> {
insertPosition?: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd'; insertPosition?: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd';
designable?: boolean; designable?: boolean;
wrap?: (s: ISchema, options?: any) => ISchema; wrap?: (s: ISchema, options?: any) => ISchema;
useWrap?: () => ((s: ISchema, options?: any) => ISchema); useWrap?: () => (s: ISchema, options?: any) => ISchema;
onSuccess?: (data: any) => void; onSuccess?: (data: any) => void;
insert?: InsertType; insert?: InsertType;
useInsert?: () => InsertType; useInsert?: () => InsertType;

View File

@ -47,8 +47,11 @@ export const SettingsCenterDropdown = () => {
return { return {
key: setting.name, key: setting.name,
icon: setting.icon, icon: setting.icon,
label: setting.link ? <div onClick={() => window.open(setting.link)}>{compile(setting.title)}</div> : label: setting.link ? (
<div onClick={() => window.open(setting.link)}>{compile(setting.title)}</div>
) : (
<Link to={setting.path}>{compile(setting.title)}</Link> <Link to={setting.path}>{compile(setting.title)}</Link>
),
}; };
}); });
}, [app, t]); }, [app, t]);

View File

@ -16,7 +16,7 @@ import fs from 'fs';
import type { TFuncKey, TOptions } from 'i18next'; import type { TFuncKey, TOptions } from 'i18next';
import { resolve } from 'path'; import { resolve } from 'path';
import { Application } from './application'; import { Application } from './application';
import { InstallOptions, getExposeChangelogUrl, getExposeReadmeUrl } from './plugin-manager'; import { getExposeChangelogUrl, getExposeReadmeUrl, InstallOptions } from './plugin-manager';
import { checkAndGetCompatible, getPluginBasePath } from './plugin-manager/utils'; import { checkAndGetCompatible, getPluginBasePath } from './plugin-manager/utils';
export interface PluginInterface { export interface PluginInterface {
@ -138,6 +138,7 @@ export abstract class Plugin<O = any> implements PluginInterface {
if (!this.name) { if (!this.name) {
throw new Error(`plugin name invalid`); throw new Error(`plugin name invalid`);
} }
await this.app.syncMessageManager.publish(this.name, message, options); await this.app.syncMessageManager.publish(this.name, message, options);
} }
@ -172,13 +173,6 @@ export abstract class Plugin<O = any> implements PluginInterface {
}); });
} }
private async getPluginBasePath() {
if (!this.options.packageName) {
return;
}
return getPluginBasePath(this.options.packageName);
}
/** /**
* @internal * @internal
*/ */
@ -245,6 +239,13 @@ export abstract class Plugin<O = any> implements PluginInterface {
return results; return results;
} }
private async getPluginBasePath() {
if (!this.options.packageName) {
return;
}
return getPluginBasePath(this.options.packageName);
}
} }
export default Plugin; export default Plugin;

View File

@ -39,17 +39,18 @@ export class SyncMessageManager {
const timer = setTimeout(() => { const timer = setTimeout(() => {
reject(new Error('publish timeout')); reject(new Error('publish timeout'));
}, 5000); }, 5000);
transaction.afterCommit(async () => { transaction.afterCommit(async () => {
try { try {
const r = await this.app.pubSubManager.publish(`${this.app.name}.sync.${channel}`, message, { const r = await this.app.pubSubManager.publish(`${this.app.name}.sync.${channel}`, message, {
skipSelf: true, skipSelf: true,
...others, ...others,
}); });
clearTimeout(timer);
resolve(r); resolve(r);
} catch (error) { } catch (error) {
clearTimeout(timer);
reject(error); reject(error);
} finally {
clearTimeout(timer);
} }
}); });
}); });

View File

@ -0,0 +1,25 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { CollectionManager, DataSource } from '@nocobase/data-source-manager';
import { waitSecond } from '@nocobase/test';
export class MockDataSource extends DataSource {
static testConnection(options?: any): Promise<boolean> {
return Promise.resolve(true);
}
async load(): Promise<void> {
await waitSecond(1000);
}
createCollectionManager(options?: any): any {
return new CollectionManager(options);
}
}

View File

@ -11,10 +11,10 @@ import { mockDatabase } from '@nocobase/database';
import { Application, ApplicationOptions, AppSupervisor, Gateway, PluginManager } from '@nocobase/server'; import { Application, ApplicationOptions, AppSupervisor, Gateway, PluginManager } from '@nocobase/server';
import { uid } from '@nocobase/utils'; import { uid } from '@nocobase/utils';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import _ from 'lodash';
import qs from 'qs'; import qs from 'qs';
import supertest, { SuperAgentTest } from 'supertest'; import supertest, { SuperAgentTest } from 'supertest';
import { MemoryPubSubAdapter } from './memory-pub-sub-adapter'; import { MemoryPubSubAdapter } from './memory-pub-sub-adapter';
import { MockDataSource } from './mock-data-source';
interface ActionParams { interface ActionParams {
filterByTk?: any; filterByTk?: any;
@ -77,6 +77,10 @@ interface ExtendedAgent extends SuperAgentTest {
} }
export class MockServer extends Application { export class MockServer extends Application {
registerMockDataSource() {
this.dataSourceManager.factory.register('mock', MockDataSource);
}
async loadAndInstall(options: any = {}) { async loadAndInstall(options: any = {}) {
await this.load({ method: 'install' }); await this.load({ method: 'install' });
@ -231,13 +235,15 @@ export function mockServer(options: ApplicationOptions = {}) {
PluginManager.findPackagePatched = true; PluginManager.findPackagePatched = true;
} }
const app = new MockServer({ const mockServerOptions = {
acl: false, acl: false,
syncMessageManager: { syncMessageManager: {
debounce: 500, debounce: 500,
}, },
...options, ...options,
}); };
const app = new MockServer(mockServerOptions);
const basename = app.options.pubSubManager?.channelPrefix; const basename = app.options.pubSubManager?.channelPrefix;
@ -280,16 +286,27 @@ export async function createMockCluster({
...options ...options
}: MockClusterOptions = {}) { }: MockClusterOptions = {}) {
const nodes: MockServer[] = []; const nodes: MockServer[] = [];
let dbOptions;
for (let i = 0; i < number; i++) { for (let i = 0; i < number; i++) {
if (dbOptions) {
options['database'] = {
...dbOptions,
};
}
const app: MockServer = await createMockServer({ const app: MockServer = await createMockServer({
...options, ...options,
skipSupervisor: true, skipSupervisor: true,
name: clusterName + '_' + appName, name: clusterName + '_' + appName,
skipInstall: Boolean(i),
pubSubManager: { pubSubManager: {
channelPrefix: clusterName, channelPrefix: clusterName,
}, },
}); });
if (!dbOptions) {
dbOptions = app.db.options;
}
console.log('-------------', await app.isInstalled()); console.log('-------------', await app.isInstalled());
nodes.push(app); nodes.push(app);
} }

View File

@ -17,7 +17,7 @@ export class RoleResourceModel extends Model {
role.revokeResource(resourceName); role.revokeResource(resourceName);
} }
async writeToACL(options: { acl: ACL; transaction: any }) { async writeToACL(options: { acl: ACL; transaction?: any }) {
const { acl } = options; const { acl } = options;
const resourceName = this.get('name') as string; const resourceName = this.get('name') as string;
const roleName = this.get('roleName') as string; const roleName = this.get('roleName') as string;

View File

@ -0,0 +1,235 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { createMockCluster, sleep } from '@nocobase/test';
describe('cluster', () => {
let cluster;
beforeEach(async () => {
cluster = await createMockCluster({
plugins: ['error-handler', 'data-source-main', 'ui-schema-storage'],
acl: false,
});
});
afterEach(async () => {
await cluster.destroy();
});
describe('sync collection', () => {
test('create collection', async () => {
const [app1, app2] = cluster.nodes;
await app1.db.getRepository('collections').create({
values: {
name: 'tests',
fields: [
{
name: 'name',
type: 'string',
},
],
},
context: {},
});
await sleep(2000);
const testsCollection = app2.db.getCollection('tests');
expect(testsCollection).toBeTruthy();
});
test('update collection', async () => {
const [app1, app2] = cluster.nodes;
await app1.db.getRepository('collections').create({
values: {
name: 'tests',
fields: [
{
name: 'name',
type: 'string',
},
],
description: 'test collection',
},
context: {},
});
await sleep(2000);
const testsCollection = app2.db.getCollection('tests');
expect(testsCollection).toBeTruthy();
await app1.db.getRepository('collections').update({
filterByTk: 'tests',
values: {
description: 'new test collection',
},
context: {},
});
await sleep(2000);
expect(testsCollection.options.description).toBe('new test collection');
});
test('destroy collection', async () => {
const [app1, app2] = cluster.nodes;
await app1.db.getRepository('collections').create({
values: {
name: 'tests',
fields: [
{
name: 'name',
type: 'string',
},
],
},
context: {},
});
await sleep(2000);
const testsCollection = app2.db.getCollection('tests');
expect(testsCollection).toBeTruthy();
await app1.db.getRepository('collections').destroy({
filterByTk: 'tests',
context: {},
});
await sleep(2000);
expect(app2.db.getCollection('tests')).toBeFalsy();
});
});
describe('sync fields', () => {
test('create field', async () => {
const [app1, app2] = cluster.nodes;
await app1.db.getRepository('collections').create({
values: {
name: 'tests',
fields: [
{
name: 'name',
type: 'string',
},
],
},
context: {},
});
await sleep(2000);
const testsCollection = app2.db.getCollection('tests');
expect(testsCollection).toBeTruthy();
await app1.db.getRepository('fields').create({
values: {
name: 'age',
type: 'integer',
collectionName: 'tests',
},
context: {},
});
await sleep(2000);
const ageField = testsCollection.getField('age');
expect(ageField).toBeTruthy();
});
test('update field', async () => {
const [app1, app2] = cluster.nodes;
await app1.db.getRepository('collections').create({
values: {
name: 'tests',
fields: [
{
name: 'name',
type: 'string',
},
],
},
context: {},
});
await sleep(2000);
const testsCollection = app2.db.getCollection('tests');
expect(testsCollection).toBeTruthy();
await app1.db.getRepository('fields').create({
values: {
name: 'age',
type: 'integer',
collectionName: 'tests',
},
context: {},
});
await sleep(2000);
await app1.db.getRepository('collections.fields', 'tests').update({
filterByTk: 'age',
values: {
description: 'age field',
},
context: {},
});
await sleep(2000);
const ageField = testsCollection.getField('age');
expect(ageField).toBeTruthy();
expect(ageField.options.description).toBe('age field');
});
test('destroy field', async () => {
const [app1, app2] = cluster.nodes;
await app1.db.getRepository('collections').create({
values: {
name: 'tests',
fields: [
{
name: 'name',
type: 'string',
},
{
name: 'age',
type: 'integer',
},
],
},
context: {},
});
await sleep(2000);
const testsCollection = app2.db.getCollection('tests');
expect(testsCollection).toBeTruthy();
await app1.db.getRepository('collections.fields', 'tests').destroy({
filterByTk: 'age',
context: {},
});
await sleep(2000);
expect(testsCollection.getField('age')).toBeFalsy();
});
});
});

View File

@ -43,7 +43,8 @@ export class PluginDataSourceMainServer extends Plugin {
async handleSyncMessage(message) { async handleSyncMessage(message) {
const { type, collectionName } = message; const { type, collectionName } = message;
if (type === 'newCollection') {
if (type === 'syncCollection') {
const collectionModel: CollectionModel = await this.app.db.getCollection('collections').repository.findOne({ const collectionModel: CollectionModel = await this.app.db.getCollection('collections').repository.findOne({
filter: { filter: {
name: collectionName, name: collectionName,
@ -52,6 +53,26 @@ export class PluginDataSourceMainServer extends Plugin {
await collectionModel.load(); await collectionModel.load();
} }
if (type === 'removeField') {
const { collectionName, fieldName } = message;
const collection = this.app.db.getCollection(collectionName);
if (!collection) {
return;
}
return collection.removeFieldFromDb(fieldName);
}
if (type === 'removeCollection') {
const { collectionName } = message;
const collection = this.app.db.getCollection(collectionName);
if (!collection) {
return;
}
collection.remove();
}
} }
async beforeLoad() { async beforeLoad() {
@ -92,10 +113,15 @@ export class PluginDataSourceMainServer extends Plugin {
transaction, transaction,
}); });
this.sendSyncMessage({ this.sendSyncMessage(
type: 'newCollection', {
type: 'syncCollection',
collectionName: model.get('name'), collectionName: model.get('name'),
}); },
{
transaction,
},
);
} }
}, },
); );
@ -113,6 +139,16 @@ export class PluginDataSourceMainServer extends Plugin {
} }
await model.remove(removeOptions); await model.remove(removeOptions);
this.sendSyncMessage(
{
type: 'removeCollection',
collectionName: model.get('name'),
},
{
transaction: options.transaction,
},
);
}); });
// 要在 beforeInitOptions 之前处理 // 要在 beforeInitOptions 之前处理
@ -262,6 +298,16 @@ export class PluginDataSourceMainServer extends Plugin {
}; };
await collection.sync(syncOptions); await collection.sync(syncOptions);
this.sendSyncMessage(
{
type: 'syncCollection',
collectionName: model.get('collectionName'),
},
{
transaction,
},
);
} }
}); });
@ -273,6 +319,17 @@ export class PluginDataSourceMainServer extends Plugin {
this.app.db.on('fields.beforeDestroy', async (model: FieldModel, options) => { this.app.db.on('fields.beforeDestroy', async (model: FieldModel, options) => {
await mutex.runExclusive(async () => { await mutex.runExclusive(async () => {
await model.remove(options); await model.remove(options);
this.sendSyncMessage(
{
type: 'removeField',
collectionName: model.get('collectionName'),
fieldName: model.get('name'),
},
{
transaction: options.transaction,
},
);
}); });
}); });

View File

@ -0,0 +1,49 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { createMockCluster, waitSecond } from '@nocobase/test';
describe('cluster', () => {
let cluster;
beforeEach(async () => {
cluster = await createMockCluster({
plugins: ['nocobase'],
acl: false,
});
for (const node of cluster.nodes) {
node.registerMockDataSource();
}
});
afterEach(async () => {
await cluster.destroy();
});
test('create data source', async () => {
const app1 = cluster.nodes[0];
await app1.db.getRepository('dataSources').create({
values: {
key: 'mockInstance1',
type: 'mock',
displayName: 'Mock',
options: {},
},
});
await waitSecond(2000);
const dataSource = app1.dataSourceManager.get('mockInstance1');
expect(dataSource).toBeDefined();
const dataSourceInApp2 = cluster.nodes[1].dataSourceManager.get('mockInstance1');
expect(dataSourceInApp2).toBeDefined();
});
});

View File

@ -36,6 +36,96 @@ export class PluginDataSourceManagerServer extends Plugin {
[dataSourceKey: string]: DataSourceState; [dataSourceKey: string]: DataSourceState;
} = {}; } = {};
async handleSyncMessage(message) {
const { type } = message;
if (type === 'syncRole') {
const { roleName, dataSourceKey } = message;
const dataSource = this.app.dataSourceManager.dataSources.get(dataSourceKey);
const dataSourceRole: DataSourcesRolesModel = await this.app.db.getRepository('dataSourcesRoles').findOne({
filter: {
dataSourceKey,
roleName,
},
});
await dataSourceRole.writeToAcl({
acl: dataSource.acl,
});
}
if (type === 'syncRoleResource') {
const { roleName, dataSourceKey, resourceName } = message;
const dataSource = this.app.dataSourceManager.dataSources.get(dataSourceKey);
const dataSourceRoleResource: DataSourcesRolesResourcesModel = await this.app.db
.getRepository('dataSourcesRolesResources')
.findOne({
filter: {
dataSourceKey,
roleName,
name: resourceName,
},
});
await dataSourceRoleResource.writeToACL({
acl: dataSource.acl,
});
}
if (type === 'loadDataSource') {
const { dataSourceKey } = message;
const dataSourceModel = await this.app.db.getRepository('dataSources').findOne({
filter: {
key: dataSourceKey,
},
});
if (!dataSourceModel) {
return;
}
await dataSourceModel.loadIntoApplication({
app: this.app,
});
}
if (type === 'loadDataSourceField') {
const { key } = message;
const fieldModel = await this.app.db.getRepository('dataSourcesFields').findOne({
filter: {
key,
},
});
fieldModel.load({
app: this.app,
});
}
if (type === 'removeDataSourceCollection') {
const { dataSourceKey, collectionName } = message;
const dataSource = this.app.dataSourceManager.dataSources.get(dataSourceKey);
dataSource.collectionManager.removeCollection(collectionName);
}
if (type === 'removeDataSourceField') {
const { key } = message;
const fieldModel = await this.app.db.getRepository('dataSourcesFields').findOne({
filter: {
key,
},
});
fieldModel.unload({
app: this.app,
});
}
if (type === 'removeDataSource') {
const { dataSourceKey } = message;
this.app.dataSourceManager.dataSources.delete(dataSourceKey);
}
}
async beforeLoad() { async beforeLoad() {
this.app.db.registerModels({ this.app.db.registerModels({
DataSourcesCollectionModel, DataSourcesCollectionModel,
@ -100,6 +190,16 @@ export class PluginDataSourceManagerServer extends Plugin {
model.loadIntoApplication({ model.loadIntoApplication({
app: this.app, app: this.app,
}); });
this.sendSyncMessage(
{
type: 'loadDataSource',
dataSourceKey: model.get('key'),
},
{
transaction: options.transaction,
},
);
} }
}); });
@ -264,6 +364,7 @@ export class PluginDataSourceManagerServer extends Plugin {
} }
}); });
const self = this;
this.app.actions({ this.app.actions({
async ['dataSources:listEnabled'](ctx, next) { async ['dataSources:listEnabled'](ctx, next) {
const dataSources = await ctx.db.getRepository('dataSources').find({ const dataSources = await ctx.db.getRepository('dataSources').find({
@ -302,6 +403,7 @@ export class PluginDataSourceManagerServer extends Plugin {
async ['dataSources:refresh'](ctx, next) { async ['dataSources:refresh'](ctx, next) {
const { filterByTk, clientStatus } = ctx.action.params; const { filterByTk, clientStatus } = ctx.action.params;
const dataSourceModel: DataSourceModel = await ctx.db.getRepository('dataSources').findOne({ const dataSourceModel: DataSourceModel = await ctx.db.getRepository('dataSources').findOne({
filter: { filter: {
key: filterByTk, key: filterByTk,
@ -317,6 +419,11 @@ export class PluginDataSourceManagerServer extends Plugin {
dataSourceModel.loadIntoApplication({ dataSourceModel.loadIntoApplication({
app: ctx.app, app: ctx.app,
}); });
ctx.app.syncMessageManager.publish(self.name, {
type: 'loadDataSource',
dataSourceKey: dataSourceModel.get('key'),
});
} }
ctx.body = { ctx.body = {
@ -352,21 +459,52 @@ export class PluginDataSourceManagerServer extends Plugin {
} }
}); });
this.app.db.on('dataSourcesCollections.afterDestroy', async (model: DataSourcesCollectionModel) => { this.app.db.on('dataSourcesCollections.afterDestroy', async (model: DataSourcesCollectionModel, options) => {
const dataSource = this.app.dataSourceManager.dataSources.get(model.get('dataSourceKey')); const dataSource = this.app.dataSourceManager.dataSources.get(model.get('dataSourceKey'));
dataSource.collectionManager.removeCollection(model.get('name')); dataSource.collectionManager.removeCollection(model.get('name'));
this.sendSyncMessage(
{
type: 'removeDataSourceCollection',
dataSourceKey: model.get('dataSourceKey'),
collectionName: model.get('name'),
},
{
transaction: options.transaction,
},
);
}); });
this.app.db.on('dataSourcesFields.afterSaveWithAssociations', async (model: DataSourcesFieldModel) => { this.app.db.on('dataSourcesFields.afterSaveWithAssociations', async (model: DataSourcesFieldModel, options) => {
model.load({ model.load({
app: this.app, app: this.app,
}); });
this.sendSyncMessage(
{
type: 'loadDataSourceField',
key: model.get('key'),
},
{
transaction: options.transaction,
},
);
}); });
this.app.db.on('dataSourcesFields.afterDestroy', async (model: DataSourcesFieldModel) => { this.app.db.on('dataSourcesFields.afterDestroy', async (model: DataSourcesFieldModel, options) => {
model.unload({ model.unload({
app: this.app, app: this.app,
}); });
this.sendSyncMessage(
{
type: 'removeDataSourceField',
key: model.get('key'),
},
{
transaction: options.transaction,
},
);
}); });
this.app.db.on( this.app.db.on(
@ -379,8 +517,18 @@ export class PluginDataSourceManagerServer extends Plugin {
}, },
); );
this.app.db.on('dataSources.afterDestroy', async (model: DataSourceModel) => { this.app.db.on('dataSources.afterDestroy', async (model: DataSourceModel, options) => {
this.app.dataSourceManager.dataSources.delete(model.get('key')); this.app.dataSourceManager.dataSources.delete(model.get('key'));
this.sendSyncMessage(
{
type: 'removeDataSource',
dataSourceKey: model.get('key'),
},
{
transaction: options.transaction,
},
);
}); });
this.app.on('afterStart', async (app: Application) => { this.app.on('afterStart', async (app: Application) => {
@ -412,6 +560,19 @@ export class PluginDataSourceManagerServer extends Plugin {
acl: dataSource.acl, acl: dataSource.acl,
transaction: transaction, transaction: transaction,
}); });
// sync roles resources between nodes
this.sendSyncMessage(
{
type: 'syncRoleResource',
roleName: model.get('roleName'),
dataSourceKey: model.get('dataSourceKey'),
resourceName: model.get('name'),
},
{
transaction,
},
);
}, },
); );
@ -429,6 +590,18 @@ export class PluginDataSourceManagerServer extends Plugin {
acl: dataSource.acl, acl: dataSource.acl,
transaction: transaction, transaction: transaction,
}); });
this.sendSyncMessage(
{
type: 'syncRoleResource',
roleName: resource.get('roleName'),
dataSourceKey: resource.get('dataSourceKey'),
resourceName: resource.get('name'),
},
{
transaction,
},
);
}, },
); );
@ -440,6 +613,18 @@ export class PluginDataSourceManagerServer extends Plugin {
if (role) { if (role) {
role.revokeResource(model.get('name')); role.revokeResource(model.get('name'));
} }
this.sendSyncMessage(
{
type: 'syncRoleResource',
roleName,
dataSourceKey: model.get('dataSourceKey'),
resourceName: model.get('name'),
},
{
transaction: options.transaction,
},
);
}); });
this.app.db.on('dataSourcesRoles.afterSave', async (model: DataSourcesRolesModel, options) => { this.app.db.on('dataSourcesRoles.afterSave', async (model: DataSourcesRolesModel, options) => {
@ -462,6 +647,18 @@ export class PluginDataSourceManagerServer extends Plugin {
hooks: false, hooks: false,
transaction, transaction,
}); });
// sync role between nodes
this.sendSyncMessage(
{
type: 'syncRole',
roleName: model.get('roleName'),
dataSourceKey: model.get('dataSourceKey'),
},
{
transaction,
},
);
}); });
this.app.on('acl:writeResources', async ({ roleName, transaction }) => { this.app.on('acl:writeResources', async ({ roleName, transaction }) => {

View File

@ -46,36 +46,34 @@ const app = mockApp({
'mobileRoutes:list': { 'mobileRoutes:list': {
data: [ data: [
{ {
"id": 10, id: 10,
"createdAt": "2024-07-08T13:22:33.763Z", createdAt: '2024-07-08T13:22:33.763Z',
"updatedAt": "2024-07-08T13:22:33.763Z", updatedAt: '2024-07-08T13:22:33.763Z',
"parentId": null, parentId: null,
"title": "Test1", title: 'Test1',
"icon": "AppstoreOutlined", icon: 'AppstoreOutlined',
"schemaUid": "test", schemaUid: 'test',
"type": "page", type: 'page',
"options": null, options: null,
"sort": 1, sort: 1,
"createdById": 1, createdById: 1,
"updatedById": 1, updatedById: 1,
}, },
{ {
"id": 13, id: 13,
"createdAt": "2024-07-08T13:23:01.929Z", createdAt: '2024-07-08T13:23:01.929Z',
"updatedAt": "2024-07-08T13:23:12.433Z", updatedAt: '2024-07-08T13:23:12.433Z',
"parentId": null, parentId: null,
"title": "Test2", title: 'Test2',
"icon": "aliwangwangoutlined", icon: 'aliwangwangoutlined',
"schemaUid": null, schemaUid: null,
"type": "link", type: 'link',
"options": { options: {
"schemaUid": null, schemaUid: null,
"url": "https://github.com", url: 'https://github.com',
"params": [ params: [{}],
{} },
] },
}
}
], ],
}, },
}, },

View File

@ -15,8 +15,8 @@ const Demo = () => {
title: 'Link', title: 'Link',
icon: 'AppstoreOutlined', icon: 'AppstoreOutlined',
options: { options: {
url: 'https://github.com' url: 'https://github.com',
} },
}), }),
)} )}
/> />

View File

@ -16,8 +16,8 @@ const schema = getMobileTabBarItemSchema({
title: 'Link', title: 'Link',
icon: 'AppstoreOutlined', icon: 'AppstoreOutlined',
options: { options: {
url: 'https://github.com' url: 'https://github.com',
} },
}); });
const Demo = () => { const Demo = () => {

View File

@ -14,13 +14,7 @@ const schema = getMobileTabBarItemSchema({
}); });
const Demo = () => { const Demo = () => {
return ( return <SchemaComponent schema={schemaViewer(schema)} />;
<SchemaComponent
schema={schemaViewer(
schema,
)}
/>
);
}; };
class MyPlugin extends Plugin { class MyPlugin extends Plugin {

View File

@ -18,5 +18,5 @@ export function getMobileTabBarItemSchema(routeItem: MobileRouteItem) {
schemaUid: routeItem.schemaUid, schemaUid: routeItem.schemaUid,
...(routeItem.options || {}), ...(routeItem.options || {}),
}, },
} };
} }

View File

@ -143,6 +143,34 @@ export class PluginMultiAppManagerServer extends Plugin {
return lodash.cloneDeep(lodash.omit(oldConfig, ['migrator'])); return lodash.cloneDeep(lodash.omit(oldConfig, ['migrator']));
} }
async handleSyncMessage(message) {
const { type } = message;
if (type === 'startApp') {
const { appName } = message;
const model = await this.app.db.getRepository('applications').findOne({
filter: {
name: appName,
},
});
if (!model) {
return;
}
const subApp = model.registerToSupervisor(this.app, {
appOptionsFactory: this.appOptionsFactory,
});
subApp.runCommand('start', '--quickstart');
}
if (type === 'removeApp') {
const { appName } = message;
await AppSupervisor.getInstance().removeApp(appName);
}
}
setSubAppUpgradeHandler(handler: SubAppUpgradeHandler) { setSubAppUpgradeHandler(handler: SubAppUpgradeHandler) {
this.subAppUpgradeHandler = handler; this.subAppUpgradeHandler = handler;
} }
@ -186,6 +214,16 @@ export class PluginMultiAppManagerServer extends Plugin {
context: options.context, context: options.context,
}); });
this.sendSyncMessage(
{
type: 'startApp',
appName: name,
},
{
transaction,
},
);
const startPromise = subApp.runCommand('start', '--quickstart'); const startPromise = subApp.runCommand('start', '--quickstart');
if (options?.context?.waitSubAppInstall) { if (options?.context?.waitSubAppInstall) {
@ -194,8 +232,18 @@ export class PluginMultiAppManagerServer extends Plugin {
}, },
); );
this.db.on('applications.afterDestroy', async (model: ApplicationModel) => { this.db.on('applications.afterDestroy', async (model: ApplicationModel, options) => {
await AppSupervisor.getInstance().removeApp(model.get('name') as string); await AppSupervisor.getInstance().removeApp(model.get('name') as string);
this.sendSyncMessage(
{
type: 'removeApp',
appName: model.get('name'),
},
{
transaction: options.transaction,
},
);
}); });
const self = this; const self = this;