groupware: add support for anchor and anchor offset pagination

* add query parameters 'anchor' and 'offset':
   - anchor is an object identifier
   - offset is a numeric offset relative to the anchor
This commit is contained in:
Pascal Bleser
2026-04-27 10:29:47 +02:00
parent ce9b4d59bc
commit ffc0e1f42a
16 changed files with 154 additions and 52 deletions

View File

@@ -74,14 +74,14 @@ func (r *ContactCardSearchResults) GetTotal() *uint { return r.Tota
func (r *ContactCardSearchResults) RemoveResults() { r.Results = nil }
func (r *ContactCardSearchResults) SetLimit(limit *uint) { r.Limit = limit }
func (j *Client) QueryContactCards(accountIds []string,
func (j *Client) QueryContactCards(accountIds []string, //NOSONAR
filter ContactCardFilterElement, sortBy []ContactCardComparator,
position int, limit *uint, calculateTotal bool,
position int, anchor string, anchorOffset *int, limit *uint, calculateTotal bool,
ctx Context) (map[string]*ContactCardSearchResults, SessionState, State, Language, Error) {
return queryN(j, "QueryContactCards", ContactCardType,
[]ContactCardComparator{{Property: ContactCardPropertyUpdated, IsAscending: false}},
func(accountId string, filter ContactCardFilterElement, sortBy []ContactCardComparator, position int, limit *uint) ContactCardQueryCommand {
return ContactCardQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Limit: limit, CalculateTotal: calculateTotal}
func(accountId string, filter ContactCardFilterElement, sortBy []ContactCardComparator, position int, anchor string, anchorOffset *int, limit *uint) ContactCardQueryCommand {
return ContactCardQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Anchor: anchor, AnchorOffset: anchorOffset, Limit: limit, CalculateTotal: calculateTotal}
},
func(accountId string, cmd Command, path string, rof string) ContactCardGetRefCommand {
return ContactCardGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: cmd, Path: path, ResultOf: rof}}
@@ -96,7 +96,7 @@ func (j *Client) QueryContactCards(accountIds []string,
}
},
accountIds,
filter, sortBy, limit, position, ctx,
filter, sortBy, position, anchor, anchorOffset, limit, ctx,
)
}

View File

@@ -125,7 +125,7 @@ func (r *EmailSearchResults) SetLimit(limit *uint) { r.Limit = limit }
// Retrieve all the Emails in a given Mailbox by its id.
func (j *Client) GetAllEmailsInMailbox(accountId string, mailboxId string, //NOSONAR
position int, limit *uint, collapseThreads bool, fetchBodies bool, maxBodyValueBytes uint, withThreads bool,
position int, anchor string, anchorOffset *int, limit *uint, collapseThreads bool, fetchBodies bool, maxBodyValueBytes uint, withThreads bool,
ctx Context) (*EmailSearchResults, SessionState, State, Language, Error) {
logger := j.loggerParams("GetAllEmailsInMailbox", ctx, func(z zerolog.Context) zerolog.Context {
l := z.Bool(logFetchBodies, fetchBodies).Int(logPosition, position)
@@ -143,6 +143,8 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, mailboxId string, //NOS
CollapseThreads: collapseThreads,
CalculateTotal: true,
Position: position,
Anchor: anchor,
AnchorOffset: anchorOffset,
Limit: limit,
}

View File

@@ -28,12 +28,12 @@ func (j *Client) GetCalendarEvents(accountId string, eventIds []string, ctx Cont
func (j *Client) QueryCalendarEvents(accountIds []string, //NOSONAR
filter CalendarEventFilterElement, sortBy []CalendarEventComparator,
position int, limit *uint, calculateTotal bool,
position int, anchor string, anchorOffset *int, limit *uint, calculateTotal bool,
ctx Context) (map[string]*CalendarEventSearchResults, SessionState, State, Language, Error) {
return queryN(j, "QueryCalendarEvents", CalendarEventType,
[]CalendarEventComparator{{Property: CalendarEventPropertyStart, IsAscending: false}},
func(accountId string, filter CalendarEventFilterElement, sortBy []CalendarEventComparator, position int, limit *uint) CalendarEventQueryCommand {
return CalendarEventQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Limit: limit, CalculateTotal: calculateTotal}
func(accountId string, filter CalendarEventFilterElement, sortBy []CalendarEventComparator, position int, anchor string, anchorOffset *int, limit *uint) CalendarEventQueryCommand {
return CalendarEventQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Anchor: anchor, AnchorOffset: anchorOffset, Limit: limit, CalculateTotal: calculateTotal}
},
func(accountId string, cmd Command, path string, rof string) CalendarEventGetRefCommand {
return CalendarEventGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: cmd, Path: path, ResultOf: rof}}
@@ -48,7 +48,7 @@ func (j *Client) QueryCalendarEvents(accountIds []string, //NOSONAR
}
},
accountIds,
filter, sortBy, limit, position, ctx,
filter, sortBy, position, anchor, anchorOffset, limit, ctx,
)
}

