diff --git a/api/openapispec/docs.go b/api/openapispec/docs.go index d5fe70600..59af0f00b 100644 --- a/api/openapispec/docs.go +++ b/api/openapispec/docs.go @@ -1046,6 +1046,18 @@ const docTemplate = `{ "description": "The size of the page. Default to 10", "name": "pageSize", "in": "query" + }, + { + "type": "string", + "description": "Which field to sort the list by. Default to id", + "name": "sortBy", + "in": "query" + }, + { + "type": "boolean", + "description": "Whether to sort the list in ascending order. Default to false", + "name": "ascending", + "in": "query" } ], "responses": { @@ -1649,6 +1661,18 @@ const docTemplate = `{ "description": "The size of the page. Default to 10", "name": "pageSize", "in": "query" + }, + { + "type": "string", + "description": "Which field to sort the list by. Default to id", + "name": "sortBy", + "in": "query" + }, + { + "type": "boolean", + "description": "Whether to sort the list in ascending order. Default to false", + "name": "ascending", + "in": "query" } ], "responses": { @@ -3866,18 +3890,18 @@ const docTemplate = `{ "constant.SourceProviderType": { "type": "string", "enum": [ - "git", "git", "github", "oci", - "local" + "local", + "git" ], "x-enum-varnames": [ - "DefaultSourceType", "SourceProviderTypeGit", "SourceProviderTypeGithub", "SourceProviderTypeOCI", - "SourceProviderTypeLocal" + "SourceProviderTypeLocal", + "DefaultSourceType" ] }, "constant.StackState": { @@ -4277,6 +4301,10 @@ const docTemplate = `{ "description": "ResourceType is the type of the resource.", "type": "string" }, + "resourceURN": { + "description": "ResourceURN is the urn of the resource.", + "type": "string" + }, "status": { "description": "Status is the status of the resource.", "type": "string" diff --git a/api/openapispec/swagger.json b/api/openapispec/swagger.json index bcacf9170..9234be105 100644 --- a/api/openapispec/swagger.json +++ b/api/openapispec/swagger.json @@ -1035,6 +1035,18 @@ "description": "The size of the page. Default to 10", "name": "pageSize", "in": "query" + }, + { + "type": "string", + "description": "Which field to sort the list by. Default to id", + "name": "sortBy", + "in": "query" + }, + { + "type": "boolean", + "description": "Whether to sort the list in ascending order. Default to false", + "name": "ascending", + "in": "query" } ], "responses": { @@ -1638,6 +1650,18 @@ "description": "The size of the page. Default to 10", "name": "pageSize", "in": "query" + }, + { + "type": "string", + "description": "Which field to sort the list by. Default to id", + "name": "sortBy", + "in": "query" + }, + { + "type": "boolean", + "description": "Whether to sort the list in ascending order. Default to false", + "name": "ascending", + "in": "query" } ], "responses": { @@ -3855,18 +3879,18 @@ "constant.SourceProviderType": { "type": "string", "enum": [ - "git", "git", "github", "oci", - "local" + "local", + "git" ], "x-enum-varnames": [ - "DefaultSourceType", "SourceProviderTypeGit", "SourceProviderTypeGithub", "SourceProviderTypeOCI", - "SourceProviderTypeLocal" + "SourceProviderTypeLocal", + "DefaultSourceType" ] }, "constant.StackState": { @@ -4266,6 +4290,10 @@ "description": "ResourceType is the type of the resource.", "type": "string" }, + "resourceURN": { + "description": "ResourceURN is the urn of the resource.", + "type": "string" + }, "status": { "description": "Status is the status of the resource.", "type": "string" diff --git a/api/openapispec/swagger.yaml b/api/openapispec/swagger.yaml index 6c9bedc8e..0467b8412 100644 --- a/api/openapispec/swagger.yaml +++ b/api/openapispec/swagger.yaml @@ -30,17 +30,17 @@ definitions: constant.SourceProviderType: enum: - git - - git - github - oci - local + - git type: string x-enum-varnames: - - DefaultSourceType - SourceProviderTypeGit - SourceProviderTypeGithub - SourceProviderTypeOCI - SourceProviderTypeLocal + - DefaultSourceType constant.StackState: enum: - UnSynced @@ -325,6 +325,9 @@ definitions: resourceType: description: ResourceType is the type of the resource. type: string + resourceURN: + description: ResourceURN is the urn of the resource. + type: string status: description: Status is the status of the resource. type: string @@ -2086,6 +2089,14 @@ paths: in: query name: pageSize type: integer + - description: Which field to sort the list by. Default to id + in: query + name: sortBy + type: string + - description: Whether to sort the list in ascending order. Default to false + in: query + name: ascending + type: boolean produces: - application/json responses: @@ -2478,6 +2489,14 @@ paths: in: query name: pageSize type: integer + - description: Which field to sort the list by. Default to id + in: query + name: sortBy + type: string + - description: Whether to sort the list in ascending order. Default to false + in: query + name: ascending + type: boolean produces: - application/json responses: diff --git a/pkg/domain/constant/global.go b/pkg/domain/constant/global.go index 13b6c0181..9a0208570 100644 --- a/pkg/domain/constant/global.go +++ b/pkg/domain/constant/global.go @@ -27,6 +27,10 @@ const ( ResourcePageSizeLarge = 1000 CommonPageDefault = 1 CommonPageSizeDefault = 10 + SortByCreateTimestamp = "createTimestamp" + SortByModifiedTimestamp = "modifiedTimestamp" + SortByName = "name" + SortByID = "id" ) var ( diff --git a/pkg/domain/entity/types.go b/pkg/domain/entity/types.go index 96ae0837d..fd2db97e8 100644 --- a/pkg/domain/entity/types.go +++ b/pkg/domain/entity/types.go @@ -4,3 +4,8 @@ type Pagination struct { Page int `json:"page"` PageSize int `json:"pageSize"` } + +type SortOptions struct { + Field string + Ascending bool +} diff --git a/pkg/domain/repository/repository.go b/pkg/domain/repository/repository.go index ee12a5816..65fd9badd 100644 --- a/pkg/domain/repository/repository.go +++ b/pkg/domain/repository/repository.go @@ -37,7 +37,7 @@ type ProjectRepository interface { // GetByName retrieves a project by its name. GetByName(ctx context.Context, name string) (*entity.Project, error) // List retrieves all existing projects. - List(ctx context.Context, filter *entity.ProjectFilter) (*entity.ProjectListResult, error) + List(ctx context.Context, filter *entity.ProjectFilter, sortOptions *entity.SortOptions) (*entity.ProjectListResult, error) } // StackRepository is an interface that defines the repository operations @@ -150,5 +150,5 @@ type RunRepository interface { // Get retrieves a run by its ID. Get(ctx context.Context, id uint) (*entity.Run, error) // List retrieves all existing run. - List(ctx context.Context, filter *entity.RunFilter) (*entity.RunListResult, error) + List(ctx context.Context, filter *entity.RunFilter, sortOptions *entity.SortOptions) (*entity.RunListResult, error) } diff --git a/pkg/infra/persistence/project.go b/pkg/infra/persistence/project.go index 671dd9dee..49353929d 100644 --- a/pkg/infra/persistence/project.go +++ b/pkg/infra/persistence/project.go @@ -110,13 +110,20 @@ func (r *projectRepository) GetByName(ctx context.Context, name string) (*entity } // List retrieves all projects. -func (r *projectRepository) List(ctx context.Context, filter *entity.ProjectFilter) (*entity.ProjectListResult, error) { +func (r *projectRepository) List(ctx context.Context, filter *entity.ProjectFilter, sortOptions *entity.SortOptions) (*entity.ProjectListResult, error) { var dataModel []ProjectModel projectEntityList := make([]*entity.Project, 0) pattern, args := GetProjectQuery(filter) + + sortArgs := sortOptions.Field + if !sortOptions.Ascending { + sortArgs += " DESC" + } + searchResult := r.db.WithContext(ctx). Preload("Source"). Preload("Organization"). + Order(sortArgs). Where(pattern, args...) // Get total rows diff --git a/pkg/infra/persistence/project_test.go b/pkg/infra/persistence/project_test.go index e8b9bf180..4183b6531 100644 --- a/pkg/infra/persistence/project_test.go +++ b/pkg/infra/persistence/project_test.go @@ -162,7 +162,7 @@ func TestProjectRepository(t *testing.T) { sqlmock.NewRows([]string{"count"}). AddRow(2)) - sqlMock.ExpectQuery("SELECT .* FROM `project` .* IS NULL LIMIT"). + sqlMock.ExpectQuery("SELECT .* FROM `project` .* IS NULL .* LIMIT"). WillReturnRows( sqlmock.NewRows([]string{"id", "name", "path", "Organization__id", "Organization__name", "Organization__owners", "Source__id", "Source__remote", "Source__source_provider"}). AddRow(expectedID, expectedName, expectedPath, 1, "mockedOrg", expectedOrgOwners, 1, "https://github.com/test/repo", constant.SourceProviderTypeGithub). @@ -173,6 +173,8 @@ func TestProjectRepository(t *testing.T) { Page: constant.CommonPageDefault, PageSize: constant.CommonPageSizeDefault, }, + }, &entity.SortOptions{ + Field: constant.SortByID, }) require.NoError(t, err) require.Len(t, actual.Projects, 2) diff --git a/pkg/infra/persistence/run.go b/pkg/infra/persistence/run.go index f03af249f..e644d4f7b 100644 --- a/pkg/infra/persistence/run.go +++ b/pkg/infra/persistence/run.go @@ -96,15 +96,22 @@ func (r *runRepository) Get(ctx context.Context, id uint) (*entity.Run, error) { } // List retrieves all runs. -func (r *runRepository) List(ctx context.Context, filter *entity.RunFilter) (*entity.RunListResult, error) { +func (r *runRepository) List(ctx context.Context, filter *entity.RunFilter, sortOptions *entity.SortOptions) (*entity.RunListResult, error) { var dataModel []RunModel runEntityList := make([]*entity.Run, 0) pattern, args := GetRunQuery(filter) + + sortArgs := sortOptions.Field + if !sortOptions.Ascending { + sortArgs += " DESC" + } + searchResult := r.db.WithContext(ctx). Preload("Stack").Preload("Stack.Project"). Joins("JOIN stack ON stack.id = run.stack_id"). Joins("JOIN project ON project.id = stack.project_id"). Joins("JOIN workspace ON workspace.name = run.workspace"). + Order(sortArgs). Where(pattern, args...) // Get total rows diff --git a/pkg/server/handler/module/handler.go b/pkg/server/handler/module/handler.go index 2aefa28be..008855c77 100644 --- a/pkg/server/handler/module/handler.go +++ b/pkg/server/handler/module/handler.go @@ -97,7 +97,7 @@ func (h *Handler) DeleteModule() http.HandlerFunc { // @Failure 429 {object} error "Too Many Requests" // @Failure 404 {object} error "Not Found" // @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/modules/{moduleName} [put] +// @Router /api/v1/modules/{moduleName} [put] func (h *Handler) UpdateModule() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context. @@ -139,7 +139,7 @@ func (h *Handler) UpdateModule() http.HandlerFunc { // @Failure 429 {object} error "Too Many Requests" // @Failure 404 {object} error "Not Found" // @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/modules/{moduleName} [get] +// @Router /api/v1/modules/{moduleName} [get] func (h *Handler) GetModule() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context. diff --git a/pkg/server/handler/project/handler.go b/pkg/server/handler/project/handler.go index a27f8bc84..325cbcfbc 100644 --- a/pkg/server/handler/project/handler.go +++ b/pkg/server/handler/project/handler.go @@ -174,6 +174,8 @@ func (h *Handler) GetProject() http.HandlerFunc { // @Param fuzzyName query string false "Fuzzy match project name to filter project list by." // @Param page query uint false "The current page to fetch. Default to 1" // @Param pageSize query uint false "The size of the page. Default to 10" +// @Param sortBy query string false "Which field to sort the list by. Default to id" +// @Param ascending query bool false "Whether to sort the list in ascending order. Default to false" // @Success 200 {object} handler.Response{data=[]response.PaginatedProjectResponse} "Success" // @Failure 400 {object} error "Bad Request" // @Failure 401 {object} error "Unauthorized" @@ -189,13 +191,13 @@ func (h *Handler) ListProjects() http.HandlerFunc { logger.Info("Listing project...") query := r.URL.Query() - filter, err := h.projectManager.BuildProjectFilter(ctx, &query) + filter, projectSortOptions, err := h.projectManager.BuildProjectFilterAndSortOptions(ctx, &query) if err != nil { render.Render(w, r, handler.FailureResponse(ctx, err)) return } - projectEntities, err := h.projectManager.ListProjects(ctx, filter) + projectEntities, err := h.projectManager.ListProjects(ctx, filter, projectSortOptions) if err != nil { render.Render(w, r, handler.FailureResponse(ctx, err)) return diff --git a/pkg/server/handler/stack/handler.go b/pkg/server/handler/stack/handler.go index e43443329..3328ec52d 100644 --- a/pkg/server/handler/stack/handler.go +++ b/pkg/server/handler/stack/handler.go @@ -1,4 +1,3 @@ -//nolint:dupl package stack import ( diff --git a/pkg/server/handler/stack/run.go b/pkg/server/handler/stack/run.go index f369c0c8d..76c914ee9 100644 --- a/pkg/server/handler/stack/run.go +++ b/pkg/server/handler/stack/run.go @@ -1,4 +1,3 @@ -//nolint:dupl package stack import ( @@ -87,6 +86,8 @@ func (h *Handler) GetRunResult() http.HandlerFunc { // @Param endTime query string false "EndTime to filter runs by. Default to all. Format: RFC3339" // @Param page query uint false "The current page to fetch. Default to 1" // @Param pageSize query uint false "The size of the page. Default to 10" +// @Param sortBy query string false "Which field to sort the list by. Default to id" +// @Param ascending query bool false "Whether to sort the list in ascending order. Default to false" // @Success 200 {object} handler.Response{data=response.PaginatedRunResponse} "Success" // @Failure 400 {object} error "Bad Request" // @Failure 401 {object} error "Unauthorized" @@ -102,14 +103,14 @@ func (h *Handler) ListRuns() http.HandlerFunc { logger.Info("Listing runs...") query := r.URL.Query() - filter, err := h.stackManager.BuildRunFilter(ctx, &query) + filter, runSortOptions, err := h.stackManager.BuildRunFilterAndSortOptions(ctx, &query) if err != nil { render.Render(w, r, handler.FailureResponse(ctx, err)) return } // List runs - runEntities, err := h.stackManager.ListRuns(ctx, filter) + runEntities, err := h.stackManager.ListRuns(ctx, filter, runSortOptions) if err != nil { render.Render(w, r, handler.FailureResponse(ctx, err)) return diff --git a/pkg/server/handler/workspace/configs.go b/pkg/server/handler/workspace/configs.go index 9a35c8d3f..b1203ca96 100644 --- a/pkg/server/handler/workspace/configs.go +++ b/pkg/server/handler/workspace/configs.go @@ -116,7 +116,7 @@ func (h *Handler) UpdateWorkspaceConfigs() http.HandlerFunc { // @Failure 429 {object} error "Too Many Requests" // @Failure 404 {object} error "Not Found" // @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/workspaces/{workspaceID}/configs/mod-deps [post] +// @Router /api/v1/workspaces/{workspaceID}/configs/mod-deps [post] func (h *Handler) CreateWorkspaceModDeps() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context. diff --git a/pkg/server/manager/project/project_manager.go b/pkg/server/manager/project/project_manager.go index 4c2cb8595..835b0ad6d 100644 --- a/pkg/server/manager/project/project_manager.go +++ b/pkg/server/manager/project/project_manager.go @@ -14,8 +14,8 @@ import ( logutil "kusionstack.io/kusion/pkg/server/util/logging" ) -func (m *ProjectManager) ListProjects(ctx context.Context, filter *entity.ProjectFilter) (*entity.ProjectListResult, error) { - projectEntities, err := m.projectRepo.List(ctx, filter) +func (m *ProjectManager) ListProjects(ctx context.Context, filter *entity.ProjectFilter, sortOptions *entity.SortOptions) (*entity.ProjectListResult, error) { + projectEntities, err := m.projectRepo.List(ctx, filter, sortOptions) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrGettingNonExistingProject @@ -177,7 +177,7 @@ func (m *ProjectManager) CreateProject(ctx context.Context, requestPayload reque return &createdEntity, nil } -func (m *ProjectManager) BuildProjectFilter(ctx context.Context, query *url.Values) (*entity.ProjectFilter, error) { +func (m *ProjectManager) BuildProjectFilterAndSortOptions(ctx context.Context, query *url.Values) (*entity.ProjectFilter, *entity.SortOptions, error) { logger := logutil.GetLogger(ctx) logger.Info("Building project filter...") @@ -187,7 +187,7 @@ func (m *ProjectManager) BuildProjectFilter(ctx context.Context, query *url.Valu if orgIDParam != "" { orgID, err := strconv.Atoi(orgIDParam) if err != nil { - return nil, constant.ErrInvalidOrganizationID + return nil, nil, constant.ErrInvalidOrganizationID } filter.OrgID = uint(orgID) } @@ -203,7 +203,7 @@ func (m *ProjectManager) BuildProjectFilter(ctx context.Context, query *url.Valu } if name != "" && fuzzyName != "" { - return nil, constant.ErrProjectNameAndFuzzyName + return nil, nil, constant.ErrProjectNameAndFuzzyName } // Set pagination parameters. @@ -220,5 +220,17 @@ func (m *ProjectManager) BuildProjectFilter(ctx context.Context, query *url.Valu PageSize: pageSize, } - return &filter, nil + // Build sort options + sortBy := query.Get("sortBy") + sortBy, err := validateProjectSortOptions(sortBy) + if err != nil { + return nil, nil, err + } + SortOrderAscending, _ := strconv.ParseBool(query.Get("ascending")) + projectSortOptions := &entity.SortOptions{ + Field: sortBy, + Ascending: SortOrderAscending, + } + + return &filter, projectSortOptions, nil } diff --git a/pkg/server/manager/project/project_manager_test.go b/pkg/server/manager/project/project_manager_test.go index 5e3d37a83..8513ab914 100644 --- a/pkg/server/manager/project/project_manager_test.go +++ b/pkg/server/manager/project/project_manager_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/stretchr/testify/mock" + "kusionstack.io/kusion/pkg/domain/constant" "kusionstack.io/kusion/pkg/domain/entity" "kusionstack.io/kusion/pkg/domain/request" ) @@ -46,7 +47,7 @@ func (m *mockProjectRepository) Delete(ctx context.Context, id uint) error { return args.Error(0) } -func (m *mockProjectRepository) List(ctx context.Context, filter *entity.ProjectFilter) (*entity.ProjectListResult, error) { +func (m *mockProjectRepository) List(ctx context.Context, filter *entity.ProjectFilter, sortOptions *entity.SortOptions) (*entity.ProjectListResult, error) { args := m.Called(ctx, filter) return &entity.ProjectListResult{ Projects: args.Get(0).([]*entity.Project), @@ -163,6 +164,9 @@ func (m *mockSourceRepository) GetByRemote(ctx context.Context, remote string) ( func TestProjectManager_ListProjects(t *testing.T) { ctx := context.TODO() filter := &entity.ProjectFilter{} + sortOptions := &entity.SortOptions{ + Field: constant.SortByID, + } mockRepo := &mockProjectRepository{} expectedProjects := []*entity.Project{ { @@ -174,7 +178,7 @@ func TestProjectManager_ListProjects(t *testing.T) { manager := &ProjectManager{ projectRepo: mockRepo, } - projects, err := manager.ListProjects(ctx, filter) + projects, err := manager.ListProjects(ctx, filter, sortOptions) if !reflect.DeepEqual(projects.Projects, expectedProjects) { t.Errorf("ListProjects() returned unexpected projects.\nExpected: %v\nGot: %v", expectedProjects, projects) } diff --git a/pkg/server/manager/project/util.go b/pkg/server/manager/project/util.go index b647dda66..e7d587d1a 100644 --- a/pkg/server/manager/project/util.go +++ b/pkg/server/manager/project/util.go @@ -4,6 +4,8 @@ import ( "fmt" "net/url" "strings" + + "kusionstack.io/kusion/pkg/domain/constant" ) // GenerateDefaultSourceName generates a default source name based on the remote URL @@ -25,3 +27,19 @@ func GenerateDefaultSourceName(remoteURL string) (string, error) { return sourceName, nil } + +func validateProjectSortOptions(sortBy string) (string, error) { + if sortBy == "" { + return constant.SortByID, nil + } + if sortBy != constant.SortByID && sortBy != constant.SortByName && sortBy != constant.SortByCreateTimestamp { + return "", fmt.Errorf("invalid sort option: %s. Can only sort by id, name or create timestamp", sortBy) + } + switch sortBy { + case constant.SortByCreateTimestamp: + return "created_at", nil + case constant.SortByModifiedTimestamp: + return "updated_at", nil + } + return sortBy, nil +} diff --git a/pkg/server/manager/stack/run.go b/pkg/server/manager/stack/run.go index a4b290be7..cc5864c0c 100644 --- a/pkg/server/manager/stack/run.go +++ b/pkg/server/manager/stack/run.go @@ -16,8 +16,8 @@ import ( logutil "kusionstack.io/kusion/pkg/server/util/logging" ) -func (m *StackManager) ListRuns(ctx context.Context, filter *entity.RunFilter) (*entity.RunListResult, error) { - runEntities, err := m.runRepo.List(ctx, filter) +func (m *StackManager) ListRuns(ctx context.Context, filter *entity.RunFilter, sortOptions *entity.SortOptions) (*entity.RunListResult, error) { + runEntities, err := m.runRepo.List(ctx, filter, sortOptions) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrGettingNonExistingStack diff --git a/pkg/server/manager/stack/stack_manager_test.go b/pkg/server/manager/stack/stack_manager_test.go index 6d4afbff5..77da9a7c8 100644 --- a/pkg/server/manager/stack/stack_manager_test.go +++ b/pkg/server/manager/stack/stack_manager_test.go @@ -267,7 +267,7 @@ func (m *mockProjectRepository) Delete(ctx context.Context, id uint) error { return args.Error(0) } -func (m *mockProjectRepository) List(ctx context.Context, filter *entity.ProjectFilter) (*entity.ProjectListResult, error) { +func (m *mockProjectRepository) List(ctx context.Context, filter *entity.ProjectFilter, sortOptions *entity.SortOptions) (*entity.ProjectListResult, error) { args := m.Called(ctx, filter) return &entity.ProjectListResult{ Projects: args.Get(0).([]*entity.Project), diff --git a/pkg/server/manager/stack/util.go b/pkg/server/manager/stack/util.go index 0ecd45059..9f0179f67 100644 --- a/pkg/server/manager/stack/util.go +++ b/pkg/server/manager/stack/util.go @@ -297,7 +297,7 @@ func (m *StackManager) BuildStackFilter(ctx context.Context, query *url.Values) return &filter, nil } -func (m *StackManager) BuildRunFilter(ctx context.Context, query *url.Values) (*entity.RunFilter, error) { +func (m *StackManager) BuildRunFilterAndSortOptions(ctx context.Context, query *url.Values) (*entity.RunFilter, *entity.SortOptions, error) { logger := logutil.GetLogger(ctx) logger.Info("Building run filter...") @@ -314,7 +314,7 @@ func (m *StackManager) BuildRunFilter(ctx context.Context, query *url.Values) (* // if project id is present, use project id projectID, err := strconv.Atoi(projectIDParam) if err != nil { - return nil, constant.ErrInvalidProjectID + return nil, nil, constant.ErrInvalidProjectID } filter.ProjectID = uint(projectID) } @@ -322,7 +322,7 @@ func (m *StackManager) BuildRunFilter(ctx context.Context, query *url.Values) (* // if project id is present, use project id stackID, err := strconv.Atoi(stackIDParam) if err != nil { - return nil, constant.ErrInvalidStackID + return nil, nil, constant.ErrInvalidStackID } filter.StackID = uint(stackID) } @@ -343,7 +343,7 @@ func (m *StackManager) BuildRunFilter(ctx context.Context, query *url.Values) (* // if start time is present, use start time startTime, err := time.Parse(time.RFC3339, startTimeParam) if err != nil { - return nil, err + return nil, nil, err } filter.StartTime = startTime } @@ -351,11 +351,11 @@ func (m *StackManager) BuildRunFilter(ctx context.Context, query *url.Values) (* // if end time is present, use end time endTime, err := time.Parse(time.RFC3339, endTimeParam) if err != nil { - return nil, err + return nil, nil, err } // validate end time is after start time if !filter.StartTime.IsZero() && endTime.Before(filter.StartTime) { - return nil, fmt.Errorf("end time must be after start time") + return nil, nil, fmt.Errorf("end time must be after start time") } filter.EndTime = endTime } @@ -372,7 +372,19 @@ func (m *StackManager) BuildRunFilter(ctx context.Context, query *url.Values) (* Page: page, PageSize: pageSize, } - return &filter, nil + + // Build sort options + sortBy := query.Get("sortBy") + sortBy, err := validateRunSortOptions(sortBy) + if err != nil { + return nil, nil, err + } + SortOrderAscending, _ := strconv.ParseBool(query.Get("ascending")) + runSortOptions := &entity.SortOptions{ + Field: sortBy, + Ascending: SortOrderAscending, + } + return &filter, runSortOptions, nil } func (m *StackManager) ImportTerraformResourceID(ctx context.Context, sp *v1.Spec, importedResources map[string]string) { @@ -530,3 +542,19 @@ func validateExecuteRequestParams(params *StackRequestParams) error { } return nil } + +func validateRunSortOptions(sortBy string) (string, error) { + if sortBy == "" { + return constant.SortByID, nil + } + if sortBy != constant.SortByID && sortBy != constant.SortByCreateTimestamp { + return "", fmt.Errorf("invalid sort option: %s. Can only sort by id or create timestamp", sortBy) + } + switch sortBy { + case constant.SortByCreateTimestamp: + return "created_at", nil + case constant.SortByModifiedTimestamp: + return "updated_at", nil + } + return sortBy, nil +}