kopia lustrzana https://github.com/ToolJet/ToolJet
[Feature] Make plugins installable (#3069)
* feat: add user avatar
* update: @nest/platform-express from 8.0.0 to 8.4.4
* add avatar_id in login response
* add user avatar upload in frontend
* align cross divider with layout icons'
* generate nest model - extensions
* Add extensions module
* Add extension to datasouce
* add not implemented check
* create extension
* refactor
* cleanup
* fix tests
* reduce the avatar size on homepage
* poc: run js code from string
* resolve conflicts
* fix conflicts
* add globals
* add new route
* add icon, manifest file upload
* complete user flow
* add flow for data queries
* add dynamic manifest instead of local datasource types
* add version attr
* remove unused code
* add version
* rename extension(s) -> plugins(s)
* add test connection method
* feat: add marketplace listing page
* Add install plugin cmd + missing attrs {name, repo, desc} to plugin
* add missing icon
* - Add npm workspaces for marketplace monorepo
- Added cassandra datasource plugin
- Created upload to s3 script
- Created plugins.json entry file
* install plugin from s3 bucket
* cleanup
* update pkg locks
* fix icon render
* cleanup
* marketplace changes
* ui changes
* operations file load fix + revert vm2
* update module from string to 3.2.1
* load plugins.json from local file instead of remote
* install plugin from local file if not production environment
* add sqlite
* feat: add plivo api plugin
* exp: add heroku 22 stack
* update assets include path
* Revert "exp: add heroku 22 stack"
This reverts commit a8926b36e1
.
* add integrations link
* Add casl ability for plugin
* load host from env else fallback to default
* update imports
* remove sqlite
* typo
* add marketplace flag to cli command
* move ts and ncc to devDep
* add hygen templates for marketplace
* cli tree-node path fix
* template indent fix
* TOOLJET_URL -> MARKETPLACE_TOOLJET_URL
* add tests
* refactor: move to plugins.helper for get-service helper utility
* fix; typo
* update package-lock.json
* review changes
* remove a href
* remove bg color + redirect issue due to href
* add test url
* fix crash on search
* remove extra slash
* feat: allow plugin to be installed from github repository
* remove unwanted args from cli command
* add repo attr while save
* feat: add feature toggle for marketplace feature
* fix: make default config as false
* chore: remove hyperlink
* fix: failing build
* chore: update s3 url to point to prod
* fix failing test
* fix test
* fix: test case
* update module from string pkg
* update env
* fix test
* fix test
* add readme file
* Update README.md
Co-authored-by: Akshay Sasidharan <akshaysasidharan93@gmail.com>
pull/4640/head
rodzic
510be16753
commit
a1fd1fc301
|
@ -48,6 +48,7 @@ SENTRY_DEBUG=
|
|||
# FEATURE TOGGLE
|
||||
COMMENT_FEATURE_ENABLE=
|
||||
ENABLE_MULTIPLAYER_EDITING=true
|
||||
ENABLE_MARKETPLACE_FEATURE=
|
||||
|
||||
# SSO (Applicable only for Multi-Workspace)
|
||||
SSO_GOOGLE_OAUTH2_CLIENT_ID=
|
||||
|
|
|
@ -11,7 +11,7 @@ $ npm install -g @tooljet/cli
|
|||
$ tooljet COMMAND
|
||||
running command...
|
||||
$ tooljet (--version)
|
||||
@tooljet/cli/0.0.12 darwin-arm64 node-v14.17.3
|
||||
@tooljet/cli/0.0.13 darwin-x64 node-v14.17.3
|
||||
$ tooljet --help [COMMAND]
|
||||
USAGE
|
||||
$ tooljet COMMAND
|
||||
|
@ -40,20 +40,23 @@ DESCRIPTION
|
|||
This command returns the information about where tooljet is being run
|
||||
```
|
||||
|
||||
_See code: [dist/commands/info.ts](https://github.com/tooljet/tooljet/blob/v0.0.13/dist/commands/info.ts)_
|
||||
|
||||
## `tooljet plugin create PLUGIN_NAME`
|
||||
|
||||
Creates a new tooljet plugin
|
||||
Create a new tooljet plugin
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ tooljet plugin create [PLUGIN_NAME] [--type database|api|cloud-storage] [-b]
|
||||
$ tooljet plugin create [PLUGIN_NAME] [--type database|api|cloud-storage] [-b] [-m]
|
||||
|
||||
ARGUMENTS
|
||||
PLUGIN_NAME Name of the plugin
|
||||
|
||||
FLAGS
|
||||
-b, --build
|
||||
--type=<option> <options: database|api|cloud-storage>
|
||||
-m, --marketplace
|
||||
--type=<option> <options: database|api|cloud-storage>
|
||||
|
||||
DESCRIPTION
|
||||
Create a new tooljet plugin
|
||||
|
@ -64,7 +67,7 @@ EXAMPLES
|
|||
|
||||
## `tooljet plugin delete PLUGIN_NAME`
|
||||
|
||||
Deletes a tooljet plugin
|
||||
Delete a tooljet plugin
|
||||
|
||||
```
|
||||
USAGE
|
||||
|
@ -77,7 +80,7 @@ FLAGS
|
|||
-b, --build
|
||||
|
||||
DESCRIPTION
|
||||
Deletes a tooljet plugin
|
||||
Delete a tooljet plugin
|
||||
|
||||
EXAMPLES
|
||||
$ tooljet plugin delete <name> [--build]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@tooljet/cli",
|
||||
"description": "tooljet cli tool",
|
||||
"version": "0.0.12",
|
||||
"version": "0.0.13",
|
||||
"bin": {
|
||||
"tooljet": "./bin/run"
|
||||
},
|
||||
|
|
|
@ -11,6 +11,7 @@ export default class Create extends Command {
|
|||
static flags = {
|
||||
type: Flags.string({ options: ['database', 'api', 'cloud-storage'] }),
|
||||
build: Flags.boolean({ char: 'b' }),
|
||||
marketplace: Flags.boolean({ char: 'm' }),
|
||||
};
|
||||
static description = 'Create a new tooljet plugin';
|
||||
|
||||
|
@ -26,7 +27,7 @@ export default class Create extends Command {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
let { type } = flags;
|
||||
let { type, marketplace } = flags;
|
||||
|
||||
const name = await CliUx.ux.prompt('Enter plugin display name');
|
||||
|
||||
|
@ -47,18 +48,47 @@ export default class Create extends Command {
|
|||
type = responses.type;
|
||||
}
|
||||
|
||||
const pluginsPath = 'plugins';
|
||||
if (!marketplace) {
|
||||
const responses: any = await inquirer.prompt([
|
||||
{
|
||||
name: 'marketplace',
|
||||
message: 'is it a marketplace integration?',
|
||||
type: 'confirm',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
marketplace = responses.marketplace;
|
||||
}
|
||||
|
||||
const pluginsPath = marketplace ? 'marketplace' : 'plugins';
|
||||
const docsPath = 'docs';
|
||||
const defaultTemplates = path.join('plugins', '_templates');
|
||||
const defaultTemplates = path.join(pluginsPath, '_templates');
|
||||
|
||||
if (!(fs.existsSync(pluginsPath) && fs.existsSync(docsPath) && fs.existsSync(defaultTemplates))) {
|
||||
this.log(
|
||||
'\x1b[41m%s\x1b[0m',
|
||||
'Error : plugins, docs or plugins/_templates directory missing, make sure that you are running this command in Tooljet directory'
|
||||
`Error : ${pluginsPath}, docs or ${pluginsPath}/_templates directory missing, make sure that you are runing this command in Tooljet directory`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let repoUrl;
|
||||
|
||||
if (marketplace) {
|
||||
const buffer = fs.readFileSync(path.join('server', 'src', 'assets', 'marketplace', 'plugins.json'), 'utf8');
|
||||
const pluginsJson = JSON.parse(buffer);
|
||||
pluginsJson.map((plugin: any) => {
|
||||
if (plugin.id === args.plugin_name.toLowerCase()) {
|
||||
this.log('\x1b[41m%s\x1b[0m', 'Error : Plugin id already exists');
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
repoUrl = await CliUx.ux.prompt('Please enter the repository URL if hosted on GitHub', {
|
||||
required: false,
|
||||
});
|
||||
}
|
||||
|
||||
const hygenArgs = [
|
||||
'plugin',
|
||||
'new',
|
||||
|
@ -76,7 +106,7 @@ export default class Create extends Command {
|
|||
|
||||
CliUx.ux.action.start('creating plugin');
|
||||
|
||||
runner(hygenArgs, {
|
||||
await runner(hygenArgs, {
|
||||
templates: defaultTemplates,
|
||||
cwd: process.cwd(),
|
||||
logger: new Logger(console.log.bind(console)),
|
||||
|
@ -88,23 +118,49 @@ export default class Create extends Command {
|
|||
debug: !!process.env.DEBUG,
|
||||
});
|
||||
|
||||
await execa('npx', ['lerna', 'link', 'convert'], { cwd: pluginsPath });
|
||||
CliUx.ux.action.stop();
|
||||
if (marketplace) {
|
||||
await execa('npm', ['i'], { cwd: pluginsPath });
|
||||
|
||||
if (flags.build) {
|
||||
CliUx.ux.action.start('building plugins');
|
||||
await execa.command('npm run build:plugins', { cwd: process.cwd() });
|
||||
CliUx.ux.action.stop();
|
||||
const buffer = fs.readFileSync(path.join('server', 'src', 'assets', 'marketplace', 'plugins.json'), 'utf8');
|
||||
const pluginsJson = JSON.parse(buffer);
|
||||
const plugin = {
|
||||
name: args.plugin_name,
|
||||
repo: repoUrl || '',
|
||||
description: `${type} plugin from ${args.plugin_name}`,
|
||||
version: '1.0.0',
|
||||
id: `${args.plugin_name.toLowerCase()}`,
|
||||
author: 'Tooljet',
|
||||
timestamp: new Date().toUTCString(),
|
||||
};
|
||||
|
||||
pluginsJson.push(plugin);
|
||||
|
||||
const jsonString = JSON.stringify(pluginsJson, null, 2);
|
||||
fs.writeFileSync(path.join('server', 'src', 'assets', 'marketplace', 'plugins.json'), jsonString);
|
||||
} else {
|
||||
await execa('npx', ['lerna', 'link', 'convert'], { cwd: pluginsPath });
|
||||
}
|
||||
|
||||
CliUx.ux.action.stop();
|
||||
|
||||
this.log('\x1b[42m', '\x1b[30m', `Plugin: ${args.plugin_name} created successfully`, '\x1b[0m');
|
||||
|
||||
if (flags.build) {
|
||||
CliUx.ux.action.start('building plugins');
|
||||
if (marketplace) {
|
||||
await execa('npm', ['run', 'build', '--workspaces'], { cwd: pluginsPath });
|
||||
} else {
|
||||
await execa.command('npm run build:plugins', { cwd: process.cwd() });
|
||||
}
|
||||
CliUx.ux.action.stop();
|
||||
}
|
||||
|
||||
const tree = CliUx.ux.tree();
|
||||
tree.insert('plugins');
|
||||
tree.insert(pluginsPath);
|
||||
|
||||
const subtree = CliUx.ux.tree();
|
||||
subtree.insert(`${args.plugin_name}`);
|
||||
tree.nodes.plugins.insert('packages', subtree);
|
||||
tree.nodes[pluginsPath].insert(marketplace ? 'plugins' : 'packages', subtree);
|
||||
|
||||
tree.display();
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
"axios": "^0.24.0",
|
||||
"bootstrap": "^4.6.0",
|
||||
"classnames": "^2.3.1",
|
||||
"compression-webpack-plugin": "^10.0.0",
|
||||
"css-loader": "^6.5.1",
|
||||
"date-fns": "^2.28.0",
|
||||
"deep-object-diff": "^1.1.7",
|
||||
"dompurify": "^2.2.7",
|
||||
|
|
|
@ -3,7 +3,7 @@ import config from 'config';
|
|||
import { BrowserRouter, Route, Redirect } from 'react-router-dom';
|
||||
import { history } from '@/_helpers';
|
||||
import { authenticationService, tooljetService } from '@/_services';
|
||||
import { PrivateRoute } from '@/_components';
|
||||
import { PrivateRoute, AdminRoute } from '@/_components';
|
||||
import { HomePage } from '@/HomePage';
|
||||
import { LoginPage } from '@/LoginPage';
|
||||
import { SignupPage } from '@/SignupPage';
|
||||
|
@ -18,6 +18,7 @@ import { SettingsPage } from '../SettingsPage/SettingsPage';
|
|||
import { OnboardingModal } from '@/Onboarding/OnboardingModal';
|
||||
import { ForgotPassword } from '@/ForgotPassword';
|
||||
import { ResetPassword } from '@/ResetPassword';
|
||||
import { MarketplacePage } from '@/MarketplacePage';
|
||||
import { ManageSSO } from '@/ManageSSO';
|
||||
import { ManageOrgVars } from '@/ManageOrgVars';
|
||||
import { lt } from 'semver';
|
||||
|
@ -258,6 +259,15 @@ class App extends React.Component {
|
|||
switchDarkMode={this.switchDarkMode}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
{config.ENABLE_MARKETPLACE_FEATURE && (
|
||||
<AdminRoute
|
||||
exact
|
||||
path="/integrations"
|
||||
component={MarketplacePage}
|
||||
switchDarkMode={this.switchDarkMode}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
<Toast toastOptions={toastOptions} />
|
||||
|
|
|
@ -108,7 +108,12 @@ export function CodeBuilder({ initialValue, onChange, components, dataQueries })
|
|||
}
|
||||
|
||||
function renderQueryVariables(query) {
|
||||
const dataSourceMeta = DataSourceTypes.find((source) => query.kind === source.kind);
|
||||
let dataSourceMeta;
|
||||
if (query?.pluginId) {
|
||||
dataSourceMeta = query.manifestFile.data.source;
|
||||
} else {
|
||||
dataSourceMeta = DataSourceTypes.find((source) => query.kind === source.kind);
|
||||
}
|
||||
const exposedVariables = dataSourceMeta.exposedVariables;
|
||||
|
||||
return renderVariables('queries', query.name, Object.keys(exposedVariables));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { datasourceService, authenticationService } from '@/_services';
|
||||
import { datasourceService, authenticationService, pluginsService } from '@/_services';
|
||||
import { Modal, Button, Tab, Row, Col, ListGroup } from 'react-bootstrap';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { getSvgIcon } from '@/_helpers/appUtils';
|
||||
|
@ -8,6 +8,7 @@ import {
|
|||
DataBaseSources,
|
||||
ApiSources,
|
||||
DataSourceTypes,
|
||||
SourceComponent,
|
||||
SourceComponents,
|
||||
CloudStorageSources,
|
||||
} from './SourceComponents';
|
||||
|
@ -28,7 +29,7 @@ class DataSourceManagerComponent extends React.Component {
|
|||
if (props.selectedDataSource) {
|
||||
selectedDataSource = props.selectedDataSource;
|
||||
options = selectedDataSource.options;
|
||||
dataSourceMeta = DataSourceTypes.find((source) => source.kind === selectedDataSource.kind);
|
||||
dataSourceMeta = this.getDataSourceMeta(selectedDataSource);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
|
@ -41,6 +42,7 @@ class DataSourceManagerComponent extends React.Component {
|
|||
isSaving: false,
|
||||
isCopied: false,
|
||||
queryString: null,
|
||||
plugins: [],
|
||||
filteredDatasources: [],
|
||||
activeDatasourceList: '#alldatasources',
|
||||
suggestingDatasources: false,
|
||||
|
@ -51,23 +53,43 @@ class DataSourceManagerComponent extends React.Component {
|
|||
this.setState({
|
||||
appId: this.props.appId,
|
||||
});
|
||||
|
||||
pluginsService
|
||||
.findAll()
|
||||
.then(({ data = [] }) => this.setState({ plugins: data }))
|
||||
.catch((error) => {
|
||||
toast.error(error?.message || 'failed to fetch plugins');
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.selectedDataSource !== this.props.selectedDataSource) {
|
||||
let dataSourceMeta = this.getDataSourceMeta(this.props.selectedDataSource);
|
||||
|
||||
this.setState({
|
||||
selectedDataSource: this.props.selectedDataSource,
|
||||
options: this.props.selectedDataSource?.options,
|
||||
dataSourceMeta: DataSourceTypes.find((source) => source.kind === this.props.selectedDataSource?.kind),
|
||||
dataSourceMeta,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getDataSourceMeta = (dataSource) => {
|
||||
if (dataSource?.pluginId) {
|
||||
return dataSource.manifestFile?.data.source;
|
||||
}
|
||||
|
||||
return DataSourceTypes.find((source) => source.kind === dataSource.kind);
|
||||
};
|
||||
|
||||
selectDataSource = (source) => {
|
||||
this.setState({
|
||||
dataSourceMeta: source,
|
||||
selectedDataSource: source,
|
||||
name: source.kind,
|
||||
dataSourceMeta: source.manifestFile?.data?.source ?? source,
|
||||
selectedDataSource: source.manifestFile?.data?.source ?? source,
|
||||
selectedDataSourceIcon: source.iconFile?.data,
|
||||
name: source.manifestFile?.data?.source?.kind ?? source.kind,
|
||||
dataSourceSchema: source.manifestFile?.data,
|
||||
selectedDataSourcePluginId: source.id,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -114,9 +136,10 @@ class DataSourceManagerComponent extends React.Component {
|
|||
};
|
||||
|
||||
createDataSource = () => {
|
||||
const { appId, options, selectedDataSource } = this.state;
|
||||
const { appId, options, selectedDataSource, selectedDataSourcePluginId } = this.state;
|
||||
const name = selectedDataSource.name;
|
||||
const kind = selectedDataSource.kind;
|
||||
const pluginId = selectedDataSourcePluginId;
|
||||
const appVersionId = this.props.editingVersionId;
|
||||
|
||||
const parsedOptions = Object.keys(options).map((key) => {
|
||||
|
@ -141,7 +164,7 @@ class DataSourceManagerComponent extends React.Component {
|
|||
});
|
||||
} else {
|
||||
this.setState({ isSaving: true });
|
||||
datasourceService.create(appId, appVersionId, name, kind, parsedOptions).then(() => {
|
||||
datasourceService.create(appId, appVersionId, pluginId, name, kind, parsedOptions).then(() => {
|
||||
this.setState({ isSaving: false });
|
||||
this.hideModal();
|
||||
toast.success(
|
||||
|
@ -192,9 +215,10 @@ class DataSourceManagerComponent extends React.Component {
|
|||
const { options, isSaving } = this.state;
|
||||
|
||||
const sourceComponentName = kind.charAt(0).toUpperCase() + kind.slice(1);
|
||||
const ComponentToRender = SourceComponents[sourceComponentName];
|
||||
const ComponentToRender = SourceComponents[sourceComponentName] || SourceComponent;
|
||||
return (
|
||||
<ComponentToRender
|
||||
dataSourceSchema={this.state.dataSourceSchema}
|
||||
optionsChanged={(options = {}) => this.setState({ options })}
|
||||
optionchanged={this.optionchanged}
|
||||
createDataSource={this.createDataSource}
|
||||
|
@ -314,6 +338,7 @@ class DataSourceManagerComponent extends React.Component {
|
|||
databases: DataBaseSources,
|
||||
apis: ApiSources,
|
||||
cloudStorages: CloudStorageSources,
|
||||
plugins: this.state.plugins,
|
||||
filteredDatasources: this.state.filteredDatasources,
|
||||
};
|
||||
const dataSourceList = [
|
||||
|
@ -341,6 +366,12 @@ class DataSourceManagerComponent extends React.Component {
|
|||
list: allDataSourcesList.cloudStorages,
|
||||
renderDatasources: () => this.renderCardGroup(allDataSourcesList.cloudStorages, 'Cloud Storages'),
|
||||
},
|
||||
{
|
||||
type: 'Plugins',
|
||||
key: '#plugins',
|
||||
list: allDataSourcesList.plugins,
|
||||
renderDatasources: () => this.renderCardGroup(allDataSourcesList.plugins, 'Plugins'),
|
||||
},
|
||||
{
|
||||
type: 'Filtered Datasources',
|
||||
key: '#filtereddatasources',
|
||||
|
@ -353,7 +384,7 @@ class DataSourceManagerComponent extends React.Component {
|
|||
};
|
||||
|
||||
renderSidebarList = () => {
|
||||
const dataSourceList = this.datasourcesGroups().splice(0, 4);
|
||||
const dataSourceList = this.datasourcesGroups().splice(0, 5);
|
||||
|
||||
const updateSuggestionState = () => {
|
||||
this.updateSuggestedDatasources();
|
||||
|
@ -391,26 +422,17 @@ class DataSourceManagerComponent extends React.Component {
|
|||
|
||||
if (this.state.queryString && this.state.queryString.length > 0) {
|
||||
const filteredDatasources = this.state.filteredDatasources.map((datasource) => {
|
||||
const src = datasource.iconFile?.data
|
||||
? `data:image/svg+xml;base64,${datasource.iconFile?.data}`
|
||||
: datasource.kind.toLowerCase();
|
||||
|
||||
return {
|
||||
...datasource,
|
||||
src: datasource.kind.toLowerCase(),
|
||||
src,
|
||||
title: datasource.name,
|
||||
};
|
||||
});
|
||||
|
||||
// if (filteredDatasources.length === 0) {
|
||||
// return (
|
||||
// <div className="empty-state-wrapper row">
|
||||
// <EmptyStateContainer
|
||||
// queryString={this.state.queryString}
|
||||
// handleBackToAllDatasources={this.handleBackToAllDatasources}
|
||||
// darkMode={this.props.darkMode}
|
||||
// placeholder={'Tell us what you were looking for?'}
|
||||
// />
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="row row-deck mt-4">
|
||||
|
@ -421,7 +443,7 @@ class DataSourceManagerComponent extends React.Component {
|
|||
title={item.title}
|
||||
src={item.src}
|
||||
handleClick={() => renderSelectedDatasource(item)}
|
||||
usepluginIcon={true}
|
||||
usePluginIcon={isEmpty(item.iconFile?.data)}
|
||||
height="35px"
|
||||
width="35px"
|
||||
/>
|
||||
|
@ -465,7 +487,7 @@ class DataSourceManagerComponent extends React.Component {
|
|||
title={item.title}
|
||||
src={item.src}
|
||||
handleClick={() => renderSelectedDatasource(item)}
|
||||
usepluginIcon={true}
|
||||
usePluginIcon={true}
|
||||
height="35px"
|
||||
width="35px"
|
||||
/>
|
||||
|
@ -481,7 +503,7 @@ class DataSourceManagerComponent extends React.Component {
|
|||
title={item.title}
|
||||
src={item.src}
|
||||
handleClick={() => renderSelectedDatasource(item)}
|
||||
usepluginIcon={true}
|
||||
usePluginIcon={true}
|
||||
height="35px"
|
||||
width="35px"
|
||||
/>
|
||||
|
@ -497,7 +519,7 @@ class DataSourceManagerComponent extends React.Component {
|
|||
title={item.title}
|
||||
src={item.src}
|
||||
handleClick={() => renderSelectedDatasource(item)}
|
||||
usepluginIcon={true}
|
||||
usePluginIcon={true}
|
||||
height="35px"
|
||||
width="35px"
|
||||
/>
|
||||
|
@ -509,9 +531,13 @@ class DataSourceManagerComponent extends React.Component {
|
|||
}
|
||||
|
||||
const datasources = source.map((datasource) => {
|
||||
const src = datasource.iconFile?.data
|
||||
? `data:image/svg+xml;base64,${datasource.iconFile?.data}`
|
||||
: datasource.kind.toLowerCase();
|
||||
|
||||
return {
|
||||
...datasource,
|
||||
src: datasource.kind.toLowerCase(),
|
||||
src,
|
||||
title: datasource.name,
|
||||
};
|
||||
});
|
||||
|
@ -524,9 +550,9 @@ class DataSourceManagerComponent extends React.Component {
|
|||
<Card
|
||||
key={item.key}
|
||||
title={item.title}
|
||||
src={item.src}
|
||||
src={item?.src}
|
||||
handleClick={() => renderSelectedDatasource(item)}
|
||||
usepluginIcon={true}
|
||||
usePluginIcon={isEmpty(item.iconFile?.data)}
|
||||
height="35px"
|
||||
width="35px"
|
||||
/>
|
||||
|
@ -537,8 +563,15 @@ class DataSourceManagerComponent extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { dataSourceMeta, selectedDataSource, options, isSaving, connectionTestError, isCopied } = this.state;
|
||||
|
||||
const {
|
||||
dataSourceMeta,
|
||||
selectedDataSource,
|
||||
selectedDataSourceIcon,
|
||||
options,
|
||||
isSaving,
|
||||
connectionTestError,
|
||||
isCopied,
|
||||
} = this.state;
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
|
@ -563,7 +596,7 @@ class DataSourceManagerComponent extends React.Component {
|
|||
<Modal.Title>
|
||||
{selectedDataSource && (
|
||||
<div className="row">
|
||||
{getSvgIcon(dataSourceMeta.kind.toLowerCase(), 35, 35)}
|
||||
{getSvgIcon(dataSourceMeta.kind?.toLowerCase(), 35, 35, selectedDataSourceIcon)}
|
||||
<div className="input-icon" style={{ width: '160px' }}>
|
||||
<input
|
||||
type="text"
|
||||
|
@ -685,6 +718,7 @@ class DataSourceManagerComponent extends React.Component {
|
|||
<div className="col-auto">
|
||||
<TestConnection
|
||||
kind={selectedDataSource.kind}
|
||||
pluginId={selectedDataSource?.pluginId}
|
||||
options={options}
|
||||
onConnectionTestFailed={this.onConnectionTestFailed}
|
||||
darkMode={this.props.darkMode}
|
||||
|
@ -827,6 +861,7 @@ const SearchBoxContainer = ({ onChange, onClear, queryString, activeDatasourceLi
|
|||
if (queryString === null) {
|
||||
setSearchText('');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [queryString]);
|
||||
React.useEffect(() => {
|
||||
if (searchText === '') {
|
||||
|
|
|
@ -25,3 +25,5 @@ export const SourceComponents = Object.keys(allManifests).reduce((accumulator, c
|
|||
accumulator[currentValue] = (props) => <DynamicForm schema={allManifests[currentValue]} {...props} />;
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
export const SourceComponent = (props) => <DynamicForm schema={props.dataSourceSchema} {...props} />;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { toast } from 'react-hot-toast';
|
|||
import { datasourceService } from '@/_services';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const TestConnection = ({ kind, options, onConnectionTestFailed, darkMode }) => {
|
||||
export const TestConnection = ({ kind, options, pluginId, onConnectionTestFailed, darkMode }) => {
|
||||
const [isTesting, setTestingStatus] = useState(false);
|
||||
const [connectionStatus, setConnectionStatus] = useState('unknown');
|
||||
const [buttonText, setButtonText] = useState('Test Connection');
|
||||
|
@ -26,7 +26,7 @@ export const TestConnection = ({ kind, options, onConnectionTestFailed, darkMode
|
|||
function testDataSource() {
|
||||
setTestingStatus(true);
|
||||
|
||||
datasourceService.test(kind, options).then(
|
||||
datasourceService.test(kind, options, pluginId).then(
|
||||
(data) => {
|
||||
setTestingStatus(false);
|
||||
if (data.status === 'ok') {
|
||||
|
|
|
@ -46,7 +46,6 @@ import queryString from 'query-string';
|
|||
import toast from 'react-hot-toast';
|
||||
import produce, { enablePatches, setAutoFreeze, applyPatches } from 'immer';
|
||||
import Logo from './Icons/logo.svg';
|
||||
import RunjsIcon from './Icons/runjs.svg';
|
||||
import EditIcon from './Icons/edit.svg';
|
||||
import MobileSelectedIcon from './Icons/mobile-selected.svg';
|
||||
import DesktopSelectedIcon from './Icons/desktop-selected.svg';
|
||||
|
@ -293,11 +292,19 @@ class EditorComponent extends React.Component {
|
|||
() => {
|
||||
let queryState = {};
|
||||
data.data_queries.forEach((query) => {
|
||||
queryState[query.name] = {
|
||||
...DataSourceTypes.find((source) => source.kind === query.kind).exposedVariables,
|
||||
kind: DataSourceTypes.find((source) => source.kind === query.kind).kind,
|
||||
...this.state.currentState.queries[query.name],
|
||||
};
|
||||
if (query.plugin_id) {
|
||||
queryState[query.name] = {
|
||||
...query.plugin.manifest_file.data.source.exposedVariables,
|
||||
kind: query.plugin.manifest_file.data.source.kind,
|
||||
...this.state.currentState.queries[query.name],
|
||||
};
|
||||
} else {
|
||||
queryState[query.name] = {
|
||||
...DataSourceTypes.find((source) => source.kind === query.kind).exposedVariables,
|
||||
kind: DataSourceTypes.find((source) => source.kind === query.kind).kind,
|
||||
...this.state.currentState.queries[query.name],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Select first query by default
|
||||
|
@ -741,8 +748,18 @@ class EditorComponent extends React.Component {
|
|||
this.saveApp(id, { name }, notify);
|
||||
};
|
||||
|
||||
getSourceMetaData = (dataSource) => {
|
||||
if (dataSource.plugin_id) {
|
||||
return dataSource.plugin?.manifest_file?.data.source;
|
||||
}
|
||||
|
||||
return DataSourceTypes.find((source) => source.kind === dataSource.kind);
|
||||
};
|
||||
|
||||
renderDataSource = (dataSource) => {
|
||||
const sourceMeta = DataSourceTypes.find((source) => source.kind === dataSource.kind);
|
||||
const sourceMeta = this.getSourceMetaData(dataSource);
|
||||
const icon = getSvgIcon(sourceMeta.kind.toLowerCase(), 25, 25, dataSource?.plugin?.icon_file?.data);
|
||||
|
||||
return (
|
||||
<tr
|
||||
role="button"
|
||||
|
@ -755,7 +772,7 @@ class EditorComponent extends React.Component {
|
|||
}}
|
||||
>
|
||||
<td>
|
||||
{getSvgIcon(sourceMeta.kind.toLowerCase(), 25, 25)} {dataSource.name}
|
||||
{icon} {dataSource.name}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
@ -792,7 +809,8 @@ class EditorComponent extends React.Component {
|
|||
};
|
||||
|
||||
renderDataQuery = (dataQuery) => {
|
||||
const sourceMeta = DataSourceTypes.find((source) => source.kind === dataQuery.kind);
|
||||
const sourceMeta = this.getSourceMetaData(dataQuery);
|
||||
const icon = getSvgIcon(sourceMeta.kind.toLowerCase(), 25, 25, dataQuery?.plugin?.icon_file?.data);
|
||||
|
||||
let isSeletedQuery = false;
|
||||
if (this.state.selectedQuery) {
|
||||
|
@ -817,11 +835,7 @@ class EditorComponent extends React.Component {
|
|||
onMouseLeave={() => this.setShowHiddenOptionsForDataQuery(null)}
|
||||
>
|
||||
<div className="col-auto" style={{ width: '28px' }}>
|
||||
{sourceMeta.kind === 'runjs' ? (
|
||||
<RunjsIcon style={{ height: 25, width: 25 }} />
|
||||
) : (
|
||||
getSvgIcon(sourceMeta.kind.toLowerCase(), 25, 25)
|
||||
)}
|
||||
{icon}
|
||||
</div>
|
||||
<div className="col">
|
||||
<OverlayTrigger
|
||||
|
|
|
@ -55,8 +55,18 @@ export const LeftSidebarDataSources = ({
|
|||
setSelectedDataSource(null);
|
||||
};
|
||||
|
||||
const getSourceMetaData = (dataSource) => {
|
||||
if (dataSource.plugin_id) {
|
||||
return dataSource.plugin?.manifest_file?.data.source;
|
||||
}
|
||||
|
||||
return DataSourceTypes.find((source) => source.kind === dataSource.kind);
|
||||
};
|
||||
|
||||
const renderDataSource = (dataSource, idx) => {
|
||||
const sourceMeta = DataSourceTypes.find((source) => source.kind === dataSource.kind);
|
||||
const sourceMeta = getSourceMetaData(dataSource);
|
||||
const icon = getSvgIcon(sourceMeta.kind.toLowerCase(), 25, 25, dataSource?.plugin?.icon_file?.data);
|
||||
|
||||
return (
|
||||
<div className="row py-1" key={idx}>
|
||||
<div
|
||||
|
@ -67,7 +77,7 @@ export const LeftSidebarDataSources = ({
|
|||
}}
|
||||
className="col"
|
||||
>
|
||||
{getSvgIcon(sourceMeta.kind.toLowerCase(), 25, 25)}
|
||||
{icon}
|
||||
<span className="font-500" style={{ paddingLeft: 5 }}>
|
||||
{dataSource.name}
|
||||
</span>
|
||||
|
|
|
@ -19,3 +19,5 @@ export const allSources = {
|
|||
Stripe,
|
||||
Openapi,
|
||||
};
|
||||
|
||||
export const source = (props) => <DynamicForm schema={props.pluginSchema} {...props} />;
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import { dataqueryService } from '@/_services';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { allSources } from './QueryEditors';
|
||||
import { allSources, source } from './QueryEditors';
|
||||
import { Transformation } from './Transformation';
|
||||
import { previewQuery } from '@/_helpers/appUtils';
|
||||
import { EventManager } from '../Inspector/EventManager';
|
||||
|
@ -64,8 +64,13 @@ class QueryManagerComponent extends React.Component {
|
|||
const selectedQuery = props.selectedQuery;
|
||||
const dataSourceId = selectedQuery?.data_source_id;
|
||||
const source = props.dataSources.find((datasource) => datasource.id === dataSourceId);
|
||||
let dataSourceMeta = DataSourceTypes.find((source) => source.kind === selectedQuery?.kind);
|
||||
const paneHeightChanged = this.state.queryPanelHeight !== props.queryPanelHeight;
|
||||
let dataSourceMeta;
|
||||
if (selectedQuery?.pluginId) {
|
||||
dataSourceMeta = selectedQuery.manifestFile.data.source;
|
||||
} else {
|
||||
dataSourceMeta = DataSourceTypes.find((source) => source.kind === selectedQuery?.kind);
|
||||
}
|
||||
const paneHeightChanged = this.state.queryPaneHeight !== props.queryPaneHeight;
|
||||
const dataQueries = props.dataQueries?.length ? props.dataQueries : this.state.dataQueries;
|
||||
const queryPaneDragged = this.state.isQueryPaneDragging !== props.isQueryPaneDragging;
|
||||
this.setState(
|
||||
|
@ -287,6 +292,7 @@ class QueryManagerComponent extends React.Component {
|
|||
const appVersionId = this.props.editingVersionId;
|
||||
const kind = selectedDataSource.kind;
|
||||
const dataSourceId = selectedDataSource.id === 'null' ? null : selectedDataSource.id;
|
||||
const pluginId = selectedDataSource.plugin_id;
|
||||
|
||||
const isQueryNameValid = this.validateQueryName();
|
||||
if (!isQueryNameValid) {
|
||||
|
@ -316,7 +322,7 @@ class QueryManagerComponent extends React.Component {
|
|||
} else {
|
||||
this.setState({ isCreating: true });
|
||||
dataqueryService
|
||||
.create(appId, appVersionId, queryName, kind, options, dataSourceId)
|
||||
.create(appId, appVersionId, queryName, kind, options, dataSourceId, pluginId)
|
||||
.then((data) => {
|
||||
toast.success('Query Added');
|
||||
this.setState({
|
||||
|
@ -433,7 +439,7 @@ class QueryManagerComponent extends React.Component {
|
|||
|
||||
if (selectedDataSource) {
|
||||
const sourcecomponentName = selectedDataSource.kind.charAt(0).toUpperCase() + selectedDataSource.kind.slice(1);
|
||||
ElementToRender = allSources[sourcecomponentName];
|
||||
ElementToRender = allSources[sourcecomponentName] || source;
|
||||
}
|
||||
|
||||
let dropDownButtonText = mode === 'edit' ? 'Save' : 'Create';
|
||||
|
@ -502,6 +508,7 @@ class QueryManagerComponent extends React.Component {
|
|||
|
||||
const query = {
|
||||
data_source_id: selectedDataSource.id === 'null' ? null : selectedDataSource.id,
|
||||
pluginId: selectedDataSource.plugin_id,
|
||||
options: _options,
|
||||
kind: selectedDataSource.kind,
|
||||
};
|
||||
|
@ -643,6 +650,7 @@ class QueryManagerComponent extends React.Component {
|
|||
{selectedDataSource && (
|
||||
<div>
|
||||
<ElementToRender
|
||||
pluginSchema={this.state.selectedDataSource?.plugin?.operations_file?.data}
|
||||
selectedDataSource={selectedDataSource}
|
||||
options={this.state.options}
|
||||
optionsChanged={this.optionsChanged}
|
||||
|
|
|
@ -83,10 +83,17 @@ class ViewerComponent extends React.Component {
|
|||
|
||||
let queryState = {};
|
||||
data.data_queries.forEach((query) => {
|
||||
queryState[query.name] = {
|
||||
...DataSourceTypes.find((source) => source.kind === query.kind).exposedVariables,
|
||||
...this.state.currentState.queries[query.name],
|
||||
};
|
||||
if (query.plugin_id) {
|
||||
queryState[query.name] = {
|
||||
...query.plugin.manifest_file.data.source.exposedVariables,
|
||||
...this.state.currentState.queries[query.name],
|
||||
};
|
||||
} else {
|
||||
queryState[query.name] = {
|
||||
...DataSourceTypes.find((source) => source.kind === query.kind).exposedVariables,
|
||||
...this.state.currentState.queries[query.name],
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const variables = await this.fetchOrgEnvironmentVariables(data.slug, data.is_public);
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
import React from 'react';
|
||||
import { pluginsService } from '@/_services';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import Spinner from '@/_ui/Spinner';
|
||||
|
||||
export const InstalledPlugins = ({ isActive, darkMode }) => {
|
||||
const [plugins, setPlugins] = React.useState([]);
|
||||
const [fetching, setFetching] = React.useState(false);
|
||||
|
||||
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 = (id) => {
|
||||
const { error } = pluginsService.deletePlugin(id);
|
||||
if (error) {
|
||||
toast.error(error?.message || 'unable to delete plugin');
|
||||
return;
|
||||
}
|
||||
fetchPlugins();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="col-9">
|
||||
{fetching && (
|
||||
<div className="m-auto text-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
{!fetching && (
|
||||
<div className="row row-cards">
|
||||
{plugins?.map((plugin) => (
|
||||
<div key={plugin.id} className="col-sm-6 col-lg-4">
|
||||
<div className="card card-sm card-borderless">
|
||||
<div className="card-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<span className="text-white avatar">
|
||||
<img height="32" width="32" src={`data:image/svg+xml;base64,${plugin.iconFile.data}`} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="font-weight-medium text-capitalize">{plugin.name}</div>
|
||||
<div className="text-muted">{plugin.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<sub>v{plugin.version}</sub>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<div className="cursor-pointer link-primary" onClick={() => deletePlugin(plugin.id)}>
|
||||
Remove
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!fetching && plugins?.length === 0 && (
|
||||
<div className="empty">
|
||||
<p className="empty-title">No results found</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
|
||||
export const ListGroupItem = ({ active, handleClick, text }) => {
|
||||
return (
|
||||
<div
|
||||
className={cx('list-group-item list-group-item-action d-flex align-items-center cursor-pointer', {
|
||||
active,
|
||||
})}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,77 @@
|
|||
import React from 'react';
|
||||
import config from 'config';
|
||||
import cx from 'classnames';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { pluginsService } from '@/_services';
|
||||
|
||||
export const MarketplaceCard = ({ id, name, repo, description, version, isInstalled = false }) => {
|
||||
const [installed, setInstalled] = React.useState(isInstalled);
|
||||
const [installing, setInstalling] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setInstalled(isInstalled);
|
||||
}, [isInstalled]);
|
||||
|
||||
const installPlugin = async () => {
|
||||
const body = {
|
||||
id,
|
||||
name,
|
||||
repo,
|
||||
description,
|
||||
version,
|
||||
};
|
||||
|
||||
setInstalling(true);
|
||||
const { error } = await pluginsService.installPlugin(body);
|
||||
setInstalling(false);
|
||||
|
||||
if (error) {
|
||||
toast.error(error?.message || `Unable to install ${name}`);
|
||||
return;
|
||||
}
|
||||
setInstalled(true);
|
||||
};
|
||||
|
||||
let iconSrc;
|
||||
|
||||
if (repo) {
|
||||
iconSrc = `https://raw.githubusercontent.com/${repo}/main/lib/icon.svg`;
|
||||
} else {
|
||||
iconSrc = `${config.TOOLJET_MARKETPLACE_URL}/marketplace-assets/${id}/lib/icon.svg`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="col-sm-6 col-lg-4">
|
||||
<div className="card card-sm card-borderless">
|
||||
<div className="card-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<span className="text-white avatar">
|
||||
<img height="40" width="40" src={iconSrc} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="font-weight-medium text-capitalize">{name}</div>
|
||||
<div className="text-muted">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<sub>v{version}</sub>
|
||||
</div>
|
||||
<div className={cx('col-auto', { disabled: installing || installed })} onClick={installPlugin}>
|
||||
<div className="link-primary cursor-pointer">Install{installed && 'ed'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{installing && (
|
||||
<div className="progress progress-sm">
|
||||
<div className="progress-bar progress-bar-indeterminate"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
import React from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { MarketplaceCard } from './MarketplaceCard';
|
||||
import { marketplaceService, pluginsService } from '@/_services';
|
||||
|
||||
export const MarketplacePlugins = ({ isActive }) => {
|
||||
const [plugins, setPlugins] = React.useState([]);
|
||||
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
|
||||
.findAll()
|
||||
.then(({ data = [] }) => {
|
||||
const installedPlugins = data.reduce((acc, { pluginId }) => {
|
||||
acc[pluginId] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
setInstalledPlugins(installedPlugins);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error?.message || 'something went wrong');
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="col-9">
|
||||
<div className="row row-cards">
|
||||
{plugins?.map(({ id, name, repo, version, description }) => {
|
||||
return (
|
||||
<MarketplaceCard
|
||||
key={id}
|
||||
id={id}
|
||||
isInstalled={installedPlugins[id]}
|
||||
name={name}
|
||||
repo={repo}
|
||||
version={version}
|
||||
description={description}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
import React from 'react';
|
||||
import { Header } from '@/_components';
|
||||
import { ListGroupItem } from './ListGroupItem';
|
||||
import { InstalledPlugins } from './InstalledPlugins';
|
||||
import { MarketplacePlugins } from './MarketplacePlugins';
|
||||
|
||||
const MarketplacePage = ({ darkMode, switchDarkMode }) => {
|
||||
const [active, setActive] = React.useState('installed');
|
||||
|
||||
return (
|
||||
<div className="wrapper">
|
||||
<Header switchDarkMode={switchDarkMode} darkMode={darkMode} />
|
||||
<div className="page-wrapper">
|
||||
<div className="container-xl">
|
||||
<div className="page-header d-print-none">
|
||||
<div className="row g-2 align-items-center">
|
||||
<div className="col">
|
||||
<h2 className="page-title">Marketplace</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-body">
|
||||
<div className="container-xl">
|
||||
<div className="row g-4">
|
||||
<div className="col-3">
|
||||
<div className="subheader mb-2">Plugins</div>
|
||||
<div className="list-group list-group-transparent mb-3">
|
||||
<ListGroupItem
|
||||
active={active === 'installed'}
|
||||
handleClick={() => setActive('installed')}
|
||||
text="Installed"
|
||||
/>
|
||||
<ListGroupItem
|
||||
active={active === 'marketplace'}
|
||||
handleClick={() => setActive('marketplace')}
|
||||
text="Marketplace"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{active === 'installed' ? (
|
||||
<InstalledPlugins isActive={active === 'installed'} darkMode={darkMode} />
|
||||
) : (
|
||||
<MarketplacePlugins isActive={active === 'marketplace'} darkMode={darkMode} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { MarketplacePage };
|
|
@ -26,3 +26,37 @@ export const PrivateRoute = ({ component: Component, switchDarkMode, darkMode, .
|
|||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const AdminRoute = ({ component: Component, switchDarkMode, darkMode, ...rest }) => (
|
||||
<Route
|
||||
{...rest}
|
||||
render={(props) => {
|
||||
const currentUser = authenticationService.currentUserValue;
|
||||
if (!currentUser && !props.location.pathname.startsWith('/applications/')) {
|
||||
return (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: '/login',
|
||||
search: `?redirectTo=${props.location.pathname}`,
|
||||
state: { from: props.location },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentUser?.admin) {
|
||||
return (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: '/',
|
||||
search: `?redirectTo=${props.location.pathname}`,
|
||||
state: { from: props.location },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Component {...props} switchDarkMode={switchDarkMode} darkMode={darkMode} />;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -14,6 +14,7 @@ import Tooltip from 'react-bootstrap/Tooltip';
|
|||
import { componentTypes } from '@/Editor/WidgetManager/components';
|
||||
import generateCSV from '@/_lib/generate-csv';
|
||||
import generateFile from '@/_lib/generate-file';
|
||||
import RunjsIcon from '@/Editor/Icons/runjs.svg';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { allSvgs } from '@tooljet/plugins/client';
|
||||
|
@ -985,7 +986,9 @@ export function computeComponentState(_ref, components = {}) {
|
|||
});
|
||||
}
|
||||
|
||||
export const getSvgIcon = (key, height = 50, width = 50) => {
|
||||
export const getSvgIcon = (key, height = 50, width = 50, iconFile) => {
|
||||
if (iconFile) return <img src={`data:image/svg+xml;base64,${iconFile}`} style={{ height, width }} />;
|
||||
if (key === 'runjs') return <RunjsIcon style={{ height, width }} />;
|
||||
const Icon = allSvgs[key];
|
||||
|
||||
return <Icon style={{ height, width }} />;
|
||||
|
|
|
@ -19,7 +19,7 @@ function getAll(appId, appVersionId) {
|
|||
return fetch(`${config.apiUrl}/data_queries?` + searchParams, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function create(app_id, app_version_id, name, kind, options, data_source_id) {
|
||||
function create(app_id, app_version_id, name, kind, options, data_source_id, plugin_id) {
|
||||
const body = {
|
||||
app_id,
|
||||
app_version_id,
|
||||
|
@ -27,6 +27,7 @@ function create(app_id, app_version_id, name, kind, options, data_source_id) {
|
|||
kind,
|
||||
options,
|
||||
data_source_id: kind === 'runjs' ? null : data_source_id,
|
||||
plugin_id,
|
||||
};
|
||||
|
||||
const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
|
||||
|
|
|
@ -20,10 +20,11 @@ function getAll(appId, appVersionId) {
|
|||
return fetch(`${config.apiUrl}/data_sources?` + searchParams, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function create(app_id, app_version_id, name, kind, options) {
|
||||
function create(app_id, app_version_id, plugin_id, name, kind, options) {
|
||||
const body = {
|
||||
app_id,
|
||||
app_version_id,
|
||||
plugin_id,
|
||||
name,
|
||||
kind,
|
||||
options,
|
||||
|
@ -49,10 +50,11 @@ function deleteDataSource(id) {
|
|||
return fetch(`${config.apiUrl}/data_sources/${id}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function test(kind, options) {
|
||||
function test(kind, options, plugin_id) {
|
||||
const body = {
|
||||
kind,
|
||||
options,
|
||||
plugin_id,
|
||||
};
|
||||
|
||||
const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
|
||||
|
|
|
@ -13,3 +13,5 @@ export * from './tooljet.service';
|
|||
export * from './comments.service';
|
||||
export * from './commentNotifications.service';
|
||||
export * from './library-app.service';
|
||||
export * from './plugins.service';
|
||||
export * from './marketplace.service';
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import HttpClient from '@/_helpers/http-client';
|
||||
import config from 'config';
|
||||
|
||||
const adapter = new HttpClient({ host: config.apiUrl.replace('/api', '') });
|
||||
|
||||
function findAll() {
|
||||
return adapter.get(`/assets/marketplace/plugins.json`);
|
||||
}
|
||||
|
||||
export const marketplaceService = {
|
||||
findAll,
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
import HttpClient from '@/_helpers/http-client';
|
||||
|
||||
const adapter = new HttpClient();
|
||||
|
||||
function findAll() {
|
||||
return adapter.get(`/plugins`);
|
||||
}
|
||||
|
||||
function installPlugin(body) {
|
||||
return adapter.post(`/plugins/install`, body);
|
||||
}
|
||||
|
||||
function deletePlugin(id) {
|
||||
return adapter.delete(`/plugins/${id}`);
|
||||
}
|
||||
|
||||
export const pluginsService = {
|
||||
findAll,
|
||||
installPlugin,
|
||||
deletePlugin,
|
||||
};
|
|
@ -2,14 +2,14 @@ import React from 'react';
|
|||
// eslint-disable-next-line import/no-unresolved
|
||||
import { allSvgs } from '@tooljet/plugins/client';
|
||||
|
||||
const Card = ({ title, src, handleClick, height = 50, width = 50, usepluginIcon = false }) => {
|
||||
const Card = ({ title, src, handleClick, height = 50, width = 50, usePluginIcon = false }) => {
|
||||
const displayIcon = (src) => {
|
||||
if (usepluginIcon) {
|
||||
if (usePluginIcon) {
|
||||
const Icon = allSvgs[src];
|
||||
return <Icon style={{ height, width }} />;
|
||||
}
|
||||
|
||||
return <img src={src} width="50" height="50" alt={title} />;
|
||||
return <img src={src} width={width} height={height} alt={title} />;
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -158,7 +158,10 @@ module.exports = {
|
|||
apiUrl: `${stripTrailingSlash(API_URL[environment]) || ''}/api`,
|
||||
SERVER_IP: process.env.SERVER_IP,
|
||||
COMMENT_FEATURE_ENABLE: process.env.COMMENT_FEATURE_ENABLE ?? true,
|
||||
ENABLE_MARKETPLACE_FEATURE: process.env.ENABLE_MARKETPLACE_FEATURE ?? false,
|
||||
ENABLE_MULTIPLAYER_EDITING: true,
|
||||
TOOLJET_MARKETPLACE_URL:
|
||||
process.env.TOOLJET_MARKETPLACE_URL || 'https://tooljet-plugins-production.s3.us-east-2.amazonaws.com',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
node_modules
|
||||
lerna-debug.log
|
||||
npm-debug.log
|
||||
packages/*/dist
|
||||
packages/*/coverage
|
||||
.vscode/
|
||||
pip-wheel-metadata/
|
||||
tsconfig.tsbuildinfo
|
||||
.DS_Store
|
||||
.idea
|
||||
.vs
|
||||
dist
|
||||
.vscode
|
||||
*.tsbuildinfo
|
||||
*.tabl.json
|
||||
*.swp
|
||||
*.snk
|
||||
client.js
|
||||
server.ts
|
|
@ -0,0 +1,20 @@
|
|||
# Tooljet marketplace
|
||||
|
||||
## Steps to install npm package to a plugin
|
||||
|
||||
```bash
|
||||
npm i <npm-package-name> --workspace=<plugin-name-in-package-json>
|
||||
```
|
||||
|
||||
## Steps to build
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build --workspaces
|
||||
```
|
||||
|
||||
## Update the plugins to S3 bucket
|
||||
|
||||
```bash
|
||||
AWS_ACCESS_KEY_ID=<key> SECRET_ACCESS_KEY=<secret> AWS_BUCKET=<bucket> node scripts/upload-to-s3.js
|
||||
```
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
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
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
to: <%= plugins_path %>/plugins/<%= name %>/.gitignore
|
||||
---
|
||||
node_modules
|
||||
lib/*.d.*
|
||||
lib/*.js
|
||||
lib/*.js.map
|
||||
dist/*
|
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
to: <%= plugins_path %>/plugins/<%= name %>/lib/icon.svg
|
||||
---
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<rect x="445.176" y="222.066" width="15.208" height="32.606"/>
|
||||
<path d="M465.275,103.536V55.237c0-10.89-8.859-19.75-19.749-19.75H292.721c-10.89,0-19.75,8.86-19.75,19.75v48.298h-38.498
|
||||
V55.237c0-10.89-8.86-19.75-19.75-19.75H61.918c-10.891,0-19.75,8.86-19.75,19.75v48.298H0v372.977h512V103.536H465.275z
|
||||
M288.178,55.237c0-2.505,2.038-4.542,4.542-4.542h152.805c2.504,0,4.541,2.038,4.541,4.542v48.298H422.47V64.268h-15.208v39.268
|
||||
H288.178V55.237z M57.374,55.237c0-2.505,2.038-4.542,4.542-4.542h152.804c2.504,0,4.542,2.038,4.542,4.542v48.298h-29.126
|
||||
V64.268h-15.208v39.268H57.374V55.237z M496.792,461.305h-36.404V269.298H445.18v192.007H15.208V118.743h26.959h192.305h38.498
|
||||
h192.305h31.517V461.305z"/>
|
||||
<path d="M385.777,275.757c-1.028-0.078-2.067-0.118-3.092-0.118c-10.329,0-20.117,3.942-27.562,11.096
|
||||
c-0.054,0.051-0.105,0.101-0.159,0.15v-81.028h-81.027c0.05-0.054,0.099-0.105,0.149-0.158
|
||||
c7.838-8.156,11.84-19.33,10.978-30.656c-1.449-19.026-16.24-34.353-35.171-36.443c-1.494-0.164-3.008-0.248-4.5-0.248
|
||||
c-21.939,0-39.787,17.848-39.787,39.788c0,10.414,3.994,20.254,11.25,27.719h-81.031v99.334h7.604
|
||||
c6.248,0,12.237-2.492,16.431-6.835c5.122-5.303,12.213-8.006,19.621-7.435c11.548,0.878,21.213,10.196,22.481,21.673
|
||||
c0.787,7.121-1.381,13.948-6.105,19.223c-4.661,5.204-11.337,8.189-18.317,8.189c-6.631,0-12.846-2.602-17.502-7.328
|
||||
c-3.186-3.232-7.177-5.463-11.542-6.449c-0.757-0.172-1.534-0.259-2.307-0.259c-5.715,0-10.364,4.639-10.364,10.343v88.681
|
||||
h88.991c5.703,0,10.343-4.639,10.343-10.342c0-6.248-2.492-12.238-6.836-16.432c-5.285-5.103-7.994-12.256-7.435-19.622
|
||||
c0.879-11.548,10.197-21.212,21.675-22.48c0.943-0.105,1.895-0.157,2.831-0.157c13.553,0,24.58,11.027,24.58,24.58
|
||||
c0,6.606-2.585,12.805-7.281,17.456c-4.555,4.514-7.065,10.396-7.065,16.565c0,5.753,4.68,10.433,10.434,10.433h88.9v-81.031
|
||||
c7.465,7.255,17.304,11.25,27.719,11.25c11.299,0,22.105-4.83,29.646-13.251c7.537-8.415,11.142-19.728,9.893-31.037
|
||||
C420.13,291.995,404.803,277.205,385.777,275.757z M401.001,331.817c-4.661,5.205-11.338,8.19-18.317,8.19
|
||||
c-6.457,0-12.557-2.486-17.174-7c-4.843-4.737-11.287-7.344-18.148-7.344h-7.604v84.126h-67.227
|
||||
c0.262-0.342,0.551-0.671,0.868-0.987c7.6-7.528,11.785-17.564,11.785-28.259c0-21.94-17.848-39.788-39.788-39.788
|
||||
c-1.491,0-3.005,0.084-4.5,0.248c-18.931,2.093-33.722,17.419-35.17,36.442c-0.905,11.898,3.482,23.458,12.036,31.718
|
||||
c0.202,0.195,0.391,0.404,0.569,0.625h-67.295v-64.691c7.3,6.542,16.618,10.117,26.507,10.117
|
||||
c11.298,0,22.104-4.83,29.646-13.251c7.537-8.415,11.142-19.728,9.892-31.038c-2.092-18.93-17.419-33.72-36.444-35.168
|
||||
c-1.027-0.078-2.068-0.118-3.092-0.118c-9.906,0-19.212,3.571-26.508,10.116v-64.689l84.126-0.005v-7.604
|
||||
c0-6.856-2.608-13.299-7.344-18.144c-4.515-4.618-7.001-10.716-7.001-17.174c0-13.553,11.026-24.58,24.579-24.58
|
||||
c0.936,0,1.888,0.053,2.831,0.157c11.478,1.267,20.796,10.932,21.675,22.481c0.541,7.117-1.867,13.851-6.78,18.964
|
||||
c-4.831,5.026-7.491,11.525-7.491,18.3v7.604h84.126v84.126h7.604c6.775,0,13.273-2.66,18.301-7.491
|
||||
c5.061-4.863,11.901-7.314,18.963-6.78c11.548,0.878,21.213,10.196,22.481,21.675
|
||||
C407.893,319.717,405.725,326.542,401.001,331.817z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
to: <%= plugins_path %>/plugins/<%= name %>/lib/index.ts
|
||||
---
|
||||
|
||||
import { QueryError, QueryResult, QueryService, ConnectionTestResult } from '@tooljet-marketplace/common';
|
||||
import { SourceOptions, QueryOptions } from './types';
|
||||
|
||||
export default class <%= Name %> implements QueryService {
|
||||
async run(sourceOptions: SourceOptions, queryOptions: QueryOptions, dataSourceId: string): Promise<QueryResult> {
|
||||
return {
|
||||
status: 'ok',
|
||||
data: {},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
to: <%= plugins_path %>/plugins/<%= name %>/lib/manifest.json
|
||||
---
|
||||
<%
|
||||
Display_name = h.capitalize(display_name)
|
||||
%>
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/manifest.schema.json",
|
||||
"title": "<%= Display_name %> datasource",
|
||||
"description": "A schema defining <%= Display_name %> datasource",
|
||||
"type": "<%= type %>",
|
||||
"source": {
|
||||
"name": "<%= Display_name %>",
|
||||
"kind": "<%= name %>",
|
||||
"exposedVariables": {
|
||||
"isLoading": false,
|
||||
"data": {},
|
||||
"rawData": {}
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"defaults": {},
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
to: <%= plugins_path %>/plugins/<%= name %>/lib/operations.json
|
||||
---
|
||||
<%
|
||||
Display_name = h.capitalize(display_name)
|
||||
%>
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/operations.schema.json",
|
||||
"title": "<%= Display_name %> datasource",
|
||||
"description": "A schema defining <%= Display_name %> datasource",
|
||||
"type": "<%= type %>",
|
||||
"defaults": {},
|
||||
"properties": {}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
to: <%= plugins_path %>/plugins/<%= name %>/package.json
|
||||
---
|
||||
{
|
||||
"name": "@tooljet-marketplace/<%= name %>",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"directories": {
|
||||
"lib": "lib",
|
||||
"test": "__tests__"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "echo \"Error: run tests from root\" && exit 1",
|
||||
"build": "ncc build lib/index.ts -o dist"
|
||||
},
|
||||
"homepage": "https://github.com/tooljet/tooljet#readme",
|
||||
"dependencies": {
|
||||
"@tooljet-marketplace/common": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.7.4",
|
||||
"@vercel/ncc": "^0.34.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
to: <%= plugins_path %>/plugins/<%= name %>/README.md
|
||||
---
|
||||
<%
|
||||
Display_name = h.capitalize(display_name)
|
||||
%>
|
||||
# <%= Display_name %>
|
||||
|
||||
Documentation on: https://docs.tooljet.com/docs/data-sources/<%= name %>
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
to: <%= plugins_path %>/plugins/<%= name %>/__tests__/index.js
|
||||
---
|
||||
'use strict';
|
||||
|
||||
const <%= name %> = require('../lib');
|
||||
|
||||
describe('<%= name %>', () => {
|
||||
it.todo('needs tests');
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
to: <%= plugins_path %>/plugins/<%= name %>/tsconfig.json
|
||||
---
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "lib"
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
to: <%= plugins_path %>/plugins/<%= name %>/lib/types.ts
|
||||
---
|
||||
export type SourceOptions = {};
|
||||
export type QueryOptions = {
|
||||
operation: string;
|
||||
};
|
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "tooljet-marketplace",
|
||||
"type": "module",
|
||||
"workspaces": [
|
||||
"plugins/*"
|
||||
],
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"aws-sdk": "^2.1185.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"recursive-readdir": "^2.2.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
lib/*.d.*
|
||||
lib/*.js
|
||||
lib/*.js.map
|
||||
dist/*
|
|
@ -0,0 +1 @@
|
|||
# tooljet-plugin-cassandra
|
File diff suppressed because one or more lines are too long
Po Szerokość: | Wysokość: | Rozmiar: 55 KiB |
|
@ -0,0 +1,61 @@
|
|||
import { QueryError, QueryService } from '@tooljet-marketplace/common';
|
||||
const cassandra = require('cassandra-driver');
|
||||
|
||||
export default class CassandraQueryService implements QueryService {
|
||||
async run(sourceOptions: any, queryOptions: any): Promise<any> {
|
||||
let result = {};
|
||||
const query = queryOptions.query;
|
||||
|
||||
const client = await this.getConnection(sourceOptions);
|
||||
|
||||
try {
|
||||
result = await client.execute(query);
|
||||
} catch (err) {
|
||||
await client.shutdown();
|
||||
throw new QueryError('Query could not be completed', err.message, {});
|
||||
}
|
||||
|
||||
return { status: 'ok', data: result };
|
||||
}
|
||||
|
||||
async testConnection(sourceOptions: any): Promise<any> {
|
||||
const client = await this.getConnection(sourceOptions);
|
||||
await client.execute('');
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
};
|
||||
}
|
||||
|
||||
async getConnection(sourceOptions: any): Promise<any> {
|
||||
const contactPoints = sourceOptions.contactPoints.split(',');
|
||||
const localDataCenter = sourceOptions.localDataCenter;
|
||||
const keyspace = sourceOptions.keyspace;
|
||||
const username = sourceOptions.username;
|
||||
const password = sourceOptions.password;
|
||||
const secureConnectBundle = sourceOptions.secureConnectBundle;
|
||||
|
||||
let client;
|
||||
|
||||
if(secureConnectBundle) {
|
||||
client = new cassandra.Client({
|
||||
cloud: {
|
||||
secureConnectBundle,
|
||||
},
|
||||
credentials: {
|
||||
username,
|
||||
password,
|
||||
}});
|
||||
} else {
|
||||
client = new cassandra.Client({
|
||||
contactPoints,
|
||||
localDataCenter,
|
||||
keyspace
|
||||
});
|
||||
}
|
||||
|
||||
await client.connect();
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/manifest.schema.json",
|
||||
"title": "Cassandra datasource",
|
||||
"description": "A schema defining cassandra datasource",
|
||||
"type": "database",
|
||||
"source": {
|
||||
"name": "Cassandra",
|
||||
"kind": "cassandra",
|
||||
"exposedVariables": {
|
||||
"isLoading": false,
|
||||
"data": {},
|
||||
"rawData": {}
|
||||
},
|
||||
"options": {
|
||||
"localDataCenter": {
|
||||
"type": "string"
|
||||
},
|
||||
"keyspace": {
|
||||
"type": "string"
|
||||
},
|
||||
"contactPoints": {
|
||||
"type": "string"
|
||||
},
|
||||
"secureConnectBundle": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaults": {
|
||||
"localDataCenter": {
|
||||
"value": "datacenter1"
|
||||
},
|
||||
"keyspace": {
|
||||
"value": "ks1"
|
||||
},
|
||||
"contactPoints": {
|
||||
"value": ""
|
||||
},
|
||||
"secureConnectBundle": {
|
||||
"value": ""
|
||||
},
|
||||
"username": {
|
||||
"value": ""
|
||||
},
|
||||
"password": {
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"localDataCenter": {
|
||||
"label": "Local data center",
|
||||
"key": "localDataCenter",
|
||||
"type": "text",
|
||||
"description": "Enter local data center"
|
||||
},
|
||||
"keyspace": {
|
||||
"label": "Keyspace",
|
||||
"key": "keyspace",
|
||||
"type": "text",
|
||||
"description": "Enter keyspace"
|
||||
},
|
||||
"contactPoints": {
|
||||
"label": "Contact points",
|
||||
"key": "contactPoints",
|
||||
"type": "text",
|
||||
"description": "Enter contact points"
|
||||
},
|
||||
"secureConnectBundle": {
|
||||
"label": "Secure connect bundle path",
|
||||
"key": "secureConnectBundle",
|
||||
"type": "text",
|
||||
"description": "Enter secure connect bundle path"
|
||||
},
|
||||
"username": {
|
||||
"label": "Username",
|
||||
"key": "username",
|
||||
"type": "text",
|
||||
"description": "Enter username"
|
||||
},
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"key": "password",
|
||||
"type": "text",
|
||||
"description": "Enter password"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"scheme",
|
||||
"localDataCenter",
|
||||
"keyspace"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/operations.schema.json",
|
||||
"title": "Cassandra data query schema",
|
||||
"description": "A schema defining cassandra data query",
|
||||
"type": "database",
|
||||
"defaults": {
|
||||
"query": ""
|
||||
},
|
||||
"properties": {
|
||||
"query": {
|
||||
"key": "query",
|
||||
"type": "codehinter",
|
||||
"description": "Enter cassandra query",
|
||||
"placeholder": "SELECT name, email FROM users WHERE key = ?",
|
||||
"height": "150px"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"query"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
{
|
||||
"name": "@tooljet-marketplace/cassandra",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@tooljet-marketplace/cassandra",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"cassandra-driver": "^4.6.4",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/long": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
|
||||
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.1.tgz",
|
||||
"integrity": "sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg=="
|
||||
},
|
||||
"node_modules/@vercel/ncc": {
|
||||
"version": "0.34.0",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.34.0.tgz",
|
||||
"integrity": "sha512-G9h5ZLBJ/V57Ou9vz5hI8pda/YQX5HQszCs3AmIus3XzsmRn/0Ptic5otD3xVST8QLKk7AMk7AqpsyQGN7MZ9A==",
|
||||
"bin": {
|
||||
"ncc": "dist/ncc/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/adm-zip": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
|
||||
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg==",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cassandra-driver": {
|
||||
"version": "4.6.4",
|
||||
"resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.6.4.tgz",
|
||||
"integrity": "sha512-SksbIK0cZ2QZRx8ti7w+PnLqldyY+6kU2gRWFChwXFTtrD/ce8cQICDEHxyPwx+DeILwRnMrPf9cjUGizYw9Vg==",
|
||||
"dependencies": {
|
||||
"@types/long": "^4.0.0",
|
||||
"@types/node": ">=8",
|
||||
"adm-zip": "^0.5.3",
|
||||
"long": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz",
|
||||
"integrity": "sha512-ijUtjmO/n2A5PaosNG9ZGDsQ3vxJg7ZW8vsY8Kp0f2yIZWhSJvjmegV7t+9RPQKxKrvj8yKGehhS+po14hPLGQ==",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/long": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
|
||||
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.1.tgz",
|
||||
"integrity": "sha512-z+2vB6yDt1fNwKOeGbckpmirO+VBDuQqecXkgeIqDlaOtmKn6hPR/viQ8cxCfqLU4fTlvM3+YjM367TukWdxpg=="
|
||||
},
|
||||
"@vercel/ncc": {
|
||||
"version": "0.34.0",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.34.0.tgz",
|
||||
"integrity": "sha512-G9h5ZLBJ/V57Ou9vz5hI8pda/YQX5HQszCs3AmIus3XzsmRn/0Ptic5otD3xVST8QLKk7AMk7AqpsyQGN7MZ9A=="
|
||||
},
|
||||
"adm-zip": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.9.tgz",
|
||||
"integrity": "sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg=="
|
||||
},
|
||||
"cassandra-driver": {
|
||||
"version": "4.6.4",
|
||||
"resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.6.4.tgz",
|
||||
"integrity": "sha512-SksbIK0cZ2QZRx8ti7w+PnLqldyY+6kU2gRWFChwXFTtrD/ce8cQICDEHxyPwx+DeILwRnMrPf9cjUGizYw9Vg==",
|
||||
"requires": {
|
||||
"@types/long": "^4.0.0",
|
||||
"@types/node": ">=8",
|
||||
"adm-zip": "^0.5.3",
|
||||
"long": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"long": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz",
|
||||
"integrity": "sha512-ijUtjmO/n2A5PaosNG9ZGDsQ3vxJg7ZW8vsY8Kp0f2yIZWhSJvjmegV7t+9RPQKxKrvj8yKGehhS+po14hPLGQ=="
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ=="
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "@tooljet-marketplace/cassandra",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "ncc build lib/index.ts -o dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tooljet-marketplace/common": "^1.0.0",
|
||||
"cassandra-driver": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "lib"
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
lib/*.d.*
|
||||
lib/*.js
|
||||
lib/*.js.map
|
|
@ -0,0 +1,11 @@
|
|||
# `common`
|
||||
|
||||
> TODO: description
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
const common = require('common');
|
||||
|
||||
// TODO: DEMONSTRATE API
|
||||
```
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
// const common = require('../lib/utils.helper');
|
||||
|
||||
describe('common', () => {
|
||||
it.todo('needs tests');
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
export type ConnectionTestResult = {
|
||||
status: 'ok' | 'failed';
|
||||
message?: string;
|
||||
data?: object;
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
import { QueryError, OAuthUnauthorizedClientError } from './query.error';
|
||||
import { QueryResult } from './query_result.type';
|
||||
import { QueryService } from './query_service.interface';
|
||||
import { cacheConnection, getCachedConnection, parseJson, cleanSensitiveData } from './utils.helper';
|
||||
import { ConnectionTestResult } from './connection_test_result.type';
|
||||
|
||||
export {
|
||||
QueryError,
|
||||
OAuthUnauthorizedClientError,
|
||||
QueryResult,
|
||||
QueryService,
|
||||
cacheConnection,
|
||||
getCachedConnection,
|
||||
parseJson,
|
||||
ConnectionTestResult,
|
||||
cleanSensitiveData,
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
export class QueryError extends Error {
|
||||
data: Record<string, unknown>;
|
||||
description: any;
|
||||
constructor(message: string | undefined, description: any, data: Record<string, unknown>) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
this.data = data;
|
||||
this.description = description;
|
||||
|
||||
console.log(this.description);
|
||||
}
|
||||
}
|
||||
|
||||
export class OAuthUnauthorizedClientError extends Error {
|
||||
data: Record<string, unknown>;
|
||||
description: any;
|
||||
constructor(message: string | undefined, description: any, data: Record<string, unknown>) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
this.data = data;
|
||||
this.description = description;
|
||||
|
||||
console.log(this.description);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export type QueryResult = {
|
||||
status: 'ok' | 'failed' | 'needs_oauth';
|
||||
errorMessage?: string;
|
||||
data: Array<object> | object;
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
import { ConnectionTestResult } from './connection_test_result.type';
|
||||
import { QueryResult } from './query_result.type';
|
||||
|
||||
export interface QueryService {
|
||||
run(
|
||||
sourceOptions: object,
|
||||
queryOptions: object,
|
||||
dataSourceId?: string,
|
||||
dataSourceUpdatedAt?: string
|
||||
): Promise<QueryResult>;
|
||||
getConnection?(queryOptions: object, options: any, checkCache: boolean, dataSourceId: string): Promise<object>;
|
||||
testConnection?(sourceOptions: object): Promise<ConnectionTestResult>;
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import { QueryError } from './query.error';
|
||||
|
||||
const CACHED_CONNECTIONS: any = {};
|
||||
|
||||
export function parseJson(jsonString: string, errorMessage?: string): object {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (err) {
|
||||
throw new QueryError(errorMessage, err.message, {});
|
||||
}
|
||||
}
|
||||
|
||||
export function cacheConnection(dataSourceId: string, connection: any): any {
|
||||
const updatedAt = new Date();
|
||||
CACHED_CONNECTIONS[dataSourceId] = { connection, updatedAt };
|
||||
}
|
||||
|
||||
export function getCachedConnection(dataSourceId: string | number, dataSourceUpdatedAt: any): any {
|
||||
const cachedData = CACHED_CONNECTIONS[dataSourceId];
|
||||
|
||||
if (cachedData) {
|
||||
const updatedAt = new Date(dataSourceUpdatedAt || null);
|
||||
const cachedAt = new Date(cachedData.updatedAt || null);
|
||||
|
||||
const diffTime = (cachedAt.getTime() - updatedAt.getTime()) / 1000;
|
||||
|
||||
if (diffTime < 0) {
|
||||
return null;
|
||||
} else {
|
||||
return cachedData['connection'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanSensitiveData(data, keys) {
|
||||
if (!data || typeof data !== 'object') return;
|
||||
|
||||
const dataObj = { ...data };
|
||||
clearData(dataObj, keys);
|
||||
return dataObj;
|
||||
}
|
||||
|
||||
function clearData(data, keys) {
|
||||
if (!data || typeof data !== 'object') return;
|
||||
|
||||
for (const key in data) {
|
||||
if (keys.includes(key)) {
|
||||
delete data[key];
|
||||
} else {
|
||||
clearData(data[key], keys);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"name": "common",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "@tooljet-marketplace/common",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"directories": {
|
||||
"lib": "lib",
|
||||
"test": "__tests__"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
"clean": "rimraf ./dist && rimraf tsconfig.tsbuildinfo"
|
||||
},
|
||||
"dependencies": {
|
||||
"rimraf": "^3.0.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "lib"
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
# tooljet-plugin-plivo
|
File diff suppressed because one or more lines are too long
Po Szerokość: | Wysokość: | Rozmiar: 19 KiB |
|
@ -0,0 +1,27 @@
|
|||
import { QueryError, QueryService, QueryResult } from '@tooljet-marketplace/common';
|
||||
const plivo = require('plivo');
|
||||
|
||||
export default class PlivoService implements QueryService {
|
||||
getClient(authId: string, authToken: string): any {
|
||||
return new plivo.Client(authId, authToken);
|
||||
}
|
||||
|
||||
async run(sourceOptions: any, queryOptions: any): Promise<QueryResult> {
|
||||
let result = {};
|
||||
|
||||
try {
|
||||
const client = this.getClient(sourceOptions.authId, sourceOptions.authToken);
|
||||
|
||||
if (queryOptions.operation === 'send_sms') {
|
||||
result = await client.messages.create(queryOptions.from, queryOptions.to, queryOptions.body);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new QueryError('Query could not be completed', error.message, {});
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/manifest.schema.json",
|
||||
"title": "Plivo datasource",
|
||||
"description": "A schema defining plivo datasource",
|
||||
"type": "api",
|
||||
"source": {
|
||||
"name": "Plivo",
|
||||
"kind": "plivo",
|
||||
"exposedVariables": {
|
||||
"isLoading": false,
|
||||
"data": {},
|
||||
"rawData": {}
|
||||
},
|
||||
"options": {
|
||||
"authId": {
|
||||
"type": "string"
|
||||
},
|
||||
"authToken": {
|
||||
"type": "string",
|
||||
"encrypted": true
|
||||
}
|
||||
},
|
||||
"customTesting": true
|
||||
},
|
||||
"defaults": {
|
||||
"authToken": {
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"authToken": {
|
||||
"label": "Auth Token",
|
||||
"key": "authToken",
|
||||
"type": "password",
|
||||
"description": "Auth Token for plivo",
|
||||
"helpText": "For generating Auth Token, visit: <a href='https://console.plivo.com/' target='_blank' rel='noreferrer'>Plivo Console</a>"
|
||||
},
|
||||
"authId": {
|
||||
"label": "Auth ID",
|
||||
"key": "authId",
|
||||
"type": "text",
|
||||
"description": "Auth id for plivo",
|
||||
"helpText": "For generating Auth ID, visit: <a href='https://console.plivo.com/' target='_blank' rel='noreferrer'>Plivo Console</a>"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"authToken",
|
||||
"authId"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/operations.schema.json",
|
||||
"title": "Plivo datasource",
|
||||
"description": "A schema defining Plivo datasource",
|
||||
"type": "api",
|
||||
"defaults": {},
|
||||
"properties": {
|
||||
"operation": {
|
||||
"label": "Operation",
|
||||
"key": "operation",
|
||||
"type": "dropdown-component-flip",
|
||||
"description": "Single select dropdown for operation",
|
||||
"list": [
|
||||
{
|
||||
"value": "send_sms",
|
||||
"name": "Send SMS"
|
||||
}
|
||||
]
|
||||
},
|
||||
"send_sms": {
|
||||
"to": {
|
||||
"label": "To Number",
|
||||
"key": "to",
|
||||
"type": "codehinter",
|
||||
"lineNumbers": false,
|
||||
"description": "Enter to number",
|
||||
"width": "320px",
|
||||
"height": "36px",
|
||||
"className": "codehinter-plugins"
|
||||
},
|
||||
"from": {
|
||||
"label": "From Number",
|
||||
"key": "from",
|
||||
"type": "codehinter",
|
||||
"lineNumbers": false,
|
||||
"description": "Enter from number",
|
||||
"width": "320px",
|
||||
"height": "36px",
|
||||
"className": "codehinter-plugins"
|
||||
},
|
||||
"body": {
|
||||
"label": "Body",
|
||||
"key": "body",
|
||||
"type": "codehinter",
|
||||
"lineNumbers": false,
|
||||
"description": "Enter message body",
|
||||
"width": "320px",
|
||||
"height": "36px",
|
||||
"className": "codehinter-plugins",
|
||||
"placeholder": "Enter message body"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "@tooljet-marketplace/plivo",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "ncc build lib/index.ts -o dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tooljet-marketplace/common": "^1.0.0",
|
||||
"plivo": "^4.33.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "lib"
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import { createReadStream } from 'fs';
|
||||
import readDir from 'recursive-readdir';
|
||||
import { resolve as _resolve } from 'path';
|
||||
import aws from 'aws-sdk';
|
||||
import { lookup } from 'mime-types';
|
||||
|
||||
const { config, S3 } = aws;
|
||||
const __dirname = _resolve();
|
||||
|
||||
config.update({
|
||||
region: process.env.AWS_REGION || 'us-west-1',
|
||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: process.env.SECRET_ACCESS_KEY,
|
||||
maxRetries: 3
|
||||
});
|
||||
|
||||
const directoryPath = _resolve(__dirname, 'plugins');
|
||||
|
||||
const getDirectoryFilesRecursive = (dir, ignores = []) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
readDir(dir, ignores, (err, files) => (err ? reject(err) : resolve(files)));
|
||||
});
|
||||
};
|
||||
|
||||
const generateFileKey = fileName => {
|
||||
const S3objectPath = fileName.split('/marketplace/plugins/')[1];
|
||||
return `marketplace-assets/${S3objectPath}`;
|
||||
};
|
||||
|
||||
const s3 = new S3();
|
||||
|
||||
const uploadToS3 = async () => {
|
||||
try {
|
||||
const fileArray = await getDirectoryFilesRecursive(directoryPath, [
|
||||
'common',
|
||||
'.DS_Store',
|
||||
'.gitignore',
|
||||
'index.d.ts',
|
||||
'index.d.ts.map',
|
||||
'README.md',
|
||||
'package-lock.json',
|
||||
'package.json',
|
||||
'tsconfig.json'
|
||||
]);
|
||||
|
||||
fileArray.map(file => {
|
||||
// Configuring parameters for S3 Object
|
||||
const S3params = {
|
||||
Bucket: process.env.AWS_BUCKET,
|
||||
Body: createReadStream(file),
|
||||
Key: generateFileKey(file),
|
||||
ContentType: lookup(file),
|
||||
ContentEncoding: 'utf-8',
|
||||
CacheControl: 'immutable,max-age=31536000,public'
|
||||
};
|
||||
s3.upload(S3params, function(err, data) {
|
||||
if (err) {
|
||||
// Set the exit code while letting
|
||||
// the process exit gracefully.
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`Assets uploaded to S3: `, data);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
uploadToS3();
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"strict": false,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"target": "es2015",
|
||||
"composite": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"exclude": ["plugins/*/lib/*.json", "<rootDir>/__tests__/*", "dist"]
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
|
||||
|
||||
export class CreatePlugins1651056032052 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'plugins',
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
isGenerated: true,
|
||||
default: 'gen_random_uuid()',
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
name: 'plugin_id',
|
||||
type: 'varchar',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'varchar',
|
||||
},
|
||||
{
|
||||
name: 'repo',
|
||||
type: 'varchar',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'varchar',
|
||||
},
|
||||
{
|
||||
name: 'version',
|
||||
type: 'varchar',
|
||||
},
|
||||
{
|
||||
name: 'index_file_id',
|
||||
type: 'uuid',
|
||||
},
|
||||
{
|
||||
name: 'operations_file_id',
|
||||
type: 'uuid',
|
||||
},
|
||||
{
|
||||
name: 'icon_file_id',
|
||||
type: 'uuid',
|
||||
},
|
||||
{
|
||||
name: 'manifest_file_id',
|
||||
type: 'uuid',
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
type: 'timestamp',
|
||||
isNullable: true,
|
||||
default: 'now()',
|
||||
},
|
||||
{
|
||||
name: 'updated_at',
|
||||
type: 'timestamp',
|
||||
isNullable: true,
|
||||
default: 'now()',
|
||||
},
|
||||
],
|
||||
foreignKeys: [
|
||||
{
|
||||
referencedTableName: 'files',
|
||||
referencedColumnNames: ['id'],
|
||||
columnNames: ['index_file_id'],
|
||||
},
|
||||
{
|
||||
referencedTableName: 'files',
|
||||
referencedColumnNames: ['id'],
|
||||
columnNames: ['operations_file_id'],
|
||||
},
|
||||
{
|
||||
referencedTableName: 'files',
|
||||
referencedColumnNames: ['id'],
|
||||
columnNames: ['icon_file_id'],
|
||||
},
|
||||
{
|
||||
referencedTableName: 'files',
|
||||
referencedColumnNames: ['id'],
|
||||
columnNames: ['manifest_file_id'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
|
||||
|
||||
export class AddPluginToDataSource1652768881087 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.addColumn(
|
||||
'data_sources',
|
||||
new TableColumn({
|
||||
name: 'plugin_id',
|
||||
type: 'uuid',
|
||||
isNullable: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
|
||||
|
||||
export class AddPluginToDataQuery1652768887877 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.addColumn(
|
||||
'data_queries',
|
||||
new TableColumn({
|
||||
name: 'plugin_id',
|
||||
type: 'uuid',
|
||||
isNullable: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"assets": [
|
||||
{ "include": "**/*.html", "outDir": "./dist/src", "watchAssets": true }
|
||||
{ "include": "assets/**/*", "outDir": "./dist/src", "watchAssets": true }
|
||||
],
|
||||
"watchAssets": true
|
||||
}
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -40,7 +40,7 @@
|
|||
"@nestjs/jwt": "^8.0.0",
|
||||
"@nestjs/mapped-types": "^1.0.1",
|
||||
"@nestjs/passport": "^8.2.1",
|
||||
"@nestjs/platform-express": "^8.4.4",
|
||||
"@nestjs/platform-express": "^8.4.6",
|
||||
"@nestjs/platform-ws": "^8.0.10",
|
||||
"@nestjs/serve-static": "^2.2.2",
|
||||
"@nestjs/typeorm": "^8.0.0",
|
||||
|
@ -61,6 +61,9 @@
|
|||
"helmet": "^4.6.0",
|
||||
"humps": "^2.0.1",
|
||||
"joi": "^17.4.1",
|
||||
"js-base64": "^3.7.2",
|
||||
"jszip": "^3.10.1",
|
||||
"module-from-string": "^3.3.0",
|
||||
"nestjs-pino": "^1.4.0",
|
||||
"nodemailer": "^6.6.3",
|
||||
"passport": "^0.4.1",
|
||||
|
@ -81,12 +84,10 @@
|
|||
"@nestjs/cli": "^8.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.3.5",
|
||||
"preview-email": "^3.0.4",
|
||||
"rimraf": "^3.0.2",
|
||||
"@golevelup/ts-jest": "^0.3.2",
|
||||
"@nestjs/schematics": "^8.0.0",
|
||||
"@nestjs/testing": "^8.0.0",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/got": "^9.6.12",
|
||||
"@types/humps": "^2.0.1",
|
||||
|
@ -98,7 +99,6 @@
|
|||
"@types/sanitize-html": "^2.6.2",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@types/ws": "^8.2.2",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.31.1",
|
||||
"@typescript-eslint/parser": "^4.31.1",
|
||||
"eslint": "^7.32.0",
|
||||
|
@ -108,12 +108,15 @@
|
|||
"eslint-plugin-prettier": "^3.4.1",
|
||||
"jest": "^27.0.6",
|
||||
"prettier": "^2.3.2",
|
||||
"preview-email": "^3.0.4",
|
||||
"rimraf": "^3.0.2",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "^27.0.3",
|
||||
"ts-loader": "^9.2.3"
|
||||
"ts-loader": "^9.2.3",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17.3",
|
||||
"npm": "<=7.20.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ import { LibraryAppModule } from './modules/library_app/library_app.module';
|
|||
import { ThreadModule } from './modules/thread/thread.module';
|
||||
import { EventsModule } from './events/events.module';
|
||||
import { GroupPermissionsModule } from './modules/group_permissions/group_permissions.module';
|
||||
import { PluginsModule } from './modules/plugins/plugins.module';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
|
@ -84,6 +85,7 @@ const imports = [
|
|||
LibraryAppModule,
|
||||
GroupPermissionsModule,
|
||||
FilesModule,
|
||||
PluginsModule,
|
||||
EventsModule,
|
||||
];
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
[
|
||||
{
|
||||
"name": "Cassandra plugin",
|
||||
"description": "Datasource plugin for Cassandra",
|
||||
"author": "Tooljet",
|
||||
"version": "1.0.0",
|
||||
"id": "cassandra"
|
||||
},
|
||||
{
|
||||
"name": "Plivo plugin",
|
||||
"description": "API plugin for plivo",
|
||||
"author": "Tooljet",
|
||||
"version": "1.0.0",
|
||||
"id": "plivo"
|
||||
}
|
||||
]
|
|
@ -19,6 +19,7 @@ import { AppsAbilityFactory } from 'src/modules/casl/abilities/apps-ability.fact
|
|||
import { AppsService } from '@services/apps.service';
|
||||
import { CreateDataQueryDto, UpdateDataQueryDto } from '@dto/data-query.dto';
|
||||
import { User } from 'src/decorators/user.decorator';
|
||||
import { decode } from 'js-base64';
|
||||
|
||||
@Controller('data_queries')
|
||||
export class DataQueriesController {
|
||||
|
@ -47,6 +48,14 @@ export class DataQueriesController {
|
|||
const decamelizedQuery = decamelizeKeys(query);
|
||||
|
||||
decamelizedQuery['options'] = query.options;
|
||||
|
||||
if (query.pluginId) {
|
||||
decamelizedQuery['plugin'].manifest_file.data = JSON.parse(
|
||||
decode(query.plugin.manifestFile.data.toString('utf8'))
|
||||
);
|
||||
decamelizedQuery['plugin'].icon_file.data = query.plugin.iconFile.data.toString('utf8');
|
||||
}
|
||||
|
||||
seralizedQueries.push(decamelizedQuery);
|
||||
}
|
||||
|
||||
|
@ -58,10 +67,11 @@ export class DataQueriesController {
|
|||
@UseGuards(JwtAuthGuard)
|
||||
@Post()
|
||||
async create(@User() user, @Body() dataQueryDto: CreateDataQueryDto): Promise<object> {
|
||||
const { kind, name, options, app_id, app_version_id, data_source_id } = dataQueryDto;
|
||||
const { kind, name, options, app_id, app_version_id, data_source_id, plugin_id } = dataQueryDto;
|
||||
const appId = app_id;
|
||||
const appVersionId = app_version_id;
|
||||
const dataSourceId = data_source_id;
|
||||
const pluginId = plugin_id;
|
||||
|
||||
const app = await this.appsService.find(appId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, appId);
|
||||
|
@ -78,6 +88,7 @@ export class DataQueriesController {
|
|||
}
|
||||
}
|
||||
|
||||
// todo: pass the whole dto instead of indv. values
|
||||
const dataQuery = await this.dataQueriesService.create(
|
||||
user,
|
||||
name,
|
||||
|
@ -85,7 +96,8 @@ export class DataQueriesController {
|
|||
options,
|
||||
appId,
|
||||
dataSourceId,
|
||||
appVersionId
|
||||
appVersionId,
|
||||
pluginId
|
||||
);
|
||||
return decamelizeKeys(dataQuery);
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
TestDataSourceDto,
|
||||
UpdateDataSourceDto,
|
||||
} from '@dto/data-source.dto';
|
||||
import { decode } from 'js-base64';
|
||||
import { User } from 'src/decorators/user.decorator';
|
||||
|
||||
@Controller('data_sources')
|
||||
|
@ -46,6 +47,15 @@ export class DataSourcesController {
|
|||
}
|
||||
|
||||
const dataSources = await this.dataSourcesService.all(user, query);
|
||||
for (const dataSource of dataSources) {
|
||||
if (dataSource.pluginId) {
|
||||
dataSource.plugin.iconFile.data = dataSource.plugin.iconFile.data.toString('utf8');
|
||||
dataSource.plugin.manifestFile.data = JSON.parse(decode(dataSource.plugin.manifestFile.data.toString('utf8')));
|
||||
dataSource.plugin.operationsFile.data = JSON.parse(
|
||||
decode(dataSource.plugin.operationsFile.data.toString('utf8'))
|
||||
);
|
||||
}
|
||||
}
|
||||
const response = decamelizeKeys({ data_sources: dataSources });
|
||||
|
||||
return response;
|
||||
|
@ -54,9 +64,10 @@ export class DataSourcesController {
|
|||
@UseGuards(JwtAuthGuard)
|
||||
@Post()
|
||||
async create(@User() user, @Body() createDataSourceDto: CreateDataSourceDto) {
|
||||
const { kind, name, options, app_id, app_version_id } = createDataSourceDto;
|
||||
const { kind, name, options, app_id, app_version_id, plugin_id } = createDataSourceDto;
|
||||
const appId = app_id;
|
||||
const appVersionId = app_version_id;
|
||||
const pluginId = plugin_id;
|
||||
|
||||
const app = await this.appsService.find(appId);
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, appId);
|
||||
|
@ -65,7 +76,7 @@ export class DataSourcesController {
|
|||
throw new ForbiddenException('you do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const dataSource = await this.dataSourcesService.create(name, kind, options, appId, appVersionId);
|
||||
const dataSource = await this.dataSourcesService.create(name, kind, options, appId, appVersionId, pluginId);
|
||||
return decamelizeKeys(dataSource);
|
||||
}
|
||||
|
||||
|
@ -110,8 +121,8 @@ export class DataSourcesController {
|
|||
@UseGuards(JwtAuthGuard)
|
||||
@Post('test_connection')
|
||||
async testConnection(@User() user, @Body() testDataSourceDto: TestDataSourceDto) {
|
||||
const { kind, options } = testDataSourceDto;
|
||||
return await this.dataSourcesService.testConnection(kind, options);
|
||||
const { kind, options, plugin_id } = testDataSourceDto;
|
||||
return await this.dataSourcesService.testConnection(kind, options, plugin_id);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
UseInterceptors,
|
||||
ClassSerializerInterceptor,
|
||||
UseGuards,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Plugin } from 'src/entities/plugin.entity';
|
||||
import { PluginsService } from '../services/plugins.service';
|
||||
import { CreatePluginDto } from '../dto/create-plugin.dto';
|
||||
import { UpdatePluginDto } from '../dto/update-plugin.dto';
|
||||
import { decode } from 'js-base64';
|
||||
import { PluginsAbilityFactory } from 'src/modules/casl/abilities/plugins-ability.factory';
|
||||
import { JwtAuthGuard } from 'src/modules/auth/jwt-auth.guard';
|
||||
import { User } from 'src/decorators/user.decorator';
|
||||
|
||||
@Controller('plugins')
|
||||
@UseInterceptors(ClassSerializerInterceptor)
|
||||
export class PluginsController {
|
||||
constructor(private readonly pluginsService: PluginsService, private pluginsAbilityFactory: PluginsAbilityFactory) {}
|
||||
|
||||
@Post('install')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async install(@User() user, @Body() createPluginDto: CreatePluginDto) {
|
||||
const ability = await this.pluginsAbilityFactory.pluginActions(user);
|
||||
|
||||
if (!ability.can('installPlugin', Plugin)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
return this.pluginsService.install(createPluginDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async findAll() {
|
||||
const plugins = await this.pluginsService.findAll();
|
||||
return plugins.map((plugin) => {
|
||||
plugin.iconFile.data = plugin.iconFile.data.toString('utf8');
|
||||
plugin.manifestFile.data = JSON.parse(decode(plugin.manifestFile.data.toString('utf8')));
|
||||
return plugin;
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.pluginsService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async update(@User() user, @Param('id') id: string, @Body() updatePluginDto: UpdatePluginDto) {
|
||||
const ability = await this.pluginsAbilityFactory.pluginActions(user);
|
||||
|
||||
if (!ability.can('updatePlugin', Plugin)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
return this.pluginsService.update(id, updatePluginDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async remove(@User() user, @Param('id') id: string) {
|
||||
const ability = await this.pluginsAbilityFactory.pluginActions(user);
|
||||
|
||||
if (!ability.can('deletePlugin', Plugin)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
return this.pluginsService.remove(id);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { IsNotEmpty, IsString, IsUUID, IsOptional, IsSemVer } from 'class-validator';
|
||||
|
||||
export class CreatePluginDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
repo: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
description: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsSemVer()
|
||||
version: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
organizationId: string;
|
||||
}
|
|
@ -10,6 +10,10 @@ export class CreateDataQueryDto {
|
|||
@IsUUID()
|
||||
app_version_id: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
plugin_id: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
data_source_id: string;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Transform } from 'class-transformer';
|
||||
import { IsUUID, IsString, IsNotEmpty, IsDefined } from 'class-validator';
|
||||
import { IsUUID, IsString, IsOptional, IsNotEmpty, IsDefined } from 'class-validator';
|
||||
import { sanitizeInput } from 'src/helpers/utils.helper';
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
|
||||
|
@ -10,6 +10,10 @@ export class CreateDataSourceDto {
|
|||
@IsUUID()
|
||||
app_version_id: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
plugin_id: string;
|
||||
|
||||
@IsString()
|
||||
@Transform(({ value }) => sanitizeInput(value))
|
||||
@IsNotEmpty()
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreatePluginDto } from './create-plugin.dto';
|
||||
|
||||
export class UpdatePluginDto extends PartialType(CreatePluginDto) {}
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { App } from './app.entity';
|
||||
import { AppVersion } from './app_version.entity';
|
||||
import { DataSource } from './data_source.entity';
|
||||
import { Plugin } from './plugin.entity';
|
||||
|
||||
@Entity({ name: 'data_queries' })
|
||||
export class DataQuery extends BaseEntity {
|
||||
|
@ -32,6 +33,9 @@ export class DataQuery extends BaseEntity {
|
|||
@Column({ name: 'app_id' })
|
||||
appId: string;
|
||||
|
||||
@Column({ name: 'plugin_id' })
|
||||
pluginId: string;
|
||||
|
||||
@Column({ name: 'app_version_id' })
|
||||
appVersionId: string;
|
||||
|
||||
|
@ -52,4 +56,8 @@ export class DataQuery extends BaseEntity {
|
|||
@ManyToOne(() => DataSource, (dataSource) => dataSource.id)
|
||||
@JoinColumn({ name: 'data_source_id' })
|
||||
dataSource: DataSource;
|
||||
|
||||
@ManyToOne(() => Plugin, (plugin) => plugin.id, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'plugin_id' })
|
||||
plugin: Plugin;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
} from 'typeorm';
|
||||
import { App } from './app.entity';
|
||||
import { AppVersion } from './app_version.entity';
|
||||
import { Plugin } from './plugin.entity';
|
||||
|
||||
@Entity({ name: 'data_sources' })
|
||||
export class DataSource extends BaseEntity {
|
||||
|
@ -28,6 +29,9 @@ export class DataSource extends BaseEntity {
|
|||
@Column({ name: 'app_id' })
|
||||
appId: string;
|
||||
|
||||
@Column({ name: 'plugin_id' })
|
||||
pluginId: string;
|
||||
|
||||
@Column({ name: 'app_version_id' })
|
||||
appVersionId: string;
|
||||
|
||||
|
@ -44,4 +48,8 @@ export class DataSource extends BaseEntity {
|
|||
@ManyToOne(() => App, (app) => app.id)
|
||||
@JoinColumn({ name: 'app_id' })
|
||||
app: App;
|
||||
|
||||
@ManyToOne(() => Plugin, (plugin) => plugin.id, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'plugin_id' })
|
||||
plugin: Plugin;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { File } from 'src/entities/file.entity';
|
||||
|
||||
@Entity({ name: 'plugins' })
|
||||
export class Plugin {
|
||||
@PrimaryGeneratedColumn()
|
||||
public id: string;
|
||||
|
||||
@Column({ name: 'plugin_id' })
|
||||
pluginId: string;
|
||||
|
||||
@Column({ name: 'name' })
|
||||
name: string;
|
||||
|
||||
@Column({ name: 'repo' })
|
||||
repo: string;
|
||||
|
||||
@Column({ name: 'version' })
|
||||
version: string;
|
||||
|
||||
@Column({ name: 'description' })
|
||||
description: string;
|
||||
|
||||
@Column({ name: 'index_file_id' })
|
||||
indexFileId: string;
|
||||
|
||||
@Column({ name: 'operations_file_id' })
|
||||
operationsFileId: string;
|
||||
|
||||
@Column({ name: 'icon_file_id' })
|
||||
iconFileId: string;
|
||||
|
||||
@Column({ name: 'manifest_file_id' })
|
||||
manifestFileId: string;
|
||||
|
||||
@CreateDateColumn({ default: () => 'now()', name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ default: () => 'now()', name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
@OneToOne(() => File, (file) => file.id)
|
||||
@JoinColumn({ name: 'index_file_id' })
|
||||
indexFile?: File;
|
||||
|
||||
@OneToOne(() => File, (file) => file.id)
|
||||
@JoinColumn({ name: 'operations_file_id' })
|
||||
operationsFile?: File;
|
||||
|
||||
@OneToOne(() => File, (file) => file.id)
|
||||
@JoinColumn({ name: 'icon_file_id' })
|
||||
iconFile?: File;
|
||||
|
||||
@OneToOne(() => File, (file) => file.id)
|
||||
@JoinColumn({ name: 'manifest_file_id' })
|
||||
manifestFile?: File;
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import { decode } from 'js-base64';
|
||||
import { requireFromString } from 'module-from-string';
|
||||
import { Plugin } from 'src/entities/plugin.entity';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import allPlugins from '@tooljet/plugins/dist/server';
|
||||
|
||||
@Injectable()
|
||||
export class PluginsHelper {
|
||||
private readonly plugins: any = {};
|
||||
private static instance: PluginsHelper;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Plugin)
|
||||
private pluginsRepository: Repository<Plugin>
|
||||
) {
|
||||
if (PluginsHelper.instance) {
|
||||
return PluginsHelper.instance;
|
||||
}
|
||||
|
||||
PluginsHelper.instance = this;
|
||||
return PluginsHelper.instance;
|
||||
}
|
||||
|
||||
async getService(pluginId: string, kind: string) {
|
||||
try {
|
||||
if (pluginId) {
|
||||
let decoded: string;
|
||||
if (this.plugins[pluginId]) {
|
||||
decoded = this.plugins[pluginId];
|
||||
} else {
|
||||
const plugin = await this.pluginsRepository.findOne({ where: { id: pluginId } });
|
||||
decoded = decode(plugin.indexFile.data.toString());
|
||||
this.plugins[pluginId] = decoded;
|
||||
}
|
||||
const code = requireFromString(decoded, { useCurrentGlobal: true });
|
||||
const service = new code.default();
|
||||
return service;
|
||||
} else {
|
||||
return new allPlugins[kind]();
|
||||
}
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(error);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { WsAdapter } from '@nestjs/platform-ws';
|
||||
import * as compression from 'compression';
|
||||
import { AppModule } from './app.module';
|
||||
|
@ -9,13 +10,14 @@ import { AllExceptionsFilter } from './all-exceptions-filter';
|
|||
import { RequestMethod, ValidationPipe } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { bootstrap as globalAgentBootstrap } from 'global-agent';
|
||||
import { join } from 'path';
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
globalThis.TOOLJET_VERSION = fs.readFileSync('./.version', 'utf8').trim();
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
bufferLogs: true,
|
||||
abortOnError: false,
|
||||
});
|
||||
|
@ -81,6 +83,7 @@ async function bootstrap() {
|
|||
|
||||
app.use(json({ limit: '50mb' }));
|
||||
app.use(urlencoded({ extended: true, limit: '50mb', parameterLimit: 1000000 }));
|
||||
app.useStaticAssets(join(__dirname, 'assets'), { prefix: '/assets' });
|
||||
|
||||
const port = parseInt(process.env.PORT) || 3000;
|
||||
|
||||
|
|
|
@ -27,6 +27,9 @@ import { DataSourcesService } from '@services/data_sources.service';
|
|||
import { CredentialsService } from '@services/credentials.service';
|
||||
import { EncryptionService } from '@services/encryption.service';
|
||||
import { Credential } from 'src/entities/credential.entity';
|
||||
import { PluginsService } from '@services/plugins.service';
|
||||
import { Plugin } from 'src/entities/plugin.entity';
|
||||
import { PluginsHelper } from 'src/helpers/plugins.helper';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -46,6 +49,7 @@ import { Credential } from 'src/entities/credential.entity';
|
|||
UserGroupPermission,
|
||||
Credential,
|
||||
File,
|
||||
Plugin,
|
||||
]),
|
||||
CaslModule,
|
||||
],
|
||||
|
@ -59,6 +63,8 @@ import { Credential } from 'src/entities/credential.entity';
|
|||
CredentialsService,
|
||||
EncryptionService,
|
||||
FilesService,
|
||||
PluginsService,
|
||||
PluginsHelper,
|
||||
],
|
||||
controllers: [AppsController, AppUsersController],
|
||||
})
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { User } from 'src/entities/user.entity';
|
||||
import { InferSubjects, AbilityBuilder, Ability, AbilityClass, ExtractSubjectType } from '@casl/ability';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UsersService } from 'src/services/users.service';
|
||||
import { Plugin } from 'src/entities/plugin.entity';
|
||||
|
||||
type Actions = 'installPlugin' | 'updatePlugin' | 'deletePlugin';
|
||||
|
||||
type Subjects = InferSubjects<typeof User | typeof Plugin> | 'all';
|
||||
|
||||
export type PluginsAbility = Ability<[Actions, Subjects]>;
|
||||
|
||||
@Injectable()
|
||||
export class PluginsAbilityFactory {
|
||||
constructor(private usersService: UsersService) {}
|
||||
|
||||
async pluginActions(user: User) {
|
||||
const { can, build } = new AbilityBuilder<Ability<[Actions, Subjects]>>(Ability as AbilityClass<PluginsAbility>);
|
||||
|
||||
if (await this.usersService.userCan(user, 'create', 'Plugin')) {
|
||||
can('installPlugin', Plugin);
|
||||
}
|
||||
|
||||
if (await this.usersService.userCan(user, 'update', 'Plugin')) {
|
||||
can('updatePlugin', Plugin);
|
||||
}
|
||||
|
||||
if (await this.usersService.userCan(user, 'delete', 'Plugin')) {
|
||||
can('deletePlugin', Plugin);
|
||||
}
|
||||
|
||||
return build({
|
||||
detectSubjectType: (item) => item.constructor as ExtractSubjectType<Subjects>,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import { User } from 'src/entities/user.entity';
|
|||
import { AppsAbilityFactory } from './abilities/apps-ability.factory';
|
||||
import { ThreadsAbilityFactory } from './abilities/threads-ability.factory';
|
||||
import { CommentsAbilityFactory } from './abilities/comments-ability.factory';
|
||||
import { PluginsAbilityFactory } from './abilities/plugins-ability.factory';
|
||||
import { CaslAbilityFactory } from './casl-ability.factory';
|
||||
import { FoldersAbilityFactory } from './abilities/folders-ability.factory';
|
||||
import { FilesService } from '@services/files.service';
|
||||
|
@ -27,6 +28,7 @@ import { OrgEnvironmentVariablesAbilityFactory } from './abilities/org-environme
|
|||
AppsAbilityFactory,
|
||||
ThreadsAbilityFactory,
|
||||
CommentsAbilityFactory,
|
||||
PluginsAbilityFactory,
|
||||
FoldersAbilityFactory,
|
||||
OrgEnvironmentVariablesAbilityFactory,
|
||||
],
|
||||
|
@ -35,6 +37,7 @@ import { OrgEnvironmentVariablesAbilityFactory } from './abilities/org-environme
|
|||
AppsAbilityFactory,
|
||||
ThreadsAbilityFactory,
|
||||
CommentsAbilityFactory,
|
||||
PluginsAbilityFactory,
|
||||
FoldersAbilityFactory,
|
||||
OrgEnvironmentVariablesAbilityFactory,
|
||||
],
|
||||
|
|
|
@ -23,6 +23,8 @@ import { OrganizationUser } from 'src/entities/organization_user.entity';
|
|||
import { Organization } from 'src/entities/organization.entity';
|
||||
import { AppImportExportService } from '@services/app_import_export.service';
|
||||
import { FilesService } from '@services/files.service';
|
||||
import { Plugin } from 'src/entities/plugin.entity';
|
||||
import { PluginsHelper } from 'src/helpers/plugins.helper';
|
||||
import { OrgEnvironmentVariable } from 'src/entities/org_envirnoment_variable.entity';
|
||||
|
||||
@Module({
|
||||
|
@ -42,6 +44,7 @@ import { OrgEnvironmentVariable } from 'src/entities/org_envirnoment_variable.en
|
|||
User,
|
||||
OrganizationUser,
|
||||
Organization,
|
||||
Plugin,
|
||||
]),
|
||||
CaslModule,
|
||||
],
|
||||
|
@ -54,6 +57,7 @@ import { OrgEnvironmentVariable } from 'src/entities/org_envirnoment_variable.en
|
|||
UsersService,
|
||||
AppImportExportService,
|
||||
FilesService,
|
||||
PluginsHelper,
|
||||
],
|
||||
controllers: [DataQueriesController],
|
||||
})
|
||||
|
|
|
@ -23,6 +23,9 @@ import { OrganizationUser } from 'src/entities/organization_user.entity';
|
|||
import { Organization } from 'src/entities/organization.entity';
|
||||
import { AppImportExportService } from '@services/app_import_export.service';
|
||||
import { FilesService } from '@services/files.service';
|
||||
import { PluginsService } from '@services/plugins.service';
|
||||
import { PluginsHelper } from 'src/helpers/plugins.helper';
|
||||
import { Plugin } from 'src/entities/plugin.entity';
|
||||
import { OrgEnvironmentVariable } from 'src/entities/org_envirnoment_variable.entity';
|
||||
|
||||
@Module({
|
||||
|
@ -34,6 +37,7 @@ import { OrgEnvironmentVariable } from 'src/entities/org_envirnoment_variable.en
|
|||
OrgEnvironmentVariable,
|
||||
App,
|
||||
File,
|
||||
Plugin,
|
||||
AppVersion,
|
||||
AppUser,
|
||||
FolderApp,
|
||||
|
@ -54,6 +58,8 @@ import { OrgEnvironmentVariable } from 'src/entities/org_envirnoment_variable.en
|
|||
UsersService,
|
||||
AppImportExportService,
|
||||
FilesService,
|
||||
PluginsService,
|
||||
PluginsHelper,
|
||||
],
|
||||
controllers: [DataSourcesController],
|
||||
})
|
||||
|
|
|
@ -10,15 +10,23 @@ import { EncryptionService } from '@services/encryption.service';
|
|||
import { Credential } from 'src/entities/credential.entity';
|
||||
import { DataSource } from 'src/entities/data_source.entity';
|
||||
import { CaslModule } from '../casl/casl.module';
|
||||
import { FilesService } from '@services/files.service';
|
||||
import { File } from 'src/entities/file.entity';
|
||||
import { PluginsService } from '@services/plugins.service';
|
||||
import { Plugin } from 'src/entities/plugin.entity';
|
||||
import { PluginsHelper } from 'src/helpers/plugins.helper';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([App, Credential, DataSource]), CaslModule],
|
||||
imports: [TypeOrmModule.forFeature([App, Credential, File, Plugin, DataSource]), CaslModule],
|
||||
providers: [
|
||||
EncryptionService,
|
||||
CredentialsService,
|
||||
DataSourcesService,
|
||||
LibraryAppCreationService,
|
||||
AppImportExportService,
|
||||
FilesService,
|
||||
PluginsService,
|
||||
PluginsHelper,
|
||||
],
|
||||
controllers: [LibraryAppsController],
|
||||
})
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue