mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 13:37:28 +00:00
Feature: Import export tjdb schema (#5752)
* add ability to import export app and tjdb schema * init * feat ::global settings popover new ui * feat :: ui for version export modal * fix :: import export modal * cleanup * ui updates * header footer style fixes * closing settings modal while showing export modal * style fix header * feat :: added button to download table schema * fix :: styling for fx * add ability to import and export apps with tjdb schema * handle duplicate table in workspace * fix table rename * fix selected table on edit and delete * fix invalid toast on table delete * fix column default value * handle exports to strip '::' and quotes * make import/export backward compatible * handle page redirects based on resource import * handle import without tjdb schema * fix column delete and addition * make data migrations to be run per organizations * wip * update migration * fix credentials to be included * fix specific version export * make use of apps ability for import export resource * fix import navigation * fix lint * fix failing tests * fix lint * enable tjdb for public apps * update export error message on tjdb table blank * fix table not selected after creation * fix :: styling for imp exp modal , and functionality bug fixes after dev merge * fixes blank slate and columns selection * fix table delete * fix invalid toast on table edit * fix column information missing tjdb query manager * make ds imports to either reuse global or create * export only unique table ids * create default datasources if not present in export data * reuse existing table on imports * add timestamp to table name if name already exists * add ability to clone with tjdb * make imports work with marketplace plugin * skip dataqueries for which plugins are not installed * fix filter input width * fix failing spec * fix marketplace plugin installation in diff workspaces * fix check for plugin installed in workspace * fix export when table name is empty --------- Co-authored-by: stepinfwd <stepinfwd@gmail.com>
This commit is contained in:
parent
d265cd792b
commit
c6fe0aa45e
54 changed files with 2024 additions and 734 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -36,3 +36,4 @@
|
|||
/frontend/cypress/videos
|
||||
|
||||
.idea/*
|
||||
ti-*
|
||||
|
|
|
|||
|
|
@ -935,9 +935,10 @@
|
|||
"tip": "Global Settings",
|
||||
"hideHeader": "Hide header for launched apps",
|
||||
"maintenanceMode": "Maintenance mode",
|
||||
"maxWidthOfCanvas": "Max width of canvas",
|
||||
"maxHeightOfCanvas": "Max height of canvas",
|
||||
"backgroundColorOfCanvas": "Background color of canvas"
|
||||
"maxWidthOfCanvas": "Max canvas width",
|
||||
"maxHeightOfCanvas": "Max canvas height",
|
||||
"backgroundColorOfCanvas": "Canvas BG",
|
||||
"exportApp": "Export app"
|
||||
},
|
||||
"Back": {
|
||||
"text": "Back",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default function FxButton({ active, onPress, dataCy }) {
|
|||
onClick={onPress}
|
||||
data-cy={`${dataCy}-fx-button`}
|
||||
>
|
||||
Fx
|
||||
fx
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||
import cx from 'classnames';
|
||||
import { SketchPicker } from 'react-color';
|
||||
import { Confirm } from '../Viewer/Confirm';
|
||||
import { HeaderSection } from '@/_ui/LeftSidebar';
|
||||
import { LeftSidebarItem } from '../LeftSidebar/SidebarItem';
|
||||
import FxButton from '../CodeBuilder/Elements/FxButton';
|
||||
import { CodeHinter } from '../CodeBuilder/CodeHinter';
|
||||
|
|
@ -10,6 +9,7 @@ import { resolveReferences } from '@/_helpers/utils';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import _ from 'lodash';
|
||||
import Popover from '@/_ui/Popover';
|
||||
import ExportAppModal from '../../HomePage/ExportAppModal';
|
||||
import { useCurrentState } from '@/_stores/currentStateStore';
|
||||
import { useAppVersionStore } from '@/_stores/appVersionStore';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
|
@ -20,6 +20,7 @@ export const GlobalSettings = ({
|
|||
darkMode,
|
||||
toggleAppMaintenance,
|
||||
is_maintenance_on,
|
||||
app,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { hideHeader, canvasMaxWidth, canvasMaxWidthType, canvasBackgroundColor, backgroundFxQuery } = globalSettings;
|
||||
|
|
@ -29,6 +30,7 @@ export const GlobalSettings = ({
|
|||
const [realState, setRealState] = React.useState(currentState);
|
||||
const [showConfirmation, setConfirmationShow] = React.useState(false);
|
||||
const [show, setShow] = React.useState('');
|
||||
const [isExportingApp, setIsExportingApp] = React.useState(false);
|
||||
const { isVersionReleased } = useAppVersionStore(
|
||||
(state) => ({
|
||||
isVersionReleased: state.isVersionReleased,
|
||||
|
|
@ -58,16 +60,13 @@ export const GlobalSettings = ({
|
|||
const popoverContent = (
|
||||
<div id="global-settings-popover" className={cx({ 'theme-dark': darkMode, disabled: isVersionReleased })}>
|
||||
<div bsPrefix="global-settings-popover">
|
||||
<HeaderSection darkMode={darkMode}>
|
||||
<HeaderSection.PanelHeader title="Global settings" />
|
||||
</HeaderSection>
|
||||
<div className="card-body">
|
||||
<div>
|
||||
<div>
|
||||
<div className="d-flex mb-3">
|
||||
<div className="d-flex justify-content-start">
|
||||
<span data-cy={`label-hide-header-for-launched-apps`}>
|
||||
{t('leftSidebar.Settings.hideHeader', 'Hide header for launched apps')}
|
||||
</span>
|
||||
<div className="ms-auto form-check form-switch position-relative">
|
||||
<div className="form-check form-switch">
|
||||
<input
|
||||
data-cy={`toggle-hide-header-for-launched-apps`}
|
||||
className="form-check-input"
|
||||
|
|
@ -76,12 +75,15 @@ export const GlobalSettings = ({
|
|||
onChange={(e) => globalSettingsChanged('hideHeader', e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<span className="global-popover-text">
|
||||
{t('leftSidebar.Settings.hideHeader', 'Hide header for launched apps')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="d-flex mb-3">
|
||||
<div className="d-flex justify-content-start">
|
||||
<span data-cy={`label-maintenance-mode`}>
|
||||
{t('leftSidebar.Settings.maintenanceMode', 'Maintenance mode')}
|
||||
</span>
|
||||
<div className="ms-auto form-check form-switch position-relative">
|
||||
<div className="form-check form-switch">
|
||||
<input
|
||||
data-cy={`toggle-maintenance-mode`}
|
||||
className="form-check-input"
|
||||
|
|
@ -90,17 +92,20 @@ export const GlobalSettings = ({
|
|||
onChange={() => setConfirmationShow(true)}
|
||||
/>
|
||||
</div>
|
||||
<span className="global-popover-text">
|
||||
{t('leftSidebar.Settings.maintenanceMode', 'Maintenance mode')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="d-flex mb-3">
|
||||
<div className="d-flex mb-3 global-popover-div-wrap ">
|
||||
<span data-cy={`label-max-canvas-width`} className="w-full m-auto">
|
||||
{t('leftSidebar.Settings.maxWidthOfCanvas', 'Max width of canvas')}
|
||||
</span>
|
||||
<div className="position-relative">
|
||||
<div className="global-popover-div-wrap global-popover-div-wrap-width">
|
||||
<div className="input-with-icon">
|
||||
<input
|
||||
data-cy="maximum-canvas-width-input-field"
|
||||
type="text"
|
||||
className={`form-control form-control-sm`}
|
||||
className={`form-control form-control-sm maximum-canvas-width-input-field`}
|
||||
placeholder={'0'}
|
||||
onChange={(e) => {
|
||||
const width = e.target.value;
|
||||
|
|
@ -109,8 +114,8 @@ export const GlobalSettings = ({
|
|||
value={canvasMaxWidth}
|
||||
/>
|
||||
<select
|
||||
className="maximum-canvas-width-input-select"
|
||||
data-cy={`dropdown-max-canvas-width-type`}
|
||||
className="form-select"
|
||||
aria-label="Select canvas width type"
|
||||
onChange={(event) => {
|
||||
const newCanvasMaxWidthType = event.currentTarget.value;
|
||||
|
|
@ -136,7 +141,7 @@ export const GlobalSettings = ({
|
|||
<span className="w-full m-auto" data-cy={`label-max-canvas-height`}>
|
||||
{t('leftSidebar.Settings.maxHeightOfCanvas', 'Max height of canvas')}
|
||||
</span>
|
||||
<div className="position-relative">
|
||||
<div className="global-popover-div-wrap global-popover-div-wrap-width">
|
||||
<div className="input-with-icon">
|
||||
<input
|
||||
data-cy="maximum-canvas-height-input-field"
|
||||
|
|
@ -149,7 +154,6 @@ export const GlobalSettings = ({
|
|||
}}
|
||||
value={canvasMaxHeight}
|
||||
/>
|
||||
<span className="input-group-text">px</span>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
|
@ -175,7 +179,7 @@ export const GlobalSettings = ({
|
|||
)}
|
||||
{forceCodeBox && (
|
||||
<div
|
||||
className="row mx-0 form-control form-control-sm canvas-background-holder"
|
||||
className="form-control form-control-sm canvas-background-holder"
|
||||
onClick={() => setShowPicker(true)}
|
||||
>
|
||||
<div
|
||||
|
|
@ -183,15 +187,13 @@ export const GlobalSettings = ({
|
|||
className="col-auto"
|
||||
style={{
|
||||
float: 'right',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
width: '13.33px',
|
||||
height: '13.33px',
|
||||
backgroundColor: canvasBackgroundColor,
|
||||
border: `0.25px solid ${
|
||||
['#ffffff', '#fff', '#1f2936'].includes(canvasBackgroundColor) && '#c5c8c9'
|
||||
}`,
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
></div>
|
||||
<div className="col">{canvasBackgroundColor}</div>
|
||||
<div className="">{canvasBackgroundColor}</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
|
|
@ -225,6 +227,26 @@ export const GlobalSettings = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex align-items-center global-popover-div-wrap">
|
||||
<p className="global-popover-text">Export app</p>
|
||||
<button
|
||||
className="export-app-btn"
|
||||
onClick={() => {
|
||||
setIsExportingApp(true);
|
||||
document.getElementById('maintenance-app-modal').click();
|
||||
}}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.5 5.78906V17.2891V18.0391C2.5 19.834 3.98027 21.2891 5.77519 21.2891C7.5425 21.2891 9 19.8564 9 18.0891C9 17.6472 9.35817 17.2891 9.8 17.2891H18.5V5.78906C18.5 4.13221 17.1569 2.78906 15.5 2.78906H5.5C3.84315 2.78906 2.5 4.13221 2.5 5.78906ZM9.75 9.61723C9.70334 9.65231 9.65858 9.69108 9.61612 9.73355L8.03033 11.3193C7.73744 11.6122 7.26256 11.6122 6.96967 11.3193C6.67678 11.0264 6.67678 10.5516 6.96967 10.2587L8.55546 8.67289C9.6294 7.59895 11.3706 7.59895 12.4445 8.67289L14.0303 10.2587C14.3232 10.5516 14.3232 11.0264 14.0303 11.3193C13.7374 11.6122 13.2626 11.6122 12.9697 11.3193L11.3839 9.73355C11.3414 9.69108 11.2967 9.65231 11.25 9.61723V13.789C11.25 14.2032 10.9142 14.539 10.5 14.539C10.0858 14.539 9.75 14.2032 9.75 13.789V9.61723ZM22.3766 19.7789C21.9361 21.5093 20.3675 22.7891 18.5 22.7891H6.5C8.36748 22.7891 9.93606 21.5093 10.3766 19.7789C10.5128 19.2437 10.9477 18.7891 11.5 18.7891H21.5C22.0523 18.7891 22.5128 19.2437 22.3766 19.7789Z"
|
||||
fill="#3E63DD"
|
||||
/>
|
||||
</svg>
|
||||
<span style={{ paddingLeft: '6px' }}>Export this app</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -244,6 +266,18 @@ export const GlobalSettings = ({
|
|||
onCancel={() => setConfirmationShow(false)}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
{isExportingApp && app.hasOwnProperty('id') && (
|
||||
<ExportAppModal
|
||||
show={isExportingApp}
|
||||
closeModal={() => {
|
||||
setIsExportingApp(false);
|
||||
}}
|
||||
customClassName="modal-version-lists"
|
||||
title={'Select a version to export'}
|
||||
app={app}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
)}
|
||||
<Popover
|
||||
handleToggle={(show) => {
|
||||
if (show) setShow('settings');
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ export default function EditorHeader({
|
|||
darkMode={darkMode}
|
||||
toggleAppMaintenance={toggleAppMaintenance}
|
||||
is_maintenance_on={is_maintenance_on}
|
||||
app={app}
|
||||
/>
|
||||
<EditAppName appId={app.id} appName={app.name} onNameChanged={onNameChanged} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
const [operation, setOperation] = useState(options['operation'] || '');
|
||||
const [columns, setColumns] = useState([]);
|
||||
const [tables, setTables] = useState([]);
|
||||
const [selectedTable, setSelectedTable] = useState(options['table_name']);
|
||||
const [selectedTableId, setSelectedTableId] = useState(options['table_id']);
|
||||
const [selectedTableName, setSelectedTableName] = useState(null);
|
||||
const [listRowsOptions, setListRowsOptions] = useState(() => options['list_rows'] || {});
|
||||
const [updateRowsOptions, setUpdateRowsOptions] = useState(
|
||||
options['update_rows'] || { columns: {}, where_filters: {} }
|
||||
|
|
@ -38,6 +39,19 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (tables.length > 0) {
|
||||
const tableInfo = tables.find((table) => table.table_id == selectedTableId);
|
||||
tableInfo && setSelectedTableName(tableInfo.table_name);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tables]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedTableName && fetchTableInformation(selectedTableName);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedTableName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mounted) {
|
||||
optionchanged('operation', operation);
|
||||
|
|
@ -90,8 +104,10 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
setTables,
|
||||
columns,
|
||||
setColumns,
|
||||
selectedTable,
|
||||
setSelectedTable,
|
||||
selectedTableId,
|
||||
setSelectedTableId,
|
||||
selectedTableName,
|
||||
setSelectedTableName,
|
||||
listRowsOptions,
|
||||
setListRowsOptions,
|
||||
limitOptionChanged,
|
||||
|
|
@ -102,7 +118,16 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
updateRowsOptions,
|
||||
handleUpdateRowsOptionsChange,
|
||||
}),
|
||||
[organizationId, tables, columns, selectedTable, listRowsOptions, deleteRowsOptions, updateRowsOptions]
|
||||
[
|
||||
organizationId,
|
||||
tables,
|
||||
columns,
|
||||
selectedTableName,
|
||||
selectedTableId,
|
||||
listRowsOptions,
|
||||
deleteRowsOptions,
|
||||
updateRowsOptions,
|
||||
]
|
||||
);
|
||||
|
||||
const fetchTables = async () => {
|
||||
|
|
@ -114,12 +139,14 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
}
|
||||
|
||||
if (Array.isArray(data?.result)) {
|
||||
setTables(data.result.map((table) => table.table_name) || []);
|
||||
const selectedTableInfo = data.result.find((table) => table.id === options['table_id']);
|
||||
|
||||
if (selectedTable) {
|
||||
console.log('fetchTableInformation');
|
||||
fetchTableInformation(selectedTable);
|
||||
}
|
||||
selectedTableInfo && setSelectedTableId(selectedTableInfo.id);
|
||||
setTables(
|
||||
data.result.map((table) => {
|
||||
return { table_name: table.table_name, table_id: table.id };
|
||||
}) || []
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -144,21 +171,22 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
}
|
||||
};
|
||||
|
||||
const generateListForDropdown = (list) => {
|
||||
return list.map((value) =>
|
||||
const generateListForDropdown = (tableList) => {
|
||||
return tableList.map((tableMap) =>
|
||||
Object.fromEntries([
|
||||
['name', value],
|
||||
['value', value],
|
||||
['name', tableMap.table_name],
|
||||
['value', tableMap.table_id],
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
const handleTableNameSelect = (tableName) => {
|
||||
setSelectedTable(tableName);
|
||||
fetchTableInformation(tableName);
|
||||
const handleTableNameSelect = (tableId) => {
|
||||
setSelectedTableId(tableId);
|
||||
const { table_name: tableName } = tables.find((t) => t.table_id === tableId);
|
||||
tableName && setSelectedTableName(tableName);
|
||||
|
||||
optionchanged('organization_id', organizationId);
|
||||
optionchanged('table_name', tableName);
|
||||
optionchanged('table_id', tableId);
|
||||
};
|
||||
|
||||
const getComponent = () => {
|
||||
|
|
@ -185,7 +213,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
|
|||
<div className={cx({ 'flex-grow-1': isHorizontalLayout })}>
|
||||
<Select
|
||||
options={generateListForDropdown(tables)}
|
||||
value={selectedTable}
|
||||
value={selectedTableId}
|
||||
onChange={(value) => handleTableNameSelect(value)}
|
||||
width="100%"
|
||||
// useMenuPortal={false}
|
||||
|
|
|
|||
|
|
@ -8,16 +8,16 @@ export const tooljetDbOperations = {
|
|||
perform,
|
||||
};
|
||||
|
||||
async function perform(queryOptions, organizationId, currentState) {
|
||||
switch (queryOptions.operation) {
|
||||
async function perform(dataQuery, currentState) {
|
||||
switch (dataQuery.options.operation) {
|
||||
case 'list_rows':
|
||||
return listRows(queryOptions, organizationId, currentState);
|
||||
return listRows(dataQuery, currentState);
|
||||
case 'create_row':
|
||||
return createRow(queryOptions, organizationId, currentState);
|
||||
return createRow(dataQuery, currentState);
|
||||
case 'update_rows':
|
||||
return updateRows(queryOptions, organizationId, currentState);
|
||||
return updateRows(dataQuery, currentState);
|
||||
case 'delete_rows':
|
||||
return deleteRows(queryOptions, organizationId, currentState);
|
||||
return deleteRows(dataQuery, currentState);
|
||||
|
||||
default:
|
||||
return {
|
||||
|
|
@ -52,8 +52,8 @@ function buildPostgrestQuery(filters) {
|
|||
return postgrestQueryBuilder.url.toString();
|
||||
}
|
||||
|
||||
async function listRows(queryOptions, organizationId, currentState) {
|
||||
let query = [];
|
||||
async function listRows(dataQuery, currentState) {
|
||||
const queryOptions = dataQuery.options;
|
||||
const resolvedOptions = resolveReferences(queryOptions, currentState);
|
||||
if (hasEqualWithNull(resolvedOptions, 'list_rows')) {
|
||||
return {
|
||||
|
|
@ -64,7 +64,8 @@ async function listRows(queryOptions, organizationId, currentState) {
|
|||
data: {},
|
||||
};
|
||||
}
|
||||
const { table_name: tableName, list_rows: listRows } = resolvedOptions;
|
||||
const { table_id: tableId, list_rows: listRows } = resolvedOptions;
|
||||
let query = [];
|
||||
|
||||
if (!isEmpty(listRows)) {
|
||||
const { limit, where_filters: whereFilters, order_filters: orderFilters } = listRows;
|
||||
|
|
@ -86,19 +87,23 @@ async function listRows(queryOptions, organizationId, currentState) {
|
|||
!isEmpty(orderQuery) && query.push(orderQuery);
|
||||
!isEmpty(limit) && query.push(`limit=${limit}`);
|
||||
}
|
||||
return await tooljetDatabaseService.findOne(organizationId, tableName, query.join('&'));
|
||||
const headers = { 'data-query-id': dataQuery.id };
|
||||
return await tooljetDatabaseService.findOne(headers, tableId, query.join('&'));
|
||||
}
|
||||
|
||||
async function createRow(queryOptions, organizationId, currentState) {
|
||||
async function createRow(dataQuery, currentState) {
|
||||
const queryOptions = dataQuery.options;
|
||||
const resolvedOptions = resolveReferences(queryOptions, currentState);
|
||||
const columns = Object.values(resolvedOptions.create_row).reduce((acc, colOpts) => {
|
||||
if (isEmpty(colOpts.column)) return acc;
|
||||
return { ...acc, ...{ [colOpts.column]: colOpts.value } };
|
||||
}, {});
|
||||
return await tooljetDatabaseService.createRow(organizationId, resolvedOptions.table_name, columns);
|
||||
const headers = { 'data-query-id': dataQuery.id };
|
||||
return await tooljetDatabaseService.createRow(headers, resolvedOptions.table_id, columns);
|
||||
}
|
||||
|
||||
async function updateRows(queryOptions, organizationId, currentState) {
|
||||
async function updateRows(dataQuery, currentState) {
|
||||
const queryOptions = dataQuery.options;
|
||||
const resolvedOptions = resolveReferences(queryOptions, currentState);
|
||||
if (hasEqualWithNull(resolvedOptions, 'update_rows')) {
|
||||
return {
|
||||
|
|
@ -109,7 +114,7 @@ async function updateRows(queryOptions, organizationId, currentState) {
|
|||
data: {},
|
||||
};
|
||||
}
|
||||
const { table_name: tableName, update_rows: updateRows } = resolvedOptions;
|
||||
const { table_id: tableId, update_rows: updateRows } = resolvedOptions;
|
||||
const { where_filters: whereFilters, columns } = updateRows;
|
||||
|
||||
let query = [];
|
||||
|
|
@ -121,10 +126,12 @@ async function updateRows(queryOptions, organizationId, currentState) {
|
|||
|
||||
!isEmpty(whereQuery) && query.push(whereQuery);
|
||||
|
||||
return await tooljetDatabaseService.updateRows(organizationId, tableName, body, query.join('&') + '&order=id');
|
||||
const headers = { 'data-query-id': dataQuery.id };
|
||||
return await tooljetDatabaseService.updateRows(headers, tableId, body, query.join('&') + '&order=id');
|
||||
}
|
||||
|
||||
async function deleteRows(queryOptions, organizationId, currentState) {
|
||||
async function deleteRows(dataQuery, currentState) {
|
||||
const queryOptions = dataQuery.options;
|
||||
const resolvedOptions = resolveReferences(queryOptions, currentState);
|
||||
if (hasEqualWithNull(resolvedOptions, 'delete_rows')) {
|
||||
return {
|
||||
|
|
@ -135,7 +142,7 @@ async function deleteRows(queryOptions, organizationId, currentState) {
|
|||
data: {},
|
||||
};
|
||||
}
|
||||
const { table_name: tableName, delete_rows: deleteRows = { whereFilters: {} } } = resolvedOptions;
|
||||
const { table_id: tableId, delete_rows: deleteRows = { whereFilters: {} } } = resolvedOptions;
|
||||
const { where_filters: whereFilters, limit = 1 } = deleteRows;
|
||||
|
||||
let query = [];
|
||||
|
|
@ -163,5 +170,6 @@ async function deleteRows(queryOptions, organizationId, currentState) {
|
|||
!isEmpty(whereQuery) && query.push(whereQuery);
|
||||
limit && limit !== '' && query.push(`limit=${limit}&order=id`);
|
||||
|
||||
return await tooljetDatabaseService.deleteRow(organizationId, tableName, query.join('&'));
|
||||
const headers = { 'data-query-id': dataQuery.id };
|
||||
return await tooljetDatabaseService.deleteRows(headers, tableId, query.join('&'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -592,7 +592,6 @@ class ViewerComponent extends React.Component {
|
|||
return (
|
||||
<div className="viewer wrapper">
|
||||
<Confirm
|
||||
darkMode={this.props.darkMode}
|
||||
show={queryConfirmationList.length > 0}
|
||||
message={'Do you want to run this query?'}
|
||||
onConfirm={(queryConfirmationData) => onQueryConfirmOrCancel(this, queryConfirmationData, true, 'view')}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ export const ListItem = ({ dataSource, key, active, onDelete, updateSelectedData
|
|||
};
|
||||
|
||||
const sourceMeta = getSourceMetaData(dataSource);
|
||||
const icon = getSvgIcon(sourceMeta.kind.toLowerCase(), 24, 24, dataSource?.plugin?.iconFile?.data);
|
||||
|
||||
// sourceMeta would be missing on development setup when switching between branches
|
||||
// if ds is already in branch while not available in another
|
||||
const icon = getSvgIcon(sourceMeta?.kind?.toLowerCase(), 24, 24, dataSource?.plugin?.iconFile?.data);
|
||||
|
||||
const focusModal = () => {
|
||||
const element = document.getElementsByClassName('form-control-plaintext form-control-plaintext-sm')[0];
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { default as BootstrapModal } from 'react-bootstrap/Modal';
|
||||
import moment from 'moment';
|
||||
import { appService } from '../_services/app.service';
|
||||
import { appService } from '@/_services/app.service';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { ButtonSolid } from '@/_components/AppButton';
|
||||
|
||||
export default function ExportAppModal({ title, show, closeModal, customClassName, app, darkMode }) {
|
||||
const currentVersion = app.editing_version;
|
||||
const [versions, getVersions] = useState(undefined);
|
||||
const [versionId, setVersionId] = useState(currentVersion.id);
|
||||
const currentVersion = app?.editing_version;
|
||||
const [versions, setVersions] = useState(undefined);
|
||||
const [tables, setTables] = useState(undefined);
|
||||
const [versionId, setVersionId] = useState(currentVersion?.id);
|
||||
const [exportTjDb, setExportTjDb] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchAppVersions() {
|
||||
try {
|
||||
const fetchVersions = await appService.getVersions(app.id);
|
||||
const { versions } = await fetchVersions;
|
||||
getVersions(versions);
|
||||
const { versions } = fetchVersions;
|
||||
setVersions(versions);
|
||||
} catch (error) {
|
||||
toast.error('Could not fetch the versions.', {
|
||||
position: 'top-center',
|
||||
|
|
@ -22,13 +25,40 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
|
|||
closeModal();
|
||||
}
|
||||
}
|
||||
async function fetchAppTables() {
|
||||
try {
|
||||
const fetchTables = await appService.getTables(app.id);
|
||||
const { tables } = fetchTables;
|
||||
setTables(tables);
|
||||
} catch (error) {
|
||||
toast.error('Could not fetch the tables.', {
|
||||
position: 'top-center',
|
||||
});
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
fetchAppVersions();
|
||||
fetchAppTables();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const exportApp = (appId, versionId = undefined) => {
|
||||
const exportApp = (app, versionId, exportTjDb, tables) => {
|
||||
const appOpts = {
|
||||
app: [
|
||||
{
|
||||
id: app.id,
|
||||
...(versionId && { search_params: { version_id: versionId } }),
|
||||
},
|
||||
],
|
||||
};
|
||||
const requestBody = {
|
||||
...appOpts,
|
||||
...(exportTjDb && { tooljet_database: tables }),
|
||||
organization_id: app.organization_id,
|
||||
};
|
||||
|
||||
appService
|
||||
.exportApp(appId, versionId)
|
||||
.exportResource(requestBody)
|
||||
.then((data) => {
|
||||
const appName = app.name.replace(/\s+/g, '-').toLowerCase();
|
||||
const fileName = `${appName}-export-${new Date().getTime()}`;
|
||||
|
|
@ -44,8 +74,8 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
|
|||
document.body.removeChild(link);
|
||||
closeModal();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Could not export the app.', {
|
||||
.catch((error) => {
|
||||
toast.error(`Could not export app: ${error.data.message}`, {
|
||||
position: 'top-center',
|
||||
});
|
||||
closeModal();
|
||||
|
|
@ -55,11 +85,10 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
|
|||
return (
|
||||
<BootstrapModal
|
||||
onHide={() => closeModal(false)}
|
||||
contentClassName={`home-modal-component ${customClassName ? ` ${customClassName}` : ''} ${
|
||||
darkMode && 'dark-theme'
|
||||
}`}
|
||||
contentClassName={`home-modal-component home-version-modal-component ${
|
||||
customClassName ? ` ${customClassName}` : ''
|
||||
} ${darkMode && 'dark-theme'}`}
|
||||
show={show}
|
||||
size="md"
|
||||
backdrop={true}
|
||||
keyboard={true}
|
||||
enforceFocus={false}
|
||||
|
|
@ -68,7 +97,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
|
|||
centered
|
||||
data-cy={'modal-component'}
|
||||
>
|
||||
<BootstrapModal.Header className="border-bottom">
|
||||
<BootstrapModal.Header>
|
||||
<BootstrapModal.Title data-cy={`${title.toLowerCase().replace(/\s+/g, '-')}-title`}>
|
||||
{title}
|
||||
</BootstrapModal.Title>
|
||||
|
|
@ -82,27 +111,28 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
|
|||
{Array.isArray(versions) ? (
|
||||
<>
|
||||
<BootstrapModal.Body>
|
||||
<div className="py-2">
|
||||
<div className="current-version py-2" data-cy="current-version-section">
|
||||
<span className="text-muted" data-cy="current-version-label">
|
||||
<div>
|
||||
<div className="current-version " data-cy="current-version-section">
|
||||
<span data-cy="current-version-label" className="current-version-label">
|
||||
Current Version
|
||||
</span>
|
||||
<InputRadioField
|
||||
versionId={currentVersion.id}
|
||||
data-cy={`${currentVersion.id.toLowerCase().replace(/\s+/g, '-')}-value`}
|
||||
versionName={currentVersion.name}
|
||||
versionCreatedAt={currentVersion.createdAt}
|
||||
checked={versionId === currentVersion.id}
|
||||
versionId={currentVersion?.id}
|
||||
data-cy={`${currentVersion?.id.toLowerCase().replace(/\s+/g, '-')}-value`}
|
||||
versionName={currentVersion?.name}
|
||||
versionCreatedAt={currentVersion?.createdAt}
|
||||
checked={versionId === currentVersion?.id}
|
||||
setVersionId={setVersionId}
|
||||
className="current-version-wrap"
|
||||
/>
|
||||
</div>
|
||||
{versions.length >= 2 ? (
|
||||
<div className="other-versions py-2" data-cy="other-version-section">
|
||||
<span className="text-muted" data-cy="other-version-label">
|
||||
<div className="other-versions" data-cy="other-version-section">
|
||||
<span data-cy="other-version-label" className="other-version-label">
|
||||
Other Versions
|
||||
</span>
|
||||
{versions.map((version) => {
|
||||
if (version.id !== currentVersion.id) {
|
||||
if (version.id !== currentVersion?.id) {
|
||||
return (
|
||||
<InputRadioField
|
||||
versionId={version.id}
|
||||
|
|
@ -112,32 +142,39 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
|
|||
key={version.name}
|
||||
checked={versionId === version.id}
|
||||
setVersionId={setVersionId}
|
||||
className="other-version-wrap"
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="other-versions py-2" data-cy="other-version-section">
|
||||
<span className="text-muted" data-cy="no-other-versions-found-text">
|
||||
No other versions found
|
||||
</span>
|
||||
<div className="other-versions" data-cy="other-version-section">
|
||||
<span data-cy="no-other-versions-found-text">No other versions found</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</BootstrapModal.Body>
|
||||
<BootstrapModal.Footer className="export-app-modal-footer d-flex justify-content-end border-top align-items-center py-2">
|
||||
<span role="button" className="btn btn-light" data-cy="export-all-button" onClick={() => exportApp(app.id)}>
|
||||
<div className="tj-version-wrap-sub-footer">
|
||||
<input type="checkbox" checked={exportTjDb} onChange={() => setExportTjDb(!exportTjDb)} />
|
||||
<p>Export ToolJet table schema</p>
|
||||
</div>
|
||||
<BootstrapModal.Footer className="export-app-modal-footer d-flex justify-content-end align-items-center ">
|
||||
<ButtonSolid
|
||||
className="import-export-footer-btns"
|
||||
variant="tertiary"
|
||||
data-cy="export-all-button"
|
||||
onClick={() => exportApp(app, null, exportTjDb, tables)}
|
||||
>
|
||||
Export All
|
||||
</span>
|
||||
<span
|
||||
role="button"
|
||||
className="btn btn-primary"
|
||||
</ButtonSolid>
|
||||
<ButtonSolid
|
||||
className="import-export-footer-btns"
|
||||
data-cy="export-selected-version-button"
|
||||
onClick={() => exportApp(app.id, versionId)}
|
||||
onClick={() => exportApp(app, versionId, exportTjDb, tables)}
|
||||
>
|
||||
Export selected version
|
||||
</span>
|
||||
</ButtonSolid>
|
||||
</BootstrapModal.Footer>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -154,11 +191,12 @@ function InputRadioField({
|
|||
checked = undefined,
|
||||
key = undefined,
|
||||
setVersionId,
|
||||
className,
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
key={key}
|
||||
className="version-wrapper my-2 py-2 cursor-pointer"
|
||||
className={`version-wrapper cursor-pointer ${className}`}
|
||||
data-cy={`${String(versionName).toLowerCase().replace(/\s+/g, '-')}-version-wrapper`}
|
||||
>
|
||||
<input
|
||||
|
|
@ -178,9 +216,9 @@ function InputRadioField({
|
|||
style={{ paddingLeft: '0.75rem' }}
|
||||
>
|
||||
<span data-cy={`${String(versionName).toLowerCase().replace(/\s+/g, '-')}-text`}>{versionName}</span>
|
||||
<span className="text-secondary" data-cy="created-date-label">{`Created on ${moment(versionCreatedAt).format(
|
||||
'Do MMM YYYY'
|
||||
)}`}</span>
|
||||
<span className="export-creation-date tj-text-sm" data-cy="created-date-label">{`Created on ${moment(
|
||||
versionCreatedAt
|
||||
).format('Do MMM YYYY')}`}</span>
|
||||
</label>
|
||||
</span>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import HomeHeader from './Header';
|
|||
import Modal from './Modal';
|
||||
import configs from './Configs/AppIcon.json';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { sample } from 'lodash';
|
||||
import { sample, isEmpty } from 'lodash';
|
||||
import ExportAppModal from './ExportAppModal';
|
||||
import Footer from './Footer';
|
||||
import { OrganizationList } from '@/_components/OrganizationManager/List';
|
||||
|
|
@ -141,11 +141,11 @@ class HomePageComponent extends React.Component {
|
|||
cloneApp = (app) => {
|
||||
this.setState({ isCloningApp: true });
|
||||
appService
|
||||
.cloneApp(app.id)
|
||||
.cloneResource({ app: [{ id: app.id }], organization_id: getWorkspaceId() })
|
||||
.then((data) => {
|
||||
toast.success('App cloned successfully.');
|
||||
this.setState({ isCloningApp: false });
|
||||
this.props.navigate(`/${getWorkspaceId()}/apps/${data.id}`);
|
||||
this.props.navigate(`/${getWorkspaceId()}/apps/${data.imports.app[0].id}`);
|
||||
})
|
||||
.catch(({ _error }) => {
|
||||
toast.error('Could not clone the app.');
|
||||
|
|
@ -165,24 +165,35 @@ class HomePageComponent extends React.Component {
|
|||
const fileContent = event.target.result;
|
||||
this.setState({ isImportingApp: true });
|
||||
try {
|
||||
const requestBody = JSON.parse(fileContent);
|
||||
const organization_id = getWorkspaceId();
|
||||
let importJSON = JSON.parse(fileContent);
|
||||
// For backward compatibility with legacy app import
|
||||
const isLegacyImport = isEmpty(importJSON.tooljet_version);
|
||||
if (isLegacyImport) {
|
||||
importJSON = { app: [{ definition: importJSON }], tooljet_version: importJSON.tooljetVersion };
|
||||
}
|
||||
const requestBody = { organization_id, ...importJSON };
|
||||
appService
|
||||
.importApp(requestBody)
|
||||
.importResource(requestBody)
|
||||
.then((data) => {
|
||||
toast.success('App imported successfully.');
|
||||
toast.success('Imported successfully.');
|
||||
this.setState({
|
||||
isImportingApp: false,
|
||||
});
|
||||
this.props.navigate(`/${getWorkspaceId()}/apps/${data.id}`);
|
||||
if (!isEmpty(data.imports.app)) {
|
||||
this.props.navigate(`/${getWorkspaceId()}/apps/${data.imports.app[0].id}`);
|
||||
} else if (!isEmpty(data.imports.tooljet_database)) {
|
||||
this.props.navigate(`/${getWorkspaceId()}/database`);
|
||||
}
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error(`Could not import the app: ${error}`);
|
||||
toast.error(`Could not import: ${error}`);
|
||||
this.setState({
|
||||
isImportingApp: false,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error(`Could not import the app: ${error}`);
|
||||
toast.error(`Could not import: ${error}`);
|
||||
this.setState({
|
||||
isImportingApp: false,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const CreateColumnDrawer = ({ setIsCreateColumnDrawerOpen, isCreateColumnDrawerO
|
|||
<Drawer isOpen={isCreateColumnDrawerOpen} onClose={() => setIsCreateColumnDrawerOpen(false)} position="right">
|
||||
<CreateColumnForm
|
||||
onCreate={() => {
|
||||
tooljetDatabaseService.viewTable(organizationId, selectedTable).then(({ data = [], error }) => {
|
||||
tooljetDatabaseService.viewTable(organizationId, selectedTable.table_name).then(({ data = [], error }) => {
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Error fetching columns for table "${selectedTable}"`);
|
||||
return;
|
||||
|
|
@ -43,9 +43,9 @@ const CreateColumnDrawer = ({ setIsCreateColumnDrawerOpen, isCreateColumnDrawerO
|
|||
);
|
||||
}
|
||||
});
|
||||
tooljetDatabaseService.findOne(organizationId, selectedTable).then(({ data = [], error }) => {
|
||||
tooljetDatabaseService.findOne(organizationId, selectedTable.id).then(({ data = [], error }) => {
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Failed to fetch table "${selectedTable}"`);
|
||||
toast.error(error?.message ?? `Failed to fetch table "${selectedTable.table_name}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ const CreateRowDrawer = ({ isCreateRowDrawerOpen, setIsCreateRowDrawerOpen }) =>
|
|||
<Drawer isOpen={isCreateRowDrawerOpen} onClose={() => setIsCreateRowDrawerOpen(false)} position="right">
|
||||
<CreateRowForm
|
||||
onCreate={() => {
|
||||
tooljetDatabaseService.findOne(organizationId, selectedTable).then(({ headers, data = [], error }) => {
|
||||
tooljetDatabaseService.findOne(organizationId, selectedTable.id).then(({ headers, data = [], error }) => {
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Failed to fetch table "${selectedTable}"`);
|
||||
toast.error(error?.message ?? `Failed to fetch table "${selectedTable.table_name}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export default function CreateTableDrawer() {
|
|||
</div>
|
||||
<Drawer isOpen={isCreateTableDrawerOpen} onClose={() => setIsCreateTableDrawerOpen(false)} position="right">
|
||||
<CreateTableForm
|
||||
onCreate={(tableName) => {
|
||||
onCreate={(tableInfo) => {
|
||||
tooljetDatabaseService.findAll(organizationId).then(({ data = [], error }) => {
|
||||
if (error) {
|
||||
toast.error(error?.message ?? 'Failed to fetch tables');
|
||||
|
|
@ -35,9 +35,9 @@ export default function CreateTableDrawer() {
|
|||
}
|
||||
|
||||
if (Array.isArray(data?.result) && data.result.length > 0) {
|
||||
setSelectedTable({ table_name: tableInfo.table_name, id: tableInfo.id });
|
||||
updateSidebarNAV(tableInfo.table_name);
|
||||
setTables(data.result || []);
|
||||
setSelectedTable(tableName);
|
||||
updateSidebarNAV(tableName);
|
||||
}
|
||||
});
|
||||
setIsCreateTableDrawerOpen(false);
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ const EditRowDrawer = ({ isCreateRowDrawerOpen, setIsCreateRowDrawerOpen }) => {
|
|||
<Drawer isOpen={isCreateRowDrawerOpen} onClose={() => setIsCreateRowDrawerOpen(false)} position="right">
|
||||
<EditRowForm
|
||||
onEdit={() => {
|
||||
tooljetDatabaseService.findOne(organizationId, selectedTable).then(({ headers, data = [], error }) => {
|
||||
tooljetDatabaseService.findOne(organizationId, selectedTable.id).then(({ headers, data = [], error }) => {
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Failed to fetch table "${selectedTable}"`);
|
||||
toast.error(error?.message ?? `Failed to fetch table "${selectedTable.table_name}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
13
frontend/src/TooljetDatabase/ExportSchema/ExportSchema.jsx
Normal file
13
frontend/src/TooljetDatabase/ExportSchema/ExportSchema.jsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
|
||||
function ExportSchema({ onClick }) {
|
||||
return (
|
||||
<button className={`export-table-button tj-text-xsm font-weight-500 ghost-black-operation`} onClick={onClick}>
|
||||
<SolidIcon name="arrowsortrectangle" width="14" fill={'#889096'} />
|
||||
Export table
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExportSchema;
|
||||
|
|
@ -36,7 +36,7 @@ const ColumnForm = ({ onCreate, onClose }) => {
|
|||
|
||||
const { error } = await tooljetDatabaseService.createColumn(
|
||||
organizationId,
|
||||
selectedTable,
|
||||
selectedTable.table_name,
|
||||
columnName,
|
||||
dataType,
|
||||
defaultValue
|
||||
|
|
@ -45,7 +45,7 @@ const ColumnForm = ({ onCreate, onClose }) => {
|
|||
setFetching(false);
|
||||
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Failed to create a new column in "${selectedTable}" table`);
|
||||
toast.error(error?.message ?? `Failed to create a new column in "${selectedTable.table_name}" table`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,15 +63,15 @@ const ColumnsForm = ({ columns, setColumns }) => {
|
|||
className="form-control"
|
||||
placeholder="Enter name"
|
||||
data-cy={`name-input-field-${columns[index].column_name}`}
|
||||
disabled={columns[index].constraint === 'PRIMARY KEY'}
|
||||
disabled={columns[index].constraint_type === 'PRIMARY KEY'}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-3" data-cy="type-dropdown-field" style={{ marginRight: '16px' }}>
|
||||
<Select
|
||||
width="120px"
|
||||
isDisabled={columns[index].constraint === 'PRIMARY KEY'}
|
||||
isDisabled={columns[index].constraint_type === 'PRIMARY KEY'}
|
||||
useMenuPortal={false}
|
||||
options={columns[index].constraint === 'PRIMARY KEY' ? primaryKeydataTypes : dataTypes}
|
||||
options={columns[index].constraint_type === 'PRIMARY KEY' ? primaryKeydataTypes : dataTypes}
|
||||
value={columns[index].data_type}
|
||||
onChange={(value) => {
|
||||
const prevColumns = { ...columns };
|
||||
|
|
@ -85,18 +85,18 @@ const ColumnsForm = ({ columns, setColumns }) => {
|
|||
onChange={(e) => {
|
||||
e.persist();
|
||||
const prevColumns = { ...columns };
|
||||
prevColumns[index].default = e.target.value;
|
||||
prevColumns[index].column_default = e.target.value;
|
||||
setColumns(prevColumns);
|
||||
}}
|
||||
value={columns[index].default}
|
||||
value={columns[index].column_default}
|
||||
type="text"
|
||||
className="form-control"
|
||||
data-cy="default-input-field"
|
||||
placeholder="NULL"
|
||||
disabled={columns[index].constraint === 'PRIMARY KEY' || columns[index].data_type === 'serial'}
|
||||
disabled={columns[index].constraint_type === 'PRIMARY KEY' || columns[index].data_type === 'serial'}
|
||||
/>
|
||||
</div>
|
||||
{columns[index].constraint === 'PRIMARY KEY' && (
|
||||
{columns[index].constraint_type === 'PRIMARY KEY' && (
|
||||
<div className="col-2">
|
||||
<span
|
||||
className={`badge badge-outline ${darkMode ? 'text-white' : 'text-indigo'}`}
|
||||
|
|
@ -107,7 +107,7 @@ const ColumnsForm = ({ columns, setColumns }) => {
|
|||
</div>
|
||||
)}
|
||||
<div className="col-1 cursor-pointer" data-cy="column-delete-icon" onClick={() => handleDelete(index)}>
|
||||
{columns[index].constraint !== 'PRIMARY KEY' && <DeleteIcon />}
|
||||
{columns[index].constraint_type !== 'PRIMARY KEY' && <DeleteIcon />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -49,10 +49,10 @@ const EditRowForm = ({ onEdit, onClose }) => {
|
|||
const handleSubmit = async () => {
|
||||
setFetching(true);
|
||||
const query = `id=eq.${selectedRow}&order=id`;
|
||||
const { error } = await tooljetDatabaseService.updateRows(organizationId, selectedTable, rowData, query);
|
||||
const { error } = await tooljetDatabaseService.updateRows(organizationId, selectedTable.id, rowData, query);
|
||||
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Failed to create a new column table "${selectedTable}"`);
|
||||
toast.error(error?.message ?? `Failed to create a new column table "${selectedTable.table_name}"`);
|
||||
return;
|
||||
}
|
||||
setFetching(false);
|
||||
|
|
|
|||
|
|
@ -70,11 +70,11 @@ export const FilterForm = ({ filters, setFilters, index, column = '', operator =
|
|||
customWrap={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-4 tj-app-input">
|
||||
<div className="col-4">
|
||||
<input
|
||||
value={filterInputValue}
|
||||
type="text"
|
||||
className="form-control"
|
||||
className="form-control css-zz6spl-container"
|
||||
data-cy="value-input-field"
|
||||
placeholder="Value"
|
||||
onChange={(event) => {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const RowForm = ({ onCreate, onClose }) => {
|
|||
|
||||
const handleSubmit = async () => {
|
||||
setFetching(true);
|
||||
const { error } = await tooljetDatabaseService.createRow(organizationId, selectedTable, data);
|
||||
const { error } = await tooljetDatabaseService.createRow(organizationId, selectedTable.id, data);
|
||||
setFetching(false);
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Failed to create a new column table "${selectedTable}"`);
|
||||
|
|
|
|||
|
|
@ -8,15 +8,15 @@ import { isEmpty } from 'lodash';
|
|||
import { BreadCrumbContext } from '@/App/App';
|
||||
|
||||
const TableForm = ({
|
||||
selectedTable = '',
|
||||
selectedColumns = { 0: { column_name: 'id', data_type: 'serial', constraint: 'PRIMARY KEY' } },
|
||||
selectedTable = {},
|
||||
selectedColumns = { 0: { column_name: 'id', data_type: 'serial', constraint_type: 'PRIMARY KEY' } },
|
||||
onCreate,
|
||||
onEdit,
|
||||
onClose,
|
||||
updateSelectedTable,
|
||||
}) => {
|
||||
const [fetching, setFetching] = useState(false);
|
||||
const [tableName, setTableName] = useState(selectedTable);
|
||||
const [tableName, setTableName] = useState(selectedTable.table_name);
|
||||
const [columns, setColumns] = useState(selectedColumns);
|
||||
const { organizationId } = useContext(TooljetDatabaseContext);
|
||||
const isEditMode = !isEmpty(selectedTable);
|
||||
|
|
@ -56,7 +56,7 @@ const TableForm = ({
|
|||
}
|
||||
|
||||
setFetching(true);
|
||||
const { error } = await tooljetDatabaseService.createTable(organizationId, tableName, Object.values(columns));
|
||||
const { error, data } = await tooljetDatabaseService.createTable(organizationId, tableName, Object.values(columns));
|
||||
setFetching(false);
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Failed to create a new table "${tableName}"`);
|
||||
|
|
@ -64,14 +64,14 @@ const TableForm = ({
|
|||
}
|
||||
|
||||
toast.success(`${tableName} created successfully`);
|
||||
onCreate && onCreate(tableName);
|
||||
onCreate && onCreate({ id: data.result.id, table_name: tableName });
|
||||
};
|
||||
|
||||
const handleEdit = async () => {
|
||||
if (!validateTableName()) return;
|
||||
|
||||
setFetching(true);
|
||||
const { error } = await tooljetDatabaseService.renameTable(organizationId, selectedTable, tableName);
|
||||
const { error } = await tooljetDatabaseService.renameTable(organizationId, selectedTable.table_name, tableName);
|
||||
setFetching(false);
|
||||
|
||||
if (error) {
|
||||
|
|
@ -81,7 +81,7 @@ const TableForm = ({
|
|||
|
||||
toast.success(`${tableName} edited successfully`);
|
||||
updateSidebarNAV(tableName);
|
||||
updateSelectedTable(tableName);
|
||||
updateSelectedTable({ ...selectedTable, table_name: tableName });
|
||||
|
||||
onEdit && onEdit();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect, useState, useContext } from 'react';
|
||||
import React, { useEffect, useState, useContext, useRef } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { useTable, useRowSelect } from 'react-table';
|
||||
import { isBoolean } from 'lodash';
|
||||
import { isBoolean, isEmpty } from 'lodash';
|
||||
import { tooljetDatabaseService } from '@/_services';
|
||||
import { TooljetDatabaseContext } from '../index';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
|
@ -29,26 +29,31 @@ const Table = ({ openCreateRowDrawer, openCreateColumnDrawer }) => {
|
|||
const [isEditColumnDrawerOpen, setIsEditColumnDrawerOpen] = useState(false);
|
||||
const [selectedColumn, setSelectedColumn] = useState();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const prevSelectedTableRef = useRef({});
|
||||
|
||||
const fetchTableMetadata = () => {
|
||||
tooljetDatabaseService.viewTable(organizationId, selectedTable).then(({ data = [], error }) => {
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Error fetching metadata for table "${selectedTable}"`);
|
||||
return;
|
||||
}
|
||||
if (!isEmpty(selectedTable)) {
|
||||
tooljetDatabaseService.viewTable(organizationId, selectedTable.table_name).then(({ data = [], error }) => {
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Error fetching metadata for table "${selectedTable.table_name}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.result?.length > 0) {
|
||||
setColumns(
|
||||
data?.result.map(({ column_name, data_type, keytype, ...rest }) => ({
|
||||
Header: column_name,
|
||||
accessor: column_name,
|
||||
dataType: data_type,
|
||||
isPrimaryKey: keytype?.toLowerCase() === 'primary key',
|
||||
...rest,
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
if (data?.result?.length > 0) {
|
||||
setColumns(
|
||||
data?.result.map(({ column_name, data_type, keytype, ...rest }) => ({
|
||||
Header: column_name,
|
||||
accessor: column_name,
|
||||
dataType: data_type,
|
||||
isPrimaryKey: keytype?.toLowerCase() === 'primary key',
|
||||
...rest,
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setColumns([]);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTableData = (queryParams = '', pagesize = 50, pagecount = 1) => {
|
||||
|
|
@ -56,10 +61,10 @@ const Table = ({ openCreateRowDrawer, openCreateColumnDrawer }) => {
|
|||
let params = queryParams ? queryParams : defaultQueryParams;
|
||||
setLoading(true);
|
||||
|
||||
tooljetDatabaseService.findOne(organizationId, selectedTable, params).then(({ headers, data = [], error }) => {
|
||||
tooljetDatabaseService.findOne(organizationId, selectedTable.id, params).then(({ headers, data = [], error }) => {
|
||||
setLoading(false);
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Error fetching table "${selectedTable}" data`);
|
||||
toast.error(error?.message ?? `Error fetching table "${selectedTable.table_name}" data`);
|
||||
return;
|
||||
}
|
||||
const totalContentRangeRecords = headers['content-range'].split('/')[1] || 0;
|
||||
|
|
@ -76,9 +81,10 @@ const Table = ({ openCreateRowDrawer, openCreateColumnDrawer }) => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTable) {
|
||||
if (prevSelectedTableRef.current.id !== selectedTable.id && !isEmpty(selectedTable)) {
|
||||
onSelectedTableChange();
|
||||
}
|
||||
prevSelectedTableRef.current = selectedTable;
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedTable]);
|
||||
|
|
@ -163,14 +169,14 @@ const Table = ({ openCreateRowDrawer, openCreateColumnDrawer }) => {
|
|||
|
||||
let query = `?${primaryKey.accessor}=in.(${deletionKeys.toString()})`;
|
||||
|
||||
const { error } = await tooljetDatabaseService.deleteRow(organizationId, selectedTable, query);
|
||||
const { error } = await tooljetDatabaseService.deleteRows(organizationId, selectedTable.id, query);
|
||||
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Error deleting rows from table "${selectedTable}"`);
|
||||
toast.error(error?.message ?? `Error deleting rows from table "${selectedTable.table_name}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(`Deleted ${selectedRows.length} rows from table "${selectedTable}"`);
|
||||
toast.success(`Deleted ${selectedRows.length} rows from table "${selectedTable.table_name}"`);
|
||||
fetchTableData();
|
||||
}
|
||||
};
|
||||
|
|
@ -178,13 +184,13 @@ const Table = ({ openCreateRowDrawer, openCreateColumnDrawer }) => {
|
|||
const handleDeleteColumn = async (columnName) => {
|
||||
const shouldDelete = confirm(`Are you sure you want to delete the column "${columnName}"?`);
|
||||
if (shouldDelete) {
|
||||
const { error } = await tooljetDatabaseService.deleteColumn(organizationId, selectedTable, columnName);
|
||||
const { error } = await tooljetDatabaseService.deleteColumn(organizationId, selectedTable.table_name, columnName);
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Error deleting column "${columnName}" from table "${selectedTable}"`);
|
||||
return;
|
||||
}
|
||||
await fetchTableMetadata();
|
||||
toast.success(`Deleted ${columnName} from table "${selectedTable}"`);
|
||||
toast.success(`Deleted ${columnName} from table "${selectedTable.table_name}"`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -27,10 +27,14 @@ const List = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(data?.result)) {
|
||||
if (!isEmpty(data?.result)) {
|
||||
setTables(data.result || []);
|
||||
setSelectedTable(data?.result[0]?.table_name);
|
||||
updateSidebarNAV(data?.result[0]?.table_name);
|
||||
setSelectedTable({ table_name: data.result[0].table_name, id: data.result[0].id });
|
||||
updateSidebarNAV(data.result[0].table_name);
|
||||
} else {
|
||||
setTables([]);
|
||||
setSelectedTable({});
|
||||
updateSidebarNAV(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -39,6 +43,12 @@ const List = () => {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const renamedTableList = tables.map((table) => (table.id === selectedTable.id ? selectedTable : table));
|
||||
setTables(renamedTableList);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedTable]);
|
||||
|
||||
let filteredTables = [...tables];
|
||||
|
||||
if (!isEmpty(searchParam)) {
|
||||
|
|
@ -78,14 +88,14 @@ const List = () => {
|
|||
<div className="list-group mb-3">
|
||||
{loading && <Skeleton count={3} height={22} />}
|
||||
{!loading &&
|
||||
filteredTables?.map(({ table_name }, index) => (
|
||||
filteredTables?.map(({ id, table_name }, index) => (
|
||||
<ListItem
|
||||
key={index}
|
||||
active={table_name === selectedTable}
|
||||
active={id === selectedTable.id}
|
||||
text={table_name}
|
||||
onDeleteCallback={fetchTables}
|
||||
onClick={() => {
|
||||
setSelectedTable(table_name);
|
||||
setSelectedTable({ table_name, id });
|
||||
updateSidebarNAV(table_name);
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ export const ListItem = ({ active, onClick, text = '', onDeleteCallback }) => {
|
|||
const [isEditTableDrawerOpen, setIsEditTableDrawerOpen] = useState(false);
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
|
||||
function updateSelectedTable(tablename) {
|
||||
setSelectedTable(tablename);
|
||||
function updateSelectedTable(tableObj) {
|
||||
setSelectedTable(tableObj);
|
||||
}
|
||||
|
||||
const handleDeleteTable = async () => {
|
||||
|
|
@ -70,14 +70,7 @@ export const ListItem = ({ active, onClick, text = '', onDeleteCallback }) => {
|
|||
selectedColumns={formColumns}
|
||||
selectedTable={selectedTable}
|
||||
updateSelectedTable={updateSelectedTable}
|
||||
onEdit={() => {
|
||||
tooljetDatabaseService.findAll(organizationId).then(({ data = [] }) => {
|
||||
if (Array.isArray(data?.result) && data.result.length > 0) {
|
||||
setTables(data.result || []);
|
||||
}
|
||||
});
|
||||
setIsEditTableDrawerOpen(false);
|
||||
}}
|
||||
onEdit={() => setIsEditTableDrawerOpen(false)}
|
||||
onClose={() => setIsEditTableDrawerOpen(false)}
|
||||
/>
|
||||
</Drawer>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ import Sort from '../Sort';
|
|||
import Sidebar from '../Sidebar';
|
||||
import { TooljetDatabaseContext } from '../index';
|
||||
import EmptyFoldersIllustration from '@assets/images/icons/no-queries-added.svg';
|
||||
import ExportSchema from '../ExportSchema/ExportSchema';
|
||||
import { appService } from '@/_services/app.service';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
const TooljetDatabasePage = ({ totalTables }) => {
|
||||
const {
|
||||
|
|
@ -22,6 +26,7 @@ const TooljetDatabasePage = ({ totalTables }) => {
|
|||
setQueryFilters,
|
||||
sortFilters,
|
||||
setSortFilters,
|
||||
organizationId,
|
||||
} = useContext(TooljetDatabaseContext);
|
||||
|
||||
const [isCreateRowDrawerOpen, setIsCreateRowDrawerOpen] = useState(false);
|
||||
|
|
@ -51,19 +56,45 @@ const TooljetDatabasePage = ({ totalTables }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const exportTable = () => {
|
||||
appService
|
||||
.exportResource({
|
||||
tooljet_database: [{ table_id: selectedTable.id }],
|
||||
organization_id: organizationId,
|
||||
})
|
||||
.then((data) => {
|
||||
const tableName = selectedTable.table_name.replace(/\s+/g, '-').toLowerCase();
|
||||
const fileName = `${tableName}-export-${new Date().getTime()}`;
|
||||
// simulate link click download
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const href = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = href;
|
||||
link.download = fileName + '.json';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Could not export table.', {
|
||||
position: 'top-center',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row gx-0">
|
||||
<Sidebar />
|
||||
<div className={cx('col animation-fade database-page-content-wrap')}>
|
||||
{totalTables === 0 && <EmptyState />}
|
||||
|
||||
{selectedTable && (
|
||||
{!isEmpty(selectedTable) && (
|
||||
<>
|
||||
<div className="database-table-header-wrapper">
|
||||
<div className="card border-0">
|
||||
<div className="card-body tj-db-operaions-header">
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<div className="col d-flex">
|
||||
<CreateColumnDrawer
|
||||
isCreateColumnDrawerOpen={isCreateColumnDrawerOpen}
|
||||
setIsCreateColumnDrawerOpen={setIsCreateColumnDrawerOpen}
|
||||
|
|
@ -82,6 +113,7 @@ const TooljetDatabasePage = ({ totalTables }) => {
|
|||
handleBuildSortQuery={handleBuildSortQuery}
|
||||
resetSortQuery={resetSortQuery}
|
||||
/>
|
||||
<ExportSchema onClick={exportTable} />
|
||||
<CreateRowDrawer
|
||||
isCreateRowDrawerOpen={isCreateRowDrawerOpen}
|
||||
setIsCreateRowDrawerOpen={setIsCreateRowDrawerOpen}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export const TooljetDatabase = (props) => {
|
|||
const [columns, setColumns] = useState([]);
|
||||
const [tables, setTables] = useState([]);
|
||||
const [searchParam, setSearchParam] = useState('');
|
||||
const [selectedTable, setSelectedTable] = useState('');
|
||||
const [selectedTable, setSelectedTable] = useState({});
|
||||
const [selectedTableData, setSelectedTableData] = useState([]);
|
||||
|
||||
const [totalRecords, setTotalRecords] = useState(0);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const usePostgrestQueryBuilder = ({ organizationId, selectedTable, setSel
|
|||
'&' +
|
||||
postgrestQueryBuilder.current.paginationQuery.url.toString();
|
||||
|
||||
const { headers, data, error } = await tooljetDatabaseService.findOne(organizationId, selectedTable, query);
|
||||
const { headers, data, error } = await tooljetDatabaseService.findOne(organizationId, selectedTable.id, query);
|
||||
|
||||
if (error) {
|
||||
toast.error(error?.message ?? 'Something went wrong');
|
||||
|
|
@ -83,6 +83,7 @@ export const usePostgrestQueryBuilder = ({ organizationId, selectedTable, setSel
|
|||
};
|
||||
|
||||
const resetAll = () => {
|
||||
console.log('resetAll');
|
||||
postgrestQueryBuilder.current.sortQuery = new PostgrestQueryBuilder();
|
||||
|
||||
postgrestQueryBuilder.current.paginationQuery.limit(50);
|
||||
|
|
|
|||
|
|
@ -828,12 +828,7 @@ export function previewQuery(_ref, query, calledFromQuery = false, parameters =
|
|||
hasParamSupport
|
||||
);
|
||||
} else if (query.kind === 'tooljetdb') {
|
||||
const currentSessionValue = authenticationService.currentSessionValue;
|
||||
queryExecutionPromise = tooljetDbOperations.perform(
|
||||
query.options,
|
||||
currentSessionValue?.current_organization_id,
|
||||
getCurrentState()
|
||||
);
|
||||
queryExecutionPromise = tooljetDbOperations.perform(query, getCurrentState());
|
||||
} else if (query.kind === 'runpy') {
|
||||
queryExecutionPromise = executeRunPycode(_ref, query.options.code, query, true, 'edit');
|
||||
} else {
|
||||
|
|
@ -961,12 +956,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode =
|
|||
} else if (query.kind === 'runpy') {
|
||||
queryExecutionPromise = executeRunPycode(_self, query.options.code, query, false, mode);
|
||||
} else if (query.kind === 'tooljetdb') {
|
||||
const currentSessionValue = authenticationService.currentSessionValue;
|
||||
queryExecutionPromise = tooljetDbOperations.perform(
|
||||
query.options,
|
||||
currentSessionValue?.current_organization_id,
|
||||
getCurrentState()
|
||||
);
|
||||
queryExecutionPromise = tooljetDbOperations.perform(query, getCurrentState());
|
||||
} else {
|
||||
queryExecutionPromise = dataqueryService.run(queryId, options, query?.options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ export const appService = {
|
|||
cloneApp,
|
||||
exportApp,
|
||||
importApp,
|
||||
exportResource,
|
||||
importResource,
|
||||
cloneResource,
|
||||
changeIcon,
|
||||
deleteApp,
|
||||
getApp,
|
||||
|
|
@ -22,6 +25,7 @@ export const appService = {
|
|||
setPasswordFromToken,
|
||||
acceptInvite,
|
||||
getVersions,
|
||||
getTables,
|
||||
};
|
||||
|
||||
function getConfig() {
|
||||
|
|
@ -56,6 +60,38 @@ function exportApp(id, versionId) {
|
|||
);
|
||||
}
|
||||
|
||||
function exportResource(body) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
body: JSON.stringify(body),
|
||||
credentials: 'include',
|
||||
};
|
||||
|
||||
return fetch(`${config.apiUrl}/v2/resources/export`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function importResource(body) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body),
|
||||
};
|
||||
return fetch(`${config.apiUrl}/v2/resources/import`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function cloneResource(body) {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: authHeader(),
|
||||
body: JSON.stringify(body),
|
||||
credentials: 'include',
|
||||
};
|
||||
|
||||
return fetch(`${config.apiUrl}/v2/resources/clone`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getVersions(id) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/${id}/versions`, requestOptions).then(handleResponse);
|
||||
|
|
@ -66,6 +102,11 @@ function importApp(body) {
|
|||
return fetch(`${config.apiUrl}/apps/import`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getTables(id) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/apps/${id}/tables`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function changeIcon(icon, appId) {
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ import HttpClient from '@/_helpers/http-client';
|
|||
|
||||
const tooljetAdapter = new HttpClient();
|
||||
|
||||
function findOne(organizationId, tableName, query = '') {
|
||||
return tooljetAdapter.get(`/tooljet_db/organizations/${organizationId}/proxy/\${${tableName}}?${query}`);
|
||||
function findOne(headers, tableId, query = '') {
|
||||
tooljetAdapter.headers = { ...tooljetAdapter.headers, ...headers };
|
||||
return tooljetAdapter.get(`/tooljet_db/proxy/${tableId}?${query}`, headers);
|
||||
}
|
||||
|
||||
function findAll(organizationId) {
|
||||
|
|
@ -21,16 +22,16 @@ function viewTable(organizationId, tableName) {
|
|||
return tooljetAdapter.get(`/tooljet_db/organizations/${organizationId}/table/${tableName}`);
|
||||
}
|
||||
|
||||
function createRow(organizationId, tableName, data) {
|
||||
return tooljetAdapter.post(`/tooljet_db/organizations/${organizationId}/proxy/\${${tableName}}`, data);
|
||||
function createRow(headers, tableId, data) {
|
||||
return tooljetAdapter.post(`/tooljet_db/proxy/${tableId}`, data, headers);
|
||||
}
|
||||
|
||||
function createColumn(organizationId, tableName, columnName, dataType, defaultValue) {
|
||||
return tooljetAdapter.post(`/tooljet_db/organizations/${organizationId}/table/${tableName}/column`, {
|
||||
function createColumn(organizationId, tableId, columnName, dataType, defaultValue) {
|
||||
return tooljetAdapter.post(`/tooljet_db/organizations/${organizationId}/table/${tableId}/column`, {
|
||||
column: {
|
||||
column_name: columnName,
|
||||
data_type: dataType,
|
||||
default: defaultValue,
|
||||
column_default: defaultValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -51,12 +52,12 @@ function renameTable(organizationId, tableName, newTableName) {
|
|||
});
|
||||
}
|
||||
|
||||
function updateRows(organizationId, tableName, data, query = '') {
|
||||
return tooljetAdapter.patch(`/tooljet_db/organizations/${organizationId}/proxy/\${${tableName}}?${query}`, data);
|
||||
function updateRows(headers, tableId, data, query = '') {
|
||||
return tooljetAdapter.patch(`/tooljet_db/proxy/${tableId}?${query}`, data, headers);
|
||||
}
|
||||
|
||||
function deleteRow(organizationId, tableName, query = '') {
|
||||
return tooljetAdapter.delete(`/tooljet_db/organizations/${organizationId}/proxy/\${${tableName}}?${query}`);
|
||||
function deleteRows(headers, tableId, query = '') {
|
||||
return tooljetAdapter.delete(`/tooljet_db/proxy/${tableId}?${query}`, headers);
|
||||
}
|
||||
|
||||
function deleteColumn(organizationId, tableName, columnName) {
|
||||
|
|
@ -76,7 +77,7 @@ export const tooljetDatabaseService = {
|
|||
createColumn,
|
||||
updateTable,
|
||||
updateRows,
|
||||
deleteRow,
|
||||
deleteRows,
|
||||
deleteColumn,
|
||||
deleteTable,
|
||||
renameTable,
|
||||
|
|
|
|||
|
|
@ -1307,15 +1307,17 @@ button {
|
|||
}
|
||||
|
||||
.fx-button {
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
color: #61656c;
|
||||
color: #3E63DD;
|
||||
font-family: 'IBM Plex Mono';
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.fx-button:hover,
|
||||
.fx-button.active {
|
||||
font-weight: 600;
|
||||
color: $primary-light;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
@ -1729,7 +1731,6 @@ button {
|
|||
.select-search-dark__input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
// padding: 0.4375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.4285714;
|
||||
|
|
@ -4536,10 +4537,8 @@ input[type="text"] {
|
|||
|
||||
.canvas-background-holder {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-width: 120px;
|
||||
margin: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.canvas-background-picker {
|
||||
|
|
@ -4792,8 +4791,30 @@ input[type="text"] {
|
|||
background-color: $bg-dark-light !important;
|
||||
color: $white !important;
|
||||
|
||||
.modal-title {
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
.tj-version-wrap-sub-footer {
|
||||
background-color: $bg-dark-light !important;
|
||||
border-top: 1px solid #3A3F42 !important;
|
||||
|
||||
|
||||
p {
|
||||
color: $white !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.current-version-wrap,
|
||||
.other-version-wrap {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background-color: $bg-dark-light !important;
|
||||
background-color: $bg-dark-light !important;
|
||||
color: $white !important;
|
||||
border-bottom: 2px solid #3A3F42 !important;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
|
|
@ -5225,22 +5246,23 @@ div#driver-page-overlay {
|
|||
.canvas-codehinter-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 158px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.hinter-canvas-input {
|
||||
display: flex;
|
||||
width: 120px;
|
||||
height: 32px;
|
||||
margin-top: 1px;
|
||||
|
||||
.canvas-hinter-wrap {
|
||||
width: 135px;
|
||||
height: 42px !important;
|
||||
width: 120px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.hinter-canvas-input {
|
||||
width: 180px !important;
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
height: 41.2px !important;
|
||||
margin-top: 1px;
|
||||
|
||||
.CodeMirror-sizer {
|
||||
border-right-width: 1px !important;
|
||||
}
|
||||
|
|
@ -5253,35 +5275,37 @@ div#driver-page-overlay {
|
|||
.canvas-codehinter-container {
|
||||
.code-hinter-col {
|
||||
margin-bottom: 1px !important;
|
||||
width: 136px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.fx-canvas {
|
||||
background: #1c252f;
|
||||
padding: 2px;
|
||||
display: flex;
|
||||
height: 41px;
|
||||
border: solid 1px rgba(255, 255, 255, 0.09) !important;
|
||||
border-radius: 4px;
|
||||
justify-content: center;
|
||||
font-weight: 400;
|
||||
align-items: center;
|
||||
|
||||
div {
|
||||
background: #1c252f !important;
|
||||
width: 35px !important;
|
||||
background: #121212 !important;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
width: 39px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.code-hinter-wrapper {
|
||||
width: 120px !important;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.fx-canvas-light {
|
||||
background: #f4f6fa !important;
|
||||
border: 1px solid #dadcde !important;
|
||||
|
||||
div {
|
||||
background: #f4f6fa !important;
|
||||
background: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6920,7 +6944,31 @@ tbody {
|
|||
}
|
||||
}
|
||||
|
||||
.import-export-footer-btns {
|
||||
margin: 0px !important;
|
||||
}
|
||||
|
||||
.home-version-modal-component {
|
||||
border-bottom-right-radius: 0px !important;
|
||||
border-bottom-left-radius: 0px !important;
|
||||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08),
|
||||
0px 4px 6px -2px rgba(16, 24, 40, 0.03) !important;
|
||||
}
|
||||
|
||||
.current-version-label,
|
||||
.other-version-label {
|
||||
color: var(--slate11);
|
||||
}
|
||||
|
||||
.home-modal-component.modal-version-lists {
|
||||
width: 466px;
|
||||
height: 668px;
|
||||
background: var(--base);
|
||||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
border-top-right-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
|
||||
|
||||
.modal-header {
|
||||
.btn-close {
|
||||
top: auto;
|
||||
|
|
@ -6936,16 +6984,66 @@ tbody {
|
|||
overflow: auto;
|
||||
}
|
||||
|
||||
.export-creation-date {
|
||||
color: var(--slate11);
|
||||
}
|
||||
|
||||
.modal-footer,
|
||||
.modal-header {
|
||||
height: 10%;
|
||||
padding-bottom: 24px;
|
||||
padding: 12px 28px;
|
||||
gap: 10px;
|
||||
width: 466px;
|
||||
height: 56px;
|
||||
background-color: var(--base);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 24px 32px;
|
||||
gap: 8px;
|
||||
width: 466px;
|
||||
height: 88px;
|
||||
}
|
||||
|
||||
.tj-version-wrap-sub-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 16px 28px;
|
||||
gap: 10px;
|
||||
height: 52px;
|
||||
background: var(--base);
|
||||
border-top: 1px solid var(--slate5);
|
||||
border-bottom: 1px solid var(--slate5);
|
||||
|
||||
|
||||
|
||||
p {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--slate12);
|
||||
}
|
||||
}
|
||||
|
||||
.version-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
padding: 0.75rem 0.25rem;
|
||||
border: 1px solid var(--slate7);
|
||||
}
|
||||
|
||||
.current-version-wrap,
|
||||
.other-version-wrap {
|
||||
|
||||
span:first-child {
|
||||
color: var(--slate12) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.current-version-wrap {
|
||||
background: var(--indigo3) !important;
|
||||
margin-bottom: 24px;
|
||||
border-radius: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -7733,7 +7831,14 @@ tbody {
|
|||
}
|
||||
|
||||
.maximum-canvas-height-input-field {
|
||||
width: 90px;
|
||||
width: 156px;
|
||||
height: 32px;
|
||||
padding: 6px 10px;
|
||||
gap: 17px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #D7DBDF;
|
||||
border-radius: 6px;
|
||||
|
||||
}
|
||||
|
||||
.layout-header {
|
||||
|
|
@ -10619,6 +10724,166 @@ tbody {
|
|||
}
|
||||
}
|
||||
|
||||
#global-settings-popover {
|
||||
padding: 24px;
|
||||
gap: 20px;
|
||||
max-width: 377px !important;
|
||||
height: 316px !important;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E6E8EB;
|
||||
box-shadow: 0px 32px 64px -12px rgba(16, 24, 40, 0.14);
|
||||
border-radius: 6px;
|
||||
margin-top: -13px;
|
||||
|
||||
|
||||
.input-with-icon {
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.form-check-input {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.global-popover-div-wrap-width {
|
||||
width: 156px !important;
|
||||
}
|
||||
|
||||
.form-switch {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.global-popover-div-wrap {
|
||||
padding: 0px;
|
||||
gap: 75px;
|
||||
width: 329px;
|
||||
height: 32px;
|
||||
margin-bottom: 20px !important;
|
||||
justify-content: space-between;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.global-popover-text {
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: #11181C;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.maximum-canvas-width-input-select {
|
||||
padding: 6px 10px;
|
||||
gap: 17px;
|
||||
width: 60px;
|
||||
height: 32px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #D7DBDF;
|
||||
border-radius: 0px 6px 6px 0px;
|
||||
}
|
||||
|
||||
.maximum-canvas-width-input-field {
|
||||
padding: 6px 10px;
|
||||
gap: 17px;
|
||||
width: 97px;
|
||||
height: 32px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #D7DBDF;
|
||||
border-top-left-radius: 6px;
|
||||
border-bottom-left-radius: 6px;
|
||||
border-right: none !important;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.canvas-background-holder {
|
||||
padding: 6px 10px;
|
||||
gap: 6px;
|
||||
width: 120px;
|
||||
height: 32px;
|
||||
background: #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #D7DBDF;
|
||||
border-radius: 6px;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.export-app-btn {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 6px 16px;
|
||||
gap: 6px;
|
||||
width: 158px;
|
||||
height: 32px;
|
||||
font-family: 'IBM Plex Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #3E63DD;
|
||||
background: #F0F4FF;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.tj-btn-tertiary {
|
||||
padding: 10px 20px;
|
||||
gap: 8px;
|
||||
width: 112px;
|
||||
height: 40px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #D7DBDF;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid #C1C8CD;
|
||||
color: #687076;
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: 1px solid #11181C;
|
||||
color: #11181C;
|
||||
}
|
||||
}
|
||||
|
||||
.export-table-button {
|
||||
width: 135px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
#global-settings-popover.theme-dark {
|
||||
background-color: $bg-dark-light !important;
|
||||
border: 1px solid #2B2F31;
|
||||
|
||||
.global-popover-text {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.maximum-canvas-width-input-select {
|
||||
background-color: $bg-dark-light !important;
|
||||
border: 1px solid #324156;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.export-app-btn {
|
||||
background: #192140;
|
||||
}
|
||||
|
||||
.fx-canvas div {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.released-version-popup-container {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
|
|
@ -10926,10 +11191,6 @@ tbody {
|
|||
}
|
||||
}
|
||||
|
||||
.confirm-dialogue-modal {
|
||||
background: var(--base);
|
||||
}
|
||||
|
||||
.theme-dark {
|
||||
.icon-widget-popover {
|
||||
.search-box-wrapper input {
|
||||
|
|
@ -10960,20 +11221,8 @@ tbody {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.workspace-folder-modal {
|
||||
.tj-app-input {
|
||||
padding-bottom: 0px !important;
|
||||
}
|
||||
|
||||
.tj-input-error {
|
||||
height: 32px;
|
||||
color: #ED5F00;
|
||||
font-weight: 400;
|
||||
font-size: 10px;
|
||||
height: 0px;
|
||||
padding: 4px 0px 20px 0px;
|
||||
}
|
||||
.confirm-dialogue-modal {
|
||||
background: var(--base);
|
||||
}
|
||||
|
||||
.table-editor-component-row {
|
||||
|
|
@ -11230,4 +11479,4 @@ tbody {
|
|||
background-color: #F1F3F5;
|
||||
color: #C1C8CD;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"tooljet_database": [
|
||||
{
|
||||
"id": "a7426184-4d4d-4191-9f74-d668cc779ef9",
|
||||
"table_name": "new",
|
||||
"schema": {
|
||||
"columns": [
|
||||
{
|
||||
"column_name": "id",
|
||||
"data_type": "integer",
|
||||
"column_default": "nextval('\"a7426184-4d4d-4191-9f74-d668cc779ef9_id_seq\"'::regclass)",
|
||||
"character_maximum_length": null,
|
||||
"numeric_precision": 32,
|
||||
"is_nullable": "NO",
|
||||
"constraint_type": "PRIMARY KEY",
|
||||
"keytype": "PRIMARY KEY"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"tooljet_version": "2.4.9"
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { DataSource } from 'src/entities/data_source.entity';
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { InternalTable } from 'src/entities/internal_table.entity';
|
||||
import { Organization } from 'src/entities/organization.entity';
|
||||
import { DataQuery } from 'src/entities/data_query.entity';
|
||||
|
||||
export class ReplaceTooljetDbTableNamesWithId1679604241777 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
let progress = 0;
|
||||
const entityManager = queryRunner.manager;
|
||||
const organizations = await entityManager.find(Organization, { select: ['id'] });
|
||||
const orgCount = organizations.length;
|
||||
console.log(`Total Organizations: ${orgCount}`);
|
||||
|
||||
for (const organization of organizations) {
|
||||
console.log(
|
||||
`ReplaceTooljetDbTableNamesWithId1679604241777 Progress ${Math.round((progress / orgCount) * 100)} %`
|
||||
);
|
||||
console.log(`Replacing for organization ${organization.name}: ${organization.id}`);
|
||||
|
||||
const tjDbDataSources = await entityManager
|
||||
.createQueryBuilder(DataSource, 'data_sources')
|
||||
.select(['data_sources.id', 'data_sources.appVersionId', 'apps.id', 'apps.name'])
|
||||
.innerJoin('data_sources.appVersion', 'app_versions')
|
||||
.innerJoin('app_versions.app', 'apps', 'apps.organizationId = :organizationId', {
|
||||
organizationId: organization.id,
|
||||
})
|
||||
.where('data_sources.kind = :kind', { kind: 'tooljetdb' })
|
||||
.getRawMany();
|
||||
|
||||
const tjDbDataSourcesCount = tjDbDataSources.length;
|
||||
console.log(`TjDb datasources: ${tjDbDataSourcesCount}`);
|
||||
|
||||
for (const tjDbSource of tjDbDataSources) {
|
||||
console.log(`App ${tjDbSource.apps_name}: ${tjDbSource.apps_id}`);
|
||||
const dataQueriesToReplaceWithIds = await entityManager.find(DataQuery, {
|
||||
where: { dataSourceId: tjDbSource.data_sources_id },
|
||||
select: ['id', 'options'],
|
||||
});
|
||||
console.log(`TjDb dataqueries: ${dataQueriesToReplaceWithIds.length}`);
|
||||
|
||||
for (const dataQuery of dataQueriesToReplaceWithIds) {
|
||||
const options = dataQuery.options;
|
||||
const { table_name: tableName } = options;
|
||||
|
||||
const internalTable = await entityManager.findOne(InternalTable, {
|
||||
where: { organizationId: organization.id, tableName },
|
||||
select: ['id', 'tableName'],
|
||||
});
|
||||
|
||||
// There was a bug wherein if the table name had changed, the name in app definition
|
||||
// will not be changed. So there could be occurences where table with the name on
|
||||
// app definition won't be found. In such cases we don't make any change for that
|
||||
// query. User will have to explicitly link the table again for that query in the
|
||||
// editor after this migration is run.
|
||||
if (isEmpty(internalTable)) continue;
|
||||
|
||||
dataQuery.options = { ...options, table_id: internalTable.id };
|
||||
await dataQuery.save();
|
||||
}
|
||||
}
|
||||
|
||||
progress++;
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ import { AppEnvironmentsModule } from './modules/app_environments/app_environmen
|
|||
import { OrganizationConstantModule } from './modules/organization_constants/organization_constants.module';
|
||||
import { RequestContextModule } from './modules/request_context/request-context.module';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { ImportExportResourcesModule } from './modules/import_export_resources/import_export_resources.module';
|
||||
|
||||
const imports = [
|
||||
ScheduleModule.forRoot(),
|
||||
|
|
@ -97,6 +98,7 @@ const imports = [
|
|||
PluginsModule,
|
||||
EventsModule,
|
||||
AppEnvironmentsModule,
|
||||
ImportExportResourcesModule,
|
||||
CopilotModule,
|
||||
OrganizationConstantModule,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -355,4 +355,18 @@ export class AppsController {
|
|||
const appUser = await this.appsService.update(app.id, appUpdateDto);
|
||||
return decamelizeKeys(appUser);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseInterceptors(ValidAppInterceptor)
|
||||
@Get(':id/tables')
|
||||
async tables(@User() user, @AppDecorator() app: App) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(user, app.id);
|
||||
|
||||
if (!ability.can('cloneApp', app)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const result = await this.appsService.findTooljetDbTables(app.id);
|
||||
return { tables: result };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
61
server/src/controllers/import_export_resources.controller.ts
Normal file
61
server/src/controllers/import_export_resources.controller.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { Controller, Post, UseGuards, Body, ForbiddenException } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
|
||||
import { User } from 'src/decorators/user.decorator';
|
||||
import { ExportResourcesDto } from '@dto/export-resources.dto';
|
||||
import { ImportResourcesDto } from '@dto/import-resources.dto';
|
||||
import { ImportExportResourcesService } from '@services/import_export_resources.service';
|
||||
import { App } from 'src/entities/app.entity';
|
||||
import { AppsAbilityFactory } from 'src/modules/casl/abilities/apps-ability.factory';
|
||||
import { CloneResourcesDto } from '@dto/clone-resources.dto';
|
||||
|
||||
@Controller({
|
||||
path: 'resources',
|
||||
version: '2',
|
||||
})
|
||||
export class ImportExportResourcesController {
|
||||
constructor(
|
||||
private importExportResourcesService: ImportExportResourcesService,
|
||||
private appsAbilityFactory: AppsAbilityFactory
|
||||
) {}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('/export')
|
||||
async export(@User() user, @Body() exportResourcesDto: ExportResourcesDto) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(user);
|
||||
|
||||
if (!ability.can('createApp', App)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
const result = await this.importExportResourcesService.export(user, exportResourcesDto);
|
||||
return {
|
||||
...result,
|
||||
tooljet_version: globalThis.TOOLJET_VERSION,
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('/import')
|
||||
async import(@User() user, @Body() importResourcesDto: ImportResourcesDto) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(user);
|
||||
|
||||
if (!ability.can('cloneApp', App)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const imports = await this.importExportResourcesService.import(user, importResourcesDto);
|
||||
return { imports, success: true };
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('/clone')
|
||||
async clone(@User() user, @Body() cloneResourcesDto: CloneResourcesDto) {
|
||||
const ability = await this.appsAbilityFactory.appsActions(user);
|
||||
|
||||
if (!ability.can('cloneApp', App)) {
|
||||
throw new ForbiddenException('You do not have permissions to perform this action');
|
||||
}
|
||||
|
||||
const imports = await this.importExportResourcesService.clone(user, cloneResourcesDto);
|
||||
return { imports, success: true };
|
||||
}
|
||||
}
|
||||
|
|
@ -9,63 +9,64 @@ import { CheckPolicies } from 'src/modules/casl/check_policies.decorator';
|
|||
import { Action, TooljetDbAbility } from 'src/modules/casl/abilities/tooljet-db-ability.factory';
|
||||
import { TooljetDbGuard } from 'src/modules/casl/tooljet-db.guard';
|
||||
import { CreatePostgrestTableDto, RenamePostgrestTableDto, PostgrestTableColumnDto } from '@dto/tooljet-db.dto';
|
||||
import { OrganizationAuthGuard } from 'src/modules/auth/organization-auth.guard';
|
||||
|
||||
@Controller('tooljet_db/organizations')
|
||||
@UseGuards(JwtAuthGuard, ActiveWorkspaceGuard)
|
||||
@Controller('tooljet_db')
|
||||
export class TooljetDbController {
|
||||
constructor(
|
||||
private readonly tooljetDbService: TooljetDbService,
|
||||
private readonly postgrestProxyService: PostgrestProxyService
|
||||
) {}
|
||||
|
||||
@All('/:organizationId/proxy/*')
|
||||
@All('/proxy/*')
|
||||
@UseGuards(OrganizationAuthGuard, TooljetDbGuard)
|
||||
@CheckPolicies((ability: TooljetDbAbility) => ability.can(Action.ProxyPostgrest, 'all'))
|
||||
async proxy(@Req() req, @Res() res, @Next() next, @Param('organizationId') organizationId) {
|
||||
return this.postgrestProxyService.perform(req, res, next, organizationId);
|
||||
async proxy(@Req() req, @Res() res, @Next() next) {
|
||||
return this.postgrestProxyService.perform(req, res, next);
|
||||
}
|
||||
|
||||
@Get('/:organizationId/tables')
|
||||
@UseGuards(TooljetDbGuard)
|
||||
@Get('/organizations/:organizationId/tables')
|
||||
@UseGuards(JwtAuthGuard, ActiveWorkspaceGuard, TooljetDbGuard)
|
||||
@CheckPolicies((ability: TooljetDbAbility) => ability.can(Action.ViewTables, 'all'))
|
||||
async tables(@Param('organizationId') organizationId) {
|
||||
const result = await this.tooljetDbService.perform(organizationId, 'view_tables');
|
||||
return decamelizeKeys({ result });
|
||||
}
|
||||
|
||||
@Get('/:organizationId/table/:tableName')
|
||||
@UseGuards(TooljetDbGuard)
|
||||
@Get('/organizations/:organizationId/table/:tableName')
|
||||
@UseGuards(JwtAuthGuard, ActiveWorkspaceGuard, TooljetDbGuard)
|
||||
@CheckPolicies((ability: TooljetDbAbility) => ability.can(Action.ViewTable, 'all'))
|
||||
async table(@Body() body, @Param('organizationId') organizationId, @Param('tableName') tableName) {
|
||||
const result = await this.tooljetDbService.perform(organizationId, 'view_table', { table_name: tableName });
|
||||
return decamelizeKeys({ result });
|
||||
}
|
||||
|
||||
@Post('/:organizationId/table')
|
||||
@UseGuards(TooljetDbGuard)
|
||||
@Post('/organizations/:organizationId/table')
|
||||
@UseGuards(JwtAuthGuard, ActiveWorkspaceGuard, TooljetDbGuard)
|
||||
@CheckPolicies((ability: TooljetDbAbility) => ability.can(Action.CreateTable, 'all'))
|
||||
async createTable(@Body() createTableDto: CreatePostgrestTableDto, @Param('organizationId') organizationId) {
|
||||
const result = await this.tooljetDbService.perform(organizationId, 'create_table', createTableDto);
|
||||
return decamelizeKeys({ result });
|
||||
}
|
||||
|
||||
@Patch('/:organizationId/table/:tableName')
|
||||
@UseGuards(TooljetDbGuard)
|
||||
@Patch('/organizations/:organizationId/table/:tableName')
|
||||
@UseGuards(JwtAuthGuard, ActiveWorkspaceGuard, TooljetDbGuard)
|
||||
@CheckPolicies((ability: TooljetDbAbility) => ability.can(Action.RenameTable, 'all'))
|
||||
async renameTable(@Body() renameTableDto: RenamePostgrestTableDto, @Param('organizationId') organizationId) {
|
||||
const result = await this.tooljetDbService.perform(organizationId, 'rename_table', renameTableDto);
|
||||
return decamelizeKeys({ result });
|
||||
}
|
||||
|
||||
@Delete('/:organizationId/table/:tableName')
|
||||
@UseGuards(TooljetDbGuard)
|
||||
@Delete('/organizations/:organizationId/table/:tableName')
|
||||
@UseGuards(JwtAuthGuard, ActiveWorkspaceGuard, TooljetDbGuard)
|
||||
@CheckPolicies((ability: TooljetDbAbility) => ability.can(Action.DropTable, 'all'))
|
||||
async dropTable(@Param('organizationId') organizationId, @Param('tableName') tableName) {
|
||||
const result = await this.tooljetDbService.perform(organizationId, 'drop_table', { table_name: tableName });
|
||||
return decamelizeKeys({ result });
|
||||
}
|
||||
|
||||
@Post('/:organizationId/table/:tableName/column')
|
||||
@UseGuards(TooljetDbGuard)
|
||||
@Post('/organizations/:organizationId/table/:tableName/column')
|
||||
@UseGuards(JwtAuthGuard, ActiveWorkspaceGuard, TooljetDbGuard)
|
||||
@CheckPolicies((ability: TooljetDbAbility) => ability.can(Action.AddColumn, 'all'))
|
||||
async addColumn(
|
||||
@Body('column') columnDto: PostgrestTableColumnDto,
|
||||
|
|
@ -80,8 +81,8 @@ export class TooljetDbController {
|
|||
return decamelizeKeys({ result });
|
||||
}
|
||||
|
||||
@Delete('/:organizationId/table/:tableName/column/:columnName')
|
||||
@UseGuards(TooljetDbGuard)
|
||||
@Delete('/organizations/:organizationId/table/:tableName/column/:columnName')
|
||||
@UseGuards(JwtAuthGuard, ActiveWorkspaceGuard, TooljetDbGuard)
|
||||
@CheckPolicies((ability: TooljetDbAbility) => ability.can(Action.DropColumn, 'all'))
|
||||
async dropColumn(
|
||||
@Param('organizationId') organizationId,
|
||||
|
|
|
|||
22
server/src/dto/clone-resources.dto.ts
Normal file
22
server/src/dto/clone-resources.dto.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { IsUUID, IsOptional } from 'class-validator';
|
||||
|
||||
export class CloneResourcesDto {
|
||||
@IsOptional()
|
||||
app: CloneAppDto[];
|
||||
|
||||
@IsOptional()
|
||||
tooljet_database: CloneTooljetDatabaseDto[];
|
||||
|
||||
@IsUUID()
|
||||
organization_id: string;
|
||||
}
|
||||
|
||||
export class CloneAppDto {
|
||||
@IsUUID()
|
||||
id: string;
|
||||
}
|
||||
|
||||
export class CloneTooljetDatabaseDto {
|
||||
@IsUUID()
|
||||
id: string;
|
||||
}
|
||||
28
server/src/dto/export-resources.dto.ts
Normal file
28
server/src/dto/export-resources.dto.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { IsUUID, IsOptional } from 'class-validator';
|
||||
|
||||
export class ExportResourcesDto {
|
||||
@IsOptional()
|
||||
app: ExportAppDto[];
|
||||
|
||||
@IsOptional()
|
||||
tooljet_database: ExportTooljetDatabaseDto[];
|
||||
|
||||
@IsUUID()
|
||||
organization_id: string;
|
||||
}
|
||||
|
||||
export class ExportAppDto {
|
||||
@IsUUID()
|
||||
id: string;
|
||||
|
||||
@IsOptional()
|
||||
search_params: any;
|
||||
}
|
||||
|
||||
export class ExportTooljetDatabaseDto {
|
||||
@IsUUID()
|
||||
table_id: string;
|
||||
|
||||
// @IsOptional()
|
||||
// data: boolean;
|
||||
}
|
||||
34
server/src/dto/import-resources.dto.ts
Normal file
34
server/src/dto/import-resources.dto.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { IsUUID, IsOptional, IsString, IsDefined } from 'class-validator';
|
||||
|
||||
export class ImportResourcesDto {
|
||||
@IsUUID()
|
||||
organization_id: string;
|
||||
|
||||
@IsString()
|
||||
tooljet_version: string;
|
||||
|
||||
@IsOptional()
|
||||
app: ImportAppDto[];
|
||||
|
||||
@IsOptional()
|
||||
tooljet_database: ImportTooljetDatabaseDto[];
|
||||
}
|
||||
|
||||
export class ImportAppDto {
|
||||
@IsDefined()
|
||||
definition: any;
|
||||
}
|
||||
|
||||
export class ImportTooljetDatabaseDto {
|
||||
@IsUUID()
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
table_name: string;
|
||||
|
||||
@IsDefined()
|
||||
schema: any;
|
||||
|
||||
// @IsOptional()
|
||||
// data: boolean;
|
||||
}
|
||||
|
|
@ -140,7 +140,7 @@ export class PostgrestTableColumnDto {
|
|||
@Transform(({ value }) => sanitizeInput(value))
|
||||
@IsOptional()
|
||||
@Validate(SQLInjectionValidator)
|
||||
constraint: string;
|
||||
constraint_type: string;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value, obj }) => {
|
||||
|
|
@ -151,7 +151,7 @@ export class PostgrestTableColumnDto {
|
|||
message: 'Default value must match the data type',
|
||||
})
|
||||
@Validate(SQLInjectionValidator, { message: 'Default value does not support special characters except "." and "@"' })
|
||||
default: string | number | boolean;
|
||||
column_default: string | number | boolean;
|
||||
}
|
||||
|
||||
export class RenamePostgrestTableDto {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,9 @@ export class DataSource extends BaseEntity {
|
|||
@UpdateDateColumn({ default: () => 'now()', name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
@ManyToOne(() => AppVersion, (appVersion) => appVersion.id, { onDelete: 'CASCADE' })
|
||||
@ManyToOne(() => AppVersion, (appVersion) => appVersion.id, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'app_version_id' })
|
||||
appVersion: AppVersion;
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,10 @@ async function bootstrap() {
|
|||
app.use(json({ limit: '50mb' }));
|
||||
app.use(urlencoded({ extended: true, limit: '50mb', parameterLimit: 1000000 }));
|
||||
app.useStaticAssets(join(__dirname, 'assets'), { prefix: (UrlPrefix ? UrlPrefix : '/') + 'assets' });
|
||||
app.enableVersioning({
|
||||
type: VersioningType.URI,
|
||||
defaultVersion: VERSION_NEUTRAL,
|
||||
});
|
||||
|
||||
app.enableVersioning({
|
||||
type: VersioningType.URI,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { User } from 'src/entities/user.entity';
|
|||
import { AbilityBuilder, Ability, AbilityClass, ExtractSubjectType } from '@casl/ability';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UsersService } from 'src/services/users.service';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
export enum Action {
|
||||
ProxyPostgrest = 'proxyPostgrest',
|
||||
|
|
@ -24,7 +25,10 @@ export class TooljetDbAbilityFactory {
|
|||
|
||||
async actions(user: User, params: any) {
|
||||
const { can, build } = new AbilityBuilder<Ability<[Action, Subjects]>>(Ability as AbilityClass<TooljetDbAbility>);
|
||||
const isAdmin = await this.usersService.hasGroup(user, 'admin', params.oraganizationId);
|
||||
const { organizationId, dataQuery } = params;
|
||||
const isPublicAppRequest = isEmpty(organizationId) && !isEmpty(dataQuery) && dataQuery.app.isPublic;
|
||||
const isUserLoggedin = !isEmpty(user) && !isEmpty(organizationId);
|
||||
const isAdmin = !isEmpty(user) ? await this.usersService.hasGroup(user, 'admin', params.organizationId) : false;
|
||||
|
||||
if (isAdmin) {
|
||||
can(Action.CreateTable, 'all');
|
||||
|
|
@ -33,7 +37,11 @@ export class TooljetDbAbilityFactory {
|
|||
can(Action.DropColumn, 'all');
|
||||
can(Action.RenameTable, 'all');
|
||||
}
|
||||
can(Action.ProxyPostgrest, 'all');
|
||||
|
||||
if (isPublicAppRequest || isUserLoggedin) {
|
||||
can(Action.ProxyPostgrest, 'all');
|
||||
}
|
||||
|
||||
can(Action.ViewTables, 'all');
|
||||
can(Action.ViewTable, 'all');
|
||||
|
||||
|
|
|
|||
|
|
@ -3,17 +3,36 @@ import { Reflector } from '@nestjs/core';
|
|||
import { TooljetDbAbility, TooljetDbAbilityFactory } from './abilities/tooljet-db-ability.factory';
|
||||
import { CHECK_POLICIES_KEY } from './check_policies.decorator';
|
||||
import { PolicyHandler } from './policyhandler.interface';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { DataQuery } from 'src/entities/data_query.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TooljetDbGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector, private tooljetDbAbilityFactory: TooljetDbAbilityFactory) {}
|
||||
constructor(
|
||||
private reflector: Reflector,
|
||||
private tooljetDbAbilityFactory: TooljetDbAbilityFactory,
|
||||
private manager: EntityManager
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const policyHandlers = this.reflector.get<PolicyHandler[]>(CHECK_POLICIES_KEY, context.getHandler()) || [];
|
||||
|
||||
const { user, params } = context.switchToHttp().getRequest();
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const dataQueryId = request.headers['data-query-id'];
|
||||
const organizationId = request.headers['tj-workspace-id'] == 'null' ? null : request.headers['tj-workspace-id'];
|
||||
const isPublicAppRequest = isEmpty(organizationId);
|
||||
|
||||
const ability = await this.tooljetDbAbilityFactory.actions(user, params);
|
||||
let dataQuery: DataQuery;
|
||||
if (isPublicAppRequest && !isEmpty(dataQueryId)) {
|
||||
dataQuery = await this.manager.findOne(DataQuery, {
|
||||
where: { id: dataQueryId },
|
||||
relations: ['apps'],
|
||||
});
|
||||
}
|
||||
request.dataQuery = dataQuery;
|
||||
|
||||
const ability = await this.tooljetDbAbilityFactory.actions(request.user, { dataQuery, organizationId });
|
||||
|
||||
return policyHandlers.every((handler) => this.execPolicyHandler(handler, ability));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ImportExportResourcesController } from '@controllers/import_export_resources.controller';
|
||||
import { TooljetDbService } from '@services/tooljet_db.service';
|
||||
import { ImportExportResourcesService } from '@services/import_export_resources.service';
|
||||
import { AppImportExportService } from '@services/app_import_export.service';
|
||||
import { TooljetDbImportExportService } from '@services/tooljet_db_import_export_service';
|
||||
import { DataSourcesService } from '@services/data_sources.service';
|
||||
import { AppEnvironmentService } from '@services/app_environments.service';
|
||||
import { Plugin } from 'src/entities/plugin.entity';
|
||||
import { PluginsHelper } from 'src/helpers/plugins.helper';
|
||||
import { CredentialsService } from '@services/credentials.service';
|
||||
import { DataSource } from 'src/entities/data_source.entity';
|
||||
import { tooljetDbOrmconfig } from '../../../ormconfig';
|
||||
import { PluginsModule } from '../plugins/plugins.module';
|
||||
import { EncryptionService } from '@services/encryption.service';
|
||||
import { Credential } from '../../../src/entities/credential.entity';
|
||||
import { CaslModule } from '../casl/casl.module';
|
||||
import { AppsService } from '@services/apps.service';
|
||||
import { App } from 'src/entities/app.entity';
|
||||
import { AppVersion } from 'src/entities/app_version.entity';
|
||||
import { AppUser } from 'src/entities/app_user.entity';
|
||||
|
||||
const imports = [
|
||||
PluginsModule,
|
||||
CaslModule,
|
||||
TypeOrmModule.forFeature([AppUser, AppVersion, App, Credential, Plugin, DataSource]),
|
||||
];
|
||||
|
||||
if (process.env.ENABLE_TOOLJET_DB === 'true') {
|
||||
imports.unshift(TypeOrmModule.forRoot(tooljetDbOrmconfig));
|
||||
}
|
||||
|
||||
@Module({
|
||||
imports,
|
||||
controllers: [ImportExportResourcesController],
|
||||
providers: [
|
||||
EncryptionService,
|
||||
ImportExportResourcesService,
|
||||
AppImportExportService,
|
||||
TooljetDbImportExportService,
|
||||
DataSourcesService,
|
||||
AppEnvironmentService,
|
||||
TooljetDbService,
|
||||
PluginsHelper,
|
||||
AppsService,
|
||||
CredentialsService,
|
||||
],
|
||||
})
|
||||
export class ImportExportResourcesModule {}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -654,4 +654,22 @@ export class AppsService {
|
|||
};
|
||||
});
|
||||
}
|
||||
|
||||
async findTooljetDbTables(appId: string): Promise<{ table_id: string }[]> {
|
||||
return await dbTransactionWrap(async (manager: EntityManager) => {
|
||||
const tooljetDbDataQueries = await manager
|
||||
.createQueryBuilder(DataQuery, 'data_queries')
|
||||
.innerJoin(DataSource, 'data_sources', 'data_queries.data_source_id = data_sources.id')
|
||||
.innerJoin(AppVersion, 'app_versions', 'app_versions.id = data_sources.app_version_id')
|
||||
.where('app_versions.app_id = :appId', { appId })
|
||||
.andWhere('data_sources.kind = :kind', { kind: 'tooljetdb' })
|
||||
.getMany();
|
||||
|
||||
const uniqTableIds = [...new Set(tooljetDbDataQueries.map((dq) => dq.options['table_id']))];
|
||||
|
||||
return uniqTableIds.map((table_id) => {
|
||||
return { table_id };
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
88
server/src/services/import_export_resources.service.ts
Normal file
88
server/src/services/import_export_resources.service.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { User } from 'src/entities/user.entity';
|
||||
import { ExportResourcesDto } from '@dto/export-resources.dto';
|
||||
import { AppImportExportService } from './app_import_export.service';
|
||||
import { TooljetDbImportExportService } from './tooljet_db_import_export_service';
|
||||
import { ImportResourcesDto } from '@dto/import-resources.dto';
|
||||
import { AppsService } from './apps.service';
|
||||
import { CloneResourcesDto } from '@dto/clone-resources.dto';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class ImportExportResourcesService {
|
||||
constructor(
|
||||
private readonly appImportExportService: AppImportExportService,
|
||||
private readonly appsService: AppsService,
|
||||
private readonly tooljetDbImportExportService: TooljetDbImportExportService
|
||||
) {}
|
||||
|
||||
async export(user: User, exportResourcesDto: ExportResourcesDto) {
|
||||
const resourcesExport = {};
|
||||
if (exportResourcesDto.tooljet_database) {
|
||||
resourcesExport['tooljet_database'] = [];
|
||||
|
||||
for (const tjdb of exportResourcesDto.tooljet_database) {
|
||||
!isEmpty(tjdb) &&
|
||||
resourcesExport['tooljet_database'].push(
|
||||
await this.tooljetDbImportExportService.export(exportResourcesDto.organization_id, tjdb)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (exportResourcesDto.app) {
|
||||
resourcesExport['app'] = [];
|
||||
|
||||
for (const app of exportResourcesDto.app) {
|
||||
resourcesExport['app'].push({
|
||||
definition: await this.appImportExportService.export(user, app.id, app.search_params),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return resourcesExport;
|
||||
}
|
||||
|
||||
async import(user: User, importResourcesDto: ImportResourcesDto, cloning = false) {
|
||||
const tableNameMapping = {};
|
||||
const imports = { app: [], tooljet_database: [] };
|
||||
|
||||
if (importResourcesDto.tooljet_database) {
|
||||
for (const tjdbImportDto of importResourcesDto.tooljet_database) {
|
||||
const createdTable = await this.tooljetDbImportExportService.import(
|
||||
importResourcesDto.organization_id,
|
||||
tjdbImportDto,
|
||||
cloning
|
||||
);
|
||||
tableNameMapping[tjdbImportDto.id] = createdTable;
|
||||
imports.tooljet_database.push(createdTable);
|
||||
}
|
||||
}
|
||||
|
||||
if (importResourcesDto.app) {
|
||||
for (const appImportDto of importResourcesDto.app) {
|
||||
user.organizationId = importResourcesDto.organization_id;
|
||||
const createdApp = await this.appImportExportService.import(user, appImportDto.definition, {
|
||||
tooljet_database: tableNameMapping,
|
||||
});
|
||||
imports.app.push({ id: createdApp.id, name: createdApp.name });
|
||||
}
|
||||
}
|
||||
|
||||
return imports;
|
||||
}
|
||||
|
||||
async clone(user: User, cloneResourcesDto: CloneResourcesDto) {
|
||||
const tablesForApp = await this.appsService.findTooljetDbTables(cloneResourcesDto.app[0].id);
|
||||
|
||||
const exportResourcesDto = new ExportResourcesDto();
|
||||
exportResourcesDto.organization_id = cloneResourcesDto.organization_id;
|
||||
exportResourcesDto.app = [{ id: cloneResourcesDto.app[0].id, search_params: null }];
|
||||
exportResourcesDto.tooljet_database = tablesForApp;
|
||||
|
||||
const resourceExport = await this.export(user, exportResourcesDto);
|
||||
resourceExport['organization_id'] = cloneResourcesDto.organization_id;
|
||||
const clonedResource = await this.import(user, resourceExport as ImportResourcesDto, true);
|
||||
|
||||
return clonedResource;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,8 @@ import { maybeSetSubPath } from '../helpers/utils.helper';
|
|||
export class PostgrestProxyService {
|
||||
constructor(private readonly manager: EntityManager, private readonly configService: ConfigService) {}
|
||||
|
||||
async perform(req, res, next, organizationId) {
|
||||
async perform(req, res, next) {
|
||||
const organizationId = req.headers['tj-workspace-id'] || req.dataQuery?.app?.organizationId;
|
||||
req.url = await this.replaceTableNamesAtPlaceholder(req, organizationId);
|
||||
const authToken = 'Bearer ' + this.signJwtPayload(this.configService.get<string>('PG_USER'));
|
||||
req.headers = {};
|
||||
|
|
@ -25,8 +26,8 @@ export class PostgrestProxyService {
|
|||
|
||||
private httpProxy = proxy(this.configService.get<string>('PGRST_HOST'), {
|
||||
proxyReqPathResolver: function (req) {
|
||||
const path = '/api/tooljet_db/organizations';
|
||||
const pathRegex = new RegExp(`${maybeSetSubPath(path)}/.{36}/proxy`);
|
||||
const path = '/api/tooljet_db';
|
||||
const pathRegex = new RegExp(`${maybeSetSubPath(path)}/proxy`);
|
||||
const parts = req.url.split('?');
|
||||
const queryString = parts[1];
|
||||
const updatedPath = parts[0].replace(pathRegex, '');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, NotFoundException, Optional } from '@nestjs/common';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { InjectEntityManager } from '@nestjs/typeorm';
|
||||
import { InternalTable } from 'src/entities/internal_table.entity';
|
||||
|
|
@ -8,6 +8,7 @@ import { isString } from 'lodash';
|
|||
export class TooljetDbService {
|
||||
constructor(
|
||||
private readonly manager: EntityManager,
|
||||
@Optional()
|
||||
@InjectEntityManager('tooljetDb')
|
||||
private tooljetDbManager: EntityManager
|
||||
) {}
|
||||
|
|
@ -34,30 +35,47 @@ export class TooljetDbService {
|
|||
}
|
||||
|
||||
private async viewTable(organizationId: string, params) {
|
||||
const { table_name: tableName } = params;
|
||||
const { table_name: tableName, id: id } = params;
|
||||
|
||||
const internalTable = await this.manager.findOne(InternalTable, {
|
||||
where: { organizationId, tableName },
|
||||
where: {
|
||||
organizationId,
|
||||
...(tableName && { tableName }),
|
||||
...(id && { id }),
|
||||
},
|
||||
});
|
||||
|
||||
if (!internalTable) throw new NotFoundException('Internal table not found: ' + tableName);
|
||||
|
||||
return await this.tooljetDbManager.query(
|
||||
`SELECT c.COLUMN_NAME, c.DATA_TYPE, c.Column_default, c.character_maximum_length, c.numeric_precision, c.is_nullable
|
||||
,CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 'PRIMARY KEY' ELSE '' END AS KeyType
|
||||
FROM INFORMATION_SCHEMA.COLUMNS c
|
||||
LEFT JOIN (
|
||||
SELECT ku.TABLE_CATALOG,ku.TABLE_SCHEMA,ku.TABLE_NAME,ku.COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc
|
||||
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS ku
|
||||
ON tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
||||
AND tc.CONSTRAINT_NAME = ku.CONSTRAINT_NAME
|
||||
) pk
|
||||
ON c.TABLE_CATALOG = pk.TABLE_CATALOG
|
||||
AND c.TABLE_SCHEMA = pk.TABLE_SCHEMA
|
||||
AND c.TABLE_NAME = pk.TABLE_NAME
|
||||
AND c.COLUMN_NAME = pk.COLUMN_NAME
|
||||
WHERE c.TABLE_NAME = '${internalTable.id}'
|
||||
ORDER BY c.TABLE_SCHEMA,c.TABLE_NAME, c.ORDINAL_POSITION;
|
||||
`
|
||||
SELECT c.COLUMN_NAME, c.DATA_TYPE,
|
||||
CASE
|
||||
WHEN pk.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
||||
THEN c.Column_default
|
||||
WHEN c.Column_default LIKE '%::%'
|
||||
THEN replace(substring(c.Column_default from '^''?(.*?)''?::'), '''', '')
|
||||
ELSE c.Column_default
|
||||
END AS Column_default,
|
||||
c.character_maximum_length, c.numeric_precision, c.is_nullable,
|
||||
pk.CONSTRAINT_TYPE,
|
||||
CASE
|
||||
WHEN pk.COLUMN_NAME IS NOT NULL THEN 'PRIMARY KEY'
|
||||
ELSE ''
|
||||
END AS KeyType
|
||||
FROM INFORMATION_SCHEMA.COLUMNS c
|
||||
LEFT JOIN (
|
||||
SELECT ku.TABLE_CATALOG,ku.TABLE_SCHEMA,ku.TABLE_NAME,ku.COLUMN_NAME, tc.CONSTRAINT_TYPE
|
||||
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc
|
||||
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS ku
|
||||
ON tc.CONSTRAINT_NAME = ku.CONSTRAINT_NAME
|
||||
) pk
|
||||
ON c.TABLE_CATALOG = pk.TABLE_CATALOG
|
||||
AND c.TABLE_SCHEMA = pk.TABLE_SCHEMA
|
||||
AND c.TABLE_NAME = pk.TABLE_NAME
|
||||
AND c.COLUMN_NAME = pk.COLUMN_NAME
|
||||
WHERE c.TABLE_NAME = '${internalTable.id}'
|
||||
ORDER BY c.TABLE_SCHEMA,c.TABLE_NAME, c.ORDINAL_POSITION;
|
||||
`
|
||||
);
|
||||
}
|
||||
|
|
@ -65,7 +83,7 @@ export class TooljetDbService {
|
|||
private async viewTables(organizationId: string) {
|
||||
return await this.manager.find(InternalTable, {
|
||||
where: { organizationId },
|
||||
select: ['tableName'],
|
||||
select: ['id', 'tableName'],
|
||||
order: { tableName: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
|
@ -76,28 +94,26 @@ export class TooljetDbService {
|
|||
}
|
||||
|
||||
private async createTable(organizationId: string, params) {
|
||||
let primaryKeyExist = false;
|
||||
|
||||
// primary keys are only supported as serial type
|
||||
params.columns = params.columns.map((column) => {
|
||||
if (column['constraint_type'] === 'PRIMARY KEY') {
|
||||
primaryKeyExist = true;
|
||||
return { ...column, data_type: 'serial', column_default: null };
|
||||
}
|
||||
return column;
|
||||
});
|
||||
|
||||
if (!primaryKeyExist) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
const {
|
||||
table_name: tableName,
|
||||
columns: [column, ...restColumns],
|
||||
} = params;
|
||||
|
||||
// Validate first column -> should be primary key with name: id
|
||||
if (
|
||||
!(
|
||||
column &&
|
||||
column['column_name'] === 'id' &&
|
||||
column['data_type'] === 'serial' &&
|
||||
column['constraint'] === 'PRIMARY KEY'
|
||||
)
|
||||
) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
// Validate other columns -> should not be a primary key
|
||||
if (restColumns && restColumns.some((rc) => rc['constraint'] === 'PRIMARY_KEY')) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
const queryRunner = this.manager.connection.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
|
@ -111,14 +127,14 @@ export class TooljetDbService {
|
|||
|
||||
const createTableString = `CREATE TABLE "${internalTable.id}" `;
|
||||
let query = `${column['column_name']} ${column['data_type']}`;
|
||||
if (column['default']) query += ` DEFAULT ${this.addQuotesIfString(column['default'])}`;
|
||||
if (column['constraint']) query += ` ${column['constraint']}`;
|
||||
if (column['column_default']) query += ` DEFAULT ${this.addQuotesIfString(column['column_default'])}`;
|
||||
if (column['constraint_type']) query += ` ${column['constraint_type']}`;
|
||||
|
||||
if (restColumns)
|
||||
for (const col of restColumns) {
|
||||
query += `, ${col['column_name']} ${col['data_type']}`;
|
||||
if (col['default']) query += ` DEFAULT ${this.addQuotesIfString(col['default'])}`;
|
||||
if (col['constraint']) query += ` ${col['constraint']}`;
|
||||
if (col['column_default']) query += ` DEFAULT ${this.addQuotesIfString(col['column_default'])}`;
|
||||
if (col['constraint_type']) query += ` ${col['constraint_type']}`;
|
||||
}
|
||||
|
||||
// if tooljetdb query fails in this connection, we must rollback internal table
|
||||
|
|
@ -126,7 +142,7 @@ export class TooljetDbService {
|
|||
await this.tooljetDbManager.query(createTableString + '(' + query + ');');
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return true;
|
||||
return { id: internalTable.id, table_name: tableName };
|
||||
} catch (err) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw err;
|
||||
|
|
@ -194,7 +210,7 @@ export class TooljetDbService {
|
|||
if (!internalTable) throw new NotFoundException('Internal table not found: ' + tableName);
|
||||
|
||||
let query = `ALTER TABLE "${internalTable.id}" ADD ${column['column_name']} ${column['data_type']}`;
|
||||
if (column['default']) query += ` DEFAULT ${this.addQuotesIfString(column['default'])}`;
|
||||
if (column['column_default']) query += ` DEFAULT ${this.addQuotesIfString(column['column_default'])}`;
|
||||
if (column['constraint']) query += ` ${column['constraint']};`;
|
||||
|
||||
const result = await this.tooljetDbManager.query(query);
|
||||
|
|
|
|||
66
server/src/services/tooljet_db_import_export_service.ts
Normal file
66
server/src/services/tooljet_db_import_export_service.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ExportTooljetDatabaseDto } from '@dto/export-resources.dto';
|
||||
import { ImportTooljetDatabaseDto } from '@dto/import-resources.dto';
|
||||
import { TooljetDbService } from './tooljet_db.service';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { InternalTable } from 'src/entities/internal_table.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TooljetDbImportExportService {
|
||||
constructor(private readonly tooljetDbService: TooljetDbService, private readonly manager: EntityManager) {}
|
||||
|
||||
async export(organizationId: string, tjDbDto: ExportTooljetDatabaseDto) {
|
||||
const internalTable = await this.manager.findOne(InternalTable, {
|
||||
where: { organizationId, id: tjDbDto.table_id },
|
||||
});
|
||||
|
||||
if (!internalTable) throw new NotFoundException('Tooljet database table not found');
|
||||
|
||||
const columnSchema = await this.tooljetDbService.perform(organizationId, 'view_table', {
|
||||
id: tjDbDto.table_id,
|
||||
});
|
||||
|
||||
return {
|
||||
id: internalTable.id,
|
||||
table_name: internalTable.tableName,
|
||||
schema: { columns: columnSchema },
|
||||
};
|
||||
}
|
||||
|
||||
async import(organizationId: string, tjDbDto: ImportTooljetDatabaseDto, cloning = false) {
|
||||
const internalTableWithSameNameExists = await this.manager.findOne(InternalTable, {
|
||||
where: {
|
||||
tableName: tjDbDto.table_name,
|
||||
organizationId,
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
cloning &&
|
||||
internalTableWithSameNameExists &&
|
||||
(await this.isTableColumnsSubset(internalTableWithSameNameExists, tjDbDto))
|
||||
)
|
||||
return { id: internalTableWithSameNameExists.id, name: internalTableWithSameNameExists.tableName };
|
||||
|
||||
const tableName = internalTableWithSameNameExists
|
||||
? `${tjDbDto.table_name}_${new Date().getTime()}`
|
||||
: tjDbDto.table_name;
|
||||
|
||||
return await this.tooljetDbService.perform(organizationId, 'create_table', {
|
||||
table_name: tableName,
|
||||
...tjDbDto.schema,
|
||||
});
|
||||
}
|
||||
|
||||
async isTableColumnsSubset(internalTable: InternalTable, tjDbDto: ImportTooljetDatabaseDto): Promise<boolean> {
|
||||
const dtoColumns = new Set<string>(tjDbDto.schema.columns.map((c) => c.column_name));
|
||||
|
||||
const internalTableColumnSchema = await this.tooljetDbService.perform(internalTable.organizationId, 'view_table', {
|
||||
id: internalTable.id,
|
||||
});
|
||||
const internalTableColumns = new Set<string>(internalTableColumnSchema.map((c) => c.column_name));
|
||||
const isSubset = (subset: Set<string>, superset: Set<string>) => [...subset].every((item) => superset.has(item));
|
||||
|
||||
return isSubset(dtoColumns, internalTableColumns);
|
||||
}
|
||||
}
|
||||
|
|
@ -102,6 +102,8 @@ describe('AppImportExportService', () => {
|
|||
});
|
||||
const dataQuery1 = await createDataQuery(nestApp, {
|
||||
dataSource: dataSource1,
|
||||
appVersion: appVersion1,
|
||||
name: 'test_query_1',
|
||||
kind: 'test_kind',
|
||||
});
|
||||
|
||||
|
|
@ -115,6 +117,7 @@ describe('AppImportExportService', () => {
|
|||
name: 'test_name_2',
|
||||
});
|
||||
const dataQuery2 = await createDataQuery(nestApp, {
|
||||
appVersion: appVersion2,
|
||||
dataSource: dataSource2,
|
||||
name: 'test_query_2',
|
||||
});
|
||||
|
|
@ -123,7 +126,7 @@ describe('AppImportExportService', () => {
|
|||
where: { id: application.id },
|
||||
});
|
||||
|
||||
let { appV2: result } = await service.export(adminUser, exportedApp.id, { versionId: appVersion1.id });
|
||||
let { appV2: result } = await service.export(adminUser, exportedApp.id, { version_id: appVersion1.id });
|
||||
|
||||
expect(result.id).toBe(exportedApp.id);
|
||||
expect(result.name).toBe(exportedApp.name);
|
||||
|
|
@ -137,7 +140,7 @@ describe('AppImportExportService', () => {
|
|||
expect(result.appVersions.length).toBe(1);
|
||||
expect(result.appVersions[0].name).toEqual(appVersion1.name);
|
||||
|
||||
const res = await service.export(adminUser, exportedApp.id, { versionId: appVersion2.id });
|
||||
const res = await service.export(adminUser, exportedApp.id, { version_id: appVersion2.id });
|
||||
result = res.appV2;
|
||||
|
||||
expect(result.id).toBe(exportedApp.id);
|
||||
|
|
@ -246,6 +249,7 @@ describe('AppImportExportService', () => {
|
|||
//create default dataQuery
|
||||
await createDataQuery(nestApp, {
|
||||
dataSource: firstDs,
|
||||
appVersion: applicationVersion,
|
||||
options: {},
|
||||
});
|
||||
|
||||
|
|
@ -263,11 +267,9 @@ describe('AppImportExportService', () => {
|
|||
const appVersion = importedApp.appVersions[0];
|
||||
expect(appVersion.appId).toEqual(importedApp.id);
|
||||
|
||||
const dataSource = importedApp['dataSources'].reverse()[0];
|
||||
expect(dataSource['appVersionId']).toEqual(appVersion.id);
|
||||
|
||||
const dataQuery = importedApp['dataQueries'][0];
|
||||
expect(dataQuery['dataSourceId']).toEqual(dataSource.id);
|
||||
const dataSourceForTheDataQuery = importedApp['dataSources'].find((ds) => ds.id === dataQuery.dataSourceId);
|
||||
expect(dataSourceForTheDataQuery).toBeDefined();
|
||||
|
||||
// assert all fields except primary keys, foreign keys and timestamps are same
|
||||
const deleteFieldsNotToCheck = (entity) => {
|
||||
|
|
@ -287,10 +289,9 @@ describe('AppImportExportService', () => {
|
|||
const importedDataQueries = importedApp['dataQueries'].map((query) => deleteFieldsNotToCheck(query));
|
||||
const exportedDataQueries = exportedApp['dataQueries'].map((query) => deleteFieldsNotToCheck(query));
|
||||
|
||||
expect(importedAppVersions).toEqual(exportedAppVersions);
|
||||
console.log('inside', importedDataSources, exportedDataSources);
|
||||
expect(importedDataSources).toEqual(exportedDataSources);
|
||||
expect(importedDataQueries).toEqual(exportedDataQueries);
|
||||
expect(new Set(importedAppVersions)).toEqual(new Set(exportedAppVersions));
|
||||
expect(new Set(importedDataSources)).toEqual(new Set(exportedDataSources));
|
||||
expect(new Set(importedDataQueries)).toEqual(new Set(exportedDataQueries));
|
||||
|
||||
// assert group permissions are valid
|
||||
const appGroupPermissions = await getManager().find(AppGroupPermission, {
|
||||
|
|
|
|||
Loading…
Reference in a new issue