mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-02 03:02:19 +08:00
* fix: sort field with table dose not have primary key * feat: fixed params merger * chore(plugins/acl): fixed params * chore(plugins/acl): allowConfigure of collections * chore(plugins/china-region): disable actions other than list * chore(plugins/collection-manager): allowConfigure permission * chore(plugins/file-manager): acl fixed params * chore: acl fixed params * chore: rolesResourcesScopes onDelete cascade * fix: install error * chore: test * fix: root user fixed params * fix: role resource scope onDelete * chore: test * chore: test * fix: acl * chore: disable index.html cache * chore: disable index.html cache * test: destory user role * test: destory throught table * fix: test * fix: test * chore: add rolesUsers to fixed params * feat: permission logging * feat: permission logging * fix: test * fix: test * chore: disable grant target action * fix: appends with fields * fix: get action params * fix: associationActions * chore: change AssociationField using relation type * chore: typo * refactor: allow to skip * fix: prettier * chore: attachments association action * fix: allowConfigure condition * fix: deprecated allow * fix: please use skip instead * feat: table column aclcheck * chore: test * feat: throw error when detory no permission record * chore: test * chore: acl test * feat: field acl * chore: after action middleware * fix: destory permission check * chore: middleware use * fix: test * feat: filter match * feat: subform/subtable field acl check * feat: action permision by scope * feat: action permision by scope * feat: list action with allowedActions * chore: all allowed action * fix: pk error * fix: merge error * fix: create query sql * fix: skip permission * fix: scope with association field * feat: action acl fix * feat: action acl fix * fix: update submodule * Feat: setting center permission (#1214) * feat: add setting center permissions * feat: setting center permissions backlist * feat: setting center permissions BLACKLIST * feat: setting center permissions blacklist * feat: setting center permissions blacklist * feat: setting center permission * feat: configure plugin tab expand Co-authored-by: chenos <chenlinxh@gmail.com> * Feat :field acl (#1211) Co-authored-by: chenos <chenlinxh@gmail.com> * fix: build error * test: acl snippet * feat: set field * fix: test * fix: build error * fix: utils Dependency cycles * feat: general permissions * feat: delete pluginTabBlacklist * fix: test * feat: snippetManager allow method * feat: acl role snippetAllowed method * feat: array field repository * feat: ArrayFieldRepository * fix: test * fix: ci * fix: ci error * fix: add set parse * test: array field repository * chore: addSnippetPatten * fix: start * feat: sync role snippets * feat: snippets check * feat: snippets check * chore: acl role snippet api * fix: test * fix: test * refactor: acl role snippets * chore: registerACLSettingSnippet * chore: default snippets * feat: snippets match * feat: snippets check * feat: snippets check * feat: pm permision check * feat: pm permision check * feat: snippet pattern match * feat: pluginManagerToolbar check * feat: pluginManagerToolbar check * chore: snippets default value * feat: set role snippets migration * chore: snippets * feat: acl local * feat: acl local * feat: bookmask fix * feat: plugin-manger & ui-editor snippet * feat: set allowConfigure to false when upgrade to snippets * feat: destory action acl fix * feat: destory action acl fix * fix: association resource params merge * fix: ui editor snippet * feat: action acl fix * chore: move list meta middleware into plugins/acl * fix: test * feat: action acl fix * feat: action acl check fix * feat: plugins toolbar fix * feat: gitmodules * fix: subproject * chore: add avaiableActions to snippet * chore: change plugin-manager snippet * feat: configure action acl fix * feat: plugin tab acl check fix * chore: roles snippets * fix: add actions to snippet * feat: allowconfigure fix * fix: count with filterBy * fix: build error * feat: get action with allowedActions * feat: acl route check fix * feat: aclActionProvider fix * feat: actionscpe fix * feat: actionname alias * feat: setting center fix * feat: acl provider fix * fix: role collection * feat: associate resource acl * feat: associate resource acl * feat: redirect to 403 * feat: route redirct * feat: acl scope check by record * fix: fields appends fix * fix: fields appends fix * fix: fields appends fix * fix: allowedActions fix * fix: menu items * fix: rename * fix: improve code * fix: improve code * fix: improve code * fix: ctx?.data?.data * fix: styling * fix: allowAll after ignore scope * chore: allowConfigure condition * fix: collections.fields:* * fix: acl test * fix: update submodule * fix: acl test * fix: acl snippet * fix: updates * fix: only load history for logged-in users * fix: this.app.acl.registerSnippet * fix: downloadXlsxTemplate * fix: 404 * feat: allowedAction in association list response * fix: listData get * fix: test * fix: x-collection-field * fix: update record error * fix: calendar template * test: allow manager * fix: fetch action step * fix: update submodule * fix: refresh * fix: refresh * fix: rolesResourcesScopes * test: snippets * fix: snippets * fix: test * fix: omit filter.createdById * fix: improve code * fix: collections path * fix: test error * fix: upgrade error * fix: errors * fix: read allowed actions error * fix: kanban error * fix: error Co-authored-by: chenos <chenlinxh@gmail.com> Co-authored-by: katherinehhh <katherine_15995@163.com>
388 lines
10 KiB
TypeScript
388 lines
10 KiB
TypeScript
import { CleanOptions, Collection, SyncOptions } from '@nocobase/database';
|
|
import { requireModule } from '@nocobase/utils';
|
|
import execa from 'execa';
|
|
import fs from 'fs';
|
|
import net from 'net';
|
|
import { resolve } from 'path';
|
|
import xpipe from 'xpipe';
|
|
import Application from '../application';
|
|
import { Plugin } from '../plugin';
|
|
import collectionOptions from './options/collection';
|
|
import resourceOptions from './options/resource';
|
|
import { PluginManagerRepository } from './PluginManagerRepository';
|
|
|
|
export interface PluginManagerOptions {
|
|
app: Application;
|
|
plugins?: any[];
|
|
}
|
|
|
|
export interface InstallOptions {
|
|
cliArgs?: any[];
|
|
clean?: CleanOptions | boolean;
|
|
sync?: SyncOptions;
|
|
}
|
|
|
|
export class PluginManager {
|
|
app: Application;
|
|
collection: Collection;
|
|
repository: PluginManagerRepository;
|
|
plugins = new Map<string, Plugin>();
|
|
server: net.Server;
|
|
pmSock: string;
|
|
_tmpPluginArgs = [];
|
|
|
|
constructor(options: PluginManagerOptions) {
|
|
this.app = options.app;
|
|
const f = resolve(process.cwd(), 'storage', 'pm.sock');
|
|
this.pmSock = xpipe.eq(this.app.options.pmSock || f);
|
|
this.app.db.registerRepositories({
|
|
PluginManagerRepository,
|
|
});
|
|
this.collection = this.app.db.collection(collectionOptions);
|
|
this.repository = this.collection.repository as PluginManagerRepository;
|
|
this.repository.setPluginManager(this);
|
|
this.app.resourcer.define(resourceOptions);
|
|
|
|
this.app.acl.registerSnippet({
|
|
name: 'pm',
|
|
actions: ['pm:*', 'applicationPlugins:list'],
|
|
});
|
|
|
|
this.server = net.createServer((socket) => {
|
|
socket.on('data', async (data) => {
|
|
const { method, plugins } = JSON.parse(data.toString());
|
|
try {
|
|
console.log(method, plugins);
|
|
await this[method](plugins);
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
}
|
|
});
|
|
socket.pipe(socket);
|
|
});
|
|
|
|
this.app.on('beforeLoad', async (app, options) => {
|
|
if (options?.method && ['install', 'upgrade'].includes(options.method)) {
|
|
await this.collection.sync();
|
|
}
|
|
|
|
const exists = await this.app.db.collectionExistsInDb('applicationPlugins');
|
|
|
|
if (!exists) {
|
|
return;
|
|
}
|
|
|
|
if (options?.method !== 'install' || options.reload) {
|
|
await this.repository.load();
|
|
}
|
|
});
|
|
this.app.on('beforeUpgrade', async () => {
|
|
await this.collection.sync();
|
|
});
|
|
|
|
this.addStaticMultiple(options.plugins);
|
|
}
|
|
|
|
addStaticMultiple(plugins: any) {
|
|
for (let plugin of plugins || []) {
|
|
if (typeof plugin == 'string') {
|
|
this.addStatic(plugin);
|
|
} else {
|
|
this.addStatic(...plugin);
|
|
}
|
|
}
|
|
}
|
|
|
|
getPlugins() {
|
|
return this.plugins;
|
|
}
|
|
|
|
get(name: string) {
|
|
return this.plugins.get(name);
|
|
}
|
|
|
|
has(name: string) {
|
|
return this.plugins.has(name);
|
|
}
|
|
|
|
clientWrite(data: any) {
|
|
const { method, plugins } = data;
|
|
if (method === 'create') {
|
|
try {
|
|
console.log(method, plugins);
|
|
this[method](plugins);
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
}
|
|
return;
|
|
}
|
|
const client = new net.Socket();
|
|
client.connect(this.pmSock, () => {
|
|
client.write(JSON.stringify(data));
|
|
client.end();
|
|
});
|
|
client.on('error', async () => {
|
|
try {
|
|
console.log(method, plugins);
|
|
await this[method](plugins);
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
}
|
|
});
|
|
}
|
|
|
|
async listen(): Promise<net.Server> {
|
|
if (fs.existsSync(this.pmSock)) {
|
|
await fs.promises.unlink(this.pmSock);
|
|
}
|
|
return new Promise((resolve) => {
|
|
this.server.listen(this.pmSock, () => {
|
|
resolve(this.server);
|
|
});
|
|
});
|
|
}
|
|
|
|
async create(name: string | string[]) {
|
|
console.log('creating...');
|
|
const pluginNames = Array.isArray(name) ? name : [name];
|
|
const { run } = require('@nocobase/cli/src/util');
|
|
const createPlugin = async (name) => {
|
|
const { PluginGenerator } = require('@nocobase/cli/src/plugin-generator');
|
|
const generator = new PluginGenerator({
|
|
cwd: resolve(process.cwd(), name),
|
|
args: {},
|
|
context: {
|
|
name,
|
|
},
|
|
});
|
|
await generator.run();
|
|
};
|
|
await Promise.all(pluginNames.map((pluginName) => createPlugin(pluginName)));
|
|
await run('yarn', ['install']);
|
|
}
|
|
|
|
clone() {
|
|
const pm = new PluginManager({
|
|
app: this.app,
|
|
});
|
|
for (const arg of this._tmpPluginArgs) {
|
|
pm.addStatic(...arg);
|
|
}
|
|
return pm;
|
|
}
|
|
|
|
addStatic(plugin?: any, options?: any) {
|
|
if (!options?.async) {
|
|
this._tmpPluginArgs.push([plugin, options]);
|
|
}
|
|
let name: string;
|
|
if (typeof plugin === 'string') {
|
|
name = plugin;
|
|
plugin = PluginManager.resolvePlugin(plugin);
|
|
} else {
|
|
name = plugin.name;
|
|
if (!name) {
|
|
throw new Error(`plugin name invalid`);
|
|
}
|
|
}
|
|
|
|
const instance = new plugin(this.app, {
|
|
name,
|
|
enabled: true,
|
|
...options,
|
|
});
|
|
const pluginName = instance.getName();
|
|
if (this.plugins.has(pluginName)) {
|
|
throw new Error(`plugin name [${pluginName}] already exists`);
|
|
}
|
|
this.plugins.set(pluginName, instance);
|
|
return instance;
|
|
}
|
|
|
|
async generateClientFile(plugin: string, packageName: string) {
|
|
const file = resolve(
|
|
process.cwd(),
|
|
'packages',
|
|
process.env.APP_PACKAGE_ROOT || 'app',
|
|
'client/src/plugins',
|
|
`${plugin}.ts`,
|
|
);
|
|
if (!fs.existsSync(file)) {
|
|
try {
|
|
require.resolve(`${packageName}/client`);
|
|
await fs.promises.writeFile(file, `export { default } from '${packageName}/client';`);
|
|
const { run } = require('@nocobase/cli/src/util');
|
|
await run('yarn', ['nocobase', 'postinstall']);
|
|
} catch (error) {}
|
|
}
|
|
}
|
|
|
|
async add(plugin: any, options: any = {}, transaction?: any) {
|
|
if (Array.isArray(plugin)) {
|
|
const t = transaction || (await this.app.db.sequelize.transaction());
|
|
try {
|
|
const items = await Promise.all(plugin.map((p) => this.add(p, options, t)));
|
|
await t.commit();
|
|
return items;
|
|
} catch (error) {
|
|
await t.rollback();
|
|
throw error;
|
|
}
|
|
}
|
|
const packageName = await PluginManager.findPackage(plugin);
|
|
const packageJson = require(`${packageName}/package.json`);
|
|
await this.generateClientFile(plugin, packageName);
|
|
const instance = this.addStatic(plugin, {
|
|
...options,
|
|
async: true,
|
|
});
|
|
let model = await this.repository.findOne({
|
|
transaction,
|
|
filter: { name: plugin },
|
|
});
|
|
if (!model) {
|
|
const { enabled, builtIn, installed, ...others } = options;
|
|
model = await this.repository.create({
|
|
transaction,
|
|
values: {
|
|
name: plugin,
|
|
version: packageJson.version,
|
|
enabled: !!enabled,
|
|
builtIn: !!builtIn,
|
|
installed: !!installed,
|
|
options: {
|
|
...others,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
return instance;
|
|
}
|
|
|
|
async load(options: any = {}) {
|
|
for (const [name, plugin] of this.plugins) {
|
|
if (!plugin.enabled) {
|
|
continue;
|
|
}
|
|
await plugin.beforeLoad();
|
|
}
|
|
|
|
for (const [name, plugin] of this.plugins) {
|
|
if (!plugin.enabled) {
|
|
continue;
|
|
}
|
|
await this.app.emitAsync('beforeLoadPlugin', plugin, options);
|
|
await plugin.load();
|
|
await this.app.emitAsync('afterLoadPlugin', plugin, options);
|
|
}
|
|
}
|
|
|
|
async install(options: InstallOptions = {}) {
|
|
for (const [name, plugin] of this.plugins) {
|
|
if (!plugin.enabled) {
|
|
continue;
|
|
}
|
|
await this.app.emitAsync('beforeInstallPlugin', plugin, options);
|
|
await plugin.install(options);
|
|
await this.app.emitAsync('afterInstallPlugin', plugin, options);
|
|
}
|
|
}
|
|
|
|
async enable(name: string | string[]) {
|
|
try {
|
|
const pluginNames = await this.repository.enable(name);
|
|
await this.app.reload();
|
|
await this.app.db.sync();
|
|
for (const pluginName of pluginNames) {
|
|
const plugin = this.app.getPlugin(pluginName);
|
|
if (!plugin) {
|
|
throw new Error(`${name} plugin does not exist`);
|
|
}
|
|
await plugin.install();
|
|
await plugin.afterEnable();
|
|
}
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async disable(name: string | string[]) {
|
|
try {
|
|
const pluginNames = await this.repository.disable(name);
|
|
await this.app.reload();
|
|
for (const pluginName of pluginNames) {
|
|
const plugin = this.app.getPlugin(pluginName);
|
|
if (!plugin) {
|
|
throw new Error(`${name} plugin does not exist`);
|
|
}
|
|
await plugin.afterDisable();
|
|
}
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async remove(name: string | string[]) {
|
|
const pluginNames = typeof name === 'string' ? [name] : name;
|
|
for (const pluginName of pluginNames) {
|
|
const plugin = this.app.getPlugin(pluginName);
|
|
if (!plugin) {
|
|
throw new Error(`${name} plugin does not exist`);
|
|
}
|
|
await plugin.remove();
|
|
}
|
|
await this.repository.remove(name);
|
|
this.app.reload();
|
|
}
|
|
|
|
static getPackageName(name: string) {
|
|
const prefixes = this.getPluginPkgPrefix();
|
|
for (const prefix of prefixes) {
|
|
try {
|
|
require.resolve(`${prefix}${name}`);
|
|
return `${prefix}${name}`;
|
|
} catch (error) {
|
|
continue;
|
|
}
|
|
}
|
|
throw new Error(`${name} plugin does not exist`);
|
|
}
|
|
|
|
static getPluginPkgPrefix() {
|
|
return (process.env.PLUGIN_PACKAGE_PREFIX || '@nocobase/plugin-,@nocobase/preset-,@nocobase/plugin-pro-').split(
|
|
',',
|
|
);
|
|
}
|
|
|
|
static async findPackage(name: string) {
|
|
try {
|
|
const packageName = this.getPackageName(name);
|
|
return packageName;
|
|
} catch (error) {
|
|
console.log(`\`${name}\` plugin not found locally`);
|
|
const prefixes = this.getPluginPkgPrefix();
|
|
for (const prefix of prefixes) {
|
|
try {
|
|
const packageName = `${prefix}${name}`;
|
|
console.log(`Try to find ${packageName}`);
|
|
await execa('npm', ['v', packageName, 'versions']);
|
|
console.log(`${packageName} downloading`);
|
|
await execa('yarn', ['add', packageName, '-W']);
|
|
console.log(`${packageName} downloaded`);
|
|
return packageName;
|
|
} catch (error) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
throw new Error(`No available packages found, ${name} plugin does not exist`);
|
|
}
|
|
|
|
static resolvePlugin(pluginName: string) {
|
|
const packageName = this.getPackageName(pluginName);
|
|
return requireModule(packageName);
|
|
}
|
|
}
|
|
|
|
export default PluginManager;
|