Skip to content

Commit fbe70ea

Browse files
easyCZroboquat
authored andcommitted
[public-api] Implement ProjectsService.ListProjects
1 parent 1dabfa7 commit fbe70ea

File tree

4 files changed

+271
-1
lines changed

4 files changed

+271
-1
lines changed

components/public-api-server/pkg/apiv1/pagination.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,22 @@ func paginationToDB(p *v1.Pagination) db.Pagination {
3636
PageSize: int(validated.GetPageSize()),
3737
}
3838
}
39+
40+
func pageFromResults[T any](results []T, p *v1.Pagination) []T {
41+
pagination := validatePagination(p)
42+
43+
size := len(results)
44+
45+
start := int((pagination.Page - 1) * pagination.PageSize)
46+
end := int(pagination.Page * pagination.PageSize)
47+
48+
if start > size {
49+
return nil
50+
}
51+
52+
if end > size {
53+
end = size
54+
}
55+
56+
return results[start:end]
57+
}

components/public-api-server/pkg/apiv1/pagination_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,33 @@ func TestValidatePagination(t *testing.T) {
7575
Page: 9,
7676
}))
7777
})
78+
}
79+
80+
func TestPageFromResults(t *testing.T) {
81+
var results []int
82+
for i := 0; i < 26; i++ {
83+
results = append(results, i)
84+
}
85+
86+
require.EqualValues(t, results[0:25], pageFromResults(results, &v1.Pagination{}), "defaults to first page and 25 records")
87+
require.EqualValues(t, results[0:5], pageFromResults(results, &v1.Pagination{
88+
PageSize: 5,
89+
}), "defaults to first page, 10 records")
90+
require.EqualValues(t, results[5:10], pageFromResults(results, &v1.Pagination{
91+
PageSize: 5,
92+
Page: 2,
93+
}), "second page, 5 records")
94+
require.EqualValues(t, results[10:15], pageFromResults(results, &v1.Pagination{
95+
PageSize: 5,
96+
Page: 3,
97+
}), "third page, 5 records")
98+
require.EqualValues(t, results[25:], pageFromResults(results, &v1.Pagination{
99+
PageSize: 5,
100+
Page: 6,
101+
}), "last page, 5 records")
102+
require.Len(t, pageFromResults(results, &v1.Pagination{
103+
PageSize: 5,
104+
Page: 7,
105+
}), 0, "out of bound page, 5 records")
78106

79107
}

components/public-api-server/pkg/apiv1/project.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,59 @@ func (s *ProjectsService) CreateProject(ctx context.Context, req *connect.Reques
9191
}), nil
9292
}
9393

