Merge pull request #11162 from ToolJet/chore/marketplace-merge

Chore: Marketplace and ToolJet Database updates sync
This commit is contained in:
Johnson Cherian 2024-10-29 11:32:11 +05:30 committed by GitHub
commit 817a5159c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
180 changed files with 54689 additions and 7815 deletions

View file

@ -1,5 +1,5 @@
import React, { useContext, useMemo, useState } from 'react';
import { NoCondition } from './NoConditionUI';
import { NoCondition } from '../NoConditionUI';
import './style.scss';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { isEmpty } from 'lodash';
@ -352,16 +352,20 @@ export const AggregateFilter = ({ darkMode, operation = '' }) => {
return (
<>
<div className="d-flex" style={{ marginBottom: '1.5rem' }}>
<label className="form-label" data-cy="label-column-filter">
<label className="form-label flex-shrink-0" data-cy="label-column-filter">
Aggregate
</label>
<div className="field-container col d-flex custom-gap-8 flex-column ">
<div
className={`field-container col d-flex custom-gap-8 flex-column ${
!isEmpty(operationDetails?.aggregates) && 'minw-400-w-400'
}`}
>
{isEmpty(operationDetails?.aggregates || {}) && <NoCondition />}
{operationDetails?.aggregates &&
!isEmpty(operationDetails?.aggregates) &&
Object.entries(operationDetails.aggregates).map(([aggregateKey, aggregateDetails]) => {
return (
<div key={aggregateKey} className="d-flex flex-row minw-400px-maxw-45perc">
<div key={aggregateKey} className="d-flex flex-row ">
<div
style={{ minWidth: '25%', borderRadius: '4px 0 0 4px' }}
className="border overflow-hidden border-width-except-right"
@ -439,13 +443,13 @@ export const AggregateFilter = ({ darkMode, operation = '' }) => {
</div>
</div>
<div className="d-flex" style={{ marginBottom: '1.5rem' }}>
<label className="form-label" data-cy="label-column-filter">
<label className="form-label flex-shrink-0" data-cy="label-column-filter">
Group by
</label>
<div className="field-container col minw-400px-maxw-45perc">
<div className="field-container col minw-400-w-400">
{/* tooltip is not working */}
{operation === 'listRows' && (
<div className="border rounded">
<div className="border rounded ">
<SelectBox
width="100%"
height="32"
@ -461,7 +465,7 @@ export const AggregateFilter = ({ darkMode, operation = '' }) => {
</div>
)}
{operation === 'joinTable' && (
<div className="d-flex flex-column custom-gap-8 join-group-bys">
<div className="d-flex flex-column custom-gap-8 join-group-bys ">
<div className="border rounded d-flex">
<ToolTip
message={selectedTableName}

View file

@ -20,7 +20,8 @@
.width-fit-content{
width: fit-content;
}
.minw-400px-maxw-45perc{
min-width: 400px;
.minw-400-w-400{
width: 45% !important;
max-width: 45%;
min-width: 45%;
}

View file

@ -1,17 +1,16 @@
import React, { useState, useEffect, useContext } from 'react';
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
import Select from '@/_ui/Select';
import { v4 as uuidv4 } from 'uuid';
import { isEmpty } from 'lodash';
import { useMounted } from '@/_hooks/use-mount';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import CodeHinter from '@/Editor/CodeEditor';
import RenderColumnUI from './RenderColumnUI';
import { NoCondition } from './NoConditionUI';
export const CreateRow = React.memo(({ optionchanged, options, darkMode }) => {
const mounted = useMounted();
const { columns } = useContext(TooljetDatabaseContext);
const [columnOptions, setColumnOptions] = useState(options['create_row'] || {});
useEffect(() => {
mounted && optionchanged('create_row', columnOptions);
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -39,32 +38,34 @@ export const CreateRow = React.memo(({ optionchanged, options, darkMode }) => {
if (Object.keys(columnOptions).length === columns.length) {
return;
}
const existingColumnOption = Object.values ? Object.values(columnOptions) : [];
const existingColumnOption = columnOptions || {};
const emptyColumnOption = { column: '', value: '' };
handleColumnOptionChange({ ...existingColumnOption, ...{ [uuidv4()]: emptyColumnOption } });
}
return (
<div className="row tj-db-field-wrapper">
<div className="tab-content-wrapper d-flex" style={{ marginTop: '16px' }}>
<label className="form-label" data-cy="label-column-filter">
<div className="tab-content-wrapper d-flex tooljetdb-worflow-operations" style={{ marginTop: '16px' }}>
<label className="form-label flex-shrink-0" data-cy="label-column-filter">
Columns
</label>
<div className="field-container flex-grow-1 col">
{Object.entries(columnOptions).map(([key, value]) => (
<RenderColumnOptions
key={key}
columnOptions={columnOptions}
column={value.column}
columns={columns}
value={value.value}
handleColumnOptionChange={handleColumnOptionChange}
darkMode={darkMode}
removeColumnOptionsPair={removeColumnOptionsPair}
id={key}
/>
))}
<div className={`field-container flex-grow-1 ${!isEmpty(columnOptions) && 'minw-400-w-400'}`}>
{isEmpty(columnOptions) && <NoCondition text="There are no columns" />}
{!isEmpty(columnOptions) &&
Object.entries(columnOptions).map(([key, value]) => (
<RenderColumnOptions
key={key}
columnOptions={columnOptions}
column={value.column}
columns={columns}
value={value.value}
handleColumnOptionChange={handleColumnOptionChange}
darkMode={darkMode}
removeColumnOptionsPair={removeColumnOptionsPair}
id={key}
/>
))}
{Object.keys(columnOptions).length !== columns.length && (
<ButtonSolid
@ -114,7 +115,7 @@ const RenderColumnOptions = ({
const handleColumnChange = (selectedOption) => {
const updatedOption = {
...columnOptions[id],
column: selectedOption,
column: selectedOption.value,
};
const newColumnOptions = { ...columnOptions, [id]: updatedOption };
@ -134,48 +135,15 @@ const RenderColumnOptions = ({
};
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container">
<div className="field col-4 me-3">
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
/>
</div>
<div className="field col-6 mx-1">
<CodeHinter
type="basic"
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
/>
</div>
<div className="col cursor-pointer m-1 mx-3">
<svg
onClick={() => {
removeColumnOptionsPair(id);
}}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</div>
</div>
</div>
<RenderColumnUI
column={column}
displayColumns={displayColumns}
handleColumnChange={handleColumnChange}
darkMode={darkMode}
value={value}
handleValueChange={handleValueChange}
removeColumnOptionsPair={removeColumnOptionsPair}
id={id}
/>
);
};

View file

@ -2,11 +2,12 @@ import React, { useContext } from 'react';
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
import { v4 as uuidv4 } from 'uuid';
import { isEmpty } from 'lodash';
import Select from '@/_ui/Select';
import { operators } from '@/TooljetDatabase/constants';
import { isOperatorOptions } from './util';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import CodeHinter from '@/Editor/CodeEditor';
import CodeHinter from '@/AppBuilder/CodeEditor';
import RenderFilterSectionUI from './RenderFilterSectionUI';
import { NoCondition } from './NoConditionUI';
export const DeleteRows = React.memo(({ darkMode }) => {
const { columns, deleteOperationLimitOptionChanged, deleteRowsOptions, handleDeleteRowsOptionsChange } =
@ -48,23 +49,25 @@ export const DeleteRows = React.memo(({ darkMode }) => {
return (
<div className="tab-content-wrapper tj-db-field-wrapper mt-2">
<div className="d-flex">
<label className="form-label" data-cy="label-column-filter">
<div className="d-flex tooljetdb-worflow-operations">
<label className="form-label flex-shrink-0" data-cy="label-column-filter">
Filter
</label>
<div className="field-container flex-grow-1 mb-2 col">
{Object.values(deleteRowsOptions?.where_filters || {}).map((filter) => (
<RenderFilterFields
key={filter.id}
{...filter}
removeFilterConditionPair={removeFilterConditionPair}
updateFilterOptionsChanged={updateFilterOptionsChanged}
deleteRowsOptions={deleteRowsOptions}
columns={columns}
darkMode={darkMode}
/>
))}
{isEmpty(deleteRowsOptions?.where_filters || {}) && <NoCondition />}
{!isEmpty(deleteRowsOptions?.where_filters || {}) &&
Object.values(deleteRowsOptions?.where_filters || {}).map((filter) => (
<RenderFilterFields
key={filter.id}
{...filter}
removeFilterConditionPair={removeFilterConditionPair}
updateFilterOptionsChanged={updateFilterOptionsChanged}
deleteRowsOptions={deleteRowsOptions}
columns={columns}
darkMode={darkMode}
/>
))}
<ButtonSolid
variant="ghostBlue"
@ -84,11 +87,11 @@ export const DeleteRows = React.memo(({ darkMode }) => {
</ButtonSolid>
</div>
</div>
<div className="field-container d-flex">
<label className="form-label" data-cy="label-column-limit">
<div className="field-container d-flex tooljetdb-worflow-operations delete-limit">
<label className="form-label flex-shrink-0" data-cy="label-column-limit">
Limit
</label>
<div className="field flex-grow-1">
<div className="field flex-grow-1 minw-400-w-400 tjdb-limit-offset-codehinter">
<CodeHinter
type="basic"
initialValue={deleteRowsOptions?.limit ?? 1}
@ -118,12 +121,14 @@ const RenderFilterFields = ({
label: accessor,
}));
operator = operators.find((val) => val.value === operator);
const handleColumnChange = (selectedOption) => {
updateFilterOptionsChanged({ ...deleteRowsOptions?.where_filters[id], ...{ column: selectedOption } });
updateFilterOptionsChanged({ ...deleteRowsOptions?.where_filters[id], ...{ column: selectedOption.value } });
};
const handleOperatorChange = (selectedOption) => {
updateFilterOptionsChanged({ ...deleteRowsOptions?.where_filters[id], ...{ operator: selectedOption } });
updateFilterOptionsChanged({ ...deleteRowsOptions?.where_filters[id], ...{ operator: selectedOption.value } });
};
const handleValueChange = (newValue) => {
@ -131,69 +136,19 @@ const RenderFilterFields = ({
};
return (
<div className="mt-1 row-container w-100">
<div className="d-flex fields-container">
<div className="field" style={{ width: '32%' }}>
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
width="auto"
/>
</div>
<div className="field col mx-1" style={{ width: '32%' }}>
<Select
useMenuPortal={true}
placeholder="Select operation"
value={operator}
options={operators}
onChange={handleOperatorChange}
width="auto"
/>
</div>
<div className="field" style={{ width: '32%' }}>
{operator === 'is' ? (
<Select
useMenuPortal={true}
placeholder="Select value"
value={value}
options={isOperatorOptions}
onChange={handleValueChange}
width="auto"
/>
) : (
<CodeHinter
type="basic"
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
/>
)}
</div>
<div
className="col-1 cursor-pointer m-1 d-flex align-item-center justify-content-center"
style={{ width: '4%' }}
>
<svg
onClick={() => removeFilterConditionPair(id)}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</div>
</div>
</div>
<RenderFilterSectionUI
column={column}
displayColumns={displayColumns}
handleColumnChange={handleColumnChange}
darkMode={darkMode}
operator={operator}
operators={operators}
handleOperatorChange={handleOperatorChange}
value={value}
isOperatorOptions={isOperatorOptions}
handleValueChange={handleValueChange}
removeFilterConditionPair={removeFilterConditionPair}
id={id}
/>
);
};

View file

@ -8,6 +8,7 @@ import CheveronDown from '@/_ui/Icon/bulkIcons/CheveronDown';
import Remove from '@/_ui/Icon/bulkIcons/Remove';
import { v4 as uuidv4 } from 'uuid';
import { isEmpty } from 'lodash';
import { ToolTip } from '@/_components/ToolTip';
const DropDownSelect = ({
darkMode,
@ -79,6 +80,9 @@ const DropDownSelect = ({
const [isInitialForeignKeyDataLoaded, setIsInitialForeignKeyDataLoaded] = useState(false);
const [totalRecords, setTotalRecords] = useState(0);
const [pageNumber, setPageNumber] = useState(1);
//following two states are to determine whether the value is truncated or not to show tooltip
const valueRef = useRef(null);
const [isTruncated, setIsTruncated] = useState(false);
useEffect(() => {
if (shouldCloseFkMenu) {
@ -96,6 +100,12 @@ const DropDownSelect = ({
if (Array.isArray(value) || selected?.value !== value?.value || selected?.label !== value?.label) {
setSelected(value);
}
const element = valueRef.current;
if (element) {
// Check if the text is truncated
const truncated = element.scrollWidth > element.clientWidth;
setIsTruncated(truncated);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
@ -116,6 +126,7 @@ const DropDownSelect = ({
if (isNewOverFlown !== isOverflown) {
setIsOverflown(isNewOverFlown);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selected]);
@ -148,6 +159,24 @@ const DropDownSelect = ({
return true;
}
const fetchTooltipMessageForDropdownSelected = (selected) => {
let message = '';
if (Array.isArray(selected)) {
message = selected.reduce((accumulator, value) => {
if (accumulator) {
accumulator = `${accumulator}, ${value?.label || ''}`;
} else {
accumulator = value?.label || '';
}
return accumulator;
}, '');
console.log('manish --->', { selected, message });
return message;
}
message = selected?.label || '';
return message;
};
return (
<OverlayTrigger
show={showMenu && !disabled}
@ -266,7 +295,7 @@ const DropDownSelect = ({
background: (selected.label === null || selected.label === undefined) && 'var(--slate3)',
}}
>
{selected.label === null || selected.label === undefined ? 'Null' : selected.label}
{selected?.label ?? 'Null'}
</p>
</div>
) : (
@ -301,51 +330,56 @@ const DropDownSelect = ({
)}
data-cy={`show-ds-popover-button`}
>
<div className={`text-truncate`}>
{renderSelected && renderSelected(selected)}
<>
{!renderSelected && isValidInput(selected) ? (
Array.isArray(selected) ? (
!isOverflown && (
<MultiSelectValueBadge
options={options}
selected={selected}
setSelected={setSelected}
onChange={onChange}
/>
<ToolTip
message={fetchTooltipMessageForDropdownSelected(selected)}
show={!!fetchTooltipMessageForDropdownSelected(selected) && isTruncated}
>
<div className={`text-truncate`} ref={valueRef}>
{renderSelected && renderSelected(selected)}
<>
{!renderSelected && isValidInput(selected) ? (
Array.isArray(selected) ? (
!isOverflown && (
<MultiSelectValueBadge
options={options}
selected={selected}
setSelected={setSelected}
onChange={onChange}
/>
)
) : (
selected?.label
)
) : (
selected?.label
)
) : showPlaceHolder ? (
<span style={{ color: '#9e9e9e', fontSize: '12px', fontWeight: '400', lineHeight: '20px' }}>
{foreignKeyAccessInRowForm || showPlaceHolderInForeignKeyDrawer
? topPlaceHolder
: placeholder
? placeholder
: 'Select...'}
</span>
) : (
''
)}
{!renderSelected && isOverflown && !Array.isArray(selected) && (
<Badge className="me-1 dd-select-value-badge" bg="secondary">
{selected?.length} selected
<span
role="button"
onClick={(e) => {
setSelected([]);
onChange && onChange([]);
e.preventDefault();
e.stopPropagation();
}}
>
<Remove fill="var(--slate12)" width="12px" />
) : showPlaceHolder ? (
<span style={{ color: '#9e9e9e', fontSize: '12px', fontWeight: '400', lineHeight: '20px' }}>
{foreignKeyAccessInRowForm || showPlaceHolderInForeignKeyDrawer
? topPlaceHolder
: placeholder
? placeholder
: 'Select...'}
</span>
</Badge>
)}
</>
</div>
) : (
''
)}
{!renderSelected && isOverflown && !Array.isArray(selected) && (
<Badge className="me-1 dd-select-value-badge" bg="secondary">
{selected?.length} selected
<span
role="button"
onClick={(e) => {
setSelected([]);
onChange && onChange([]);
e.preventDefault();
e.stopPropagation();
}}
>
<Remove fill="var(--slate12)" width="12px" />
</span>
</Badge>
)}
</>
</div>
</ToolTip>
<div className="dd-select-control-chevron">
<CheveronDown width="15" height="15" />
</div>

View file

@ -1,12 +1,12 @@
import React, { useContext } from 'react';
import { Col, Container, Row } from 'react-bootstrap';
import { Col, Row } from 'react-bootstrap';
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
import DropDownSelect from './DropDownSelect';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import Trash from '@/_ui/Icon/solidIcons/Trash';
import AddRectangle from '@/_ui/Icon/bulkIcons/AddRectangle';
import { isEmpty } from 'lodash';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { NoCondition } from './NoConditionUI';
export default function JoinSort({ darkMode }) {
const { tableInfo, joinOrderByOptions, setJoinOrderByOptions, joinOptions, findTableDetails } =
@ -53,29 +53,17 @@ export default function JoinSort({ darkMode }) {
];
return (
<Container fluid className="p-0">
<div className="p-0">
{isEmpty(joinOrderByOptions) ? (
<Row className="mb-2 mx-0">
<div
style={{
gap: '4px',
height: '30px',
border: '1px dashed var(--slate-08, #C1C8CD)',
}}
className="px-4 py-2 text-center rounded-1"
>
<SolidIcon name="information" style={{ height: 14, width: 14 }} width={14} height={14} /> There are no
conditions
</div>
</Row>
<NoCondition />
) : (
joinOrderByOptions.map((options, i) => {
const tableDetails = options?.table ? findTableDetails(options?.table) : '';
return (
<Row className="mb-2 mx-0" key={i}>
<Row className="mb-2 mx-0 " key={i}>
<Col sm="6" className="p-0">
<DropDownSelect
buttonClasses="border border-end-0 rounded-start"
buttonClasses="border border-end-0 rounded-start overflow-hidden"
showPlaceHolder
options={tableList}
darkMode={darkMode}
@ -105,7 +93,7 @@ export default function JoinSort({ darkMode }) {
<Col sm="6" className="p-0 d-flex">
<div className="flex-grow-1">
<DropDownSelect
buttonClasses="border border-end-0"
buttonClasses="border border-end-0 overflow-hidden"
showPlaceHolder
options={sortbyConstants}
darkMode={darkMode}
@ -150,6 +138,6 @@ export default function JoinSort({ darkMode }) {
</ButtonSolid>
</Col>
</Row>
</Container>
</div>
);
}

View file

@ -9,10 +9,10 @@ import DropDownSelect from './DropDownSelect';
import JoinConstraint from './JoinConstraint';
import JoinSelect from './JoinSelect';
import JoinSort from './JoinSort';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { filterOperatorOptions, nullOperatorOptions } from './util';
import CodeHinter from '@/Editor/CodeEditor';
import { AggregateFilter } from './AggregateUI';
import { NoCondition } from './NoConditionUI';
export const JoinTable = React.memo(({ darkMode }) => {
return (
@ -95,6 +95,8 @@ const SelectTableMenu = ({ darkMode }) => {
return isGroupByUsed || !isEmpty(aggregates) ? false : true;
}, [joinTableOptions]);
const { joinOrderByOptions } = useContext(TooljetDatabaseContext);
return (
<div>
{/* Join Section */}
@ -150,16 +152,16 @@ const SelectTableMenu = ({ darkMode }) => {
</div>
</div>
{/* Sort Section */}
<div className="field-container d-flex" style={{ marginBottom: '1.5rem' }}>
<div className="field-container tooljetdb-worflow-operations d-flex" style={{ marginBottom: '1.5rem' }}>
<label className="form-label flex-shrink-0">Sort</label>
<div className="field flex-grow-1">
<div className={`field flex-grow-1 ${!isEmpty(joinOrderByOptions) && 'minw-400-w-400'} `}>
<JoinSort darkMode={darkMode} />
</div>
</div>
{/* Limit Section */}
<div className="field-container d-flex" style={{ marginBottom: '1.5rem' }}>
<div className="field-container tooljetdb-worflow-operations d-flex" style={{ marginBottom: '1.5rem' }}>
<label className="form-label flex-shrink-0">Limit</label>
<div className="field flex-grow-1">
<div className="field flex-grow-1 minw-400-w-400 tjdb-limit-offset-codehinter">
<CodeHinter
type="basic"
className="tjdb-codehinter border rounded"
@ -177,9 +179,9 @@ const SelectTableMenu = ({ darkMode }) => {
</div>
</div>
{/* Offset Section */}
<div className="field-container d-flex" style={{ marginBottom: '1.5rem' }}>
<div className="field-container tooljetdb-worflow-operations d-flex" style={{ marginBottom: '1.5rem' }}>
<label className="form-label flex-shrink-0">Offset</label>
<div className="field flex-grow-1">
<div className="field flex-grow-1 minw-400-w-400 tjdb-limit-offset-codehinter">
<CodeHinter
className="tjdb-codehinter border rounded"
placeholder="Enter offset"
@ -197,7 +199,7 @@ const SelectTableMenu = ({ darkMode }) => {
</div>
{/* Select Section */}
{showSelectSection() && (
<div className="field-container d-flex" style={{ marginBottom: '1.5rem' }}>
<div className="field-container tooljetdb-worflow-operations d-flex" style={{ marginBottom: '1.5rem' }}>
<label className="form-label flex-shrink-0">Select</label>
<div className="field flex-grow-1">
<JoinSelect darkMode={darkMode} />
@ -432,7 +434,7 @@ const RenderFilterSection = ({ darkMode }) => {
darkMode={darkMode}
/>
</Col>
<Col sm="3" className="p-0">
<Col sm="2" className="p-0">
<DropDownSelect
buttonClasses="border border-end-0"
showPlaceHolder
@ -443,7 +445,7 @@ const RenderFilterSection = ({ darkMode }) => {
/>
</Col>
<Col className="p-0 d-flex">
<div className="col-10">
<div className="col-11 tjdb-codhinter-wrapper">
{operator === 'IS' ? (
<DropDownSelect
buttonClasses="border border-end-0"
@ -477,6 +479,7 @@ const RenderFilterSection = ({ darkMode }) => {
<ButtonSolid
customStyles={{
height: '30px',
maxWidth: '30px',
}}
size="sm"
variant="ghostBlack"
@ -492,21 +495,7 @@ const RenderFilterSection = ({ darkMode }) => {
return (
<Container fluid className="p-0">
{conditionsList.length === 0 && (
<Row className="mb-2 mx-0">
<div
style={{
gap: '4px',
height: '30px',
border: '1px dashed var(--slate-08, #C1C8CD)',
}}
className="px-4 py-2 text-center rounded-1"
>
<SolidIcon name="information" style={{ height: 14, width: 14 }} width={14} height={14} /> There are no
conditions
</div>
</Row>
)}
{conditionsList.length === 0 && <NoCondition />}
{filterComponents}
<Row className="mx-1 mb-1">
<Col className="p-0">

View file

@ -2,12 +2,14 @@ import React, { useContext } from 'react';
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
import { v4 as uuidv4 } from 'uuid';
import { isEmpty } from 'lodash';
import Select from '@/_ui/Select';
import { operators } from '@/TooljetDatabase/constants';
import { isOperatorOptions } from './util';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import CodeHinter from '@/Editor/CodeEditor';
import CodeHinter from '@/AppBuilder/CodeEditor';
import { AggregateFilter } from './AggregateUI';
import RenderFilterSectionUI from './RenderFilterSectionUI';
import RenderSortUI from './RenderSortUI';
import { NoCondition } from './NoConditionUI';
export const ListRows = React.memo(({ darkMode }) => {
const { columns, listRowsOptions, limitOptionChanged, handleOptionsChange, offsetOptionChanged } =
@ -86,22 +88,24 @@ export const ListRows = React.memo(({ darkMode }) => {
<div className="row my-2 tj-db-field-wrapper">
<div className="tab-content-wrapper">
<AggregateFilter darkMode={darkMode} operation="listRows" />
<div className="d-flex mb-2">
<label className="form-label" data-cy="label-column-filter">
<div className="d-flex tooljetdb-worflow-operations mb-2">
<label className="form-label flex-shrink-0" data-cy="label-column-filter">
Filter
</label>
<div className="field-container col">
{Object.values(listRowsOptions?.where_filters || {}).map((filter) => (
<RenderFilterFields
key={filter.id}
{...filter}
columns={columns}
listRowsOptions={listRowsOptions}
updateFilterOptionsChanged={updateFilterOptionsChanged}
darkMode={darkMode}
removeFilterConditionPair={removeFilterConditionPair}
/>
))}
<div className="field-container flex-grow-1">
{isEmpty(listRowsOptions?.where_filters || {}) && <NoCondition />}
{!isEmpty(listRowsOptions.where_filters) &&
Object.values(listRowsOptions?.where_filters || {}).map((filter) => (
<RenderFilterFields
key={filter.id}
{...filter}
columns={columns}
listRowsOptions={listRowsOptions}
updateFilterOptionsChanged={updateFilterOptionsChanged}
darkMode={darkMode}
removeFilterConditionPair={removeFilterConditionPair}
/>
))}
<ButtonSolid
variant="ghostBlue"
@ -123,21 +127,26 @@ export const ListRows = React.memo(({ darkMode }) => {
</div>
{/* sort */}
<div className="fields-container d-flex mb-2">
<label className="form-label" data-cy="label-column-sort">
<div className="fields-container tooljetdb-worflow-operations d-flex mb-2">
<label className="form-label flex-shrink-0" data-cy="label-column-sort">
Sort
</label>
<div className="field-container flex-grow-1">
{Object.values(listRowsOptions?.order_filters || {}).map((filter) => (
<RenderSortFields
key={filter.id}
{...filter}
removeSortConditionPair={removeSortConditionPair}
listRowsOptions={listRowsOptions}
columns={columns}
updateSortOptionsChanged={updateSortOptionsChanged}
/>
))}
<div
className={`field-container flex-grow-1 ${!isEmpty(listRowsOptions?.order_filters) && 'minw-400-w-400'} `}
>
{isEmpty(listRowsOptions?.order_filters || {}) && <NoCondition />}
{!isEmpty(listRowsOptions?.order_filters) &&
Object.values(listRowsOptions?.order_filters || {}).map((filter) => (
<RenderSortFields
key={filter.id}
{...filter}
removeSortConditionPair={removeSortConditionPair}
listRowsOptions={listRowsOptions}
columns={columns}
updateSortOptionsChanged={updateSortOptionsChanged}
darkMode={darkMode}
/>
))}
<ButtonSolid
variant="ghostBlue"
size="sm"
@ -158,11 +167,11 @@ export const ListRows = React.memo(({ darkMode }) => {
</div>
{/* Limit */}
<div className="field-container d-flex mb-2">
<label className="form-label" data-cy="label-column-limit">
<div className="field-container tooljetdb-worflow-operations d-flex mb-2">
<label className="form-label flex-shrink-0" data-cy="label-column-limit">
Limit
</label>
<div className="field flex-grow-1">
<div className="field flex-grow-1 minw-400-w-400 tjdb-limit-offset-codehinter">
<CodeHinter
type="basic"
initialValue={listRowsOptions?.limit ?? ''}
@ -173,11 +182,11 @@ export const ListRows = React.memo(({ darkMode }) => {
</div>
</div>
{/* Offset */}
<div className="field-container d-flex">
<label className="form-label" data-cy="label-column-offset">
<div className="field-container tooljetdb-worflow-operations d-flex">
<label className="form-label flex-shrink-0" data-cy="label-column-offset">
Offset
</label>
<div className="field flex-grow-1">
<div className="field flex-grow-1 minw-400-w-400 tjdb-limit-offset-codehinter">
<CodeHinter
type="basic"
initialValue={listRowsOptions?.offset ?? ''}
@ -201,11 +210,15 @@ const RenderSortFields = ({
listRowsOptions,
columns,
updateSortOptionsChanged,
darkMode,
}) => {
const orders = [
{ value: 'asc', label: 'Ascending' },
{ value: 'desc', label: 'Descending' },
];
order = orders.find((val) => val.value === order);
const existingColumnOptions = Object.values(listRowsOptions?.order_filters).map((item) => item.column);
let displayColumns = columns.map(({ accessor }) => ({
value: accessor,
@ -219,53 +232,25 @@ const RenderSortFields = ({
}
const handleColumnChange = (selectedOption) => {
updateSortOptionsChanged({ ...listRowsOptions?.order_filters[id], ...{ column: selectedOption } });
updateSortOptionsChanged({ ...listRowsOptions?.order_filters[id], ...{ column: selectedOption.value } });
};
const handleDirectionChange = (selectedOption) => {
updateSortOptionsChanged({ ...listRowsOptions?.order_filters[id], ...{ order: selectedOption } });
updateSortOptionsChanged({ ...listRowsOptions?.order_filters[id], ...{ order: selectedOption.value } });
};
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container mb-2">
<div className="field col">
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
/>
</div>
<div className="field col mx-1">
<Select
useMenuPortal={true}
placeholder="Select direction"
value={order}
options={orders}
onChange={handleDirectionChange}
/>
</div>
<div className="col cursor-pointer m-1 ms-1">
<svg
onClick={() => removeSortConditionPair(id)}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</div>
</div>
</div>
<RenderSortUI
column={column}
displayColumns={displayColumns}
handleColumnChange={handleColumnChange}
darkMode={darkMode}
order={order}
orders={orders}
handleDirectionChange={handleDirectionChange}
removeSortConditionPair={removeSortConditionPair}
id={id}
/>
);
};
@ -284,13 +269,14 @@ const RenderFilterFields = ({
value: accessor,
label: accessor,
}));
operator = operators.find((val) => val.value === operator);
const handleColumnChange = (selectedOption) => {
updateFilterOptionsChanged({ ...listRowsOptions?.where_filters[id], ...{ column: selectedOption } });
updateFilterOptionsChanged({ ...listRowsOptions?.where_filters[id], ...{ column: selectedOption.value } });
};
const handleOperatorChange = (selectedOption) => {
updateFilterOptionsChanged({ ...listRowsOptions?.where_filters[id], ...{ operator: selectedOption } });
updateFilterOptionsChanged({ ...listRowsOptions?.where_filters[id], ...{ operator: selectedOption.value } });
};
const handleValueChange = (newValue) => {
@ -298,71 +284,19 @@ const RenderFilterFields = ({
};
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container ">
<div className="field" style={{ width: '32%' }}>
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
// useCustomStyles
// styles={{ container: (styles) => ({ width: 'auto', ...styles }) }}
width={'auto'}
/>
</div>
<div className="field mx-1" style={{ width: '32%' }}>
<Select
useMenuPortal={true}
placeholder="Select operation"
value={operator}
options={operators}
onChange={handleOperatorChange}
width={'auto'}
/>
</div>
<div className="field" style={{ width: '32%' }}>
{operator === 'is' ? (
<Select
useMenuPortal={true}
placeholder="Select value"
value={value}
options={isOperatorOptions}
onChange={handleValueChange}
width={'auto'}
/>
) : (
<CodeHinter
type="basic"
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
/>
)}
</div>
<div
className="col-1 cursor-pointer m-1 d-flex align-item-center justify-content-center"
style={{ width: '4%' }}
>
<svg
onClick={() => removeFilterConditionPair(id)}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</div>
</div>
</div>
<RenderFilterSectionUI
column={column}
displayColumns={displayColumns}
handleColumnChange={handleColumnChange}
darkMode={darkMode}
operator={operator}
operators={operators}
handleOperatorChange={handleOperatorChange}
value={value}
isOperatorOptions={isOperatorOptions}
handleValueChange={handleValueChange}
removeFilterConditionPair={removeFilterConditionPair}
id={id}
/>
);
};

View file

@ -1,11 +1,11 @@
import React from 'react';
import Information from '@/_ui/Icon/solidIcons/Information';
export const NoCondition = () => {
export const NoCondition = ({ text = 'There are no condition' }) => {
return (
<div className="border-dashed d-flex justify-content-center align-items-center h-32 border-radius-6">
<Information width="14" />
<span className="tj-text-sm" style={{ color: 'var(--slate11)' }}>
There are no condition
{text}
</span>
</div>
);

View file

@ -0,0 +1,60 @@
import CodeHinter from '@/Editor/CodeEditor';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import Trash from '@/_ui/Icon/solidIcons/Trash';
import React from 'react';
import { Col, Container, Row } from 'react-bootstrap';
import DropDownSelect from './DropDownSelect';
const RenderColumnUI = ({
column,
displayColumns,
handleColumnChange,
darkMode,
value,
handleValueChange,
removeColumnOptionsPair,
id,
}) => {
column = typeof column === 'object' && column !== null ? column : { label: column, value: column };
return (
<div className="">
<Container fluid className="p-0">
<Row className="mb-2 mx-0">
<Col sm="6" className="p-0">
<DropDownSelect
useMenuPortal={true}
showPlaceHolder
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
darkMode={darkMode}
buttonClasses="border border-end-0 rounded-start overflow-hidden"
/>
</Col>
<Col sm="6" className="p-0 d-flex tjdb-codhinter-wrapper">
<CodeHinter
type="basic"
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
/>
<ButtonSolid
size="sm"
variant="ghostBlack"
className="px-1 rounded-0 border rounded-end"
customStyles={{
height: '30px',
}}
onClick={() => removeColumnOptionsPair(id)}
>
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
</ButtonSolid>
</Col>
</Row>
</Container>
</div>
);
};
export default RenderColumnUI;

View file

@ -0,0 +1,114 @@
import CodeHinter from '@/Editor/CodeEditor';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import Trash from '@/_ui/Icon/solidIcons/Trash';
import React from 'react';
import { Col, Container, Row } from 'react-bootstrap';
import DropDownSelect from './DropDownSelect';
const RenderFilterSectionUI = ({
column,
displayColumns,
handleColumnChange,
darkMode,
operator,
operators,
handleOperatorChange,
value,
isOperatorOptions,
handleValueChange,
removeFilterConditionPair,
id,
}) => {
column = typeof column === 'object' && column !== null ? column : { label: column, value: column };
operator = typeof operator === 'object' && operator !== null ? operator : { label: operator, value: operator };
const valueForDropdown = typeof value === 'object' && value !== null ? value : { label: value, value: value };
return (
<div className="">
<Container fluid className="p-0">
<Row className="mb-2 mx-0">
<Col sm="4" className="p-0">
<DropDownSelect
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
// width={'auto'}
buttonClasses="border border-end-0 rounded-start overflow-hidden"
showPlaceHolder
darkMode={darkMode}
isMulti={false}
/>
</Col>
<Col sm="4" className="p-0">
<DropDownSelect
useMenuPortal={true}
placeholder="Select operation"
value={operator}
options={operators}
onChange={handleOperatorChange}
// width={'auto'}
buttonClasses="border border-end-0 overflow-hidden"
showPlaceHolder
darkMode={darkMode}
/>
</Col>
<Col sm="4" className="p-0 tjdb-codhinter-wrapper d-flex">
<div style={{ width: 'calc(100% - 35px)' }}>
{operator === 'is' ? (
<DropDownSelect
useMenuPortal={true}
placeholder="Select value"
// value={{ label: value, value: value }}
value={valueForDropdown}
options={isOperatorOptions}
onChange={handleValueChange}
// width={'auto'}
buttonClasses="border border-end-0 rounded-start overflow-hidden"
showPlaceHolder
darkMode={darkMode}
/>
) : (
<CodeHinter
type="basic"
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
height="28"
/>
)}
</div>
<ButtonSolid
size="sm"
variant="ghostBlack"
className="px-1 rounded-0 border rounded-end"
customStyles={{
height: '30px',
}}
onClick={() => removeFilterConditionPair(id)}
>
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
</ButtonSolid>
</Col>
{/* <Col sm="content" className="p-0">
<ButtonSolid
size="sm"
variant="ghostBlack"
className="px-1 rounded-0 border rounded-end"
customStyles={{
height: '30px',
}}
onClick={() => removeFilterConditionPair(id)}
>
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
</ButtonSolid>
</Col> */}
</Row>
</Container>
</div>
);
};
export default RenderFilterSectionUI;

View file

@ -0,0 +1,69 @@
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import Trash from '@/_ui/Icon/solidIcons/Trash';
import React from 'react';
import { Col, Container, Row } from 'react-bootstrap';
import DropDownSelect from './DropDownSelect';
const RenderSortUI = ({
column,
displayColumns,
handleColumnChange,
darkMode,
order,
orders,
handleDirectionChange,
removeSortConditionPair,
id,
}) => {
column = typeof column === 'object' && column !== null ? column : { label: column, value: column };
order = typeof order === 'object' && order !== null ? order : { label: order, value: order };
return (
<div className="">
<Container fluid className="p-0 ">
<Row className="mb-2 mx-0 ">
<Col sm="6" className="p-0">
<DropDownSelect
buttonClasses="border border-end-0 rounded-start overflow-hidden"
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
showPlaceHolder
width="auto"
darkMode={darkMode}
/>
</Col>
<Col sm="6" className="p-0 d-flex">
<div className="flex-grow-1">
<DropDownSelect
buttonClasses="border border-end-0 overflow-hidden"
useMenuPortal={true}
placeholder="Select direction"
value={order}
options={orders}
onChange={handleDirectionChange}
showPlaceHolder
darkMode={darkMode}
width="auto"
/>
</div>
<ButtonSolid
size="sm"
variant="ghostBlack"
className="px-1 rounded-0 border rounded-end"
customStyles={{
height: '30px',
}}
onClick={() => removeSortConditionPair(id)}
>
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
</ButtonSolid>
</Col>
</Row>
</Container>
</div>
);
};
export default RenderSortUI;

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useState, useEffect, useMemo, useRef } from 'react';
import cx from 'classnames';
import { tooljetDatabaseService, authenticationService } from '@/_services';
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
@ -16,10 +16,15 @@ import { getPrivateRoute } from '@/_helpers/routes';
import { useNavigate } from 'react-router-dom';
import { deepClone } from '@/_helpers/utilities/utils.helpers';
const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLayout }) => {
import './styles.scss';
import CodeHinter from '@/Editor/CodeEditor';
import { useCurrentState } from '@/_stores/currentStateStore';
const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLayout, optionsChanged }) => {
const computeSelectStyles = (darkMode, width) => {
return queryManagerSelectComponentStyle(darkMode, width);
};
const currentState = useCurrentState();
const navigate = useNavigate();
const { current_organization_id: organizationId } = authenticationService.currentSessionValue;
const mounted = useMounted();
@ -27,6 +32,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
const [columns, setColumns] = useState([]);
const [tables, setTables] = useState([]);
const [tableInfo, setTableInfo] = useState({});
const [activeTab, setActiveTab] = useState(options?.activeTab || 'GUI mode');
const [selectedTableId, setSelectedTableId] = useState(options['table_id']);
const [listRowsOptions, setListRowsOptions] = useState(() => options['list_rows'] || {});
const [updateRowsOptions, setUpdateRowsOptions] = useState(
@ -464,6 +470,25 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
});
};
//Following ref is responsible to hold the value of prev operation while shifting between the active tabs
const [prevOperationBetweenModeChange, setPrevOperationBetweenModeChange] = useState(null);
const handleTabClick = (mode) => {
const optionsToUpdate = {
activeTab: mode,
};
if (mode === 'SQL mode') {
// prevOperationBetweenModeChange.current = options?.operation;
setPrevOperationBetweenModeChange(options?.operation || '');
optionsToUpdate['operation'] = 'sql_execution';
optionsToUpdate['organization_id'] = organizationId;
} else {
optionsToUpdate['operation'] = prevOperationBetweenModeChange ?? '';
}
optionsChanged(optionsToUpdate);
setActiveTab(mode);
};
const getComponent = () => {
switch (operation) {
case 'list_rows':
@ -492,65 +517,154 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
return (
<TooljetDatabaseContext.Provider value={value}>
{/* table name dropdown */}
<div className={cx({ row: !isHorizontalLayout })}>
<div className={cx({ 'col-4': !isHorizontalLayout, 'd-flex': isHorizontalLayout })}>
<label className={cx('form-label', 'flex-shrink-0')}>Table name</label>
<div
className={cx(
{ 'flex-grow-1': isHorizontalLayout },
'border',
'rounded',
'overflow-hidden',
'minw-400px-maxw-45perc'
)}
>
<DropDownSelect
customBorder={false}
showPlaceHolder
options={generateListForDropdown(tables)}
darkMode={darkMode}
onChange={(value) => {
value?.value && handleTableNameSelect(value?.value);
}}
onAdd={() => triggerTooljetDBStatus('addTJDBTable')}
addBtnLabel={'Add new table'}
value={generateListForDropdown(tables).find((val) => val?.value === selectedTableId)}
/>
</div>
</div>
</div>
{/* operation selection dropdown */}
<div className={cx('my-3 py-1', { row: !isHorizontalLayout })}>
<div className={cx({ 'col-4': !isHorizontalLayout, 'd-flex tooljetdb-worflow-operations': isHorizontalLayout })}>
<label className={cx('form-label', 'flex-shrink-0')}>Mode</label>
<div
/* className="my-2 col-4" */
className={cx({ 'col-4': !isHorizontalLayout, 'd-flex': isHorizontalLayout })}
className={cx('d-flex align-items-center justify-content-start gap-2', {
'row-tabs-dark': darkMode,
'row-tabs': !darkMode,
})}
>
<label className={cx('form-label', 'flex-shrink-0')}>Operations</label>
<div
className={cx(
{ 'flex-grow-1': isHorizontalLayout },
'border',
'rounded',
'overflow-hidden',
'minw-400px-maxw-45perc'
)}
onClick={() => handleTabClick('GUI mode')}
style={{
backgroundColor:
activeTab === 'GUI mode' && !darkMode
? 'white'
: activeTab === 'GUI mode' && darkMode
? '#242f3c'
: 'transparent',
color:
activeTab === 'GUI mode' && !darkMode
? '#3E63DD'
: activeTab === 'GUI mode' && darkMode
? 'white'
: '#687076',
}}
className="row-tab-content"
>
<DropDownSelect
showPlaceHolder
options={tooljetDbOperationList}
darkMode={darkMode}
onChange={(value) => {
value?.value && setOperation(value?.value);
}}
value={tooljetDbOperationList.find((val) => val?.value === operation)}
/>
GUI mode
</div>
<div
onClick={() => handleTabClick('SQL mode')}
style={{
backgroundColor:
activeTab === 'SQL mode' && !darkMode
? 'white'
: activeTab === 'SQL mode' && darkMode
? '#242f3c'
: 'transparent',
color:
activeTab === 'SQL mode' && !darkMode
? '#3E63DD'
: activeTab === 'SQL mode' && darkMode
? 'white'
: '#687076',
}}
className="row-tab-content"
>
SQL mode
</div>
</div>
</div>
{/* component to render based on the operation */}
{ComponentToRender && <ComponentToRender options={options} optionchanged={optionchanged} darkMode={darkMode} />}
{activeTab === 'GUI mode' && (
<>
<div className={cx({ row: !isHorizontalLayout, 'my-3': isHorizontalLayout })}>
<div
className={cx({
'col-4': !isHorizontalLayout,
'd-flex tooljetdb-worflow-operations': isHorizontalLayout,
})}
>
<label className={cx('form-label', 'flex-shrink-0')}>Table name</label>
<div
className={cx(
{ 'flex-grow-1': isHorizontalLayout },
'border',
'rounded',
'overflow-hidden',
'minw-400-w-400'
)}
>
<DropDownSelect
customBorder={false}
showPlaceHolder
options={generateListForDropdown(tables)}
darkMode={darkMode}
onChange={(value) => {
value?.value && handleTableNameSelect(value?.value);
}}
onAdd={() => navigate(getPrivateRoute('database'))}
addBtnLabel={'Add new table'}
value={generateListForDropdown(tables).find((val) => val?.value === selectedTableId)}
/>
</div>
</div>
</div>
{/* operation selection dropdown */}
<div className={cx('my-3 py-1', { row: !isHorizontalLayout })}>
<div
/* className="my-2 col-4" */
className={cx({
'col-4': !isHorizontalLayout,
'd-flex tooljetdb-worflow-operations': isHorizontalLayout,
})}
>
<label className={cx('form-label', 'flex-shrink-0')}>Operations</label>
<div
className={cx(
{ 'flex-grow-1': isHorizontalLayout },
'border',
'rounded',
'overflow-hidden',
'minw-400-w-400'
)}
>
<DropDownSelect
showPlaceHolder
options={tooljetDbOperationList}
darkMode={darkMode}
onChange={(value) => {
value?.value && setOperation(value?.value);
}}
value={tooljetDbOperationList.find((val) => val?.value === operation)}
/>
</div>
</div>
</div>
{/* component to render based on the operation */}
{ComponentToRender && (
<ComponentToRender
currentState={currentState}
s
options={options}
optionchanged={optionchanged}
darkMode={darkMode}
/>
)}
</>
)}
{activeTab === 'SQL mode' && (
<div className={cx('mt-3', { 'col-4': !isHorizontalLayout, 'd-flex': isHorizontalLayout })}>
<label className="form-label flex-shrink-0" style={{ minWidth: '100px' }}></label>
<CodeHinter
type="multiline"
initialValue={options?.sql_execution?.sqlQuery ?? 'SELECT * from users'}
lang="sql"
height={150}
onChange={(value) => {
optionchanged('sql_execution', { sqlQuery: value });
}}
componentName="TooljetDatabase"
delayOnChange={false}
/>
</div>
)}
</TooljetDatabaseContext.Provider>
);
};

View file

@ -1,12 +1,13 @@
import React, { useContext } from 'react';
import { TooljetDatabaseContext } from '@/TooljetDatabase/index';
import Select from '@/_ui/Select';
import { operators } from '@/TooljetDatabase/constants';
import { v4 as uuidv4 } from 'uuid';
import { isEmpty } from 'lodash';
import { isOperatorOptions } from './util';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import CodeHinter from '@/Editor/CodeEditor';
import RenderFilterSectionUI from './RenderFilterSectionUI';
import RenderColumnUI from './RenderColumnUI';
import { NoCondition } from './NoConditionUI';
export const UpdateRows = React.memo(({ darkMode }) => {
const { columns, updateRowsOptions, handleUpdateRowsOptionsChange } = useContext(TooljetDatabaseContext);
@ -74,23 +75,25 @@ export const UpdateRows = React.memo(({ darkMode }) => {
return (
<div className="tab-content-wrapper tj-db-field-wrapper mt-2">
<div className="d-flex mb-2">
<label className="form-label" data-cy="label-column-filter">
<div className="d-flex tooljetdb-worflow-operations mb-2">
<label className="form-label flex-shrink-0" data-cy="label-column-filter">
Filter
</label>
<div className="field-container flex-grow-1 col">
{Object.values(updateRowsOptions?.where_filters || {}).map((filter) => (
<RenderFilterFields
key={filter.id}
{...filter}
columns={columns}
updateFilterOptionsChanged={updateFilterOptionsChanged}
updateRowsOptions={updateRowsOptions}
darkMode={darkMode}
removeFilterConditionPair={removeFilterConditionPair}
/>
))}
{isEmpty(updateRowsOptions?.where_filters || {}) && <NoCondition />}
{!isEmpty(updateRowsOptions?.where_filters) &&
Object.values(updateRowsOptions?.where_filters || {}).map((filter) => (
<RenderFilterFields
key={filter.id}
{...filter}
columns={columns}
updateFilterOptionsChanged={updateFilterOptionsChanged}
updateRowsOptions={updateRowsOptions}
darkMode={darkMode}
removeFilterConditionPair={removeFilterConditionPair}
/>
))}
<ButtonSolid
variant="ghostBlue"
@ -111,26 +114,28 @@ export const UpdateRows = React.memo(({ darkMode }) => {
</div>
</div>
<div className="fields-container d-flex">
<label className="form-label" data-cy="label-column-filter">
<div className="fields-container d-flex tooljetdb-worflow-operations">
<label className="form-label flex-shrink-0" data-cy="label-column-filter">
Columns
</label>
<div className="field-container flex-grow-1 col">
{Object.entries(updateRowsOptions?.columns).map(([key, value]) => {
return (
<RenderColumnOptions
key={key}
column={value.column}
value={value.value}
id={key}
columns={columns}
updateRowsOptions={updateRowsOptions}
handleColumnOptionChange={handleColumnOptionChange}
darkMode={darkMode}
removeColumnOptionsPair={removeColumnOptionsPair}
/>
);
})}
<div className={`field-container flex-grow-1 ${!isEmpty(updateRowsOptions?.columns) && 'minw-400-w-400'}`}>
{isEmpty(updateRowsOptions?.columns) && <NoCondition text="There are no columns" />}
{!isEmpty(updateRowsOptions?.columns) &&
Object.entries(updateRowsOptions?.columns).map(([key, value]) => {
return (
<RenderColumnOptions
key={key}
column={value.column}
value={value.value}
id={key}
columns={columns}
updateRowsOptions={updateRowsOptions}
handleColumnOptionChange={handleColumnOptionChange}
darkMode={darkMode}
removeColumnOptionsPair={removeColumnOptionsPair}
/>
);
})}
{Object.keys(updateRowsOptions?.columns).length !== columns.length && (
<ButtonSolid
@ -170,12 +175,14 @@ const RenderFilterFields = ({
label: accessor,
}));
operator = operators.find((val) => val.value === operator);
const handleColumnChange = (selectedOption) => {
updateFilterOptionsChanged({ ...updateRowsOptions?.where_filters[id], ...{ column: selectedOption } });
updateFilterOptionsChanged({ ...updateRowsOptions?.where_filters[id], ...{ column: selectedOption.value } });
};
const handleOperatorChange = (selectedOption) => {
updateFilterOptionsChanged({ ...updateRowsOptions?.where_filters[id], ...{ operator: selectedOption } });
updateFilterOptionsChanged({ ...updateRowsOptions?.where_filters[id], ...{ operator: selectedOption.value } });
};
const handleValueChange = (newValue) => {
@ -183,70 +190,20 @@ const RenderFilterFields = ({
};
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container ">
<div className="field" style={{ width: '32%' }}>
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
width="auto"
/>
</div>
<div className="field mx-1" style={{ width: '32%' }}>
<Select
useMenuPortal={true}
placeholder="Select operation"
value={operator}
options={operators}
onChange={handleOperatorChange}
width="auto"
/>
</div>
<div className="field" style={{ width: '32%' }}>
{operator === 'is' ? (
<Select
useMenuPortal={true}
placeholder="Select value"
value={value}
options={isOperatorOptions}
onChange={handleValueChange}
width="auto"
/>
) : (
<CodeHinter
type="basic"
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
/>
)}
</div>
<div
className="col-1 cursor-pointer m-1 d-flex align-item-center justify-content-center"
style={{ width: '4%' }}
>
<svg
onClick={() => removeFilterConditionPair(id)}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</div>
</div>
</div>
<RenderFilterSectionUI
column={column}
displayColumns={displayColumns}
handleColumnChange={handleColumnChange}
darkMode={darkMode}
operator={operator}
operators={operators}
handleOperatorChange={handleOperatorChange}
value={value}
isOperatorOptions={isOperatorOptions}
handleValueChange={handleValueChange}
removeFilterConditionPair={removeFilterConditionPair}
id={id}
/>
);
};
@ -277,7 +234,7 @@ const RenderColumnOptions = ({
const columnOptions = updateRowsOptions?.columns;
const updatedOption = {
...columnOptions[id],
column: selectedOption,
column: selectedOption.value,
};
const newColumnOptions = { ...columnOptions, [id]: updatedOption };
@ -298,48 +255,15 @@ const RenderColumnOptions = ({
};
return (
<div className="mt-1 row-container">
<div className="d-flex fields-container">
<div className="field col-4 me-3">
<Select
useMenuPortal={true}
placeholder="Select column"
value={column}
options={displayColumns}
onChange={handleColumnChange}
/>
</div>
<div className="field col-6 mx-1">
<CodeHinter
type="basic"
initialValue={value ? (typeof value === 'string' ? value : JSON.stringify(value)) : value}
className="codehinter-plugins"
placeholder="key"
onChange={(newValue) => handleValueChange(newValue)}
/>
</div>
<div className="col cursor-pointer m-1 mx-3">
<svg
onClick={() => {
removeColumnOptionsPair(id);
}}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.72386 0.884665C3.97391 0.634616 4.31304 0.494141 4.66667 0.494141H7.33333C7.68696 0.494141 8.02609 0.634616 8.27614 0.884665C8.52619 1.13471 8.66667 1.47385 8.66667 1.82747V3.16081H10.6589C10.6636 3.16076 10.6683 3.16076 10.673 3.16081H11.3333C11.7015 3.16081 12 3.45928 12 3.82747C12 4.19566 11.7015 4.49414 11.3333 4.49414H11.2801L10.6664 11.858C10.6585 12.3774 10.4488 12.8738 10.0809 13.2417C9.70581 13.6168 9.1971 13.8275 8.66667 13.8275H3.33333C2.8029 13.8275 2.29419 13.6168 1.91912 13.2417C1.55125 12.8738 1.34148 12.3774 1.33357 11.858L0.719911 4.49414H0.666667C0.298477 4.49414 0 4.19566 0 3.82747C0 3.45928 0.298477 3.16081 0.666667 3.16081H1.32702C1.33174 3.16076 1.33644 3.16076 1.34113 3.16081H3.33333V1.82747C3.33333 1.47385 3.47381 1.13471 3.72386 0.884665ZM2.05787 4.49414L2.66436 11.7721C2.6659 11.7905 2.66667 11.809 2.66667 11.8275C2.66667 12.0043 2.7369 12.1739 2.86193 12.2989C2.98695 12.4239 3.15652 12.4941 3.33333 12.4941H8.66667C8.84348 12.4941 9.01305 12.4239 9.13807 12.2989C9.2631 12.1739 9.33333 12.0043 9.33333 11.8275C9.33333 11.809 9.3341 11.7905 9.33564 11.7721L9.94213 4.49414H2.05787ZM7.33333 3.16081H4.66667V1.82747H7.33333V3.16081ZM4.19526 7.63221C3.93491 7.37186 3.93491 6.94975 4.19526 6.6894C4.45561 6.42905 4.87772 6.42905 5.13807 6.6894L6 7.55133L6.86193 6.6894C7.12228 6.42905 7.54439 6.42905 7.80474 6.6894C8.06509 6.94975 8.06509 7.37186 7.80474 7.63221L6.94281 8.49414L7.80474 9.35607C8.06509 9.61642 8.06509 10.0385 7.80474 10.2989C7.54439 10.5592 7.12228 10.5592 6.86193 10.2989L6 9.43695L5.13807 10.2989C4.87772 10.5592 4.45561 10.5592 4.19526 10.2989C3.93491 10.0385 3.93491 9.61642 4.19526 9.35607L5.05719 8.49414L4.19526 7.63221Z"
fill="#E54D2E"
/>
</svg>
</div>
</div>
</div>
<RenderColumnUI
column={column}
displayColumns={displayColumns}
handleColumnChange={handleColumnChange}
darkMode={darkMode}
value={value}
handleValueChange={handleValueChange}
removeColumnOptionsPair={removeColumnOptionsPair}
id={id}
/>
);
};

View file

@ -0,0 +1,33 @@
.row-tabs {
background-color: #F1F3F5;
padding: 3px;
border-radius: 6px;
margin-bottom: 0.3rem;
width:max-content;
.row-tab-content {
cursor: pointer;
padding: 3px 7px 3px 7px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
line-height: 20px;
}
}
.row-tabs-dark {
background-color: #1c252f !important;
padding: 3px;
border-radius: 6px;
margin-bottom: 0.3rem;
width:max-content;
.row-tab-content {
cursor: pointer;
padding: 3px 7px 3px 7px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
line-height: 20px;
}
}

View file

@ -1,5 +1,4 @@
import { get } from 'lodash';
/**
* Checks if the queryOptions object contains a filter with an 'eq' (equal) operator and a value equal to '{{null}}'.
*

View file

@ -1,7 +1,7 @@
import React from 'react';
import queryString from 'query-string';
import { datasourceService } from '@/_services';
import { RedirectLoader } from '@/_components';
import { TJLoader } from '@/_ui/TJLoader/TJLoader';
import { withTranslation } from 'react-i18next';
import { getCookie } from '@/_helpers/cookie';
import { withRouter } from '@/_hoc/withRouter';
@ -34,12 +34,24 @@ class AuthorizeComponent extends React.Component {
});
})
.catch((error) => {
this.setState({ isLoading: false, authSuccess: false, error: error?.error });
this.setState({
isLoading: false,
authSuccess: false,
error: error?.error,
});
});
} else {
localStorage.setItem('OAuthCode', code);
this.setState({ isLoading: false, authSuccess: true });
}
this.timer = setTimeout(() => {
window.close();
}, 2000);
}
componentWillUnmount() {
clearTimeout(this.timer);
}
render() {
@ -47,7 +59,7 @@ class AuthorizeComponent extends React.Component {
return (
<div>
{isLoading ? (
<RedirectLoader />
<TJLoader />
) : (
<div>
{!authSuccess ? (
@ -105,7 +117,14 @@ class AuthorizeComponent extends React.Component {
Success
</h4>
<div>
<div>Authorization successful, you can close this tab now.</div>
<div>Authorization successful! You will be redirected in a few seconds.</div>
<div>
Dont want to wait?{' '}
<span style={{ color: 'blue', cursor: 'pointer' }} onClick={() => window.close()}>
Click here
</span>{' '}
to go now.
</div>
</div>
</div>
)}

View file

@ -23,9 +23,9 @@ export function FileDropzone({
const { getRootProps, getInputProps, isDragActive, acceptedFiles } = useDropzone({
accept: { parsedFileType: ['text/csv'] },
onDrop: (acceptedFiles) => {
if (acceptedFiles.length === 1) {
if (acceptedFiles.length > 0) {
const file = acceptedFiles[0];
setFileData(file); // Set the file data
setFileData(file);
handleFileChange(file);
}
},
@ -70,19 +70,13 @@ export function FileDropzone({
<>
{fileData?.name ? (
<div className="bulkUpload-file">
{/* <ul>
{acceptedFiles.map((file) => (
<li key={file.path}>{file.path}</li>
))}
</ul> */}
<div className="fileName mt-3" ref={divRef}>
{fileData?.name && (
<ul className="m-0 p-0" data-cy="uploaded-file-data">{` ${fileData?.name} - ${fileData?.size} bytes`}</ul>
)}
</div>
<div>
<div style={{ width: '486px' }}>
{progress < 100 && (
<progress style={{ width: divWidth }} className="progress progress-sm mt-3" value={progress} max="100" />
)}

View file

@ -88,9 +88,12 @@ function BulkUploadDrawer({
</h3>
</div>
<div className="card-body tjdb-bulkupload-drawer">
<div className="manage-users-drawer-content-bulk">
<div className="manage-users-drawer-content-bulk-download-prompt">
<div className="user-csv-template-wrap">
<div className="manage-users-drawer-content-bulk d-flex flex-column align-items-center m-0">
<div
className="manage-users-drawer-content-bulk-download-prompt"
style={{ justifyContent: 'flex-start' }}
>
<div className="user-csv-template-wrap" style={{ marginTop: '0px' }}>
<div>
<SolidIcon name="information" fill="#F76808" width="26" />
</div>

View file

@ -34,18 +34,20 @@
}
.error-1 {
padding: 12px 0px 0px 10px !important;
margin: 12px 0px !important;
display: flex;
gap: 5px;
align-items: center;
.file-upload-error {
color: var(--tomato10) !important;
margin: 12px 0px 0px 10px !important;
}
}
.error-2 {
margin: 12px 0px !important;
.file-upload-error {
color: var(--tomato10) !important;
margin: 12px 0px 0px 35px !important;
overflow-wrap: break-word;
}
}
}

View file

@ -1,4 +1,4 @@
import React, { useState, useContext } from 'react';
import React, { useState, useContext, useEffect } from 'react';
import { toast } from 'react-hot-toast';
import Drawer from '@/_ui/Drawer';
import CreateTableForm from '../../Forms/TableForm';
@ -6,18 +6,36 @@ import { TooljetDatabaseContext } from '../../index';
import { tooljetDatabaseService } from '@/_services';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
import { BreadCrumbContext } from '@/App/App';
// FIXME: Conditionally render LicenseBanner on repo restructure
// import { LicenseBanner } from '@/LicenseBanner';
export default function CreateTableDrawer() {
const { organizationId, setSelectedTable, setTables } = useContext(TooljetDatabaseContext);
export default function CreateTableDrawer({ bannerVisible, setBannerVisible }) {
const { organizationId, setSelectedTable, setTables, tables } = useContext(TooljetDatabaseContext);
const [isCreateTableDrawerOpen, setIsCreateTableDrawerOpen] = useState(false);
const { updateSidebarNAV } = useContext(BreadCrumbContext);
const [tablesLimit, setTablesLimit] = useState({});
setBannerVisible(tablesLimit?.current >= tablesLimit?.total - 1 || false);
useEffect(() => {
async function fetchTablesLimit() {
try {
const data = await tooljetDatabaseService.getTablesLimit();
setTablesLimit(data?.data?.tablesCount);
setBannerVisible(tablesLimit?.current >= tablesLimit?.total - 1 || false);
} catch (error) {
console.error('Error fetching tables limit:', error);
}
}
fetchTablesLimit();
}, [tables, setBannerVisible]); // Empty dependency array to execute the effect only once when the component mounts
return (
<>
<div>
<div>
<div style={{ marginBottom: '16px' }}>
<ButtonSolid
type="button"
variant="primary"
disabled={tablesLimit?.current >= tablesLimit?.total}
onClick={() => setIsCreateTableDrawerOpen(!isCreateTableDrawerOpen)}
className="create-new-table-btn"
data-cy="add-table-button"
@ -25,6 +43,15 @@ export default function CreateTableDrawer() {
Create new table
</ButtonSolid>
</div>
{/* <LicenseBanner
classes="mb-3 small"
limits={tablesLimit}
type="tables"
size="small"
style={{ marginTop: '20px' }}
z-index="10000"
/> */}
<Drawer
isOpen={isCreateTableDrawerOpen}
onClose={() => setIsCreateTableDrawerOpen(false)}
@ -51,6 +78,6 @@ export default function CreateTableDrawer() {
initiator="CreateTableForm"
/>
</Drawer>
</>
</div>
);
}

View file

@ -206,11 +206,11 @@ const ColumnForm = ({
// toast.success(`Foreign key Added successfully for selected column`);
};
const referenceTableDetails = referencedColumnDetails.map((item) => {
const referenceTableDetails = referencedColumnDetails?.map((item) => {
const [key, _value] = Object.entries(item);
return {
label: key[1] === null ? 'Null' : key[1],
value: key[1] === null ? 'Null' : key[1],
label: key[1],
value: key[1],
};
});
@ -515,9 +515,9 @@ const ColumnForm = ({
</label>
</div>
<div className="col d-flex flex-column">
<p className="m-0 p-0 fw-500 tj-switch-text">{isNotNull ? 'NOT NULL' : 'NULL'}</p>
<p className="m-0 p-0 fw-500 tj-switch-text">{'NOT NULL'}</p>
<p className="fw-400 secondary-text tj-text-xsm mb-2 tj-switch-text">
{isNotNull ? 'Not null constraint is added' : 'This field can accept NULL value'}
This constraint will restrict entry of NULL values in this column.
</p>
</div>
</div>
@ -537,9 +537,9 @@ const ColumnForm = ({
</label>
</div>
<div className="col d-flex flex-column">
<p className="m-0 p-0 fw-500">{isUniqueConstraint ? 'UNIQUE' : 'NOT UNIQUE'}</p>
<p className="m-0 p-0 fw-500 tj-switch-text">{'UNIQUE'}</p>
<p className="fw-400 secondary-text tj-text-xsm">
{isUniqueConstraint ? 'Unique value constraint is added' : 'Unique value constraint is not added'}
This constraint restricts entry of duplicate values in this column.
</p>
</div>
</div>

View file

@ -291,7 +291,7 @@ const ColumnForm = ({
column: {
column_name: selectedColumn?.Header,
data_type: selectedColumn?.dataType,
...(selectedColumn?.dataType !== 'serial' && { column_default: defaultValue === 'Null' ? null : defaultValue }),
...(selectedColumn?.dataType !== 'serial' && { column_default: defaultValue }),
constraints_type: {
is_not_null: isNotNull,
is_primary_key: selectedColumn?.constraints_type?.is_primary_key ?? false,
@ -411,10 +411,10 @@ const ColumnForm = ({
const newChangesInForeignKey = changesInForeignKey();
const referenceTableDetails = referencedColumnDetails.map((item) => {
const [key, _value] = Object.entries(item);
const [key, value] = Object.entries(item);
return {
label: key[1] === null ? 'Null' : key[1],
value: key[1] === null ? 'Null' : key[1],
label: key[1],
value: key[1],
};
});
@ -837,9 +837,9 @@ const ColumnForm = ({
</label>
</div>
<div className="col d-flex flex-column">
<p className="m-0 p-0 fw-500 tj-switch-text">{isNotNull ? 'NOT NULL' : 'NULL'}</p>
<p className="m-0 p-0 fw-500 tj-switch-text">NOT NULL</p>
<p className="fw-400 secondary-text tj-text-xsm mb-2 tj-switch-text">
{isNotNull ? 'Not null constraint is added' : 'This field can accept NULL value'}
This constraint will restrict entry of NULL values in this column.
</p>
</div>
</div>
@ -886,15 +886,9 @@ const ColumnForm = ({
</label>
</div>
<div className="col d-flex flex-column">
<p className="m-0 p-0 fw-500 tj-switch-text">
{isUniqueConstraint || (!isUniqueConstraint && selectedColumn?.constraints_type?.is_primary_key)
? 'UNIQUE'
: 'NOT UNIQUE'}
</p>
<p className="m-0 p-0 fw-500 tj-switch-text">{'UNIQUE'}</p>
<p className="fw-400 secondary-text tj-text-xsm tj-switch-text">
{isUniqueConstraint || (!isUniqueConstraint && selectedColumn?.constraints_type?.is_primary_key)
? 'Unique value constraint is added'
: 'Unique value constraint is not added'}
This constraint restricts entry of duplicate values in this column.
</p>
</div>
</div>
@ -905,27 +899,17 @@ const ColumnForm = ({
isEditMode={true}
fetching={fetching}
onClose={onClose}
onEdit={() => {
if (foreignKeyDetails?.length > 0 && !isForeignKey && isMatchingForeignKeyColumn(columnName)) {
setOnDeletePopup(true);
} else {
handleEdit();
}
}}
onEdit={handleEdit}
shouldDisableCreateBtn={columnName === ''}
showToolTipForFkOnReadDocsSection={true}
initiator={initiator}
/>
</div>
<ConfirmDialog
title={'Delete foreign key relation'}
title={'Delete foreign key'}
show={onDeletePopup}
message={'Deleting the foreign key relation cannot be reversed. Are you sure you want to continue?'}
onConfirm={
foreignKeyDetails?.length > 0 && !isForeignKey && isMatchingForeignKeyColumn(columnName)
? handleEdit
: handleDeleteForeignKeyColumn
}
onConfirm={handleDeleteForeignKeyColumn}
onCancel={() => {
setOnDeletePopup(false);
}}

View file

@ -193,13 +193,19 @@ const EditRowForm = ({
const handleInputChange = (index, value, columnName) => {
const newInputValues = [...inputValues];
const isNull = value === null || value === 'Null';
newInputValues[index] = {
value: value === 'Null' ? null : value,
disabled: false,
label: value === 'Null' ? null : value,
value: isNull ? null : value,
disabled: isNull,
label: isNull ? null : value,
};
setInputValues(newInputValues);
setRowData({ ...rowData, [columnName]: value === 'Null' ? null : value });
setRowData({ ...rowData, [columnName]: isNull ? null : value });
if (isNull) {
const newActiveTabs = [...activeTab];
newActiveTabs[index] = 'Null';
setActiveTab(newActiveTabs);
}
};
useEffect(() => {

View file

@ -442,7 +442,7 @@ function ForeignKeyRelation({
/>
</Drawer>
<ConfirmDialog
title={'Delete foreign key relation'}
title={'Delete foreign key'}
show={onDeletePopup}
message={'Deleting the foreign key relation cannot be reversed. Are you sure you want to continue?'}
onConfirm={() => {

View file

@ -121,13 +121,22 @@ const RowForm = ({
return matchingColumn;
}
const handleDisabledInputClick = (index, tabData, defaultValue, nullValue, columnName, dataType, currentValue) => {
handleTabClick(index, tabData, defaultValue, nullValue, columnName, dataType, currentValue);
const handleDisabledInputClick = (index, columnName) => {
if (inputRefs.current[columnName]) {
setTimeout(() => {
inputRefs.current[columnName].focus();
}, 0);
}
const newInputValues = [...inputValues];
const isCurrentlyDisabled = newInputValues[index].disabled;
newInputValues[index] = {
...newInputValues[index],
disabled: !isCurrentlyDisabled,
};
setInputValues(newInputValues);
if (isCurrentlyDisabled) {
setData((prevData) => ({ ...prevData, [columnName]: newInputValues[index].value }));
}
};
const handleTabClick = (index, tabData, defaultValue, nullValue, columnName, dataType) => {
@ -183,14 +192,20 @@ const RowForm = ({
const handleInputChange = (index, value, columnName) => {
const newInputValues = [...inputValues];
const isNull = value === null || value === 'Null';
newInputValues[index] = {
value: value === 'Null' ? null : value,
checkboxValue: inputValues[index].checkboxValue,
disabled: false,
disabled: isNull,
label: value === 'Null' ? null : value,
};
setInputValues(newInputValues);
setData({ ...data, [columnName]: value === 'Null' ? null : value });
if (isNull) {
const newActiveTabs = [...activeTab];
newActiveTabs[index] = 'Null';
setActiveTab(newActiveTabs);
}
};
const handleCheckboxChange = (index, value, columnName) => {
@ -316,6 +331,11 @@ const RowForm = ({
const renderElement = (columnName, dataType, isPrimaryKey, defaultValue, index, isNullable) => {
const isSerialDataTypeColumn = dataType === 'serial';
const handleInputFocus = () => {
if (activeTab[index] === 'Null') {
handleTabClick(index, 'Custom', defaultValue, null, columnName, dataType);
}
};
switch (dataType) {
case 'character varying':
case 'integer':
@ -373,6 +393,7 @@ const RowForm = ({
? ''
: inputValues[index]?.value
}
onFocus={handleInputFocus}
onChange={(e) => handleInputChange(index, e.target.value, columnName)}
disabled={isSerialDataTypeColumn || inputValues[index]?.disabled}
placeholder={
@ -395,17 +416,7 @@ const RowForm = ({
)}
{inputValues[index]?.disabled && (
<div
onClick={() =>
handleDisabledInputClick(
index,
'Custom',
inputValues[index]?.column_default,
isNullable,
columnName,
dataType,
inputValues[index]?.value
)
}
onClick={() => handleDisabledInputClick(index, columnName)}
style={{
position: 'absolute',
top: 0,
@ -468,17 +479,7 @@ const RowForm = ({
/>
{inputValues[index]?.disabled && (
<div
onClick={() =>
handleDisabledInputClick(
index,
'Custom',
inputValues[index]?.column_default,
isNullable,
columnName,
dataType,
inputValues[index]?.value
)
}
onClick={() => handleDisabledInputClick(index, columnName)}
style={{
position: 'absolute',
top: 0,

View file

@ -570,17 +570,13 @@ function TableSchema({
.replace(/\s+/g, '-')}-text`}
className="m-0"
>
{columnDetails[index]?.constraints_type?.is_not_null ?? false ? (
<span
className={`${
columnDetails[index]?.constraints_type?.is_primary_key === true ? 'not-null-with-disable' : ''
}`}
>
NOT NULL
</span>
) : (
<span>NULL</span>
)}
<span
className={`${
columnDetails[index]?.constraints_type?.is_primary_key === true ? 'not-null-with-disable' : ''
}`}
>
NOT NULL
</span>
</p>
</div>
</ToolTip>

View file

@ -1,17 +1,18 @@
import React from 'react';
import React, { useState } from 'react';
import List from '../TableList';
import CreateTableDrawer from '../Drawers/CreateTableDrawer';
import { OrganizationList } from '@/_components/OrganizationManager/List';
import cx from 'classnames';
export default function Sidebar({ collapseSidebar }) {
const [bannerVisible, setBannerVisible] = useState(false);
return (
<div className={cx('tooljet-database-sidebar col', { 'visually-hidden': collapseSidebar })}>
<div className="sidebar-container">
<CreateTableDrawer />
<div className={cx('tooljet-database-sidebar col d-flex flex-column', { 'visually-hidden': collapseSidebar })}>
<div className={`sidebar-container ${!bannerVisible ? '' : 'sidebar-container-with-banner'}`}>
<CreateTableDrawer bannerVisible={bannerVisible} setBannerVisible={setBannerVisible} />
</div>
<div className="col table-left-sidebar" data-cy="all-table-column">
<div className="sidebar-list-wrap">
<div className={`sidebar-list-wrap ${!bannerVisible ? '' : 'sidebar-list-wrap-with-banner'}`}>
<List />
</div>
<OrganizationList />

View file

@ -55,8 +55,8 @@ export const UniqueConstraintPopOver = ({
};
const popover = (
<Popover className={`create-table-list-items ${darkMode && 'dark-theme'}`}>
<Popover.Body>
<Popover className={`create-table-list-items ${darkMode && ' dark-theme'}`}>
<Popover.Body className={` ${darkMode && 'theme-dark'}`}>
<div className="unique-constraint-parent">
{showUniqueConstraintInfo() && (
<div className="unique-constraint-info">
@ -169,25 +169,12 @@ export const UniqueConstraintPopOver = ({
</label>
<div>
<div className="tj-text-xsm unique-tag">
{columns[index]?.constraints_type?.is_primary_key || columns[index]?.constraints_type?.is_unique
? 'UNIQUE'
: 'NOT UNIQUE'}
</div>
<div className="tj-text-xsm">
{columns[index]?.constraints_type?.is_primary_key || columns[index]?.constraints_type?.is_unique
? 'Unique value constraint is added'
: 'Unique value constraint is not added'}
</div>
<div className="tj-text-xsm unique-tag">{'UNIQUE'}</div>
<div className="tj-text-xsm">This constraint restricts entry of duplicate values in this column.</div>
</div>
</div>
</ToolTip>
</div>
{/* <div className="col unique-helper-text px-2 py-1">
{columns[index]?.constraints_type?.is_primary_key || columns[index]?.constraints_type?.is_unique
? 'Unique value constraint is added'
: 'Unique value constraint is not added'}
</div> */}
</div>
<hr />

View file

@ -786,7 +786,6 @@ const Table = ({ collapseSidebar }) => {
// Optimised by avoiding Refetch API call on Cell-Edit Save and state is updated
const selectedTableDataCopy = [...selectedTableData];
console.log('cellValue', cellValue);
if (selectedTableDataCopy[rIndex][cellKey] !== undefined) {
selectedTableDataCopy[rIndex][cellKey] = directToggle === true ? !cellValue : cellValue;
setSelectedTableData([...selectedTableDataCopy]);

View file

@ -417,6 +417,16 @@
.tjdb-th-bg-dark {
background: #1C252F !important;
}
&::-webkit-scrollbar{
display: none;
}
&::-webkit-scrollbar-track{
background-color: transparent;
}
&:hover::-webkit-scrollbar{
display: block;
}
}
.create-table-list-items {

View file

@ -4,6 +4,7 @@ import Input from '@/_ui/Input';
import Textarea from '@/_ui/Textarea';
import Select from '@/_ui/Select';
import Headers from '@/_ui/HttpHeaders';
import Sort from '@/_ui/Sort';
import OAuth from '@/_ui/OAuth';
import Toggle from '@/_ui/Toggle';
import OpenApi from '@/_ui/OpenAPI';
@ -16,9 +17,14 @@ import { ConditionFilter, CondtionSort, MultiColumn } from '@/_components/MultiC
import Salesforce from '@/_components/Salesforce';
import ToolJetDbOperations from '@/AppBuilder/QueryManager/QueryEditors/TooljetDatabase/ToolJetDbOperations';
import { orgEnvironmentVariableService, orgEnvironmentConstantService } from '../_services';
import { find, isEmpty } from 'lodash';
import { filter, find, isEmpty } from 'lodash';
import { ButtonSolid } from './AppButton';
import { useGlobalDataSourcesStatus } from '@/_stores/dataSourcesStore';
import { canDeleteDataSource, canUpdateDataSource } from '@/_helpers';
import { Constants } from '@/_helpers/utils';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import Sharepoint from '@/_components/Sharepoint';
import AccordionForm from './AccordionForm';
const DynamicForm = ({
schema,
@ -33,11 +39,17 @@ const DynamicForm = ({
queryName,
computeSelectStyles = false,
currentAppEnvironmentId,
setDefaultOptions,
disableMenuPortal = false,
onBlur,
layout = 'vertical',
}) => {
const [computedProps, setComputedProps] = React.useState({});
const isHorizontalLayout = layout === 'horizontal';
const prevDataSourceIdRef = React.useRef(selectedDataSource?.id);
const globalDataSourcesStatus = useGlobalDataSourcesStatus();
const { isEditing: isDataSourceEditing } = globalDataSourcesStatus;
const [workspaceVariables, setWorkspaceVariables] = React.useState([]);
const [currentOrgEnvironmentConstants, setCurrentOrgEnvironmentConstants] = React.useState([]);
@ -45,6 +57,7 @@ const DynamicForm = ({
// if(schema.properties) todo add empty check
React.useLayoutEffect(() => {
if (!isEditMode || isEmpty(options)) {
typeof setDefaultOptions === 'function' && setDefaultOptions(schema?.defaults);
optionsChanged(schema?.defaults ?? {});
}
@ -92,34 +105,80 @@ const DynamicForm = ({
}, [currentAppEnvironmentId]);
React.useEffect(() => {
const prevDataSourceId = prevDataSourceIdRef.current;
prevDataSourceIdRef.current = selectedDataSource?.id;
const { properties } = schema;
if (!isEmpty(properties)) {
let fields = {};
let encrpytedFieldsProps = {};
let encryptedFieldsProps = {};
const flipComponentDropdown = find(properties, ['type', 'dropdown-component-flip']);
if (flipComponentDropdown) {
const selector = options?.[flipComponentDropdown?.key]?.value;
fields = { ...flipComponentDropdown?.commonFields, ...properties[selector] };
const commonFieldsFromSslCertificate = properties[selector]?.ssl_certificate?.commonFields;
fields = { ...commonFieldsFromSslCertificate, ...flipComponentDropdown?.commonFields, ...properties[selector] };
} else {
fields = { ...properties };
}
Object.keys(fields).length > 0 &&
Object.keys(fields).map((key) => {
const { type, encrypted, key: propertyKey } = fields[key];
if ((type === 'password' || encrypted) && !(propertyKey in computedProps)) {
//Editable encrypted fields only if datasource doesn't exists
encrpytedFieldsProps[propertyKey] = {
const processFields = (fieldsObject) => {
Object.keys(fieldsObject).forEach((key) => {
const field = fieldsObject[key];
const { type, encrypted, key: propertyKey } = field;
if (!canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource()) {
encryptedFieldsProps[propertyKey] = {
disabled: !!selectedDataSource?.id,
};
} else if (!isDataSourceEditing) {
if (type === 'password' || encrypted) {
encryptedFieldsProps[propertyKey] = {
disabled: true,
};
}
} else {
if ((type === 'password' || encrypted) && !(propertyKey in computedProps)) {
encryptedFieldsProps[propertyKey] = {
disabled: !!selectedDataSource?.id,
};
}
}
// To check for nested dropdown-component-flip
if (type === 'dropdown-component-flip') {
const selectedOption = options?.[field.key]?.value;
if (field.commonFields) {
processFields(field.commonFields);
}
if (selectedOption && fieldsObject[selectedOption]) {
processFields(fieldsObject[selectedOption]);
}
}
});
setComputedProps({ ...computedProps, ...encrpytedFieldsProps });
}
};
processFields(fields);
if (properties.renderForm) {
Object.keys(properties.renderForm).forEach((sectionKey) => {
const section = properties.renderForm[sectionKey];
const { inputs } = section;
if (inputs) {
processFields(inputs);
}
});
}
if (prevDataSourceId !== selectedDataSource?.id) {
setComputedProps({ ...encryptedFieldsProps });
} else {
setComputedProps({ ...computedProps, ...encryptedFieldsProps });
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [options]);
}, [selectedDataSource?.id, options, isDataSourceEditing]);
const getElement = (type) => {
switch (type) {
@ -140,6 +199,8 @@ const DynamicForm = ({
return ToolJetDbOperations;
case 'react-component-headers':
return Headers;
case 'react-component-sort':
return Sort;
case 'react-component-oauth-authentication':
return OAuth;
case 'react-component-google-sheets':
@ -160,6 +221,8 @@ const DynamicForm = ({
return CondtionSort;
case 'react-component-salesforce':
return Salesforce;
case 'react-component-sharepoint':
return Sharepoint;
default:
return <div>Type is invalid</div>;
}
@ -190,9 +253,10 @@ const DynamicForm = ({
className,
controller,
encrypted,
editorType = 'basic',
placeholders = {},
editorType = 'basic',
disabled = false,
buttonText,
text,
subtext,
}) => {
@ -210,7 +274,7 @@ const DynamicForm = ({
return {
type,
placeholder: useEncrypted ? '**************' : description,
className: `form-control${handleToggle(controller)} mb-0`,
className: `form-control${handleToggle(controller)} ${useEncrypted && 'dynamic-form-encrypted-field'}`,
style: { marginBottom: '0px !important' },
value: options?.[key]?.value || '',
...(type === 'textarea' && { rows: rows }),
@ -238,12 +302,12 @@ const DynamicForm = ({
value: options?.[key]?.value || options?.[key],
onChange: (value) => optionchanged(key, value),
width: width || '100%',
useMenuPortal: queryName ? true : false,
useMenuPortal: disableMenuPortal ? false : queryName ? true : false,
styles: computeSelectStyles ? computeSelectStyles('100%') : {},
useCustomStyles: computeSelectStyles ? true : false,
isDisabled: !canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource(),
encrypted: options?.[key]?.encrypted,
};
case 'checkbox-group':
return {
options: list,
@ -268,7 +332,31 @@ const DynamicForm = ({
optionchanged,
isRenderedAsQueryEditor,
workspaceConstants: currentOrgEnvironmentConstants,
isDisabled: !canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource(),
encrypted: options?.[key]?.encrypted,
buttonText,
width: width,
};
}
case 'react-component-sort': {
let isRenderedAsQueryEditor;
if (isGDS) {
isRenderedAsQueryEditor = false;
} else {
isRenderedAsQueryEditor = !isGDS;
}
return {
getter: key,
options: isRenderedAsQueryEditor
? options?.[key] ?? schema?.defaults?.[key]
: options?.[key]?.value ?? schema?.defaults?.[key]?.value,
optionchanged,
isRenderedAsQueryEditor,
workspaceConstants: currentOrgEnvironmentConstants,
isDisabled: !canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource(),
encrypted: options?.[key]?.encrypted,
buttonText,
width: width,
};
}
case 'react-component-oauth-authentication':
@ -291,11 +379,13 @@ const DynamicForm = ({
bearer_token: options?.bearer_token?.value,
auth_url: options?.auth_url?.value,
auth_key: options?.auth_key?.value,
audience: options?.audience?.value,
custom_auth_params: options?.custom_auth_params?.value,
custom_query_params: options?.custom_query_params?.value,
multiple_auth_enabled: options?.multiple_auth_enabled?.value,
optionchanged,
workspaceConstants: currentOrgEnvironmentConstants,
isDisabled: !canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource(),
options,
optionsChanged,
selectedDataSource,
@ -304,13 +394,16 @@ const DynamicForm = ({
case 'react-component-slack':
case 'react-component-zendesk':
case 'react-component-salesforce':
case 'react-component-sharepoint':
return {
optionchanged,
createDataSource,
options,
isSaving,
selectedDataSource,
currentAppEnvironmentId,
workspaceConstants: currentOrgEnvironmentConstants,
isDisabled: !canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource(),
optionsChanged,
};
case 'tooljetdb-operations':
@ -321,6 +414,7 @@ const DynamicForm = ({
isSaving,
selectedDataSource,
darkMode,
optionsChanged,
};
case 'codehinter':
return {
@ -367,6 +461,7 @@ const DynamicForm = ({
custom_query_params: options.custom_query_params?.value,
spec: options.spec?.value,
workspaceConstants: currentOrgEnvironmentConstants,
isDisabled: !canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource(),
};
case 'filters':
return {
@ -388,6 +483,7 @@ const DynamicForm = ({
onChange: (value) => optionchanged(key, value),
placeholders,
};
default:
return {};
}
@ -402,6 +498,9 @@ const DynamicForm = ({
}
const handleEncryptedFieldsToggle = (event, field) => {
if (!canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource()) {
return;
}
const isEditing = computedProps[field]['disabled'];
if (isEditing) {
optionchanged(field, '');
@ -425,13 +524,39 @@ const DynamicForm = ({
});
};
const renderLabel = (label, tooltip) => {
const labelElement = (
<label
className="form-label"
data-cy={`label-${String(label).toLowerCase().replace(/\s+/g, '-')}`}
style={{ textDecoration: tooltip ? 'underline 2px dashed' : 'none', textDecorationColor: 'var(--slate8)' }}
>
{label}
</label>
);
if (tooltip) {
return (
<OverlayTrigger
placement="top"
trigger="click"
rootClose
overlay={<Tooltip id={`tooltip-${label}`}>{tooltip}</Tooltip>}
>
{labelElement}
</OverlayTrigger>
);
}
return labelElement;
};
return (
<div className={`${isHorizontalLayout ? '' : 'row'}`}>
{Object.keys(obj).map((key) => {
const { label, type, encrypted, className, key: propertyKey } = obj[key];
const Element = getElement(type);
const isSpecificComponent = ['tooljetdb-operations'].includes(type);
return (
<div
className={cx('my-2', {
@ -448,15 +573,10 @@ const DynamicForm = ({
'form-label': isHorizontalLayout,
'align-items-center': !isHorizontalLayout,
})}
style={{ minWidth: '100px' }}
>
{label && (
<label
className="form-label"
data-cy={`label-${String(label).toLocaleLowerCase().replace(/\s+/g, '-')}`}
>
{label}
</label>
)}
{label && renderLabel(label, obj[key].tooltip)}
{(type === 'password' || encrypted) && selectedDataSource?.id && (
<div className="mx-1 col">
<ButtonSolid
@ -465,6 +585,7 @@ const DynamicForm = ({
variant="tertiary"
target="_blank"
rel="noreferrer"
disabled={!canUpdateDataSource() && !canDeleteDataSource()}
onClick={(event) => handleEncryptedFieldsToggle(event, propertyKey)}
>
{computedProps?.[propertyKey]?.['disabled'] ? 'Edit' : 'Cancel'}
@ -511,16 +632,17 @@ const DynamicForm = ({
);
};
const isFlipComponentDropdown = (obj) => {
const flipComponentDropdown = find(obj, ['type', 'dropdown-component-flip']);
if (flipComponentDropdown) {
// options[key].value for datasource
// options[key] for dataquery
const FlipComponentDropdown = (obj) => {
const flipComponentDropdowns = filter(obj, ['type', 'dropdown-component-flip']);
const dropdownComponents = flipComponentDropdowns.map((flipComponentDropdown) => {
const selector = options?.[flipComponentDropdown?.key]?.value || options?.[flipComponentDropdown?.key];
return (
<>
<div className={`${isHorizontalLayout ? '' : 'row'}`}>
<div key={flipComponentDropdown.key}>
<div className={isHorizontalLayout ? '' : 'row'}>
{flipComponentDropdown.commonFields && getLayout(flipComponentDropdown.commonFields)}
<div
className={cx('my-2', {
'col-md-12': !flipComponentDropdown.className && !isHorizontalLayout,
@ -539,6 +661,7 @@ const DynamicForm = ({
{flipComponentDropdown.label}
</label>
)}
<div data-cy={'query-select-dropdown'} className={cx({ 'flex-grow-1': isHorizontalLayout })}>
<Select
{...getElementProps(flipComponentDropdown)}
@ -551,17 +674,55 @@ const DynamicForm = ({
)}
</div>
</div>
{getLayout(obj[selector])}
</>
</div>
);
});
const normalComponents = Object.keys(obj).map((key) => {
const component = obj[key];
if (component.type && component.type !== 'dropdown-component-flip') {
return <div key={key}>{getLayout({ [key]: component })}</div>;
}
return null;
});
return (
<>
{normalComponents}
{dropdownComponents}
</>
);
};
const isFormComponent = (obj, getLayout) => {
const formComponent = find(obj, ['type', 'react-form-component']);
if (formComponent) {
return <AccordionForm formComponent={formComponent} getLayout={getLayout} />;
}
return null;
};
const isFlipComponentDropdown = (obj) => {
const checkFlipComponents = filter(obj, ['type', 'dropdown-component-flip']);
if (checkFlipComponents.length > 0) {
return FlipComponentDropdown(obj);
} else {
return null;
}
};
const flipComponentDropdown = isFlipComponentDropdown(schema.properties);
const formComponent = isFormComponent(schema.properties, getLayout);
if (flipComponentDropdown) {
return flipComponentDropdown;
}
if (formComponent) {
return formComponent;
}
return getLayout(schema.properties);
};

View file

@ -0,0 +1,146 @@
import React, { useState } from 'react';
import { datasourceService } from '@/_services';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-hot-toast';
import Button from '@/_ui/Button';
import Input from '@/_ui/Input';
import cx from 'classnames';
const Sharepoint = ({
optionchanged,
createDataSource,
options,
isSaving,
selectedDataSource,
currentAppEnvironmentId,
workspaceConstants,
isDisabled,
}) => {
const [authStatus, setAuthStatus] = useState(null);
const { t } = useTranslation();
const hostUrl = window.public_config?.TOOLJET_HOST;
const subPathUrl = window.public_config?.SUB_PATH;
const fullUrl = `${hostUrl}${subPathUrl ? subPathUrl : '/'}oauth2/authorize`;
const redirectUri = fullUrl;
function authSharepoint() {
const provider = 'sharepoint';
const plugin_id = selectedDataSource?.plugin?.id;
const source_options = options;
setAuthStatus('waiting_for_url');
const scope = 'https://graph.microsoft.com/.default+offline_access';
datasourceService
.fetchOauth2BaseUrl(provider, plugin_id, source_options)
.then((data) => {
const authUrl = `${data.url}&scope=${scope}&state=12345&response_mode=query`;
localStorage.setItem('sourceWaitingForOAuth', 'newSource');
localStorage.setItem('currentAppEnvironmentIdForOauth', currentAppEnvironmentId);
optionchanged('provider', provider).then(() => {
optionchanged('oauth2', true);
optionchanged('plugin_id', plugin_id);
});
setAuthStatus('waiting_for_token');
openUrl(authUrl);
})
.catch(({ error }) => {
toast.error(error);
setAuthStatus(null);
});
}
function saveDataSource() {
optionchanged('code', localStorage.getItem('OAuthCode')).then(() => {
createDataSource();
});
}
function openUrl(url) {
const width = window.innerWidth * 0.4;
const height = window.innerHeight * 0.8;
const left = window.screenX + (window.innerWidth - width) / 2;
const top = window.screenY + (window.innerHeight - height) / 2;
const windowFeatures = `width=${width},height=${height},top=${top},left=${left},resizable=yes,scrollbars=yes`;
const authWindow = window.open(url, '_blank', windowFeatures);
if (authWindow) {
authWindow.focus();
}
}
return (
<div>
<div>
<label className="form-label mt-3">Client ID (App ID)</label>
<Input
type="text"
className="form-control"
onChange={(e) => optionchanged('sp_client_id', e.target.value)}
value={options?.sp_client_id?.value ?? ''}
placeholder="Enter client ID"
workspaceConstants={workspaceConstants}
/>
</div>
<div>
<label className="form-label mt-3">Client secret</label>
<Input
type="password"
className="form-control dynamic-form-encrypted-field"
onChange={(e) => optionchanged('sp_client_secret', e.target.value)}
value={options?.sp_client_secret?.value || ''}
placeholder="**************"
workspaceConstants={workspaceConstants}
encrypted={true}
/>
</div>
<div>
<label className="form-label mt-3">Tenant ID</label>
<Input
type="text"
className="form-control"
onChange={(e) => optionchanged('sp_tenant_id', e.target.value)}
value={options?.sp_tenant_id?.value ?? ''}
placeholder="Enter tenant ID"
workspaceConstants={workspaceConstants}
/>
</div>
<div>
<label className="form-label mt-3">Redirect URI</label>
<Input
value={redirectUri}
helpText="In Microsoft Entra admin center, use the URL above when prompted to enter an redirect URI"
type="copyToClipboard"
disabled="true"
className="form-control"
/>
</div>
<div className="row mt-3">
<center>
{authStatus === 'waiting_for_token' && (
<div>
<Button
className={`m2 ${isSaving ? ' loading' : ''}`}
disabled={isSaving || isDisabled}
onClick={() => saveDataSource()}
>
{isSaving ? t('globals.saving', 'Saving...') : t('globals.saveDatasource', 'Save data source')}
</Button>
</div>
)}
{(!authStatus || authStatus === 'waiting_for_url') && (
<Button
className={cx('m2', { 'btn-loading': authStatus === 'waiting_for_url' })}
disabled={isSaving || isDisabled}
onClick={() => authSharepoint()}
>
{t('globals.connect', 'Connect')} {t('sharepoint.toSharepoint', 'to Sharepoint')}
</Button>
)}
</center>
</div>
</div>
);
};
export default Sharepoint;

File diff suppressed because it is too large Load diff

View file

@ -29,5 +29,6 @@
"start:dev": "npm run build && npm run start:watch",
"lint": "eslint . '**/*.ts'",
"format": "eslint . --fix '**/*.ts'"
}
}
},
"dependencies": {}
}

4
marketplace/plugins/jira/.gitignore vendored Normal file
View file

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

View file

@ -0,0 +1,4 @@
# Jira
Documentation on: https://docs.tooljet.com/docs/data-sources/jira

View file

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

View file

@ -0,0 +1,15 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.1515 2.01416H11.5342C11.5342 3.16558 11.9916 4.26984 12.8058 5.08401C13.6199 5.89819 14.7242 6.35559 15.8756 6.35559H17.6472V8.06613C17.6488 10.4617 19.5903 12.4033 21.9859 12.4048V2.84847C21.9859 2.38782 21.6124 2.01416 21.1515 2.01416Z" fill="#2684FF"/>
<path d="M16.3937 6.80615H6.77637C6.77785 9.20167 8.71938 11.1433 11.115 11.1448H12.8866V12.8609C12.8897 15.2564 14.8325 17.1967 17.228 17.1967V7.64062C17.228 7.17981 16.8545 6.80615 16.3937 6.80615Z" fill="url(#paint0_linear_498_6720)"/>
<path d="M11.6315 11.5952H2.01416C2.01416 13.9929 3.95796 15.9366 6.35559 15.9366H8.13277V17.6471C8.13434 20.0404 10.0725 21.9812 12.4659 21.9857V12.4296C12.4659 11.9688 12.0923 11.5952 11.6315 11.5952Z" fill="url(#paint1_linear_498_6720)"/>
<defs>
<linearGradient id="paint0_linear_498_6720" x1="2622.8" y1="11.5899" x2="1585.28" y2="1224.08" gradientUnits="userSpaceOnUse">
<stop offset="0.18" stop-color="#0052CC"/>
<stop offset="1" stop-color="#2684FF"/>
</linearGradient>
<linearGradient id="paint1_linear_498_6720" x1="2688.34" y1="25.1143" x2="1488.2" y2="1347.56" gradientUnits="userSpaceOnUse">
<stop offset="0.18" stop-color="#0052CC"/>
<stop offset="1" stop-color="#2684FF"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,75 @@
import { QueryError, QueryResult, QueryService, ConnectionTestResult } from '@tooljet-marketplace/common';
import { QueryOptions, SourceOptions } from './types';
import { JiraClient } from './jira-client';
import { boardResource, issueResource, userResource, worklogResource } from './query_operations';
export { userResource, issueResource, worklogResource, boardResource, JiraClient };
export default class Jira implements QueryService {
async getConnection(sourceOptions: SourceOptions) {
const { url } = sourceOptions;
const client = new JiraClient({
host: url,
authentication: {
basic: {
username: sourceOptions.email,
password: sourceOptions.personal_token,
},
},
});
return client;
}
async run(sourceOptions: SourceOptions, queryOptions: QueryOptions): Promise<QueryResult> {
const { resource } = queryOptions;
const client = await this.getConnection(sourceOptions);
let res: any;
try {
switch (resource) {
case 'issue': {
res = await issueResource(queryOptions, client);
break;
}
case 'user': {
res = await userResource(queryOptions, client);
break;
}
case 'worklog': {
res = await worklogResource(queryOptions, client);
break;
}
case 'board': {
res = await boardResource(queryOptions, client);
break;
}
default: {
throw new Error('Select an operation');
}
}
} catch (error) {
res = error.toString();
throw new QueryError('Query could not be completed', error.message, res);
}
return {
status: 'ok',
data: res,
};
}
async testConnection(sourceOptions: SourceOptions): Promise<ConnectionTestResult> {
const client = await this.getConnection(sourceOptions);
await client.myself.getCurrentUser({}).catch((error) => {
throw new Error(error);
});
return {
status: 'ok',
};
}
}

View file

@ -0,0 +1,62 @@
import { BaseClient } from 'jira.js/out/clients';
import { Board } from 'jira.js/out/agile';
import { IssueWorklogs, IssueSearch, Issues, UserSearch, Users, Myself } from 'jira.js/out/version3';
export class JiraClient extends BaseClient {
private _board?: Board;
private _issues?: Issues;
private _issueSearch?: IssueSearch;
private _issueWorklogs?: IssueWorklogs;
private _userSearch?: UserSearch;
private _users?: Users;
private _myself?: Myself;
get board(): Board {
if (!this._board) {
this._board = new Board(this);
}
return this._board;
}
get issues(): Issues {
if (!this._issues) {
this._issues = new Issues(this);
}
return this._issues;
}
get issueSearch(): IssueSearch {
if (!this._issueSearch) {
this._issueSearch = new IssueSearch(this);
}
return this._issueSearch;
}
get issueWorklogs(): IssueWorklogs {
if (!this._issueWorklogs) {
this._issueWorklogs = new IssueWorklogs(this);
}
return this._issueWorklogs;
}
get userSearch(): UserSearch {
if (!this._userSearch) {
this._userSearch = new UserSearch(this);
}
return this._userSearch;
}
get users(): Users {
if (!this._users) {
this._users = new Users(this);
}
return this._users;
}
get myself(): Myself {
if (!this._myself) {
this._myself = new Myself(this);
}
return this._myself;
}
}

View file

@ -0,0 +1,67 @@
{
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/manifest.schema.json",
"title": "Jira datasource",
"description": "A schema defining Jira datasource",
"type": "api",
"source": {
"name": "Jira",
"kind": "jira",
"exposedVariables": {
"isLoading": false,
"data": {},
"rawData": {}
},
"customTesting": false,
"options": {
"url": {
"type": "string"
}
}
},
"defaults": {
"auth_type": {
"value": "personal_access_token"
}
},
"properties": {
"credentials": {
"label": "Authentication",
"key": "auth_type",
"type": "dropdown-component-flip",
"description": "A single select dropdown to choose credentials",
"list": [
{
"value": "personal_access_token",
"name": "Basic"
}
],
"commonFields": {
"url": {
"label": "URL",
"key": "url",
"type": "text",
"description": "https://your-domain.atlassian.net"
}
}
},
"personal_access_token": {
"email": {
"label": "Email",
"key": "email",
"type": "text",
"description": "Enter email",
"hint": ""
},
"token": {
"label": "Token",
"key": "personal_token",
"type": "password",
"description": "Enter your api token",
"hint": "You can generate a personal access token from your Jira account 'Manage account'."
}
}
},
"required": [
"url"
]
}

View file

@ -0,0 +1,669 @@
{
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/operations.schema.json",
"title": "Jira datasource",
"description": "A schema defining Jira datasource",
"type": "api",
"defaults": {},
"properties": {
"resource": {
"label": "Resource",
"key": "resource",
"type": "dropdown-component-flip",
"description": "Resource select",
"list": [
{
"value": "issue",
"name": "Issue"
},
{
"value": "user",
"name": "User"
},
{
"value": "worklog",
"name": "Worklog"
},
{
"value": "board",
"name": "Board"
}
]
},
"issue": {
"operation": {
"label": "Operation",
"key": "operation",
"type": "dropdown-component-flip",
"description": "Operation select",
"list": [
{
"value": "get_issue",
"name": "Get issue"
},
{
"value": "create_issue",
"name": "Create issue"
},
{
"value": "delete_issue",
"name": "Delete issue"
},
{
"value": "assign_issue",
"name": "Assign issue"
},
{
"value": "edit_issue",
"name": "Edit issue"
}
]
},
"get_issue": {
"issue_key": {
"label": "Issue key",
"key": "issue_key",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter issue key",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "SCRUM-1"
},
"properties": {
"label": "Params/Body",
"key": "properties",
"type": "codehinter",
"mode": "javascript",
"placeholder": "{\n \"fields\": \"summary, description, created, ...\"\n \"expand\": \"renderedFields, names, schema, transitions, operations, editmeta, changelog, versionedRepresentations\"\n \"properties\": \"...\"\n \"updateHistory\": \"...\"\n}",
"description": "Enter query properties",
"height": "200px"
}
},
"create_issue": {
"properties": {
"label": "Params/Body",
"key": "properties",
"type": "codehinter",
"mode": "javascript",
"placeholder": "{\n \"fields\": {\n \"project\":\n { \n \"key\": \"PROJECT_KEY\"\n },\n \"summary\": \"A particular bug needs to be fixed.\",\n \"description\": \"The XYZ feature is not working as expected.\",\n \"issuetype\": {\n \"name\": \"Bug\"\n },\n \"assignee\": {\n \"accountId\": \"712020:4581444c-054e-41d8-90ed-6d1d849557f7\"\n },\n \"labels\": [\n \"bug\",\n \"urgent\"\n ]\n }\n}",
"description": "Enter properties",
"height": "200px"
}
},
"delete_issue": {
"issue_key": {
"label": "Issue key",
"key": "issue_key",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter issue key",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "SCRUM-1"
},
"delete_subtasks": {
"label": "Delete subtasks",
"key": "delete_subtasks",
"className": "col-4",
"type": "dropdown",
"description": "Single select dropdown for delete subtasks",
"list": [
{
"value": "Yes",
"name": "Yes"
},
{
"value": "No",
"name": "No"
}
]
}
},
"assign_issue": {
"issue_key": {
"label": "Issue key",
"key": "issue_key",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter issue key",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "SCRUM-1"
},
"account_id": {
"label": "Account id",
"key": "account_id",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter user unique id",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "712020:4581444c-054e-41d8-90ed-6d1d849557f7"
}
},
"edit_issue": {
"issue_key": {
"label": "Issue key",
"key": "issue_key",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter issue key",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "SCRUM-1"
},
"properties": {
"label": "Params/Body",
"key": "properties",
"type": "codehinter",
"mode": "javascript",
"placeholder": "{\n \"fields\": {\n \"project\":\n { \n \"key\": \"PROJECT_KEY\"\n },\n \"summary\": \"A particular bug needs to be fixed.\",\n \"description\": \"The XYZ feature is not working as expected.\",\n \"issuetype\": {\n \"name\": \"Bug\"\n },\n \"assignee\": {\n \"accountId\": \"712020:4581444c-054e-41d8-90ed-6d1d849557f7\"\n },\n \"reporter\": {\n \"accountId\": \"712020:4581444c-054e-41d8-90ed-6d1d849557f7\"\n },\n \"labels\": [\n \"bug\",\n \"urgent\"\n ]\n }\n}",
"description": "Enter properties",
"height": "200px"
}
}
},
"user": {
"operation": {
"label": "Operation",
"key": "operation",
"type": "dropdown-component-flip",
"description": "Operation select",
"list": [
{
"value": "get_user",
"name": "Get user"
},
{
"value": "find_users_by_query",
"name": "Find users by query"
},
{
"value": "find_assignable_users",
"name": "Assignable users"
}
]
},
"get_user": {
"account_id": {
"label": "Account id",
"key": "account_id",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter user unique id",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "712020:4581444c-054e-41d8-90ed-6d1d849557f7"
},
"expand": {
"label": "Expand",
"key": "expand",
"type": "codehinter",
"lineNumbers": false,
"description": "Use expand to include additional information about users in the response",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "widgets"
}
},
"find_users_by_query": {
"query": {
"label": "Query",
"key": "query",
"type": "codehinter",
"mode": "javascript",
"placeholder": "Enter a query, e.g., is assignee of {issue_key or project_key}",
"description": "Enter query",
"height": "200px"
},
"start_at": {
"label": "Start at",
"key": "start_at",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter index of the first item to return in a page of results (page offset)",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "0"
},
"max_results": {
"label": "Max results",
"key": "max_results",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter the maximum number of items to return per page",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "100"
}
},
"find_assignable_users": {
"query": {
"label": "Query",
"key": "query",
"type": "codehinter",
"mode": "javascript",
"placeholder": "Enter a query, e.g., string matched against name, email, or displayName",
"description": "A query string that is matched against user attributes, such as displayName, and emailAddress",
"height": "200px"
},
"account_id": {
"label": "Account id",
"key": "account_id",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter user unique id",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "712020:4581444c-054e-41d8-90ed-6d1d849557f7"
},
"project_key": {
"label": "Project key",
"key": "project_key",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter project key",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "PROJ"
},
"issue_key": {
"label": "Issue key",
"key": "issue_key",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter issue key",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "PROJ-1"
},
"start_at": {
"label": "Start at",
"key": "start_at",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter index of the first item to return in a page of results (page offset)",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "0"
},
"max_results": {
"label": "Max results",
"key": "max_results",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter the maximum number of items to return per page",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "100"
},
"action_descriptor_id": {
"label": "Action descriptor id",
"key": "action_descriptor_id",
"type": "codehinter",
"lineNumbers": false,
"description": "The ID of the action descriptor for filtering",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter ID of the action descriptor for filtering"
},
"recommend": {
"label": "Recommended",
"key": "recommend",
"className": "col-4",
"type": "dropdown",
"description": "Single select dropdown for recommend",
"list": [
{
"value": "Yes",
"name": "Recommend"
},
{
"value": "No",
"name": "Don't recommend"
}
]
}
}
},
"worklog": {
"operation": {
"label": "Operation",
"key": "operation",
"type": "dropdown-component-flip",
"description": "Operation select",
"list": [
{
"value": "issue_worklogs",
"name": "Get issue worklogs"
},
{
"value": "add_worklog",
"name": "Add worklog"
},
{
"value": "delete_worklog",
"name": "Delete worklog"
}
]
},
"issue_worklogs": {
"issue_key": {
"label": "Issue key",
"key": "issue_key",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter issue key",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "SCRUM-1"
},
"start_at": {
"label": "Start at",
"key": "start_at",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter index of the first item to return in a page of results (page offset)",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "0"
},
"max_results": {
"label": "Max results",
"key": "max_results",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter the maximum number of items to return per page",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "100"
},
"started_after": {
"label": "Started after",
"key": "started_after",
"type": "codehinter",
"lineNumbers": false,
"description": "The worklog start date and time, as a UNIX timestamp in milliseconds",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter worklog start date and time, as a UNIX timestamp in milliseconds"
},
"started_before": {
"label": "Started before",
"key": "started_before",
"type": "codehinter",
"lineNumbers": false,
"description": "The worklog start date and time, as a UNIX timestamp in milliseconds",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter worklog start date and time, as a UNIX timestamp in milliseconds"
}
},
"add_worklog": {
"issue_key": {
"label": "Issue key",
"key": "issue_key",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter issue key",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "SCRUM-1"
},
"properties": {
"label": "Params/Body",
"key": "properties",
"type": "codehinter",
"mode": "javascript",
"placeholder": "{\n \"comment\": \"I did some work here.\",\n \"created\": \"2017-03-14T10:35:37.097+0000\",\n \"id\": \"100028\",\n \"issueId\": \"SCRUM-1\",\n \"started\": \"2017-03-14T10:35:37.097+0000\",\n \"timeSpent\": \"3h 20m\"\n}",
"description": "Enter properties",
"height": "200px"
}
},
"delete_worklog": {
"issue_key": {
"label": "Issue key",
"key": "issue_key",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter issue key",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "SCRUM-1"
},
"worklog_id": {
"label": "Worklog id",
"key": "worklog_id",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter worklog id",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "10000"
},
"properties": {
"label": "Params/Body",
"key": "properties",
"type": "codehinter",
"mode": "javascript",
"placeholder": "{notifyUsers:true, ...}",
"description": "Enter properties",
"height": "200px"
}
}
},
"board": {
"operation": {
"label": "Operation",
"key": "operation",
"type": "dropdown-component-flip",
"type": "dropdown-component-flip",
"description": "Operation select",
"list": [
{
"value": "get_issues_for_backlog",
"name": "Get issues for backlog"
},
{
"value": "get_all_boards",
"name": "Get all boards"
},
{
"value": "get_issues_for_board",
"name": "Get issues for board"
}
]
},
"get_issues_for_backlog": {
"board_id": {
"label": "Board id",
"key": "board_id",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter board id",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "1"
},
"start_at": {
"label": "Start at",
"key": "start_at",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter index of the first item to return in a page of results (page offset)",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "0"
},
"max_results": {
"label": "Max results",
"key": "max_results",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter the maximum number of items to return per page",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "100"
},
"expand": {
"label": "Expand",
"key": "expand",
"type": "codehinter",
"lineNumbers": false,
"description": "Use expand to include additional information about users in the response",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "widgets"
},
"properties": {
"label": "Params/Body",
"key": "properties",
"type": "codehinter",
"mode": "javascript",
"placeholder": "{ fields: \"exampleField, ...\" }",
"description": "Enter properties",
"height": "200px"
}
},
"get_all_boards": {
"project_key": {
"label": "Project key",
"key": "project_key",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter project key",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "SCRUM"
},
"start_at": {
"label": "Start at",
"key": "start_at",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter index of the first item to return in a page of results (page offset)",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "0"
},
"board_name": {
"label": "Name",
"key": "board_name",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter the name of the board",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter board name"
},
"max_results": {
"label": "Max results",
"key": "max_results",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter the maximum number of items to return per page",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "100"
},
"expand": {
"label": "Expand",
"key": "expand",
"type": "codehinter",
"lineNumbers": false,
"description": "Use expand to include additional information about users in the response",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "admins, permissions, favourite"
}
},
"get_issues_for_board": {
"board_id": {
"label": "Board id",
"key": "board_id",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter board id",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "1"
},
"start_at": {
"label": "Start at",
"key": "start_at",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter index of the first item to return in a page of results (page offset)",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "0"
},
"max_results": {
"label": "Max results",
"key": "max_results",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter the maximum number of items to return per page",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "100"
},
"expand": {
"label": "Expand",
"key": "expand",
"type": "codehinter",
"lineNumbers": false,
"description": "Use expand to include additional information about users in the response",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "widgets"
},
"properties": {
"label": "Params/Body",
"key": "properties",
"type": "codehinter",
"mode": "javascript",
"placeholder": "{ fields: \"exampleField, ...\" }",
"description": "Enter properties",
"height": "200px"
}
}
}
}
}

View file

@ -0,0 +1,376 @@
import { QueryOptions } from './types';
import JSON5 from 'json5';
import { JiraClient } from './jira-client';
function returnObject(data: any) {
if (!data) {
return undefined;
}
return typeof data === 'string' ? JSON5.parse(data) : data;
}
function returnNumber(data: any) {
if (!data) {
return undefined;
}
return typeof data === 'string' ? Number.parseInt(data) : data;
}
export async function userResource(queryOptions: QueryOptions, client: JiraClient) {
const { operation } = queryOptions;
let res;
switch (operation) {
case 'get_user': {
res = await getUser(queryOptions, client);
break;
}
case 'find_users_by_query': {
res = await findUsersByQuery(queryOptions, client);
break;
}
case 'find_assignable_users': {
res = await findAssignableUsers(queryOptions, client);
break;
}
default: {
throw new Error('Select an operation');
}
}
return res;
}
async function getUser(queryOptions: QueryOptions, client: JiraClient) {
let returnValue = {};
await client.users
.getUser({ accountId: queryOptions.account_id, expand: queryOptions.expand })
.then((res) => {
returnValue = res;
})
.catch((err) => {
returnValue = { statusCode: err.response?.status, response: err?.response?.data };
});
return returnValue;
}
async function findUsersByQuery(queryOptions: QueryOptions, client: JiraClient) {
const { query, start_at, max_results } = queryOptions;
let returnValue = {};
await client.userSearch
.findUsersByQuery({ query: query, startAt: returnNumber(start_at), maxResults: returnNumber(max_results) })
.then((res) => {
returnValue = res;
})
.catch((err) => {
returnValue = { statusCode: err.response?.status, response: err?.response?.data };
});
return returnValue;
}
async function findAssignableUsers(queryOptions: QueryOptions, client: JiraClient) {
let returnValue = {};
const isRecommended = queryOptions.recommend === 'Yes' ? true : false;
await client.userSearch
.findAssignableUsers({
query: queryOptions.query,
accountId: queryOptions.account_id,
project: queryOptions.project_key,
issueKey: queryOptions.issue_key,
startAt: returnNumber(queryOptions.start_at),
maxResults: returnNumber(queryOptions.max_results),
actionDescriptorId: queryOptions.action_descriptor_id,
recommend: isRecommended,
})
.then((res) => {
returnValue = res;
})
.catch((err) => {
returnValue = { statusCode: err.response?.status, response: err?.response?.data };
});
return returnValue;
}
export async function issueResource(queryOptions: QueryOptions, client: JiraClient) {
const { operation } = queryOptions;
let res;
switch (operation) {
case 'create_issue': {
res = await createIssue(queryOptions, client);
break;
}
case 'delete_issue': {
res = await deleteIssue(queryOptions, client);
break;
}
case 'assign_issue': {
res = await assignIssue(queryOptions, client);
break;
}
case 'get_issue': {
res = await getIssue(queryOptions, client);
break;
}
case 'edit_issue': {
res = await editIssue(queryOptions, client);
break;
}
default: {
throw new Error('Select an operation');
}
}
return res;
}
async function createIssue(queryOptions: QueryOptions, client: JiraClient) {
const { properties } = queryOptions;
let returnValue = {};
await client.issues
.createIssue(returnObject(properties))
.then((response) => {
returnValue = response;
})
.catch((err) => {
returnValue = { statusCode: err.response?.status, response: err?.response?.data };
});
return returnValue;
}
async function getIssue(queryOptions: QueryOptions, client: JiraClient) {
let returnValue = {};
await client.issues
.getIssue({ issueIdOrKey: queryOptions.issue_key, ...returnObject(queryOptions.properties) })
.then((res) => {
returnValue = res;
})
.catch((err) => {
returnValue = { statusCode: err.response?.status, response: err?.response?.data };
});
return returnValue;
}
async function deleteIssue(queryOptions: QueryOptions, client: JiraClient): Promise<any> {
let returnValue: any = {};
const isSubtasks = queryOptions.delete_subtasks === 'Yes' ? true : false;
const successMessage = isSubtasks
? `The issue '${queryOptions.issue_key}' and its subtasks have been deleted.`
: `The issue '${queryOptions.issue_key}' has been deleted.`;
try {
await client.issues.deleteIssue({ issueIdOrKey: queryOptions.issue_key, deleteSubtasks: isSubtasks });
returnValue = {
response: { data: successMessage },
};
} catch (err) {
returnValue = { statusCode: err.response?.status, response: err?.response?.data };
}
return returnValue;
}
async function assignIssue(queryOptions: QueryOptions, client: JiraClient) {
let returnValue = {};
try {
await client.issues.assignIssue({ issueIdOrKey: queryOptions.issue_key, accountId: queryOptions.account_id });
returnValue = {
response: {
data: `The issue '${queryOptions.issue_key}' has been assigned to accound id '${queryOptions.account_id}'.`,
},
};
} catch (err) {
returnValue = { statusCode: err.response?.status, response: err?.response?.data };
}
return returnValue;
}
async function editIssue(queryOptions: QueryOptions, client: JiraClient) {
let returnValue = {};
const { properties } = queryOptions;
try {
await client.issues.editIssue({ issueIdOrKey: queryOptions.issue_key, ...returnObject(properties) });
returnValue = {
response: { data: `The issue '${queryOptions.issue_key}' has been updated.` },
};
} catch (err) {
returnValue = { statusCode: err.response?.status, response: err?.response?.data };
}
return returnValue;
}
export async function worklogResource(queryOptions: QueryOptions, client: JiraClient) {
const { operation } = queryOptions;
let res;
switch (operation) {
case 'issue_worklogs': {
res = await issueWorklogs(queryOptions, client);
break;
}
case 'add_worklog': {
res = await addWorklog(queryOptions, client);
break;
}
case 'delete_worklog': {
res = await deleteWorklog(queryOptions, client);
break;
}
default: {
throw new Error('Select an operation');
}
}
return res;
}
async function issueWorklogs(queryOptions: QueryOptions, client: JiraClient) {
let returnValue = {};
await client.issueWorklogs
.getIssueWorklog({
issueIdOrKey: queryOptions.issue_key,
startAt: returnNumber(queryOptions.start_at),
maxResults: returnNumber(queryOptions.max_results),
startedAfter: returnNumber(queryOptions.started_after),
startedBefore: returnNumber(queryOptions.started_before),
})
.then((res) => {
returnValue = res;
})
.catch((err) => {
returnValue = { statusCode: err.response?.status, response: err?.response?.data };
});
return returnValue;
}
async function addWorklog(queryOptions: QueryOptions, client: JiraClient) {
let returnValue = {};
await client.issueWorklogs
.addWorklog({
issueIdOrKey: queryOptions.issue_key,
...returnObject(queryOptions.properties),
})
.then((res) => {
returnValue = res;
})
.catch((err) => {
returnValue = { statusCode: err.response?.status, response: err?.response?.data };
});
return returnValue;
}
async function deleteWorklog(queryOptions: QueryOptions, client: JiraClient) {
let returnValue = {};
try {
await client.issueWorklogs.deleteWorklog({
issueIdOrKey: queryOptions.issue_key,
id: queryOptions.worklog_id,
...returnObject(queryOptions.properties),
});
returnValue = {
response: { data: `The worklog with id '${queryOptions.worklog_id}' has been deleted.` },
};
} catch (err) {
returnValue = { statusCode: err.response?.status, response: err?.response?.data };
}
return returnValue;
}
export async function boardResource(queryOptions: QueryOptions, client: JiraClient) {
const { operation } = queryOptions;
let res;
switch (operation) {
case 'get_issues_for_backlog': {
res = await getIssuesForBacklog(queryOptions, client);
break;
}
case 'get_all_boards': {
res = await getAllBoards(queryOptions, client);
break;
}
case 'get_issues_for_board': {
res = await getIssuesForBoard(queryOptions, client);
break;
}
default: {
throw new Error('Select an operation');
}
}
return res;
}
async function getAllBoards(queryOptions: QueryOptions, client: JiraClient) {
let returnValue = {};
await client.board
.getAllBoards({
projectKeyOrId: queryOptions.project_key,
startAt: queryOptions.start_at,
maxResults: queryOptions.max_results,
name: queryOptions.board_name,
expand: queryOptions.expand,
})
.then((res) => {
returnValue = res;
})
.catch((err) => {
returnValue = { statusCode: err.response?.status, response: err?.response?.data };
});
return returnValue;
}
async function getIssuesForBacklog(queryOptions: QueryOptions, client: JiraClient) {
let returnValue = {};
await client.board
.getIssuesForBacklog({
boardId: queryOptions.board_id,
startAt: queryOptions.start_at,
maxResults: queryOptions.max_results,
expand: queryOptions.expand,
id: queryOptions.worklog_id,
...returnObject(queryOptions.properties),
})
.then((res) => {
returnValue = res;
})
.catch((err) => {
returnValue = { statusCode: err.response?.status, response: err?.response?.data };
});
return returnValue;
}
async function getIssuesForBoard(queryOptions: QueryOptions, client: JiraClient) {
let returnValue = {};
await client.board
.getIssuesForBoard({
boardId: queryOptions.board_id,
startAt: queryOptions.start_at,
maxResults: queryOptions.max_results,
...returnObject(queryOptions.properties),
})
.then((res) => {
returnValue = res;
})
.catch((err) => {
returnValue = { statusCode: err.response?.status, response: err?.response?.data };
});
return returnValue;
}

View file

@ -0,0 +1,37 @@
export type QueryOptions = {
resource?: string;
properties?: any;
account_id?: string;
issue_key?: string;
issue_keys?: string;
project_key?: string;
operation?: string;
query?: string;
start_at?: any;
max_results?: any;
board_name?: string;
expand?: string;
action_descriptor_id?: any;
recommend?: string;
delete_subtasks?: string;
started_after?: any;
started_before?: any;
// worklog
worklog_id?: any;
// board
board_id?: any;
done?: string;
};
export type SourceOptions = {
url: string;
// auth_type: string;
personal_token: string;
email: string;
};

View file

@ -0,0 +1,24 @@
{
"name": "@tooljet-plugins/jira",
"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",
"watch": "ncc build lib/index.ts -o dist --watch"
},
"homepage": "https://github.com/tooljet/tooljet#readme",
"dependencies": {
"@tooljet-plugins/common": "file:../common",
"jira.js": "^4.0.1",
"react": "^17.0.2"
}
}

View file

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

View file

@ -1,7 +1,7 @@
import { QueryError, QueryResult, QueryService, ConnectionTestResult } from '@tooljet-marketplace/common';
import { SourceOptions, QueryOptions, Operation } from './types';
import OpenAI from 'openai';
import { getCompletion, getChatCompletion } from './query_operations';
import OpenAI from 'openai'; // Correct import for SDK 4.56.0
import { getCompletion, getChatCompletion, generateImage } from './query_operations';
export default class Openai implements QueryService {
async run(sourceOptions: SourceOptions, queryOptions: QueryOptions, dataSourceId: string): Promise<QueryResult> {
@ -19,6 +19,10 @@ export default class Openai implements QueryService {
result = await getChatCompletion(openai, queryOptions);
break;
case Operation.ImageGeneration:
result = await generateImage(openai, queryOptions);
break;
default:
throw new QueryError('Query could not be completed', 'Invalid operation', {});
}
@ -34,13 +38,17 @@ export default class Openai implements QueryService {
async testConnection(sourceOptions: SourceOptions): Promise<ConnectionTestResult> {
const openai: OpenAI = await this.getConnection(sourceOptions);
console.log();
try {
const response = await openai.models.list();
if (response.data) {
const response = await openai.models.list(); // The response doesn't have a 'status'
if (response.data.length > 0) {
// Checking if models exist in the response
return {
status: 'ok',
};
} else {
throw new QueryError('No models found', 'The models list is empty', {});
}
} catch (error) {
throw new QueryError('Connection could not be established', error?.message, {});
@ -50,14 +58,18 @@ export default class Openai implements QueryService {
async getConnection(sourceOptions: SourceOptions): Promise<OpenAI> {
const { apiKey, organizationId = null } = sourceOptions;
const config: OpenAI.FunctionParameters = {
apiKey: apiKey,
const creds = {
apiKey: apiKey, // No hardcoding, pulling from sourceOptions
};
if (organizationId) {
config.organization = organizationId;
creds['organizationId'] = organizationId;
}
const openai = new OpenAI(config);
// Initialize OpenAI instance directly with API key
const openai = new OpenAI({
apiKey: apiKey,
});
return openai;
}
}
}

View file

@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/operations.schema.json",
"title": "OpenAI datasource",
"title": "OpenAI Datasource",
"description": "A schema defining OpenAI datasource",
"type": "api",
"defaults": {},
@ -11,114 +11,295 @@
"type": "dropdown-component-flip",
"description": "Single select dropdown for operation",
"list": [
{
"value": "completion",
"name": "Completion"
},
{
"value": "chat",
"name": "Chat"
}
{ "value": "chat", "name": "Chat" },
{ "value": "completion", "name": "Completion" },
{ "value": "image_generation", "name": "Generate AI Image(s)" }
]
},
"completion": {
"prompt": {
"label": "Prompt",
"key": "prompt",
"type": "codehinter",
"description": "Enter prompt",
"height": "150px"
"chat": {
"model": {
"label": "Model",
"key": "model",
"type": "dropdown-component-flip",
"description": "Select OpenAI Model",
"list": [
{ "value": "gpt-4o", "name": "GPT-4.0" },
{ "value": "gpt-4o-mini", "name": "GPT-4.0 mini" },
{ "value": "gpt-4-turbo", "name": "GPT-4 Turbo" },
{ "value": "gpt-3.5-turbo-0125", "name": "GPT-3.5 Turbo" }
]
},
"max_tokens": {
"label": "Max Tokens",
"key": "max_tokens",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter from 1 to 2048",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins"
"gpt-4o": {
"prompt": {
"label": "Message",
"key": "prompt",
"type": "codehinter",
"description": "Enter message",
"placeholder": "Draft an email or other piece of writing",
"height": "150px"
},
"max_tokens": {
"label": "Max Tokens",
"key": "max_tokens",
"type": "codehinter",
"description": "Enter from 1 to 2048",
"width": "320px",
"height": "36px"
},
"temperature": {
"label": "Temperature",
"key": "temperature",
"type": "codehinter",
"description": "Enter from 0 to 1",
"width": "320px",
"height": "36px"
},
"stop_sequence": {
"label": "Stop Sequence",
"key": "stop_sequence",
"type": "codehinter",
"description": "Enter stop sequence",
"width": "320px",
"height": "36px"
},
"suffix": {
"label": "Suffix",
"key": "suffix",
"type": "codehinter",
"description": "Enter suffix",
"width": "320px",
"height": "36px"
}
},
"temperature": {
"label": "Temperature",
"key": "temperature",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter from 0 to 1",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins"
"gpt-4o-mini": {
"prompt": {
"label": "Message",
"key": "prompt",
"type": "codehinter",
"description": "Enter message",
"placeholder": "Draft an email or other piece of writing",
"height": "150px"
},
"max_tokens": {
"label": "Max Tokens",
"key": "max_tokens",
"type": "codehinter",
"description": "Enter from 1 to 2048",
"width": "320px",
"height": "36px"
},
"temperature": {
"label": "Temperature",
"key": "temperature",
"type": "codehinter",
"description": "Enter from 0 to 1",
"width": "320px",
"height": "36px"
},
"stop_sequence": {
"label": "Stop Sequence",
"key": "stop_sequence",
"type": "codehinter",
"description": "Enter stop sequence",
"width": "320px",
"height": "36px"
},
"suffix": {
"label": "Suffix",
"key": "suffix",
"type": "codehinter",
"description": "Enter suffix",
"width": "320px",
"height": "36px"
}
},
"stop_sequence": {
"label": "Stop Sequence",
"key": "stop_sequence",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter stop sequence",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins"
"gpt-4-turbo": {
"prompt": {
"label": "Message",
"key": "prompt",
"type": "codehinter",
"description": "Enter message",
"placeholder": "Draft an email or other piece of writing",
"height": "150px"
},
"max_tokens": {
"label": "Max Tokens",
"key": "max_tokens",
"type": "codehinter",
"description": "Enter from 1 to 2048",
"width": "320px",
"height": "36px"
},
"temperature": {
"label": "Temperature",
"key": "temperature",
"type": "codehinter",
"description": "Enter from 0 to 1",
"width": "320px",
"height": "36px"
},
"stop_sequence": {
"label": "Stop Sequence",
"key": "stop_sequence",
"type": "codehinter",
"description": "Enter stop sequence",
"width": "320px",
"height": "36px"
},
"suffix": {
"label": "Suffix",
"key": "suffix",
"type": "codehinter",
"description": "Enter suffix",
"width": "320px",
"height": "36px"
}
},
"suffix": {
"label": "Suffix",
"key": "suffix",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter suffix",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins"
"gpt-3.5-turbo-0125": {
"prompt": {
"label": "Message",
"key": "prompt",
"type": "codehinter",
"description": "Enter message",
"placeholder": "Draft an email or other piece of writing",
"height": "150px"
},
"max_tokens": {
"label": "Max Tokens",
"key": "max_tokens",
"type": "codehinter",
"description": "Enter from 1 to 2048",
"width": "320px",
"height": "36px"
},
"temperature": {
"label": "Temperature",
"key": "temperature",
"type": "codehinter",
"description": "Enter from 0 to 1",
"width": "320px",
"height": "36px"
},
"stop_sequence": {
"label": "Stop Sequence",
"key": "stop_sequence",
"type": "codehinter",
"description": "Enter stop sequence",
"width": "320px",
"height": "36px"
},
"suffix": {
"label": "Suffix",
"key": "suffix",
"type": "codehinter",
"description": "Enter suffix",
"width": "320px",
"height": "36px"
}
}
},
"chat": {
"prompt": {
"label": "Message",
"key": "prompt",
"type": "codehinter",
"description": "Enter message",
"placeholder": "Draft an email or other piece of writing",
"height": "150px"
"completion": {
"model": {
"label": "Model",
"key": "model",
"type": "dropdown-component-flip",
"description": "Select OpenAI Model",
"list": [
{ "value": "gpt-3.5-turbo-instruct", "name": "GPT 3.5 Turbo" }
]
},
"max_tokens": {
"label": "Max Tokens",
"key": "max_tokens",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter from 1 to 2048",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins"
},
"temperature": {
"label": "Temperature",
"key": "temperature",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter from 0 to 1",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins"
},
"stop_sequence": {
"label": "Stop Sequence",
"key": "stop_sequence",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter stop sequence",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins"
},
"suffix": {
"label": "Suffix",
"key": "suffix",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter suffix",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins"
}
"gpt-3.5-turbo-instruct": {
"prompt": {
"label": "Prompt",
"key": "prompt",
"type": "codehinter",
"description": "Enter prompt",
"height": "150px"
},
"max_tokens": {
"label": "Max Tokens",
"key": "max_tokens",
"type": "codehinter",
"description": "Enter from 1 to 2048",
"width": "320px",
"height": "36px"
},
"temperature": {
"label": "Temperature",
"key": "temperature",
"type": "codehinter",
"description": "Enter from 0 to 1",
"width": "320px",
"height": "36px"
},
"stop_sequence": {
"label": "Stop Sequence",
"key": "stop_sequence",
"type": "codehinter",
"description": "Enter stop sequence",
"width": "320px",
"height": "36px"
},
"suffix": {
"label": "Suffix",
"key": "suffix",
"type": "codehinter",
"description": "Enter suffix",
"width": "320px",
"height": "36px"
}
}
},
"image_generation": {
"model": {
"label": "Model",
"key": "model",
"type": "dropdown-component-flip",
"description": "Select OpenAI Model",
"list": [
{ "value": "dall-e-3", "name": "DALL-E 3" },
{ "value": "dall-e-2", "name": "DALL-E 2" }
],
"disabled": true,
"default": "dall-e-3"
},
"dall-e-3": {
"prompt": {
"label": "Prompt",
"key": "prompt",
"type": "codehinter",
"description": "Enter prompt for image generation",
"height": "150px"
},
"size": {
"label": "Size (in pixels)",
"key": "size",
"type": "codehinter",
"description": "Enter image size in pixels (e.g., 1024x1024)",
"placeholder": "1024x1024, 1792x1024 or 1024x1792. By default 1024x1024 sized image is generated",
"width": "320px",
"height": "36px"
}
},
"dall-e-2": {
"prompt": {
"label": "Prompt",
"key": "prompt",
"type": "codehinter",
"description": "Enter prompt for image generation",
"height": "150px"
},
"size": {
"label": "Size (in pixels)",
"key": "size",
"type": "codehinter",
"description": "Enter image size in pixels (e.g., 1024x1024)",
"placeholder": "256x256, 512x512 or 1024x1024. By default 1024x1024 sized image is generated",
"width": "320px",
"height": "36px"
}
}
}
}
}

View file

@ -1,37 +1,73 @@
import OpenAI from 'openai';
import OpenAI from 'openai'; // Updated SDK version
import { QueryOptions } from './types';
// Updated utility function to handle size validation based on model
const getSizeEnum = (
model: string | undefined,
size: string | undefined
): '256x256' | '512x512' | '1024x1024' | '1792x1024' | '1024x1792' => {
// If the model is DALL-E 3, only allow 1024x1024, 1792x1024, or 1024x1792
if (model === 'dall-e-3') {
switch (size) {
case '1024x1024':
return '1024x1024';
case '1792x1024':
return '1792x1024';
case '1024x1792':
return '1024x1792';
default:
return '1024x1024'; // Default size for DALL-E 3
}
}
// If the model is DALL-E 2, only allow 1024x1024, 512x512, or 256x256
if (model === 'dall-e-2') {
switch (size) {
case '1024x1024':
return '1024x1024';
case '512x512':
return '512x512';
case '256x256':
return '256x256';
default:
return '1024x1024'; // Default size for DALL-E 2
}
}
// Default size if model is not recognized
return '1024x1024';
};
//Utility function to convert number of images from string or number
/*const getNumberOfImages = (num_images: number | string | undefined): number => {
const num = typeof num_images === 'string' ? parseInt(num_images) : num_images;
return isNaN(num) ? 1 : Math.max(1, Math.min(10, num)); // Ensure it's between 1 and 10
};*/
export async function getCompletion(
openai: OpenAI,
options: QueryOptions
): Promise<string | { error: string; statusCode: number }> {
const { prompt, max_tokens, temperature, stop_sequence, suffix } = options;
const { model, prompt, max_tokens, temperature, stop_sequence, suffix } = options;
try {
const completion = await openai.completions.create({
model: 'gpt-3.5-turbo-instruct',
const response = await openai.completions.create({
model: model || 'gpt-3.5-turbo-instruct',
prompt: prompt,
temperature: typeof temperature === 'string' ? parseFloat(temperature) : temperature || 0,
max_tokens: typeof max_tokens === 'string' ? parseInt(max_tokens) : max_tokens || 67,
stop: stop_sequence || undefined,
suffix: suffix || undefined,
stop: stop_sequence || null,
suffix: suffix || null,
});
return completion.choices[0].text;
return response.choices[0].text; // Access the response correctly
} catch (error) {
console.log('error openapi ===============', error);
console.log('Error openai ===============', error);
if (error instanceof OpenAI.APIError) {
return {
error: error.message,
statusCode: error.status,
};
} else {
return {
error: 'An unknown error occurred',
statusCode: 500,
};
}
return {
error: error?.message,
statusCode: error?.response?.status,
};
}
}
@ -39,34 +75,59 @@ export async function getChatCompletion(
openai: OpenAI,
options: QueryOptions
): Promise<string | { error: string; statusCode: number }> {
const { prompt, max_tokens, temperature, stop_sequence } = options;
const { model, prompt, max_tokens, temperature, stop_sequence } = options;
try {
const chatCompletion = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
const response = await openai.chat.completions.create({
model: model || 'gpt-4-turbo',
temperature: typeof temperature === 'string' ? parseFloat(temperature) : temperature || 0,
max_tokens: typeof max_tokens === 'string' ? parseInt(max_tokens) : max_tokens || 67,
stop: stop_sequence || undefined,
stop: stop_sequence || null,
messages: [
{
role: 'assistant',
role: 'user',
content: prompt,
},
],
});
return chatCompletion.choices[0].message.content;
return response.choices[0].message.content; // Ensure to access the correct part of the response
} catch (error) {
if (error instanceof OpenAI.APIError) {
return {
error: error.message,
statusCode: error.status,
};
} else {
return {
error: 'An unknown error occurred',
statusCode: 500,
};
}
return {
error: error?.message,
statusCode: error?.response?.status,
};
}
}
}
export async function generateImage(
openai: OpenAI,
options: QueryOptions
): Promise<{ status: string; message: string; description?: string; data?: any }> {
const { model, prompt, size /* , n */ } = options;
try {
const response = await openai.images.generate({
model: model || 'dall-e-3',
prompt: prompt || '',
size: getSizeEnum(model, size), // Convert and validate image size based on the model
//n: getNumberOfImages(num_images), Convert and validate number of images
});
// Return the URL of the first image as a JSON object
return {
status: 'success',
message: 'Image generated successfully',
data: { url: response.data[0].url },
};
} catch (error: any) {
console.error('Error in image generation:', error);
return {
status: 'failed',
message: 'Query could not be completed',
description: error?.response?.data?.error?.message || 'An unexpected error occurred',
data: error?.response?.data || {},
};
}
}

View file

@ -4,15 +4,19 @@ export type SourceOptions = {
};
export type QueryOptions = {
model?: string; // Added model as an optional field
operation: Operation;
prompt?: string;
max_tokens?: number | string;
temperature?: number | string;
stop_sequence?: string;
suffix?: string | null;
//num_images?: number | string; // Number of images for generation
size?: string; // Size of the generated image
};
export enum Operation {
Completion = 'completion',
Chat = 'chat',
ImageGeneration = 'image_generation' // New operation for image generation
}

View file

@ -18,8 +18,7 @@
"homepage": "https://github.com/tooljet/tooljet#readme",
"dependencies": {
"@tooljet-marketplace/common": "^1.0.0",
"octokit": "4.0.2",
"openai": "^4.52.1"
"openai": "^4.56.0"
},
"devDependencies": {
"@vercel/ncc": "^0.34.0",

5
marketplace/plugins/presto/.gitignore vendored Normal file
View file

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

View file

@ -0,0 +1,4 @@
# PrestoDB
Documentation on: https://docs.tooljet.com/docs/marketplace/plugins/marketplace-plugin-prestodb

View file

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

View file

@ -0,0 +1,23 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.5785 5.33379C13.9276 4.98463 13.9276 4.41854 13.5785 4.0694C13.2293 3.72025 12.6632 3.72026 12.3141 4.06942C11.9649 4.41858 11.9649 4.98467 12.3141 5.33381C12.6632 5.68296 13.2293 5.68295 13.5785 5.33379Z" fill="#5B87DA"/>
<path d="M16.1051 7.86124C16.4929 7.47347 16.4928 6.84479 16.1051 6.45703C15.7173 6.06928 15.0886 6.06929 14.7009 6.45706C14.3131 6.84483 14.3131 7.47352 14.7009 7.86127C15.0887 8.24902 15.7173 8.24901 16.1051 7.86124Z" fill="#5B87DA"/>
<path d="M18.6328 10.3887C19.0594 9.96206 19.0594 9.27039 18.6327 8.84378C18.2061 8.41718 17.5145 8.4172 17.0879 8.84381C16.6613 9.27043 16.6613 9.9621 17.0879 10.3887C17.5145 10.8153 18.2062 10.8153 18.6328 10.3887Z" fill="#5B87DA"/>
<path d="M21.1577 12.9135C21.6229 12.4483 21.6229 11.694 21.1577 11.2288C20.6924 10.7636 19.9382 10.7636 19.473 11.2288C19.0077 11.6941 19.0078 12.4483 19.473 12.9135C19.9382 13.3787 20.6925 13.3787 21.1577 12.9135Z" fill="#5B87DA"/>
<path d="M8.59598 5.26315C8.90628 4.95284 8.90627 4.44973 8.59596 4.13943C8.28565 3.82913 7.78255 3.82915 7.47225 4.13946C7.16195 4.44977 7.16195 4.95287 7.47226 5.26317C7.78257 5.57347 8.28568 5.57346 8.59598 5.26315Z" fill="#1EDCFF"/>
<path d="M11.1235 7.79149C11.4727 7.44233 11.4727 6.87624 11.1235 6.5271C10.7743 6.17795 10.2083 6.17797 9.85911 6.52712C9.50997 6.87628 9.50998 7.44237 9.85913 7.79151C10.2083 8.14066 10.7744 8.14065 11.1235 7.79149Z" fill="#1EDCFF"/>
<path d="M13.6481 10.316C14.0358 9.92819 14.0358 9.2995 13.648 8.91175C13.2603 8.52399 12.6316 8.52401 12.2438 8.91177C11.8561 9.29954 11.8561 9.92823 12.2439 10.316C12.6316 10.7037 13.2603 10.7037 13.6481 10.316Z" fill="#1EDCFF"/>
<path d="M16.1751 12.8421C16.6017 12.4155 16.6017 11.7238 16.1751 11.2972C15.7485 10.8706 15.0568 10.8706 14.6302 11.2972C14.2036 11.7238 14.2036 12.4155 14.6302 12.8421C15.0569 13.2687 15.7485 13.2687 16.1751 12.8421Z" fill="#1EDCFF"/>
<path d="M18.7022 15.3708C19.1675 14.9056 19.1674 14.1513 18.7022 13.6861C18.237 13.2209 17.4827 13.2209 17.0175 13.6861C16.5523 14.1514 16.5523 14.9056 17.0175 15.3708C17.4828 15.8361 18.237 15.836 18.7022 15.3708Z" fill="#1EDCFF"/>
<path d="M3.61745 5.19633C3.88914 4.92463 3.88914 4.48412 3.61744 4.21243C3.34574 3.94074 2.90523 3.94075 2.63354 4.21245C2.36185 4.48415 2.36186 4.92465 2.63356 5.19634C2.90526 5.46803 3.34576 5.46803 3.61745 5.19633Z" fill="black"/>
<path d="M6.14016 7.71908C6.45046 7.40877 6.45046 6.90566 6.14015 6.59537C5.82984 6.28507 5.32674 6.28508 5.01644 6.59539C4.70614 6.9057 4.70614 7.4088 5.01645 7.7191C5.32676 8.0294 5.82987 8.02939 6.14016 7.71908Z" fill="black"/>
<path d="M8.66906 10.2486C9.01821 9.89949 9.0182 9.3334 8.66904 8.98425C8.31988 8.63511 7.75379 8.63512 7.40465 8.98428C7.0555 9.33344 7.05551 9.89953 7.40467 10.2487C7.75383 10.5978 8.31991 10.5978 8.66906 10.2486Z" fill="black"/>
<path d="M11.1948 12.7744C11.5826 12.3867 11.5826 11.758 11.1948 11.3702C10.807 10.9825 10.1783 10.9825 9.79059 11.3702C9.40284 11.758 9.40285 12.3867 9.79062 12.7744C10.1784 13.1622 10.8071 13.1622 11.1948 12.7744Z" fill="black"/>
<path d="M13.7207 15.301C14.1473 14.8743 14.1473 14.1827 13.7206 13.7561C13.294 13.3295 12.6024 13.3295 12.1758 13.7561C11.7492 14.1827 11.7492 14.8744 12.1758 15.301C12.6024 15.7276 13.2941 15.7276 13.7207 15.301Z" fill="black"/>
<path d="M16.2446 17.8241C16.7098 17.3588 16.7098 16.6046 16.2446 16.1394C15.7793 15.6741 15.0251 15.6742 14.5599 16.1394C14.0947 16.6046 14.0947 17.3589 14.5599 17.8241C15.0251 18.2893 15.7794 18.2893 16.2446 17.8241Z" fill="black"/>
<path d="M8.59656 15.0868C8.90689 14.7765 8.90689 14.2733 8.59654 13.9629C8.28619 13.6526 7.78302 13.6526 7.47268 13.963C7.16234 14.2733 7.16235 14.7765 7.4727 15.0868C7.78305 15.3972 8.28622 15.3972 8.59656 15.0868Z" fill="#5B87DA"/>
<path d="M6.14087 17.5426C6.45121 17.2322 6.4512 16.7291 6.14085 16.4187C5.8305 16.1084 5.32733 16.1084 5.017 16.4187C4.70666 16.7291 4.70667 17.2323 5.01702 17.5426C5.32736 17.8529 5.83053 17.8529 6.14087 17.5426Z" fill="#5B87DA"/>
<path d="M3.68579 20.0003C3.99613 19.6899 3.99612 19.1867 3.68577 18.8764C3.37542 18.5661 2.87225 18.5661 2.56192 18.8764C2.25158 19.1868 2.25159 19.6899 2.56194 20.0003C2.87229 20.3106 3.37545 20.3106 3.68579 20.0003Z" fill="#5B87DA"/>
<path d="M8.5968 19.9989C8.90714 19.6885 8.90713 19.1854 8.59678 18.875C8.28643 18.5647 7.78326 18.5647 7.47293 18.875C7.16259 19.1854 7.1626 19.6886 7.47295 19.9989C7.7833 20.3092 8.28646 20.3092 8.5968 19.9989Z" fill="#5B87DA"/>
<path d="M13.5092 20.0007C13.8195 19.6904 13.8195 19.1872 13.5091 18.8769C13.1988 18.5665 12.6956 18.5665 12.3853 18.8769C12.0749 19.1872 12.075 19.6904 12.3853 20.0007C12.6956 20.3111 13.1988 20.3111 13.5092 20.0007Z" fill="#5B87DA"/>
<path d="M11.0522 17.5425C11.3626 17.2321 11.3626 16.729 11.0522 16.4186C10.7419 16.1083 10.2387 16.1083 9.92837 16.4187C9.61803 16.729 9.61804 17.2322 9.92839 17.5425C10.2387 17.8528 10.7419 17.8528 11.0522 17.5425Z" fill="#5B87DA"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -0,0 +1,74 @@
import { QueryError, QueryResult, QueryService, ConnectionTestResult } from '@tooljet-marketplace/common';
import { SourceOptions, QueryOptions } from './types';
import PrestoClient from '@prestodb/presto-js-client';
export default class Presto implements QueryService {
async run(sourceOptions: SourceOptions, queryOptions: QueryOptions, dataSourceId: string): Promise<QueryResult> {
const prestoClient = await this.getConnection(sourceOptions);
const { presto_sql_query } = queryOptions;
try {
const prestoQueryResult = await prestoClient.query(presto_sql_query);
return {
status: 'ok',
data: prestoQueryResult,
};
} catch (error) {
throw new QueryError('Query could not be completed', error, {});
}
}
async getConnection(sourceOptions: SourceOptions): Promise<any> {
const {
db_auth_username,
db_auth_password,
db_config_catalog,
db_config_host,
db_config_port,
db_config_schema,
db_config_user,
db_config_timezone,
db_config_extra_headers,
} = sourceOptions;
const headersObj =
db_config_extra_headers
?.filter(([key, value]) => key !== '' && value !== '')
?.reduce((acc, [key, value]) => {
acc[key] = value;
return acc;
}, {}) || {};
const client = new PrestoClient({
basicAuthentication: {
user: db_auth_username,
password: db_auth_password,
},
catalog: db_config_catalog,
...(Object.keys(headersObj).length > 0 && { extraHeaders: headersObj }),
host: db_config_host,
port: db_config_port,
schema: db_config_schema,
timezone: db_config_timezone,
user: db_config_user,
});
return client;
}
async testConnection(sourceOptions: SourceOptions): Promise<ConnectionTestResult> {
const prestoClient = await this.getConnection(sourceOptions);
try {
const result = await prestoClient.query('select 1');
if (result.data[0][0] === 1) {
return { status: 'ok' };
}
} catch (error) {
return {
status: 'failed',
message: error?.message ?? 'Failed to establish connection',
};
}
}
}

View file

@ -0,0 +1,104 @@
{
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/manifest.schema.json",
"title": "PrestoDB data source",
"description": "A schema defining PrestoDB data source",
"type": "database",
"source": {
"name": "PrestoDB",
"kind": "Presto",
"exposedVariables": {
"isLoading": false,
"data": {},
"rawData": {}
},
"options": {
"username": {
"type": "string"
},
"password": {
"type": "password",
"encrypted": true
},
"catalog": {
"type": "string"
},
"host": {
"type": "string"
},
"port": {
"type": "number"
},
"schema": {
"type": "string"
},
"user": {
"type": "string"
},
"timezone": {
"type": "string"
},
"extra_headers": {
"type": "array"
}
}
},
"defaults": {},
"properties": {
"username": {
"label": "Username",
"key": "db_auth_username",
"type": "text",
"description": "my-user"
},
"password": {
"label": "Password",
"key": "db_auth_password",
"type": "password",
"decription": "Password for PrestoDB Database"
},
"catalog": {
"label": "Catalog",
"key": "db_config_catalog",
"type": "text",
"description": "system"
},
"host": {
"label": "Host",
"key": "db_config_host",
"type": "text",
"description": "http://localhost"
},
"port": {
"label": "Port",
"key": "db_config_port",
"type": "text",
"description": "8080"
},
"schema": {
"label": "Schema",
"key": "db_config_schema",
"type": "text",
"description": "information_schema"
},
"user": {
"label": "User",
"key": "db_config_user",
"type": "text",
"description": "root"
},
"timezone": {
"label": "Timezone",
"key": "db_config_timezone",
"type": "text",
"description": "Enter timezone"
},
"extra_headers": {
"label": "Extra headers",
"key": "db_config_extra_headers",
"type": "react-component-headers",
"description": "Extra headers for Presto Client"
}
},
"required": []
}

View file

@ -0,0 +1,17 @@
{
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/operations.schema.json",
"title": "PrestoDB data source",
"description": "A schema defining PrestoDB data source",
"type": "database",
"defaults": {},
"properties": {
"sql_query": {
"key": "presto_sql_query",
"type": "codehinter",
"description": "Enter your SQL query",
"height": "150px",
"editorType": "extendedSingleLine"
}
}
}

View file

@ -0,0 +1,15 @@
export type SourceOptions = {
db_auth_username: string;
db_auth_password: string;
db_config_catalog: string;
db_config_host: string;
db_config_port: number;
db_config_schema: string;
db_config_user: string;
db_config_timezone: string;
db_config_extra_headers: [string, string][];
};
export type QueryOptions = {
operation: string;
presto_sql_query: string;
};

View file

@ -0,0 +1,27 @@
{
"name": "@tooljet-marketplace/presto",
"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",
"watch": "ncc build lib/index.ts -o dist --watch"
},
"homepage": "https://github.com/tooljet/tooljet#readme",
"dependencies": {
"@prestodb/presto-js-client": "^1.0.0",
"@tooljet-marketplace/common": "^1.0.0"
},
"devDependencies": {
"@vercel/ncc": "^0.34.0",
"typescript": "^4.7.4"
}
}

View file

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

View file

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

View file

@ -0,0 +1,4 @@
# Sharepoint
Documentation on: https://docs.tooljet.com/docs/data-sources/sharepoint

View file

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

View file

@ -0,0 +1,23 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_507_12450)">
<path d="M12.2328 13.3955C15.3153 13.3955 17.8142 10.8966 17.8142 7.81406C17.8142 4.73154 15.3153 2.23267 12.2328 2.23267C9.15024 2.23267 6.65137 4.73154 6.65137 7.81406C6.65137 10.8966 9.15024 13.3955 12.2328 13.3955Z" fill="#036C70"/>
<path d="M16.8839 18.0465C19.7095 18.0465 22.0001 15.7559 22.0001 12.9302C22.0001 10.1046 19.7095 7.81396 16.8839 7.81396C14.0582 7.81396 11.7676 10.1046 11.7676 12.9302C11.7676 15.7559 14.0582 18.0465 16.8839 18.0465Z" fill="#1A9BA1"/>
<path d="M12.93 21.7676C15.1135 21.7676 16.8835 19.9975 16.8835 17.8141C16.8835 15.6306 15.1135 13.8606 12.93 13.8606C10.7466 13.8606 8.97656 15.6306 8.97656 17.8141C8.97656 19.9975 10.7466 21.7676 12.93 21.7676Z" fill="#37C6D0"/>
<path opacity="0.1" d="M13.1628 7.26986V17.1954C13.1605 17.5403 12.9515 17.8502 12.6326 17.9815C12.5311 18.0244 12.4219 18.0466 12.3117 18.0466H8.98144C8.9768 17.9675 8.97679 17.8931 8.97679 17.814C8.97525 17.7364 8.97835 17.6588 8.9861 17.5815C9.07123 16.0947 9.98715 14.7829 11.3535 14.1908V13.3257C8.31257 12.8438 6.23802 9.98792 6.71991 6.94695C6.72326 6.92589 6.72671 6.90483 6.73029 6.88381C6.75344 6.72695 6.78607 6.57163 6.82797 6.4187H12.3117C12.781 6.42049 13.1611 6.80052 13.1628 7.26986Z" fill="black"/>
<path opacity="0.2" d="M11.8467 6.88379H6.73038C6.21354 9.91929 8.25531 12.799 11.2908 13.3159C11.3827 13.3315 11.475 13.3449 11.5676 13.3559C10.1257 14.0396 9.07503 15.9815 8.98573 17.5815C8.97799 17.6587 8.97488 17.7364 8.97642 17.814C8.97642 17.8931 8.97642 17.9675 8.98107 18.0466C8.98946 18.2029 9.00968 18.3584 9.04153 18.5117H11.8462C12.1911 18.5094 12.5009 18.3004 12.6322 17.9815C12.6752 17.8799 12.6973 17.7708 12.6973 17.6605V7.73495C12.6956 7.26579 12.3158 6.88584 11.8467 6.88379Z" fill="black"/>
<path opacity="0.2" d="M11.8466 6.88379H6.73034C6.21361 9.91957 8.25569 12.7994 11.2915 13.3162C11.3536 13.3267 11.4158 13.3363 11.4783 13.3447C10.0829 14.0777 9.07361 16.015 8.98616 17.5815H11.8466C12.3152 17.5779 12.6942 17.1989 12.6978 16.7303V7.73495C12.696 7.26561 12.316 6.88558 11.8466 6.88379Z" fill="black"/>
<path opacity="0.2" d="M11.3814 6.88379H6.73019C6.24226 9.7496 8.03799 12.5094 10.8558 13.2243C9.78883 14.4433 9.13442 15.9683 8.98601 17.5815H11.3814C11.8507 17.5797 12.2307 17.1996 12.2325 16.7303V7.73495C12.2323 7.26498 11.8513 6.88405 11.3814 6.88379Z" fill="black"/>
<path d="M2.85256 6.88379H11.38C11.8509 6.88379 12.2326 7.26549 12.2326 7.73635V16.2638C12.2326 16.7346 11.8509 17.1163 11.38 17.1163H2.85256C2.3817 17.1163 2 16.7346 2 16.2638V7.73635C2 7.26549 2.3817 6.88379 2.85256 6.88379Z" fill="url(#paint0_linear_507_12450)"/>
<path d="M5.80776 11.8958C5.60797 11.7633 5.44108 11.5869 5.31985 11.38C5.20239 11.1638 5.14385 10.9204 5.15008 10.6744C5.13962 10.3413 5.25202 10.016 5.46589 9.76048C5.69064 9.5046 5.98113 9.31511 6.30589 9.21257C6.67602 9.09074 7.0637 9.03071 7.45333 9.0349C7.96573 9.01617 8.47741 9.08782 8.96496 9.24653V10.3163C8.75312 10.188 8.52238 10.0938 8.28124 10.0372C8.01958 9.97306 7.7511 9.94089 7.4817 9.94141C7.19761 9.93098 6.91534 9.99073 6.65984 10.1154C6.46259 10.2004 6.33467 10.3945 6.33426 10.6093C6.33346 10.7396 6.38352 10.8651 6.4738 10.9591C6.58043 11.0699 6.70656 11.1601 6.84589 11.2251C7.00093 11.3023 7.23349 11.405 7.54357 11.5331C7.57771 11.5438 7.61099 11.5572 7.6431 11.5731C7.94828 11.6923 8.24269 11.8375 8.5231 12.007C8.73546 12.1379 8.91368 12.3174 9.04311 12.5307C9.1758 12.7725 9.24011 13.0459 9.22915 13.3214C9.2443 13.6634 9.13968 13.9999 8.93334 14.2731C8.72767 14.5241 8.45269 14.7092 8.14264 14.8051C7.77795 14.9195 7.39733 14.9747 7.01519 14.9689C6.67234 14.9704 6.32999 14.9424 5.99194 14.8851C5.70651 14.8384 5.42796 14.7566 5.16264 14.6414V13.5135C5.41625 13.6946 5.69959 13.83 5.99985 13.9135C6.2991 14.0068 6.61017 14.0566 6.92357 14.0614C7.21363 14.0798 7.50318 14.0183 7.76078 13.8838C7.94123 13.7819 8.05098 13.589 8.04637 13.3819C8.04757 13.2377 7.99055 13.0992 7.88823 12.9977C7.76098 12.8728 7.61366 12.7701 7.45242 12.694C7.26638 12.601 6.99242 12.4783 6.63056 12.3261C6.34269 12.2103 6.06706 12.0662 5.80776 11.8958Z" fill="white"/>
</g>
<defs>
<linearGradient id="paint0_linear_507_12450" x1="3.7776" y1="6.21762" x2="10.455" y2="17.7825" gradientUnits="userSpaceOnUse">
<stop stop-color="#058F92"/>
<stop offset="0.5" stop-color="#038489"/>
<stop offset="1" stop-color="#026D71"/>
</linearGradient>
<clipPath id="clip0_507_12450">
<rect width="20" height="19.5349" fill="white" transform="translate(2 2.23267)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -0,0 +1,353 @@
import { OAuthUnauthorizedClientError, QueryError, QueryResult, QueryService } from '@tooljet-marketplace/common';
import { SourceOptions, QueryOptions } from './types';
export default class Sharepoint implements QueryService {
authUrl(sourceOptions): string {
const host = process.env.TOOLJET_HOST;
const subpath = process.env.SUB_PATH;
const fullUrl = `${host}${subpath ? subpath : '/'}`;
const clientId = sourceOptions.sp_client_id;
const clientSecret = sourceOptions.sp_client_secret.value;
const tenant = sourceOptions.sp_tenant_id;
if (!clientId || !clientSecret || !tenant) {
throw Error('You need to enter the client ID, client secret and tenant ID for authentication.');
}
return (
'https://login.microsoftonline.com/common/oauth2/v2.0/authorize' +
`?client_id=${clientId.value}&response_type=code` +
`&redirect_uri=${fullUrl}oauth2/authorize`
);
}
async accessDetailsFrom(authCode: string, sourceOptions: any, resetSecureData = false): Promise<object> {
if (resetSecureData) {
return [
['access_token', ''],
['refresh_token', ''],
];
}
let sp_client_id = '';
let sp_client_secret = '';
let tenant = '';
for (const item of sourceOptions) {
if (item.key === 'sp_client_id') {
sp_client_id = item.value;
}
if (item.key === 'sp_client_secret') {
sp_client_secret = item.value;
}
if (item.key === 'sp_tenant_id') {
tenant = item.value;
}
}
const accessTokenUrl = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`;
const host = process.env.TOOLJET_HOST;
const subpath = process.env.SUB_PATH;
const fullUrl = `${host}${subpath ? subpath : '/'}`;
const redirectUri = `${fullUrl}oauth2/authorize`;
const data = new URLSearchParams({
code: authCode,
client_id: sp_client_id,
client_secret: sp_client_secret,
grant_type: 'authorization_code',
redirect_uri: redirectUri,
scope: 'https://graph.microsoft.com/.default+offline_access',
});
const authDetails = [];
try {
const response = await fetch(accessTokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data.toString(),
});
const result = await response.json();
if (!response.ok) {
console.error(`Error occurred: `, result);
throw new Error(result.error_description);
}
if (result['access_token']) {
authDetails.push(['access_token', result['access_token']]);
}
if (result['refresh_token']) {
authDetails.push(['refresh_token', result['refresh_token']]);
}
} catch (error) {
throw Error(`Could not connect to Sharepoint:\n${error?.message}`);
}
return authDetails;
}
async run(sourceOptions: SourceOptions, queryOptions: QueryOptions, dataSourceId: string): Promise<QueryResult> {
const rootApiUrl = 'https://graph.microsoft.com/v1.0/sites';
const accessToken = sourceOptions.access_token;
let response = null;
let data = null;
try {
const requestOptions = await this.fetchRequestOptsForOperation(accessToken, queryOptions);
const endpoint = requestOptions?.endpoint;
const apiUrl = `${rootApiUrl}${endpoint}`;
const method = requestOptions?.method;
const header = requestOptions?.headers;
const body = requestOptions.body || {};
if (requestOptions?.paginationFeature && queryOptions.sp_page) {
const regex = /^[1-9]\d*(\.\d+)?$/;
if (regex.test(queryOptions.sp_page)) {
const pageNo = parseInt(queryOptions.sp_page || '1');
const paginatedResponse = await this.getPageData(apiUrl, pageNo, header);
response = paginatedResponse.response;
data = paginatedResponse.data;
} else {
throw new Error('Page field value should be a number >= 1.');
}
} else {
response = await fetch(apiUrl, {
method: method,
headers: header,
...(Object.keys(body).length !== 0 && { body: JSON.stringify(body) }),
});
if (
!response.ok &&
response.status !== 401 &&
response.status !== 403 &&
response.status !== 204 &&
response.status !== 201
) {
const data = await response.json();
const errorMessage = data?.error?.message || 'An unknown error occurred';
throw new QueryError('Query could not be completed', errorMessage, {
statusCode: response.status,
...data?.error,
});
}
if (response.status === 204) {
return {
status: 'ok',
data: {
code: response.status,
status: response.statusText,
message: `Item having id '${queryOptions.sp_item_id}' in List '${queryOptions.sp_list_id}' has been deleted.`,
},
};
}
data = await response.json();
}
} catch (error) {
const errorMessage = error?.message === 'Query could not be completed' ? error?.description : error?.message;
throw new QueryError('Query could not be completed', errorMessage, error?.data || {});
}
if (response.status === 401 || response.status === 403) {
throw new OAuthUnauthorizedClientError('Unauthorized client error', response.statusText, data);
}
return {
status: 'ok',
data: data,
};
}
async getPageData(apiUrl: string, pageNo: number, header: any): Promise<any> {
let currentPage = 1;
let nextApiUrl = apiUrl;
let result = null;
while (currentPage <= pageNo) {
const response = await fetch(nextApiUrl, {
method: 'GET',
headers: header,
});
const data = await response.json();
if (!response.ok && response.status !== 401 && response.status !== 403) {
throw new QueryError('Query could not be completed', data?.error?.message || 'An unknown error occurred', {
statusCode: response.status,
...data?.error,
});
}
if (currentPage === pageNo) {
result = { response: response, data: data };
break;
}
if (!data['@odata.nextLink']) {
throw new Error('No more pages available.');
}
nextApiUrl = data['@odata.nextLink'];
currentPage++;
}
return result;
}
async fetchRequestOptsForOperation(accessToken: string, queryOptions: QueryOptions): Promise<any> {
const {
sp_operation,
sp_site_id,
sp_time_interval,
sp_list_id,
sp_list_name,
sp_item_id,
sp_list_object,
sp_item_object,
sp_top,
} = queryOptions;
const authHeader = {
Authorization: `Bearer ${accessToken}`,
};
switch (sp_operation) {
case 'get_sites':
return {
method: 'GET',
endpoint: `?search=*${sp_top ? `&$top=${sp_top}` : ''}`,
headers: { ...authHeader },
paginationFeature: true,
};
case 'get_site':
return {
method: 'GET',
endpoint: `/${sp_site_id}`,
headers: { ...authHeader },
};
case 'get_analytics':
return {
method: 'GET',
endpoint: `/${sp_site_id}/analytics/${sp_time_interval}`,
headers: { ...authHeader },
};
case 'get_pages':
return {
method: 'GET',
endpoint: `/${sp_site_id}/pages${sp_top ? `?&$top=${sp_top}` : ''}`,
headers: { ...authHeader, 'Content-Type': 'application/json' },
paginationFeature: true,
};
case 'get_lists':
return {
method: 'GET',
endpoint: `/${sp_site_id}/lists`,
headers: { ...authHeader },
paginationFeature: true,
};
case 'get_metadata':
return {
method: 'GET',
endpoint: `/${sp_site_id}/lists/${sp_list_id || sp_list_name}?expand=columns,items(expand=fields)`,
headers: { ...authHeader },
};
case 'get_items':
return {
method: 'GET',
endpoint: `/${sp_site_id}/lists/${sp_list_id}/items?$expand=fields${sp_top ? `&$top=${sp_top}` : ''}`,
headers: { ...authHeader },
paginationFeature: true,
};
case 'create_list':
return {
method: 'POST',
endpoint: `/${sp_site_id}/lists`,
headers: { ...authHeader, 'Content-Type': 'application/json' },
body: JSON.parse(sp_list_object),
};
case 'add_item':
return {
method: 'POST',
endpoint: `/${sp_site_id}/lists/${sp_list_id}/items`,
headers: { ...authHeader, 'Content-Type': 'application/json' },
body: JSON.parse(sp_item_object),
};
case 'update_item':
return {
method: 'PATCH',
endpoint: `/${sp_site_id}/lists/${sp_list_id}/items/${sp_item_id}/fields`,
headers: { ...authHeader, 'Content-Type': 'application/json' },
body: JSON.parse(sp_item_object),
};
case 'delete_item':
return {
method: 'DELETE',
endpoint: `/${sp_site_id}/lists/${sp_list_id}/items/${sp_item_id}`,
headers: { ...authHeader },
};
default:
return { method: '', endpoint: '', headers: {}, body: {} };
}
}
async refreshToken(sourceOptions) {
if (!sourceOptions['refresh_token']) {
throw new QueryError('Query could not be completed', 'Refresh token empty', {});
}
const tenant = sourceOptions.sp_tenant_id;
const accessTokenUrl = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`;
const clientId = sourceOptions.sp_client_id;
const clientSecret = sourceOptions.sp_client_secret;
const data = new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
grant_type: 'refresh_token',
refresh_token: sourceOptions['refresh_token'],
scope: 'https://graph.microsoft.com/.default',
});
const accessTokenDetails = {};
try {
const response = await fetch(accessTokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data.toString(),
});
const result = await response.json();
if (result['access_token']) {
accessTokenDetails['access_token'] = result['access_token'];
accessTokenDetails['refresh_token'] = result['refresh_token'];
} else {
throw new QueryError(
'access_token not found in the response',
{},
{
responseObject: {
statusCode: response.status,
responseBody: result,
},
responseHeaders: response.headers,
}
);
}
} catch (error) {
console.error(`Error while Sharepoint refresh token call. ${JSON.stringify(error)}`);
throw new QueryError('could not connect to Sharepoint', JSON.stringify(error), {});
}
return accessTokenDetails;
}
}

View file

@ -0,0 +1,56 @@
{
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/manifest.schema.json",
"title": "Sharepoint datasource",
"description": "A schema defining Sharepoint datasource",
"type": "api",
"source": {
"name": "Sharepoint",
"kind": "sharepoint",
"exposedVariables": {
"isLoading": false,
"data": {},
"rawData": {}
},
"options": {
"sp_client_id": {
"type": "string"
},
"sp_client_secret": {
"type": "string",
"encrypted": true
},
"sp_tenant_id": {
"type": "string"
},
"redirect_url": {
"type": "string"
},
"provider": {
"type": "string"
},
"oauth2": {
"type": "boolean"
},
"plugin_id": {
"type": "string"
}
},
"customTesting": true,
"hideSave": true
},
"defaults": {
"redirect_url":{
"value": ""
}
},
"properties": {
"sharepoint": {
"label": "",
"key": "sharepoint",
"type": "react-component-sharepoint",
"description": "A component for sharepoint"
}
},
"required": []
}

View file

@ -0,0 +1,377 @@
{
"$schema": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/operations.schema.json",
"title": "Sharepoint datasource",
"description": "A schema defining Sharepoint datasource",
"type": "api",
"defaults": {},
"properties": {
"operation": {
"label": "Operation",
"key": "sp_operation",
"type": "dropdown-component-flip",
"description": "Select operation",
"list": [
{
"name": "Get all sites",
"value": "get_sites"
},
{
"name": "Get site",
"value": "get_site"
},
{
"name": "Get analytics",
"value": "get_analytics"
},
{
"name": "Get pages of a site",
"value": "get_pages"
},
{
"name": "Get all lists",
"value": "get_lists"
},
{
"name": "Get metadata of a list",
"value": "get_metadata"
},
{
"name": "Create a list",
"value": "create_list"
},
{
"name": "Get items of a list",
"value": "get_items"
},
{
"name": "Update item of a list",
"value": "update_item"
},
{
"name": "Delete item of a list",
"value": "delete_item"
},
{
"name": "Add item to a list",
"value": "add_item"
}
]
},
"get_sites": {
"top": {
"label": "Top",
"key": "sp_top",
"description": "Enter the number of items a response page should contain",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter the number of items a response page should contain",
"height": "36px"
},
"page": {
"label": "Page",
"key": "sp_page",
"description": "Enter the page number",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter the page number",
"height": "36px"
}
},
"get_site": {
"site_id": {
"label": "Site id",
"key": "sp_site_id",
"description": "Enter site id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter site id",
"height": "36px"
}
},
"get_analytics": {
"site_id": {
"label": "Site id",
"key": "sp_site_id",
"description": "Enter site id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter site id",
"height": "36px"
},
"time_interval": {
"label": "Time interval",
"key": "sp_time_interval",
"description": "Select time interval",
"type": "dropdown",
"list": [
{
"name": "Last 7 days",
"value": "lastSevenDays"
},
{
"name": "All time",
"value": "allTime"
}
]
}
},
"get_pages": {
"site_id": {
"label": "Site id",
"key": "sp_site_id",
"description": "Enter site id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter site id",
"height": "36px"
},
"top": {
"label": "Top",
"key": "sp_top",
"description": "Enter the number of items a response page should contain",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter the number of items a response page should contain",
"height": "36px"
},
"page": {
"label": "Page",
"key": "sp_page",
"description": "Enter the page number",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter the page number",
"height": "36px"
}
},
"get_lists": {
"site_id": {
"label": "Site id",
"key": "sp_site_id",
"description": "Enter site id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter site id",
"height": "36px"
},
"page": {
"label": "Page",
"key": "sp_page",
"description": "Enter the page number",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter the page number",
"height": "36px"
}
},
"get_metadata": {
"site_id": {
"label": "Site id",
"key": "sp_site_id",
"description": "Enter site id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter site id",
"height": "36px"
},
"list_name": {
"label": "List Name",
"key": "sp_list_name",
"description": "Enter list name",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter list name",
"height": "36px"
},
"list_id": {
"label": "List id",
"key": "sp_list_id",
"description": "Enter list id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter list id",
"height": "36px"
}
},
"create_list": {
"site_id": {
"label": "Site id",
"key": "sp_site_id",
"description": "Enter site id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter site id",
"height": "36px"
},
"list_object": {
"label": "Body",
"key": "sp_list_object",
"type": "codehinter",
"mode": "javascript",
"placeholder": "{\n \"displayName\": \"Books\",\n \"columns\": [\n {\n \"name\": \"Author\",\n \"text\": { }\n },\n {\n \"name\": \"PageCount\",\n \"number\": { }\n }\n ],\n \"list\": {\n \"template\": \"genericList\"\n }\n}",
"description": "Enter list object",
"height": "200px"
}
},
"get_items": {
"site_id": {
"label": "Site id",
"key": "sp_site_id",
"description": "Enter site id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter site id",
"height": "36px"
},
"list_id": {
"label": "List id",
"key": "sp_list_id",
"description": "Enter list id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter list id",
"height": "36px"
},
"top": {
"label": "Top",
"key": "sp_top",
"description": "Enter the number of items a response page should contain",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter the number of items a response page should contain",
"height": "36px"
},
"page": {
"label": "Page",
"key": "sp_page",
"description": "Enter the page number",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter the page number",
"height": "36px"
}
},
"update_item": {
"site_id": {
"label": "Site id",
"key": "sp_site_id",
"description": "Enter site id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter site id",
"height": "36px"
},
"list_id": {
"label": "List id",
"key": "sp_list_id",
"description": "Enter list id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter list id",
"height": "36px"
},
"item_id": {
"label": "Item id",
"key": "sp_item_id",
"description": "Enter item id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter item id",
"height": "36px"
},
"item_object": {
"label": "Body",
"key": "sp_item_object",
"type": "codehinter",
"mode": "javascript",
"placeholder": "{\n \"Color\": \"Fuchsia\",\n \"Quantity\": 934\n}",
"description": "Enter item object",
"height": "200px"
}
},
"delete_item": {
"site_id": {
"label": "Site id",
"key": "sp_site_id",
"description": "Enter site id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter site id",
"height": "36px"
},
"list_id": {
"label": "List id",
"key": "sp_list_id",
"description": "Enter list id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter list id",
"height": "36px"
},
"item_id": {
"label": "Item id",
"key": "sp_item_id",
"description": "Enter item id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter item id",
"height": "36px"
}
},
"add_item": {
"site_id": {
"label": "Site id",
"key": "sp_site_id",
"description": "Enter site id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter site id",
"height": "36px"
},
"list_id": {
"label": "List id",
"key": "sp_list_id",
"description": "Enter list id",
"type": "codehinter",
"lineNumbers": false,
"className": "codehinter-plugins",
"placeholder": "Enter list id",
"height": "36px"
},
"item_object": {
"label": "Body",
"key": "sp_item_object",
"type": "codehinter",
"mode": "javascript",
"placeholder": "{\n \"fields\": {\n \"Title\": \"Widget\",\n \"Color\": \"Purple\",\n \"Weight\": 32\n }\n}",
"description": "Enter item object",
"height": "200px"
}
}
}
}

View file

@ -0,0 +1,19 @@
export type SourceOptions = {
sp_client_id: string;
sp_client_secret: string;
sp_tenant_id: string;
access_token: string;
};
export type QueryOptions = {
operation: string;
sp_operation: string;
sp_site_id: string;
sp_time_interval: string;
sp_list_name: string;
sp_list_id: string;
sp_list_object: string;
sp_item_id: string;
sp_item_object: string;
sp_top: string;
sp_page: string;
};

View file

@ -0,0 +1,26 @@
{
"name": "@tooljet-marketplace/sharepoint",
"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",
"watch": "ncc build lib/index.ts -o dist --watch"
},
"homepage": "https://github.com/tooljet/tooljet#readme",
"dependencies": {
"@tooljet-marketplace/common": "^1.0.0"
},
"devDependencies": {
"typescript": "^4.7.4",
"@vercel/ncc": "^0.34.0"
}
}

View file

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

View file

@ -28,6 +28,7 @@
"@tooljet-plugins/graphql": "file:packages/graphql",
"@tooljet-plugins/grpc": "file:packages/grpc",
"@tooljet-plugins/influxdb": "file:packages/influxdb",
"@tooljet-plugins/jira": "file:packages/jira",
"@tooljet-plugins/mailgun": "file:packages/mailgun",
"@tooljet-plugins/mariadb": "file:packages/mariadb",
"@tooljet-plugins/minio": "file:packages/minio",
@ -5885,6 +5886,10 @@
"resolved": "packages/influxdb",
"link": true
},
"node_modules/@tooljet-plugins/jira": {
"resolved": "packages/jira",
"link": true
},
"node_modules/@tooljet-plugins/mailgun": {
"resolved": "packages/mailgun",
"link": true
@ -12173,6 +12178,31 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/jira.js": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jira.js/-/jira.js-4.0.1.tgz",
"integrity": "sha512-2zf8LozW9rgx5wgTdGSJMhUXDK1g8a/ngm1xDWnREX/h8kuBhNkMro4XELA2XRVvaNTbRMIK3PBgOvWFDddhIw==",
"dependencies": {
"axios": "^1.7.2",
"form-data": "^4.0.0",
"tslib": "^2.6.3"
}
},
"node_modules/jira.js/node_modules/axios": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/jira.js/node_modules/tslib": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
},
"node_modules/jmespath": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz",
@ -19457,6 +19487,15 @@
"react": "^17.0.2"
}
},
"packages/jira": {
"name": "@tooljet-plugins/jira",
"version": "1.0.0",
"dependencies": {
"@tooljet-plugins/common": "file:../common",
"jira.js": "^4.0.1",
"react": "^17.0.2"
}
},
"packages/mailgun": {
"name": "@tooljet-plugins/mailgun",
"version": "1.0.0",
@ -19561,6 +19600,7 @@
"version": "1.0.0",
"dependencies": {
"@tooljet-plugins/common": "file:../common",
"knex": "^3.1.0",
"pg": "^8.9.0",
"react": "^17.0.2",
"rimraf": "^3.0.2"

View file

@ -39,6 +39,7 @@
"@tooljet-plugins/graphql": "file:packages/graphql",
"@tooljet-plugins/grpc": "file:packages/grpc",
"@tooljet-plugins/influxdb": "file:packages/influxdb",
"@tooljet-plugins/jira": "file:packages/jira",
"@tooljet-plugins/mailgun": "file:packages/mailgun",
"@tooljet-plugins/mariadb": "file:packages/mariadb",
"@tooljet-plugins/minio": "file:packages/minio",

View file

@ -1,5 +1,6 @@
import { QueryError, QueryResult, QueryService } from '@tooljet-plugins/common';
import { SourceOptions, QueryOptions } from './types';
import { sanitizeSortPairs } from '@tooljet-plugins/common';
import got, { Headers } from 'got';
export default class AirtableQueryService implements QueryService {
@ -22,16 +23,61 @@ export default class AirtableQueryService implements QueryService {
try {
switch (operation) {
case 'list_records': {
const pageSize = queryOptions.page_size || '';
const offset = queryOptions.offset || '';
const pageSize = queryOptions.page_size || null;
const offset = queryOptions.offset || null;
const fields = queryOptions.fields || null;
const filterFormula = queryOptions.filter_by_formula || null;
const timezone = queryOptions.timezone || null;
const user_locale = queryOptions.user_locale || null;
const cell_format = queryOptions.cell_format || null;
const view = queryOptions.view || null;
const sort = queryOptions.sort || null;
response = await got(
`https://api.airtable.com/v0/${baseId}/${tableName}/?pageSize=${pageSize}&offset=${offset}`,
{
method: 'get',
headers: this.authHeader(apiToken),
const requestBody: any = {};
if (fields) {
try {
const parsedFields = JSON.parse(fields);
requestBody.fields = parsedFields;
} catch (error) {
throw new Error('Invalid JSON format for fields');
}
);
}
if (filterFormula) {
requestBody.filterByFormula = filterFormula;
}
if (pageSize) {
requestBody.pageSize = Number(pageSize);
}
if (offset) {
requestBody.offset = offset;
}
if (timezone) {
requestBody.timeZone = timezone.trim();
}
if (user_locale) {
requestBody.userLocale = user_locale.trim();
}
if (cell_format) {
requestBody.cellFormat = cell_format.trim();
}
if (view) {
requestBody.view = view.trim();
}
if (sort) {
const sanitizedSort = sanitizeSortPairs(sort);
const formattedSort = sanitizedSort.map(([field, direction]) => ({
field,
direction,
}));
requestBody.sort = formattedSort;
}
response = await got(`https://api.airtable.com/v0/${baseId}/${tableName}/listRecords`, {
method: 'post',
headers: this.authHeader(apiToken),
json: requestBody,
});
result = JSON.parse(response.body);
break;

View file

@ -3,7 +3,9 @@
"title": "Airtable datasource",
"description": "A schema defining Airtable datasource",
"type": "api",
"defaults": {},
"defaults": {
"cell_format": "json"
},
"properties": {
"operation": {
"label": "Operation",
@ -77,6 +79,79 @@
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "recAOzdIHaRpvRaGE"
},
"fields": {
"label": "Fields",
"key": "fields",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter fields name",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "[\"column1\",\"column2\",\"column3\"]"
},
"filter_by_formula": {
"label": "Filter by formula",
"key": "filter_by_formula",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter formula",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "column1 * column2 > 100"
},
"timezone": {
"label": "Timezone",
"key": "timezone",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter timezone",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "America/Chicago"
},
"user_locale": {
"label": "User locale",
"key": "user_locale",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter user locale",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "en-gb"
},
"cell_format": {
"label": "Cell format",
"key": "cell_format",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter cell format",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "string"
},
"view": {
"label": "View",
"key": "view",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter view",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "GridView"
},
"sort": {
"label": "Sort",
"key": "sort",
"type": "react-component-sort",
"description": "Enter sort values",
"buttonText": "Add"
}
},
"retrieve_record": {
@ -143,7 +218,6 @@
"type": "codehinter",
"description": "Enter list of records",
"height": "150px",
"editorType": "extendedSingleLine",
"placeholder": "[{ \"fields\": {} }]"
}
},
@ -186,8 +260,7 @@
"key": "body",
"type": "codehinter",
"description": "Enter body",
"height": "150px",
"editorType": "extendedSingleLine"
"height": "150px"
}
},
"delete_record": {
@ -226,4 +299,4 @@
}
}
}
}
}

View file

@ -11,4 +11,11 @@ export type QueryOptions = {
record_id: string;
body: string;
offset: string;
fields: string;
filter_by_formula: string;
timezone: string;
user_locale: string;
cell_format: string;
view: string;
sort: [string, string][];
};

View file

@ -1,7 +1,7 @@
{
"name": "@tooljet-plugins/clickhouse",
"version": "1.0.0",
"lockfileVersion": 3,
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
@ -9,7 +9,7 @@
"version": "1.0.0",
"dependencies": {
"@tooljet-plugins/common": "file:../common",
"clickhouse": "^2.6.0",
"clickhouse": "^2.5.1",
"react": "^17.0.2"
}
},
@ -17,11 +17,7 @@
"version": "1.0.0",
"dependencies": {
"react": "^17.0.2",
"rimraf": "^3.0.2",
"tough-cookie": "^4.1.3"
},
"devDependencies": {
"@types/tough-cookie": "^4.0.2"
"rimraf": "^3.0.2"
}
},
"node_modules/@tooljet-plugins/common": {
@ -73,9 +69,9 @@
}
},
"node_modules/aws4": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz",
"integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g=="
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
},
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
@ -91,9 +87,9 @@
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="
},
"node_modules/clickhouse": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/clickhouse/-/clickhouse-2.6.0.tgz",
"integrity": "sha512-HC5OV99GJOup4qZsTuWWPpXlj+847Z0OeygDU2x22rNYost0V/vWapzFWYZdV/5iRbGMrhFQPOyQEzmGvoaWRQ==",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/clickhouse/-/clickhouse-2.5.1.tgz",
"integrity": "sha512-6f37nhJ0t9PaHS1OhEKemTGeqJrb8256vGS793ZoaH6SCQFdK/oIa/Bh2ybvICHUaZp0/wCLzfdNg8AvufCrkw==",
"dependencies": {
"JSONStream": "1.3.4",
"lodash": "4.17.21",
@ -459,9 +455,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sshpk": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
"integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
"integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==",
"dependencies": {
"asn1": "~0.2.3",
"assert-plus": "^1.0.0",
@ -534,9 +530,9 @@
}
},
"node_modules/uri-js/node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"engines": {
"node": ">=6"
}
@ -563,5 +559,433 @@
"extsprintf": "^1.2.0"
}
}
},
"dependencies": {
"@tooljet-plugins/common": {
"version": "file:../common",
"requires": {
"react": "^17.0.2",
"rimraf": "^3.0.2"
}
},
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"requires": {
"safer-buffer": "~2.1.0"
}
},
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA=="
},
"aws4": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
},
"bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
"requires": {
"tweetnacl": "^0.14.3"
}
},
"caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="
},
"clickhouse": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/clickhouse/-/clickhouse-2.5.1.tgz",
"integrity": "sha512-6f37nhJ0t9PaHS1OhEKemTGeqJrb8256vGS793ZoaH6SCQFdK/oIa/Bh2ybvICHUaZp0/wCLzfdNg8AvufCrkw==",
"requires": {
"JSONStream": "1.3.4",
"lodash": "4.17.21",
"querystring": "0.2.0",
"request": "2.88.0",
"stream2asynciter": "1.0.3",
"through": "2.3.8",
"tsv": "0.2.0",
"uuid": "3.4.0"
}
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
"requires": {
"assert-plus": "^1.0.0"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
"integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
"requires": {
"jsbn": "~0.1.0",
"safer-buffer": "^2.1.0"
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g=="
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
},
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="
},
"form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
},
"getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
"requires": {
"assert-plus": "^1.0.0"
}
},
"har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
"integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q=="
},
"har-validator": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
"requires": {
"ajv": "^6.12.3",
"har-schema": "^2.0.0"
}
},
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==",
"requires": {
"assert-plus": "^1.0.0",
"jsprim": "^1.2.2",
"sshpk": "^1.7.0"
}
},
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
},
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="
},
"json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
},
"jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
"integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="
},
"JSONStream": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.4.tgz",
"integrity": "sha512-Y7vfi3I5oMOYIr+WxV8NZxDSwcbNgzdKYsTNInmycOq9bUYwGg9ryu57Wg5NLmCjqdFPNUmpMBo3kSJN9tCbXg==",
"requires": {
"jsonparse": "^1.2.0",
"through": ">=2.2.7 <3"
}
},
"jsprim": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
"integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
"requires": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
"json-schema": "0.4.0",
"verror": "1.10.0"
}
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"requires": {
"mime-db": "1.52.0"
}
},
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
},
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
},
"psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
"integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag=="
},
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
},
"qs": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA=="
},
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g=="
},
"react": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"request": {
"version": "2.88.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.0",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.4.3",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sshpk": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
"integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==",
"requires": {
"asn1": "~0.2.3",
"assert-plus": "^1.0.0",
"bcrypt-pbkdf": "^1.0.0",
"dashdash": "^1.12.0",
"ecc-jsbn": "~0.1.1",
"getpass": "^0.1.1",
"jsbn": "~0.1.0",
"safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0"
}
},
"stream2asynciter": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/stream2asynciter/-/stream2asynciter-1.0.3.tgz",
"integrity": "sha512-9/dEZW+LQjuW6ub5hmWi4n9Pn8W8qA8k7NAE1isecesA164e73xTdy1CJ3S9o9YS+O21HuiK7T+4uS7FgKDy4w=="
},
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
},
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
"requires": {
"psl": "^1.1.24",
"punycode": "^1.4.1"
}
},
"tsv": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/tsv/-/tsv-0.2.0.tgz",
"integrity": "sha512-GG6xbOP85giXXom0dS6z9uyDsxktznjpa1AuDlPrIXDqDnbhjr9Vk6Us8iz6U1nENL4CPS2jZDvIjEdaZsmc4Q=="
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"requires": {
"safe-buffer": "^5.0.1"
}
},
"tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
},
"uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"requires": {
"punycode": "^2.1.0"
},
"dependencies": {
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
}
}
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
},
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
"requires": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
"extsprintf": "^1.2.0"
}
}
}
}

View file

@ -9,18 +9,20 @@ import {
getCachedConnection,
parseJson,
cleanSensitiveData,
redactHeaders,
getCurrentToken,
sanitizeHeaders,
sanitizeCookies,
cookiesToString,
sanitizeSearchParams,
fetchHttpsCertsForCustomCA,
sanitizeSortPairs,
} from './utils.helper';
import { ConnectionTestResult } from './connection_test_result.type';
import {
getRefreshedToken,
getAuthUrl,
sanitizeCustomParams,
sanitizeParams,
checkIfContentTypeIsURLenc,
checkIfContentTypeIsMultipartFormData,
validateAndSetRequestOptionsBasedOnAuthType,
@ -42,7 +44,7 @@ export {
getCurrentToken,
getRefreshedToken,
getAuthUrl,
sanitizeCustomParams,
sanitizeParams,
sanitizeHeaders,
sanitizeCookies,
sanitizeSearchParams,
@ -50,5 +52,7 @@ export {
checkIfContentTypeIsMultipartFormData,
validateAndSetRequestOptionsBasedOnAuthType,
fetchHttpsCertsForCustomCA,
redactHeaders,
cookiesToString,
sanitizeSortPairs,
};

View file

@ -2,24 +2,23 @@ import got, { HTTPError, OptionsOfTextResponseBody } from 'got';
import urrl from 'url';
import { QueryError, OAuthUnauthorizedClientError } from './query.error';
import { getCurrentToken } from './utils.helper';
import { QueryResult } from './query_result.type';
import { QueryResult, RequestBody } from './query_result.type';
import { App } from './app.type';
import { User } from './user.type';
import { CookieJar } from 'tough-cookie';
import { isEmpty } from 'lodash';
export function checkIfContentTypeIsURLenc(headers: [] = []) {
const objectHeaders = Object.fromEntries(headers);
const contentType = objectHeaders['content-type'] ?? objectHeaders['Content-Type'];
return contentType === 'application/x-www-form-urlencoded';
export function checkIfContentTypeIsURLenc(headers: [string, string][] = []): boolean {
const contentType = headers.find(([key, _]) => key.toLowerCase() === 'content-type')?.[1];
return contentType?.toLowerCase() === 'application/x-www-form-urlencoded';
}
export function checkIfContentTypeIsMultipartFormData(headers: [] = []) {
const objectHeaders = Object.fromEntries(headers);
const contentType = objectHeaders['content-type'] ?? objectHeaders['Content-Type'];
return contentType === 'multipart/form-data';
export function checkIfContentTypeIsMultipartFormData(headers: [string, string][] = []): boolean {
const contentType = headers.find(([key, _]) => key.toLowerCase() === 'content-type')?.[1];
return contentType?.toLowerCase().startsWith('multipart/form-data') ?? false;
}
export function sanitizeCustomParams(customArray: any) {
export function sanitizeParams(customArray: any) {
const params = Object.fromEntries(customArray ?? []);
Object.keys(params).forEach((key) => (params[key] === '' ? delete params[key] : {}));
return params;
@ -30,7 +29,7 @@ export function validateAndSetRequestOptionsBasedOnAuthType(
context: { user?: User; app?: App },
requestOptions: OptionsOfTextResponseBody,
additionalOptions?: any
): QueryResult {
): QueryResult | Promise<QueryResult> {
switch (sourceOptions['auth_type']) {
case 'oauth2':
case 'oauth':
@ -46,13 +45,13 @@ export function validateAndSetRequestOptionsBasedOnAuthType(
}
}
function handleOAuthAuthentication(
async function handleOAuthAuthentication(
sourceOptions: any,
context: { user?: User; app?: App },
requestOptions: any
): QueryResult {
): Promise<QueryResult> {
const headers = { ...requestOptions.headers };
const oAuthValidatedResult = validateAndMaybeSetOAuthHeaders(sourceOptions, context, headers);
const oAuthValidatedResult = await validateAndMaybeSetOAuthHeaders(sourceOptions, context, headers);
if (oAuthValidatedResult.status !== 'ok') {
return oAuthValidatedResult;
}
@ -96,12 +95,13 @@ function handleBasicAuthentication(sourceOptions: any, requestOptions: any): Que
};
}
function validateAndMaybeSetOAuthHeaders(sourceOptions, context, headers): QueryResult {
async function validateAndMaybeSetOAuthHeaders(sourceOptions, context, headers): Promise<QueryResult> {
const authType = sourceOptions['auth_type'];
const requiresOauth = authType === 'oauth2' || authType === 'oauth';
if (requiresOauth) {
const isMultiAuthEnabled = sourceOptions['multiple_auth_enabled'];
const grantType = sourceOptions['grant_type'];
const tokenData = sourceOptions['tokenData'];
const isAppPublic = context?.app.isPublic;
const userData = context?.user;
@ -112,10 +112,11 @@ function validateAndMaybeSetOAuthHeaders(sourceOptions, context, headers): Query
}
if (!currentToken) {
return {
status: 'needs_oauth',
data: { auth_url: getAuthUrl(sourceOptions) },
};
if (grantType === 'client_credentials') {
return handleClientCredentialsGrant(sourceOptions, headers);
} else {
return handleAuthorizationCodeGrant(sourceOptions);
}
} else {
const accessToken = currentToken['access_token'];
if (sourceOptions['add_token_to'] === 'header') {
@ -128,8 +129,69 @@ function validateAndMaybeSetOAuthHeaders(sourceOptions, context, headers): Query
return { status: 'ok', data: headers };
}
async function handleClientCredentialsGrant(sourceOptions: any, headers: any): Promise<QueryResult> {
try {
const data = await getTokenForClientCredentialsGrant(sourceOptions);
const accessToken = data['access_token'];
if (sourceOptions['add_token_to'] === 'header') {
headers['Authorization'] = `Bearer ${accessToken}`;
}
return { status: 'ok', data: headers };
} catch (error) {
throw new QueryError('Failed to fetch access token', {}, {});
}
}
function handleAuthorizationCodeGrant(sourceOptions: any): QueryResult {
return {
status: 'needs_oauth',
data: { auth_url: getAuthUrl(sourceOptions) },
};
}
async function getTokenForClientCredentialsGrant(sourceOptions: any) {
if (
isEmpty(sourceOptions.access_token_url) ||
isEmpty(sourceOptions.client_id) ||
isEmpty(sourceOptions.client_secret)
) {
throw new Error('Missing required fields in sourceOptions');
}
const headersObject = sanitizeParams(sourceOptions.access_token_custom_headers);
try {
const requestBody: RequestBody = {
grant_type: sourceOptions.grant_type,
client_id: sourceOptions.client_id,
client_secret: sourceOptions.client_secret,
};
if (sourceOptions.audience) {
requestBody.audience = sourceOptions.audience;
}
if (sourceOptions.scopes) {
requestBody.scope = sourceOptions.scopes;
}
const response = await got.post(sourceOptions.access_token_url, {
headers: {
'Content-Type': 'application/json',
...(Object.keys(headersObject).length > 0 && headersObject),
},
json: requestBody,
responseType: 'json',
});
return response.body;
} catch (error) {
throw new Error(`Failed to fetch token: ${error.message}`);
}
}
export function getAuthUrl(sourceOptions: any): string {
const customQueryParams = sanitizeCustomParams(sourceOptions['custom_query_params']);
const customQueryParams = sanitizeParams(sourceOptions['custom_query_params']);
const host = process.env.TOOLJET_HOST;
const subpath = process.env.SUB_PATH;
const fullUrl = `${host}${subpath ? subpath : '/'}`;
@ -186,7 +248,7 @@ export const getRefreshedToken = async (sourceOptions: any, error: any, userId:
const clientSecret = sourceOptions['client_secret'];
const grantType = 'refresh_token';
const isUrlEncoded = checkIfContentTypeIsURLenc(sourceOptions['access_token_custom_headers']);
const customAccessTokenHeaders = sanitizeCustomParams(sourceOptions['access_token_custom_headers']);
const customAccessTokenHeaders = sanitizeParams(sourceOptions['access_token_custom_headers']);
const data = {
client_id: clientId,

View file

@ -4,4 +4,13 @@ export type QueryResult = {
status: 'ok' | 'failed' | 'needs_oauth';
errorMessage?: string;
data: Array<object> | object | OptionsOfTextResponseBody;
metadata?: Array<object> | object;
};
export interface RequestBody {
grant_type: string;
client_id: string;
client_secret: string;
audience?: string;
scope?: string;
}

View file

@ -1,5 +1,4 @@
import { QueryError } from './query.error';
import { Headers } from 'got';
import * as tls from 'tls';
import { readFileSync } from 'fs';
@ -78,18 +77,23 @@ export const getCurrentToken = (isMultiAuthEnabled: boolean, tokenData: any, use
}
};
export const sanitizeHeaders = (sourceOptions: any, queryOptions: any, hasDataSource = true): Headers => {
const _headers = (queryOptions.headers || []).filter((o) => {
return o.some((e) => !isEmpty(e));
});
export const sanitizeHeaders = (
sourceOptions: any,
queryOptions: any,
hasDataSource = true
): { [k: string]: string } => {
const cleanHeaders = (headers) =>
headers.filter(([_, v]) => !isEmpty(v)).map(([k, v]) => [k.trim().toLowerCase(), v]);
if (!hasDataSource) return Object.fromEntries(_headers);
const _queryHeaders = cleanHeaders(queryOptions.headers || []);
const queryHeaders = Object.fromEntries(_queryHeaders);
const headerData = _headers.concat(sourceOptions.headers || []);
const headers = Object.fromEntries(headerData);
Object.keys(headers).forEach((key) => (headers[key] === '' ? delete headers[key] : {}));
if (!hasDataSource) return queryHeaders;
return headers;
const _sourceHeaders = cleanHeaders(sourceOptions.headers || []);
const sourceHeaders = Object.fromEntries(_sourceHeaders);
return { ...queryHeaders, ...sourceHeaders };
};
export const sanitizeCookies = (sourceOptions: any, queryOptions: any, hasDataSource = true): object => {
@ -123,6 +127,13 @@ export const sanitizeSearchParams = (sourceOptions: any, queryOptions: any, hasD
return urlParams;
};
export const sanitizeSortPairs = (options): Array<[string, string]> => {
const sanitizedOptions = (options || []).filter((o) => {
return o.every((e) => !isEmpty(e));
});
return sanitizedOptions;
};
export const fetchHttpsCertsForCustomCA = () => {
if (!process.env.NODE_EXTRA_CA_CERTS) return {};
@ -132,3 +143,95 @@ export const fetchHttpsCertsForCustomCA = () => {
},
};
};
// Headers to be redacted
// For more information on OWASP Secure Headers Project, visit:
// https://owasp.org/www-project-secure-headers/#prevent-information-disclosure-via-http-headers
const headersToRedact = [
'$wsep',
'Host-Header',
'K-Proxy-Request',
'Liferay-Portal',
'OracleCommerceCloud-Version',
'Pega-Host',
'Powered-By',
'Product',
'Server',
'SourceMap',
'X-AspNet-Version',
'X-AspNetMvc-Version',
'X-Atmosphere-error',
'X-Atmosphere-first-request',
'X-Atmosphere-tracking-id',
'X-B3-ParentSpanId',
'X-B3-Sampled',
'X-B3-SpanId',
'X-B3-TraceId',
'X-BEServer',
'X-CF-Powered-By',
'X-CMS',
'X-CalculatedBETarget',
'X-Cocoon-Version',
'X-Content-Encoded-By',
'X-DiagInfo',
'X-Envoy-Attempt-Count',
'X-Envoy-External-Address',
'X-Envoy-Internal',
'X-Envoy-Original-Dst-Host',
'X-Envoy-Upstream-Service-Time',
'X-FEServer',
'X-Framework',
'X-Generated-By',
'X-Generator',
'X-Jitsi-Release',
'X-Kubernetes-PF-FlowSchema-UI',
'X-Kubernetes-PF-PriorityLevel-UID',
'X-LiteSpeed-Cache',
'X-LiteSpeed-Purge',
'X-LiteSpeed-Tag',
'X-LiteSpeed-Vary',
'X-Litespeed-Cache-Control',
'X-Mod-Pagespeed',
'X-Nextjs-Cache',
'X-Nextjs-Matched-Path',
'X-Nextjs-Page',
'X-Nextjs-Redirect',
'X-OWA-Version',
'X-Old-Content-Length',
'X-OneAgent-JS-Injection',
'X-Page-Speed',
'X-Php-Version',
'X-Powered-By',
'X-Powered-By-Plesk',
'X-Powered-CMS',
'X-Redirect-By',
'X-Server-Powered-By',
'X-SourceFiles',
'X-SourceMap',
'X-Turbo-Charged-By',
'X-Umbraco-Version',
'X-Varnish-Backend',
'X-Varnish-Server',
'X-dtAgentId',
'X-dtHealthCheck',
'X-dtInjectedServlet',
'X-ruxit-JS-Agent',
'server',
// Additional headers explicitly defined for redaction
'authorization', // Contains sensitive authentication information
'x-api-key', // Often used for API authentication
'proxy-authorization', // Similar to authorization, but for proxy authentication
'www-authenticate', // Contains authentication scheme information
'authentication-info', // Provides additional authentication details
];
export const redactHeaders = (headers) => {
const redactedHeaders = { ...headers };
headersToRedact.forEach((key) => {
if (Object.prototype.hasOwnProperty.call(redactedHeaders, key)) {
redactedHeaders[key] = '[REDACTED]';
}
});
return redactedHeaders;
};

View file

@ -1,6 +1,19 @@
import { ConnectionTestResult, QueryService, QueryResult, QueryError } from '@tooljet-plugins/common';
import { getDocument, updateDocument } from './operations';
import { indexDocument, search } from './operations';
import {
getDocument,
updateDocument,
indexDocument,
search,
deleteDocument,
bulkOperation,
countDocuments,
documentExists,
multiGet,
scrollSearch,
clearScroll,
getCatIndices,
getClusterHealth,
} from './operations';
import { Client, ClientOptions } from '@opensearch-project/opensearch';
import { SourceOptions, QueryOptions } from './types';
@ -13,7 +26,7 @@ export default class ElasticsearchService implements QueryService {
try {
switch (operation) {
case 'search':
result = await search(client, queryOptions.index, queryOptions.query);
result = await search(client, queryOptions.index, queryOptions.query, queryOptions.scroll);
break;
case 'index_document':
result = await indexDocument(client, queryOptions.index, queryOptions.body);
@ -24,6 +37,35 @@ export default class ElasticsearchService implements QueryService {
case 'update':
result = await updateDocument(client, queryOptions.index, queryOptions.id, queryOptions.body);
break;
case 'delete':
result = await deleteDocument(client, queryOptions.index, queryOptions.id);
break;
case 'bulk':
result = await bulkOperation(client, queryOptions.operations);
break;
case 'count':
result = await countDocuments(client, queryOptions.index, queryOptions.query);
break;
case 'exists':
result = await documentExists(client, queryOptions.index, queryOptions.id);
break;
case 'mget':
result = await multiGet(client, queryOptions.operations);
break;
case 'scroll':
result = await scrollSearch(client, queryOptions.scroll_id, queryOptions.scroll);
break;
case 'clear_scroll':
result = await clearScroll(client, queryOptions.scroll_id);
break;
case 'cat_indices':
result = await getCatIndices(client);
break;
case 'cluster_health':
result = await getClusterHealth(client);
break;
default:
throw new Error(`Unsupported operation: ${operation}`);
}
} catch (err) {
console.log(err);

View file

@ -55,7 +55,7 @@
"ssl_enabled": {
"value": true
},
"ssl_certificate":{
"ssl_certificate": {
"value": "none"
}
},
@ -79,7 +79,7 @@
"name": "None"
}
],
"commonFields":{
"commonFields": {
"host": {
"label": "Host",
"key": "host",
@ -112,7 +112,7 @@
}
}
},
"ca_certificate":{
"ca_certificate": {
"ca_cert": {
"label": "CA Cert",
"key": "ca_cert",
@ -121,7 +121,7 @@
"description": "Enter ca certificate"
}
},
"client_certificate":{
"client_certificate": {
"client_key": {
"label": "Client Key",
"key": "client_key",

View file

@ -29,6 +29,42 @@
{
"value": "update",
"name": "Update a document"
},
{
"value": "delete",
"name": "Delete a document"
},
{
"value": "bulk",
"name": "Bulk operation"
},
{
"value": "count",
"name": "Count documents"
},
{
"value": "exists",
"name": "Check document exists"
},
{
"value": "mget",
"name": "Multi-get documents"
},
{
"value": "scroll",
"name": "Scroll search"
},
{
"value": "clear_scroll",
"name": "Clear scroll"
},
{
"value": "cat_indices",
"name": "Get indices info"
},
{
"value": "cluster_health",
"name": "Get cluster health"
}
]
},
@ -48,10 +84,21 @@
"label": "Query",
"key": "query",
"type": "codehinter",
"placeholder": "{ \"name\": \"\" }",
"placeholder": "{ \"query\": { \"match\": { \"field\": \"value\" } } }",
"description": "Enter query",
"height": "150px",
"editorType": "extendedSingleLine"
},
"scroll": {
"label": "Scroll",
"key": "scroll",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter scroll value",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "[number][time unit]' (e.g., '5m' for 5 minute)"
}
},
"index_document": {
@ -71,7 +118,7 @@
"key": "body",
"type": "codehinter",
"mode": "javascript",
"placeholder": "{ \"name\": \"The Hitchhikers Guide to the Galaxy\" }",
"placeholder": "{ \"field1\": \"value1\", \"field2\": \"value2\" }",
"description": "Enter body",
"height": "150px",
"editorType": "extendedSingleLine"
@ -125,15 +172,148 @@
"placeholder": "Enter id"
},
"body": {
"label": "body",
"label": "Body",
"key": "body",
"type": "codehinter",
"mode": "javascript",
"placeholder": "{ doc: { page_count: 225 } }",
"placeholder": "{ \"doc\": { \"field\": \"new_value\" } }",
"description": "Enter body",
"height": "150px",
"editorType": "extendedSingleLine"
}
}
},
"delete": {
"index": {
"label": "Index",
"key": "index",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter index",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter index"
},
"id": {
"label": "Id",
"key": "id",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter document id",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter id"
}
},
"bulk": {
"operations": {
"label": "Operations",
"key": "operations",
"type": "codehinter",
"mode": "javascript",
"placeholder": "[{ \"index\": { \"_index\": \"index_name\", \"_id\": \"1\" } }, { \"field\": \"value\" }]",
"description": "Enter bulk operations",
"height": "150px",
"editorType": "extendedSingleLine"
}
},
"count": {
"index": {
"label": "Index",
"key": "index",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter index",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter index"
},
"query": {
"label": "Query",
"key": "query",
"type": "codehinter",
"placeholder": "{ \"query\": { \"match_all\": {} } }",
"description": "Enter query (optional)",
"height": "150px",
"editorType": "extendedSingleLine"
}
},
"exists": {
"index": {
"label": "Index",
"key": "index",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter index",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter index"
},
"id": {
"label": "Id",
"key": "id",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter document id",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter id"
}
},
"mget": {
"operations": {
"label": "Operations",
"key": "operations",
"type": "codehinter",
"mode": "javascript",
"placeholder": "{ \"docs\": [{ \"_index\": \"index_name\", \"_id\": \"1\" }, { \"_index\": \"index_name\", \"_id\": \"2\" }] }",
"description": "Enter multi-get operations",
"height": "150px",
"editorType": "extendedSingleLine"
}
},
"scroll": {
"scroll_id": {
"label": "Scroll ID",
"key": "scroll_id",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter scroll ID",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter scroll ID"
},
"scroll": {
"label": "Scroll",
"key": "scroll",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter scroll value in the format '[number][time unit]'",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "[number][time unit]' (e.g., '5m' for 5 minute)"
}
},
"clear_scroll": {
"scroll_id": {
"label": "Scroll ID",
"key": "scroll_id",
"type": "codehinter",
"lineNumbers": false,
"description": "Enter scroll ID",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter scroll ID"
}
},
"cat_indices": {},
"cluster_health": {}
}
}

View file

@ -1,8 +1,14 @@
export async function search(client, index: string, query: string): Promise<object> {
const result = await client.search({
export async function search(client, index: string, query: string, scroll?: string): Promise<object> {
const searchParams: any = {
index,
body: JSON.parse(query),
});
};
if (scroll) {
searchParams.scroll = scroll;
}
const result = await client.search(searchParams);
return result;
}
@ -28,6 +34,60 @@ export async function updateDocument(client, index: string, id: string, body: st
id,
body: JSON.parse(body),
});
return result;
}
export async function deleteDocument(client, index: string, id: string): Promise<object> {
const result = await client.delete({ index, id });
return result;
}
export async function bulkOperation(client, operations: string): Promise<object> {
const result = await client.bulk({
body: JSON.parse(operations),
});
return result;
}
export async function countDocuments(client, index: string, query?: string): Promise<object> {
const body = query ? JSON.parse(query) : undefined;
const result = await client.count({ index, body });
return result;
}
export async function documentExists(client, index: string, id: string): Promise<boolean> {
const result = await client.exists({ index, id });
return result;
}
export async function multiGet(client, operations: string): Promise<object> {
const result = await client.mget({
body: JSON.parse(operations),
});
return result;
}
export async function scrollSearch(client, scrollId: string, scroll: string): Promise<object> {
const result = await client.scroll({
scroll_id: scrollId,
scroll: scroll,
});
return result;
}
export async function clearScroll(client, scrollId: string): Promise<object> {
const result = await client.clearScroll({
scroll_id: scrollId,
});
return result;
}
export async function getCatIndices(client): Promise<object> {
const result = await client.cat.indices({ format: 'json' });
return result;
}
export async function getClusterHealth(client): Promise<object> {
const result = await client.cluster.health();
return result;
}

View file

@ -13,9 +13,12 @@ export type SourceOptions = {
};
export type QueryOptions = {
index: string;
query: string;
body: string;
id: string;
operation: string;
index?: string;
query?: string;
body?: string;
id?: string;
operations?: string;
scroll_id?: string;
scroll?: string;
};

View file

@ -15,6 +15,7 @@
}
},
"../common": {
"name": "@tooljet-plugins/common",
"version": "1.0.0",
"dependencies": {
"react": "^17.0.2",

View file

@ -22,7 +22,14 @@ export default class GooglesheetsQueryService implements QueryService {
);
}
async accessDetailsFrom(authCode: string): Promise<object> {
async accessDetailsFrom(authCode: string, options: any, resetSecureData = false): Promise<object> {
if (resetSecureData) {
return [
['access_token', ''],
['refresh_token', ''],
];
}
const accessTokenUrl = 'https://oauth2.googleapis.com/token';
const clientId = process.env.GOOGLE_CLIENT_ID;
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
@ -134,7 +141,7 @@ export default class GooglesheetsQueryService implements QueryService {
} catch (error) {
console.error({ statusCode: error?.response?.statusCode, message: error?.response?.body });
if (error?.response?.statusCode === 401) {
if (error?.response?.statusCode === 401 || error?.response?.statusCode === 403) {
throw new OAuthUnauthorizedClientError('Query could not be completed', error.message, { ...error });
}
throw new QueryError('Query could not be completed', error.message, {});

View file

@ -13,6 +13,7 @@ import {
fetchHttpsCertsForCustomCA,
getRefreshedToken,
getAuthUrl,
redactHeaders,
} from '@tooljet-plugins/common';
import { QueryOptions, SourceOptions } from './types';
@ -55,7 +56,7 @@ export default class GraphqlQueryService implements QueryService {
...fetchHttpsCertsForCustomCA(),
};
const authValidatedRequestOptions = validateAndSetRequestOptionsBasedOnAuthType(
const authValidatedRequestOptions = await validateAndSetRequestOptionsBasedOnAuthType(
sourceOptions,
context,
_requestOptions
@ -65,10 +66,24 @@ export default class GraphqlQueryService implements QueryService {
const requestOptions = data as OptionsOfTextResponseBody;
let result = {};
let requestObject = {};
let responseObject = {};
try {
const response = await this.sendRequest(url, requestOptions);
result = JSON.parse(response.body);
requestObject = {
url: response.requestUrl,
method: response.request.options.method,
headers: redactHeaders(response.request.options.headers),
params: urrl.parse(response.request.requestUrl, true).query,
};
responseObject = {
statusCode: response.statusCode,
headers: redactHeaders(response.headers),
};
} catch (error) {
console.error(
`Error while calling GraphQL end point. status code: ${error?.response?.statusCode} message: ${error?.response?.body}`
@ -101,6 +116,10 @@ export default class GraphqlQueryService implements QueryService {
return {
status: 'ok',
data: result,
metadata: {
request: requestObject,
response: responseObject,
},
};
}

View file

@ -165,16 +165,18 @@
"label": "Headers",
"key": "headers",
"type": "react-component-headers",
"description": "key-value pair headers for graphql api"
"description": "key-value pair headers for graphql api",
"width":"316px"
},
"url_params": {
"label": "URL parameters",
"key": "url_params",
"type": "react-component-headers",
"description": "key-value pair url parameters for graphql api"
"description": "key-value pair url parameters for graphql api",
"width":"316px"
},
"auth_type": {
"label": "Authentication Type",
"label": "Authentication type",
"key": "auth_type",
"type": "react-component-oauth-authentication",
"description": "key-value pair headers for graphQL api"

View file

@ -27,7 +27,8 @@
"type": "react-component-headers",
"description": "key-value pair headers for graphql api",
"height": "150px",
"editorType": "extendedSingleLine"
"editorType": "extendedSingleLine",
"buttonText": "Add Header"
}
},
"required": [

View file

@ -71,7 +71,7 @@
"description": "0.0.0.0:50051"
},
"auth_type": {
"label": "Authentication Type",
"label": "Authentication type",
"key": "auth_type",
"type": "react-component-oauth-authentication",
"description": "key-value pair headers for rest api"

View file

@ -15,6 +15,7 @@
}
},
"../common": {
"name": "@tooljet-plugins/common",
"version": "1.0.0",
"dependencies": {
"react": "^17.0.2",
@ -26,9 +27,9 @@
}
},
"node_modules/@grpc/grpc-js": {
"version": "1.10.10",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.10.tgz",
"integrity": "sha512-HPa/K5NX6ahMoeBv15njAc/sfF4/jmiXLar9UlC2UfHFKZzsCVLc3wbe7+7qua7w9VPh2/L6EBxyAV7/E8Wftg==",
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.11.1.tgz",
"integrity": "sha512-gyt/WayZrVPH2w/UTLansS7F9Nwld472JxxaETamrM8HNlsa+jSLNyKAZmhxI2Me4c3mQHFiS1wWHDY1g1Kthw==",
"dependencies": {
"@grpc/proto-loader": "^0.7.13",
"@js-sdsl/ordered-map": "^4.4.2"
@ -122,9 +123,9 @@
"link": true
},
"node_modules/@types/node": {
"version": "20.14.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz",
"integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==",
"version": "20.14.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz",
"integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==",
"dependencies": {
"undici-types": "~5.26.4"
}

View file

@ -59,7 +59,7 @@
},
"properties": {
"connection_type": {
"label": "",
"label": "Connection type",
"key": "connection_type",
"type": "dropdown-component-flip",
"description": "Single select dropdown for connection_type",

View file

@ -8,6 +8,9 @@ import {
getCachedConnection,
} from '@tooljet-plugins/common';
import { SourceOptions, QueryOptions } from './types';
import { isEmpty } from '@tooljet-plugins/common';
const STATEMENT_TIMEOUT = 10000;
export default class MssqlQueryService implements QueryService {
private static _instance: MssqlQueryService;
@ -21,33 +24,73 @@ export default class MssqlQueryService implements QueryService {
return MssqlQueryService._instance;
}
connectionOptions(sourceOptions: SourceOptions) {
const _connectionOptions = (sourceOptions?.connection_options || []).filter((o) => {
return o.some((e) => !isEmpty(e));
});
const connectionOptions = Object.fromEntries(_connectionOptions);
Object.keys(connectionOptions).forEach((key) =>
connectionOptions[key] === '' ? delete connectionOptions[key] : {}
);
return connectionOptions;
}
async run(
sourceOptions: SourceOptions,
queryOptions: QueryOptions,
dataSourceId: string,
dataSourceUpdatedAt: string
): Promise<QueryResult> {
let result = {};
let query = queryOptions.query;
const knexInstance = await this.getConnection(sourceOptions, {}, true, dataSourceId, dataSourceUpdatedAt);
try {
if (queryOptions.mode === 'gui') {
if (queryOptions.operation === 'bulk_update_pkey') {
query = this.buildBulkUpdateQuery(queryOptions);
}
const knexInstance = await this.getConnection(sourceOptions, {}, true, dataSourceId, dataSourceUpdatedAt);
switch (queryOptions.mode) {
case 'sql':
return await this.handleRawQuery(knexInstance, queryOptions);
case 'gui':
return await this.handleGuiQuery(knexInstance, queryOptions);
default:
throw new Error("Invalid query mode. Must be either 'sql' or 'gui'.");
}
result = await knexInstance.raw(query);
} catch (err) {
throw new QueryError('Query could not be completed', err.message, {});
throw new QueryError(
'Query could not be completed',
err instanceof Error ? err.message : 'An unknown error occurred',
{}
);
}
}
private async handleGuiQuery(knexInstance: Knex, queryOptions: QueryOptions): Promise<any> {
if (queryOptions.operation !== 'bulk_update_pkey') {
return { rows: [] };
}
const query = this.buildBulkUpdateQuery(queryOptions);
return await this.executeQuery(knexInstance, query);
}
private async handleRawQuery(knexInstance: Knex, queryOptions: QueryOptions): Promise<QueryResult> {
const { query, query_params } = queryOptions;
const queryParams = query_params || [];
const sanitizedQueryParams: Record<string, any> = Object.fromEntries(queryParams.filter(([key]) => !isEmpty(key)));
const result = await this.executeQuery(knexInstance, query, sanitizedQueryParams);
return { status: 'ok', data: result };
}
private async executeQuery(knexInstance: Knex, query: string, sanitizedQueryParams: Record<string, any> = {}) {
if (isEmpty(query)) throw new Error('Query is empty');
const result = await knexInstance.raw(query, sanitizedQueryParams).timeout(STATEMENT_TIMEOUT);
return result;
}
async testConnection(sourceOptions: SourceOptions): Promise<ConnectionTestResult> {
const knexInstance = await this.getConnection(sourceOptions, {}, false);
await knexInstance.raw('select @@version;');
await knexInstance.raw('select @@version;').timeout(STATEMENT_TIMEOUT);
knexInstance.destroy();
return {
@ -55,7 +98,7 @@ export default class MssqlQueryService implements QueryService {
};
}
async buildConnection(sourceOptions: SourceOptions) {
async buildConnection(sourceOptions: SourceOptions): Promise<Knex> {
const config: Knex.Config = {
client: 'mssql',
connection: {
@ -70,6 +113,7 @@ export default class MssqlQueryService implements QueryService {
},
pool: { min: 0 },
},
...this.connectionOptions(sourceOptions),
};
return knex(config);
@ -81,7 +125,7 @@ export default class MssqlQueryService implements QueryService {
checkCache: boolean,
dataSourceId?: string,
dataSourceUpdatedAt?: string
): Promise<any> {
): Promise<Knex> {
if (checkCache) {
let connection = await getCachedConnection(dataSourceId, dataSourceUpdatedAt);

View file

@ -3,7 +3,9 @@
"title": "Mysql data query schema",
"description": "A schema defining mysql data query",
"type": "database",
"defaults": {},
"defaults": {
"mode": "sql"
},
"properties": {
"mode": {
"label": "",
@ -27,7 +29,16 @@
"type": "codehinter",
"description": "Enter query",
"height": "150px",
"editorType": "multiline"
"editorType": "multiline",
"placeholder": "SELECT * FROM users"
},
"query_params": {
"key": "query_params",
"label": "SQL Parameters",
"type": "react-component-headers",
"description": "Parameters for the SQL query",
"buttonText": "Add SQL parameter",
"tooltip": "Use these parameters in the SQL query using named notation < :parameter_name >"
}
},
"gui": {
@ -79,4 +90,4 @@
}
},
"required": []
}
}

View file

@ -6,6 +6,7 @@ export type SourceOptions = {
username: string;
password: string;
azure: boolean;
connection_options: string[][];
};
export type QueryOptions = {
operation: string;
@ -13,5 +14,6 @@ export type QueryOptions = {
mode: string;
table: string;
primary_key_column: string;
records: any;
records: Record<string, unknown>[];
query_params: string[][];
};

View file

@ -16,6 +16,7 @@
}
},
"../common": {
"name": "@tooljet-plugins/common",
"version": "1.0.0",
"dependencies": {
"react": "^17.0.2",
@ -269,30 +270,30 @@
}
},
"node_modules/@azure/msal-browser": {
"version": "3.17.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.17.0.tgz",
"integrity": "sha512-csccKXmW2z7EkZ0I3yAoW/offQt+JECdTIV/KrnRoZyM7wCSsQWODpwod8ZhYy7iOyamcHApR9uCh0oD1M+0/A==",
"version": "3.18.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.18.0.tgz",
"integrity": "sha512-jvK5bDUWbpOaJt2Io/rjcaOVcUzkqkrCme/WntdV1SMUc67AiTcEdKuY6G/nMQ7N5Cfsk9SfpugflQwDku53yg==",
"dependencies": {
"@azure/msal-common": "14.12.0"
"@azure/msal-common": "14.13.0"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-common": {
"version": "14.12.0",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz",
"integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==",
"version": "14.13.0",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.13.0.tgz",
"integrity": "sha512-b4M/tqRzJ4jGU91BiwCsLTqChveUEyFK3qY2wGfZ0zBswIBZjAxopx5CYt5wzZFKuN15HqRDYXQbztttuIC3nA==",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-node": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.9.2.tgz",
"integrity": "sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ==",
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.10.0.tgz",
"integrity": "sha512-JxsSE0464a8IA/+q5EHKmchwNyUFJHtCH00tSXsLaOddwLjG6yVvTH6lGgPcWMhO7YWUXj/XVgVgeE9kZtsPUQ==",
"dependencies": {
"@azure/msal-common": "14.12.0",
"@azure/msal-common": "14.13.0",
"jsonwebtoken": "^9.0.0",
"uuid": "^8.3.0"
},
@ -310,17 +311,17 @@
"link": true
},
"node_modules/@types/node": {
"version": "20.14.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz",
"integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==",
"version": "20.14.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz",
"integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/readable-stream": {
"version": "4.0.14",
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.14.tgz",
"integrity": "sha512-xZn/AuUbCMShGsqH/ehZtGDwQtbx00M9rZ2ENLe4tOjFZ/JFeWMhEZkk2fEe1jAUqqEAURIkFJ7Az/go8mM1/w==",
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.15.tgz",
"integrity": "sha512-oAZ3kw+kJFkEqyh7xORZOku1YAKvsFTogRY8kVl4vHpEKiDkfnSA/My8haRE7fvmix5Zyy+1pwzOi7yycGLBJw==",
"dependencies": {
"@types/node": "*",
"safe-buffer": "~5.1.1"
@ -436,7 +437,7 @@
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"node_modules/debug": {
"version": "4.3.4",
@ -471,9 +472,9 @@
}
},
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
"engines": {
"node": ">=6"
}
@ -505,7 +506,7 @@
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"node_modules/function-bind": {
"version": "1.1.2",
@ -529,15 +530,15 @@
"integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA=="
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
@ -572,9 +573,9 @@
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
"integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
"integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
"dependencies": {
"agent-base": "^7.0.2",
"debug": "4"
@ -616,7 +617,7 @@
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"dependencies": {
"once": "^1.3.0",
@ -877,7 +878,7 @@
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dependencies": {
"wrappy": "1"
}
@ -901,7 +902,7 @@
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"engines": {
"node": ">=0.10.0"
}
@ -1083,9 +1084,9 @@
}
},
"node_modules/tedious": {
"version": "18.2.1",
"resolved": "https://registry.npmjs.org/tedious/-/tedious-18.2.1.tgz",
"integrity": "sha512-DKsTgGBC0ZeZexAd5OObfeKd0Tlx3jx3kNoKImsxfBKdRuV216u9n6Sr+4w6vzn+S4r43XmWAXQwM7UkDkbIEg==",
"version": "18.2.3",
"resolved": "https://registry.npmjs.org/tedious/-/tedious-18.2.3.tgz",
"integrity": "sha512-AMdO1sodcoMU3vqDiU2d+Bdck6LcMAj4s4/fkxWXAgWGVnbZOQKaQrn6f+cRAZpdJhn5b8vX7cOfmB7oKNMUqQ==",
"dependencies": {
"@azure/identity": "^4.2.1",
"@azure/keyvault-keys": "^4.4.0",
@ -1130,7 +1131,7 @@
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}
}
}

View file

@ -1,4 +1,4 @@
import { Knex, knex } from 'knex';
import knex, { Knex } from 'knex';
import {
cacheConnection,
getCachedConnection,
@ -8,6 +8,9 @@ import {
QueryError,
} from '@tooljet-plugins/common';
import { SourceOptions, QueryOptions } from './types';
import { isEmpty } from '@tooljet-plugins/common';
const STATEMENT_TIMEOUT = 10000;
export default class MysqlQueryService implements QueryService {
private static _instance: MysqlQueryService;
@ -16,7 +19,6 @@ export default class MysqlQueryService implements QueryService {
if (MysqlQueryService._instance) {
return MysqlQueryService._instance;
}
MysqlQueryService._instance = this;
return MysqlQueryService._instance;
}
@ -27,53 +29,86 @@ export default class MysqlQueryService implements QueryService {
dataSourceId: string,
dataSourceUpdatedAt: string
): Promise<QueryResult> {
let result = {
rows: [],
};
let query = '';
if (queryOptions.mode === 'gui') {
if (queryOptions.operation === 'bulk_update_pkey') {
query = await this.buildBulkUpdateQuery(queryOptions);
}
} else {
query = queryOptions.query;
}
const knexInstance = await this.getConnection(sourceOptions, {}, true, dataSourceId, dataSourceUpdatedAt);
try {
result = await knexInstance.raw(query);
} catch (err) {
console.log(err);
throw new QueryError('Query could not be completed', err.message, {});
}
const knexInstance = await this.getConnection(sourceOptions, {}, true, dataSourceId, dataSourceUpdatedAt);
return {
status: 'ok',
data: result[0],
};
switch (queryOptions.mode) {
case 'sql':
return await this.handleRawQuery(knexInstance, queryOptions);
case 'gui':
return await this.handleGuiQuery(knexInstance, queryOptions);
default:
throw new Error("Invalid query mode. Must be either 'sql' or 'gui'.");
}
} catch (err) {
throw new QueryError(
'Query could not be completed',
err instanceof Error ? err.message : 'An unknown error occurred',
{}
);
}
}
async testConnection(sourceOptions: SourceOptions): Promise<ConnectionTestResult> {
const knexInstance = await this.getConnection(sourceOptions, {}, false);
await knexInstance.raw('select @@version;');
await knexInstance.raw('select @@version;').timeout(STATEMENT_TIMEOUT);
knexInstance.destroy();
return {
status: 'ok',
};
return { status: 'ok' };
}
async buildConnection(sourceOptions: SourceOptions) {
// either use socket_path or host/port + ssl
private async handleGuiQuery(knexInstance: Knex, queryOptions: QueryOptions): Promise<any> {
if (queryOptions.operation !== 'bulk_update_pkey') {
return { rows: [] };
}
const query = this.buildBulkUpdateQuery(queryOptions);
return await this.executeQuery(knexInstance, query);
}
private async handleRawQuery(knexInstance: Knex, queryOptions: QueryOptions): Promise<QueryResult> {
const { query, query_params } = queryOptions;
const queryParams = query_params || [];
const sanitizedQueryParams: Record<string, any> = Object.fromEntries(queryParams.filter(([key]) => !isEmpty(key)));
const result = await this.executeQuery(knexInstance, query, sanitizedQueryParams);
return { status: 'ok', data: result[0] };
}
private async executeQuery(knexInstance: Knex, query: string, sanitizedQueryParams: Record<string, any> = {}) {
if (isEmpty(query)) throw new Error('Query is empty');
const result = await knexInstance.raw(query, sanitizedQueryParams).timeout(STATEMENT_TIMEOUT);
return result;
}
private connectionOptions(sourceOptions: SourceOptions) {
const _connectionOptions = (sourceOptions?.connection_options || []).filter((o) => o.some((e) => !isEmpty(e)));
const connectionOptions = Object.fromEntries(_connectionOptions);
Object.keys(connectionOptions).forEach((key) =>
connectionOptions[key] === '' ? delete connectionOptions[key] : {}
);
return connectionOptions;
}
private async buildConnection(sourceOptions: SourceOptions): Promise<Knex> {
const props = sourceOptions.socket_path
? { socketPath: sourceOptions.socket_path }
: {
host: sourceOptions.host,
port: +sourceOptions.port,
ssl: sourceOptions.ssl_enabled ?? false, // Disabling by default for backward compatibility
ssl: sourceOptions.ssl_enabled ?? false,
};
const sslObject = { rejectUnauthorized: (sourceOptions.ssl_certificate ?? 'none') != 'none' };
if (sourceOptions.ssl_certificate === 'ca_certificate') {
sslObject['ca'] = sourceOptions.ca_cert;
}
if (sourceOptions.ssl_certificate === 'self_signed') {
sslObject['ca'] = sourceOptions.root_cert;
sslObject['key'] = sourceOptions.client_key;
sslObject['cert'] = sourceOptions.client_cert;
}
const config: Knex.Config = {
client: 'mysql2',
connection: {
@ -82,10 +117,10 @@ export default class MysqlQueryService implements QueryService {
password: sourceOptions.password,
database: sourceOptions.database,
multipleStatements: true,
...(sourceOptions.ssl_enabled && { ssl: { rejectUnauthorized: false } }),
...(sourceOptions.ssl_enabled && { ssl: sslObject }),
},
...this.connectionOptions(sourceOptions),
};
return knex(config);
}
@ -95,28 +130,21 @@ export default class MysqlQueryService implements QueryService {
checkCache: boolean,
dataSourceId?: string,
dataSourceUpdatedAt?: string
): Promise<any> {
): Promise<Knex> {
if (checkCache) {
let connection = await getCachedConnection(dataSourceId, dataSourceUpdatedAt);
if (connection) {
return connection;
} else {
connection = await this.buildConnection(sourceOptions);
dataSourceId && cacheConnection(dataSourceId, connection);
return connection;
}
} else {
return await this.buildConnection(sourceOptions);
const cachedConnection = await getCachedConnection(dataSourceId, dataSourceUpdatedAt);
if (cachedConnection) return cachedConnection;
}
const connection = await this.buildConnection(sourceOptions);
if (checkCache && dataSourceId) cacheConnection(dataSourceId, connection);
return connection;
}
async buildBulkUpdateQuery(queryOptions: any): Promise<string> {
buildBulkUpdateQuery(queryOptions: QueryOptions): string {
let queryText = '';
const tableName = queryOptions['table'];
const primaryKey = queryOptions['primary_key_column'];
const records = queryOptions['records'];
const { table: tableName, primary_key_column: primaryKey, records } = queryOptions;
for (const record of records) {
const primaryKeyValue = typeof record[primaryKey] === 'string' ? `'${record[primaryKey]}'` : record[primaryKey];

Some files were not shown because too many files have changed in this diff Show more