Skip to content

Stevenmasley/paged users #1057

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type Options struct {

SecureAuthCookie bool
SSHKeygenAlgorithm gitsshkey.Algorithm
APIRateLimit int
}

// New constructs the Coder API into an HTTP handler.
Expand All @@ -48,6 +49,9 @@ func New(options *Options) (http.Handler, func()) {
if options.AgentConnectionUpdateFrequency == 0 {
options.AgentConnectionUpdateFrequency = 3 * time.Second
}
if options.APIRateLimit == 0 {
options.APIRateLimit = 512
}
api := &api{
Options: options,
}
Expand All @@ -57,7 +61,7 @@ func New(options *Options) (http.Handler, func()) {
r.Use(
chitrace.Middleware(),
// Specific routes can specify smaller limits.
httpmw.RateLimitPerMinute(512),
httpmw.RateLimitPerMinute(options.APIRateLimit),
debugLogRequest(api.Logger),
)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -140,6 +144,7 @@ func New(options *Options) (http.Handler, func()) {
})
})
r.Route("/users", func(r chi.Router) {
r.Get("/", api.getPaginatedUsers)
r.Get("/first", api.firstUser)
r.Post("/first", api.postFirstUser)
r.Post("/login", api.postLogin)
Expand Down
2 changes: 2 additions & 0 deletions coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type Options struct {
AWSInstanceIdentity awsidentity.Certificates
GoogleInstanceIdentity *idtoken.Validator
SSHKeygenAlgorithm gitsshkey.Algorithm
APIRateLimit int
}

// New constructs an in-memory coderd instance and returns
Expand Down Expand Up @@ -117,6 +118,7 @@ func New(t *testing.T, options *Options) *codersdk.Client {
AWSCertificates: options.AWSInstanceIdentity,
GoogleTokenValidator: options.GoogleInstanceIdentity,
SSHKeygenAlgorithm: options.SSHKeygenAlgorithm,
APIRateLimit: options.APIRateLimit,
})
t.Cleanup(func() {
srv.Close()
Expand Down
9 changes: 9 additions & 0 deletions coderd/database/databasefake/databasefake.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package databasefake
import (
"context"
"database/sql"
"golang.org/x/xerrors"
"strings"
"sync"

Expand Down Expand Up @@ -1373,3 +1374,11 @@ func (q *fakeQuerier) DeleteGitSSHKey(_ context.Context, userID uuid.UUID) error
}
return sql.ErrNoRows
}

func (q *fakeQuerier) PaginatedUsersAfter(ctx context.Context, arg database.PaginatedUsersAfterParams) ([]database.User, error) {
return nil, xerrors.Errorf("not implemented")
}

func (q *fakeQuerier) PaginatedUsersBefore(ctx context.Context, arg database.PaginatedUsersBeforeParams) ([]database.User, error) {
return nil, xerrors.Errorf("not implemented")
}
2 changes: 2 additions & 0 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

132 changes: 132 additions & 0 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 53 additions & 0 deletions coderd/database/queries/users.sql
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,56 @@ SET
updated_at = $5
WHERE
id = $1 RETURNING *;


-- name: PaginatedUsersAfter :many
SELECT
*
FROM
users
WHERE
CASE
WHEN @after::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
created_at > (SELECT created_at FROM users WHERE id = @after)
-- If the after field is not provided, just return the first page
ELSE true
END
AND
CASE
WHEN @email::text != '' THEN
email LIKE '%' || @email || '%'
ELSE true
END
ORDER BY
created_at ASC
LIMIT
@limit_opt;

-- name: PaginatedUsersBefore :many
SELECT users_before.* FROM
(SELECT
*
FROM
users
WHERE
CASE
WHEN @before::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN
created_at < (SELECT created_at FROM users WHERE id = @before)
-- If the 'before' field is not provided, this will return the last page.
-- Kinda odd, it's just a consequence of spliting the pagination queries into 2
-- functions.
ELSE true
END
AND
CASE
WHEN @email::text != '' THEN
email LIKE '%' || @email || '%'
ELSE true
END
ORDER BY
created_at DESC
LIMIT
@limit_opt) AS users_before
-- Maintain the original ordering of the rows so the pages are the same order
-- as PaginatedUsersAfter.
ORDER BY users_before.created_at ASC;
6 changes: 6 additions & 0 deletions coderd/httpmw/ratelimit.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import (
// RateLimitPerMinute returns a handler that limits requests per-minute based
// on IP, endpoint, and user ID (if available).
func RateLimitPerMinute(count int) func(http.Handler) http.Handler {
// -1 is no rate limit
if count == -1 {
return func(handler http.Handler) http.Handler {
return handler
}
}
return httprate.Limit(
count,
1*time.Minute,
Expand Down
Loading