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';
|
||||
designable?: boolean;
|
||||
wrap?: (s: ISchema, options?: any) => ISchema;
|
||||
useWrap?: () => ((s: ISchema, options?: any) => ISchema);
|
||||
useWrap?: () => (s: ISchema, options?: any) => ISchema;
|
||||
onSuccess?: (data: any) => void;
|
||||
insert?: InsertType;
|
||||
useInsert?: () => InsertType;
|
||||
|
@ -47,8 +47,11 @@ export const SettingsCenterDropdown = () => {
|
||||
return {
|
||||
key: setting.name,
|
||||
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>
|
||||
),
|
||||
};
|
||||
});
|
||||
}, [app, t]);
|
||||
|
@ -16,7 +16,7 @@ import fs from 'fs';
|
||||
import type { TFuncKey, TOptions } from 'i18next';
|
||||
import { resolve } from 'path';
|
||||
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';
|
||||
|
||||
export interface PluginInterface {
|
||||
@ -138,6 +138,7 @@ export abstract class Plugin<O = any> implements PluginInterface {
|
||||
if (!this.name) {
|
||||
throw new Error(`plugin name invalid`);
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
@ -245,6 +239,13 @@ export abstract class Plugin<O = any> implements PluginInterface {
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private async getPluginBasePath() {
|
||||
if (!this.options.packageName) {
|
||||
return;
|
||||
}
|
||||
return getPluginBasePath(this.options.packageName);
|
||||
}
|
||||
}
|
||||
|
||||
export default Plugin;
|
||||
|
@ -39,17 +39,18 @@ export class SyncMessageManager {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error('publish timeout'));
|
||||
}, 5000);
|
||||
|
||||
transaction.afterCommit(async () => {
|
||||
try {
|
||||
const r = await this.app.pubSubManager.publish(`${this.app.name}.sync.${channel}`, message, {
|
||||
skipSelf: true,
|
||||
...others,
|
||||
});
|
||||
clearTimeout(timer);
|
||||
resolve(r);
|
||||
} catch (error) {
|
||||
clearTimeout(timer);
|
||||
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 { uid } from '@nocobase/utils';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import _ from 'lodash';
|
||||
import qs from 'qs';
|
||||
import supertest, { SuperAgentTest } from 'supertest';
|
||||
import { MemoryPubSubAdapter } from './memory-pub-sub-adapter';
|
||||
import { MockDataSource } from './mock-data-source';
|
||||
|
||||
interface ActionParams {
|
||||
filterByTk?: any;
|
||||
@ -77,6 +77,10 @@ interface ExtendedAgent extends SuperAgentTest {
|
||||
}
|
||||
|
||||
export class MockServer extends Application {
|
||||
registerMockDataSource() {
|
||||
this.dataSourceManager.factory.register('mock', MockDataSource);
|
||||
}
|
||||
|
||||
async loadAndInstall(options: any = {}) {
|
||||
await this.load({ method: 'install' });
|
||||
|
||||
@ -231,13 +235,15 @@ export function mockServer(options: ApplicationOptions = {}) {
|
||||
PluginManager.findPackagePatched = true;
|
||||
}
|
||||
|
||||
const app = new MockServer({
|
||||
const mockServerOptions = {
|
||||
acl: false,
|
||||
syncMessageManager: {
|
||||
debounce: 500,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
const app = new MockServer(mockServerOptions);
|
||||
|
||||
const basename = app.options.pubSubManager?.channelPrefix;
|
||||
|
||||
@ -280,16 +286,27 @@ export async function createMockCluster({
|
||||
...options
|
||||
}: MockClusterOptions = {}) {
|
||||
const nodes: MockServer[] = [];
|
||||
let dbOptions;
|
||||
|
||||
for (let i = 0; i < number; i++) {
|
||||
if (dbOptions) {
|
||||
options['database'] = {
|
||||
...dbOptions,
|
||||
};
|
||||
}
|
||||
|
||||
const app: MockServer = await createMockServer({
|
||||
...options,
|
||||
skipSupervisor: true,
|
||||
name: clusterName + '_' + appName,
|
||||
skipInstall: Boolean(i),
|
||||
pubSubManager: {
|
||||
channelPrefix: clusterName,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dbOptions) {
|
||||
dbOptions = app.db.options;
|
||||
}
|
||||
console.log('-------------', await app.isInstalled());
|
||||
nodes.push(app);
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ export class RoleResourceModel extends Model {
|
||||
role.revokeResource(resourceName);
|
||||
}
|
||||
|
||||
async writeToACL(options: { acl: ACL; transaction: any }) {
|
||||
async writeToACL(options: { acl: ACL; transaction?: any }) {
|
||||
const { acl } = options;
|
||||
const resourceName = this.get('name') 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) {
|
||||
const { type, collectionName } = message;
|
||||
if (type === 'newCollection') {
|
||||
|
||||
if (type === 'syncCollection') {
|
||||
const collectionModel: CollectionModel = await this.app.db.getCollection('collections').repository.findOne({
|
||||
filter: {
|
||||
name: collectionName,
|
||||
@ -52,6 +53,26 @@ export class PluginDataSourceMainServer extends Plugin {
|
||||
|
||||
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() {
|
||||
@ -92,10 +113,15 @@ export class PluginDataSourceMainServer extends Plugin {
|
||||
transaction,
|
||||
});
|
||||
|
||||
this.sendSyncMessage({
|
||||
type: 'newCollection',
|
||||
this.sendSyncMessage(
|
||||
{
|
||||
type: 'syncCollection',
|
||||
collectionName: model.get('name'),
|
||||
});
|
||||
},
|
||||
{
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -113,6 +139,16 @@ export class PluginDataSourceMainServer extends Plugin {
|
||||
}
|
||||
|
||||
await model.remove(removeOptions);
|
||||
|
||||
this.sendSyncMessage(
|
||||
{
|
||||
type: 'removeCollection',
|
||||
collectionName: model.get('name'),
|
||||
},
|
||||
{
|
||||
transaction: options.transaction,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// 要在 beforeInitOptions 之前处理
|
||||
@ -262,6 +298,16 @@ export class PluginDataSourceMainServer extends Plugin {
|
||||
};
|
||||
|
||||
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) => {
|
||||
await mutex.runExclusive(async () => {
|
||||
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;
|
||||
} = {};
|
||||
|
||||
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() {
|
||||
this.app.db.registerModels({
|
||||
DataSourcesCollectionModel,
|
||||
@ -100,6 +190,16 @@ export class PluginDataSourceManagerServer extends Plugin {
|
||||
model.loadIntoApplication({
|
||||
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({
|
||||
async ['dataSources:listEnabled'](ctx, next) {
|
||||
const dataSources = await ctx.db.getRepository('dataSources').find({
|
||||
@ -302,6 +403,7 @@ export class PluginDataSourceManagerServer extends Plugin {
|
||||
|
||||
async ['dataSources:refresh'](ctx, next) {
|
||||
const { filterByTk, clientStatus } = ctx.action.params;
|
||||
|
||||
const dataSourceModel: DataSourceModel = await ctx.db.getRepository('dataSources').findOne({
|
||||
filter: {
|
||||
key: filterByTk,
|
||||
@ -317,6 +419,11 @@ export class PluginDataSourceManagerServer extends Plugin {
|
||||
dataSourceModel.loadIntoApplication({
|
||||
app: ctx.app,
|
||||
});
|
||||
|
||||
ctx.app.syncMessageManager.publish(self.name, {
|
||||
type: 'loadDataSource',
|
||||
dataSourceKey: dataSourceModel.get('key'),
|
||||
});
|
||||
}
|
||||
|
||||
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'));
|
||||
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({
|
||||
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({
|
||||
app: this.app,
|
||||
});
|
||||
|
||||
this.sendSyncMessage(
|
||||
{
|
||||
type: 'removeDataSourceField',
|
||||
key: model.get('key'),
|
||||
},
|
||||
{
|
||||
transaction: options.transaction,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
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.sendSyncMessage(
|
||||
{
|
||||
type: 'removeDataSource',
|
||||
dataSourceKey: model.get('key'),
|
||||
},
|
||||
{
|
||||
transaction: options.transaction,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
this.app.on('afterStart', async (app: Application) => {
|
||||
@ -412,6 +560,19 @@ export class PluginDataSourceManagerServer extends Plugin {
|
||||
acl: dataSource.acl,
|
||||
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,
|
||||
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) {
|
||||
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) => {
|
||||
@ -462,6 +647,18 @@ export class PluginDataSourceManagerServer extends Plugin {
|
||||
hooks: false,
|
||||
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 }) => {
|
||||
|
@ -46,36 +46,34 @@ const app = mockApp({
|
||||
'mobileRoutes:list': {
|
||||
data: [
|
||||
{
|
||||
"id": 10,
|
||||
"createdAt": "2024-07-08T13:22:33.763Z",
|
||||
"updatedAt": "2024-07-08T13:22:33.763Z",
|
||||
"parentId": null,
|
||||
"title": "Test1",
|
||||
"icon": "AppstoreOutlined",
|
||||
"schemaUid": "test",
|
||||
"type": "page",
|
||||
"options": null,
|
||||
"sort": 1,
|
||||
"createdById": 1,
|
||||
"updatedById": 1,
|
||||
id: 10,
|
||||
createdAt: '2024-07-08T13:22:33.763Z',
|
||||
updatedAt: '2024-07-08T13:22:33.763Z',
|
||||
parentId: null,
|
||||
title: 'Test1',
|
||||
icon: 'AppstoreOutlined',
|
||||
schemaUid: 'test',
|
||||
type: 'page',
|
||||
options: null,
|
||||
sort: 1,
|
||||
createdById: 1,
|
||||
updatedById: 1,
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"createdAt": "2024-07-08T13:23:01.929Z",
|
||||
"updatedAt": "2024-07-08T13:23:12.433Z",
|
||||
"parentId": null,
|
||||
"title": "Test2",
|
||||
"icon": "aliwangwangoutlined",
|
||||
"schemaUid": null,
|
||||
"type": "link",
|
||||
"options": {
|
||||
"schemaUid": null,
|
||||
"url": "https://github.com",
|
||||
"params": [
|
||||
{}
|
||||
]
|
||||
}
|
||||
}
|
||||
id: 13,
|
||||
createdAt: '2024-07-08T13:23:01.929Z',
|
||||
updatedAt: '2024-07-08T13:23:12.433Z',
|
||||
parentId: null,
|
||||
title: 'Test2',
|
||||
icon: 'aliwangwangoutlined',
|
||||
schemaUid: null,
|
||||
type: 'link',
|
||||
options: {
|
||||
schemaUid: null,
|
||||
url: 'https://github.com',
|
||||
params: [{}],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -15,8 +15,8 @@ const Demo = () => {
|
||||
title: 'Link',
|
||||
icon: 'AppstoreOutlined',
|
||||
options: {
|
||||
url: 'https://github.com'
|
||||
}
|
||||
url: 'https://github.com',
|
||||
},
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
|
@ -16,8 +16,8 @@ const schema = getMobileTabBarItemSchema({
|
||||
title: 'Link',
|
||||
icon: 'AppstoreOutlined',
|
||||
options: {
|
||||
url: 'https://github.com'
|
||||
}
|
||||
url: 'https://github.com',
|
||||
},
|
||||
});
|
||||
|
||||
const Demo = () => {
|
||||
|
@ -14,13 +14,7 @@ const schema = getMobileTabBarItemSchema({
|
||||
});
|
||||
|
||||
const Demo = () => {
|
||||
return (
|
||||
<SchemaComponent
|
||||
schema={schemaViewer(
|
||||
schema,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
return <SchemaComponent schema={schemaViewer(schema)} />;
|
||||
};
|
||||
|
||||
class MyPlugin extends Plugin {
|
||||
|
@ -18,5 +18,5 @@ export function getMobileTabBarItemSchema(routeItem: MobileRouteItem) {
|
||||
schemaUid: routeItem.schemaUid,
|
||||
...(routeItem.options || {}),
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -143,6 +143,34 @@ export class PluginMultiAppManagerServer extends Plugin {
|
||||
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) {
|
||||
this.subAppUpgradeHandler = handler;
|
||||
}
|
||||
@ -186,6 +214,16 @@ export class PluginMultiAppManagerServer extends Plugin {
|
||||
context: options.context,
|
||||
});
|
||||
|
||||
this.sendSyncMessage(
|
||||
{
|
||||
type: 'startApp',
|
||||
appName: name,
|
||||
},
|
||||
{
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
|
||||
const startPromise = subApp.runCommand('start', '--quickstart');
|
||||
|
||||
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);
|
||||
|
||||
this.sendSyncMessage(
|
||||
{
|
||||
type: 'removeApp',
|
||||
appName: model.get('name'),
|
||||
},
|
||||
{
|
||||
transaction: options.transaction,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const self = this;
|
||||
|
Loading…
x
Reference in New Issue
Block a user