View File

@@ -185,8 +185,8 @@ func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, //NOS
func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []string, ctx Context) (map[string]*[]string, SessionState, State, Language, Error) {
return queryN(j, "GetMailboxRolesForMultipleAccounts", MailboxType,
[]MailboxComparator{{Property: MailboxPropertySortOrder, IsAscending: true}},
func(accountId string, filter MailboxFilterCondition, sortBy []MailboxComparator, _ int, _ *uint) MailboxQueryCommand {
return MailboxQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, SortAsTree: false, FilterAsTree: false, Position: 0, Limit: nil, CalculateTotal: false}
func(accountId string, filter MailboxFilterCondition, sortBy []MailboxComparator, _ int, _ string, _ *int, _ *uint) MailboxQueryCommand {
return MailboxQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, SortAsTree: false, FilterAsTree: false, Position: 0, Anchor: "", AnchorOffset: nil, Limit: nil, CalculateTotal: false}
},
func(accountId string, cmd Command, path, rof string) MailboxGetRefCommand {
return MailboxGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: cmd, Path: path, ResultOf: rof}}
@@ -196,7 +196,7 @@ func (j *Client) GetMailboxRolesForMultipleAccounts(accountIds []string, ctx Con
slices.Sort(roles)
return &roles
},
accountIds, MailboxFilterCondition{HasAnyRole: truep}, nil, nil, 0,
accountIds, MailboxFilterCondition{HasAnyRole: truep}, nil, 0, "", nil, nil,
ctx,
)
}

View File

