Skip to content

Commit d5b04ef

Browse files
mafredrikylecarbs
authored andcommitted
feat: Implement unified pagination and add template versions support (#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 (#1318) * Add scripts/apitypings/main.go to Makefile
1 parent af2e809 commit d5b04ef

File tree

18 files changed

+540
-167
lines changed

18 files changed

+540
-167
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
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

Lines changed: 59 additions & 20 deletions
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

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

Lines changed: 54 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/templateversions.sql

Lines changed: 27 additions & 1 deletion
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

Lines changed: 13 additions & 16 deletions
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

Lines changed: 57 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy