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++ {