Merge pull request #8272 from ToolJet/main

Merge main to develop
This commit is contained in:
Midhun G S 2023-12-08 20:47:31 +05:30 committed by GitHub
commit 469ce6306b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 7848 additions and 6639 deletions

View file

@ -1 +1 @@
2.24.5
2.25.0

View file

@ -23,6 +23,7 @@ export const databaseSelectors = {
tableKebabIcon: '[data-cy="table-kebab-icon"]',
tableEditOption: '[data-cy="edit-option"]',
tableExportOption: '[data-cy="export-table-option"]',
tableDeleteOption: '[data-cy="delete-option"]',
editTableHeader: '[data-cy="edit-table-header"]',
@ -32,19 +33,25 @@ export const databaseSelectors = {
deleteRecordButton: '[data-cy="delete-row-records-button"]',
nameInputField: (value) => {
return `[data-cy="name-input-field-${value}"]`
return `[data-cy="name-input-field-${value}"]`;
},
currentTable: (tableName) => {
return `[data-cy="${String(tableName).toLowerCase().replace(/\s+/g, "-")}-table"]`;
return `[data-cy="${String(tableName)
.toLowerCase()
.replace(/\s+/g, "-")}-table"]`;
},
currentTableName: (tableName) => {
return `[data-cy="${String(tableName).toLowerCase().replace(/\s+/g, "-")}-table-name"]`;
return `[data-cy="${String(tableName)
.toLowerCase()
.replace(/\s+/g, "-")}-table-name"]`;
},
columnHeader: (columnName) => {
return `[data-cy="${String(columnName).toLowerCase().replace(/\s+/g, "-")}-column-header"]`;
return `[data-cy="${String(columnName)
.toLowerCase()
.replace(/\s+/g, "-")}-column-header"]`;
},
checkboxCell: (idColumn) => {
return `[data-cy="${idColumn}-checkbox-table-cell"]> div > input`
return `[data-cy="${idColumn}-checkbox-table-cell"]> div > input`;
},
};
@ -66,12 +73,15 @@ export const createNewRowSelectors = {
serialDataTypeLabel: '[data-cy="integer-data-type-label"]',
idColumnInputField: '[data-cy="id-input-field"]',
columnNameLabel: (columnName) => {
return `[data-cy="${String(columnName).toLowerCase().replace(/\s+/g, "-")}-column-name-label"]`;
return `[data-cy="${String(columnName)
.toLowerCase()
.replace(/\s+/g, "-")}-column-name-label"]`;
},
columnNameInputField: (columnName) => {
return `[data-cy="${String(columnName).toLowerCase().replace(/\s+/g, "-")}-input-field"]`;
return `[data-cy="${String(columnName)
.toLowerCase()
.replace(/\s+/g, "-")}-input-field"]`;
},
};
@ -98,6 +108,20 @@ export const editRowSelectors = {
idColumnNameLabel: '[data-cy="id-column-name-label"]',
selectRowDropdown: '[data-cy="select-row-dropdown"]',
getRowData: (rowNumber, columnName) => {
return `[data-cy="id-${String(rowNumber).toLowerCase().replace(/\s+/g, "-")}-column-${String(columnName).toLowerCase().replace(/\s+/g, "-")}-table-cell"]`
}
};
return `[data-cy="id-${String(rowNumber)
.toLowerCase()
.replace(/\s+/g, "-")}-column-${String(columnName)
.toLowerCase()
.replace(/\s+/g, "-")}-table-cell"]`;
},
};
export const bulkUploadDataSelectors = {
bulkUploadDataButton: '[data-cy="bulk-upload-data-button"]',
bulkUploadbuttonText: '[data-cy="bulk-upload-button-text"]',
bulkUploadDataHeaderText: '[data-cy="bulk-upload-data-header"]',
templateHelperText: '[data-cy="helper-text-bulk-upload"]',
templateDownloadButton: '[data-cy="button-download-template"]',
bulkUploadInputField: '[data-cy="input-field-bulk-upload"]',
uploadDataButton: '[data-cy="upload-data-button"]',
};

View file

@ -39,4 +39,6 @@ export const importSelectors = {
importAnApplication: '[data-cy="import-an-application"]',
importOptionLabel: '[data-cy="import-option-label"]',
importOptionInput: '[data-cy="import-option-input"]',
importAppTitle: '[data-cy="import-app-title"]',
importAppButton: '[data-cy="import-app"]',
};

View file

