fix: Refactor entities with specific rules (#17523)

* Refactor changes for DSL

* Spaces

* Action and collection refactor logic

* Changes to some logic for DSL

* Fixed tests, added dynamic trigger path list logic as well

* Added test for dynamicTriggerList condition

* added analytics data to response in ast

* Fix for peer closed connection on AST

* Added comments for clarity

* Added logs for time taken by AST call

* handle export default and update success param accordingly

* updates for review comments

Co-authored-by: ChandanBalajiBP <chandan@appsmith.com>
This commit is contained in:
Nidhi 2022-10-26 20:23:06 +05:30 committed by GitHub
parent c1e9bea10a
commit 204a187bc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 2000 additions and 131 deletions

View File

@ -13,6 +13,7 @@ type entityRefactorType = {
script: string;
oldName: string;
newName: string;
isJSObject: boolean;
evalVersion?: number;
};
@ -74,15 +75,16 @@ export default class AstController extends BaseController {
async entityRefactorController(req: Request, res: Response) {
try {
// By default the application eval version is set to be 2
const { script, oldName, newName, evalVersion }: entityRefactorType =
const { script, oldName, newName, isJSObject, evalVersion }: entityRefactorType =
req.body;
const data = await AstService.entityRefactor(
script,
oldName,
newName,
isJSObject,
evalVersion
);
return super.sendResponse(res, data);
return super.sendEntityResponse(res, data.body, data.isSuccess);
} catch (err) {
return super.sendError(
res,

View File

@ -35,6 +35,18 @@ export default class BaseController {
});
}
sendEntityResponse(
response: Response,
result?: unknown,
success?: boolean,
code: number = StatusCodes.OK
): Response<ResponseData> {
return response.status(code).json({
success,
data: result,
});
}
sendError(
response: Response,
error: string,

View File

@ -12,4 +12,19 @@ export default class AstValidator {
min: 1,
})
.withMessage("Multiple scripts are required");
static getEntityRefactorValidator = () => [
body("script")
.isString()
.withMessage("Script is required and can only be a string"),
body("oldName")
.isString()
.withMessage("OldName is required and can only be a string"),
body("newName")
.isString()
.withMessage("NewName is required and can only be a string"),
body("isJSObject")
.isBoolean()
.withMessage("isJSObject is required and can only be a boolean"),
];
}

View File

@ -22,7 +22,7 @@ router.post(
);
router.post(
"/entity-refactor",
AstRules.getScriptValidator(),
AstRules.getEntityRefactorValidator(),
validator.validateRequest,
astController.entityRefactorController
);

View File

@ -25,6 +25,7 @@ export default class AstService {
script,
oldName,
newName,
isJSObject,
evalVersion
): Promise<any> {
return new Promise((resolve, reject) => {
@ -33,6 +34,7 @@ export default class AstService {
script,
oldName,
newName,
isJSObject,
evalVersion
);

View File

@ -18,23 +18,37 @@ const entityRefactor = [
script: "ApiNever",
oldName: "ApiNever",
newName: "ApiForever",
isJSObject: false,
evalVersion: 2,
},
{
script: "ApiNever.data",
oldName: "ApiNever",
newName: "ApiForever",
isJSObject: false,
},
{
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",
newName: "ApiForever",
isJSObject: false,
evalVersion: 2,
},
{
script:
"//ApiNever \n function ApiNever(abc) {let ApiNever = \"I'm getting data from ApiNever but don't rename this string\" + ApiNever.data; \n if(true) { return ApiNever }}",
oldName: "ApiNever",
newName: "ApiForever",
isJSObject: false,
},
{
script:
"export default {\n\tmyVar1: [],\n\tmyVar2: {},\n\t\tsearch: () => {\n\t\tif(Input1Copy.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(Input1Copy.text.toLowerCase())))\n\t\t}\n\t},\n}",
oldName: "Input1Copy",
newName: "Input1",
isJSObject: true,
evalVersion: 2,
},
];
@ -93,17 +107,22 @@ describe("AST tests", () => {
entityRefactor.forEach(async (input, index) => {
it(`Entity refactor test case ${index + 1}`, async () => {
const expectedResponse = [
{ script: "ApiForever", count: 1 },
{ script: "ApiForever.data", count: 1 },
{ script: "ApiForever", refactorCount: 1 },
{ script: "ApiForever.data", refactorCount: 1 },
{
script:
"// ApiNever \n function ApiNever(abc) {let foo = \"I'm getting data from ApiNever but don't rename this string\" + ApiForever.data; \n if(true) { return ApiForever }}",
count: 2,
refactorCount: 2,
},
{
script:
"//ApiNever \n function ApiNever(abc) {let ApiNever = \"I'm getting data from ApiNever but don't rename this string\" + ApiNever.data; \n if(true) { return ApiNever }}",
count: 0,
refactorCount: 0,
},
{
script:
"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,
},
];
@ -118,10 +137,31 @@ describe("AST tests", () => {
expect(response.body.data.script).toEqual(
expectedResponse[index].script
);
expect(response.body.data.count).toEqual(
expectedResponse[index].count
expect(response.body.data.refactorCount).toEqual(
expectedResponse[index].refactorCount
);
});
});
});
it("Entity refactor syntax error", async () => {
let request = {
script: "ApiNever++++",
oldName: "ApiNever",
newName: "ApiForever",
isJSObject: true,
evalVersion: 2,
};
await supertest(app)
.post(`${RTS_BASE_API_PATH}/ast/entity-refactor`, {
JSON: true,
})
.send(request)
.expect(200)
.then((response) => {
expect(response.body.success).toEqual(false);
expect(response.body.data.error).toEqual("Syntax Error");
});
});
});

View File

@ -11,6 +11,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@ -35,12 +36,23 @@ public class WebClientUtils {
.build();
}
public static WebClient create(ConnectionProvider provider) {
return builder(provider)
.build();
}
public static WebClient create(String baseUrl) {
return builder()
.baseUrl(baseUrl)
.build();
}
public static WebClient create(String baseUrl, ConnectionProvider provider) {
return builder(provider)
.baseUrl(baseUrl)
.build();
}
private static boolean shouldUseSystemProxy() {
return "true".equals(System.getProperty("java.net.useSystemProxies"))
&& (!System.getProperty("http.proxyHost", "").isEmpty() || !System.getProperty("https.proxyHost", "").isEmpty());
@ -50,6 +62,10 @@ public class WebClientUtils {
return builder(HttpClient.create());
}
public static WebClient.Builder builder(ConnectionProvider provider) {
return builder(HttpClient.create(provider));
}
public static WebClient.Builder builder(HttpClient httpClient) {
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(makeSafeHttpClient(httpClient)));

View File

@ -78,6 +78,8 @@ public class FieldName {
public static String PUBLISHED_APPLICATION = "deployed application";
public static final String TOKEN = "token";
public static String WIDGET_TYPE = "type";
public static String LIST_WIDGET_TEMPLATE = "template";
public static String LIST_WIDGET = "LIST_WIDGET";
public static String TABLE_WIDGET = "TABLE_WIDGET";
public static String CONTAINER_WIDGET = "CONTAINER_WIDGET";
public static String CANVAS_WIDGET = "CANVAS_WIDGET";

View File

@ -0,0 +1,107 @@
package com.appsmith.server.helpers;
import com.appsmith.external.helpers.MustacheHelper;
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;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
public class DslUtils {
public static Set<String> getMustacheValueSetFromSpecificDynamicBindingPath(JsonNode dsl, String fieldPath) {
DslNodeWalkResponse dslWalkResponse = getDslWalkResponse(dsl, fieldPath);
// Only extract mustache keys from leaf nodes
if (dslWalkResponse != null && dslWalkResponse.isLeafNode) {
// We found the path. But if the path does not have any mustache bindings, return with empty set
if (!MustacheHelper.laxIsBindingPresentInString(((TextNode) dslWalkResponse.currentNode).asText())) {
return new HashSet<>();
}
// Stricter extraction of dynamic bindings
Set<String> mustacheKeysFromFields = MustacheHelper.extractMustacheKeysFromFields(((TextNode) dslWalkResponse.currentNode).asText());
return mustacheKeysFromFields;
}
// This was not a text node, we do not know how to handle this
return new HashSet<>();
}
public static JsonNode replaceValuesInSpecificDynamicBindingPath(JsonNode dsl, String fieldPath, Map<String, String> replacementMap) {
DslNodeWalkResponse dslWalkResponse = getDslWalkResponse(dsl, fieldPath);
if (dslWalkResponse != null && dslWalkResponse.isLeafNode) {
final String oldValue = ((TextNode) dslWalkResponse.currentNode).asText();
final String newValue = StringUtils.replaceEach(
oldValue,
replacementMap.keySet().toArray(new String[0]),
replacementMap.values().toArray(new String[0]));
((ObjectNode) dslWalkResponse.parentNode).set(dslWalkResponse.currentKey, new TextNode(newValue));
}
return dsl;
}
private static DslNodeWalkResponse getDslWalkResponse(JsonNode dsl, String fieldPath) {
String[] fields = fieldPath.split("[].\\[]");
// For nested fields, the parent dsl to search in would shift by one level every iteration
Object currentNode = dsl;
Object parent = null;
Iterator<String> fieldsIterator = Arrays.stream(fields).filter(fieldToken -> !fieldToken.isBlank()).iterator();
boolean isLeafNode = false;
String nextKey = null;
// This loop will end at either a leaf node, or the last identified JSON field (by throwing an exception)
// Valid forms of the fieldPath for this search could be:
// root.field.list[index].childField.anotherList.indexWithDotOperator.multidimensionalList[index1][index2]
while (fieldsIterator.hasNext()) {
nextKey = fieldsIterator.next();
parent = currentNode;
if (currentNode instanceof ArrayNode) {
if (Pattern.matches(Pattern.compile("[0-9]+").toString(), nextKey)) {
try {
currentNode = ((ArrayNode) currentNode).get(Integer.parseInt(nextKey));
} catch (IndexOutOfBoundsException e) {
// The index being referred does not exist, hence the path would not exist.
return null;
}
} else {
// This is an array but the fieldPath does not have an index to refer to
return null;
}
} else {
currentNode = ((JsonNode) currentNode).get(nextKey);
}
// After updating the currentNode, check for the types
if (currentNode == null) {
return null;
} else if (currentNode instanceof TextNode) {
// If we get String value, then this is a leaf node
isLeafNode = true;
break;
}
}
return new DslNodeWalkResponse(currentNode, parent, nextKey, isLeafNode);
}
@AllArgsConstructor
private static class DslNodeWalkResponse {
Object currentNode;
Object parentNode;
String currentKey;
Boolean isLeafNode;
}
}

View File

@ -2,6 +2,7 @@ package com.appsmith.server.services.ce;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.Set;
public interface AstServiceCE {
@ -18,4 +19,6 @@ public interface AstServiceCE {
* @return A mono of list of strings that represent all valid global references in the binding string
*/
Mono<Set<String>> getPossibleReferencesFromDynamicBinding(String bindingValue, int evalVersion);
Mono<Map<String, String>> refactorNameInDynamicBindings(Set<String> bindingValues, String oldName, String newName, int evalVersion);
}

View File

@ -13,10 +13,17 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.netty.resources.ConnectionProvider;
import reactor.util.function.Tuple2;
import java.time.Duration;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
@RequiredArgsConstructor
@ -26,6 +33,15 @@ public class AstServiceCEImpl implements AstServiceCE {
private final InstanceConfig instanceConfig;
private final WebClient webClient = WebClientUtils.create(ConnectionProvider.builder("rts-provider")
.maxConnections(500)
.maxIdleTime(Duration.ofSeconds(5))
.maxLifeTime(Duration.ofSeconds(10))
.pendingAcquireTimeout(Duration.ofSeconds(10))
.evictInBackground(Duration.ofSeconds(20)).build());
private final static long MAX_API_RESPONSE_TIME = 50;
@Override
public Mono<Set<String>> getPossibleReferencesFromDynamicBinding(String bindingValue, int evalVersion) {
if (!StringUtils.hasLength(bindingValue)) {
@ -38,16 +54,61 @@ public class AstServiceCEImpl implements AstServiceCE {
return Mono.just(new HashSet<>(MustacheHelper.getPossibleParentsOld(bindingValue)));
}
return WebClientUtils.create(commonConfig.getRtsBaseDomain() + "/rts-api/v1/ast/single-script-data")
return webClient
.post()
.uri(commonConfig.getRtsBaseDomain() + "/rts-api/v1/ast/single-script-data")
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(new GetIdentifiersRequest(bindingValue, evalVersion)))
.retrieve()
.bodyToMono(GetIdentifiersResponse.class)
.elapsed()
.map(tuple -> {
log.debug("Time elapsed since AST get identifiers call: {} ms", tuple.getT1());
if (tuple.getT1() > MAX_API_RESPONSE_TIME) {
log.debug("This call took longer than expected. The binding was: {}", bindingValue);
}
return tuple.getT2();
})
.map(response -> response.data.references);
// TODO: add error handling scenario for when RTS is not accessible in fat container
}
@Override
public Mono<Map<String, String>> refactorNameInDynamicBindings(Set<String> bindingValues, String oldName, String newName, int evalVersion) {
if (bindingValues == null || bindingValues.isEmpty()) {
return Mono.empty();
}
return Flux.fromIterable(bindingValues)
.flatMap(bindingValue -> {
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)))
.retrieve()
.bodyToMono(EntityRefactorResponse.class)
.elapsed()
.map(tuple -> {
log.debug("Time elapsed since AST refactor call: {} ms", tuple.getT1());
if (tuple.getT1() > MAX_API_RESPONSE_TIME) {
log.debug("This call took longer than expected. The binding was: {}", bindingValue);
}
return tuple.getT2();
})
.map(EntityRefactorResponse::getData)
.filter(details -> details.refactorCount > 0)
.flatMap(response -> Mono.just(bindingValue).zipWith(Mono.just(response.script)))
.onErrorResume(error -> {
var temp = bindingValue;
// If there is a problem with parsing and refactoring this binding, we just ignore it and move ahead
// The expectation is that this binding would error out during eval anyway
return Mono.empty();
});
})
.collect(Collectors.toMap(Tuple2::getT1, Tuple2::getT2));
}
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ -90,4 +151,32 @@ public class AstServiceCEImpl implements AstServiceCE {
Set<String> functionalParams;
Set<String> variables;
}
@NoArgsConstructor
@AllArgsConstructor
@Getter
static class EntityRefactorRequest {
String script;
String oldName;
String newName;
int evalVersion;
}
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
static class EntityRefactorResponse {
EntityRefactorResponseDetails data;
}
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
static class EntityRefactorResponseDetails {
String script;
int referenceCount;
int refactorCount;
}
}

View File

@ -1,7 +1,10 @@
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.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;
@ -19,12 +22,18 @@ public class RefactoringSolutionImpl extends RefactoringSolutionCEImpl implement
NewActionService newActionService,
ActionCollectionService actionCollectionService,
ResponseUtils responseUtils,
LayoutActionService layoutActionService) {
LayoutActionService layoutActionService,
ApplicationService applicationService,
AstService astService,
InstanceConfig instanceConfig) {
super(objectMapper,
newPageService,
newActionService,
actionCollectionService,
responseUtils,
layoutActionService);
layoutActionService,
applicationService,
astService,
instanceConfig);
}
}

View File

@ -2,6 +2,9 @@ package com.appsmith.server.solutions.ce;
import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionDTO;
import com.appsmith.external.models.PluginType;
import com.appsmith.server.configurations.InstanceConfig;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.dtos.ActionCollectionDTO;
@ -11,8 +14,11 @@ import com.appsmith.server.dtos.RefactorActionNameDTO;
import com.appsmith.server.dtos.RefactorNameDTO;
import com.appsmith.server.exceptions.AppsmithError;
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.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;
@ -21,44 +27,78 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONObject;
import org.jetbrains.annotations.NotNull;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
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;
@Slf4j
@RequiredArgsConstructor
public class RefactoringSolutionCEImpl implements RefactoringSolutionCE {
private final ObjectMapper objectMapper;
private final NewPageService newPageService;
private final NewActionService newActionService;
private final ActionCollectionService actionCollectionService;
private final ResponseUtils responseUtils;
private final LayoutActionService layoutActionService;
private final ApplicationService applicationService;
private final AstService astService;
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";
/*
* 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
* at run time to create the regex pattern.
*/
private final String preWord = "\\b(";
private final String postWord = ")\\b";
private static final String preWord = "\\b(";
private static final String postWord = ")\\b";
public RefactoringSolutionCEImpl(ObjectMapper objectMapper,
NewPageService newPageService,
NewActionService newActionService,
ActionCollectionService actionCollectionService,
ResponseUtils responseUtils,
LayoutActionService layoutActionService,
ApplicationService applicationService,
AstService astService,
InstanceConfig instanceConfig) {
this.objectMapper = objectMapper;
this.newPageService = newPageService;
this.newActionService = newActionService;
this.actionCollectionService = actionCollectionService;
this.responseUtils = responseUtils;
this.layoutActionService = layoutActionService;
this.applicationService = applicationService;
this.astService = astService;
this.instanceConfig = instanceConfig;
// TODO Remove this variable and access the field directly when RTS API is ready
this.isRtsAccessible = false;
}
@Override
public Mono<LayoutDTO> refactorWidgetName(RefactorNameDTO refactorNameDTO) {
@ -77,7 +117,7 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE {
@Override
public Mono<LayoutDTO> refactorWidgetName(RefactorNameDTO refactorNameDTO, String branchName) {
if (StringUtils.isEmpty(branchName)) {
if (!StringUtils.hasLength(branchName)) {
return refactorWidgetName(refactorNameDTO);
}
@ -94,13 +134,13 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE {
String pageId = refactorActionNameDTO.getPageId();
String layoutId = refactorActionNameDTO.getLayoutId();
String oldName = refactorActionNameDTO.getOldName();
final String oldFullyQualifiedName = StringUtils.isEmpty(refactorActionNameDTO.getCollectionName()) ?
oldName :
refactorActionNameDTO.getCollectionName() + "." + oldName;
final String oldFullyQualifiedName = StringUtils.hasLength(refactorActionNameDTO.getCollectionName()) ?
refactorActionNameDTO.getCollectionName() + "." + oldName :
oldName;
String newName = refactorActionNameDTO.getNewName();
final String newFullyQualifiedName = StringUtils.isEmpty(refactorActionNameDTO.getCollectionName()) ?
newName :
refactorActionNameDTO.getCollectionName() + "." + newName;
final String newFullyQualifiedName = StringUtils.hasLength(refactorActionNameDTO.getCollectionName()) ?
refactorActionNameDTO.getCollectionName() + "." + newName :
newName;
String actionId = refactorActionNameDTO.getActionId();
return Mono.just(newActionService.validateActionName(newName))
.flatMap(isValidName -> {
@ -118,7 +158,7 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE {
})
.flatMap(action -> {
action.setName(newName);
if (!StringUtils.isEmpty(refactorActionNameDTO.getCollectionName())) {
if (StringUtils.hasLength(refactorActionNameDTO.getCollectionName())) {
action.setFullyQualifiedName(newFullyQualifiedName);
}
return newActionService.updateUnpublishedAction(actionId, action);
@ -139,34 +179,48 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE {
.map(responseUtils::updateLayoutDTOWithDefaultResources);
}
/**
* Assumption here is that the refactoring name provided is indeed unique and is fit to be replaced everywhere.
* <p>
* At this point, the user must have MANAGE_PAGES and MANAGE_ACTIONS permissions for page and action respectively
*
* @param pageId
* @param layoutId
* @param oldName
* @param newName
* @return
* @param pageId : The page that this entity belongs to
* @param layoutId : The layout to parse through for replacement
* @param oldName : The original name to look for
* @param newName : The new name to refactor all references to
* @return : The DSL after refactor updates
*/
@Override
public Mono<LayoutDTO> refactorName(String pageId, String layoutId, String oldName, String newName) {
String regexPattern = preWord + oldName + postWord;
Pattern oldNamePattern = Pattern.compile(regexPattern);
Mono<PageDTO> updatePageMono = newPageService
Mono<PageDTO> pageMono = newPageService
// fetch the unpublished page
.findPageById(pageId, MANAGE_PAGES, false)
.cache();
Mono<Integer> evalVersionMono = pageMono
.flatMap(page -> {
return applicationService.findById(page.getApplicationId())
.map(application -> {
Integer evaluationVersion = application.getEvaluationVersion();
if (evaluationVersion == null) {
evaluationVersion = EVALUATION_VERSION;
}
return evaluationVersion;
});
})
.cache();
Mono<PageDTO> updatePageMono = Mono.zip(pageMono, evalVersionMono)
.flatMap(tuple -> {
PageDTO page = tuple.getT1();
int evalVersion = tuple.getT2();
List<Layout> layouts = page.getLayouts();
for (Layout layout : layouts) {
if (layoutId.equals(layout.getId()) && layout.getDsl() != null) {
final JsonNode dslNode = objectMapper.convertValue(layout.getDsl(), JsonNode.class);
final JsonNode dslNodeAfterReplacement = this.replaceStringInJsonNode(dslNode, oldNamePattern, newName);
layout.setDsl(objectMapper.convertValue(dslNodeAfterReplacement, JSONObject.class));
// DSL has removed all the old names and replaced it with new name. If the change of name
// was one of the mongoEscaped widgets, then update the names in the set as well
Set<String> mongoEscapedWidgetNames = layout.getMongoEscapedWidgetNames();
@ -174,9 +228,18 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE {
mongoEscapedWidgetNames.remove(oldName);
mongoEscapedWidgetNames.add(newName);
}
page.setLayouts(layouts);
final JsonNode dslNode = objectMapper.convertValue(layout.getDsl(), JsonNode.class);
Mono<PageDTO> refactorNameInDslMono = this.refactorNameInDsl(dslNode, oldName, newName, evalVersion, oldNamePattern)
.then(Mono.fromCallable(() -> {
layout.setDsl(objectMapper.convertValue(dslNode, JSONObject.class));
page.setLayouts(layouts);
return page;
}));
// Since the page has most probably changed, save the page and return.
return newPageService.saveUnpublishedPage(page);
return refactorNameInDslMono
.flatMap(newPageService::saveUnpublishedPage);
}
}
// If we have reached here, the layout was not found and the page should be returned as is.
@ -187,62 +250,76 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE {
Mono<Set<String>> updateActionsMono = newActionService
.findByPageIdAndViewMode(pageId, false, MANAGE_ACTIONS)
.flatMap(newAction -> Mono.just(newAction).zipWith(evalVersionMono))
/*
* Assuming that the datasource should not be dependent on the widget and hence not going through the same
* to look for replacement pattern.
*/
.flatMap(newAction1 -> {
final NewAction newAction = newAction1;
.flatMap(tuple -> {
final NewAction newAction = tuple.getT1();
final Integer evalVersion = tuple.getT2();
// We need actionDTO to be populated with pluginType from NewAction
// so that we can check for the JS path
Mono<ActionDTO> actionMono = newActionService.generateActionByViewMode(newAction, false);
return actionMono.flatMap(action -> {
newAction.setUnpublishedAction(action);
boolean actionUpdateRequired = false;
ActionConfiguration actionConfiguration = action.getActionConfiguration();
Set<String> jsonPathKeys = action.getJsonPathKeys();
if (jsonPathKeys != null && !jsonPathKeys.isEmpty()) {
// Since json path keys actually contain the entire inline js function instead of just the widget/action
// name, we can not simply use the set.contains(obj) function. We need to iterate over all the keys
// in the set and see if the old name is a substring of the json path key.
for (String key : jsonPathKeys) {
if (oldNamePattern.matcher(key).find()) {
actionUpdateRequired = true;
break;
}
}
}
if (!actionUpdateRequired || actionConfiguration == null) {
if (action.getActionConfiguration() == null) {
return Mono.just(newAction);
}
// if actionUpdateRequired is true AND actionConfiguration is not null
if (action.getCollectionId() != null) {
// If this is a JS function rename, add this collection for rename
// because the action configuration won't tell us this
if (StringUtils.hasLength(action.getCollectionId()) && newName.equals(action.getValidName())) {
updatableCollectionIds.add(action.getCollectionId());
}
final JsonNode actionConfigurationNode = objectMapper.convertValue(actionConfiguration, JsonNode.class);
final JsonNode actionConfigurationNodeAfterReplacement = replaceStringInJsonNode(actionConfigurationNode, oldNamePattern, newName);
ActionConfiguration newActionConfiguration = objectMapper.convertValue(actionConfigurationNodeAfterReplacement, ActionConfiguration.class);
action.setActionConfiguration(newActionConfiguration);
NewAction newAction2 = newActionService.extractAndSetJsonPathKeys(newAction);
return newActionService.save(newAction2);
newAction.setUnpublishedAction(action);
return this.refactorNameInAction(action, oldName, newName, evalVersion, oldNamePattern)
.flatMap(updates -> {
if (updates.isEmpty()) {
return Mono.just(newAction);
}
if (StringUtils.hasLength(action.getCollectionId())) {
updatableCollectionIds.add(action.getCollectionId());
}
newActionService.extractAndSetJsonPathKeys(newAction);
return newActionService.save(newAction);
});
});
})
.map(savedAction -> savedAction.getUnpublishedAction().getName())
.collect(toSet())
.flatMap(updatedActions -> {
.zipWith(evalVersionMono)
.flatMap(tuple -> {
Set<String> updatedActions = tuple.getT1();
Integer evalVersion = tuple.getT2();
// If these actions belonged to collections, update the collection body
return Flux.fromIterable(updatableCollectionIds)
.flatMap(collectionId -> actionCollectionService.findById(collectionId, MANAGE_ACTIONS))
.flatMap(actionCollection -> {
final ActionCollectionDTO unpublishedCollection = actionCollection.getUnpublishedCollection();
Matcher matcher = oldNamePattern.matcher(unpublishedCollection.getBody());
String newBodyAsString = matcher.replaceAll(newName);
unpublishedCollection.setBody(newBodyAsString);
return actionCollectionService.save(actionCollection);
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<String> 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));
}
})
.collectList()
.thenReturn(updatedActions);
@ -264,59 +341,248 @@ public class RefactoringSolutionCEImpl implements RefactoringSolutionCE {
});
}
Mono<Set<String>> refactorNameInDsl(JsonNode dsl, String oldName, String newName, int evalVersion, Pattern oldNamePattern) {
private JsonNode replaceStringInJsonNode(JsonNode jsonNode, Pattern oldNamePattern, String newName) {
// If this is a text node, perform replacement directly
if (jsonNode.isTextual()) {
Matcher matcher = oldNamePattern.matcher(jsonNode.asText());
String valueAfterReplacement = matcher.replaceAll(newName);
return new TextNode(valueAfterReplacement);
Mono<Set<String>> refactorNameInWidgetMono = Mono.just(new HashSet<>());
Mono<Set<String>> recursiveRefactorNameInDslMono = Mono.just(new HashSet<>());
// if current object is widget,
if (dsl.has(FieldName.WIDGET_ID)) {
// enter parse widget method
refactorNameInWidgetMono = refactorNameInWidget(dsl, oldName, newName, evalVersion, oldNamePattern);
}
// if current object has children,
if (dsl.has("children")) {
ArrayNode dslChildren = (ArrayNode) dsl.get("children");
// recurse over each child
recursiveRefactorNameInDslMono = Flux.fromStream(StreamSupport.stream(dslChildren.spliterator(), true))
.flatMap(child -> refactorNameInDsl(child, oldName, newName, evalVersion, oldNamePattern))
.reduce(new HashSet<>(), (x, y) -> {
// for each child, aggregate the refactored paths
y.addAll(x);
return y;
});
}
// TODO This is special handling for the list widget that has been added to allow refactoring of
// just the default widgets inside the list. This is required because for the list, the widget names
// exist as keys at the location List1.template(.Text1) [Ref #9281]
// Ideally, we should avoid any non-structural elements as keys. This will be improved in list widget v2
if (jsonNode.has("type") && "LIST_WIDGET".equals(jsonNode.get("type").asText())) {
final JsonNode template = jsonNode.get("template");
return refactorNameInWidgetMono
.zipWith(recursiveRefactorNameInDslMono)
.map(tuple -> {
tuple.getT1().addAll(tuple.getT2());
return tuple.getT1();
});
}
Mono<Set<String>> refactorNameInWidget(JsonNode widgetDsl, String oldName, String newName, int evalVersion, Pattern oldNamePattern) {
boolean isRefactoredWidget = false;
boolean isRefactoredTemplate = false;
String widgetName = "";
// If the name of this widget matches the old name, replace the name
if (widgetDsl.has(FieldName.WIDGET_NAME)) {
widgetName = widgetDsl.get(FieldName.WIDGET_NAME).asText();
if (oldName.equals(widgetName)) {
((ObjectNode) widgetDsl).set(FieldName.WIDGET_NAME, new TextNode(newName));
// We mark this widget name as being a path that was refactored using this boolean value
isRefactoredWidget = true;
}
}
// This is special handling for the list widget that has been added to allow refactoring of
// just the default widgets inside the list. This is required because for the list, the widget names
// exist as keys at the location List1.template(.Text1) [Ref #9281]
// Ideally, we should avoid any non-structural elements as keys. This will be improved in list widget v2
if (widgetDsl.has(FieldName.WIDGET_TYPE) && FieldName.LIST_WIDGET.equals(widgetDsl.get(FieldName.WIDGET_TYPE).asText())) {
final JsonNode template = widgetDsl.get(FieldName.LIST_WIDGET_TEMPLATE);
JsonNode newJsonNode = null;
String fieldName = null;
final Iterator<String> templateIterator = template.fieldNames();
while (templateIterator.hasNext()) {
fieldName = templateIterator.next();
// For each element within template, check whether it would match the replacement pattern
final Matcher listWidgetTemplateKeyMatcher = oldNamePattern.matcher(fieldName);
if (listWidgetTemplateKeyMatcher.find()) {
if (oldName.equals(fieldName)) {
newJsonNode = template.get(fieldName);
break;
}
}
if (newJsonNode != null) {
// If we are here, it means that the widget being refactored was from a list widget template
// Go ahead and refactor this template as well
((ObjectNode) newJsonNode).set(FieldName.WIDGET_NAME, new TextNode(newName));
// If such a pattern is found, remove that element and attach it back with the new name
((ObjectNode) template).remove(fieldName);
((ObjectNode) template).set(newName, newJsonNode);
// We mark this template path as being a path that was refactored using this boolean value
isRefactoredTemplate = true;
}
}
final Iterator<Map.Entry<String, JsonNode>> iterator = jsonNode.fields();
// Go through each field to recursively operate on it
while (iterator.hasNext()) {
final Map.Entry<String, JsonNode> next = iterator.next();
final JsonNode value = next.getValue();
if (value.isArray()) {
// If this field is an array type, iterate through each element and perform replacement
final ArrayNode arrayNode = (ArrayNode) value;
final ArrayNode newArrayNode = objectMapper.createArrayNode();
arrayNode.forEach(x -> newArrayNode.add(replaceStringInJsonNode(x, oldNamePattern, newName)));
// Make this array node created from replaced values the new value
next.setValue(newArrayNode);
} else {
// This is either directly a text node or another json node
// In either case, recurse over the entire value to get the replaced value
next.setValue(replaceStringInJsonNode(value, oldNamePattern, newName));
Mono<Set<String>> refactorDynamicBindingsMono = Mono.just(new HashSet<>());
Mono<Set<String>> refactorTriggerBindingsMono = Mono.just(new HashSet<>());
// If there are dynamic bindings in this action configuration, inspect them
if (widgetDsl.has(FieldName.DYNAMIC_BINDING_PATH_LIST) && !widgetDsl.get(FieldName.DYNAMIC_BINDING_PATH_LIST).isEmpty()) {
ArrayNode dslDynamicBindingPathList = (ArrayNode) widgetDsl.get(FieldName.DYNAMIC_BINDING_PATH_LIST);
// recurse over each child
refactorDynamicBindingsMono = refactorBindingsUsingBindingPaths(
widgetDsl,
oldName,
newName,
evalVersion,
oldNamePattern,
dslDynamicBindingPathList,
widgetName);
}
// If there are dynamic triggers in this action configuration, inspect them
if (widgetDsl.has(FieldName.DYNAMIC_TRIGGER_PATH_LIST) && !widgetDsl.get(FieldName.DYNAMIC_TRIGGER_PATH_LIST).isEmpty()) {
ArrayNode dslDynamicTriggerPathList = (ArrayNode) widgetDsl.get(FieldName.DYNAMIC_TRIGGER_PATH_LIST);
// recurse over each child
refactorTriggerBindingsMono = refactorBindingsUsingBindingPaths(
widgetDsl,
oldName,
newName,
evalVersion,
oldNamePattern,
dslDynamicTriggerPathList,
widgetName);
}
final String finalWidgetNamePath = widgetName + ".widgetName";
final boolean finalIsRefactoredWidget = isRefactoredWidget;
final boolean finalIsRefactoredTemplate = isRefactoredTemplate;
final String finalWidgetTemplatePath = widgetName + ".template";
return refactorDynamicBindingsMono.zipWith(refactorTriggerBindingsMono)
.map(tuple -> {
tuple.getT1().addAll(tuple.getT2());
return tuple.getT1();
})
.map(refactoredBindings -> {
if (Boolean.TRUE.equals(finalIsRefactoredWidget)) {
refactoredBindings.add(finalWidgetNamePath);
}
if (Boolean.TRUE.equals(finalIsRefactoredTemplate)) {
refactoredBindings.add(finalWidgetTemplatePath);
}
return refactoredBindings;
});
}
@NotNull
private Mono<Set<String>> refactorBindingsUsingBindingPaths(JsonNode widgetDsl, String oldName, String newName, int evalVersion, Pattern oldNamePattern, ArrayNode bindingPathList, String widgetName) {
Mono<Set<String>> refactorBindingsMono;
refactorBindingsMono = Flux.fromStream(StreamSupport.stream(bindingPathList.spliterator(), true))
.flatMap(bindingPath -> {
String key = bindingPath.get(FieldName.KEY).asText();
// This is inside a list widget, and the path starts with template.<oldName>,
// We need to update the binding path list entry itself as well
if (widgetDsl.has(FieldName.WIDGET_TYPE) &&
FieldName.LIST_WIDGET.equals(widgetDsl.get(FieldName.WIDGET_TYPE).asText()) &&
key.startsWith("template." + oldName)) {
key = key.replace(oldName, newName);
((ObjectNode) bindingPath).set(FieldName.KEY, new TextNode(key));
}
// Find values inside mustache bindings in this path
Set<String> mustacheValues = DslUtils.getMustacheValueSetFromSpecificDynamicBindingPath(widgetDsl, key);
final String finalKey = key;
// Perform refactor for each mustache value
return this.replaceValueInMustacheKeys(mustacheValues, oldName, newName, evalVersion, oldNamePattern)
.flatMap(replacementMap -> {
if (replacementMap.isEmpty()) {
// If the map is empty, it means that this path did not have anything that had to be refactored
return Mono.empty();
}
// Replace the binding path value with the new mustache values
DslUtils.replaceValuesInSpecificDynamicBindingPath(widgetDsl, finalKey, replacementMap);
// Mark this path as refactored
String entityPath = StringUtils.hasLength(widgetName) ? widgetName + "." : "";
return Mono.just(entityPath + finalKey);
});
})
.collect(Collectors.toSet());
return refactorBindingsMono;
}
Mono<Set<String>> refactorNameInAction(ActionDTO actionDTO, String oldName, String newName,
int evalVersion, Pattern oldNamePattern) {
// 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)) {
Set<String> jsonPathKeys = actionDTO.getJsonPathKeys();
boolean isReferenceFound = false;
if (jsonPathKeys != null && !jsonPathKeys.isEmpty()) {
// Since json path keys actually contain the entire inline js function instead of just the widget/action
// name, we can not simply use the set.contains(obj) function. We need to iterate over all the keys
// in the set and see if the old name is a substring of the json path key.
for (String key : jsonPathKeys) {
if (oldNamePattern.matcher(key).find()) {
isReferenceFound = true;
break;
}
}
}
// If no reference was found, return with an empty set
if (Boolean.FALSE.equals(isReferenceFound)) {
return Mono.just(new HashSet<>());
}
}
return jsonNode;
ActionConfiguration actionConfiguration = actionDTO.getActionConfiguration();
final JsonNode actionConfigurationNode = objectMapper.convertValue(actionConfiguration, JsonNode.class);
Mono<Set<String>> refactorDynamicBindingsMono = Mono.just(new HashSet<>());
// If there are dynamic bindings in this action configuration, inspect them
if (actionDTO.getDynamicBindingPathList() != null && !actionDTO.getDynamicBindingPathList().isEmpty()) {
// recurse over each child
refactorDynamicBindingsMono = Flux.fromIterable(actionDTO.getDynamicBindingPathList())
.flatMap(dynamicBindingPath -> {
String key = dynamicBindingPath.getKey();
Set<String> mustacheValues = new HashSet<>();
if (PluginType.JS.equals(actionDTO.getPluginType()) && "body".equals(key)) {
mustacheValues.add(actionConfiguration.getBody());
} else {
mustacheValues = DslUtils.getMustacheValueSetFromSpecificDynamicBindingPath(actionConfigurationNode, key);
}
return this.replaceValueInMustacheKeys(mustacheValues, oldName, newName, evalVersion, oldNamePattern)
.flatMap(replacementMap -> {
if (replacementMap.isEmpty()) {
return Mono.empty();
}
DslUtils.replaceValuesInSpecificDynamicBindingPath(actionConfigurationNode, key, replacementMap);
String entityPath = StringUtils.hasLength(actionDTO.getValidName()) ? actionDTO.getValidName() + "." : "";
return Mono.just(entityPath + key);
});
})
.collect(Collectors.toSet())
.map(entityPaths -> {
actionDTO.setActionConfiguration(objectMapper.convertValue(actionConfigurationNode, ActionConfiguration.class));
return entityPaths;
});
}
return refactorDynamicBindingsMono;
}
Mono<Map<String, String>> replaceValueInMustacheKeys(Set<String> 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, oldNamePattern, newName);
}
Mono<Map<String, String>> replaceValueInMustacheKeys(Set<String> mustacheKeySet, Pattern
oldNamePattern, String newName) {
return Flux.fromIterable(mustacheKeySet)
.flatMap(mustacheKey -> {
Matcher matcher = oldNamePattern.matcher(mustacheKey);
if (matcher.find()) {
return Mono.zip(Mono.just(mustacheKey), Mono.just(matcher.replaceAll(newName)));
}
return Mono.empty();
})
.collectMap(Tuple2::getT1, Tuple2::getT2);
}
}

View File

@ -301,6 +301,7 @@ public class ActionCollectionServiceTest {
action1.getActionConfiguration().setBody("mockBody");
actionCollectionDTO1.setActions(List.of(action1));
actionCollectionDTO1.setPluginType(PluginType.JS);
actionCollectionDTO1.setBody("export default { x: 1 }");
final ActionCollectionDTO createdActionCollectionDTO1 = layoutCollectionService.createCollection(actionCollectionDTO1).block();
@ -316,7 +317,7 @@ public class ActionCollectionServiceTest {
action2.getActionConfiguration().setBody("testCollection1.testAction1()");
actionCollectionDTO2.setActions(List.of(action2));
actionCollectionDTO2.setPluginType(PluginType.JS);
actionCollectionDTO2.setBody("testCollection1.testAction1()");
actionCollectionDTO2.setBody("export default { x: testCollection1.testAction1() }");
final ActionCollectionDTO createdActionCollectionDTO2 = layoutCollectionService.createCollection(actionCollectionDTO2).block();
@ -337,7 +338,7 @@ public class ActionCollectionServiceTest {
StepVerifier.create(actionCollectionMono)
.assertNext(actionCollection -> {
assertEquals(
"testCollection1.newTestAction1()",
"export default { x: testCollection1.newTestAction1() }",
actionCollection.getUnpublishedCollection().getBody()
);
})
@ -379,6 +380,7 @@ public class ActionCollectionServiceTest {
action1.getActionConfiguration().setBody("mockBody");
actionCollectionDTO1.setActions(List.of(action1));
actionCollectionDTO1.setPluginType(PluginType.JS);
actionCollectionDTO1.setBody("export default { x: 1 }");
final ActionCollectionDTO createdActionCollectionDTO1 = layoutCollectionService.createCollection(actionCollectionDTO1).block();
@ -394,7 +396,7 @@ public class ActionCollectionServiceTest {
action2.getActionConfiguration().setBody("Api1.run()");
actionCollectionDTO2.setActions(List.of(action2));
actionCollectionDTO2.setPluginType(PluginType.JS);
actionCollectionDTO2.setBody("Api1.run()");
actionCollectionDTO2.setBody("export default { x: Api1.run() }");
final ActionCollectionDTO createdActionCollectionDTO2 = layoutCollectionService.createCollection(actionCollectionDTO2).block();
@ -415,7 +417,7 @@ public class ActionCollectionServiceTest {
StepVerifier.create(actionCollectionMono)
.assertNext(actionCollection -> {
assertEquals(
"Api1.run()",
"export default { x: Api1.run() }",
actionCollection.getUnpublishedCollection().getBody()
);
})

View File

@ -0,0 +1,135 @@
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.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.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.io.IOException;
import java.io.InputStream;
import java.util.Set;
import java.util.regex.Pattern;
@ExtendWith(SpringExtension.class)
@Slf4j
class RefactoringSolutionCEImplTest {
RefactoringSolutionCEImpl refactoringSolutionCE;
@MockBean
private ObjectMapper objectMapper;
@MockBean
private NewPageService newPageService;
@MockBean
private NewActionService newActionService;
@MockBean
private ActionCollectionService actionCollectionService;
@MockBean
private ResponseUtils responseUtils;
@MockBean
private LayoutActionService layoutActionService;
@MockBean
private ApplicationService applicationService;
@MockBean
private AstService astService;
@MockBean
private InstanceConfig instanceConfig;
ObjectMapper mapper = new ObjectMapper();
private final String preWord = "\\b(";
private final String postWord = ")\\b";
@BeforeEach
public void setUp() {
refactoringSolutionCE = new RefactoringSolutionCEImpl(objectMapper,
newPageService,
newActionService,
actionCollectionService,
responseUtils,
layoutActionService,
applicationService,
astService,
instanceConfig);
}
@Test
void testRefactorNameInDsl_whenRenamingTextWidget_replacesAllReferences() {
try (InputStream initialStream = this.getClass().getResourceAsStream("refactorDslWithOnlyWidgets.json");
InputStream finalStream = this.getClass().getResourceAsStream("refactorDslWithOnlyWidgetsWithNewText.json")) {
assert initialStream != null;
JsonNode dslAsJsonNode = mapper.readTree(initialStream);
final String oldName = "Text3";
Mono<Set<String>> updatesMono = refactoringSolutionCE.refactorNameInDsl(
dslAsJsonNode,
oldName,
"newText",
2,
Pattern.compile(preWord + oldName + postWord));
StepVerifier.create(updatesMono)
.assertNext(updatedPaths -> {
Assertions.assertThat(updatedPaths).hasSize(3);
Assertions.assertThat(updatedPaths).containsExactlyInAnyOrder(
"Text3.widgetName",
"List1.template",
"List1.onListItemClick");
})
.verifyComplete();
JsonNode finalDslAsJsonNode = mapper.readTree(finalStream);
Assertions.assertThat(dslAsJsonNode).isEqualTo(finalDslAsJsonNode);
} catch (IOException e) {
Assertions.fail("Unexpected IOException", e);
}
}
@Test
void testRefactorNameInDsl_whenRenamingListWidget_replacesTemplateReferences() {
try (InputStream initialStream = this.getClass().getResourceAsStream("refactorDslWithOnlyWidgets.json");
InputStream finalStream = this.getClass().getResourceAsStream("refactorDslWithOnlyWidgetsWithNewList.json")) {
assert initialStream != null;
JsonNode dslAsJsonNode = mapper.readTree(initialStream);
final String oldName = "List1";
Mono<Set<String>> updatesMono = refactoringSolutionCE.refactorNameInDsl(
dslAsJsonNode,
oldName,
"newList",
2,
Pattern.compile(preWord + oldName + postWord));
StepVerifier.create(updatesMono)
.assertNext(updatedPaths -> {
Assertions.assertThat(updatedPaths).hasSize(4);
Assertions.assertThat(updatedPaths).containsExactlyInAnyOrder(
"List1.widgetName",
"List1.template.Text4.text",
"List1.template.Image1.image",
"List1.template.Text3.text");
})
.verifyComplete();
JsonNode finalDslAsJsonNode = mapper.readTree(finalStream);
Assertions.assertThat(dslAsJsonNode).isEqualTo(finalDslAsJsonNode);
} catch (IOException e) {
Assertions.fail("Unexpected IOException", e);
}
}
}

View File

@ -4,6 +4,7 @@ import com.appsmith.external.models.ActionConfiguration;
import com.appsmith.external.models.ActionDTO;
import com.appsmith.external.models.Datasource;
import com.appsmith.external.models.PluginType;
import com.appsmith.external.models.Property;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.ActionCollection;
import com.appsmith.server.domains.Application;
@ -247,9 +248,12 @@ class RefactoringSolutionCETest {
action.setDatasource(datasource);
JSONObject dsl = new JSONObject();
dsl.put("widgetId", "firstWidgetId");
dsl.put("widgetName", "firstWidget");
JSONArray temp = new JSONArray();
temp.addAll(List.of(new JSONObject(Map.of("key", "testField"))));
temp.addAll(List.of(new JSONObject(Map.of("key", "innerArrayReference[0].innerK")),
new JSONObject(Map.of("key", "innerObjectReference.k")),
new JSONObject(Map.of("key", "testField"))));
dsl.put("dynamicBindingPathList", temp);
dsl.put("testField", "{{ \tbeforeNameChange.data }}");
final JSONObject innerObjectReference = new JSONObject();
@ -311,9 +315,12 @@ class RefactoringSolutionCETest {
action.setDatasource(datasource);
JSONObject dsl = new JSONObject();
dsl.put("widgetId", "firstWidgetId");
dsl.put("widgetName", "firstWidget");
JSONArray temp = new JSONArray();
temp.addAll(List.of(new JSONObject(Map.of("key", "testField"))));
temp.addAll(List.of(new JSONObject(Map.of("key", "innerArrayReference[0].innerK")),
new JSONObject(Map.of("key", "innerObjectReference.k")),
new JSONObject(Map.of("key", "testField"))));
dsl.put("dynamicBindingPathList", temp);
dsl.put("testField", "{{ \tbeforeNameChange.data }}");
final JSONObject innerObjectReference = new JSONObject();
@ -477,6 +484,7 @@ class RefactoringSolutionCETest {
action.setDatasource(datasource);
JSONObject dsl = new JSONObject();
dsl.put("widgetId", "firstWidgetId");
dsl.put("widgetName", "firstWidget");
JSONArray temp = new JSONArray();
temp.addAll(List.of(new JSONObject(Map.of("key", "testField"))));
@ -545,6 +553,7 @@ class RefactoringSolutionCETest {
Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor()));
JSONObject dsl = new JSONObject();
dsl.put("widgetId", "testId");
dsl.put("widgetName", "Table1");
dsl.put("type", "TABLE_WIDGET");
Map primaryColumns = new HashMap<String, Object>();
@ -591,6 +600,7 @@ class RefactoringSolutionCETest {
Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor()));
JSONObject dsl = new JSONObject();
dsl.put("widgetId", "simpleRefactorId");
dsl.put("widgetName", "Table1");
dsl.put("type", "TABLE_WIDGET");
Layout layout = testPage.getLayouts().get(0);
@ -626,13 +636,17 @@ class RefactoringSolutionCETest {
Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())).thenReturn(Mono.just(new MockPluginExecutor()));
JSONObject dsl = new JSONObject();
dsl.put("widgetId", "testId");
dsl.put("widgetName", "List1");
dsl.put("type", "LIST_WIDGET");
JSONObject template = new JSONObject();
template.put("oldWidgetName", "irrelevantContent");
JSONObject oldWidgetTemplate = new JSONObject();
oldWidgetTemplate.put("widgetName", "oldWidgetName");
template.put("oldWidgetName", oldWidgetTemplate);
dsl.put("template", template);
final JSONArray children = new JSONArray();
final JSONObject defaultWidget = new JSONObject();
defaultWidget.put("widgetId", "testId2");
defaultWidget.put("widgetName", "oldWidgetName");
defaultWidget.put("type", "TEXT_WIDGET");
children.add(defaultWidget);
@ -648,7 +662,7 @@ class RefactoringSolutionCETest {
refactorNameDTO.setOldName("oldWidgetName");
refactorNameDTO.setNewName("newWidgetName");
Mono<LayoutDTO> widgetRenameMono = refactoringSolution.refactorWidgetName(refactorNameDTO).cache();
Mono<LayoutDTO> widgetRenameMono = refactoringSolution.refactorWidgetName(refactorNameDTO);
StepVerifier
.create(widgetRenameMono)
@ -667,6 +681,7 @@ class RefactoringSolutionCETest {
// Set up table widget in DSL
JSONObject dsl = new JSONObject();
dsl.put("widgetId", "testId");
dsl.put("widgetName", "Table1");
dsl.put("type", "TABLE_WIDGET");
Layout layout = testPage.getLayouts().get(0);
@ -681,11 +696,16 @@ class RefactoringSolutionCETest {
actionCollectionDTO1.setApplicationId(testApp.getId());
actionCollectionDTO1.setWorkspaceId(testApp.getWorkspaceId());
actionCollectionDTO1.setPluginId(jsDatasource.getPluginId());
ActionDTO action1 = new ActionDTO();
action1.setName("testAction1");
action1.setActionConfiguration(new ActionConfiguration());
action1.getActionConfiguration().setBody("\tTable1");
actionCollectionDTO1.setBody("\tTable1");
action1.setDynamicBindingPathList(List.of(new Property("body", null)));
action1.setPluginType(PluginType.JS);
actionCollectionDTO1.setActions(List.of(action1));
actionCollectionDTO1.setBody("export default { x : \tTable1 }");
actionCollectionDTO1.setActions(List.of(action1));
actionCollectionDTO1.setPluginType(PluginType.JS);
@ -710,7 +730,7 @@ class RefactoringSolutionCETest {
.assertNext(tuple -> {
final ActionCollection actionCollection = tuple.getT1();
final NewAction action = tuple.getT2();
assertThat(actionCollection.getUnpublishedCollection().getBody()).isEqualTo("\tNewNameTable1");
assertThat(actionCollection.getUnpublishedCollection().getBody()).isEqualTo("export default { x : \tNewNameTable1 }");
final ActionDTO unpublishedAction = action.getUnpublishedAction();
assertThat(unpublishedAction.getJsonPathKeys().size()).isEqualTo(1);
final Optional<String> first = unpublishedAction.getJsonPathKeys().stream().findFirst();
@ -734,6 +754,7 @@ class RefactoringSolutionCETest {
originalActionCollectionDTO.setPageId(testPage.getId());
originalActionCollectionDTO.setPluginId(jsDatasource.getPluginId());
originalActionCollectionDTO.setPluginType(PluginType.JS);
originalActionCollectionDTO.setBody("export default { x: 1 }");
ActionDTO action1 = new ActionDTO();
action1.setName("testAction1");
@ -747,7 +768,7 @@ class RefactoringSolutionCETest {
ActionCollectionDTO actionCollectionDTO = new ActionCollectionDTO();
assert dto != null;
actionCollectionDTO.setId(dto.getId());
actionCollectionDTO.setBody("body");
actionCollectionDTO.setBody("export default { x: Table1 }");
actionCollectionDTO.setName("newName");
RefactorActionNameInCollectionDTO refactorActionNameInCollectionDTO = new RefactorActionNameInCollectionDTO();
@ -772,7 +793,7 @@ class RefactoringSolutionCETest {
final ActionCollectionDTO actionCollectionDTOResult = tuple.getT1().getUnpublishedCollection();
final NewAction newAction = tuple.getT2();
assertEquals("originalName", actionCollectionDTOResult.getName());
assertEquals("body", actionCollectionDTOResult.getBody());
assertEquals("export default { x: Table1 }", actionCollectionDTOResult.getBody());
assertEquals("newTestAction", newAction.getUnpublishedAction().getName());
assertEquals("originalName.newTestAction", newAction.getUnpublishedAction().getFullyQualifiedName());
})

View File

@ -0,0 +1,374 @@
{
"widgetName": "MainContainer",
"widgetId": "0",
"type": "CANVAS_WIDGET",
"dynamicBindingPathList": [],
"children": [
{
"widgetName": "Text1",
"type": "TEXT_WIDGET",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"dynamicBindingPathList": [
{
"key": "fontFamily"
},
{
"key": "borderRadius"
}
],
"text": "Label",
"key": "3pqpn28ba4",
"widgetId": "wemfst2t7m",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
{
"widgetName": "Text2",
"type": "TEXT_WIDGET",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"dynamicBindingPathList": [
{
"key": "fontFamily"
},
{
"key": "borderRadius"
},
{
"key": "text"
}
],
"text": "{{Text1.text}}",
"key": "3pqpn28ba4",
"widgetId": "2bensj901c",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
{
"template": {
"Image1": {
"image": "{{List1.listData.map((currentItem) => currentItem.img)}}",
"widgetName": "Image1",
"type": "IMAGE_WIDGET",
"key": "e0c7wcn17q",
"dynamicBindingPathList": [
{
"key": "image"
},
{
"key": "borderRadius"
}
],
"widgetId": "bvixbymoxr",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"Text3": {
"text": "{{List1.listData.map((currentItem) => currentItem.name)}}",
"widgetName": "Text3",
"type": "TEXT_WIDGET",
"key": "3pqpn28ba4",
"dynamicBindingPathList": [
{
"key": "text"
},
{
"key": "fontFamily"
},
{
"key": "borderRadius"
}
],
"widgetId": "6ox4ujv63y",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"Text4": {
"text": "{{List1.listData.map((currentItem, currentIndex) => {\n return (function(){\n return currentItem.id + Text1.text;\n })();\n })}}",
"widgetName": "Text4",
"type": "TEXT_WIDGET",
"key": "3pqpn28ba4",
"dynamicBindingPathList": [
{
"key": "text"
},
{
"key": "fontFamily"
},
{
"key": "borderRadius"
}
],
"widgetId": "rtlyvpkvhc",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
},
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}",
"widgetName": "List1",
"type": "LIST_WIDGET",
"dynamicBindingPathList": [
{
"key": "accentColor"
},
{
"key": "borderRadius"
},
{
"key": "boxShadow"
},
{
"key": "template.Image1.image"
},
{
"key": "template.Text3.text"
},
{
"key": "template.Text4.text"
}
],
"dynamicTriggerPathList": [
{
"key": "onListItemClick"
}
],
"onListItemClick": "{{Text3.text}}",
"children": [
{
"widgetName": "Canvas1",
"type": "CANVAS_WIDGET",
"dynamicBindingPathList": [
{
"key": "borderRadius"
},
{
"key": "accentColor"
}
],
"children": [
{
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}",
"widgetName": "Container1",
"dynamicBindingPathList": [
{
"key": "borderRadius"
},
{
"key": "boxShadow"
}
],
"children": [
{
"widgetName": "Canvas2",
"type": "CANVAS_WIDGET",
"dynamicBindingPathList": [
{
"key": "borderRadius"
},
{
"key": "accentColor"
}
],
"children": [
{
"widgetName": "Image1",
"type": "IMAGE_WIDGET",
"dynamicBindingPathList": [
{
"key": "image"
},
{
"key": "borderRadius"
}
],
"key": "e0c7wcn17q",
"image": "{{currentItem.img}}",
"widgetId": "bvixbymoxr",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
{
"widgetName": "Text3",
"type": "TEXT_WIDGET",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"dynamicBindingPathList": [
{
"key": "text"
},
{
"key": "fontFamily"
},
{
"key": "borderRadius"
}
],
"text": "{{currentItem.name}}",
"key": "3pqpn28ba4",
"widgetId": "6ox4ujv63y",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
{
"widgetName": "Text4",
"type": "TEXT_WIDGET",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"dynamicBindingPathList": [
{
"key": "text"
},
{
"key": "fontFamily"
},
{
"key": "borderRadius"
}
],
"text": "{{currentItem.id + Text1.text}}",
"key": "3pqpn28ba4",
"widgetId": "rtlyvpkvhc",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
],
"key": "3m0y9rrh1o",
"widgetId": "zdz4f503fm",
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
],
"key": "sca9shlkpb",
"widgetId": "vt8i2g9u5r",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
],
"key": "3m0y9rrh1o",
"widgetId": "ki75z4pfxm",
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
],
"key": "t35n4gddpu",
"widgetId": "bunz1f076j",
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
{
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}",
"type": "TABLE_WIDGET_V2",
"dynamicBindingPathList": [
{
"key": "primaryColumns.step.computedValue"
},
{
"key": "primaryColumns.task.computedValue"
},
{
"key": "primaryColumns.status.computedValue"
},
{
"key": "primaryColumns.action.computedValue"
},
{
"key": "primaryColumns.action.buttonColor"
},
{
"key": "primaryColumns.action.borderRadius"
},
{
"key": "primaryColumns.action.boxShadow"
},
{
"key": "accentColor"
},
{
"key": "borderRadius"
},
{
"key": "boxShadow"
},
{
"key": "childStylesheet.button.buttonColor"
},
{
"key": "childStylesheet.button.borderRadius"
},
{
"key": "childStylesheet.menuButton.menuColor"
},
{
"key": "childStylesheet.menuButton.borderRadius"
},
{
"key": "childStylesheet.iconButton.buttonColor"
},
{
"key": "childStylesheet.iconButton.borderRadius"
},
{
"key": "childStylesheet.editActions.saveButtonColor"
},
{
"key": "childStylesheet.editActions.saveBorderRadius"
},
{
"key": "childStylesheet.editActions.discardButtonColor"
},
{
"key": "childStylesheet.editActions.discardBorderRadius"
}
],
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
"childStylesheet": {
"button": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"menuButton": {
"menuColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"iconButton": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"editActions": {
"saveButtonColor": "{{appsmith.theme.colors.primaryColor}}",
"saveBorderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"discardButtonColor": "{{appsmith.theme.colors.primaryColor}}",
"discardBorderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
},
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"widgetName": "Table1",
"primaryColumns": {
"step": {
"id": "step",
"originalId": "step",
"alias": "step",
"label": "step",
"computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"step\"]))}}"
},
"task": {
"id": "task",
"originalId": "task",
"alias": "task",
"label": "task",
"computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( Text1.text + \" \" + currentRow[\"task\"]))}}"
},
"status": {
"id": "status",
"originalId": "status",
"alias": "status",
"label": "status",
"computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"status\"]))}}"
},
"action": {
"id": "action",
"originalId": "action",
"alias": "action",
"label": "action",
"onClick": "{{currentRow.step === '#1' ? showAlert('Done', 'success') : currentRow.step === '#2' ? navigateTo('https://docs.appsmith.com/core-concepts/connecting-to-data-sources/querying-a-database',undefined,'NEW_WINDOW') : navigateTo('https://docs.appsmith.com/core-concepts/displaying-data-read/display-data-tables',undefined,'NEW_WINDOW')}}",
"computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"action\"]))}}",
"buttonColor": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( appsmith.theme.colors.primaryColor))}}",
"borderRadius": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( appsmith.theme.borderRadius.appBorderRadius))}}",
"boxShadow": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( 'none'))}}"
}
},
"key": "ouqfcjyuwa",
"widgetId": "vrcp6kbiz8"
}
]
}

View File

@ -0,0 +1,374 @@
{
"widgetName": "MainContainer",
"widgetId": "0",
"type": "CANVAS_WIDGET",
"dynamicBindingPathList": [],
"children": [
{
"widgetName": "Text1",
"type": "TEXT_WIDGET",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"dynamicBindingPathList": [
{
"key": "fontFamily"
},
{
"key": "borderRadius"
}
],
"text": "Label",
"key": "3pqpn28ba4",
"widgetId": "wemfst2t7m",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
{
"widgetName": "Text2",
"type": "TEXT_WIDGET",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"dynamicBindingPathList": [
{
"key": "fontFamily"
},
{
"key": "borderRadius"
},
{
"key": "text"
}
],
"text": "{{Text1.text}}",
"key": "3pqpn28ba4",
"widgetId": "2bensj901c",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
{
"template": {
"Image1": {
"image": "{{newList.listData.map((currentItem) => currentItem.img)}}",
"widgetName": "Image1",
"type": "IMAGE_WIDGET",
"key": "e0c7wcn17q",
"dynamicBindingPathList": [
{
"key": "image"
},
{
"key": "borderRadius"
}
],
"widgetId": "bvixbymoxr",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"Text3": {
"text": "{{newList.listData.map((currentItem) => currentItem.name)}}",
"widgetName": "Text3",
"type": "TEXT_WIDGET",
"key": "3pqpn28ba4",
"dynamicBindingPathList": [
{
"key": "text"
},
{
"key": "fontFamily"
},
{
"key": "borderRadius"
}
],
"widgetId": "6ox4ujv63y",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"Text4": {
"text": "{{newList.listData.map((currentItem, currentIndex) => {\n return (function(){\n return currentItem.id + Text1.text;\n })();\n })}}",
"widgetName": "Text4",
"type": "TEXT_WIDGET",
"key": "3pqpn28ba4",
"dynamicBindingPathList": [
{
"key": "text"
},
{
"key": "fontFamily"
},
{
"key": "borderRadius"
}
],
"widgetId": "rtlyvpkvhc",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
},
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}",
"widgetName": "newList",
"type": "LIST_WIDGET",
"dynamicBindingPathList": [
{
"key": "accentColor"
},
{
"key": "borderRadius"
},
{
"key": "boxShadow"
},
{
"key": "template.Image1.image"
},
{
"key": "template.Text3.text"
},
{
"key": "template.Text4.text"
}
],
"dynamicTriggerPathList": [
{
"key": "onListItemClick"
}
],
"onListItemClick": "{{Text3.text}}",
"children": [
{
"widgetName": "Canvas1",
"type": "CANVAS_WIDGET",
"dynamicBindingPathList": [
{
"key": "borderRadius"
},
{
"key": "accentColor"
}
],
"children": [
{
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}",
"widgetName": "Container1",
"dynamicBindingPathList": [
{
"key": "borderRadius"
},
{
"key": "boxShadow"
}
],
"children": [
{
"widgetName": "Canvas2",
"type": "CANVAS_WIDGET",
"dynamicBindingPathList": [
{
"key": "borderRadius"
},
{
"key": "accentColor"
}
],
"children": [
{
"widgetName": "Image1",
"type": "IMAGE_WIDGET",
"dynamicBindingPathList": [
{
"key": "image"
},
{
"key": "borderRadius"
}
],
"key": "e0c7wcn17q",
"image": "{{currentItem.img}}",
"widgetId": "bvixbymoxr",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
{
"widgetName": "Text3",
"type": "TEXT_WIDGET",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"dynamicBindingPathList": [
{
"key": "text"
},
{
"key": "fontFamily"
},
{
"key": "borderRadius"
}
],
"text": "{{currentItem.name}}",
"key": "3pqpn28ba4",
"widgetId": "6ox4ujv63y",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
{
"widgetName": "Text4",
"type": "TEXT_WIDGET",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"dynamicBindingPathList": [
{
"key": "text"
},
{
"key": "fontFamily"
},
{
"key": "borderRadius"
}
],
"text": "{{currentItem.id + Text1.text}}",
"key": "3pqpn28ba4",
"widgetId": "rtlyvpkvhc",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
],
"key": "3m0y9rrh1o",
"widgetId": "zdz4f503fm",
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
],
"key": "sca9shlkpb",
"widgetId": "vt8i2g9u5r",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
],
"key": "3m0y9rrh1o",
"widgetId": "ki75z4pfxm",
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
],
"key": "t35n4gddpu",
"widgetId": "bunz1f076j",
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
{
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}",
"type": "TABLE_WIDGET_V2",
"dynamicBindingPathList": [
{
"key": "primaryColumns.step.computedValue"
},
{
"key": "primaryColumns.task.computedValue"
},
{
"key": "primaryColumns.status.computedValue"
},
{
"key": "primaryColumns.action.computedValue"
},
{
"key": "primaryColumns.action.buttonColor"
},
{
"key": "primaryColumns.action.borderRadius"
},
{
"key": "primaryColumns.action.boxShadow"
},
{
"key": "accentColor"
},
{
"key": "borderRadius"
},
{
"key": "boxShadow"
},
{
"key": "childStylesheet.button.buttonColor"
},
{
"key": "childStylesheet.button.borderRadius"
},
{
"key": "childStylesheet.menuButton.menuColor"
},
{
"key": "childStylesheet.menuButton.borderRadius"
},
{
"key": "childStylesheet.iconButton.buttonColor"
},
{
"key": "childStylesheet.iconButton.borderRadius"
},
{
"key": "childStylesheet.editActions.saveButtonColor"
},
{
"key": "childStylesheet.editActions.saveBorderRadius"
},
{
"key": "childStylesheet.editActions.discardButtonColor"
},
{
"key": "childStylesheet.editActions.discardBorderRadius"
}
],
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
"childStylesheet": {
"button": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"menuButton": {
"menuColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"iconButton": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"editActions": {
"saveButtonColor": "{{appsmith.theme.colors.primaryColor}}",
"saveBorderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"discardButtonColor": "{{appsmith.theme.colors.primaryColor}}",
"discardBorderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
},
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"widgetName": "Table1",
"primaryColumns": {
"step": {
"id": "step",
"originalId": "step",
"alias": "step",
"label": "step",
"computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"step\"]))}}"
},
"task": {
"id": "task",
"originalId": "task",
"alias": "task",
"label": "task",
"computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( Text1.text + \" \" + currentRow[\"task\"]))}}"
},
"status": {
"id": "status",
"originalId": "status",
"alias": "status",
"label": "status",
"computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"status\"]))}}"
},
"action": {
"id": "action",
"originalId": "action",
"alias": "action",
"label": "action",
"onClick": "{{currentRow.step === '#1' ? showAlert('Done', 'success') : currentRow.step === '#2' ? navigateTo('https://docs.appsmith.com/core-concepts/connecting-to-data-sources/querying-a-database',undefined,'NEW_WINDOW') : navigateTo('https://docs.appsmith.com/core-concepts/displaying-data-read/display-data-tables',undefined,'NEW_WINDOW')}}",
"computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"action\"]))}}",
"buttonColor": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( appsmith.theme.colors.primaryColor))}}",
"borderRadius": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( appsmith.theme.borderRadius.appBorderRadius))}}",
"boxShadow": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( 'none'))}}"
}
},
"key": "ouqfcjyuwa",
"widgetId": "vrcp6kbiz8"
}
]
}

View File

@ -0,0 +1,374 @@
{
"widgetName": "MainContainer",
"widgetId": "0",
"type": "CANVAS_WIDGET",
"dynamicBindingPathList": [],
"children": [
{
"widgetName": "Text1",
"type": "TEXT_WIDGET",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"dynamicBindingPathList": [
{
"key": "fontFamily"
},
{
"key": "borderRadius"
}
],
"text": "Label",
"key": "3pqpn28ba4",
"widgetId": "wemfst2t7m",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
{
"widgetName": "Text2",
"type": "TEXT_WIDGET",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"dynamicBindingPathList": [
{
"key": "fontFamily"
},
{
"key": "borderRadius"
},
{
"key": "text"
}
],
"text": "{{Text1.text}}",
"key": "3pqpn28ba4",
"widgetId": "2bensj901c",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
{
"template": {
"Image1": {
"image": "{{List1.listData.map((currentItem) => currentItem.img)}}",
"widgetName": "Image1",
"type": "IMAGE_WIDGET",
"key": "e0c7wcn17q",
"dynamicBindingPathList": [
{
"key": "image"
},
{
"key": "borderRadius"
}
],
"widgetId": "bvixbymoxr",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"newText": {
"text": "{{List1.listData.map((currentItem) => currentItem.name)}}",
"widgetName": "newText",
"type": "TEXT_WIDGET",
"key": "3pqpn28ba4",
"dynamicBindingPathList": [
{
"key": "text"
},
{
"key": "fontFamily"
},
{
"key": "borderRadius"
}
],
"widgetId": "6ox4ujv63y",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"Text4": {
"text": "{{List1.listData.map((currentItem, currentIndex) => {\n return (function(){\n return currentItem.id + Text1.text;\n })();\n })}}",
"widgetName": "Text4",
"type": "TEXT_WIDGET",
"key": "3pqpn28ba4",
"dynamicBindingPathList": [
{
"key": "text"
},
{
"key": "fontFamily"
},
{
"key": "borderRadius"
}
],
"widgetId": "rtlyvpkvhc",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
},
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}",
"widgetName": "List1",
"type": "LIST_WIDGET",
"dynamicBindingPathList": [
{
"key": "accentColor"
},
{
"key": "borderRadius"
},
{
"key": "boxShadow"
},
{
"key": "template.Image1.image"
},
{
"key": "template.newText.text"
},
{
"key": "template.Text4.text"
}
],
"dynamicTriggerPathList": [
{
"key": "onListItemClick"
}
],
"onListItemClick": "{{newText.text}}",
"children": [
{
"widgetName": "Canvas1",
"type": "CANVAS_WIDGET",
"dynamicBindingPathList": [
{
"key": "borderRadius"
},
{
"key": "accentColor"
}
],
"children": [
{
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}",
"widgetName": "Container1",
"dynamicBindingPathList": [
{
"key": "borderRadius"
},
{
"key": "boxShadow"
}
],
"children": [
{
"widgetName": "Canvas2",
"type": "CANVAS_WIDGET",
"dynamicBindingPathList": [
{
"key": "borderRadius"
},
{
"key": "accentColor"
}
],
"children": [
{
"widgetName": "Image1",
"type": "IMAGE_WIDGET",
"dynamicBindingPathList": [
{
"key": "image"
},
{
"key": "borderRadius"
}
],
"key": "e0c7wcn17q",
"image": "{{currentItem.img}}",
"widgetId": "bvixbymoxr",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
{
"widgetName": "newText",
"type": "TEXT_WIDGET",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"dynamicBindingPathList": [
{
"key": "text"
},
{
"key": "fontFamily"
},
{
"key": "borderRadius"
}
],
"text": "{{currentItem.name}}",
"key": "3pqpn28ba4",
"widgetId": "6ox4ujv63y",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
{
"widgetName": "Text4",
"type": "TEXT_WIDGET",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"dynamicBindingPathList": [
{
"key": "text"
},
{
"key": "fontFamily"
},
{
"key": "borderRadius"
}
],
"text": "{{currentItem.id + Text1.text}}",
"key": "3pqpn28ba4",
"widgetId": "rtlyvpkvhc",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
],
"key": "3m0y9rrh1o",
"widgetId": "zdz4f503fm",
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
],
"key": "sca9shlkpb",
"widgetId": "vt8i2g9u5r",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
],
"key": "3m0y9rrh1o",
"widgetId": "ki75z4pfxm",
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
],
"key": "t35n4gddpu",
"widgetId": "bunz1f076j",
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
{
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}",
"type": "TABLE_WIDGET_V2",
"dynamicBindingPathList": [
{
"key": "primaryColumns.step.computedValue"
},
{
"key": "primaryColumns.task.computedValue"
},
{
"key": "primaryColumns.status.computedValue"
},
{
"key": "primaryColumns.action.computedValue"
},
{
"key": "primaryColumns.action.buttonColor"
},
{
"key": "primaryColumns.action.borderRadius"
},
{
"key": "primaryColumns.action.boxShadow"
},
{
"key": "accentColor"
},
{
"key": "borderRadius"
},
{
"key": "boxShadow"
},
{
"key": "childStylesheet.button.buttonColor"
},
{
"key": "childStylesheet.button.borderRadius"
},
{
"key": "childStylesheet.menuButton.menuColor"
},
{
"key": "childStylesheet.menuButton.borderRadius"
},
{
"key": "childStylesheet.iconButton.buttonColor"
},
{
"key": "childStylesheet.iconButton.borderRadius"
},
{
"key": "childStylesheet.editActions.saveButtonColor"
},
{
"key": "childStylesheet.editActions.saveBorderRadius"
},
{
"key": "childStylesheet.editActions.discardButtonColor"
},
{
"key": "childStylesheet.editActions.discardBorderRadius"
}
],
"accentColor": "{{appsmith.theme.colors.primaryColor}}",
"childStylesheet": {
"button": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"menuButton": {
"menuColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"iconButton": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"editActions": {
"saveButtonColor": "{{appsmith.theme.colors.primaryColor}}",
"saveBorderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"discardButtonColor": "{{appsmith.theme.colors.primaryColor}}",
"discardBorderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
},
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"widgetName": "Table1",
"primaryColumns": {
"step": {
"id": "step",
"originalId": "step",
"alias": "step",
"label": "step",
"computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"step\"]))}}"
},
"task": {
"id": "task",
"originalId": "task",
"alias": "task",
"label": "task",
"computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( Text1.text + \" \" + currentRow[\"task\"]))}}"
},
"status": {
"id": "status",
"originalId": "status",
"alias": "status",
"label": "status",
"computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"status\"]))}}"
},
"action": {
"id": "action",
"originalId": "action",
"alias": "action",
"label": "action",
"onClick": "{{currentRow.step === '#1' ? showAlert('Done', 'success') : currentRow.step === '#2' ? navigateTo('https://docs.appsmith.com/core-concepts/connecting-to-data-sources/querying-a-database',undefined,'NEW_WINDOW') : navigateTo('https://docs.appsmith.com/core-concepts/displaying-data-read/display-data-tables',undefined,'NEW_WINDOW')}}",
"computedValue": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( currentRow[\"action\"]))}}",
"buttonColor": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( appsmith.theme.colors.primaryColor))}}",
"borderRadius": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( appsmith.theme.borderRadius.appBorderRadius))}}",
"boxShadow": "{{Table1.processedTableData.map((currentRow, currentIndex) => ( 'none'))}}"
}
},
"key": "ouqfcjyuwa",
"widgetId": "vrcp6kbiz8"
}
]
}

View File

@ -3,7 +3,7 @@ import { ancestor, simple } from "acorn-walk";
import { ECMA_VERSION, NodeTypes } from "./constants/ast";
import { has, isFinite, isString, memoize, toPath } from "lodash";
import { isTrueObject, sanitizeScript } from "./utils";
import { jsObjectDeclaration } from "./jsObject/index";
/*
* Valuable links:
*
@ -107,6 +107,11 @@ type NodeWithLocation<NodeType> = NodeType & {
type AstOptions = Omit<Options, "ecmaVersion">;
type EntityRefactorResponse = {
isSuccess: boolean;
body: { script: string; refactorCount: number } | { error: string };
};
/* We need these functions to typescript casts the nodes with the correct types */
export const isIdentifierNode = (node: Node): node is IdentifierNode => {
return node.type === NodeTypes.Identifier;
@ -213,7 +218,6 @@ export const extractIdentifierInfoFromCode = (
evaluationVersion: number,
invalidIdentifiers?: Record<string, unknown>
): IdentifierInfo => {
let ast: Node = { end: 0, start: 0, type: "" };
try {
const sanitizedScript = sanitizeScript(code, evaluationVersion);
@ -262,9 +266,12 @@ export const entityRefactorFromCode = (
script: string,
oldName: string,
newName: string,
isJSObject: boolean,
evaluationVersion: number,
invalidIdentifiers?: Record<string, unknown>
): Record<string, string | number> | string => {
): EntityRefactorResponse => {
//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;
@ -284,7 +291,7 @@ export const entityRefactorFromCode = (
identifierList,
}: NodeList = ancestorWalk(ast);
let identifierArray = Array.from(identifierList) as Array<IdentifierNode>;
const referencesArr = Array.from(references).filter((reference, index) => {
Array.from(references).forEach((reference, index) => {
const topLevelIdentifier = toPath(reference)[0];
let shouldUpdateNode = !(
functionalParams.has(topLevelIdentifier) ||
@ -308,13 +315,17 @@ export const entityRefactorFromCode = (
refactorOffset += nameLengthDiff;
++refactorCount;
}
return shouldUpdateNode;
});
return { script: refactorScript, count: refactorCount };
//If script is a JSObject then revert decalartion to export default.
if (isJSObject) refactorScript = jsCodeToObject(refactorScript);
return {
isSuccess: true,
body: { script: refactorScript, refactorCount },
};
} catch (e) {
if (e instanceof SyntaxError) {
// Syntax error. Ignore and return empty list
return "Syntax Error";
return { isSuccess: false, body: { error: "Syntax Error" } };
}
throw e;
}
@ -598,3 +609,15 @@ const ancestorWalk = (ast: Node): NodeList => {
identifierList,
};
};
//Replace export default by a variable declaration.
//This is required for acorn to parse code into AST.
const jsObjectToCode = (script: string) => {
return script.replace(/export default/g, jsObjectDeclaration);
};
//Revert the string replacement from 'jsObjectToCode'.
//variable declaration is replaced back by export default.
const jsCodeToObject = (script: string) => {
return script.replace(jsObjectDeclaration, "export default");
};

View File

@ -1,7 +1,7 @@
import { Node } from 'acorn';
import { getAST } from '../index';
import { generate } from 'astring';
import { simple } from 'acorn-walk';
import { Node } from "acorn";
import { getAST } from "../index";
import { generate } from "astring";
import { simple } from "acorn-walk";
import {
getFunctionalParamsFromNode,
isPropertyAFunctionNode,
@ -9,7 +9,7 @@ import {
isObjectExpression,
PropertyNode,
functionParam,
} from '../index';
} from "../index";
type JsObjectProperty = {
key: string;
@ -18,6 +18,11 @@ type JsObjectProperty = {
arguments?: Array<functionParam>;
};
const jsObjectVariableName =
"____INTERNAL_JS_OBJECT_NAME_USED_FOR_PARSING_____";
export const jsObjectDeclaration = `var ${jsObjectVariableName} =`;
export const parseJSObjectWithAST = (
jsObjectBody: string
): Array<JsObjectProperty> => {
@ -26,9 +31,7 @@ export const parseJSObjectWithAST = (
if the variable name will be same then also we won't have problem here as jsObjectVariableName will be last node in VariableDeclarator hence overriding the previous JSObjectProperties.
Keeping this just for sanity check if any caveat was missed.
*/
const jsObjectVariableName =
'____INTERNAL_JS_OBJECT_NAME_USED_FOR_PARSING_____';
const jsCode = `var ${jsObjectVariableName} = ${jsObjectBody}`;
const jsCode = `${jsObjectDeclaration} ${jsObjectBody}`;
const ast = getAST(jsCode);