mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 09:28:31 +00:00
commit
469ce6306b
72 changed files with 7848 additions and 6639 deletions
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
2.24.5
|
||||
2.25.0
|
||||
|
|
|
|||
|
|
@ -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"]',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"]',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 won’t be able to recognise files in any other format.",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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?",
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@ export const multiselectText = {
|
|||
noEventsMessage: "No event handlers",
|
||||
dropdwonOptionSelectAll: "Select All",
|
||||
|
||||
enableSelectAllOptions: "Enable select All option",
|
||||
enableSelectAllOptions: "Enable select all option",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
BIN
docs/static/img/datasource-reference/rest-api/multipart-form-data.png
vendored
Normal file
BIN
docs/static/img/datasource-reference/rest-api/multipart-form-data.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
|
|
@ -1 +1 @@
|
|||
2.24.5
|
||||
2.25.0
|
||||
|
|
|
|||
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
: {}),
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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
5136
plugins/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -20,6 +20,7 @@ import {
|
|||
getAuthUrl,
|
||||
sanitizeCustomParams,
|
||||
checkIfContentTypeIsURLenc,
|
||||
checkIfContentTypeIsMultipartFormData,
|
||||
validateAndSetRequestOptionsBasedOnAuthType,
|
||||
} from './oauth';
|
||||
|
||||
|
|
@ -43,6 +44,7 @@ export {
|
|||
sanitizeHeaders,
|
||||
sanitizeSearchParams,
|
||||
checkIfContentTypeIsURLenc,
|
||||
checkIfContentTypeIsMultipartFormData,
|
||||
validateAndSetRequestOptionsBasedOnAuthType,
|
||||
fetchHttpsCertsForCustomCA,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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] : {}));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
1217
plugins/packages/restapi/package-lock.json
generated
1217
plugins/packages/restapi/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export type QueryOptions = {
|
|||
};
|
||||
|
||||
export enum Operation {
|
||||
CreateBucket = 'create_bucket',
|
||||
ListBuckets = 'list_buckets',
|
||||
ListObjects = 'list_objects',
|
||||
GetObject = 'get_object',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2.24.5
|
||||
2.25.0
|
||||
|
|
|
|||
|
|
@ -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
6522
server/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
160
server/src/dto/tooljet-db-join.dto.ts
Normal file
160
server/src/dto/tooljet-db-join.dto.ts
Normal 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;
|
||||
}
|
||||
17
server/src/filters/tooljetdb-join-exceptions-filter.ts
Normal file
17
server/src/filters/tooljetdb-join-exceptions-filter.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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'] === '') &&
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue