groupware: add tracking of backend call durations

* add new configuration setting GROUPWARE_SEND_DURATIONS_RESPONSE
   (defaults to false)

 * keep track of lists of durations of backend calls

 * when enabled, report them as response headers Durations (human
   readable) and Durations-Nanos (as raw nanosecond values for machine
   consumption)
This commit is contained in:
Pascal Bleser
2026-06-03 18:36:21 +02:00
parent c807c64d1c
commit 6cbd695d7f
38 changed files with 543 additions and 345 deletions

2
.vscode/launch.json vendored
View File

@@ -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",

View File

@@ -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()}
}

View File

@@ -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) {

View File

@@ -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{}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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))

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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 {

View File

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

View File

@@ -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)
}
}

View File

@@ -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",

View File

@@ -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"`
}

View File

@@ -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)
}

View File

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

View File

@@ -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)

View File

@@ -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)))
}
})
}

View File

@@ -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.
}
}

View File

@@ -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)

View File

@@ -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, ",")

View File

@@ -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)

View File

@@ -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...)
}

View File

@@ -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)

View File

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

View File

@@ -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())
}
}

View File

@@ -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 {

View File

@@ -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{}

View File

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

View File

@@ -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

View File

@@ -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,
}
}

View File

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

View File

@@ -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{})
}

View File

@@ -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)