groupware: add support for multi-supplier getting a Contact by ID

This commit is contained in:
Pascal Bleser
2026-06-25 10:53:49 +02:00
parent 79e342dffc
commit 111373edd9
6 changed files with 213 additions and 120 deletions

View File

@@ -6,32 +6,24 @@ import (
"github.com/opencloud-eu/opencloud/pkg/jmap"
)
type JmapContactCardSupplier struct {
type JmapAddressBookSupplier struct {
client *jmap.Client
}
var _ ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse] = &JmapContactCardSupplier{}
var _ QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator] = &JmapContactCardSupplier{}
var _ ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse] = &JmapAddressBookSupplier{}
func newJmapContactCardSupplier(client *jmap.Client) *JmapContactCardSupplier {
return &JmapContactCardSupplier{client: client}
func newJmapAddressBookSupplier(client *jmap.Client) *JmapAddressBookSupplier {
return &JmapAddressBookSupplier{client: client}
}
const jmapContactCardSupplierid = SupplierId("jmap")
const jmapAddressBookSupplierId = SupplierId("jmap")
func (c *JmapContactCardSupplier) GetId() SupplierId {
return jmapContactCardSupplierid
func (c *JmapAddressBookSupplier) GetId() SupplierId {
return jmapAddressBookSupplierId
}
func (c *JmapContactCardSupplier) IsMine(id string) bool {
func (c *JmapAddressBookSupplier) IsMine(id string) bool {
return id != "" && !strings.Contains(id, ":")
}
func (c *JmapContactCardSupplier) GetAll(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.AddressBookGetResponse], error) {
func (c *JmapAddressBookSupplier) GetAll(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.AddressBookGetResponse], error) {
return c.client.GetAddressbooks(accountId, ids, ctx)
}
func (c *JmapContactCardSupplier) Query(accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, filter jmap.ContactCardFilterElement, sortBy []jmap.ContactCardComparator, calculateTotal bool, ctx jmap.Context) (jmap.Result[map[jmap.AccountId]*jmap.ContactCardSearchResults], error) { //NOSONAR
if m, err := mapQueryParams(c.GetId(), accountIds, qps); err != nil {
return jmap.ZeroResultV[map[jmap.AccountId]*jmap.ContactCardSearchResults](), err
} else {
return c.client.QueryContactCards(m, limit, filter, sortBy, calculateTotal, ctx)
}
}

View File

@@ -5,16 +5,14 @@ import (
"strings"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/jscontact"
"github.com/opencloud-eu/opencloud/pkg/structs"
)
type MockContactCardSupplier struct {
type MockAddressBookSupplier struct {
addressBook jmap.AddressBook
contacts []jmap.ContactCard
}
var MockContactCardSupplierInstance *MockContactCardSupplier = &MockContactCardSupplier{
var MockAddressBookSupplierInstance *MockAddressBookSupplier = &MockAddressBookSupplier{
addressBook: jmap.AddressBook{
Id: "mock:1",
Name: "Automatic Addressbook",
@@ -28,49 +26,21 @@ var MockContactCardSupplierInstance *MockContactCardSupplier = &MockContactCardS
MayDelete: false,
},
},
contacts: []jmap.ContactCard{
{
Id: "mock:alan",
AddressBookIds: map[string]bool{"mock:1": true},
Type: jscontact.ContactCardType,
Version: jmap.DEFAULT_CONTACT_CARD_VERSION,
Created: mustParseTime("2026-05-26T10:21:00.000Z"),
Kind: jscontact.ContactCardKindIndividual,
ProdId: "OC:mock",
Uid: "dc2858d2-4826-412d-afc9-c4492f8f84bc",
Updated: mustParseTime("2026-05-26T10:21:00.000Z"),
Name: &jscontact.Name{
Type: jscontact.NameType,
Components: []jscontact.NameComponent{
{Kind: jscontact.NameComponentKindGiven, Value: "Alan"},
{Kind: jscontact.NameComponentKindSurname, Value: "Turing"},
},
DefaultSeparator: " ",
IsOrdered: true,
},
Emails: map[string]jscontact.EmailAddress{
"eedujae1": {
Address: "alan@example.com",
},
},
},
},
}
var _ ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse] = &MockContactCardSupplier{}
var _ QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator] = &MockContactCardSupplier{}
var _ ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse] = &MockAddressBookSupplier{}
func newMockContactCardSupplier() *MockContactCardSupplier {
return MockContactCardSupplierInstance
func newMockAddressBookSupplier() *MockAddressBookSupplier {
return MockAddressBookSupplierInstance
}
func (c *MockContactCardSupplier) GetId() SupplierId {
func (c *MockAddressBookSupplier) GetId() SupplierId {
return SupplierId("mock")
}
func (c *MockContactCardSupplier) IsMine(id string) bool {
func (c *MockAddressBookSupplier) IsMine(id string) bool {
return strings.HasPrefix(id, "mock:")
}
func (c *MockContactCardSupplier) GetAll(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.AddressBookGetResponse], error) {
func (c *MockAddressBookSupplier) GetAll(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.AddressBookGetResponse], error) {
abooks := []jmap.AddressBook{c.addressBook}
if len(ids) > 0 {
abooks = structs.Filter(abooks, func(a jmap.AddressBook) bool { return slices.Contains(ids, a.Id) })
@@ -87,60 +57,3 @@ func (c *MockContactCardSupplier) GetAll(accountId jmap.AccountId, ids []string,
nil,
), nil
}
func (c *MockContactCardSupplier) Query(accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, filter jmap.ContactCardFilterElement, sortBy []jmap.ContactCardComparator, calculateTotal bool, ctx jmap.Context) (jmap.Result[map[jmap.AccountId]*jmap.ContactCardSearchResults], error) { //NOSONAR
payload := make(map[jmap.AccountId]*jmap.ContactCardSearchResults, len(accountIds))
total := len(c.contacts)
for _, accountId := range accountIds {
all := []jmap.ContactCard{}
var qp jmap.QueryParams
if q, ok, err := qps.ForSupplier(c.GetId(), accountId); err != nil {
return jmap.ZeroResultV[map[jmap.AccountId]*jmap.ContactCardSearchResults](), err
} else if ok {
qp = q
} else {
qp = jmap.NullQueryParams
}
p := uint(qp.Position)
if qp.Position < total {
all = c.contacts[qp.Position:]
}
if qp.Anchor != "" {
a := slices.IndexFunc(all, func(e jmap.ContactCard) bool { return e.Id == qp.Anchor })
if a >= 0 {
if qp.AnchorOffset != nil {
a += int(*qp.AnchorOffset)
} else {
a += 1
}
p += uint(a)
if a < len(all) {
all = all[a:]
} else {
all = []jmap.ContactCard{}
}
} else {
all = []jmap.ContactCard{}
}
}
if limit != nil {
if len(all) > int(*limit) {
all = all[:int(*limit)]
}
}
res := &jmap.ContactCardSearchResults{
Results: all,
CanCalculateChanges: false,
Position: &p,
Limit: limit,
}
if calculateTotal {
t := uint(total)
res.Total = &t
}
payload[accountId] = res
}
return jmap.NewResult(payload, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, nil), nil
}

View File

@@ -46,12 +46,12 @@ func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Requ
return jmap.ContactCardFilterCondition{InAddressBook: addressbookId}
},
[]jmap.ContactCardComparator{{Property: jmap.ContactCardPropertyCreated, IsAscending: true}},
curryQueryFunc(g.contacts),
curryQueryFunc(g.queryContacts),
)
}
func (g *Groupware) GetContactById(w http.ResponseWriter, r *http.Request) {
get(Contact, w, r, g, g.jmap.GetContactCards)
get(Contact, w, r, g, g.contacts)
}
func (g *Groupware) GetAllContacts(w http.ResponseWriter, r *http.Request) {
@@ -60,7 +60,7 @@ func (g *Groupware) GetAllContacts(w http.ResponseWriter, r *http.Request) {
return jmap.ContactCardFilterCondition{}
},
[]jmap.ContactCardComparator{{Property: jmap.ContactCardPropertyCreated, IsAscending: true}},
curryQueryFunc(g.contacts),
curryQueryFunc(g.queryContacts),
)
}
@@ -82,7 +82,13 @@ func (g *Groupware) ModifyContact(w http.ResponseWriter, r *http.Request) {
modify(Contact, w, r, g, g.jmap.UpdateContactCard)
}
func (g *Groupware) contacts(accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, //NOSONAR
func (g *Groupware) contacts(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.ContactCardGetResponse], error) {
return slist(g.contactCardListSuppliers, accountId, ids, ctx, func(accountId jmap.AccountId, state jmap.State, notFound []string, list []jmap.ContactCard) jmap.ContactCardGetResponse {
return jmap.ContactCardGetResponse{AccountId: accountId, State: state, NotFound: notFound, List: list}
})
}
func (g *Groupware) queryContacts(accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, //NOSONAR
filter jmap.ContactCardFilterElement, sortBy []jmap.ContactCardComparator, calculateTotal bool,
ctx jmap.Context) (jmap.Result[*jmap.ContactCardSearchResults], NextToken, error) {
return squery(g.contactCardQuerySuppliers, accountIds, qps, limit, filter, sortBy, calculateTotal, ctx,

View File

@@ -0,0 +1,37 @@
package groupware
import (
"strings"
"github.com/opencloud-eu/opencloud/pkg/jmap"
)
type JmapContactCardSupplier struct {
client *jmap.Client
}
var _ QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator] = &JmapContactCardSupplier{}
var _ ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse] = &JmapContactCardSupplier{}
func newJmapContactCardSupplier(client *jmap.Client) *JmapContactCardSupplier {
return &JmapContactCardSupplier{client: client}
}
const jmapContactCardSupplierId = SupplierId("jmap")
func (c *JmapContactCardSupplier) GetId() SupplierId {
return jmapContactCardSupplierId
}
func (c *JmapContactCardSupplier) IsMine(id string) bool {
return id != "" && !strings.Contains(id, ":")
}
func (c *JmapContactCardSupplier) Query(accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, filter jmap.ContactCardFilterElement, sortBy []jmap.ContactCardComparator, calculateTotal bool, ctx jmap.Context) (jmap.Result[map[jmap.AccountId]*jmap.ContactCardSearchResults], error) { //NOSONAR
if m, err := mapQueryParams(c.GetId(), accountIds, qps); err != nil {
return jmap.ZeroResultV[map[jmap.AccountId]*jmap.ContactCardSearchResults](), err
} else {
return c.client.QueryContactCards(m, limit, filter, sortBy, calculateTotal, ctx)
}
}
func (c *JmapContactCardSupplier) GetAll(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.ContactCardGetResponse], error) {
return c.client.GetContactCards(accountId, ids, ctx)
}

