`
white-space: pre-wrap;
text-align: ${(props) => props.textAlign.toLowerCase()};
}
+ .auto-layout & span {
+ min-height: 32px;
+ }
`;
const ModalContent = styled.div<{
diff --git a/app/client/src/widgets/WidgetUtils.ts b/app/client/src/widgets/WidgetUtils.ts
index 5534fdfc3c..057c24070d 100644
--- a/app/client/src/widgets/WidgetUtils.ts
+++ b/app/client/src/widgets/WidgetUtils.ts
@@ -752,7 +752,7 @@ export const flat = (array: DropdownOption[]) => {
*/
export const isAutoHeightEnabledForWidgetWithLimits = (props: WidgetProps) => {
- if (props.isFlexChild) return false;
+ if (props?.isFlexChild) return false;
return props.dynamicHeight === DynamicHeight.AUTO_HEIGHT_WITH_LIMITS;
};
@@ -763,7 +763,7 @@ export const isAutoHeightEnabledForWidgetWithLimits = (props: WidgetProps) => {
*/
export const isAutoHeightEnabledForWidget = (props: WidgetProps) => {
- if (props.isFlexChild) return false;
+ if (props?.isFlexChild) return false;
return (
props.dynamicHeight === DynamicHeight.AUTO_HEIGHT ||
diff --git a/app/client/src/workers/Evaluation/fns/index.ts b/app/client/src/workers/Evaluation/fns/index.ts
index 464427ed2b..2d7adef1d9 100644
--- a/app/client/src/workers/Evaluation/fns/index.ts
+++ b/app/client/src/workers/Evaluation/fns/index.ts
@@ -60,7 +60,7 @@ import {
stopWatchGeoLocation,
watchGeoLocation,
} from "./geolocationFns";
-import { isAsyncGuard } from "./utils/fnGuard";
+import { getFnWithGuards, isAsyncGuard } from "./utils/fnGuard";
// cloudHosting -> to use in EE
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -120,34 +120,44 @@ export const entityFns = [
name: "run",
qualifier: (entity: DataTreeEntity) => isAction(entity),
fn: (entity: DataTreeEntity, entityName: string) =>
- isAsyncGuard(run.bind(entity), `${entityName}.run`),
+ getFnWithGuards(run.bind(entity), `${entityName}.run`, [isAsyncGuard]),
},
{
name: "clear",
qualifier: (entity: DataTreeEntity) => isAction(entity),
fn: (entity: DataTreeEntity, entityName: string) =>
- isAsyncGuard(clear.bind(entity), `${entityName}.clear`),
+ getFnWithGuards(clear.bind(entity), `${entityName}.clear`, [
+ isAsyncGuard,
+ ]),
},
{
name: "getGeoLocation",
path: "appsmith.geolocation.getCurrentPosition",
qualifier: (entity: DataTreeEntity) => isAppsmithEntity(entity),
fn: () =>
- isAsyncGuard(getGeoLocation, "appsmith.geolocation.getCurrentPosition"),
+ getFnWithGuards(
+ getGeoLocation,
+ "appsmith.geolocation.getCurrentPosition",
+ [isAsyncGuard],
+ ),
},
{
name: "watchGeoLocation",
path: "appsmith.geolocation.watchPosition",
qualifier: (entity: DataTreeEntity) => isAppsmithEntity(entity),
fn: () =>
- isAsyncGuard(watchGeoLocation, "appsmith.geolocation.watchPosition"),
+ getFnWithGuards(watchGeoLocation, "appsmith.geolocation.watchPosition", [
+ isAsyncGuard,
+ ]),
},
{
name: "stopWatchGeoLocation",
path: "appsmith.geolocation.clearWatch",
qualifier: (entity: DataTreeEntity) => isAppsmithEntity(entity),
fn: () =>
- isAsyncGuard(stopWatchGeoLocation, "appsmith.geolocation.clearWatch"),
+ getFnWithGuards(stopWatchGeoLocation, "appsmith.geolocation.clearWatch", [
+ isAsyncGuard,
+ ]),
},
];
diff --git a/app/client/src/workers/Evaluation/fns/utils/fnGuard.ts b/app/client/src/workers/Evaluation/fns/utils/fnGuard.ts
index 58523cfbed..4b6b61a5c4 100644
--- a/app/client/src/workers/Evaluation/fns/utils/fnGuard.ts
+++ b/app/client/src/workers/Evaluation/fns/utils/fnGuard.ts
@@ -1,5 +1,7 @@
import { ActionCalledInSyncFieldError } from "workers/Evaluation/errorModifier";
+type FnGuard = (fn: (...args: any[]) => unknown, fnName: string) => unknown;
+
export function addFn(
ctx: any,
fnName: string,
@@ -8,10 +10,8 @@ export function addFn(
) {
Object.defineProperty(ctx, fnName, {
value: function (...args: any[]) {
- for (const guard of fnGuards) {
- fn = guard(fn, fnName);
- }
- return fn(...args);
+ const fnWithGuards = getFnWithGuards(fn, fnName, fnGuards);
+ return fnWithGuards(...args);
},
enumerable: false,
writable: true,
@@ -23,9 +23,21 @@ export function isAsyncGuard>(
fn: (...args: P) => unknown,
fnName: string,
) {
- return (...args: P) => {
- if (!self.$isDataField) return fn(...args);
+ if (self.$isDataField) {
self["$isAsync"] = true;
throw new ActionCalledInSyncFieldError(fnName);
+ }
+}
+
+export function getFnWithGuards(
+ fn: (...args: any[]) => unknown,
+ fnName: string,
+ fnGuards: FnGuard[],
+) {
+ return (...args: any[]) => {
+ for (const guard of fnGuards) {
+ guard(fn, fnName);
+ }
+ return fn(...args);
};
}
diff --git a/app/client/start-https.sh b/app/client/start-https.sh
index efe890d6ee..313dffd57f 100755
--- a/app/client/start-https.sh
+++ b/app/client/start-https.sh
@@ -272,8 +272,6 @@ $(if [[ $use_https == 1 ]]; then echo "
proxy_pass $frontend;
sub_filter __APPSMITH_SENTRY_DSN__ '${APPSMITH_SENTRY_DSN-}';
sub_filter __APPSMITH_SMART_LOOK_ID__ '${APPSMITH_SMART_LOOK_ID-}';
- sub_filter __APPSMITH_OAUTH2_GOOGLE_CLIENT_ID__ '${APPSMITH_OAUTH2_GOOGLE_CLIENT_ID-}';
- sub_filter __APPSMITH_OAUTH2_GITHUB_CLIENT_ID__ '${APPSMITH_OAUTH2_GITHUB_CLIENT_ID-}';
sub_filter __APPSMITH_MARKETPLACE_ENABLED__ '${APPSMITH_MARKETPLACE_ENABLED-}';
sub_filter __APPSMITH_SEGMENT_KEY__ '${APPSMITH_SEGMENT_KEY-}';
sub_filter __APPSMITH_ALGOLIA_API_ID__ '${APPSMITH_ALGOLIA_API_ID-}';
diff --git a/app/client/yarn.lock b/app/client/yarn.lock
index 991b0ff03f..401d6a8282 100644
--- a/app/client/yarn.lock
+++ b/app/client/yarn.lock
@@ -7267,6 +7267,14 @@ algoliasearch@^4.2.0:
"@algolia/requester-node-http" "4.5.1"
"@algolia/transporter" "4.5.1"
+ally.js@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/ally.js/-/ally.js-1.4.1.tgz#9fb7e6ba58efac4ee9131cb29aa9ee3b540bcf1e"
+ integrity sha512-ZewdfuwP6VewtMN36QY0gmiyvBfMnmEaNwbVu2nTS6zRt069viTgkYgaDiqu6vRJ1VJCriNqV0jGMu44R8zNbA==
+ dependencies:
+ css.escape "^1.5.0"
+ platform "1.3.3"
+
ansi-align@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59"
@@ -8684,11 +8692,6 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
-classnames@*:
- version "2.3.2"
- resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
- integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
-
classnames@2.x, classnames@^2.3.1:
version "2.3.1"
resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz"
@@ -9249,13 +9252,6 @@ cosmiconfig@^7.0.0:
path-type "^4.0.0"
yaml "^1.10.0"
-country-flag-emoji-polyfill@^0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/country-flag-emoji-polyfill/-/country-flag-emoji-polyfill-0.1.4.tgz#54b7ca61220c124b11d3091c46d16bd7f3ba0016"
- integrity sha512-e20azlb9yHb3mpL3lAlhkidmJgB5TELpA8oe0DPlyIfnqXhAGBmgLDpC+mm+Envh57n1xPrOfkJtXq2CrpvoGQ==
- dependencies:
- is-emoji-supported "^0.0.5"
-
cp-file@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-7.0.0.tgz#b9454cfd07fe3b974ab9ea0e5f29655791a9b8cd"
@@ -9512,7 +9508,7 @@ css-what@^5.0.0:
resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe"
integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==
-css.escape@^1.5.1:
+css.escape@^1.5.0, css.escape@^1.5.1:
version "1.5.1"
resolved "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz"
@@ -9680,6 +9676,13 @@ cypress-multi-reporters@^1.2.4:
debug "^4.1.1"
lodash "^4.17.15"
+cypress-plugin-tab@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/cypress-plugin-tab/-/cypress-plugin-tab-1.0.5.tgz#a40714148104004bb05ed62b1bf46bb544f8eb4a"
+ integrity sha512-QtTJcifOVwwbeMP3hsOzQOKf3EqKsLyjtg9ZAGlYDntrCRXrsQhe4ZQGIthRMRLKpnP6/tTk6G0gJ2sZUfRliQ==
+ dependencies:
+ ally.js "^1.4.1"
+
cypress-real-events@^1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.1.tgz#8f430d67c29ea4f05b9c5b0311780120cbc9b935"
@@ -13238,11 +13241,6 @@ is-dom@^1.0.0:
is-object "^1.0.1"
is-window "^1.0.2"
-is-emoji-supported@^0.0.5:
- version "0.0.5"
- resolved "https://registry.yarnpkg.com/is-emoji-supported/-/is-emoji-supported-0.0.5.tgz#f22301b22c63d6322935e829f39dfa59d03a7fe2"
- integrity sha512-WOlXUhDDHxYqcSmFZis+xWhhqXiK2SU0iYiqmth5Ip0FHLZQAt9rKL5ahnilE8/86WH8tZ3bmNNNC+bTzamqlw==
-
is-extendable@^0.1.0, is-extendable@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
@@ -16680,6 +16678,11 @@ pkg-up@^3.1.0:
dependencies:
find-up "^3.0.0"
+platform@1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.3.tgz#646c77011899870b6a0903e75e997e8e51da7461"
+ integrity sha512-VJK1SRmXBpjwsB4YOHYSturx48rLKMzHgCqDH2ZDa6ZbMS/N5huoNqyQdK5Fj/xayu3fqbXckn5SeCS1EbMDZg==
+
plist@^3.0.1:
version "3.0.5"
resolved "https://registry.npmjs.org/plist/-/plist-3.0.5.tgz"
@@ -18031,14 +18034,6 @@ react-documents@^1.0.4:
resolved "https://registry.npmjs.org/react-documents/-/react-documents-1.0.4.tgz"
integrity sha512-EpoY+MZEu3hPffIWA4FadUYu8daubNkr+LK2zuoPkCAVtyNY+z+/RuzzTriuhjcDydKXzgzp42kQTfAD2j3Mxw==
-react-dom@*:
- version "18.2.0"
- resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
- integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
- dependencies:
- loose-envify "^1.1.0"
- scheduler "^0.23.0"
-
react-dom@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
@@ -18537,13 +18532,6 @@ react-zoom-pan-pinch@^1.6.1:
version "1.6.1"
resolved "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-1.6.1.tgz"
-react@*:
- version "18.2.0"
- resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
- integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
- dependencies:
- loose-envify "^1.1.0"
-
react@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
@@ -19363,13 +19351,6 @@ scheduler@^0.20.2:
loose-envify "^1.1.0"
object-assign "^4.1.1"
-scheduler@^0.23.0:
- version "0.23.0"
- resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
- integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
- dependencies:
- loose-envify "^1.1.0"
-
schema-utils@2.7.0:
version "2.7.0"
resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz"
diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/ExecuteActionDTO.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/ExecuteActionDTO.java
index 97473e11f1..7cb3873eff 100644
--- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/ExecuteActionDTO.java
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/ExecuteActionDTO.java
@@ -26,31 +26,28 @@ public class ExecuteActionDTO {
/* Sample value of paramProperties
"paramProperties": {
- "k1": "string",
- "k2": "object",
- "k3": "number",
- "k4": {
- "array": [
- "string",
- "number",
- "string",
- "boolean"
- ]
- },
- "k5": "boolean"
+ "k0": {
+ "datatype": "string",
+ "blobIdentifiers": ["blobUrl1", "blobUrl2"]
+ }
}
*/
- Map paramProperties;
+ Map paramProperties;
- Map parameterMap; //e.g. {"Text1.text": "k1","Table1.data": "k2", "Api1.data": "k3"}
- Map invertParameterMap; //e.g. {"k1":"Text1.text","k2":"Table1.data", "k3": "Api1.data"}
+ Map parameterMap; // e.g. {"Text1.text": "k1","Table1.data": "k2", "Api1.data": "k3"}
+
+ // This map is where we store the string values of the blob parts for replacement into evaluated value params
+ Map blobValuesMap; // e.g. {"blobId": "stringified-blob-data"}
+
+ Map invertParameterMap; // e.g. {"k1":"Text1.text","k2":"Table1.data", "k3": "Api1.data"}
@JsonIgnore
long totalReadableByteCount;
public void setParameterMap(Map parameterMap) {
this.parameterMap = parameterMap;
- invertParameterMap = parameterMap.entrySet().stream()
+ invertParameterMap = parameterMap.entrySet()
+ .stream()
.collect(Collectors.toMap(
Map.Entry::getValue,
Map.Entry::getKey
diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/ParamProperty.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/ParamProperty.java
new file mode 100644
index 0000000000..347f77f203
--- /dev/null
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/dtos/ParamProperty.java
@@ -0,0 +1,19 @@
+package com.appsmith.external.dtos;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.util.List;
+
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class ParamProperty {
+
+ Object datatype;
+
+ List blobIdentifiers;
+}
diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/PluginUtils.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/PluginUtils.java
index 4b034fdb9d..c9d659ef4a 100644
--- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/PluginUtils.java
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/PluginUtils.java
@@ -1,29 +1,34 @@
package com.appsmith.external.helpers;
import com.appsmith.external.constants.ConditionalOperator;
+import com.appsmith.external.datatypes.ClientDataType;
+import com.appsmith.external.dtos.ExecuteActionDTO;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
import com.appsmith.external.models.Condition;
import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.Endpoint;
+import com.appsmith.external.models.Param;
import com.appsmith.external.models.Property;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
-import org.json.JSONObject;
import org.json.JSONException;
+import org.json.JSONObject;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.lang.reflect.Type;
+import java.sql.Connection;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -235,7 +240,7 @@ public class PluginUtils {
}
}
- public static void setDataValueSafelyInFormData(Map formData, String field, Object value) {
+ public static Map setDataValueSafelyInFormData(Map formData, String field, Object value) {
// In case the formData has not been initialized before the fxn call, assign a new HashMap to the variable
if (formData == null) {
@@ -268,9 +273,11 @@ public class PluginUtils {
formData.put(field, valueMap);
}
}
+
+ return formData;
}
- public static void setValueSafelyInFormData(Map formData, String field, Object value) {
+ public static Map setValueSafelyInFormData(Map formData, String field, Object value) {
// In case the formData has not been initialized before the fxn call, assign a new HashMap to the variable
if (formData == null) {
@@ -296,6 +303,8 @@ public class PluginUtils {
// This is a top level field. Set the value
formData.put(field, value);
}
+
+ return formData;
}
public static boolean endpointContainsLocalhost(Endpoint endpoint) {
@@ -452,4 +461,33 @@ public class PluginUtils {
return propertyValue.toString();
}
+
+ public static void safelyCloseSingleConnectionFromHikariCP(Connection connection, String logOnError) {
+ if (connection != null) {
+ try {
+ // Return the connection back to the pool
+ connection.close();
+ } catch (SQLException e) {
+ log.debug(logOnError, e);
+ }
+ }
+ }
+
+ public static ExecuteActionDTO getExecuteDTOForTestWithBindingAndValueAndDataType(LinkedHashMap bindingValueDataTypeMap) {
+ List params = new ArrayList<>();
+ bindingValueDataTypeMap.keySet().stream()
+ .forEach(bindingName -> {
+ String bindingValue = (String) (bindingValueDataTypeMap.get(bindingName)).get(0);
+ ClientDataType clientDataType = (ClientDataType) (bindingValueDataTypeMap.get(bindingName)).get(1);
+ Param param = new Param();
+ param.setKey(bindingName);
+ param.setValue(bindingValue);
+ param.setClientDataType(clientDataType);
+ params.add(param);
+ });
+
+ ExecuteActionDTO executeActionDTO = new ExecuteActionDTO();
+ executeActionDTO.setParams(params);
+ return executeActionDTO;
+ }
}
diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/Datasource.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/Datasource.java
index 21521c5d90..567483410b 100644
--- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/Datasource.java
+++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/Datasource.java
@@ -3,7 +3,6 @@ package com.appsmith.external.models;
import com.appsmith.external.views.Views;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonView;
-
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@@ -38,7 +37,7 @@ public class Datasource extends BranchAwareDomain {
@Transient
@JsonView(Views.Public.class)
String pluginName;
-
+
//Organizations migrated to workspaces, kept the field as deprecated to support the old migration
@Deprecated
@JsonView(Views.Public.class)
@@ -122,6 +121,7 @@ public class Datasource extends BranchAwareDomain {
* Intended to function like `.equals`, but only semantically significant fields, except for the ID. Semantically
* significant just means that if two datasource have same values for these fields, actions against them will behave
* exactly the same.
+ *
* @return true if equal, false otherwise.
*/
public boolean softEquals(Datasource other) {
diff --git a/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor/addToCollection.json b/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor/addToCollection.json
index d7c8829f06..8d6b8f11ca 100644
--- a/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor/addToCollection.json
+++ b/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor/addToCollection.json
@@ -25,7 +25,6 @@
"configProperty": "actionConfiguration.formData.timestampValuePath.data",
"controlType": "QUERY_DYNAMIC_INPUT_TEXT",
"evaluationSubstitutionType": "TEMPLATE",
- "isRequired": true,
"initialValue": "",
"placeholderText": "[ \"checkinLog.timestampKey\" ]"
}
diff --git a/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor/createDocument.json b/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor/createDocument.json
index 6c57ad3ab7..2da8632fd2 100644
--- a/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor/createDocument.json
+++ b/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor/createDocument.json
@@ -25,7 +25,6 @@
"configProperty": "actionConfiguration.formData.timestampValuePath.data",
"controlType": "QUERY_DYNAMIC_INPUT_TEXT",
"evaluationSubstitutionType": "TEMPLATE",
- "isRequired": true,
"initialValue": "",
"placeholderText": "[ \"checkinLog.timestampKey\" ]"
}
diff --git a/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor/setDocument.json b/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor/setDocument.json
index a21e03bab9..2078b02f03 100644
--- a/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor/setDocument.json
+++ b/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor/setDocument.json
@@ -25,7 +25,6 @@
"configProperty": "actionConfiguration.formData.timestampValuePath.data",
"controlType": "QUERY_DYNAMIC_INPUT_TEXT",
"evaluationSubstitutionType": "TEMPLATE",
- "isRequired": true,
"initialValue": "",
"placeholderText": "[ \"checkinLog.timestampKey\" ]"
}
diff --git a/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor/updateDocument.json b/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor/updateDocument.json
index f91b89ba00..8cd09f5906 100644
--- a/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor/updateDocument.json
+++ b/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor/updateDocument.json
@@ -34,7 +34,6 @@
"configProperty": "actionConfiguration.formData.timestampValuePath.data",
"controlType": "QUERY_DYNAMIC_INPUT_TEXT",
"evaluationSubstitutionType": "TEMPLATE",
- "isRequired": true,
"initialValue": "",
"placeholderText": "[ \"checkinLog.timestampKey\" ]"
}
diff --git a/app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/utils/SheetsUtil.java b/app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/utils/SheetsUtil.java
index 30ca8d94f8..91ebd3bfc0 100644
--- a/app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/utils/SheetsUtil.java
+++ b/app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/utils/SheetsUtil.java
@@ -15,6 +15,7 @@ import com.appsmith.external.models.DatasourceConfiguration;
public class SheetsUtil {
private static final String FILE_SPECIFIC_DRIVE_SCOPE = "https://www.googleapis.com/auth/drive.file";
+ private static final int USER_AUTHORIZED_SHEET_IDS_INDEX = 1;
static Pattern COLUMN_NAME_PATTERN = Pattern.compile("[a-zA-Z]+");
public static int getColumnNumber(String columnName) {
@@ -33,11 +34,12 @@ public class SheetsUtil {
public static Set getUserAuthorizedSheetIds(DatasourceConfiguration datasourceConfiguration) {
OAuth2 oAuth2 = (OAuth2) datasourceConfiguration.getAuthentication();
if (!isEmpty(datasourceConfiguration.getProperties())
- && datasourceConfiguration.getProperties().get(0) != null
- && datasourceConfiguration.getProperties().get(0).getValue() != null
+ && datasourceConfiguration.getProperties().size() > 1
+ && datasourceConfiguration.getProperties().get(USER_AUTHORIZED_SHEET_IDS_INDEX) != null
+ && datasourceConfiguration.getProperties().get(USER_AUTHORIZED_SHEET_IDS_INDEX).getValue() != null
&& oAuth2.getScope() != null
&& oAuth2.getScope().contains(FILE_SPECIFIC_DRIVE_SCOPE)) {
- ArrayList temp = (ArrayList) datasourceConfiguration.getProperties().get(0).getValue();
+ ArrayList temp = (ArrayList) datasourceConfiguration.getProperties().get(USER_AUTHORIZED_SHEET_IDS_INDEX).getValue();
return new HashSet(temp);
}
return null;
diff --git a/app/server/appsmith-plugins/googleSheetsPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/googleSheetsPlugin/src/main/resources/form.json
index cc5e9b71a6..5fe2a428cb 100644
--- a/app/server/appsmith-plugins/googleSheetsPlugin/src/main/resources/form.json
+++ b/app/server/appsmith-plugins/googleSheetsPlugin/src/main/resources/form.json
@@ -72,25 +72,43 @@
"hidden": true,
"initialValue": "authorization_code"
},
+ {
+ "label": "Account",
+ "configProperty": "datasourceConfiguration.properties[0].value",
+ "controlType": "INPUT_TEXT",
+ "isRequired": false,
+ "hidden": {
+ "comparison": "VIEW_MODE",
+ "value": false
+ },
+ "initialValue": "Authorize datasource to fetch account name"
+ },
{
"label": "Permissions | Scope",
"configProperty": "datasourceConfiguration.authentication.scopeString",
"controlType": "DROP_DOWN",
"options": [
{
- "label": "Read/Write | Selected Google Sheets",
+ "label": "Read / Write / Delete | Selected Google Sheets",
"value": "https://www.googleapis.com/auth/drive.file"
},
{
- "label": "Read/Write | All Google Sheets",
+ "label": "Read / Write / Delete | All Google Sheets",
"value": "https://www.googleapis.com/auth/spreadsheets,https://www.googleapis.com/auth/drive"
},
+ {
+ "label": "Read / Write | All Google Sheets",
+ "value": "https://www.googleapis.com/auth/spreadsheets,https://www.googleapis.com/auth/drive.readonly"
+ },
{
"label": "Read | All Google Sheets",
"value": "https://www.googleapis.com/auth/spreadsheets.readonly,https://www.googleapis.com/auth/drive.readonly"
}
],
- "initialValue": "https://www.googleapis.com/auth/drive.file"
+ "initialValue": "https://www.googleapis.com/auth/drive.file",
+ "customStyles": {
+ "width": "340px"
+ }
}
]
}
diff --git a/app/server/appsmith-plugins/googleSheetsPlugin/src/test/java/com/external/config/SheetsUtilTest.java b/app/server/appsmith-plugins/googleSheetsPlugin/src/test/java/com/external/config/SheetsUtilTest.java
new file mode 100644
index 0000000000..e29d4b1bd5
--- /dev/null
+++ b/app/server/appsmith-plugins/googleSheetsPlugin/src/test/java/com/external/config/SheetsUtilTest.java
@@ -0,0 +1,54 @@
+package com.external.config;
+
+import com.appsmith.external.models.DatasourceConfiguration;
+import com.appsmith.external.models.OAuth2;
+import com.appsmith.external.models.Property;
+import com.external.utils.SheetsUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+
+public class SheetsUtilTest {
+
+ @Test
+ public void testGetUserAuthorizedSheetIds_allsheets_returnsNull() throws JsonProcessingException {
+ DatasourceConfiguration dsConfig = new DatasourceConfiguration();
+ List propList = new ArrayList();
+ Property prop = new Property();
+ prop.setKey("emailAddress");
+ prop.setValue("test_email");
+ propList.add(prop);
+ dsConfig.setProperties(propList);
+ Set result = SheetsUtil.getUserAuthorizedSheetIds(dsConfig);
+ assertEquals(result, null);
+ }
+
+ @Test
+ public void testGetUserAuthorizedSheetIds_specificSheets_returnsSetOfFileIds() throws JsonProcessingException {
+ DatasourceConfiguration dsConfig = new DatasourceConfiguration();
+ List propList = new ArrayList();
+ OAuth2 oAuth2 = new OAuth2();
+
+ oAuth2.setScopeString("https://www.googleapis.com/auth/drive.file");
+ dsConfig.setAuthentication(oAuth2);
+
+ Property prop1 = new Property("emailAddress", "test_email");
+ propList.add(prop1);
+
+ List ids = new ArrayList();
+ ids.add("id1");
+
+ Property prop2 = new Property("userAuthorizedSheetIds", ids);
+ propList.add(prop2);
+
+ dsConfig.setProperties(propList);
+ Set result = SheetsUtil.getUserAuthorizedSheetIds(dsConfig);
+ assertEquals(result.size(), 1);
+ }
+}
\ No newline at end of file
diff --git a/app/server/appsmith-plugins/oraclePlugin/pom.xml b/app/server/appsmith-plugins/oraclePlugin/pom.xml
index fe667fbae6..d3e6761b1e 100755
--- a/app/server/appsmith-plugins/oraclePlugin/pom.xml
+++ b/app/server/appsmith-plugins/oraclePlugin/pom.xml
@@ -23,51 +23,7 @@
-
- org.pf4j
- pf4j-spring
- 0.8.0
- provided
-
-
- slf4j-reload4j
- org.slf4j
-
-
-
-
-
- com.appsmith
- interfaces
- 1.0-SNAPSHOT
- provided
-
-
-
- org.projectlombok
- lombok
- 1.18.22
- provided
-
-
-
-
- com.oracle.database.jdbc
- ojdbc8
- 21.9.0.0
-
-
- org.testcontainers
- oracle-xe
- 1.17.2
- test
-
-
- org.testcontainers
- jdbc-test
- 1.11.4
-
-
+
com.zaxxer
HikariCP
5.0.1
@@ -77,7 +33,21 @@
slf4j-api
-
+
+
+ com.oracle.database.jdbc
+ ojdbc8
+ 21.9.0.0
+
+
+
+
+
+ org.testcontainers
+ oracle-xe
+ 1.18.0
+ test
+
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/OraclePlugin.java b/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/OraclePlugin.java
index e0558904bc..b08c22e832 100644
--- a/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/OraclePlugin.java
+++ b/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/OraclePlugin.java
@@ -19,13 +19,12 @@ import com.appsmith.external.models.RequestParamDTO;
import com.appsmith.external.plugins.BasePlugin;
import com.appsmith.external.plugins.PluginExecutor;
import com.appsmith.external.plugins.SmartSubstitutionInterface;
+import com.external.plugins.exceptions.OracleErrorMessages;
+import com.external.plugins.exceptions.OraclePluginError;
import com.external.plugins.utils.OracleDatasourceUtils;
import com.external.plugins.utils.OracleSpecificDataTypes;
import com.zaxxer.hikari.HikariDataSource;
-import com.zaxxer.hikari.HikariPoolMXBean;
-import com.zaxxer.hikari.pool.HikariProxyConnection;
import lombok.extern.slf4j.Slf4j;
-import oracle.jdbc.OraclePreparedStatement;
import org.apache.commons.io.IOUtils;
import org.pf4j.Extension;
import org.pf4j.PluginWrapper;
@@ -45,7 +44,7 @@ import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
-import java.time.Duration;
+import java.text.MessageFormat;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashMap;
@@ -69,13 +68,14 @@ import static com.appsmith.external.helpers.SmartSubstitutionHelper.replaceQuest
import static com.external.plugins.utils.OracleDatasourceUtils.JDBC_DRIVER;
import static com.external.plugins.utils.OracleDatasourceUtils.createConnectionPool;
import static com.external.plugins.utils.OracleDatasourceUtils.getConnectionFromConnectionPool;
+import static com.external.plugins.utils.OracleDatasourceUtils.logHikariCPStatus;
import static com.external.plugins.utils.OracleExecuteUtils.closeConnectionPostExecution;
import static com.external.plugins.utils.OracleExecuteUtils.isPLSQL;
import static com.external.plugins.utils.OracleExecuteUtils.populateRowsAndColumns;
import static com.external.plugins.utils.OracleExecuteUtils.removeSemicolonFromQuery;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
-import static org.apache.commons.lang3.StringUtils.isEmpty;
+import static org.apache.commons.lang3.StringUtils.isBlank;
@Slf4j
public class OraclePlugin extends BasePlugin {
@@ -85,14 +85,15 @@ public class OraclePlugin extends BasePlugin {
}
@Extension
public static class OraclePluginExecutor implements SmartSubstitutionInterface, PluginExecutor {
- private final Scheduler scheduler = Schedulers.boundedElastic();
+ public static final Scheduler scheduler = Schedulers.boundedElastic();
@Override
public Mono datasourceCreate(DatasourceConfiguration datasourceConfiguration) {
try {
Class.forName(JDBC_DRIVER);
} catch (ClassNotFoundException e) {
- return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Error loading Oracle JDBC Driver class."));
+ return Mono.error(new AppsmithPluginException(OraclePluginError.ORACLE_PLUGIN_ERROR,
+ OracleErrorMessages.ORACLE_JDBC_DRIVER_LOADING_ERROR_MSG, e.getMessage()));
}
return Mono
@@ -114,19 +115,24 @@ public class OraclePlugin extends BasePlugin {
}
@Override
- public Mono execute(HikariDataSource connection, DatasourceConfiguration datasourceConfiguration, ActionConfiguration actionConfiguration) {
- return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Unsupported Operation"));
+ public Mono execute(HikariDataSource connection,
+ DatasourceConfiguration datasourceConfiguration,
+ ActionConfiguration actionConfiguration) {
+ return Mono.error(
+ new AppsmithPluginException(OraclePluginError.QUERY_EXECUTION_FAILED, "Unsupported Operation"));
}
@Override
- public Mono executeParameterized(HikariDataSource connection, ExecuteActionDTO executeActionDTO, DatasourceConfiguration datasourceConfiguration, ActionConfiguration actionConfiguration) {
+ public Mono executeParameterized(HikariDataSource connectionPool,
+ ExecuteActionDTO executeActionDTO,
+ DatasourceConfiguration datasourceConfiguration,
+ ActionConfiguration actionConfiguration) {
final Map formData = actionConfiguration.getFormData();
String query = getDataValueSafelyFromFormData(formData, BODY, STRING_TYPE, null);
// Check for query parameter before performing the probably expensive fetch connection from the pool op.
- if (isEmpty(query)) {
- // Next TBD: update error based on new infra
+ if (isBlank(query)) {
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
- "Missing required parameter: Query."));
+ OracleErrorMessages.MISSING_QUERY_ERROR_MSG));
}
Boolean isPreparedStatement = TRUE;
@@ -143,7 +149,7 @@ public class OraclePlugin extends BasePlugin {
// In case of non-prepared statement, simply do binding-replacement and execute
if (FALSE.equals(isPreparedStatement)) {
prepareConfigurationsForExecution(executeActionDTO, actionConfiguration, datasourceConfiguration);
- return executeCommon(connection, datasourceConfiguration, actionConfiguration, FALSE, null, null);
+ return executeCommon(connectionPool, datasourceConfiguration, actionConfiguration, FALSE, null, null);
}
// First extract all the bindings in order
@@ -161,11 +167,11 @@ public class OraclePlugin extends BasePlugin {
updatedQuery = removeSemicolonFromQuery(updatedQuery);
}
setDataValueSafelyInFormData(formData, BODY, updatedQuery);
- return executeCommon(connection, datasourceConfiguration, actionConfiguration, TRUE,
+ return executeCommon(connectionPool, datasourceConfiguration, actionConfiguration, TRUE,
mustacheKeysInOrder, executeActionDTO);
}
- private Mono executeCommon(HikariDataSource connection,
+ private Mono executeCommon(HikariDataSource connectionPool,
DatasourceConfiguration datasourceConfiguration,
ActionConfiguration actionConfiguration,
Boolean preparedStatement,
@@ -177,10 +183,9 @@ public class OraclePlugin extends BasePlugin {
final Map formData = actionConfiguration.getFormData();
String query = getDataValueSafelyFromFormData(formData, BODY, STRING_TYPE, null);
- if (isEmpty(query)) {
- // Next TBD: update error based on new error infra
+ if (isBlank(query)) {
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
- "Missing required parameter: Query."));
+ OracleErrorMessages.MISSING_QUERY_ERROR_MSG));
}
Map psParams = preparedStatement ? new LinkedHashMap<>() : null;
@@ -191,8 +196,8 @@ public class OraclePlugin extends BasePlugin {
return Mono.fromCallable(() -> {
Connection connectionFromPool;
- try {
- connectionFromPool = getConnectionFromConnectionPool(connection);
+ try {
+ connectionFromPool = getConnectionFromConnectionPool(connectionPool);
} catch (SQLException | StaleConnectionException e) {
// The function can throw either StaleConnectionException or SQLException. The underlying hikari
// library throws SQLException in case the pool is closed or there is an issue initializing
@@ -211,15 +216,10 @@ public class OraclePlugin extends BasePlugin {
PreparedStatement preparedQuery = null;
boolean isResultSet;
- HikariPoolMXBean poolProxy = connection.getHikariPoolMXBean();
+ // Log HikariCP status
+ logHikariCPStatus(MessageFormat.format("Before executing Oracle query [{0}]", query),
+ connectionPool);
- int idleConnections = poolProxy.getIdleConnections();
- int activeConnections = poolProxy.getActiveConnections();
- int totalConnections = poolProxy.getTotalConnections();
- int threadsAwaitingConnection = poolProxy.getThreadsAwaitingConnection();
- log.debug("Before executing Oracle query [{}] : Hikari Pool stats : active - {} , idle - {}, " +
- "awaiting - {} , total - {}", query, activeConnections, idleConnections,
- threadsAwaitingConnection, totalConnections);
try {
if (FALSE.equals(preparedStatement)) {
statement = connectionFromPool.createStatement();
@@ -250,15 +250,13 @@ public class OraclePlugin extends BasePlugin {
} catch (SQLException e) {
log.debug(Thread.currentThread().getName() + ": In the OraclePlugin, got action execution error");
log.debug(e.getMessage());
- return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, e.getMessage()));
+ return Mono.error(new AppsmithPluginException(OraclePluginError.QUERY_EXECUTION_FAILED,
+ OracleErrorMessages.QUERY_EXECUTION_FAILED_ERROR_MSG, e.getMessage(),
+ "SQLSTATE: " + e.getSQLState()));
} finally {
- idleConnections = poolProxy.getIdleConnections();
- activeConnections = poolProxy.getActiveConnections();
- totalConnections = poolProxy.getTotalConnections();
- threadsAwaitingConnection = poolProxy.getThreadsAwaitingConnection();
- log.debug("After executing Oracle query [{}] : Hikari Pool stats : active - {} , idle - " +
- "{}, awaiting - {} , total - {}", query, activeConnections, idleConnections,
- threadsAwaitingConnection, totalConnections);
+ // Log HikariCP status
+ logHikariCPStatus(MessageFormat.format("After executing Oracle query [{0}]", query),
+ connectionPool);
closeConnectionPostExecution(resultSet, statement, preparedQuery, connectionFromPool);
}
@@ -295,8 +293,9 @@ public class OraclePlugin extends BasePlugin {
}
@Override
- public Mono getStructure(HikariDataSource connection, DatasourceConfiguration datasourceConfiguration) {
- return OracleDatasourceUtils.getStructure(connection, datasourceConfiguration);
+ public Mono getStructure(HikariDataSource connectionPool,
+ DatasourceConfiguration datasourceConfiguration) {
+ return OracleDatasourceUtils.getStructure(connectionPool, datasourceConfiguration);
}
private Set populateHintMessages(List columnNames) {
@@ -389,6 +388,7 @@ public class OraclePlugin extends BasePlugin {
// the query. Ignore the exception
} else {
throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
+ String.format(OracleErrorMessages.QUERY_PREPARATION_FAILED_ERROR_MSG, value, binding),
e.getMessage());
}
}
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/exceptions/OracleErrorMessages.java b/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/exceptions/OracleErrorMessages.java
new file mode 100644
index 0000000000..334f2c73a3
--- /dev/null
+++ b/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/exceptions/OracleErrorMessages.java
@@ -0,0 +1,45 @@
+package com.external.plugins.exceptions;
+
+public class OracleErrorMessages {
+ private OracleErrorMessages() {
+ //Prevents instantiation
+ }
+ public static final String MISSING_QUERY_ERROR_MSG = "Missing required parameter: Query.";
+
+ public static final String QUERY_EXECUTION_FAILED_ERROR_MSG = "Your Oracle query failed to execute.";
+
+ public static final String ORACLE_JDBC_DRIVER_LOADING_ERROR_MSG = "Your Oracle query failed to execute.";
+
+ public static final String GET_STRUCTURE_ERROR_MSG = "The Appsmith server has failed to fetch the structure of your schema.";
+
+ public static final String QUERY_PREPARATION_FAILED_ERROR_MSG = "Query preparation failed while inserting value: %s"
+ + " for binding: {{%s}}.";
+
+ public static final String SSL_CONFIGURATION_ERROR_MSG = "The Appsmith server has failed to fetch SSL configuration from datasource configuration form. ";
+
+ public static final String INVALID_SSL_OPTION_ERROR_MSG = "The Appsmith server has found an unexpected SSL option: %s.";
+
+ public static final String CONNECTION_POOL_CREATION_FAILED_ERROR_MSG = "An exception occurred while creating " +
+ "connection pool. One or more arguments in the datasource configuration may be invalid.";
+
+ /*
+ ************************************************************************************************************************************************
+ Error messages related to validation of datasource.
+ ************************************************************************************************************************************************
+ */
+
+ public static final String DS_MISSING_ENDPOINT_ERROR_MSG = "Missing endpoint.";
+
+ public static final String DS_MISSING_HOSTNAME_ERROR_MSG = "Missing hostname.";
+
+ public static final String DS_INVALID_HOSTNAME_ERROR_MSG = "Host value cannot contain `/` or `:` characters. Found `%s`.";
+
+ public static final String DS_MISSING_CONNECTION_MODE_ERROR_MSG = "Missing Connection Mode.";
+
+ public static final String DS_MISSING_AUTHENTICATION_DETAILS_ERROR_MSG = "Missing authentication details.";
+
+ public static final String DS_MISSING_USERNAME_ERROR_MSG = "Missing username for authentication.";
+ public static final String DS_MISSING_PASSWORD_ERROR_MSG = "Missing password for authentication.";
+
+ public static final String DS_MISSING_SERVICE_NAME_ERROR_MSG = "Missing service name.";
+}
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/exceptions/OraclePluginError.java b/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/exceptions/OraclePluginError.java
new file mode 100644
index 0000000000..a5be33da0a
--- /dev/null
+++ b/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/exceptions/OraclePluginError.java
@@ -0,0 +1,79 @@
+package com.external.plugins.exceptions;
+
+import com.appsmith.external.exceptions.AppsmithErrorAction;
+import com.appsmith.external.exceptions.pluginExceptions.BasePluginError;
+import com.appsmith.external.models.ErrorType;
+import lombok.Getter;
+
+import java.text.MessageFormat;
+
+@Getter
+public enum OraclePluginError implements BasePluginError {
+ QUERY_EXECUTION_FAILED(
+ 500,
+ "PE-ORC-5000",
+ "{0}",
+ AppsmithErrorAction.LOG_EXTERNALLY,
+ "Query execution error",
+ ErrorType.INTERNAL_ERROR,
+ "{1}",
+ "{2}"
+ ),
+ ORACLE_PLUGIN_ERROR(
+ 500,
+ "PE-ORC-5001",
+ "{0}",
+ AppsmithErrorAction.LOG_EXTERNALLY,
+ "Query execution error",
+ ErrorType.INTERNAL_ERROR,
+ "{1}",
+ "{2}"
+ ),
+ RESPONSE_SIZE_TOO_LARGE(
+ 504,
+ "PE-ORC-5009",
+ "Response size exceeded the maximum supported size of {0} MB. Please use LIMIT to reduce the amount of data fetched.",
+ AppsmithErrorAction.DEFAULT,
+ "Large Result Set Not Supported",
+ ErrorType.INTERNAL_ERROR,
+ "{1}",
+ "{2}"
+ ),
+ ;
+ private final Integer httpErrorCode;
+ private final String appErrorCode;
+ private final String message;
+ private final String title;
+ private final AppsmithErrorAction errorAction;
+ private final ErrorType errorType;
+
+ private final String downstreamErrorMessage;
+
+ private final String downstreamErrorCode;
+
+ OraclePluginError(Integer httpErrorCode, String appErrorCode, String message, AppsmithErrorAction errorAction,
+ String title, ErrorType errorType, String downstreamErrorMessage, String downstreamErrorCode) {
+ this.httpErrorCode = httpErrorCode;
+ this.appErrorCode = appErrorCode;
+ this.errorType = errorType;
+ this.errorAction = errorAction;
+ this.message = message;
+ this.title = title;
+ this.downstreamErrorMessage = downstreamErrorMessage;
+ this.downstreamErrorCode = downstreamErrorCode;
+ }
+
+ public String getMessage(Object... args) {
+ return new MessageFormat(this.message).format(args);
+ }
+
+ public String getErrorType() { return this.errorType.toString(); }
+
+ public String getDownstreamErrorMessage(Object... args) {
+ return replacePlaceholderWithValue(this.downstreamErrorMessage, args);
+ }
+
+ public String getDownstreamErrorCode(Object... args) {
+ return replacePlaceholderWithValue(this.downstreamErrorCode, args);
+ }
+}
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/utils/OracleDatasourceUtils.java b/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/utils/OracleDatasourceUtils.java
index c9adb2d08c..de6cb82b45 100644
--- a/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/utils/OracleDatasourceUtils.java
+++ b/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/utils/OracleDatasourceUtils.java
@@ -8,21 +8,36 @@ import com.appsmith.external.models.DatasourceConfiguration;
import com.appsmith.external.models.DatasourceStructure;
import com.appsmith.external.models.Endpoint;
import com.appsmith.external.models.SSLDetails;
+import com.external.plugins.exceptions.OracleErrorMessages;
+import com.external.plugins.exceptions.OraclePluginError;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
+import com.zaxxer.hikari.HikariPoolMXBean;
import com.zaxxer.hikari.pool.HikariPool;
+import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ObjectUtils;
-import org.springframework.util.CollectionUtils;
import reactor.core.publisher.Mono;
+import java.sql.Connection;
+import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.Statement;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
+import static com.appsmith.external.helpers.PluginUtils.safelyCloseSingleConnectionFromHikariCP;
+import static com.external.plugins.OraclePlugin.OraclePluginExecutor.scheduler;
import static org.apache.commons.lang3.StringUtils.isBlank;
+import static org.springframework.util.CollectionUtils.isEmpty;
+@Slf4j
public class OracleDatasourceUtils {
public static final int MINIMUM_POOL_SIZE = 1;
public static final int MAXIMUM_POOL_SIZE = 5;
@@ -31,39 +46,92 @@ public class OracleDatasourceUtils {
public static final String ORACLE_URL_PREFIX = "jdbc:oracle:thin:@tcp://";
public static final int ORACLE_URL_PREFIX_TCPS_OFFSET = 21;
- public static void datasourceDestroy(HikariDataSource connection) {
- if (connection != null) {
- System.out.println(Thread.currentThread().getName() + ": Closing Oracle DB Connection Pool");
- connection.close();
+ public static final String ORACLE_PRIMARY_KEY_INDICATOR = "P";
+
+ /**
+ * Example output:
+ * +------------+-----------+-----------------+
+ * | TABLE_NAME |COLUMN_NAME| DATA_TYPE |
+ * +------------+-----------+-----------------+
+ * | CLUB | ID | NUMBER |
+ * | STUDENTS | NAME | VARCHAR2 |
+ * +------------+-----------+-----------------+
+ */
+ public static final String ORACLE_SQL_QUERY_TO_GET_ALL_TABLE_COLUMN_TYPE =
+ "SELECT " +
+ "table_name, column_name, data_type " +
+ "FROM " +
+ "user_tab_cols";
+
+ /**
+ * Example output:
+ * +------------+-----------+-----------------+-----------------+-------------------+
+ * | TABLE_NAME |COLUMN_NAME| CONSTRAINT_TYPE | CONSTRAINT_NAME | R_CONSTRAINT_NAME |
+ * +------------+-----------+-----------------+-----------------+-------------------+
+ * | CLUB | ID | R | FK_STUDENTS_ID | PK_STUDENTS_ID |
+ * | STUDENTS | ID | P | SYS_C006397 | null |
+ * +------------+-----------+-----------------+-----------------+-------------------+
+ */
+ public static final String ORACLE_SQL_QUERY_TO_GET_ALL_TABLE_COLUMN_KEY_CONSTRAINTS =
+ "SELECT " +
+ " cols.table_name, " +
+ " cols.column_name, " +
+ " cons.constraint_type, " +
+ " cons.constraint_name, " +
+ " cons.r_constraint_name " +
+ "FROM " +
+ " all_cons_columns cols " +
+ " JOIN all_constraints cons " +
+ " ON cols.owner = cons.owner " +
+ " AND cols.constraint_name = cons.constraint_name " +
+ " JOIN all_tab_cols tab_cols " +
+ " ON cols.owner = tab_cols.owner " +
+ " AND cols.table_name = tab_cols.table_name " +
+ " AND cols.column_name = tab_cols.column_name " +
+ "WHERE " +
+ " cons.constraint_type IN ('P', 'R') " +
+ " AND cons.owner = 'ADMIN' " +
+ "ORDER BY " +
+ " cols.table_name, " +
+ " cols.position";
+
+ public static void datasourceDestroy(HikariDataSource connectionPool) {
+ if (connectionPool != null) {
+ log.debug(Thread.currentThread().getName() + ": Closing Oracle DB Connection Pool");
+ connectionPool.close();
}
}
public static Set validateDatasource(DatasourceConfiguration datasourceConfiguration) {
Set invalids = new HashSet<>();
- if (CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) {
- invalids.add("Missing endpoint.");
+ if (isEmpty(datasourceConfiguration.getEndpoints())) {
+ invalids.add(OracleErrorMessages.DS_MISSING_ENDPOINT_ERROR_MSG);
} else {
for (final Endpoint endpoint : datasourceConfiguration.getEndpoints()) {
if (isBlank(endpoint.getHost())) {
- invalids.add("Missing hostname.");
+ invalids.add(OracleErrorMessages.DS_MISSING_HOSTNAME_ERROR_MSG);
} else if (endpoint.getHost().contains("/") || endpoint.getHost().contains(":")) {
- invalids.add("Host value cannot contain `/` or `:` characters. Found `" + endpoint.getHost() + "`.");
+ invalids.add(String.format(OracleErrorMessages.DS_INVALID_HOSTNAME_ERROR_MSG, endpoint.getHost()));
}
}
}
if (datasourceConfiguration.getAuthentication() == null) {
- invalids.add("Missing authentication details.");
+ invalids.add(OracleErrorMessages.DS_MISSING_AUTHENTICATION_DETAILS_ERROR_MSG);
} else {
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if (isBlank(authentication.getUsername())) {
- invalids.add("Missing username for authentication.");
+ invalids.add(OracleErrorMessages.DS_MISSING_USERNAME_ERROR_MSG);
+ }
+
+ if (isBlank(authentication.getPassword())) {
+ invalids.add(OracleErrorMessages.DS_MISSING_PASSWORD_ERROR_MSG);
}
if (isBlank(authentication.getDatabaseName())) {
- invalids.add("Missing database name.");
+ invalids.add(OracleErrorMessages.DS_MISSING_SERVICE_NAME_ERROR_MSG);
}
}
@@ -73,17 +141,225 @@ public class OracleDatasourceUtils {
if (datasourceConfiguration.getConnection() == null
|| datasourceConfiguration.getConnection().getSsl() == null
|| datasourceConfiguration.getConnection().getSsl().getAuthType() == null) {
- invalids.add("Appsmith server has failed to fetch SSL configuration from datasource configuration form. " +
- "Please reach out to Appsmith customer support to resolve this.");
+ invalids.add(OracleErrorMessages.SSL_CONFIGURATION_ERROR_MSG);
}
return invalids;
}
- public static Mono getStructure(HikariDataSource connection,
+ public static Mono getStructure(HikariDataSource connectionPool,
DatasourceConfiguration datasourceConfiguration) {
- // Next TBD: fill it
- return Mono.just(new DatasourceStructure());
+ final DatasourceStructure structure = new DatasourceStructure();
+ final Map tableNameToTableMap = new LinkedHashMap<>();
+
+ return Mono.fromSupplier(() -> {
+ Connection connectionFromPool;
+ try {
+ connectionFromPool = getConnectionFromConnectionPool(connectionPool);
+ } catch (SQLException | StaleConnectionException e) {
+ // The function can throw either StaleConnectionException or SQLException. The
+ // underlying hikari library throws SQLException in case the pool is closed or there is an issue
+ // initializing the connection pool which can also be translated in our world to
+ // StaleConnectionException and should then trigger the destruction and recreation of the pool.
+ return Mono.error(e instanceof StaleConnectionException ? e : new StaleConnectionException());
+ }
+
+ logHikariCPStatus("Before getting Oracle DB schema", connectionPool);
+
+ try (Statement statement = connectionFromPool.createStatement()) {
+ // Set table names. For each table set its column names and column types.
+ setTableNamesAndColumnNamesAndColumnTypes(statement, tableNameToTableMap);
+
+ // Set primary key and foreign key constraints.
+ setPrimaryAndForeignKeyInfoInTables(statement, tableNameToTableMap);
+
+ } catch (SQLException throwable) {
+ return Mono.error(new AppsmithPluginException( AppsmithPluginError.PLUGIN_GET_STRUCTURE_ERROR,
+ OracleErrorMessages.GET_STRUCTURE_ERROR_MSG, throwable.getCause(),
+ "SQLSTATE: " + throwable.getSQLState()));
+ } finally {
+ logHikariCPStatus("After getting Oracle DB schema", connectionPool);
+ safelyCloseSingleConnectionFromHikariCP(connectionFromPool, "Error returning Oracle connection to pool " +
+ "during get structure");
+ }
+
+ // Set SQL query templates
+ setSQLQueryTemplates(tableNameToTableMap);
+
+ structure.setTables(new ArrayList<>(tableNameToTableMap.values()));
+ return structure;
+ })
+ .map(resultStructure -> (DatasourceStructure) resultStructure)
+ .subscribeOn(scheduler);
+ }
+
+ /**
+ * Run a SQL query to fetch all user accessible tables along with their column names and if the column is a
+ * primary or foreign key. Since the remote table relationship for a foreign key column is not explicitly defined
+ * we create a 1:1 map here for primary_key -> table, and foreign_key -> table so that we can find both the
+ * tables to which a foreign key is related to.
+ * Please check the SQL query macro definition to find a sample response as comment.
+ */
+ private static void setPrimaryAndForeignKeyInfoInTables(Statement statement, Map tableNameToTableMap) throws SQLException {
+ Map primaryKeyConstraintNameToTableNameMap = new HashMap<>();
+ Map primaryKeyConstraintNameToColumnNameMap = new HashMap<>();
+ Map foreignKeyConstraintNameToTableNameMap = new HashMap<>();
+ Map foreignKeyConstraintNameToColumnNameMap = new HashMap<>();
+ Map foreignKeyConstraintNameToRemoteConstraintNameMap = new HashMap<>();
+
+ try (ResultSet columnsResultSet =
+ statement.executeQuery(ORACLE_SQL_QUERY_TO_GET_ALL_TABLE_COLUMN_KEY_CONSTRAINTS)) {
+ while (columnsResultSet.next()) {
+ final String tableName = columnsResultSet.getString("TABLE_NAME");
+ final String columnName = columnsResultSet.getString("COLUMN_NAME");
+ final String constraintType = columnsResultSet.getString("CONSTRAINT_TYPE");
+ final String constraintName = columnsResultSet.getString("CONSTRAINT_NAME");
+ final String remoteConstraintName = columnsResultSet.getString("R_CONSTRAINT_NAME");
+
+ if (ORACLE_PRIMARY_KEY_INDICATOR.equalsIgnoreCase(constraintType)) {
+ primaryKeyConstraintNameToTableNameMap.put(constraintName, tableName);
+ primaryKeyConstraintNameToColumnNameMap.put(constraintName, columnName);
+ }
+ else {
+ foreignKeyConstraintNameToTableNameMap.put(constraintName, tableName);
+ foreignKeyConstraintNameToColumnNameMap.put(constraintName, columnName);
+ foreignKeyConstraintNameToRemoteConstraintNameMap.put(constraintName, remoteConstraintName);
+ }
+ }
+
+ primaryKeyConstraintNameToColumnNameMap.keySet().stream()
+ .filter(constraintName -> {
+ String tableName = primaryKeyConstraintNameToTableNameMap.get(constraintName);
+ return tableNameToTableMap.keySet().contains(tableName);
+ })
+ .forEach(constraintName -> {
+ String tableName = primaryKeyConstraintNameToTableNameMap.get(constraintName);
+ DatasourceStructure.Table table = tableNameToTableMap.get(tableName);
+ String columnName = primaryKeyConstraintNameToColumnNameMap.get(constraintName);
+ table.getKeys().add(new DatasourceStructure.PrimaryKey(constraintName,
+ List.of(columnName)));
+ });
+
+ foreignKeyConstraintNameToColumnNameMap.keySet().stream()
+ .filter(constraintName -> {
+ String tableName = foreignKeyConstraintNameToTableNameMap.get(constraintName);
+ return tableNameToTableMap.keySet().contains(tableName);
+ })
+ .forEach(constraintName -> {
+ String tableName = foreignKeyConstraintNameToTableNameMap.get(constraintName);
+ DatasourceStructure.Table table = tableNameToTableMap.get(tableName);
+ String columnName = foreignKeyConstraintNameToColumnNameMap.get(constraintName);
+ String remoteConstraintName =
+ foreignKeyConstraintNameToRemoteConstraintNameMap.get(constraintName);
+ String remoteColumn = primaryKeyConstraintNameToColumnNameMap.get(remoteConstraintName);
+ table.getKeys().add(new DatasourceStructure.ForeignKey(constraintName,
+ List.of(columnName), List.of(remoteColumn)));
+ });
+ }
+ }
+
+ /**
+ * Run a SQL query to fetch all tables accessible to user along with their columns and data type of each column.
+ * Then read the response and populate Appsmith's Table object with the same.
+ * Please check the SQL query macro definition to find a sample response as comment.
+ */
+ private static void setTableNamesAndColumnNamesAndColumnTypes(Statement statement, Map tableNameToTableMap) throws SQLException {
+ try (ResultSet columnsResultSet =
+ statement.executeQuery(ORACLE_SQL_QUERY_TO_GET_ALL_TABLE_COLUMN_TYPE)) {
+ while (columnsResultSet.next()) {
+ final String tableName = columnsResultSet.getString("TABLE_NAME");
+ if (!tableNameToTableMap.containsKey(tableName)) {
+ tableNameToTableMap.put(tableName, new DatasourceStructure.Table(
+ DatasourceStructure.TableType.TABLE,
+ "",
+ tableName,
+ new ArrayList<>(),
+ new ArrayList<>(),
+ new ArrayList<>()));
+ }
+ final DatasourceStructure.Table table = tableNameToTableMap.get(tableName);
+ table.getColumns().add(new DatasourceStructure.Column(
+ columnsResultSet.getString("COLUMN_NAME"),
+ columnsResultSet.getString("DATA_TYPE"),
+ null,
+ false));
+ }
+ }
+ }
+
+ private static void setSQLQueryTemplates(Map tableNameToTableMap) {
+ tableNameToTableMap.values().stream()
+ .forEach(table -> {
+ LinkedHashMap columnNameToSampleColumnDataMap =
+ new LinkedHashMap<>();
+ table.getColumns().stream()
+ .forEach(column -> {
+ columnNameToSampleColumnDataMap.put(column.getName(),
+ getSampleColumnData(column.getType()));
+ });
+
+ String selectQueryTemplate = MessageFormat.format("SELECT * FROM {0} WHERE " +
+ "ROWNUM < 10", table.getName());
+ String insertQueryTemplate = MessageFormat.format("INSERT INTO {0} ({1}) " +
+ "VALUES ({2})", table.getName(),
+ getSampleColumnNamesCSVString(columnNameToSampleColumnDataMap),
+ getSampleColumnDataCSVString(columnNameToSampleColumnDataMap));
+ String updateQueryTemplate = MessageFormat.format("UPDATE {0} SET {1} WHERE " +
+ "1=0 -- Specify a valid condition here. Removing the condition may " +
+ "update every row in the table!", table.getName(),
+ getSampleOneColumnUpdateString(columnNameToSampleColumnDataMap));
+ String deleteQueryTemplate = MessageFormat.format("DELETE FROM {0} WHERE 1=0" +
+ " -- Specify a valid condition here. Removing the condition may " +
+ "delete everything in the table!", table.getName());
+
+ table.getTemplates().add(new DatasourceStructure.Template("SELECT", null,
+ Map.of("body", Map.of("data", selectQueryTemplate))));
+ table.getTemplates().add(new DatasourceStructure.Template("INSERT", null,
+ Map.of("body", Map.of("data", insertQueryTemplate))));
+ table.getTemplates().add(new DatasourceStructure.Template("UPDATE", null,
+ Map.of("body", Map.of("data", updateQueryTemplate))));
+ table.getTemplates().add(new DatasourceStructure.Template("DELETE", null,
+ Map.of("body", Map.of("data", deleteQueryTemplate))));
+ });
+ }
+
+ private static String getSampleOneColumnUpdateString(LinkedHashMap columnNameToSampleColumnDataMap) {
+ return MessageFormat.format("{0}={1}", columnNameToSampleColumnDataMap.keySet().stream().findFirst().orElse(
+ "id"), columnNameToSampleColumnDataMap.values().stream().findFirst().orElse("'uid'"));
+ }
+
+ private static String getSampleColumnNamesCSVString(LinkedHashMap columnNameToSampleColumnDataMap) {
+ return String.join(", ", columnNameToSampleColumnDataMap.keySet());
+ }
+
+ private static String getSampleColumnDataCSVString(LinkedHashMap columnNameToSampleColumnDataMap) {
+ return String.join(", ", columnNameToSampleColumnDataMap.values());
+ }
+
+ private static String getSampleColumnData(String type) {
+ if (type == null) {
+ return "NULL";
+ }
+
+ switch (type.toUpperCase()) {
+ case "NUMBER":
+ return "1";
+ case "FLOAT": /* Fall through */
+ case "DOUBLE":
+ return "1.0";
+ case "CHAR": /* Fall through */
+ case "NCHAR": /* Fall through */
+ case "VARCHAR": /* Fall through */
+ case "VARCHAR2":/* Fall through */
+ case "NVARCHAR":/* Fall through */
+ case "NVARCHAR2":
+ return "'text'";
+ case "NULL": /* Fall through */
+ default:
+ return "NULL";
+ }
}
public static HikariDataSource createConnectionPool(DatasourceConfiguration datasourceConfiguration) throws AppsmithPluginException {
@@ -124,11 +400,8 @@ public class OracleDatasourceUtils {
if (datasourceConfiguration.getConnection() == null
|| datasourceConfiguration.getConnection().getSsl() == null
|| datasourceConfiguration.getConnection().getSsl().getAuthType() == null) {
- throw new AppsmithPluginException(
- AppsmithPluginError.PLUGIN_ERROR,
- "Appsmith server has failed to fetch SSL configuration from datasource configuration form. " +
- "Please reach out to Appsmith customer support to resolve this."
- );
+ throw new AppsmithPluginException(OraclePluginError.ORACLE_PLUGIN_ERROR,
+ OracleErrorMessages.SSL_CONFIGURATION_ERROR_MSG);
}
SSLDetails.AuthType sslAuthType = datasourceConfiguration.getConnection().getSsl().getAuthType();
@@ -143,11 +416,8 @@ public class OracleDatasourceUtils {
break;
default:
- throw new AppsmithPluginException(
- AppsmithPluginError.PLUGIN_ERROR,
- "Appsmith server has found an unexpected SSL option: " + sslAuthType + ". Please reach out to" +
- " Appsmith customer support to resolve this."
- );
+ throw new AppsmithPluginException(OraclePluginError.ORACLE_PLUGIN_ERROR,
+ String.format(OracleErrorMessages.INVALID_SSL_OPTION_ERROR_MSG, sslAuthType));
}
String url = urlBuilder.toString();
@@ -162,10 +432,8 @@ public class OracleDatasourceUtils {
try {
datasource = new HikariDataSource(config);
} catch (HikariPool.PoolInitializationException e) {
- throw new AppsmithPluginException(
- AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
- e.getMessage()
- );
+ throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
+ OracleErrorMessages.CONNECTION_POOL_CREATION_FAILED_ERROR_MSG, e.getMessage());
}
return datasource;
@@ -185,4 +453,14 @@ public class OracleDatasourceUtils {
return connectionPool.getConnection();
}
+
+ public static void logHikariCPStatus(String logPrefix, HikariDataSource connectionPool) {
+ HikariPoolMXBean poolProxy = connectionPool.getHikariPoolMXBean();
+ int idleConnections = poolProxy.getIdleConnections();
+ int activeConnections = poolProxy.getActiveConnections();
+ int totalConnections = poolProxy.getTotalConnections();
+ int threadsAwaitingConnection = poolProxy.getThreadsAwaitingConnection();
+ log.debug(MessageFormat.format("{0}: Hikari Pool stats : active - {1} , idle - {2}, awaiting - {3} , total - {4}",
+ logPrefix, activeConnections, idleConnections, threadsAwaitingConnection, totalConnections));
+ }
}
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/utils/OracleExecuteUtils.java b/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/utils/OracleExecuteUtils.java
index aa6c22663f..e2876677eb 100644
--- a/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/utils/OracleExecuteUtils.java
+++ b/app/server/appsmith-plugins/oraclePlugin/src/main/java/com/external/plugins/utils/OracleExecuteUtils.java
@@ -1,9 +1,9 @@
package com.external.plugins.utils;
-import com.appsmith.external.constants.DataType;
import com.appsmith.external.plugins.SmartSubstitutionInterface;
import oracle.jdbc.OracleArray;
-import oracle.sql.Datum;
+import oracle.jdbc.OracleBlob;
+import oracle.sql.CLOB;
import org.apache.commons.lang.ObjectUtils;
import java.sql.Connection;
@@ -12,31 +12,29 @@ import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
-import java.time.LocalDateTime;
+import java.text.MessageFormat;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
+import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import static com.appsmith.external.helpers.PluginUtils.getColumnsListForJdbcPlugin;
+import static com.appsmith.external.helpers.PluginUtils.safelyCloseSingleConnectionFromHikariCP;
import static java.lang.Boolean.FALSE;
public class OracleExecuteUtils implements SmartSubstitutionInterface {
public static final String DATE_COLUMN_TYPE_NAME = "date";
public static final String TIMESTAMP_TYPE_NAME = "timestamp";
public static final String TIMESTAMPTZ_TYPE_NAME = "TIMESTAMP WITH TIME ZONE";
- public static final String INTERVAL_TYPE_NAME = "interval";
+ public static final String TIMESTAMPLTZ_TYPE_NAME = "TIMESTAMP WITH LOCAL TIME ZONE";
+ public static final String CLOB_TYPE_NAME = "CLOB";
+ public static final String NCLOB_TYPE_NAME = "NCLOB";
+ public static final String RAW_TYPE_NAME = "RAW";
+ public static final String BLOB_TYPE_NAME = "BLOB";
public static final String AFFECTED_ROWS_KEY = "affectedRows";
- public static final String INT8 = "int8";
- public static final String INT4 = "int4";
- public static final String DECIMAL = "decimal";
- public static final String VARCHAR = "varchar";
- public static final String BOOL = "bool";
- public static final String DATE = "date";
- public static final String TIME = "time";
- public static final String FLOAT8 = "float8";
/**
* Every PL/SQL block must have `BEGIN` and `END` keywords to define the block. Apart from these they could also
@@ -83,15 +81,8 @@ public class OracleExecuteUtils implements SmartSubstitutionInterface {
}
}
- if (connectionFromPool != null) {
- try {
- // Return the connection back to the pool
- connectionFromPool.close();
- } catch (SQLException e) {
- System.out.println(Thread.currentThread().getName() +
- ": Execute Error returning Oracle connection to pool" + e.getMessage());
- }
- }
+ safelyCloseSingleConnectionFromHikariCP(connectionFromPool, MessageFormat.format("{0}: Execute Error returning " +
+ "Oracle connection to pool", Thread.currentThread().getName()));
}
/**
@@ -147,35 +138,39 @@ public class OracleExecuteUtils implements SmartSubstitutionInterface {
} else if (DATE_COLUMN_TYPE_NAME.equalsIgnoreCase(typeName)) {
value = DateTimeFormatter.ISO_DATE.format(resultSet.getDate(i).toLocalDate());
- } else if (TIMESTAMP_TYPE_NAME.equalsIgnoreCase(typeName)) {
- value = DateTimeFormatter.ISO_DATE_TIME.format(
- LocalDateTime.of(
- resultSet.getDate(i).toLocalDate(),
- resultSet.getTime(i).toLocalTime()
- )
- ) + "Z";
-
- } else if (TIMESTAMPTZ_TYPE_NAME.equalsIgnoreCase(typeName)) {
+ } else if (TIMESTAMP_TYPE_NAME.equalsIgnoreCase(typeName) || TIMESTAMPTZ_TYPE_NAME.equalsIgnoreCase(typeName) || TIMESTAMPLTZ_TYPE_NAME.equalsIgnoreCase(typeName)) {
value = DateTimeFormatter.ISO_DATE_TIME.format(
resultSet.getObject(i, OffsetDateTime.class)
);
-
- } else if (INTERVAL_TYPE_NAME.equalsIgnoreCase(typeName)) {
- value = resultSet.getObject(i).toString();
-
+ } else if (CLOB_TYPE_NAME.equalsIgnoreCase(typeName) || NCLOB_TYPE_NAME.equals(typeName)) {
+ /**
+ * clob, nclob are textual data.
+ * Ref: https://docs.oracle.com/javadb/10.10.1.2/ref/rrefclob.html
+ */
+ value = String.valueOf(((CLOB)resultSet.getObject(i)).getTarget().getPrefetchedData());
} else if (resultSet.getObject(i) instanceof OracleArray) {
value = ((OracleArray)resultSet.getObject(i)).getArray();
+ } else if (RAW_TYPE_NAME.equalsIgnoreCase(typeName)) {
+ /**
+ * Raw / Blob data cannot be interpreted as anything but a byte array. Hence, send it back as a
+ * base64 encoded string. The correct way to read the data for these types is for the user to
+ * cast them to a type before reading them, example:
+ * select utl_raw.cast_to_varchar2(c_raw) as c_raw, utl_raw.cast_to_varchar2(c_blob) as c_blob from TYPESTEST4
+ */
+ value = Base64.getEncoder().encodeToString((byte[]) resultSet.getObject(i));
+ }
+ else if (BLOB_TYPE_NAME.equalsIgnoreCase(typeName)) {
+ /**
+ * Raw / Blob data cannot be interpreted as anything but a byte array. Hence, send it back as a
+ * base64 encoded string. The correct way to read the data for these types is for the user to
+ * cast them to a type before reading them, example:
+ * select utl_raw.cast_to_varchar2(c_raw) as c_raw, utl_raw.cast_to_varchar2(c_blob) as c_blob from TYPESTEST4
+ */
+ value = ((OracleBlob)resultSet.getObject(i)).getBytes(1L,
+ (int) ((OracleBlob)resultSet.getObject(i)).length());
}
else {
- value = resultSet.getObject(i);
-
- /**
- * 'Datum' class is the root of Oracle native datatype hierarchy.
- * Ref: https://docs.oracle.com/cd/A97329_03/web.902/q20224/oracle/sql/Datum.html
- */
- if (value instanceof Datum) {
- value = new String(((Datum) value).getBytes());
- }
+ value = resultSet.getObject(i).toString();
}
row.put(metaData.getColumnName(i), value);
@@ -185,30 +180,4 @@ public class OracleExecuteUtils implements SmartSubstitutionInterface {
}
}
}
-
- public static String toOraclePrimitiveTypeName(DataType type) {
- switch (type) {
- case LONG:
- return INT8;
- case INTEGER:
- return INT4;
- case FLOAT:
- return DECIMAL;
- case STRING:
- return VARCHAR;
- case BOOLEAN:
- return BOOL;
- case DATE:
- return DATE;
- case TIME:
- return TIME;
- case DOUBLE:
- return FLOAT8;
- case ARRAY:
- throw new IllegalArgumentException("Array of Array datatype is not supported.");
- default:
- throw new IllegalArgumentException(
- "Unable to map the computed data type to primitive Postgresql type");
- }
- }
}
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/main/resources/form.json b/app/server/appsmith-plugins/oraclePlugin/src/main/resources/form.json
index 5547a9ac0e..49e0605d11 100755
--- a/app/server/appsmith-plugins/oraclePlugin/src/main/resources/form.json
+++ b/app/server/appsmith-plugins/oraclePlugin/src/main/resources/form.json
@@ -12,7 +12,8 @@
"configProperty": "datasourceConfiguration.endpoints[*].host",
"controlType": "KEYVALUE_ARRAY",
"validationMessage": "Please enter a valid host",
- "validationRegex": "^((?![/:]).)*$"
+ "validationRegex": "^((?![/:]).)*$",
+ "isRequired": true
},
{
"label": "Port",
@@ -23,11 +24,11 @@
]
},
{
- "label": "Database Name",
+ "label": "Service Name",
"configProperty": "datasourceConfiguration.authentication.databaseName",
"controlType": "INPUT_TEXT",
- "placeholderText": "Database name",
- "initialValue": "admin"
+ "placeholderText": "gfb284db6bcee33_testdb_high.adb.oraclecloud.com",
+ "isRequired": true
}
]
},
@@ -42,15 +43,17 @@
"label": "Username",
"configProperty": "datasourceConfiguration.authentication.username",
"controlType": "INPUT_TEXT",
- "placeholderText": "Username"
+ "placeholderText": "admin",
+ "isRequired": true
},
{
"label": "Password",
"configProperty": "datasourceConfiguration.authentication.password",
"dataType": "PASSWORD",
"controlType": "INPUT_TEXT",
- "placeholderText": "Password",
- "encrypted": true
+ "placeholderText": "password",
+ "encrypted": true,
+ "isRequired": true
}
]
}
@@ -58,20 +61,20 @@
},
{
"id": 3,
- "sectionName": "SSL (optional)",
+ "sectionName": "SSL",
"children": [
{
"label": "SSL Mode",
"configProperty": "datasourceConfiguration.connection.ssl.authType",
"controlType": "DROP_DOWN",
- "initialValue": "DISABLE",
+ "initialValue": "NO_VERIFY",
"options": [
{
"label": "Disable",
"value": "DISABLE"
},
{
- "label": "No verify",
+ "label": "TLS",
"value": "NO_VERIFY"
}
]
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/main/resources/setting.json b/app/server/appsmith-plugins/oraclePlugin/src/main/resources/setting.json
index 02b45adeed..38503374c3 100755
--- a/app/server/appsmith-plugins/oraclePlugin/src/main/resources/setting.json
+++ b/app/server/appsmith-plugins/oraclePlugin/src/main/resources/setting.json
@@ -8,24 +8,24 @@
"label": "Run query on page load",
"configProperty": "executeOnLoad",
"controlType": "SWITCH",
- "info": "Will refresh data each time the page is loaded"
+ "subtitle": "Will refresh data each time the page is loaded"
},
{
"label": "Request confirmation before running query",
"configProperty": "confirmBeforeExecute",
"controlType": "SWITCH",
- "info": "Ask confirmation from the user each time before refreshing data"
+ "subtitle": "Ask confirmation from the user each time before refreshing data"
},
{
"label": "Use Prepared Statement",
- "info": "Turning on Prepared Statement makes your queries resilient against bad things like SQL injections. However, it cannot be used if your dynamic binding contains any SQL keywords like 'SELECT', 'WHERE', 'AND', etc.",
+ "subtitle": "Turning on Prepared Statement makes your queries resilient against bad things like SQL injections. However, it cannot be used if your dynamic binding contains any SQL keywords like 'SELECT', 'WHERE', 'AND', etc.",
"configProperty": "actionConfiguration.formData.preparedStatement.data",
"controlType": "SWITCH",
"initialValue": true
},
{
"label": "Query timeout (in milliseconds)",
- "info": "Maximum time after which the query will return",
+ "subtitle": "Maximum time after which the query will return",
"configProperty": "actionConfiguration.timeoutInMillisecond",
"controlType": "INPUT_TEXT",
"dataType": "NUMBER"
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/DELETE.sql b/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/DELETE.sql
index e7014662d5..c5000f4ee8 100755
--- a/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/DELETE.sql
+++ b/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/DELETE.sql
@@ -1 +1 @@
-DELETE FROM users WHERE id = -1;
\ No newline at end of file
+DELETE FROM users WHERE id = {{idInput.text}}
\ No newline at end of file
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/CREATE.sql b/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/INSERT.sql
similarity index 97%
rename from app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/CREATE.sql
rename to app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/INSERT.sql
index b5253c2bf8..f59768c5c5 100755
--- a/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/CREATE.sql
+++ b/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/INSERT.sql
@@ -5,4 +5,4 @@ VALUES
{{ nameInput.text }},
{{ genderDropdown.selectedOptionValue }},
{{ emailInput.text }}
- );
\ No newline at end of file
+ )
\ No newline at end of file
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/SELECT.sql b/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/SELECT.sql
index 8d4afd4c3b..1001012697 100755
--- a/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/SELECT.sql
+++ b/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/SELECT.sql
@@ -1 +1 @@
-SELECT * FROM users where role = 'Admin' ORDER BY id LIMIT 10
+SELECT* FROM users WHERE ROWNUM < 10
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/UPDATE.sql b/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/UPDATE.sql
index facf9fc3f9..1e8246500e 100755
--- a/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/UPDATE.sql
+++ b/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/UPDATE.sql
@@ -1,3 +1 @@
-UPDATE users
- SET status = 'APPROVED'
- WHERE id = {{ usersTable.selectedRow.id }};
\ No newline at end of file
+UPDATE users SET status = 'APPROVED' WHERE id = {{ usersTable.selectedRow.id }}
\ No newline at end of file
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/meta.json b/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/meta.json
index b05a592845..222e2f9f27 100755
--- a/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/meta.json
+++ b/app/server/appsmith-plugins/oraclePlugin/src/main/resources/templates/meta.json
@@ -1,10 +1,10 @@
{
"templates": [
{
- "file": "CREATE.sql"
+ "file": "SELECT.sql"
},
{
- "file": "SELECT.sql"
+ "file": "INSERT.sql"
},
{
"file": "UPDATE.sql"
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OracleExecutionTest.java b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OracleExecutionTest.java
new file mode 100644
index 0000000000..62052a93f2
--- /dev/null
+++ b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OracleExecutionTest.java
@@ -0,0 +1,328 @@
+package com.external.plugins;
+
+import com.appsmith.external.datatypes.ClientDataType;
+import com.appsmith.external.dtos.ExecuteActionDTO;
+import com.appsmith.external.models.ActionConfiguration;
+import com.appsmith.external.models.ActionExecutionResult;
+import com.zaxxer.hikari.HikariDataSource;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.OracleContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.sql.SQLException;
+import java.text.MessageFormat;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.appsmith.external.helpers.PluginUtils.getExecuteDTOForTestWithBindingAndValueAndDataType;
+import static com.appsmith.external.helpers.PluginUtils.setDataValueSafelyInFormData;
+import static com.external.plugins.OracleTestDBContainerManager.getDefaultDatasourceConfig;
+import static com.external.plugins.OracleTestDBContainerManager.oraclePluginExecutor;
+import static com.external.plugins.OracleTestDBContainerManager.runSQLQueryOnOracleTestDB;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Testcontainers
+public class OracleExecutionTest {
+ public static final String SQL_QUERY_CREATE_TABLE_FORMAT =
+ "create table {0} (\n" +
+ "c_varchar2 varchar2(20),\n" +
+ "c_nvarchar2 nvarchar2(20),\n" +
+ "c_number number,\n" +
+ "c_float float,\n" +
+ "c_date date,\n" +
+ "c_binary_float binary_float,\n" +
+ "c_binary_double binary_double,\n" +
+ "c_timestamp timestamp,\n" +
+ "c_timestamp_tz timestamp with time zone,\n" +
+ "c_timestamp_ltz timestamp with local time zone,\n" +
+ "c_interval_year interval year to month,\n" +
+ "c_interval_day interval day to second,\n" +
+ "c_rowid rowid,\n" +
+ "c_urowid urowid,\n" +
+ "c_char char(20),\n" +
+ "c_nchar nchar(20),\n" +
+ "c_clob clob,\n" +
+ "c_nclob nclob\n" +
+ ")\n";
+ private static final String SQL_QUERY_TO_INSERT_ONE_ROW_FORMAT =
+ "insert into {0} values (\n" +
+ "''varchar2'',\n" +
+ "''nvarchar2'',\n" +
+ "{1},\n" +
+ "11.22,\n" +
+ "''03-OCT-02'',\n" +
+ "11.22,\n" +
+ "11.22,\n" +
+ "TIMESTAMP''1997-01-01 09:26:50.124'',\n" +
+ "TIMESTAMP''1997-01-01 09:26:56.66 +02:00'',\n" +
+ "TIMESTAMP''1999-04-05 8:00:00 US/Pacific'',\n" +
+ "INTERVAL ''1'' YEAR(3),\n" +
+ "INTERVAL ''1'' HOUR,\n" +
+ "''000001F8.0001.0006'',\n" +
+ "''000001F8.0001.0006'',\n" +
+ "''char'',\n" +
+ "''nchar'',\n" +
+ "''clob'',\n" +
+ "''nclob''\n" +
+ ")";
+
+ private static final String SQL_QUERY_TO_INSERT_ONE_ROW_WITH_BINDING_FORMAT =
+ "insert into {0} values (\n" +
+ "'{{'binding1'}}',\n" +
+ "'{{'binding2'}}',\n" +
+ "'{{'binding3'}}',\n" +
+ "'{{'binding4'}}',\n" +
+ "'{{'binding5'}}',\n" +
+ "'{{'binding6'}}',\n" +
+ "'{{'binding7'}}',\n" +
+ "TO_TIMESTAMP('{{'binding8'}}', ''YYYY-MM-DD HH24:MI:SS.FF''),\n" +
+ "TO_TIMESTAMP('{{'binding9'}}', ''YYYY-MM-DD HH24:MI:SS.FF''),\n" +
+ "TO_TIMESTAMP('{{'binding10'}}', ''YYYY-MM-DD HH24:MI:SS.FF''),\n" +
+ "NUMTOYMINTERVAL('{{'binding11'}}', ''YEAR''),\n" +
+ "NUMTODSINTERVAL('{{'binding12'}}', ''HOUR''),\n" +
+ "'{{'binding13'}}',\n" +
+ "'{{'binding14'}}',\n" +
+ "'{{'binding15'}}',\n" +
+ "'{{'binding16'}}',\n" +
+ "'{{'binding17'}}',\n" +
+ "'{{'binding18'}}'\n" +
+ ")";
+
+ public static final String SELECT_TEST_WITHOUT_PREPARED_STMT_TABLE_NAME = "testSelectWithPreparedStatementWithoutAnyBinding";
+ public static final String SELECT_TEST_WITH_PREPARED_STMT_TABLE_NAME = "testSelectWithPreparedStatementWithBinding";
+ public static final String INSERT_TEST_WITHOUT_PREPARED_STMT_TABLE_NAME = "testInsertWithPreparedStatementWithoutAnyBinding";
+ public static final String INSERT_TEST_WITH_PREPARED_STMT_TABLE_NAME = "testInsertWithPreparedStatementWithBinding";
+ public static final String UPDATE_TEST_WITHOUT_PREPARED_STMT_TABLE_NAME = "testUpdateWithPreparedStatementWithoutAnyBinding";
+ public static final String UPDATE_TEST_WITH_PREPARED_STMT_TABLE_NAME = "testUpdateWithPreparedStatementWithBinding";
+ public static final String DELETE_TEST_WITHOUT_PREPARED_STMT_TABLE_NAME = "testDeleteWithPreparedStatementWithoutAnyBinding";
+ public static final String DELETE_TEST_WITH_PREPARED_STMT_TABLE_NAME = "testDeleteWithPreparedStatementWithBinding";
+
+ @SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is pseudo-optional.
+ @Container
+ private static final OracleContainer oracleDB = OracleTestDBContainerManager.getOracleDBForTest();
+
+ private static HikariDataSource sharedConnectionPool = null;
+
+ @BeforeAll
+ public static void setup() throws SQLException {
+ sharedConnectionPool = oraclePluginExecutor.datasourceCreate(getDefaultDatasourceConfig(oracleDB)).block();
+ createTablesForTest();
+ }
+
+ public static void createTablesForTest() throws SQLException {
+ createTableWithName(SELECT_TEST_WITHOUT_PREPARED_STMT_TABLE_NAME);
+ createTableWithName(SELECT_TEST_WITH_PREPARED_STMT_TABLE_NAME);
+ createTableWithName(INSERT_TEST_WITHOUT_PREPARED_STMT_TABLE_NAME);
+ createTableWithName(INSERT_TEST_WITH_PREPARED_STMT_TABLE_NAME);
+ createTableWithName(UPDATE_TEST_WITHOUT_PREPARED_STMT_TABLE_NAME);
+ createTableWithName(UPDATE_TEST_WITH_PREPARED_STMT_TABLE_NAME);
+ createTableWithName(DELETE_TEST_WITHOUT_PREPARED_STMT_TABLE_NAME);
+ createTableWithName(DELETE_TEST_WITH_PREPARED_STMT_TABLE_NAME);
+ }
+
+ private static void createTableWithName(String tableName) throws SQLException {
+ String sqlQueryToCreateTable = MessageFormat.format(SQL_QUERY_CREATE_TABLE_FORMAT, tableName);
+ runSQLQueryOnOracleTestDB(sqlQueryToCreateTable, sharedConnectionPool);
+
+ String sqlQueryToInsertRow1 = MessageFormat.format(SQL_QUERY_TO_INSERT_ONE_ROW_FORMAT, tableName, 1);
+ runSQLQueryOnOracleTestDB(sqlQueryToInsertRow1, sharedConnectionPool);
+
+ String sqlQueryToInsertRow2 = MessageFormat.format(SQL_QUERY_TO_INSERT_ONE_ROW_FORMAT, tableName, 2);
+ runSQLQueryOnOracleTestDB(sqlQueryToInsertRow2, sharedConnectionPool);
+ }
+
+ @Test
+ public void testSelectQueryWithPreparedStatementWithoutAnyBinding() {
+ String sqlSelectQuery = MessageFormat.format("SELECT c_number FROM {0} ORDER BY c_number",
+ SELECT_TEST_WITHOUT_PREPARED_STMT_TABLE_NAME);
+ Map formData = setDataValueSafelyInFormData(null, "body", sqlSelectQuery);
+ ActionConfiguration actionConfig = new ActionConfiguration();
+ actionConfig.setFormData(formData);
+ Mono executionResultMono = oraclePluginExecutor.executeParameterized(sharedConnectionPool, new ExecuteActionDTO(),
+ getDefaultDatasourceConfig(oracleDB), actionConfig);
+ String expectedResultString = "[{\"C_NUMBER\":\"1\"},{\"C_NUMBER\":\"2\"}]";
+ verifyColumnValue(executionResultMono, expectedResultString);
+ }
+
+ @Test
+ public void testQueryWorksWithSemicolonInTheEnd() {
+ String sqlSelectQuery = MessageFormat.format("SELECT c_number FROM {0} ORDER BY c_number;",
+ SELECT_TEST_WITHOUT_PREPARED_STMT_TABLE_NAME);
+ Map formData = setDataValueSafelyInFormData(null, "body", sqlSelectQuery);
+ ActionConfiguration actionConfig = new ActionConfiguration();
+ actionConfig.setFormData(formData);
+ Mono executionResultMono = oraclePluginExecutor.executeParameterized(sharedConnectionPool, new ExecuteActionDTO(),
+ getDefaultDatasourceConfig(oracleDB), actionConfig);
+ String expectedResultString = "[{\"C_NUMBER\":\"1\"},{\"C_NUMBER\":\"2\"}]";
+ verifyColumnValue(executionResultMono, expectedResultString);
+ }
+
+ @Test
+ public void testSelectQueryWithPreparedStatementWithBinding() {
+ String sqlSelectQuery = MessageFormat.format("SELECT c_number FROM {0} WHERE " +
+ "c_varchar2='{{'binding1'}}' ORDER BY c_number DESC", SELECT_TEST_WITH_PREPARED_STMT_TABLE_NAME);
+ Map formData = setDataValueSafelyInFormData(null, "body", sqlSelectQuery);
+ ActionConfiguration actionConfig = new ActionConfiguration();
+ actionConfig.setFormData(formData);
+
+ LinkedHashMap bindingNameToValueAndDataTypeMap = new LinkedHashMap<>();
+ bindingNameToValueAndDataTypeMap.put("binding1", List.of("varchar2", ClientDataType.STRING));
+ ExecuteActionDTO executeActionDTO =
+ getExecuteDTOForTestWithBindingAndValueAndDataType(bindingNameToValueAndDataTypeMap);
+
+ Mono executionResultMono =
+ oraclePluginExecutor.executeParameterized(sharedConnectionPool, executeActionDTO,
+ getDefaultDatasourceConfig(oracleDB), actionConfig);
+ String expectedResultString = "[{\"C_NUMBER\":\"2\"},{\"C_NUMBER\":\"1\"}]";
+ verifyColumnValue(executionResultMono, expectedResultString);
+ }
+
+ @Test
+ public void testInsertQueryReturnValueWithPreparedStatementWithoutAnyBinding() {
+ String sqlInsertQuery = MessageFormat.format(SQL_QUERY_TO_INSERT_ONE_ROW_FORMAT,
+ INSERT_TEST_WITHOUT_PREPARED_STMT_TABLE_NAME, 3);
+ Map insertQueryFormData = setDataValueSafelyInFormData(null, "body", sqlInsertQuery);
+ ActionConfiguration insertQueryActionConfig = new ActionConfiguration();
+ insertQueryActionConfig.setFormData(insertQueryFormData);
+ Mono insertQueryExecutionResultMono =
+ oraclePluginExecutor.executeParameterized(sharedConnectionPool, new ExecuteActionDTO(),
+ getDefaultDatasourceConfig(oracleDB), insertQueryActionConfig);
+ String insertQueryExpectedResultString = "[{\"affectedRows\":1}]";
+ verifyColumnValue(insertQueryExecutionResultMono, insertQueryExpectedResultString);
+ }
+
+ @Test
+ public void testInsertQueryVerifyNewRowAddedWithPreparedStatementWithoutAnyBinding() {
+ String sqlInsertQuery = MessageFormat.format(SQL_QUERY_TO_INSERT_ONE_ROW_FORMAT,
+ INSERT_TEST_WITHOUT_PREPARED_STMT_TABLE_NAME, 4);
+ Map insertQueryFormData = setDataValueSafelyInFormData(null, "body", sqlInsertQuery);
+ ActionConfiguration insertQueryActionConfig = new ActionConfiguration();
+ insertQueryActionConfig.setFormData(insertQueryFormData);
+ oraclePluginExecutor.executeParameterized(sharedConnectionPool, new ExecuteActionDTO(),
+ getDefaultDatasourceConfig(oracleDB), insertQueryActionConfig).block();
+
+ String sqlSelectQuery = MessageFormat.format("SELECT * FROM {0} WHERE c_number=4",
+ INSERT_TEST_WITHOUT_PREPARED_STMT_TABLE_NAME);
+ Map selectQueryFormData = setDataValueSafelyInFormData(null, "body", sqlSelectQuery);
+ ActionConfiguration selectQueryActionConfig = new ActionConfiguration();
+ selectQueryActionConfig.setFormData(selectQueryFormData);
+ Mono selectQueryExecutionResultMono =
+ oraclePluginExecutor.executeParameterized(sharedConnectionPool, new ExecuteActionDTO(),
+ getDefaultDatasourceConfig(oracleDB), selectQueryActionConfig);
+ String selectQueryExpectedResultString = "[{\"C_VARCHAR2\":\"varchar2\",\"C_NVARCHAR2\":\"nvarchar2\"," +
+ "\"C_NUMBER\":\"4\",\"C_FLOAT\":\"11.22\",\"C_DATE\":\"2002-10-03\",\"C_BINARY_FLOAT\":\"11.22\"," +
+ "\"C_BINARY_DOUBLE\":\"11.22\",\"C_TIMESTAMP\":\"1997-01-01T09:26:50.124Z\"," +
+ "\"C_TIMESTAMP_TZ\":\"1997-01-01T09:26:56.66+02:00\",\"C_TIMESTAMP_LTZ\":\"1999-04-05T15:00:00Z\"," +
+ "\"C_INTERVAL_YEAR\":\"1-0\",\"C_INTERVAL_DAY\":\"0 1:0:0.0\",\"C_ROWID\":\"AAAAAAAAGAAAAH4AAB\"," +
+ "\"C_UROWID\":\"000001F8.0001.0006\",\"C_CHAR\":\"char \",\"C_NCHAR\":\"nchar " +
+ " \",\"C_CLOB\":\"clob\",\"C_NCLOB\":\"nclob\"}]";
+ verifyColumnValue(selectQueryExecutionResultMono, selectQueryExpectedResultString);
+ }
+
+ @Test
+ public void testInsertQueryReturnValueWithPreparedStatementWithBinding() {
+ String sqlInsertQuery = MessageFormat.format(SQL_QUERY_TO_INSERT_ONE_ROW_WITH_BINDING_FORMAT,
+ INSERT_TEST_WITH_PREPARED_STMT_TABLE_NAME);
+ Map insertQueryFormData = setDataValueSafelyInFormData(null, "body", sqlInsertQuery);
+ ActionConfiguration insertQueryActionConfig = new ActionConfiguration();
+ insertQueryActionConfig.setFormData(insertQueryFormData);
+
+ LinkedHashMap bindingNameToValueAndDataTypeMap = new LinkedHashMap<>();
+ bindingNameToValueAndDataTypeMap.put("binding1", List.of("varchar2", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding2", List.of("nvarchar2", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding3", List.of("3", ClientDataType.NUMBER));
+ bindingNameToValueAndDataTypeMap.put("binding4", List.of("11.22", ClientDataType.NUMBER));
+ bindingNameToValueAndDataTypeMap.put("binding5", List.of("03-OCT-02", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding6", List.of("11.22", ClientDataType.NUMBER));
+ bindingNameToValueAndDataTypeMap.put("binding7", List.of("11.22", ClientDataType.NUMBER));
+ bindingNameToValueAndDataTypeMap.put("binding8", List.of("1997-01-01 09:26:50.124", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding9", List.of("1997-01-01 09:26:50.124", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding10", List.of("1997-01-01 09:26:50.124", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding11", List.of("1", ClientDataType.NUMBER));
+ bindingNameToValueAndDataTypeMap.put("binding12", List.of("1", ClientDataType.NUMBER));
+ bindingNameToValueAndDataTypeMap.put("binding13", List.of("000001F8.0001.0006", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding14", List.of("000001F8.0001.0006", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding15", List.of("char", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding16", List.of("nchar", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding17", List.of("clob", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding18", List.of("nclob", ClientDataType.STRING));
+
+ ExecuteActionDTO executeActionDTO =
+ getExecuteDTOForTestWithBindingAndValueAndDataType(bindingNameToValueAndDataTypeMap);
+
+ Mono insertQueryExecutionResultMono =
+ oraclePluginExecutor.executeParameterized(sharedConnectionPool, executeActionDTO,
+ getDefaultDatasourceConfig(oracleDB), insertQueryActionConfig);
+ String insertQueryExpectedResultString = "[{\"affectedRows\":1}]";
+ verifyColumnValue(insertQueryExecutionResultMono, insertQueryExpectedResultString);
+ }
+
+ @Test
+ public void testInsertQueryVerifyNewRowAddedWithPreparedStatementWithBinding() {
+ String sqlInsertQuery = MessageFormat.format(SQL_QUERY_TO_INSERT_ONE_ROW_WITH_BINDING_FORMAT,
+ INSERT_TEST_WITH_PREPARED_STMT_TABLE_NAME);
+ Map insertQueryFormData = setDataValueSafelyInFormData(null, "body", sqlInsertQuery);
+ ActionConfiguration insertQueryActionConfig = new ActionConfiguration();
+ insertQueryActionConfig.setFormData(insertQueryFormData);
+
+ LinkedHashMap bindingNameToValueAndDataTypeMap = new LinkedHashMap<>();
+ bindingNameToValueAndDataTypeMap.put("binding1", List.of("varchar2", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding2", List.of("nvarchar2", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding3", List.of("5", ClientDataType.NUMBER));
+ bindingNameToValueAndDataTypeMap.put("binding4", List.of("11.22", ClientDataType.NUMBER));
+ bindingNameToValueAndDataTypeMap.put("binding5", List.of("03-OCT-02", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding6", List.of("11.22", ClientDataType.NUMBER));
+ bindingNameToValueAndDataTypeMap.put("binding7", List.of("11.22", ClientDataType.NUMBER));
+ bindingNameToValueAndDataTypeMap.put("binding8", List.of("1997-01-01 09:26:50.124", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding9", List.of("1997-01-01 09:26:50.124", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding10", List.of("1997-01-01 09:26:50.124", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding11", List.of("1", ClientDataType.NUMBER));
+ bindingNameToValueAndDataTypeMap.put("binding12", List.of("1", ClientDataType.NUMBER));
+ bindingNameToValueAndDataTypeMap.put("binding13", List.of("000001F8.0001.0006", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding14", List.of("000001F8.0001.0006", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding15", List.of("char", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding16", List.of("nchar", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding17", List.of("clob", ClientDataType.STRING));
+ bindingNameToValueAndDataTypeMap.put("binding18", List.of("nclob", ClientDataType.STRING));
+
+ ExecuteActionDTO executeActionDTO =
+ getExecuteDTOForTestWithBindingAndValueAndDataType(bindingNameToValueAndDataTypeMap);
+ oraclePluginExecutor.executeParameterized(sharedConnectionPool, executeActionDTO,
+ getDefaultDatasourceConfig(oracleDB), insertQueryActionConfig).block();
+
+ String sqlSelectQuery = MessageFormat.format("SELECT c_varchar2, c_nvarchar2, c_number, c_float, c_date, " +
+ "c_binary_float, c_binary_double, c_timestamp, c_interval_year, " +
+ "c_interval_day, c_rowid, c_urowid, c_char, c_nchar, c_clob, c_nclob FROM {0} WHERE c_number=5",
+ INSERT_TEST_WITH_PREPARED_STMT_TABLE_NAME);
+ Map selectQueryFormData = setDataValueSafelyInFormData(null, "body", sqlSelectQuery);
+ ActionConfiguration selectQueryActionConfig = new ActionConfiguration();
+ selectQueryActionConfig.setFormData(selectQueryFormData);
+ Mono selectQueryExecutionResultMono =
+ oraclePluginExecutor.executeParameterized(sharedConnectionPool, new ExecuteActionDTO(),
+ getDefaultDatasourceConfig(oracleDB), selectQueryActionConfig);
+ String selectQueryExpectedResultString = "[{\"C_VARCHAR2\":\"varchar2\",\"C_NVARCHAR2\":\"nvarchar2\"," +
+ "\"C_NUMBER\":\"5\",\"C_FLOAT\":\"11.22\",\"C_DATE\":\"2002-10-03\",\"C_BINARY_FLOAT\":\"11.22\"," +
+ "\"C_BINARY_DOUBLE\":\"11.22\",\"C_TIMESTAMP\":\"1997-01-01T09:26:50.124Z\"," +
+ "\"C_INTERVAL_YEAR\":\"1-0\",\"C_INTERVAL_DAY\":\"0 1:0:0.0\"," +
+ "\"C_ROWID\":\"AAAAAAAAGAAAAH4AAB\",\"C_UROWID\":\"000001F8.0001.0006\",\"C_CHAR\":\"char " +
+ " \",\"C_NCHAR\":\"nchar \",\"C_CLOB\":\"clob\",\"C_NCLOB\":\"nclob\"}]";
+ verifyColumnValue(selectQueryExecutionResultMono, selectQueryExpectedResultString);
+ }
+
+ private void verifyColumnValue(Mono executionResultMono, String expectedResult) {
+ StepVerifier.create(executionResultMono)
+ .assertNext(actionExecutionResult -> {
+ assertTrue(actionExecutionResult.getIsExecutionSuccess(), actionExecutionResult.getBody().toString());
+ if (expectedResult != null) {
+ assertEquals(expectedResult, actionExecutionResult.getBody().toString());
+ }
+ })
+ .verifyComplete();
+ }
+}
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OracleGetDBSchemaTest.java b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OracleGetDBSchemaTest.java
new file mode 100644
index 0000000000..86936a3091
--- /dev/null
+++ b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OracleGetDBSchemaTest.java
@@ -0,0 +1,163 @@
+package com.external.plugins;
+
+import com.appsmith.external.models.DatasourceStructure;
+import com.zaxxer.hikari.HikariDataSource;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.OracleContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.sql.SQLException;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static com.appsmith.external.helpers.PluginUtils.STRING_TYPE;
+import static com.appsmith.external.helpers.PluginUtils.getDataValueSafelyFromFormData;
+import static com.external.plugins.OracleTestDBContainerManager.getDefaultDatasourceConfig;
+import static com.external.plugins.OracleTestDBContainerManager.oraclePluginExecutor;
+import static com.external.plugins.OracleTestDBContainerManager.runSQLQueryOnOracleTestDB;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Testcontainers
+public class OracleGetDBSchemaTest {
+ public static final String SQL_QUERY_TO_CREATE_TABLE_WITH_PRIMARY_KEY =
+ "CREATE TABLE supplier\n" +
+ "( supplier_id numeric(10) not null,\n" +
+ " supplier_name varchar2(50) not null,\n" +
+ " contact_name varchar2(50),\n" +
+ " CONSTRAINT supplier_pk PRIMARY KEY (supplier_id)\n" +
+ ")";
+
+ public static final String SQL_QUERY_TO_CREATE_TABLE_WITH_FOREIGN_KEY =
+ "CREATE TABLE products\n" +
+ "( product_id numeric(10) not null,\n" +
+ " supplier_id numeric(10) not null,\n" +
+ " CONSTRAINT fk_supplier\n" +
+ " FOREIGN KEY (supplier_id)\n" +
+ " REFERENCES supplier(supplier_id)\n" +
+ ")";
+
+ @SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is pseudo-optional.
+ @Container
+ private static final OracleContainer oracleDB = OracleTestDBContainerManager.getOracleDBForTest();
+
+ private static HikariDataSource sharedConnectionPool = null;
+
+ @BeforeAll
+ public static void setup() throws SQLException {
+ sharedConnectionPool = oraclePluginExecutor.datasourceCreate(getDefaultDatasourceConfig(oracleDB)).block();
+ createTablesForTest();
+ }
+
+ private static void createTablesForTest() throws SQLException {
+ runSQLQueryOnOracleTestDB(SQL_QUERY_TO_CREATE_TABLE_WITH_PRIMARY_KEY, sharedConnectionPool);
+ runSQLQueryOnOracleTestDB(SQL_QUERY_TO_CREATE_TABLE_WITH_FOREIGN_KEY, sharedConnectionPool);
+ }
+
+ @Test
+ public void testDBSchemaShowsAllTables() {
+ Mono datasourceStructureMono =
+ oraclePluginExecutor.getStructure(sharedConnectionPool,
+ getDefaultDatasourceConfig(oracleDB));
+
+ StepVerifier.create(datasourceStructureMono)
+ .assertNext(datasourceStructure -> {
+ Set setOfAllTableNames = datasourceStructure.getTables().stream()
+ .map(DatasourceStructure.Table::getName)
+ .map(String::toLowerCase)
+ .collect(Collectors.toSet());
+
+ assertTrue(setOfAllTableNames.equals(Set.of("supplier","products")), setOfAllTableNames.toString());
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ public void testDBSchemaShowsAllColumnsAndTypesInATable() {
+ Mono datasourceStructureMono =
+ oraclePluginExecutor.getStructure(sharedConnectionPool,
+ getDefaultDatasourceConfig(oracleDB));
+
+ StepVerifier.create(datasourceStructureMono)
+ .assertNext(datasourceStructure -> {
+ DatasourceStructure.Table supplierTable = datasourceStructure.getTables().stream()
+ .filter(table -> "supplier".equalsIgnoreCase(table.getName()))
+ .findFirst()
+ .get();
+
+ assertTrue(supplierTable != null, "supplier table not found in DB schema");
+
+ Set allColumnNames = supplierTable.getColumns().stream()
+ .map(DatasourceStructure.Column::getName)
+ .map(String::toLowerCase)
+ .collect(Collectors.toSet());
+ Set expectedColumnNames = Set.of("supplier_id", "supplier_name", "contact_name");
+ assertEquals(expectedColumnNames, allColumnNames, allColumnNames.toString());
+
+ supplierTable.getColumns().stream()
+ .forEach(column -> {
+ String columnName = column.getName().toLowerCase();
+ String columnType = column.getType().toLowerCase();
+ String expectedColumnType = null;
+
+ if ("supplier_id".equals(columnName)) {
+ expectedColumnType = "number";
+ }
+ else {
+ expectedColumnType = "varchar2";
+ }
+
+ assertEquals(expectedColumnType, columnType, columnType);
+ });
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ public void testDynamicSqlTemplateQueriesForATable() {
+ Mono datasourceStructureMono =
+ oraclePluginExecutor.getStructure(sharedConnectionPool,
+ getDefaultDatasourceConfig(oracleDB));
+
+ StepVerifier.create(datasourceStructureMono)
+ .assertNext(datasourceStructure -> {
+ DatasourceStructure.Table supplierTable = datasourceStructure.getTables().stream()
+ .filter(table -> "supplier".equalsIgnoreCase(table.getName()))
+ .findFirst()
+ .get();
+
+ assertTrue(supplierTable != null, "supplier table not found in DB schema");
+
+ supplierTable.getTemplates().stream()
+ .filter(template -> "select".equalsIgnoreCase(template.getTitle()) || "delete".equalsIgnoreCase(template.getTitle()))
+ .forEach(template -> {
+ /**
+ * Not sure how to test query templates for insert and update queries as these
+ * queries include column names in an order that is not fixed. Hence, skipping testing
+ * them for now.
+ */
+
+ String expectedSelectQueryTemplate = null;
+ if ("select".equalsIgnoreCase(template.getTitle())) {
+ expectedSelectQueryTemplate = "select * from supplier where rownum < 10";
+ }
+ else if ("delete".equalsIgnoreCase(template.getTitle())) {
+ expectedSelectQueryTemplate = "delete from supplier where 1=0 -- specify a valid" +
+ " condition here. removing the condition may delete everything in the " +
+ "table!";
+ }
+
+ String templateQuery =
+ getDataValueSafelyFromFormData((Map) template.getConfiguration(), "body", STRING_TYPE);
+ assertEquals(expectedSelectQueryTemplate, templateQuery.toLowerCase(),
+ templateQuery.toLowerCase());
+ });
+ })
+ .verifyComplete();
+ }
+}
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OraclePluginConnectionTest.java b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OraclePluginConnectionTest.java
new file mode 100755
index 0000000000..30c8a5d87a
--- /dev/null
+++ b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OraclePluginConnectionTest.java
@@ -0,0 +1,81 @@
+package com.external.plugins;
+
+
+import com.appsmith.external.models.DBAuth;
+import com.appsmith.external.models.DatasourceConfiguration;
+import com.appsmith.external.models.DatasourceTestResult;
+import com.external.plugins.exceptions.OraclePluginError;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.OracleContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+import static com.external.plugins.OracleTestDBContainerManager.getDefaultDatasourceConfig;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Testcontainers
+public class OraclePluginConnectionTest {
+
+ OraclePlugin.OraclePluginExecutor oraclePluginExecutor = new OraclePlugin.OraclePluginExecutor();
+
+ @SuppressWarnings("rawtypes") // The type parameter for the container type is just itself and is pseudo-optional.
+ @Container
+ private static final OracleContainer oracleDB = OracleTestDBContainerManager.getOracleDBForTest();
+
+ @Test
+ public void testDatasourceConnectionTestPassWithValidConfig() {
+ Mono testDsResultMono =
+ oraclePluginExecutor.testDatasource(getDefaultDatasourceConfig(oracleDB));
+ StepVerifier.create(testDsResultMono)
+ .assertNext(testResult -> {
+ assertEquals(0, testResult.getInvalids().size());
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ public void testDatasourceConnectionTestFailWithInvalidPassword() {
+ DatasourceConfiguration invalidDsConfig = getDefaultDatasourceConfig(oracleDB);
+ ((DBAuth)invalidDsConfig.getAuthentication()).setPassword("invalid_password");
+
+ Mono testDsResultMono =
+ oraclePluginExecutor.testDatasource(invalidDsConfig);
+ StepVerifier.create(testDsResultMono)
+ .assertNext(testResult -> {
+ assertNotEquals(0, testResult.getInvalids().size());
+ String expectedError = "Failed to initialize pool: ORA-01017: invalid username/password; logon " +
+ "denied";
+ boolean isExpectedErrorReceived = testResult.getInvalids().stream()
+ .anyMatch(errorString -> expectedError.equals(errorString.trim()));
+ assertTrue(isExpectedErrorReceived);
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ public void testDatasourceConnectionTestFailWithInvalidUsername() {
+ DatasourceConfiguration invalidDsConfig = getDefaultDatasourceConfig(oracleDB);
+ ((DBAuth)invalidDsConfig.getAuthentication()).setUsername("invalid_username");
+
+ Mono testDsResultMono =
+ oraclePluginExecutor.testDatasource(invalidDsConfig);
+ StepVerifier.create(testDsResultMono)
+ .assertNext(testResult -> {
+ assertNotEquals(0, testResult.getInvalids().size());
+ String expectedError = "Failed to initialize pool: ORA-01017: invalid username/password; logon " +
+ "denied";
+ boolean isExpectedErrorReceived = testResult.getInvalids().stream()
+ .anyMatch(errorString -> expectedError.equals(errorString.trim()));
+ assertTrue(isExpectedErrorReceived);
+ })
+ .verifyComplete();
+ }
+
+}
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OraclePluginDatasourceValidityErrorsTest.java b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OraclePluginDatasourceValidityErrorsTest.java
new file mode 100644
index 0000000000..d9201f35ed
--- /dev/null
+++ b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OraclePluginDatasourceValidityErrorsTest.java
@@ -0,0 +1,64 @@
+package com.external.plugins;
+
+import com.appsmith.external.models.DBAuth;
+import com.appsmith.external.models.DatasourceConfiguration;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Set;
+
+import static com.external.plugins.OracleTestDBContainerManager.getDefaultDatasourceConfig;
+import static com.external.plugins.exceptions.OracleErrorMessages.DS_MISSING_ENDPOINT_ERROR_MSG;
+import static com.external.plugins.exceptions.OracleErrorMessages.DS_MISSING_HOSTNAME_ERROR_MSG;
+import static com.external.plugins.exceptions.OracleErrorMessages.DS_MISSING_PASSWORD_ERROR_MSG;
+import static com.external.plugins.exceptions.OracleErrorMessages.DS_MISSING_USERNAME_ERROR_MSG;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class OraclePluginDatasourceValidityErrorsTest {
+
+ OraclePlugin.OraclePluginExecutor oraclePluginExecutor = new OraclePlugin.OraclePluginExecutor();
+
+ @Test
+ public void testErrorOnMissingUsername() {
+ DatasourceConfiguration dsConfigWithMissingUsername = getDefaultDatasourceConfig(null);
+ ((DBAuth)dsConfigWithMissingUsername.getAuthentication()).setUsername("");
+
+ Set dsValidateResult = oraclePluginExecutor.validateDatasource(dsConfigWithMissingUsername);
+ boolean isExpectedErrorReceived = dsValidateResult.stream()
+ .anyMatch(errorString -> DS_MISSING_USERNAME_ERROR_MSG.equals(errorString.trim()));
+ assertTrue(isExpectedErrorReceived);
+ }
+
+ @Test
+ public void testErrorOnMissingPassword() {
+ DatasourceConfiguration dsConfigWithMissingPassword = getDefaultDatasourceConfig(null);
+ ((DBAuth)dsConfigWithMissingPassword.getAuthentication()).setPassword("");
+
+ Set dsValidateResult = oraclePluginExecutor.validateDatasource(dsConfigWithMissingPassword);
+ boolean isExpectedErrorReceived = dsValidateResult.stream()
+ .anyMatch(errorString -> DS_MISSING_PASSWORD_ERROR_MSG.equals(errorString.trim()));
+ assertTrue(isExpectedErrorReceived);
+ }
+
+ @Test
+ public void testErrorOnMissingEndpoint() {
+ DatasourceConfiguration dsConfigWithMissingEndpoint = getDefaultDatasourceConfig(null);
+ dsConfigWithMissingEndpoint.setEndpoints(new ArrayList<>());
+
+ Set dsValidateResult = oraclePluginExecutor.validateDatasource(dsConfigWithMissingEndpoint);
+ boolean isExpectedErrorReceived = dsValidateResult.stream()
+ .anyMatch(errorString -> DS_MISSING_ENDPOINT_ERROR_MSG.equals(errorString.trim()));
+ assertTrue(isExpectedErrorReceived);
+ }
+
+ @Test
+ public void testErrorOnMissingHost() {
+ DatasourceConfiguration dsConfigWithMissingHost = getDefaultDatasourceConfig(null);
+ dsConfigWithMissingHost.getEndpoints().get(0).setHost("");
+
+ Set dsValidateResult = oraclePluginExecutor.validateDatasource(dsConfigWithMissingHost);
+ boolean isExpectedErrorReceived = dsValidateResult.stream()
+ .anyMatch(errorString -> DS_MISSING_HOSTNAME_ERROR_MSG.equals(errorString.trim()));
+ assertTrue(isExpectedErrorReceived);
+ }
+}
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OraclePluginErrorsTest.java b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OraclePluginErrorsTest.java
new file mode 100644
index 0000000000..37b9df1e47
--- /dev/null
+++ b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OraclePluginErrorsTest.java
@@ -0,0 +1,18 @@
+package com.external.plugins;
+
+import com.external.plugins.exceptions.OraclePluginError;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+public class OraclePluginErrorsTest {
+ @Test
+ public void verifyUniquenessOfOraclePluginErrorCode() {
+ assert (Arrays.stream(OraclePluginError.values()).map(OraclePluginError::getAppErrorCode).distinct().count() == OraclePluginError.values().length);
+
+ assert (Arrays.stream(OraclePluginError.values()).map(OraclePluginError::getAppErrorCode)
+ .filter(appErrorCode -> appErrorCode.length() != 11 || !appErrorCode.startsWith("PE-ORC"))
+ .collect(Collectors.toList()).size() == 0);
+ }
+}
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OraclePluginTest.java b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OraclePluginTest.java
deleted file mode 100755
index 6b398c222a..0000000000
--- a/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OraclePluginTest.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.external.plugins;
-
-
-import lombok.extern.slf4j.Slf4j;
-import org.testcontainers.junit.jupiter.Testcontainers;
-
-@Slf4j
-@Testcontainers
-public class OraclePluginTest {
- // TODO: fill it
-}
diff --git a/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OracleTestDBContainerManager.java b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OracleTestDBContainerManager.java
new file mode 100644
index 0000000000..9cb628d848
--- /dev/null
+++ b/app/server/appsmith-plugins/oraclePlugin/src/test/java/com/external/plugins/OracleTestDBContainerManager.java
@@ -0,0 +1,57 @@
+package com.external.plugins;
+
+import com.appsmith.external.models.Connection;
+import com.appsmith.external.models.DBAuth;
+import com.appsmith.external.models.DatasourceConfiguration;
+import com.appsmith.external.models.Endpoint;
+import com.appsmith.external.models.SSLDetails;
+import com.zaxxer.hikari.HikariDataSource;
+import org.testcontainers.containers.OracleContainer;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+
+import static com.external.plugins.utils.OracleDatasourceUtils.getConnectionFromConnectionPool;
+import static com.external.plugins.utils.OracleExecuteUtils.closeConnectionPostExecution;
+
+public class OracleTestDBContainerManager {
+ public static final String ORACLE_USERNAME = "testUser";
+ public static final String ORACLE_PASSWORD = "testPassword";
+ public static final String ORACLE_DB_NAME = "testDB";
+ public static final String ORACLE_DOCKER_HUB_CONTAINER = "gvenzl/oracle-xe:21-slim-faststart";
+ static OraclePlugin.OraclePluginExecutor oraclePluginExecutor = new OraclePlugin.OraclePluginExecutor();
+
+ public static OracleContainer getOracleDBForTest() {
+ return new OracleContainer(ORACLE_DOCKER_HUB_CONTAINER)
+ .withDatabaseName(ORACLE_DB_NAME)
+ .withUsername(ORACLE_USERNAME)
+ .withPassword(ORACLE_PASSWORD);
+ }
+
+ public static DatasourceConfiguration getDefaultDatasourceConfig(OracleContainer oracleDB) {
+ DatasourceConfiguration dsConfig = new DatasourceConfiguration();
+ dsConfig.setAuthentication(new DBAuth());
+ ((DBAuth)dsConfig.getAuthentication()).setUsername(OracleTestDBContainerManager.ORACLE_USERNAME);
+ ((DBAuth)dsConfig.getAuthentication()).setPassword(OracleTestDBContainerManager.ORACLE_PASSWORD);
+ ((DBAuth)dsConfig.getAuthentication()).setDatabaseName(OracleTestDBContainerManager.ORACLE_DB_NAME);
+
+ dsConfig.setEndpoints(new ArrayList<>());
+ String host = oracleDB == null ? "host" : oracleDB.getHost();
+ long port = oracleDB == null ? 1521L : (long)oracleDB.getOraclePort();
+ dsConfig.getEndpoints().add(new Endpoint(host, port));
+
+ dsConfig.setConnection(new Connection());
+ dsConfig.getConnection().setSsl(new SSLDetails());
+ dsConfig.getConnection().getSsl().setAuthType(SSLDetails.AuthType.DISABLE);
+
+ return dsConfig;
+ }
+
+ static void runSQLQueryOnOracleTestDB(String sqlQuery, HikariDataSource sharedConnectionPool) throws SQLException {
+ java.sql.Connection connectionFromPool = getConnectionFromConnectionPool(sharedConnectionPool);
+ Statement statement = connectionFromPool.createStatement();
+ statement.execute(sqlQuery);
+ closeConnectionPostExecution(null, statement, null, connectionFromPool);
+ }
+}
diff --git a/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/plugins/RestApiPluginTest.java b/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/plugins/RestApiPluginTest.java
index 90bd6b5e05..a17c8e41d1 100644
--- a/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/plugins/RestApiPluginTest.java
+++ b/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/plugins/RestApiPluginTest.java
@@ -2,6 +2,7 @@ package com.external.plugins;
import com.appsmith.external.datatypes.ClientDataType;
import com.appsmith.external.dtos.ExecuteActionDTO;
+import com.appsmith.external.dtos.ParamProperty;
import com.appsmith.external.helpers.PluginUtils;
import com.appsmith.external.helpers.restApiUtils.connections.APIConnection;
import com.appsmith.external.helpers.restApiUtils.helpers.HintMessageUtils;
@@ -600,7 +601,9 @@ public class RestApiPluginTest {
param.setPseudoBindingName("k0");
executeActionDTO.setParams(Collections.singletonList(param));
- executeActionDTO.setParamProperties(Collections.singletonMap("k0", "string"));
+ ParamProperty paramProperty = new ParamProperty();
+ paramProperty.setDatatype("string");
+ executeActionDTO.setParamProperties(Collections.singletonMap("k0", paramProperty));
executeActionDTO.setParameterMap(Collections.singletonMap("Input1.text", "k0"));
executeActionDTO.setInvertParameterMap(Collections.singletonMap("k0", "Input1.text"));
diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/RedisConfig.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/RedisConfig.java
index f06692356f..a7efdc2a2d 100644
--- a/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/RedisConfig.java
+++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/configurations/RedisConfig.java
@@ -66,13 +66,12 @@ public class RedisConfig {
// Lifted from below and turned it into a bean. Wish Spring provided it as a bean.
// RedisWebSessionConfiguration.createReactiveRedisTemplate
-
@Bean
- ReactiveRedisTemplate reactiveRedisTemplate(ReactiveRedisConnectionFactory factory) {
+ ReactiveRedisTemplate reactiveRedisTemplate(ReactiveRedisConnectionFactory factory,
+ RedisSerializer