diff --git a/app/rts/package.json b/app/rts/package.json index 7b40036f29..de60f67548 100644 --- a/app/rts/package.json +++ b/app/rts/package.json @@ -36,7 +36,6 @@ "dependencies": { "express-validator": "^6.14.2", "http-status-codes": "^2.2.0", - "morgan": "^1.10.0", "supertest": "^6.2.4", "tsc-alias": "^1.7.0" } diff --git a/app/rts/src/server.ts b/app/rts/src/server.ts index c85c12d9e8..d87cdd8168 100644 --- a/app/rts/src/server.ts +++ b/app/rts/src/server.ts @@ -1,7 +1,6 @@ import http from "http"; import path from "path"; import express from "express"; -import morgan from "morgan"; import { Server } from "socket.io"; import log, { LogLevelDesc } from "loglevel"; import { VERSION as buildVersion } from "./version"; // release version of the api @@ -49,8 +48,6 @@ const io = new Server(server, { // Initializing Sockets initializeSockets(io); -//Track perf metrics for each call -app.use(morgan('tiny')); // parse incoming json requests app.use(express.json({ limit: "5mb" })); // Initializing Routes diff --git a/app/rts/src/test/server.test.ts b/app/rts/src/test/server.test.ts index 473a06a567..608ac29a47 100644 --- a/app/rts/src/test/server.test.ts +++ b/app/rts/src/test/server.test.ts @@ -50,6 +50,44 @@ const entityRefactor = [ isJSObject: true, evalVersion: 2, }, + { + script: + "// ApiNever \n function ApiNever(abc) {let foo = \"I'm getting data from ApiNever but don't rename this string\" + ApiNever.data; \n if(true) { return ApiNever }}", + oldName: "ApiNever.data", + newName: "ApiNever.input", + isJSObject: false, + evalVersion: 2, + }, + { + script: + "// ApiNever \n function ApiNever(abc) {let foo = \"I'm getting data from ApiNever but don't rename this string\" + ApiNever.data; \n if(true) { return ApiNever }}", + oldName: "ApiNever.dat", + newName: "ApiNever.input", + isJSObject: false, + evalVersion: 2, + }, + { + script: "\tApiNever.data", + oldName: "ApiNever", + newName: "ApiForever", + isJSObject: false, + evalVersion: 2, + }, + { + script: "ApiNever.data + ApiNever.data", + oldName: "ApiNever", + newName: "ApiForever", + isJSObject: false, + evalVersion: 2, + }, + { + script: + 'export default {\n\tmyVar1: [],\n\tmyVar2: {},\n\tmyFun1: () => {\n\t\t//write code here\n\t\t// ApiNever.text\n\t\treturn "ApiNever.text" + ApiNever.text\n\t},\n\tmyFun2: async () => {\n\t\t//use async-await or promises\n\t\t// ApiNever.text\n\t\treturn "ApiNever.text" + ApiNever.text\n\t}\n}', + oldName: "ApiNever", + newName: "ApiForever", + isJSObject: true, + evalVersion: 2, + }, ]; afterAll((done) => { @@ -124,6 +162,29 @@ describe("AST tests", () => { "export default {\n\tmyVar1: [],\n\tmyVar2: {},\n\t\tsearch: () => {\n\t\tif(Input1.text.length==0){\n\t\t\treturn select_repair_db.data\n\t\t}\n\t\telse{\n\t\t\treturn(select_repair_db.data.filter(word => word.cust_name.toLowerCase().includes(Input1.text.toLowerCase())))\n\t\t}\n\t},\n}", refactorCount: 2, }, + { + script: + "// ApiNever \n function ApiNever(abc) {let foo = \"I'm getting data from ApiNever but don't rename this string\" + ApiNever.input; \n if(true) { return ApiNever }}", + refactorCount: 1, + }, + { + script: + "// ApiNever \n function ApiNever(abc) {let foo = \"I'm getting data from ApiNever but don't rename this string\" + ApiNever.data; \n if(true) { return ApiNever }}", + refactorCount: 0, + }, + { + script: "\tApiForever.data", + refactorCount: 1, + }, + { + script: "ApiForever.data + ApiForever.data", + refactorCount: 2, + }, + { + script: + 'export default {\n\tmyVar1: [],\n\tmyVar2: {},\n\tmyFun1: () => {\n\t\t//write code here\n\t\t// ApiNever.text\n\t\treturn "ApiNever.text" + ApiForever.text\n\t},\n\tmyFun2: async () => {\n\t\t//use async-await or promises\n\t\t// ApiNever.text\n\t\treturn "ApiNever.text" + ApiForever.text\n\t}\n}', + refactorCount: 2, + }, ]; await supertest(app) diff --git a/app/rts/yarn.lock b/app/rts/yarn.lock index 9ac269a37c..6869d1b6c6 100644 --- a/app/rts/yarn.lock +++ b/app/rts/yarn.lock @@ -951,13 +951,6 @@ base64id@2.0.0, base64id@~2.0.0: resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== -basic-auth@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" - integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== - dependencies: - safe-buffer "5.1.2" - binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -1283,7 +1276,7 @@ denque@^1.4.1: resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de" integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ== -depd@2.0.0, depd@~2.0.0: +depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -2475,17 +2468,6 @@ mongodb@^3.6.4: optionalDependencies: saslprep "^1.0.0" -morgan@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" - integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== - dependencies: - basic-auth "~2.0.1" - debug "2.6.9" - depd "~2.0.0" - on-finished "~2.3.0" - on-headers "~1.0.2" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -2560,18 +2542,6 @@ on-finished@2.4.1: dependencies: ee-first "1.1.1" -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - once@1.4.0, once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -2857,16 +2827,16 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - safe-buffer@5.2.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/AnalyticsEvents.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/AnalyticsEvents.java index 513e015219..98cb52bb66 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/AnalyticsEvents.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/AnalyticsEvents.java @@ -60,6 +60,13 @@ public enum AnalyticsEvents { // Events to log execution time GIT_SERIALIZE_APP_RESOURCES_TO_LOCAL_FILE, GIT_DESERIALIZE_APP_RESOURCES_FROM_FILE, + + // Entity refactor related events + REFACTOR_JSOBJECT, + REFACTOR_ACTION, + REFACTOR_JSACTION, + REFACTOR_WIDGET, + INVITE_USERS_TO_USER_GROUPS, REMOVE_USERS_FROM_USER_GROUPS, ASSIGNED_TO_PERMISSION_GROUP, diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/MustacheHelper.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/MustacheHelper.java index c0be714e4b..2e1a2cffd4 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/MustacheHelper.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/helpers/MustacheHelper.java @@ -3,6 +3,7 @@ package com.appsmith.external.helpers; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.EntityDependencyNode; import com.appsmith.external.models.EntityReferenceType; +import com.appsmith.external.models.MustacheBindingToken; import lombok.extern.slf4j.Slf4j; import org.apache.commons.text.StringEscapeUtils; import org.springframework.beans.BeanWrapper; @@ -23,7 +24,6 @@ import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; -import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -79,12 +79,12 @@ public class MustacheHelper { * should give the original template back. The tokens are split such that alternative strings in the list are plain * text and the others are mustache interpolations. */ - public static List tokenize(String template) { + public static List tokenize(String template) { if (!StringUtils.hasLength(template)) { return Collections.emptyList(); } - List tokens = new ArrayList<>(); + List tokens = new ArrayList<>(); int length = template.length(); @@ -100,6 +100,7 @@ public class MustacheHelper { int braceDepth = 0; StringBuilder currentToken = new StringBuilder().append(template.charAt(0)); + int currentTokenStartIndex = 0; // The parser is implemented as a pointer (marked by `i`) that loops over each character in the template string. // There's majorly two states for the parser, plain-text-mode and mustache-mode, with the current state @@ -118,8 +119,9 @@ public class MustacheHelper { isInsideMustache = true; // Remove the `{` added to the builder. currentToken.deleteCharAt(currentToken.length() - 1); - clearAndPushToken(currentToken, tokens); + clearAndPushToken(currentToken, currentTokenStartIndex, tokens, false); currentToken.append(prevChar); + currentTokenStartIndex = i - 1; braceDepth = 2; } @@ -157,13 +159,12 @@ public class MustacheHelper { --braceDepth; currentToken.append(currentChar); if (prevChar == '}' && braceDepth <= 0) { - clearAndPushToken(currentToken, tokens); + clearAndPushToken(currentToken, currentTokenStartIndex, tokens, true); isInsideMustache = false; } } else { currentToken.append(currentChar); - } } @@ -171,7 +172,7 @@ public class MustacheHelper { } if (currentToken.length() > 0) { - tokens.add(currentToken.toString()); + tokens.add(new MustacheBindingToken(currentToken.toString(), currentTokenStartIndex, false)); } return tokens; @@ -185,15 +186,15 @@ public class MustacheHelper { * @return A Set of strings that serve as replacement keys, with the surrounding double braces stripped and then * trimmed. */ - public static Set extractMustacheKeys(String template) { - Set keys = new HashSet<>(); + public static Set extractMustacheKeys(String template) { + Set keys = new HashSet<>(); - for (String token : tokenize(template)) { - if (token.startsWith("{{") && token.endsWith("}}")) { + for (MustacheBindingToken token : tokenize(template)) { + if (token.getValue().startsWith("{{") && token.getValue().endsWith("}}")) { // Allowing empty tokens to be added, to be compatible with the previous `extractMustacheKeys` method. // Calling `.trim()` before adding because Mustache compiler strips keys in the template before looking // up a value. Addresses https://www.notion.so/appsmith/Bindings-with-a-space-at-the-start-fail-to-execute-properly-in-the-API-pane-2eb65d5c6064466b9ef059fa01ef3261 - keys.add(token.substring(2, token.length() - 2).trim()); + keys.add(new MustacheBindingToken(token.getValue().substring(2, token.getValue().length() - 2), (token.getStartIndex() + 2), false)); } } @@ -201,23 +202,23 @@ public class MustacheHelper { } // For prepared statements we should extract the bindings in order in a list and include duplicate bindings as well. - public static List extractMustacheKeysInOrder(String template) { - List keys = new ArrayList<>(); + public static List extractMustacheKeysInOrder(String template) { + List keys = new ArrayList<>(); - for (String token : tokenize(template)) { - if (token.startsWith("{{") && token.endsWith("}}")) { + for (MustacheBindingToken token : tokenize(template)) { + if (token.getValue().startsWith("{{") && token.getValue().endsWith("}}")) { // Allowing empty tokens to be added, to be compatible with the previous `extractMustacheKeys` method. // Calling `.trim()` before adding because Mustache compiler strips keys in the template before looking // up a value. Addresses https://www.notion.so/appsmith/Bindings-with-a-space-at-the-start-fail-to-execute-properly-in-the-API-pane-2eb65d5c6064466b9ef059fa01ef3261 - keys.add(token.substring(2, token.length() - 2).trim()); + keys.add(new MustacheBindingToken(token.getValue().substring(2, token.getValue().length() - 2).trim(), (token.getStartIndex() + 2), false)); } } return keys; } - public static Set extractMustacheKeysFromFields(Object object) { - final Set keys = new HashSet<>(); + public static Set extractMustacheKeysFromFields(Object object) { + final Set keys = new HashSet<>(); // Linearized recursive search. Instead of calling this function recursively for nested values, we add them to // the end of the queue and process them in a linear fashion. This strategy doesn't suffer from a stack overflow @@ -252,9 +253,9 @@ public class MustacheHelper { return keys; } - private static void clearAndPushToken(StringBuilder tokenBuilder, List tokenList) { + private static void clearAndPushToken(StringBuilder tokenBuilder, int tokenStartIndex, List tokenList, boolean includesHandleBars) { if (tokenBuilder.length() > 0) { - tokenList.add(tokenBuilder.toString()); + tokenList.add(new MustacheBindingToken(tokenBuilder.toString(), tokenStartIndex, includesHandleBars)); tokenBuilder.setLength(0); } } @@ -322,11 +323,11 @@ public class MustacheHelper { public static String render(String template, Map keyValueMap) { final StringBuilder rendered = new StringBuilder(); - for (String token : tokenize(template)) { - if (token.startsWith("{{") && token.endsWith("}}")) { - rendered.append(keyValueMap.get(token.substring(2, token.length() - 2).trim())); + for (MustacheBindingToken token : tokenize(template)) { + if (token.getValue().startsWith("{{") && token.getValue().endsWith("}}")) { + rendered.append(keyValueMap.get(token.getValue().substring(2, token.getValue().length() - 2).trim())); } else { - rendered.append(token); + rendered.append(token.getValue()); } } @@ -500,26 +501,30 @@ public class MustacheHelper { return bindingNames; } - public static String replaceMustacheWithPlaceholder(String query, List mustacheBindings) { + public static String replaceMustacheWithPlaceholder(String query, List mustacheBindings) { return replaceMustacheUsingPatterns(query, APPSMITH_SUBSTITUTION_PLACEHOLDER, mustacheBindings, placeholderTrimmingPattern, APPSMITH_SUBSTITUTION_PLACEHOLDER); } - public static String replaceMustacheWithQuestionMark(String query, List mustacheBindings) { + public static String replaceMustacheWithQuestionMark(String query, List mustacheBindings) { return replaceMustacheUsingPatterns(query, "?", mustacheBindings, quoteQuestionPattern, postQuoteTrimmingQuestionMark); } private static String replaceMustacheUsingPatterns(String query, String placeholder, - List mustacheBindings, + List mustacheBindings, Pattern sanitizePattern, String replacement) { ActionConfiguration actionConfiguration = new ActionConfiguration(); actionConfiguration.setBody(query); - Set mustacheSet = new HashSet<>(mustacheBindings); + Set mustacheSet = new HashSet<>(mustacheBindings); - Map replaceParamsMap = mustacheSet.stream().collect(Collectors.toMap(Function.identity(), v -> placeholder)); + Map replaceParamsMap = mustacheSet + .stream() + .map(mustacheToken -> mustacheToken.getValue()) + .distinct() + .collect(Collectors.toMap(k -> k, v -> placeholder)); // Replace the mustaches with the values mapped to each mustache in replaceParamsMap ActionConfiguration updatedActionConfiguration = renderFieldValues(actionConfiguration, replaceParamsMap); diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/MustacheBindingToken.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/MustacheBindingToken.java new file mode 100644 index 0000000000..d73e37b9df --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/MustacheBindingToken.java @@ -0,0 +1,22 @@ +package com.appsmith.external.models; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +@ToString +public class MustacheBindingToken { + + String value; + int startIndex; + // A token can be with or without handlebars in the value. This boolean value represents the state of the current token. + boolean includesHandleBars = false; +} diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/SmartSubstitutionInterface.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/SmartSubstitutionInterface.java index 90d63dd829..27b6a55265 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/SmartSubstitutionInterface.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/plugins/SmartSubstitutionInterface.java @@ -3,6 +3,7 @@ package com.appsmith.external.plugins; import com.appsmith.external.constants.DataType; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.external.models.Param; import java.util.Arrays; @@ -15,6 +16,7 @@ public interface SmartSubstitutionInterface { /** * !Warning! - This function changes the values of arraylist insertedParams which can then be returned * back to the caller with all the values that were finally put during substitution + * * @param input * @param mustacheValuesInOrder * @param evaluatedParams @@ -24,7 +26,7 @@ public interface SmartSubstitutionInterface { * @throws AppsmithPluginException */ default Object smartSubstitutionOfBindings(Object input, - List mustacheValuesInOrder, + List mustacheValuesInOrder, List evaluatedParams, List> insertedParams, Object... args) throws AppsmithPluginException { @@ -32,7 +34,7 @@ public interface SmartSubstitutionInterface { if (mustacheValuesInOrder != null && !mustacheValuesInOrder.isEmpty()) { for (int i = 0; i < mustacheValuesInOrder.size(); i++) { - String key = mustacheValuesInOrder.get(i); + String key = mustacheValuesInOrder.get(i).getValue(); Optional matchingParam = evaluatedParams.stream().filter(param -> param.getKey().trim().equals(key)).findFirst(); // If the evaluated value of the mustache binding is present, set it in the prepared statement @@ -75,7 +77,7 @@ public interface SmartSubstitutionInterface { static T[] append(T[] arr, T lastElement) { final int N = arr.length; - arr = Arrays.copyOf(arr, N+1); + arr = Arrays.copyOf(arr, N + 1); arr[N] = lastElement; return arr; } diff --git a/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/MustacheHelperTest.java b/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/MustacheHelperTest.java index 1f7d43cebd..6a6da44f6a 100644 --- a/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/MustacheHelperTest.java +++ b/app/server/appsmith-interfaces/src/test/java/com/appsmith/external/helpers/MustacheHelperTest.java @@ -4,6 +4,7 @@ import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.Connection; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.Endpoint; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.external.models.Property; import org.assertj.core.api.AbstractCollectionAssert; import org.assertj.core.api.ObjectAssert; @@ -15,6 +16,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import static com.appsmith.external.helpers.MustacheHelper.extractMustacheKeys; import static com.appsmith.external.helpers.MustacheHelper.extractMustacheKeysFromFields; @@ -29,15 +31,15 @@ import static org.assertj.core.api.Assertions.assertThat; ) public class MustacheHelperTest { - private void checkTokens(String template, List expected) { + private void checkTokens(String template, List expected) { assertThat(tokenize(template)).isEqualTo(expected); } - private void checkKeys(String template, Set expected) { + private void checkKeys(String template, Set expected) { assertThat(extractMustacheKeys(template)).isEqualTo(expected); } - private void check(String template, List expectedTokens, Set expectedKeys) { + private void check(String template, List expectedTokens, Set expectedKeys) { if (expectedTokens != null) { checkTokens(template, expectedTokens); } @@ -46,7 +48,7 @@ public class MustacheHelperTest { } } - private AbstractCollectionAssert, String, ObjectAssert> + private AbstractCollectionAssert, MustacheBindingToken, ObjectAssert> assertKeys(Object object) { return assertThat(extractMustacheKeysFromFields(object)); } @@ -58,20 +60,20 @@ public class MustacheHelperTest { @Test public void justSingleMustache() { - checkTokens("{{A}}", Arrays.asList("{{A}}")); - checkKeys("{{A}}", Set.of("A")); - checkKeys("{{A + B / C}}", Set.of("A + B / C")); + checkTokens("{{A}}", Arrays.asList(new MustacheBindingToken("{{A}}", 0, true))); + checkKeys("{{A}}", Set.of(new MustacheBindingToken("A", 2, false))); + checkKeys("{{A + B / C}}", Set.of(new MustacheBindingToken("A + B / C", 2, false))); } @Test public void textAndMustache() { - checkKeys("Hello {{name}}", Set.of("name")); - checkKeys("Hello {{url.hash}}", Set.of("url.hash")); + checkKeys("Hello {{name}}", Set.of(new MustacheBindingToken("name", 8, false))); + checkKeys("Hello {{url.hash}}", Set.of(new MustacheBindingToken("url.hash", 8, false))); } @Test public void mustacheAndText() { - checkKeys("{{name}} is approved!", Set.of("name")); + checkKeys("{{name}} is approved!", Set.of(new MustacheBindingToken("name", 2, false))); } @Test @@ -79,20 +81,20 @@ public class MustacheHelperTest { checkTokens( "Hello {{Customer.Name}}, the status for your order id {{orderId}} is {{status}}", Arrays.asList( - "Hello ", - "{{Customer.Name}}", - ", the status for your order id ", - "{{orderId}}", - " is ", - "{{status}}" + new MustacheBindingToken("Hello ", 0, false), + new MustacheBindingToken("{{Customer.Name}}", 6, true), + new MustacheBindingToken(", the status for your order id ", 6, false), + new MustacheBindingToken("{{orderId}}", 54, true), + new MustacheBindingToken(" is ", 54, false), + new MustacheBindingToken("{{status}}", 69, true) ) ); checkKeys( "Hello {{Customer.Name}}, the status for your order id {{orderId}} is {{status}}", Set.of( - "Customer.Name", - "orderId", - "status" + new MustacheBindingToken("Customer.Name", 8, false), + new MustacheBindingToken("orderId", 56, false), + new MustacheBindingToken("status", 71, false) ) ); } @@ -101,11 +103,11 @@ public class MustacheHelperTest { public void realWorldText2() { checkTokens( "{{data.map(datum => {return {id: datum}})}}", - Arrays.asList("{{data.map(datum => {return {id: datum}})}}") + Arrays.asList(new MustacheBindingToken("{{data.map(datum => {return {id: datum}})}}", 0, true)) ); checkKeys( "{{data.map(datum => {return {id: datum}})}}", - Set.of("data.map(datum => {return {id: datum}})") + Set.of(new MustacheBindingToken("data.map(datum => {return {id: datum}})", 2, false)) ); } @@ -113,35 +115,39 @@ public class MustacheHelperTest { public void braceDances1() { check( "{{}}{{}}}", - Arrays.asList("{{}}", "{{}}", "}"), - Set.of("") + Arrays.asList( + new MustacheBindingToken("{{}}", 0, true), + new MustacheBindingToken("{{}}", 4, true), + new MustacheBindingToken("}", 4, false)), + Set.of(new MustacheBindingToken("", 2, false), + new MustacheBindingToken("", 6, false)) ); - check("{{{}}", Arrays.asList("{{{}}"), Set.of("{")); + check("{{{}}", Arrays.asList(new MustacheBindingToken("{{{}}", 0, false)), Set.of(new MustacheBindingToken("{", 2, false))); - check("{{ {{", Arrays.asList("{{ {{"), Set.of()); + check("{{ {{", Arrays.asList(new MustacheBindingToken("{{ {{", 0, false)), Set.of()); - check("}} }}", Arrays.asList("}} }}"), Set.of()); + check("}} }}", Arrays.asList(new MustacheBindingToken("}} }}", 0, false)), Set.of()); - check("}} {{", Arrays.asList("}} ", "{{"), Set.of()); + check("}} {{", Arrays.asList(new MustacheBindingToken("}} ", 0, false), new MustacheBindingToken("{{", 3, false)), Set.of()); } @Test public void quotedStrings() { check( "{{ 'abc def'.toUpperCase() }}", - Arrays.asList("{{ 'abc def'.toUpperCase() }}"), - Set.of("'abc def'.toUpperCase()") + Arrays.asList(new MustacheBindingToken("{{ 'abc def'.toUpperCase() }}", 0, true)), + Set.of(new MustacheBindingToken(" 'abc def'.toUpperCase() ", 2, false)) ); check( "{{ \"abc def\".toUpperCase() }}", - Arrays.asList("{{ \"abc def\".toUpperCase() }}"), - Set.of("\"abc def\".toUpperCase()") + Arrays.asList(new MustacheBindingToken("{{ \"abc def\".toUpperCase() }}", 0, true)), + Set.of(new MustacheBindingToken(" \"abc def\".toUpperCase() ", 2, false)) ); check( "{{ `abc def`.toUpperCase() }}", - Arrays.asList("{{ `abc def`.toUpperCase() }}"), - Set.of("`abc def`.toUpperCase()") + Arrays.asList(new MustacheBindingToken("{{ `abc def`.toUpperCase() }}", 0, true)), + Set.of(new MustacheBindingToken(" `abc def`.toUpperCase() ", 2, false)) ); } @@ -149,38 +155,38 @@ public class MustacheHelperTest { public void singleQuotedStringsWithBraces() { check( "{{ 'The { char is a brace' }}", - Arrays.asList("{{ 'The { char is a brace' }}"), - Set.of("'The { char is a brace'") + Arrays.asList(new MustacheBindingToken("{{ 'The { char is a brace' }}", 0, true)), + Set.of(new MustacheBindingToken(" 'The { char is a brace' ", 2, false)) ); check( "{{ 'I have {{ two braces' }}", - Arrays.asList("{{ 'I have {{ two braces' }}"), - Set.of("'I have {{ two braces'") + Arrays.asList(new MustacheBindingToken("{{ 'I have {{ two braces' }}", 0, true)), + Set.of(new MustacheBindingToken(" 'I have {{ two braces' ", 2, false)) ); check( "{{ 'I have {{{ three braces' }}", - Arrays.asList("{{ 'I have {{{ three braces' }}"), - Set.of("'I have {{{ three braces'") + Arrays.asList(new MustacheBindingToken("{{ 'I have {{{ three braces' }}", 0, true)), + Set.of(new MustacheBindingToken(" 'I have {{{ three braces' ", 2, false)) ); check( "{{ 'The } char is a brace' }}", - Arrays.asList("{{ 'The } char is a brace' }}"), - Set.of("'The } char is a brace'") + Arrays.asList(new MustacheBindingToken("{{ 'The } char is a brace' }}", 0, true)), + Set.of(new MustacheBindingToken(" 'The } char is a brace' ", 2, false)) ); check( "{{ 'I have }} two braces' }}", - Arrays.asList("{{ 'I have }} two braces' }}"), - Set.of("'I have }} two braces'") + Arrays.asList(new MustacheBindingToken("{{ 'I have }} two braces' }}", 0, true)), + Set.of(new MustacheBindingToken(" 'I have }} two braces' ", 2, false)) ); check( "{{ 'I have }}} three braces' }}", - Arrays.asList("{{ 'I have }}} three braces' }}"), - Set.of("'I have }}} three braces'") + Arrays.asList(new MustacheBindingToken("{{ 'I have }}} three braces' }}", 0, true)), + Set.of(new MustacheBindingToken(" 'I have }}} three braces' ", 2, false)) ); check( "{{ 'Interpolation uses {{ and }} delimiters' }}", - Arrays.asList("{{ 'Interpolation uses {{ and }} delimiters' }}"), - Set.of("'Interpolation uses {{ and }} delimiters'") + Arrays.asList(new MustacheBindingToken("{{ 'Interpolation uses {{ and }} delimiters' }}", 0, true)), + Set.of(new MustacheBindingToken(" 'Interpolation uses {{ and }} delimiters' ", 2, false)) ); } @@ -188,38 +194,38 @@ public class MustacheHelperTest { public void doubleQuotedStringsWithBraces() { check( "{{ \"The { char is a brace\" }}", - Arrays.asList("{{ \"The { char is a brace\" }}"), - Set.of("\"The { char is a brace\"") + Arrays.asList(new MustacheBindingToken("{{ \"The { char is a brace\" }}", 0, true)), + Set.of(new MustacheBindingToken(" \"The { char is a brace\" ", 2, false)) ); check( "{{ \"I have {{ two braces\" }}", - Arrays.asList("{{ \"I have {{ two braces\" }}"), - Set.of("\"I have {{ two braces\"") + Arrays.asList(new MustacheBindingToken("{{ \"I have {{ two braces\" }}", 0, true)), + Set.of(new MustacheBindingToken(" \"I have {{ two braces\" ", 2, false)) ); check( "{{ \"I have {{{ three braces\" }}", - Arrays.asList("{{ \"I have {{{ three braces\" }}"), - Set.of("\"I have {{{ three braces\"") + Arrays.asList(new MustacheBindingToken("{{ \"I have {{{ three braces\" }}", 0, true)), + Set.of(new MustacheBindingToken(" \"I have {{{ three braces\" ", 2, false)) ); check( "{{ \"The } char is a brace\" }}", - Arrays.asList("{{ \"The } char is a brace\" }}"), - Set.of("\"The } char is a brace\"") + Arrays.asList(new MustacheBindingToken("{{ \"The } char is a brace\" }}", 0, true)), + Set.of(new MustacheBindingToken(" \"The } char is a brace\" ", 2, false)) ); check( "{{ \"I have }} two braces\" }}", - Arrays.asList("{{ \"I have }} two braces\" }}"), - Set.of("\"I have }} two braces\"") + Arrays.asList(new MustacheBindingToken("{{ \"I have }} two braces\" }}", 0, true)), + Set.of(new MustacheBindingToken(" \"I have }} two braces\" ", 2, false)) ); check( "{{ \"I have }}} three braces\" }}", - Arrays.asList("{{ \"I have }}} three braces\" }}"), - Set.of("\"I have }}} three braces\"") + Arrays.asList(new MustacheBindingToken("{{ \"I have }}} three braces\" }}", 0, true)), + Set.of(new MustacheBindingToken(" \"I have }}} three braces\" ", 2, false)) ); check( "{{ \"Interpolation uses {{ and }} delimiters\" }}", - Arrays.asList("{{ \"Interpolation uses {{ and }} delimiters\" }}"), - Set.of("\"Interpolation uses {{ and }} delimiters\"") + Arrays.asList(new MustacheBindingToken("{{ \"Interpolation uses {{ and }} delimiters\" }}", 0, true)), + Set.of(new MustacheBindingToken(" \"Interpolation uses {{ and }} delimiters\" ", 2, false)) ); } @@ -227,38 +233,38 @@ public class MustacheHelperTest { public void backQuotedStringsWithBraces() { check( "{{ `The { char is a brace` }}", - Arrays.asList("{{ `The { char is a brace` }}"), - Set.of("`The { char is a brace`") + Arrays.asList(new MustacheBindingToken("{{ `The { char is a brace` }}", 0, true)), + Set.of(new MustacheBindingToken(" `The { char is a brace` ", 2, false)) ); check( "{{ `I have {{ two braces` }}", - Arrays.asList("{{ `I have {{ two braces` }}"), - Set.of("`I have {{ two braces`") + Arrays.asList(new MustacheBindingToken("{{ `I have {{ two braces` }}", 0, true)), + Set.of(new MustacheBindingToken(" `I have {{ two braces` ", 2, false)) ); check( "{{ `I have {{{ three braces` }}", - Arrays.asList("{{ `I have {{{ three braces` }}"), - Set.of("`I have {{{ three braces`") + Arrays.asList(new MustacheBindingToken("{{ `I have {{{ three braces` }}", 0, true)), + Set.of(new MustacheBindingToken(" `I have {{{ three braces` ", 2, false)) ); check( "{{ `The } char is a brace` }}", - Arrays.asList("{{ `The } char is a brace` }}"), - Set.of("`The } char is a brace`") + Arrays.asList(new MustacheBindingToken("{{ `The } char is a brace` }}", 0, true)), + Set.of(new MustacheBindingToken(" `The } char is a brace` ", 2, false)) ); check( "{{ `I have }} two braces` }}", - Arrays.asList("{{ `I have }} two braces` }}"), - Set.of("`I have }} two braces`") + Arrays.asList(new MustacheBindingToken("{{ `I have }} two braces` }}", 0, true)), + Set.of(new MustacheBindingToken(" `I have }} two braces` ", 2, false)) ); check( "{{ `I have }}} three braces` }}", - Arrays.asList("{{ `I have }}} three braces` }}"), - Set.of("`I have }}} three braces`") + Arrays.asList(new MustacheBindingToken("{{ `I have }}} three braces` }}", 0, true)), + Set.of(new MustacheBindingToken(" `I have }}} three braces` ", 2, false)) ); check( "{{ `Interpolation uses {{ and }} delimiters` }}", - Arrays.asList("{{ `Interpolation uses {{ and }} delimiters` }}"), - Set.of("`Interpolation uses {{ and }} delimiters`") + Arrays.asList(new MustacheBindingToken("{{ `Interpolation uses {{ and }} delimiters` }}", 0, true)), + Set.of(new MustacheBindingToken(" `Interpolation uses {{ and }} delimiters` ", 2, false)) ); } @@ -266,18 +272,18 @@ public class MustacheHelperTest { public void quotedStringsWithExtras() { check( "{{ 2 + ' hello ' + 3 }}", - Arrays.asList("{{ 2 + ' hello ' + 3 }}"), - Set.of("2 + ' hello ' + 3") + Arrays.asList(new MustacheBindingToken("{{ 2 + ' hello ' + 3 }}", 0, true)), + Set.of(new MustacheBindingToken(" 2 + ' hello ' + 3 ", 2, false)) ); check( "{{ 2 + \" hello \" + 3 }}", - Arrays.asList("{{ 2 + \" hello \" + 3 }}"), - Set.of("2 + \" hello \" + 3") + Arrays.asList(new MustacheBindingToken("{{ 2 + \" hello \" + 3 }}", 0, true)), + Set.of(new MustacheBindingToken(" 2 + \" hello \" + 3 ", 2, false)) ); check( "{{ 2 + ` hello ` + 3 }}", - Arrays.asList("{{ 2 + ` hello ` + 3 }}"), - Set.of("2 + ` hello ` + 3") + Arrays.asList(new MustacheBindingToken("{{ 2 + ` hello ` + 3 }}", 0, true)), + Set.of(new MustacheBindingToken(" 2 + ` hello ` + 3 ", 2, false)) ); } @@ -285,18 +291,18 @@ public class MustacheHelperTest { public void quotedStringsWithEscapes() { check( "{{ 'Escaped \\' character' }}", - Arrays.asList("{{ 'Escaped \\' character' }}"), - Set.of("'Escaped \\' character'") + Arrays.asList(new MustacheBindingToken("{{ 'Escaped \\' character' }}", 0, true)), + Set.of(new MustacheBindingToken(" 'Escaped \\' character' ", 2, false)) ); check( "{{ \"Escaped \\\" character\" }}", - Arrays.asList("{{ \"Escaped \\\" character\" }}"), - Set.of("\"Escaped \\\" character\"") + Arrays.asList(new MustacheBindingToken("{{ \"Escaped \\\" character\" }}", 0, true)), + Set.of(new MustacheBindingToken(" \"Escaped \\\" character\" ", 2, false)) ); check( "{{ `Escaped \\` character` }}", - Arrays.asList("{{ `Escaped \\` character` }}"), - Set.of("`Escaped \\` character`") + Arrays.asList(new MustacheBindingToken("{{ `Escaped \\` character` }}", 0, true)), + Set.of(new MustacheBindingToken(" `Escaped \\` character` ", 2, false)) ); } @@ -304,8 +310,10 @@ public class MustacheHelperTest { public void conditionalExpression() { check( "Conditional: {{ 2 + 4 ? trueVal : falseVal }}", - Arrays.asList("Conditional: ", "{{ 2 + 4 ? trueVal : falseVal }}"), - Set.of("2 + 4 ? trueVal : falseVal") + Arrays.asList( + new MustacheBindingToken("Conditional: ", 0, false), + new MustacheBindingToken("{{ 2 + 4 ? trueVal : falseVal }}", 13, true)), + Set.of(new MustacheBindingToken(" 2 + 4 ? trueVal : falseVal ", 15, false)) ); } @@ -313,8 +321,8 @@ public class MustacheHelperTest { public void jsonInMustache() { check( "{{{\"foo\": \"bar\"}}}", - Arrays.asList("{{{\"foo\": \"bar\"}}}"), - Set.of("{\"foo\": \"bar\"}") + Arrays.asList(new MustacheBindingToken("{{{\"foo\": \"bar\"}}}", 0, true)), + Set.of(new MustacheBindingToken("{\"foo\": \"bar\"}", 2, false)) ); } @@ -339,11 +347,24 @@ public class MustacheHelperTest { )); configuration.setProperties(Arrays.asList( - new Property("name1", "Hello {{ propertyValue1 }}!"), - new Property("name2", "Hello {{ propertyValue2 }}!") + new Property("name1", "{{ propertyValue1 }}!"), + new Property("name2", "{{ propertyValue2 }}!") )); Map context = Map.of( + " dbName ", "rendered dbName", + " url ", "rendered url", + " headerValue1 ", "rendered headerValue1", + " headerValue2 ", "rendered headerValue2", + " host1 ", "rendered host1", + " host2 ", "rendered host2", + " propertyValue1 ", "rendered propertyValue1", + " propertyValue2 ", "rendered propertyValue2" + ); + + assertKeys(configuration).hasSameElementsAs(context.keySet().stream().map(keys -> new MustacheBindingToken(keys, 2, false)).collect(Collectors.toSet())); + + Map context2 = Map.of( "dbName", "rendered dbName", "url", "rendered url", "headerValue1", "rendered headerValue1", @@ -354,9 +375,7 @@ public class MustacheHelperTest { "propertyValue2", "rendered propertyValue2" ); - assertKeys(configuration).hasSameElementsAs(context.keySet()); - - renderFieldValues(configuration, context); + renderFieldValues(configuration, context2); assertThat(configuration.getConnection().getDefaultDatabaseName()).isEqualTo("rendered dbName"); assertThat(configuration.getUrl()).isEqualTo("rendered url"); @@ -372,8 +391,8 @@ public class MustacheHelperTest { ); assertThat(configuration.getProperties()).containsOnly( - new Property("name1", "Hello rendered propertyValue1!"), - new Property("name2", "Hello rendered propertyValue2!") + new Property("name1", "rendered propertyValue1!"), + new Property("name2", "rendered propertyValue2!") ); } @@ -408,6 +427,30 @@ public class MustacheHelperTest { )); final Map context = new HashMap<>(Map.of( + " body ", "rendered body", + " path ", "rendered path", + " next ", "rendered next", + " headerValue2 ", "rendered headerValue2", + " headerValue1 ", "rendered headerValue1", + " bodyParam1 ", "rendered bodyParam1", + " bodyParam2 ", "rendered bodyParam2", + " queryParam1 ", "rendered queryParam1", + " queryParam2 ", "rendered queryParam2" + )); + + context.putAll(Map.of( + " pluginSpecifiedProp1 ", "rendered pluginSpecifiedProp1", + " pluginSpecifiedProp2 ", "rendered pluginSpecifiedProp2" + )); + + assertKeys(configuration) + .hasSameElementsAs(context + .keySet() + .stream() + .map(keys -> new MustacheBindingToken(keys, 2, false)) + .collect(Collectors.toSet())); + + final Map context2 = new HashMap<>(Map.of( "body", "rendered body", "path", "rendered path", "next", "rendered next", @@ -418,15 +461,11 @@ public class MustacheHelperTest { "queryParam1", "rendered queryParam1", "queryParam2", "rendered queryParam2" )); - - context.putAll(Map.of( + context2.putAll(Map.of( "pluginSpecifiedProp1", "rendered pluginSpecifiedProp1", "pluginSpecifiedProp2", "rendered pluginSpecifiedProp2" )); - - assertKeys(configuration).hasSameElementsAs(context.keySet()); - - renderFieldValues(configuration, context); + renderFieldValues(configuration, context2); assertThat(configuration.getBody()).isEqualTo("rendered body"); assertThat(configuration.getPath()).isEqualTo("rendered path"); @@ -462,7 +501,7 @@ public class MustacheHelperTest { property.setKey("name"); property.setValue("Hello {{ \"there\" }}!"); configuration.setProperties(Arrays.asList(property)); - assertKeys(configuration).isEqualTo(Set.of("\"there\"")); + assertKeys(configuration).containsExactlyInAnyOrder(new MustacheBindingToken(" \"there\" ", 8, false)); } @Test @@ -472,7 +511,7 @@ public class MustacheHelperTest { property.setKey("name"); property.setValue("Hello {{ \"th\\\\ere\" }}!"); configuration.setProperties(Arrays.asList(property)); - assertKeys(configuration).isEqualTo(Set.of("\"th\\\\ere\"")); + assertKeys(configuration).containsExactlyInAnyOrder(new MustacheBindingToken(" \"th\\\\ere\" ", 8, false)); } @Test @@ -483,14 +522,14 @@ public class MustacheHelperTest { // The `\n` should be interpreted by Javascript, not Java. So we put an extra `\` before it. property.setValue("Hello {{ \"line 1\" + \"\\n\" + \"line 2\" }}!"); configuration.setProperties(Arrays.asList(property)); - assertKeys(configuration).isEqualTo(Set.of("\"line 1\" + \"\\n\" + \"line 2\"")); + assertKeys(configuration).containsExactlyInAnyOrder(new MustacheBindingToken(" \"line 1\" + \"\\n\" + \"line 2\" ", 8, false)); } @Test public void bodyInMustaches() { ActionConfiguration configuration = new ActionConfiguration(); - configuration.setBody("outside {{ab}} outside"); - assertKeys(configuration).isEqualTo(Set.of("ab")); + configuration.setBody("outside {{ ab }} outside"); + assertKeys(configuration).containsExactlyInAnyOrder(new MustacheBindingToken(" ab ", 10, false)); renderFieldValues(configuration, Map.of("ab", "rendered")); assertThat(configuration.getBody()).isEqualTo("outside rendered outside"); @@ -500,7 +539,7 @@ public class MustacheHelperTest { public void bodyWithNewlineInMustaches() { ActionConfiguration configuration = new ActionConfiguration(); configuration.setBody("outside {{a\nb}} outside"); - assertKeys(configuration).isEqualTo(Set.of("a\nb")); + assertKeys(configuration).isEqualTo(Set.of(new MustacheBindingToken("a\nb", 10, false))); renderFieldValues(configuration, Map.of("a\nb", "{\"more\": \"json\"}")); assertThat(configuration.getBody()).isEqualTo("outside {\"more\": \"json\"} outside"); @@ -510,21 +549,21 @@ public class MustacheHelperTest { public void bodyWithTabInMustaches() { ActionConfiguration configuration = new ActionConfiguration(); configuration.setBody("outside {{a\tb}} outside"); - assertKeys(configuration).isEqualTo(Set.of("a\tb")); + assertKeys(configuration).containsExactlyInAnyOrder(new MustacheBindingToken("a\tb", 10, false)); } @Test public void bodyWithMultilineJavascriptInMustaches() { ActionConfiguration configuration = new ActionConfiguration(); configuration.setBody("outside {{\n\ttrue\n\t\t? \"yes\\n\"\n\t\t: \"no\\n\"\n}} outside"); - assertKeys(configuration).isEqualTo(Set.of("true\n\t\t? \"yes\\n\"\n\t\t: \"no\\n\"")); + assertKeys(configuration).containsExactlyInAnyOrder(new MustacheBindingToken("\n\ttrue\n\t\t? \"yes\\n\"\n\t\t: \"no\\n\"\n", 10, false)); } @Test public void renderBodyWithMultilineJavascriptInMustaches() { ActionConfiguration configuration = new ActionConfiguration(); configuration.setBody("outside {{\n\ttrue\n\t\t \"yes\\n\"\n\t\t \"no\\n\"\n}} outside"); - assertKeys(configuration).isEqualTo(Set.of("true\n\t\t \"yes\\n\"\n\t\t \"no\\n\"")); + assertKeys(configuration).containsExactlyInAnyOrder(new MustacheBindingToken("\n\ttrue\n\t\t \"yes\\n\"\n\t\t \"no\\n\"\n", 10, false)); renderFieldValues(configuration, Map.of("true\n\t\t \"yes\\n\"\n\t\t \"no\\n\"", "{\"more\": \"json\"}")); assertThat(configuration.getBody()).isEqualTo("outside {\"more\": \"json\"} outside"); diff --git a/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/AmazonS3Plugin.java b/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/AmazonS3Plugin.java index 44283f7057..3513ea9043 100644 --- a/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/AmazonS3Plugin.java +++ b/app/server/appsmith-plugins/amazons3Plugin/src/main/java/com/external/plugins/AmazonS3Plugin.java @@ -31,6 +31,7 @@ import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceStructure; import com.appsmith.external.models.DatasourceTestResult; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.external.models.Param; import com.appsmith.external.models.Property; import com.appsmith.external.models.RequestParamDTO; @@ -419,7 +420,7 @@ public class AmazonS3Plugin extends BasePlugin { if (TRUE.equals(smartJsonSubstitution)) { final String body = getDataValueSafelyFromFormData(formData, BODY, STRING_TYPE, ""); // First extract all the bindings in order - List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(body); + List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(body); // Replace all the bindings with a placeholder String updatedValue = MustacheHelper.replaceMustacheWithPlaceholder(body, mustacheKeysInOrder); diff --git a/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java index 4ee4899b4b..86209d674c 100644 --- a/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java +++ b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java @@ -12,6 +12,7 @@ import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceStructure; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.external.models.PaginationField; import com.appsmith.external.models.Param; import com.appsmith.external.models.RequestParamDTO; @@ -147,7 +148,7 @@ public class FirestorePlugin extends BasePlugin { if (query != null) { // First extract all the bindings in order - List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(query); + List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(query); // Replace all the bindings with a ? as expected in a prepared statement. String updatedQuery = MustacheHelper.replaceMustacheWithPlaceholder(query, mustacheKeysInOrder); diff --git a/app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/plugins/GoogleSheetsPlugin.java b/app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/plugins/GoogleSheetsPlugin.java index 848267b8a1..bbb4358e7e 100644 --- a/app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/plugins/GoogleSheetsPlugin.java +++ b/app/server/appsmith-plugins/googleSheetsPlugin/src/main/java/com/external/plugins/GoogleSheetsPlugin.java @@ -8,6 +8,7 @@ import com.appsmith.external.helpers.MustacheHelper; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.external.models.OAuth2; import com.appsmith.external.models.Param; import com.appsmith.external.models.Property; @@ -106,7 +107,7 @@ public class GoogleSheetsPlugin extends BasePlugin { if (property != null) { // First extract all the bindings in order - List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(property); + List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(property); // Replace all the bindings with a placeholder String updatedValue = MustacheHelper.replaceMustacheWithPlaceholder(property, mustacheKeysInOrder); diff --git a/app/server/appsmith-plugins/graphqlPlugin/src/main/java/com/external/plugins/GraphQLPlugin.java b/app/server/appsmith-plugins/graphqlPlugin/src/main/java/com/external/plugins/GraphQLPlugin.java index ba633239cd..8cf34cd832 100644 --- a/app/server/appsmith-plugins/graphqlPlugin/src/main/java/com/external/plugins/GraphQLPlugin.java +++ b/app/server/appsmith-plugins/graphqlPlugin/src/main/java/com/external/plugins/GraphQLPlugin.java @@ -12,6 +12,7 @@ import com.appsmith.external.models.ActionExecutionRequest; import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.ApiContentType; import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.external.models.PaginationType; import com.appsmith.external.models.Param; import com.appsmith.external.models.Property; @@ -89,7 +90,7 @@ public class GraphQLPlugin extends BasePlugin { if (TRUE.equals(smartSubstitution)) { /* Apply smart JSON substitution logic to mustache binding values in query variables */ if (!isBlank(variables)) { - List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(variables); + List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(variables); // Replace all the bindings with a ? as expected in a prepared statement. String updatedVariables = MustacheHelper.replaceMustacheWithPlaceholder(variables, mustacheKeysInOrder); @@ -112,7 +113,7 @@ public class GraphQLPlugin extends BasePlugin { /* Apply smart substitution logic to query body */ String query = actionConfiguration.getBody(); if (!isBlank(query)) { - List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(query); + List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(query); // Replace all the bindings with a ? as expected in a prepared statement. String updatedQuery = MustacheHelper.replaceMustacheWithPlaceholder(query, mustacheKeysInOrder); @@ -238,8 +239,7 @@ public class GraphQLPlugin extends BasePlugin { return Mono.error(e); } } - } - else if (HttpMethod.GET.equals(httpMethod)) { + } else if (HttpMethod.GET.equals(httpMethod)) { /** * When a GraphQL request is sent using GET method, the GraphQL body and variables are sent as part of * query parameters in the URL. @@ -247,8 +247,7 @@ public class GraphQLPlugin extends BasePlugin { */ List additionalQueryParams = getGraphQLQueryParamsForBodyAndVariables(actionConfiguration); uri = uriUtils.addQueryParamsToURI(uri, additionalQueryParams, encodeParamsToggle); - } - else { + } else { /** * Only POST and GET HTTP methods are supported by GraphQL specifications. * Ref: https://graphql.org/learn/serving-over-http/ @@ -288,8 +287,7 @@ public class GraphQLPlugin extends BasePlugin { if (!isInputQueryBody) { String queryVariables = (String) input; return DataTypeStringUtils.jsonSmartReplacementPlaceholderWithValue(queryVariables, value, null, insertedParams, null, param); - } - else { + } else { String queryBody = (String) input; return smartlyReplaceGraphQLQueryBodyPlaceholderWithValue(queryBody, value, insertedParams); } diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java index 17f5c47298..cd80637176 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java @@ -21,6 +21,7 @@ import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceStructure; import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.Endpoint; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.external.models.Param; import com.appsmith.external.models.ParsedDataType; import com.appsmith.external.models.Property; @@ -577,7 +578,7 @@ public class MongoPlugin extends BasePlugin { List> parameters) throws AppsmithPluginException { // First extract all the bindings in order - List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(rawQuery); + List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(rawQuery); // Replace all the bindings with a ? as expected in a prepared statement. String updatedQuery = MustacheHelper.replaceMustacheWithPlaceholder(rawQuery, mustacheKeysInOrder); diff --git a/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java index 36a5bceed1..7556425217 100644 --- a/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java +++ b/app/server/appsmith-plugins/mssqlPlugin/src/main/java/com/external/plugins/MssqlPlugin.java @@ -14,6 +14,7 @@ import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.Endpoint; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.external.models.Param; import com.appsmith.external.models.Property; import com.appsmith.external.models.PsParameterDTO; @@ -156,7 +157,7 @@ public class MssqlPlugin extends BasePlugin { //Prepared Statement // First extract all the bindings in order - List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(query); + List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(query); // Replace all the bindings with a ? as expected in a prepared statement. String updatedQuery = MustacheHelper.replaceMustacheWithQuestionMark(query, mustacheKeysInOrder); actionConfiguration.setBody(updatedQuery); @@ -166,7 +167,7 @@ public class MssqlPlugin extends BasePlugin { public Mono executeCommon(HikariDataSource hikariDSConnection, ActionConfiguration actionConfiguration, Boolean preparedStatement, - List mustacheValuesInOrder, + List mustacheValuesInOrder, ExecuteActionDTO executeActionDTO) { final Map requestData = new HashMap<>(); diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java index ab0e93039d..ac09091a22 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java @@ -1,7 +1,6 @@ package com.external.plugins; import com.appsmith.external.datatypes.AppsmithType; -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; @@ -15,6 +14,7 @@ import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceStructure; import com.appsmith.external.models.Endpoint; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.external.models.Param; import com.appsmith.external.models.Property; import com.appsmith.external.models.PsParameterDTO; @@ -214,7 +214,7 @@ public class MySqlPlugin extends BasePlugin { //This has to be executed as Prepared Statement // First extract all the bindings in order - List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(query); + List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(query); // Replace all the bindings with a ? as expected in a prepared statement. String updatedQuery = MustacheHelper.replaceMustacheWithQuestionMark(query, mustacheKeysInOrder); // Set the query with bindings extracted and replaced with '?' back in config @@ -225,7 +225,7 @@ public class MySqlPlugin extends BasePlugin { public Mono executeCommon(Connection connection, ActionConfiguration actionConfiguration, Boolean preparedStatement, - List mustacheValuesInOrder, + List mustacheValuesInOrder, ExecuteActionDTO executeActionDTO, Map requestData) { @@ -351,7 +351,7 @@ public class MySqlPlugin extends BasePlugin { private Flux createAndExecuteQueryFromConnection(String query, Connection connection, Boolean preparedStatement, - List mustacheValuesInOrder, + List mustacheValuesInOrder, ExecuteActionDTO executeActionDTO, Map requestData, Map psParams) { diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java index 43e2e43ece..0a14f0f6a7 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java @@ -15,6 +15,7 @@ import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceStructure; import com.appsmith.external.models.Endpoint; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.external.models.Param; import com.appsmith.external.models.Property; import com.appsmith.external.models.PsParameterDTO; @@ -236,7 +237,7 @@ public class PostgresPlugin extends BasePlugin { // Prepared Statement // First extract all the bindings in order - List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(query); + List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(query); // Replace all the bindings with a ? as expected in a prepared statement. String updatedQuery = MustacheHelper.replaceMustacheWithQuestionMark(query, mustacheKeysInOrder); List explicitCastDataTypes = extractExplicitCasting(updatedQuery); @@ -249,7 +250,7 @@ public class PostgresPlugin extends BasePlugin { DatasourceConfiguration datasourceConfiguration, ActionConfiguration actionConfiguration, Boolean preparedStatement, - List mustacheValuesInOrder, + List mustacheValuesInOrder, ExecuteActionDTO executeActionDTO, List explicitCastDataTypes) { diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java index 5e6ccd48c9..6059bb79dc 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/plugins/RestApiPlugin.java @@ -11,6 +11,7 @@ import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionRequest; import com.appsmith.external.models.ActionExecutionResult; import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.external.models.PaginationField; import com.appsmith.external.models.PaginationType; import com.appsmith.external.models.Param; @@ -79,7 +80,7 @@ public class RestApiPlugin extends BasePlugin { if (actionConfiguration.getBody() != null) { // First extract all the bindings in order - List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(actionConfiguration.getBody()); + List mustacheKeysInOrder = MustacheHelper.extractMustacheKeysInOrder(actionConfiguration.getBody()); // Replace all the bindings with a ? as expected in a prepared statement. String updatedBody = MustacheHelper.replaceMustacheWithPlaceholder(actionConfiguration.getBody(), mustacheKeysInOrder); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java index d0a0374f00..4751d80c1f 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/exceptions/AppsmithError.java @@ -149,6 +149,7 @@ public enum AppsmithError { UNKNOWN_PLUGIN_REFERENCE(400, 4052, " Unable to find the plugin. Please reach out to Appsmith customer support to resolve this.", AppsmithErrorAction.DEFAULT, null, ErrorType.CONFIGURATION_ERROR, null), ENV_FILE_NOT_FOUND(500, 5019, "Admin Settings is unavailable. Unable to read and write to Environment file.", AppsmithErrorAction.DEFAULT, null, ErrorType.CONFIGURATION_ERROR, null), PUBLIC_APP_NO_PERMISSION_GROUP(500, 5020, "Invalid state. Public application does not have the required roles set for public access. Please reach out to Appsmith customer support to resolve this.", AppsmithErrorAction.LOG_EXTERNALLY, null, ErrorType.INTERNAL_ERROR, null), + RTS_SERVER_ERROR(500, 5021, "RTS server error while processing request: {0}", AppsmithErrorAction.LOG_EXTERNALLY, null, ErrorType.INTERNAL_ERROR, null), ; private final Integer httpErrorCode; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java index f9ff98f59e..c4babda486 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/DslUtils.java @@ -1,12 +1,12 @@ package com.appsmith.server.helpers; import com.appsmith.external.helpers.MustacheHelper; +import com.appsmith.external.models.MustacheBindingToken; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import lombok.AllArgsConstructor; -import org.apache.commons.lang3.StringUtils; import java.util.Arrays; import java.util.HashSet; @@ -17,11 +17,10 @@ import java.util.regex.Pattern; public class DslUtils { - public static Set getMustacheValueSetFromSpecificDynamicBindingPath(JsonNode dsl, String fieldPath) { + public static Set getMustacheValueSetFromSpecificDynamicBindingPath(JsonNode dsl, String fieldPath) { DslNodeWalkResponse dslWalkResponse = getDslWalkResponse(dsl, fieldPath); - // Only extract mustache keys from leaf nodes if (dslWalkResponse != null && dslWalkResponse.isLeafNode) { @@ -31,7 +30,7 @@ public class DslUtils { } // Stricter extraction of dynamic bindings - Set mustacheKeysFromFields = MustacheHelper.extractMustacheKeysFromFields(((TextNode) dslWalkResponse.currentNode).asText()); + Set mustacheKeysFromFields = MustacheHelper.extractMustacheKeysFromFields(((TextNode) dslWalkResponse.currentNode).asText()); return mustacheKeysFromFields; } @@ -39,23 +38,29 @@ public class DslUtils { return new HashSet<>(); } - public static JsonNode replaceValuesInSpecificDynamicBindingPath(JsonNode dsl, String fieldPath, Map replacementMap) { + public static JsonNode replaceValuesInSpecificDynamicBindingPath(JsonNode dsl, String fieldPath, Map replacementMap) { DslNodeWalkResponse dslWalkResponse = getDslWalkResponse(dsl, fieldPath); if (dslWalkResponse != null && dslWalkResponse.isLeafNode) { - final String oldValue = ((TextNode) dslWalkResponse.currentNode).asText(); + final StringBuilder oldValue = new StringBuilder(((TextNode) dslWalkResponse.currentNode).asText()); - final String newValue = StringUtils.replaceEach( - oldValue, - replacementMap.keySet().toArray(new String[0]), - replacementMap.values().toArray(new String[0])); + for (MustacheBindingToken mustacheBindingToken : replacementMap.keySet()) { + String tokenValue = mustacheBindingToken.getValue(); + int endIndex = mustacheBindingToken.getStartIndex() + tokenValue.length(); + if (oldValue.length() >= endIndex && oldValue.subSequence(mustacheBindingToken.getStartIndex(), endIndex).equals(tokenValue)) { + oldValue.replace(mustacheBindingToken.getStartIndex(), endIndex, replacementMap.get(mustacheBindingToken)); + } + } - ((ObjectNode) dslWalkResponse.parentNode).set(dslWalkResponse.currentKey, new TextNode(newValue)); + ((ObjectNode) dslWalkResponse.parentNode).set(dslWalkResponse.currentKey, new TextNode(oldValue.toString())); } return dsl; } private static DslNodeWalkResponse getDslWalkResponse(JsonNode dsl, String fieldPath) { + if (dsl == null) { + return null; + } String[] fields = fieldPath.split("[].\\[]"); // For nested fields, the parent dsl to search in would shift by one level every iteration Object currentNode = dsl; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java index 19d4203eba..b6fcd60f05 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java @@ -233,9 +233,9 @@ public class DatabaseChangelog { /* Add plugin to the workspace */ Update update = new Update(); - update.addToSet("plugins",new WorkspacePlugin(pluginId, WorkspacePluginStatus.FREE)); + update.addToSet("plugins", new WorkspacePlugin(pluginId, WorkspacePluginStatus.FREE)); - mongockTemplate.updateMulti(queryToFetchWorkspacesWOPlugin,update,Workspace.class); + mongockTemplate.updateMulti(queryToFetchWorkspacesWOPlugin, update, Workspace.class); } @ChangeSet(order = "001", id = "initial-plugins", author = "") @@ -755,7 +755,7 @@ public class DatabaseChangelog { } } - // Examples organization is no longer getting used. Commenting out the migrations which add/update the same. + // Examples organization is no longer getting used. Commenting out the migrations which add/update the same. // @SuppressWarnings({"unchecked", "rawtypes"}) // @ChangeSet(order = "022", id = "examples-organization", author = "") // public void examplesOrganization(MongockTemplate mongoTemplate, EncryptionService encryptionService) throws IOException { @@ -1886,7 +1886,7 @@ public class DatabaseChangelog { } // Only extract mustache keys from leaf nodes if (parent != null && isLeafNode) { - Set mustacheKeysFromFields = MustacheHelper.extractMustacheKeysFromFields(parent); + Set mustacheKeysFromFields = MustacheHelper.extractMustacheKeysFromFields(parent).stream().map(token -> token.getValue()).collect(Collectors.toSet()); // We found the path. But if the path does not have any mustache bindings, remove it from the path list if (mustacheKeysFromFields.isEmpty()) { @@ -3481,9 +3481,9 @@ public class DatabaseChangelog { * This method holds the steps to transform data before it is migrated to UQI schema. * Each transformation is uniquely identified by the combination of plugin name and the transformation name. * - * @param pluginName - name of the plugin for which the transformation is intended + * @param pluginName - name of the plugin for which the transformation is intended * @param transformationName - name of the transformation relative to the plugin - * @param value - value that needs to be transformed + * @param value - value that needs to be transformed * @return - transformed value */ public Object transformData(String pluginName, String transformationName, Object value) { @@ -3664,13 +3664,13 @@ public class DatabaseChangelog { * @param dynamicBindingPathList : old dynamicBindingPathList * @param objectMapper * @param action - * @param migrationMap : A mapping from `pluginSpecifiedTemplates` index to attribute path in UQI model. For - * reference, please check out the `s3MigrationMap` defined above. + * @param migrationMap : A mapping from `pluginSpecifiedTemplates` index to attribute path in UQI model. For + * reference, please check out the `s3MigrationMap` defined above. * @return : updated dynamicBindingPathList - ported to UQI model. */ static List getUpdatedDynamicBindingPathList(List dynamicBindingPathList, - ObjectMapper objectMapper, NewAction action, - Map> migrationMap) { + ObjectMapper objectMapper, NewAction action, + Map> migrationMap) { // Return if empty. if (CollectionUtils.isEmpty(dynamicBindingPathList)) { return dynamicBindingPathList; @@ -4101,7 +4101,7 @@ public class DatabaseChangelog { .include(fieldName(QDatasource.datasource.organizationId)); List datasources = mongockTemplate.find(datasourceQuery, Datasource.class); - for(Datasource datasource: datasources) { + for (Datasource datasource : datasources) { final Update update = new Update(); final String gitSyncId = datasource.getOrganizationId() + "_" + new ObjectId(); update.set(fieldName(QDatasource.datasource.gitSyncId), gitSyncId); @@ -4117,7 +4117,7 @@ public class DatabaseChangelog { .addCriteria(where(fieldName(QApplication.application.pages)).exists(true)); List applications = mongockTemplate.find(applicationQuery, Application.class); - for(Application application: applications) { + for (Application application : applications) { application.getPages().forEach(page -> { page.setDefaultPageId(page.getId()); }); @@ -4179,7 +4179,7 @@ public class DatabaseChangelog { defaultResourceUpdates.set(fieldName(QNewPage.newPage.publishedPage) + "." + "layouts", page.getPublishedPage().getLayouts()); } - if (!StringUtils.isEmpty(applicationId) ) { + if (!StringUtils.isEmpty(applicationId)) { mongockTemplate.updateFirst( query(where(fieldName(QNewPage.newPage.id)).is(page.getId())), defaultResourceUpdates, @@ -4622,7 +4622,7 @@ public class DatabaseChangelog { ); // Query to get action id from all Firestore actions - Query queryToGetActionIds =query( + Query queryToGetActionIds = query( where(fieldName(QNewAction.newAction.pluginId)).is(firestorePlugin.getId()) .and(fieldName(QNewAction.newAction.deleted)).ne(true) ); @@ -4647,7 +4647,7 @@ public class DatabaseChangelog { ActionDTO unpublishedAction = firestoreAction.getUnpublishedAction(); // No migrations required if action configuration does not exist. - if (unpublishedAction == null || unpublishedAction.getActionConfiguration() == null ) { + if (unpublishedAction == null || unpublishedAction.getActionConfiguration() == null) { continue; } @@ -4733,6 +4733,7 @@ public class DatabaseChangelog { /** * This method sets the key formData.aggregate.limit to 101 for all Mongo plugin actions. * It iterates over each action id one by one to avoid out of memory error. + * * @param mongoActions * @param mongockTemplate */ @@ -4760,6 +4761,7 @@ public class DatabaseChangelog { /** * Returns true only if the action has non-null published actionConfiguration. + * * @param action * @return true / false */ @@ -4783,6 +4785,7 @@ public class DatabaseChangelog { * Mongo database - this is the same value that would have been applied to the aggregate cmd so far by the * database. However, for any new action, this field's initial value is 10. * Ref: https://docs.mongodb.com/manual/tutorial/iterate-a-cursor/ + * * @param mongockTemplate */ @ChangeSet(order = "109", id = "add-limit-field-data-to-mongo-aggregate-cmd", author = "") @@ -4804,6 +4807,7 @@ public class DatabaseChangelog { /** * Returns true only if the action has non-null un-published actionConfiguration. + * * @param action * @return true / false */ @@ -4818,6 +4822,7 @@ public class DatabaseChangelog { /** * Fetch an action using id. + * * @param actionId * @param mongockTemplate * @return action @@ -4830,6 +4835,7 @@ public class DatabaseChangelog { /** * Generate query to fetch all non-deleted actions defined for a given plugin. + * * @param plugin * @return query */ @@ -4884,7 +4890,7 @@ public class DatabaseChangelog { query.addCriteria(Criteria.where("deleted").is(FALSE)); for (Application application : mongockTemplate.find(query, Application.class)) { - if(!Optional.ofNullable(application.getGitApplicationMetadata()).isEmpty()) { + if (!Optional.ofNullable(application.getGitApplicationMetadata()).isEmpty()) { GitAuth gitAuth = GitDeployKeyGenerator.generateSSHKey(null); GitApplicationMetadata gitApplicationMetadata = application.getGitApplicationMetadata(); gitApplicationMetadata.setGitAuth(gitAuth); @@ -4988,6 +4994,7 @@ public class DatabaseChangelog { /** * Adding this migration again because we've added permission to themes. * Also there are couple of changes in the system theme properties. + * * @param mongockTemplate * @throws IOException */ diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java index 24b9f48924..ed9798ebdd 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java @@ -225,7 +225,8 @@ public class AnalyticsServiceCEImpl implements AnalyticsServiceCE { /** * Generates event name tag to analytic events - * @param event AnalyticsEvents + * + * @param event AnalyticsEvents * @param object Analytic event resource object * @return String */ diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AstServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AstServiceCE.java index bea31c78ca..5044bac702 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AstServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AstServiceCE.java @@ -1,5 +1,6 @@ package com.appsmith.server.services.ce; +import com.appsmith.external.models.MustacheBindingToken; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -23,5 +24,5 @@ public interface AstServiceCE { */ Flux>> getPossibleReferencesFromDynamicBinding(List bindingValues, int evalVersion); - Mono> refactorNameInDynamicBindings(Set bindingValues, String oldName, String newName, int evalVersion); + Mono> refactorNameInDynamicBindings(Set bindingValues, String oldName, String newName, int evalVersion, boolean isJSObject); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AstServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AstServiceCEImpl.java index f7fd4463c1..5b390e92dd 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AstServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AstServiceCEImpl.java @@ -1,8 +1,11 @@ package com.appsmith.server.services.ce; import com.appsmith.external.helpers.MustacheHelper; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.server.configurations.CommonConfig; import com.appsmith.server.configurations.InstanceConfig; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.util.WebClientUtils; import lombok.AllArgsConstructor; import lombok.Getter; @@ -10,6 +13,7 @@ import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; @@ -24,6 +28,7 @@ import java.time.Duration; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -76,7 +81,7 @@ public class AstServiceCEImpl implements AstServiceCE { .retrieve() .bodyToMono(GetIdentifiersResponseBulk.class) .retryWhen(Retry.max(3)) - .flatMapIterable(getIdentifiersResponseDetails -> getIdentifiersResponseDetails.data) + .flatMapIterable(getIdentifiersResponse -> getIdentifiersResponse.data) .index() .flatMap(tuple2 -> { long currentIndex = tuple2.getT1(); @@ -87,20 +92,27 @@ public class AstServiceCEImpl implements AstServiceCE { } @Override - public Mono> refactorNameInDynamicBindings(Set bindingValues, String oldName, String newName, int evalVersion) { + public Mono> refactorNameInDynamicBindings(Set bindingValues, String oldName, String newName, int evalVersion, boolean isJSObject) { if (bindingValues == null || bindingValues.isEmpty()) { return Mono.empty(); } return Flux.fromIterable(bindingValues) .flatMap(bindingValue -> { + EntityRefactorRequest entityRefactorRequest = new EntityRefactorRequest(bindingValue.getValue(), oldName, newName, evalVersion, isJSObject); return webClient .post() .uri(commonConfig.getRtsBaseDomain() + "/rts-api/v1/ast/entity-refactor") .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromValue(new EntityRefactorRequest(bindingValue, oldName, newName, evalVersion))) + .body(BodyInserters.fromValue(entityRefactorRequest)) .retrieve() - .bodyToMono(EntityRefactorResponse.class) + .toEntity(EntityRefactorResponse.class) + .flatMap(entityRefactorResponseResponseEntity -> { + if (HttpStatus.OK.equals(entityRefactorResponseResponseEntity.getStatusCode())) { + return Mono.just(Objects.requireNonNull(entityRefactorResponseResponseEntity.getBody())); + } + return Mono.error(new AppsmithException(AppsmithError.RTS_SERVER_ERROR, entityRefactorResponseResponseEntity.getStatusCodeValue())); + }) .elapsed() .map(tuple -> { log.debug("Time elapsed since AST refactor call: {} ms", tuple.getT1()); @@ -189,6 +201,7 @@ public class AstServiceCEImpl implements AstServiceCE { String oldName; String newName; int evalVersion; + Boolean isJSObject; } @NoArgsConstructor diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/DatasourceServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/DatasourceServiceCE.java index ac32a75bca..4a1bfb3df5 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/DatasourceServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/DatasourceServiceCE.java @@ -3,6 +3,7 @@ package com.appsmith.server.services.ce; import com.appsmith.external.models.ActionDTO; import com.appsmith.external.models.Datasource; import com.appsmith.external.models.DatasourceTestResult; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.services.CrudService; import reactor.core.publisher.Flux; @@ -21,7 +22,7 @@ public interface DatasourceServiceCE extends CrudService { Mono findById(String id); - Set extractKeysFromDatasource(Datasource datasource); + Set extractKeysFromDatasource(Datasource datasource); Mono validateDatasource(Datasource datasource); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/DatasourceServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/DatasourceServiceCEImpl.java index 4eef7c30b5..9dff1cf0c5 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/DatasourceServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/DatasourceServiceCEImpl.java @@ -7,6 +7,7 @@ import com.appsmith.external.models.Datasource; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DatasourceTestResult; import com.appsmith.external.models.Endpoint; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.external.models.Policy; import com.appsmith.external.models.QDatasource; import com.appsmith.external.plugins.PluginExecutor; @@ -400,7 +401,7 @@ public class DatasourceServiceCEImpl extends BaseService extractKeysFromDatasource(Datasource datasource) { + public Set extractKeysFromDatasource(Datasource datasource) { if (datasource == null || datasource.getDatasourceConfiguration() == null) { return new HashSet<>(); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/LayoutActionServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/LayoutActionServiceCEImpl.java index dedb085a35..c13f679c3b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/LayoutActionServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/LayoutActionServiceCEImpl.java @@ -62,6 +62,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; +import java.util.stream.Collectors; import static com.appsmith.server.services.ce.ApplicationPageServiceCEImpl.EVALUATION_VERSION; import static java.lang.Boolean.FALSE; @@ -341,7 +342,7 @@ public class LayoutActionServiceCEImpl implements LayoutActionServiceCE { } // Stricter extraction of dynamic bindings - Set mustacheKeysFromFields = MustacheHelper.extractMustacheKeysFromFields(parent); + Set mustacheKeysFromFields = MustacheHelper.extractMustacheKeysFromFields(parent).stream().map(token -> token.getValue()).collect(Collectors.toSet()); String completePath = widgetName + "." + fieldPath; if (widgetDynamicBindingsMap.containsKey(completePath)) { diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/LayoutCollectionServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/LayoutCollectionServiceCEImpl.java index e6fdf2cc71..df09661eb3 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/LayoutCollectionServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/LayoutCollectionServiceCEImpl.java @@ -1,6 +1,7 @@ package com.appsmith.server.services.ce; import com.appsmith.external.helpers.AppsmithBeanUtils; +import com.appsmith.external.models.ActionDTO; import com.appsmith.external.models.DefaultResources; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.ActionCollection; @@ -9,7 +10,6 @@ import com.appsmith.server.domains.NewAction; import com.appsmith.server.domains.NewPage; import com.appsmith.server.dtos.ActionCollectionDTO; import com.appsmith.server.dtos.ActionCollectionMoveDTO; -import com.appsmith.external.models.ActionDTO; import com.appsmith.server.dtos.LayoutDTO; import com.appsmith.server.dtos.RefactorActionCollectionNameDTO; import com.appsmith.server.dtos.RefactorActionNameDTO; @@ -38,15 +38,13 @@ import reactor.core.publisher.Mono; import java.time.Instant; import java.util.HashMap; import java.util.HashSet; -import java.util.Objects; -import java.util.Map; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import static com.appsmith.external.helpers.AppsmithBeanUtils.copyNewFieldValuesIntoOldObject; -import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS; -import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES; import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toSet; @@ -319,9 +317,14 @@ public class LayoutCollectionServiceCEImpl implements LayoutCollectionServiceCE return actionUpdatesFlux .then(actionCollectionService.update(branchedActionCollection.getId(), branchedActionCollection)) .then(branchedPageIdMono) - .flatMap(branchedPageId -> refactoringSolution.refactorName(branchedPageId, layoutId, oldName, newName)); + .flatMap(branchedPageId -> refactoringSolution.refactorActionCollectionName( + branchedActionCollection.getApplicationId(), + branchedPageId, + layoutId, + oldName, + newName)); }) - .map(responseUtils::updateLayoutDTOWithDefaultResources); + .map(layoutDTO -> responseUtils.updateLayoutDTOWithDefaultResources(layoutDTO)); } @Override diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCE.java index 9a1a45ed62..f90473ed01 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCE.java @@ -2,6 +2,7 @@ package com.appsmith.server.services.ce; import com.appsmith.external.dtos.ExecuteActionDTO; import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.server.acl.AclPermission; import com.appsmith.server.domains.NewAction; import com.appsmith.server.domains.NewPage; @@ -38,7 +39,7 @@ public interface NewActionServiceCE extends CrudService { Mono executeAction(ExecuteActionDTO executeActionDTO); Mono executeAction(Flux partsFlux, String branchName); - + Mono getValidActionForExecution(ExecuteActionDTO executeActionDTO, String actionId, NewAction newAction); T variableSubstitution(T configuration, Map replaceParamsMap); @@ -85,7 +86,7 @@ public interface NewActionServiceCE extends CrudService { Mono> archiveActionsByApplicationId(String applicationId, AclPermission permission); - List extractMustacheKeysInOrder(String query); + List extractMustacheKeysInOrder(String query); String replaceMustacheWithQuestionMark(String query, List mustacheBindings); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCEImpl.java index 408667b207..e0a8a1fa1e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/NewActionServiceCEImpl.java @@ -10,12 +10,16 @@ import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException; import com.appsmith.external.helpers.MustacheHelper; import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionDTO; import com.appsmith.external.models.ActionExecutionRequest; import com.appsmith.external.models.ActionExecutionResult; +import com.appsmith.external.models.ActionProvider; import com.appsmith.external.models.Datasource; import com.appsmith.external.models.DatasourceConfiguration; import com.appsmith.external.models.DefaultResources; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.external.models.Param; +import com.appsmith.external.models.PluginType; import com.appsmith.external.models.Policy; import com.appsmith.external.models.Property; import com.appsmith.external.models.Provider; @@ -25,7 +29,6 @@ import com.appsmith.server.acl.AclPermission; import com.appsmith.server.acl.PolicyGenerator; import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Action; -import com.appsmith.external.models.ActionProvider; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.ApplicationMode; import com.appsmith.server.domains.DatasourceContext; @@ -33,9 +36,7 @@ import com.appsmith.server.domains.NewAction; import com.appsmith.server.domains.NewPage; import com.appsmith.server.domains.Page; import com.appsmith.server.domains.Plugin; -import com.appsmith.external.models.PluginType; import com.appsmith.server.domains.User; -import com.appsmith.external.models.ActionDTO; import com.appsmith.server.dtos.ActionViewDTO; import com.appsmith.server.dtos.LayoutActionUpdateDTO; import com.appsmith.server.exceptions.AppsmithError; @@ -106,11 +107,7 @@ import java.util.stream.Collectors; import static com.appsmith.external.helpers.AppsmithBeanUtils.copyNewFieldValuesIntoOldObject; import static com.appsmith.external.helpers.DataTypeStringUtils.getDisplayDataTypes; import static com.appsmith.external.helpers.PluginUtils.setValueSafelyInFormData; -import static com.appsmith.server.acl.AclPermission.EXECUTE_ACTIONS; import static com.appsmith.server.acl.AclPermission.EXECUTE_DATASOURCES; -import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS; -import static com.appsmith.server.acl.AclPermission.READ_ACTIONS; -import static com.appsmith.server.acl.AclPermission.READ_PAGES; import static com.appsmith.server.helpers.WidgetSuggestionHelper.getSuggestedWidgets; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; @@ -447,7 +444,7 @@ public class NewActionServiceCEImpl extends BaseService extractKeysFromAction(ActionDTO actionDTO) { + private Set extractKeysFromAction(ActionDTO actionDTO) { if (actionDTO == null) { return new HashSet<>(); } @@ -457,11 +454,11 @@ public class NewActionServiceCEImpl extends BaseService(); } - Set keys = MustacheHelper.extractMustacheKeysFromFields(actionConfiguration); + Set keys = MustacheHelper.extractMustacheKeysFromFields(actionConfiguration); // Add JS function body to jsonPathKeys field. if (PluginType.JS.equals(actionDTO.getPluginType()) && actionConfiguration.getBody() != null) { - keys.add(actionConfiguration.getBody()); + keys.add(new MustacheBindingToken(actionConfiguration.getBody(), 0, false)); // Since this is a JS function, we should also set the dynamic binding path list if absent List dynamicBindingPathList = actionDTO.getDynamicBindingPathList(); @@ -485,8 +482,8 @@ public class NewActionServiceCEImpl extends BaseService actionKeys = extractKeysFromAction(action); - Set datasourceKeys = datasourceService.extractKeysFromDatasource(action.getDatasource()); + Set actionKeys = extractKeysFromAction(action).stream().map(token -> token.getValue()).collect(Collectors.toSet()); + Set datasourceKeys = datasourceService.extractKeysFromDatasource(action.getDatasource()).stream().map(token -> token.getValue()).collect(Collectors.toSet()); Set keys = new HashSet<>() {{ addAll(actionKeys); addAll(datasourceKeys); @@ -1054,6 +1051,7 @@ public class NewActionServiceCEImpl extends BaseService(); } - List executionParams = paramsList.stream().map(param -> param.getValue()).collect(Collectors.toList()); + List executionParams = paramsList.stream().map(param -> param.getValue()).collect(Collectors.toList()); data.putAll(Map.of( "request", request, @@ -1913,7 +1911,7 @@ public class NewActionServiceCEImpl extends BaseService extractMustacheKeysInOrder(String query) { + public List extractMustacheKeysInOrder(String query) { return MustacheHelper.extractMustacheKeysInOrder(query); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/RefactoringSolutionImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/RefactoringSolutionImpl.java index 9accd07e81..c6e684dc98 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/RefactoringSolutionImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/RefactoringSolutionImpl.java @@ -1,13 +1,15 @@ -package com.appsmith.server.solutions; + package com.appsmith.server.solutions; import com.appsmith.server.configurations.InstanceConfig; import com.appsmith.server.helpers.ResponseUtils; import com.appsmith.server.services.ActionCollectionService; +import com.appsmith.server.services.AnalyticsService; import com.appsmith.server.services.ApplicationService; import com.appsmith.server.services.AstService; import com.appsmith.server.services.LayoutActionService; import com.appsmith.server.services.NewActionService; import com.appsmith.server.services.NewPageService; +import com.appsmith.server.services.SessionUserService; import com.appsmith.server.solutions.ce.RefactoringSolutionCEImpl; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; @@ -26,6 +28,8 @@ public class RefactoringSolutionImpl extends RefactoringSolutionCEImpl implement ApplicationService applicationService, AstService astService, InstanceConfig instanceConfig, + AnalyticsService analyticsService, + SessionUserService sessionUserService, PagePermission pagePermission, ActionPermission actionPermission) { super(objectMapper, @@ -37,6 +41,8 @@ public class RefactoringSolutionImpl extends RefactoringSolutionCEImpl implement applicationService, astService, instanceConfig, + analyticsService, + sessionUserService, pagePermission, actionPermission); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/PageLoadActionsUtilCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/PageLoadActionsUtilCEImpl.java index 723a9182d2..d7cd479515 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/PageLoadActionsUtilCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/PageLoadActionsUtilCEImpl.java @@ -41,7 +41,6 @@ import java.util.stream.Collectors; import static com.appsmith.external.helpers.MustacheHelper.ACTION_ENTITY_REFERENCES; import static com.appsmith.external.helpers.MustacheHelper.WIDGET_ENTITY_REFERENCES; import static com.appsmith.external.helpers.MustacheHelper.getPossibleParents; -import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; @@ -934,7 +933,7 @@ public class PageLoadActionsUtilCEImpl implements PageLoadActionsUtilCE { Set mustacheKeysFromFields; // Stricter extraction of dynamic bindings if (isBindingPresentInString) { - mustacheKeysFromFields = MustacheHelper.extractMustacheKeysFromFields(parent); + mustacheKeysFromFields = MustacheHelper.extractMustacheKeysFromFields(parent).stream().map(token -> token.getValue()).collect(Collectors.toSet()); } else { // this must be a JS function. No need to extract mustache. The entire string is JS body mustacheKeysFromFields = Set.of((String) parent); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/RefactoringSolutionCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/RefactoringSolutionCE.java index 669bac15ed..a661914f87 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/RefactoringSolutionCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/RefactoringSolutionCE.java @@ -4,6 +4,9 @@ import com.appsmith.server.dtos.LayoutDTO; import com.appsmith.server.dtos.RefactorActionNameDTO; import com.appsmith.server.dtos.RefactorNameDTO; import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; + +import java.util.Set; public interface RefactoringSolutionCE { @@ -15,5 +18,16 @@ public interface RefactoringSolutionCE { Mono refactorActionName(RefactorActionNameDTO refactorActionNameDTO, String branchName); - Mono refactorName(String pageId, String layoutId, String oldName, String newName); + Mono refactorActionCollectionName(String appId, String pageId, String layoutId, String oldName, String newName); + + /** + * This method is responsible for the core logic of refactoring a valid name inside an Appsmith page. + * This includes refactoring inside the DSL, in actions, and JS objects. + * @param pageId The page where the refactor needs to happen + * @param layoutId The layout where the refactor needs to happen + * @param oldName The valid name to convert from. For JS functions, this would be the FQN + * @param newName The new name to convert into. For JS functions, this would be FQN + * @return A tuple of the updated layout after refactoring and a set of all the paths in the page that ended up getting refactored + */ + Mono>> refactorName(String pageId, String layoutId, String oldName, String newName); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/RefactoringSolutionCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/RefactoringSolutionCEImpl.java index 7c9ce2dc7a..e55a6636e1 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/RefactoringSolutionCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/RefactoringSolutionCEImpl.java @@ -1,7 +1,9 @@ package com.appsmith.server.solutions.ce; +import com.appsmith.external.constants.AnalyticsEvents; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionDTO; +import com.appsmith.external.models.MustacheBindingToken; import com.appsmith.external.models.PluginType; import com.appsmith.server.configurations.InstanceConfig; import com.appsmith.server.constants.FieldName; @@ -17,11 +19,13 @@ import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.helpers.DslUtils; import com.appsmith.server.helpers.ResponseUtils; import com.appsmith.server.services.ActionCollectionService; +import com.appsmith.server.services.AnalyticsService; import com.appsmith.server.services.ApplicationService; import com.appsmith.server.services.AstService; import com.appsmith.server.services.LayoutActionService; import com.appsmith.server.services.NewActionService; import com.appsmith.server.services.NewPageService; +import com.appsmith.server.services.SessionUserService; import com.appsmith.server.solutions.ActionPermission; import com.appsmith.server.solutions.PagePermission; import com.fasterxml.jackson.databind.JsonNode; @@ -38,6 +42,7 @@ import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -49,8 +54,6 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.StreamSupport; -import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS; -import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES; import static com.appsmith.server.services.ce.ApplicationPageServiceCEImpl.EVALUATION_VERSION; import static java.util.stream.Collectors.toSet; @@ -67,11 +70,8 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { private final PagePermission pagePermission; private final ActionPermission actionPermission; private final InstanceConfig instanceConfig; - private final Boolean isRtsAccessible; - - private static final Pattern actionCollectionBodyPattern = Pattern.compile("export default(.*)", Pattern.DOTALL); - private static final String EXPORT_DEFAULT_STRING = "export default"; - + private final AnalyticsService analyticsService; + private final SessionUserService sessionUserService; /* * To replace fetchUsers in `{{JSON.stringify(fetchUsers)}}` with getUsers, the following regex is required : * `\\b(fetchUsers)\\b`. To achieve this the following strings preWord and postWord are declared here to be used @@ -89,6 +89,8 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { ApplicationService applicationService, AstService astService, InstanceConfig instanceConfig, + AnalyticsService analyticsService, + SessionUserService sessionUserService, PagePermission pagePermission, ActionPermission actionPermission) { this.objectMapper = objectMapper; @@ -100,26 +102,30 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { this.applicationService = applicationService; this.astService = astService; this.instanceConfig = instanceConfig; + this.analyticsService = analyticsService; + this.sessionUserService = sessionUserService; this.pagePermission = pagePermission; this.actionPermission = actionPermission; - - // TODO Remove this variable and access the field directly when RTS API is ready - this.isRtsAccessible = false; - } @Override public Mono refactorWidgetName(RefactorNameDTO refactorNameDTO) { + final Map analyticsProperties = new HashMap<>(); String pageId = refactorNameDTO.getPageId(); String layoutId = refactorNameDTO.getLayoutId(); String oldName = refactorNameDTO.getOldName(); String newName = refactorNameDTO.getNewName(); return layoutActionService.isNameAllowed(pageId, layoutId, newName) - .flatMap(allowed -> { - if (!allowed) { + .zipWith(newPageService.getById(pageId)) + .flatMap(tuple -> { + analyticsProperties.put(FieldName.APPLICATION_ID, tuple.getT2().getApplicationId()); + analyticsProperties.put(FieldName.PAGE_ID, pageId); + if (!tuple.getT1()) { return Mono.error(new AppsmithException(AppsmithError.NAME_CLASH_NOT_ALLOWED_IN_REFACTOR, oldName, newName)); } - return this.refactorName(pageId, layoutId, oldName, newName); + return this.refactorName(pageId, layoutId, oldName, newName) + .flatMap(tuple2 -> this.sendRefactorAnalytics(AnalyticsEvents.REFACTOR_WIDGET.getEventName(), analyticsProperties, tuple2.getT2()) + .thenReturn(tuple2.getT1())); }); } @@ -139,6 +145,7 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { @Override public Mono refactorActionName(RefactorActionNameDTO refactorActionNameDTO) { + final Map analyticsProperties = new HashMap<>(); String pageId = refactorActionNameDTO.getPageId(); String layoutId = refactorActionNameDTO.getLayoutId(); String oldName = refactorActionNameDTO.getOldName(); @@ -165,13 +172,23 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { .findActionDTObyIdAndViewMode(actionId, false, actionPermission.getEditPermission()); }) .flatMap(action -> { + analyticsProperties.put(FieldName.APPLICATION_ID, action.getApplicationId()); + analyticsProperties.put(FieldName.PAGE_ID, pageId); action.setName(newName); if (StringUtils.hasLength(refactorActionNameDTO.getCollectionName())) { action.setFullyQualifiedName(newFullyQualifiedName); } return newActionService.updateUnpublishedAction(actionId, action); }) - .then(this.refactorName(pageId, layoutId, oldFullyQualifiedName, newFullyQualifiedName)); + .then(this.refactorName(pageId, layoutId, oldFullyQualifiedName, newFullyQualifiedName) + .flatMap(tuple -> { + String eventName = AnalyticsEvents.REFACTOR_ACTION.getEventName(); + if (StringUtils.hasLength(refactorActionNameDTO.getCollectionName())) { + eventName = AnalyticsEvents.REFACTOR_JSACTION.getEventName(); + } + return this.sendRefactorAnalytics(eventName, analyticsProperties, tuple.getT2()) + .thenReturn(tuple.getT1()); + })); } @Override @@ -187,6 +204,16 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { .map(responseUtils::updateLayoutDTOWithDefaultResources); } + @Override + public Mono refactorActionCollectionName(String appId, String pageId, String layoutId, String oldName, String newName) { + final Map analyticsProperties = new HashMap<>(); + analyticsProperties.put(FieldName.APPLICATION_ID, appId); + analyticsProperties.put(FieldName.PAGE_ID, pageId); + return this.refactorName(pageId, layoutId, oldName, newName) + .flatMap(tuple -> this.sendRefactorAnalytics(AnalyticsEvents.REFACTOR_JSOBJECT.getEventName(), analyticsProperties, tuple.getT2()) + .thenReturn(tuple.getT1())); + } + /** * Assumption here is that the refactoring name provided is indeed unique and is fit to be replaced everywhere. *

