diff --git a/.vscode/launch.json b/.vscode/launch.json index 79536d1768..c4bc55c218 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -143,6 +143,8 @@ "GROUPWARE_JMAP_MASTER_USERNAME": "master", "GROUPWARE_JMAP_MASTER_PASSWORD": "admin", + "GROUPWARE_SEND_DURATIONS_RESPONSE": "true", + "AUTHAPI_HTTP_ADDR": "0.0.0.0:10000", "AUTHAPI_AUTH_REQUIRE_SHARED_SECRET": "true", "AUTHAPI_AUTH_SHARED_SECRETS": "stalwart=maethaR9eiXaiph8ahn8ohH6dahPiequ;unused=eeyaigh6hae1zo5ahGeete6oohaiquei", diff --git a/pkg/jmap/api.go b/pkg/jmap/api.go index b14aa3cac5..a370390f9b 100644 --- a/pkg/jmap/api.go +++ b/pkg/jmap/api.go @@ -4,6 +4,7 @@ import ( "context" "io" "net/url" + "time" "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/structs" @@ -68,6 +69,7 @@ type ResultMetadata interface { GetSessionState() SessionState GetState() State GetLanguage() Language + GetDurations() []time.Duration } type Result[T any] struct { @@ -75,21 +77,22 @@ type Result[T any] struct { SessionState SessionState State State Language Language + Durations []time.Duration } func RefineResultPayload[A, B any](a Result[A], refiner func(A) (B, bool, error)) (Result[B], error) { if payloads, ok, err := refiner(a.Payload); err != nil { - return ZeroResult[B](), err + return ZeroResult[B](a.Durations), err } else if ok { - return newResult(payloads, a.SessionState, a.State, a.Language), nil + return NewResult(payloads, a.SessionState, a.State, a.Language, a.Durations), nil } else { - return ZeroResult[B](), nil + return ZeroResult[B](a.Durations), nil } } func RefineResult[A, B any](a Result[A], refiner func(A, SessionState, State, Language) (B, SessionState, State, Language)) Result[B] { b, bss, bs, bl := refiner(a.Payload, a.SessionState, a.State, a.Language) - return newResult(b, bss, bs, bl) + return NewResult(b, bss, bs, bl, a.Durations) } func RefineResultSlice[A, B any](a []*Result[A], refiner func([]*A, []*SessionState, []*State, []*Language) (B, SessionState, State, Language, error)) (Result[B], error) { @@ -121,38 +124,52 @@ func RefineResultSlice[A, B any](a []*Result[A], refiner func([]*A, []*SessionSt return nil } }) + durations := structs.Flatten(structs.Map(a, func(e *Result[A]) []time.Duration { + return e.Durations + })) b, bss, bs, bl, err := refiner(payloads, sessionStates, states, languages) - return newResult(b, bss, bs, bl), err + return NewResult(b, bss, bs, bl, durations), err } func (r Result[T]) GetSessionState() SessionState { return r.SessionState } - func (r Result[T]) GetState() State { return r.State } - func (r Result[T]) GetLanguage() Language { return r.Language } +func (r Result[T]) GetDurations() []time.Duration { + return r.Durations +} -func newResult[T any](result T, sessionState SessionState, state State, language Language) Result[T] { +func NewResult[T any](payload T, sessionState SessionState, state State, language Language, durations []time.Duration) Result[T] { return Result[T]{ - Payload: result, + Payload: payload, SessionState: sessionState, State: state, Language: language, + Durations: durations, } } -func newPartialResult[T any](sessionState SessionState, language Language) Result[T] { +func newPartialResult[T any](sessionState SessionState, language Language, durations []time.Duration) Result[T] { return Result[T]{ SessionState: sessionState, Language: language, + Durations: durations, } } -func ZeroResult[T any]() Result[T] { - return Result[T]{} +func ZeroResult[T any](durations []time.Duration) Result[T] { + return Result[T]{Durations: durations} +} + +func ZeroResultV[T any]() Result[T] { + return Result[T]{Durations: nil} +} + +func ZeroResultM[T any](t Result[T]) Result[T] { + return Result[T]{Durations: t.GetDurations()} } diff --git a/pkg/jmap/api_blob.go b/pkg/jmap/api_blob.go index 7cf1037da1..87be34a1ae 100644 --- a/pkg/jmap/api_blob.go +++ b/pkg/jmap/api_blob.go @@ -21,7 +21,7 @@ func (j *Client) GetBlobMetadata(accountId AccountId, ids []string, ctx Context) invocation(get, "0"), ) if jerr != nil { - return ZeroResult[BlobGetResponse](), jerr + return ZeroResultV[BlobGetResponse](), jerr } return command(j, ctx, cmd, func(body *Response) (BlobGetResponse, State, Error) { @@ -92,7 +92,7 @@ func (j *Client) UploadBlob(accountId AccountId, data []byte, contentType string invocation(getHash, "1"), ) if jerr != nil { - return ZeroResult[UploadedBlobWithHash](), jerr + return ZeroResultV[UploadedBlobWithHash](), jerr } return command(j, ctx, cmd, func(body *Response) (UploadedBlobWithHash, State, Error) { diff --git a/pkg/jmap/api_bootstrap.go b/pkg/jmap/api_bootstrap.go index cf2c69af64..b8d65be06c 100644 --- a/pkg/jmap/api_bootstrap.go +++ b/pkg/jmap/api_bootstrap.go @@ -25,7 +25,7 @@ func (j *Client) GetBootstrap(accountIds []AccountId, ctx Context) (Result[map[A cmd, err := j.request(ctx, NS_MAIL_QUOTA, calls...) if err != nil { - return ZeroResult[map[AccountId]AccountBootstrapResult](), err + return ZeroResultV[map[AccountId]AccountBootstrapResult](), err } return command(j, ctx, cmd, func(body *Response) (map[AccountId]AccountBootstrapResult, State, Error) { identityPerAccount := map[AccountId][]Identity{} diff --git a/pkg/jmap/api_calendar.go b/pkg/jmap/api_calendar.go index 774541dd46..6516231a8b 100644 --- a/pkg/jmap/api_calendar.go +++ b/pkg/jmap/api_calendar.go @@ -10,7 +10,7 @@ func (j *Client) ParseICalendarBlob(accountId AccountId, blobIds []string, ctx C invocation(parse, "0"), ) if err != nil { - return ZeroResult[CalendarEventParseResponse](), err + return ZeroResultV[CalendarEventParseResponse](), err } return command(j, ctx, cmd, func(body *Response) (CalendarEventParseResponse, State, Error) { diff --git a/pkg/jmap/api_changes.go b/pkg/jmap/api_changes.go index df54d549e3..7e868f0751 100644 --- a/pkg/jmap/api_changes.go +++ b/pkg/jmap/api_changes.go @@ -107,7 +107,7 @@ func (j *Client) GetChanges(accountId AccountId, stateMap StateMap, maxChanges u cmd, err := j.request(ctx, NS_CHANGES, methodCalls...) if err != nil { - return ZeroResult[ObjectChanges](), err + return ZeroResultV[ObjectChanges](), err } return command(j, ctx, cmd, func(body *Response) (ObjectChanges, State, Error) { diff --git a/pkg/jmap/api_email.go b/pkg/jmap/api_email.go index 7eca1a0cab..085bd9ada8 100644 --- a/pkg/jmap/api_email.go +++ b/pkg/jmap/api_email.go @@ -51,7 +51,7 @@ func (j *Client) GetEmails(accountId AccountId, ids []string, //NOSONAR cmd, err := j.request(ctx, NS_MAIL, methodCalls...) if err != nil { - return ZeroResult[EmailGetResponse](), err + return ZeroResultV[EmailGetResponse](), err } return command(j, ctx, cmd, func(body *Response) (EmailGetResponse, State, Error) { if markAsSeen { @@ -89,7 +89,7 @@ func (j *Client) GetEmailBlobId(accountId AccountId, id string, ctx Context) (Re get := EmailGetCommand{AccountId: accountId, Ids: []string{id}, FetchAllBodyValues: false, Properties: []string{"blobId"}} cmd, err := j.request(ctx, NS_MAIL, invocation(get, "0")) if err != nil { - return ZeroResult[string](), err + return ZeroResultV[string](), err } return command(j, ctx, cmd, func(body *Response) (string, State, Error) { var response EmailGetResponse @@ -172,7 +172,7 @@ func (j *Client) GetAllEmailsInMailbox(accountId AccountId, mailboxId string, // cmd, err := j.request(ctx, NS_MAIL, invocations...) if err != nil { - return ZeroResult[*EmailSearchResults](), err + return ZeroResultV[*EmailSearchResults](), err } return command(j, ctx, cmd, func(body *Response) (*EmailSearchResults, State, Error) { @@ -259,7 +259,7 @@ func (j *Client) GetEmailChanges(accountId AccountId, invocation(getUpdated, "2"), ) if err != nil { - return ZeroResult[EmailChanges](), err + return ZeroResultV[EmailChanges](), err } return command(j, ctx, cmd, func(body *Response) (EmailChanges, State, Error) { @@ -358,7 +358,7 @@ func (j *Client) QueryEmailSnippets(accountIds []AccountId, //NOSONAR cmd, err := j.request(ctx, NS_MAIL, invocations...) if err != nil { - return ZeroResult[map[AccountId]EmailSnippetSearchResults](), err + return ZeroResultV[map[AccountId]EmailSnippetSearchResults](), err } return command(j, ctx, cmd, func(body *Response) (map[AccountId]EmailSnippetSearchResults, State, Error) { @@ -470,7 +470,7 @@ func (j *Client) QueryEmails(accountIds []AccountId, cmd, err := j.request(ctx, NS_MAIL, invocations...) if err != nil { - return ZeroResult[map[AccountId]EmailSearchResults](), err + return ZeroResultV[map[AccountId]EmailSearchResults](), err } return command(j, ctx, cmd, func(body *Response) (map[AccountId]EmailSearchResults, State, Error) { @@ -565,7 +565,7 @@ func (j *Client) QueryEmailsWithSnippets(accountIds []AccountId, //NOSONAR cmd, err := j.request(ctx, NS_MAIL, invocations...) if err != nil { - return ZeroResult[map[AccountId]EmailQueryWithSnippetsResult](), err + return ZeroResultV[map[AccountId]EmailQueryWithSnippetsResult](), err } return command(j, ctx, cmd, func(body *Response) (map[AccountId]EmailQueryWithSnippetsResult, State, Error) { @@ -659,7 +659,7 @@ func (j *Client) ImportEmail(accountId AccountId, data []byte, ctx Context) (Res invocation(getHash, "1"), ) if err != nil { - return ZeroResult[UploadedEmail](), err + return ZeroResultV[UploadedEmail](), err } return command(j, ctx, cmd, func(body *Response) (UploadedEmail, State, Error) { @@ -717,7 +717,7 @@ func (j *Client) CreateEmail(accountId AccountId, email EmailChange, replaceId s invocation(set, "0"), ) if err != nil { - return ZeroResult[*Email](), err + return ZeroResultV[*Email](), err } return command(j, ctx, cmd, func(body *Response) (*Email, State, Error) { @@ -764,7 +764,7 @@ func (j *Client) UpdateEmails(accountId AccountId, updates map[string]PatchObjec } cmd, err := j.request(ctx, NS_MAIL, invocation(set, "0")) if err != nil { - return ZeroResult[map[string]*Email](), err + return ZeroResultV[map[string]*Email](), err } return command(j, ctx, cmd, func(body *Response) (map[string]*Email, State, Error) { @@ -878,7 +878,7 @@ func (j *Client) SubmitEmail(accountId AccountId, identityId string, emailId str invocation(get, "1"), ) if err != nil { - return ZeroResult[EmailSubmission](), err + return ZeroResultV[EmailSubmission](), err } return command(j, ctx, cmd, func(body *Response) (EmailSubmission, State, Error) { @@ -936,7 +936,7 @@ func (j *Client) GetEmailSubmissionStatus(accountId AccountId, submissionIds []s } cmd, err := j.request(ctx, NS_MAIL_SUBMISSION, invocation(get, "0")) if err != nil { - return ZeroResult[EmailSubmissionGetResponse](), err + return ZeroResultV[EmailSubmissionGetResponse](), err } return command(j, ctx, cmd, func(body *Response) (EmailSubmissionGetResponse, State, Error) { @@ -978,7 +978,7 @@ func (j *Client) EmailsInThread(accountId AccountId, threadId string, invocation(get, "1"), ) if err != nil { - return ZeroResult[[]Email](), err + return ZeroResultV[[]Email](), err } return command(j, ctx, cmd, func(body *Response) ([]Email, State, Error) { @@ -1067,7 +1067,7 @@ func (j *Client) QueryEmailSummaries(accountIds []AccountId, //NOSONAR } cmd, err := j.request(ctx, NS_MAIL, invocations...) if err != nil { - return ZeroResult[map[AccountId]EmailsSummary](), err + return ZeroResultV[map[AccountId]EmailsSummary](), err } return command(j, ctx, cmd, func(body *Response) (map[AccountId]EmailsSummary, State, Error) { diff --git a/pkg/jmap/api_identity.go b/pkg/jmap/api_identity.go index d3bf5bda14..a778373c93 100644 --- a/pkg/jmap/api_identity.go +++ b/pkg/jmap/api_identity.go @@ -53,7 +53,7 @@ func (j *Client) GetIdentitiesAndMailboxes(mailboxAccountId AccountId, accountId cmd, err := j.request(ctx, NS_IDENTITY, calls...) if err != nil { - return ZeroResult[IdentitiesAndMailboxesGetResponse](), err + return ZeroResultV[IdentitiesAndMailboxesGetResponse](), err } return command(j, ctx, cmd, func(body *Response) (IdentitiesAndMailboxesGetResponse, State, Error) { identities := make(map[AccountId][]Identity, len(uniqueAccountIds)) diff --git a/pkg/jmap/api_mailbox.go b/pkg/jmap/api_mailbox.go index 48447b74bb..2096da3f4e 100644 --- a/pkg/jmap/api_mailbox.go +++ b/pkg/jmap/api_mailbox.go @@ -52,7 +52,7 @@ func (j *Client) SearchMailboxes(accountIds []AccountId, filter MailboxFilterEle } cmd, err := j.request(ctx, NS_MAILBOX, invocations...) if err != nil { - return ZeroResult[map[AccountId][]Mailbox](), err + return ZeroResultV[map[AccountId][]Mailbox](), err } return command(j, ctx, cmd, func(body *Response) (map[AccountId][]Mailbox, State, Error) { @@ -86,7 +86,7 @@ func (j *Client) SearchMailboxIdsPerRole(accountIds []AccountId, roles []string, } cmd, err := j.request(ctx, NS_MAILBOX, invocations...) if err != nil { - return ZeroResult[map[AccountId]map[string]string](), err + return ZeroResultV[map[AccountId]map[string]string](), err } return command(j, ctx, cmd, func(body *Response) (map[AccountId]map[string]string, State, Error) { @@ -210,7 +210,7 @@ func (j *Client) GetInboxNameForMultipleAccounts(accountIds []AccountId, ctx Con uniqueAccountIds := structs.Uniq(accountIds) n := len(uniqueAccountIds) if n < 1 { - return ZeroResult[map[AccountId]string](), nil + return ZeroResultV[map[AccountId]string](), nil } invocations := make([]Invocation, n*2) @@ -225,7 +225,7 @@ func (j *Client) GetInboxNameForMultipleAccounts(accountIds []AccountId, ctx Con cmd, err := j.request(ctx, NS_MAILBOX, invocations...) if err != nil { - return ZeroResult[map[AccountId]string](), err + return ZeroResultV[map[AccountId]string](), err } return command(j, ctx, cmd, func(body *Response) (map[AccountId]string, State, Error) { diff --git a/pkg/jmap/api_objects.go b/pkg/jmap/api_objects.go index 14820a84a2..d08b1beaac 100644 --- a/pkg/jmap/api_objects.go +++ b/pkg/jmap/api_objects.go @@ -90,7 +90,7 @@ func (j *Client) GetObjects(accountId AccountId, //NOSONAR cmd, err := j.request(ctx, NS_OBJECTS, methodCalls...) if err != nil { - return ZeroResult[Objects](), err + return ZeroResultV[Objects](), err } return command(j, ctx, cmd, func(body *Response) (Objects, State, Error) { diff --git a/pkg/jmap/api_vacation.go b/pkg/jmap/api_vacation.go index eab5d50b78..06df6e07ae 100644 --- a/pkg/jmap/api_vacation.go +++ b/pkg/jmap/api_vacation.go @@ -92,7 +92,7 @@ func (j *Client) SetVacationResponse(accountId AccountId, vacation VacationRespo invocation(get, "1"), ) if err != nil { - return ZeroResult[VacationResponse](), err + return ZeroResultV[VacationResponse](), err } return command(j, ctx, cmd, func(body *Response) (VacationResponse, State, Error) { var setResponse VacationResponseSetResponse diff --git a/pkg/jmap/session.go b/pkg/jmap/session.go index 143841cbf1..bfed93a2a3 100644 --- a/pkg/jmap/session.go +++ b/pkg/jmap/session.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/url" + "time" ) type SessionEventListener interface { @@ -60,6 +61,7 @@ var _ ResultMetadata = Session{} func (s Session) GetSessionState() SessionState { return s.State } func (s Session) GetState() State { return EmptyState } func (s Session) GetLanguage() Language { return NoLanguage } +func (s Session) GetDurations() []time.Duration { return nil } var ( invalidSessionResponseErrorMissingUsername = jmapError(errors.New("JMAP session response does not provide a username"), JmapErrorInvalidSessionResponse) diff --git a/pkg/jmap/templates.go b/pkg/jmap/templates.go index fffeda7ca1..a599241e30 100644 --- a/pkg/jmap/templates.go +++ b/pkg/jmap/templates.go @@ -19,7 +19,7 @@ func get[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], ID any, RESP any]( get := getCommandFactory(accountId, ids) cmd, err := client.request(ctx, objType.Namespaces, invocation(get, "0")) if err != nil { - return ZeroResult[RESP](), err + return ZeroResultV[RESP](), err } return command(client, ctx, cmd, func(body *Response) (RESP, State, Error) { @@ -70,7 +70,7 @@ func getN[T Foo, ITEM any, GETREQ GetCommand[T], GETRESP GetResponse[T], ID any, cmd, err := client.request(ctx, objType.Namespaces, invocations...) if err != nil { - return ZeroResult[RESP](), err + return ZeroResultV[RESP](), err } return command(client, ctx, cmd, func(body *Response) (RESP, State, Error) { @@ -109,7 +109,7 @@ func create[T Foo, C any, SETREQ SetCommand[T], GETREQ GetCommand[T], SETRESP Se invocation(get, "1"), ) if err != nil { - return ZeroResult[*T](), err + return ZeroResultV[*T](), err } return command(client, ctx, cmd, func(body *Response) (*T, State, Error) { @@ -162,7 +162,7 @@ func destroy[T Foo, REQ SetCommand[T], RESP SetResponse[T]](client *Client, name invocation(set, "0"), ) if err != nil { - return ZeroResult[map[string]SetError](), err + return ZeroResultV[map[string]SetError](), err } return command(client, ctx, cmd, func(body *Response) (map[string]SetError, State, Error) { @@ -210,7 +210,7 @@ func changes[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESR invocation(getUpdated, "2"), ) if err != nil { - return ZeroResult[RESP](), err + return ZeroResultV[RESP](), err } return command(client, ctx, cmd, func(body *Response) (RESP, State, Error) { @@ -268,7 +268,7 @@ func changesN[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGES uniqueAccountIds := structs.Uniq(accountIds) n := len(uniqueAccountIds) if n < 1 { - return ZeroResult[RESP](), nil + return ZeroResultV[RESP](), nil } invocations := make([]Invocation, n*3) @@ -299,7 +299,7 @@ func changesN[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGES cmd, err := client.request(ctx, objType.Namespaces, invocations...) if err != nil { - return ZeroResult[RESP](), err + return ZeroResultV[RESP](), err } return command(client, ctx, cmd, func(body *Response) (RESP, State, Error) { @@ -354,7 +354,7 @@ func updates[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESR invocation(getUpdated, "1"), ) if err != nil { - return ZeroResult[RESP](), err + return ZeroResultV[RESP](), err } return command(client, ctx, cmd, func(body *Response) (RESP, State, Error) { @@ -395,14 +395,14 @@ func update[T Foo, CHANGES Change, SET SetCommand[T], GET GetCommand[T], RESP an { patch, err := changes.AsPatch() if err != nil { - return ZeroResult[RESP](), jmapError(err, JmapErrorPatchObjectSerialization) + return ZeroResultV[RESP](), jmapError(err, JmapErrorPatchObjectSerialization) } update = setCommandFactory(map[string]PatchObject{id: patch}) } get := getCommandFactory(id) cmd, err := client.request(ctx, objType.Namespaces, invocation(update, "0"), invocation(get, "1")) if err != nil { - return ZeroResult[RESP](), err + return ZeroResultV[RESP](), err } return command(client, ctx, cmd, func(body *Response) (RESP, State, Error) { @@ -525,7 +525,7 @@ func queryN[T Foo, FILTER any, SORT any, QUERY QueryCommand[T, QUERY], GET GetCo cmd, err := client.request(ctx, objType.Namespaces, invocations...) if err != nil { - return ZeroResult[map[AccountId]*RESP](), err + return ZeroResultV[map[AccountId]*RESP](), err } return command(client, ctx, cmd, func(body *Response) (map[AccountId]*RESP, State, Error) { diff --git a/pkg/jmap/tools.go b/pkg/jmap/tools.go index e00bef9bcc..b993ee2a9d 100644 --- a/pkg/jmap/tools.go +++ b/pkg/jmap/tools.go @@ -14,6 +14,10 @@ import ( "github.com/opencloud-eu/opencloud/pkg/jscalendar" ) +func single[S any](s S) []S { + return []S{s} +} + var UintPtrOne *uint = uintPtr(1) var UintPtrZero *uint = uintPtr(0) @@ -65,18 +69,18 @@ func command[T any](client Cmdr, //NOSONAR logger := ctx.Logger + before := time.Now() responseBody, language, jmapErr := client.Api().Command(request, ctx) + duration := time.Since(before) if jmapErr != nil { - var zero Result[T] - return zero, jmapErr + return ZeroResult[T](single(duration)), jmapErr } var response Response err := json.Unmarshal(responseBody, &response) if err != nil { logger.Error().Err(err).Msgf("failed to deserialize body JSON payload into a %T", response) - var zero Result[T] - return zero, jmapError(err, JmapErrorDecodingResponseBody) + return ZeroResult[T](single(duration)), jmapError(err, JmapErrorDecodingResponseBody) } if response.SessionState != ctx.Session.State { @@ -124,20 +128,20 @@ func command[T any](client Cmdr, //NOSONAR msg := fmt.Sprintf("found method level error in response '%v', type: '%v', description: '%v'", mr.Tag, errorParameters.Type, errorParameters.Description) err = errors.New(msg) logger.Warn().Int("code", code).Str("type", errorParameters.Type).Msg(msg) - return newPartialResult[T](response.SessionState, language), jmapResponseError(code, err, errorParameters.Type, errorParameters.Description) + return newPartialResult[T](response.SessionState, language, single(duration)), jmapResponseError(code, err, errorParameters.Type, errorParameters.Description) } else { code := JmapErrorUnspecifiedType msg := fmt.Sprintf("found method level error in response '%v'", mr.Tag) err := errors.New(msg) logger.Warn().Int("code", code).Msg(msg) - return newPartialResult[T](response.SessionState, language), jmapResponseError(code, err, errorParameters.Type, errorParameters.Description) + return newPartialResult[T](response.SessionState, language, single(duration)), jmapResponseError(code, err, errorParameters.Type, errorParameters.Description) } } } result, state, jerr := mapper(&response) sessionState := response.SessionState - return newResult(result, sessionState, state, language), jerr + return NewResult(result, sessionState, state, language, single(duration)), jerr } func mapstructStringToTimeHook() mapstructure.DecodeHookFunc { diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index ce997ea7ce..dbf06856ca 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -373,3 +373,23 @@ func First[T any](values []T, predicate func(T) bool) (T, bool) { var zero T return zero, false } + +func Reduce[T any](initialValue T, s []T, reducer func(a, b T) T) T { + reduced := initialValue + for _, value := range s { + reduced = reducer(reduced, value) + } + return reduced +} + +func Flatten[T any](s [][]T) []T { + l := 0 + for _, r := range s { + l += len(r) + } + result := make([]T, 0, l) + for _, r := range s { + result = append(result, r...) + } + return result +} diff --git a/pkg/structs/structs_test.go b/pkg/structs/structs_test.go index c4eac27285..817a7b7a2b 100644 --- a/pkg/structs/structs_test.go +++ b/pkg/structs/structs_test.go @@ -275,3 +275,37 @@ func TestSet(t *testing.T) { assert.True(t, ok) } } + +func TestReduce(t *testing.T) { + { + result := Reduce(0, []int{1, 2, 3}, func(a, b int) int { return a + b }) + assert.Equal(t, 6, result) + } + { + result := Reduce(0, []int{}, func(a, b int) int { return a + b }) + assert.Equal(t, 0, result) + } +} + +func TestFlatten(t *testing.T) { + { + result := Flatten([][]int{{1, 2, 3}, {4}, {5, 6, 7}, {}, {8}}) + assert.Equal(t, []int{1, 2, 3, 4, 5, 6, 7, 8}, result) + } + { + result := Flatten([][]int{}) + assert.Equal(t, []int{}, result) + } + { + result := Flatten([][]int{nil, nil}) + assert.Equal(t, []int{}, result) + } + { + result := Flatten([][]int{nil, {}, {1}}) + assert.Equal(t, []int{1}, result) + } + { + result := Flatten[int](nil) + assert.Equal(t, []int{}, result) + } +} diff --git a/services/groupware/pkg/config/defaults/defaultconfig.go b/services/groupware/pkg/config/defaults/defaultconfig.go index b91bb1c943..cc181dc822 100644 --- a/services/groupware/pkg/config/defaults/defaultconfig.go +++ b/services/groupware/pkg/config/defaults/defaultconfig.go @@ -52,7 +52,8 @@ func DefaultConfig() *config.Config { AllowedHeaders: []string{"Authorization", "Origin", "Content-Type", "Accept", "X-Requested-With", "X-Request-Id", "Trace-Id", "Cache-Control"}, AllowCredentials: true, }, - OpenCloudPublicURL: "https://localhost:9200/", + OpenCloudPublicURL: "https://localhost:9200/", + SendDurationsResponse: false, }, Service: config.Service{ Name: "groupware", diff --git a/services/groupware/pkg/config/http.go b/services/groupware/pkg/config/http.go index 312cffc677..d939ed4036 100644 --- a/services/groupware/pkg/config/http.go +++ b/services/groupware/pkg/config/http.go @@ -12,10 +12,11 @@ type CORS struct { // HTTP defines the available http configuration. type HTTP struct { - Addr string `yaml:"addr" env:"GROUPWARE_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"1.0.0"` - TLS shared.HTTPServiceTLS `yaml:"tls"` - Root string `yaml:"root" env:"GROUPWARE_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"1.0.0"` - Namespace string `yaml:"-"` - CORS CORS `yaml:"cors"` - OpenCloudPublicURL string `yaml:"opencloud_public_url" env:"OC_URL;OC_PUBLIC_URL;GROUPWARE_PUBLIC_URL"` + Addr string `yaml:"addr" env:"GROUPWARE_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"1.0.0"` + TLS shared.HTTPServiceTLS `yaml:"tls"` + Root string `yaml:"root" env:"GROUPWARE_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"1.0.0"` + Namespace string `yaml:"-"` + CORS CORS `yaml:"cors"` + OpenCloudPublicURL string `yaml:"opencloud_public_url" env:"OC_URL;OC_PUBLIC_URL;GROUPWARE_PUBLIC_URL"` + SendDurationsResponse bool `yaml:"send_durations_response" env:"GROUPWARE_SEND_DURATIONS_RESPONSE"` } diff --git a/services/groupware/pkg/groupware/address_book_supplier_jmap.go b/services/groupware/pkg/groupware/address_book_supplier_jmap.go index 7dd86b3183..6dda818b4e 100644 --- a/services/groupware/pkg/groupware/address_book_supplier_jmap.go +++ b/services/groupware/pkg/groupware/address_book_supplier_jmap.go @@ -30,7 +30,7 @@ func (c *JmapContactCardSupplier) GetAll(accountId jmap.AccountId, ids []string, } func (c *JmapContactCardSupplier) Query(accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, filter jmap.ContactCardFilterElement, sortBy []jmap.ContactCardComparator, calculateTotal bool, ctx jmap.Context) (jmap.Result[map[jmap.AccountId]*jmap.ContactCardSearchResults], error) { //NOSONAR if m, err := mapQueryParams(c.GetId(), accountIds, qps); err != nil { - return jmap.ZeroResult[map[jmap.AccountId]*jmap.ContactCardSearchResults](), err + return jmap.ZeroResultV[map[jmap.AccountId]*jmap.ContactCardSearchResults](), err } else { return c.client.QueryContactCards(m, limit, filter, sortBy, calculateTotal, ctx) } diff --git a/services/groupware/pkg/groupware/address_book_supplier_mock.go b/services/groupware/pkg/groupware/address_book_supplier_mock.go index ab10314123..f96abf9885 100644 --- a/services/groupware/pkg/groupware/address_book_supplier_mock.go +++ b/services/groupware/pkg/groupware/address_book_supplier_mock.go @@ -75,16 +75,17 @@ func (c *MockContactCardSupplier) GetAll(accountId jmap.AccountId, ids []string, if len(ids) > 0 { abooks = structs.Filter(abooks, func(a jmap.AddressBook) bool { return slices.Contains(ids, a.Id) }) } - return jmap.Result[jmap.AddressBookGetResponse]{ - SessionState: jmap.EmptySessionState, - State: "mock", - Language: jmap.NoLanguage, - Payload: jmap.AddressBookGetResponse{ + return jmap.NewResult( + jmap.AddressBookGetResponse{ AccountId: accountId, State: "mock", List: abooks, }, - }, nil + jmap.EmptySessionState, + jmap.State("mock"), + jmap.NoLanguage, + nil, + ), nil } 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)) @@ -93,7 +94,7 @@ func (c *MockContactCardSupplier) Query(accountIds []jmap.AccountId, qps QueryPa all := []jmap.ContactCard{} var qp jmap.QueryParams if q, ok, err := qps.ForSupplier(c.GetId(), accountId); err != nil { - return jmap.ZeroResult[map[jmap.AccountId]*jmap.ContactCardSearchResults](), err + return jmap.ZeroResultV[map[jmap.AccountId]*jmap.ContactCardSearchResults](), err } else if ok { qp = q } else { @@ -141,10 +142,5 @@ func (c *MockContactCardSupplier) Query(accountIds []jmap.AccountId, qps QueryPa payload[accountId] = res } - return jmap.Result[map[jmap.AccountId]*jmap.ContactCardSearchResults]{ - Payload: payload, - SessionState: jmap.EmptySessionState, - State: jmap.EmptyState, - Language: jmap.NoLanguage, - }, nil + return jmap.NewResult(payload, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, nil), nil } diff --git a/services/groupware/pkg/groupware/api_account.go b/services/groupware/pkg/groupware/api_account.go index dbcbeafe2a..bced5bb210 100644 --- a/services/groupware/pkg/groupware/api_account.go +++ b/services/groupware/pkg/groupware/api_account.go @@ -14,7 +14,7 @@ func (g *Groupware) GetAccountById(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { accountId, account, err := req.GetAccountForMail() if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } var body jmap.Account = account return req.respond(accountId, body, AccountResponseObjectType, req.session) diff --git a/services/groupware/pkg/groupware/api_blob.go b/services/groupware/pkg/groupware/api_blob.go index e612247a1c..9290e34287 100644 --- a/services/groupware/pkg/groupware/api_blob.go +++ b/services/groupware/pkg/groupware/api_blob.go @@ -4,6 +4,7 @@ import ( "io" "net/http" "strconv" + "time" "github.com/opencloud-eu/opencloud/pkg/jmap" "github.com/opencloud-eu/opencloud/pkg/log" @@ -32,16 +33,18 @@ func (g *Groupware) UploadBlob(w http.ResponseWriter, r *http.Request) { accountId, err := req.GetAccountIdForBlob() if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } logger := log.From(req.logger.With().Str(logAccountId, log.SafeString(accountId))) ctx := req.ctx.WithLogger(logger) + before := time.Now() resp, _, jerr := g.jmap.UploadBlobStream(accountId, contentType, body, ctx) + duration := time.Since(before) if jerr != nil { return req.jmapError(accountId, jerr, req.session) } - return req.respondWithoutStatus(accountId, resp) + return req.respondWithoutStatus(accountId, resp, single(duration)) }) } @@ -55,21 +58,22 @@ func (g *Groupware) DownloadBlob(w http.ResponseWriter, r *http.Request) { blobId, err := req.PathParam(UriParamBlobId) // the unique identifier of the blob to download if err != nil { - return false, req.error(accountId, err) + return false, req.errorV(accountId, err) } name, err := req.PathParam(UriParamBlobName) // the filename of the blob to download, which is then used in the response and may be arbitrary if unknown if err != nil { - return false, req.error(accountId, err) + return false, req.errorV(accountId, err) } typ, _ := req.getStringParam(QueryParamBlobType, "") // optionally, the Content-Type of the blob, which is then used in the response logger := log.From(req.logger.With().Str(logAccountId, log.SafeString(accountId)).Str(UriParamBlobId, blobId)) ctx := req.ctx.WithLogger(logger) + before := time.Now() if err := req.serveBlob(blobId, name, typ, ctx, accountId, w); err != nil { - return false, req.error(accountId, err) + return false, req.error(accountId, err, single(time.Since(before))) } else { - return true, req.noop(accountId) + return true, req.noop(accountId, single(time.Since(before))) } }) } diff --git a/services/groupware/pkg/groupware/api_changes.go b/services/groupware/pkg/groupware/api_changes.go index 4650eeb91a..b6a8ce19db 100644 --- a/services/groupware/pkg/groupware/api_changes.go +++ b/services/groupware/pkg/groupware/api_changes.go @@ -32,13 +32,13 @@ func (g *Groupware) GetChanges(w http.ResponseWriter, r *http.Request) { //NOSON l := req.logger.With() accountId, err := req.GetAccountIdForMail() if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l = l.Str(logAccountId, log.SafeString(accountId)) var maxChanges uint = 0 if v, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0); err != nil { // The maximum amount of changes to emit for each type of object. - return req.error(accountId, err) + return req.errorV(accountId, err) } else if ok { maxChanges = v l = l.Uint(QueryParamMaxChanges, v) @@ -72,7 +72,7 @@ func (g *Groupware) GetChanges(w http.ResponseWriter, r *http.Request) { //NOSON } //if state, ok := req.getStringParam(QueryParamQuotas, ""); ok { sinceState.Quotas = ptr(toState(state)) } if sinceState.IsZero() { - return req.noop(accountId) // No content response if no object IDs were requested. + return req.noop(accountId, zeroDurations) // No content response if no object IDs were requested. } } diff --git a/services/groupware/pkg/groupware/api_emails.go b/services/groupware/pkg/groupware/api_emails.go index 5582cac8c1..055a7be1eb 100644 --- a/services/groupware/pkg/groupware/api_emails.go +++ b/services/groupware/pkg/groupware/api_emails.go @@ -45,11 +45,11 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request func(req Request, accountId jmap.AccountId, containerId string, qp jmap.QueryParams, limit *uint, ctx jmap.Context) (jmap.Result[*jmap.EmailSearchResults], error) { //NOSONAR result, jerr := g.jmap.GetAllEmailsInMailbox(accountId, containerId, qp, limit, collapseThreads, fetchBodies, g.config.maxBodyValueBytes, withThreads, ctx) if jerr != nil { - return jmap.ZeroResult[*jmap.EmailSearchResults](), jerr + return jmap.ZeroResultM(result), jerr } sanitized, err := req.sanitizeEmails(result.Payload.Results) if err != nil { - return jmap.ZeroResult[*jmap.EmailSearchResults](), err + return jmap.ZeroResultM(result), err } return jmap.RefineResultPayload(result, func(orig *jmap.EmailSearchResults) (*jmap.EmailSearchResults, bool, error) { return &jmap.EmailSearchResults{ @@ -70,12 +70,12 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { //NO g.stream(w, r, func(req Request, w http.ResponseWriter) (bool, Response) { accountId, err := req.GetAccountIdForMail() if err != nil { - return false, req.error(accountId, err) + return false, req.errorV(accountId, err) } id, err := req.PathParam(UriParamEmailId) if err != nil { - return false, req.error(accountId, err) + return false, req.errorV(accountId, err) } ids := strings.Split(id, ",") if len(ids) != 1 { @@ -84,7 +84,7 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { //NO _, ok, err := req.parseBoolParam(QueryParamMarkAsSeen, false) if err != nil { - return false, req.error(accountId, err) + return false, req.errorV(accountId, err) } if ok { return false, req.parameterErrorResponse(accountId, QueryParamMarkAsSeen, fmt.Sprintf("when the Accept header is set to '%s', the API does not support setting %s", accept, QueryParamMarkAsSeen)) @@ -97,18 +97,19 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { //NO return false, req.jmapError(accountId, jerr, result) } if result.Payload == "" { - return true, req.noop(accountId) + return true, req.noop(accountId, result.GetDurations()) } else { name := result.Payload + ".eml" typ := accept accountId, gwerr := req.GetAccountIdForBlob() if gwerr != nil { - return false, req.error(accountId, gwerr) + return false, req.error(accountId, gwerr, result.GetDurations()) } + before := time.Now() if err := req.serveBlob(result.Payload, name, typ, ctx, accountId, w); err != nil { - return false, req.error(accountId, err) + return false, req.error(accountId, err, append(result.GetDurations(), time.Since(before))) } else { - return true, req.noop(accountId) + return true, req.noop(accountId, append(result.GetDurations(), time.Since(before))) } } }) @@ -116,13 +117,13 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { //NO g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForMail() if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l := req.logger.With().Str(logAccountId, log.SafeString(accountId)) id, err := req.PathParam(UriParamEmailId) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } ids := strings.Split(id, ",") if len(ids) < 1 { @@ -131,7 +132,7 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { //NO markAsSeen, ok, err := req.parseBoolParam(QueryParamMarkAsSeen, false) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } if ok { l = l.Bool(QueryParamMarkAsSeen, markAsSeen) @@ -149,7 +150,7 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { //NO } else { sanitized, err := req.sanitizeEmail(result.Payload.List[0]) if err != nil { - return req.error(accountId, req.apiError(err)) + return req.errorV(accountId, req.apiError(err)) } return req.respond(accountId, sanitized, EmailResponseObjectType, result) } @@ -165,7 +166,7 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { //NO } else { sanitized, err := req.sanitizeEmails(result.Payload.List) if err != nil { - return req.error(accountId, req.apiError(err)) + return req.errorV(accountId, req.apiError(err)) } return req.respond(accountId, sanitized, EmailResponseObjectType, result) } @@ -209,7 +210,7 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) id, err := req.PathParam(UriParamEmailId) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } logger := log.From(l) @@ -222,7 +223,7 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) return req.notFound(accountId, EmailResponseObjectType, result) } if email, err := req.sanitizeEmail(result.Payload.List[0]); err != nil { - return req.error(accountId, req.apiError(err)) + return req.errorV(accountId, req.apiError(err)) } else { var body []jmap.EmailBodyPart = email.Attachments return req.respond(accountId, body, EmailResponseObjectType, result) @@ -241,7 +242,7 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) id, err := req.PathParam(UriParamEmailId) if err != nil { - return false, req.error(mailAccountId, err) + return false, req.errorV(mailAccountId, err) } l := req.logger.With(). @@ -256,12 +257,12 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) return false, req.jmapError(mailAccountId, jerr, result) } if len(result.Payload.List) < 1 { - return true, req.noop(mailAccountId) + return true, req.noop(mailAccountId, result.GetDurations()) } email, gwerr := req.sanitizeEmail(result.Payload.List[0]) if gwerr != nil { - return false, req.error(mailAccountId, req.apiError(gwerr)) + return false, req.errorV(mailAccountId, req.apiError(gwerr)) } var attachment *jmap.EmailBodyPart = nil for _, part := range email.Attachments { @@ -271,9 +272,10 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) } } if attachment == nil { - return true, req.noop(mailAccountId) + return true, req.noop(mailAccountId, result.GetDurations()) } + before := time.Now() blob, lang, jerr := g.jmap.DownloadBlobStream(blobAccountId, attachment.BlobId, attachment.Name, attachment.Type, ctx) if blob != nil && blob.Body != nil { defer func(Body io.ReadCloser) { @@ -288,7 +290,7 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) } if blob == nil { w.WriteHeader(http.StatusNotFound) - return true, req.noop(blobAccountId) + return true, req.noop(blobAccountId, result.GetDurations()) } if blob.Type != "" { @@ -307,11 +309,12 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) w.Header().Add("Content-Language", string(lang)) } _, cerr := io.Copy(w, blob.Body) + durations := append(result.GetDurations(), time.Since(before)) if cerr != nil { - return false, req.error(blobAccountId, req.observedParameterError(ErrorStreamingResponse)) + return false, req.error(blobAccountId, req.observedParameterError(ErrorStreamingResponse), durations) } - return true, req.noop(blobAccountId) + return true, req.noop(blobAccountId, durations) }) } } @@ -322,13 +325,13 @@ func (g *Groupware) getEmailsSince(w http.ResponseWriter, r *http.Request, since accountId, err := req.GetAccountIdForMail() if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l = l.Str(logAccountId, log.SafeString(accountId)) maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } if ok { l = l.Uint(QueryParamMaxChanges, maxChanges) @@ -555,14 +558,14 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { //NOSONA g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForMail() if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l := req.logger.With().Str(logAccountId, log.SafeString(accountId)) ok, filter, makesSnippets, position, anchor, anchorOffset, limit, logger, err := g.buildEmailFilter(req) if !ok { - return req.error(accountId, err) + return req.errorV(accountId, err) } if !filter.IsNotEmpty() { @@ -571,7 +574,7 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { //NOSONA calculateTotal := true if b, ok, err := req.parseBoolParam(QueryParamCalculateTotal, true); err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } else if ok { calculateTotal = b l = l.Bool(QueryParamCalculateTotal, calculateTotal) @@ -599,11 +602,11 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { //NOSONA flattened = nil } else { flattened = make([]EmailWithSnippets, len(results.Results)) - for i, result := range results.Results { + for i, emails := range results.Results { var snippets []SnippetWithoutEmailId if makesSnippets { - snippets := make([]SnippetWithoutEmailId, len(result.Snippets)) - for j, snippet := range result.Snippets { + snippets := make([]SnippetWithoutEmailId, len(emails.Snippets)) + for j, snippet := range emails.Snippets { snippets[j] = SnippetWithoutEmailId{ Subject: snippet.Subject, Preview: snippet.Preview, @@ -612,9 +615,9 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { //NOSONA } else { snippets = nil } - sanitized, err := req.sanitizeEmail(result.Email) + sanitized, err := req.sanitizeEmail(emails.Email) if err != nil { - return req.error(accountId, req.apiError(err)) + return req.error(accountId, req.apiError(err), result.GetDurations()) } flattened[i] = EmailWithSnippets{ Email: sanitized, @@ -647,7 +650,7 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque ok, filter, makesSnippets, position, anchor, anchorOffset, limit, logger, err := g.buildEmailFilter(req) if !ok { - return req.errorN(allAccountIds, err) + return req.errorNV(allAccountIds, err) } logger = log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(structs.ToStrings(allAccountIds)))) ctx := req.ctx.WithLogger(logger) @@ -770,7 +773,7 @@ func findDraftsMailboxId(j *jmap.Client, accountId jmap.AccountId, req Request, } // couldn't find a Mailbox with the drafts role for that account, // we have to return an error... ? - return "", req.error(accountId, apiError(req.errorId(), ErrorNoMailboxWithDraftRole)) + return "", req.error(accountId, apiError(req.errorId(), ErrorNoMailboxWithDraftRole), result.GetDurations()) } } @@ -794,7 +797,7 @@ func findSentMailboxId(j *jmap.Client, accountId jmap.AccountId, req Request, ct } } if sentMailboxId == "" { - return "", "", req.error(accountId, apiError(req.errorId(), ErrorNoMailboxWithSentRole)) + return "", "", req.error(accountId, apiError(req.errorId(), ErrorNoMailboxWithSentRole), result.GetDurations()) } draftsMailboxId := "" for _, role := range draftEmailAutoMailboxRolePrecedence { @@ -804,7 +807,7 @@ func findSentMailboxId(j *jmap.Client, accountId jmap.AccountId, req Request, ct } } if draftsMailboxId == "" { - return "", "", req.error(accountId, apiError(req.errorId(), ErrorNoMailboxWithDraftRole)) + return "", "", req.error(accountId, apiError(req.errorId(), ErrorNoMailboxWithDraftRole), result.GetDurations()) } return draftsMailboxId, sentMailboxId, Response{} } @@ -847,7 +850,7 @@ func (g *Groupware) ReplaceEmail(w http.ResponseWriter, r *http.Request) { var err *Error replaceId, err = r.PathParam(UriParamEmailId) if err != nil { - return false, r.error(accountId, err) + return false, r.errorV(accountId, err) } return true, Response{} @@ -881,13 +884,13 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request) accountId, gwerr := req.GetAccountIdForMail() if gwerr != nil { - return req.error(accountId, gwerr) + return req.errorV(accountId, gwerr) } l.Str(logAccountId, log.SafeString(accountId)) emailId, err := req.PathParam(UriParamEmailId) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l.Str(UriParamEmailId, log.SafeString(emailId)) @@ -897,11 +900,11 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request) var body emailKeywordUpdates err = req.body(&body) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } if body.IsEmpty() { - return req.noop(accountId) + return req.noopV(accountId) } patch := jmap.PatchObject{} @@ -922,12 +925,12 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request) if result.Payload == nil { return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", //NOSONAR - "An internal API behaved unexpectedly: missing Email update response from JMAP endpoint"))) //NOSONAR + "An internal API behaved unexpectedly: missing Email update response from JMAP endpoint")), result.GetDurations()) //NOSONAR } updatedEmail, ok := result.Payload[emailId] if !ok { return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", //NOSONAR - "An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint"))) //NOSONAR + "An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint")), result.GetDurations()) //NOSONAR } return req.respond(accountId, updatedEmail, EmailResponseObjectType, result) @@ -943,13 +946,13 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) { / accountId, gwerr := req.GetAccountIdForMail() if gwerr != nil { - return req.error(accountId, gwerr) + return req.errorV(accountId, gwerr) } l.Str(logAccountId, log.SafeString(accountId)) emailId, err := req.PathParam(UriParamEmailId) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l.Str(UriParamEmailId, log.SafeString(emailId)) @@ -959,11 +962,11 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) { / var body []string err = req.body(&body) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } if len(body) < 1 { - return req.noop(accountId) + return req.noopV(accountId) } patch := jmap.PatchObject{} @@ -981,12 +984,12 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) { / if result.Payload == nil { return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", - "An internal API behaved unexpectedly: missing Email update response from JMAP endpoint"))) + "An internal API behaved unexpectedly: missing Email update response from JMAP endpoint")), result.GetDurations()) } updatedEmail, ok := result.Payload[emailId] if !ok { return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", - "An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint"))) + "An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint")), result.GetDurations()) } if updatedEmail == nil { @@ -1006,13 +1009,13 @@ func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request) accountId, err := req.GetAccountIdForMail() if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l.Str(logAccountId, log.SafeString(accountId)) emailId, err := req.PathParam(UriParamEmailId) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l.Str(UriParamEmailId, log.SafeString(emailId)) @@ -1022,11 +1025,11 @@ func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request) var body []string err = req.body(&body) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } if len(body) < 1 { - return req.noop(accountId) + return req.noopV(accountId) } patch := jmap.PatchObject{} @@ -1044,12 +1047,12 @@ func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request) if result.Payload == nil { return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", - "An internal API behaved unexpectedly: missing Email update response from JMAP endpoint"))) + "An internal API behaved unexpectedly: missing Email update response from JMAP endpoint")), result.GetDurations()) } updatedEmail, ok := result.Payload[emailId] if !ok { return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", - "An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint"))) + "An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint")), result.GetDurations()) } if updatedEmail == nil { @@ -1080,19 +1083,19 @@ func (g *Groupware) SendEmail(w http.ResponseWriter, r *http.Request) { //NOSONA accountId, gwerr := req.GetAccountIdForMail() if gwerr != nil { - return req.error(accountId, gwerr) + return req.errorV(accountId, gwerr) } l.Str(logAccountId, log.SafeString(accountId)) emailId, err := req.PathParam(UriParamEmailId) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l.Str(UriParamEmailId, log.SafeString(emailId)) identityId, err := req.getMandatoryStringParam(QueryParamIdentityId) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l.Str(QueryParamIdentityId, log.SafeString(identityId)) @@ -1198,30 +1201,30 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { //N accountId, err := req.GetAccountIdForMail() if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l = l.Str(logAccountId, log.SafeString(accountId)) id, err := req.PathParam(UriParamEmailId) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l = l.Str(logEmailId, log.SafeString(id)) limit, ok, err := req.parseUIntParam(QueryParamLimit, 10) // TODO configurable default limit if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } if ok { - l = l.Uint("limit", limit) + l = l.Uint(QueryParamLimit, limit) } days, ok, err := req.parseUIntParam(QueryParamDays, 5) // TODO configurable default days if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } if ok { - l = l.Uint("days", days) + l = l.Uint(QueryParamDays, days) } logger := log.From(l) @@ -1297,7 +1300,7 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { //N }) if sanitized, err := req.sanitizeEmail(email); err != nil { - return req.error(accountId, req.apiError(err)) + return req.errorV(accountId, req.apiError(err)) } else { return req.respond(accountId, AboutEmailResponse{ Email: sanitized, @@ -1499,7 +1502,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, limit, ok, err := req.parseUIntParam(QueryParamLimit, 10) // TODO from configuration if err != nil { - return req.errorN(allAccountIds, err) + return req.errorNV(allAccountIds, err) } if ok { l = l.Uint(QueryParamLimit, limit) @@ -1507,7 +1510,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, position, ok, err := req.parseIntParam(QueryParamPosition, 0) if err != nil { - return req.errorN(allAccountIds, err) + return req.errorNV(allAccountIds, err) } if position != 0 { return req.notImplementedN(allAccountIds, EmailResponseObjectType) @@ -1528,7 +1531,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, { v, ok, err := req.parseIntParam(QueryParamAnchorOffset, 0) // optional offset relative to the anchor if err != nil { - return req.errorN(allAccountIds, err) + return req.errorNV(allAccountIds, err) } if ok { return req.notImplementedN(allAccountIds, EmailResponseObjectType) @@ -1541,7 +1544,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, seen, ok, err := req.parseBoolParam(QueryParamSeen, false) if err != nil { - return req.errorN(allAccountIds, err) + return req.errorNV(allAccountIds, err) } if ok { l = l.Bool(QueryParamSeen, seen) @@ -1549,7 +1552,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, undesirable, ok, err := req.parseBoolParam(QueryParamUndesirable, false) if err != nil { - return req.errorN(allAccountIds, err) + return req.errorNV(allAccountIds, err) } if ok { l = l.Bool(QueryParamUndesirable, undesirable) diff --git a/services/groupware/pkg/groupware/api_events.go b/services/groupware/pkg/groupware/api_events.go index 8467242d98..b2a04ec7b5 100644 --- a/services/groupware/pkg/groupware/api_events.go +++ b/services/groupware/pkg/groupware/api_events.go @@ -82,12 +82,12 @@ func (g *Groupware) ParseIcalBlob(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { accountId, err := req.GetAccountIdForBlob() if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } blobId, err := req.PathParam(UriParamBlobId) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } blobIds := strings.Split(blobId, ",") diff --git a/services/groupware/pkg/groupware/api_mailbox.go b/services/groupware/pkg/groupware/api_mailbox.go index 8bcbf85978..275c483d04 100644 --- a/services/groupware/pkg/groupware/api_mailbox.go +++ b/services/groupware/pkg/groupware/api_mailbox.go @@ -55,12 +55,12 @@ func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) { //NOS accountId, err := req.GetAccountIdForMail() if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } subscribed, set, err := req.parseBoolParam(QueryParamMailboxSearchSubscribed, false) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } if set { filter.IsSubscribed = &subscribed @@ -104,7 +104,7 @@ func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Re g.respond(w, r, func(req Request) Response { accountIds := req.AllAccountIds() if len(accountIds) < 1 { - return req.noopN(nil) // when the user has no accounts + return req.noopN(nil, zeroDurations) // when the user has no accounts } logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(structs.ToStrings(accountIds)))) ctx := req.ctx.WithLogger(logger) @@ -118,7 +118,7 @@ func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Re } if subscribed, set, err := req.parseBoolParam(QueryParamMailboxSearchSubscribed, false); err != nil { - return req.errorN(accountIds, err) + return req.errorNV(accountIds, err) } else if set { filter.IsSubscribed = &subscribed hasCriteria = true @@ -145,12 +145,12 @@ func (g *Groupware) GetMailboxByRoleForAllAccounts(w http.ResponseWriter, r *htt g.respond(w, r, func(req Request) Response { accountIds := req.AllAccountIds() if len(accountIds) < 1 { - return req.noopN(accountIds) // when the user has no accounts + return req.noopN(accountIds, zeroDurations) // when the user has no accounts } role, err := req.PathParamDoc(UriParamRole, "Role of the mailboxes to retrieve across all accounts") if err != nil { - return req.errorN(accountIds, err) + return req.errorNV(accountIds, err) } logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(structs.ToStrings(accountIds))).Str("role", role)) @@ -184,7 +184,7 @@ func (g *Groupware) GetMailboxChangesForAllAccounts(w http.ResponseWriter, r *ht sinceStateStrMap, ok, err := req.parseMapParam(QueryParamSince) if err != nil { - return req.errorN(allAccountIds, err) + return req.errorNV(allAccountIds, err) } if ok { dict := zerolog.Dict() @@ -196,7 +196,7 @@ func (g *Groupware) GetMailboxChangesForAllAccounts(w http.ResponseWriter, r *ht maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0) if err != nil { - return req.errorN(allAccountIds, err) + return req.errorNV(allAccountIds, err) } if ok { l = l.Uint(QueryParamMaxChanges, maxChanges) diff --git a/services/groupware/pkg/groupware/api_objects.go b/services/groupware/pkg/groupware/api_objects.go index e377e8cf72..01e7825ed5 100644 --- a/services/groupware/pkg/groupware/api_objects.go +++ b/services/groupware/pkg/groupware/api_objects.go @@ -43,7 +43,7 @@ func (g *Groupware) GetObjects(w http.ResponseWriter, r *http.Request) { //NOSON l := req.logger.With() accountId, err := req.GetAccountIdForMail() if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l = l.Str(logAccountId, log.SafeString(accountId)) @@ -59,7 +59,7 @@ func (g *Groupware) GetObjects(w http.ResponseWriter, r *http.Request) { //NOSON { var objects ObjectsRequest if ok, err := req.optBody(&objects); err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } else if ok { mailboxIds = append(mailboxIds, objects.Mailboxes...) emailIds = append(emailIds, objects.Emails...) @@ -74,47 +74,47 @@ func (g *Groupware) GetObjects(w http.ResponseWriter, r *http.Request) { //NOSON } if list, ok, err := req.parseOptStringListParam(QueryParamMailboxes); err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } else if ok { mailboxIds = append(mailboxIds, list...) } if list, ok, err := req.parseOptStringListParam(QueryParamEmails); err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } else if ok { emailIds = append(emailIds, list...) } if list, ok, err := req.parseOptStringListParam(QueryParamAddressbooks); err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } else if ok { addressbookIds = append(addressbookIds, list...) } if list, ok, err := req.parseOptStringListParam(QueryParamContacts); err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } else if ok { contactIds = append(contactIds, list...) } if list, ok, err := req.parseOptStringListParam(QueryParamCalendars); err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } else if ok { calendarIds = append(calendarIds, list...) } if list, ok, err := req.parseOptStringListParam(QueryParamEvents); err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } else if ok { eventIds = append(eventIds, list...) } if list, ok, err := req.parseOptStringListParam(QueryParamQuotas); err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } else if ok { quotaIds = append(quotaIds, list...) } if list, ok, err := req.parseOptStringListParam(QueryParamIdentities); err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } else if ok { identityIds = append(identityIds, list...) } if list, ok, err := req.parseOptStringListParam(QueryParamEmailSubmissions); err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } else if ok { emailSubmissionIds = append(emailSubmissionIds, list...) } diff --git a/services/groupware/pkg/groupware/api_quota.go b/services/groupware/pkg/groupware/api_quota.go index bac9727b49..ebb8ca4a23 100644 --- a/services/groupware/pkg/groupware/api_quota.go +++ b/services/groupware/pkg/groupware/api_quota.go @@ -32,7 +32,7 @@ func (g *Groupware) GetQuotaForAllAccounts(w http.ResponseWriter, r *http.Reques g.respond(w, r, func(req Request) Response { accountIds := req.AllAccountIds() if len(accountIds) < 1 { - return req.noopN(accountIds) // user has no accounts + return req.noopN(accountIds, zeroDurations) // user has no accounts } logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(structs.ToStrings(accountIds)))) ctx := req.ctx.WithLogger(logger) diff --git a/services/groupware/pkg/groupware/api_tasklists.go b/services/groupware/pkg/groupware/api_tasklists.go index 096318e069..f3c9945d16 100644 --- a/services/groupware/pkg/groupware/api_tasklists.go +++ b/services/groupware/pkg/groupware/api_tasklists.go @@ -32,7 +32,7 @@ func (g *Groupware) GetTaskListById(w http.ResponseWriter, r *http.Request) { tasklistId, err := req.PathParam(UriParamTaskListId) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } // TODO replace with proper implementation meta := TaskListsMeta{SessionState: req.session.State} @@ -41,7 +41,7 @@ func (g *Groupware) GetTaskListById(w http.ResponseWriter, r *http.Request) { return req.respond(accountId, tasklist, TaskListResponseObjectType, meta) } } - return req.etaggedNotFound(accountId, req.session.State, TaskListResponseObjectType, TaskListsState) + return req.etaggedNotFound(accountId, req.session.State, TaskListResponseObjectType, TaskListsState, zeroDurations) }) } @@ -56,7 +56,7 @@ func (g *Groupware) GetTasksInTaskList(w http.ResponseWriter, r *http.Request) { tasklistId, err := req.PathParam(UriParamTaskListId) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } // TODO replace with proper implementation meta := TaskMeta{SessionState: req.session.State} diff --git a/services/groupware/pkg/groupware/error.go b/services/groupware/pkg/groupware/error.go index 037e23d738..c5a009f17a 100644 --- a/services/groupware/pkg/groupware/error.go +++ b/services/groupware/pkg/groupware/error.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strconv" + "time" chimiddleware "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/render" @@ -750,32 +751,41 @@ func errorResponses(errors ...Error) ErrorResponse { return ErrorResponse{Errors: errors} } -func (r *Request) error(accountId jmap.AccountId, err *Error) Response { - return errorResponse(single(accountId), err, r.session.State, jmap.NoLanguage) +// Validation error prior to performing any requests +func (r *Request) errorV(accountId jmap.AccountId, err *Error) Response { + return errorResponse(single(accountId), err, r.session.State, jmap.NoLanguage, zeroDurations) +} + +func (r *Request) error(accountId jmap.AccountId, err *Error, durations []time.Duration) Response { + return errorResponse(single(accountId), err, r.session.State, jmap.NoLanguage, durations) } func (r *Request) errorS(accountId jmap.AccountId, err *Error, result jmap.ResultMetadata) Response { - return errorResponse(single(accountId), err, result.GetSessionState(), result.GetLanguage()) + return errorResponse(single(accountId), err, result.GetSessionState(), result.GetLanguage(), result.GetDurations()) } -func (r *Request) errorN(accountIds []jmap.AccountId, err *Error) Response { - return errorResponse(accountIds, err, r.session.State, jmap.NoLanguage) +func (r *Request) errorN(accountIds []jmap.AccountId, err *Error, durations []time.Duration) Response { + return errorResponse(accountIds, err, r.session.State, jmap.NoLanguage, durations) +} + +func (r *Request) errorNV(accountIds []jmap.AccountId, err *Error) Response { + return errorResponse(accountIds, err, r.session.State, jmap.NoLanguage, zeroDurations) } func (r *Request) jmapError(accountId jmap.AccountId, err error, result jmap.ResultMetadata) Response { switch e := err.(type) { case jmap.Error: if result != nil { - return errorResponse(single(accountId), r.apiErrorFromJmap(r.observeJmapError(e)), result.GetSessionState(), result.GetLanguage()) + return errorResponse(single(accountId), r.apiErrorFromJmap(r.observeJmapError(e)), result.GetSessionState(), result.GetLanguage(), result.GetDurations()) } else { - return errorResponse(single(accountId), r.apiErrorFromJmap(r.observeJmapError(e)), r.session.GetSessionState(), jmap.NoLanguage) + return errorResponse(single(accountId), r.apiErrorFromJmap(r.observeJmapError(e)), r.session.GetSessionState(), jmap.NoLanguage, zeroDurations) } case GroupwareError: errorId := r.errorId() - return r.error(accountId, apiError(errorId, e)) + return r.error(accountId, apiError(errorId, e), result.GetDurations()) default: errorId := r.errorId() - return r.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error()))) + return r.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error())), result.GetDurations()) } } @@ -783,15 +793,15 @@ func (r *Request) jmapErrorN(accountIds []jmap.AccountId, err error, result jmap switch e := err.(type) { case jmap.Error: if result != nil { - return errorResponse(accountIds, r.apiErrorFromJmap(r.observeJmapError(e)), result.GetSessionState(), result.GetLanguage()) + return errorResponse(accountIds, r.apiErrorFromJmap(r.observeJmapError(e)), result.GetSessionState(), result.GetLanguage(), result.GetDurations()) } else { - return errorResponse(accountIds, r.apiErrorFromJmap(r.observeJmapError(e)), r.session.GetSessionState(), jmap.NoLanguage) + return errorResponse(accountIds, r.apiErrorFromJmap(r.observeJmapError(e)), r.session.GetSessionState(), jmap.NoLanguage, zeroDurations) } case GroupwareError: errorId := r.errorId() - return r.errorN(accountIds, apiError(errorId, e)) + return r.errorN(accountIds, apiError(errorId, e), result.GetDurations()) default: errorId := r.errorId() - return r.errorN(accountIds, apiError(errorId, ErrorGeneric, withDetail(e.Error()))) + return r.errorN(accountIds, 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 543268f750..848e5c730e 100644 --- a/services/groupware/pkg/groupware/framework.go +++ b/services/groupware/pkg/groupware/framework.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" "slices" + "strconv" "strings" "sync/atomic" "time" @@ -82,9 +83,10 @@ type Job struct { } type groupwareConfig struct { - maxBodyValueBytes uint - sanitize bool - publicUrl *url.URL + maxBodyValueBytes uint + sanitize bool + publicUrl *url.URL + sendDurationsResponse bool } type groupwareDefaults struct { @@ -187,6 +189,7 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome defaultEmailLimit := ptrIfNot(max(config.Mail.DefaultEmailLimit, 0), 0) maxBodyValueBytes := max(config.Mail.MaxBodyValueBytes, 0) + sendDurationsResponse := config.HTTP.SendDurationsResponse defaultContactLimit := ptrIfNot(max(config.Mail.DefaultContactLimit, 0), 0) responseHeaderTimeout := max(config.Mail.ResponseHeaderTimeout, 0) sessionCacheMaxCapacity := uint64(max(config.Mail.SessionCache.MaxCapacity, 0)) @@ -426,9 +429,10 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome contactLimit: defaultContactLimit, }, config: groupwareConfig{ - maxBodyValueBytes: maxBodyValueBytes, - sanitize: sanitize, - publicUrl: publicUrl, + maxBodyValueBytes: maxBodyValueBytes, + sanitize: sanitize, + publicUrl: publicUrl, + sendDurationsResponse: sendDurationsResponse, }, eventChannel: eventChannel, jobsChannel: jobsChannel, @@ -701,11 +705,17 @@ func (g *Groupware) withSession(w http.ResponseWriter, r *http.Request, handler } const ( - SessionStateResponseHeader = "Session-State" - StateResponseHeader = "State" - ObjectTypeResponseHeader = "Object-Type" - AccountIdResponseHeader = "Account-Id" - AccountIdsResponseHeader = "Account-Ids" + SessionStateResponseHeader = "Session-State" + StateResponseHeader = "State" + ObjectTypeResponseHeader = "Object-Type" + AccountIdResponseHeader = "Account-Id" + AccountIdsResponseHeader = "Account-Ids" + LinkResponseHeader = "Link" + DurationsResponseHeader = "Durations" + DurationsNanosResponseHeader = "Durations-Nanos" + NextResponseHeader = "Next" + ContentLanguageResponseHeader = "Content-Language" + ETagResponseHeader = "ETag" ) // Send the Response object as an HTTP response. @@ -725,11 +735,11 @@ func (g *Groupware) sendResponse(w http.ResponseWriter, r *http.Request, respons } if response.contentLanguage != "" { - w.Header().Add("Content-Language", string(response.contentLanguage)) + w.Header().Add(ContentLanguageResponseHeader, string(response.contentLanguage)) } if response.next != "" && response.next != NoNextToken { - w.Header().Add("next", string(response.next)) // TODO add RESTful links for next? + w.Header().Add(NextResponseHeader, string(response.next)) // TODO add RESTful links for next? base := g.config.publicUrl.JoinPath("/groupware/contacts") @@ -745,7 +755,7 @@ func (g *Groupware) sendResponse(w http.ResponseWriter, r *http.Request, respons q.Set(QueryParamLimit, limit) } u.RawQuery = q.Encode() - w.Header().Add("Link", fmt.Sprintf(`<%s>; rel="next">`, u.String())) + w.Header().Add(LinkResponseHeader, fmt.Sprintf(`<%s>; rel="next">`, u.String())) } { u := base @@ -754,7 +764,7 @@ func (g *Groupware) sendResponse(w http.ResponseWriter, r *http.Request, respons q.Set(QueryParamLimit, limit) } u.RawQuery = q.Encode() - w.Header().Add("Link", fmt.Sprintf(`<%s>; rel="first">`, u.String())) + w.Header().Add(LinkResponseHeader, fmt.Sprintf(`<%s>; rel="first">`, u.String())) } } @@ -765,7 +775,7 @@ func (g *Groupware) sendResponse(w http.ResponseWriter, r *http.Request, respons challenge := r.Header.Get("if-none-match") // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/If-None-Match quotedEtag := "\"" + etag + "\"" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag#etag_value notModified = challenge != "" && (challenge == etag || challenge == quotedEtag) // be a bit flexible/permissive here with the quoting - w.Header().Add("ETag", quotedEtag) + w.Header().Add(ETagResponseHeader, quotedEtag) w.Header().Add(StateResponseHeader, etag) } } @@ -787,6 +797,11 @@ func (g *Groupware) sendResponse(w http.ResponseWriter, r *http.Request, respons w.Header().Add(AccountIdsResponseHeader, value) } + if g.config.sendDurationsResponse && len(response.durations) > 0 { + w.Header().Add(DurationsNanosResponseHeader, strings.Join(structs.Map(response.durations, func(d time.Duration) string { return strconv.FormatInt(d.Nanoseconds(), 10) }), ", ")) + w.Header().Add(DurationsResponseHeader, strings.Join(structs.Map(response.durations, func(d time.Duration) string { return d.String() }), ", ")) + } + if notModified { w.WriteHeader(http.StatusNotModified) } else { diff --git a/services/groupware/pkg/groupware/mock_tasks.go b/services/groupware/pkg/groupware/mock_tasks.go index d7e79f600c..60e8c43aae 100644 --- a/services/groupware/pkg/groupware/mock_tasks.go +++ b/services/groupware/pkg/groupware/mock_tasks.go @@ -208,6 +208,7 @@ type TaskListsMeta struct { func (t TaskListsMeta) GetSessionState() jmap.SessionState { return t.SessionState } func (t TaskListsMeta) GetState() jmap.State { return TaskListsState } func (t TaskListsMeta) GetLanguage() jmap.Language { return jmap.NoLanguage } +func (t TaskListsMeta) GetDurations() []time.Duration { return zeroDurations } var _ jmap.ResultMetadata = TaskListsMeta{} @@ -226,6 +227,7 @@ type TaskMeta struct { func (t TaskMeta) GetSessionState() jmap.SessionState { return t.SessionState } func (t TaskMeta) GetState() jmap.State { return TaskState } func (t TaskMeta) GetLanguage() jmap.Language { return jmap.NoLanguage } +func (t TaskMeta) GetDurations() []time.Duration { return zeroDurations } var _ jmap.ResultMetadata = TaskListsMeta{} diff --git a/services/groupware/pkg/groupware/next.go b/services/groupware/pkg/groupware/next.go index 5eb3635904..160af24baa 100644 --- a/services/groupware/pkg/groupware/next.go +++ b/services/groupware/pkg/groupware/next.go @@ -74,11 +74,11 @@ func curryNoNextMapQuery[SRES jmap.SearchResults[T], T jmap.Idable, FILTER any, ) func(req Request, accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, filter FILTER, sortBy []COMP, ctx jmap.Context) (jmap.Result[SRES], NextToken, error) { return func(_ Request, accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, filter FILTER, sortBy []COMP, ctx jmap.Context) (jmap.Result[SRES], NextToken, error) { //NOSONAR if m, err := mapQueryParams("", accountIds, qps); err != nil { - return jmap.ZeroResult[SRES](), NoNextToken, err + return jmap.ZeroResultV[SRES](), NoNextToken, err } else { result, err := f(m, limit, filter, sortBy, true, ctx) if err != nil { - return jmap.ZeroResult[SRES](), NoNextToken, err + return jmap.ZeroResult[SRES](result.Durations), NoNextToken, err } else { var singleAccountId jmap.AccountId = "" // TODO what about requests with zero accountIds, can these even happen at all? @@ -155,15 +155,15 @@ func flattenMultipleAccounts[SRES jmap.SearchResults[T], T jmap.Idable]( n[accountId] = jmap.QueryParams{Anchor: lastId, AnchorOffset: ptr(1)} } if err := fillMissingAccounts(qps, "", accountIds, n); err != nil { - return jmap.ZeroResult[SRES](), NoNextToken, err + return jmap.ZeroResult[SRES](result.Durations), NoNextToken, err } if t, err := nextSingle(n); err != nil { - return jmap.ZeroResult[SRES](), NoNextToken, err + return jmap.ZeroResult[SRES](result.Durations), NoNextToken, err } else { if r, err := jmap.RefineResultPayload(result, func(a map[jmap.AccountId]SRES) (SRES, bool, error) { return searchResultCtor(jmap.ChangeCalculation(cc), nil, limit, &total, all), true, nil }); err != nil { - return jmap.ZeroResult[SRES](), NoNextToken, err + return jmap.ZeroResult[SRES](result.Durations), NoNextToken, err } else { return r, t, nil } @@ -187,7 +187,7 @@ func flattenMultipleAccounts[SRES jmap.SearchResults[T], T jmap.Idable]( if r, err := jmap.RefineResultPayload(result, func(a map[jmap.AccountId]SRES) (SRES, bool, error) { return searchResultCtor(jmap.ChangeCalculation(cc), nil, limit, &total, all), true, nil }); err != nil { - return jmap.ZeroResult[SRES](), NoNextToken, err + return jmap.ZeroResult[SRES](result.Durations), NoNextToken, err } else { return r, NoNextToken, nil } diff --git a/services/groupware/pkg/groupware/request.go b/services/groupware/pkg/groupware/request.go index 950c561c0b..726ee2cbbc 100644 --- a/services/groupware/pkg/groupware/request.go +++ b/services/groupware/pkg/groupware/request.go @@ -224,11 +224,11 @@ func (r *Request) parameterError(param string, detail string) *Error { } func (r *Request) parameterErrorResponse(accountId jmap.AccountId, param string, detail string) Response { - return r.error(accountId, r.parameterError(param, detail)) + return r.errorV(accountId, r.parameterError(param, detail)) } func (r *Request) parameterErrorResponseN(accountIds []jmap.AccountId, param string, detail string) Response { - return r.errorN(accountIds, r.parameterError(param, detail)) + return r.errorNV(accountIds, r.parameterError(param, detail)) } type supportedQueryParams map[string]struct{} @@ -472,7 +472,7 @@ func (r *Request) observeJmapError(jerr jmap.Error) jmap.Error { func (r *Request) needBlob(accountId jmap.AccountId) (bool, Response) { if r.session.Capabilities.Blob == nil { - return false, errorResponse(single(accountId), r.apiError(&ErrorMissingBlobSessionCapability), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingBlobSessionCapability), r.session.State, jmap.NoLanguage, zeroDurations) } return true, Response{} } @@ -483,10 +483,10 @@ func (r *Request) needBlobForAccount(accountId jmap.AccountId) (bool, Response) } account, ok := r.session.Accounts[accountId] if !ok { - return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage, zeroDurations) } if account.AccountCapabilities.Blob == nil { - return false, errorResponse(single(accountId), r.apiError(&ErrorMissingBlobAccountCapability), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingBlobAccountCapability), r.session.State, jmap.NoLanguage, zeroDurations) } return true, Response{} } @@ -494,7 +494,7 @@ func (r *Request) needBlobForAccount(accountId jmap.AccountId) (bool, Response) func (r *Request) needBloblWithAccount() (bool, jmap.AccountId, Response) { accountId, err := r.GetAccountIdForBlob() if err != nil { - return false, "", r.error(accountId, err) + return false, "", r.errorV(accountId, err) } if ok, resp := r.needBlobForAccount(accountId); !ok { return false, accountId, resp @@ -504,7 +504,7 @@ func (r *Request) needBloblWithAccount() (bool, jmap.AccountId, Response) { func (r *Request) needMail(accountId jmap.AccountId) (bool, Response) { if r.session.Capabilities.Mail == nil { - return false, errorResponse(single(accountId), r.apiError(&ErrorMissingMailsSessionCapability), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingMailsSessionCapability), r.session.State, jmap.NoLanguage, zeroDurations) } return true, Response{} } @@ -515,10 +515,10 @@ func (r *Request) needMailForAccount(accountId jmap.AccountId) (bool, Response) } account, ok := r.session.Accounts[accountId] if !ok { - return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage, zeroDurations) } if account.AccountCapabilities.Contacts == nil { - return false, errorResponse(single(accountId), r.apiError(&ErrorMissingMailsAccountCapability), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingMailsAccountCapability), r.session.State, jmap.NoLanguage, zeroDurations) } return true, Response{} } @@ -526,7 +526,7 @@ func (r *Request) needMailForAccount(accountId jmap.AccountId) (bool, Response) func (r *Request) needMailWithAccount() (bool, jmap.AccountId, Response) { accountId, err := r.GetAccountIdForMail() if err != nil { - return false, "", r.error(accountId, err) + return false, "", r.errorV(accountId, err) } if ok, resp := r.needMailForAccount(accountId); !ok { return false, accountId, resp @@ -537,7 +537,7 @@ func (r *Request) needMailWithAccount() (bool, jmap.AccountId, Response) { func (r *Request) needTask(accountId jmap.AccountId) (bool, Response) { if !IgnoreSessionCapabilityChecksForTasks { if r.session.Capabilities.Tasks == nil { - return false, errorResponse(single(accountId), r.apiError(&ErrorMissingTasksSessionCapability), r.session.State, jmap.Language(r.language())) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingTasksSessionCapability), r.session.State, jmap.Language(r.language()), zeroDurations) } } return true, Response{} @@ -549,10 +549,10 @@ func (r *Request) needTaskForAccount(accountId jmap.AccountId) (bool, Response) } account, ok := r.session.Accounts[accountId] if !ok { - return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage, zeroDurations) } if account.AccountCapabilities.Tasks == nil { - return false, errorResponse(single(accountId), r.apiError(&ErrorMissingTasksAccountCapability), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingTasksAccountCapability), r.session.State, jmap.NoLanguage, zeroDurations) } return true, Response{} } @@ -560,7 +560,7 @@ func (r *Request) needTaskForAccount(accountId jmap.AccountId) (bool, Response) func (r *Request) needTaskWithAccount() (bool, jmap.AccountId, Response) { accountId, err := r.GetAccountIdForTask() if err != nil { - return false, "", r.error(accountId, err) + return false, "", r.errorV(accountId, err) } if ok, resp := r.needTaskForAccount(accountId); !ok { return false, accountId, resp @@ -570,7 +570,7 @@ func (r *Request) needTaskWithAccount() (bool, jmap.AccountId, Response) { func (r *Request) needCalendar(accountId jmap.AccountId) (bool, Response) { if r.session.Capabilities.Calendars == nil { - return false, errorResponse(single(accountId), r.apiError(&ErrorMissingCalendarsSessionCapability), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingCalendarsSessionCapability), r.session.State, jmap.NoLanguage, zeroDurations) } return true, Response{} } @@ -581,10 +581,10 @@ func (r *Request) needCalendarForAccount(accountId jmap.AccountId) (bool, Respon } account, ok := r.session.Accounts[accountId] if !ok { - return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage, zeroDurations) } if account.AccountCapabilities.Calendars == nil { - return false, errorResponse(single(accountId), r.apiError(&ErrorMissingCalendarsAccountCapability), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingCalendarsAccountCapability), r.session.State, jmap.NoLanguage, zeroDurations) } return true, Response{} } @@ -592,7 +592,7 @@ func (r *Request) needCalendarForAccount(accountId jmap.AccountId) (bool, Respon func (r *Request) needCalendarWithAccount() (bool, jmap.AccountId, Response) { accountId, err := r.GetAccountIdForCalendar() if err != nil { - return false, "", r.error(accountId, err) + return false, "", r.errorV(accountId, err) } if ok, resp := r.needCalendarForAccount(accountId); !ok { return false, accountId, resp @@ -602,7 +602,7 @@ func (r *Request) needCalendarWithAccount() (bool, jmap.AccountId, Response) { func (r *Request) needContact(accountId jmap.AccountId) (bool, Response) { if r.session.Capabilities.Contacts == nil { - return false, errorResponse(single(accountId), r.apiError(&ErrorMissingContactsSessionCapability), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingContactsSessionCapability), r.session.State, jmap.NoLanguage, zeroDurations) } return true, Response{} } @@ -613,10 +613,10 @@ func (r *Request) needContactForAccount(accountId jmap.AccountId) (bool, Respons } account, ok := r.session.Accounts[accountId] if !ok { - return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage, zeroDurations) } if account.AccountCapabilities.Contacts == nil { - return false, errorResponse(single(accountId), r.apiError(&ErrorMissingContactsAccountCapability), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingContactsAccountCapability), r.session.State, jmap.NoLanguage, zeroDurations) } return true, Response{} } @@ -624,7 +624,7 @@ func (r *Request) needContactForAccount(accountId jmap.AccountId) (bool, Respons func (r *Request) needContactWithAccount() (bool, jmap.AccountId, Response) { accountId, err := r.GetAccountIdForContact() if err != nil { - return false, "", r.error(accountId, err) + return false, "", r.errorV(accountId, err) } if ok, resp := r.needContactForAccount(accountId); !ok { return false, accountId, resp @@ -634,7 +634,7 @@ func (r *Request) needContactWithAccount() (bool, jmap.AccountId, Response) { func (r *Request) needQuota(accountId jmap.AccountId) (bool, Response) { if r.session.Capabilities.Quota == nil { - return false, errorResponse(single(accountId), r.apiError(&ErrorMissingQuotaSessionCapability), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingQuotaSessionCapability), r.session.State, jmap.NoLanguage, zeroDurations) } return true, Response{} } @@ -645,10 +645,10 @@ func (r *Request) needQuotaForAccount(accountId jmap.AccountId) (bool, Response) } account, ok := r.session.Accounts[accountId] if !ok { - return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorAccountNotFound), r.session.State, jmap.NoLanguage, zeroDurations) } if account.AccountCapabilities.Quota == nil { - return false, errorResponse(single(accountId), r.apiError(&ErrorMissingQuotaAccountCapability), r.session.State, jmap.NoLanguage) + return false, errorResponse(single(accountId), r.apiError(&ErrorMissingQuotaAccountCapability), r.session.State, jmap.NoLanguage, zeroDurations) } return true, Response{} } @@ -656,7 +656,7 @@ func (r *Request) needQuotaForAccount(accountId jmap.AccountId) (bool, Response) func (r *Request) needQuotaWithAccount() (bool, jmap.AccountId, Response) { accountId, err := r.GetAccountIdForQuota() if err != nil { - return false, "", r.error(accountId, err) + return false, "", r.errorV(accountId, err) } if ok, resp := r.needQuotaForAccount(accountId); !ok { return false, accountId, resp diff --git a/services/groupware/pkg/groupware/response.go b/services/groupware/pkg/groupware/response.go index 973650998a..2811e3db2c 100644 --- a/services/groupware/pkg/groupware/response.go +++ b/services/groupware/pkg/groupware/response.go @@ -2,6 +2,7 @@ package groupware import ( "net/http" + "time" "github.com/opencloud-eu/opencloud/pkg/jmap" ) @@ -26,6 +27,8 @@ const ( VacationResponseResponseObjectType = ResponseObjectType("vacationresponse") ) +var zeroDurations = []time.Duration{time.Duration(0)} + type Response struct { body any status int @@ -36,9 +39,16 @@ type Response struct { sessionState jmap.SessionState contentLanguage jmap.Language next NextToken + durations []time.Duration } -func errorResponse(accountIds []jmap.AccountId, err *Error, sessionState jmap.SessionState, contentLanguage jmap.Language) Response { +func errorResponse( + accountIds []jmap.AccountId, + err *Error, + sessionState jmap.SessionState, + contentLanguage jmap.Language, + durations []time.Duration, +) Response { return Response{ accountIds: accountIds, body: nil, @@ -47,10 +57,17 @@ func errorResponse(accountIds []jmap.AccountId, err *Error, sessionState jmap.Se sessionState: sessionState, contentLanguage: contentLanguage, next: NoNextToken, + durations: durations, } } -func response(accountIds []jmap.AccountId, body any, sessionState jmap.SessionState, contentLanguage jmap.Language) Response { +func response( + accountIds []jmap.AccountId, + body any, + sessionState jmap.SessionState, + contentLanguage jmap.Language, + durations []time.Duration, +) Response { return Response{ accountIds: accountIds, body: body, @@ -59,14 +76,23 @@ func response(accountIds []jmap.AccountId, body any, sessionState jmap.SessionSt sessionState: sessionState, contentLanguage: contentLanguage, next: NoNextToken, + durations: durations, } } -func (r *Request) respondWithoutStatus(accountId jmap.AccountId, body any) Response { - return response(single(accountId), body, r.session.State, jmap.Language(r.language())) +func (r *Request) respondWithoutStatus(accountId jmap.AccountId, body any, durations []time.Duration) Response { + return response(single(accountId), body, r.session.State, jmap.Language(r.language()), durations) } -func etaggedResponse(accountIds []jmap.AccountId, body any, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State, contentLanguage jmap.Language) Response { +func etaggedResponse( + accountIds []jmap.AccountId, + body any, + sessionState jmap.SessionState, + objectType ResponseObjectType, + etag jmap.State, + contentLanguage jmap.Language, + durations []time.Duration, +) Response { return Response{ accountIds: accountIds, body: body, @@ -76,18 +102,38 @@ func etaggedResponse(accountIds []jmap.AccountId, body any, sessionState jmap.Se sessionState: sessionState, contentLanguage: contentLanguage, next: NoNextToken, + durations: durations, } } -func (r *Request) respond(accountId jmap.AccountId, body any, objectType ResponseObjectType, result jmap.ResultMetadata) Response { - return etaggedResponse(single(accountId), body, result.GetSessionState(), objectType, result.GetState(), result.GetLanguage()) +func (r *Request) respond( + accountId jmap.AccountId, + body any, + objectType ResponseObjectType, + result jmap.ResultMetadata, +) Response { + return etaggedResponse(single(accountId), body, result.GetSessionState(), objectType, result.GetState(), result.GetLanguage(), result.GetDurations()) } -func (r *Request) respondN(accountIds []jmap.AccountId, body any, objectType ResponseObjectType, result jmap.ResultMetadata) Response { - return etaggedResponse(accountIds, body, result.GetSessionState(), objectType, result.GetState(), result.GetLanguage()) +func (r *Request) respondN( + accountIds []jmap.AccountId, + body any, + objectType ResponseObjectType, + result jmap.ResultMetadata, +) Response { + return etaggedResponse(accountIds, body, result.GetSessionState(), objectType, result.GetState(), result.GetLanguage(), result.GetDurations()) } -func etaggedNextResponse(accountIds []jmap.AccountId, body any, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State, contentLanguage jmap.Language, next NextToken) Response { +func etaggedNextResponse( + accountIds []jmap.AccountId, + body any, + sessionState jmap.SessionState, + objectType ResponseObjectType, + etag jmap.State, + contentLanguage jmap.Language, + next NextToken, + durations []time.Duration, +) Response { return Response{ accountIds: accountIds, body: body, @@ -97,11 +143,18 @@ func etaggedNextResponse(accountIds []jmap.AccountId, body any, sessionState jma sessionState: sessionState, contentLanguage: contentLanguage, next: next, + durations: durations, } } -func (r *Request) respondNext(accountId jmap.AccountId, body any, objectType ResponseObjectType, result jmap.ResultMetadata, next NextToken) Response { - return etaggedNextResponse(single(accountId), body, result.GetSessionState(), objectType, result.GetState(), result.GetLanguage(), next) +func (r *Request) respondNext( + accountId jmap.AccountId, + body any, + objectType ResponseObjectType, + result jmap.ResultMetadata, + next NextToken, +) Response { + return etaggedNextResponse(single(accountId), body, result.GetSessionState(), objectType, result.GetState(), result.GetLanguage(), next, result.GetDurations()) } /* @@ -117,7 +170,7 @@ func etagOnlyResponse(body any, etag jmap.State, objectType ResponseObjectType, } */ -func noContentResponse(accountIds []jmap.AccountId, sessionState jmap.SessionState) Response { +func noContentResponse(accountIds []jmap.AccountId, sessionState jmap.SessionState, durations []time.Duration) Response { return Response{ accountIds: accountIds, body: nil, @@ -126,18 +179,29 @@ func noContentResponse(accountIds []jmap.AccountId, sessionState jmap.SessionSta etag: jmap.State(sessionState), sessionState: sessionState, next: NoNextToken, + durations: durations, } } -func (r *Request) noop(accountId jmap.AccountId) Response { - return noContentResponse(single(accountId), r.session.State) +func (r *Request) noop(accountId jmap.AccountId, durations []time.Duration) Response { + return noContentResponse(single(accountId), r.session.State, durations) } -func (r *Request) noopN(accountIds []jmap.AccountId) Response { - return noContentResponse(accountIds, r.session.State) +func (r *Request) noopV(accountId jmap.AccountId) Response { + return noContentResponse(single(accountId), r.session.State, zeroDurations) } -func noContentResponseWithEtag(accountIds []jmap.AccountId, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State) Response { +func (r *Request) noopN(accountIds []jmap.AccountId, durations []time.Duration) Response { + return noContentResponse(accountIds, r.session.State, durations) +} + +func noContentResponseWithEtag( + accountIds []jmap.AccountId, + sessionState jmap.SessionState, + objectType ResponseObjectType, + etag jmap.State, + durations []time.Duration, +) Response { return Response{ accountIds: accountIds, body: nil, @@ -147,11 +211,16 @@ func noContentResponseWithEtag(accountIds []jmap.AccountId, sessionState jmap.Se objectType: objectType, sessionState: sessionState, next: NoNextToken, + durations: durations, } } -func (r *Request) noContent(accountId jmap.AccountId, objectType ResponseObjectType, result jmap.ResultMetadata) Response { - return noContentResponseWithEtag(single(accountId), result.GetSessionState(), objectType, result.GetState()) +func (r *Request) noContent( + accountId jmap.AccountId, + objectType ResponseObjectType, + result jmap.ResultMetadata, +) Response { + return noContentResponseWithEtag(single(accountId), result.GetSessionState(), objectType, result.GetState(), result.GetDurations()) } /* @@ -178,7 +247,13 @@ func timeoutResponse(sessionState jmap.SessionState) Response { } */ -func notFoundResponse(accountIds []jmap.AccountId, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State) Response { +func notFoundResponse( + accountIds []jmap.AccountId, + sessionState jmap.SessionState, + objectType ResponseObjectType, + etag jmap.State, + durations []time.Duration, +) Response { return Response{ accountIds: accountIds, body: nil, @@ -188,14 +263,26 @@ func notFoundResponse(accountIds []jmap.AccountId, sessionState jmap.SessionStat etag: etag, sessionState: sessionState, next: NoNextToken, + durations: durations, } } -func (r *Request) notFound(accountId jmap.AccountId, objectType ResponseObjectType, result jmap.ResultMetadata) Response { - return notFoundResponse(single(accountId), result.GetSessionState(), objectType, result.GetState()) +func (r *Request) notFound( + accountId jmap.AccountId, + objectType ResponseObjectType, + result jmap.ResultMetadata, +) Response { + return notFoundResponse(single(accountId), result.GetSessionState(), objectType, result.GetState(), result.GetDurations()) } -func etaggedNotFoundResponse(accountIds []jmap.AccountId, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State, contentLanguage jmap.Language) Response { +func etaggedNotFoundResponse( + accountIds []jmap.AccountId, + sessionState jmap.SessionState, + objectType ResponseObjectType, + etag jmap.State, + contentLanguage jmap.Language, + durations []time.Duration, +) Response { return Response{ accountIds: accountIds, body: nil, @@ -206,11 +293,18 @@ func etaggedNotFoundResponse(accountIds []jmap.AccountId, sessionState jmap.Sess sessionState: sessionState, contentLanguage: contentLanguage, next: NoNextToken, + durations: durations, } } -func (r *Request) etaggedNotFound(accountId jmap.AccountId, sessionState jmap.SessionState, objectType ResponseObjectType, etag jmap.State) Response { - return etaggedNotFoundResponse(single(accountId), sessionState, objectType, etag, jmap.Language(r.language())) +func (r *Request) etaggedNotFound( + accountId jmap.AccountId, + sessionState jmap.SessionState, + objectType ResponseObjectType, + etag jmap.State, + durations []time.Duration, +) Response { + return etaggedNotFoundResponse(single(accountId), sessionState, objectType, etag, jmap.Language(r.language()), durations) } func notImplementedResponse(accountIds []jmap.AccountId, sessionState jmap.SessionState, objectType ResponseObjectType) Response { @@ -222,6 +316,7 @@ func notImplementedResponse(accountIds []jmap.AccountId, sessionState jmap.Sessi objectType: objectType, sessionState: sessionState, next: NoNextToken, + durations: nil, } } diff --git a/services/groupware/pkg/groupware/suppliers.go b/services/groupware/pkg/groupware/suppliers.go index 4bf35fa302..9255148850 100644 --- a/services/groupware/pkg/groupware/suppliers.go +++ b/services/groupware/pkg/groupware/suppliers.go @@ -6,6 +6,7 @@ import ( "fmt" "slices" "strings" + "time" "github.com/opencloud-eu/opencloud/pkg/jmap" "github.com/opencloud-eu/opencloud/pkg/structs" @@ -35,7 +36,7 @@ func curryQueryFunc[SRES jmap.SearchResults[T], T jmap.Foo, FILTER any, COMP any return func(_ Request, accountIds []jmap.AccountId, qps QueryParamsSupplier, limit *uint, filter FILTER, sortBy []COMP, ctx jmap.Context) (jmap.Result[SRES], NextToken, error) { //NOSONAR result, next, err := f(accountIds, qps, limit, filter, sortBy, true, ctx) if err != nil { - return jmap.ZeroResult[SRES](), next, err + return jmap.ZeroResult[SRES](result.Durations), next, err } else { return result, next, err } @@ -91,7 +92,7 @@ func slist[T jmap.Idable, G jmap.GetResponse[T], S ListSupplier[T, G]](suppliers ctor func(accountId jmap.AccountId, state jmap.State, notFound []string, list []T) G) (jmap.Result[G], error) { switch len(suppliers) { case 0: - return jmap.ZeroResult[G](), nil + return jmap.ZeroResultV[G](), nil case 1: return suppliers[0].GetAll(accountId, ids, ctx) default: @@ -177,16 +178,16 @@ func squery[T jmap.Idable, R jmap.SearchResults[T], S QuerySupplier[T, R, F, C], ) { s := len(suppliers) if s == 0 { - return jmap.ZeroResult[R](), NoNextToken, nil + return jmap.ZeroResultV[R](), NoNextToken, nil } switch s { case 1: supplier := suppliers[0] if result, err := supplier.Query(accountIds, qps, limit, filter, sortBy, calculateTotal, ctx); err != nil { - return jmap.ZeroResult[R](), NoNextToken, err + return jmap.ZeroResult[R](result.Durations), NoNextToken, err } else if len(accountIds) == 0 { - return jmap.ZeroResult[R](), NoNextToken, nil + return jmap.ZeroResult[R](result.Durations), NoNextToken, nil } else if len(accountIds) == 1 { accountId := accountIds[0] // use anchor and anchorOffset and limit: @@ -199,7 +200,7 @@ func squery[T jmap.Idable, R jmap.SearchResults[T], S QuerySupplier[T, R, F, C], n[accountId] = jmap.QueryParams{Anchor: last.GetId(), AnchorOffset: ptr(1)} } if nextToken, err := nextSingle(n); err != nil { - return jmap.ZeroResult[R](), NoNextToken, err + return jmap.ZeroResult[R](result.Durations), NoNextToken, err } else { single, err := jmap.RefineResultPayload(result, func(m map[jmap.AccountId]R) (R, bool, error) { if r, ok := m[accountId]; ok { @@ -272,19 +273,18 @@ func squery[T jmap.Idable, R jmap.SearchResults[T], S QuerySupplier[T, R, F, C], n[accountId] = jmap.QueryParams{Anchor: lastId, AnchorOffset: ptr(1)} } if err := fillMissingAccounts(qps, supplier.GetId(), accountIds, n); err != nil { - return jmap.ZeroResult[R](), NoNextToken, err + return jmap.ZeroResult[R](result.Durations), NoNextToken, err } nextBySupplier := map[SupplierId]map[jmap.AccountId]jmap.QueryParams{supplier.GetId(): n} if nextToken, err := nextMulti(nextBySupplier); err != nil { - return jmap.ZeroResult[R](), NoNextToken, err + return jmap.ZeroResult[R](result.Durations), NoNextToken, err } else { - return jmap.Result[R]{ - Payload: r, - SessionState: result.GetSessionState(), - State: result.GetState(), - Language: result.GetLanguage(), - }, nextToken, nil + if refined, err := jmap.RefineResultPayload(result, func(a map[jmap.AccountId]R) (R, bool, error) { return r, true, nil }); err != nil { + return jmap.ZeroResult[R](result.Durations), NoNextToken, err + } else { + return refined, nextToken, nil + } } } default: @@ -295,11 +295,13 @@ func squery[T jmap.Idable, R jmap.SearchResults[T], S QuerySupplier[T, R, F, C], sessionState := jmap.EmptySessionState states := map[SupplierId]jmap.State{} lang := jmap.NoLanguage - for _, supplier := range suppliers { + durations := make([][]time.Duration, len(suppliers)) + for i, supplier := range suppliers { // we are not injecting id prefixes here for all the objects, as each supplier is responsible for doing that if necessary if result, err := supplier.Query(accountIds, qps, limit, filter, sortBy, calculateTotal, ctx); err != nil { - return jmap.ZeroResult[R](), NoNextToken, err + return jmap.ZeroResult[R](result.Durations), NoNextToken, err } else { + durations[i] = result.Durations if result.GetSessionState() != jmap.EmptySessionState { sessionState = result.GetSessionState() } @@ -375,23 +377,18 @@ func squery[T jmap.Idable, R jmap.SearchResults[T], S QuerySupplier[T, R, F, C], n[accountId] = jmap.QueryParams{Anchor: lastId, AnchorOffset: ptr(1)} } if err := fillMissingAccounts(qps, supplierId, accountIds, n); err != nil { - return jmap.ZeroResult[R](), NoNextToken, err + return jmap.ZeroResult[R](structs.Flatten(durations)), NoNextToken, err } nextBySupplier[supplierId] = n } if nextToken, err := nextMulti(nextBySupplier); err != nil { - return jmap.ZeroResult[R](), NoNextToken, err + return jmap.ZeroResult[R](structs.Flatten(durations)), NoNextToken, err } else { if state, err := combineState(states); err != nil { - return jmap.ZeroResult[R](), NoNextToken, err + return jmap.ZeroResult[R](structs.Flatten(durations)), NoNextToken, err } else { - return jmap.Result[R]{ - Payload: r, - SessionState: sessionState, - State: state, - Language: lang, - }, nextToken, nil + return jmap.NewResult(r, sessionState, state, lang, structs.Flatten(durations)), nextToken, nil } } } diff --git a/services/groupware/pkg/groupware/suppliers_test.go b/services/groupware/pkg/groupware/suppliers_test.go index e0ca3defe0..4b27e547c5 100644 --- a/services/groupware/pkg/groupware/suppliers_test.go +++ b/services/groupware/pkg/groupware/suppliers_test.go @@ -43,7 +43,7 @@ func inmemquery[T jmap.Idable, R jmap.SearchResults[T]]( for _, accountId := range accountIds { qp := jmap.NullQueryParams if q, ok, err := qps.ForSupplier(supplierId, accountId); err != nil { - return jmap.ZeroResult[map[jmap.AccountId]R](), err + return jmap.ZeroResultV[map[jmap.AccountId]R](), err } else if ok { qp = q } @@ -91,12 +91,7 @@ func inmemquery[T jmap.Idable, R jmap.SearchResults[T]]( payload[accountId] = res } - return jmap.Result[map[jmap.AccountId]R]{ - Payload: payload, - SessionState: jmap.EmptySessionState, - State: jmap.EmptyState, - Language: jmap.NoLanguage, - }, nil + return jmap.NewResult(payload, jmap.EmptySessionState, jmap.EmptyState, jmap.NoLanguage, nil), nil } func pets( @@ -158,7 +153,7 @@ func TestSquery(t *testing.T) { } n := func(accountIds []jmap.AccountId, nextToken NextToken, limit *uint) (jmap.Result[*PetSearchResults], NextToken, error) { if qps, err := unnext(nextToken); err != nil { - return jmap.ZeroResult[*PetSearchResults](), NoNextToken, err + return jmap.ZeroResultV[*PetSearchResults](), NoNextToken, err } else { return pets(suppliers, accountIds, qps, limit, PetFilterCondition{}, []PetComparator{{Property: "id", IsAscending: true}}, true, jmap.Context{}) } diff --git a/services/groupware/pkg/groupware/templates.go b/services/groupware/pkg/groupware/templates.go index 6d2c559c76..7c87ce46c2 100644 --- a/services/groupware/pkg/groupware/templates.go +++ b/services/groupware/pkg/groupware/templates.go @@ -31,7 +31,7 @@ func create[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR var create CHANGE err := req.body(&create) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } logger := log.From(l) @@ -49,10 +49,10 @@ func create[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR return req.jmapError(accountId, e, result) case GroupwareError: errorId := req.errorId() - return req.error(accountId, apiError(errorId, e)) + return req.error(accountId, apiError(errorId, e), result.GetDurations()) default: errorId := req.errorId() - return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error()))) + return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error())), result.GetDurations()) } } else { return req.respond(accountId, result.Payload, o.responseType, result) @@ -87,10 +87,10 @@ func getall[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.G return req.jmapError(accountId, e, result) case GroupwareError: errorId := req.errorId() - return req.error(accountId, apiError(errorId, e)) + return req.error(accountId, apiError(errorId, e), result.GetDurations()) default: errorId := req.errorId() - return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error()))) + return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error())), result.GetDurations()) } } else { return req.respond(accountId, result.Payload, o.responseType, result) @@ -125,7 +125,7 @@ func getallpaged[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], FILTER { v, ok, err := req.parseUIntParam(QueryParamLimit, uint(0)) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } if ok { l = l.Uint(QueryParamLimit, v) @@ -140,7 +140,7 @@ func getallpaged[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], FILTER supportedQueryParams = nextQueryParams if n, err := unnext(NextToken(s)); err != nil { errorId := req.errorId() - return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(err.Error()))) // TODO replace ErrorGeneric + return req.errorV(accountId, apiError(errorId, ErrorGeneric, withDetail(err.Error()))) // TODO replace ErrorGeneric } else { qps = n } @@ -155,7 +155,7 @@ func getallpaged[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], FILTER var err *Error containerId, err = req.PathParam(o.containerUriParamName) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l = l.Str(o.containerUriParamName, log.SafeString(containerId)) } @@ -174,10 +174,10 @@ func getallpaged[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], FILTER return req.jmapError(accountId, e, result) case GroupwareError: errorId := req.errorId() - return req.error(accountId, apiError(errorId, e)) + return req.error(accountId, apiError(errorId, e), result.GetDurations()) default: errorId := req.errorId() - return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error()))) + return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error())), result.GetDurations()) } } else { return req.respondNext(accountId, result.Payload, o.responseType, result, nextNextToken) @@ -206,7 +206,7 @@ func query[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], SEARCHRESULT var err *Error containerId, err = req.PathParam(o.containerUriParamName) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l = l.Str(o.containerUriParamName, log.SafeString(containerId)) } @@ -215,7 +215,7 @@ func query[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], SEARCHRESULT { v, ok, err := req.parseUIntParam(QueryParamLimit, 0) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } if ok { l = l.Uint(QueryParamLimit, v) @@ -232,11 +232,11 @@ func query[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], SEARCHRESULT supportedQueryParams = nextQueryParams if n, err := unnext(NextToken(s)); err != nil { errorId := req.errorId() - return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(err.Error()))) // TODO replace ErrorGeneric + return req.errorV(accountId, apiError(errorId, ErrorGeneric, withDetail(err.Error()))) // TODO replace ErrorGeneric } else { if q, ok, err := n.ForAccountId(accountId); err != nil { errorId := req.errorId() - return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(err.Error()))) // TODO replace ErrorGeneric + return req.errorV(accountId, apiError(errorId, ErrorGeneric, withDetail(err.Error()))) // TODO replace ErrorGeneric } else if ok { qp = q } else { @@ -310,10 +310,10 @@ func query[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], SEARCHRESULT return req.jmapError(accountId, e, result) case GroupwareError: errorId := req.errorId() - return req.error(accountId, apiError(errorId, e)) + return req.error(accountId, apiError(errorId, e), result.GetDurations()) default: errorId := req.errorId() - return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error()))) + return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error())), result.GetDurations()) } } else { /* @@ -350,7 +350,7 @@ func get[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.GetR if o.uriParamName != "" { id, err := req.PathParamDoc(o.uriParamName, "The unique identifier of the object to retrieve") if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l.Str(o.uriParamName, log.SafeString(id)) ids = single(id) @@ -368,10 +368,10 @@ func get[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jmap.GetR return req.jmapError(accountId, e, result) case GroupwareError: errorId := req.errorId() - return req.error(accountId, apiError(errorId, e)) + return req.error(accountId, apiError(errorId, e), result.GetDurations()) default: errorId := req.errorId() - return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error()))) + return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error())), result.GetDurations()) } } else { n := len(result.Payload.GetList()) @@ -405,7 +405,7 @@ func getFromMap[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jm l := req.logger.With().Str(logAccountId, log.SafeString(accountId)) id, err := req.PathParamDoc(o.uriParamName, "The unique identifier of the object to retrieve") if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l.Str(o.uriParamName, log.SafeString(id)) @@ -421,10 +421,10 @@ func getFromMap[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T], RESP jm return req.jmapError(accountId, e, result) case GroupwareError: errorId := req.errorId() - return req.error(accountId, apiError(errorId, e)) + return req.error(accountId, apiError(errorId, e), result.GetDurations()) default: errorId := req.errorId() - return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error()))) + return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error())), result.GetDurations()) } } else { if objs, ok := result.Payload[accountId]; ok { @@ -465,7 +465,7 @@ func changes[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } if ok { l = l.Uint(QueryParamMaxChanges, maxChanges) @@ -488,10 +488,10 @@ func changes[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR return req.jmapError(accountId, e, result) case GroupwareError: errorId := req.errorId() - return req.error(accountId, apiError(errorId, e)) + return req.error(accountId, apiError(errorId, e), result.GetDurations()) default: errorId := req.errorId() - return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error()))) + return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error())), result.GetDurations()) } } else { return req.respond(accountId, result.Payload, o.responseType, result) @@ -517,7 +517,7 @@ func delete[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR l := req.logger.With().Str(logAccountId, log.SafeString(accountId)) id, err := req.PathParamDoc(o.uriParamName, "The unique identifier of the object to delete") if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l.Str(o.uriParamName, log.SafeString(id)) @@ -533,10 +533,10 @@ func delete[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR return req.jmapError(accountId, e, result) case GroupwareError: errorId := req.errorId() - return req.error(accountId, apiError(errorId, e)) + return req.error(accountId, apiError(errorId, e), result.GetDurations()) default: errorId := req.errorId() - return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error()))) + return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error())), result.GetDurations()) } } else { for _, e := range result.Payload { @@ -546,12 +546,12 @@ func delete[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSONAR req.errorId(), o.failedToDeleteError, withDetail(e.Description), - )) + ), result.GetDurations()) } else { return req.error(accountId, apiError( req.errorId(), o.failedToDeleteError, - )) + ), result.GetDurations()) } } return req.noContent(accountId, o.responseType, result) @@ -582,7 +582,7 @@ func deleteMany[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSO if o.uriParamName != "" { pathId, err := req.PathParam(o.uriParamName) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } if ok { ids = append(ids, pathId) @@ -591,7 +591,7 @@ func deleteMany[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSO { queryIds, ok, err := req.parseOptStringListParam(QueryParamId) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } if ok { ids = append(ids, queryIds...) @@ -601,13 +601,13 @@ func deleteMany[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSO var bodyIds []string err := req.body(&bodyIds) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } ids = append(ids, bodyIds...) } switch len(ids) { case 0: - return req.noop(accountId) + return req.noop(accountId, zeroDurations) case 1: l.Str("id", log.SafeString(ids[0])) default: @@ -626,10 +626,10 @@ func deleteMany[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSO return req.jmapError(accountId, e, result) case GroupwareError: errorId := req.errorId() - return req.error(accountId, apiError(errorId, e)) + return req.error(accountId, apiError(errorId, e), result.GetDurations()) default: errorId := req.errorId() - return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error()))) + return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error())), result.GetDurations()) } } else { for _, e := range result.Payload { @@ -639,12 +639,12 @@ func deleteMany[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( //NOSO req.errorId(), o.failedToDeleteError, withDetail(e.Description), - )) + ), result.GetDurations()) } else { return req.error(accountId, apiError( req.errorId(), o.failedToDeleteError, - )) + ), result.GetDurations()) } } return req.noContent(accountId, o.responseType, result) @@ -668,7 +668,7 @@ func modify[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( l := req.logger.With().Str(logAccountId, log.SafeString(accountId)) id, err := req.PathParamDoc(o.uriParamName, "The unique identifier of the object to modify") if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } l.Str(o.uriParamName, log.SafeString(id)) @@ -679,7 +679,7 @@ func modify[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( var change CHANGE err = req.body(&change) if err != nil { - return req.error(accountId, err) + return req.errorV(accountId, err) } logger := log.From(l) @@ -690,10 +690,10 @@ func modify[T jmap.Foo, CHANGE jmap.Change, CHANGES jmap.Changes[T]]( return req.jmapError(accountId, e, result) case GroupwareError: errorId := req.errorId() - return req.error(accountId, apiError(errorId, e)) + return req.error(accountId, apiError(errorId, e), result.GetDurations()) default: errorId := req.errorId() - return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error()))) + return req.error(accountId, apiError(errorId, ErrorGeneric, withDetail(e.Error())), result.GetDurations()) } } else { return req.respond(accountId, result.Payload, o.responseType, result)