mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-09 15:39:24 +08:00
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:
parent
aa41ae4cc9
commit
28cda22a79
@ -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;
|
||||||
|
@ -108,7 +108,7 @@ export function withInitializer<T>(C: ComponentType<T>) {
|
|||||||
{...popoverProps}
|
{...popoverProps}
|
||||||
arrow={false}
|
arrow={false}
|
||||||
overlayClassName={overlayClassName}
|
overlayClassName={overlayClassName}
|
||||||
open={visible}
|
open={visible}
|
||||||
onOpenChange={setVisible}
|
onOpenChange={setVisible}
|
||||||
content={wrapSSR(
|
content={wrapSSR(
|
||||||
<div
|
<div
|
||||||
|
@ -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]);
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
25
packages/core/test/src/server/mock-data-source.ts
Normal file
25
packages/core/test/src/server/mock-data-source.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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',
|
{
|
||||||
collectionName: model.get('name'),
|
type: 'syncCollection',
|
||||||
});
|
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,
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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 }) => {
|
||||||
|
@ -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: [{}],
|
||||||
{}
|
},
|
||||||
]
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -15,8 +15,8 @@ const Demo = () => {
|
|||||||
title: 'Link',
|
title: 'Link',
|
||||||
icon: 'AppstoreOutlined',
|
icon: 'AppstoreOutlined',
|
||||||
options: {
|
options: {
|
||||||
url: 'https://github.com'
|
url: 'https://github.com',
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -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 = () => {
|
||||||
|
@ -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 {
|
||||||
|
@ -18,5 +18,5 @@ export function getMobileTabBarItemSchema(routeItem: MobileRouteItem) {
|
|||||||
schemaUid: routeItem.schemaUid,
|
schemaUid: routeItem.schemaUid,
|
||||||
...(routeItem.options || {}),
|
...(routeItem.options || {}),
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user