ChengLei Shao 71e8d07f15
test: add automated testing (#4098)
* test: string includes operator

* chore: operator test coverage

* chore: database utils test

* chore: acl test

* chore: no permission error

* chore: code

* fix: run coverage test

* chore: datasource test

* chore: datasource test

* chore: datasource test

* chore: datasource test

* chore: datasource test

* chore: datasource

* fix: build

* chore: plugin data source manager test

* chore: acl test

* chore: query interface test

* chore: ui schema storage test

* chore: save template test

* chore: ui schema insert position action

* chore: ignore migration

* chore: plugin acl test

* chore: ignore command in coverage

* chore: ignore

* chore: remove db2resource

* chore: ignore migration

* chore: ipc server test

* chore: database test

* chore: database api comments

* chore: value parser test

* chore: build

* chore: backup & restore test

* chore: plugin manager test

* chore: pm

* chore: pm ignore

* chore: skip migration

* chore: remove unused code

* fix: import

* chore: remove unused code

* chore: remove unused code

* fix: action test

* chore: data wrapping middleware

* fix: build

* fix: build

* fix: build

* test: fix T-4105

* chore: test

* fix: data-source-manager test

* fix: sql collection test

* fix: test

* fix: test

* fix: test

* fix: typo

* chore: datasource manager test

* chore: console.log

---------

Co-authored-by: xilesun <2013xile@gmail.com>
2024-04-26 17:44:59 +08:00

1072 lines
30 KiB
TypeScript

import Topo from '@hapi/topo';
import { CleanOptions, Collection, SyncOptions } from '@nocobase/database';
import { importModule, isURL } from '@nocobase/utils';
import { fsExists } from '@nocobase/utils/plugin-symlink';
import execa from 'execa';
import fg from 'fast-glob';
import fs from 'fs';
import _ from 'lodash';
import net from 'net';
import { basename, dirname, join, resolve, sep } from 'path';
import Application from '../application';
import { createAppProxy, tsxRerunning } from '../helper';
import { Plugin } from '../plugin';
import { uploadMiddleware } from './middleware';
import collectionOptions from './options/collection';
import resourceOptions from './options/resource';
import { PluginManagerRepository } from './plugin-manager-repository';
import { PluginData } from './types';
import {
copyTempPackageToStorageAndLinkToNodeModules,
downloadAndUnzipToTempDir,
getNpmInfo,
getPluginInfoByNpm,
removeTmpDir,
updatePluginByCompressedFileUrl,
} from './utils';
export const sleep = async (timeout = 0) => {
return new Promise((resolve) => {
setTimeout(resolve, timeout);
});
};
export interface PluginManagerOptions {
app: Application;
plugins?: any[];
}
export interface InstallOptions {
cliArgs?: any[];
clean?: CleanOptions | boolean;
force?: boolean;
sync?: SyncOptions;
}
export class AddPresetError extends Error {}
export class PluginManager {
/**
* @internal
*/
app: Application;
/**
* @internal
*/
collection: Collection;
/**
* @internal
*/
pluginInstances = new Map<typeof Plugin, Plugin>();
/**
* @internal
*/
pluginAliases = new Map<string, Plugin>();
/**
* @internal
*/
server: net.Server;
/**
* @internal
*/
constructor(public options: PluginManagerOptions) {
this.app = options.app;
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.allow('pm', 'listEnabled', 'public');
this.app.acl.registerSnippet({
name: 'pm',
actions: ['pm:*'],
});
this.app.db.addMigrations({
namespace: 'core/pm',
directory: resolve(__dirname, '../migrations'),
});
this.app.resourcer.use(uploadMiddleware);
}
/**
* @internal
*/
_repository: PluginManagerRepository;
get repository() {
return this.app.db.getRepository('applicationPlugins') as PluginManagerRepository;
}
/**
* @internal
*/
static async getPackageJson(packageName: string) {
const file = await fs.promises.realpath(resolve(process.env.NODE_MODULES_PATH, packageName, 'package.json'));
const data = await fs.promises.readFile(file, { encoding: 'utf-8' });
return JSON.parse(data);
}
/**
* @internal
*/
static async getPackageName(name: string) {
const prefixes = this.getPluginPkgPrefix();
for (const prefix of prefixes) {
const pkg = resolve(process.env.NODE_MODULES_PATH, `${prefix}${name}`, 'package.json');
const exists = await fsExists(pkg);
if (exists) {
return `${prefix}${name}`;
}
}
throw new Error(`${name} plugin does not exist`);
}
/**
* @internal
*/
static getPluginPkgPrefix() {
return (process.env.PLUGIN_PACKAGE_PREFIX || '@nocobase/plugin-,@nocobase/preset-,@nocobase/plugin-pro-').split(
',',
);
}
/**
* @internal
*/
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`);
}
/**
* @internal
*/
static clearCache(packageName: string) {
return;
const packageNamePath = packageName.replace('/', sep);
Object.keys(require.cache).forEach((key) => {
if (key.includes(packageNamePath)) {
delete require.cache[key];
}
});
}
/**
* @internal
*/
static async resolvePlugin(pluginName: string | typeof Plugin, isUpgrade = false, isPkg = false) {
if (typeof pluginName === 'string') {
const packageName = isPkg ? pluginName : await this.getPackageName(pluginName);
this.clearCache(packageName);
return await importModule(packageName);
} else {
return pluginName;
}
}
addPreset(plugin: string | typeof Plugin, options: any = {}) {
if (this.app.loaded) {
throw new AddPresetError('must be added before executing app.load()');
}
if (!this.options.plugins) {
this.options.plugins = [];
}
this.options.plugins.push([plugin, options]);
}
getPlugins() {
return this.app.pm.pluginInstances;
}
getAliases() {
return this.app.pm.pluginAliases.keys();
}
get(name: string | typeof Plugin) {
if (typeof name === 'string') {
return this.app.pm.pluginAliases.get(name);
}
return this.app.pm.pluginInstances.get(name);
}
has(name: string | typeof Plugin) {
if (typeof name === 'string') {
return this.app.pm.pluginAliases.has(name);
}
return this.app.pm.pluginInstances.has(name);
}
del(name: string | typeof Plugin) {
const instance = this.get(name);
if (instance) {
this.app.pm.pluginAliases.delete(instance.name);
this.app.pm.pluginInstances.delete(instance.constructor as typeof Plugin);
}
}
/* istanbul ignore next -- @preserve */
async create(pluginName: string, options?: { forceRecreate?: boolean }) {
const createPlugin = async (name) => {
const pluginDir = resolve(process.cwd(), 'packages/plugins', name);
if (options?.forceRecreate) {
await fs.promises.rm(pluginDir, { recursive: true, force: true });
}
const { PluginGenerator } = require('@nocobase/cli/src/plugin-generator');
const generator = new PluginGenerator({
cwd: process.cwd(),
args: {},
context: {
name,
},
});
await generator.run();
};
await createPlugin(pluginName);
try {
await this.app.db.auth({ retry: 1 });
const installed = await this.app.isInstalled();
if (!installed) {
console.log(`yarn pm add ${pluginName}`);
return;
}
} catch (error) {
return;
}
this.app.log.info('attempt to add the plugin to the app');
let packageName: string;
try {
packageName = await PluginManager.getPackageName(pluginName);
} catch (error) {
packageName = pluginName;
}
const json = await PluginManager.getPackageJson(packageName);
this.app.log.info(`add plugin [${packageName}]`, {
name: pluginName,
packageName: packageName,
version: json.version,
});
await this.repository.updateOrCreate({
values: {
name: pluginName,
packageName: packageName,
version: json.version,
},
filterKeys: ['name'],
});
await sleep(1000);
await tsxRerunning();
}
async add(plugin?: string | typeof Plugin, options: any = {}, insert = false, isUpgrade = false) {
if (!isUpgrade && this.has(plugin)) {
const name = typeof plugin === 'string' ? plugin : plugin.name;
this.app.log.warn(`plugin [${name}] added`);
return;
}
if (!options.name && typeof plugin === 'string') {
options.name = plugin;
}
try {
if (typeof plugin === 'string' && options.name && !options.packageName) {
const packageName = await PluginManager.getPackageName(options.name);
options['packageName'] = packageName;
}
if (options.packageName) {
const packageJson = await PluginManager.getPackageJson(options.packageName);
options['packageJson'] = packageJson;
options['version'] = packageJson.version;
}
} catch (error) {
console.error(error);
// empty
}
this.app.log.debug(`add plugin [${options.name}]`, {
method: 'add',
submodule: 'plugin-manager',
name: options.name,
});
let P: any;
try {
P = await PluginManager.resolvePlugin(options.packageName || plugin, isUpgrade, !!options.packageName);
} catch (error) {
this.app.log.warn('plugin not found', error);
return;
}
const instance: Plugin = new P(createAppProxy(this.app), options);
this.pluginInstances.set(P, instance);
if (options.name) {
this.pluginAliases.set(options.name, instance);
}
if (options.packageName) {
this.pluginAliases.set(options.packageName, instance);
}
if (insert && options.name) {
await this.repository.updateOrCreate({
values: {
...options,
},
filterKeys: ['name'],
});
}
await instance.afterAdd();
}
/**
* @internal
*/
async initPlugins() {
await this.initPresetPlugins();
await this.initOtherPlugins();
}
/**
* @internal
*/
async loadCommands() {
this.app.log.debug('load commands');
const items = await this.repository.find({
filter: {
enabled: true,
},
});
const packageNames: string[] = items.map((item) => item.packageName);
const source = [];
for (const packageName of packageNames) {
const file = require.resolve(packageName);
const sourceDir = basename(dirname(file)) === 'src' ? 'src' : 'dist';
const directory = join(
packageName,
sourceDir,
'server/commands/*.' + (basename(dirname(file)) === 'src' ? 'ts' : 'js'),
);
source.push(directory.replaceAll(sep, '/'));
}
for (const plugin of this.options.plugins || []) {
if (typeof plugin === 'string') {
const packageName = await PluginManager.getPackageName(plugin);
const file = require.resolve(packageName);
const sourceDir = basename(dirname(file)) === 'src' ? 'src' : 'lib';
const directory = join(packageName, sourceDir, 'server/commands/*.' + (sourceDir === 'src' ? 'ts' : 'js'));
source.push(directory.replaceAll(sep, '/'));
}
}
const files = await fg(source, {
ignore: ['**/*.d.ts'],
cwd: process.env.NODE_MODULES_PATH,
});
for (const file of files) {
const callback = await importModule(file);
callback(this.app);
}
}
async load(options: any = {}) {
this.app.setMaintainingMessage('loading plugins...');
const total = this.pluginInstances.size;
let current = 0;
for (const [P, plugin] of this.getPlugins()) {
if (plugin.state.loaded) {
continue;
}
const name = plugin.name || P.name;
current += 1;
this.app.setMaintainingMessage(`before load plugin [${name}], ${current}/${total}`);
if (!plugin.enabled) {
continue;
}
this.app.logger.debug(`before load plugin [${name}]`, { submodule: 'plugin-manager', method: 'load', name });
await plugin.beforeLoad();
}
current = 0;
for (const [P, plugin] of this.getPlugins()) {
if (plugin.state.loaded) {
continue;
}
const name = plugin.name || P.name;
current += 1;
this.app.setMaintainingMessage(`load plugin [${name}], ${current}/${total}`);
if (!plugin.enabled) {
continue;
}
await this.app.emitAsync('beforeLoadPlugin', plugin, options);
this.app.logger.debug(`load plugin [${name}] `, { submodule: 'plugin-manager', method: 'load', name });
await plugin.loadCollections();
await plugin.load();
plugin.state.loaded = true;
await this.app.emitAsync('afterLoadPlugin', plugin, options);
}
this.app.setMaintainingMessage('loaded plugins');
}
async install(options: InstallOptions = {}) {
this.app.setMaintainingMessage('install plugins...');
const total = this.pluginInstances.size;
let current = 0;
this.app.log.debug('call db.sync()');
await this.app.db.sync();
const toBeUpdated = [];
for (const [P, plugin] of this.getPlugins()) {
if (plugin.state.installing || plugin.state.installed) {
continue;
}
const name = plugin.name || P.name;
current += 1;
if (!plugin.enabled) {
continue;
}
plugin.state.installing = true;
this.app.setMaintainingMessage(`before install plugin [${name}], ${current}/${total}`);
await this.app.emitAsync('beforeInstallPlugin', plugin, options);
this.app.logger.debug(`install plugin [${name}]...`);
await plugin.install(options);
toBeUpdated.push(name);
plugin.state.installing = false;
plugin.state.installed = true;
plugin.installed = true;
this.app.setMaintainingMessage(`after install plugin [${name}], ${current}/${total}`);
await this.app.emitAsync('afterInstallPlugin', plugin, options);
}
await this.repository.update({
filter: {
name: toBeUpdated,
},
values: {
installed: true,
},
});
}
async enable(name: string | string[]) {
let pluginNames = name;
if (name === '*') {
const items = await this.repository.find();
pluginNames = items.map((item: any) => item.name);
}
pluginNames = this.sort(pluginNames);
this.app.log.debug(`enabling plugin ${pluginNames.join(',')}`);
this.app.setMaintainingMessage(`enabling plugin ${pluginNames.join(',')}`);
const toBeUpdated = [];
for (const pluginName of pluginNames) {
const plugin = this.get(pluginName);
if (!plugin) {
throw new Error(`${pluginName} plugin does not exist`);
}
if (plugin.enabled) {
continue;
}
await this.app.emitAsync('beforeEnablePlugin', pluginName);
try {
await plugin.beforeEnable();
plugin.enabled = true;
toBeUpdated.push(pluginName);
} catch (error) {
if (name === '*') {
this.app.log.error(error.message);
} else {
throw error;
}
}
}
if (toBeUpdated.length === 0) {
return;
}
await this.repository.update({
filter: {
name: toBeUpdated,
},
values: {
enabled: true,
},
});
try {
await this.app.reload();
this.app.log.debug(`syncing database in enable plugin ${toBeUpdated.join(',')}...`);
this.app.setMaintainingMessage(`syncing database in enable plugin ${toBeUpdated.join(',')}...`);
await this.app.db.sync();
for (const pluginName of toBeUpdated) {
const plugin = this.get(pluginName);
if (!plugin.installed) {
this.app.log.debug(`installing plugin ${pluginName}...`);
this.app.setMaintainingMessage(`installing plugin ${pluginName}...`);
await plugin.install();
plugin.installed = true;
}
}
await this.repository.update({
filter: {
name: toBeUpdated,
},
values: {
installed: true,
},
});
for (const pluginName of toBeUpdated) {
const plugin = this.get(pluginName);
this.app.log.debug(`emit afterEnablePlugin event...`);
await plugin.afterEnable();
await this.app.emitAsync('afterEnablePlugin', pluginName);
this.app.log.debug(`afterEnablePlugin event emitted`);
}
await this.app.tryReloadOrRestart();
} catch (error) {
await this.repository.update({
filter: {
name: toBeUpdated,
},
values: {
enabled: false,
installed: false,
},
});
await this.app.tryReloadOrRestart({
recover: true,
});
throw error;
}
}
async disable(name: string | string[]) {
const pluginNames = _.castArray(name);
this.app.log.debug(`disabling plugin ${pluginNames.join(',')}`);
this.app.setMaintainingMessage(`disabling plugin ${pluginNames.join(',')}`);
const toBeUpdated = [];
for (const pluginName of pluginNames) {
const plugin = this.get(pluginName);
if (!plugin) {
throw new Error(`${pluginName} plugin does not exist`);
}
if (!plugin.enabled) {
continue;
}
await this.app.emitAsync('beforeDisablePlugin', pluginName);
await plugin.beforeDisable();
plugin.enabled = false;
toBeUpdated.push(pluginName);
}
if (toBeUpdated.length === 0) {
return;
}
await this.repository.update({
filter: {
name: toBeUpdated,
},
values: {
enabled: false,
},
});
try {
await this.app.tryReloadOrRestart();
for (const pluginName of pluginNames) {
const plugin = this.get(pluginName);
this.app.log.debug(`emit afterDisablePlugin event...`);
await plugin.afterDisable();
await this.app.emitAsync('afterDisablePlugin', pluginName);
this.app.log.debug(`afterDisablePlugin event emitted`);
}
} catch (error) {
await this.repository.update({
filter: {
name: toBeUpdated,
},
values: {
enabled: true,
},
});
await this.app.tryReloadOrRestart({
recover: true,
});
throw error;
}
}
async remove(name: string | string[], options?: { removeDir?: boolean; force?: boolean }) {
const pluginNames = _.castArray(name);
const records = pluginNames.map((name) => {
return {
name: name,
packageName: name,
};
});
const removeDir = async () => {
await Promise.all(
records.map(async (plugin) => {
const dir = resolve(process.env.NODE_MODULES_PATH, plugin.packageName);
try {
const realDir = await fs.promises.realpath(dir);
this.app.log.debug(`rm -rf ${realDir}`);
return fs.promises.rm(realDir, { force: true, recursive: true });
} catch (error) {
return false;
}
}),
);
await execa('yarn', ['nocobase', 'postinstall']);
};
if (options?.force) {
await this.repository.destroy({
filter: {
name: pluginNames,
},
});
} else {
await this.app.load();
for (const pluginName of pluginNames) {
const plugin = this.get(pluginName);
if (!plugin) {
continue;
}
if (plugin.enabled) {
throw new Error(`plugin is enabled [${pluginName}]`);
}
await plugin.beforeRemove();
}
await this.repository.destroy({
filter: {
name: pluginNames,
},
});
const plugins: Plugin[] = [];
for (const pluginName of pluginNames) {
const plugin = this.get(pluginName);
if (!plugin) {
continue;
}
plugins.push(plugin);
this.del(pluginName);
await plugin.afterRemove();
}
if (await this.app.isStarted()) {
await this.app.tryReloadOrRestart();
}
}
if (options?.removeDir) {
await removeDir();
}
await execa('yarn', ['nocobase', 'refresh']);
}
/**
* @internal
*/
async addViaCLI(urlOrName: string, options?: PluginData) {
if (isURL(urlOrName)) {
await this.addByCompressedFileUrl({
...options,
compressedFileUrl: urlOrName,
});
} else if (await fsExists(urlOrName)) {
await this.addByCompressedFileUrl({
...(options as any),
compressedFileUrl: urlOrName,
});
} else if (options?.registry) {
if (!options.name) {
const model = await this.repository.findOne({ filter: { packageName: urlOrName } });
if (model) {
options['name'] = model?.name;
}
if (!options.name) {
options['name'] = urlOrName.replace('@nocobase/plugin-', '');
}
}
await this.addByNpm({
...(options as any),
packageName: urlOrName,
});
} else {
const opts = {
...options,
};
const model = await this.repository.findOne({ filter: { packageName: urlOrName } });
if (model) {
opts['name'] = model.name;
}
if (!opts['packageName']) {
opts['packageName'] = urlOrName;
}
await this.add(opts['name'] || urlOrName, opts, true);
}
await this.app.emitStartedEvent();
await execa('yarn', ['nocobase', 'postinstall']);
}
/**
* @internal
*/
async addByNpm(options: { packageName: string; name?: string; registry: string; authToken?: string }) {
let { name = '', registry, packageName, authToken } = options;
name = name.trim();
registry = registry.trim();
packageName = packageName.trim();
authToken = authToken?.trim();
const { compressedFileUrl } = await getPluginInfoByNpm({
packageName,
registry,
authToken,
});
return this.addByCompressedFileUrl({ name, compressedFileUrl, registry, authToken, type: 'npm' });
}
/**
* @internal
*/
async addByFile(options: { file: string; registry?: string; authToken?: string; type?: string; name?: string }) {
const { file, authToken } = options;
const { packageName, tempFile, tempPackageContentDir } = await downloadAndUnzipToTempDir(file, authToken);
const name = options.name || packageName;
if (this.has(name)) {
await removeTmpDir(tempFile, tempPackageContentDir);
throw new Error(`plugin name [${name}] already exists`);
}
await copyTempPackageToStorageAndLinkToNodeModules(tempFile, tempPackageContentDir, packageName);
return this.add(name, { packageName }, true);
}
/**
* @internal
*/
async addByCompressedFileUrl(options: {
compressedFileUrl: string;
registry?: string;
authToken?: string;
type?: string;
name?: string;
}) {
const { compressedFileUrl, authToken } = options;
const { packageName, tempFile, tempPackageContentDir } = await downloadAndUnzipToTempDir(
compressedFileUrl,
authToken,
);
const name = options.name || packageName;
if (this.has(name)) {
await removeTmpDir(tempFile, tempPackageContentDir);
throw new Error(`plugin name [${name}] already exists`);
}
await copyTempPackageToStorageAndLinkToNodeModules(tempFile, tempPackageContentDir, packageName);
return this.add(name, { packageName }, true);
}
async update(options: PluginData) {
if (options['url']) {
options.compressedFileUrl = options['url'];
}
if (!options.name) {
const model = await this.repository.findOne({ filter: { packageName: options.packageName } });
options['name'] = model.name;
}
if (options.compressedFileUrl) {
await this.upgradeByCompressedFileUrl(options);
} else {
await this.upgradeByNpm(options as any);
}
const file = resolve(process.cwd(), 'storage/app-upgrading');
await fs.promises.writeFile(file, '', 'utf-8');
// await this.app.upgrade();
if (process.env.IS_DEV_CMD) {
await tsxRerunning();
} else {
await execa('yarn', ['nocobase', 'pm2-restart'], {
env: process.env,
});
}
}
/**
* @internal
*/
async upgradeByNpm(values: PluginData) {
const name = values.name;
const plugin = this.get(name);
if (!this.has(name)) {
throw new Error(`plugin name [${name}] not exists`);
}
if (!plugin.options.packageName || !values.registry) {
throw new Error(`plugin name [${name}] not installed by npm`);
}
const version = values.version?.trim();
const registry = values.registry?.trim() || plugin.options.registry;
const authToken = values.authToken?.trim() || plugin.options.authToken;
const { compressedFileUrl } = await getPluginInfoByNpm({
packageName: plugin.options.packageName,
registry: registry,
authToken: authToken,
version,
});
return this.upgradeByCompressedFileUrl({ compressedFileUrl, name, version, registry, authToken });
}
/**
* @internal
*/
async upgradeByCompressedFileUrl(options: PluginData) {
const { name, compressedFileUrl, authToken } = options;
const data = await this.repository.findOne({ filter: { name } });
const { version } = await updatePluginByCompressedFileUrl({
compressedFileUrl,
packageName: data.packageName,
authToken: authToken,
});
await this.add(name, { version, packageName: data.packageName }, true, true);
}
/**
* @internal
*/
getNameByPackageName(packageName: string) {
const prefixes = PluginManager.getPluginPkgPrefix();
const prefix = prefixes.find((prefix) => packageName.startsWith(prefix));
if (!prefix) {
throw new Error(
`package name [${packageName}] invalid, just support ${prefixes.join(
', ',
)}. You can modify process.env.PLUGIN_PACKAGE_PREFIX add more prefix.`,
);
}
return packageName.replace(prefix, '');
}
async list(options: any = {}) {
const { locale = 'en-US', isPreset = false } = options;
return Promise.all(
[...this.getPlugins().keys()]
.map((name) => {
const plugin = this.get(name);
if (!isPreset && plugin.options.isPreset) {
return;
}
return plugin.toJSON({ locale });
})
.filter(Boolean),
);
}
/**
* @internal
*/
async getNpmVersionList(name: string) {
const plugin = this.get(name);
const npmInfo = await getNpmInfo(plugin.options.packageName, plugin.options.registry, plugin.options.authToken);
return Object.keys(npmInfo.versions);
}
/**
* @internal
*/
async loadPresetMigrations() {
const migrations = {
beforeLoad: [],
afterSync: [],
afterLoad: [],
};
for (const [P, plugin] of this.getPlugins()) {
if (!plugin.isPreset) {
continue;
}
const { beforeLoad, afterSync, afterLoad } = await plugin.loadMigrations();
migrations.beforeLoad.push(...beforeLoad);
migrations.afterSync.push(...afterSync);
migrations.afterLoad.push(...afterLoad);
}
return {
beforeLoad: {
up: async () => {
this.app.log.debug('run preset migrations(beforeLoad)');
const migrator = this.app.db.createMigrator({ migrations: migrations.beforeLoad });
await migrator.up();
},
},
afterSync: {
up: async () => {
this.app.log.debug('run preset migrations(afterSync)');
const migrator = this.app.db.createMigrator({ migrations: migrations.afterSync });
await migrator.up();
},
},
afterLoad: {
up: async () => {
this.app.log.debug('run preset migrations(afterLoad)');
const migrator = this.app.db.createMigrator({ migrations: migrations.afterLoad });
await migrator.up();
},
},
};
}
/**
* @internal
*/
async loadOtherMigrations() {
const migrations = {
beforeLoad: [],
afterSync: [],
afterLoad: [],
};
for (const [P, plugin] of this.getPlugins()) {
if (plugin.isPreset) {
continue;
}
if (!plugin.enabled) {
continue;
}
const { beforeLoad, afterSync, afterLoad } = await plugin.loadMigrations();
migrations.beforeLoad.push(...beforeLoad);
migrations.afterSync.push(...afterSync);
migrations.afterLoad.push(...afterLoad);
}
return {
beforeLoad: {
up: async () => {
this.app.log.debug('run others migrations(beforeLoad)');
const migrator = this.app.db.createMigrator({ migrations: migrations.beforeLoad });
await migrator.up();
},
},
afterSync: {
up: async () => {
this.app.log.debug('run others migrations(afterSync)');
const migrator = this.app.db.createMigrator({ migrations: migrations.afterSync });
await migrator.up();
},
},
afterLoad: {
up: async () => {
this.app.log.debug('run others migrations(afterLoad)');
const migrator = this.app.db.createMigrator({ migrations: migrations.afterLoad });
await migrator.up();
},
},
};
}
/**
* @internal
*/
async loadPresetPlugins() {
await this.initPresetPlugins();
await this.load();
}
async upgrade() {
this.app.log.info('run upgrade');
const toBeUpdated = [];
for (const [P, plugin] of this.getPlugins()) {
if (plugin.state.upgraded) {
continue;
}
if (!plugin.enabled) {
continue;
}
if (!plugin.isPreset && !plugin.installed) {
this.app.log.info(`install built-in plugin [${plugin.name}]`);
await plugin.install();
toBeUpdated.push(plugin.name);
}
this.app.log.debug(`upgrade plugin [${plugin.name}]`);
await plugin.upgrade();
plugin.state.upgraded = true;
}
await this.repository.update({
filter: {
name: toBeUpdated,
},
values: {
installed: true,
},
});
}
/**
* @internal
*/
async initOtherPlugins() {
if (this['_initOtherPlugins']) {
return;
}
await this.repository.init();
this['_initOtherPlugins'] = true;
}
/**
* @internal
*/
async initPresetPlugins() {
if (this['_initPresetPlugins']) {
return;
}
for (const plugin of this.options.plugins) {
const [p, opts = {}] = Array.isArray(plugin) ? plugin : [plugin];
await this.add(p, { enabled: true, isPreset: true, ...opts });
}
this['_initPresetPlugins'] = true;
}
private sort(names: string | string[]) {
const pluginNames = _.castArray(names);
if (pluginNames.length === 1) {
return pluginNames;
}
const sorter = new Topo.Sorter<string>();
for (const pluginName of pluginNames) {
const plugin = this.get(pluginName);
const peerDependencies = Object.keys(plugin.options?.packageJson?.peerDependencies || {});
sorter.add(pluginName, { after: peerDependencies, group: plugin.options?.packageName || pluginName });
}
return sorter.nodes;
}
}
export default PluginManager;