@@ -26,14 +26,14 @@ func (r *PrincipalSearchResults) GetTotal() *uint { return r.Total
func (r *PrincipalSearchResults) RemoveResults() { r.Results = nil }
func (r *PrincipalSearchResults) SetLimit(limit *uint) { r.Limit = limit }
func (j *Client) QueryPrincipals(accountId string,
func (j *Client) QueryPrincipals(accountId string, //NOSONAR
filter PrincipalFilterElement, sortBy []PrincipalComparator,
position uint, limit *uint, calculateTotal bool,
position int, anchor string, anchorOffset *int, limit *uint, calculateTotal bool,
ctx Context) (*PrincipalSearchResults, SessionState, State, Language, Error) {
return query(j, "QueryPrincipals", PrincipalType,
[]PrincipalComparator{{Property: PrincipalPropertyName, IsAscending: true}},
func(filter PrincipalFilterElement, sortBy []PrincipalComparator, position uint, limit *uint) PrincipalQueryCommand {
return PrincipalQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Limit: limit, CalculateTotal: calculateTotal}
func(filter PrincipalFilterElement, sortBy []PrincipalComparator, position int, anchor string, anchorOffset *int, limit *uint) PrincipalQueryCommand {
return PrincipalQueryCommand{AccountId: accountId, Filter: filter, Sort: sortBy, Position: position, Anchor: anchor, AnchorOffset: anchorOffset, Limit: limit, CalculateTotal: calculateTotal}
},
func(cmd Command, path string, rof string) PrincipalGetRefCommand {
return PrincipalGetRefCommand{AccountId: accountId, IdsRef: &ResultReference{Name: cmd, Path: path, ResultOf: rof}}
@@ -47,6 +47,6 @@ func (j *Client) QueryPrincipals(accountId string,
Limit: ptrIf(query.Limit, limit != nil),
}
},
filter, sortBy, limit, position, ctx,
filter, sortBy, position, anchor, anchorOffset, limit, ctx,
)
}

View File

@@ -100,14 +100,14 @@ func TestContacts(t *testing.T) {
{Property: ContactCardPropertyCreated, IsAscending: true},
}
contactsByAccount, ss, os, _, err := s.client.QueryContactCards([]string{accountId}, filter, sortBy, 0, nil, true, ctx)
contactsByAccount, ss, os, _, err := s.client.QueryContactCards([]string{accountId}, filter, sortBy, 0, "", nil, nil, true, ctx)
require.NoError(err)
require.Len(contactsByAccount, 1)
require.Contains(contactsByAccount, accountId)
results := contactsByAccount[accountId]
require.Len(results.Results, int(count))
require.Equal(uint(0), results.Limit)
require.Nil(results.Limit)
require.Equal(uint(0), results.Position)
require.NotNil(results.Total)
require.Equal(count, *results.Total)
@@ -142,6 +142,70 @@ func TestContacts(t *testing.T) {
matchContact(t, fetched.List[0], actual)
}
{
limit := uint(10)
slices := count / limit
remainder := count
require.Greater(slices, uint(1), "we need to have more than 10 objects in order to test the pagination of search results")
for i := range slices {
position := int(i * limit)
page := min(remainder, limit)
m, sessionState, _, _, err := s.client.QueryContactCards([]string{accountId}, filter, sortBy, position, "", nil, &limit, true, ctx)
require.NoError(err)
require.Len(m, 1)
require.Contains(m, accountId)
results := m[accountId]
require.Equal(len(results.Results), int(page))
require.NotNil(results.Limit)
require.Equal(limit, *results.Limit)
require.Equal(uint(position), results.Position)
require.Equal(true, results.CanCalculateChanges)
require.NotNil(results.Total)
require.Equal(count, *results.Total)
remainder -= uint(len(results.Results))
require.Equal(ss, sessionState)
}
}
{
chunkSize := 3
anchor := results.Results[0].Id
offset := 0
i := 0
for chunk := range slices.Chunk(results.Results, chunkSize) {
m, sessionState, _, _, err := s.client.QueryContactCards([]string{accountId}, filter, sortBy, 0, anchor, &offset, uintPtr(chunkSize), true, ctx)
require.Equal(ss, sessionState)
require.NoError(err)
require.Len(m, 1)
require.Contains(m, accountId)
results := m[accountId]
l := len(results.Results)
require.LessOrEqual(l, chunkSize)
require.NotZero(l)
require.NotNil(results.Limit)
require.Equal(uint(chunkSize), *results.Limit)
//require.Equal(uint(i*chunkSize), results.Position)
require.Equal(true, results.CanCalculateChanges)
require.NotNil(results.Total)
require.Equal(count, *results.Total)
fmt.Printf("\x1b[34;1m===[%d]========================================\x1b[0m\n", i)
fmt.Printf("pos: %d\n", results.Position)
fmt.Printf("chunk : %s\n", strings.Join(structs.Map(chunk, func(c ContactCard) string { return c.Id }), " | "))
fmt.Printf("results: %s\n", strings.Join(structs.Map(results.Results, func(c ContactCard) string { return c.Id }), " | "))
fmt.Printf("============================================\n")
for i := range l {
require.Equal(chunk[i].Id, results.Results[i].Id)
}
anchor = chunk[len(chunk)-1].Id
offset = 1
i++
}
require.True(false)
}
{
now := time.Now().Truncate(time.Duration(1) * time.Second).UTC()
for _, event := range expectedContactCardsById {
@@ -169,7 +233,7 @@ func TestContacts(t *testing.T) {
os = state
}
{
shouldBeEmpty, sessionState, state, _, err := s.client.QueryContactCards([]string{accountId}, filter, sortBy, 0, nil, true, ctx)
shouldBeEmpty, sessionState, state, _, err := s.client.QueryContactCards([]string{accountId}, filter, sortBy, 0, "", nil, nil, true, ctx)
require.NoError(err)
require.Contains(shouldBeEmpty, accountId)
resp := shouldBeEmpty[accountId]

View File

@@ -93,7 +93,7 @@ func TestEvents(t *testing.T) {
ss := EmptySessionState
os := EmptyState
{
resultsByAccount, sessionState, state, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, 0, nil, true, ctx)
resultsByAccount, sessionState, state, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, 0, "", nil, nil, true, ctx)
require.NoError(err)
require.Len(resultsByAccount, 1)
@@ -124,7 +124,7 @@ func TestEvents(t *testing.T) {
for i := range slices {
position := int(i * limit)
page := min(remainder, limit)
m, sessionState, _, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, position, &limit, true, ctx)
m, sessionState, _, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, position, "", nil, &limit, true, ctx)
require.NoError(err)
require.Len(m, 1)
require.Contains(m, accountId)
@@ -173,7 +173,7 @@ func TestEvents(t *testing.T) {
}
{
shouldBeEmpty, sessionState, state, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, 0, nil, true, ctx)
shouldBeEmpty, sessionState, state, _, err := s.client.QueryCalendarEvents([]string{accountId}, filter, sortBy, 0, "", nil, nil, true, ctx)
require.NoError(err)
require.Contains(shouldBeEmpty, accountId)
resp := shouldBeEmpty[accountId]

View File

@@ -81,7 +81,7 @@ func TestEmails(t *testing.T) {
}
{
resp, sessionState, _, _, err := s.client.GetAllEmailsInMailbox(accountId, inboxId, 0, nil, true, false, 0, true, ctx)
resp, sessionState, _, _, err := s.client.GetAllEmailsInMailbox(accountId, inboxId, 0, "", nil, nil, true, false, 0, true, ctx)
require.NoError(err)
require.Equal(session.State, sessionState)
@@ -95,7 +95,7 @@ func TestEmails(t *testing.T) {
}
{
resp, sessionState, _, _, err := s.client.GetAllEmailsInMailbox(accountId, inboxId, 0, nil, false, false, 0, true, ctx)
resp, sessionState, _, _, err := s.client.GetAllEmailsInMailbox(accountId, inboxId, 0, "", nil, nil, false, false, 0, true, ctx)
require.NoError(err)
require.Equal(session.State, sessionState)

View File

@@ -223,7 +223,7 @@ func withDirectoryQueries(allowDirectoryQueries bool) func(map[string]any) {
}
func newStalwartTest(t *testing.T, options ...func(map[string]any)) (*StalwartTest, error) { //NOSONAR
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
var _ context.CancelFunc = cancel // ignore context leak warning: it is passed in the struct and called in Close()
// A master user name different from "master" does not seem to work as of the current Stalwart version

View File

@@ -1833,7 +1833,7 @@ type MailboxQueryCommand struct {
//
// For example, -1 means the object immediately preceding the anchor is the first result in
// the list returned.
AnchorOffset int `json:"anchorOffset,omitzero" doc:"opt" default:"0"`
AnchorOffset *int `json:"anchorOffset,omitempty" doc:"opt" default:"0"`
// The maximum number of results to return.
//
@@ -2129,7 +2129,7 @@ type EmailQueryCommand struct {
//
// For example, -1 means the Email immediately preceding the anchor is the first result in
// the list returned.
AnchorOffset int `json:"anchorOffset,omitzero" doc:"opt" default:"0"`
AnchorOffset *int `json:"anchorOffset,omitempty" doc:"opt" default:"0"`
// The maximum number of results to return.
//
@@ -7074,7 +7074,7 @@ type ContactCardQueryCommand struct {
//
// For example, -1 means the Email immediately preceding the anchor is the first result in
// the list returned.
AnchorOffset int `json:"anchorOffset,omitzero" default:"0" doc:"opt"`
AnchorOffset *int `json:"anchorOffset,omitempty" default:"0" doc:"opt"`
// The maximum number of results to return.
//
@@ -7804,7 +7804,7 @@ type CalendarEventQueryCommand struct {
//
// For example, -1 means the Email immediately preceding the anchor is the first result in
// the list returned.
AnchorOffset int `json:"anchorOffset,omitzero" doc:"opt" default:"0"`
AnchorOffset *int `json:"anchorOffset,omitempty" doc:"opt" default:"0"`
// The maximum number of results to return.
//
@@ -8265,7 +8265,7 @@ type PrincipalQueryCommand struct {
//
// If the index is greater than or equal to the total number of objects in the results
// list, then the ids array in the response will be empty, but this is not an error.
Position uint `json:"position,omitzero" default:"0" doc:"opt"`
Position int `json:"position,omitzero" default:"0" doc:"opt"`
// An Email id.
//
@@ -8281,7 +8281,7 @@ type PrincipalQueryCommand struct {
//
// For example, -1 means the Principal immediately preceding the anchor is the first result in
// the list returned.
AnchorOffset int `json:"anchorOffset,omitzero" default:"0" doc:"opt"`
AnchorOffset *int `json:"anchorOffset,omitempty" default:"0" doc:"opt"`
// The maximum number of results to return.
//

View File

@@ -430,10 +430,10 @@ func update[T Foo, CHANGES Change, SET SetCommand[T], GET GetCommand[T], RESP an
func query[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T], QUERYRESP QueryResponse[T], GETRESP GetResponse[T], RESP any]( //NOSONAR
client *Client, name string, objType ObjectType,
defaultSortBy []SORT,
queryCommandFactory func(filter FILTER, sortBy []SORT, position uint, limit *uint) QUERY,
queryCommandFactory func(filter FILTER, sortBy []SORT, position int, anchor string, anchorOffset *int, limit *uint) QUERY,
getCommandFactory func(cmd Command, path string, rof string) GET,
respMapper func(query QUERYRESP, get GETRESP) *RESP,
filter FILTER, sortBy []SORT, limit *uint, position uint,
filter FILTER, sortBy []SORT, position int, anchor string, anchorOffset *int, limit *uint,
ctx Context) (*RESP, SessionState, State, Language, Error) {
logger := client.logger(name, ctx)
@@ -443,7 +443,7 @@ func query[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T]
sortBy = defaultSortBy
}
query := queryCommandFactory(filter, sortBy, position, limit)
query := queryCommandFactory(filter, sortBy, position, anchor, anchorOffset, limit)
get := getCommandFactory(query.GetCommand(), "/ids/*", "0")
cmd, err := client.request(ctx, objType.Namespaces, invocation(query, "0"), invocation(get, "1"))
@@ -469,11 +469,11 @@ func query[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T]
func queryN[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T], QUERYRESP QueryResponse[T], GETRESP GetResponse[T], RESP any]( //NOSONAR
client *Client, name string, objType ObjectType,
defaultSortBy []SORT,
queryCommandFactory func(accountId string, filter FILTER, sortBy []SORT, position int, limit *uint) QUERY,
queryCommandFactory func(accountId string, filter FILTER, sortBy []SORT, position int, anchor string, anchorOffset *int, imit *uint) QUERY,
getCommandFactory func(accountId string, cmd Command, path string, rof string) GET,
respMapper func(query QUERYRESP, get GETRESP) *RESP,
accountIds []string,
filter FILTER, sortBy []SORT, limit *uint, position int,
filter FILTER, sortBy []SORT, position int, anchor string, anchorOffset *int, limit *uint,
ctx Context) (map[string]*RESP, SessionState, State, Language, Error) {
logger := client.logger(name, ctx)
ctx = ctx.WithLogger(logger)
@@ -488,7 +488,7 @@ func queryN[T Foo, FILTER any, SORT any, QUERY QueryCommand[T], GET GetCommand[T
var g GET
var q QUERY
for i, accountId := range uniqueAccountIds {
query := queryCommandFactory(accountId, filter, sortBy, position, limit)
query := queryCommandFactory(accountId, filter, sortBy, position, anchor, anchorOffset, limit)
get := getCommandFactory(accountId, query.GetCommand(), "/ids/*", mcid(accountId, "0"))
invocations[i*2+0] = invocation(query, mcid(accountId, "0"))
invocations[i*2+1] = invocation(get, mcid(accountId, "1"))

View File

@@ -409,8 +409,8 @@ func identity1[T any](t T) T {
func list[T Foo, GETRESP GetResponse[T]](r GETRESP) []T { return r.GetList() }
func getid[T Idable](r T) string { return r.GetId() }
func uintPtr(i uint) *uint {
return ptr(i)
func uintPtr[T int | uint](i T) *uint {
return ptr(uint(i))
}
func valueIf[T any | uint | int | bool](value *T, condition bool) *T {

View File

@@ -42,8 +42,8 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request
fetchBodies := false
withThreads := true
query(Email, w, r, g, g.defaults.emailLimit,
func(req Request, accountId, containerId string, position int, limit *uint, ctx jmap.Context) (*jmap.EmailSearchResults, jmap.SessionState, jmap.State, jmap.Language, *Error) {
emails, sessionState, state, lang, jerr := g.jmap.GetAllEmailsInMailbox(accountId, containerId, position, limit, collapseThreads, fetchBodies, g.config.maxBodyValueBytes, withThreads, ctx)
func(req Request, accountId, containerId string, position int, anchor string, anchorOffset *int, limit *uint, ctx jmap.Context) (*jmap.EmailSearchResults, jmap.SessionState, jmap.State, jmap.Language, *Error) { //NOSONAR
emails, sessionState, state, lang, jerr := g.jmap.GetAllEmailsInMailbox(accountId, containerId, position, anchor, anchorOffset, limit, collapseThreads, fetchBodies, g.config.maxBodyValueBytes, withThreads, ctx)
if jerr != nil {
return emails, sessionState, state, lang, req.apiErrorFromJmap(req.observeJmapError(jerr))
}

View File

@@ -72,10 +72,10 @@ func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request)
}
func curryMapQuery[SRES jmap.SearchResults[T], T jmap.Foo, FILTER any, COMP any](
f func(accountIds []string, filter FILTER, sortBy []COMP, position int, limit *uint, calculateTotal bool, ctx jmap.Context) (map[string]SRES, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
) func(req Request, accountId string, filter FILTER, sortBy []COMP, position int, limit *uint, ctx jmap.Context) (SRES, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
return func(req Request, accountId string, filter FILTER, sortBy []COMP, position int, limit *uint, ctx jmap.Context) (SRES, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
m, sessionState, state, lang, err := f(single(accountId), filter, sortBy, position, limit, true, ctx)
f func(accountIds []string, filter FILTER, sortBy []COMP, position int, anchor string, anchorOffset *int, limit *uint, calculateTotal bool, ctx jmap.Context) (map[string]SRES, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
) func(req Request, accountId string, filter FILTER, sortBy []COMP, position int, anchor string, anchorOffset *int, limit *uint, ctx jmap.Context) (SRES, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) {
return func(req Request, accountId string, filter FILTER, sortBy []COMP, position int, anchor string, anchorOffset *int, limit *uint, ctx jmap.Context) (SRES, jmap.SessionState, jmap.State, jmap.Language, jmap.Error) { //NOSONAR
m, sessionState, state, lang, err := f(single(accountId), filter, sortBy, position, anchor, anchorOffset, limit, true, ctx)
return m[accountId], sessionState, state, lang, err
}
}

View File

@@ -49,6 +49,8 @@ const (
QueryParamSearchKeyword = "keyword"
QueryParamSearchMessageId = "messageId"
QueryParamPosition = "position"
QueryParamAnchor = "anchor"
QueryParamAnchorOffset = "offset"
QueryParamLimit = "limit"
QueryParamDays = "days"
QueryParamPartId = "partId"

View File

@@ -80,7 +80,7 @@ func getall[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.G
})
}
var paginationQueryParams = toSupportedQueryParams(QueryParamPosition, QueryParamLimit)
var paginationQueryParams = toSupportedQueryParams(QueryParamPosition, QueryParamAnchor, QueryParamAnchorOffset, QueryParamLimit)
// Retrieve all the {{.Name}} with support for paging using the {{.QueryParam.QueryParamPosition.Name}} and {{.QueryParam.QueryParamLimit.Name}} query parameters.
// @api:response 200:SEARCHRESULTS returns the {{.Names}} within the requested range, as well as the total amount of {{.Names}}
@@ -91,7 +91,7 @@ func getallpaged[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], FILTER
withContainerId bool,
filterFunc func(containerId string) FILTER,
sortBy []COMP,
queryFunc func(req Request, accountId string, filter FILTER, sortBy []COMP, position int, limit *uint, ctx jmap.Context) (SEARCHRESULTS, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
queryFunc func(req Request, accountId string, filter FILTER, sortBy []COMP, position int, anchor string, anchorOffset *int, limit *uint, ctx jmap.Context) (SEARCHRESULTS, jmap.SessionState, jmap.State, jmap.Language, jmap.Error),
) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := o.accountFunc(&req)
@@ -108,6 +108,23 @@ func getallpaged[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], FILTER
l = l.Int(QueryParamPosition, position)
}
anchor, ok := req.getStringParam(QueryParamAnchor, "")
if ok {
l = l.Str(QueryParamAnchor, log.SafeString(anchor))
}
var anchorOffset *int = nil
{
v, ok, err := req.parseIntParam(QueryParamAnchorOffset, 0)
if err != nil {
return req.error(accountId, err)
}
if ok {
l = l.Int(QueryParamAnchorOffset, v)
anchorOffset = &v
}
}
var limit *uint = nil
{
v, ok, err := req.parseUIntParam(QueryParamLimit, uint(0))
@@ -143,7 +160,7 @@ func getallpaged[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], FILTER
logger := log.From(l)
ctx := req.ctx.WithLogger(logger)
results, sessionState, state, lang, jerr := queryFunc(req, accountId, filter, sortBy, position, jmaplimit, ctx)
results, sessionState, state, lang, jerr := queryFunc(req, accountId, filter, sortBy, position, anchor, anchorOffset, jmaplimit, ctx)
if jerr != nil {
return req.jmapError(accountId, jerr, sessionState, lang)
}
@@ -164,7 +181,7 @@ func query[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], SEARCHRESULT
w http.ResponseWriter, r *http.Request,
g *Groupware,
defaultLimit uint,
queryFunc func(req Request, accountId string, containerId string, position int, limit *uint, ctx jmap.Context) (SEARCHRESULTS, jmap.SessionState, jmap.State, jmap.Language, *Error),
queryFunc func(req Request, accountId string, containerId string, position int, anchor string, anchorOffset *int, limit *uint, ctx jmap.Context) (SEARCHRESULTS, jmap.SessionState, jmap.State, jmap.Language, *Error),
) {
g.respond(w, r, func(req Request) Response {
ok, accountId, resp := o.accountFunc(&req)
@@ -191,6 +208,23 @@ func query[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], SEARCHRESULT
l = l.Int(QueryParamPosition, position)
}
anchor, ok := req.getStringParam(QueryParamAnchor, "")
if ok {
l = l.Str(QueryParamAnchor, log.SafeString(anchor))
}
var anchorOffset *int = nil
{
v, ok, err := req.parseIntParam(QueryParamAnchorOffset, 0)
if err != nil {
return req.error(accountId, err)
}
if ok {
l = l.Int(QueryParamAnchorOffset, v)
anchorOffset = &v
}
}
var limit *uint = nil
{
v, ok, err := req.parseUIntParam(QueryParamLimit, defaultLimit)
@@ -213,7 +247,7 @@ func query[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], SEARCHRESULT
jmaplimit = UintPtrOne
}
results, sessionState, state, lang, err := queryFunc(req, accountId, containerId, position, jmaplimit, ctx)
results, sessionState, state, lang, err := queryFunc(req, accountId, containerId, position, anchor, anchorOffset, jmaplimit, ctx)
if err != nil {
return req.error(accountId, err)
}