mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-07-13 00:52:01 -04:00
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
This commit is contained in:
@@ -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{}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"`
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++ {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user