diff --git a/pkg/jmap/api_contact.go b/pkg/jmap/api_contact.go index 4e5d8d0cff..1185e24ef1 100644 --- a/pkg/jmap/api_contact.go +++ b/pkg/jmap/api_contact.go @@ -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, ) } diff --git a/pkg/jmap/api_email.go b/pkg/jmap/api_email.go index d4cb25f7bf..c4678178ef 100644 --- a/pkg/jmap/api_email.go +++ b/pkg/jmap/api_email.go @@ -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, } diff --git a/pkg/jmap/api_event.go b/pkg/jmap/api_event.go index 1e3edad647..9fbb8450d4 100644 --- a/pkg/jmap/api_event.go +++ b/pkg/jmap/api_event.go @@ -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, ) } diff --git a/pkg/jmap/api_mailbox.go b/pkg/jmap/api_mailbox.go index c72a23bc7d..28e151d071 100644 --- a/pkg/jmap/api_mailbox.go +++ b/pkg/jmap/api_mailbox.go @@ -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, ) } diff --git a/pkg/jmap/api_principal.go b/pkg/jmap/api_principal.go index e5198a0b43..c8d48b37d6 100644 --- a/pkg/jmap/api_principal.go +++ b/pkg/jmap/api_principal.go @@ -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, ) } diff --git a/pkg/jmap/integration_contact_test.go b/pkg/jmap/integration_addressbook_test.go similarity index 88% rename from pkg/jmap/integration_contact_test.go rename to pkg/jmap/integration_addressbook_test.go index c9e47db610..447d676a72 100644 --- a/pkg/jmap/integration_contact_test.go +++ b/pkg/jmap/integration_addressbook_test.go @@ -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] diff --git a/pkg/jmap/integration_calendar_test.go b/pkg/jmap/integration_calendar_test.go index 95220c1388..cc2fa51c4a 100644 --- a/pkg/jmap/integration_calendar_test.go +++ b/pkg/jmap/integration_calendar_test.go @@ -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] diff --git a/pkg/jmap/integration_email_test.go b/pkg/jmap/integration_email_test.go index 96303ef009..59c10949fe 100644 --- a/pkg/jmap/integration_email_test.go +++ b/pkg/jmap/integration_email_test.go @@ -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) diff --git a/pkg/jmap/integration_test.go b/pkg/jmap/integration_test.go index 054cc84729..a9c46c4fb1 100644 --- a/pkg/jmap/integration_test.go +++ b/pkg/jmap/integration_test.go @@ -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 diff --git a/pkg/jmap/model.go b/pkg/jmap/model.go index 78e5acc1cb..e1e00995aa 100644 --- a/pkg/jmap/model.go +++ b/pkg/jmap/model.go @@ -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. // diff --git a/pkg/jmap/templates.go b/pkg/jmap/templates.go index 57aa11ed2b..61f826bdad 100644 --- a/pkg/jmap/templates.go +++ b/pkg/jmap/templates.go @@ -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")) diff --git a/pkg/jmap/tools.go b/pkg/jmap/tools.go index 7a2f3ac85f..c01f4a12de 100644 --- a/pkg/jmap/tools.go +++ b/pkg/jmap/tools.go @@ -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 { diff --git a/services/groupware/pkg/groupware/api_emails.go b/services/groupware/pkg/groupware/api_emails.go index f8e52f1d04..5f59f61991 100644 --- a/services/groupware/pkg/groupware/api_emails.go +++ b/services/groupware/pkg/groupware/api_emails.go @@ -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)) } diff --git a/services/groupware/pkg/groupware/api_events.go b/services/groupware/pkg/groupware/api_events.go index b179fd189f..d3ac836622 100644 --- a/services/groupware/pkg/groupware/api_events.go +++ b/services/groupware/pkg/groupware/api_events.go @@ -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 } } diff --git a/services/groupware/pkg/groupware/route.go b/services/groupware/pkg/groupware/route.go index 5ecc18883d..57f5fe9322 100644 --- a/services/groupware/pkg/groupware/route.go +++ b/services/groupware/pkg/groupware/route.go @@ -49,6 +49,8 @@ const ( QueryParamSearchKeyword = "keyword" QueryParamSearchMessageId = "messageId" QueryParamPosition = "position" + QueryParamAnchor = "anchor" + QueryParamAnchorOffset = "offset" QueryParamLimit = "limit" QueryParamDays = "days" QueryParamPartId = "partId" diff --git a/services/groupware/pkg/groupware/templates.go b/services/groupware/pkg/groupware/templates.go index 751d7a8625..bd3b1905b8 100644 --- a/services/groupware/pkg/groupware/templates.go +++ b/services/groupware/pkg/groupware/templates.go @@ -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) }