rickchengx commented on code in PR #15321: URL: https://github.com/apache/dolphinscheduler/pull/15321#discussion_r1496789777
########## dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/v2/controller/ProjectV2Controller.java: ########## @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.api.v2.controller; + +import org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant; +import org.apache.dolphinscheduler.api.controller.BaseController; +import org.apache.dolphinscheduler.api.dto.project.ProjectCreateRequest; +import org.apache.dolphinscheduler.api.dto.project.ProjectUpdateRequest; +import org.apache.dolphinscheduler.api.enums.Status; +import org.apache.dolphinscheduler.api.exceptions.ApiException; +import org.apache.dolphinscheduler.api.v2.annotation.Auth; +import org.apache.dolphinscheduler.api.v2.dto.QueryResult; +import org.apache.dolphinscheduler.api.v2.service.ProjectServiceV2; +import org.apache.dolphinscheduler.common.constants.Constants; +import org.apache.dolphinscheduler.common.enums.AuthorizationType; +import org.apache.dolphinscheduler.dao.entity.Project; +import org.apache.dolphinscheduler.dao.entity.User; + +import javax.validation.Valid; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; + +/** + * project controller + */ +@Tag(name = "PROJECT_TAG") +@RestController +@RequestMapping("/v2/projects") +public class ProjectV2Controller extends BaseController { + + @Autowired + private ProjectServiceV2 projectService; + + /** + * create project + * + * @param loginUser login user + * @param projectCreateRequest projectCreateRequest + * @return ProjectResponse ProjectResponse + */ + @Operation(summary = "create", description = "CREATE_PROJECT_NOTES") + @PostMapping(consumes = {"application/json"}) + @ApiException(Status.CREATE_PROJECT_ERROR) + @Auth(permissionKey = ApiFuncIdentificationConstant.PROJECT_CREATE, authorizationType = AuthorizationType.PROJECTS_V2) + @ResponseStatus(HttpStatus.CREATED) + @ResponseBody + public Project createProject(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser, + @Valid @RequestBody ProjectCreateRequest projectCreateRequest) { + return projectService.createProject(loginUser, projectCreateRequest.getProjectName(), + projectCreateRequest.getDescription()); + } + + /** + * update project + * + * @param loginUser login user + * @param code project code + * @param projectUpdateReq projectUpdateRequest + * @return result Result + */ + @Operation(summary = "update", description = "UPDATE_PROJECT_NOTES") + @PatchMapping(value = "/{code}", consumes = {"application/json"}) + @ApiException(Status.UPDATE_PROJECT_ERROR) + @Auth(permissionKey = ApiFuncIdentificationConstant.PROJECT_UPDATE, authorizationType = AuthorizationType.PROJECTS_V2, needCheckResource = true) + @ResponseStatus(HttpStatus.OK) + @ResponseBody + public Project updateProject(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser, + @PathVariable("code") Long code, + @Valid @RequestBody ProjectUpdateRequest projectUpdateReq) { + return projectService.update(loginUser, code, projectUpdateReq.getProjectName(), + projectUpdateReq.getDescription()); + } + + /** + * query project details by project code + * + * @param loginUser login user + * @param code project code + * @return project detail information + */ + @Operation(summary = "queryProjectByCode", description = "QUERY_PROJECT_BY_ID_NOTES") + @Parameters({ + @Parameter(name = "code", description = "PROJECT_CODE", schema = @Schema(implementation = long.class, example = "123456", required = true)) + }) + @GetMapping(value = "/{code}") + @ApiException(Status.QUERY_PROJECT_DETAILS_BY_CODE_ERROR) + @Auth(permissionKey = ApiFuncIdentificationConstant.PROJECT, authorizationType = AuthorizationType.PROJECTS_V2, needCheckResource = true) + @ResponseStatus(HttpStatus.OK) + @ResponseBody + public Project queryProjectByCode(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser, + @PathVariable("code") long code) { + return projectService.queryByCode(loginUser, code); + } + + /** + * delete project by code + * + * @param loginUser login user + * @param code project code + * @return delete result code + */ + @Operation(summary = "delete", description = "DELETE_PROJECT_BY_ID_NOTES") + @Parameters({ + @Parameter(name = "code", description = "PROJECT_CODE", schema = @Schema(implementation = long.class, example = "123456", required = true)) + }) + @DeleteMapping(value = "/{code}") + @ApiException(Status.DELETE_PROJECT_ERROR) + @Auth(permissionKey = ApiFuncIdentificationConstant.PROJECT_DELETE, authorizationType = AuthorizationType.PROJECTS_V2, needCheckResource = true) + @ResponseStatus(HttpStatus.NO_CONTENT) + @ResponseBody + public Boolean deleteProject(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser, + @PathVariable("code") Long code) { + return projectService.deleteProject(loginUser, code); + } + + /** + * query project list paging + * + * @param loginUser login user + * @param searchVal search value + * @param pageSize page size + * @param pageNo page number + * @return project list which the login user have permission to see + */ + @Operation(summary = "queryProjectListPaging", description = "QUERY_PROJECT_LIST_PAGING_NOTES") Review Comment: The `listXXX` API generally has two methods for pagination: Offset-based pagination or Cursor-based (token-based) pagination. Currently, DS uses the offset-based pagination (e.g., pageSize and pageNo). When designing the v2 API, I think we need to discuss what paging method to use. ### Offset-based Pagination Users can see page numbers directly on the web UI and jump between different pages. But there are 2 problems with this approach: * Data conflicts and loss: If a data item is inserted or deleted during paging, users may see duplicate data items on the next page, or data item may be lost. * Performance issue: When the offset is large, query efficiency will become lower ### Cursor-based Pagination Instead of relying on numeric offsets, cursor-based pagination uses a unique identifier or token to mark the position in the dataset. Token-based pagination does not have data conflict/loss or performance issue. And cloud vendors (e.g., AWS / Azure / GCP) also use cursor-based pagination extensively. ### An example of Cursor-based Pagination * Assume table `records` is sorted using `id` and `update_time` * Assume that a page returns 10 data items, then take the `id` and `update_time` of the last data item as the cursor (usually encoded instead of returned to the user in plain text). * The user uses the cursor obtained from the previous query to initiate a second paging query, and DS decodes it to obtain the `last_data.id` and `last_data.update_time` of the last piece of data in the previous query. ``` SELECT * FROM records WHERE user_id = xxx AND ( update_time < last_data.update_time OR ( update_time = last_data.update_time AND id < last_data.id ) ) ORDER BY update_time DESC, id DESC LIMIT 10; ``` I prefer to use cursor-based pagination in the v2 API of DS since it does not have data conflict/loss or performance issue. And I don't think users have a strong need to jump directly to a specific page. I'll start a discussion on the mailing list about pagination in v2-API. Any comments or suggestions are welcome. Ref: [1] [Offset vs Cursor-Based Pagination: Which is the Right Choice for Your Project?](https://medium.com/@oshiryaeva/offset-vs-cursor-based-pagination-which-is-the-right-choice-for-your-project-e46f65db062f) [2] [Offset vs Cursor-Based Pagination: Choosing the Best Approach](https://medium.com/@maryam-bit/offset-vs-cursor-based-pagination-choosing-the-best-approach-2e93702a118b) -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: commits-unsubscr...@dolphinscheduler.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org