chore: Add ApplicationCreationDTO for Application creation API body (#33200)
This PR cleans up the application creation API with stricter validations on the input. We're also moving the `workspaceId` from the query parameter into the body, so it can be validated togather and all input data is in one place. Simplifies code both in client and server. No additional changes for EE, and no conflicts either, and al unit and Cypress tests pass. /ok-to-test tags="@tag.Sanity" <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/9130707553> > Commit: 916b54c802a163910c738e3f8ceb203314a38a6c > Cypress dashboard url: <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=9130707553&attempt=1" target="_blank">Click here!</a> <!-- end of auto-generated comment: Cypress test results -->
This commit is contained in:
parent
8682dd2b90
commit
b831a3943b
|
|
@ -954,9 +954,7 @@ Cypress.Commands.add("startServerAndRoutes", () => {
|
||||||
|
|
||||||
cy.intercept("GET", "/api/v1/plugins/*/form").as("getPluginForm");
|
cy.intercept("GET", "/api/v1/plugins/*/form").as("getPluginForm");
|
||||||
cy.intercept("DELETE", "/api/v1/applications/*").as("deleteApplication");
|
cy.intercept("DELETE", "/api/v1/applications/*").as("deleteApplication");
|
||||||
cy.intercept("POST", "/api/v1/applications?workspaceId=*").as(
|
cy.intercept("POST", "/api/v1/applications").as("createNewApplication");
|
||||||
"createNewApplication",
|
|
||||||
);
|
|
||||||
cy.intercept("PUT", "/api/v1/applications/*").as("updateApplication");
|
cy.intercept("PUT", "/api/v1/applications/*").as("updateApplication");
|
||||||
cy.intercept("PUT", "/api/v1/actions/*").as("saveAction");
|
cy.intercept("PUT", "/api/v1/actions/*").as("saveAction");
|
||||||
cy.intercept("PUT", "/api/v1/actions/move").as("moveAction");
|
cy.intercept("PUT", "/api/v1/actions/move").as("moveAction");
|
||||||
|
|
|
||||||
|
|
@ -283,8 +283,6 @@ export class ApplicationApi extends Api {
|
||||||
static baseURL = "v1/applications";
|
static baseURL = "v1/applications";
|
||||||
static publishURLPath = (applicationId: string) =>
|
static publishURLPath = (applicationId: string) =>
|
||||||
`/publish/${applicationId}`;
|
`/publish/${applicationId}`;
|
||||||
static createApplicationPath = (workspaceId: string) =>
|
|
||||||
`?workspaceId=${workspaceId}`;
|
|
||||||
static changeAppViewAccessPath = (applicationId: string) =>
|
static changeAppViewAccessPath = (applicationId: string) =>
|
||||||
`/${applicationId}/changeAccess`;
|
`/${applicationId}/changeAccess`;
|
||||||
static setDefaultPagePath = (request: SetDefaultPageRequest) =>
|
static setDefaultPagePath = (request: SetDefaultPageRequest) =>
|
||||||
|
|
@ -341,28 +339,14 @@ export class ApplicationApi extends Api {
|
||||||
static async createApplication(
|
static async createApplication(
|
||||||
request: CreateApplicationRequest,
|
request: CreateApplicationRequest,
|
||||||
): Promise<AxiosPromise<PublishApplicationResponse>> {
|
): Promise<AxiosPromise<PublishApplicationResponse>> {
|
||||||
const applicationDetail = {
|
return Api.post(ApplicationApi.baseURL, {
|
||||||
appPositioning: {
|
workspaceId: request.workspaceId,
|
||||||
type: request.layoutSystemType,
|
name: request.name,
|
||||||
},
|
color: request.color,
|
||||||
} as any;
|
icon: request.icon,
|
||||||
|
positioningType: request.layoutSystemType,
|
||||||
if (request.showNavbar !== undefined) {
|
showNavbar: request.showNavbar ?? null,
|
||||||
applicationDetail.navigationSetting = {
|
});
|
||||||
showNavbar: request.showNavbar,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return Api.post(
|
|
||||||
ApplicationApi.baseURL +
|
|
||||||
ApplicationApi.createApplicationPath(request.workspaceId),
|
|
||||||
{
|
|
||||||
name: request.name,
|
|
||||||
color: request.color,
|
|
||||||
icon: request.icon,
|
|
||||||
applicationDetail,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async setDefaultApplicationPage(
|
static async setDefaultApplicationPage(
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import com.appsmith.server.domains.Application;
|
||||||
import com.appsmith.server.domains.GitAuth;
|
import com.appsmith.server.domains.GitAuth;
|
||||||
import com.appsmith.server.domains.Theme;
|
import com.appsmith.server.domains.Theme;
|
||||||
import com.appsmith.server.dtos.ApplicationAccessDTO;
|
import com.appsmith.server.dtos.ApplicationAccessDTO;
|
||||||
|
import com.appsmith.server.dtos.ApplicationCreationDTO;
|
||||||
import com.appsmith.server.dtos.ApplicationImportDTO;
|
import com.appsmith.server.dtos.ApplicationImportDTO;
|
||||||
import com.appsmith.server.dtos.ApplicationJson;
|
import com.appsmith.server.dtos.ApplicationJson;
|
||||||
import com.appsmith.server.dtos.ApplicationPagesDTO;
|
import com.appsmith.server.dtos.ApplicationPagesDTO;
|
||||||
|
|
@ -19,8 +20,6 @@ import com.appsmith.server.dtos.GitAuthDTO;
|
||||||
import com.appsmith.server.dtos.PartialExportFileDTO;
|
import com.appsmith.server.dtos.PartialExportFileDTO;
|
||||||
import com.appsmith.server.dtos.ReleaseItemsDTO;
|
import com.appsmith.server.dtos.ReleaseItemsDTO;
|
||||||
import com.appsmith.server.dtos.ResponseDTO;
|
import com.appsmith.server.dtos.ResponseDTO;
|
||||||
import com.appsmith.server.exceptions.AppsmithError;
|
|
||||||
import com.appsmith.server.exceptions.AppsmithException;
|
|
||||||
import com.appsmith.server.exports.internal.ExportService;
|
import com.appsmith.server.exports.internal.ExportService;
|
||||||
import com.appsmith.server.exports.internal.partial.PartialExportService;
|
import com.appsmith.server.exports.internal.partial.PartialExportService;
|
||||||
import com.appsmith.server.fork.internal.ApplicationForkingService;
|
import com.appsmith.server.fork.internal.ApplicationForkingService;
|
||||||
|
|
@ -77,14 +76,10 @@ public class ApplicationControllerCE {
|
||||||
@JsonView(Views.Public.class)
|
@JsonView(Views.Public.class)
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
public Mono<ResponseDTO<Application>> create(
|
public Mono<ResponseDTO<Application>> create(@Valid @RequestBody ApplicationCreationDTO resource) {
|
||||||
@Valid @RequestBody Application resource, @RequestParam String workspaceId) {
|
log.debug("Going to create application in workspace {}", resource.workspaceId());
|
||||||
if (workspaceId == null) {
|
|
||||||
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, "workspace id"));
|
|
||||||
}
|
|
||||||
log.debug("Going to create application in workspace {}", workspaceId);
|
|
||||||
return applicationPageService
|
return applicationPageService
|
||||||
.createApplication(resource, workspaceId)
|
.createApplication(resource.toApplication())
|
||||||
.map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null));
|
.map(created -> new ResponseDTO<>(HttpStatus.CREATED.value(), created, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -428,12 +428,13 @@ public class Application extends BaseDomain implements Artifact {
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public static class AppPositioning {
|
public static class AppPositioning {
|
||||||
@JsonView({Views.Public.class, Git.class})
|
@JsonView({Views.Public.class, Git.class})
|
||||||
Type type;
|
Type type;
|
||||||
|
|
||||||
public AppPositioning(Type type) {
|
public AppPositioning(String type) {
|
||||||
this.type = type;
|
setType(Type.valueOf(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Type {
|
public enum Type {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.appsmith.server.dtos;
|
||||||
|
|
||||||
|
import com.appsmith.server.domains.Application;
|
||||||
|
import com.appsmith.server.domains.ApplicationDetail;
|
||||||
|
import com.appsmith.server.meta.validations.IconName;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
public record ApplicationCreationDTO(
|
||||||
|
@NotBlank @Size(max = 99) String workspaceId,
|
||||||
|
@NotBlank @Size(max = 99) String name,
|
||||||
|
@IconName String icon,
|
||||||
|
@Pattern(regexp = "#[A-F0-9]{6}") String color,
|
||||||
|
Application.AppPositioning positioningType,
|
||||||
|
Boolean showNavBar) {
|
||||||
|
|
||||||
|
public Application toApplication() {
|
||||||
|
final Application application = new Application();
|
||||||
|
application.setWorkspaceId(workspaceId);
|
||||||
|
application.setName(name.trim());
|
||||||
|
application.setIcon(StringUtils.isBlank(icon) ? null : icon.trim());
|
||||||
|
application.setColor(color);
|
||||||
|
|
||||||
|
final ApplicationDetail applicationDetail = new ApplicationDetail();
|
||||||
|
application.setApplicationDetail(applicationDetail);
|
||||||
|
|
||||||
|
applicationDetail.setAppPositioning(positioningType);
|
||||||
|
|
||||||
|
final Application.NavigationSetting navigationSetting = new Application.NavigationSetting();
|
||||||
|
navigationSetting.setShowNavbar(showNavBar);
|
||||||
|
applicationDetail.setNavigationSetting(navigationSetting);
|
||||||
|
|
||||||
|
return application;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,13 +8,13 @@ import jakarta.validation.constraints.Size;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public record PageCreationDTO(
|
public record PageCreationDTO(
|
||||||
@FileName(message = "Page names must be valid file names") @Size(max = 30) String name,
|
@FileName(message = "Page names must be valid file names", isNullValid = false) @Size(max = 30) String name,
|
||||||
@NotEmpty @Size(min = 24, max = 50) String applicationId,
|
@NotEmpty @Size(min = 24, max = 50) String applicationId,
|
||||||
@NotEmpty List<Layout> layouts) {
|
@NotEmpty List<Layout> layouts) {
|
||||||
|
|
||||||
public PageDTO toPageDTO() {
|
public PageDTO toPageDTO() {
|
||||||
final PageDTO page = new PageDTO();
|
final PageDTO page = new PageDTO();
|
||||||
page.setName(name);
|
page.setName(name.trim());
|
||||||
page.setApplicationId(applicationId);
|
page.setApplicationId(applicationId);
|
||||||
page.setLayouts(layouts);
|
page.setLayouts(layouts);
|
||||||
return page;
|
return page;
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,21 @@
|
||||||
package com.appsmith.server.dtos;
|
package com.appsmith.server.dtos;
|
||||||
|
|
||||||
import com.appsmith.server.meta.validations.FileName;
|
import com.appsmith.server.meta.validations.FileName;
|
||||||
|
import com.appsmith.server.meta.validations.IconName;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
public record PageUpdateDTO(
|
public record PageUpdateDTO(
|
||||||
@FileName(message = "Page names must be valid file names") @Size(max = 30) String name,
|
@FileName(message = "Page names must be valid file names") @Size(max = 30) String name,
|
||||||
@Pattern(regexp = "[-a-z]+") @Size(max = 20) String icon,
|
@IconName String icon,
|
||||||
@Pattern(regexp = "[-\\w]*") String customSlug,
|
@Pattern(regexp = "[-\\w]*") String customSlug,
|
||||||
Boolean isHidden) {
|
Boolean isHidden) {
|
||||||
|
|
||||||
public PageDTO toPageDTO() {
|
public PageDTO toPageDTO() {
|
||||||
final PageDTO page = new PageDTO();
|
final PageDTO page = new PageDTO();
|
||||||
page.setName(name);
|
page.setName(name == null ? null : name.trim());
|
||||||
page.setIcon(icon);
|
page.setIcon(StringUtils.isBlank(icon) ? null : icon.trim());
|
||||||
page.setCustomSlug(customSlug);
|
page.setCustomSlug(customSlug);
|
||||||
page.setIsHidden(isHidden);
|
page.setIsHidden(isHidden);
|
||||||
return page;
|
return page;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.appsmith.server.meta.validations;
|
||||||
|
|
||||||
|
import jakarta.validation.Constraint;
|
||||||
|
import jakarta.validation.Payload;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Documented
|
||||||
|
@Constraint(validatedBy = {IconNameValidator.class})
|
||||||
|
@Target({ElementType.FIELD, ElementType.METHOD})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface IconName {
|
||||||
|
String message() default "Invalid icon name";
|
||||||
|
|
||||||
|
Class<?>[] groups() default {};
|
||||||
|
|
||||||
|
Class<? extends Payload>[] payload() default {};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.appsmith.server.meta.validations;
|
||||||
|
|
||||||
|
import jakarta.validation.ConstraintValidator;
|
||||||
|
import jakarta.validation.ConstraintValidatorContext;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class IconNameValidator implements ConstraintValidator<IconName, String> {
|
||||||
|
|
||||||
|
private static final Pattern PATTERN = Pattern.compile("[-a-z]{1,20}");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||||
|
return value == null || PATTERN.matcher(value).matches();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -58,12 +58,9 @@ public class EqualityTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testApplicationDetail() {
|
void testApplicationDetail() {
|
||||||
Application.AppPositioning p1 = new Application.AppPositioning();
|
Application.AppPositioning p1 = new Application.AppPositioning(Application.AppPositioning.Type.AUTO);
|
||||||
p1.setType(Application.AppPositioning.Type.AUTO);
|
Application.AppPositioning p2 = new Application.AppPositioning(Application.AppPositioning.Type.AUTO);
|
||||||
Application.AppPositioning p2 = new Application.AppPositioning();
|
Application.AppPositioning p3 = new Application.AppPositioning(Application.AppPositioning.Type.FIXED);
|
||||||
p2.setType(Application.AppPositioning.Type.AUTO);
|
|
||||||
Application.AppPositioning p3 = new Application.AppPositioning();
|
|
||||||
p3.setType(Application.AppPositioning.Type.FIXED);
|
|
||||||
assertThat(p1).isEqualTo(p2).isNotEqualTo(p3);
|
assertThat(p1).isEqualTo(p2).isNotEqualTo(p3);
|
||||||
|
|
||||||
ApplicationDetail d1 = new ApplicationDetail();
|
ApplicationDetail d1 = new ApplicationDetail();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user