@ -1,8 +1,8 @@
export const buttonText = {
defaultWidgetText: "Button",
defaultWidgetName: "button1",
buttonTextLabel: "Button Text",
loadingState: "Loading State",
buttonTextLabel: "Button text",
loadingState: "Loading state",
buttonDocumentationLink: "Read documentation for Button",
backgroundColor: "Background Color",
textColor: "Text color",

View file

@ -196,13 +196,13 @@ export const commonWidgetText = {
parameterShowOnMobile: "Show on mobile",
parameterVisibility: "Visibility",
parameterDisable: "Disable",
parameterBorderRadius: "Border Radius",
parameterBorderRadius: "Border radius",
borderRadiusInput: ["{{", "20}}"],
parameterOptionLabels: "Option labels",
parameterBoxShadow: "Box Shadow",
parameterBoxShadow: "Box shadow",
boxShadowDefaultValue: "#00000040",
parameterOptionvalues: "Option values",
boxShadowColor: "Box Shadow Color",
boxShadowColor: "Box shadow Color",
boxShadowFxValue: "-5px 6px 5px 8px #ee121240",
codeMirrorLabelTrue: "{{true}}",
@ -213,7 +213,7 @@ export const commonWidgetText = {
addEventHandlerLink: "New event handler",
inspectorComponentLabel: "components",
componentValueLabel: "Value",
labelDefaultValue: "Default Value",
labelDefaultValue: "Default value",
parameterLabel: "Label",
labelMinimumValue: "Minimum value",
labelMaximumValue: "Maximum value",

View file

@ -81,3 +81,9 @@ export const editRowText = {
selectRowToEditText: "Select a row to edit",
rowEditedSuccessfullyToast: "Row edited successfully",
};
export const bulkUploadDataText = {
bulkUploadbuttonText: "Bulk upload data",
templateHelperText:
"Download the template to add your data or format your file in the same as the template. ToolJet wont be able to recognise files in any other format.",
};

View file

@ -7,7 +7,7 @@ export const datePickerText = {
},
datepicker1: "datepicker1",
labelDefaultValue: "Default Value",
labelDefaultValue: "Default value",
labelformat: "Format",
labelEnableDateSection: "Enable date selection?",
labelEnableTimeSection: "Enable time selection?",

View file

@ -5,5 +5,5 @@ export const multiselectText = {
noEventsMessage: "No event handlers",
dropdwonOptionSelectAll: "Select All",
enableSelectAllOptions: "Enable select All option",
enableSelectAllOptions: "Enable select all option",
};

View file

@ -1,4 +1,8 @@
import { filterSelectors, sortSelectors } from "Selectors/database";
import {
databaseSelectors,
filterSelectors,
sortSelectors,
} from "Selectors/database";
import { databaseText, filterText, sortText } from "Texts/database";
import { navigateToDatabase } from "Support/utils/common";
import {
@ -15,6 +19,8 @@ import {
deleteRowAndVerify,
editRowWithInvalidData,
editRowAndVerify,
exportTableAndVerify,
bulkUploadDataTemplateDownloadAndVerify,
} from "Support/utils/database";
import { fake } from "Fixtures/fake";
import { randomNumber } from "Support/utils/commonWidget";
@ -75,7 +81,9 @@ describe("Database Functionality", () => {
let column1 = columnDetails();
let column2 = columnDetails();
navigateToDatabase();
verifyAllElementsOfPage();
cy.get(databaseSelectors.allTablesSection).should("be.visible");
cy.get(databaseSelectors.allTableSubheader).should("be.visible");
cy.get(databaseSelectors.addTableButton).should("be.visible");
createTableAndVerifyToastMessage(data.tableName1, false);
createTableAndVerifyToastMessage(
data.tableName2,
@ -85,6 +93,7 @@ describe("Database Functionality", () => {
true,
[column1.defaultValueVarchar, column1.defaultValueInt]
);
verifyAllElementsOfPage();
});
it("Verify all operations of table", () => {
const data = {};
@ -183,5 +192,15 @@ describe("Database Functionality", () => {
[databaseText.idColumnName, column1.name, column2.name],
[row4.varcharData, row4.intData]
);
exportTableAndVerify(data.tableName, [
databaseText.idColumnName,
column1.name,
column2.name,
]);
bulkUploadDataTemplateDownloadAndVerify(data.tableName, [
databaseText.idColumnName,
column1.name,
column2.name,
]);
});
});

View file

@ -138,7 +138,7 @@ describe("Data sources MySql", () => {
fillDataSourceTextField(
postgreSqlText.labelDbName,
postgreSqlText.placeholderNameOfDB,
"testdb"
"testdv"
);
fillDataSourceTextField(
postgreSqlText.labelUserName,
@ -205,7 +205,7 @@ describe("Data sources MySql", () => {
fillConnectionForm({
Host: Cypress.env("mysql_host"),
Port: Cypress.env("mysql_port"),
"Database Name": "testdb",
"Database Name": "testdv",
Username: Cypress.env("mysql_user"),
Password: Cypress.env("mysql_password"),
});
@ -383,7 +383,7 @@ describe("Data sources MySql", () => {
fillConnectionForm({
Host: Cypress.env("mysql_host"),
Port: Cypress.env("mysql_port"),
"Database Name": "testdb",
"Database Name": "testdv",
Username: Cypress.env("mysql_user"),
Password: Cypress.env("mysql_password"),
});
@ -416,7 +416,7 @@ describe("Data sources MySql", () => {
cy.get(".p-3").should(
"have.text",
`[{"Tables_in_testdb (${dbName})":"${dbName}"}]`
`[{"Tables_in_testdv (${dbName})":"${dbName}"}]`
);
// addQuery(
@ -458,7 +458,7 @@ describe("Data sources MySql", () => {
fillConnectionForm({
Host: Cypress.env("mysql_host"),
Port: "3318",
"Database Name": "testdb",
"Database Name": "testdv",
Username: Cypress.env("mysql_user"),
Password: Cypress.env("mysql_password"),
});

View file

@ -82,9 +82,14 @@ describe("Editor- Inspector", () => {
cy.get('[data-cy="switch-page-label-and-input"] > .select-search')
.click()
.type("home{enter}");
cy.get('[data-cy="button-add-query-param"]').click();
cy.wait(1000);
cy.get('[data-cy="button-add-query-param"]').click();
cy.wait(3000);
cy.get("body").then(($body) => {
if ($body.find('[data-cy="query-param-key-input-field"]').length == 0) {
cy.get('[data-cy="button-add-query-param"]').click();
}
});
addSupportCSAData("query-param-key", "key");
addSupportCSAData("query-param-value", "value");

View file

@ -326,7 +326,7 @@ describe("Modal", () => {
);
cy.get("#inspector-tab-properties").click();
typeOnFx("Loading State", "{{components.toggleswitch3.value");
typeOnFx("Loading state", "{{components.toggleswitch3.value");
cy.get("[data-cy='modal-header']").realClick();
typeOnFx("Hide title bar", "{{components.toggleswitch4.value");

View file

@ -262,7 +262,7 @@ describe("Table", () => {
cy.get('[data-cy="label-action-button-text"]').verifyVisibleElement(
"have.text",
"Button Text"
"Button text"
);
cy.get('[data-cy="action-button-text-input-field"]').type(
"{selectAll}{backspace}FakeName1"
@ -273,7 +273,7 @@ describe("Table", () => {
);
cy.get('[data-cy="label-action-button-position"]').verifyVisibleElement(
"have.text",
"Button Position"
"Button position"
); // dropdown_type
cy.forceClickOnCanvas();
cy.waitForAutoSave();
@ -351,7 +351,7 @@ describe("Table", () => {
cy.get('[data-index="0"]>.select-search-option:eq(1)').realClick();
verifyAndEnterColumnOptionInput("key", "name");
verifyAndEnterColumnOptionInput("Text color", "red");
verifyAndEnterColumnOptionInput("Cell Background Color", "yellow");
verifyAndEnterColumnOptionInput("Cell Background color", "yellow");
cy.get(
'[data-cy="input-and-label-cell-background-color"] > .form-label'
).click();
@ -647,7 +647,7 @@ describe("Table", () => {
// cy.get("[data-cy='border-radius-fx-button']:eq(1)").click();
verifyAndModifyParameter(
"Action Button Radius",
"Action button radius",
commonWidgetText.borderRadiusInput
);
@ -668,7 +668,7 @@ describe("Table", () => {
cy.get(commonWidgetSelector.buttonStylesEditorSideBar).click();
verifyAndModifyParameter(
"Border Radius",
"Border radius",
commonWidgetText.borderRadiusInput
);
cy.get(commonWidgetSelector.buttonCloseEditorSideBar).click();
@ -844,7 +844,7 @@ describe("Table", () => {
// cy.get('[data-cy="show-search-box-toggle-button"]').click();
// verifyAndModifyToggleFx("Server-side search", " ", true);
verifyAndModifyToggleFx("Loading State", "{{false}}", true);
verifyAndModifyToggleFx("Loading state", "{{false}}", true);
});
it("should verify download", () => {

View file

@ -66,8 +66,8 @@ describe("App Import Functionality", () => {
cy.get(importSelectors.importOptionInput).eq(0).selectFile(appFile, {
force: true,
});
cy.get('[data-cy="import-app-title"]').should("be.visible");
cy.get('[data-cy="Import app"]').click();
cy.get(importSelectors.importAppTitle).should("be.visible");
cy.get(importSelectors.importAppButton).click();
cy.get(".go3958317564")
.should("be.visible")
.and("have.text", importText.appImportedToastMessage);
@ -116,8 +116,8 @@ describe("App Import Functionality", () => {
force: true,
});
cy.get('[data-cy="import-app-title"]').should("be.visible");
cy.get('[data-cy="Import app"]').click();
cy.get(importSelectors.importAppTitle).should("be.visible");
cy.get(importSelectors.importAppButton).click();
cy.get(".go3958317564")
.should("be.visible")
.and("have.text", importText.appImportedToastMessage);
@ -188,8 +188,8 @@ describe("App Import Functionality", () => {
force: true,
}
);
cy.get('[data-cy="import-app-title"]').should("be.visible");
cy.get('[data-cy="Import app"]').click();
cy.get(importSelectors.importAppTitle).should("be.visible");
cy.get(importSelectors.importAppButton).click();
cy.get(".go3958317564")
.should("be.visible")
.and("have.text", importText.appImportedToastMessage);

View file

@ -1,3 +1,4 @@
import { deleteDownloadsFolder } from "Support/utils/common";
import {
databaseSelectors,
createNewColumnSelectors,
@ -5,12 +6,14 @@ import {
filterSelectors,
sortSelectors,
editRowSelectors,
bulkUploadDataSelectors,
} from "Selectors/database";
import {
databaseText,
createNewColumnText,
createNewRowText,
editRowText,
bulkUploadDataText,
} from "Texts/database";
import { commonSelectors } from "Selectors/common";
import { commonText } from "Texts/common";
@ -28,6 +31,10 @@ export const verifyAllElementsOfPage = () => {
//cy.get(databaseSelectors.searchTableInputField).should("be.visible");
cy.get(databaseSelectors.allTablesSection).should("be.visible");
cy.get(databaseSelectors.allTableSubheader).should("be.visible");
cy.get(createNewColumnSelectors.addNewColumnButton).should("be.visible");
cy.get(createNewRowSelectors.addNewRowButton).should("be.visible");
cy.get(editRowSelectors.editRowbutton).should("be.visible");
cy.get(bulkUploadDataSelectors.bulkUploadDataButton).should("be.visible");
};
export const navigateToTable = (tableName) => {
cy.get(databaseSelectors.currentTable(tableName))
@ -74,16 +81,22 @@ export const createTableAndVerifyToastMessage = (
databaseText.noRecordsText
);
};
export const editTableNameAndVerifyToastMessage = (tableName, newTableName) => {
export const selectTableOperationOption = (tableName, operationOption) => {
navigateToTable(tableName);
cy.get(databaseSelectors.currentTable(tableName))
.find(databaseSelectors.tableKebabIcon)
.invoke("show")
.trigger("mouseover")
.trigger("mouseover")
.trigger("mousemove")
.trigger("mousedown")
.trigger("mouseup")
.click();
cy.get(databaseSelectors.tableEditOption).click();
cy.get(operationOption).click();
cy.wait(500);
};
export const editTableNameAndVerifyToastMessage = (tableName, newTableName) => {
selectTableOperationOption(tableName, databaseSelectors.tableEditOption);
cy.get(databaseSelectors.editTableHeader).verifyVisibleElement(
"have.text",
databaseText.editTableHeader
@ -111,18 +124,11 @@ export const editTableNameAndVerifyToastMessage = (tableName, newTableName) => {
);
};
export const deleteTableAndVerifyToastMessage = (tableName) => {
cy.get(databaseSelectors.currentTable(tableName))
.find(databaseSelectors.tableKebabIcon)
.invoke("show")
.trigger("mouseover")
.trigger("mousemove")
.trigger("mousedown")
.trigger("mouseup")
.click();
cy.get(databaseSelectors.tableDeleteOption).click();
selectTableOperationOption(tableName, databaseSelectors.tableDeleteOption);
// cy.on('window:confirm', (ConfirmAlertText) => {
// expect(ConfirmAlertText).to.contains(`Are you sure you want to delete the table "${tableName}"?`);
// });
cy.wait(500);
cy.verifyToastMessage(
commonSelectors.toastMessage,
databaseText.tableDeletedSuccessfullyToast(tableName)
@ -491,7 +497,6 @@ export const editRowAndVerify = (
);
verifyRowData(rowNumber, columnName, rowFieldData);
};
export const editRowWithInvalidData = (
tableName,
rowNumber,
@ -540,3 +545,73 @@ export const editRowWithInvalidData = (
);
cy.get(commonSelectors.buttonSelector(commonText.cancelButton)).click();
};
export const exportTableAndVerify = (tableName, columnName) => {
deleteDownloadsFolder();
cy.reload();
selectTableOperationOption(tableName, databaseSelectors.tableExportOption);
verifyDownloadedTableSchema(tableName, columnName);
cy.exec("cd ./cypress/downloads/ && rm -rf *");
};
export const verifyDownloadedTableSchema = (tableName, columnName) => {
cy.exec("ls ./cypress/downloads/").then((result) => {
const downloadedExportTableFileName = result.stdout.split("\n")[0];
let exportedTableFilePath = `cypress/downloads/${downloadedExportTableFileName}`;
cy.readFile(exportedTableFilePath).then((table) => {
let exportedTableData = table;
expect(downloadedExportTableFileName).to.contain.string(
tableName.toLowerCase()
);
for (let i = 0; i <= columnName.length - 1; i++) {
cy.get(databaseSelectors.columnHeader(columnName[i])).each(($el) => {
cy.wrap($el)
.should("be.visible")
.and(
"contain.text",
exportedTableData.tooljet_database[0].schema.columns[
i
].column_name.toLowerCase()
);
});
}
});
});
};
export const bulkUploadDataTemplateDownloadAndVerify = (
tableName,
columnName
) => {
deleteDownloadsFolder();
cy.reload();
cy.intercept("GET", "api/tooljet_db/organizations/**").as("dbLoad");
navigateToTable(tableName);
cy.wait(1000);
cy.get(bulkUploadDataSelectors.bulkUploadbuttonText).verifyVisibleElement(
"have.text",
bulkUploadDataText.bulkUploadbuttonText
);
cy.get(bulkUploadDataSelectors.bulkUploadDataButton)
.should("be.visible")
.click();
cy.get(bulkUploadDataSelectors.bulkUploadDataHeaderText).verifyVisibleElement(
"have.text",
bulkUploadDataText.bulkUploadbuttonText
);
cy.get(bulkUploadDataSelectors.templateHelperText).verifyVisibleElement(
"have.text",
bulkUploadDataText.templateHelperText
);
cy.get(bulkUploadDataSelectors.templateDownloadButton)
.should("be.visible")
.click();
cy.readFile(`cypress/downloads/${tableName}.csv`, "utf-8").then((table) => {
let exportedTableData = table.split(",");
cy.log(exportedTableData);
for (let i = 0; i <= columnName.length - 1; i++) {
cy.get(databaseSelectors.columnHeader(columnName[i])).each(($el) => {
cy.wrap($el)
.should("be.visible")
.and("contain.text", exportedTableData[i].toLowerCase());
});
}
});
};

View file

@ -1,6 +1,6 @@
---
id: restapi
title: REST API
title: REST API
---
ToolJet can establish a connection with any available REST API endpoint and create queries to interact with it.
@ -74,9 +74,19 @@ Whenever a request is made to the REST API, a **tj-x-forwarded-for** header is a
</div>
## Request types
The plugin will send a **JSON** formatted body by default. If a file object from a [`FilePicker` widget](/docs/widgets/file-picker) is set as a value, the body is automatically converted to be sent as a `multipart/form-data` request.
<div style={{textAlign: 'center'}}>
<img className="screenshot-full" src="/img/datasource-reference/rest-api/multipart-form-data.png" alt="ToolJet - Data source - REST API" />
</div>
## Response types
REST APIs can return data in a variety of formats, including **JSON** and **Base64**. JSON is a common format used for data exchange in REST APIs, while Base64 is often used for encoding binary data, such as images or video, within a JSON response.
REST APIs can return data in a variety of formats, including **JSON** and **Base64**. JSON is a common format used for data exchange in REST APIs, while Base64 is often used for encoding binary data, such as images or video, within a JSON response.
When the response `content-type` is **image**, the response will be a `base64` string.
### Example JSON response

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View file

@ -1 +1 @@
2.24.5
2.25.0

View file

@ -20295,6 +20295,7 @@
"version": "1.0.0",
"dependencies": {
"@tooljet-plugins/common": "file:../common",
"form-data": "^4.0.0",
"got": "^11.8.6",
"react": "^17.0.2",
"rimraf": "^3.0.2",
@ -20369,9 +20370,9 @@
"version": "1.0.0",
"dependencies": {
"@tooljet-plugins/common": "file:../common",
"@types/snowflake-sdk": "^1.6.12",
"@types/snowflake-sdk": "^1.6.17",
"react": "^17.0.2",
"snowflake-sdk": "^1.6.23"
"snowflake-sdk": "^1.9.1"
}
},
"../plugins/packages/stripe": {
@ -68031,6 +68032,7 @@
"version": "file:../plugins/packages/restapi",
"requires": {
"@tooljet-plugins/common": "file:../common",
"form-data": "^4.0.0",
"got": "^11.8.6",
"react": "^17.0.2",
"rimraf": "^3.0.2",
@ -68087,9 +68089,9 @@
"version": "file:../plugins/packages/snowflake",
"requires": {
"@tooljet-plugins/common": "file:../common",
"@types/snowflake-sdk": "^1.6.12",
"@types/snowflake-sdk": "^1.6.17",
"react": "^17.0.2",
"snowflake-sdk": "^1.6.23"
"snowflake-sdk": "^1.9.1"
}
},
"@tooljet-plugins/stripe": {

View file

@ -22,6 +22,8 @@ const DropDownSelect = ({
emptyError,
shouldCenterAlignText = false,
showPlaceHolder = false,
highlightSelected = true,
buttonClasses = '',
}) => {
const popoverId = useRef(`dd-select-${uuidv4()}`);
const popoverBtnId = useRef(`dd-select-btn-${uuidv4()}`);
@ -124,11 +126,12 @@ const DropDownSelect = ({
onAdd={onAdd}
addBtnLabel={addBtnLabel}
emptyError={emptyError}
highlightSelected={highlightSelected}
/>
</Popover>
}
>
<span className="col-auto" id={popoverBtnId.current}>
<div className={`col-auto ${buttonClasses}`} id={popoverBtnId.current}>
<ButtonSolid
size="sm"
variant="tertiary"
@ -198,7 +201,7 @@ const DropDownSelect = ({
<CheveronDown width="15" height="15" />
</div>
</ButtonSolid>
</span>
</div>
</OverlayTrigger>
);
};

View file

@ -88,13 +88,22 @@ const JoinConstraint = ({ darkMode, index, onRemove, onChange, data }) => {
</Col>
)}
</Row>
<Row className="border rounded mb-2 mx-0">
<Col sm="2" className="p-0 border-end">
<div className="tj-small-btn px-2">Join</div>
<Row className="mb-2 mx-0">
<Col sm="2" className="p-0">
<div
style={{
borderRadius: 0,
height: '30px',
}}
className="tj-small-btn px-2 border border-end-0 rounded-start"
>
Join
</div>
</Col>
<Col sm="4" className="p-0 border-end">
<Col sm="4" className="p-0">
{index ? (
<DropDownSelect
buttonClasses="border border-end-0"
showPlaceHolder
options={leftTableList}
darkMode={darkMode}
@ -128,11 +137,20 @@ const JoinConstraint = ({ darkMode, index, onRemove, onChange, data }) => {
value={leftTableList.find((val) => val?.value === leftFieldTable)}
/>
) : (
<div className="tj-small-btn px-2">{baseTableDetails?.table_name ?? ''}</div>
<div
style={{
borderRadius: 0,
height: '30px',
}}
className="tj-small-btn px-2 border border-end-0"
>
{baseTableDetails?.table_name ?? ''}
</div>
)}
</Col>
<Col sm="1" className="p-0 border-end">
<Col sm="1" className="p-0">
<DropDownSelect
buttonClasses="border border-end-0"
shouldCenterAlignText
options={staticJoinOperationsList}
darkMode={darkMode}
@ -151,6 +169,7 @@ const JoinConstraint = ({ darkMode, index, onRemove, onChange, data }) => {
</Col>
<Col sm="5" className="p-0">
<DropDownSelect
buttonClasses="border rounded-end"
showPlaceHolder
options={tableList}
darkMode={darkMode}
@ -216,7 +235,7 @@ const JoinConstraint = ({ darkMode, index, onRemove, onChange, data }) => {
}}
/>
))}
<Row className="mb-2 mx-0">
<Row className="mb-2 mx-1">
<Col className="p-0">
<ButtonSolid
variant="ghostBlue"
@ -282,10 +301,10 @@ const JoinOn = ({
];
return (
<Row className="border rounded mb-2 mx-0">
<Row className="mb-2 mx-0">
<Col
sm="2"
className="p-0 border-end"
className="p-0"
// data-tooltip-id={`tdb-join-operator-tooltip-${index}`}
// data-tooltip-content={
// index > 1
@ -295,6 +314,7 @@ const JoinOn = ({
>
{index == 1 && (
<DropDownSelect
buttonClasses="border border-end-0 rounded-start"
showPlaceHolder
options={groupOperators}
darkMode={darkMode}
@ -304,15 +324,33 @@ const JoinOn = ({
}}
/>
)}
{index == 0 && <div className="tj-small-btn px-2">On</div>}
{index == 0 && (
<div
style={{
height: '30px',
borderRadius: 0,
}}
className="tj-small-btn px-2 border border-end-0 rounded-start"
>
On
</div>
)}
{index > 1 && (
<div className="tj-small-btn px-2" style={{ color: 'var(--slate9)' }}>
<div
style={{
height: '30px',
borderRadius: 0,
color: 'var(--slate9)',
}}
className="tj-small-btn px-2 border border-end-0 rounded-start"
>
{groupOperator}
</div>
)}
</Col>
<Col sm="4" className="p-0 border-end">
<Col sm="4" className="p-0">
<DropDownSelect
buttonClasses="border border-end-0"
showPlaceHolder
options={leftFieldOptions}
darkMode={darkMode}
@ -337,7 +375,7 @@ const JoinOn = ({
}}
/>
</Col>
<Col sm="1" className="p-0 border-end">
<Col sm="1" className="p-0">
{/* <DropDownSelect
options={operators}
darkMode={darkMode}
@ -349,11 +387,14 @@ const JoinOn = ({
{/* Above line is commented and value is hardcoded as below */}
<div className="tj-small-btn px-2 text-center">{operator}</div>
<div style={{ height: '30px', borderRadius: 0 }} className="tj-small-btn px-2 text-center border border-end-0">
{operator}
</div>
</Col>
<Col sm="5" className="p-0 d-flex">
<div className="flex-grow-1">
<DropDownSelect
buttonClasses={`border ${index === 0 && 'rounded-end'}`}
showPlaceHolder
options={rightFieldOptions}
emptyError={
@ -379,7 +420,13 @@ const JoinOn = ({
/>
</div>
{index > 0 && (
<ButtonSolid size="sm" variant="ghostBlack" className="px-1 rounded-0 border-start" onClick={onRemove}>
<ButtonSolid
customStyles={{ height: '30px' }}
size="sm"
variant="ghostBlack"
className="px-1 rounded-0 border border-start-0 rounded-end"
onClick={onRemove}
>
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
</ButtonSolid>
)}

View file

@ -90,12 +90,22 @@ export default function JoinSelect({ darkMode }) {
const respectiveTableSelectedOptions = joinSelectOptions.filter((val) => val?.table === table);
const respectiveTableOptions = tableOptions[table] ?? [];
return (
<Row key={table} className="border rounded mb-2 mx-0">
<Col sm="3" className="p-0 border-end">
<div className="tj-small-btn px-2">{findTableDetails(table)?.table_name ?? ''}</div>
<Row key={table} className="mb-2 mx-0">
<Col sm="3" className="p-0">
<div
style={{
height: '30px',
borderRadius: 0,
}}
className="tj-small-btn px-2 border border-end-0 rounded-start"
>
{findTableDetails(table)?.table_name ?? ''}
</div>
</Col>
<Col sm="9" className="p-0 border-end">
<Col sm="9" className="p-0">
<DropDownSelect
buttonClasses="border rounded-end"
highlightSelected={false}
showPlaceHolder
options={[
{ label: 'Select All', value: 'SELECT ALL' },

View file

@ -72,9 +72,10 @@ export default function JoinSort({ darkMode }) {
joinOrderByOptions.map((options, i) => {
const tableDetails = options?.table ? findTableDetails(options?.table) : '';
return (
<Row className="border rounded mb-2 mx-0" key={i}>
<Col sm="6" className="p-0 border-end">
<Row className="mb-2 mx-0" key={i}>
<Col sm="6" className="p-0">
<DropDownSelect
buttonClasses="border border-end-0 rounded-start"
showPlaceHolder
options={tableList}
darkMode={darkMode}
@ -102,8 +103,9 @@ export default function JoinSort({ darkMode }) {
/>
</Col>
<Col sm="6" className="p-0 d-flex">
<div className="flex-grow-1 border-end">
<div className="flex-grow-1 overflow-hidden">
<DropDownSelect
buttonClasses="border border-end-0"
showPlaceHolder
options={sortbyConstants}
darkMode={darkMode}
@ -126,7 +128,10 @@ export default function JoinSort({ darkMode }) {
<ButtonSolid
size="sm"
variant="ghostBlack"
className="px-1 rounded-0"
className="px-1 rounded-0 border rounded-end"
customStyles={{
height: '30px',
}}
onClick={() => setJoinOrderByOptions(joinOrderByOptions.filter((opt, idx) => idx !== i))}
>
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
@ -137,7 +142,7 @@ export default function JoinSort({ darkMode }) {
})
)}
{/* Dynamically render below Row */}
<Row className="mx-0">
<Row className="mx-1 mb-1">
<Col className="p-0">
<ButtonSolid variant="ghostBlue" size="sm" onClick={() => setJoinOrderByOptions([...joinOrderByOptions, {}])}>
<AddRectangle width="15" fill="#3E63DD" opacity="1" secondaryFill="#ffffff" />

View file

@ -90,8 +90,8 @@ const SelectTableMenu = ({ darkMode }) => {
<div>
{/* Join Section */}
<div className="field-container d-flex" style={{ marginBottom: '1.5rem' }}>
<label className="form-label">From</label>
<div className="field flex-grow-1 mt-1">
<label className="form-label flex-shrink-0">From</label>
<div className="field flex-grow-1 mt-1 overflow-hidden">
{joins.map((join, joinIndex) => (
<JoinConstraint
darkMode={darkMode}
@ -133,24 +133,24 @@ const SelectTableMenu = ({ darkMode }) => {
</div>
{/* Filter Section */}
<div className="tdb-join-filtersection field-container d-flex" style={{ marginBottom: '1.5rem' }}>
<label className="form-label">Filter</label>
<div className="field flex-grow-1">
<label className="form-label flex-shrink-0">Filter</label>
<div className="field flex-grow-1 overflow-hidden">
<RenderFilterSection darkMode={darkMode} />
</div>
</div>
{/* Sort Section */}
<div className="field-container d-flex" style={{ marginBottom: '1.5rem' }}>
<label className="form-label">Sort</label>
<div className="field flex-grow-1">
<label className="form-label flex-shrink-0">Sort</label>
<div className="field flex-grow-1 overflow-hidden">
<JoinSort darkMode={darkMode} />
</div>
</div>
{/* Limit Section */}
<div className="field-container d-flex" style={{ marginBottom: '1.5rem' }}>
<label className="form-label">Limit</label>
<div className="field flex-grow-1">
<label className="form-label flex-shrink-0">Limit</label>
<div className="field flex-grow-1 overflow-hidden">
<CodeHinter
className="codehinter-plugins"
className="tjdb-codehinter border rounded"
theme={darkMode ? 'monokai' : 'default'}
height={'32px'}
placeholder="Enter limit"
@ -168,10 +168,10 @@ const SelectTableMenu = ({ darkMode }) => {
</div>
{/* Offset Section */}
<div className="field-container d-flex" style={{ marginBottom: '1.5rem' }}>
<label className="form-label">Offset</label>
<div className="field flex-grow-1">
<label className="form-label flex-shrink-0">Offset</label>
<div className="field flex-grow-1 overflow-hidden">
<CodeHinter
className="codehinter-plugins"
className="tjdb-codehinter border rounded"
theme={darkMode ? 'monokai' : 'default'}
height={'32px'}
placeholder="Enter offset"
@ -189,8 +189,8 @@ const SelectTableMenu = ({ darkMode }) => {
</div>
{/* Select Section */}
<div className="field-container d-flex" style={{ marginBottom: '1.5rem' }}>
<label className="form-label">Select</label>
<div className="field flex-grow-1">
<label className="form-label flex-shrink-0">Select</label>
<div className="field flex-grow-1 overflow-hidden">
<JoinSelect darkMode={darkMode} />
</div>
</div>
@ -222,6 +222,7 @@ const RenderFilterSection = ({ darkMode }) => {
};
} else {
editedFilterCondition = {
operator: 'AND',
...conditions,
conditionsList: [...conditionsList, { ...emptyConditionTemplate }],
};
@ -364,10 +365,11 @@ const RenderFilterSection = ({ darkMode }) => {
const { operator = '', leftField = {}, rightField = {} } = conditionDetail;
const LeftSideTableDetails = leftField?.table ? findTableDetails(leftField?.table) : '';
return (
<Row className="border rounded mb-2 mx-0" key={index}>
<Col sm="2" className="p-0 border-end">
<Row className="mb-2 mx-0" key={index}>
<Col sm="2" className="p-0">
{index === 1 && (
<DropDownSelect
buttonClasses="border border-end-0 rounded-start"
showPlaceHolder
onChange={(change) => updateOperatorForConditions(change?.value)}
options={groupOperators}
@ -375,11 +377,32 @@ const RenderFilterSection = ({ darkMode }) => {
value={groupOperators.find((op) => op.value === conditions.operator)}
/>
)}
{index === 0 && <div className="tj-small-btn px-2">Where</div>}
{index > 1 && <div className="tj-small-btn px-2">{conditions?.operator}</div>}
{index === 0 && (
<div
style={{
borderRadius: 0,
height: '30px',
}}
className="tj-small-btn px-2 border border-end-0 rounded-start"
>
Where
</div>
)}
{index > 1 && (
<div
style={{
borderRadius: 0,
height: '30px',
}}
className="tj-small-btn px-2 rounded-start border border-end-0"
>
{conditions?.operator}
</div>
)}
</Col>
<Col sm="3" className="p-0 border-end">
<Col sm="3" className="p-0">
<DropDownSelect
buttonClasses="border border-end-0"
showPlaceHolder
onChange={(newValue) =>
updateFilterConditionEntry('Column', index, {
@ -399,8 +422,9 @@ const RenderFilterSection = ({ darkMode }) => {
darkMode={darkMode}
/>
</Col>
<Col sm="3" className="p-0 border-end">
<Col sm="3" className="p-0">
<DropDownSelect
buttonClasses="border border-end-0"
showPlaceHolder
onChange={(change) => updateFilterConditionEntry('Operator', index, { operator: change?.value })}
value={filterOperatorOptions.find((op) => op.value === operator)}
@ -409,9 +433,10 @@ const RenderFilterSection = ({ darkMode }) => {
/>
</Col>
<Col sm="4" className="p-0 d-flex">
<div className="flex-grow-1">
<div className="flex-grow-1 overflow-hidden">
{operator === 'IS' ? (
<DropDownSelect
buttonClasses="border border-end-0"
showPlaceHolder
onChange={(change) =>
updateFilterConditionEntry('Value', index, { value: change?.value, isLeftSideCondition: false })
@ -429,9 +454,9 @@ const RenderFilterSection = ({ darkMode }) => {
: JSON.stringify(rightField?.value)
: rightField?.value
}
className="codehinter-plugins"
className="border border-end-0 fs-12 tjdb-codehinter"
theme={darkMode ? 'monokai' : 'default'}
height={'28px'}
height={'30px'}
placeholder="Value"
onChange={(newValue) =>
updateFilterConditionEntry('Value', index, { value: newValue, isLeftSideCondition: false })
@ -439,10 +464,14 @@ const RenderFilterSection = ({ darkMode }) => {
/>
)}
</div>
<ButtonSolid
customStyles={{
height: '30px',
}}
size="sm"
variant="ghostBlack"
className="px-1 rounded-0 border-start"
className="px-1 rounded-0 border rounded-end"
onClick={() => removeFilterConditionEntry(index)}
>
<Trash fill="var(--slate9)" style={{ height: '16px' }} />
@ -470,7 +499,7 @@ const RenderFilterSection = ({ darkMode }) => {
</Row>
)}
{filterComponents}
<Row className="mx-0">
<Row className="mx-1 mb-1">
<Col className="p-0">
<ButtonSolid variant="ghostBlue" size="sm" onClick={() => addNewFilterConditionEntry()}>
<AddRectangle width="15" fill="#3E63DD" opacity="1" secondaryFill="#ffffff" />

View file

@ -9,7 +9,8 @@ import { isOperatorOptions } from './util';
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
export const ListRows = React.memo(({ darkMode }) => {
const { columns, listRowsOptions, limitOptionChanged, handleOptionsChange } = useContext(TooljetDatabaseContext);
const { columns, listRowsOptions, limitOptionChanged, handleOptionsChange, offsetOptionChanged } =
useContext(TooljetDatabaseContext);
function handleWhereFiltersChange(filters) {
handleOptionsChange('where_filters', filters);
@ -155,7 +156,7 @@ export const ListRows = React.memo(({ darkMode }) => {
</div>
{/* Limit */}
<div className="field-container d-flex">
<div className="field-container d-flex mb-2">
<label className="form-label" data-cy="label-column-limit">
Limit
</label>
@ -170,6 +171,22 @@ export const ListRows = React.memo(({ darkMode }) => {
/>
</div>
</div>
{/* Offset */}
<div className="field-container d-flex">
<label className="form-label" data-cy="label-column-offset">
Offset
</label>
<div className="field flex-grow-1">
<CodeHinter
initialValue={listRowsOptions?.offset ?? ''}
className="codehinter-plugins"
theme={darkMode ? 'monokai' : 'default'}
height={'32px'}
placeholder="Enter offset"
onChange={(newValue) => offsetOptionChanged(newValue)}
/>
</div>
</div>
</div>
</div>
</div>

View file

@ -19,6 +19,7 @@ function DataSourceSelect({
addBtnLabel,
selected,
emptyError,
highlightSelected,
}) {
const handleChangeDataSource = (source) => {
onSelect && onSelect(source);
@ -88,7 +89,7 @@ function DataSourceSelect({
/>
))}
<span className={`${props?.data?.icon ? 'ms-1 ' : ''}flex-grow-1`}>{children}</span>
{props.isSelected && (
{props.isSelected && highlightSelected && (
<SolidIcon
fill="var(--indigo9)"
name="tick"
@ -162,11 +163,12 @@ function DataSourceSelect({
...style,
cursor: 'pointer',
color: 'inherit',
backgroundColor: isSelected
? 'var(--indigo3, #F0F4FF)'
: isFocused && !isNested
? 'var(--slate4)'
: 'transparent',
backgroundColor:
isSelected && highlightSelected
? 'var(--indigo3, #F0F4FF)'
: isFocused && !isNested
? 'var(--slate4)'
: 'transparent',
...(isNested
? { padding: '0 8px', marginLeft: '19px', borderLeft: '1px solid var(--slate5)', width: 'auto' }
: {}),

View file

@ -210,6 +210,10 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
setListRowsOptions((prev) => ({ ...prev, limit: value }));
};
const offsetOptionChanged = (value) => {
setListRowsOptions((prev) => ({ ...prev, offset: value }));
};
const deleteOperationLimitOptionChanged = (limit) => {
setDeleteRowsOptions((prev) => ({ ...prev, limit: limit }));
};
@ -290,6 +294,7 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
listRowsOptions,
setListRowsOptions,
limitOptionChanged,
offsetOptionChanged,
handleOptionsChange,
deleteRowsOptions,
handleDeleteRowsOptionsChange,
@ -467,9 +472,10 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
{/* table name dropdown */}
<div className={cx({ row: !isHorizontalLayout })}>
<div className={cx({ 'col-4': !isHorizontalLayout, 'd-flex': isHorizontalLayout })}>
<label className={cx('form-label')}>Table name</label>
<div className={cx({ 'flex-grow-1': isHorizontalLayout }, 'border', 'rounded')}>
<label className={cx('form-label', 'flex-shrink-0')}>Table name</label>
<div className={cx({ 'flex-grow-1': isHorizontalLayout }, 'border', 'rounded', 'overflow-hidden')}>
<DropDownSelect
customBorder={false}
showPlaceHolder
options={generateListForDropdown(tables)}
darkMode={darkMode}
@ -490,8 +496,8 @@ const ToolJetDbOperations = ({ optionchanged, options, darkMode, isHorizontalLay
/* className="my-2 col-4" */
className={cx({ 'col-4': !isHorizontalLayout, 'd-flex': isHorizontalLayout })}
>
<label className={cx('form-label')}>Operations</label>
<div className={cx({ 'flex-grow-1': isHorizontalLayout }, 'border', 'rounded')}>
<label className={cx('form-label', 'flex-shrink-0')}>Operations</label>
<div className={cx({ 'flex-grow-1': isHorizontalLayout }, 'border', 'rounded', 'overflow-hidden')}>
<DropDownSelect
showPlaceHolder
options={tooljetDbOperationList}

View file

@ -2,7 +2,7 @@ import { tooljetDatabaseService, authenticationService } from '@/_services';
import { isEmpty } from 'lodash';
import PostgrestQueryBuilder from '@/_helpers/postgrestQueryBuilder';
import { resolveReferences } from '@/_helpers/utils';
import { hasEqualWithNull } from './util';
import { hasNullValueInFilters } from './util';
export const tooljetDbOperations = {
perform,
@ -46,8 +46,8 @@ function buildPostgrestQuery(filters) {
postgrestQueryBuilder.order(column, order);
}
if (!isEmpty(column) && !isEmpty(operator) && value && value !== '') {
postgrestQueryBuilder[operator](column, value.toString());
if (!isEmpty(column) && !isEmpty(operator)) {
postgrestQueryBuilder[operator](column, value);
}
}
});
@ -57,7 +57,7 @@ function buildPostgrestQuery(filters) {
async function listRows(dataQuery, currentState) {
const queryOptions = dataQuery.options;
const resolvedOptions = resolveReferences(queryOptions, currentState);
if (hasEqualWithNull(resolvedOptions, 'list_rows')) {
if (hasNullValueInFilters(resolvedOptions, 'list_rows')) {
return {
status: 'failed',
statusText: 'failed',
@ -70,7 +70,7 @@ async function listRows(dataQuery, currentState) {
let query = [];
if (!isEmpty(listRows)) {
const { limit, where_filters: whereFilters, order_filters: orderFilters } = listRows;
const { limit, where_filters: whereFilters, order_filters: orderFilters, offset } = listRows;
if (limit && isNaN(limit)) {
return {
@ -88,6 +88,7 @@ async function listRows(dataQuery, currentState) {
!isEmpty(whereQuery) && query.push(whereQuery);
!isEmpty(orderQuery) && query.push(orderQuery);
!isEmpty(limit) && query.push(`limit=${limit}`);
!isEmpty(offset) && query.push(`offset=${offset}`);
}
const headers = { 'data-query-id': dataQuery.id };
return await tooljetDatabaseService.findOne(headers, tableId, query.join('&'));
@ -107,7 +108,7 @@ async function createRow(dataQuery, currentState) {
async function updateRows(dataQuery, currentState) {
const queryOptions = dataQuery.options;
const resolvedOptions = resolveReferences(queryOptions, currentState);
if (hasEqualWithNull(resolvedOptions, 'update_rows')) {
if (hasNullValueInFilters(resolvedOptions, 'update_rows')) {
return {
status: 'failed',
statusText: 'failed',
@ -135,7 +136,7 @@ async function updateRows(dataQuery, currentState) {
async function deleteRows(dataQuery, currentState) {
const queryOptions = dataQuery.options;
const resolvedOptions = resolveReferences(queryOptions, currentState);
if (hasEqualWithNull(resolvedOptions, 'delete_rows')) {
if (hasNullValueInFilters(resolvedOptions, 'delete_rows')) {
return {
status: 'failed',
statusText: 'failed',

View file

@ -3,7 +3,7 @@ import { get } from 'lodash';
/**
* Checks if the queryOptions object contains a filter with an 'eq' (equal) operator and a value equal to '{{null}}'.
*
* @function hasEqualWithNull
* @function hasNullValueInFilters
* @param {Object} queryOptions - The query options object to check for the presence of the specified filter.
* @property {Object} queryOptions.list_rows.where_filters - An object containing the filters to be checked.
* @returns {boolean} - Returns true if the specified filter is found, false otherwise.
@ -20,9 +20,9 @@ import { get } from 'lodash';
* },
* };
*
* const result = hasEqualWithNull(queryOptions); // true
* const result = hasNullValueInFilters(queryOptions); // true
*/
export const hasEqualWithNull = (queryOptions, operation) => {
export const hasNullValueInFilters = (queryOptions, operation) => {
const filters = get(queryOptions, `${operation}.where_filters`);
if (filters) {
const filterKeys = Object.keys(filters);

View file

@ -9,6 +9,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
const currentVersion = app?.editing_version;
const [versions, setVersions] = useState(undefined);
const [tables, setTables] = useState(undefined);
const [allTables, setAllTables] = useState(undefined);
const [versionId, setVersionId] = useState(currentVersion?.id);
const [exportTjDb, setExportTjDb] = useState(true);
@ -27,9 +28,33 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
}
async function fetchAppTables() {
try {
const fetchTables = await appsService.getTables(app.id);
const fetchTables = await appsService.getTables(app.id); // this is used to get all tables
const { tables } = fetchTables;
setTables(tables);
const tbl = await appsService.getAppByVersion(app.id, versionId); // this is used to get particular App by version
const { dataQueries } = tbl;
const extractedIdData = [];
dataQueries.forEach((item) => {
if (item.kind === 'tooljetdb') {
const joinOptions = item.options?.join_table?.joins ?? [];
(joinOptions || []).forEach((join) => {
const { table, conditions } = join;
if (table) extractedIdData.push(table);
conditions?.conditionsList?.forEach((condition) => {
const { leftField, rightField } = condition;
if (leftField?.table) {
extractedIdData.push(leftField?.table);
}
if (rightField?.table) {
extractedIdData.push(rightField?.table);
}
});
});
}
});
const uniqueSet = new Set(extractedIdData);
const selectedVersiontable = Array.from(uniqueSet).map((item) => ({ table_id: item }));
setTables(selectedVersiontable);
setAllTables(tables);
} catch (error) {
toast.error('Could not fetch the tables.', {
position: 'top-center',
@ -40,9 +65,9 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
fetchAppVersions();
fetchAppTables();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [versionId]);
const exportApp = (app, versionId, exportTjDb, tables) => {
const exportApp = (app, versionId, exportTjDb, exportTables) => {
const appOpts = {
app: [
{
@ -51,10 +76,11 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
},
],
};
const requestBody = {
...appOpts,
...(exportTjDb && { tooljet_database: tables }),
organization_id: app.organization_id ?? app.organizationId,
...(exportTjDb && { tooljet_database: exportTables }),
organization_id: app.organization_id,
};
appsService
@ -164,7 +190,7 @@ export default function ExportAppModal({ title, show, closeModal, customClassNam
className="import-export-footer-btns"
variant="tertiary"
data-cy="export-all-button"
onClick={() => exportApp(app, null, exportTjDb, tables)}
onClick={() => exportApp(app, null, exportTjDb, allTables)}
>
Export All
</ButtonSolid>

View file

@ -64,9 +64,10 @@ function BulkUploadDrawer({
<button
onClick={() => setIsBulkUploadDrawerOpen(!isBulkUploadDrawerOpen)}
className={`ghost-black-operation ${isBulkUploadDrawerOpen ? 'open' : ''}`}
data-cy={`bulk-upload-data-button`}
>
<SolidIcon name="fileupload" width="14" fill={isBulkUploadDrawerOpen ? '#3E63DD' : '#889096'} />
<span className=" tj-text-xsm font-weight-500" style={{ marginLeft: '6px' }}>
<span className=" tj-text-xsm font-weight-500" style={{ marginLeft: '6px' }} data-cy="bulk-upload-button-text">
Bulk upload data
</span>
</button>
@ -79,7 +80,7 @@ function BulkUploadDrawer({
>
<div className="drawer-card-wrapper">
<div className="drawer-card-title ">
<h3 className="" data-cy="create-new-column-header">
<h3 className="" data-cy="bulk-upload-data-header">
Bulk upload data
</h3>
</div>
@ -127,7 +128,7 @@ function BulkUploadDrawer({
</ButtonSolid>
<ButtonSolid
disabled={!bulkUploadFile || errors.client.length > 0 || errors.server.length > 0}
data-cy={`save-changes-button`}
data-cy={`upload-data-button`}
onClick={handleBulkUpload}
fill="#fff"
leftIcon="floppydisk"

View file

@ -13,6 +13,7 @@ const EditRowDrawer = ({ isCreateRowDrawerOpen, setIsCreateRowDrawerOpen }) => {
<button
onClick={() => setIsCreateRowDrawerOpen(!isCreateRowDrawerOpen)}
className={`ghost-black-operation ${isCreateRowDrawerOpen ? 'open' : ''}`}
data-cy="edit-row-button-"
>
{/* <SolidIcon name="editrectangle" width="14" fill={isCreateRowDrawerOpen ? '#3E63DD' : '#889096'} /> */}
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="13" viewBox="0 0 12 13" fill="none">

View file

@ -302,10 +302,11 @@ const Table = ({ openCreateRowDrawer, openCreateColumnDrawer }) => {
cell.column.id === 'selection'
? `${cell.row.values?.id}-checkbox`
: `id-${cell.row.values?.id}-column-${cell.column.id}`;
const cellValue = cell.value === null ? '' : cell.value;
return (
<td
key={`cell.value-${index}`}
title={cell.value || ''}
title={cellValue || ''}
className="table-cell"
data-cy={`${dataCy.toLocaleLowerCase().replace(/\s+/g, '-')}-table-cell`}
{...cell.getCellProps()}

View file

@ -1,10 +1,10 @@
import React, { useState } from 'react';
import { datasourceService } from '@/_services';
import { useTranslation } from 'react-i18next';
import { toast } from 'react-hot-toast';
import Button from '@/_ui/Button';
const Slack = ({ optionchanged, createDataSource, options, isSaving, selectedDataSource }) => {
const Slack = ({ optionchanged, createDataSource, options, isSaving, _selectedDataSource }) => {
const [authStatus, setAuthStatus] = useState(null);
const { t } = useTranslation();
@ -18,19 +18,22 @@ const Slack = ({ optionchanged, createDataSource, options, isSaving, selectedDat
scope = `${scope},chat:write`;
}
datasourceService.fetchOauth2BaseUrl(provider).then((data) => {
const authUrl = `${data.url}&scope=${scope}&access_type=offline&prompt=select_account`;
if (selectedDataSource?.id) {
localStorage.setItem('sourceWaitingForOAuth', selectedDataSource.id);
} else {
datasourceService
.fetchOauth2BaseUrl(provider)
.then((data) => {
const authUrl = `${data.url}&scope=${scope}&access_type=offline&prompt=select_account`;
localStorage.setItem('sourceWaitingForOAuth', 'newSource');
}
optionchanged('provider', provider).then(() => {
optionchanged('oauth2', true);
optionchanged('provider', provider).then(() => {
optionchanged('oauth2', true);
});
setAuthStatus('waiting_for_token');
window.open(authUrl);
})
.catch(({ error }) => {
toast.error(error);
setAuthStatus(null);
});
setAuthStatus('waiting_for_token');
window.open(authUrl);
});
}
function saveDataSource() {

View file

@ -1567,6 +1567,12 @@ $border-radius: 4px;
}
}
.tjdb-codehinter {
.CodeMirror {
font-size: 12px !important;
}
}
.tdb-dropdown-btn {
&:active {
border: 1px solid var(--indigo-09, #3E63DD) !important;

View file

@ -12,6 +12,7 @@
@import "./ui-operations.scss";
@import 'react-loading-skeleton/dist/skeleton.css';
@import './table-component.scss';
/* ibm-plex-sans-100 - latin */
@font-face {
font-display: swap;
@ -269,7 +270,8 @@ button {
.emoji-mart-scroll+.emoji-mart-bar {
display: none;
}
.accordion-item{
.accordion-item {
border: solid var(--slate5);
border-width: 0px 0px 1px 0px;
}
@ -301,6 +303,7 @@ button {
.accordion-body {
padding: 6px 16px 20px 16px !important;
.form-label {
font-weight: 400;
font-size: 12px;
@ -327,7 +330,7 @@ button {
.resizer-select,
.resizer-active {
border: solid 1px $primary !important;
border: solid 1px $primary !important;
.top-right,
.top-left,
@ -827,7 +830,7 @@ button {
.list-group.list-group-transparent.dark .all-apps-link,
.list-group-item-action.dark.active {
background-color: $dark-background !important;
background-color: $dark-background !important;
}
}
@ -1559,7 +1562,7 @@ button {
.select-search-dark input {
width: 224px !important;
height: 32px !important;
border-radius: $border-radius !important;
border-radius: $border-radius !important;
}
}
@ -1570,7 +1573,7 @@ button {
.select-search__value input,
.select-search-dark input {
height: 32px !important;
border-radius: $border-radius !important;
border-radius: $border-radius !important;
}
}
@ -1631,7 +1634,7 @@ button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border-radius: $border-radius !important;
border-radius: $border-radius !important;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
@ -1971,6 +1974,7 @@ button {
text-align: center;
color: #888;
}
// jet-table-footer is common class used in other components other than table
.jet-table-footer {
.table-footer {
@ -2904,12 +2908,14 @@ input:focus-visible {
width: 210px !important; //adjusted with padding
box-shadow: 0px 4px 6px -2px rgba(16, 24, 40, 0.03), 0px 12px 16px -4px rgba(16, 24, 40, 0.08) !important;
color: var(--slate12);
.flexbox-fix:nth-child(3) {
div:nth-child(1) {
input{
input {
width: 100% !important;
}
label{
label {
color: var(--slate12) !important;
}
}
@ -3085,6 +3091,7 @@ input:focus-visible {
.DateRangePickerInput__withBorder {
border: 1px solid #1f2936;
}
.main .canvas-container .canvas-area {
background: #2f3c4c;
}
@ -3673,7 +3680,7 @@ input[type="text"] {
.nav-tabs .nav-link.active {
font-weight: 400 !important;
color: $primary !important;
color: $primary !important;
}
.empty {
@ -4199,7 +4206,7 @@ input[type="text"] {
.tabs-inspector.dark {
.nav-link.active {
border-bottom: 1px solid $primary !important;
border-bottom: 1px solid $primary !important;
}
}
@ -4448,7 +4455,7 @@ input[type="text"] {
}
input {
border-radius: $border-radius !important;
border-radius: $border-radius !important;
padding-left: 1.75rem !important;
}
}
@ -4617,8 +4624,8 @@ input[type="text"] {
}
.modal-content.home-modal-component.dark {
background-color: $bg-dark-light !important;
color: $white !important;
background-color: $bg-dark-light !important;
color: $white !important;
.modal-title {
color: $white !important;
@ -4651,22 +4658,22 @@ input[type="text"] {
}
.form-control {
border-color: $border-grey-dark !important;
border-color: $border-grey-dark !important;
color: inherit;
}
input {
background-color: $bg-dark-light !important;
background-color: $bg-dark-light !important;
}
.form-select {
background-color: $bg-dark !important;
color: $white !important;
border-color: $border-grey-dark !important;
background-color: $bg-dark !important;
color: $white !important;
border-color: $border-grey-dark !important;
}
.text-muted {
color: $white !important;
color: $white !important;
}
}
@ -4977,7 +4984,7 @@ div#driver-page-overlay {
}
.dark-theme-walkthrough#driver-popover-item {
background-color: $bg-dark-light !important;
background-color: $bg-dark-light !important;
border-color: rgba(101, 109, 119, 0.16) !important;
.driver-popover-title {
@ -4985,7 +4992,7 @@ div#driver-page-overlay {
}
.driver-popover-tip {
border-color: transparent transparent transparent $bg-dark-light !important;
border-color: transparent transparent transparent $bg-dark-light !important;
}
.driver-popover-description {
@ -5017,7 +5024,7 @@ div#driver-page-overlay {
.driver-next-btn,
.driver-prev-btn {
color: $primary !important;
color: $primary !important;
}
.driver-disabled {
@ -5141,7 +5148,7 @@ div#driver-page-overlay {
}
.fx-canvas {
background:var(--slate4);
background: var(--slate4);
padding: 0px;
display: flex;
height: 32px;
@ -5153,7 +5160,7 @@ div#driver-page-overlay {
align-items: center;
div {
background:var(--slate4) !important;
background: var(--slate4) !important;
display: flex;
justify-content: center;
align-items: center;
@ -5161,6 +5168,7 @@ div#driver-page-overlay {
padding: 0px;
}
}
.org-name {
color: var(--slate12) !important;
font-size: 12px;
@ -5489,7 +5497,7 @@ div#driver-page-overlay {
}
.selected-node {
border-color: $primary-light !important;
border-color: $primary-light !important;
}
.json-tree-icon-container .selected-node>svg:first-child {
@ -5580,7 +5588,7 @@ div#driver-page-overlay {
}
.selected-node {
border-color: $primary-light !important;
border-color: $primary-light !important;
}
.selected-node .group-object-container .badge {
@ -5898,7 +5906,7 @@ div#driver-page-overlay {
//Kanban board
.kanban-container.dark-themed {
background-color: $bg-dark-light !important;
background-color: $bg-dark-light !important;
.kanban-column {
.card-header {
@ -5944,7 +5952,7 @@ div#driver-page-overlay {
}
.dnd-card.card.card-dark {
background-color: $bg-dark !important;
background-color: $bg-dark !important;
}
}
@ -7182,7 +7190,7 @@ tbody {
}
.application-brand {
a{
a {
height: 48px;
position: relative;
display: flex;
@ -7940,8 +7948,9 @@ tbody {
width: 240px;
height: 28px;
flex-direction: row;
div{
a{
div {
a {
text-decoration: none;
}
}
@ -8786,7 +8795,7 @@ tbody {
flex-direction: row !important;
justify-content: center !important;
align-items: center !important;
padding: 4px 16px !important;
//padding: 4px 16px !important;
width: 100% !important;
height: 28px !important;
background: var(--grass2) !important;
@ -8799,7 +8808,7 @@ tbody {
flex-direction: row !important;
justify-content: center !important;
align-items: center !important;
padding: 4px 16px !important;
//padding: 4px 16px !important;
width: 100% !important;
height: 28px !important;
border-radius: 6px !important;
@ -10226,7 +10235,7 @@ tbody {
border-radius: 6px !important;
margin-bottom: 4px !important;
color: var(--slate12) !important;
transition:none;
transition: none;
&:hover {
@ -10250,13 +10259,15 @@ tbody {
box-shadow: 0 0 0 1000px var(--base) inset !important;
-webkit-text-fill-color: var(--slate12) !important;
&:hover {
box-shadow: 0 0 0 1000px var(--slate1) inset !important;
-webkit-text-fill-color: var(--slate12) !important;}
&:hover {
box-shadow: 0 0 0 1000px var(--slate1) inset !important;
-webkit-text-fill-color: var(--slate12) !important;
}
&:focus-visible {
&:focus-visible {
box-shadow: 0 0 0 1000px var(--indigo2) inset !important;
-webkit-text-fill-color: var(--slate12) !important;}
-webkit-text-fill-color: var(--slate12) !important;
}
}
@ -11822,14 +11833,17 @@ tbody {
width: 170px !important;
}
}
.custom-gap-8{
.custom-gap-8 {
gap: 8px;
}
.color-slate-11{
.color-slate-11 {
color: var(--slate11) !important;
}
.custom-gap-6{
gap:6px
.custom-gap-6 {
gap: 6px
}
// ToolJet Database buttons
@ -11839,22 +11853,26 @@ tbody {
padding: 4px 10px;
}
.custom-gap-2{
gap:2px
.custom-gap-2 {
gap: 2px
}
.custom-gap-4{
.custom-gap-4 {
gap: 4px;
}
.text-black-000{
.text-black-000 {
color: var(--text-black-000) !important;
}
.custom-gap-12{
gap:12px
.custom-gap-12 {
gap: 12px
}
#inspector-tabpane-properties{
#inspector-tabpane-properties {
.accordion {
.accordion-item:last-child{
.accordion-item:last-child {
border-bottom: none !important;
}
}
}
}

View file

@ -1,4 +1,5 @@
import {
createBucket,
getObject,
uploadObject,
listBuckets,
@ -19,6 +20,9 @@ export default class S3QueryService implements QueryService {
try {
switch (operation) {
case Operation.CreateBucket:
result = await createBucket(client, queryOptions);
break;
case Operation.ListBuckets:
result = await listBuckets(client, {});
break;

View file

@ -13,6 +13,10 @@
"type": "dropdown-component-flip",
"description": "Single select dropdown for operation",
"list": [
{
"value": "create_bucket",
"name": "Create a new bucket"
},
{
"value": "get_object",
"name": "Read object"
@ -43,6 +47,19 @@
}
]
},
"create_bucket": {
"bucket": {
"label": "Bucket Name",
"key": "bucket",
"type": "codehinter",
"lineNumbers": false,
"description": "Enters a name for the new bucket",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter New Bucket Name"
}
},
"get_object": {
"bucket": {
"label": "Bucket",

View file

@ -3,8 +3,9 @@ import {
ListBucketsCommand,
PutObjectCommand,
DeleteObjectCommand,
S3Client,
ListObjectsV2Command,
CreateBucketCommand,
S3Client,
} from '@aws-sdk/client-s3';
// https://aws.amazon.com/blogs/developer/generate-presigned-url-modular-aws-sdk-javascript/
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
@ -38,6 +39,13 @@ export async function signedUrlForGet(client: S3Client, options: QueryOptions):
return { url };
}
export async function createBucket(client: S3Client, options: QueryOptions): Promise<object> {
const createBucketCommand = new CreateBucketCommand({
Bucket: options.bucket,
});
return await client.send(createBucketCommand);
}
export async function getObject(client: S3Client, options: QueryOptions): Promise<object> {
// Create a helper function to convert a ReadableStream to a string.
const streamToString = (stream) =>

View file

@ -15,6 +15,7 @@ export type QueryOptions = {
};
export enum Operation {
CreateBucket = 'create_bucket',
ListBuckets = 'list_buckets',
ListObjects = 'list_objects',
GetObject = 'get_object',

5136
plugins/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -20,6 +20,7 @@ import {
getAuthUrl,
sanitizeCustomParams,
checkIfContentTypeIsURLenc,
checkIfContentTypeIsMultipartFormData,
validateAndSetRequestOptionsBasedOnAuthType,
} from './oauth';
@ -43,6 +44,7 @@ export {
sanitizeHeaders,
sanitizeSearchParams,
checkIfContentTypeIsURLenc,
checkIfContentTypeIsMultipartFormData,
validateAndSetRequestOptionsBasedOnAuthType,
fetchHttpsCertsForCustomCA,
};

View file

@ -13,6 +13,12 @@ export function checkIfContentTypeIsURLenc(headers: [] = []) {
return contentType === 'application/x-www-form-urlencoded';
}
export function checkIfContentTypeIsMultipartFormData(headers: [] = []) {
const objectHeaders = Object.fromEntries(headers);
const contentType = objectHeaders['content-type'] ?? objectHeaders['Content-Type'];
return contentType === 'multipart/form-data';
}
export function sanitizeCustomParams(customArray: any) {
const params = Object.fromEntries(customArray ?? []);
Object.keys(params).forEach((key) => (params[key] === '' ? delete params[key] : {}));

View file

@ -11,16 +11,33 @@ import {
OAuthUnauthorizedClientError,
getRefreshedToken,
checkIfContentTypeIsURLenc,
checkIfContentTypeIsMultipartFormData,
isEmpty,
validateAndSetRequestOptionsBasedOnAuthType,
sanitizeHeaders,
sanitizeSearchParams,
getAuthUrl,
} from '@tooljet-plugins/common';
const FormData = require('form-data');
const JSON5 = require('json5');
import got, { HTTPError, OptionsOfTextResponseBody } from 'got';
import { SourceOptions } from './types';
function isFileObject(value) {
const keys = Object.keys(value);
return (
typeof value === 'object' &&
keys.length > 0 &&
keys.includes('name') && // example.zip
keys.includes('type') && // application/zip
keys.includes('content') && // raw'ish bytes (contains new lines - \n)
keys.includes('dataURL') && // data url representation
keys.includes('base64Data') && // data in base64
keys.includes('filePath')
);
}
interface RestAPIResult extends QueryResult {
request?: Array<object> | object;
response?: Array<object> | object;
@ -67,6 +84,7 @@ export default class RestapiQueryService implements QueryService {
/* REST API queries can be adhoc or associated with a REST API datasource */
const hasDataSource = dataSourceId !== undefined;
const isUrlEncoded = checkIfContentTypeIsURLenc(queryOptions['headers']);
const isMultipartFormData = checkIfContentTypeIsMultipartFormData(queryOptions['headers']);
/* Prefixing the base url of datasource if datasource exists */
const url = hasDataSource ? `${sourceOptions.url || ''}${queryOptions.url || ''}` : queryOptions.url;
@ -83,9 +101,36 @@ export default class RestapiQueryService implements QueryService {
...paramsFromUrl,
...sanitizeSearchParams(sourceOptions, queryOptions, hasDataSource),
},
...(isUrlEncoded ? { form: json } : { json }),
};
const hasFiles = (json) => {
return Object.values(json || {}).some((item) => {
return isFileObject(item);
});
};
if (isUrlEncoded) {
_requestOptions.form = json;
} else if (isMultipartFormData && hasFiles(json)) {
const form = new FormData();
for (const key in json) {
const value = json[key];
if (isFileObject(value)) {
const fileBuffer = Buffer.from(value?.base64Data || '', 'base64');
form.append(key, fileBuffer, {
filename: value?.name || '',
contentType: value?.type || '',
knownLength: fileBuffer.length,
});
} else if (value !== undefined && value !== null) {
form.append(key, value);
}
}
_requestOptions.body = form;
} else {
_requestOptions.json = json;
}
const authValidatedRequestOptions = validateAndSetRequestOptionsBasedOnAuthType(
sourceOptions,
context,

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@
},
"dependencies": {
"@tooljet-plugins/common": "file:../common",
"form-data": "^4.0.0",
"got": "^11.8.6",
"react": "^17.0.2",
"rimraf": "^3.0.2",

View file

@ -1,4 +1,5 @@
import {
createBucket,
getObject,
uploadObject,
listBuckets,
@ -23,6 +24,9 @@ export default class S3QueryService implements QueryService {
try {
switch (operation) {
case Operation.CreateBucket:
result = await createBucket(client, queryOptions);
break;
case Operation.ListBuckets:
result = await listBuckets(client, {});
break;

View file

@ -13,6 +13,10 @@
"type": "dropdown-component-flip",
"description": "Single select dropdown for operation",
"list": [
{
"value": "create_bucket",
"name": "Create a new bucket"
},
{
"value": "get_object",
"name": "Read object"
@ -43,6 +47,19 @@
}
]
},
"create_bucket": {
"bucket": {
"label": "Bucket Name",
"key": "bucket",
"type": "codehinter",
"lineNumbers": false,
"description": "Enters a name for the new bucket",
"width": "320px",
"height": "36px",
"className": "codehinter-plugins",
"placeholder": "Enter New Bucket Name"
}
},
"get_object": {
"bucket": {
"label": "Bucket",

View file

@ -4,6 +4,7 @@ import {
PutObjectCommand,
DeleteObjectCommand,
S3Client,
CreateBucketCommand,
ListObjectsV2Command,
} from '@aws-sdk/client-s3';
// https://aws.amazon.com/blogs/developer/generate-presigned-url-modular-aws-sdk-javascript/
@ -37,7 +38,12 @@ export async function signedUrlForGet(client: S3Client, options: QueryOptions):
});
return { url };
}
export async function createBucket(client: S3Client, options: QueryOptions): Promise<object> {
const createBucketCommand = new CreateBucketCommand({
Bucket: options.bucket,
});
return await client.send(createBucketCommand);
}
export async function getObject(client: S3Client, options: QueryOptions): Promise<object> {
// Create a helper function to convert a ReadableStream to a string.
const streamToString = (stream) =>

View file

@ -19,6 +19,7 @@ export type QueryOptions = {
};
export enum Operation {
CreateBucket = 'create_bucket',
ListBuckets = 'list_buckets',
ListObjects = 'list_objects',
GetObject = 'get_object',

View file

@ -52,9 +52,12 @@ export default class Snowflake implements QueryService {
}
async testConnection(sourceOptions: SourceOptions): Promise<ConnectionTestResult> {
await this.getConnection(sourceOptions, {}, false);
const connection = await this.getConnection(sourceOptions, {}, false);
const isConnectionValid = await connection.isValidAsync();
return { status: 'ok' };
if (isConnectionValid) return { status: 'ok' };
throw new Error('Connection is invalid');
}
async connAsync(connection: snowflake.Connection) {
@ -76,6 +79,7 @@ export default class Snowflake implements QueryService {
schema: sourceOptions.schema,
role: sourceOptions.role,
clientSessionKeepAlive: true,
clientSessionKeepAliveHeartbeatFrequency: 900,
});
return await this.connAsync(connection);
@ -91,7 +95,7 @@ export default class Snowflake implements QueryService {
if (checkCache) {
let connection = await getCachedConnection(dataSourceId, dataSourceUpdatedAt);
if (connection) {
if (connection && (await connection.isValidAsync())) {
return connection;
} else {
connection = await this.buildConnection(sourceOptions);

View file

@ -18,8 +18,8 @@
"homepage": "https://github.com/tooljet/tooljet#readme",
"dependencies": {
"@tooljet-plugins/common": "file:../common",
"@types/snowflake-sdk": "^1.6.12",
"@types/snowflake-sdk": "^1.6.17",
"react": "^17.0.2",
"snowflake-sdk": "^1.6.23"
"snowflake-sdk": "^1.9.1"
}
}

View file

@ -1 +1 @@
2.24.5
2.25.0

View file

@ -79,6 +79,7 @@ function buildToolJetDbConnectionOptions(data): TypeOrmModuleOptions {
password: data.TOOLJET_DB_PASS,
host: data.TOOLJET_DB_HOST,
connectTimeoutMS: 5000,
logging: data.ORM_LOGGING || false,
extra: {
max: 25,
},

6522
server/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,7 @@
"start:dev": "NODE_ENV=development nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "NODE_ENV=production node dist/src/main",
"test": "NODE_ENV=test jest",
"test": "NODE_ENV=test jest --config jest.config.ts",
"test:watch": "NODE_ENV=test jest --watch",
"test:cov": "NODE_ENV=test jest --coverage",
"test:debug": "NODE_ENV=test node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
@ -117,12 +117,12 @@
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-jest": "^24.4.2",
"eslint-plugin-prettier": "^3.4.1",
"jest": "^27.0.6",
"jest": "^29.7.0",
"prettier": "^2.3.2",
"preview-email": "^3.0.19",
"rimraf": "^3.0.2",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
"ts-jest": "^29.1.1",
"ts-loader": "^9.2.3",
"typescript": "^4.3.5"
},

View file

@ -14,6 +14,7 @@ import {
UseInterceptors,
UploadedFile,
BadRequestException,
UseFilters,
} from '@nestjs/common';
import { Express } from 'express';
import { JwtAuthGuard } from 'src/modules/auth/jwt-auth.guard';
@ -29,16 +30,23 @@ import { CreatePostgrestTableDto, RenamePostgrestTableDto, PostgrestTableColumnD
import { OrganizationAuthGuard } from 'src/modules/auth/organization-auth.guard';
import { FileInterceptor } from '@nestjs/platform-express';
import { TooljetDbBulkUploadService } from '@services/tooljet_db_bulk_upload.service';
import { TooljetDbJoinDto } from '@dto/tooljet-db-join.dto';
import { TooljetDbJoinExceptionFilter } from 'src/filters/tooljetdb-join-exceptions-filter';
import { Logger } from 'nestjs-pino';
const MAX_CSV_FILE_SIZE = 1024 * 1024 * 2; // 2MB
@Controller('tooljet-db')
export class TooljetDbController {
private readonly pinoLogger: Logger;
constructor(
private readonly tooljetDbService: TooljetDbService,
private readonly postgrestProxyService: PostgrestProxyService,
private readonly tooljetDbBulkUploadService: TooljetDbBulkUploadService
) {}
private readonly tooljetDbBulkUploadService: TooljetDbBulkUploadService,
private readonly logger: Logger
) {
this.pinoLogger = logger;
}
@All('/proxy/*')
@UseGuards(OrganizationAuthGuard, TooljetDbGuard)
@ -136,11 +144,12 @@ export class TooljetDbController {
}
@Post('/organizations/:organizationId/join')
@UseFilters(new TooljetDbJoinExceptionFilter())
@UseGuards(TooljetDbGuard)
@CheckPolicies((ability: TooljetDbAbility) => ability.can(Action.JoinTables, 'all'))
async joinTables(@Body() joinQueryJsonDto: any, @Param('organizationId') organizationId) {
async joinTables(@Body() tooljetDbJoinDto: TooljetDbJoinDto, @Param('organizationId') organizationId) {
const params = {
joinQueryJson: { ...joinQueryJsonDto },
joinQueryJson: { ...tooljetDbJoinDto },
};
const result = await this.tooljetDbService.perform(organizationId, 'join_tables', params);

View file

@ -0,0 +1,160 @@
import { IsString, IsArray, ValidateNested, IsIn, IsOptional, IsObject, IsNotEmpty } from 'class-validator';
import { Type } from 'class-transformer';
// TODO: We need to remove custom error messages and make use of dto
// default errors and let frontend show the errors on the specific fields
class Table {
@IsString()
@IsNotEmpty({ message: '::Table name for join not selected' })
name: string;
@IsString()
@IsNotEmpty({ message: '::Table type for join not selected' })
type: string;
}
class Field {
@IsString()
@IsNotEmpty({ message: '::Columns names for join not selected' })
name: string;
@IsString()
@IsNotEmpty({ message: '::Table names for join not selected' })
table: string;
}
class Conditions {
@IsString()
@IsIn(['AND', 'OR'], { message: '::Operator for condition not selected (AND | OR)' })
@IsOptional()
operator: string;
@IsArray()
@ValidateNested({ each: true })
@Type(() => ConditionsList)
conditionsList: ConditionsList[];
}
class ConditionField {
@IsString()
@IsIn(['Column', 'Value'], { message: '::Condition parameter not specified' })
type: string;
@IsOptional() // present only when type is value
value: unknown;
@IsString()
@IsOptional() // present only when type is column
table: string;
@IsString()
@IsOptional() // present only when type is column
columnName: string;
}
class ConditionsList {
@IsObject()
@IsNotEmpty({ message: '::Condition value is empty' })
@ValidateNested()
@Type(() => ConditionField)
leftField: ConditionField;
@IsString()
@IsIn(['=', '>', '>=', '<', '<=', '!=', 'LIKE', 'NOT LIKE', 'ILIKE', 'NOT ILIKE', '~', '~*', 'IN', 'NOT IN', 'IS'], {
message: '::Condition operator not selected',
})
operator: string;
@IsObject()
@IsNotEmpty({ message: '::Condition value is empty' })
@ValidateNested()
@Type(() => ConditionField)
rightField: ConditionField;
@ValidateNested()
@Type(() => Conditions)
@IsOptional()
conditions: Conditions;
}
class Join {
@IsString()
@IsIn(['INNER', 'LEFT', 'RIGHT', 'FULL OUTER'], { message: '::Join type not selected' })
joinType: string;
@IsString()
@IsNotEmpty({ message: '::Join table is not selected' })
table: string;
@ValidateNested()
@IsNotEmpty({ message: '::Join condition is not selected' })
@Type(() => Conditions)
conditions: Conditions;
}
class GroupBy {
@IsString()
@IsNotEmpty()
table: string;
@IsString()
@IsNotEmpty()
columnName: string;
}
class Order {
@IsString()
@IsNotEmpty({ message: '::Sort column not selected' })
columnName: string;
@IsString()
@IsNotEmpty({ message: '::Sort table not selected' })
table: string;
@IsIn(['ASC', 'DESC'], { message: '::Sort direction not selected' })
direction: string;
}
export class TooljetDbJoinDto {
@ValidateNested()
@Type(() => Table)
@IsNotEmpty({ message: '::Join table is empty' })
from: Table;
@IsArray()
@ValidateNested({ each: true })
@Type(() => Field)
@IsNotEmpty({ message: '::Join fields are empty' })
fields: Field[];
@IsArray()
@ValidateNested({ each: true })
@Type(() => Join)
@IsNotEmpty({ message: '::Join parameters are empty' })
joins: Join[];
@ValidateNested()
@Type(() => Conditions)
@IsOptional()
conditions: Conditions;
@IsArray()
@ValidateNested({ each: true })
@Type(() => GroupBy)
@IsOptional()
group_by: GroupBy[];
@IsArray()
@ValidateNested({ each: true })
@Type(() => Order)
@IsOptional()
order_by: Order[];
@IsString()
@IsOptional()
limit: string;
@IsString()
@IsOptional()
offset: string;
}

View file

@ -0,0 +1,17 @@
import { Catch, ArgumentsHost, ExceptionFilter, BadRequestException } from '@nestjs/common';
@Catch(BadRequestException)
export class TooljetDbJoinExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const next = host.switchToHttp().getNext();
if (Array.isArray(exception.response.message)) {
const totalErrors = exception.response.message.length;
const firstErrorMessage = exception.response.message[0].split('::')[1];
const strippedErrorMessage = `Error: ${firstErrorMessage} (1/${totalErrors})`;
exception.response.message = strippedErrorMessage;
}
next(exception);
}
}

View file

@ -7,7 +7,7 @@ import { AppModule } from './app.module';
import * as helmet from 'helmet';
import { Logger } from 'nestjs-pino';
import { urlencoded, json } from 'express';
import { AllExceptionsFilter } from './all-exceptions-filter';
import { AllExceptionsFilter } from './filters/all-exceptions-filter';
import { RequestMethod, ValidationPipe, VersioningType, VERSION_NEUTRAL } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { bootstrap as globalAgentBootstrap } from 'global-agent';

View file

@ -20,7 +20,6 @@ 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';
import { PostgrestProxyService } from '@services/postgrest_proxy.service';
const imports = [
PluginsModule,
@ -46,7 +45,6 @@ if (process.env.ENABLE_TOOLJET_DB === 'true') {
PluginsHelper,
AppsService,
CredentialsService,
PostgrestProxyService,
],
exports: [ImportExportResourcesService],
})

View file

@ -1515,8 +1515,90 @@ export class AppImportExportService {
);
}
// Entire function should be santised for Undefined values
replaceTooljetDbTableIds(queryOptions: any, tooljetDatabaseMapping: any) {
return { ...queryOptions, table_id: tooljetDatabaseMapping[queryOptions.table_id]?.id };
if (queryOptions?.operation && queryOptions.operation === 'join_tables') {
const joinOptions = { ...(queryOptions?.join_table ?? {}) };
// JOIN Section
if (joinOptions?.joins && joinOptions.joins.length > 0) {
const joinsTableIdUpdatedList = joinOptions.joins.map((joinCondition) => {
const updatedJoinCondition = { ...joinCondition };
// Updating Join tableId
if (updatedJoinCondition.table)
updatedJoinCondition.table =
tooljetDatabaseMapping[updatedJoinCondition.table]?.id ?? updatedJoinCondition.table;
// Updating TableId on Conditions in Join Query
if (updatedJoinCondition.conditions) {
const updatedJoinConditionFilter = this.updateNewTableIdForFilter(
updatedJoinCondition.conditions,
tooljetDatabaseMapping
);
updatedJoinCondition.conditions = updatedJoinConditionFilter.conditions;
}
return updatedJoinCondition;
});
joinOptions.joins = joinsTableIdUpdatedList;
}
// Filter Section
if (joinOptions?.conditions) {
joinOptions.conditions = this.updateNewTableIdForFilter(
joinOptions.conditions,
tooljetDatabaseMapping
).conditions;
}
// Select Section
if (joinOptions?.fields) {
joinOptions.fields = joinOptions.fields.map((eachField) => {
if (eachField.table) {
eachField.table = tooljetDatabaseMapping[eachField.table]?.id ?? eachField.table;
return eachField;
}
return eachField;
});
}
// From Section
if (joinOptions?.from) {
const { name = '' } = joinOptions.from;
joinOptions.from = { ...joinOptions.from, name: tooljetDatabaseMapping[name]?.id ?? name };
}
// Sort Section
if (joinOptions?.order_by) {
joinOptions.order_by = joinOptions.order_by.map((eachOrderBy) => {
if (eachOrderBy.table) {
eachOrderBy.table = tooljetDatabaseMapping[eachOrderBy.table]?.id ?? eachOrderBy.table;
return eachOrderBy;
}
return eachOrderBy;
});
}
return { ...queryOptions, table_id: tooljetDatabaseMapping[queryOptions.table_id]?.id, join_table: joinOptions };
} else {
return { ...queryOptions, table_id: tooljetDatabaseMapping[queryOptions.table_id]?.id };
}
}
updateNewTableIdForFilter(joinConditions, tooljetDatabaseMapping) {
const { conditionsList = [] } = { ...joinConditions };
const updatedConditionList = conditionsList.map((condition) => {
if (condition.conditions) {
return this.updateNewTableIdForFilter(condition.conditions, tooljetDatabaseMapping);
} else {
const { operator = '=', leftField = {}, rightField = {} } = { ...condition };
if (leftField?.type && leftField.type === 'Column')
leftField['table'] = tooljetDatabaseMapping[leftField.table]?.id ?? leftField.table;
if (rightField?.type && rightField.type === 'Column')
rightField['table'] = tooljetDatabaseMapping[rightField.table]?.id ?? rightField.table;
return { operator, leftField, rightField };
}
});
return { conditions: { ...joinConditions, conditionsList: [...updatedConditionList] } };
}
async updateEventActionsForNewVersionWithNewMappingIds(

View file

@ -983,9 +983,28 @@ export class AppsService {
.andWhere('data_sources.kind = :kind', { kind: 'tooljetdb' })
.getMany();
const uniqTableIds = [...new Set(tooljetDbDataQueries.map((dq) => dq.options['table_id']))];
const uniqTableIds = new Set();
tooljetDbDataQueries.forEach((dq) => {
if (dq.options?.operation === 'join_tables') {
const joinOptions = dq.options?.join_table?.joins ?? [];
(joinOptions || []).forEach((join) => {
const { table, conditions } = join;
if (table) uniqTableIds.add(table);
conditions?.conditionsList?.forEach((condition) => {
const { leftField, rightField } = condition;
if (leftField?.table) {
uniqTableIds.add(leftField?.table);
}
if (rightField?.table) {
uniqTableIds.add(rightField?.table);
}
});
});
}
if (dq.options.table_id) uniqTableIds.add(dq.options.table_id);
});
return uniqTableIds.map((table_id) => {
return [...uniqTableIds].map((table_id) => {
return { table_id };
});
});

View file

@ -14,6 +14,7 @@ import { EncryptionService } from './encryption.service';
import { App } from 'src/entities/app.entity';
import { AppEnvironmentService } from './app_environments.service';
import { dbTransactionWrap } from 'src/helpers/utils.helper';
import allPlugins from '@tooljet/plugins/dist/server';
import { DataSourceScopes } from 'src/helpers/data_source.constants';
import { EventHandler } from 'src/entities/event_handler.entity';
@ -351,6 +352,22 @@ export class DataQueriesService {
}
};
/* this function only for getting auth token for googlesheets and related plugins*/
async fetchAPITokenFromPlugins(dataSource: DataSource, code: string, sourceOptions: any) {
const queryService = new allPlugins[dataSource.kind]();
const accessDetails = await queryService.accessDetailsFrom(code, sourceOptions);
const options = [];
for (const row of accessDetails) {
const option = {};
option['key'] = row[0];
option['value'] = row[1];
option['encrypted'] = true;
options.push(option);
}
return options;
}
/* This function fetches access token from authorization code */
async authorizeOauth2(
dataSource: DataSource,
@ -360,22 +377,27 @@ export class DataQueriesService {
organizationId?: string
): Promise<void> {
const sourceOptions = await this.parseSourceOptions(dataSource.options, organizationId, environmentId);
const isMultiAuthEnabled = dataSource.options['multiple_auth_enabled']?.value;
const newToken = await this.fetchOAuthToken(sourceOptions, code, userId, isMultiAuthEnabled);
const tokenData = this.getCurrentToken(
isMultiAuthEnabled,
dataSource.options['tokenData']?.value,
newToken,
userId
);
let tokenOptions: any;
if (['googlesheets', 'slack', 'zendesk'].includes(dataSource.kind)) {
tokenOptions = await this.fetchAPITokenFromPlugins(dataSource, code, sourceOptions);
} else {
const isMultiAuthEnabled = dataSource.options['multiple_auth_enabled']?.value;
const newToken = await this.fetchOAuthToken(sourceOptions, code, userId, isMultiAuthEnabled);
const tokenData = this.getCurrentToken(
isMultiAuthEnabled,
dataSource.options['tokenData']?.value,
newToken,
userId
);
const tokenOptions = [
{
key: 'tokenData',
value: tokenData,
encrypted: false,
},
];
tokenOptions = [
{
key: 'tokenData',
value: tokenData,
encrypted: false,
},
];
}
await this.dataSourcesService.updateOptions(dataSource.id, tokenOptions, organizationId, environmentId);
return;

View file

@ -411,7 +411,9 @@ export class DataSourcesService {
for (const option of optionsWithOauth) {
if (option['encrypted']) {
const existingCredentialId =
dataSource.options[option['key']] && dataSource.options[option['key']]['credential_id'];
dataSource?.options &&
dataSource.options[option['key']] &&
dataSource.options[option['key']]['credential_id'];
if (existingCredentialId) {
(option['value'] || option['value'] === '') &&

View file

@ -1,8 +1,8 @@
import { BadRequestException, HttpException, Injectable, NotFoundException, Optional } from '@nestjs/common';
import { EntityManager, In, QueryFailedError } from 'typeorm';
import { EntityManager, In, ObjectLiteral, QueryFailedError, SelectQueryBuilder, TypeORMError } from 'typeorm';
import { InjectEntityManager } from '@nestjs/typeorm';
import { InternalTable } from 'src/entities/internal_table.entity';
import { isString, isEmpty } from 'lodash';
import { isString, isEmpty, camelCase } from 'lodash';
export type TableColumnSchema = {
column_name: string;
@ -17,6 +17,25 @@ export type TableColumnSchema = {
export type SupportedDataTypes = 'character varying' | 'integer' | 'bigint' | 'serial' | 'double precision' | 'boolean';
// Patching TypeORM SelectQueryBuilder to handle for right and full outer joins
declare module 'typeorm' {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface SelectQueryBuilder<Entity> {
rightJoin(entityOrProperty: string, alias: string, condition?: string, parameters?: ObjectLiteral): this;
fullOuterJoin(entityOrProperty: string, alias: string, condition?: string, parameters?: ObjectLiteral): this;
}
}
SelectQueryBuilder.prototype.rightJoin = function (entityOrProperty, alias, condition, parameters) {
this.join('RIGHT', entityOrProperty, alias, condition, parameters);
return this;
};
SelectQueryBuilder.prototype.fullOuterJoin = function (entityOrProperty, alias, condition, parameters) {
this.join('FULL OUTER', entityOrProperty, alias, condition, parameters);
return this;
};
@Injectable()
export class TooljetDbService {
constructor(
@ -288,14 +307,13 @@ export class TooljetDbService {
};
}, {});
const finalQuery = await this.buildJoinQuery(organizationId, joinQueryJson, internalTableIdToNameMap);
try {
return await this.tooljetDbManager.query(finalQuery);
const queryBuilder = this.buildJoinQuery(joinQueryJson, internalTableIdToNameMap);
return await queryBuilder.getRawMany();
} catch (error) {
// custom error handling - for Query error
if (error instanceof QueryFailedError) {
let customErrorMessage: string = (error as QueryFailedError).message;
if (error instanceof QueryFailedError || error instanceof TypeORMError) {
let customErrorMessage: string = error.message;
Object.entries(internalTableIdToNameMap).forEach(([key, value]) => {
customErrorMessage = customErrorMessage.replace(key, value as string);
});
@ -305,137 +323,96 @@ export class TooljetDbService {
}
}
private async buildJoinQuery(_organizationId: string, queryJson, internalTableIdToNameMap) {
// Pending: For Subquery, Alias is its table name. Need to handle it on Internal Table details mapping
// Pending: SELECT Statement - Nested params --> SUM( price * quantity )
private buildJoinQuery(queryJson, internalTableIdToNameMap): SelectQueryBuilder<any> {
const queryBuilder: SelectQueryBuilder<any> = this.tooljetDbManager.createQueryBuilder();
// @description: Only SELECT & FROM statement is Mandatory, else is Optional
let finalQuery = ``;
finalQuery += `SELECT ${await this.constructSelectStatement(queryJson.fields, internalTableIdToNameMap)}`;
finalQuery += `\nFROM ${await this.constructFromStatement(queryJson, internalTableIdToNameMap)}`;
if (queryJson?.joins?.length)
finalQuery += `\n${await this.constructJoinStatements(queryJson.joins, internalTableIdToNameMap)}`;
if (
queryJson?.conditions &&
Object.keys(queryJson?.conditions).length &&
queryJson?.conditions?.conditionsList.length
)
finalQuery += `\nWHERE ${await this.constructWhereStatement(queryJson.conditions, internalTableIdToNameMap)}`;
if (queryJson?.group_by?.length)
finalQuery += `\nGROUP BY ${await this.constructGroupByStatement(queryJson.group_by, internalTableIdToNameMap)}`;
if (queryJson?.having && Object.keys(queryJson?.having).length)
finalQuery += `\nHAVING ${await this.constructWhereStatement(queryJson.having, internalTableIdToNameMap)}`;
if (queryJson?.order_by?.length)
finalQuery += `\nORDER BY ${await this.constructOrderByStatement(queryJson.order_by, internalTableIdToNameMap)}`;
if (queryJson?.limit && queryJson?.limit.length) finalQuery += `\nLIMIT ${queryJson.limit}`;
if (queryJson?.offset && queryJson?.offset.length) finalQuery += `\nOFFSET ${queryJson.offset}`;
// mandatory attributes
if (isEmpty(queryJson.fields)) throw new BadRequestException('Select statement is empty');
if (isEmpty(queryJson.from)) throw new BadRequestException('From table is not selected');
return finalQuery;
// select with aliased column names
queryJson.fields.forEach((field) => {
const fieldName = `"${internalTableIdToNameMap[field.table]}"."${field.name}"`;
const fieldAlias = `${internalTableIdToNameMap[field.table]}_${field.name}`;
queryBuilder.addSelect(fieldName, fieldAlias);
});
// from table
queryBuilder.from(queryJson.from.name, internalTableIdToNameMap[queryJson.from.name]);
// join tables with conditions
queryJson.joins.forEach((join) => {
const joinAlias = internalTableIdToNameMap[join.table];
const conditions = this.constructFilterConditions(join.conditions, internalTableIdToNameMap);
const joinFunction = queryBuilder[camelCase(join.joinType) + 'Join'];
joinFunction.call(queryBuilder, join.table, joinAlias, conditions.query, conditions.params);
});
// conditions
if (queryJson.conditions) {
const conditions = this.constructFilterConditions(queryJson.conditions, internalTableIdToNameMap);
queryBuilder.where(conditions.query, conditions.params);
}
// order by
if (queryJson.order_by) {
queryJson.order_by.forEach((order) => {
const orderByColumn = `"${internalTableIdToNameMap[order.table]}"."${order.columnName}"`;
queryBuilder.addOrderBy(orderByColumn, order.direction as 'ASC' | 'DESC');
});
}
// limit and offset
if (queryJson.limit) queryBuilder.limit(parseInt(queryJson.limit, 10));
if (queryJson.offset) queryBuilder.offset(parseInt(queryJson.offset, 10));
return queryBuilder;
}
// Assuming tableId is being passed, tableName to tableId mapping is removed
private constructSelectStatement(selectStatementInputList, internalTableIdToNameMap) {
if (selectStatementInputList.length) {
const selectQueryFields = selectStatementInputList
.map((field) => {
let fieldExpression = ``;
if (field.function) fieldExpression += `${field.function}(`;
fieldExpression += `${field.table ? '"' + field.table + '"' + '.' : ''}${field.name}`;
if (field.function) fieldExpression += `)`;
if (field.alias) {
fieldExpression += ` AS ${field.alias}`;
} else {
// By Default Alias has been added here for tooljetdb join flow
fieldExpression += ` AS ${internalTableIdToNameMap[field.table]}_${field.name}`;
private constructFilterConditions(conditions, internalTableIdToNameMap) {
let conditionString = '';
const conditionParams = {};
const maybeParameterizeValue = (operator, paramName, value) => {
switch (operator) {
case 'IS':
if (value !== 'NULL' && value !== 'NOT NULL') {
throw new BadRequestException('Invalid value for IS operator. Allowed values are NULL or NOT NULL.');
}
return fieldExpression;
})
.join(', ');
return selectQueryFields;
}
return value;
case 'IN':
if (!Array.isArray(value)) {
throw new BadRequestException('Invalid value for IN operator. Expected an array.');
}
return `(:...${paramName})`;
default:
return `:${paramName}`;
}
};
throw new BadRequestException('Select statement is empty');
}
conditions.conditionsList.forEach((condition, index) => {
const paramName = `${condition.leftField.columnName}_${index}`;
private constructFromStatement(queryJson, _internalTableIdToNameMap) {
const { from } = queryJson;
if (from.name) {
return `${'"' + from.name + '"'} ${from.alias ? from.alias : ''}`;
}
const leftField =
condition.leftField.type == 'Column'
? `"${internalTableIdToNameMap[condition.leftField.table]}"."${condition.leftField.columnName}"`
: `${condition.leftField.columnName}`;
throw new BadRequestException('From table is not selected');
}
const rightField =
condition.rightField.type == 'Column'
? `"${internalTableIdToNameMap[condition.rightField.table]}"."${condition.rightField.columnName}"`
: maybeParameterizeValue(condition.operator, paramName, condition.rightField.value);
private constructJoinStatements(joinsInputList, internalTableIdToNameMap) {
const joinStatementOutput = joinsInputList
.map((joinCondition) => {
const { table, joinType, conditions } = joinCondition;
return `${joinType} JOIN ${'"' + table + '"'} ${
joinCondition.alias ? joinCondition.alias : ''
} ON ${this.constructWhereStatement(conditions, internalTableIdToNameMap)}`;
})
.join('\n');
return joinStatementOutput;
}
conditionString += `${leftField} ${condition.operator} ${rightField}`;
private constructWhereStatement(whereStatementConditions, internalTableIdToNameMap) {
const { operator = 'AND', conditionsList = [] } = whereStatementConditions;
const whereConditionOutput = conditionsList
.map((condition) => {
// @description: Recursive call to build - Sub-condition
if (condition.conditions)
return `(${this.constructWhereStatement(condition.conditions, internalTableIdToNameMap)})`;
// @description: Building a Condition for 'WHERE & HAVING statements' - LHS, operator and RHS
// @description: In LHS & RHS it is not mandatory to provide table name, but column name is mandatory
// @description: In LHS & RHS - We get function only in HAVING statement
const { operator, leftField, rightField } = condition;
// @desc: When 'IS' operator is choosed, 'NULL' & 'NOT NULL' keywords will be provided as value and it should not be converted to string
const keywords = ['NULL', 'NOT NULL'];
conditionParams[paramName] = condition.rightField.value;
let leftSideInput = ``;
if (leftField.type === 'Value') {
const dontAddQuotes =
(keywords.includes(leftField.value) && operator === 'IS') || operator === 'IN' || operator === 'NOT IN';
if (index < conditions.conditionsList.length - 1) {
conditionString += ` ${conditions.operator} `;
}
});
leftSideInput += dontAddQuotes ? leftField.value : this.addQuotesIfString(leftField.value);
} else {
if (leftField.function) leftSideInput += `${leftField.function}(`;
leftSideInput += `${leftField.table ? '"' + leftField.table + '"' + '.' : ''}${leftField.columnName}`;
if (leftField.function) leftSideInput += `)`;
}
let rightSideInput = ``;
if (rightField.type === 'Value') {
const dontAddQuotes =
(keywords.includes(rightField.value) && operator === 'IS') || operator === 'IN' || operator === 'NOT IN';
rightSideInput += dontAddQuotes ? rightField.value : this.addQuotesIfString(rightField.value);
} else {
if (rightField.function) rightSideInput += `${rightField.function}(`;
rightSideInput += `${rightField.table ? '"' + rightField.table + '"' + '.' : ''}${rightField.columnName}`;
if (rightField.function) rightSideInput += `)`;
}
return `${leftSideInput} ${operator} ${rightSideInput}`;
})
.join(` ${operator} `);
return whereConditionOutput;
}
private constructGroupByStatement(groupByInputList, _internalTableIdToNameMap) {
return groupByInputList
.map((groupByInput) => `${'"' + groupByInput.table + '"'}.${groupByInput.columnName}`)
.join(', ');
}
private constructOrderByStatement(orderByInputList, internalTableIdToNameMap) {
// @description: For "ORDER BY" statement table field is optional. But column_name & order_by direction is mandatory
return orderByInputList
.map((orderByInput) => {
const { columnName, direction } = orderByInput;
return `${orderByInput.table ? '"' + orderByInput.table + '"' + '.' : ''}${columnName} ${direction}`;
})
.join(`, `);
return { query: `(${conditionString})`, params: conditionParams };
}
private async findOrFailInternalTableFromTableId(requestedTableIdList: Array<string>, organizationId: string) {

View file

@ -1,7 +1,7 @@
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
import { clearDB, createUser, createNestAppInstanceWithEnvMock, generateRedirectUrl } from '../../test.helper';
import { mocked } from 'ts-jest/utils';
import { mocked } from 'jest-mock';
import got from 'got';
import { Organization } from 'src/entities/organization.entity';
import { Repository } from 'typeorm';
@ -90,8 +90,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
await request(app.getHttpServer())
.post('/api/oauth/sign-in/common/git')
.send({ token, organizationId: current_organization.id })
@ -124,8 +124,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
await request(app.getHttpServer())
.post('/api/oauth/sign-in/common/git')
.send({ token, organizationId: current_organization.id })
@ -171,8 +171,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
await request(app.getHttpServer()).post('/api/oauth/sign-in/common/git').send({ token }).expect(401);
});
@ -215,8 +215,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
await request(app.getHttpServer()).post('/api/oauth/sign-in/common/git').send({ token }).expect(401);
});
@ -261,8 +261,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
await request(app.getHttpServer())
.post('/api/oauth/sign-in/common/git')
@ -310,8 +310,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer()).post('/api/oauth/sign-in/common/git').send({ token });
@ -348,8 +348,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer())
.post('/api/oauth/sign-in/common/git')
@ -386,8 +386,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer()).post('/api/oauth/sign-in/common/git').send({ token });
@ -426,8 +426,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer())
.post('/api/oauth/sign-in/common/git')
@ -467,8 +467,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer())
.post('/api/oauth/sign-in/common/git')
@ -514,8 +514,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer()).post('/api/oauth/sign-in/common/git').send({ token });
@ -563,8 +563,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer())
.post('/api/oauth/sign-in/common/git')
@ -615,8 +615,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer()).post('/api/oauth/sign-in/common/git').send({ token });
@ -666,8 +666,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer())
.post('/api/oauth/sign-in/common/git')
@ -733,8 +733,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer()).post('/api/oauth/sign-in/common/git').send({ token });
@ -801,8 +801,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer())
.post('/api/oauth/sign-in/common/git')

View file

@ -1,7 +1,7 @@
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
import { clearDB, createUser, createNestAppInstanceWithEnvMock, generateRedirectUrl } from '../../test.helper';
import { mocked } from 'ts-jest/utils';
import { mocked } from 'jest-mock';
import got from 'got';
import { Organization } from 'src/entities/organization.entity';
import { Repository } from 'typeorm';
@ -97,8 +97,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
await request(app.getHttpServer())
.post('/api/oauth/sign-in/' + sso_configs.id)
.send({ token })
@ -131,8 +131,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
await request(app.getHttpServer())
.post('/api/oauth/sign-in/' + sso_configs.id)
@ -166,8 +166,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer())
.post('/api/oauth/sign-in/' + sso_configs.id)
@ -209,8 +209,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer())
.post('/api/oauth/sign-in/' + sso_configs.id)
@ -249,8 +249,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer())
.post('/api/oauth/sign-in/' + sso_configs.id)
@ -288,8 +288,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer())
.post('/api/oauth/sign-in/' + sso_configs.id)
@ -346,9 +346,9 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
mockedGot.mockImplementationOnce(gitGetUserEmailResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserEmailResponse);
const response = await request(app.getHttpServer())
.post('/api/oauth/sign-in/' + sso_configs.id)
@ -395,8 +395,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer())
.post('/api/oauth/sign-in/' + sso_configs.id)
@ -446,8 +446,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer())
.post('/api/oauth/sign-in/' + sso_configs.id)
@ -502,8 +502,8 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer())
.post('/api/oauth/sign-in/' + sso_configs.id)
@ -571,9 +571,9 @@ describe('oauth controller', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
mockedGot.mockImplementationOnce(gitGetUserEmailResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserEmailResponse);
const response = await request(app.getHttpServer())
.post('/api/oauth/sign-in/' + sso_configs.id)

View file

@ -16,11 +16,9 @@ import {
verifyInviteToken,
} from '../../test.helper';
import { getManager, Repository } from 'typeorm';
import { mocked } from 'ts-jest/utils';
import got from 'got';
jest.mock('got');
const mockedGot = mocked(got);
const mockedGot = jest.createMockFromModule('got');
describe.skip('Git Onboarding', () => {
let app: INestApplication;
@ -83,8 +81,8 @@ describe.skip('Git Onboarding', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer()).post('/api/oauth/sign-in/common/git').send({ token });
@ -287,8 +285,8 @@ describe.skip('Git Onboarding', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer()).post('/api/oauth/sign-in/common/git').send({ token });
@ -333,8 +331,8 @@ describe.skip('Git Onboarding', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer()).post('/api/oauth/sign-in/common/git').send({ token });
@ -430,8 +428,8 @@ describe.skip('Git Onboarding', () => {
};
});
mockedGot.mockImplementationOnce(gitAuthResponse);
mockedGot.mockImplementationOnce(gitGetUserResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitAuthResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(gitGetUserResponse);
const response = await request(app.getHttpServer()).post('/api/oauth/sign-in/common/git').send({ token });

View file

@ -3,7 +3,7 @@ import { INestApplication } from '@nestjs/common';
import { authHeaderForUser, clearDB, createUser, createNestAppInstanceWithEnvMock } from '../test.helper';
import { getManager, QueryFailedError } from 'typeorm';
import { InternalTable } from 'src/entities/internal_table.entity';
import { mocked } from 'ts-jest/utils';
import { mocked } from 'jest-mock';
import got from 'got';
jest.mock('got');
@ -111,7 +111,7 @@ describe.skip('Tooljet DB controller', () => {
};
});
mockedGot.mockImplementationOnce(postgrestResponse);
(mockedGot as unknown as jest.Mock).mockImplementationOnce(postgrestResponse);
const response = await request(nestApp.getHttpServer())
.get(

View file

@ -22,7 +22,7 @@ import { ThreadRepository } from 'src/repositories/thread.repository';
import { GroupPermission } from 'src/entities/group_permission.entity';
import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
import { AllExceptionsFilter } from 'src/all-exceptions-filter';
import { AllExceptionsFilter } from 'src/filters/all-exceptions-filter';
import { Logger } from 'nestjs-pino';
import { WsAdapter } from '@nestjs/platform-ws';
import { AppsModule } from 'src/modules/apps/apps.module';