Skip to content

Commit 2032359

Browse files
committed
feat: add sort options in runs and projects list api
1 parent a062c84 commit 2032359

File tree

14 files changed

+113
-26
lines changed

14 files changed

+113
-26
lines changed

pkg/domain/constant/global.go

+4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ const (
2727
ResourcePageSizeLarge = 1000
2828
CommonPageDefault = 1
2929
CommonPageSizeDefault = 10
30+
SortByCreateTimestamp = "createTimestamp"
31+
SortByModifiedTimestamp = "modifiedTimestamp"
32+
SortByName = "name"
33+
SortByID = "id"
3034
)
3135

3236
var (

pkg/domain/entity/types.go

+5
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,8 @@ type Pagination struct {
44
Page int `json:"page"`
55
PageSize int `json:"pageSize"`
66
}
7+
8+
type SortOptions struct {
9+
Field string
10+
Ascending bool
11+
}

pkg/domain/repository/repository.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ type ProjectRepository interface {
3737
// GetByName retrieves a project by its name.
3838
GetByName(ctx context.Context, name string) (*entity.Project, error)
3939
// List retrieves all existing projects.
40-
List(ctx context.Context, filter *entity.ProjectFilter) (*entity.ProjectListResult, error)
40+
List(ctx context.Context, filter *entity.ProjectFilter, sortOptions *entity.SortOptions) (*entity.ProjectListResult, error)
4141
}
4242

4343
// StackRepository is an interface that defines the repository operations
@@ -150,5 +150,5 @@ type RunRepository interface {
150150
// Get retrieves a run by its ID.
151151
Get(ctx context.Context, id uint) (*entity.Run, error)
152152
// List retrieves all existing run.
153-
List(ctx context.Context, filter *entity.RunFilter) (*entity.RunListResult, error)
153+
List(ctx context.Context, filter *entity.RunFilter, sortOptions *entity.SortOptions) (*entity.RunListResult, error)
154154
}

pkg/infra/persistence/project.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,20 @@ func (r *projectRepository) GetByName(ctx context.Context, name string) (*entity
110110
}
111111

112112
// List retrieves all projects.
113-
func (r *projectRepository) List(ctx context.Context, filter *entity.ProjectFilter) (*entity.ProjectListResult, error) {
113+
func (r *projectRepository) List(ctx context.Context, filter *entity.ProjectFilter, sortOptions *entity.SortOptions) (*entity.ProjectListResult, error) {
114114
var dataModel []ProjectModel
115115
projectEntityList := make([]*entity.Project, 0)
116116
pattern, args := GetProjectQuery(filter)
117+
118+
sortArgs := sortOptions.Field
119+
if !sortOptions.Ascending {
120+
sortArgs = sortArgs + " DESC"
121+
}
122+
117123
searchResult := r.db.WithContext(ctx).
118124
Preload("Source").
119125
Preload("Organization").
126+
Order(sortArgs).
120127
Where(pattern, args...)
121128

122129
// Get total rows

pkg/infra/persistence/project_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ func TestProjectRepository(t *testing.T) {
173173
Page: constant.CommonPageDefault,
174174
PageSize: constant.CommonPageSizeDefault,
175175
},
176+
}, &entity.SortOptions{
177+
Field: constant.SortByID,
176178
})
177179
require.NoError(t, err)
178180
require.Len(t, actual.Projects, 2)

pkg/infra/persistence/run.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,22 @@ func (r *runRepository) Get(ctx context.Context, id uint) (*entity.Run, error) {
9696
}
9797

9898
// List retrieves all runs.
99-
func (r *runRepository) List(ctx context.Context, filter *entity.RunFilter) (*entity.RunListResult, error) {
99+
func (r *runRepository) List(ctx context.Context, filter *entity.RunFilter, sortOptions *entity.SortOptions) (*entity.RunListResult, error) {
100100
var dataModel []RunModel
101101
runEntityList := make([]*entity.Run, 0)
102102
pattern, args := GetRunQuery(filter)
103+
104+
sortArgs := sortOptions.Field
105+
if !sortOptions.Ascending {
106+
sortArgs = sortArgs + " DESC"
107+
}
108+
103109
searchResult := r.db.WithContext(ctx).
104110
Preload("Stack").Preload("Stack.Project").
105111
Joins("JOIN stack ON stack.id = run.stack_id").
106112
Joins("JOIN project ON project.id = stack.project_id").
107113
Joins("JOIN workspace ON workspace.name = run.workspace").
114+
Order(sortArgs).
108115
Where(pattern, args...)
109116

110117
// Get total rows

pkg/server/handler/project/handler.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -189,13 +189,13 @@ func (h *Handler) ListProjects() http.HandlerFunc {
189189
logger.Info("Listing project...")
190190

191191
query := r.URL.Query()
192-
filter, err := h.projectManager.BuildProjectFilter(ctx, &query)
192+
filter, projectSortOptions, err := h.projectManager.BuildProjectFilterAndSortOptions(ctx, &query)
193193
if err != nil {
194194
render.Render(w, r, handler.FailureResponse(ctx, err))
195195
return
196196
}
197197

198-
projectEntities, err := h.projectManager.ListProjects(ctx, filter)
198+
projectEntities, err := h.projectManager.ListProjects(ctx, filter, projectSortOptions)
199199
if err != nil {
200200
render.Render(w, r, handler.FailureResponse(ctx, err))
201201
return

pkg/server/handler/stack/run.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,14 @@ func (h *Handler) ListRuns() http.HandlerFunc {
102102
logger.Info("Listing runs...")
103103

104104
query := r.URL.Query()
105-
filter, err := h.stackManager.BuildRunFilter(ctx, &query)
105+
filter, runSortOptions, err := h.stackManager.BuildRunFilterAndSortOptions(ctx, &query)
106106
if err != nil {
107107
render.Render(w, r, handler.FailureResponse(ctx, err))
108108
return
109109
}
110110

111111
// List runs
112-
runEntities, err := h.stackManager.ListRuns(ctx, filter)
112+
runEntities, err := h.stackManager.ListRuns(ctx, filter, runSortOptions)
113113
if err != nil {
114114
render.Render(w, r, handler.FailureResponse(ctx, err))
115115
return

pkg/server/manager/project/project_manager.go

+18-6
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import (
1414
logutil "kusionstack.io/kusion/pkg/server/util/logging"
1515
)
1616

17-
func (m *ProjectManager) ListProjects(ctx context.Context, filter *entity.ProjectFilter) (*entity.ProjectListResult, error) {
18-
projectEntities, err := m.projectRepo.List(ctx, filter)
17+
func (m *ProjectManager) ListProjects(ctx context.Context, filter *entity.ProjectFilter, sortOptions *entity.SortOptions) (*entity.ProjectListResult, error) {
18+
projectEntities, err := m.projectRepo.List(ctx, filter, sortOptions)
1919
if err != nil {
2020
if errors.Is(err, gorm.ErrRecordNotFound) {
2121
return nil, ErrGettingNonExistingProject
@@ -177,7 +177,7 @@ func (m *ProjectManager) CreateProject(ctx context.Context, requestPayload reque
177177
return &createdEntity, nil
178178
}
179179

180-
func (m *ProjectManager) BuildProjectFilter(ctx context.Context, query *url.Values) (*entity.ProjectFilter, error) {
180+
func (m *ProjectManager) BuildProjectFilterAndSortOptions(ctx context.Context, query *url.Values) (*entity.ProjectFilter, *entity.SortOptions, error) {
181181
logger := logutil.GetLogger(ctx)
182182
logger.Info("Building project filter...")
183183

@@ -187,7 +187,7 @@ func (m *ProjectManager) BuildProjectFilter(ctx context.Context, query *url.Valu
187187
if orgIDParam != "" {
188188
orgID, err := strconv.Atoi(orgIDParam)
189189
if err != nil {
190-
return nil, constant.ErrInvalidOrganizationID
190+
return nil, nil, constant.ErrInvalidOrganizationID
191191
}
192192
filter.OrgID = uint(orgID)
193193
}
@@ -203,7 +203,7 @@ func (m *ProjectManager) BuildProjectFilter(ctx context.Context, query *url.Valu
203203
}
204204

205205
if name != "" && fuzzyName != "" {
206-
return nil, constant.ErrProjectNameAndFuzzyName
206+
return nil, nil, constant.ErrProjectNameAndFuzzyName
207207
}
208208

209209
// Set pagination parameters.
@@ -220,5 +220,17 @@ func (m *ProjectManager) BuildProjectFilter(ctx context.Context, query *url.Valu
220220
PageSize: pageSize,
221221
}
222222

223-
return &filter, nil
223+
// Build sort options
224+
sortBy := query.Get("sortBy")
225+
sortBy, err := validateProjectSortOptions(sortBy)
226+
if err != nil {
227+
return nil, nil, err
228+
}
229+
SortOrderAscending, _ := strconv.ParseBool(query.Get("ascending"))
230+
projectSortOptions := &entity.SortOptions{
231+
Field: sortBy,
232+
Ascending: SortOrderAscending,
233+
}
234+
235+
return &filter, projectSortOptions, nil
224236
}

pkg/server/manager/project/project_manager_test.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
"github.com/stretchr/testify/mock"
10+
"kusionstack.io/kusion/pkg/domain/constant"
1011
"kusionstack.io/kusion/pkg/domain/entity"
1112
"kusionstack.io/kusion/pkg/domain/request"
1213
)
@@ -46,7 +47,7 @@ func (m *mockProjectRepository) Delete(ctx context.Context, id uint) error {
4647
return args.Error(0)
4748
}
4849

49-
func (m *mockProjectRepository) List(ctx context.Context, filter *entity.ProjectFilter) (*entity.ProjectListResult, error) {
50+
func (m *mockProjectRepository) List(ctx context.Context, filter *entity.ProjectFilter, sortOptions *entity.SortOptions) (*entity.ProjectListResult, error) {
5051
args := m.Called(ctx, filter)
5152
return &entity.ProjectListResult{
5253
Projects: args.Get(0).([]*entity.Project),
@@ -163,6 +164,9 @@ func (m *mockSourceRepository) GetByRemote(ctx context.Context, remote string) (
163164
func TestProjectManager_ListProjects(t *testing.T) {
164165
ctx := context.TODO()
165166
filter := &entity.ProjectFilter{}
167+
sortOptions := &entity.SortOptions{
168+
Field: constant.SortByID,
169+
}
166170
mockRepo := &mockProjectRepository{}
167171
expectedProjects := []*entity.Project{
168172
{
@@ -174,7 +178,7 @@ func TestProjectManager_ListProjects(t *testing.T) {
174178
manager := &ProjectManager{
175179
projectRepo: mockRepo,
176180
}
177-
projects, err := manager.ListProjects(ctx, filter)
181+
projects, err := manager.ListProjects(ctx, filter, sortOptions)
178182
if !reflect.DeepEqual(projects.Projects, expectedProjects) {
179183
t.Errorf("ListProjects() returned unexpected projects.\nExpected: %v\nGot: %v", expectedProjects, projects)
180184
}

pkg/server/manager/project/util.go

+18
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"fmt"
55
"net/url"
66
"strings"
7+
8+
"kusionstack.io/kusion/pkg/domain/constant"
79
)
810

911
// GenerateDefaultSourceName generates a default source name based on the remote URL
@@ -25,3 +27,19 @@ func GenerateDefaultSourceName(remoteURL string) (string, error) {
2527

2628
return sourceName, nil
2729
}
30+
31+
func validateProjectSortOptions(sortBy string) (string, error) {
32+
if sortBy == "" {
33+
return constant.SortByID, nil
34+
}
35+
if sortBy != constant.SortByID && sortBy != constant.SortByName && sortBy != constant.SortByCreateTimestamp {
36+
return "", fmt.Errorf("invalid sort option: %s. Can only sort by id, name or create timestamp", sortBy)
37+
}
38+
switch sortBy {
39+
case constant.SortByCreateTimestamp:
40+
return "created_at", nil
41+
case constant.SortByModifiedTimestamp:
42+
return "updated_at", nil
43+
}
44+
return sortBy, nil
45+
}

pkg/server/manager/stack/run.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import (
1616
logutil "kusionstack.io/kusion/pkg/server/util/logging"
1717
)
1818

19-
func (m *StackManager) ListRuns(ctx context.Context, filter *entity.RunFilter) (*entity.RunListResult, error) {
20-
runEntities, err := m.runRepo.List(ctx, filter)
19+
func (m *StackManager) ListRuns(ctx context.Context, filter *entity.RunFilter, sortOptions *entity.SortOptions) (*entity.RunListResult, error) {
20+
runEntities, err := m.runRepo.List(ctx, filter, sortOptions)
2121
if err != nil {
2222
if errors.Is(err, gorm.ErrRecordNotFound) {
2323
return nil, ErrGettingNonExistingStack

pkg/server/manager/stack/stack_manager_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ func (m *mockProjectRepository) Delete(ctx context.Context, id uint) error {
267267
return args.Error(0)
268268
}
269269

270-
func (m *mockProjectRepository) List(ctx context.Context, filter *entity.ProjectFilter) (*entity.ProjectListResult, error) {
270+
func (m *mockProjectRepository) List(ctx context.Context, filter *entity.ProjectFilter, sortOptions *entity.SortOptions) (*entity.ProjectListResult, error) {
271271
args := m.Called(ctx, filter)
272272
return &entity.ProjectListResult{
273273
Projects: args.Get(0).([]*entity.Project),

pkg/server/manager/stack/util.go

+35-7
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ func (m *StackManager) BuildStackFilter(ctx context.Context, query *url.Values)
297297
return &filter, nil
298298
}
299299

300-
func (m *StackManager) BuildRunFilter(ctx context.Context, query *url.Values) (*entity.RunFilter, error) {
300+
func (m *StackManager) BuildRunFilterAndSortOptions(ctx context.Context, query *url.Values) (*entity.RunFilter, *entity.SortOptions, error) {
301301
logger := logutil.GetLogger(ctx)
302302
logger.Info("Building run filter...")
303303

@@ -314,15 +314,15 @@ func (m *StackManager) BuildRunFilter(ctx context.Context, query *url.Values) (*
314314
// if project id is present, use project id
315315
projectID, err := strconv.Atoi(projectIDParam)
316316
if err != nil {
317-
return nil, constant.ErrInvalidProjectID
317+
return nil, nil, constant.ErrInvalidProjectID
318318
}
319319
filter.ProjectID = uint(projectID)
320320
}
321321
if stackIDParam != "" {
322322
// if project id is present, use project id
323323
stackID, err := strconv.Atoi(stackIDParam)
324324
if err != nil {
325-
return nil, constant.ErrInvalidStackID
325+
return nil, nil, constant.ErrInvalidStackID
326326
}
327327
filter.StackID = uint(stackID)
328328
}
@@ -343,19 +343,19 @@ func (m *StackManager) BuildRunFilter(ctx context.Context, query *url.Values) (*
343343
// if start time is present, use start time
344344
startTime, err := time.Parse(time.RFC3339, startTimeParam)
345345
if err != nil {
346-
return nil, err
346+
return nil, nil, err
347347
}
348348
filter.StartTime = startTime
349349
}
350350
if endTimeParam != "" {
351351
// if end time is present, use end time
352352
endTime, err := time.Parse(time.RFC3339, endTimeParam)
353353
if err != nil {
354-
return nil, err
354+
return nil, nil, err
355355
}
356356
// validate end time is after start time
357357
if !filter.StartTime.IsZero() && endTime.Before(filter.StartTime) {
358-
return nil, fmt.Errorf("end time must be after start time")
358+
return nil, nil, fmt.Errorf("end time must be after start time")
359359
}
360360
filter.EndTime = endTime
361361
}
@@ -372,7 +372,19 @@ func (m *StackManager) BuildRunFilter(ctx context.Context, query *url.Values) (*
372372
Page: page,
373373
PageSize: pageSize,
374374
}
375-
return &filter, nil
375+
376+
// Build sort options
377+
sortBy := query.Get("sortBy")
378+
sortBy, err := validateRunSortOptions(sortBy)
379+
if err != nil {
380+
return nil, nil, err
381+
}
382+
SortOrderAscending, _ := strconv.ParseBool(query.Get("ascending"))
383+
runSortOptions := &entity.SortOptions{
384+
Field: sortBy,
385+
Ascending: SortOrderAscending,
386+
}
387+
return &filter, runSortOptions, nil
376388
}
377389

378390
func (m *StackManager) ImportTerraformResourceID(ctx context.Context, sp *v1.Spec, importedResources map[string]string) {
@@ -530,3 +542,19 @@ func validateExecuteRequestParams(params *StackRequestParams) error {
530542
}
531543
return nil
532544
}
545+
546+
func validateRunSortOptions(sortBy string) (string, error) {
547+
if sortBy == "" {
548+
return constant.SortByID, nil
549+
}
550+
if sortBy != constant.SortByID && sortBy != constant.SortByCreateTimestamp {
551+
return "", fmt.Errorf("invalid sort option: %s. Can only sort by id or create timestamp", sortBy)
552+
}
553+
switch sortBy {
554+
case constant.SortByCreateTimestamp:
555+
return "created_at", nil
556+
case constant.SortByModifiedTimestamp:
557+
return "updated_at", nil
558+
}
559+
return sortBy, nil
560+
}

0 commit comments

Comments
 (0)