From 5ee5dddf5e0f3ea0474a14b80ad7e296c39c8ed1 Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Thu, 25 Jun 2026 16:58:07 +0200
Subject: [PATCH] groupware: add suppliers support for object changes
---
pkg/structs/structs.go | 9 +
.../pkg/groupware/api_addressbooks.go | 10 +-
.../groupware/pkg/groupware/api_contacts.go | 18 +-
services/groupware/pkg/groupware/framework.go | 28 +-
.../groupware/supplier_addressbook_jmap.go | 7 +
.../groupware/supplier_addressbook_mock.go | 29 +-
.../groupware/supplier_contactcard_jmap.go | 8 +
.../groupware/supplier_contactcard_mock.go | 24 +-
services/groupware/pkg/groupware/suppliers.go | 255 +++++++++++++++---
9 files changed, 339 insertions(+), 49 deletions(-)
diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go
index 800b63dfec..fd10340f24 100644
--- a/pkg/structs/structs.go
+++ b/pkg/structs/structs.go
@@ -397,3 +397,12 @@ func Flatten[T any](s [][]T) []T {
}
return result
}
+
+func AllMatch[T any](s []T, predicate func(e T) bool) bool {
+ for _, e := range s {
+ if !predicate(e) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/services/groupware/pkg/groupware/api_addressbooks.go b/services/groupware/pkg/groupware/api_addressbooks.go
index 45621b2859..2092c654cb 100644
--- a/services/groupware/pkg/groupware/api_addressbooks.go
+++ b/services/groupware/pkg/groupware/api_addressbooks.go
@@ -19,7 +19,7 @@ func (g *Groupware) GetAddressbookById(w http.ResponseWriter, r *http.Request) {
// Get the changes to Address Books since a certain State.
// @api:tags addressbook,changes
func (g *Groupware) GetAddressBookChanges(w http.ResponseWriter, r *http.Request) {
- changes(AddressBook, w, r, g, g.jmap.GetAddressbookChanges)
+ changes(AddressBook, w, r, g, g.addressbooksChanges)
}
func (g *Groupware) CreateAddressBook(w http.ResponseWriter, r *http.Request) {
@@ -39,3 +39,11 @@ func (g *Groupware) addressbooks(accountId jmap.AccountId, ids []string, ctx jma
return jmap.AddressBookGetResponse{AccountId: accountId, State: state, NotFound: notFound, List: list}
})
}
+
+func (g *Groupware) addressbooksChanges(accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.Result[jmap.AddressBookChanges], error) {
+ return schanges(g.addressBookChangesSuppliers, accountId, sinceState, maxChanges, ctx,
+ func(accountId jmap.AccountId, oldState, newState jmap.State, created, updated []jmap.AddressBook, destroyed []string, hasMoreChanges bool) jmap.AddressBookChanges {
+ return jmap.AddressBookChanges{HasMoreChanges: hasMoreChanges, OldState: oldState, NewState: newState, Created: created, Updated: updated, Destroyed: destroyed}
+ },
+ )
+}
diff --git a/services/groupware/pkg/groupware/api_contacts.go b/services/groupware/pkg/groupware/api_contacts.go
index 22b3043ee9..4a0602adbf 100644
--- a/services/groupware/pkg/groupware/api_contacts.go
+++ b/services/groupware/pkg/groupware/api_contacts.go
@@ -67,7 +67,7 @@ func (g *Groupware) GetAllContacts(w http.ResponseWriter, r *http.Request) {
// Get changes to Contacts since a given State
// @api:tags contact,changes
func (g *Groupware) GetContactsChanges(w http.ResponseWriter, r *http.Request) {
- changes(Contact, w, r, g, g.jmap.GetContactCardChanges)
+ changes(Contact, w, r, g, g.contactsChanges)
}
func (g *Groupware) CreateContact(w http.ResponseWriter, r *http.Request) {
@@ -83,9 +83,19 @@ func (g *Groupware) ModifyContact(w http.ResponseWriter, r *http.Request) {
}
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}
- })
+ 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) contactsChanges(accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.Result[jmap.ContactCardChanges], error) {
+ return schanges(g.contactCardChangesSuppliers, accountId, sinceState, maxChanges, ctx,
+ func(accountId jmap.AccountId, oldState, newState jmap.State, created, updated []jmap.ContactCard, destroyed []string, hasMoreChanges bool) jmap.ContactCardChanges {
+ return jmap.ContactCardChanges{HasMoreChanges: hasMoreChanges, OldState: oldState, NewState: newState, Created: created, Updated: updated, Destroyed: destroyed}
+ },
+ )
}
func (g *Groupware) queryContacts(accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, //NOSONAR
diff --git a/services/groupware/pkg/groupware/framework.go b/services/groupware/pkg/groupware/framework.go
index 8175240ff8..842b05997f 100644
--- a/services/groupware/pkg/groupware/framework.go
+++ b/services/groupware/pkg/groupware/framework.go
@@ -117,9 +117,11 @@ type Groupware struct {
// A threadsafe counter to generate the job IDs.
jobCounter atomic.Uint64
- addressBookListSuppliers []ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse]
- contactCardQuerySuppliers []QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator]
- contactCardListSuppliers []ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse]
+ addressBookListSuppliers []ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse]
+ addressBookChangesSuppliers []ChangesSupplier[jmap.AddressBook, jmap.AddressBookChanges]
+ contactCardQuerySuppliers []QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator]
+ contactCardListSuppliers []ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse]
+ contactCardChangesSuppliers []ChangesSupplier[jmap.ContactCard, jmap.ContactCardChanges]
}
// An error during the Groupware initialization.
@@ -393,27 +395,33 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
}
addressBookListSuppliers := []ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse]{}
+ addressBookChangesSuppliers := []ChangesSupplier[jmap.AddressBook, jmap.AddressBookChanges]{}
contactCardQuerySuppliers := []QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator]{}
contactCardListSuppliers := []ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse]{}
+ contactCardChangesSuppliers := []ChangesSupplier[jmap.ContactCard, jmap.ContactCardChanges]{}
{
{
j := newJmapAddressBookSupplier(&jmapClient)
addressBookListSuppliers = append(addressBookListSuppliers, j)
+ addressBookChangesSuppliers = append(addressBookChangesSuppliers, j)
}
{
j := newJmapContactCardSupplier(&jmapClient)
contactCardQuerySuppliers = append(contactCardQuerySuppliers, j)
contactCardListSuppliers = append(contactCardListSuppliers, j)
+ contactCardChangesSuppliers = append(contactCardChangesSuppliers, j)
}
if config.EnableMockData {
{
m := newMockAddressBookSupplier()
addressBookListSuppliers = append(addressBookListSuppliers, m)
+ addressBookChangesSuppliers = append(addressBookChangesSuppliers, m)
}
{
m := newMockContactCardSupplier()
contactCardQuerySuppliers = append(contactCardQuerySuppliers, m)
contactCardListSuppliers = append(contactCardListSuppliers, m)
+ contactCardChangesSuppliers = append(contactCardChangesSuppliers, m)
}
}
}
@@ -446,12 +454,14 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
publicUrl: publicUrl,
sendDurationsResponse: sendDurationsResponse,
},
- eventChannel: eventChannel,
- jobsChannel: jobsChannel,
- jobCounter: atomic.Uint64{},
- addressBookListSuppliers: addressBookListSuppliers,
- contactCardQuerySuppliers: contactCardQuerySuppliers,
- contactCardListSuppliers: contactCardListSuppliers,
+ eventChannel: eventChannel,
+ jobsChannel: jobsChannel,
+ jobCounter: atomic.Uint64{},
+ addressBookListSuppliers: addressBookListSuppliers,
+ addressBookChangesSuppliers: addressBookChangesSuppliers,
+ contactCardQuerySuppliers: contactCardQuerySuppliers,
+ contactCardListSuppliers: contactCardListSuppliers,
+ contactCardChangesSuppliers: contactCardChangesSuppliers,
}
for w := 1; w <= workerPoolSize; w++ {
diff --git a/services/groupware/pkg/groupware/supplier_addressbook_jmap.go b/services/groupware/pkg/groupware/supplier_addressbook_jmap.go
index 86e9439dc8..539ce6db64 100644
--- a/services/groupware/pkg/groupware/supplier_addressbook_jmap.go
+++ b/services/groupware/pkg/groupware/supplier_addressbook_jmap.go
@@ -11,6 +11,7 @@ type JmapAddressBookSupplier struct {
}
var _ ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse] = &JmapAddressBookSupplier{}
+var _ ChangesSupplier[jmap.ContactCard, jmap.ContactCardChanges] = &JmapContactCardSupplier{}
func newJmapAddressBookSupplier(client *jmap.Client) *JmapAddressBookSupplier {
return &JmapAddressBookSupplier{client: client}
@@ -21,9 +22,15 @@ const jmapAddressBookSupplierId = SupplierId("jmap")
func (c *JmapAddressBookSupplier) GetId() SupplierId {
return jmapAddressBookSupplierId
}
+
func (c *JmapAddressBookSupplier) IsMine(id string) bool {
return id != "" && !strings.Contains(id, ":")
}
+
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 *JmapAddressBookSupplier) GetChanges(accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.Result[jmap.AddressBookChanges], error) {
+ return c.client.GetAddressbookChanges(accountId, sinceState, maxChanges, ctx)
+}
diff --git a/services/groupware/pkg/groupware/supplier_addressbook_mock.go b/services/groupware/pkg/groupware/supplier_addressbook_mock.go
index f963e3b2c1..f9ba57ff22 100644
--- a/services/groupware/pkg/groupware/supplier_addressbook_mock.go
+++ b/services/groupware/pkg/groupware/supplier_addressbook_mock.go
@@ -9,11 +9,12 @@ import (
)
type MockAddressBookSupplier struct {
- addressBook jmap.AddressBook
+ addressBooks []jmap.AddressBook
+ state jmap.State
}
var MockAddressBookSupplierInstance *MockAddressBookSupplier = &MockAddressBookSupplier{
- addressBook: jmap.AddressBook{
+ addressBooks: []jmap.AddressBook{{
Id: "mock:1",
Name: "Automatic Addressbook",
Description: "Users",
@@ -25,7 +26,8 @@ var MockAddressBookSupplierInstance *MockAddressBookSupplier = &MockAddressBookS
MayAdmin: false,
MayDelete: false,
},
- },
+ }},
+ state: jmap.State("1"),
}
var _ ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse] = &MockAddressBookSupplier{}
@@ -41,7 +43,7 @@ func (c *MockAddressBookSupplier) IsMine(id string) bool {
return strings.HasPrefix(id, "mock:")
}
func (c *MockAddressBookSupplier) GetAll(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.AddressBookGetResponse], error) {
- abooks := []jmap.AddressBook{c.addressBook}
+ abooks := c.addressBooks
if len(ids) > 0 {
abooks = structs.Filter(abooks, func(a jmap.AddressBook) bool { return slices.Contains(ids, a.Id) })
}
@@ -57,3 +59,22 @@ func (c *MockAddressBookSupplier) GetAll(accountId jmap.AccountId, ids []string,
nil,
), nil
}
+
+func (c *MockAddressBookSupplier) GetChanges(accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.Result[jmap.AddressBookChanges], error) {
+ if sinceState == c.state {
+ ch := jmap.AddressBookChanges{
+ HasMoreChanges: false,
+ OldState: c.state,
+ NewState: c.state,
+ }
+ return jmap.NewResult(ch, jmap.EmptySessionState, c.state, jmap.NoLanguage, nil), nil
+ } else {
+ // what happens when the state is unknown?
+ ch := jmap.AddressBookChanges{
+ HasMoreChanges: false,
+ NewState: c.state,
+ Created: c.addressBooks,
+ }
+ return jmap.NewResult(ch, jmap.EmptySessionState, c.state, jmap.NoLanguage, nil), nil
+ }
+}
diff --git a/services/groupware/pkg/groupware/supplier_contactcard_jmap.go b/services/groupware/pkg/groupware/supplier_contactcard_jmap.go
index 34a46f2306..963f80498d 100644
--- a/services/groupware/pkg/groupware/supplier_contactcard_jmap.go
+++ b/services/groupware/pkg/groupware/supplier_contactcard_jmap.go
@@ -12,6 +12,7 @@ type JmapContactCardSupplier struct {
var _ QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator] = &JmapContactCardSupplier{}
var _ ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse] = &JmapContactCardSupplier{}
+var _ ChangesSupplier[jmap.ContactCard, jmap.ContactCardChanges] = &JmapContactCardSupplier{}
func newJmapContactCardSupplier(client *jmap.Client) *JmapContactCardSupplier {
return &JmapContactCardSupplier{client: client}
@@ -22,9 +23,11 @@ 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
@@ -32,6 +35,11 @@ func (c *JmapContactCardSupplier) Query(accountIds []jmap.AccountId, qps QueryPa
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)
}
+
+func (c *JmapContactCardSupplier) GetChanges(accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.Result[jmap.ContactCardChanges], error) {
+ return c.client.GetContactCardChanges(accountId, sinceState, maxChanges, ctx)
+}
diff --git a/services/groupware/pkg/groupware/supplier_contactcard_mock.go b/services/groupware/pkg/groupware/supplier_contactcard_mock.go
index 4d9ddc27bf..1a77fe329e 100644
--- a/services/groupware/pkg/groupware/supplier_contactcard_mock.go
+++ b/services/groupware/pkg/groupware/supplier_contactcard_mock.go
@@ -11,6 +11,7 @@ import (
type MockContactCardSupplier struct {
contacts []jmap.ContactCard
+ state jmap.State
}
var MockContactCardSupplierInstance *MockContactCardSupplier = &MockContactCardSupplier{
@@ -41,10 +42,12 @@ var MockContactCardSupplierInstance *MockContactCardSupplier = &MockContactCardS
},
},
},
+ state: jmap.State("1"),
}
var _ QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator] = &MockContactCardSupplier{}
var _ ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse] = &MockContactCardSupplier{}
+var _ ChangesSupplier[jmap.ContactCard, jmap.ContactCardChanges] = &MockContactCardSupplier{}
func newMockContactCardSupplier() *MockContactCardSupplier {
return MockContactCardSupplierInstance
@@ -128,5 +131,24 @@ func (c *MockContactCardSupplier) Query(accountIds []jmap.AccountId, qps QueryPa
payload[accountId] = res
}
- return jmap.NewResult(payload, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, nil), nil
+ return jmap.NewResult(payload, jmap.EmptySessionState, c.state, jmap.NoLanguage, nil), nil
+}
+
+func (c *MockContactCardSupplier) GetChanges(accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.Result[jmap.ContactCardChanges], error) {
+ if sinceState == c.state {
+ ch := jmap.ContactCardChanges{
+ HasMoreChanges: false,
+ OldState: c.state,
+ NewState: c.state,
+ }
+ return jmap.NewResult(ch, jmap.EmptySessionState, c.state, jmap.NoLanguage, nil), nil
+ } else {
+ // what happens when the state is unknown?
+ ch := jmap.ContactCardChanges{
+ HasMoreChanges: false,
+ NewState: c.state,
+ Created: c.contacts,
+ }
+ return jmap.NewResult(ch, jmap.EmptySessionState, c.state, jmap.NoLanguage, nil), nil
+ }
}
diff --git a/services/groupware/pkg/groupware/suppliers.go b/services/groupware/pkg/groupware/suppliers.go
index d7535f465b..04f41d2888 100644
--- a/services/groupware/pkg/groupware/suppliers.go
+++ b/services/groupware/pkg/groupware/suppliers.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"slices"
+ "strings"
"time"
"github.com/opencloud-eu/opencloud/pkg/jmap"
@@ -15,6 +16,8 @@ type SupplierId string
const EmptySupplierId = ""
+const DefaultSupplierId = SupplierId("jmap")
+
type Supplier[T jmap.Foo] interface {
GetId() SupplierId
IsMine(id string) bool
@@ -30,6 +33,11 @@ type QuerySupplier[T jmap.Foo, R jmap.SearchResults[T], F jmap.FilterElement[T],
Supplier[T]
}
+type ChangesSupplier[T jmap.Foo, C jmap.Changes[T]] interface {
+ GetChanges(accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.Result[C], error)
+ Supplier[T]
+}
+
// queryFunc func(req Request, accountIds []string, qps QueryParamsSupplier, limit *uint, filter FILTER, sortBy []COMP, ctx jmap.Context) (jmap.Result[SEARCHRESULTS], NextToken, error),
func curryQueryFunc[SRES jmap.SearchResults[T], T jmap.Foo, FILTER any, COMP any](
f func(accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, filter FILTER, sortBy []COMP, calculateTotal bool, ctx jmap.Context) (jmap.Result[SRES], NextToken, error),
@@ -89,6 +97,86 @@ func agg[T jmap.Idable, R jmap.GetResponse[T]](accountId jmap.AccountId, supplie
return ctor(accountId, state, notFounds, lists), nil
}
+func aggChanges[T jmap.Idable, R jmap.Changes[T]](accountId jmap.AccountId, supplierIds []SupplierId, responses []*R, //NOSONAR
+ ctor func(accountId jmap.AccountId, oldState jmap.State, newState jmap.State, created []T, updated []T, destroyed []string, hasMoreChanges bool) R,
+) (R, error) {
+ if len(responses) < 1 {
+ var zero R
+ return zero, fmt.Errorf("requires at least one response")
+ }
+ created := structs.Concat(structs.Map(responses, func(e *R) []T {
+ if e != nil {
+ return (*e).GetCreated()
+ } else {
+ return []T{}
+ }
+ })...)
+ updated := structs.Concat(structs.Map(responses, func(e *R) []T {
+ if e != nil {
+ return (*e).GetUpdated()
+ } else {
+ return []T{}
+ }
+ })...)
+ destroyed := structs.Concat(structs.Map(responses, func(e *R) []string {
+ if e != nil {
+ return (*e).GetDestroyed()
+ } else {
+ return []string{}
+ }
+ })...)
+ oldStates, err := structs.MeshMap(supplierIds, responses, func(id SupplierId, e *R) (SupplierId, jmap.State, bool) {
+ if e != nil {
+ state := (*e).GetOldState()
+ if state != jmap.EmptyState {
+ return id, state, true
+ } else {
+ return id, jmap.EmptyState, false
+ }
+ } else {
+ return "", jmap.EmptyState, false
+ }
+ })
+ if err != nil {
+ var zero R
+ return zero, err
+ }
+ oldState, err := combineState(oldStates)
+ if err != nil {
+ var zero R
+ return zero, err
+ }
+ newStates, err := structs.MeshMap(supplierIds, responses, func(id SupplierId, e *R) (SupplierId, jmap.State, bool) {
+ if e != nil {
+ state := (*e).GetNewState()
+ if state != jmap.EmptyState {
+ return id, state, true
+ } else {
+ return id, jmap.EmptyState, false
+ }
+ } else {
+ return "", jmap.EmptyState, false
+ }
+ })
+ if err != nil {
+ var zero R
+ return zero, err
+ }
+ newState, err := combineState(newStates)
+ if err != nil {
+ var zero R
+ return zero, err
+ }
+ hasMoreChanges := structs.AllMatch(structs.Map(responses, func(e *R) bool {
+ if e != nil {
+ return (*e).GetHasMoreChanges()
+ } else {
+ return false
+ }
+ }), func(b bool) bool { return b })
+ return ctor(accountId, oldState, newState, created, updated, destroyed, hasMoreChanges), nil
+}
+
func slist[T jmap.Idable, G jmap.GetResponse[T], S ListSupplier[T, G]](suppliers []S, accountId jmap.AccountId, ids []string, ctx jmap.Context, //NOSONAR
ctor func(accountId jmap.AccountId, state jmap.State, notFound []string, list []T) G) (jmap.Result[G], error) {
switch len(suppliers) {
@@ -117,34 +205,143 @@ func slist[T jmap.Idable, G jmap.GetResponse[T], S ListSupplier[T, G]](suppliers
}
}
- return jmap.RefineResultSlice(results, func(payloads []*G, sessionStates []*jmap.SessionState, states []*jmap.State, langs []*jmap.Language) (G, jmap.SessionState, jmap.State, jmap.Language, error) {
- resp, err := agg(accountId, supplierIds, payloads, ctor)
- if err != nil {
- return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
- }
- m, err := structs.MeshMap(supplierIds, sessionStates, func(id SupplierId, state *jmap.SessionState) (SupplierId, jmap.SessionState, bool) {
- if state != nil && *state != jmap.EmptySessionState {
- return id, *state, true
- } else {
- return id, jmap.EmptySessionState, false
- }
- })
- if err != nil {
- return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
- }
- sessionState, err := combineState(m)
- if err != nil {
- return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
- }
- lang := jmap.NoLanguage
- if f, ok := structs.First(langs, func(l *jmap.Language) bool { return l != nil && *l != jmap.NoLanguage }); ok {
- lang = *f
- }
- return resp, sessionState, resp.GetState(), lang, nil
+ return smeld[T](supplierIds, results, func(payloads []*G) (G, error) {
+ return agg(accountId, supplierIds, payloads, ctor)
+ }, func(resp G) jmap.State {
+ return resp.GetState()
})
+
+ /*
+
+ return jmap.RefineResultSlice(results, func(payloads []*G, sessionStates []*jmap.SessionState, states []*jmap.State, langs []*jmap.Language) (G, jmap.SessionState, jmap.State, jmap.Language, error) {
+ resp, err := agg(accountId, supplierIds, payloads, ctor)
+ if err != nil {
+ return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
+ }
+ m, err := structs.MeshMap(supplierIds, sessionStates, func(id SupplierId, state *jmap.SessionState) (SupplierId, jmap.SessionState, bool) {
+ if state != nil && *state != jmap.EmptySessionState {
+ return id, *state, true
+ } else {
+ return id, jmap.EmptySessionState, false
+ }
+ })
+ if err != nil {
+ return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
+ }
+ sessionState, err := combineState(m)
+ if err != nil {
+ return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
+ }
+ lang := jmap.NoLanguage
+ if f, ok := structs.First(langs, func(l *jmap.Language) bool { return l != nil && *l != jmap.NoLanguage }); ok {
+ lang = *f
+ }
+ return resp, sessionState, resp.GetState(), lang, nil
+ })
+ */
}
}
+func schanges[T jmap.Idable, C jmap.Changes[T], S ChangesSupplier[T, C]](suppliers []S, accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context, //NOSONAR
+ ctor func(accountId jmap.AccountId, oldState jmap.State, newState jmap.State, created []T, updated []T, destroyed []string, hasMoreChanges bool) C,
+) (jmap.Result[C], error) {
+ switch len(suppliers) {
+ case 0:
+ return jmap.ZeroResultV[C](), nil
+ case 1:
+ return suppliers[0].GetChanges(accountId, sinceState, maxChanges, ctx)
+ default:
+ results := make([]*jmap.Result[C], len(suppliers))
+ supplierIds := make([]SupplierId, len(suppliers))
+ stateBySupplierId, err := splitState(sinceState, DefaultSupplierId)
+ if err != nil {
+ return jmap.ZeroResultV[C](), err
+ }
+ for i, supplier := range suppliers {
+ supplierIds[i] = supplier.GetId()
+ state := jmap.EmptyState
+ if v, ok := stateBySupplierId[supplier.GetId()]; ok {
+ state = v
+ }
+ // we are not injecting id prefixes here for all the objects, as each supplier is responsible for doing that if necessary
+ if result, err := supplier.GetChanges(accountId, state, maxChanges, ctx); err != nil {
+ return result, err
+ } else {
+ results[i] = &result
+ }
+ }
+
+ // TODO need to reduce the maxChanges? this is quite complex to implement, if even possible, not doing that for now
+
+ return smeld[T](supplierIds, results, func(payloads []*C) (C, error) {
+ return aggChanges(accountId, supplierIds, payloads, ctor)
+ }, func(resp C) jmap.State {
+ return resp.GetNewState()
+ })
+
+ /*
+ return jmap.RefineResultSlice(results, func(payloads []*C, sessionStates []*jmap.SessionState, states []*jmap.State, langs []*jmap.Language) (C, jmap.SessionState, jmap.State, jmap.Language, error) {
+ resp, err := aggChanges(accountId, supplierIds, payloads, ctor)
+ if err != nil {
+ return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
+ }
+ m, err := structs.MeshMap(supplierIds, sessionStates, func(id SupplierId, state *jmap.SessionState) (SupplierId, jmap.SessionState, bool) {
+ if state != nil && *state != jmap.EmptySessionState {
+ return id, *state, true
+ } else {
+ return id, jmap.EmptySessionState, false
+ }
+ })
+ if err != nil {
+ return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
+ }
+ sessionState, err := combineState(m)
+ if err != nil {
+ return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
+ }
+ lang := jmap.NoLanguage
+ if f, ok := structs.First(langs, func(l *jmap.Language) bool { return l != nil && *l != jmap.NoLanguage }); ok {
+ lang = *f
+ }
+ return resp, sessionState, resp.GetNewState(), lang, nil
+ })
+ */
+ }
+}
+
+func smeld[T jmap.Idable, C any](
+ supplierIds []SupplierId,
+ results []*jmap.Result[C],
+ aggregator func(payloads []*C) (C, error),
+ stateSupplier func(resp C) jmap.State,
+) (jmap.Result[C], error) {
+ return jmap.RefineResultSlice(results, func(payloads []*C, sessionStates []*jmap.SessionState, states []*jmap.State, langs []*jmap.Language) (C, jmap.SessionState, jmap.State, jmap.Language, error) {
+ resp, err := aggregator(payloads)
+ if err != nil {
+ return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
+ }
+ m, err := structs.MeshMap(supplierIds, sessionStates, func(id SupplierId, state *jmap.SessionState) (SupplierId, jmap.SessionState, bool) {
+ if state != nil && *state != jmap.EmptySessionState {
+ return id, *state, true
+ } else {
+ return id, jmap.EmptySessionState, false
+ }
+ })
+ if err != nil {
+ return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
+ }
+ sessionState, err := combineState(m)
+ if err != nil {
+ return resp, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, err
+ }
+ lang := jmap.NoLanguage
+ if f, ok := structs.First(langs, func(l *jmap.Language) bool { return l != nil && *l != jmap.NoLanguage }); ok {
+ lang = *f
+ }
+ return resp, sessionState, stateSupplier(resp), lang, nil
+ })
+}
+
func fillMissingAccounts(qps QueryParamsSupplier, supplierId SupplierId, accountIds []jmap.AccountId, n map[jmap.AccountId]jmap.QueryParams) error {
for _, accountId := range accountIds {
if _, ok := n[accountId]; !ok {
@@ -403,7 +600,7 @@ func squery[T jmap.Idable, R jmap.SearchResults[T], S QuerySupplier[T, R, F, C],
const combinedStateEncodingPrefix = "="
-func combineState[K ~string, S jmap.State | jmap.SessionState](m map[K]S) (S, error) {
+func combineState[S jmap.State | jmap.SessionState](m map[SupplierId]S) (S, error) {
if b, err := json.Marshal(m); err != nil {
return "", err
} else {
@@ -412,14 +609,13 @@ func combineState[K ~string, S jmap.State | jmap.SessionState](m map[K]S) (S, er
}
}
-/*
-func splitState[K ~string, S jmap.State | jmap.SessionState](state S) (map[K]S, error) {
+func splitState[S jmap.State | jmap.SessionState](state S, defaultSupplierId SupplierId) (map[SupplierId]S, error) {
s := string(state)
if strings.HasPrefix(s, combinedStateEncodingPrefix) {
if b, err := base64.RawURLEncoding.DecodeString(s[len(combinedStateEncodingPrefix):]); err != nil {
return nil, err
} else {
- m := map[K]S{}
+ m := map[SupplierId]S{}
if err := json.Unmarshal(b, &m); err != nil {
return nil, err
} else {
@@ -427,7 +623,6 @@ func splitState[K ~string, S jmap.State | jmap.SessionState](state S) (map[K]S,
}
}
} else {
- return map[K]S{K("jmap"): state}, nil
+ return map[SupplierId]S{defaultSupplierId: state}, nil
}
}
-*/