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