diff --git a/cli/src/commands/plugin/create.ts b/cli/src/commands/plugin/create.ts
index c18b3fb066..cd0466d58f 100644
--- a/cli/src/commands/plugin/create.ts
+++ b/cli/src/commands/plugin/create.ts
@@ -74,6 +74,21 @@ export default class Create extends Command {
let repoUrl;
+ const commonHygenArgs = [
+ 'plugin',
+ 'new',
+ '--name',
+ `${args.plugin_name}`,
+ '--type',
+ `${type}`,
+ '--display_name',
+ `${name}`,
+ '--plugins_path',
+ `${pluginsPath}`,
+ ];
+
+ const hygenArgs = !marketplace ? [...commonHygenArgs, '--docs_path', `${docsPath}`] : commonHygenArgs;
+
if (marketplace) {
const buffer = fs.readFileSync(path.join('server', 'src', 'assets', 'marketplace', 'plugins.json'), 'utf8');
const pluginsJson = JSON.parse(buffer);
@@ -89,21 +104,6 @@ export default class Create extends Command {
});
}
- const hygenArgs = [
- 'plugin',
- 'new',
- '--name',
- `${args.plugin_name}`,
- '--type',
- `${type}`,
- '--display_name',
- `${name}`,
- '--plugins_path',
- `${pluginsPath}`,
- '--docs_path',
- `${docsPath}`,
- ];
-
CliUx.ux.action.start('creating plugin');
await runner(hygenArgs, {
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index ab6b1ad90a..d3379eb0a7 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -28,6 +28,7 @@
"date-fns": "^2.29.3",
"deep-object-diff": "^1.1.9",
"dompurify": "^3.0.0",
+ "dotenv": "^16.0.3",
"draft-js": "^0.11.7",
"draft-js-export-html": "^1.4.1",
"driver.js": "^0.9.8",
@@ -31098,6 +31099,14 @@
"tslib": "^2.0.3"
}
},
+ "node_modules/dotenv": {
+ "version": "16.0.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
+ "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/draft-js": {
"version": "0.11.7",
"license": "MIT",
@@ -68445,6 +68454,11 @@
"tslib": "^2.0.3"
}
},
+ "dotenv": {
+ "version": "16.0.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
+ "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ=="
+ },
"draft-js": {
"version": "0.11.7",
"requires": {
diff --git a/frontend/package.json b/frontend/package.json
index 9de83a2717..be7d04f9de 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -23,6 +23,7 @@
"date-fns": "^2.29.3",
"deep-object-diff": "^1.1.9",
"dompurify": "^3.0.0",
+ "dotenv": "^16.0.3",
"draft-js": "^0.11.7",
"draft-js-export-html": "^1.4.1",
"driver.js": "^0.9.8",
diff --git a/frontend/src/MarketplacePage/InstalledPlugins.jsx b/frontend/src/MarketplacePage/InstalledPlugins.jsx
index d2e9cfa417..c1b96ad1f1 100644
--- a/frontend/src/MarketplacePage/InstalledPlugins.jsx
+++ b/frontend/src/MarketplacePage/InstalledPlugins.jsx
@@ -1,41 +1,53 @@
import React from 'react';
import cx from 'classnames';
-import { marketplaceService, pluginsService } from '@/_services';
+import { pluginsService } from '@/_services';
import { toast } from 'react-hot-toast';
import Spinner from '@/_ui/Spinner';
import { capitalizeFirstLetter } from './utils';
-export const InstalledPlugins = ({ isActive }) => {
- const [plugins, setPlugins] = React.useState([]);
+export const InstalledPlugins = ({
+ allPlugins = [],
+ installedPlugins,
+ fetching,
+ fetchPlugins,
+ ENABLE_MARKETPLACE_DEV_MODE,
+}) => {
+ return (
+
+ {fetching && (
+
+
+
+ )}
+ {!fetching && allPlugins.length > 0 && (
+
+ {installedPlugins?.map((plugin) => {
+ const marketplacePlugin = allPlugins?.find((m) => m.id === plugin.pluginId);
+ const isUpdateAvailable = marketplacePlugin?.version !== plugin.version;
+ return (
+
+ );
+ })}
+ {!fetching && installedPlugins?.length === 0 && (
+
+ )}
+
+ )}
+
+ );
+};
+
+const InstalledPluginCard = ({ plugin, marketplacePlugin, fetchPlugins, isDevMode, isUpdateAvailable }) => {
const [updating, setUpdating] = React.useState(false);
- const [marketplacePlugins, setMarketplacePlugins] = React.useState([]);
- const [fetching, setFetching] = React.useState(false);
-
- React.useEffect(() => {
- marketplaceService
- .findAll()
- .then(({ data = [] }) => setMarketplacePlugins(data))
- .catch((error) => {
- toast.error(error?.message || 'something went wrong');
- });
- }, [isActive]);
-
- const fetchPlugins = async () => {
- setFetching(true);
- const { data, error } = await pluginsService.findAll();
- setFetching(false);
-
- if (error) {
- toast.error(error?.message || 'something went wrong');
- return;
- }
-
- setPlugins(data);
- };
-
- React.useEffect(() => {
- fetchPlugins();
- }, [isActive]);
const deletePlugin = async ({ id, name }) => {
var result = confirm('Are you sure you want to delete ' + name + '?');
@@ -70,77 +82,97 @@ export const InstalledPlugins = ({ isActive }) => {
fetchPlugins();
};
+ const reloadPlugin = async ({ id, name }) => {
+ setUpdating(true);
+ const { error } = await pluginsService.reloadPlugin(id);
+ setUpdating(false);
+
+ if (error) {
+ toast.error(error?.message || `Unable to reload ${name}`);
+ return;
+ }
+ toast.success(`${name} reloaded`);
+ };
+
return (
-
- {fetching && (
-
-
-
- )}
- {!fetching && (
-
- {plugins?.map((plugin) => {
- const marketplacePlugin = marketplacePlugins.find((m) => m.id === plugin.pluginId);
- const isUpdateAvailable = marketplacePlugin?.version !== plugin.version;
- return (
-
-
-
-
-
-
-
-
-
-
-
{plugin.name}
-
{plugin.description}
-
-
-
-
-
-
- v{plugin.version}{' '}
- {isUpdateAvailable && (
- updatePlugin(plugin, marketplacePlugin.version)}
- >
-
- (click to update to v{marketplacePlugin.version})
-
-
- )}
-
-
-
-
deletePlugin(plugin)}
- >
- Remove
-
-
-
-
-
- {updating && (
-
+
+
+
+
+
+
+
+
+
+
+
{plugin.name}
+
{plugin.description}
+
+
+ {isDevMode && (
+
+ )}
+
+
+
+
+
+
+ v{plugin.version}{' '}
+ {isUpdateAvailable && (
+ updatePlugin(plugin, marketplacePlugin?.version)}
+ >
+ (click to update to v{marketplacePlugin?.version})
+
)}
+
+
+
+
deletePlugin(plugin)}
+ >
+ Remove
- );
- })}
- {!fetching && plugins?.length === 0 && (
-
- )}
+
- )}
+ {updating && (
+
+ )}
+
);
};
+
+InstalledPlugins.Plugin = InstalledPluginCard;
diff --git a/frontend/src/MarketplacePage/MarketplacePlugins.jsx b/frontend/src/MarketplacePage/MarketplacePlugins.jsx
index 2cf708f72f..97cda233ee 100644
--- a/frontend/src/MarketplacePage/MarketplacePlugins.jsx
+++ b/frontend/src/MarketplacePage/MarketplacePlugins.jsx
@@ -1,19 +1,10 @@
import React from 'react';
import { toast } from 'react-hot-toast';
import { MarketplaceCard } from './MarketplaceCard';
-import { marketplaceService, pluginsService } from '@/_services';
+import { pluginsService } from '@/_services';
-export const MarketplacePlugins = ({ isActive }) => {
- const [plugins, setPlugins] = React.useState([]);
+export const MarketplacePlugins = ({ allPlugins = [] }) => {
const [installedPlugins, setInstalledPlugins] = React.useState({});
- React.useEffect(() => {
- marketplaceService
- .findAll()
- .then(({ data = [] }) => setPlugins(data))
- .catch((error) => {
- toast.error(error?.message || 'something went wrong');
- });
- }, [isActive]);
React.useEffect(() => {
pluginsService
@@ -28,12 +19,16 @@ export const MarketplacePlugins = ({ isActive }) => {
.catch((error) => {
toast.error(error?.message || 'something went wrong');
});
+
+ return () => {
+ setInstalledPlugins({});
+ };
}, []);
return (
- {plugins?.map(({ id, name, repo, version, description }) => {
+ {allPlugins?.map(({ id, name, repo, version, description }) => {
return (
{
const [active, setActive] = React.useState('installed');
+ const [marketplacePlugins, setMarketplacePlugins] = React.useState([]);
+ const [installedPlugins, setInstalledPlugins] = React.useState([]);
+ const [fetchingInstalledPlugins, setFetching] = React.useState(false);
+
+ const ENABLE_MARKETPLACE_DEV_MODE = config.ENABLE_MARKETPLACE_DEV_MODE === 'true';
+
+ React.useEffect(() => {
+ marketplaceService
+ .findAll()
+ .then(({ data = [] }) => setMarketplacePlugins(data))
+ .catch((error) => {
+ toast.error(error?.message || 'something went wrong');
+ });
+
+ fetchPlugins();
+
+ () => {
+ setMarketplacePlugins([]);
+ setInstalledPlugins([]);
+ };
+ }, [active]);
+
+ const fetchPlugins = async () => {
+ setFetching(true);
+ const { data, error } = await pluginsService.findAll();
+ setFetching(false);
+
+ if (error) {
+ toast.error(error?.message || 'something went wrong');
+ return;
+ }
+
+ setInstalledPlugins(data);
+ };
return (
@@ -30,9 +67,15 @@ const MarketplacePage = ({ darkMode, switchDarkMode }) => {
{active === 'installed' ? (
-
+
) : (
-
+
)}
diff --git a/frontend/src/_components/DynamicForm.jsx b/frontend/src/_components/DynamicForm.jsx
index 01a450b634..0f4cbd9b8d 100644
--- a/frontend/src/_components/DynamicForm.jsx
+++ b/frontend/src/_components/DynamicForm.jsx
@@ -40,29 +40,30 @@ const DynamicForm = ({
React.useEffect(() => {
const { properties } = schema;
- if (isEmpty(properties)) return null;
+ if (!isEmpty(properties)) {
+ let fields = {};
+ let encrpytedFieldsProps = {};
+ const flipComponentDropdown = find(properties, ['type', 'dropdown-component-flip']);
- let fields = {};
- let encrpytedFieldsProps = {};
- const flipComponentDropdown = find(properties, ['type', 'dropdown-component-flip']);
+ if (flipComponentDropdown) {
+ const selector = options?.[flipComponentDropdown?.key]?.value;
+ fields = { ...flipComponentDropdown?.commonFields, ...properties[selector] };
+ } else {
+ fields = { ...properties };
+ }
- if (flipComponentDropdown) {
- const selector = options?.[flipComponentDropdown?.key]?.value;
- fields = { ...flipComponentDropdown?.commonFields, ...properties[selector] };
- } else {
- fields = { ...properties };
+ Object.keys(fields).map((key) => {
+ const { type, encrypted } = fields[key];
+ if ((type === 'password' || encrypted) && !(key in computedProps)) {
+ //Editable encrypted fields only if datasource doesn't exists
+ encrpytedFieldsProps[key] = {
+ disabled: !!selectedDataSource?.id,
+ };
+ }
+ });
+ setComputedProps({ ...computedProps, ...encrpytedFieldsProps });
}
- Object.keys(fields).map((key) => {
- const { type, encrypted } = fields[key];
- if ((type === 'password' || encrypted) && !(key in computedProps)) {
- //Editable encrypted fields only if datasource doesn't exists
- encrpytedFieldsProps[key] = {
- disabled: !!selectedDataSource?.id,
- };
- }
- });
- setComputedProps({ ...computedProps, ...encrpytedFieldsProps });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [options]);
diff --git a/frontend/src/_services/plugins.service.js b/frontend/src/_services/plugins.service.js
index ba91693b04..14437b1c97 100644
--- a/frontend/src/_services/plugins.service.js
+++ b/frontend/src/_services/plugins.service.js
@@ -18,9 +18,14 @@ function deletePlugin(id) {
return adapter.delete(`/plugins/${id}`);
}
+function reloadPlugin(id) {
+ return adapter.post(`/plugins/${id}/reload`);
+}
+
export const pluginsService = {
findAll,
installPlugin,
updatePlugin,
deletePlugin,
+ reloadPlugin,
};
diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js
index f8e0cf51bc..cedc4c41c9 100644
--- a/frontend/webpack.config.js
+++ b/frontend/webpack.config.js
@@ -3,6 +3,7 @@ const webpack = require('webpack');
const path = require('path');
const CompressionPlugin = require('compression-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
+require('dotenv').config({ path: '../.env' });
const hash = require('string-hash');
const environment = process.env.NODE_ENV === 'production' ? 'production' : 'development';
@@ -169,6 +170,7 @@ module.exports = {
COMMENT_FEATURE_ENABLE: process.env.COMMENT_FEATURE_ENABLE ?? true,
ENABLE_TOOLJET_DB: process.env.ENABLE_TOOLJET_DB ?? true,
ENABLE_MULTIPLAYER_EDITING: true,
+ ENABLE_MARKETPLACE_DEV_MODE: process.env.ENABLE_MARKETPLACE_DEV_MODE,
TOOLJET_MARKETPLACE_URL:
process.env.TOOLJET_MARKETPLACE_URL || 'https://tooljet-plugins-production.s3.us-east-2.amazonaws.com',
}),
diff --git a/marketplace/_templates/plugin/new/docs.ejs.t b/marketplace/_templates/plugin/new/docs.ejs.t
deleted file mode 100644
index 8abaa751c0..0000000000
--- a/marketplace/_templates/plugin/new/docs.ejs.t
+++ /dev/null
@@ -1,16 +0,0 @@
----
-to: <%= docs_path %>/docs/data-sources/<%= name %>.md
----
-<%
- Display_name = h.capitalize(display_name)
-%>
-# <%= name %>
-
-ToolJet can connect to <%= Display_name %> datasource to read and write data.
-
-- [Connection](#connection)
-- [Getting Started](#querying-<%= name %>)
-
-## Connection
-
-## Querying <%= Display_name %> operations
\ No newline at end of file
diff --git a/marketplace/_templates/plugin/new/packagejson.ejs.t b/marketplace/_templates/plugin/new/packagejson.ejs.t
index 38de1878e3..aa2768d483 100644
--- a/marketplace/_templates/plugin/new/packagejson.ejs.t
+++ b/marketplace/_templates/plugin/new/packagejson.ejs.t
@@ -15,7 +15,8 @@ to: <%= plugins_path %>/plugins/<%= name %>/package.json
],
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
- "build": "ncc build lib/index.ts -o dist"
+ "build": "ncc build lib/index.ts -o dist",
+ "watch": "ncc build lib/index.ts -o dist --watch"
},
"homepage": "https://github.com/tooljet/tooljet#readme",
"dependencies": {
diff --git a/marketplace/lerna.json b/marketplace/lerna.json
new file mode 100644
index 0000000000..aebebbab22
--- /dev/null
+++ b/marketplace/lerna.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "node_modules/lerna/schemas/lerna-schema.json",
+ "useWorkspaces": true,
+ "version": "0.0.0"
+}
diff --git a/marketplace/package.json b/marketplace/package.json
index 2de149b6e5..d5b3775b9b 100644
--- a/marketplace/package.json
+++ b/marketplace/package.json
@@ -7,7 +7,13 @@
"version": "1.0.0",
"devDependencies": {
"aws-sdk": "^2.1326.0",
+ "lerna": "^6.4.1",
"mime-types": "^2.1.35",
"recursive-readdir": "^2.2.3"
+ },
+ "scripts": {
+ "start:watch": "lerna run watch --stream --parallel",
+ "build": "npm run build --workspaces",
+ "start:dev": "npm run build && npm run start:watch"
}
}
diff --git a/marketplace/plugins/github/lib/index.ts b/marketplace/plugins/github/lib/index.ts
index 2c01e10efd..efe163588a 100644
--- a/marketplace/plugins/github/lib/index.ts
+++ b/marketplace/plugins/github/lib/index.ts
@@ -8,15 +8,9 @@ export default class Github implements QueryService {
async run(sourceOptions: SourceOptions, queryOptions: QueryOptions, dataSourceId: string): Promise
{
const operation: Operation = queryOptions.operation;
- console.log('---octakit operation', {
- queryOptions
-
- });
-
const octokit:Octokit = await this.getConnection(sourceOptions);
let result = {};
-
try {
switch (operation) {
case Operation.GetUserInfo:
diff --git a/marketplace/plugins/github/package.json b/marketplace/plugins/github/package.json
index f578479171..cb8abf9256 100644
--- a/marketplace/plugins/github/package.json
+++ b/marketplace/plugins/github/package.json
@@ -12,7 +12,8 @@
],
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
- "build": "ncc build lib/index.ts -o dist"
+ "build": "ncc build lib/index.ts -o dist",
+ "watch": "ncc build lib/index.ts -o dist --watch"
},
"homepage": "https://github.com/tooljet/tooljet#readme",
"dependencies": {
diff --git a/marketplace/plugins/plivo/package.json b/marketplace/plugins/plivo/package.json
index c8a01e3b22..757a1ca092 100644
--- a/marketplace/plugins/plivo/package.json
+++ b/marketplace/plugins/plivo/package.json
@@ -12,7 +12,8 @@
],
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
- "build": "ncc build lib/index.ts -o dist"
+ "build": "ncc build lib/index.ts -o dist",
+ "watch": "ncc build lib/index.ts -o dist --watch"
},
"homepage": "https://github.com/tooljet/tooljet#readme",
"dependencies": {
diff --git a/marketplace/plugins/s3/package.json b/marketplace/plugins/s3/package.json
index 71ab13278f..a37407ed00 100644
--- a/marketplace/plugins/s3/package.json
+++ b/marketplace/plugins/s3/package.json
@@ -12,7 +12,8 @@
],
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1",
- "build": "ncc build lib/index.ts -o dist"
+ "build": "ncc build lib/index.ts -o dist",
+ "watch": "ncc build lib/index.ts -o dist --watch"
},
"homepage": "https://github.com/tooljet/tooljet#readme",
"dependencies": {
diff --git a/server/src/controllers/plugins.controller.ts b/server/src/controllers/plugins.controller.ts
index f78e2d10f5..39ffac4e59 100644
--- a/server/src/controllers/plugins.controller.ts
+++ b/server/src/controllers/plugins.controller.ts
@@ -77,4 +77,10 @@ export class PluginsController {
return this.pluginsService.remove(id);
}
+
+ @Post(':id/reload')
+ @UseGuards(JwtAuthGuard)
+ async reload(@Param('id') id: string) {
+ return this.pluginsService.reload(id);
+ }
}
diff --git a/server/src/helpers/plugins.helper.ts b/server/src/helpers/plugins.helper.ts
index cc01ad5fd8..a7e977e0dc 100644
--- a/server/src/helpers/plugins.helper.ts
+++ b/server/src/helpers/plugins.helper.ts
@@ -24,10 +24,13 @@ export class PluginsHelper {
}
async getService(pluginId: string, kind: string) {
+ const isMarketPlaceDev = process.env.ENABLE_MARKETPLACE_DEV_MODE === 'true';
+
try {
if (pluginId) {
let decoded: string;
- if (this.plugins[pluginId]) {
+
+ if (!isMarketPlaceDev && this.plugins[pluginId]) {
decoded = this.plugins[pluginId];
} else {
const plugin = await this.pluginsRepository.findOne({ where: { id: pluginId }, relations: ['indexFile'] });
diff --git a/server/src/services/plugins.service.ts b/server/src/services/plugins.service.ts
index be9f1cdb84..eb5b4867b0 100644
--- a/server/src/services/plugins.service.ts
+++ b/server/src/services/plugins.service.ts
@@ -14,6 +14,7 @@ import { dbTransactionWrap } from 'src/helpers/utils.helper';
import { UpdateFileDto } from '@dto/update-file.dto';
const jszipInstance = new jszip();
+const fs = require('fs');
@Injectable()
export class PluginsService {
@@ -194,23 +195,39 @@ export class PluginsService {
return [indexFile, operationsFile, iconFile, manifestFile];
}
- const fs = require('fs/promises');
+ async function readFile(filePath) {
+ return new Promise((resolve, reject) => {
+ const readStream = fs.createReadStream(filePath, { encoding: 'utf8' });
+ let fileContent = '';
+
+ readStream.on('data', (chunk) => {
+ fileContent += chunk;
+ });
+
+ readStream.on('error', (err) => {
+ reject(err);
+ });
+
+ readStream.on('end', () => {
+ resolve(fileContent);
+ });
+ });
+ }
- // NOTE: big files are going to have a major impact on the memory consumption and speed of execution of the program
- // as `fs.readFile` read the full content of the file in memory before returning the data.
- // In this case, a better option is to read the file content using streams.
const [indexFile, operationsFile, iconFile, manifestFile] = await Promise.all([
- fs.readFile(`../marketplace/plugins/${id}/dist/index.js`, 'utf8'),
- fs.readFile(`../marketplace/plugins/${id}/lib/operations.json`, 'utf8'),
- fs.readFile(`../marketplace/plugins/${id}/lib/icon.svg`, 'utf8'),
- fs.readFile(`../marketplace/plugins/${id}/lib/manifest.json`, 'utf8'),
+ readFile(`../marketplace/plugins/${id}/dist/index.js`),
+ readFile(`../marketplace/plugins/${id}/lib/operations.json`),
+ readFile(`../marketplace/plugins/${id}/lib/icon.svg`),
+ readFile(`../marketplace/plugins/${id}/lib/manifest.json`),
]);
return [indexFile, operationsFile, iconFile, manifestFile];
}
fetchPluginFiles(id: string, repo: string) {
- if (repo) return this.fetchPluginFilesFromRepo(repo);
+ if (repo && repo.length > 0) {
+ return this.fetchPluginFilesFromRepo(repo);
+ }
return this.fetchPluginFilesFromS3(id);
}
@@ -230,4 +247,47 @@ export class PluginsService {
async remove(id: string) {
return await this.pluginsRepository.delete(id);
}
+
+ async reload(id: string) {
+ const queryRunner = this.connection.createQueryRunner();
+
+ await queryRunner.connect();
+ await queryRunner.startTransaction();
+
+ try {
+ const plugin = await this.findOne(id);
+ const { pluginId, repo, version } = plugin;
+
+ const [index, operations, icon, manifest] = await this.fetchPluginFiles(pluginId, repo);
+
+ const files = { index, operations, icon, manifest };
+
+ const uploadedFiles: { index?: File; operations?: File; icon?: File; manifest?: File } = {};
+ await Promise.all(
+ Object.keys(files).map(async (key) => {
+ return await dbTransactionWrap(async (manager: EntityManager) => {
+ const file = files[key];
+ const fileDto = new UpdateFileDto();
+ fileDto.data = encode(file);
+ fileDto.filename = key;
+ uploadedFiles[key] = await this.filesService.update(plugin[`${key}FileId`], fileDto, manager);
+ });
+ })
+ );
+
+ const updatedPlugin = new Plugin();
+
+ updatedPlugin.id = plugin.id;
+ updatedPlugin.repo = repo || '';
+ updatedPlugin.version = version;
+
+ return this.pluginsRepository.save(updatedPlugin);
+ } catch (error) {
+ await queryRunner.rollbackTransaction();
+ throw new InternalServerErrorException(error);
+ } finally {
+ await queryRunner.commitTransaction();
+ await queryRunner.release();
+ }
+ }
}