Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2d3dc43

Browse files
authored
feat: Implement unified pagination and add template versions support (coder#1308)
* feat: Implement pagination for template versions * feat: Use unified pagination between users and template versions * Sync codepaths between users and template versions * Create requestOption type in codersdk and add test * Fix created_at edge case for pagination cursor in queries * feat: Add support for json omitempty and embedded structs in apitypings (coder#1318) * Add scripts/apitypings/main.go to Makefile
1 parent dc115b8 commit 2d3dc43

18 files changed

+540
-167
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ site/out/index.html: $(shell find ./site -not -path './site/node_modules/*' -typ
8383
# Restores GITKEEP files!
8484
git checkout HEAD site/out
8585

86-
site/src/api/typesGenerated.ts: $(shell find codersdk -type f -name '*.go')
86+
site/src/api/typesGenerated.ts: scripts/apitypings/main.go $(shell find codersdk -type f -name '*.go')
8787
go run scripts/apitypings/main.go > site/src/api/typesGenerated.ts
8888
cd site && yarn run format:types
8989

coderd/database/databasefake/databasefake.go

+59-20
Original file line numberDiff line numberDiff line change
@@ -172,25 +172,25 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
172172
q.mutex.RLock()
173173
defer q.mutex.RUnlock()
174174

175-
users := q.users
175+
// Avoid side-effect of sorting.
176+
users := make([]database.User, len(q.users))
177+
copy(users, q.users)
178+
176179
// Database orders by created_at
177-
sort.Slice(users, func(i, j int) bool {
178-
if users[i].CreatedAt.Equal(users[j].CreatedAt) {
180+
slices.SortFunc(users, func(a, b database.User) bool {
181+
if a.CreatedAt.Equal(b.CreatedAt) {
179182
// Technically the postgres database also orders by uuid. So match
180183
// that behavior
181-
return users[i].ID.String() < users[j].ID.String()
184+
return a.ID.String() < b.ID.String()
182185
}
183-
return users[i].CreatedAt.Before(users[j].CreatedAt)
186+
return a.CreatedAt.Before(b.CreatedAt)
184187
})
185188

186-
if params.AfterUser != uuid.Nil {
189+
if params.AfterID != uuid.Nil {
187190
found := false
188-
for i := range users {
189-
if users[i].ID == params.AfterUser {
191+
for i, v := range users {
192+
if v.ID == params.AfterID {
190193
// We want to return all users after index i.
191-
if i+1 >= len(users) {
192-
return []database.User{}, nil
193-
}
194194
users = users[i+1:]
195195
found = true
196196
break
@@ -199,7 +199,7 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
199199

200200
// If no users after the time, then we return an empty list.
201201
if !found {
202-
return []database.User{}, nil
202+
return nil, sql.ErrNoRows
203203
}
204204
}
205205

@@ -227,7 +227,7 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
227227

228228
if params.OffsetOpt > 0 {
229229
if int(params.OffsetOpt) > len(users)-1 {
230-
return []database.User{}, nil
230+
return nil, sql.ErrNoRows
231231
}
232232
users = users[params.OffsetOpt:]
233233
}
@@ -239,10 +239,7 @@ func (q *fakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams
239239
users = users[:params.LimitOpt]
240240
}
241241

242-
tmp := make([]database.User, len(users))
243-
copy(tmp, users)
244-
245-
return tmp, nil
242+
return users, nil
246243
}
247244

248245
func (q *fakeQuerier) GetAllUserRoles(_ context.Context, userID uuid.UUID) (database.GetAllUserRolesRow, error) {
@@ -621,20 +618,62 @@ func (q *fakeQuerier) GetTemplateByOrganizationAndName(_ context.Context, arg da
621618
return database.Template{}, sql.ErrNoRows
622619
}
623620

624-
func (q *fakeQuerier) GetTemplateVersionsByTemplateID(_ context.Context, templateID uuid.UUID) ([]database.TemplateVersion, error) {
621+
func (q *fakeQuerier) GetTemplateVersionsByTemplateID(_ context.Context, arg database.GetTemplateVersionsByTemplateIDParams) (version []database.TemplateVersion, err error) {
625622
q.mutex.RLock()
626623
defer q.mutex.RUnlock()
627624

628-
version := make([]database.TemplateVersion, 0)
629625
for _, templateVersion := range q.templateVersions {
630-
if templateVersion.TemplateID.UUID.String() != templateID.String() {
626+
if templateVersion.TemplateID.UUID.String() != arg.TemplateID.String() {
631627
continue
632628
}
633629
version = append(version, templateVersion)
634630
}
631+
632+
// Database orders by created_at
633+
slices.SortFunc(version, func(a, b database.TemplateVersion) bool {
634+
if a.CreatedAt.Equal(b.CreatedAt) {
635+
// Technically the postgres database also orders by uuid. So match
636+
// that behavior
637+
return a.ID.String() < b.ID.String()
638+
}
639+
return a.CreatedAt.Before(b.CreatedAt)
640+
})
641+
642+
if arg.AfterID != uuid.Nil {
643+
found := false
644+
for i, v := range version {
645+
if v.ID == arg.AfterID {
646+
// We want to return all users after index i.
647+
version = version[i+1:]
648+
found = true
649+
break
650+
}
651+
}
652+
653+
// If no users after the time, then we return an empty list.
654+
if !found {
655+
return nil, sql.ErrNoRows
656+
}
657+
}
658+
659+
if arg.OffsetOpt > 0 {
660+
if int(arg.OffsetOpt) > len(version)-1 {
661+
return nil, sql.ErrNoRows
662+
}
663+
version = version[arg.OffsetOpt:]
664+
}
665+
666+
if arg.LimitOpt > 0 {
667+
if int(arg.LimitOpt) > len(version) {
668+
arg.LimitOpt = int32(len(version))
669+
}
670+
version = version[:arg.LimitOpt]
671+
}
672+
635673
if len(version) == 0 {
636674
return nil, sql.ErrNoRows
637675
}
676+
638677
return version, nil
639678
}
640679

coderd/database/querier.go

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

+54-19
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/templateversions.sql

+27-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,33 @@ SELECT
44
FROM
55
template_versions
66
WHERE
7-
template_id = $1 :: uuid;
7+
template_id = @template_id :: uuid
8+
AND CASE
9+
-- This allows using the last element on a page as effectively a cursor.
10+
-- This is an important option for scripts that need to paginate without
11+
-- duplicating or missing data.
12+
WHEN @after_id :: uuid != '00000000-00000000-00000000-00000000' THEN (
13+
-- The pagination cursor is the last ID of the previous page.
14+
-- The query is ordered by the created_at field, so select all
15+
-- rows after the cursor.
16+
(created_at, id) > (
17+
SELECT
18+
created_at, id
19+
FROM
20+
template_versions
21+
WHERE
22+
id = @after_id
23+
)
24+
)
25+
ELSE true
26+
END
27+
ORDER BY
28+
-- Deterministic and consistent ordering of all rows, even if they share
29+
-- a timestamp. This is to ensure consistent pagination.
30+
(created_at, id) ASC OFFSET @offset_opt
31+
LIMIT
32+
-- A null limit means "no limit", so -1 means return all
33+
NULLIF(@limit_opt :: int, -1);
834

935
-- name: GetTemplateVersionByJobID :one
1036
SELECT

coderd/database/queries/users.sql

+13-16
Original file line numberDiff line numberDiff line change
@@ -77,23 +77,20 @@ WHERE
7777
-- This allows using the last element on a page as effectively a cursor.
7878
-- This is an important option for scripts that need to paginate without
7979
-- duplicating or missing data.
80-
WHEN @after_user :: uuid != '00000000-00000000-00000000-00000000' THEN (
81-
-- The pagination cursor is the last user of the previous page.
82-
-- The query is ordered by the created_at field, so select all
83-
-- users after the cursor. We also want to include any users
84-
-- that share the created_at (super rare).
85-
created_at >= (
86-
SELECT
87-
created_at
88-
FROM
89-
users
90-
WHERE
91-
id = @after_user
92-
)
93-
-- Omit the cursor from the final.
94-
AND id != @after_user
80+
WHEN @after_id :: uuid != '00000000-00000000-00000000-00000000' THEN (
81+
-- The pagination cursor is the last ID of the previous page.
82+
-- The query is ordered by the created_at field, so select all
83+
-- rows after the cursor.
84+
(created_at, id) > (
85+
SELECT
86+
created_at, id
87+
FROM
88+
users
89+
WHERE
90+
id = @after_id
9591
)
96-
ELSE true
92+
)
93+
ELSE true
9794
END
9895
-- Start filters
9996
-- Filter by name, email or username

coderd/pagination.go

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package coderd
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strconv"
7+
8+
"github.com/google/uuid"
9+
10+
"github.com/coder/coder/coderd/httpapi"
11+
"github.com/coder/coder/codersdk"
12+
)
13+
14+
// parsePagination extracts pagination query params from the http request.
15+
// If an error is encountered, the error is written to w and ok is set to false.
16+
func parsePagination(w http.ResponseWriter, r *http.Request) (p codersdk.Pagination, ok bool) {
17+
var (
18+
afterID = uuid.Nil
19+
limit = -1 // Default to no limit and return all results.
20+
offset = 0
21+
)
22+
23+
var err error
24+
if s := r.URL.Query().Get("after_id"); s != "" {
25+
afterID, err = uuid.Parse(r.URL.Query().Get("after_id"))
26+
if err != nil {
27+
httpapi.Write(w, http.StatusBadRequest, httpapi.Response{
28+
Message: fmt.Sprintf("after_id must be a valid uuid: %s", err.Error()),
29+
})
30+
return p, false
31+
}
32+
}
33+
if s := r.URL.Query().Get("limit"); s != "" {
34+
limit, err = strconv.Atoi(s)
35+
if err != nil {
36+
httpapi.Write(w, http.StatusBadRequest, httpapi.Response{
37+
Message: fmt.Sprintf("limit must be an integer: %s", err.Error()),
38+
})
39+
return p, false
40+
}
41+
}
42+
if s := r.URL.Query().Get("offset"); s != "" {
43+
offset, err = strconv.Atoi(s)
44+
if err != nil {
45+
httpapi.Write(w, http.StatusBadRequest, httpapi.Response{
46+
Message: fmt.Sprintf("offset must be an integer: %s", err.Error()),
47+
})
48+
return p, false
49+
}
50+
}
51+
52+
return codersdk.Pagination{
53+
AfterID: afterID,
54+
Limit: limit,
55+
Offset: offset,
56+
}, true
57+
}

0 commit comments

Comments
 (0)