From 111373edd9edabae69d15ea332e38184ba112b2f Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Thu, 25 Jun 2026 10:53:49 +0200
Subject: [PATCH] groupware: add support for multi-supplier getting a Contact
by ID
---
.../groupware/address_book_supplier_jmap.go | 26 ++--
.../groupware/address_book_supplier_mock.go | 103 ++------------
.../groupware/pkg/groupware/api_contacts.go | 14 +-
.../pkg/groupware/contact_supplier_jmap.go | 37 +++++
.../pkg/groupware/contact_supplier_mock.go | 132 ++++++++++++++++++
services/groupware/pkg/groupware/framework.go | 21 ++-
6 files changed, 213 insertions(+), 120 deletions(-)
create mode 100644 services/groupware/pkg/groupware/contact_supplier_jmap.go
create mode 100644 services/groupware/pkg/groupware/contact_supplier_mock.go
diff --git a/services/groupware/pkg/groupware/address_book_supplier_jmap.go b/services/groupware/pkg/groupware/address_book_supplier_jmap.go
index 6dda818b4e..86e9439dc8 100644
--- a/services/groupware/pkg/groupware/address_book_supplier_jmap.go
+++ b/services/groupware/pkg/groupware/address_book_supplier_jmap.go
@@ -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)
- }
-}
diff --git a/services/groupware/pkg/groupware/address_book_supplier_mock.go b/services/groupware/pkg/groupware/address_book_supplier_mock.go
index 8c13f0dcf2..f963e3b2c1 100644
--- a/services/groupware/pkg/groupware/address_book_supplier_mock.go
+++ b/services/groupware/pkg/groupware/address_book_supplier_mock.go
@@ -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
-}
diff --git a/services/groupware/pkg/groupware/api_contacts.go b/services/groupware/pkg/groupware/api_contacts.go
index dc460459c9..22b3043ee9 100644
--- a/services/groupware/pkg/groupware/api_contacts.go
+++ b/services/groupware/pkg/groupware/api_contacts.go
@@ -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,
diff --git a/services/groupware/pkg/groupware/contact_supplier_jmap.go b/services/groupware/pkg/groupware/contact_supplier_jmap.go
new file mode 100644
index 0000000000..34a46f2306
--- /dev/null
+++ b/services/groupware/pkg/groupware/contact_supplier_jmap.go
@@ -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)
+}
diff --git a/services/groupware/pkg/groupware/contact_supplier_mock.go b/services/groupware/pkg/groupware/contact_supplier_mock.go
new file mode 100644
index 0000000000..4d9ddc27bf
--- /dev/null
+++ b/services/groupware/pkg/groupware/contact_supplier_mock.go
@@ -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
+}
diff --git a/services/groupware/pkg/groupware/framework.go b/services/groupware/pkg/groupware/framework.go
index 538dc5da08..8175240ff8 100644
--- a/services/groupware/pkg/groupware/framework.go
+++ b/services/groupware/pkg/groupware/framework.go
@@ -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++ {