2024-08-21 06:57:01 +00:00
|
|
|
/**
|
|
|
|
* 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 { DataSource, SequelizeCollectionManager } from '@nocobase/data-source-manager';
|
2024-08-26 09:49:32 +00:00
|
|
|
import { Collection, DestroyOptions, Model, SyncOptions } from '@nocobase/database';
|
|
|
|
import { Plugin } from '@nocobase/server';
|
2024-08-21 06:57:01 +00:00
|
|
|
import lodash from 'lodash';
|
2024-08-26 09:49:32 +00:00
|
|
|
import { Transaction } from 'sequelize';
|
2024-08-21 06:57:01 +00:00
|
|
|
import { TreeCollection } from './tree-collection';
|
|
|
|
|
|
|
|
class PluginCollectionTreeServer extends Plugin {
|
|
|
|
async beforeLoad() {
|
|
|
|
const condition = (options) => {
|
|
|
|
return options.tree;
|
|
|
|
};
|
|
|
|
|
|
|
|
this.app.db.collectionFactory.registerCollectionType(TreeCollection, {
|
|
|
|
condition,
|
|
|
|
});
|
|
|
|
|
|
|
|
this.app.dataSourceManager.afterAddDataSource((dataSource: DataSource) => {
|
|
|
|
const collectionManager = dataSource.collectionManager;
|
|
|
|
if (collectionManager instanceof SequelizeCollectionManager) {
|
|
|
|
collectionManager.db.on('afterDefineCollection', (collection: Collection) => {
|
|
|
|
if (!condition(collection.options)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const name = `${dataSource.name}_${collection.name}_path`;
|
|
|
|
const parentForeignKey = collection.treeParentField?.foreignKey || 'parentId';
|
|
|
|
|
|
|
|
//always define tree path collection
|
2024-08-26 09:49:32 +00:00
|
|
|
const options = {};
|
|
|
|
if (collection.options.schema) {
|
|
|
|
options['schema'] = collection.options.schema;
|
|
|
|
}
|
|
|
|
this.defineTreePathCollection(name, options);
|
2024-08-21 06:57:01 +00:00
|
|
|
|
|
|
|
//afterSync
|
|
|
|
collectionManager.db.on(`${collection.name}.afterSync`, async ({ transaction }) => {
|
|
|
|
// trigger tree path collection create logic
|
|
|
|
await this.db.getCollection(name).sync({ transaction } as SyncOptions);
|
|
|
|
});
|
|
|
|
|
|
|
|
//afterCreate
|
|
|
|
this.db.on(`${collection.name}.afterCreate`, async (model: Model, options) => {
|
|
|
|
const { transaction } = options;
|
2024-10-30 02:47:12 +00:00
|
|
|
const tk = collection.filterTargetKey;
|
2024-08-21 06:57:01 +00:00
|
|
|
let path = `/${model.get(tk)}`;
|
2024-10-30 02:47:12 +00:00
|
|
|
path = await this.getTreePath(model, path, collection, name, transaction);
|
2024-08-21 06:57:01 +00:00
|
|
|
const rootPk = path.split('/')[1];
|
|
|
|
await this.app.db.getRepository(name).create({
|
|
|
|
values: {
|
|
|
|
nodePk: model.get(tk),
|
|
|
|
path: path,
|
|
|
|
rootPk: rootPk ? Number(rootPk) : null,
|
|
|
|
},
|
|
|
|
transaction,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
//afterUpdate
|
|
|
|
this.db.on(`${collection.name}.afterUpdate`, async (model: Model, options) => {
|
2024-10-30 02:47:12 +00:00
|
|
|
const tk = collection.filterTargetKey;
|
2024-08-21 06:57:01 +00:00
|
|
|
// only update parentId and filterTargetKey
|
|
|
|
if (!(model._changed.has(tk) || model._changed.has(parentForeignKey))) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const { transaction } = options;
|
2024-10-30 02:47:12 +00:00
|
|
|
await this.updateTreePath(model, collection, name, transaction);
|
2024-10-28 02:19:45 +00:00
|
|
|
});
|
2024-08-21 06:57:01 +00:00
|
|
|
|
2024-10-28 02:19:45 +00:00
|
|
|
// after remove
|
|
|
|
this.db.on(`${collection.name}.afterBulkUpdate`, async (options) => {
|
2024-10-30 02:47:12 +00:00
|
|
|
const tk = collection.filterTargetKey;
|
2024-10-28 02:19:45 +00:00
|
|
|
if (!(options.where && options.where[tk])) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const instances = await this.db.getRepository(collection.name).find({
|
|
|
|
where: {
|
|
|
|
[tk]: options.where[tk],
|
2024-08-21 06:57:01 +00:00
|
|
|
},
|
2024-10-28 02:19:45 +00:00
|
|
|
transaction: options.transaction,
|
2024-08-21 06:57:01 +00:00
|
|
|
});
|
2024-10-28 02:19:45 +00:00
|
|
|
for (const model of instances) {
|
2024-10-30 02:47:12 +00:00
|
|
|
await this.updateTreePath(model, collection, name, options.transaction);
|
2024-08-21 06:57:01 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
//afterDestroy
|
|
|
|
this.db.on(`${collection.name}.afterDestroy`, async (model: Model, options: DestroyOptions) => {
|
2024-10-30 02:47:12 +00:00
|
|
|
const tk = collection.filterTargetKey;
|
2024-08-21 06:57:01 +00:00
|
|
|
await this.app.db.getRepository(name).destroy({
|
|
|
|
filter: {
|
|
|
|
nodePk: model.get(tk),
|
|
|
|
},
|
|
|
|
transaction: options.transaction,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.db.on('collections.afterDestroy', async (collection: Model, { transaction }) => {
|
|
|
|
const name = `main_${collection.get('name')}_path`;
|
|
|
|
if (!condition(collection.options)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const collectionTree = this.db.getCollection(name);
|
|
|
|
if (collectionTree) {
|
|
|
|
await this.db.getCollection(name).removeFromDb({ transaction });
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-08-26 09:49:32 +00:00
|
|
|
private async defineTreePathCollection(name: string, options: { schema?: string }) {
|
2024-08-21 06:57:01 +00:00
|
|
|
this.db.collection({
|
|
|
|
name,
|
|
|
|
autoGenId: false,
|
|
|
|
timestamps: false,
|
|
|
|
fields: [
|
|
|
|
{ type: 'integer', name: 'nodePk' },
|
|
|
|
{ type: 'string', name: 'path', length: 1024 },
|
|
|
|
{ type: 'integer', name: 'rootPk' },
|
|
|
|
],
|
|
|
|
indexes: [
|
|
|
|
{
|
|
|
|
fields: [{ name: 'path', length: 191 }],
|
|
|
|
},
|
|
|
|
],
|
2024-08-26 09:49:32 +00:00
|
|
|
...options,
|
2024-08-21 06:57:01 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private async getTreePath(
|
|
|
|
model: Model,
|
|
|
|
path: string,
|
|
|
|
collection: Collection,
|
|
|
|
pathCollectionName: string,
|
|
|
|
transaction?: Transaction,
|
|
|
|
) {
|
2024-10-30 02:47:12 +00:00
|
|
|
const tk = collection.filterTargetKey;
|
2024-08-21 06:57:01 +00:00
|
|
|
const parentForeignKey = collection.treeParentField?.foreignKey || 'parentId';
|
|
|
|
if (model.get(parentForeignKey) && model.get(parentForeignKey) !== null) {
|
|
|
|
const parent = await this.app.db.getRepository(collection.name).findOne({
|
|
|
|
filter: {
|
|
|
|
[tk]: model.get(parentForeignKey),
|
|
|
|
},
|
|
|
|
transaction,
|
|
|
|
});
|
|
|
|
if (parent && parent.get(parentForeignKey) !== model.get(tk)) {
|
|
|
|
path = `/${parent.get(tk)}${path}`;
|
|
|
|
if (parent.get(parentForeignKey) !== null) {
|
|
|
|
const collectionTreePath = this.app.db.getCollection(pathCollectionName);
|
|
|
|
const nodePkColumnName = collectionTreePath.getField('nodePk').columnName();
|
|
|
|
const parentPathData = await this.app.db.getRepository(pathCollectionName).findOne({
|
|
|
|
filter: {
|
|
|
|
[nodePkColumnName]: parent.get(tk),
|
|
|
|
},
|
|
|
|
transaction,
|
|
|
|
});
|
|
|
|
const parentPath = lodash.get(parentPathData, 'path', null);
|
|
|
|
if (parentPath == null) {
|
2024-10-30 02:47:12 +00:00
|
|
|
path = await this.getTreePath(parent, path, collection, pathCollectionName, transaction);
|
2024-08-21 06:57:01 +00:00
|
|
|
} else {
|
|
|
|
path = `${parentPath}/${model.get(tk)}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return path;
|
|
|
|
}
|
2024-10-28 02:19:45 +00:00
|
|
|
|
|
|
|
private async updateTreePath(
|
|
|
|
model: Model,
|
|
|
|
collection: Collection,
|
|
|
|
pathCollectionName: string,
|
|
|
|
transaction: Transaction,
|
|
|
|
) {
|
2024-10-30 02:47:12 +00:00
|
|
|
const tk = collection.filterTargetKey;
|
2024-10-28 02:19:45 +00:00
|
|
|
let path = `/${model.get(tk)}`;
|
2024-10-30 02:47:12 +00:00
|
|
|
path = await this.getTreePath(model, path, collection, pathCollectionName, transaction);
|
2024-10-28 02:19:45 +00:00
|
|
|
const collectionTreePath = this.db.getCollection(pathCollectionName);
|
|
|
|
const nodePkColumnName = collectionTreePath.getField('nodePk').columnName();
|
|
|
|
const pathData = await this.app.db.getRepository(pathCollectionName).findOne({
|
|
|
|
filter: {
|
|
|
|
[nodePkColumnName]: model.get(tk),
|
|
|
|
},
|
|
|
|
transaction,
|
|
|
|
});
|
|
|
|
|
|
|
|
const relatedNodes = await this.app.db.getRepository(pathCollectionName).find({
|
|
|
|
filter: {
|
|
|
|
path: {
|
|
|
|
$startsWith: `${pathData.get('path')}`,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
transaction,
|
|
|
|
});
|
|
|
|
const rootPk = path.split('/')[1];
|
|
|
|
for (const node of relatedNodes) {
|
|
|
|
const newPath = node.get('path').replace(pathData.get('path'), path);
|
|
|
|
await this.app.db.getRepository(pathCollectionName).update({
|
|
|
|
values: {
|
|
|
|
path: newPath,
|
|
|
|
rootPk: rootPk ? Number(rootPk) : null,
|
|
|
|
},
|
|
|
|
filter: {
|
|
|
|
[nodePkColumnName]: node.get('nodePk'),
|
|
|
|
},
|
|
|
|
transaction,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2024-08-21 06:57:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export default PluginCollectionTreeServer;
|