94+
func (s *ProjectsService) ListProjects(ctx context.Context, req *connect.Request[v1.ListProjectsRequest]) (*connect.Response[v1.ListProjectsResponse], error) {
95+
userID, teamID := req.Msg.GetUserId(), req.Msg.GetTeamId()
96+
if userID == "" && teamID == "" {
97+
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Neither User ID nor Team ID specified. Specify one of them."))
98+
}
99+
100+
if userID != "" && teamID != "" {
101+
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Specifying both User ID and Team ID is not allowed."))
102+
}
103+
104+
conn, err := s.getConnection(ctx)
105+
if err != nil {
106+
return nil, err
107+
}
108+
109+
var projects []*protocol.Project
110+
111+
if userID != "" {
112+
_, err := uuid.Parse(userID)
113+
if err != nil {
114+
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("User ID is not a valid UUID."))
115+
}
116+
117+
projects, err = conn.GetUserProjects(ctx)
118+
if err != nil {
119+
return nil, proxy.ConvertError(err)
120+
}
121+
}
122+
123+
if teamID != "" {
124+
_, err := uuid.Parse(teamID)
125+
if err != nil {
126+
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Team ID is not a valid UUID."))
127+
}
128+
129+
projects, err = conn.GetTeamProjects(ctx, teamID)
130+
if err != nil {
131+
return nil, proxy.ConvertError(err)
132+
}
133+
}
134+
135+
// We're extracting a particular page of results from the full set of results.
136+
// This is wasteful, but necessary, until we either:
137+
// * Add new APIs to server which support pagination
138+
// * Port the query logic to Public API
139+
results := pageFromResults(projects, req.Msg.GetPagination())
140+
141+
return connect.NewResponse(&v1.ListProjectsResponse{
142+
Projects: projectsToAPIResponse(results),
143+
TotalResults: int32(len(projects)),
144+
}), nil
145+
}
146+
94147
func (s *ProjectsService) getConnection(ctx context.Context) (protocol.APIInterface, error) {
95148
token, err := auth.TokenFromContext(ctx)
96149
if err != nil {
@@ -106,6 +159,15 @@ func (s *ProjectsService) getConnection(ctx context.Context) (protocol.APIInterf
106159
return conn, nil
107160
}
108161

162+
func projectsToAPIResponse(ps []*protocol.Project) []*v1.Project {
163+
var projects []*v1.Project
164+
for _, p := range ps {
165+
projects = append(projects, projectToAPIResponse(p))
166+
}
167+
168+
return projects
169+
}
170+
109171
func projectToAPIResponse(p *protocol.Project) *v1.Project {
110172
return &v1.Project{
111173
Id: p.ID,

components/public-api-server/pkg/apiv1/project_test.go

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
func TestProjectsService_CreateProject(t *testing.T) {
2626

2727
t.Run("returns invalid argument when request validation fails", func(t *testing.T) {
28-
2928
for _, s := range []struct {
3029
Name string
3130
Spec *v1.Project
@@ -149,7 +148,169 @@ func TestProjectsService_CreateProject(t *testing.T) {
149148
}, response.Msg)
150149

151150
})
151+
}
152+
153+
func TestProjectsService_ListProjects(t *testing.T) {
154+
155+
t.Run("invalid argument when both team id and project id are missing", func(t *testing.T) {
156+
_, client := setupProjectsService(t)
157+
158+
_, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{}))
159+
require.Error(t, err)
160+
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
161+
})
162+
163+
t.Run("invalid argument when both team id and project id are set", func(t *testing.T) {
164+
_, client := setupProjectsService(t)
165+
166+
_, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
167+
UserId: "user-id",
168+
TeamId: "team-id",
169+
}))
170+
require.Error(t, err)
171+
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
172+
})
173+
174+
t.Run("invalid argument when user ID is not a valid UUID", func(t *testing.T) {
175+
_, client := setupProjectsService(t)
176+
177+
_, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
178+
UserId: "some-id",
179+
}))
180+
require.Error(t, err)
181+
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
182+
})
183+
184+
t.Run("invalid argument when team ID is not a valid UUID", func(t *testing.T) {
185+
_, client := setupProjectsService(t)
186+
187+
_, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
188+
TeamId: "some-id",
189+
}))
190+
require.Error(t, err)
191+
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
192+
})
193+
194+
t.Run("no projects from server return empty list", func(t *testing.T) {
195+
serverMock, client := setupProjectsService(t)
152196

197+
serverMock.EXPECT().GetUserProjects(gomock.Any()).Return(nil, nil)
198+
199+
response, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
200+
UserId: uuid.New().String(),
201+
}))
202+
require.NoError(t, err)
203+
requireEqualProto(t, &v1.ListProjectsResponse{
204+
Projects: nil,
205+
TotalResults: 0,
206+
}, response.Msg)
207+
})
208+
209+
t.Run("retrieves projects for user, when user ID is specified and paginates", func(t *testing.T) {
210+
serverMock, client := setupProjectsService(t)
211+
212+
projects := []*protocol.Project{
213+
newProject(&protocol.Project{}),
214+
newProject(&protocol.Project{}),
215+
newProject(&protocol.Project{}),
216+
newProject(&protocol.Project{}),
217+
newProject(&protocol.Project{}),
218+
}
219+
220+
serverMock.EXPECT().GetUserProjects(gomock.Any()).Return(projects, nil).Times(3)
221+
222+
firstPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
223+
UserId: uuid.New().String(),
224+
Pagination: &v1.Pagination{
225+
PageSize: 2,
226+
},
227+
}))
228+
require.NoError(t, err)
229+
requireEqualProto(t, &v1.ListProjectsResponse{
230+
Projects: projectsToAPIResponse(projects[0:2]),
231+
TotalResults: int32(len(projects)),
232+
}, firstPage.Msg)
233+
234+
secondPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
235+
UserId: uuid.New().String(),
236+
Pagination: &v1.Pagination{
237+
PageSize: 2,
238+
Page: 2,
239+
},
240+
}))
241+
require.NoError(t, err)
242+
requireEqualProto(t, &v1.ListProjectsResponse{
243+
Projects: projectsToAPIResponse(projects[2:4]),
244+
TotalResults: int32(len(projects)),
245+
}, secondPage.Msg)
246+
247+
thirdPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
248+
UserId: uuid.New().String(),
249+
Pagination: &v1.Pagination{
250+
PageSize: 2,
251+
Page: 3,
252+
},
253+
}))
254+
require.NoError(t, err)
255+
requireEqualProto(t, &v1.ListProjectsResponse{
256+
Projects: projectsToAPIResponse(projects[4:]),
257+
TotalResults: int32(len(projects)),
258+
}, thirdPage.Msg)
259+
})
260+
261+
t.Run("retrieves projects for team, when team ID is specified and paginates", func(t *testing.T) {
262+
serverMock, client := setupProjectsService(t)
263+
264+
teamID := uuid.New().String()
265+
266+
projects := []*protocol.Project{
267+
newProject(&protocol.Project{}),
268+
newProject(&protocol.Project{}),
269+
newProject(&protocol.Project{}),
270+
newProject(&protocol.Project{}),
271+
newProject(&protocol.Project{}),
272+
}
273+
274+
serverMock.EXPECT().GetTeamProjects(gomock.Any(), teamID).Return(projects, nil).Times(3)
275+
276+
firstPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
277+
TeamId: teamID,
278+
Pagination: &v1.Pagination{
279+
PageSize: 2,
280+
},
281+
}))
282+
require.NoError(t, err)
283+
requireEqualProto(t, &v1.ListProjectsResponse{
284+
Projects: projectsToAPIResponse(projects[0:2]),
285+
TotalResults: int32(len(projects)),
286+
}, firstPage.Msg)
287+
288+
secondPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
289+
TeamId: teamID,
290+
Pagination: &v1.Pagination{
291+
PageSize: 2,
292+
Page: 2,
293+
},
294+
}))
295+
require.NoError(t, err)
296+
requireEqualProto(t, &v1.ListProjectsResponse{
297+
Projects: projectsToAPIResponse(projects[2:4]),
298+
TotalResults: int32(len(projects)),
299+
}, secondPage.Msg)
300+
301+
thirdPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
302+
TeamId: teamID,
303+
Pagination: &v1.Pagination{
304+
PageSize: 2,
305+
Page: 3,
306+
},
307+
}))
308+
require.NoError(t, err)
309+
requireEqualProto(t, &v1.ListProjectsResponse{
310+
Projects: projectsToAPIResponse(projects[4:]),
311+
TotalResults: int32(len(projects)),
312+
}, thirdPage.Msg)
313+
})
153314
}
154315

155316
func setupProjectsService(t *testing.T) (*protocol.MockAPIInterface, v1connect.ProjectsServiceClient) {

0 commit comments

Comments
 (0)