[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
Gandharv 2022-10-27 18:29:43 +07:00 zatwierdzone przez GitHub
rodzic 510be16753
commit a1fd1fc301
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
108 zmienionych plików z 5562 dodań i 685 usunięć

Wyświetl plik

@ -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=

Wyświetl plik

@ -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]

Wyświetl plik

@ -1,7 +1,7 @@
{
"name": "@tooljet/cli",
"description": "tooljet cli tool",
"version": "0.0.12",
"version": "0.0.13",
"bin": {
"tooljet": "./bin/run"
},

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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",

Wyświetl plik

@ -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} />

Wyświetl plik

@ -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));

Wyświetl plik

@ -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 === '') {

Wyświetl plik

@ -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} />;

Wyświetl plik

@ -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') {

Wyświetl plik

@ -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

Wyświetl plik

@ -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>

Wyświetl plik

@ -19,3 +19,5 @@ export const allSources = {
Stripe,
Openapi,
};
export const source = (props) => <DynamicForm schema={props.pluginSchema} {...props} />;

Wyświetl plik

@ -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}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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>
);
};

Wyświetl plik

@ -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>
);
};

Wyświetl plik

@ -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>
);
};

Wyświetl plik

@ -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>
);
};

Wyświetl plik

@ -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 };

Wyświetl plik

@ -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} />;
}}
/>
);

Wyświetl plik

@ -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 }} />;

Wyświetl plik

@ -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) };

Wyświetl plik

@ -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) };

Wyświetl plik

@ -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';

Wyświetl plik

@ -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,
};

Wyświetl plik

@ -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,
};

Wyświetl plik

@ -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 (

Wyświetl plik

@ -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',
}),
},
};

19
marketplace/.gitignore vendored 100644
Wyświetl plik

@ -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

Wyświetl plik

@ -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
```

Wyświetl plik

@ -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

Wyświetl plik

@ -0,0 +1,8 @@
---
to: <%= plugins_path %>/plugins/<%= name %>/.gitignore
---
node_modules
lib/*.d.*
lib/*.js
lib/*.js.map
dist/*

Wyświetl plik

@ -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>

Wyświetl plik

@ -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: {},
};
}
}

Wyświetl plik

@ -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": []
}

Wyświetl plik

@ -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": {}
}

Wyświetl plik

@ -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"
}
}

Wyświetl plik

@ -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 %>

Wyświetl plik

@ -0,0 +1,10 @@
---
to: <%= plugins_path %>/plugins/<%= name %>/__tests__/index.js
---
'use strict';
const <%= name %> = require('../lib');
describe('<%= name %>', () => {
it.todo('needs tests');
});

Wyświetl plik

@ -0,0 +1,14 @@
---
to: <%= plugins_path %>/plugins/<%= name %>/tsconfig.json
---
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "lib"
},
"exclude": [
"node_modules",
"dist"
]
}

Wyświetl plik

@ -0,0 +1,7 @@
---
to: <%= plugins_path %>/plugins/<%= name %>/lib/types.ts
---
export type SourceOptions = {};
export type QueryOptions = {
operation: string;
};

2224
marketplace/package-lock.json wygenerowano 100644

Plik diff jest za duży Load Diff

Wyświetl plik

@ -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"
}
}

Wyświetl plik

@ -0,0 +1,5 @@
node_modules
lib/*.d.*
lib/*.js
lib/*.js.map
dist/*

Wyświetl plik

@ -0,0 +1 @@
# tooljet-plugin-cassandra

File diff suppressed because one or more lines are too long

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 55 KiB

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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"
]
}

Wyświetl plik

@ -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"
]
}

Wyświetl plik

@ -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=="
}
}
}

Wyświetl plik

@ -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"
}
}

Wyświetl plik

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "lib"
},
"exclude": [
"node_modules",
"dist"
]
}

Wyświetl plik

@ -0,0 +1,4 @@
node_modules
lib/*.d.*
lib/*.js
lib/*.js.map

Wyświetl plik

@ -0,0 +1,11 @@
# `common`
> TODO: description
## Usage
```
const common = require('common');
// TODO: DEMONSTRATE API
```

Wyświetl plik

@ -0,0 +1,7 @@
'use strict';
// const common = require('../lib/utils.helper');
describe('common', () => {
it.todo('needs tests');
});

Wyświetl plik

@ -0,0 +1,5 @@
export type ConnectionTestResult = {
status: 'ok' | 'failed';
message?: string;
data?: object;
};

Wyświetl plik

@ -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,
};

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -0,0 +1,5 @@
export type QueryResult = {
status: 'ok' | 'failed' | 'needs_oauth';
errorMessage?: string;
data: Array<object> | object;
};

Wyświetl plik

@ -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>;
}

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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="
}
}
}

Wyświetl plik

@ -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"
}
}

Wyświetl plik

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "lib"
},
"exclude": [
"node_modules",
"dist"
]
}

Wyświetl plik

@ -0,0 +1 @@
# tooljet-plugin-plivo

File diff suppressed because one or more lines are too long

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 19 KiB

Wyświetl plik

@ -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,
};
}
}

Wyświetl plik

@ -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"
]
}

Wyświetl plik

@ -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"
}
}
}
}

Wyświetl plik

@ -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"
}
}

Wyświetl plik

@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "lib"
},
"exclude": [
"node_modules",
"dist"
]
}

Wyświetl plik

@ -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();

Wyświetl plik

@ -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"]
}

Wyświetl plik

@ -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> {}
}

Wyświetl plik

@ -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> {}
}

Wyświetl plik

@ -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> {}
}

Wyświetl plik

@ -3,7 +3,7 @@
"sourceRoot": "src",
"compilerOptions": {
"assets": [
{ "include": "**/*.html", "outDir": "./dist/src", "watchAssets": true }
{ "include": "assets/**/*", "outDir": "./dist/src", "watchAssets": true }
],
"watchAssets": true
}

1425
server/package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -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"
}
}
}

Wyświetl plik

@ -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,
];

Wyświetl plik

@ -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"
}
]

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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)

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -10,6 +10,10 @@ export class CreateDataQueryDto {
@IsUUID()
app_version_id: string;
@IsUUID()
@IsOptional()
plugin_id: string;
@IsUUID()
@IsOptional()
data_source_id: string;

Wyświetl plik

@ -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()

Wyświetl plik

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreatePluginDto } from './create-plugin.dto';
export class UpdatePluginDto extends PartialType(CreatePluginDto) {}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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],
})

Wyświetl plik

@ -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>,
});
}
}

Wyświetl plik

@ -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,
],

Wyświetl plik

@ -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],
})

Wyświetl plik

@ -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],
})

Wyświetl plik

@ -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