From 74e48caa5c26744a11c9b8178f8863f8bc2c965a Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Tue, 30 Jun 2026 18:42:31 +0200
Subject: [PATCH] groupware: fully implement suppliers for addressbooks and
contacts
* implement supplier templating to allow addressbooks and contacts to
come from various sources ("suppliers", thus) as opposed to solely
from JMAP
* add creating, deleting, updating, getting changes
* add some search filter parameters, currently incomplete
---
pkg/jmap/api_vacation.go | 6 +-
pkg/jmap/export_integration_test.go | 2 +-
pkg/jmap/model.go | 69 +++-
pkg/jmap/templates.go | 2 +-
pkg/structs/structs.go | 15 +
pkg/structs/structs_test.go | 33 ++
.../pkg/groupware/api_addressbooks.go | 32 +-
.../groupware/pkg/groupware/api_calendars.go | 2 +-
.../groupware/pkg/groupware/api_contacts.go | 99 ++++--
.../groupware/pkg/groupware/api_emails.go | 2 +-
.../groupware/pkg/groupware/api_events.go | 29 +-
.../groupware/pkg/groupware/api_identity.go | 2 +-
.../groupware/pkg/groupware/api_mailbox.go | 2 +-
services/groupware/pkg/groupware/error.go | 13 +
services/groupware/pkg/groupware/framework.go | 56 +---
services/groupware/pkg/groupware/objtypes.go | 2 +-
services/groupware/pkg/groupware/request.go | 53 ++-
services/groupware/pkg/groupware/route.go | 133 ++++----
.../groupware/supplier_addressbook_jmap.go | 11 +-
.../groupware/supplier_contactcard_jmap.go | 9 +
.../groupware/supplier_contactcard_mock.go | 32 +-
services/groupware/pkg/groupware/suppliers.go | 305 ++++++++++++++----
services/groupware/pkg/groupware/templates.go | 43 ++-
services/groupware/pkg/groupware/tools.go | 14 +
24 files changed, 710 insertions(+), 256 deletions(-)
diff --git a/pkg/jmap/api_vacation.go b/pkg/jmap/api_vacation.go
index 06df6e07ae..c8146f83c6 100644
--- a/pkg/jmap/api_vacation.go
+++ b/pkg/jmap/api_vacation.go
@@ -47,12 +47,16 @@ type VacationResponseChange struct {
HtmlBody string `json:"htmlBody,omitempty"`
}
-var _ Change = VacationResponseChange{}
+var _ Change[VacationResponse] = VacationResponseChange{}
func (m VacationResponseChange) AsPatch() (PatchObject, error) {
return toPatchObject(m)
}
+func (m VacationResponseChange) GetMarker() VacationResponse {
+ return VacationResponse{}
+}
+
type VacationResponseChanges ChangesTemplate[VacationResponse]
var _ Changes[VacationResponse] = VacationResponseChanges{}
diff --git a/pkg/jmap/export_integration_test.go b/pkg/jmap/export_integration_test.go
index 564638a1fe..6abc5018d6 100644
--- a/pkg/jmap/export_integration_test.go
+++ b/pkg/jmap/export_integration_test.go
@@ -1744,7 +1744,7 @@ func deepEqual[T any](t *testing.T, expected, actual T) {
require.Empty(t, diff)
}
-func containerTest[OBJ Idable, RESP GetResponse[OBJ], BOXES any, CHANGE Change](t *testing.T, //NOSONAR
+func containerTest[OBJ Idable, RESP GetResponse[OBJ], BOXES any, CHANGE Change[OBJ]](t *testing.T, //NOSONAR
acc func(session *Session) AccountId,
obj func(RESP) []OBJ,
id func(OBJ) string,
diff --git a/pkg/jmap/model.go b/pkg/jmap/model.go
index 152fd3b125..3c617a802b 100644
--- a/pkg/jmap/model.go
+++ b/pkg/jmap/model.go
@@ -29,6 +29,12 @@ func (o ObjectType) String() string {
// For example, `"2014-10-30T06:12:00Z"`.
type UTCDate string
+// Time Format string for parsing a `UTCDate`.
+//
+// According to Section 1.4 of RFC 8620 (JMAP Core), the specification strictly dictates that for a UTCDate type,
+// the time-offset component MUST be "Z". Other timezone numerical specs (like +02:00) are explicitly forbidden.
+const UTCDateFormat = "2026-06-29T15:59:59Z"
+
// Where `LocalDate` is given as a type, it means a string in the same format as `Date`
// (see [RFC8620, Section 1.4](https://www.rfc-editor.org/rfc/rfc8620.html#section-1.4)),
// but with the time-offset omitted from the end.
@@ -1370,8 +1376,9 @@ type SetResponse[T Foo] interface {
GetNewState() State
}
-type Change interface {
+type Change[T Foo] interface {
AsPatch() (PatchObject, error)
+ GetMarker() T
}
type ChangesCommand[T Foo] interface {
@@ -1755,12 +1762,16 @@ type MailboxChange struct {
IsSubscribed *bool `json:"isSubscribed,omitempty"`
}
-var _ Change = MailboxChange{}
+var _ Change[Mailbox] = MailboxChange{}
func (m MailboxChange) AsPatch() (PatchObject, error) {
return toPatchObject(m)
}
+func (m MailboxChange) GetMarker() Mailbox {
+ return Mailbox{}
+}
+
type MailboxGetCommand struct {
AccountId AccountId `json:"accountId"`
Ids []string `json:"ids,omitempty"`
@@ -3634,12 +3645,16 @@ type EmailChange struct {
Attachments []EmailBodyPart `json:"attachments,omitempty"`
}
-var _ Change = EmailChange{}
+var _ Change[Email] = EmailChange{}
func (e EmailChange) AsPatch() (PatchObject, error) {
return toPatchObject(e)
}
+func (m EmailChange) GetMarker() Email {
+ return Email{}
+}
+
type EmailSetCommand struct {
// The id of the account to use.
AccountId AccountId `json:"accountId"`
@@ -4091,12 +4106,16 @@ type IdentityChange struct {
HtmlSignature *string `json:"htmlSignature,omitempty"`
}
-var _ Change = IdentityChange{}
+var _ Change[Identity] = IdentityChange{}
func (i IdentityChange) AsPatch() (PatchObject, error) {
return toPatchObject(i)
}
+func (m IdentityChange) GetMarker() Identity {
+ return Identity{}
+}
+
type IdentityGetResponse struct {
AccountId AccountId `json:"accountId"`
State State `json:"state"`
@@ -4340,12 +4359,16 @@ func (f Blob) GetId() string { return f.Id }
type BlobChange struct {
}
-var _ Change = BlobChange{}
+var _ Change[Blob] = BlobChange{}
func (m BlobChange) AsPatch() (PatchObject, error) {
return nil, fmt.Errorf("BlobChange is unsupported")
}
+func (m BlobChange) GetMarker() Blob {
+ return Blob{}
+}
+
type BlobChanges ChangesTemplate[Blob]
var _ Changes[Blob] = BlobChanges{}
@@ -5148,12 +5171,16 @@ type ContactCardChange struct {
PersonalInfo map[string]jscontact.PersonalInfo `json:"personalInfo,omitempty"`
}
-var _ Change = ContactCardChange{}
+var _ Change[ContactCard] = ContactCardChange{}
func (e ContactCardChange) AsPatch() (PatchObject, error) {
return toPatchObject(e)
}
+func (m ContactCardChange) GetMarker() ContactCard {
+ return ContactCard{}
+}
+
type CalendarRights struct {
// The user may read the free-busy information for this calendar.
MayReadFreeBusy bool `json:"mayReadFreeBusy"`
@@ -5498,12 +5525,16 @@ type CalendarChange struct {
MyRights *CalendarRights `json:"myRights,omitempty"`
}
-var _ Change = CalendarChange{}
+var _ Change[Calendar] = CalendarChange{}
func (c CalendarChange) AsPatch() (PatchObject, error) {
return toPatchObject(c)
}
+func (m CalendarChange) GetMarker() Calendar {
+ return Calendar{}
+}
+
// A CalendarEvent object contains information about an event, or recurring series of events,
// that takes place at a particular time.
//
@@ -5654,12 +5685,16 @@ type CalendarEventChange struct {
jscalendar.EventChange
}
-var _ Change = CalendarEventChange{}
+var _ Change[CalendarEvent] = CalendarEventChange{}
func (e CalendarEventChange) AsPatch() (PatchObject, error) {
return toPatchObject(e)
}
+func (m CalendarEventChange) GetMarker() CalendarEvent {
+ return CalendarEvent{}
+}
+
var _ Idable = &CalendarEvent{}
func (f CalendarEvent) GetObjectType() ObjectType { return CalendarEventType }
@@ -6536,12 +6571,16 @@ func (f Quota) GetId() string { return f.Id }
type QuotaChange struct {
}
-var _ Change = QuotaChange{}
+var _ Change[Quota] = QuotaChange{}
func (m QuotaChange) AsPatch() (PatchObject, error) {
return nil, fmt.Errorf("QuotaChange is unsupported")
}
+func (m QuotaChange) GetMarker() Quota {
+ return Quota{}
+}
+
// See [RFC8098] for the exact meaning of these different fields.
//
// These fields are defined as case insensitive in [RFC8098] but are case sensitive in this RFC
@@ -6815,12 +6854,16 @@ type AddressBookChange struct {
ShareWith map[PrincipalId]AddressBookRights `json:"shareWith,omitempty"`
}
-var _ Change = AddressBookChange{}
+var _ Change[AddressBook] = AddressBookChange{}
func (a AddressBookChange) AsPatch() (PatchObject, error) {
return toPatchObject(a)
}
+func (m AddressBookChange) GetMarker() AddressBook {
+ return AddressBook{}
+}
+
type AddressBookSetCommand struct {
AccountId AccountId `json:"accountId"`
IfInState State `json:"ifInState,omitempty"`
@@ -7007,7 +7050,7 @@ type ContactCardFilterCondition struct {
HasMember string `json:"hasMember,omitempty"`
// A card must have a type property that equals this string exactly to match.
- Kind string `json:"kind,omitempty"`
+ Kind jscontact.ContactCardKind `json:"kind,omitempty"`
// The “created” date-time of the ContactCard must be before this date-time to match the condition.
CreatedBefore UTCDate `json:"createdBefore,omitzero"`
@@ -7047,11 +7090,11 @@ type ContactCardFilterCondition struct {
// matches the value.
Organization string `json:"organization,omitempty"`
- // A card matches this condition if the “address” or “label” of any EmailAddress in the “emails” property of the
+ // A card matches this condition if the “address” or “label” of any EmailAddress in the “emails” property of the
// card matches the value.
Email string `json:"email,omitempty"`
- // A card matches this condition if the “number” or “label” of any Phone in the “phones” property of the card
+ // A card matches this condition if the “number” or “label” of any Phone in the “phones” property of the card
// matches the value.
Phone string `json:"phone,omitempty"`
diff --git a/pkg/jmap/templates.go b/pkg/jmap/templates.go
index d28db883a6..50e921a55b 100644
--- a/pkg/jmap/templates.go
+++ b/pkg/jmap/templates.go
@@ -380,7 +380,7 @@ func updates[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESR
})
}
-func update[T Foo, CHANGES Change, SET SetCommand[T], GET GetCommand[T], RESP any, SETRESP SetResponse[T], GETRESP GetResponse[T]]( //NOSONAR
+func update[T Foo, CHANGES Change[T], SET SetCommand[T], GET GetCommand[T], RESP any, SETRESP SetResponse[T], GETRESP GetResponse[T]]( //NOSONAR
client *Client, name string, objType ObjectType,
setCommandFactory func(map[string]PatchObject) SET,
getCommandFactory func(string) GET,
diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go
index fd10340f24..7f7d1c00e8 100644
--- a/pkg/structs/structs.go
+++ b/pkg/structs/structs.go
@@ -406,3 +406,18 @@ func AllMatch[T any](s []T, predicate func(e T) bool) bool {
}
return true
}
+
+func Distribute[T comparable, V any](s []V, distributor func(e V) T) map[T][]V {
+ result := map[T][]V{}
+ for _, e := range s {
+ k := distributor(e)
+ if l, ok := result[k]; ok {
+ l = append(l, e)
+ result[k] = l
+ } else {
+ l := []V{e}
+ result[k] = l
+ }
+ }
+ return result
+}
diff --git a/pkg/structs/structs_test.go b/pkg/structs/structs_test.go
index 23a7985c9f..3cf2a7bd47 100644
--- a/pkg/structs/structs_test.go
+++ b/pkg/structs/structs_test.go
@@ -309,3 +309,36 @@ func TestFlatten(t *testing.T) {
assert.Equal(t, []int{}, result)
}
}
+
+func TestAllMatch(t *testing.T) {
+ assert.True(t, AllMatch([]int{1, 2, 3}, func(i int) bool { return i > 0 }))
+ assert.False(t, AllMatch([]int{1, 2, 0, 3}, func(i int) bool { return i > 0 }))
+ assert.False(t, AllMatch([]int{1, 2, 3, 0}, func(i int) bool { return i > 0 }))
+ assert.False(t, AllMatch([]int{0}, func(i int) bool { return i > 0 }))
+ assert.True(t, AllMatch([]int{}, func(i int) bool { return i > 0 }))
+}
+
+func TestDistribute(t *testing.T) {
+ {
+ result := Distribute([]string{"Z", "a", "b", "X", "c", "Y"}, func(e string) int {
+ if strings.ToUpper(e) == e {
+ return 1
+ } else {
+ return 0
+ }
+ })
+ assert.Len(t, result, 2)
+ assert.Contains(t, result, 1)
+ assert.Equal(t, result[1], []string{"Z", "X", "Y"})
+ assert.Contains(t, result, 0)
+ assert.Equal(t, result[0], []string{"a", "b", "c"})
+ }
+ {
+ result := Distribute([]string{}, func(e string) int { return 1 })
+ assert.Empty(t, result)
+ }
+ {
+ result := Distribute(nil, func(e string) int { return 1 })
+ assert.Empty(t, result)
+ }
+}
diff --git a/services/groupware/pkg/groupware/api_addressbooks.go b/services/groupware/pkg/groupware/api_addressbooks.go
index 2092c654cb..5755fe40a4 100644
--- a/services/groupware/pkg/groupware/api_addressbooks.go
+++ b/services/groupware/pkg/groupware/api_addressbooks.go
@@ -8,40 +8,52 @@ import (
// Get all addressbooks of an account.
func (g *Groupware) GetAddressbooks(w http.ResponseWriter, r *http.Request) {
- getall(AddressBook, w, r, g, g.addressbooks)
+ getall(AddressBook, w, r, g, g.listAddressBooks)
}
// Get an addressbook of an account by its identifier.
func (g *Groupware) GetAddressbookById(w http.ResponseWriter, r *http.Request) {
- get(AddressBook, w, r, g, g.addressbooks)
+ get(AddressBook, w, r, g, g.listAddressBooks)
}
// 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.addressbooksChanges)
+ changes(AddressBook, w, r, g, g.addressBooksChanges)
}
func (g *Groupware) CreateAddressBook(w http.ResponseWriter, r *http.Request) {
- create(AddressBook, w, r, g, nil, g.jmap.CreateAddressBook)
+ create(AddressBook, w, r, g, nil, g.createAddressBook)
}
func (g *Groupware) DeleteAddressBook(w http.ResponseWriter, r *http.Request) {
- delete(AddressBook, w, r, g, g.jmap.DeleteAddressBook)
+ deleteById(AddressBook, w, r, g, g.deleteAddressBooks)
}
func (g *Groupware) ModifyAddressBook(w http.ResponseWriter, r *http.Request) {
- modify(AddressBook, w, r, g, g.jmap.UpdateAddressBook)
+ modify(AddressBook, w, r, g, g.updateAddressBook)
}
-func (g *Groupware) addressbooks(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.AddressBookGetResponse], error) {
- return slist(g.addressBookListSuppliers, accountId, ids, ctx, func(accountId jmap.AccountId, state jmap.State, notFound []string, list []jmap.AddressBook) jmap.AddressBookGetResponse {
+func (g *Groupware) createAddressBook(accountId jmap.AccountId, addressbook jmap.AddressBookChange, ctx jmap.Context) (jmap.Result[*jmap.AddressBook], error) {
+ return screate(g.suppliers.addressBooks.create, accountId, addressbook, ctx)
+}
+
+func (g *Groupware) updateAddressBook(accountId jmap.AccountId, id string, addressbook jmap.AddressBookChange, ctx jmap.Context) (jmap.Result[jmap.AddressBook], error) {
+ return supdate(g.suppliers.addressBooks.update, accountId, id, addressbook, ctx)
+}
+
+func (g *Groupware) deleteAddressBooks(accountId jmap.AccountId, destroyIds []string, ctx jmap.Context) (jmap.Result[map[string]jmap.SetError], error) {
+ return sdelete[jmap.AddressBook](g.suppliers.addressBooks.delete, accountId, destroyIds, ctx)
+}
+
+func (g *Groupware) listAddressBooks(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.AddressBookGetResponse], error) {
+ return slist(g.suppliers.addressBooks.list, accountId, ids, ctx, func(accountId jmap.AccountId, state jmap.State, notFound []string, list []jmap.AddressBook) jmap.AddressBookGetResponse {
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 (g *Groupware) addressBooksChanges(accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.Result[jmap.AddressBookChanges], error) {
+ return schanges(g.suppliers.addressBooks.changes, 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_calendars.go b/services/groupware/pkg/groupware/api_calendars.go
index 76e4b07c1b..1a487befb8 100644
--- a/services/groupware/pkg/groupware/api_calendars.go
+++ b/services/groupware/pkg/groupware/api_calendars.go
@@ -25,7 +25,7 @@ func (g *Groupware) CreateCalendar(w http.ResponseWriter, r *http.Request) {
}
func (g *Groupware) DeleteCalendar(w http.ResponseWriter, r *http.Request) {
- delete(Calendar, w, r, g, g.jmap.DeleteCalendar)
+ deleteById(Calendar, w, r, g, g.jmap.DeleteCalendar)
}
func (g *Groupware) ModifyCalendar(w http.ResponseWriter, r *http.Request) {
diff --git a/services/groupware/pkg/groupware/api_contacts.go b/services/groupware/pkg/groupware/api_contacts.go
index 4a0602adbf..41eb8f8c6f 100644
--- a/services/groupware/pkg/groupware/api_contacts.go
+++ b/services/groupware/pkg/groupware/api_contacts.go
@@ -4,6 +4,8 @@ import (
"net/http"
"github.com/opencloud-eu/opencloud/pkg/jmap"
+ "github.com/opencloud-eu/opencloud/pkg/jscontact"
+ "github.com/opencloud-eu/opencloud/pkg/log"
)
var (
@@ -42,66 +44,125 @@ var (
// Get all the contacts in an addressbook of an account by its identifier.
func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Request) { //NOSONAR
getallpaged(Contact, w, r, g, true,
- func(addressbookId string) jmap.ContactCardFilterElement {
- return jmap.ContactCardFilterCondition{InAddressBook: addressbookId}
- },
+ g.buildContactsFilter, supportedContactsFilterQueryParams,
[]jmap.ContactCardComparator{{Property: jmap.ContactCardPropertyCreated, IsAscending: true}},
- curryQueryFunc(g.queryContacts),
+ curryQueryFunc(g.queryContactCards),
)
}
func (g *Groupware) GetContactById(w http.ResponseWriter, r *http.Request) {
- get(Contact, w, r, g, g.contacts)
+ get(Contact, w, r, g, g.listContactCards)
}
func (g *Groupware) GetAllContacts(w http.ResponseWriter, r *http.Request) {
getallpaged(Contact, w, r, g, false,
- func(_ string) jmap.ContactCardFilterElement {
- return jmap.ContactCardFilterCondition{}
- },
+ g.buildContactsFilter, supportedContactsFilterQueryParams,
[]jmap.ContactCardComparator{{Property: jmap.ContactCardPropertyCreated, IsAscending: true}},
- curryQueryFunc(g.queryContacts),
+ curryQueryFunc(g.queryContactCards),
)
}
+var supportedContactsFilterQueryParams = toSupportedQueryParams(
+ QueryParamContactFilterUid,
+ QueryParamContactFilterUid,
+ QueryParamContactFilterCreatedAfter,
+ QueryParamContactFilterCreatedBefore,
+ QueryParamContactFilterEmail,
+ QueryParamContactFilterMember,
+ QueryParamContactFilterKind,
+ QueryParamContactFilterName,
+ // TODO add more as they are added below
+)
+
+func (g *Groupware) buildContactsFilter(addressbookId string, req Request, _ *log.Logger) (jmap.ContactCardFilterElement, *Error) {
+ filter := jmap.ContactCardFilterCondition{}
+ if addressbookId != "" {
+ filter.InAddressBook = addressbookId
+ }
+ if v, ok := req.getStringParam(QueryParamContactFilterUid, ""); ok {
+ filter.Uid = v
+ }
+ if v, ok := req.getStringParam(QueryParamContactFilterAddress, ""); ok {
+ filter.Address = v
+ }
+ if v, ok, err := req.parseUTCDateParam(QueryParamContactFilterCreatedAfter); err != nil {
+ return filter, err
+ } else if ok {
+ filter.CreatedAfter = v
+ }
+ if v, ok, err := req.parseUTCDateParam(QueryParamContactFilterCreatedBefore); err != nil {
+ return filter, err
+ } else if ok {
+ filter.CreatedBefore = v
+ }
+ if v, ok := req.getStringParam(QueryParamContactFilterEmail, ""); ok {
+ filter.Email = v
+ }
+ if v, ok := req.getStringParam(QueryParamContactFilterMember, ""); ok {
+ filter.HasMember = v
+ }
+ if v, ok, err := req.getValidStringParam(QueryParamContactFilterKind, "", striter(jscontact.ContactCardKinds)); err != nil {
+ return filter, err
+ } else if ok {
+ filter.Kind = jscontact.ContactCardKind(v)
+ }
+ if v, ok := req.getStringParam(QueryParamContactFilterName, ""); ok {
+ filter.Name = v
+ }
+ // TODO more filter conditions for ContactCard
+ return filter, nil
+}
+
// 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.contactsChanges)
+ changes(Contact, w, r, g, g.contactCardsChanges)
}
func (g *Groupware) CreateContact(w http.ResponseWriter, r *http.Request) {
- create(Contact, w, r, g, nil, g.jmap.CreateContactCard)
+ create(Contact, w, r, g, nil, g.createContactCard)
}
func (g *Groupware) DeleteContact(w http.ResponseWriter, r *http.Request) {
- delete(Contact, w, r, g, g.jmap.DeleteContactCard)
+ deleteById(Contact, w, r, g, g.deleteContactCards)
}
func (g *Groupware) ModifyContact(w http.ResponseWriter, r *http.Request) {
- modify(Contact, w, r, g, g.jmap.UpdateContactCard)
+ modify(Contact, w, r, g, g.updateContactCard)
}
-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 (g *Groupware) createContactCard(accountId jmap.AccountId, contact jmap.ContactCardChange, ctx jmap.Context) (jmap.Result[*jmap.ContactCard], error) {
+ return screate(g.suppliers.contactCards.create, accountId, contact, ctx)
+}
+
+func (g *Groupware) updateContactCard(accountId jmap.AccountId, id string, contactCard jmap.ContactCardChange, ctx jmap.Context) (jmap.Result[jmap.ContactCard], error) {
+ return supdate(g.suppliers.contactCards.update, accountId, id, contactCard, ctx)
+}
+
+func (g *Groupware) deleteContactCards(accountId jmap.AccountId, destroyIds []string, ctx jmap.Context) (jmap.Result[map[string]jmap.SetError], error) {
+ return sdelete[jmap.ContactCard](g.suppliers.contactCards.delete, accountId, destroyIds, ctx)
+}
+
+func (g *Groupware) listContactCards(accountId jmap.AccountId, ids []string, ctx jmap.Context) (jmap.Result[jmap.ContactCardGetResponse], error) {
+ return slist(g.suppliers.contactCards.list, 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 (g *Groupware) contactCardsChanges(accountId jmap.AccountId, sinceState jmap.State, maxChanges uint, ctx jmap.Context) (jmap.Result[jmap.ContactCardChanges], error) {
+ return schanges(g.suppliers.contactCards.changes, 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
+func (g *Groupware) queryContactCards(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,
+ return squery(g.suppliers.contactCards.query, accountIds, qps, limit, filter, sortBy, calculateTotal, ctx,
func(supplier QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator], filter jmap.ContactCardFilterElement) bool {
switch c := filter.(type) {
case jmap.ContactCardFilterCondition:
diff --git a/services/groupware/pkg/groupware/api_emails.go b/services/groupware/pkg/groupware/api_emails.go
index 055a7be1eb..2708e02fc5 100644
--- a/services/groupware/pkg/groupware/api_emails.go
+++ b/services/groupware/pkg/groupware/api_emails.go
@@ -1065,7 +1065,7 @@ func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request)
// Delete an email by its unique identifier.
func (g *Groupware) DeleteEmail(w http.ResponseWriter, r *http.Request) {
- delete(Email, w, r, g, g.jmap.DeleteEmails)
+ deleteById(Email, w, r, g, g.jmap.DeleteEmails)
}
// Delete a set of emails by their unique identifiers.
diff --git a/services/groupware/pkg/groupware/api_events.go b/services/groupware/pkg/groupware/api_events.go
index b2a04ec7b5..4b0416cdb3 100644
--- a/services/groupware/pkg/groupware/api_events.go
+++ b/services/groupware/pkg/groupware/api_events.go
@@ -12,9 +12,10 @@ import (
func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request) { //NOSONAR
getallpaged(Event, w, r, g,
true,
- func(calendarId string) jmap.CalendarEventFilterElement {
- return jmap.CalendarEventFilterCondition{InCalendar: calendarId}
+ func(calendarId string, req Request, logger *log.Logger) (jmap.CalendarEventFilterElement, *Error) {
+ return g.buildEventsFilter(calendarId, req, logger)
},
+ supportedEventsFilterQueryParams,
[]jmap.CalendarEventComparator{{Property: jmap.CalendarEventPropertyStart, IsAscending: true}},
curryNoNextMapQuery(
g.jmap.QueryCalendarEvents,
@@ -35,7 +36,10 @@ func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request)
func (g *Groupware) GetAllEvents(w http.ResponseWriter, r *http.Request) {
getallpaged(Event, w, r, g,
false,
- func(_ string) jmap.CalendarEventFilterElement { return jmap.CalendarEventFilterCondition{} },
+ func(calendarId string, req Request, logger *log.Logger) (jmap.CalendarEventFilterElement, *Error) {
+ return g.buildEventsFilter(calendarId, req, logger)
+ },
+ supportedEventsFilterQueryParams,
[]jmap.CalendarEventComparator{{Property: jmap.CalendarEventPropertyStart, IsAscending: true}},
curryNoNextMapQuery(
g.jmap.QueryCalendarEvents,
@@ -53,6 +57,23 @@ func (g *Groupware) GetAllEvents(w http.ResponseWriter, r *http.Request) {
)
}
+var supportedEventsFilterQueryParams = toSupportedQueryParams(
+ QueryParamEventsFilterUid,
+ // TODO add more as they are added below
+)
+
+func (g *Groupware) buildEventsFilter(calendarId string, req Request, _ *log.Logger) (jmap.CalendarEventFilterCondition, *Error) {
+ filter := jmap.CalendarEventFilterCondition{}
+ if calendarId != "" {
+ filter.InCalendar = calendarId
+ }
+ if v, ok := req.getStringParam(QueryParamEventsFilterUid, ""); ok {
+ filter.Uid = v
+ }
+ // TODO more filter conditions for Event
+ return filter, nil
+}
+
func (g *Groupware) GetEventById(w http.ResponseWriter, r *http.Request) {
get(Event, w, r, g, g.jmap.GetCalendarEvents)
}
@@ -68,7 +89,7 @@ func (g *Groupware) CreateEvent(w http.ResponseWriter, r *http.Request) {
}
func (g *Groupware) DeleteEvent(w http.ResponseWriter, r *http.Request) {
- delete(Event, w, r, g, g.jmap.DeleteCalendarEvent)
+ deleteById(Event, w, r, g, g.jmap.DeleteCalendarEvent)
}
func (g *Groupware) ModifyEvent(w http.ResponseWriter, r *http.Request) {
diff --git a/services/groupware/pkg/groupware/api_identity.go b/services/groupware/pkg/groupware/api_identity.go
index 15d464d170..6592eeb15a 100644
--- a/services/groupware/pkg/groupware/api_identity.go
+++ b/services/groupware/pkg/groupware/api_identity.go
@@ -23,7 +23,7 @@ func (g *Groupware) ModifyIdentity(w http.ResponseWriter, r *http.Request) {
// Delete an identity.
func (g *Groupware) DeleteIdentity(w http.ResponseWriter, r *http.Request) {
- delete(Identity, w, r, g, g.jmap.DeleteIdentity)
+ deleteById(Identity, w, r, g, g.jmap.DeleteIdentity)
}
// Get changes to Identities since a given State
diff --git a/services/groupware/pkg/groupware/api_mailbox.go b/services/groupware/pkg/groupware/api_mailbox.go
index 275c483d04..efa4fa09b0 100644
--- a/services/groupware/pkg/groupware/api_mailbox.go
+++ b/services/groupware/pkg/groupware/api_mailbox.go
@@ -239,7 +239,7 @@ func (g *Groupware) CreateMailbox(w http.ResponseWriter, r *http.Request) {
}
func (g *Groupware) DeleteMailbox(w http.ResponseWriter, r *http.Request) {
- delete(Mailbox, w, r, g, g.jmap.DeleteMailboxes)
+ deleteById(Mailbox, w, r, g, g.jmap.DeleteMailboxes)
}
var mailboxRoleSortOrderScore = map[string]int{
diff --git a/services/groupware/pkg/groupware/error.go b/services/groupware/pkg/groupware/error.go
index c5a009f17a..73462226e1 100644
--- a/services/groupware/pkg/groupware/error.go
+++ b/services/groupware/pkg/groupware/error.go
@@ -805,3 +805,16 @@ func (r *Request) jmapErrorN(accountIds []jmap.AccountId, err error, result jmap
return r.errorN(accountIds, apiError(errorId, ErrorGeneric, withDetail(e.Error())), result.GetDurations())
}
}
+
+func (r *Request) errorResponse(accountId jmap.AccountId, err error, result jmap.ResultMetadata) Response {
+ switch e := err.(type) {
+ case jmap.Error:
+ return r.jmapError(accountId, e, result)
+ case GroupwareError:
+ errorId := r.errorId()
+ return r.error(accountId, apiError(errorId, e), result.GetDurations())
+ default:
+ errorId := r.errorId()
+ return r.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error())), result.GetDurations())
+ }
+}
diff --git a/services/groupware/pkg/groupware/framework.go b/services/groupware/pkg/groupware/framework.go
index 842b05997f..9215fcd9d1 100644
--- a/services/groupware/pkg/groupware/framework.go
+++ b/services/groupware/pkg/groupware/framework.go
@@ -116,12 +116,8 @@ type Groupware struct {
jobsChannel chan Job
// A threadsafe counter to generate the job IDs.
jobCounter atomic.Uint64
-
- 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]
+ // Implementations of suppliers to use to provide Groupware objects.
+ suppliers Suppliers
}
// An error during the Groupware initialization.
@@ -394,36 +390,20 @@ 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]{}
+ var suppliers Suppliers
{
- {
- 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)
+ impls := []Supplier[jmap.Foo]{
+ newJmapAddressBookSupplier(&jmapClient),
+ newJmapContactCardSupplier(&jmapClient),
}
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)
- }
+ impls = append(impls,
+ newMockAddressBookSupplier(),
+ newMockContactCardSupplier(),
+ )
}
+
+ suppliers, err = newSuppliers(impls...)
}
var publicUrl *url.URL
@@ -454,14 +434,10 @@ 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,
- addressBookChangesSuppliers: addressBookChangesSuppliers,
- contactCardQuerySuppliers: contactCardQuerySuppliers,
- contactCardListSuppliers: contactCardListSuppliers,
- contactCardChangesSuppliers: contactCardChangesSuppliers,
+ eventChannel: eventChannel,
+ jobsChannel: jobsChannel,
+ jobCounter: atomic.Uint64{},
+ suppliers: suppliers,
}
for w := 1; w <= workerPoolSize; w++ {
diff --git a/services/groupware/pkg/groupware/objtypes.go b/services/groupware/pkg/groupware/objtypes.go
index 8a51ad64ed..d153cb3cf0 100644
--- a/services/groupware/pkg/groupware/objtypes.go
+++ b/services/groupware/pkg/groupware/objtypes.go
@@ -4,7 +4,7 @@ import (
"github.com/opencloud-eu/opencloud/pkg/jmap"
)
-type ObjectType[T jmap.Foo, CH jmap.Change, CHS jmap.Changes[T]] struct {
+type ObjectType[T jmap.Foo, CH jmap.Change[T], CHS jmap.Changes[T]] struct {
name string
plural string
responseType ResponseObjectType
diff --git a/services/groupware/pkg/groupware/request.go b/services/groupware/pkg/groupware/request.go
index 726ee2cbbc..c5a51e8a6c 100644
--- a/services/groupware/pkg/groupware/request.go
+++ b/services/groupware/pkg/groupware/request.go
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
+ "iter"
"net/http"
"slices"
"strconv"
@@ -239,12 +240,15 @@ func toSupportedQueryParams(params ...string) supportedQueryParams {
var noSupportedQueryParams supportedQueryParams = toSupportedQueryParams()
-func (r *Request) unsupportedQueryParams(accountIds []jmap.AccountId, allowed supportedQueryParams) (bool, Response) {
+func (r *Request) unsupportedQueryParams(accountIds []jmap.AccountId, allowed ...supportedQueryParams) (bool, Response) {
q := r.r.URL.Query()
for n := range q {
- if _, ok := allowed[n]; !ok {
- return true, r.parameterErrorResponseN(accountIds, n, "Unsupported query parameter")
+ for _, a := range allowed {
+ if _, ok := a[n]; ok {
+ return false, Response{}
+ }
}
+ return true, r.parameterErrorResponseN(accountIds, n, "Unsupported query parameter")
}
return false, Response{}
}
@@ -261,6 +265,27 @@ func (r *Request) getStringParam(param string, defaultValue string) (string, boo
return str, true
}
+func (r *Request) getValidStringParam(param string, defaultValue string, possibleValues iter.Seq[string]) (string, bool, *Error) {
+ q := r.r.URL.Query()
+ if !q.Has(param) {
+ return defaultValue, false, nil
+ }
+ str := q.Get(param)
+ if str == "" {
+ return defaultValue, true, nil
+ }
+ for p := range possibleValues {
+ if p == str {
+ return str, true, nil
+ }
+ }
+ msg := fmt.Sprintf("Invalid value for query parameter '%v'", param)
+ return "", true, r.observedParameterError(ErrorInvalidRequestParameter,
+ withDetail(msg),
+ withSource(&ErrorSource{Parameter: param}),
+ )
+}
+
func (r *Request) getMandatoryStringParam(param string) (string, *Error) {
str := ""
q := r.r.URL.Query()
@@ -347,6 +372,28 @@ func (r *Request) parseDateParam(param string) (time.Time, bool, *Error) {
return t, true, nil
}
+func (r *Request) parseUTCDateParam(param string) (jmap.UTCDate, bool, *Error) {
+ q := r.r.URL.Query()
+ if !q.Has(param) {
+ return "", false, nil
+ }
+
+ str := q.Get(param)
+ if str == "" {
+ return "", false, nil
+ }
+
+ if _, err := time.Parse(jmap.UTCDateFormat, str); err != nil {
+ msg := fmt.Sprintf("Invalid UTCDate value for query parameter '%v': '%s': %s", param, log.SafeString(str), err.Error())
+ return "", true, r.observedParameterError(ErrorInvalidRequestParameter,
+ withDetail(msg),
+ withSource(&ErrorSource{Parameter: param}),
+ )
+ } else {
+ return jmap.UTCDate(str), true, nil
+ }
+}
+
func (r *Request) parseBoolParam(param string, defaultValue bool) (bool, bool, *Error) {
q := r.r.URL.Query()
if !q.Has(param) {
diff --git a/services/groupware/pkg/groupware/route.go b/services/groupware/pkg/groupware/route.go
index a873c801ca..3468952c5b 100644
--- a/services/groupware/pkg/groupware/route.go
+++ b/services/groupware/pkg/groupware/route.go
@@ -11,68 +11,77 @@ var (
)
const (
- UriParamAccountId = "accountid" // Identifier of the account
- UriParamMailboxId = "mailboxid" // Identifier of the mailbox
- UriParamEmailId = "emailid" // Identifier of the email
- UriParamIdentityId = "identityid" // Identifier of the identity
- UriParamBlobId = "blobid" // Identifier of theblob
- UriParamStreamId = "stream" // Identifier of the stream
- UriParamAddressBookId = "addressbookid" // Identifier of the address book
- UriParamCalendarId = "calendarid" // Identifier of the calendar
- UriParamTaskListId = "tasklistid" // Identifier of the tasklist
- UriParamContactId = "contactid" // Identifier of the contact
- UriParamEventId = "eventid" // Idenfitier of the event
- UriParamBlobName = "blobname"
- UriParamRole = "role"
- QueryParamMailboxSearchName = "name"
- QueryParamMailboxSearchRole = "role"
- QueryParamMailboxSearchSubscribed = "subscribed"
- QueryParamBlobType = "type"
- QueryParamSince = "since"
- QueryParamMaxChanges = "maxchanges"
- QueryParamMailboxId = "mailbox"
- QueryParamIdentityId = "identity"
- QueryParamMoveFromMailboxId = "move-from"
- QueryParamMoveToMailboxId = "move-to"
- QueryParamNotInMailboxId = "notmailbox"
- QueryParamSearchText = "text"
- QueryParamSearchFrom = "from"
- QueryParamSearchTo = "to"
- QueryParamSearchCc = "cc"
- QueryParamSearchBcc = "bcc"
- QueryParamSearchSubject = "subject"
- QueryParamSearchBody = "body"
- QueryParamSearchBefore = "before"
- QueryParamSearchAfter = "after"
- QueryParamSearchMinSize = "minsize"
- QueryParamSearchMaxSize = "maxsize"
- QueryParamSearchKeyword = "keyword"
- QueryParamSearchMessageId = "messageId"
- QueryParamPosition = "position"
- QueryParamAnchor = "anchor"
- QueryParamAnchorOffset = "offset"
- QueryParamLimit = "limit"
- QueryParamDays = "days"
- QueryParamPartId = "partId"
- QueryParamAttachmentName = "name"
- QueryParamAttachmentBlobId = "blobId"
- QueryParamSeen = "seen"
- QueryParamUndesirable = "undesirable"
- QueryParamMarkAsSeen = "markAsSeen"
- QueryParamSort = "sort"
- QueryParamMailboxes = "mailboxes"
- QueryParamEmails = "emails"
- QueryParamAddressbooks = "addressbooks"
- QueryParamContacts = "contacts"
- QueryParamCalendars = "calendars"
- QueryParamEvents = "events"
- QueryParamQuotas = "quotas"
- QueryParamIdentities = "identities"
- QueryParamEmailSubmissions = "submissions"
- QueryParamId = "id"
- QueryParamCalculateTotal = "total"
- QueryParamNext = "next"
- HeaderParamSince = "if-none-match"
+ UriParamAccountId = "accountid" // Identifier of the account
+ UriParamMailboxId = "mailboxid" // Identifier of the mailbox
+ UriParamEmailId = "emailid" // Identifier of the email
+ UriParamIdentityId = "identityid" // Identifier of the identity
+ UriParamBlobId = "blobid" // Identifier of theblob
+ UriParamStreamId = "stream" // Identifier of the stream
+ UriParamAddressBookId = "addressbookid" // Identifier of the address book
+ UriParamCalendarId = "calendarid" // Identifier of the calendar
+ UriParamTaskListId = "tasklistid" // Identifier of the tasklist
+ UriParamContactId = "contactid" // Identifier of the contact
+ UriParamEventId = "eventid" // Idenfitier of the event
+ UriParamBlobName = "blobname"
+ UriParamRole = "role"
+ QueryParamMailboxSearchName = "name"
+ QueryParamMailboxSearchRole = "role"
+ QueryParamMailboxSearchSubscribed = "subscribed"
+ QueryParamBlobType = "type"
+ QueryParamSince = "since"
+ QueryParamMaxChanges = "maxchanges"
+ QueryParamMailboxId = "mailbox"
+ QueryParamIdentityId = "identity"
+ QueryParamMoveFromMailboxId = "move-from"
+ QueryParamMoveToMailboxId = "move-to"
+ QueryParamNotInMailboxId = "notmailbox"
+ QueryParamSearchText = "text"
+ QueryParamSearchFrom = "from"
+ QueryParamSearchTo = "to"
+ QueryParamSearchCc = "cc"
+ QueryParamSearchBcc = "bcc"
+ QueryParamSearchSubject = "subject"
+ QueryParamSearchBody = "body"
+ QueryParamSearchBefore = "before"
+ QueryParamSearchAfter = "after"
+ QueryParamSearchMinSize = "minsize"
+ QueryParamSearchMaxSize = "maxsize"
+ QueryParamSearchKeyword = "keyword"
+ QueryParamSearchMessageId = "messageId"
+ QueryParamPosition = "position"
+ QueryParamAnchor = "anchor"
+ QueryParamAnchorOffset = "offset"
+ QueryParamLimit = "limit"
+ QueryParamDays = "days"
+ QueryParamPartId = "partId"
+ QueryParamAttachmentName = "name"
+ QueryParamAttachmentBlobId = "blobId"
+ QueryParamSeen = "seen"
+ QueryParamUndesirable = "undesirable"
+ QueryParamMarkAsSeen = "markAsSeen"
+ QueryParamSort = "sort"
+ QueryParamMailboxes = "mailboxes"
+ QueryParamEmails = "emails"
+ QueryParamAddressbooks = "addressbooks"
+ QueryParamContacts = "contacts"
+ QueryParamCalendars = "calendars"
+ QueryParamEvents = "events"
+ QueryParamQuotas = "quotas"
+ QueryParamIdentities = "identities"
+ QueryParamEmailSubmissions = "submissions"
+ QueryParamId = "id"
+ QueryParamCalculateTotal = "total"
+ QueryParamNext = "next"
+ QueryParamContactFilterUid = "uid"
+ QueryParamContactFilterAddress = "address"
+ QueryParamContactFilterCreatedAfter = "after"
+ QueryParamContactFilterCreatedBefore = "before"
+ QueryParamContactFilterEmail = "email"
+ QueryParamContactFilterMember = "member"
+ QueryParamContactFilterKind = "kind"
+ QueryParamContactFilterName = "name"
+ QueryParamEventsFilterUid = "uid"
+ HeaderParamSince = "if-none-match"
)
func (g *Groupware) Route(r chi.Router) {
diff --git a/services/groupware/pkg/groupware/supplier_addressbook_jmap.go b/services/groupware/pkg/groupware/supplier_addressbook_jmap.go
index 539ce6db64..36603104fa 100644
--- a/services/groupware/pkg/groupware/supplier_addressbook_jmap.go
+++ b/services/groupware/pkg/groupware/supplier_addressbook_jmap.go
@@ -11,7 +11,8 @@ type JmapAddressBookSupplier struct {
}
var _ ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse] = &JmapAddressBookSupplier{}
-var _ ChangesSupplier[jmap.ContactCard, jmap.ContactCardChanges] = &JmapContactCardSupplier{}
+var _ ChangesSupplier[jmap.AddressBook, jmap.AddressBookChanges] = &JmapAddressBookSupplier{}
+var _ CreateSupplier[jmap.AddressBook, jmap.AddressBookChange] = &JmapAddressBookSupplier{}
func newJmapAddressBookSupplier(client *jmap.Client) *JmapAddressBookSupplier {
return &JmapAddressBookSupplier{client: client}
@@ -34,3 +35,11 @@ func (c *JmapAddressBookSupplier) GetAll(accountId jmap.AccountId, ids []string,
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)
}
+
+func (c *JmapAddressBookSupplier) CanCreate(accountId jmap.AccountId, create jmap.AddressBookChange, ctx jmap.Context) bool {
+ return true
+}
+
+func (c *JmapAddressBookSupplier) Create(accountId jmap.AccountId, create jmap.AddressBookChange, ctx jmap.Context) (jmap.Result[*jmap.AddressBook], error) {
+ return c.client.CreateAddressBook(accountId, create, ctx)
+}
diff --git a/services/groupware/pkg/groupware/supplier_contactcard_jmap.go b/services/groupware/pkg/groupware/supplier_contactcard_jmap.go
index 963f80498d..39eef26d64 100644
--- a/services/groupware/pkg/groupware/supplier_contactcard_jmap.go
+++ b/services/groupware/pkg/groupware/supplier_contactcard_jmap.go
@@ -13,6 +13,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{}
+var _ CreateSupplier[jmap.ContactCard, jmap.ContactCardChange] = &JmapContactCardSupplier{}
func newJmapContactCardSupplier(client *jmap.Client) *JmapContactCardSupplier {
return &JmapContactCardSupplier{client: client}
@@ -43,3 +44,11 @@ func (c *JmapContactCardSupplier) GetAll(accountId jmap.AccountId, ids []string,
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)
}
+
+func (c *JmapContactCardSupplier) CanCreate(accountId jmap.AccountId, create jmap.ContactCardChange, ctx jmap.Context) bool {
+ return true
+}
+
+func (c *JmapContactCardSupplier) Create(accountId jmap.AccountId, create jmap.ContactCardChange, ctx jmap.Context) (jmap.Result[*jmap.ContactCard], error) {
+ return c.client.CreateContactCard(accountId, create, ctx)
+}
diff --git a/services/groupware/pkg/groupware/supplier_contactcard_mock.go b/services/groupware/pkg/groupware/supplier_contactcard_mock.go
index 1a77fe329e..c0ba8f6cfe 100644
--- a/services/groupware/pkg/groupware/supplier_contactcard_mock.go
+++ b/services/groupware/pkg/groupware/supplier_contactcard_mock.go
@@ -78,7 +78,35 @@ func (c *MockContactCardSupplier) GetAll(accountId jmap.AccountId, ids []string,
}
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)
+
+ hits := []jmap.ContactCard{}
+ switch cond := filter.(type) {
+ case jmap.ContactCardFilterCondition:
+ for _, h := range c.contacts {
+ if cond.Name != "" {
+ for _, comp := range h.Name.Components {
+ if strings.Contains(comp.Value, cond.Name) {
+ hits = append(hits, h)
+ continue
+ }
+ }
+ continue
+ }
+ if cond.Email != "" {
+ for _, email := range h.Emails {
+ if strings.Contains(email.Address, cond.Email) {
+ hits = append(hits, h)
+ continue
+ }
+ }
+ continue
+ }
+ // TODO add more filter conditions
+ hits = append(hits, h)
+ }
+ }
+
+ total := len(hits)
for _, accountId := range accountIds {
all := []jmap.ContactCard{}
var qp jmap.QueryParams
@@ -92,7 +120,7 @@ func (c *MockContactCardSupplier) Query(accountIds []jmap.AccountId, qps QueryPa
p := uint(qp.Position)
if qp.Position < total {
- all = c.contacts[qp.Position:]
+ all = hits[qp.Position:]
}
if qp.Anchor != "" {
a := slices.IndexFunc(all, func(e jmap.ContactCard) bool { return e.Id == qp.Anchor })
diff --git a/services/groupware/pkg/groupware/suppliers.go b/services/groupware/pkg/groupware/suppliers.go
index 04f41d2888..cdd0175809 100644
--- a/services/groupware/pkg/groupware/suppliers.go
+++ b/services/groupware/pkg/groupware/suppliers.go
@@ -38,7 +38,134 @@ type ChangesSupplier[T jmap.Foo, C jmap.Changes[T]] interface {
Supplier[T]
}
-// queryFunc func(req Request, accountIds []string, qps QueryParamsSupplier, limit *uint, filter FILTER, sortBy []COMP, ctx jmap.Context) (jmap.Result[SEARCHRESULTS], NextToken, error),
+type CreateSupplier[T jmap.Foo, C jmap.Change[T]] interface {
+ CanCreate(accountId jmap.AccountId, create C, ctx jmap.Context) bool
+ Create(accountId jmap.AccountId, create C, ctx jmap.Context) (jmap.Result[*T], error)
+ Supplier[T]
+}
+
+type UpdateSupplier[T jmap.Foo, C jmap.Change[T]] interface {
+ Update(accountId jmap.AccountId, id string, change C, ctx jmap.Context) (jmap.Result[T], error)
+ Supplier[T]
+}
+
+type DeleteSupplier[T jmap.Foo] interface {
+ Delete(accountId jmap.AccountId, destroyIds []string, ctx jmap.Context) (jmap.Result[map[string]jmap.SetError], error)
+ Supplier[T]
+}
+
+type containerSuppliers[
+ T jmap.Foo,
+ G jmap.GetResponse[T],
+ C jmap.Change[T],
+ D jmap.Changes[T],
+] struct {
+ create []CreateSupplier[T, C]
+ update []UpdateSupplier[T, C]
+ delete []DeleteSupplier[T]
+ list []ListSupplier[T, G]
+ changes []ChangesSupplier[T, D]
+}
+
+type containedSuppliers[
+ T jmap.Foo,
+ G jmap.GetResponse[T],
+ C jmap.Change[T],
+ D jmap.Changes[T],
+ S jmap.SearchResults[T],
+ F jmap.FilterElement[T],
+ O jmap.Comparator[T],
+] struct {
+ create []CreateSupplier[T, C]
+ update []UpdateSupplier[T, C]
+ delete []DeleteSupplier[T]
+ query []QuerySupplier[T, S, F, O]
+ list []ListSupplier[T, G]
+ changes []ChangesSupplier[T, D]
+}
+
+type Suppliers struct {
+ addressBooks containerSuppliers[jmap.AddressBook, jmap.AddressBookGetResponse, jmap.AddressBookChange, jmap.AddressBookChanges]
+ contactCards containedSuppliers[jmap.ContactCard, jmap.ContactCardGetResponse, jmap.ContactCardChange, jmap.ContactCardChanges, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator]
+}
+
+func newSuppliers(suppliers ...Supplier[jmap.Foo]) (Suppliers, error) {
+ result := Suppliers{
+ addressBooks: containerSuppliers[jmap.AddressBook, jmap.AddressBookGetResponse, jmap.AddressBookChange, jmap.AddressBookChanges]{},
+ contactCards: containedSuppliers[jmap.ContactCard, jmap.ContactCardGetResponse, jmap.ContactCardChange, jmap.ContactCardChanges, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator]{},
+ }
+ unsupported := []Supplier[jmap.Foo]{}
+
+ for _, s := range suppliers {
+ hits := 0
+ switch s := s.(type) {
+ case CreateSupplier[jmap.AddressBook, jmap.AddressBookChange]:
+ result.addressBooks.create = append(result.addressBooks.create, s)
+ hits++
+ }
+ switch s := s.(type) {
+ case UpdateSupplier[jmap.AddressBook, jmap.AddressBookChange]:
+ result.addressBooks.update = append(result.addressBooks.update, s)
+ hits++
+ }
+ switch s := s.(type) {
+ case DeleteSupplier[jmap.AddressBook]:
+ result.addressBooks.delete = append(result.addressBooks.delete, s)
+ hits++
+ }
+ switch s := s.(type) {
+ case ListSupplier[jmap.AddressBook, jmap.AddressBookGetResponse]:
+ result.addressBooks.list = append(result.addressBooks.list, s)
+ hits++
+ }
+ switch s := s.(type) {
+ case ChangesSupplier[jmap.AddressBook, jmap.AddressBookChanges]:
+ result.addressBooks.changes = append(result.addressBooks.changes, s)
+ hits++
+ }
+
+ switch s := s.(type) {
+ case CreateSupplier[jmap.ContactCard, jmap.ContactCardChange]:
+ result.contactCards.create = append(result.contactCards.create, s)
+ hits++
+ }
+ switch s := s.(type) {
+ case UpdateSupplier[jmap.ContactCard, jmap.ContactCardChange]:
+ result.contactCards.update = append(result.contactCards.update, s)
+ hits++
+ }
+ switch s := s.(type) {
+ case DeleteSupplier[jmap.ContactCard]:
+ result.contactCards.delete = append(result.contactCards.delete, s)
+ hits++
+ }
+ switch s := s.(type) {
+ case QuerySupplier[jmap.ContactCard, *jmap.ContactCardSearchResults, jmap.ContactCardFilterElement, jmap.ContactCardComparator]:
+ result.contactCards.query = append(result.contactCards.query, s)
+ hits++
+ }
+ switch s := s.(type) {
+ case ListSupplier[jmap.ContactCard, jmap.ContactCardGetResponse]:
+ result.contactCards.list = append(result.contactCards.list, s)
+ hits++
+ }
+ switch s := s.(type) {
+ case ChangesSupplier[jmap.ContactCard, jmap.ContactCardChanges]:
+ result.contactCards.changes = append(result.contactCards.changes, s)
+ hits++
+ }
+
+ if hits < 1 {
+ unsupported = append(unsupported, s)
+ }
+ }
+ if len(unsupported) > 0 {
+ return result, fmt.Errorf("%d unsupported suppliers: %v", len(unsupported), unsupported)
+ } else {
+ return result, nil
+ }
+}
+
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),
) func(req Request, accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, filter FILTER, sortBy []COMP, ctx jmap.Context) (jmap.Result[SRES], NextToken, error) {
@@ -56,7 +183,7 @@ func agg[T jmap.Idable, R jmap.GetResponse[T]](accountId jmap.AccountId, supplie
ctor func(accountId jmap.AccountId, state jmap.State, notFound []string, list []T) R) (R, error) {
if len(responses) < 1 {
var zero R
- return zero, fmt.Errorf("requires at least one response")
+ return zero, fmt.Errorf("requires at least one response") //NOSONAR
}
lists := structs.Concat(structs.Map(responses, func(e *R) []T {
if e != nil {
@@ -173,10 +300,25 @@ func aggChanges[T jmap.Idable, R jmap.Changes[T]](accountId jmap.AccountId, supp
} else {
return false
}
- }), func(b bool) bool { return b })
+ }), identity)
return ctor(accountId, oldState, newState, created, updated, destroyed, hasMoreChanges), nil
}
+func aggDeletes(responses []*map[string]jmap.SetError) (map[string]jmap.SetError, error) { //NOSONAR
+ if len(responses) < 1 {
+ return nil, fmt.Errorf("requires at least one response")
+ }
+ payload := map[string]jmap.SetError{}
+ for _, r := range responses {
+ if r != nil {
+ for k, v := range *r {
+ payload[k] = v
+ }
+ }
+ }
+ return payload, 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) {
@@ -205,40 +347,9 @@ func slist[T jmap.Idable, G jmap.GetResponse[T], S ListSupplier[T, G]](suppliers
}
}
- return smeld[T](supplierIds, results, func(payloads []*G) (G, error) {
+ return smeld(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
- })
- */
+ }, G.GetState)
}
}
@@ -271,45 +382,18 @@ func schanges[T jmap.Idable, C jmap.Changes[T], S ChangesSupplier[T, C]](supplie
}
}
- // TODO need to reduce the maxChanges? this is quite complex to implement, if even possible, not doing that for now
+ // TODO need to reduce the maxChanges? this is quite complex to implement, if even possible at all, not doing that for now
+ // which means that a request with multiple suppliers with maxChanges=10 can yield a lot more changes than that, but that is
+ // a limitation we have to live with, at least for now, but probably forever, since we don't have the state for each change
+ // and if we cap the changes from a supplier, we do not know which state to use to resume from there
- return smeld[T](supplierIds, results, func(payloads []*C) (C, error) {
+ return smeld(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
- })
- */
+ }, C.GetNewState)
}
}
-func smeld[T jmap.Idable, C any](
+func smeld[C any](
supplierIds []SupplierId,
results []*jmap.Result[C],
aggregator func(payloads []*C) (C, error),
@@ -342,6 +426,87 @@ func smeld[T jmap.Idable, C any](
})
}
+func screate[T jmap.Idable, C jmap.Change[T], S CreateSupplier[T, C]](suppliers []S, accountId jmap.AccountId, create C, ctx jmap.Context) (jmap.Result[*T], error) {
+ candidates := structs.Filter(suppliers, func(s S) bool { return s.CanCreate(accountId, create, ctx) })
+ switch len(candidates) {
+ case 0:
+ return jmap.ZeroResultV[*T](), fmt.Errorf("TODO found no suppliers to create, must return 5XX Not Implemented") // TODO groupware error for this
+ case 1:
+ return candidates[0].Create(accountId, create, ctx)
+ default:
+ // found more than one Supplier that would be capable of creating this object
+ // there are a few ways to deal with this, but in order to be deal with this in a
+ // predictable way, we will return an error
+ return jmap.ZeroResultV[*T](), fmt.Errorf("TODO found multiple suppliers to create, must return 5XX Conflict") // TODO groupware error for this
+ }
+}
+
+func supdate[T jmap.Idable, C jmap.Change[T], S UpdateSupplier[T, C]](suppliers []S, accountId jmap.AccountId, id string, change C, ctx jmap.Context) (jmap.Result[T], error) {
+ candidates := structs.Filter(suppliers, func(s S) bool { return s.IsMine(id) })
+ switch len(candidates) {
+ case 0:
+ return jmap.ZeroResultV[T](), fmt.Errorf("TODO found no suppliers to update, must return 5XX Not Implemented") // TODO groupware error for this
+ case 1:
+ return candidates[0].Update(accountId, id, change, ctx)
+ default:
+ // found more than one Supplier that would be capable of creating this object
+ // there are a few ways to deal with this, but in order to be deal with this in a
+ // predictable way, we will return an error
+ return jmap.ZeroResultV[T](), fmt.Errorf("TODO found multiple suppliers to update, must return 5XX Conflict") // TODO groupware error for this
+ }
+}
+
+func sdelete[T jmap.Idable, S DeleteSupplier[T]](suppliers []S, accountId jmap.AccountId, destroyIds []string, ctx jmap.Context) (jmap.Result[map[string]jmap.SetError], error) {
+ idsBySupplier := structs.Distribute(destroyIds, func(id string) SupplierId {
+ for _, s := range suppliers {
+ if s.IsMine(id) {
+ return s.GetId()
+ }
+ }
+ return EmptySupplierId
+ })
+
+ result := map[string]jmap.SetError{}
+ if ids, ok := idsBySupplier[EmptySupplierId]; ok {
+ for _, id := range ids {
+ result[id] = jmap.SetError{
+ Type: jmap.SetErrorTypeNotFound,
+ Description: "no supplier supports that id",
+ }
+ }
+ }
+ delete(idsBySupplier, EmptySupplierId)
+
+ switch len(idsBySupplier) {
+ case 0:
+ return jmap.NewResult(result, ctx.Session.GetSessionState(), jmap.EmptyState, jmap.NoLanguage, nil), nil
+ default:
+ suppliersById := structs.Index(suppliers, S.GetId)
+ results := []*jmap.Result[map[string]jmap.SetError]{}
+ stateBySupplierId := map[SupplierId]jmap.State{}
+ for supplierId, ids := range idsBySupplier {
+ if supplier, ok := suppliersById[supplierId]; ok {
+ if r, err := supplier.Delete(accountId, ids, ctx); err != nil {
+ return jmap.ZeroResult[map[string]jmap.SetError](nil), err
+ } else {
+ results = append(results, &r)
+ stateBySupplierId[supplierId] = r.GetState()
+ }
+ }
+ }
+ if len(results) == 1 {
+ return *results[0], nil
+ } else {
+ supplierIds := structs.Keys(suppliersById)
+ state, err := combineState(stateBySupplierId)
+ if err != nil {
+ return jmap.ZeroResultV[map[string]jmap.SetError](), err
+ }
+ return smeld(supplierIds, results, aggDeletes, func(m map[string]jmap.SetError) jmap.State { return state })
+ }
+ }
+}
+
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 {
diff --git a/services/groupware/pkg/groupware/templates.go b/services/groupware/pkg/groupware/templates.go
index 7c87ce46c2..76e82b8148 100644
--- a/services/groupware/pkg/groupware/templates.go
+++ b/services/groupware/pkg/groupware/templates.go
@@ -10,7 +10,7 @@ import (
// Create a new {{.Name}} using the JSON payload in the body of the `{{.Verb}}` operation.
// @api:response 200:T returns the {{.Name}} that was just created
// @api:body CHANGE the {{.Name}} to create
-func create[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR
+func create[T jmap.Foo, CHANGE jmap.Change[T], CHANGES jmap.Changes[T]]( //NOSONAR
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
@@ -62,7 +62,7 @@ func create[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR
// Retrieve all the {{.Name}}.
// @api:response 200:[]T returns all the {{.Names}}
-func getall[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.GetResponse[T]]( //NOSONAR
+func getall[T jmap.Foo, CHANGE jmap.Change[T], CHANGES jmap.Changes[T], RESP jmap.GetResponse[T]]( //NOSONAR
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
@@ -105,12 +105,13 @@ var noQueryParams = toSupportedQueryParams()
// Retrieve all the {{.Name}} with support for paging using the {{.QueryParam.QueryParamPosition.Name}} and {{.QueryParam.QueryParamLimit.Name}} query parameters.
// @api:response 200:SEARCHRESULTS returns the {{.Names}} within the requested range, as well as the total amount of {{.Names}}
-func getallpaged[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], FILTER any, COMP any, SEARCHRESULTS jmap.SearchResults[T]]( //NOSONAR
+func getallpaged[T jmap.Foo, CHANGE jmap.Change[T], CHANGES jmap.Changes[T], FILTER any, COMP any, SEARCHRESULTS jmap.SearchResults[T]]( //NOSONAR
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
withContainerId bool,
- filterFunc func(containerId string) FILTER,
+ filterFunc func(containerId string, req Request, logger *log.Logger) (FILTER, *Error),
+ supportedFilterParams supportedQueryParams,
sortBy []COMP,
queryFunc func(req Request, accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, filter FILTER, sortBy []COMP, ctx jmap.Context) (jmap.Result[SEARCHRESULTS], NextToken, error),
) {
@@ -160,25 +161,19 @@ func getallpaged[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], FILTER
l = l.Str(o.containerUriParamName, log.SafeString(containerId))
}
- if notok, resp := req.unsupportedQueryParams(single(accountId), supportedQueryParams); notok {
+ if notok, resp := req.unsupportedQueryParams(single(accountId), supportedQueryParams, supportedFilterParams); notok {
return resp
}
- filter := filterFunc(containerId)
-
logger := log.From(l)
+ filter, err := filterFunc(containerId, req, logger)
+ if err != nil {
+ return req.errorV(accountId, err)
+ }
+
ctx := req.ctx.WithLogger(logger)
if result, nextNextToken, err := queryFunc(req, single(accountId), qps, limit, filter, sortBy, ctx); err != nil {
- switch e := err.(type) {
- case jmap.Error:
- return req.jmapError(accountId, e, result)
- case GroupwareError:
- errorId := req.errorId()
- return req.error(accountId, apiError(errorId, e), result.GetDurations())
- default:
- errorId := req.errorId()
- return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error())), result.GetDurations())
- }
+ return req.errorResponse(accountId, err, result)
} else {
return req.respondNext(accountId, result.Payload, o.responseType, result, nextNextToken)
}
@@ -187,7 +182,7 @@ func getallpaged[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], FILTER
// Query all the {{.Name}} with support for paging using the {{.QueryParam.QueryParamPosition.Name}} and {{.QueryParam.QueryParamLimit.Name}} query parameters.
// @api:response 200:SEARCHRESULTS returns the {{.Names}} that match the filter, within the requested range, as well as the total amount of matches
-func query[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], SEARCHRESULTS jmap.SearchResults[T]]( //NOSONAR
+func query[T jmap.Foo, CHANGE jmap.Change[T], CHANGES jmap.Changes[T], SEARCHRESULTS jmap.SearchResults[T]]( //NOSONAR
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
@@ -334,7 +329,7 @@ func query[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], SEARCHRESULT
// Retrieve a specific {{.Name}} referenced by its unique identifier as specified in the path parameter `{{.UriParamName}}` in the path `{{.Path}}`
// @api:response 200:T returns the {{.Name}} that matches the requested identifier, if it exists
// @api:response 404 when there is no {{.Name}} for the requested identifier
-func get[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.GetResponse[T]]( //NOSONAR
+func get[T jmap.Foo, CHANGE jmap.Change[T], CHANGES jmap.Changes[T], RESP jmap.GetResponse[T]]( //NOSONAR
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
@@ -391,7 +386,7 @@ func get[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.GetR
// Retrieve a specific {{.Name}} referenced by its unique identifier as specified in the path parameter `{{.UriParamName}}` in the path `{{.Path}}`
// @api:response 200:T returns the {{.Name}} that matches the requested identifier, if it exists
// @api:response 404 when there is no {{.Name}} for the requested identifier
-func getFromMap[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.GetResponse[T]]( //NOSONAR
+func getFromMap[T jmap.Foo, CHANGE jmap.Change[T], CHANGES jmap.Changes[T], RESP jmap.GetResponse[T]]( //NOSONAR
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
@@ -450,7 +445,7 @@ var changesSupportedQueryParams = toSupportedQueryParams(QueryParamMaxChanges)
// Retrieve the changes that occured for {{.Name}}, optionally since an opaque state specified using the header `{{.HeaderParam.HeaderParamSince}}`,
// optionally bounded by the query parameter `{{.QueryParam.QueryParamMaxChanges}}`.
// @api:response 200:CHANGES returns the changes to {{.Names}}: created, updated, and identifiers of destroyed {{.Names}}
-func changes[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR
+func changes[T jmap.Foo, CHANGE jmap.Change[T], CHANGES jmap.Changes[T]]( //NOSONAR
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
@@ -503,7 +498,7 @@ func changes[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR
// @api:success 204
// @api:response 204 when the referenced {{.Name}} has been deleted successfully
// @api:response 404 when there is no {{.Name}} for the requested identifier
-func delete[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR
+func deleteById[T jmap.Foo, CHANGE jmap.Change[T], CHANGES jmap.Changes[T]]( //NOSONAR
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
@@ -565,7 +560,7 @@ var deleteManySupportedQueryParams = toSupportedQueryParams(QueryParamId)
// or using the query parameter `{{.QueryParam.QueryParamId}}`.
// @api:response 204 when the referenced {{.Names}} have all been deleted successfully
// @api:body ?[]string an array of identifiers of {{.Names}} to delete
-func deleteMany[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR
+func deleteMany[T jmap.Foo, CHANGE jmap.Change[T], CHANGES jmap.Changes[T]]( //NOSONAR
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
@@ -654,7 +649,7 @@ func deleteMany[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSO
// Modify the specified {{.Name}} referenced its unique identifier, changes to attributes being specified as a JSON map in the request body.
// @api:response 200:T the modified {{.Name}}
-func modify[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]](
+func modify[T jmap.Foo, CHANGE jmap.Change[T], CHANGES jmap.Changes[T]](
o ObjectType[T, CHANGE, CHANGES],
w http.ResponseWriter, r *http.Request,
g *Groupware,
diff --git a/services/groupware/pkg/groupware/tools.go b/services/groupware/pkg/groupware/tools.go
index 08416d4a7c..bd728b4175 100644
--- a/services/groupware/pkg/groupware/tools.go
+++ b/services/groupware/pkg/groupware/tools.go
@@ -41,3 +41,17 @@ func ptrIfNot[T comparable](t T, notThis T) *T {
return nil
}
}
+
+func identity[T any | uint | int | bool](value T) T {
+ return value
+}
+
+func striter[T ~string](s []T) iter.Seq[string] {
+ return func(yield func(string) bool) {
+ for _, v := range s {
+ if !yield(string(v)) {
+ return
+ }
+ }
+ }
+}