@@ -199,9 +226,10 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { * @return : The DSL after refactor updates */ @Override - public Mono refactorName(String pageId, String layoutId, String oldName, String newName) { + public Mono>> refactorName(String pageId, String layoutId, String oldName, String newName) { String regexPattern = preWord + oldName + postWord; Pattern oldNamePattern = Pattern.compile(regexPattern); + final Set updatedBindingPaths = new HashSet<>(); Mono pageMono = newPageService // fetch the unpublished page @@ -239,11 +267,12 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { final JsonNode dslNode = objectMapper.convertValue(layout.getDsl(), JsonNode.class); Mono refactorNameInDslMono = this.refactorNameInDsl(dslNode, oldName, newName, evalVersion, oldNamePattern) - .then(Mono.fromCallable(() -> { + .flatMap(dslBindingPaths -> { + updatedBindingPaths.addAll(dslBindingPaths); layout.setDsl(objectMapper.convertValue(dslNode, JSONObject.class)); page.setLayouts(layouts); - return page; - })); + return Mono.just(page); + }); // Since the page has most probably changed, save the page and return. return refactorNameInDslMono @@ -284,6 +313,7 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { if (updates.isEmpty()) { return Mono.just(newAction); } + updatedBindingPaths.addAll(updates); if (StringUtils.hasLength(action.getCollectionId())) { updatableCollectionIds.add(action.getCollectionId()); } @@ -305,29 +335,23 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { .flatMap(actionCollection -> { final ActionCollectionDTO unpublishedCollection = actionCollection.getUnpublishedCollection(); - Matcher matcher = actionCollectionBodyPattern.matcher(unpublishedCollection.getBody()); - if (matcher.find()) { - String parsableBody = matcher.group(1); - return this.replaceValueInMustacheKeys( - new HashSet<>(Collections.singletonList(parsableBody)), - oldName, - newName, - evalVersion, - oldNamePattern) - .flatMap(replacedMap -> { - Optional replacedValue = replacedMap.values().stream().findFirst(); - // This value should always be there - if (replacedValue.isPresent()) { - final String replacedBody = EXPORT_DEFAULT_STRING + replacedValue.get(); - unpublishedCollection.setBody(replacedBody); - return actionCollectionService.save(actionCollection); - } - return Mono.just(actionCollection); - }); - } else { - // TODO make this error more informative, users should never edit JS objects to this state - return Mono.error(new AppsmithException(AppsmithError.INTERNAL_SERVER_ERROR)); - } + return this.replaceValueInMustacheKeys( + new HashSet<>(Collections.singletonList(new MustacheBindingToken(unpublishedCollection.getBody(), 0, true))), + oldName, + newName, + evalVersion, + oldNamePattern, + true) + .flatMap(replacedMap -> { + Optional replacedValue = replacedMap.values().stream().findFirst(); + // This value should always be there + if (replacedValue.isPresent()) { + unpublishedCollection.setBody(replacedValue.get()); + return actionCollectionService.save(actionCollection); + } + return Mono.just(actionCollection); + }); + }) .collectList() .thenReturn(updatedActions); @@ -342,7 +366,8 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { for (Layout layout : layouts) { if (layoutId.equals(layout.getId())) { layout.setDsl(layoutActionService.unescapeMongoSpecialCharacters(layout)); - return layoutActionService.updateLayout(page.getId(), page.getApplicationId(), layout.getId(), layout); + return layoutActionService.updateLayout(page.getId(), page.getApplicationId(), layout.getId(), layout) + .zipWith(Mono.just(updatedBindingPaths)); } } return Mono.empty(); @@ -489,7 +514,7 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { ((ObjectNode) bindingPath).set(FieldName.KEY, new TextNode(key)); } // Find values inside mustache bindings in this path - Set mustacheValues = DslUtils.getMustacheValueSetFromSpecificDynamicBindingPath(widgetDsl, key); + Set mustacheValues = DslUtils.getMustacheValueSetFromSpecificDynamicBindingPath(widgetDsl, key); final String finalKey = key; // Perform refactor for each mustache value return this.replaceValueInMustacheKeys(mustacheValues, oldName, newName, evalVersion, oldNamePattern) @@ -514,7 +539,7 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { // If we're going the fallback route (without AST), we can first filter actions to be refactored // By performing a check on whether json path keys had a reference // This is not needed in the AST way since it would be costlier to make double the number of API calls - if (Boolean.FALSE.equals(this.isRtsAccessible)) { + if (Boolean.FALSE.equals(this.instanceConfig.getIsRtsAccessible())) { Set jsonPathKeys = actionDTO.getJsonPathKeys(); boolean isReferenceFound = false; @@ -546,9 +571,9 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { refactorDynamicBindingsMono = Flux.fromIterable(actionDTO.getDynamicBindingPathList()) .flatMap(dynamicBindingPath -> { String key = dynamicBindingPath.getKey(); - Set mustacheValues = new HashSet<>(); + Set mustacheValues = new HashSet<>(); if (PluginType.JS.equals(actionDTO.getPluginType()) && "body".equals(key)) { - mustacheValues.add(actionConfiguration.getBody()); + mustacheValues.add(new MustacheBindingToken(actionConfiguration.getBody(), 0, false)); } else { mustacheValues = DslUtils.getMustacheValueSetFromSpecificDynamicBindingPath(actionConfigurationNode, key); @@ -573,19 +598,24 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { return refactorDynamicBindingsMono; } - Mono> replaceValueInMustacheKeys(Set mustacheKeySet, String oldName, String + Mono> replaceValueInMustacheKeys(Set mustacheKeySet, String oldName, String newName, int evalVersion, Pattern oldNamePattern) { - if (Boolean.TRUE.equals(this.isRtsAccessible)) { - return astService.refactorNameInDynamicBindings(mustacheKeySet, oldName, newName, evalVersion); + return this.replaceValueInMustacheKeys(mustacheKeySet, oldName, newName, evalVersion, oldNamePattern, false); + } + + Mono> replaceValueInMustacheKeys(Set mustacheKeySet, String oldName, String + newName, int evalVersion, Pattern oldNamePattern, boolean isJSObject) { + if (Boolean.TRUE.equals(this.instanceConfig.getIsRtsAccessible())) { + return astService.refactorNameInDynamicBindings(mustacheKeySet, oldName, newName, evalVersion, isJSObject); } return this.replaceValueInMustacheKeys(mustacheKeySet, oldNamePattern, newName); } - Mono> replaceValueInMustacheKeys(Set mustacheKeySet, Pattern + Mono> replaceValueInMustacheKeys(Set mustacheKeySet, Pattern oldNamePattern, String newName) { return Flux.fromIterable(mustacheKeySet) .flatMap(mustacheKey -> { - Matcher matcher = oldNamePattern.matcher(mustacheKey); + Matcher matcher = oldNamePattern.matcher(mustacheKey.getValue()); if (matcher.find()) { return Mono.zip(Mono.just(mustacheKey), Mono.just(matcher.replaceAll(newName))); } @@ -593,4 +623,16 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE { }) .collectMap(Tuple2::getT1, Tuple2::getT2); } + + Mono sendRefactorAnalytics(String event, Map properties, Set updatedPaths) { + return sessionUserService.getCurrentUser() + .map(user -> { + final Map analyticsProperties = new HashMap<>(properties); + analyticsProperties.put("updatedPaths", updatedPaths.toString()); + analyticsProperties.put("userId", user.getUsername()); + analyticsService.sendEvent(event, user.getUsername(), analyticsProperties); + return true; + }) + .then(); + } } diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/DslUtilsTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/DslUtilsTest.java new file mode 100644 index 0000000000..09969077d8 --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/DslUtilsTest.java @@ -0,0 +1,94 @@ +package com.appsmith.server.helpers; + +import com.appsmith.external.models.MustacheBindingToken; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Set; + +class DslUtilsTest { + + @Test + void getMustacheValueSetFromSpecificDynamicBindingPath_withNullOrEmptyDsl_returnsEmptySet() { + Set tokensInNullDsl = DslUtils.getMustacheValueSetFromSpecificDynamicBindingPath(null, "irrelevantPath"); + Set tokensInEmptyDsl = DslUtils.getMustacheValueSetFromSpecificDynamicBindingPath(new TextNode(""), "irrelevantPath"); + + Assertions.assertThat(tokensInNullDsl).isEmpty(); + Assertions.assertThat(tokensInEmptyDsl).isEmpty(); + } + + @Test + void getMustacheValueSetFromSpecificDynamicBindingPath_withComplicatedPathAndMultipleBindings_parsesDslCorrectly() throws JsonProcessingException { + String fieldPath = "root.field.list[0].childField.anotherList.0.multidimensionalList[0][0]"; + String jsonString = "{ " + + "\"root\": { " + + " \"field\": { " + + " \"list\": [ " + + " { " + + " \"childField\": { " + + " \"anotherList\": [ " + + " { " + + " \"multidimensionalList\" : [ " + + " [\"{{ retrievedBinding1.text }} {{ retrievedBinding2.text }}\"]" + + " ] " + + " } " + + " ] " + + " } " + + " } " + + " ] " + + " } " + + " } " + + "}"; + + ObjectMapper mapper = new ObjectMapper(); + JsonNode dsl = mapper.readTree(jsonString); + + Set tokens = DslUtils.getMustacheValueSetFromSpecificDynamicBindingPath(dsl, fieldPath); + + Assertions.assertThat(tokens).containsExactlyInAnyOrder( + new MustacheBindingToken(" retrievedBinding1.text ", 2, false), + new MustacheBindingToken(" retrievedBinding2.text ", 31, false)); + } + + @Test + void replaceValuesInSpecificDynamicBindingPath_whenFieldPathNotFound() { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode dsl = mapper.createObjectNode(); + dsl.put("fieldKey", "fieldValue"); + JsonNode replacedDsl = DslUtils.replaceValuesInSpecificDynamicBindingPath(dsl, "nonExistentPath", new HashMap<>()); + Assertions.assertThat(replacedDsl).isEqualTo(dsl); + } + + @Test + void replaceValuesInSpecificDynamicBindingPath_whenReplacementKeyNotFound() { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode dsl = mapper.createObjectNode(); + dsl.put("existingPath", "fieldValue"); + HashMap replacementMap = new HashMap<>(); + replacementMap.put(new MustacheBindingToken("nonExistentBinding", 0, false), "newNonExistentBinding"); + JsonNode replacedDsl = DslUtils.replaceValuesInSpecificDynamicBindingPath(dsl, "existingPath", replacementMap); + ObjectNode newDsl = mapper.createObjectNode(); + newDsl.put("existingPath", "fieldValue"); + Assertions.assertThat(replacedDsl).isEqualTo(newDsl); + } + + @Test + void replaceValuesInSpecificDynamicBindingPath_withSuccessfulMultipleReplacements() { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode dsl = mapper.createObjectNode(); + dsl.put("existingPath", "oldFieldValue1 oldFieldValue2"); + HashMap replacementMap = new HashMap<>(); + replacementMap.put(new MustacheBindingToken("oldFieldValue1", 0, false), "newFieldValue1"); + replacementMap.put(new MustacheBindingToken("oldFieldValue2", 15, false), "newFieldValue2"); + JsonNode replacedDsl = DslUtils.replaceValuesInSpecificDynamicBindingPath(dsl, "existingPath", replacementMap); + ObjectNode newDsl = mapper.createObjectNode(); + newDsl.put("existingPath", "newFieldValue1 newFieldValue2"); + Assertions.assertThat(replacedDsl).isEqualTo(dsl); + } +} \ No newline at end of file diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionCollectionServiceImplTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionCollectionServiceImplTest.java index 2ea62be9c9..6054ff3582 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionCollectionServiceImplTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/ActionCollectionServiceImplTest.java @@ -280,7 +280,7 @@ public class ActionCollectionServiceImplTest { Mockito .when(layoutActionService.updatePageLayoutsByPageId(Mockito.anyString())) .thenAnswer(invocationOnMock -> { - return Mono.just(actionCollectionDTO.getPageId()); + return Mono.just(actionCollectionDTO.getPageId()); }); Mockito @@ -525,7 +525,7 @@ public class ActionCollectionServiceImplTest { Mockito .when(layoutActionService.updatePageLayoutsByPageId(Mockito.anyString())) .thenAnswer(invocationOnMock -> { - return Mono.just(actionCollection.getUnpublishedCollection().getPageId()); + return Mono.just(actionCollection.getUnpublishedCollection().getPageId()); }); @@ -799,7 +799,7 @@ public class ActionCollectionServiceImplTest { jsonObject.put("key", "value"); layout.setDsl(jsonObject); Mockito - .when(refactoringSolution.refactorName(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) + .when(refactoringSolution.refactorActionCollectionName(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(Mono.just(layout)); Mockito @@ -877,7 +877,7 @@ public class ActionCollectionServiceImplTest { layout.setActionUpdates(new ArrayList<>()); layout.setLayoutOnLoadActions(new ArrayList<>()); Mockito - .when(refactoringSolution.refactorName(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) + .when(refactoringSolution.refactorActionCollectionName(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())) .thenReturn(Mono.just(layout)); Mockito diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/LayoutServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/LayoutServiceTest.java index d1c2a98d4f..33d0872d01 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/LayoutServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/LayoutServiceTest.java @@ -937,7 +937,7 @@ public class LayoutServiceTest { Mockito.when(astService.getPossibleReferencesFromDynamicBinding(List.of("\"anIgnoredAction.data:\" + aGetAction.data"), EVALUATION_VERSION)) .thenReturn(Flux.just(Tuples.of("\"anIgnoredAction.data:\" + aGetAction.data", new HashSet<>(Set.of("aGetAction.data"))))); - String bindingValue = "(function(ignoredAction1){\n" + + String bindingValue = "\n(function(ignoredAction1){\n" + "\tlet a = ignoredAction1.data\n" + "\tlet ignoredAction2 = { data: \"nothing\" }\n" + "\tlet b = ignoredAction2.data\n" + @@ -952,8 +952,8 @@ public class LayoutServiceTest { .thenReturn(Flux.just(Tuples.of("aPostActionWithAutoExec.data", new HashSet<>(Set.of("aPostActionWithAutoExec.data"))))); Mockito.when(astService.getPossibleReferencesFromDynamicBinding(List.of("aDBAction.data[0].irrelevant"), EVALUATION_VERSION)) .thenReturn(Flux.just(Tuples.of("aDBAction.data[0].irrelevant", new HashSet<>(Set.of("aDBAction.data[0].irrelevant"))))); - Mockito.when(astService.getPossibleReferencesFromDynamicBinding(List.of("anotherDBAction.data.optional"), EVALUATION_VERSION)) - .thenReturn(Flux.just(Tuples.of("anotherDBAction.data.optional", new HashSet<>(Set.of("anotherDBAction.data.optional"))))); + Mockito.when(astService.getPossibleReferencesFromDynamicBinding(List.of(" anotherDBAction.data.optional "), EVALUATION_VERSION)) + .thenReturn(Flux.just(Tuples.of(" anotherDBAction.data.optional ", new HashSet<>(Set.of("anotherDBAction.data.optional"))))); Mockito.when(astService.getPossibleReferencesFromDynamicBinding(List.of("aTableAction.data.child"), EVALUATION_VERSION)) .thenReturn(Flux.just(Tuples.of("aTableAction.data.child", new HashSet<>(Set.of("aTableAction.data.child"))))); Mockito.when(astService.getPossibleReferencesFromDynamicBinding(List.of("Collection.anAsyncCollectionActionWithoutCall.data"), EVALUATION_VERSION)) diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/RefactoringSolutionCEImplTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/RefactoringSolutionCEImplTest.java index 7e11092dca..0ac3511d8f 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/RefactoringSolutionCEImplTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/RefactoringSolutionCEImplTest.java @@ -3,11 +3,13 @@ package com.appsmith.server.solutions.ce; import com.appsmith.server.configurations.InstanceConfig; import com.appsmith.server.helpers.ResponseUtils; import com.appsmith.server.services.ActionCollectionService; +import com.appsmith.server.services.AnalyticsService; import com.appsmith.server.services.ApplicationService; import com.appsmith.server.services.AstService; import com.appsmith.server.services.LayoutActionService; import com.appsmith.server.services.NewActionService; import com.appsmith.server.services.NewPageService; +import com.appsmith.server.services.SessionUserService; import com.appsmith.server.solutions.ActionPermission; import com.appsmith.server.solutions.ActionPermissionImpl; import com.appsmith.server.solutions.PagePermission; @@ -52,6 +54,10 @@ class RefactoringSolutionCEImplTest { private AstService astService; @MockBean private InstanceConfig instanceConfig; + @MockBean + private AnalyticsService analyticsService; + @MockBean + private SessionUserService sessionUserService; PagePermission pagePermission; ActionPermission actionPermission; @@ -73,7 +79,9 @@ class RefactoringSolutionCEImplTest { layoutActionService, applicationService, astService, - instanceConfig, + instanceConfig, + analyticsService, + sessionUserService, pagePermission, actionPermission); } diff --git a/app/shared/ast/src/index.ts b/app/shared/ast/src/index.ts index eeba8fcda3..73fce5f40d 100644 --- a/app/shared/ast/src/index.ts +++ b/app/shared/ast/src/index.ts @@ -41,6 +41,13 @@ interface IdentifierNode extends Node { name: string; } +//Using this to handle the Variable property refactor +interface RefactorIdentifierNode extends Node { + type: NodeTypes.Identifier; + name: string; + property?: IdentifierNode; +} + // doc: https://github.com/estree/estree/blob/master/es5.md#variabledeclarator interface VariableDeclaratorNode extends Node { type: NodeTypes.VariableDeclarator; @@ -270,50 +277,81 @@ export const entityRefactorFromCode = ( evaluationVersion: number, invalidIdentifiers?: Record ): EntityRefactorResponse => { + //Sanitizing leads to removal of special charater. + //Hence we are not sanatizing the script. Fix(#18492) //If script is a JSObject then replace export default to decalartion. if (isJSObject) script = jsObjectToCode(script); let ast: Node = { end: 0, start: 0, type: "" }; //Copy of script to refactor let refactorScript = script; //Difference in length of oldName and newName - let nameLengthDiff: number = newName.length - oldName.length; + const nameLengthDiff: number = newName.length - oldName.length; //Offset index used for deciding location of oldName. let refactorOffset: number = 0; //Count of refactors on the script let refactorCount: number = 0; try { - const sanitizedScript = sanitizeScript(script, evaluationVersion); - ast = getAST(sanitizedScript); + ast = getAST(script); let { references, functionalParams, variableDeclarations, identifierList, }: NodeList = ancestorWalk(ast); - let identifierArray = Array.from(identifierList) as Array; - Array.from(references).forEach((reference, index) => { + const identifierArray = Array.from( + identifierList + ) as Array; + //To handle if oldName has property ("JSObject.myfunc") + const oldNameArr = oldName.split("."); + const referencesArr = Array.from(references).filter((reference) => { + // To remove references derived from declared variables and function params, + // We extract the topLevelIdentifier Eg. Api1.name => Api1 const topLevelIdentifier = toPath(reference)[0]; - let shouldUpdateNode = !( + return !( functionalParams.has(topLevelIdentifier) || variableDeclarations.has(topLevelIdentifier) || has(invalidIdentifiers, topLevelIdentifier) ); - //check if node should be updated - if (shouldUpdateNode && identifierArray[index].name === oldName) { - //Replace the oldName by newName - //Get start index from node and get subarray from index 0 till start - //Append above with new name - //Append substring from end index from the node till end of string - //Offset variable is used to alter the position based on `refactorOffset` - refactorScript = - refactorScript.substring( - 0, - identifierArray[index].start + refactorOffset - ) + - newName + - refactorScript.substring(identifierArray[index].end + refactorOffset); - refactorOffset += nameLengthDiff; - ++refactorCount; + }); + //Traverse through all identifiers in the script + identifierArray.forEach((identifier) => { + if (identifier.name === oldNameArr[0]) { + let index = 0; + while (index < referencesArr.length) { + if (identifier.name === referencesArr[index].split(".")[0]) { + //Replace the oldName by newName + //Get start index from node and get subarray from index 0 till start + //Append above with new name + //Append substring from end index from the node till end of string + //Offset variable is used to alter the position based on `refactorOffset` + //In case of nested JS action get end postion fro the property. + ///Default end index + let endIndex = identifier.end; + const propertyNode = identifier.property; + //Flag variable : true if property should be updated + //false if property should not be updated + let propertyCondFlag = + oldNameArr.length > 1 && + propertyNode && + oldNameArr[1] === propertyNode.name; + //Condition to validate if Identifier || Property should be updated?? + if (oldNameArr.length === 1 || propertyCondFlag) { + //Condition to extend end index in case of property match + if (propertyCondFlag && propertyNode) { + endIndex = propertyNode.end; + } + refactorScript = + refactorScript.substring(0, identifier.start + refactorOffset) + + newName + + refactorScript.substring(endIndex + refactorOffset); + refactorOffset += nameLengthDiff; + ++refactorCount; + //We are only looking for one match in refrence for the identifier name. + break; + } + } + index++; + } } }); //If script is a JSObject then revert decalartion to export default. @@ -507,8 +545,8 @@ export const extractInvalidTopLevelMemberExpressionsFromCode = ( }; const ancestorWalk = (ast: Node): NodeList => { - //List of all Identifier nodes - const identifierList = new Array(); + //List of all Identifier nodes with their property(if exists). + const identifierList = new Array(); // List of all references found const references = new Set(); // List of variables declared within the script. All identifiers and member expressions derived from declared variables will be removed @@ -554,7 +592,15 @@ const ancestorWalk = (ast: Node): NodeList => { break; } } - identifierList.push(node as IdentifierNode); + //If parent is a Member expression then attach property to the Node. + //else push Identifier Node. + const parentNode = ancestors[ancestors.length - 2]; + if (isMemberExpressionNode(parentNode)) { + identifierList.push({ + ...(node as IdentifierNode), + property: parentNode.property as IdentifierNode, + }); + } else identifierList.push(node as RefactorIdentifierNode); if (isIdentifierNode(candidateTopLevelNode)) { // If the node is an Identifier, just save that references.add(candidateTopLevelNode.name);