diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js index 19b06dddac..072e8d0019 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/Bind_TableTextPagination_spec.js @@ -23,7 +23,7 @@ describe("Test Create Api and Bind to Table widget", function() { /**Bind Table with Textwidget with selected row */ cy.SearchEntityandOpen("Text1"); cy.testJsontext("text", "{{Table1.selectedRow.url}}"); - cy.get(commonlocators.editPropCrossButton).click(); + cy.SearchEntityandOpen("Table1"); cy.readTabledata("0", "0").then(tabData => { const tableData = tabData; localStorage.setItem("tableDataPage1", tableData); diff --git a/app/client/cypress/manual_TestSuite/Deletion _of_Duplicate_App.js b/app/client/cypress/manual_TestSuite/Deletion _of_Duplicate_App.js new file mode 100644 index 0000000000..bc2aa2edfa --- /dev/null +++ b/app/client/cypress/manual_TestSuite/Deletion _of_Duplicate_App.js @@ -0,0 +1,19 @@ +const homePage = require("../../../locators/HomePage.json"); + + +describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() { + it("Duplicating an application", function() + { + // Navigate to home Page + // Click on any application action icon (Three dots) + // Click on "Duplicate" option + // Ensure the application gets copied + // Click on "Appsmith" to navigate to homepage + // Click on action icon + // Click on Delete option + // Click on "Are You Sure?" option + // Ensure the App gets deleted + } + ) +} +) \ No newline at end of file diff --git a/app/client/cypress/manual_TestSuite/Duplicate_App.js b/app/client/cypress/manual_TestSuite/Duplicate_App.js new file mode 100644 index 0000000000..46134860f6 --- /dev/null +++ b/app/client/cypress/manual_TestSuite/Duplicate_App.js @@ -0,0 +1,15 @@ +const homePage = require("../../../locators/HomePage.json"); + + +describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() { + it("Duplicating an application", function() + { + // Navigate to home Page + // Click on any application action icon (Three dots) + // Click on "Duplicate" option + // Ensure the application gets copied + // Ensure the name is appended with the word "Copy" + } + ) +} +) \ No newline at end of file diff --git a/app/client/cypress/manual_TestSuite/Duplicate_App_Spec.js b/app/client/cypress/manual_TestSuite/Duplicate_App_Spec.js new file mode 100644 index 0000000000..c724d651c8 --- /dev/null +++ b/app/client/cypress/manual_TestSuite/Duplicate_App_Spec.js @@ -0,0 +1,28 @@ +const homePage = require("../../../locators/HomePage.json"); + + +describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() { + it("Duplicating an application", function() + { + // Navigate to home Page + // Click on any application action icon (Three dots) + // Click on "Duplicate" option + // Ensure the application gets copied + // Ensure the name is appended with the word "Copy" + } + ) + it("Deleting the duplicated Application ", function() + { + // Navigate to home Page + // Click on any application action icon (Three dots) + // Click on "Duplicate" option + // Ensure the application gets copied + // Click on "Appsmith" to navigate to homepage + // Click on action icon + // Click on Delete option + // Click on "Are You Sure?" option + // Ensure the App gets deleted + } + ) +} +) \ No newline at end of file diff --git a/app/client/cypress/manual_TestSuite/Org_Logo_Del.js b/app/client/cypress/manual_TestSuite/Org_Logo_Del.js new file mode 100644 index 0000000000..9f90bcf735 --- /dev/null +++ b/app/client/cypress/manual_TestSuite/Org_Logo_Del.js @@ -0,0 +1,19 @@ +const homePage = require("../../../locators/HomePage.json"); + + +describe("Deletion of organisational Logo ", function() { + it(" org logo upload ", function() + { + //Click on the dropdown next to organisational Name + // Navigate between tabs + // Naviagte to General Tab + // Add an Organisational Logo + // Wait until it loads + // Switch between Tabs + // Click on the remove Icon + //Ensure the organisational Logo is deleted + } + ) +} +) + diff --git a/app/client/cypress/manual_TestSuite/Org_Logo_Set.js b/app/client/cypress/manual_TestSuite/Org_Logo_Set.js new file mode 100644 index 0000000000..9ef5c824ef --- /dev/null +++ b/app/client/cypress/manual_TestSuite/Org_Logo_Set.js @@ -0,0 +1,18 @@ +const homePage = require("../../../locators/HomePage.json"); + + +describe("insert organisational Logo ", function() { + it(" org logo upload ", function() + { + //Click on the dropdown next to organisational Name + // Navigate between tabs + // Naviagte to General Tab + // Add an Organisational Logo + //Wait until it loads + // Switch between Tabs + // Navigate to General Tab and ensure the logo exsits + //navigate back to Homepage + } + ) +} +) \ No newline at end of file diff --git a/app/client/cypress/manual_TestSuite/Organisation_Name.js b/app/client/cypress/manual_TestSuite/Organisation_Name.js new file mode 100644 index 0000000000..f4314ed760 --- /dev/null +++ b/app/client/cypress/manual_TestSuite/Organisation_Name.js @@ -0,0 +1,15 @@ +const homePage = require("../../../locators/HomePage.json"); + + +describe("Checking for error message on Organisation Name ", function() { + it("Ensure of Inactive Submit button ", function() + { + // Navigate to home Page + // Click on Create Organisation + // Type "Space" as first character + // Ensure "Submit" button does not get Active + // Now click on "X" (Close icon) ensure the pop up closes + } + ) +} +) \ No newline at end of file diff --git a/app/client/cypress/manual_TestSuite/Organisation_Name_Spec.js b/app/client/cypress/manual_TestSuite/Organisation_Name_Spec.js new file mode 100644 index 0000000000..88ca89850f --- /dev/null +++ b/app/client/cypress/manual_TestSuite/Organisation_Name_Spec.js @@ -0,0 +1,36 @@ +const homePage = require("../../../locators/HomePage.json"); + + +describe("Checking for error message on Organisation Name ", function() { + it("Ensure of Inactive Submit button ", function() + { + // Navigate to home Page + // Click on Create Organisation + // Type "Space" as first character + // Ensure "Submit" button does not get Active + // Now click on "X" (Close icon) ensure the pop up closes + } + ) + it("Reuse the name of the deleted application name ", function() + { + // Navigate to home Page + // Create an Application by name "XYZ" + // Add some widgets + // Navigate back to the application + // Delete the Application + // Click on "Create New" option under samee organisation + // Enter the name "XYZ" + // Ensure the application can be created with the same name + } + ) + it("Adding Special Character ", function() + { + // Navigate to home Page + // Click on Create Organisation + // Add special as first character + // Ensure "Submit" get Active + // Now click outside and ensure the pop up closes + } + ) +} +) \ No newline at end of file diff --git a/app/client/cypress/manual_TestSuite/Reusing_Name_of_Deleted_App.js b/app/client/cypress/manual_TestSuite/Reusing_Name_of_Deleted_App.js new file mode 100644 index 0000000000..186ba3b586 --- /dev/null +++ b/app/client/cypress/manual_TestSuite/Reusing_Name_of_Deleted_App.js @@ -0,0 +1,18 @@ +const homePage = require("../../../locators/HomePage.json"); + + +describe("Reuse the name of the deleted application name inside the same organisation", function() { + it("Reuse the name of the deleted application name ", function() + { + // Navigate to home Page + // Create an Application by name "XYZ" + // Add some widgets + // Navigate back to the application + // Delete the Application + // Click on "Create New" option under samee organisation + // Enter the name "XYZ" + // Ensure the application can be created with the same name + } + ) +} +) \ No newline at end of file diff --git a/app/client/cypress/manual_TestSuite/Share_User_Icon.js b/app/client/cypress/manual_TestSuite/Share_User_Icon.js new file mode 100644 index 0000000000..dd662a021b --- /dev/null +++ b/app/client/cypress/manual_TestSuite/Share_User_Icon.js @@ -0,0 +1,17 @@ +const homePage = require("../../../locators/HomePage.json"); + + +describe("Shared user icon ", function() { + it(" User Icon is disaplyed to user ", function() + { + // Navigate to home Page + //Click on Share Icon + // Click on Field to add an Email Id + // Click on the Roles field + // Add an role from the Dropdown + // CLick on Invite + //Now observe the icon next to the Share Icon + } + ) +} +) \ No newline at end of file diff --git a/app/client/cypress/manual_TestSuite/Spl_Chracter_Org_Name.js b/app/client/cypress/manual_TestSuite/Spl_Chracter_Org_Name.js new file mode 100644 index 0000000000..195fc06229 --- /dev/null +++ b/app/client/cypress/manual_TestSuite/Spl_Chracter_Org_Name.js @@ -0,0 +1,15 @@ +const homePage = require("../../../locators/HomePage.json"); + + +describe("Adding Special Character ", function() { + it("Adding Special Character ", function() + { + // Navigate to home Page + // Click on Create Organisation + // Add special as first character + // Ensure "Submit" get Active + // Now click outside and ensure the pop up closes + } + ) +} +) \ No newline at end of file diff --git a/app/client/src/components/ads/MenuItem.tsx b/app/client/src/components/ads/MenuItem.tsx index d679ac5bc3..e778e62e0c 100644 --- a/app/client/src/components/ads/MenuItem.tsx +++ b/app/client/src/components/ads/MenuItem.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from "react"; +import React, { forwardRef, ReactNode, Ref } from "react"; import { CommonComponentProps, Classes } from "./common"; import styled from "styled-components"; import Icon, { IconName, IconSize } from "./Icon"; @@ -13,22 +13,31 @@ export type MenuItemProps = CommonComponentProps & { href?: string; type?: "warning"; ellipsize?: number; + selected?: boolean; onSelect?: () => void; }; -const ItemRow = styled.a<{ disabled?: boolean }>` +const ItemRow = styled.a<{ disabled?: boolean; selected?: boolean }>` display: flex; align-items: center; justify-content: space-between; text-decoration: none; padding: 0px ${props => props.theme.spaces[6]}px; + background-color: ${props => + props.selected ? props.theme.colors.menuItem.hoverBg : "transparent"}; .${Classes.TEXT} { - color: ${props => props.theme.colors.menuItem.normalText}; + color: ${props => + props.selected + ? props.theme.colors.menuItem.hoverText + : props.theme.colors.menuItem.normalText}; } .${Classes.ICON} { svg { path { - fill: ${props => props.theme.colors.menuItem.normalIcon}; + fill: ${props => + props.selected + ? props.theme.colors.menuItem.hoverIcon + : props.theme.colors.menuItem.normalIcon}; } } } @@ -78,40 +87,46 @@ const IconContainer = styled.span` margin-right: ${props => props.theme.spaces[5]}px; } `; - -function MenuItem(props: MenuItemProps) { - return props.ellipsize && props.text.length > props.ellipsize ? ( - - - - ) : ( - - ); -} - -function MenuItemContent(props: MenuItemProps) { - return ( - - - {props.icon ? : null} - {props.text ? ( - - {props.ellipsize - ? ellipsize(props.ellipsize, props.text) - : props.text} - - ) : null} - - {props.label ? props.label : null} - - ); -} +const MenuItem = forwardRef( + (props: MenuItemProps, ref: Ref) => { + return props.ellipsize && props.text.length > props.ellipsize ? ( + + + + ) : ( + + ); + }, +); +const MenuItemContent = forwardRef( + (props: MenuItemProps, ref: Ref) => { + return ( + + + {props.icon ? : null} + {props.text ? ( + + {props.ellipsize + ? ellipsize(props.ellipsize, props.text) + : props.text} + + ) : null} + + {props.label ? props.label : null} + + ); + }, +); +MenuItemContent.displayName = "MenuItemContent"; +MenuItem.displayName = "MenuItem"; function ellipsize(length: number, text: string) { return text.length > length ? text.slice(0, length).concat(" ...") : text; diff --git a/app/client/src/components/designSystems/appsmith/header/DeployLinkButton.tsx b/app/client/src/components/designSystems/appsmith/header/DeployLinkButton.tsx index 98fbcb4df9..711a42bbd6 100644 --- a/app/client/src/components/designSystems/appsmith/header/DeployLinkButton.tsx +++ b/app/client/src/components/designSystems/appsmith/header/DeployLinkButton.tsx @@ -88,11 +88,12 @@ export const DeployLinkButton = (props: Props) => { content={ diff --git a/app/client/src/components/editorComponents/ErrorBoundry.tsx b/app/client/src/components/editorComponents/ErrorBoundry.tsx index dd3a256695..caaa0cd71d 100644 --- a/app/client/src/components/editorComponents/ErrorBoundry.tsx +++ b/app/client/src/components/editorComponents/ErrorBoundry.tsx @@ -2,10 +2,10 @@ import React, { ReactNode } from "react"; import styled from "styled-components"; import * as Sentry from "@sentry/react"; -type Props = { isValid: boolean; children: ReactNode }; +type Props = { children: ReactNode }; type State = { hasError: boolean }; -const ErrorBoundaryContainer = styled.div<{ isValid: boolean }>` +const ErrorBoundaryContainer = styled.div` height: 100%; width: 100%; @@ -41,7 +41,7 @@ class ErrorBoundary extends React.Component { render() { return ( - + {this.state.hasError ? (

Oops, Something went wrong. diff --git a/app/client/src/pages/Applications/index.tsx b/app/client/src/pages/Applications/index.tsx index ef021eddc9..3621464441 100644 --- a/app/client/src/pages/Applications/index.tsx +++ b/app/client/src/pages/Applications/index.tsx @@ -1,4 +1,4 @@ -import React, { Component, Fragment, useState } from "react"; +import React, { Component, Fragment, useEffect, useRef, useState } from "react"; import styled from "styled-components"; import { connect, useSelector, useDispatch } from "react-redux"; import { AppState } from "reducers"; @@ -340,6 +340,8 @@ const ApplicationAddCardWrapper = styled(Card)` `; function LeftPane() { + const menuRef = useRef(null); + const [selectedOrg, setSelectedOrg] = useState(""); const fetchedUserOrgs = useSelector(getUserApplicationsOrgs); const isFetchingApplications = useSelector(getIsFetchingApplications); const NewWorkspaceTrigger = ( @@ -359,6 +361,20 @@ function LeftPane() { userOrgs = loadingUserOrgs as any; } + const urlHash = decodeURI( + window.location.hash.substring(1, window.location.hash.length), + ); + + useEffect(() => { + const timer = setTimeout(() => { + if (menuRef && menuRef.current) { + menuRef.current.scrollIntoView({ behavior: "smooth" }); + menuRef.current.click(); + } + }, 0); + return () => clearTimeout(timer); + }, [fetchedUserOrgs]); + return ( ( setSelectedOrg(org.organization.id)} + selected={ + selectedOrg === org.organization.id && + urlHash === org.organization.name + } /> ))} diff --git a/app/client/src/pages/Editor/PropertyPaneTitle.tsx b/app/client/src/pages/Editor/PropertyPaneTitle.tsx index f1393f7aee..cd9abc18a6 100644 --- a/app/client/src/pages/Editor/PropertyPaneTitle.tsx +++ b/app/client/src/pages/Editor/PropertyPaneTitle.tsx @@ -156,6 +156,7 @@ const PropertyPaneTitle = memo((props: PropertyPaneTitleProps) => { } position={Position.TOP} hoverOpenDelay={200} + boundary="window" > diff --git a/app/client/src/pages/Editor/QueryEditor/Table.tsx b/app/client/src/pages/Editor/QueryEditor/Table.tsx index fc2e9b8687..e1c3a1e5fd 100644 --- a/app/client/src/pages/Editor/QueryEditor/Table.tsx +++ b/app/client/src/pages/Editor/QueryEditor/Table.tsx @@ -5,6 +5,7 @@ import styled from "styled-components"; import AutoToolTipComponent from "components/designSystems/appsmith/AutoToolTipComponent"; import { getType, Types } from "utils/TypeHelpers"; import { Colors } from "constants/Colors"; +import ErrorBoundary from "components/editorComponents/ErrorBoundry"; interface TableProps { data: Record[]; @@ -214,59 +215,61 @@ const Table = (props: TableProps) => { if (rows.length === 0 || headerGroups.length === 0) return null; return ( - -

-
- {headerGroups.map((headerGroup: any, index: number) => ( -
- {headerGroup.headers.map((column: any, columnIndex: number) => ( -
+ + +
+
+ {headerGroups.map((headerGroup: any, index: number) => ( +
+ {headerGroup.headers.map((column: any, columnIndex: number) => (
- {column.render("Header")} +
+ {column.render("Header")} +
-
- ))} + ))} +
+ ))} +
+ {rows.map((row: any, index: number) => { + prepareRow(row); + return ( +
+ {row.cells.map((cell: any, cellIndex: number) => { + return ( +
+ + {cell.render("Cell")} + +
+ ); + })} +
+ ); + })}
- ))} -
- {rows.map((row: any, index: number) => { - prepareRow(row); - return ( -
- {row.cells.map((cell: any, cellIndex: number) => { - return ( -
- - {cell.render("Cell")} - -
- ); - })} -
- ); - })}
-
- + + ); }; diff --git a/app/client/src/reducers/evalutationReducers/dependencyReducer.ts b/app/client/src/reducers/evaluationReducers/dependencyReducer.ts similarity index 100% rename from app/client/src/reducers/evalutationReducers/dependencyReducer.ts rename to app/client/src/reducers/evaluationReducers/dependencyReducer.ts diff --git a/app/client/src/reducers/evalutationReducers/index.ts b/app/client/src/reducers/evaluationReducers/index.ts similarity index 100% rename from app/client/src/reducers/evalutationReducers/index.ts rename to app/client/src/reducers/evaluationReducers/index.ts diff --git a/app/client/src/reducers/evalutationReducers/treeReducer.ts b/app/client/src/reducers/evaluationReducers/treeReducer.ts similarity index 76% rename from app/client/src/reducers/evalutationReducers/treeReducer.ts rename to app/client/src/reducers/evaluationReducers/treeReducer.ts index 1379cc14e2..5c95524c7e 100644 --- a/app/client/src/reducers/evalutationReducers/treeReducer.ts +++ b/app/client/src/reducers/evaluationReducers/treeReducer.ts @@ -1,4 +1,4 @@ -import { createReducer } from "utils/AppsmithUtils"; +import { createImmerReducer } from "utils/AppsmithUtils"; import { DataTree } from "entities/DataTree/dataTreeFactory"; import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; @@ -6,7 +6,7 @@ export type EvaluatedTreeState = DataTree; const initialState: EvaluatedTreeState = {}; -const evaluatedTreeReducer = createReducer(initialState, { +const evaluatedTreeReducer = createImmerReducer(initialState, { [ReduxActionTypes.SET_EVALUATED_TREE]: ( state: EvaluatedTreeState, action: ReduxAction, diff --git a/app/client/src/reducers/index.tsx b/app/client/src/reducers/index.tsx index b1fe49e2a0..86e972cbad 100644 --- a/app/client/src/reducers/index.tsx +++ b/app/client/src/reducers/index.tsx @@ -1,7 +1,7 @@ import { combineReducers } from "redux"; import entityReducer from "./entityReducers"; import uiReducer from "./uiReducers"; -import evaluationsReducer from "./evalutationReducers"; +import evaluationsReducer from "./evaluationReducers"; import { reducer as formReducer } from "redux-form"; import { CanvasWidgetsReduxState } from "./entityReducers/canvasWidgetsReducer"; import { EditorReduxState } from "./uiReducers/editorReducer"; @@ -35,8 +35,8 @@ import { PageCanvasStructureReduxState } from "./uiReducers/pageCanvasStructure" import { ConfirmRunActionReduxState } from "./uiReducers/confirmRunActionReducer"; import { AppDataState } from "reducers/entityReducers/appReducer"; import { DatasourceNameReduxState } from "./uiReducers/datasourceNameReducer"; -import { EvaluatedTreeState } from "./evalutationReducers/treeReducer"; -import { EvaluationDependencyState } from "./evalutationReducers/dependencyReducer"; +import { EvaluatedTreeState } from "./evaluationReducers/treeReducer"; +import { EvaluationDependencyState } from "./evaluationReducers/dependencyReducer"; import { PageWidgetsReduxState } from "./uiReducers/pageWidgetsReducer"; const appReducer = combineReducers({ diff --git a/app/client/src/sagas/ActionExecutionSagas.ts b/app/client/src/sagas/ActionExecutionSagas.ts index 8aa9728775..2a9e03cd3f 100644 --- a/app/client/src/sagas/ActionExecutionSagas.ts +++ b/app/client/src/sagas/ActionExecutionSagas.ts @@ -249,11 +249,42 @@ export function* evaluateDynamicBoundValueSaga( const EXECUTION_PARAM_REFERENCE_REGEX = /this.params/g; +/** + * Api1 + * URL: https://example.com/{{Text1.text}} + * Body: { + * "name": "{{this.params.name}}", + * "age": {{this.params.age}}, + * "gender": {{Dropdown1.selectedOptionValue}} + * } + * + * If you call + * Api1.run(undefined, undefined, { name: "Hetu", age: Input1.text }); + * + * executionParams is { name: "Hetu", age: Input1.text } + * bindings is [ + * "Text1.text", + * "Dropdown1.selectedOptionValue", + * "this.params.name", + * "this.params.age", + * ] + * + * Return will be [ + * { key: "Text1.text", value: "updateUser" }, + * { key: "Dropdown1.selectedOptionValue", value: "M" }, + * { key: "this.params.name", value: "Hetu" }, + * { key: "this.params.age", value: 26 }, + * ] + * @param bindings + * @param executionParams + */ export function* getActionParams( bindings: string[] | undefined, executionParams?: Record, ) { if (_.isNil(bindings)) return []; + // This might look like a bug, but isn't. + // We send in stringified executionParams, but get back an object const evaluatedExecutionParams = yield evaluateDynamicBoundValueSaga( JSON.stringify(executionParams), ); diff --git a/app/client/src/sagas/evaluationsSaga.ts b/app/client/src/sagas/evaluationsSaga.ts index c7f7b3cbef..c4bcbc2b34 100644 --- a/app/client/src/sagas/evaluationsSaga.ts +++ b/app/client/src/sagas/evaluationsSaga.ts @@ -117,10 +117,11 @@ export function* evaluateSingleValue( ) { if (evaluationWorker) { const dataTree = yield select(getDataTree); - dataTree[EXECUTION_PARAM_KEY] = executionParams; evaluationWorker.postMessage({ action: EVAL_WORKER_ACTIONS.EVAL_SINGLE, - dataTree, + dataTree: Object.assign({}, dataTree, { + [EXECUTION_PARAM_KEY]: executionParams, + }), binding, }); const workerResponse = yield take(workerChannel); diff --git a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts index c495a92908..b6460ce412 100644 --- a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts +++ b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.ts @@ -16,7 +16,7 @@ export const dataTreeTypeDefCreator = (dataTree: DataTree) => { }; Object.keys(dataTree).forEach(entityName => { const entity = dataTree[entityName]; - if ("ENTITY_TYPE" in entity) { + if (entity && "ENTITY_TYPE" in entity) { if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) { const widgetType = entity.type; if (widgetType in entityDefinitions) { diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx index 4f0be931e8..a6efc38aae 100644 --- a/app/client/src/widgets/BaseWidget.tsx +++ b/app/client/src/widgets/BaseWidget.tsx @@ -15,7 +15,6 @@ import { CSSUnit, CONTAINER_GRID_PADDING, } from "constants/WidgetConstants"; -import _ from "lodash"; import DraggableComponent from "components/editorComponents/DraggableComponent"; import ResizableComponent from "components/editorComponents/ResizableComponent"; import { ExecuteActionPayload } from "constants/ActionConstants"; @@ -205,8 +204,8 @@ abstract class BaseWidget< ); } - addErrorBoundary(content: ReactNode, isValid: boolean) { - return {content}; + addErrorBoundary(content: ReactNode) { + return {content}; } private getWidgetView(): ReactNode { @@ -226,7 +225,7 @@ abstract class BaseWidget< case RenderModes.PAGE: content = this.getPageView(); if (this.props.isVisible) { - content = this.addErrorBoundary(content, true); + content = this.addErrorBoundary(content); if (!this.props.detachFromLayout) { content = this.makePositioned(content); } @@ -241,13 +240,8 @@ abstract class BaseWidget< abstract getPageView(): ReactNode; getCanvasView(): ReactNode { - let isValid = true; - if (this.props.invalidProps) { - isValid = _.keys(this.props.invalidProps).length === 0; - } - if (this.props.isLoading) isValid = true; const content = this.getPageView(); - return this.addErrorBoundary(content, isValid); + return this.addErrorBoundary(content); } // TODO(abhinav): Maybe make this a pure component to bailout from updating altogether. diff --git a/app/client/src/widgets/TableWidget.tsx b/app/client/src/widgets/TableWidget.tsx index c3c2328cdf..73062fee62 100644 --- a/app/client/src/widgets/TableWidget.tsx +++ b/app/client/src/widgets/TableWidget.tsx @@ -239,7 +239,7 @@ class TableWidget extends BaseWidget { let inputFormat; try { const type = column.metaProperties.inputFormat; - if (type !== "EPOCH" && type !== "Milliseconds") { + if (type !== "Epoch" && type !== "Milliseconds") { inputFormat = type; moment(value, inputFormat); } else if (!isNumber(value)) { @@ -253,6 +253,8 @@ class TableWidget extends BaseWidget { outputFormat = inputFormat; } if (column.metaProperties.inputFormat === "Milliseconds") { + value = Number(value); + } else if (column.metaProperties.inputFormat === "Epoch") { value = 1000 * Number(value); } tableRow[accessor] = moment(value, inputFormat).format( @@ -656,8 +658,10 @@ class TableWidget extends BaseWidget { }; handleRowClick = (rowData: Record, index: number) => { - const { selectedRowIndices } = this.props; if (this.props.multiRowSelection) { + const selectedRowIndices = this.props.selectedRowIndices + ? [...this.props.selectedRowIndices] + : []; if (selectedRowIndices.includes(index)) { const rowIndex = selectedRowIndices.indexOf(index); selectedRowIndices.splice(rowIndex, 1); diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/annotations/DocumentType.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/annotations/DocumentType.java new file mode 100644 index 0000000000..3c737b3a34 --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/annotations/DocumentType.java @@ -0,0 +1,21 @@ +package com.appsmith.external.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is meant to introduce polymorphic behaviour in persistent objects. Since we do not expect Spring to + * be able to automatically detect such objects, objects marked with this annotation are specifically registered in the + * type mapper for {@link org.springframework.data.mongodb.core.MongoTemplate} + * + * The value associated to this annotation functions as an alias for the entity. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DocumentType { + + public String value() default ""; + +} \ No newline at end of file diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/annotations/DocumentTypeMapper.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/annotations/DocumentTypeMapper.java new file mode 100644 index 0000000000..c595aa97a5 --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/annotations/DocumentTypeMapper.java @@ -0,0 +1,101 @@ +package com.appsmith.external.annotations; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.data.convert.TypeInformationMapper; +import org.springframework.data.mapping.Alias; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This {@link TypeInformationMapper} implementation makes use of the {@link DocumentType} annotation to register all + * such entities as possible candidates for domain mapping. + */ +public class DocumentTypeMapper implements TypeInformationMapper { + + private final Map> aliasToTypeMap; + private final Map, String> typeToAliasMap; + + private DocumentTypeMapper(List basePackagesToScan) { + aliasToTypeMap = new HashMap<>(); + typeToAliasMap = new HashMap<>(); + + // Upon initialization, read all aliases from annotated entities + populateTypeMap(basePackagesToScan); + } + + private void populateTypeMap(List basePackagesToScan) { + ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); + + scanner.addIncludeFilter(new AnnotationTypeFilter(DocumentType.class)); + + for (String basePackage : basePackagesToScan) { + for (BeanDefinition bd : scanner.findCandidateComponents(basePackage)) { + try { + Class clazz = Class.forName(bd.getBeanClassName()); + DocumentType documentTypeAnnotation = clazz.getAnnotation(DocumentType.class); + + ClassTypeInformation type = ClassTypeInformation.from(clazz); + String alias = documentTypeAnnotation.value(); + + aliasToTypeMap.put(alias, type); + typeToAliasMap.put(type, alias); + + } catch (ClassNotFoundException e) { + throw new IllegalStateException(String.format("Class [%s] could not be loaded.", bd.getBeanClassName()), e); + } + } + } + } + + @Override + public TypeInformation resolveTypeFrom(Alias alias) { + if (aliasToTypeMap.containsKey((String) alias.getValue())) { + return aliasToTypeMap.get(alias.getValue()); + } + return null; + } + + @Override + public Alias createAliasFor(TypeInformation typeInformation) { + if (typeToAliasMap.containsKey(typeInformation)) { + return Alias.of(typeToAliasMap.get(typeInformation)); + } + return Alias.NONE; + } + + public static class Builder { + List basePackagesToScan; + + public Builder() { + basePackagesToScan = new ArrayList<>(); + } + + public Builder withBasePackage(String basePackage) { + basePackagesToScan.add(basePackage); + return this; + } + + public Builder withBasePackages(String[] basePackages) { + basePackagesToScan.addAll(Arrays.asList(basePackages)); + return this; + } + + public Builder withBasePackages(Collection< ? extends String> basePackages) { + basePackagesToScan.addAll(basePackages); + return this; + } + + public DocumentTypeMapper build() { + return new DocumentTypeMapper(basePackagesToScan); + } + } +} diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/AuthType.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/AuthType.java new file mode 100644 index 0000000000..81c58d9163 --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/AuthType.java @@ -0,0 +1,6 @@ +package com.appsmith.external.constants; + +public class AuthType { + public static final String DB_AUTH = "dbAuth"; + public static final String OAUTH2 = "oAuth2"; +} diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/FieldName.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/FieldName.java new file mode 100644 index 0000000000..b1eb89d5ac --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/FieldName.java @@ -0,0 +1,8 @@ +package com.appsmith.external.constants; + +public class FieldName { + public static final String CLIENT_SECRET = "clientSecret"; + public static final String TOKEN = "token"; + + public static final String PASSWORD = "password"; +} diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/AuthenticationDTO.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/AuthenticationDTO.java index e679ae347b..f7fd6d5825 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/AuthenticationDTO.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/AuthenticationDTO.java @@ -1,30 +1,53 @@ package com.appsmith.external.models; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; +import com.appsmith.external.constants.AuthType; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; -import lombok.ToString; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; @Getter @Setter -@ToString -@NoArgsConstructor -@AllArgsConstructor +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.EXISTING_PROPERTY, + visible = true, + property = "type", + defaultImpl = DBAuth.class) +@JsonSubTypes({ + @JsonSubTypes.Type(value = DBAuth.class, name = AuthType.DB_AUTH), + @JsonSubTypes.Type(value = OAuth2.class, name = AuthType.OAUTH2) +}) public class AuthenticationDTO { + // In principle, this class should've been abstract. However, when this class is abstract, Spring's deserialization + // routines choke on identifying the correct class to instantiate and ends up trying to instantiate this abstract + // class and fails. - public enum Type { - SCRAM_SHA_1, SCRAM_SHA_256, MONGODB_CR, USERNAME_PASSWORD + @JsonIgnore + private Boolean isEncrypted; + + @JsonIgnore + public Map getEncryptionFields() { + return Collections.emptyMap(); } - Type authType; + public void setEncryptionFields(Map encryptedFields) { + // This is supposed to be overridden by implementations. + } - String username; + @JsonIgnore + public Set getEmptyEncryptionFields() { + return Collections.emptySet(); + } - @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) - String password; - - String databaseName; + @JsonIgnore + public boolean isEncrypted() { + return Boolean.TRUE.equals(isEncrypted); + } } diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DBAuth.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DBAuth.java new file mode 100644 index 0000000000..8f083d2658 --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DBAuth.java @@ -0,0 +1,59 @@ +package com.appsmith.external.models; + +import com.appsmith.external.annotations.DocumentType; +import com.appsmith.external.constants.AuthType; +import com.appsmith.external.constants.FieldName; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.util.Map; +import java.util.Set; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@DocumentType(AuthType.DB_AUTH) +public class DBAuth extends AuthenticationDTO { + + public enum Type { + SCRAM_SHA_1, SCRAM_SHA_256, MONGODB_CR, USERNAME_PASSWORD + } + + Type authType; + + String username; + + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + String password; + + String databaseName; + + @Override + public Map getEncryptionFields() { + if (this.password != null && !this.password.isEmpty()) { + return Map.of(FieldName.PASSWORD, this.password); + } + return Map.of(); + } + + @Override + public void setEncryptionFields(Map encryptedFields) { + if (encryptedFields != null && encryptedFields.containsKey(FieldName.PASSWORD)) { + this.password = encryptedFields.get(FieldName.PASSWORD); + } + } + + @Override + public Set getEmptyEncryptionFields() { + if (this.password == null || this.password.isEmpty()) { + return Set.of(FieldName.PASSWORD); + } + return Set.of(); + } +} diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/OAuth2.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/OAuth2.java new file mode 100644 index 0000000000..72eb6c9dd3 --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/OAuth2.java @@ -0,0 +1,84 @@ +package com.appsmith.external.models; + +import com.appsmith.external.annotations.DocumentType; +import com.appsmith.external.constants.AuthType; +import com.appsmith.external.constants.FieldName; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.time.Instant; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@DocumentType(AuthType.OAUTH2) +public class OAuth2 extends AuthenticationDTO { + public enum Type { + CLIENT_CREDENTIALS, + } + + Type authType; + + String clientId; + + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + String clientSecret; + + String accessTokenUrl; + + String scope; + + @JsonIgnore + String token; + + @JsonIgnore + Instant expiresAt; + + @Override + public Map getEncryptionFields() { + Map map = new HashMap<>(); + if (this.clientSecret != null) { + map.put(FieldName.CLIENT_SECRET, this.clientSecret); + } + if (this.token != null) { + map.put(FieldName.TOKEN, this.token); + } + return map; + } + + @Override + public void setEncryptionFields(Map encryptedFields) { + if (encryptedFields != null) { + if (encryptedFields.containsKey(FieldName.CLIENT_SECRET)) { + this.clientSecret = encryptedFields.get(FieldName.CLIENT_SECRET); + } + if (encryptedFields.containsKey(FieldName.TOKEN)) { + this.token = encryptedFields.get(FieldName.TOKEN); + } + } + } + + @Override + public Set getEmptyEncryptionFields() { + Set set = new HashSet<>(); + if (this.clientSecret == null || this.clientSecret.isEmpty()) { + set.add(FieldName.CLIENT_SECRET); + } + if (this.token == null || this.token.isEmpty()) { + set.add(FieldName.TOKEN); + } + return set; + } + +} diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/UpdatableConnection.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/UpdatableConnection.java new file mode 100644 index 0000000000..fbe6ccd5f4 --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/UpdatableConnection.java @@ -0,0 +1,9 @@ +package com.appsmith.external.models; + +public interface UpdatableConnection { + void updateDatasource(DatasourceConfiguration datasourceConfiguration); + + default boolean isUpdated() { + return false; + } +} diff --git a/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/DynamoPlugin.java b/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/DynamoPlugin.java index 775cea22fa..4bfa79fea9 100644 --- a/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/DynamoPlugin.java +++ b/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/DynamoPlugin.java @@ -2,7 +2,7 @@ package com.external.plugins; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; -import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.Endpoint; @@ -138,7 +138,7 @@ public class DynamoPlugin extends BasePlugin { builder.endpointOverride(URI.create("http://" + endpoint.getHost() + ":" + endpoint.getPort())); } - final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + final DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); if (authentication == null || StringUtils.isEmpty(authentication.getDatabaseName())) { return Mono.error(new AppsmithPluginException( AppsmithPluginError.PLUGIN_ERROR, @@ -169,7 +169,7 @@ public class DynamoPlugin extends BasePlugin { public Set validateDatasource(@NonNull DatasourceConfiguration datasourceConfiguration) { Set invalids = new HashSet<>(); - final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + final DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); if (authentication == null) { invalids.add("Missing AWS Access Key ID and Secret Access Key."); } else { diff --git a/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/DynamoPluginTest.java b/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/DynamoPluginTest.java index cd55139d3d..0a1ad1350e 100644 --- a/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/DynamoPluginTest.java +++ b/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/DynamoPluginTest.java @@ -2,7 +2,7 @@ package com.external.plugins; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; -import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.Endpoint; import lombok.extern.log4j.Log4j; @@ -96,10 +96,11 @@ public class DynamoPluginTest { Endpoint endpoint = new Endpoint(); endpoint.setHost(host); endpoint.setPort(port.longValue()); - dsConfig.setAuthentication(new AuthenticationDTO()); - dsConfig.getAuthentication().setUsername("dummy"); - dsConfig.getAuthentication().setPassword("dummy"); - dsConfig.getAuthentication().setDatabaseName(Region.AP_SOUTH_1.toString()); + DBAuth auth = new DBAuth(); + auth.setUsername("dummy"); + auth.setPassword("dummy"); + auth.setDatabaseName(Region.AP_SOUTH_1.toString()); + dsConfig.setAuthentication(auth); dsConfig.setEndpoints(List.of(endpoint)); } diff --git a/app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/com/external/plugins/ElasticSearchPlugin.java b/app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/com/external/plugins/ElasticSearchPlugin.java index 3612e8ed1c..b0d99b4b52 100644 --- a/app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/com/external/plugins/ElasticSearchPlugin.java +++ b/app/server/appsmith-plugins/elasticSearchPlugin/src/main/java/com/external/plugins/ElasticSearchPlugin.java @@ -2,7 +2,7 @@ package com.external.plugins; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; -import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.Endpoint; @@ -137,7 +137,7 @@ public class ElasticSearchPlugin extends BasePlugin { final RestClientBuilder clientBuilder = RestClient.builder(hosts.toArray(new HttpHost[]{})); - final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + final DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); if (authentication != null && !StringUtils.isEmpty(authentication.getUsername()) && !StringUtils.isEmpty(authentication.getPassword())) { diff --git a/app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java b/app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java index 95cbf26660..0f4e748259 100644 --- a/app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java +++ b/app/server/appsmith-plugins/elasticSearchPlugin/src/test/java/com/external/plugins/ElasticSearchPluginTest.java @@ -2,7 +2,6 @@ package com.external.plugins; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; -import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.Endpoint; import lombok.extern.slf4j.Slf4j; diff --git a/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java index 84c63a9d06..76e0ebe7f5 100644 --- a/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java +++ b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java @@ -2,7 +2,7 @@ package com.external.plugins; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; -import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceStructure; import com.appsmith.external.models.DatasourceTestResult; @@ -17,6 +17,7 @@ import com.google.cloud.firestore.CollectionReference; import com.google.cloud.firestore.DocumentReference; import com.google.cloud.firestore.DocumentSnapshot; import com.google.cloud.firestore.Firestore; +import com.google.cloud.firestore.Query; import com.google.cloud.firestore.QueryDocumentSnapshot; import com.google.cloud.firestore.QuerySnapshot; import com.google.cloud.firestore.WriteResult; @@ -132,7 +133,7 @@ public class FirestorePlugin extends BasePlugin { if (method.isDocumentLevel()) { return handleDocumentLevelMethod(connection, path, method, mapBody); } else { - return handleCollectionLevelMethod(connection, path, method, properties); + return handleCollectionLevelMethod(connection, path, method, properties, mapBody); } }) .subscribeOn(scheduler); @@ -221,15 +222,33 @@ public class FirestorePlugin extends BasePlugin { Firestore connection, String path, com.external.plugins.Method method, - List properties + List properties, + Map mapBody ) { + final CollectionReference collection = connection.collection(path); + + if (method == Method.GET_COLLECTION) { + return methodGetCollection(collection, properties); + + } else if (method == Method.ADD_TO_COLLECTION) { + return methodAddToCollection(collection, mapBody); + + } + + return Mono.error(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Unsupported collection-level command: " + method + )); + } + + private Mono methodGetCollection(CollectionReference query, List properties) { final String orderBy = properties.size() > 1 && properties.get(1) != null ? properties.get(1).getValue() : null; final int limit = properties.size() > 2 && properties.get(2) != null ? Integer.parseInt(properties.get(2).getValue()) : 10; final String queryFieldPath = properties.size() > 3 && properties.get(3) != null ? properties.get(3).getValue() : null; final Op operator = properties.size() > 4 && properties.get(4) != null ? Op.valueOf(properties.get(4).getValue()) : null; final String queryValue = properties.size() > 5 && properties.get(5) != null ? properties.get(5).getValue() : null; - return Mono.just(connection.collection(path)) + return Mono.just(query) // Apply ordering, if provided. .map(query1 -> StringUtils.isEmpty(orderBy) ? query1 : query1.orderBy(orderBy)) // Apply where condition, if provided. @@ -285,17 +304,7 @@ public class FirestorePlugin extends BasePlugin { // Apply limit, always provided, since without it we can inadvertently end up processing too much data. .map(query1 -> query1.limit(limit)) // Run the Firestore query to get a Future of the results. - .flatMap(query1 -> { - switch (method) { - case GET_COLLECTION: - return Mono.just(query1.get()); - default: - return Mono.error(new AppsmithPluginException( - AppsmithPluginError.PLUGIN_ERROR, - "Unknown collection method: " + method.toString() + "." - )); - } - }) + .map(Query::get) // Consume the future to get the actual results. .flatMap(resultFuture -> { try { @@ -315,7 +324,35 @@ public class FirestorePlugin extends BasePlugin { result.setIsExecutionSuccess(true); System.out.println( Thread.currentThread().getName() - + ": In the Firestore Plugin, got action execution result" + + ": In the Firestore Plugin, got action execution result for get collection" + ); + return Mono.just(result); + }); + } + + private Mono methodAddToCollection(CollectionReference collection, Map mapBody) { + return Mono.justOrEmpty(collection.add(mapBody)) + .flatMap(future -> { + try { + return Mono.just(future.get()); + } catch (InterruptedException | ExecutionException e) { + return Mono.error(new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + e.getMessage() + )); + } + }) + .flatMap(opResult -> { + ActionExecutionResult result = new ActionExecutionResult(); + try { + result.setBody(resultToMap(opResult)); + } catch (AppsmithPluginException e) { + return Mono.error(e); + } + result.setIsExecutionSuccess(true); + System.out.println( + Thread.currentThread().getName() + + ": In the Firestore Plugin, got action execution result for add to collection" ); return Mono.just(result); }); @@ -344,6 +381,13 @@ public class FirestorePlugin extends BasePlugin { } return documents; + } else if (objResult instanceof DocumentReference) { + DocumentReference documentReference = (DocumentReference) objResult; + Map resultMap = new HashMap<>(); + resultMap.put("id", documentReference.getId()); + resultMap.put("path", documentReference.getPath()); + return resultMap; + } else { throw new AppsmithPluginException( AppsmithPluginError.PLUGIN_ERROR, @@ -355,7 +399,7 @@ public class FirestorePlugin extends BasePlugin { @Override public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { - final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + final DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); final Set errors = validateDatasource(datasourceConfiguration); if (!CollectionUtils.isEmpty(errors)) { @@ -405,7 +449,7 @@ public class FirestorePlugin extends BasePlugin { @Override public Set validateDatasource(DatasourceConfiguration datasourceConfiguration) { - final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + final DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); Set invalids = new HashSet<>(); diff --git a/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/Method.java b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/Method.java index 086bc8953f..c3a27ee743 100644 --- a/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/Method.java +++ b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/Method.java @@ -5,6 +5,7 @@ public enum Method { GET_COLLECTION(false, false), SET_DOCUMENT(true, true), CREATE_DOCUMENT(true, true), + ADD_TO_COLLECTION(false, true), UPDATE_DOCUMENT(true, true), DELETE_DOCUMENT(true, false), ; diff --git a/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor.json b/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor.json index 38e261a5b0..97f2e828ee 100644 --- a/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor.json +++ b/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor.json @@ -34,6 +34,10 @@ "label": "Create Document", "value": "CREATE_DOCUMENT" }, + { + "label": "Add Document to Collection", + "value": "ADD_TO_COLLECTION" + }, { "label": "Update Document", "value": "UPDATE_DOCUMENT" diff --git a/app/server/appsmith-plugins/firestorePlugin/src/test/java/com/external/plugins/FirestorePluginTest.java b/app/server/appsmith-plugins/firestorePlugin/src/test/java/com/external/plugins/FirestorePluginTest.java index fa2cc2c56d..386d38ae9a 100644 --- a/app/server/appsmith-plugins/firestorePlugin/src/test/java/com/external/plugins/FirestorePluginTest.java +++ b/app/server/appsmith-plugins/firestorePlugin/src/test/java/com/external/plugins/FirestorePluginTest.java @@ -2,7 +2,7 @@ package com.external.plugins; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; -import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.Property; import com.google.cloud.NoCredentials; @@ -25,6 +25,7 @@ import java.util.concurrent.ExecutionException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; /** @@ -60,9 +61,10 @@ public class FirestorePluginTest { firestoreConnection.document("changing/to-delete").set(Map.of("value", 1)).get(); dsConfig.setUrl(emulator.getEmulatorEndpoint()); - dsConfig.setAuthentication(new AuthenticationDTO()); - dsConfig.getAuthentication().setUsername("test-project"); - dsConfig.getAuthentication().setPassword(""); + DBAuth auth = new DBAuth(); + auth.setUsername("test-project"); + auth.setPassword(""); + dsConfig.setAuthentication(auth); } @Test @@ -203,4 +205,27 @@ public class FirestorePluginTest { .verifyComplete(); } + @Test + public void testAddToCollection() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setPath("changing"); + + actionConfiguration.setPluginSpecifiedTemplates(List.of(new Property("method", "ADD_TO_COLLECTION"))); + + actionConfiguration.setBody("{\n" + + " \"question\": \"What is the answer to life, universe and everything else?\",\n" + + " \"answer\": 42\n" + + "}"); + + Mono resultMono = pluginExecutor + .execute(firestoreConnection, dsConfig, actionConfiguration); + + StepVerifier.create(resultMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(firestoreConnection.document("changing/" + ((Map) result.getBody()).get("id"))); + }) + .verifyComplete(); + } + } diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java index e95782fce1..40e698401c 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java @@ -2,8 +2,8 @@ package com.external.plugins; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; -import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.Connection; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceStructure; import com.appsmith.external.models.DatasourceTestResult; @@ -53,10 +53,10 @@ import java.util.stream.Collectors; public class MongoPlugin extends BasePlugin { - private static final Set VALID_AUTH_TYPES = Set.of( - AuthenticationDTO.Type.SCRAM_SHA_1, - AuthenticationDTO.Type.SCRAM_SHA_256, - AuthenticationDTO.Type.MONGODB_CR // NOTE: Deprecated in the driver. + private static final Set VALID_AUTH_TYPES = Set.of( + DBAuth.Type.SCRAM_SHA_1, + DBAuth.Type.SCRAM_SHA_256, + DBAuth.Type.MONGODB_CR // NOTE: Deprecated in the driver. ); private static final String VALID_AUTH_TYPES_STR = VALID_AUTH_TYPES.stream() @@ -180,7 +180,7 @@ public class MongoPlugin extends BasePlugin { String databaseName = datasourceConfiguration.getConnection().getDefaultDatabaseName(); // If that's not available, pick the authentication database. - final AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + final DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); if (StringUtils.isEmpty(databaseName) && authentication != null) { databaseName = authentication.getDatabaseName(); } @@ -221,7 +221,7 @@ public class MongoPlugin extends BasePlugin { builder.append("mongodb://"); } - AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); if (authentication != null) { builder .append(urlEncode(authentication.getUsername())) @@ -293,12 +293,12 @@ public class MongoPlugin extends BasePlugin { } - AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); if (authentication == null) { invalids.add("Missing authentication details."); } else { - AuthenticationDTO.Type authType = authentication.getAuthType(); + DBAuth.Type authType = authentication.getAuthType(); if (authType != null && VALID_AUTH_TYPES.contains(authType)) { diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/templates/meta.json b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/templates/meta.json new file mode 100644 index 0000000000..833afaa6ea --- /dev/null +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/templates/meta.json @@ -0,0 +1,16 @@ +{ + "templates": [ + { + "file": "CREATE.json" + }, + { + "file": "READ.json" + }, + { + "file": "UPDATE.json" + }, + { + "file": "DELETE.json" + } + ] +} diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java index 583eadf3ba..2e02be6364 100644 --- a/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java @@ -2,7 +2,7 @@ package com.external.plugins; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; -import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.Endpoint; @@ -196,7 +196,7 @@ public class MssqlPlugin extends BasePlugin { )); } - AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); com.appsmith.external.models.Connection configurationConnection = datasourceConfiguration.getConnection(); @@ -282,15 +282,16 @@ public class MssqlPlugin extends BasePlugin { invalids.add("Missing Connection Mode."); } - if (datasourceConfiguration.getAuthentication() == null) { + DBAuth auth = (DBAuth) datasourceConfiguration.getAuthentication(); + if (auth == null) { invalids.add("Missing authentication details."); } else { - if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getUsername())) { + if (StringUtils.isEmpty(auth.getUsername())) { invalids.add("Missing username for authentication."); } - if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getPassword())) { + if (StringUtils.isEmpty(auth.getPassword())) { invalids.add("Missing password for authentication."); } diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/meta.json b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/meta.json new file mode 100644 index 0000000000..b05a592845 --- /dev/null +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/resources/templates/meta.json @@ -0,0 +1,16 @@ +{ + "templates": [ + { + "file": "CREATE.sql" + }, + { + "file": "SELECT.sql" + }, + { + "file": "UPDATE.sql" + }, + { + "file": "DELETE.sql" + } + ] +} diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java b/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java index 8b15d04dd6..14072c7252 100644 --- a/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java +++ b/app/server/appsmith-plugins/mssqlPlugin/src/test/java/com/external/plugins/MssqlPluginTest.java @@ -2,7 +2,7 @@ package com.external.plugins; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; -import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.Endpoint; import com.appsmith.external.pluginExceptions.AppsmithPluginException; @@ -107,8 +107,8 @@ public class MssqlPluginTest { } private DatasourceConfiguration createDatasourceConfiguration() { - AuthenticationDTO authDTO = new AuthenticationDTO(); - authDTO.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD); + DBAuth authDTO = new DBAuth(); + authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD); authDTO.setUsername(username); authDTO.setPassword(password); @@ -208,8 +208,9 @@ public class MssqlPluginTest { DatasourceConfiguration dsConfig = createDatasourceConfiguration(); // Set up random username and password and try to connect - dsConfig.getAuthentication().setUsername(new ObjectId().toString()); - dsConfig.getAuthentication().setPassword(new ObjectId().toString()); + DBAuth auth = (DBAuth) dsConfig.getAuthentication(); + auth.setUsername(new ObjectId().toString()); + auth.setPassword(new ObjectId().toString()); Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java index cf24cd4247..f6d7b7b59e 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java @@ -2,7 +2,7 @@ package com.external.plugins; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; -import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceStructure; import com.appsmith.external.models.DatasourceTestResult; @@ -55,15 +55,15 @@ public class MySqlPlugin extends BasePlugin { private static final String TIMESTAMP_COLUMN_TYPE_NAME = "timestamp"; /** - Example output for COLUMNS_QUERY: - +------------+-----------+-------------+-------------+-------------+------------+----------------+ - | table_name | column_id | column_name | column_type | is_nullable | COLUMN_KEY | EXTRA | - +------------+-----------+-------------+-------------+-------------+------------+----------------+ - | test | 1 | id | int | 0 | PRI | auto_increment | - | test | 2 | firstname | varchar | 1 | | | - | test | 3 | middlename | varchar | 1 | | | - | test | 4 | lastname | varchar | 1 | | | - +------------+-----------+-------------+-------------+-------------+------------+----------------+ + * Example output for COLUMNS_QUERY: + * +------------+-----------+-------------+-------------+-------------+------------+----------------+ + * | table_name | column_id | column_name | column_type | is_nullable | COLUMN_KEY | EXTRA | + * +------------+-----------+-------------+-------------+-------------+------------+----------------+ + * | test | 1 | id | int | 0 | PRI | auto_increment | + * | test | 2 | firstname | varchar | 1 | | | + * | test | 3 | middlename | varchar | 1 | | | + * | test | 4 | lastname | varchar | 1 | | | + * +------------+-----------+-------------+-------------+-------------+------------+----------------+ */ private static final String COLUMNS_QUERY = "select tab.table_name as table_name,\n" + " col.ordinal_position as column_id,\n" + @@ -82,12 +82,12 @@ public class MySqlPlugin extends BasePlugin { " col.ordinal_position;"; /** - Example output for KEYS_QUERY: - +-----------------+-------------+------------+-----------------+-------------+----------------+---------------+----------------+ - | CONSTRAINT_NAME | self_schema | self_table | constraint_type | self_column | foreign_schema | foreign_table | foreign_column | - +-----------------+-------------+------------+-----------------+-------------+----------------+---------------+----------------+ - | PRIMARY | mytestdb | test | p | id | NULL | NULL | NULL | - +-----------------+-------------+------------+-----------------+-------------+----------------+---------------+----------------+ + * Example output for KEYS_QUERY: + * +-----------------+-------------+------------+-----------------+-------------+----------------+---------------+----------------+ + * | CONSTRAINT_NAME | self_schema | self_table | constraint_type | self_column | foreign_schema | foreign_table | foreign_column | + * +-----------------+-------------+------------+-----------------+-------------+----------------+---------------+----------------+ + * | PRIMARY | mytestdb | test | p | id | NULL | NULL | NULL | + * +-----------------+-------------+------------+-----------------+-------------+----------------+---------------+----------------+ */ private static final String KEYS_QUERY = "select i.constraint_name,\n" + " i.TABLE_SCHEMA as self_schema,\n" + @@ -123,18 +123,17 @@ public class MySqlPlugin extends BasePlugin { Iterator iterator = (Iterator) meta.getColumnMetadatas().iterator(); Map processedRow = new LinkedHashMap<>(); - while(iterator.hasNext()) { + while (iterator.hasNext()) { ColumnMetadata metaData = iterator.next(); String columnName = metaData.getName(); String typeName = metaData.getJavaType().toString(); Object columnValue = row.get(columnName); - if(java.time.LocalDate.class.toString().equalsIgnoreCase(typeName) + if (java.time.LocalDate.class.toString().equalsIgnoreCase(typeName) && columnValue != null) { columnValue = DateTimeFormatter.ISO_DATE.format(row.get(columnName, LocalDate.class)); - } - else if ((java.time.LocalDateTime.class.toString().equalsIgnoreCase(typeName)) + } else if ((java.time.LocalDateTime.class.toString().equalsIgnoreCase(typeName)) && columnValue != null) { columnValue = DateTimeFormatter.ISO_DATE_TIME.format( LocalDateTime.of( @@ -142,17 +141,14 @@ public class MySqlPlugin extends BasePlugin { row.get(columnName, LocalDateTime.class).toLocalTime() ) ) + "Z"; - } - else if(java.time.LocalTime.class.toString().equalsIgnoreCase(typeName) + } else if (java.time.LocalTime.class.toString().equalsIgnoreCase(typeName) && columnValue != null) { columnValue = DateTimeFormatter.ISO_TIME.format(row.get(columnName, LocalTime.class)); - } - else if (java.time.Year.class.toString().equalsIgnoreCase(typeName) + } else if (java.time.Year.class.toString().equalsIgnoreCase(typeName) && columnValue != null) { columnValue = row.get(columnName, LocalDate.class).getYear(); - } - else { + } else { columnValue = row.get(columnName); } @@ -165,10 +161,10 @@ public class MySqlPlugin extends BasePlugin { /** * 1. Check the type of sql query - i.e Select ... or Insert/Update/Drop * 2. In case sql queries are chained together, then decide the type based on the last query. i.e In case of - * query "select * from test; updated test ..." the type of query will be based on the update statement. + * query "select * from test; updated test ..." the type of query will be based on the update statement. * 3. This is used because the output returned to client is based on the type of the query. In case of a - * select query rows are returned, whereas, in case of any other query the number of updated rows is - * returned. + * select query rows are returned, whereas, in case of any other query the number of updated rows is + * returned. */ private boolean getIsSelectOrShowQuery(String query) { String[] queries = query.split(";"); @@ -189,17 +185,16 @@ public class MySqlPlugin extends BasePlugin { boolean isSelectOrShowQuery = getIsSelectOrShowQuery(query); final List> rowsList = new ArrayList<>(50); Flux resultFlux = Mono.from(connection.validate(ValidationDepth.REMOTE)) - .flatMapMany(isValid -> { - if(isValid) { - return connection.createStatement(query).execute(); - } - else { - return Flux.error(new StaleConnectionException()); - } - }); + .flatMapMany(isValid -> { + if (isValid) { + return connection.createStatement(query).execute(); + } else { + return Flux.error(new StaleConnectionException()); + } + }); Mono>> resultMono = null; - if(isSelectOrShowQuery) { + if (isSelectOrShowQuery) { resultMono = resultFlux .flatMap(result -> { return result.map((row, meta) -> { @@ -211,8 +206,7 @@ public class MySqlPlugin extends BasePlugin { .flatMap(execResult -> { return Mono.just(rowsList); }); - } - else { + } else { resultMono = resultFlux .flatMap(result -> result.getRowsUpdated()) .collectList() @@ -243,7 +237,7 @@ public class MySqlPlugin extends BasePlugin { @Override public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) { - AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); com.appsmith.external.models.Connection configurationConnection = datasourceConfiguration.getConnection(); StringBuilder urlBuilder = new StringBuilder(); @@ -328,16 +322,17 @@ public class MySqlPlugin extends BasePlugin { if (datasourceConfiguration.getAuthentication() == null) { invalids.add("Missing authentication details."); } else { - if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getUsername())) { + DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); + if (StringUtils.isEmpty(authentication.getUsername())) { invalids.add("Missing username for authentication."); } - if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getPassword())) { + if (StringUtils.isEmpty(authentication.getPassword())) { invalids.add("Missing password for authentication."); } - if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getDatabaseName())) { - invalids.add("Missing database name"); + if (StringUtils.isEmpty(authentication.getDatabaseName())) { + invalids.add("Missing database name."); } } @@ -516,11 +511,11 @@ public class MySqlPlugin extends BasePlugin { .collectList() .thenMany(Flux.from(connection.createStatement(KEYS_QUERY).execute())) .flatMap(result -> { - return result.map((row, meta) -> { - getKeyInfo(row, meta, tablesByName, keyRegistry); + return result.map((row, meta) -> { + getKeyInfo(row, meta, tablesByName, keyRegistry); - return result; - }); + return result; + }); }) .collectList() .map(list -> { diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/meta.json b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/meta.json new file mode 100644 index 0000000000..b05a592845 --- /dev/null +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/resources/templates/meta.json @@ -0,0 +1,16 @@ +{ + "templates": [ + { + "file": "CREATE.sql" + }, + { + "file": "SELECT.sql" + }, + { + "file": "UPDATE.sql" + }, + { + "file": "DELETE.sql" + } + ] +} diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java index cb1d28520f..a5f12cb1da 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java +++ b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java @@ -1,6 +1,12 @@ package com.external.plugins; -import com.appsmith.external.models.*; +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.DBAuth; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.DatasourceStructure; +import com.appsmith.external.models.Endpoint; +import com.appsmith.external.models.Property; import com.appsmith.external.pluginExceptions.StaleConnectionException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -9,7 +15,6 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactories; -import io.r2dbc.spi.Batch; import lombok.extern.log4j.Log4j; import org.junit.Assert; import org.junit.BeforeClass; @@ -119,8 +124,8 @@ public class MySqlPluginTest { } private static DatasourceConfiguration createDatasourceConfiguration() { - AuthenticationDTO authDTO = new AuthenticationDTO(); - authDTO.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD); + DBAuth authDTO = new DBAuth(); + authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD); authDTO.setUsername(username); authDTO.setPassword(password); authDTO.setDatabaseName(database); @@ -147,8 +152,8 @@ public class MySqlPluginTest { @Test public void testConnectMySQLContainerWithInvalidTimezone() { - AuthenticationDTO authDTO = new AuthenticationDTO(); - authDTO.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD); + DBAuth authDTO = new DBAuth(); + authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD); authDTO.setUsername(mySQLContainerWithInvalidTimezone.getUsername()); authDTO.setPassword(mySQLContainerWithInvalidTimezone.getPassword()); authDTO.setDatabaseName(mySQLContainerWithInvalidTimezone.getDatabaseName()); @@ -227,9 +232,10 @@ public class MySqlPluginTest { @Test public void testValidateDatasourceNullCredentials() { dsConfig.setConnection(new com.appsmith.external.models.Connection()); - dsConfig.getAuthentication().setUsername(null); - dsConfig.getAuthentication().setPassword(null); - dsConfig.getAuthentication().setDatabaseName("someDbName"); + DBAuth auth = (DBAuth) dsConfig.getAuthentication(); + auth.setUsername(null); + auth.setPassword(null); + auth.setDatabaseName("someDbName"); Set output = pluginExecutor.validateDatasource(dsConfig); assertTrue(output.contains("Missing username for authentication.")); assertTrue(output.contains("Missing password for authentication.")); @@ -237,10 +243,10 @@ public class MySqlPluginTest { @Test public void testValidateDatasourceMissingDBName() { - dsConfig.getAuthentication().setDatabaseName(""); + ((DBAuth) dsConfig.getAuthentication()).setDatabaseName(""); Set output = pluginExecutor.validateDatasource(dsConfig); assertEquals(output.size(), 1); - assertTrue(output.contains("Missing database name")); + assertTrue(output.contains("Missing database name.")); } @Test diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java index 33d582e2d6..e3ff0a0933 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java @@ -2,7 +2,7 @@ package com.external.plugins; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; -import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceStructure; import com.appsmith.external.models.DatasourceTestResult; @@ -288,15 +288,16 @@ public class PostgresPlugin extends BasePlugin { invalids.add("Missing authentication details."); } else { - if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getUsername())) { + DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); + if (StringUtils.isEmpty(authentication.getUsername())) { invalids.add("Missing username for authentication."); } - if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getPassword())) { + if (StringUtils.isEmpty(authentication.getPassword())) { invalids.add("Missing password for authentication."); } - if (StringUtils.isEmpty(datasourceConfiguration.getAuthentication().getDatabaseName())) { + if (StringUtils.isEmpty(authentication.getDatabaseName())) { invalids.add("Missing database name."); } @@ -509,7 +510,7 @@ public class PostgresPlugin extends BasePlugin { config.addDataSourceProperty(SSL, isSslEnabled); // Set authentication properties - AuthenticationDTO authentication = datasourceConfiguration.getAuthentication(); + DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); if (authentication.getUsername() != null) { config.setUsername(authentication.getUsername()); } diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/resources/templates/meta.json b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/templates/meta.json new file mode 100644 index 0000000000..b05a592845 --- /dev/null +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/templates/meta.json @@ -0,0 +1,16 @@ +{ + "templates": [ + { + "file": "CREATE.sql" + }, + { + "file": "SELECT.sql" + }, + { + "file": "UPDATE.sql" + }, + { + "file": "DELETE.sql" + } + ] +} diff --git a/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java b/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java index e9ec5ecfdd..c312c123d4 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java @@ -3,6 +3,7 @@ package com.external.plugins; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceStructure; import com.appsmith.external.models.Endpoint; @@ -138,8 +139,8 @@ public class PostgresPluginTest { } private DatasourceConfiguration createDatasourceConfiguration() { - AuthenticationDTO authDTO = new AuthenticationDTO(); - authDTO.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD); + DBAuth authDTO = new DBAuth(); + authDTO.setAuthType(DBAuth.Type.USERNAME_PASSWORD); authDTO.setUsername(username); authDTO.setPassword(password); authDTO.setDatabaseName("postgres"); diff --git a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java index d2716dbb63..b862a7869f 100644 --- a/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java +++ b/app/server/appsmith-plugins/redisPlugin/src/main/java/com/external/plugins/RedisPlugin.java @@ -2,7 +2,7 @@ package com.external.plugins; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; -import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.Endpoint; @@ -113,8 +113,8 @@ public class RedisPlugin extends BasePlugin { Integer port = (int) (long) ObjectUtils.defaultIfNull(endpoint.getPort(), DEFAULT_PORT); Jedis jedis = new Jedis(endpoint.getHost(), port); - AuthenticationDTO auth = datasourceConfiguration.getAuthentication(); - if (auth != null && AuthenticationDTO.Type.USERNAME_PASSWORD.equals(auth.getAuthType())) { + DBAuth auth = (DBAuth) datasourceConfiguration.getAuthentication(); + if (auth != null && DBAuth.Type.USERNAME_PASSWORD.equals(auth.getAuthType())) { jedis.auth(auth.getUsername(), auth.getPassword()); } @@ -158,13 +158,13 @@ public class RedisPlugin extends BasePlugin { } } - AuthenticationDTO auth = datasourceConfiguration.getAuthentication(); - if (auth != null && AuthenticationDTO.Type.USERNAME_PASSWORD.equals(auth.getAuthType())) { - if (StringUtils.isNullOrEmpty(datasourceConfiguration.getAuthentication().getUsername())) { + DBAuth auth = (DBAuth) datasourceConfiguration.getAuthentication(); + if (auth != null && DBAuth.Type.USERNAME_PASSWORD.equals(auth.getAuthType())) { + if (StringUtils.isNullOrEmpty(auth.getUsername())) { invalids.add("Missing username for authentication."); } - if (StringUtils.isNullOrEmpty(datasourceConfiguration.getAuthentication().getPassword())) { + if (StringUtils.isNullOrEmpty(auth.getPassword())) { invalids.add("Missing password for authentication."); } } diff --git a/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java b/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java index 419b699d36..f49085c6b7 100644 --- a/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java +++ b/app/server/appsmith-plugins/redisPlugin/src/test/java/com/external/plugins/RedisPluginTest.java @@ -2,7 +2,7 @@ package com.external.plugins; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; -import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.Endpoint; @@ -99,8 +99,8 @@ public class RedisPluginTest { Endpoint endpoint = new Endpoint(); endpoint.setHost("test-host"); - AuthenticationDTO invalidAuth = new AuthenticationDTO(); - invalidAuth.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD); + DBAuth invalidAuth = new DBAuth(); + invalidAuth.setAuthType(DBAuth.Type.USERNAME_PASSWORD); invalidDatasourceConfiguration.setAuthentication(invalidAuth); invalidDatasourceConfiguration.setEndpoints(Collections.singletonList(endpoint)); @@ -115,8 +115,8 @@ public class RedisPluginTest { public void itShouldValidateDatasource() { DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); - AuthenticationDTO auth = new AuthenticationDTO(); - auth.setAuthType(AuthenticationDTO.Type.USERNAME_PASSWORD); + DBAuth auth = new DBAuth(); + auth.setAuthType(DBAuth.Type.USERNAME_PASSWORD); auth.setUsername("test-username"); auth.setPassword("test-password"); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/MongoConfig.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/MongoConfig.java index 5c0580ee80..5568d0e5a4 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/MongoConfig.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/MongoConfig.java @@ -1,5 +1,7 @@ package com.appsmith.server.configurations; +import com.appsmith.external.annotations.DocumentTypeMapper; +import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.server.configurations.mongo.SoftDeleteMongoRepositoryFactoryBean; import com.appsmith.server.repositories.BaseRepositoryImpl; import com.github.cloudyrock.mongock.SpringBootMongock; @@ -8,10 +10,21 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.convert.DefaultTypeMapper; +import org.springframework.data.convert.SimpleTypeInformationMapper; +import org.springframework.data.convert.TypeInformationMapper; +import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.config.EnableMongoAuditing; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.convert.DefaultMongoTypeMapper; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.MongoTypeMapper; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories; +import java.util.Arrays; + /** * This configures the JPA Mongo repositories. The default base implementation is defined in {@link BaseRepositoryImpl}. * This is required to add default clauses for default JPA queries defined by Spring Data. @@ -39,4 +52,28 @@ public class MongoConfig { .build(); } + @Bean + public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory, MappingMongoConverter mappingMongoConverter) { + return new MongoTemplate(mongoDbFactory, mappingMongoConverter); + } + + // Custom type mapper here includes our annotation based mapper that is meant to ensure correct mapping for sub-classes + // We have currently only included the package which contains the DTOs that need this mapping + @Bean + public DefaultTypeMapper typeMapper() { + TypeInformationMapper typeInformationMapper = new DocumentTypeMapper + .Builder() + .withBasePackages(new String[]{AuthenticationDTO.class.getPackageName()}) + .build(); + // This is a hack to include the default mapper as a fallback, because Spring seems to override its list instead of appending mappers + return new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, Arrays.asList(typeInformationMapper, new SimpleTypeInformationMapper())); + } + + @Bean + public MappingMongoConverter mappingMongoConverter(DefaultTypeMapper typeMapper, MongoMappingContext context) { + MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, context); + converter.setTypeMapper((MongoTypeMapper) typeMapper); + return converter; + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/OrganizationController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/OrganizationController.java index 127d883681..18d2269bb0 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/OrganizationController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/OrganizationController.java @@ -9,17 +9,17 @@ import com.appsmith.server.services.UserOrganizationService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.codec.multipart.Part; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; import java.util.List; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Organization.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Organization.java index eef0ba665a..3d9a45181a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Organization.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Organization.java @@ -9,8 +9,6 @@ import lombok.Setter; import lombok.ToString; import org.springframework.data.mongodb.core.mapping.Document; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotBlank; import java.util.List; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/GlobalExceptionHandler.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/GlobalExceptionHandler.java index eb4dc34a81..e2a7ddc1ec 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/GlobalExceptionHandler.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/GlobalExceptionHandler.java @@ -2,8 +2,9 @@ package com.appsmith.server.exceptions; import com.appsmith.external.pluginExceptions.AppsmithPluginException; import com.appsmith.server.dtos.ResponseDTO; +import io.sentry.Sentry; +import io.sentry.SentryLevel; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.validation.FieldError; @@ -13,17 +14,12 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; -import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; -import io.sentry.Sentry; -import io.sentry.SentryEvent; -import io.sentry.SentryOptions; -import io.sentry.SentryLevel; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.HashMap; import java.util.Map; -import java.io.StringWriter; -import java.io.PrintWriter; /** diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/BeanCopyUtils.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/BeanCopyUtils.java index 36f1e85d33..7ca08a737c 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/BeanCopyUtils.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/BeanCopyUtils.java @@ -74,7 +74,9 @@ public final class BeanCopyUtils { Object targetValue = targetBeanWrapper.getPropertyValue(name); - if (targetValue != null && isDomainModel(propertyDescriptor.getPropertyType())) { + if (targetValue != null + && sourceValue.getClass().isAssignableFrom(targetValue.getClass()) + && isDomainModel(propertyDescriptor.getPropertyType())) { // Go deeper *only* if the property belongs to Appsmith's models, and both the source and target values // are not null. copyNestedNonNullProperties(sourceValue, targetValue); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java index eb7553425d..89260f746e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java @@ -1,7 +1,7 @@ package com.appsmith.server.migrations; -import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.BaseDomain; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.Policy; import com.appsmith.server.acl.AppsmithRole; import com.appsmith.server.constants.FieldName; @@ -38,14 +38,21 @@ import com.appsmith.server.services.OrganizationService; import com.github.cloudyrock.mongock.ChangeLog; import com.github.cloudyrock.mongock.ChangeSet; import com.google.gson.Gson; +import com.mongodb.MongoException; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoCursor; +import com.mongodb.client.model.Filters; import lombok.extern.slf4j.Slf4j; import net.minidev.json.JSONObject; import org.apache.commons.lang.ObjectUtils; +import org.bson.Document; import org.bson.types.ObjectId; import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.dao.DataAccessException; import org.springframework.dao.DuplicateKeyException; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.UncategorizedMongoDbException; +import org.springframework.data.mongodb.core.CollectionCallback; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.index.CompoundIndexDefinition; import org.springframework.data.mongodb.core.index.Index; @@ -556,7 +563,7 @@ public class DatabaseChangelog { ); for (final Datasource datasource : datasources) { - AuthenticationDTO authentication = datasource.getDatasourceConfiguration().getAuthentication(); + DBAuth authentication = (DBAuth) datasource.getDatasourceConfiguration().getAuthentication(); authentication.setPassword(encryptionService.encryptString(authentication.getPassword())); mongoTemplate.save(datasource); } @@ -1408,4 +1415,84 @@ public class DatabaseChangelog { } } + @ChangeSet(order = "045", id = "update-authentication-type", author = "") + public void updateAuthenticationTypes(MongoTemplate mongoTemplate) { + mongoTemplate.execute("datasource", new CollectionCallback() { + @Override + public String doInCollection(MongoCollection collection) throws MongoException, DataAccessException { + // Only update _class for authentication objects that exist + MongoCursor cursor = collection.find(Filters.exists("datasourceConfiguration.authentication")).cursor(); + while (cursor.hasNext()) { + Document current = (Document) cursor.next(); + Document old = Document.parse(current.toJson()); + + // Extra precaution to only update _class for authentication objects that don't already have this + // Is this condition required? What does production datasource look like? + ((Document) ((Document) current.get("datasourceConfiguration")) + .get("authentication")) + .putIfAbsent("_class", "dbAuth"); + + // Replace old document with the new one + collection.findOneAndReplace(old, current); + } + return null; + } + }); + + mongoTemplate.execute("newAction", new CollectionCallback() { + @Override + public String doInCollection(MongoCollection collection) throws MongoException, DataAccessException { + // Only update _class for authentication objects that exist + MongoCursor cursor = collection + .find(Filters.and( + Filters.exists("unpublishedAction.datasource"), + Filters.exists("unpublishedAction.datasource.datasourceConfiguration"), + Filters.exists("unpublishedAction.datasource.datasourceConfiguration.authentication"))).cursor(); + while (cursor.hasNext()) { + Document current = (Document) cursor.next(); + Document old = Document.parse(current.toJson()); + + // Extra precaution to only update _class for authentication objects that don't already have this + // Is this condition required? What does production datasource look like? + ((Document) ((Document) ((Document) ((Document) current.get("unpublishedAction")) + .get("datasource")) + .get("datasourceConfiguration")) + .get("authentication")) + .putIfAbsent("_class", "dbAuth"); + + // Replace old document with the new one + collection.findOneAndReplace(old, current); + } + return null; + } + }); + + mongoTemplate.execute("newAction", new CollectionCallback() { + @Override + public String doInCollection(MongoCollection collection) throws MongoException, DataAccessException { + // Only update _class for authentication objects that exist + MongoCursor cursor = collection + .find(Filters.and( + Filters.exists("publishedAction.datasource"), + Filters.exists("publishedAction.datasource.datasourceConfiguration"), + Filters.exists("publishedAction.datasource.datasourceConfiguration.authentication"))).cursor(); + while (cursor.hasNext()) { + Document current = (Document) cursor.next(); + Document old = Document.parse(current.toJson()); + + // Extra precaution to only update _class for authentication objects that don't already have this + // Is this condition required? What does production datasource look like? + ((Document) ((Document) ((Document) ((Document) current.get("publishedAction")) + .get("datasource")) + .get("datasourceConfiguration")) + .get("authentication")) + .putIfAbsent("_class", "dbAuth"); + + // Replace old document with the new one + collection.findOneAndReplace(old, current); + } + return null; + } + }); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceContextServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceContextServiceImpl.java index dd22618a22..150d42543d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceContextServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceContextServiceImpl.java @@ -15,6 +15,7 @@ import reactor.core.publisher.Mono; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import java.util.stream.Collectors; import static com.appsmith.server.acl.AclPermission.EXECUTE_DATASOURCES; @@ -170,10 +171,15 @@ public class DatasourceContextServiceImpl implements DatasourceContextService { } @Override - public AuthenticationDTO decryptSensitiveFields(AuthenticationDTO authenticationDTO) { - if (authenticationDTO != null && authenticationDTO.getPassword() != null) { - authenticationDTO.setPassword(encryptionService.decryptString(authenticationDTO.getPassword())); + public AuthenticationDTO decryptSensitiveFields(AuthenticationDTO authentication) { + if (authentication != null && authentication.isEncrypted()) { + Map decryptedFields = authentication.getEncryptionFields().entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> encryptionService.decryptString(e.getValue()))); + authentication.setEncryptionFields(decryptedFields); + authentication.setIsEncrypted(false); } - return authenticationDTO; + return authentication; } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceService.java index 1e15af6fa5..b78d67c45b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceService.java @@ -1,5 +1,6 @@ package com.appsmith.server.services; +import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.domains.Datasource; @@ -28,4 +29,6 @@ public interface DatasourceService extends CrudService { Flux findAllByOrganizationId(String organizationId, AclPermission readDatasources); Flux saveAll(List datasourceList); + + AuthenticationDTO encryptAuthenticationFields(AuthenticationDTO authentication); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceServiceImpl.java index 8ec077abd1..a54f3b5317 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/DatasourceServiceImpl.java @@ -3,7 +3,6 @@ package com.appsmith.server.services; import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceTestResult; -import com.appsmith.external.models.Endpoint; import com.appsmith.external.models.Policy; import com.appsmith.external.plugins.PluginExecutor; import com.appsmith.server.acl.AclPermission; @@ -12,7 +11,6 @@ import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Datasource; import com.appsmith.server.domains.Organization; import com.appsmith.server.domains.Plugin; -import com.appsmith.server.domains.PluginType; import com.appsmith.server.domains.User; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; @@ -36,6 +34,7 @@ import javax.validation.Validator; import javax.validation.constraints.NotNull; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -111,23 +110,23 @@ public class DatasourceServiceImpl extends BaseService sessionUserService.getCurrentUser() - .flatMap(user -> { - // Create policies for this datasource -> This datasource should inherit its permissions and policies from - // the organization and this datasource should also allow the current user to crud this datasource. - return organizationService.findById(datasource1.getOrganizationId(), AclPermission.ORGANIZATION_MANAGE_APPLICATIONS) - .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ORGANIZATION, datasource1.getOrganizationId()))) - .map(org -> { - Set policySet = org.getPolicies().stream() - .filter(policy -> - policy.getPermission().equals(ORGANIZATION_MANAGE_APPLICATIONS.getValue()) || - policy.getPermission().equals(ORGANIZATION_READ_APPLICATIONS.getValue()) - ).collect(Collectors.toSet()); + .flatMap(user -> { + // Create policies for this datasource -> This datasource should inherit its permissions and policies from + // the organization and this datasource should also allow the current user to crud this datasource. + return organizationService.findById(datasource1.getOrganizationId(), AclPermission.ORGANIZATION_MANAGE_APPLICATIONS) + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ORGANIZATION, datasource1.getOrganizationId()))) + .map(org -> { + Set policySet = org.getPolicies().stream() + .filter(policy -> + policy.getPermission().equals(ORGANIZATION_MANAGE_APPLICATIONS.getValue()) || + policy.getPermission().equals(ORGANIZATION_READ_APPLICATIONS.getValue()) + ).collect(Collectors.toSet()); - Set documentPolicies = policyGenerator.getAllChildPolicies(policySet, Organization.class, Datasource.class); - datasource1.setPolicies(documentPolicies); - return datasource1; - }); - }) + Set documentPolicies = policyGenerator.getAllChildPolicies(policySet, Organization.class, Datasource.class); + datasource1.setPolicies(documentPolicies); + return datasource1; + }); + }) ) .flatMap(this::validateAndSaveDatasourceToRepository); } @@ -157,10 +156,17 @@ public class DatasourceServiceImpl extends BaseService encryptedFields = authentication.getEncryptionFields().entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> encryptionService.encryptString(e.getValue()))); + authentication.setEncryptionFields(encryptedFields); + authentication.setIsEncrypted(true); } return authentication; } @@ -232,30 +238,36 @@ public class DatasourceServiceImpl extends BaseService testDatasource(Datasource datasource) { Mono datasourceMono = null; - - // Fetch the password from the db if the datasource being tested does not have password set. + // Fetch any fields that maybe encrypted from the db if the datasource being tested does not have those fields set. // This scenario would happen whenever an existing datasource is being tested and no changes are present in the - // password field (because password is not sent over the network after encryption back to the client - if (datasource.getId() != null && datasource.getDatasourceConfiguration()!=null && - datasource.getDatasourceConfiguration().getAuthentication()!=null) { - String password = datasource.getDatasourceConfiguration().getAuthentication().getPassword(); - if (password == null || password.isEmpty()) { + // encrypted field (because encrypted fields are not sent over the network after encryption back to the client + if (datasource.getId() != null && datasource.getDatasourceConfiguration() != null && + datasource.getDatasourceConfiguration().getAuthentication() != null) { + Set emptyFields = datasource.getDatasourceConfiguration().getAuthentication().getEmptyEncryptionFields(); + if (emptyFields != null && !emptyFields.isEmpty()) { datasourceMono = getById(datasource.getId()) - // If datasource has encrypted password, decrypt and set it in the datasource which is being tested - .map(datasourceFromRepo-> { - if (datasourceFromRepo.getDatasourceConfiguration()!=null && datasourceFromRepo.getDatasourceConfiguration().getAuthentication()!=null) { + // If datasource has encrypted fields, decrypt and set it in the datasource which is being tested + .map(datasourceFromRepo -> { + if (datasourceFromRepo.getDatasourceConfiguration() != null && datasourceFromRepo.getDatasourceConfiguration().getAuthentication() != null) { AuthenticationDTO authentication = datasourceFromRepo.getDatasourceConfiguration().getAuthentication(); - if (authentication.getPassword() != null) { - String decryptedPassword = encryptionService.decryptString(authentication.getPassword()); - datasource.getDatasourceConfiguration().getAuthentication().setPassword(decryptedPassword); + + if (!authentication.getEncryptionFields().isEmpty()) { + Map decryptedFields = authentication.getEncryptionFields(); + decryptedFields = decryptedFields.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> encryptionService.decryptString(e.getValue()))); + datasource.getDatasourceConfiguration().getAuthentication().setEncryptionFields(decryptedFields); + datasource.getDatasourceConfiguration().getAuthentication().setIsEncrypted(false); } + } return datasource; }) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/EncryptionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/EncryptionServiceImpl.java index 03f63d3a6d..fad320571c 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/EncryptionServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/EncryptionServiceImpl.java @@ -7,8 +7,6 @@ import org.springframework.security.crypto.encrypt.Encryptors; import org.springframework.security.crypto.encrypt.TextEncryptor; import org.springframework.stereotype.Service; -import java.math.BigInteger; - @Service public class EncryptionServiceImpl implements EncryptionService { private final EncryptionConfig encryptionConfig; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/MarketplaceServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/MarketplaceServiceImpl.java index be2950f3cd..24aae60893 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/MarketplaceServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/MarketplaceServiceImpl.java @@ -10,7 +10,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java index 549de280ef..7e8c53d101 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NewActionServiceImpl.java @@ -253,6 +253,17 @@ public class NewActionServiceImpl extends BaseService datasourceMono; if (action.getDatasource().getId() == null) { + if (action.getDatasource().getDatasourceConfiguration() != null && + action.getDatasource().getDatasourceConfiguration().getAuthentication() != null) { + action.getDatasource() + .getDatasourceConfiguration() + .setAuthentication(datasourceService.encryptAuthenticationFields(action + .getDatasource() + .getDatasourceConfiguration() + .getAuthentication() + )); + } + datasourceMono = Mono.just(action.getDatasource()) .flatMap(datasourceService::validateDatasource); } else { @@ -394,9 +405,6 @@ public class NewActionServiceImpl extends BaseService updatedActionMono = repository.findById(id, MANAGE_ACTIONS) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, id))) .map(dbAction -> { @@ -412,7 +420,7 @@ public class NewActionServiceImpl extends BaseService analyticsUpdateMono = updatedActionMono .flatMap(analyticsService::sendUpdateEvent); - // First Update the Action + // First Update the Action return savedUpdatedActionMono // Now send the update event to analytics service .then(analyticsUpdateMono) @@ -438,38 +446,38 @@ public class NewActionServiceImpl extends BaseService actionMono = repository.findById(actionId, EXECUTE_ACTIONS) - .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, actionId))) - .flatMap(dbAction -> { - ActionDTO action; - if (TRUE.equals(executeActionDTO.getViewMode())) { - action = dbAction.getPublishedAction(); - // If the action has not been published, return error - if (action == null) { - return Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, actionId)); - } - } else { - action = dbAction.getUnpublishedAction(); + .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, actionId))) + .flatMap(dbAction -> { + ActionDTO action; + if (TRUE.equals(executeActionDTO.getViewMode())) { + action = dbAction.getPublishedAction(); + // If the action has not been published, return error + if (action == null) { + return Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.ACTION, actionId)); } + } else { + action = dbAction.getUnpublishedAction(); + } - // Now check for erroneous situations which would deter the execution of the action : + // Now check for erroneous situations which would deter the execution of the action : - // Error out with in case of an invalid action - if (Boolean.FALSE.equals(action.getIsValid())) { - return Mono.error(new AppsmithException( - AppsmithError.INVALID_ACTION, - action.getName(), - actionId, - ArrayUtils.toString(action.getInvalids().toArray()) - )); - } + // Error out with in case of an invalid action + if (Boolean.FALSE.equals(action.getIsValid())) { + return Mono.error(new AppsmithException( + AppsmithError.INVALID_ACTION, + action.getName(), + actionId, + ArrayUtils.toString(action.getInvalids().toArray()) + )); + } - // Error out in case of JS Plugin (this is currently client side execution only) - if (dbAction.getPluginType() == PluginType.JS) { - return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); - } - return Mono.just(action); - }) - .cache(); + // Error out in case of JS Plugin (this is currently client side execution only) + if (dbAction.getPluginType() == PluginType.JS) { + return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION)); + } + return Mono.just(action); + }) + .cache(); // 3. Instantiate the implementation class based on the query type @@ -683,7 +691,7 @@ public class NewActionServiceImpl extends BaseService new AppsmithException( - AppsmithError.PLUGIN_LOAD_TEMPLATES_FAIL, Exceptions.unwrap(throwable).getMessage()) - ) + .onErrorMap(throwable -> { + log.error("Error loading templates for plugin {}.", plugin.getPackageName(), throwable); + return new AppsmithException( + AppsmithError.PLUGIN_LOAD_TEMPLATES_FAIL, + Exceptions.unwrap(throwable).getMessage() + ); + }) .cache(); templateCache.put(pluginId, mono); @@ -354,27 +361,49 @@ public class PluginServiceImpl extends BaseService templates = new HashMap<>(); - - Resource[] resources; + final PluginTemplatesMeta pluginTemplatesMeta; try { - resources = resolver.getResources("templates/*"); + pluginTemplatesMeta = objectMapper.readValue( + resolver.getResource("templates/meta.json").getInputStream(), + PluginTemplatesMeta.class + ); + } catch (IOException e) { - log.error("Error resolving templates in plugin for id: " + plugin.getId()); + log.error("Error loading templates metadata in plugin for id: " + plugin.getId()); throw Exceptions.propagate(e); + } - for (final Resource resource : resources) { - final String filename = resource.getFilename(); + if (pluginTemplatesMeta.getTemplates() == null) { + log.warn("Missing templates key in plugin templates meta."); + return Collections.emptyMap(); + } + + final Map templates = new LinkedHashMap<>(); + + for (final PluginTemplate template : pluginTemplatesMeta.getTemplates()) { + final String filename = template.getFile(); + + if (filename == null) { + log.warn("Empty or missing file for a template in plugin {}.", plugin.getPackageName()); + continue; + } + + final Resource resource = resolver.getResource("templates/" + filename); + final String title = StringUtils.isEmpty(template.getTitle()) + ? filename.replaceFirst("\\.\\w+$", "") + : template.getTitle(); + try { - final String templateContent = StreamUtils.copyToString( - resource.getInputStream(), Charset.defaultCharset()); - if (filename != null) { - templates.put(filename.replaceFirst("\\.\\w+$", ""), templateContent); - } + templates.put( + title, + StreamUtils.copyToString(resource.getInputStream(), Charset.defaultCharset()) + ); + } catch (IOException e) { log.error("Error loading template {} for plugin {}", filename, plugin.getId()); throw Exceptions.propagate(e); + } } @@ -403,4 +432,15 @@ public class PluginServiceImpl extends BaseService templates; + } + + @Data + static class PluginTemplate { + String file; + String title = null; + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationServiceImpl.java index 2964ccdb11..7c0f70b31c 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserOrganizationServiceImpl.java @@ -16,9 +16,9 @@ import com.appsmith.server.domains.UserRole; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.PolicyUtils; +import com.appsmith.server.notifications.EmailSender; import com.appsmith.server.repositories.OrganizationRepository; import com.appsmith.server.repositories.UserRepository; -import com.appsmith.server.notifications.EmailSender; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Example; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/DatasourceStructureSolution.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/DatasourceStructureSolution.java index fc31190a24..aa6f7dfb29 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/DatasourceStructureSolution.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/DatasourceStructureSolution.java @@ -22,6 +22,8 @@ import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; import java.time.Duration; +import java.util.Map; +import java.util.stream.Collectors; @Component @RequiredArgsConstructor @@ -39,16 +41,16 @@ public class DatasourceStructureSolution { public Mono getStructure(String datasourceId, boolean ignoreCache) { return datasourceService.getById(datasourceId) - .flatMap(datasource -> getStructure(datasource, ignoreCache)); + .flatMap(datasource -> getStructure(datasource, ignoreCache)); } public Mono getStructure(Datasource datasource, boolean ignoreCache) { // This mono, when computed, will yield the cached structure if applicable, or resolve to an empty mono. - // If the structure is `null` inside the datasource, this will resolve to empty as well. + // If the structure is `null` inside the datasource, this will resolve to empty as well. final Mono cachedStructureMono = - ignoreCache ? Mono.empty() : Mono.justOrEmpty(datasource.getStructure()); + ignoreCache ? Mono.empty() : Mono.justOrEmpty(datasource.getStructure()); - decryptPasswordInDatasource(datasource); + decryptEncryptedFieldsInDatasource(datasource); // This mono, when computed, will load the structure of the datasource by calling the plugin method. final Mono loadStructureMono = pluginExecutorHelper @@ -83,12 +85,17 @@ public class DatasourceStructureSolution { .defaultIfEmpty(new DatasourceStructure()); } - private Datasource decryptPasswordInDatasource(Datasource datasource) { - // If datasource has encrypted password, decrypt and set it in the datasource. + private Datasource decryptEncryptedFieldsInDatasource(Datasource datasource) { + // If datasource has encrypted fields, decrypt and set it in the datasource. if (datasource.getDatasourceConfiguration() != null) { AuthenticationDTO authentication = datasource.getDatasourceConfiguration().getAuthentication(); - if (authentication != null && authentication.getPassword() != null) { - authentication.setPassword(encryptionService.decryptString(authentication.getPassword())); + if (authentication != null && authentication.getEmptyEncryptionFields().isEmpty() && authentication.isEncrypted()) { + Map decryptedFields = authentication.getEncryptionFields().entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> encryptionService.decryptString(e.getValue()))); + authentication.setEncryptionFields(decryptedFields); + authentication.setIsEncrypted(false); } } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/DatasourceContextServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/DatasourceContextServiceTest.java index 978e7059a2..474af7bfa8 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/DatasourceContextServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/DatasourceContextServiceTest.java @@ -1,6 +1,6 @@ package com.appsmith.server.services; -import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.domains.Datasource; @@ -64,7 +64,7 @@ public class DatasourceContextServiceTest { datasource.setName("test datasource name for authenticated fields decryption test"); DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); datasourceConfiguration.setUrl("http://test.com"); - AuthenticationDTO authenticationDTO = new AuthenticationDTO(); + DBAuth authenticationDTO = new DBAuth(); String username = "username"; String password = "password"; authenticationDTO.setUsername(username); @@ -81,8 +81,8 @@ public class DatasourceContextServiceTest { StepVerifier .create(datasourceMono) .assertNext(savedDatasource -> { - AuthenticationDTO authentication = savedDatasource.getDatasourceConfiguration().getAuthentication(); - AuthenticationDTO decryptedAuthentication = datasourceContextService.decryptSensitiveFields(authentication); + DBAuth authentication = (DBAuth) savedDatasource.getDatasourceConfiguration().getAuthentication(); + DBAuth decryptedAuthentication = (DBAuth) datasourceContextService.decryptSensitiveFields(authentication); assertThat(decryptedAuthentication.getPassword()).isEqualTo(password); }) .verifyComplete(); @@ -98,7 +98,7 @@ public class DatasourceContextServiceTest { datasource.setName("test datasource name for authenticated fields decryption test null password"); DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); datasourceConfiguration.setUrl("http://test.com"); - AuthenticationDTO authenticationDTO = new AuthenticationDTO(); + DBAuth authenticationDTO = new DBAuth(); datasourceConfiguration.setAuthentication(authenticationDTO); datasource.setDatasourceConfiguration(datasourceConfiguration); datasource.setOrganizationId(orgId); @@ -111,8 +111,8 @@ public class DatasourceContextServiceTest { StepVerifier .create(datasourceMono) .assertNext(savedDatasource -> { - AuthenticationDTO authentication = savedDatasource.getDatasourceConfiguration().getAuthentication(); - AuthenticationDTO decryptedAuthentication = datasourceContextService.decryptSensitiveFields(authentication); + DBAuth authentication = (DBAuth) savedDatasource.getDatasourceConfiguration().getAuthentication(); + DBAuth decryptedAuthentication = (DBAuth) datasourceContextService.decryptSensitiveFields(authentication); assertThat(decryptedAuthentication.getPassword()).isNull(); }) .verifyComplete(); diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/DatasourceServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/DatasourceServiceTest.java index 517ad8962b..9169ef30c9 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/DatasourceServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/DatasourceServiceTest.java @@ -1,11 +1,12 @@ package com.appsmith.server.services; import com.appsmith.external.models.ActionConfiguration; -import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.Connection; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.Endpoint; +import com.appsmith.external.models.OAuth2; import com.appsmith.external.models.Policy; import com.appsmith.external.models.SSLDetails; import com.appsmith.external.models.UploadedFile; @@ -74,7 +75,7 @@ public class DatasourceServiceTest { @MockBean PluginExecutorHelper pluginExecutorHelper; - String orgId = ""; + String orgId = ""; @Before @WithUserDetails(value = "api_user") @@ -234,9 +235,10 @@ public class DatasourceServiceTest { Connection connection1 = new Connection(); SSLDetails ssl = new SSLDetails(); ssl.setKeyFile(new UploadedFile()); - ssl.getKeyFile().setName("ssl_key_file_id"); + ssl.getKeyFile().setName("ssl_key_file_id2"); connection1.setSsl(ssl); datasourceConfiguration1.setConnection(connection1); + updates.setDatasourceConfiguration(datasourceConfiguration1); return datasourceService.update(datasource1.getId(), updates); }); @@ -246,7 +248,72 @@ public class DatasourceServiceTest { assertThat(createdDatasource.getId()).isNotEmpty(); assertThat(createdDatasource.getPluginId()).isEqualTo(datasource.getPluginId()); assertThat(createdDatasource.getName()).isEqualTo(datasource.getName()); - assertThat(createdDatasource.getDatasourceConfiguration().getConnection().getSsl().getKeyFile().getName()).isEqualTo("ssl_key_file_id"); + assertThat(createdDatasource.getDatasourceConfiguration().getConnection().getSsl().getKeyFile().getName()).isEqualTo("ssl_key_file_id2"); + + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void createAndUpdateDatasourceDifferentAuthentication() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); + + Datasource datasource = new Datasource(); + datasource.setName("test db datasource1"); + datasource.setOrganizationId(orgId); + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + Connection connection = new Connection(); + connection.setMode(Connection.Mode.READ_ONLY); + connection.setType(Connection.Type.REPLICA_SET); + SSLDetails sslDetails = new SSLDetails(); + sslDetails.setAuthType(SSLDetails.AuthType.CA_CERTIFICATE); + sslDetails.setKeyFile(new UploadedFile("ssl_key_file_id", "")); + sslDetails.setCertificateFile(new UploadedFile("ssl_cert_file_id", "")); + connection.setSsl(sslDetails); + datasourceConfiguration.setConnection(connection); + DBAuth auth = new DBAuth(); + auth.setUsername("test"); + auth.setPassword("test"); + datasourceConfiguration.setAuthentication(auth); + datasource.setDatasourceConfiguration(datasourceConfiguration); + + datasource.setOrganizationId(orgId); + + Mono pluginMono = pluginService.findByName("Installed Plugin Name"); + + Mono datasourceMono = pluginMono + .map(plugin -> { + datasource.setPluginId(plugin.getId()); + return datasource; + }) + .flatMap(datasourceService::create) + .flatMap(datasource1 -> { + Datasource updates = new Datasource(); + DatasourceConfiguration datasourceConfiguration1 = new DatasourceConfiguration(); + Connection connection1 = new Connection(); + SSLDetails ssl = new SSLDetails(); + ssl.setKeyFile(new UploadedFile()); + ssl.getKeyFile().setName("ssl_key_file_id2"); + connection1.setSsl(ssl); + OAuth2 auth2 = new OAuth2(); + auth2.setClientId("test"); + auth2.setClientSecret("test"); + datasourceConfiguration1.setAuthentication(auth2); + datasourceConfiguration1.setConnection(connection1); + updates.setDatasourceConfiguration(datasourceConfiguration1); + + return datasourceService.update(datasource1.getId(), updates); + }); + + StepVerifier + .create(datasourceMono) + .assertNext(createdDatasource -> { + assertThat(createdDatasource.getId()).isNotEmpty(); + assertThat(createdDatasource.getPluginId()).isEqualTo(datasource.getPluginId()); + assertThat(createdDatasource.getName()).isEqualTo(datasource.getName()); + assertThat(createdDatasource.getDatasourceConfiguration().getConnection().getSsl().getKeyFile().getName()).isEqualTo("ssl_key_file_id2"); + assertThat(createdDatasource.getDatasourceConfiguration().getAuthentication() instanceof OAuth2).isTrue(); }) .verifyComplete(); } @@ -270,10 +337,10 @@ public class DatasourceServiceTest { final Mono> datasourcesMono = pluginMono .flatMap(plugin -> { - datasource1.setPluginId(plugin.getId()); - datasource2.setPluginId(plugin.getId()); - return datasourceService.create(datasource1); - }) + datasource1.setPluginId(plugin.getId()); + datasource2.setPluginId(plugin.getId()); + return datasourceService.create(datasource1); + }) .zipWhen(datasource -> datasourceService.create(datasource2)); StepVerifier @@ -323,6 +390,54 @@ public class DatasourceServiceTest { .verifyComplete(); } + @Test + @WithUserDetails(value = "api_user") + public void testDatasourceEmptyFields() { + + Datasource datasource = new Datasource(); + datasource.setName("test db datasource empty"); + datasource.setOrganizationId(orgId); + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + Connection connection = new Connection(); + connection.setMode(Connection.Mode.READ_ONLY); + connection.setType(Connection.Type.REPLICA_SET); + SSLDetails sslDetails = new SSLDetails(); + sslDetails.setAuthType(SSLDetails.AuthType.CA_CERTIFICATE); + sslDetails.setKeyFile(new UploadedFile("ssl_key_file_id", "")); + sslDetails.setCertificateFile(new UploadedFile("ssl_cert_file_id", "")); + connection.setSsl(sslDetails); + datasourceConfiguration.setConnection(connection); + DBAuth auth = new DBAuth(); + auth.setUsername("test"); + auth.setPassword("test"); + datasourceConfiguration.setAuthentication(auth); + datasource.setDatasourceConfiguration(datasourceConfiguration); + + datasource.setOrganizationId(orgId); + + Mono pluginMono = pluginService.findByName("Installed Plugin Name"); + + Mono datasourceMono = pluginMono.map(plugin -> { + datasource.setPluginId(plugin.getId()); + return datasource; + }).flatMap(datasourceService::create); + + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); + + Mono testResultMono = datasourceMono.flatMap(datasource1 -> { + ((DBAuth) datasource1.getDatasourceConfiguration().getAuthentication()).setPassword(null); + return datasourceService.testDatasource(datasource1); + }); + + StepVerifier + .create(testResultMono) + .assertNext(testResult -> { + assertThat(testResult).isNotNull(); + assertThat(testResult.getInvalids()).isEmpty(); + }) + .verifyComplete(); + } + @Test @WithUserDetails(value = "api_user") public void deleteDatasourceWithoutActions() { @@ -424,13 +539,13 @@ public class DatasourceServiceTest { @WithUserDetails(value = "api_user") public void checkEncryptionOfAuthenticationDTOTest() { Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor())); - + Mono pluginMono = pluginService.findByName("Installed Plugin Name"); Datasource datasource = new Datasource(); datasource.setName("test datasource name for authenticated fields encryption test"); DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); datasourceConfiguration.setUrl("http://test.com"); - AuthenticationDTO authenticationDTO = new AuthenticationDTO(); + DBAuth authenticationDTO = new DBAuth(); String username = "username"; String password = "password"; authenticationDTO.setUsername(username); @@ -447,7 +562,7 @@ public class DatasourceServiceTest { StepVerifier .create(datasourceMono) .assertNext(savedDatasource -> { - AuthenticationDTO authentication = savedDatasource.getDatasourceConfiguration().getAuthentication(); + DBAuth authentication = (DBAuth) savedDatasource.getDatasourceConfiguration().getAuthentication(); assertThat(authentication.getUsername()).isEqualTo(username); assertThat(authentication.getPassword()).isEqualTo(encryptionService.encryptString(password)); }) @@ -464,7 +579,7 @@ public class DatasourceServiceTest { datasource.setName("test datasource name for authenticated fields encryption test null password."); DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); datasourceConfiguration.setUrl("http://test.com"); - AuthenticationDTO authenticationDTO = new AuthenticationDTO(); + DBAuth authenticationDTO = new DBAuth(); authenticationDTO.setDatabaseName("admin"); datasourceConfiguration.setAuthentication(authenticationDTO); datasource.setDatasourceConfiguration(datasourceConfiguration); @@ -478,9 +593,10 @@ public class DatasourceServiceTest { StepVerifier .create(datasourceMono) .assertNext(savedDatasource -> { - AuthenticationDTO authentication = savedDatasource.getDatasourceConfiguration().getAuthentication(); + DBAuth authentication = (DBAuth) savedDatasource.getDatasourceConfiguration().getAuthentication(); assertThat(authentication.getUsername()).isNull(); assertThat(authentication.getPassword()).isNull(); + assertThat(authentication.isEncrypted()).isFalse(); }) .verifyComplete(); } @@ -495,7 +611,7 @@ public class DatasourceServiceTest { datasource.setName("test datasource name for authenticated fields encryption test post update"); DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); datasourceConfiguration.setUrl("http://test.com"); - AuthenticationDTO authenticationDTO = new AuthenticationDTO(); + DBAuth authenticationDTO = new DBAuth(); String username = "username"; String password = "password"; authenticationDTO.setUsername(username); @@ -519,9 +635,10 @@ public class DatasourceServiceTest { StepVerifier .create(datasourceMono) .assertNext(updatedDatasource -> { - AuthenticationDTO authentication = updatedDatasource.getDatasourceConfiguration().getAuthentication(); + DBAuth authentication = (DBAuth) updatedDatasource.getDatasourceConfiguration().getAuthentication(); assertThat(authentication.getUsername()).isEqualTo(username); assertThat(authentication.getPassword()).isEqualTo(encryptionService.encryptString(password)); + assertThat(authentication.isEncrypted()).isTrue(); }) .verifyComplete(); } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java index edf494b74d..2012bbb83d 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ExamplesOrganizationClonerTests.java @@ -1,7 +1,7 @@ package com.appsmith.server.solutions; import com.appsmith.external.models.ActionConfiguration; -import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.Property; import com.appsmith.server.constants.FieldName; @@ -361,8 +361,9 @@ public class ExamplesOrganizationClonerTests { ds2.setName("datasource 2"); ds2.setOrganizationId(organization.getId()); ds2.setDatasourceConfiguration(new DatasourceConfiguration()); - ds2.getDatasourceConfiguration().setAuthentication(new AuthenticationDTO()); - ds2.getDatasourceConfiguration().getAuthentication().setPassword("answer-to-life"); + DBAuth auth = new DBAuth(); + auth.setPassword("answer-to-life"); + ds2.getDatasourceConfiguration().setAuthentication(auth); return Mono.when( datasourceService.create(ds1), @@ -398,7 +399,7 @@ public class ExamplesOrganizationClonerTests { .findFirst() .orElseThrow(); assertThat(ds2.getDatasourceConfiguration().getAuthentication()).isNotNull(); - assertThat(ds2.getDatasourceConfiguration().getAuthentication().getPassword()) + assertThat(((DBAuth) ds2.getDatasourceConfiguration().getAuthentication()).getPassword()) .isEqualTo(encryptionService.encryptString("answer-to-life")); assertThat(data.applications).isEmpty(); diff --git a/deploy/install.sh b/deploy/install.sh index 34af6b42a9..3c5d0eea8a 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -524,8 +524,8 @@ echo "Installing Appsmith to '$install_dir'." mkdir -p "$install_dir" echo "" -if confirm y "Would you like to initialize the default database?"; then - echo "Appsmith needs to create a MongoDB instance." +echo "Appsmith needs a MongoDB instance to run" +if confirm y "Initialise a new database? (Recommended)"; then mongo_host="mongo" mongo_database="appsmith"