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 } } -*/