Skip to content
Merged
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
13 changes: 10 additions & 3 deletions core/pagination/pagination.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import (
)

const (
DefaultPage = 1
DefaultLimit = 10
DefaultPage = 1
DefaultLimit = 10
DefaultMaxLimit = 100
)

type Params struct {
Expand All @@ -24,13 +25,19 @@ type Pagination struct {
HasMore bool `json:"has_more" required:"true" nullable:"false"`
}

func Clamp(params Params) Params {
func Clamp(params Params, maxLimit int) Params {
if maxLimit <= 0 {
maxLimit = DefaultMaxLimit
}
if params.Page < DefaultPage {
params.Page = DefaultPage
}
if params.Limit <= 0 {
params.Limit = DefaultLimit
}
if params.Limit > maxLimit {
params.Limit = maxLimit
}
return params
}

Expand Down
56 changes: 53 additions & 3 deletions core/pagination/pagination_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,91 @@ func TestClamp(t *testing.T) {
tests := []struct {
name string
params pagination.Params
maxLimit int
expected pagination.Params
}{
{
name: "valid params are untouched",
params: pagination.Params{Page: 1, Limit: 10},
maxLimit: pagination.DefaultMaxLimit,
expected: pagination.Params{Page: 1, Limit: 10},
},
{
name: "page zero becomes the first page",
params: pagination.Params{Page: 0, Limit: 10},
maxLimit: pagination.DefaultMaxLimit,
expected: pagination.Params{Page: 1, Limit: 10},
},
{
name: "negative page becomes the first page",
params: pagination.Params{Page: -5, Limit: 10},
maxLimit: pagination.DefaultMaxLimit,
expected: pagination.Params{Page: 1, Limit: 10},
},
{
name: "zero limit falls back to the default limit",
params: pagination.Params{Page: 3, Limit: 0},
maxLimit: pagination.DefaultMaxLimit,
expected: pagination.Params{Page: 3, Limit: pagination.DefaultLimit},
},
{
name: "negative limit falls back to the default limit",
params: pagination.Params{Page: 3, Limit: -1},
maxLimit: pagination.DefaultMaxLimit,
expected: pagination.Params{Page: 3, Limit: pagination.DefaultLimit},
},
{
name: "large limits are left to the caller",
name: "limit exactly at the maximum is not capped",
params: pagination.Params{Page: 1, Limit: pagination.DefaultMaxLimit},
maxLimit: pagination.DefaultMaxLimit,
expected: pagination.Params{Page: 1, Limit: pagination.DefaultMaxLimit},
},
{
name: "limit above the maximum is capped",
params: pagination.Params{Page: 1, Limit: pagination.DefaultMaxLimit + 1},
maxLimit: pagination.DefaultMaxLimit,
expected: pagination.Params{Page: 1, Limit: pagination.DefaultMaxLimit},
},
{
name: "an absurdly large limit is capped",
params: pagination.Params{Page: 1, Limit: 1_000_000},
maxLimit: pagination.DefaultMaxLimit,
expected: pagination.Params{Page: 1, Limit: pagination.DefaultMaxLimit},
},
{
name: "a zero maximum falls back to the default maximum",
params: pagination.Params{Page: 1, Limit: 1_000_000},
maxLimit: 0,
expected: pagination.Params{Page: 1, Limit: pagination.DefaultMaxLimit},
},
{
name: "a negative maximum falls back to the default maximum",
params: pagination.Params{Page: 1, Limit: 1_000_000},
expected: pagination.Params{Page: 1, Limit: 1_000_000},
maxLimit: -1,
expected: pagination.Params{Page: 1, Limit: pagination.DefaultMaxLimit},
},
{
name: "a configured maximum above the default is honoured",
params: pagination.Params{Page: 1, Limit: 400},
maxLimit: 500,
expected: pagination.Params{Page: 1, Limit: 400},
},
{
name: "a limit above a configured maximum is capped to it",
params: pagination.Params{Page: 1, Limit: 501},
maxLimit: 500,
expected: pagination.Params{Page: 1, Limit: 500},
},
{
name: "a maximum below the default limit also caps the default",
params: pagination.Params{Page: 1, Limit: 0},
maxLimit: 5,
expected: pagination.Params{Page: 1, Limit: 5},
},
{
name: "high page numbers are legal",
params: pagination.Params{Page: 999999, Limit: 10},
maxLimit: pagination.DefaultMaxLimit,
expected: pagination.Params{Page: 999999, Limit: 10},
},
}
Expand All @@ -59,7 +109,7 @@ func TestClamp(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

require.Equal(t, tt.expected, pagination.Clamp(tt.params))
require.Equal(t, tt.expected, pagination.Clamp(tt.params, tt.maxLimit))
})
}
}
Expand Down
20 changes: 15 additions & 5 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2404,20 +2404,22 @@
"Organizations"
],
"summary": "List organizations",
"description": "Lists every organization the authenticated user can access, both the ones they own and the ones they are a member of, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected.",
"description": "Lists every organization the authenticated user can access, both the ones they own and the ones they are a member of, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. `limit` is capped by the server's configured maximum page size, which defaults to 100; a larger value is silently reduced to the cap rather than rejected, and values below the minimum fall back to the defaults. The `pagination` object in the response always reports the values actually applied.",
"operationId": "listOrganizations",
"parameters": [
{
"name": "page",
"in": "query",
"schema": {
"default": 1,
"type": "integer"
}
},
{
"name": "limit",
"in": "query",
"schema": {
"default": 10,
"type": "integer"
}
}
Expand Down Expand Up @@ -2572,20 +2574,22 @@
"Organization Invitations"
],
"summary": "List invitations",
"description": "Lists the invitations for an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected.",
"description": "Lists the invitations for an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. `limit` is capped by the server's configured maximum page size, which defaults to 100; a larger value is silently reduced to the cap rather than rejected, and values below the minimum fall back to the defaults. The `pagination` object in the response always reports the values actually applied.",
"operationId": "listOrganizationInvitations",
"parameters": [
{
"name": "page",
"in": "query",
"schema": {
"default": 1,
"type": "integer"
}
},
{
"name": "limit",
"in": "query",
"schema": {
"default": 10,
"type": "integer"
}
},
Expand Down Expand Up @@ -2840,20 +2844,22 @@
"Organization Members"
],
"summary": "List members",
"description": "Lists the members of an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected.",
"description": "Lists the members of an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. `limit` is capped by the server's configured maximum page size, which defaults to 100; a larger value is silently reduced to the cap rather than rejected, and values below the minimum fall back to the defaults. The `pagination` object in the response always reports the values actually applied.",
"operationId": "listOrganizationMembers",
"parameters": [
{
"name": "page",
"in": "query",
"schema": {
"default": 1,
"type": "integer"
}
},
{
"name": "limit",
"in": "query",
"schema": {
"default": 10,
"type": "integer"
}
},
Expand Down Expand Up @@ -3090,20 +3096,22 @@
"Organization Teams"
],
"summary": "List teams",
"description": "Lists the teams within an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected.",
"description": "Lists the teams within an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. `limit` is capped by the server's configured maximum page size, which defaults to 100; a larger value is silently reduced to the cap rather than rejected, and values below the minimum fall back to the defaults. The `pagination` object in the response always reports the values actually applied.",
"operationId": "listOrganizationTeams",
"parameters": [
{
"name": "page",
"in": "query",
"schema": {
"default": 1,
"type": "integer"
}
},
{
"name": "limit",
"in": "query",
"schema": {
"default": 10,
"type": "integer"
}
},
Expand Down Expand Up @@ -3300,20 +3308,22 @@
"Organization Team Members"
],
"summary": "List team members",
"description": "Lists the members of a team, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected.",
"description": "Lists the members of a team, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. `limit` is capped by the server's configured maximum page size, which defaults to 100; a larger value is silently reduced to the cap rather than rejected, and values below the minimum fall back to the defaults. The `pagination` object in the response always reports the values actually applied.",
"operationId": "listOrganizationTeamMembers",
"parameters": [
{
"name": "page",
"in": "query",
"schema": {
"default": 1,
"type": "integer"
}
},
{
"name": "limit",
"in": "query",
"schema": {
"default": 10,
"type": "integer"
}
},
Expand Down
2 changes: 1 addition & 1 deletion plugins/organizations/handlers/handler_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func defaultServiceUtils() *orgservices.ServiceUtils {
memberRepo := &orgtests.MockOrganizationMemberRepository{}
orgRepo.On("GetByID", mock.Anything, mock.Anything).Return(&orgtypes.Organization{ID: "org-1", OwnerID: "user-1"}, nil).Maybe()
memberRepo.On("GetByOrganizationIDAndUserID", mock.Anything, mock.Anything, mock.Anything).Return(&orgtypes.OrganizationMember{ID: "mem-1", OrganizationID: "org-1", UserID: "user-1", Role: "admin"}, nil).Maybe()
return orgservices.NewServiceUtils(orgRepo, memberRepo, nil)
return orgservices.NewServiceUtils(orgRepo, memberRepo, nil, 0)
}

func defaultAccessControlService() *orgtests.AccessControlServiceStub {
Expand Down
10 changes: 5 additions & 5 deletions plugins/organizations/openapi/openapi_docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func RegisterOpenAPIDocs(svc openapi.OpenAPIService) error {
"/organizations",
openapi.WithOperationID("listOrganizations"),
openapi.WithSummary("List organizations"),
openapi.WithDescription("Lists every organization the authenticated user can access, both the ones they own and the ones they are a member of, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected."),
openapi.WithDescription("Lists every organization the authenticated user can access, both the ones they own and the ones they are a member of, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. `limit` is capped by the server's configured maximum page size, which defaults to 100; a larger value is silently reduced to the cap rather than rejected, and values below the minimum fall back to the defaults. The `pagination` object in the response always reports the values actually applied."),
openapi.WithTags("Organizations"),
openapi.WithRequest(&types.ListOrganizationsRequest{}),
openapi.WithResponseStatus(http.StatusOK, &types.ListOrganizationsResponse{}),
Expand Down Expand Up @@ -81,7 +81,7 @@ func RegisterOpenAPIDocs(svc openapi.OpenAPIService) error {
"/organizations/{organization_id}/invitations",
openapi.WithOperationID("listOrganizationInvitations"),
openapi.WithSummary("List invitations"),
openapi.WithDescription("Lists the invitations for an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected."),
openapi.WithDescription("Lists the invitations for an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. `limit` is capped by the server's configured maximum page size, which defaults to 100; a larger value is silently reduced to the cap rather than rejected, and values below the minimum fall back to the defaults. The `pagination` object in the response always reports the values actually applied."),
openapi.WithTags("Organization Invitations"),
openapi.WithRequest(&types.ListOrganizationInvitationsRequest{}),
openapi.WithResponseStatus(http.StatusOK, &types.ListOrganizationInvitationsResponse{}),
Expand Down Expand Up @@ -144,7 +144,7 @@ func RegisterOpenAPIDocs(svc openapi.OpenAPIService) error {
"/organizations/{organization_id}/members",
openapi.WithOperationID("listOrganizationMembers"),
openapi.WithSummary("List members"),
openapi.WithDescription("Lists the members of an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected."),
openapi.WithDescription("Lists the members of an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. `limit` is capped by the server's configured maximum page size, which defaults to 100; a larger value is silently reduced to the cap rather than rejected, and values below the minimum fall back to the defaults. The `pagination` object in the response always reports the values actually applied."),
openapi.WithTags("Organization Members"),
openapi.WithRequest(&types.ListOrganizationMembersRequest{}),
openapi.WithResponseStatus(http.StatusOK, &types.ListOrganizationMembersResponse{}),
Expand Down Expand Up @@ -208,7 +208,7 @@ func RegisterOpenAPIDocs(svc openapi.OpenAPIService) error {
"/organizations/{organization_id}/teams",
openapi.WithOperationID("listOrganizationTeams"),
openapi.WithSummary("List teams"),
openapi.WithDescription("Lists the teams within an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected."),
openapi.WithDescription("Lists the teams within an organization, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. `limit` is capped by the server's configured maximum page size, which defaults to 100; a larger value is silently reduced to the cap rather than rejected, and values below the minimum fall back to the defaults. The `pagination` object in the response always reports the values actually applied."),
openapi.WithTags("Organization Teams"),
openapi.WithRequest(&types.ListOrganizationTeamsRequest{}),
openapi.WithResponseStatus(http.StatusOK, &types.ListOrganizationTeamsResponse{}),
Expand Down Expand Up @@ -262,7 +262,7 @@ func RegisterOpenAPIDocs(svc openapi.OpenAPIService) error {
"/organizations/{organization_id}/teams/{team_id}/members",
openapi.WithOperationID("listOrganizationTeamMembers"),
openapi.WithSummary("List team members"),
openapi.WithDescription("Lists the members of a team, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. There is no upper bound on `limit`, so a sufficiently large value returns the whole collection in a single request. Values below the minimum fall back to the defaults rather than being rejected."),
openapi.WithDescription("Lists the members of a team, newest first. Results are paginated: `page` defaults to 1 and `limit` defaults to 10. `limit` is capped by the server's configured maximum page size, which defaults to 100; a larger value is silently reduced to the cap rather than rejected, and values below the minimum fall back to the defaults. The `pagination` object in the response always reports the values actually applied."),
openapi.WithTags("Organization Team Members"),
openapi.WithRequest(&types.ListOrganizationTeamMembersRequest{}),
openapi.WithResponseStatus(http.StatusOK, &types.ListOrganizationTeamMembersResponse{}),
Expand Down
6 changes: 5 additions & 1 deletion plugins/organizations/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ func (p *OrganizationsPlugin) Init(ctx *models.PluginContext) error {
p.teamRepo = repositories.NewBunOrganizationTeamRepository(ctx.DB)
p.teamMemberRepo = repositories.NewBunOrganizationTeamMemberRepository(ctx.DB)

p.serviceUtils = services.NewServiceUtils(p.organizationRepo, p.memberRepo, p.teamRepo)
maxPageLimit := 0
if p.pluginConfig.MaxPageLimit != nil {
maxPageLimit = *p.pluginConfig.MaxPageLimit
}
p.serviceUtils = services.NewServiceUtils(p.organizationRepo, p.memberRepo, p.teamRepo, maxPageLimit)
p.organizationService = services.NewOrganizationService(p.organizationRepo, p.memberRepo, p.serviceUtils, accessControlService, p.pluginConfig.OrganizationsLimit, ctx.DB, p.hooksExecutor)
emailTemplateManager, err := newOrganizationEmailTemplateManager()
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ func (s *organizationInvitationService) ListAllOrganizationInvitationsByOrgIDWit
return nil, coreerrors.ErrNotFound
}

params = pagination.Clamp(params)
params = s.serviceUtils.ClampPagination(params)

invitations, total, err := s.orgInvitationRepo.ListAllByOrganizationIDWithOrg(ctx, organizationID, params.Page, params.Limit)
if err != nil {
Expand Down
Loading