View File

@@ -0,0 +1,132 @@
package groupware
import (
"slices"
"strings"
"github.com/opencloud-eu/opencloud/pkg/jmap"
"github.com/opencloud-eu/opencloud/pkg/jscontact"
"github.com/opencloud-eu/opencloud/pkg/structs"
)
type MockContactCardSupplier struct {
contacts []jmap.ContactCard
}
var MockContactCardSupplierInstance *MockContactCardSupplier = &MockContactCardSupplier{
contacts: []jmap.ContactCard{
{
Id: "mock:alan",
AddressBookIds: map[string]bool{"mock:1": true},
Type: jscontact.ContactCardType,
Version: jmap.DEFAULT_CONTACT_CARD_VERSION,
Created: mustParseTime("2026-05-26T10:21:00.000Z"),
Kind: jscontact.ContactCardKindIndividual,
ProdId: "OC:mock",
Uid: "dc2858d2-4826-412d-afc9-c4492f8f84bc",
Updated: mustParseTime("2026-05-26T10:21:00.000Z"),
Name: &jscontact.Name{
Type: jscontact.NameType,
Components: []jscontact.NameComponent{
{Kind: jscontact.NameComponentKindGiven, Value: "Alan"},
{Kind: jscontact.NameComponentKindSurname, Value: "Turing"},
},
DefaultSeparator: " ",
IsOrdered: true,
},
Emails: map[string]jscontact.EmailAddress{
"eedujae1": {
Address: "alan@example.com",
},
},
},
},
}
var _ QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator] = &MockContactCardSupplier{}
var _ ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse] = &MockContactCardSupplier{}
func newMockContactCardSupplier() *MockContactCardSupplier {
return MockContactCardSupplierInstance
}
func (c *MockContactCardSupplier) GetId() SupplierId {
return SupplierId("mock")
}
func (c *MockContactCardSupplier) IsMine(id string) bool {
return strings.HasPrefix(id, "mock:")
}
func (c *MockContactCardSupplier) GetAll(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.ContactCardGetResponse], error) {
contacts := c.contacts
if len(ids) > 0 {
contacts = structs.Filter(contacts, func(a jmap.ContactCard) bool { return slices.Contains(ids, a.Id) })
}
return jmap.NewResult(
jmap.ContactCardGetResponse{
AccountId: accountId,
State: "mock",
List: contacts,
},
jmap.EmptySessionState,
jmap.State("mock"),
jmap.NoLanguage,
nil,
), nil
}
func (c *MockContactCardSupplier) Query(accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, filter jmap.ContactCardFilterElement, sortBy []jmap.ContactCardComparator, calculateTotal bool, ctx jmap.Context) (jmap.Result[map[jmap.AccountId]*jmap.ContactCardSearchResults], error) { //NOSONAR
payload := make(map[jmap.AccountId]*jmap.ContactCardSearchResults, len(accountIds))
total := len(c.contacts)
for _, accountId := range accountIds {
all := []jmap.ContactCard{}
var qp jmap.QueryParams
if q, ok, err := qps.ForSupplier(c.GetId(), accountId); err != nil {
return jmap.ZeroResultV[map[jmap.AccountId]*jmap.ContactCardSearchResults](), err
} else if ok {
qp = q
} else {
qp = jmap.NullQueryParams
}
p := uint(qp.Position)
if qp.Position < total {
all = c.contacts[qp.Position:]
}
if qp.Anchor != "" {
a := slices.IndexFunc(all, func(e jmap.ContactCard) bool { return e.Id == qp.Anchor })
if a >= 0 {
if qp.AnchorOffset != nil {
a += int(*qp.AnchorOffset)
} else {
a += 1
}
p += uint(a)
if a < len(all) {
all = all[a:]
} else {
all = []jmap.ContactCard{}
}
} else {
all = []jmap.ContactCard{}
}
}
if limit != nil {
if len(all) > int(*limit) {
all = all[:int(*limit)]
}
}
res := &jmap.ContactCardSearchResults{
Results: all,
CanCalculateChanges: false,
Position: &p,
Limit: limit,
}
if calculateTotal {
t := uint(total)
res.Total = &t
}
payload[accountId] = res
}
return jmap.NewResult(payload, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, nil), nil
}

View File

@@ -119,6 +119,7 @@ type Groupware struct {
addressBookListSuppliers []ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse]
contactCardQuerySuppliers []QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator]
contactCardListSuppliers []ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse]
}
// An error during the Groupware initialization.
@@ -393,16 +394,27 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
addressBookListSuppliers := []ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse]{}
contactCardQuerySuppliers := []QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator]{}
contactCardListSuppliers := []ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse]{}
{
{
j := newJmapContactCardSupplier(&jmapClient)
j := newJmapAddressBookSupplier(&jmapClient)
addressBookListSuppliers = append(addressBookListSuppliers, j)
}
{
j := newJmapContactCardSupplier(&jmapClient)
contactCardQuerySuppliers = append(contactCardQuerySuppliers, j)
contactCardListSuppliers = append(contactCardListSuppliers, j)
}
if config.EnableMockData {
m := newMockContactCardSupplier()
addressBookListSuppliers = append(addressBookListSuppliers, m)
contactCardQuerySuppliers = append(contactCardQuerySuppliers, m)
{
m := newMockAddressBookSupplier()
addressBookListSuppliers = append(addressBookListSuppliers, m)
}
{
m := newMockContactCardSupplier()
contactCardQuerySuppliers = append(contactCardQuerySuppliers, m)
contactCardListSuppliers = append(contactCardListSuppliers, m)
}
}
}
@@ -439,6 +451,7 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
jobCounter: atomic.Uint64{},
addressBookListSuppliers: addressBookListSuppliers,
contactCardQuerySuppliers: contactCardQuerySuppliers,
contactCardListSuppliers: contactCardListSuppliers,
}
for w := 1; w <= workerPoolSize; w++ {