From fa00debe3784f700f7c9aaf200621607de1654f8 Mon Sep 17 00:00:00 2001 From: Marco Ardizzone Date: Mon, 3 Aug 2026 13:56:10 +0200 Subject: [PATCH] feat(api): add the teams query endpoint Team CRUD had create, get and update but no way to enumerate a project's teams. Add POST /teams/query, taking the same required project_id query parameter as create and get, and gated exactly like getTeam. Results are paginated with an opaque cursor and can be filtered and sorted by createdAt. Each listed team answers with the shared team body, so it carries its status: teams of every status are listed, and a deactivated team is told apart by that field rather than by absence. The filter-field enum lives in team-filter-field.yaml rather than filter-field.yaml because ogen derives the generated type name from the file name, and projects/query already claims FilterField. Closes #620. Co-Authored-By: Claude Opus 5 --- .changeset/team-list-query.md | 5 + api/generated/oas_client_gen.go | 148 +++ api/generated/oas_defaults_gen.go | 8 + api/generated/oas_handlers_gen.go | 203 ++++ api/generated/oas_interfaces_gen.go | 4 + api/generated/oas_json_gen.go | 965 ++++++++++++++++-- api/generated/oas_operations_gen.go | 1 + api/generated/oas_parameters_gen.go | 73 ++ api/generated/oas_request_decoders_gen.go | 79 ++ api/generated/oas_request_encoders_gen.go | 14 + api/generated/oas_response_decoders_gen.go | 267 +++++ api/generated/oas_response_encoders_gen.go | 118 +++ api/generated/oas_router_gen.go | 85 +- api/generated/oas_schemas_gen.go | 248 +++++ api/generated/oas_security_gen.go | 3 + api/generated/oas_server_gen.go | 7 + api/generated/oas_unimplemented_gen.go | 10 + api/generated/oas_validators_gen.go | 208 ++++ .../endpoints/teams/query/methods.yaml | 65 ++ .../teams/query/query-teams-request.yaml | 38 + .../teams/query/query-teams-response.yaml | 16 + .../teams/query/team-filter-field.yaml | 3 + api/openapi/openapi-spec.yaml | 2 + internal/api/integration_test/authz_test.go | 11 + internal/api/integration_test/team_test.go | 182 ++++ internal/api/team.go | 47 + 26 files changed, 2690 insertions(+), 120 deletions(-) create mode 100644 .changeset/team-list-query.md create mode 100644 api/openapi/endpoints/teams/query/methods.yaml create mode 100644 api/openapi/endpoints/teams/query/query-teams-request.yaml create mode 100644 api/openapi/endpoints/teams/query/query-teams-response.yaml create mode 100644 api/openapi/endpoints/teams/query/team-filter-field.yaml diff --git a/.changeset/team-list-query.md b/.changeset/team-list-query.md new file mode 100644 index 000000000..e45c2898b --- /dev/null +++ b/.changeset/team-list-query.md @@ -0,0 +1,5 @@ +--- +"@zitadel/server": minor +--- + +You can now list the teams of a project with `POST /teams/query`. Pass the project as the required `project_id` query parameter, page through results with `limit` and `page_token`, and filter or sort by `createdAt`. Every team in the result carries its `status`, and deactivated teams are returned alongside active ones. The endpoint needs the same read access as `GET /teams/{team_id}`. diff --git a/api/generated/oas_client_gen.go b/api/generated/oas_client_gen.go index ace164543..955b8c4a4 100644 --- a/api/generated/oas_client_gen.go +++ b/api/generated/oas_client_gen.go @@ -403,6 +403,13 @@ type Invoker interface { // // POST /projects/query QueryProjects(ctx context.Context, request *QueryProjectsRequest) (QueryProjectsRes, error) + // QueryTeams invokes queryTeams operation. + // + // Returns the teams of a project, paginated with a cursor. + // Teams of every lifecycle status are returned; each carries its `status`. + // + // POST /teams/query + QueryTeams(ctx context.Context, request *QueryTeamsRequest, params QueryTeamsParams) (QueryTeamsRes, error) // RevokeMySession invokes revokeMySession operation. // // Revokes the session immediately (`state: revoked`). This is the logout operation. @@ -6966,6 +6973,147 @@ func (c *Client) sendQueryProjects(ctx context.Context, request *QueryProjectsRe return result, nil } +// QueryTeams invokes queryTeams operation. +// +// Returns the teams of a project, paginated with a cursor. +// Teams of every lifecycle status are returned; each carries its `status`. +// +// POST /teams/query +func (c *Client) QueryTeams(ctx context.Context, request *QueryTeamsRequest, params QueryTeamsParams) (QueryTeamsRes, error) { + res, err := c.sendQueryTeams(ctx, request, params) + return res, err +} + +func (c *Client) sendQueryTeams(ctx context.Context, request *QueryTeamsRequest, params QueryTeamsParams) (res QueryTeamsRes, err error) { + // Validate request before sending. + if err := func() error { + if err := request.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("queryTeams"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.URLTemplateKey.String("/teams/query"), + } + otelAttrs = append(otelAttrs, c.cfg.Attributes...) + + // Run stopwatch. + startTime := time.Now() + defer func() { + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedDuration := time.Since(startTime) + c.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), metric.WithAttributes(otelAttrs...)) + }() + + // Increment request counter. + c.requests.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + + // Start a span for this request. + ctx, span := c.cfg.Tracer.Start(ctx, QueryTeamsOperation, + trace.WithAttributes(otelAttrs...), + clientSpanKind, + ) + // Track stage for error reporting. + var stage string + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, stage) + c.errors.Add(ctx, 1, metric.WithAttributes(otelAttrs...)) + } + span.End() + }() + + stage = "BuildURL" + u := uri.Clone(c.requestURL(ctx)) + var pathParts [1]string + pathParts[0] = "/teams/query" + uri.AddPathParts(u, pathParts[:]...) + + stage = "EncodeQueryParams" + q := uri.NewQueryEncoder() + { + // Encode "project_id" parameter. + cfg := uri.QueryParameterEncodingConfig{ + Name: "project_id", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.EncodeParam(cfg, func(e uri.Encoder) error { + if unwrapped := string(params.ProjectID); true { + return e.EncodeValue(conv.StringToString(unwrapped)) + } + return nil + }); err != nil { + return res, errors.Wrap(err, "encode query") + } + } + u.RawQuery = q.Values().Encode() + + stage = "EncodeRequest" + r, err := ht.NewRequest(ctx, "POST", u) + if err != nil { + return res, errors.Wrap(err, "create request") + } + if err := encodeQueryTeamsRequest(request, r); err != nil { + return res, errors.Wrap(err, "encode request") + } + + { + type bitset = [1]uint8 + var satisfied bitset + { + stage = "Security:OAuth2" + switch err := c.securityOAuth2(ctx, QueryTeamsOperation, r); { + case err == nil: // if NO error + satisfied[0] |= 1 << 0 + case errors.Is(err, ogenerrors.ErrSkipClientSecurity): + // Skip this security. + default: + return res, errors.Wrap(err, "security \"OAuth2\"") + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + return res, ogenerrors.ErrSecurityRequirementIsNotSatisfied + } + } + + stage = "SendRequest" + resp, err := c.cfg.Client.Do(r) + if err != nil { + return res, errors.Wrap(err, "do request") + } + body := resp.Body + defer body.Close() + + stage = "DecodeResponse" + result, err := decodeQueryTeamsResponse(resp) + if err != nil { + return res, errors.Wrap(err, "decode response") + } + + return result, nil +} + // RevokeMySession invokes revokeMySession operation. // // Revokes the session immediately (`state: revoked`). This is the logout operation. diff --git a/api/generated/oas_defaults_gen.go b/api/generated/oas_defaults_gen.go index 99a888cb5..fdd1d1646 100644 --- a/api/generated/oas_defaults_gen.go +++ b/api/generated/oas_defaults_gen.go @@ -62,6 +62,14 @@ func (s *QueryProjectsRequest) setDefaults() { } } +// setDefaults set default value of fields. +func (s *QueryTeamsRequest) setDefaults() { + { + val := int(20) + s.Limit.SetTo(Limit(val)) + } +} + // setDefaults set default value of fields. func (s *StepAction) setDefaults() { { diff --git a/api/generated/oas_handlers_gen.go b/api/generated/oas_handlers_gen.go index 480b64d3a..42df767c6 100644 --- a/api/generated/oas_handlers_gen.go +++ b/api/generated/oas_handlers_gen.go @@ -8511,6 +8511,209 @@ func (s *Server) handleQueryProjectsRequest(args [0]string, argsEscaped bool, w } } +// handleQueryTeamsRequest handles queryTeams operation. +// +// Returns the teams of a project, paginated with a cursor. +// Teams of every lifecycle status are returned; each carries its `status`. +// +// POST /teams/query +func (s *Server) handleQueryTeamsRequest(args [0]string, argsEscaped bool, w http.ResponseWriter, r *http.Request) { + statusWriter := &codeRecorder{ResponseWriter: w} + w = statusWriter + otelAttrs := []attribute.KeyValue{ + otelogen.OperationID("queryTeams"), + semconv.HTTPRequestMethodKey.String("POST"), + semconv.HTTPRouteKey.String("/teams/query"), + } + // Add attributes from config. + otelAttrs = append(otelAttrs, s.cfg.Attributes...) + + // Start a span for this request. + ctx, span := s.cfg.Tracer.Start(r.Context(), QueryTeamsOperation, + trace.WithAttributes(otelAttrs...), + serverSpanKind, + ) + defer span.End() + + // Add Labeler to context. + labeler := &Labeler{attrs: otelAttrs} + ctx = contextWithLabeler(ctx, labeler) + + // Run stopwatch. + startTime := time.Now() + defer func() { + elapsedDuration := time.Since(startTime) + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + code := statusWriter.status + if code != 0 { + codeAttr := semconv.HTTPResponseStatusCode(code) + attrs = append(attrs, codeAttr) + span.SetAttributes(codeAttr) + } + attrOpt := metric.WithAttributes(attrs...) + + // Increment request counter. + s.requests.Add(ctx, 1, attrOpt) + + // Use floating point division here for higher precision (instead of Millisecond method). + s.duration.Record(ctx, float64(elapsedDuration)/float64(time.Millisecond), attrOpt) + }() + + var ( + recordError = func(stage string, err error) { + span.RecordError(err) + + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#status + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, + // unless there was another error (e.g., network error receiving the response body; or 3xx codes with + // max redirects exceeded), in which case status MUST be set to Error. + code := statusWriter.status + if code < 100 || code >= 500 { + span.SetStatus(codes.Error, stage) + } + + attrSet := labeler.AttributeSet() + attrs := attrSet.ToSlice() + if code != 0 { + attrs = append(attrs, semconv.HTTPResponseStatusCode(code)) + } + + s.errors.Add(ctx, 1, metric.WithAttributes(attrs...)) + } + err error + opErrContext = ogenerrors.OperationContext{ + Name: QueryTeamsOperation, + ID: "queryTeams", + } + ) + { + type bitset = [1]uint8 + var satisfied bitset + { + sctx, ok, err := s.securityOAuth2(ctx, QueryTeamsOperation, r) + if err != nil { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Security: "OAuth2", + Err: err, + } + defer recordError("Security:OAuth2", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + if ok { + satisfied[0] |= 1 << 0 + ctx = sctx + } + } + + if ok := func() bool { + nextRequirement: + for _, requirement := range []bitset{ + {0b00000001}, + } { + for i, mask := range requirement { + if satisfied[i]&mask != mask { + continue nextRequirement + } + } + return true + } + return false + }(); !ok { + err = &ogenerrors.SecurityError{ + OperationContext: opErrContext, + Err: ogenerrors.ErrSecurityRequirementIsNotSatisfied, + } + defer recordError("Security", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + } + params, err := decodeQueryTeamsParams(args, argsEscaped, r) + if err != nil { + err = &ogenerrors.DecodeParamsError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeParams", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + var rawBody []byte + request, rawBody, close, err := s.decodeQueryTeamsRequest(r) + if err != nil { + err = &ogenerrors.DecodeRequestError{ + OperationContext: opErrContext, + Err: err, + } + defer recordError("DecodeRequest", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + defer func() { + if err := close(); err != nil { + recordError("CloseRequest", err) + } + }() + + var response QueryTeamsRes + if m := s.cfg.Middleware; m != nil { + mreq := middleware.Request{ + Context: ctx, + OperationName: QueryTeamsOperation, + OperationSummary: "Query teams", + OperationID: "queryTeams", + Body: request, + RawBody: rawBody, + Params: middleware.Parameters{ + { + Name: "project_id", + In: "query", + }: params.ProjectID, + }, + Raw: r, + } + + type ( + Request = *QueryTeamsRequest + Params = QueryTeamsParams + Response = QueryTeamsRes + ) + response, err = middleware.HookMiddleware[ + Request, + Params, + Response, + ]( + m, + mreq, + unpackQueryTeamsParams, + func(ctx context.Context, request Request, params Params) (response Response, err error) { + response, err = s.h.QueryTeams(ctx, request, params) + return response, err + }, + ) + } else { + response, err = s.h.QueryTeams(ctx, request, params) + } + if err != nil { + defer recordError("Internal", err) + s.cfg.ErrorHandler(ctx, w, r, err) + return + } + + if err := encodeQueryTeamsResponse(response, w, span); err != nil { + defer recordError("EncodeResponse", err) + if !errors.Is(err, ht.ErrInternalServerErrorResponse) { + s.cfg.ErrorHandler(ctx, w, r, err) + } + return + } +} + // handleRevokeMySessionRequest handles revokeMySession operation. // // Revokes the session immediately (`state: revoked`). This is the logout operation. diff --git a/api/generated/oas_interfaces_gen.go b/api/generated/oas_interfaces_gen.go index 97ea84bd9..f806567dd 100644 --- a/api/generated/oas_interfaces_gen.go +++ b/api/generated/oas_interfaces_gen.go @@ -185,6 +185,10 @@ type QueryProjectsRes interface { queryProjectsRes() } +type QueryTeamsRes interface { + queryTeamsRes() +} + type RevokeMySessionRes interface { revokeMySessionRes() } diff --git a/api/generated/oas_json_gen.go b/api/generated/oas_json_gen.go index 8fc90dfa4..5dab38c46 100644 --- a/api/generated/oas_json_gen.go +++ b/api/generated/oas_json_gen.go @@ -17443,6 +17443,39 @@ func (s *OptQueryProjectsRequestSorting) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode encodes QueryTeamsRequestSorting as json. +func (o OptQueryTeamsRequestSorting) Encode(e *jx.Encoder) { + if !o.Set { + return + } + o.Value.Encode(e) +} + +// Decode decodes QueryTeamsRequestSorting from json. +func (o *OptQueryTeamsRequestSorting) Decode(d *jx.Decoder) error { + if o == nil { + return errors.New("invalid: unable to decode OptQueryTeamsRequestSorting to nil") + } + o.Set = true + if err := o.Value.Decode(d); err != nil { + return err + } + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s OptQueryTeamsRequestSorting) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *OptQueryTeamsRequestSorting) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode encodes SchemaURI as json. func (o OptSchemaURI) Encode(e *jx.Encoder) { if !o.Set { @@ -19170,50 +19203,610 @@ func (s *ProjectResponse) Decode(d *jx.Decoder) error { if err != nil { return err } - s.PreviewOrigins = append(s.PreviewOrigins, elem) + s.PreviewOrigins = append(s.PreviewOrigins, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"previewOrigins\"") + } + case "createdAt": + requiredBitSet[0] |= 1 << 3 + if err := func() error { + v, err := json.DecodeDateTime(d) + s.CreatedAt = v + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"createdAt\"") + } + case "updatedAt": + requiredBitSet[0] |= 1 << 4 + if err := func() error { + v, err := json.DecodeDateTime(d) + s.UpdatedAt = v + if err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"updatedAt\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode ProjectResponse") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00011011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfProjectResponse) { + name = jsonFieldsNameOfProjectResponse[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *ProjectResponse) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *ProjectResponse) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes QueryProjectsBadRequest as json. +func (s *QueryProjectsBadRequest) Encode(e *jx.Encoder) { + unwrapped := (*ErrorDetails)(s) + + unwrapped.Encode(e) +} + +// Decode decodes QueryProjectsBadRequest from json. +func (s *QueryProjectsBadRequest) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode QueryProjectsBadRequest to nil") + } + var unwrapped ErrorDetails + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = QueryProjectsBadRequest(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *QueryProjectsBadRequest) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *QueryProjectsBadRequest) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes QueryProjectsForbidden as json. +func (s *QueryProjectsForbidden) Encode(e *jx.Encoder) { + unwrapped := (*ErrorDetails)(s) + + unwrapped.Encode(e) +} + +// Decode decodes QueryProjectsForbidden from json. +func (s *QueryProjectsForbidden) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode QueryProjectsForbidden to nil") + } + var unwrapped ErrorDetails + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = QueryProjectsForbidden(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *QueryProjectsForbidden) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *QueryProjectsForbidden) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *QueryProjectsRequest) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *QueryProjectsRequest) encodeFields(e *jx.Encoder) { + { + if s.Limit.Set { + e.FieldStart("limit") + s.Limit.Encode(e) + } + } + { + if s.PageToken.Set { + e.FieldStart("page_token") + s.PageToken.Encode(e) + } + } + { + if s.Sorting.Set { + e.FieldStart("sorting") + s.Sorting.Encode(e) + } + } + { + if s.Filter != nil { + e.FieldStart("filter") + e.ArrStart() + for _, elem := range s.Filter { + elem.Encode(e) + } + e.ArrEnd() + } + } +} + +var jsonFieldsNameOfQueryProjectsRequest = [4]string{ + 0: "limit", + 1: "page_token", + 2: "sorting", + 3: "filter", +} + +// Decode decodes QueryProjectsRequest from json. +func (s *QueryProjectsRequest) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode QueryProjectsRequest to nil") + } + s.setDefaults() + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "limit": + if err := func() error { + s.Limit.Reset() + if err := s.Limit.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"limit\"") + } + case "page_token": + if err := func() error { + s.PageToken.Reset() + if err := s.PageToken.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"page_token\"") + } + case "sorting": + if err := func() error { + s.Sorting.Reset() + if err := s.Sorting.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"sorting\"") + } + case "filter": + if err := func() error { + s.Filter = make([]QueryProjectsRequestFilterItem, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem QueryProjectsRequestFilterItem + if err := elem.Decode(d); err != nil { + return err + } + s.Filter = append(s.Filter, elem) + return nil + }); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"filter\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode QueryProjectsRequest") + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *QueryProjectsRequest) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *QueryProjectsRequest) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *QueryProjectsRequestFilterItem) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *QueryProjectsRequestFilterItem) encodeFields(e *jx.Encoder) { + { + e.FieldStart("field") + s.Field.Encode(e) + } + { + if s.Value.Set { + e.FieldStart("value") + s.Value.Encode(e) + } + } + { + e.FieldStart("operation") + s.Operation.Encode(e) + } +} + +var jsonFieldsNameOfQueryProjectsRequestFilterItem = [3]string{ + 0: "field", + 1: "value", + 2: "operation", +} + +// Decode decodes QueryProjectsRequestFilterItem from json. +func (s *QueryProjectsRequestFilterItem) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode QueryProjectsRequestFilterItem to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "field": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Field.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"field\"") + } + case "value": + if err := func() error { + s.Value.Reset() + if err := s.Value.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"value\"") + } + case "operation": + requiredBitSet[0] |= 1 << 2 + if err := func() error { + if err := s.Operation.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"operation\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode QueryProjectsRequestFilterItem") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000101, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfQueryProjectsRequestFilterItem) { + name = jsonFieldsNameOfQueryProjectsRequestFilterItem[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *QueryProjectsRequestFilterItem) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *QueryProjectsRequestFilterItem) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *QueryProjectsRequestSorting) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *QueryProjectsRequestSorting) encodeFields(e *jx.Encoder) { + { + e.FieldStart("field") + s.Field.Encode(e) + } + { + e.FieldStart("direction") + s.Direction.Encode(e) + } +} + +var jsonFieldsNameOfQueryProjectsRequestSorting = [2]string{ + 0: "field", + 1: "direction", +} + +// Decode decodes QueryProjectsRequestSorting from json. +func (s *QueryProjectsRequestSorting) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode QueryProjectsRequestSorting to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "field": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + if err := s.Field.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"field\"") + } + case "direction": + requiredBitSet[0] |= 1 << 1 + if err := func() error { + if err := s.Direction.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"direction\"") + } + default: + return d.Skip() + } + return nil + }); err != nil { + return errors.Wrap(err, "decode QueryProjectsRequestSorting") + } + // Validate required fields. + var failures []validate.FieldError + for i, mask := range [1]uint8{ + 0b00000011, + } { + if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { + // Mask only required fields and check equality to mask using XOR. + // + // If XOR result is not zero, result is not equal to expected, so some fields are missed. + // Bits of fields which would be set are actually bits of missed fields. + missed := bits.OnesCount8(result) + for bitN := 0; bitN < missed; bitN++ { + bitIdx := bits.TrailingZeros8(result) + fieldIdx := i*8 + bitIdx + var name string + if fieldIdx < len(jsonFieldsNameOfQueryProjectsRequestSorting) { + name = jsonFieldsNameOfQueryProjectsRequestSorting[fieldIdx] + } else { + name = strconv.Itoa(fieldIdx) + } + failures = append(failures, validate.FieldError{ + Name: name, + Error: validate.ErrFieldRequired, + }) + // Reset bit. + result &^= 1 << bitIdx + } + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *QueryProjectsRequestSorting) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *QueryProjectsRequestSorting) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode implements json.Marshaler. +func (s *QueryProjectsResponse) Encode(e *jx.Encoder) { + e.ObjStart() + s.encodeFields(e) + e.ObjEnd() +} + +// encodeFields encodes fields. +func (s *QueryProjectsResponse) encodeFields(e *jx.Encoder) { + { + e.FieldStart("projects") + e.ArrStart() + for _, elem := range s.Projects { + elem.Encode(e) + } + e.ArrEnd() + } + { + if s.NextPageToken.Set { + e.FieldStart("next_page_token") + s.NextPageToken.Encode(e) + } + } +} + +var jsonFieldsNameOfQueryProjectsResponse = [2]string{ + 0: "projects", + 1: "next_page_token", +} + +// Decode decodes QueryProjectsResponse from json. +func (s *QueryProjectsResponse) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode QueryProjectsResponse to nil") + } + var requiredBitSet [1]uint8 + + if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { + switch string(k) { + case "projects": + requiredBitSet[0] |= 1 << 0 + if err := func() error { + s.Projects = make([]ProjectResponse, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem ProjectResponse + if err := elem.Decode(d); err != nil { + return err + } + s.Projects = append(s.Projects, elem) return nil }); err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"previewOrigins\"") - } - case "createdAt": - requiredBitSet[0] |= 1 << 3 - if err := func() error { - v, err := json.DecodeDateTime(d) - s.CreatedAt = v - if err != nil { - return err - } - return nil - }(); err != nil { - return errors.Wrap(err, "decode field \"createdAt\"") + return errors.Wrap(err, "decode field \"projects\"") } - case "updatedAt": - requiredBitSet[0] |= 1 << 4 + case "next_page_token": if err := func() error { - v, err := json.DecodeDateTime(d) - s.UpdatedAt = v - if err != nil { + s.NextPageToken.Reset() + if err := s.NextPageToken.Decode(d); err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"updatedAt\"") + return errors.Wrap(err, "decode field \"next_page_token\"") } default: return d.Skip() } return nil }); err != nil { - return errors.Wrap(err, "decode ProjectResponse") + return errors.Wrap(err, "decode QueryProjectsResponse") } // Validate required fields. var failures []validate.FieldError for i, mask := range [1]uint8{ - 0b00011011, + 0b00000001, } { if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { // Mask only required fields and check equality to mask using XOR. @@ -19225,8 +19818,8 @@ func (s *ProjectResponse) Decode(d *jx.Decoder) error { bitIdx := bits.TrailingZeros8(result) fieldIdx := i*8 + bitIdx var name string - if fieldIdx < len(jsonFieldsNameOfProjectResponse) { - name = jsonFieldsNameOfProjectResponse[fieldIdx] + if fieldIdx < len(jsonFieldsNameOfQueryProjectsResponse) { + name = jsonFieldsNameOfQueryProjectsResponse[fieldIdx] } else { name = strconv.Itoa(fieldIdx) } @@ -19247,29 +19840,29 @@ func (s *ProjectResponse) Decode(d *jx.Decoder) error { } // MarshalJSON implements stdjson.Marshaler. -func (s *ProjectResponse) MarshalJSON() ([]byte, error) { +func (s *QueryProjectsResponse) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *ProjectResponse) UnmarshalJSON(data []byte) error { +func (s *QueryProjectsResponse) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes QueryProjectsBadRequest as json. -func (s *QueryProjectsBadRequest) Encode(e *jx.Encoder) { +// Encode encodes QueryProjectsTooManyRequests as json. +func (s *QueryProjectsTooManyRequests) Encode(e *jx.Encoder) { unwrapped := (*ErrorDetails)(s) unwrapped.Encode(e) } -// Decode decodes QueryProjectsBadRequest from json. -func (s *QueryProjectsBadRequest) Decode(d *jx.Decoder) error { +// Decode decodes QueryProjectsTooManyRequests from json. +func (s *QueryProjectsTooManyRequests) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode QueryProjectsBadRequest to nil") + return errors.New("invalid: unable to decode QueryProjectsTooManyRequests to nil") } var unwrapped ErrorDetails if err := func() error { @@ -19280,34 +19873,34 @@ func (s *QueryProjectsBadRequest) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "alias") } - *s = QueryProjectsBadRequest(unwrapped) + *s = QueryProjectsTooManyRequests(unwrapped) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s *QueryProjectsBadRequest) MarshalJSON() ([]byte, error) { +func (s *QueryProjectsTooManyRequests) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *QueryProjectsBadRequest) UnmarshalJSON(data []byte) error { +func (s *QueryProjectsTooManyRequests) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes QueryProjectsForbidden as json. -func (s *QueryProjectsForbidden) Encode(e *jx.Encoder) { +// Encode encodes QueryProjectsUnauthorized as json. +func (s *QueryProjectsUnauthorized) Encode(e *jx.Encoder) { unwrapped := (*ErrorDetails)(s) unwrapped.Encode(e) } -// Decode decodes QueryProjectsForbidden from json. -func (s *QueryProjectsForbidden) Decode(d *jx.Decoder) error { +// Decode decodes QueryProjectsUnauthorized from json. +func (s *QueryProjectsUnauthorized) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode QueryProjectsForbidden to nil") + return errors.New("invalid: unable to decode QueryProjectsUnauthorized to nil") } var unwrapped ErrorDetails if err := func() error { @@ -19318,32 +19911,146 @@ func (s *QueryProjectsForbidden) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "alias") } - *s = QueryProjectsForbidden(unwrapped) + *s = QueryProjectsUnauthorized(unwrapped) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s *QueryProjectsForbidden) MarshalJSON() ([]byte, error) { +func (s *QueryProjectsUnauthorized) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *QueryProjectsForbidden) UnmarshalJSON(data []byte) error { +func (s *QueryProjectsUnauthorized) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes QueryTeamsBadRequest as json. +func (s *QueryTeamsBadRequest) Encode(e *jx.Encoder) { + unwrapped := (*ErrorDetails)(s) + + unwrapped.Encode(e) +} + +// Decode decodes QueryTeamsBadRequest from json. +func (s *QueryTeamsBadRequest) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode QueryTeamsBadRequest to nil") + } + var unwrapped ErrorDetails + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = QueryTeamsBadRequest(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *QueryTeamsBadRequest) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *QueryTeamsBadRequest) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes QueryTeamsForbidden as json. +func (s *QueryTeamsForbidden) Encode(e *jx.Encoder) { + unwrapped := (*ErrorDetails)(s) + + unwrapped.Encode(e) +} + +// Decode decodes QueryTeamsForbidden from json. +func (s *QueryTeamsForbidden) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode QueryTeamsForbidden to nil") + } + var unwrapped ErrorDetails + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = QueryTeamsForbidden(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *QueryTeamsForbidden) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *QueryTeamsForbidden) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + +// Encode encodes QueryTeamsNotFound as json. +func (s *QueryTeamsNotFound) Encode(e *jx.Encoder) { + unwrapped := (*ErrorDetails)(s) + + unwrapped.Encode(e) +} + +// Decode decodes QueryTeamsNotFound from json. +func (s *QueryTeamsNotFound) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode QueryTeamsNotFound to nil") + } + var unwrapped ErrorDetails + if err := func() error { + if err := unwrapped.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "alias") + } + *s = QueryTeamsNotFound(unwrapped) + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s *QueryTeamsNotFound) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *QueryTeamsNotFound) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } // Encode implements json.Marshaler. -func (s *QueryProjectsRequest) Encode(e *jx.Encoder) { +func (s *QueryTeamsRequest) Encode(e *jx.Encoder) { e.ObjStart() s.encodeFields(e) e.ObjEnd() } // encodeFields encodes fields. -func (s *QueryProjectsRequest) encodeFields(e *jx.Encoder) { +func (s *QueryTeamsRequest) encodeFields(e *jx.Encoder) { { if s.Limit.Set { e.FieldStart("limit") @@ -19374,17 +20081,17 @@ func (s *QueryProjectsRequest) encodeFields(e *jx.Encoder) { } } -var jsonFieldsNameOfQueryProjectsRequest = [4]string{ +var jsonFieldsNameOfQueryTeamsRequest = [4]string{ 0: "limit", 1: "page_token", 2: "sorting", 3: "filter", } -// Decode decodes QueryProjectsRequest from json. -func (s *QueryProjectsRequest) Decode(d *jx.Decoder) error { +// Decode decodes QueryTeamsRequest from json. +func (s *QueryTeamsRequest) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode QueryProjectsRequest to nil") + return errors.New("invalid: unable to decode QueryTeamsRequest to nil") } s.setDefaults() @@ -19422,9 +20129,9 @@ func (s *QueryProjectsRequest) Decode(d *jx.Decoder) error { } case "filter": if err := func() error { - s.Filter = make([]QueryProjectsRequestFilterItem, 0) + s.Filter = make([]QueryTeamsRequestFilterItem, 0) if err := d.Arr(func(d *jx.Decoder) error { - var elem QueryProjectsRequestFilterItem + var elem QueryTeamsRequestFilterItem if err := elem.Decode(d); err != nil { return err } @@ -19442,34 +20149,34 @@ func (s *QueryProjectsRequest) Decode(d *jx.Decoder) error { } return nil }); err != nil { - return errors.Wrap(err, "decode QueryProjectsRequest") + return errors.Wrap(err, "decode QueryTeamsRequest") } return nil } // MarshalJSON implements stdjson.Marshaler. -func (s *QueryProjectsRequest) MarshalJSON() ([]byte, error) { +func (s *QueryTeamsRequest) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *QueryProjectsRequest) UnmarshalJSON(data []byte) error { +func (s *QueryTeamsRequest) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } // Encode implements json.Marshaler. -func (s *QueryProjectsRequestFilterItem) Encode(e *jx.Encoder) { +func (s *QueryTeamsRequestFilterItem) Encode(e *jx.Encoder) { e.ObjStart() s.encodeFields(e) e.ObjEnd() } // encodeFields encodes fields. -func (s *QueryProjectsRequestFilterItem) encodeFields(e *jx.Encoder) { +func (s *QueryTeamsRequestFilterItem) encodeFields(e *jx.Encoder) { { e.FieldStart("field") s.Field.Encode(e) @@ -19486,16 +20193,16 @@ func (s *QueryProjectsRequestFilterItem) encodeFields(e *jx.Encoder) { } } -var jsonFieldsNameOfQueryProjectsRequestFilterItem = [3]string{ +var jsonFieldsNameOfQueryTeamsRequestFilterItem = [3]string{ 0: "field", 1: "value", 2: "operation", } -// Decode decodes QueryProjectsRequestFilterItem from json. -func (s *QueryProjectsRequestFilterItem) Decode(d *jx.Decoder) error { +// Decode decodes QueryTeamsRequestFilterItem from json. +func (s *QueryTeamsRequestFilterItem) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode QueryProjectsRequestFilterItem to nil") + return errors.New("invalid: unable to decode QueryTeamsRequestFilterItem to nil") } var requiredBitSet [1]uint8 @@ -19536,7 +20243,7 @@ func (s *QueryProjectsRequestFilterItem) Decode(d *jx.Decoder) error { } return nil }); err != nil { - return errors.Wrap(err, "decode QueryProjectsRequestFilterItem") + return errors.Wrap(err, "decode QueryTeamsRequestFilterItem") } // Validate required fields. var failures []validate.FieldError @@ -19553,8 +20260,8 @@ func (s *QueryProjectsRequestFilterItem) Decode(d *jx.Decoder) error { bitIdx := bits.TrailingZeros8(result) fieldIdx := i*8 + bitIdx var name string - if fieldIdx < len(jsonFieldsNameOfQueryProjectsRequestFilterItem) { - name = jsonFieldsNameOfQueryProjectsRequestFilterItem[fieldIdx] + if fieldIdx < len(jsonFieldsNameOfQueryTeamsRequestFilterItem) { + name = jsonFieldsNameOfQueryTeamsRequestFilterItem[fieldIdx] } else { name = strconv.Itoa(fieldIdx) } @@ -19575,27 +20282,27 @@ func (s *QueryProjectsRequestFilterItem) Decode(d *jx.Decoder) error { } // MarshalJSON implements stdjson.Marshaler. -func (s *QueryProjectsRequestFilterItem) MarshalJSON() ([]byte, error) { +func (s *QueryTeamsRequestFilterItem) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *QueryProjectsRequestFilterItem) UnmarshalJSON(data []byte) error { +func (s *QueryTeamsRequestFilterItem) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } // Encode implements json.Marshaler. -func (s *QueryProjectsRequestSorting) Encode(e *jx.Encoder) { +func (s *QueryTeamsRequestSorting) Encode(e *jx.Encoder) { e.ObjStart() s.encodeFields(e) e.ObjEnd() } // encodeFields encodes fields. -func (s *QueryProjectsRequestSorting) encodeFields(e *jx.Encoder) { +func (s *QueryTeamsRequestSorting) encodeFields(e *jx.Encoder) { { e.FieldStart("field") s.Field.Encode(e) @@ -19606,15 +20313,15 @@ func (s *QueryProjectsRequestSorting) encodeFields(e *jx.Encoder) { } } -var jsonFieldsNameOfQueryProjectsRequestSorting = [2]string{ +var jsonFieldsNameOfQueryTeamsRequestSorting = [2]string{ 0: "field", 1: "direction", } -// Decode decodes QueryProjectsRequestSorting from json. -func (s *QueryProjectsRequestSorting) Decode(d *jx.Decoder) error { +// Decode decodes QueryTeamsRequestSorting from json. +func (s *QueryTeamsRequestSorting) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode QueryProjectsRequestSorting to nil") + return errors.New("invalid: unable to decode QueryTeamsRequestSorting to nil") } var requiredBitSet [1]uint8 @@ -19645,7 +20352,7 @@ func (s *QueryProjectsRequestSorting) Decode(d *jx.Decoder) error { } return nil }); err != nil { - return errors.Wrap(err, "decode QueryProjectsRequestSorting") + return errors.Wrap(err, "decode QueryTeamsRequestSorting") } // Validate required fields. var failures []validate.FieldError @@ -19662,8 +20369,8 @@ func (s *QueryProjectsRequestSorting) Decode(d *jx.Decoder) error { bitIdx := bits.TrailingZeros8(result) fieldIdx := i*8 + bitIdx var name string - if fieldIdx < len(jsonFieldsNameOfQueryProjectsRequestSorting) { - name = jsonFieldsNameOfQueryProjectsRequestSorting[fieldIdx] + if fieldIdx < len(jsonFieldsNameOfQueryTeamsRequestSorting) { + name = jsonFieldsNameOfQueryTeamsRequestSorting[fieldIdx] } else { name = strconv.Itoa(fieldIdx) } @@ -19684,31 +20391,31 @@ func (s *QueryProjectsRequestSorting) Decode(d *jx.Decoder) error { } // MarshalJSON implements stdjson.Marshaler. -func (s *QueryProjectsRequestSorting) MarshalJSON() ([]byte, error) { +func (s *QueryTeamsRequestSorting) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *QueryProjectsRequestSorting) UnmarshalJSON(data []byte) error { +func (s *QueryTeamsRequestSorting) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } // Encode implements json.Marshaler. -func (s *QueryProjectsResponse) Encode(e *jx.Encoder) { +func (s *QueryTeamsResponse) Encode(e *jx.Encoder) { e.ObjStart() s.encodeFields(e) e.ObjEnd() } // encodeFields encodes fields. -func (s *QueryProjectsResponse) encodeFields(e *jx.Encoder) { +func (s *QueryTeamsResponse) encodeFields(e *jx.Encoder) { { - e.FieldStart("projects") + e.FieldStart("teams") e.ArrStart() - for _, elem := range s.Projects { + for _, elem := range s.Teams { elem.Encode(e) } e.ArrEnd() @@ -19721,37 +20428,37 @@ func (s *QueryProjectsResponse) encodeFields(e *jx.Encoder) { } } -var jsonFieldsNameOfQueryProjectsResponse = [2]string{ - 0: "projects", +var jsonFieldsNameOfQueryTeamsResponse = [2]string{ + 0: "teams", 1: "next_page_token", } -// Decode decodes QueryProjectsResponse from json. -func (s *QueryProjectsResponse) Decode(d *jx.Decoder) error { +// Decode decodes QueryTeamsResponse from json. +func (s *QueryTeamsResponse) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode QueryProjectsResponse to nil") + return errors.New("invalid: unable to decode QueryTeamsResponse to nil") } var requiredBitSet [1]uint8 if err := d.ObjBytes(func(d *jx.Decoder, k []byte) error { switch string(k) { - case "projects": + case "teams": requiredBitSet[0] |= 1 << 0 if err := func() error { - s.Projects = make([]ProjectResponse, 0) + s.Teams = make([]TeamResponse, 0) if err := d.Arr(func(d *jx.Decoder) error { - var elem ProjectResponse + var elem TeamResponse if err := elem.Decode(d); err != nil { return err } - s.Projects = append(s.Projects, elem) + s.Teams = append(s.Teams, elem) return nil }); err != nil { return err } return nil }(); err != nil { - return errors.Wrap(err, "decode field \"projects\"") + return errors.Wrap(err, "decode field \"teams\"") } case "next_page_token": if err := func() error { @@ -19768,7 +20475,7 @@ func (s *QueryProjectsResponse) Decode(d *jx.Decoder) error { } return nil }); err != nil { - return errors.Wrap(err, "decode QueryProjectsResponse") + return errors.Wrap(err, "decode QueryTeamsResponse") } // Validate required fields. var failures []validate.FieldError @@ -19785,8 +20492,8 @@ func (s *QueryProjectsResponse) Decode(d *jx.Decoder) error { bitIdx := bits.TrailingZeros8(result) fieldIdx := i*8 + bitIdx var name string - if fieldIdx < len(jsonFieldsNameOfQueryProjectsResponse) { - name = jsonFieldsNameOfQueryProjectsResponse[fieldIdx] + if fieldIdx < len(jsonFieldsNameOfQueryTeamsResponse) { + name = jsonFieldsNameOfQueryTeamsResponse[fieldIdx] } else { name = strconv.Itoa(fieldIdx) } @@ -19807,29 +20514,29 @@ func (s *QueryProjectsResponse) Decode(d *jx.Decoder) error { } // MarshalJSON implements stdjson.Marshaler. -func (s *QueryProjectsResponse) MarshalJSON() ([]byte, error) { +func (s *QueryTeamsResponse) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *QueryProjectsResponse) UnmarshalJSON(data []byte) error { +func (s *QueryTeamsResponse) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes QueryProjectsTooManyRequests as json. -func (s *QueryProjectsTooManyRequests) Encode(e *jx.Encoder) { +// Encode encodes QueryTeamsTooManyRequests as json. +func (s *QueryTeamsTooManyRequests) Encode(e *jx.Encoder) { unwrapped := (*ErrorDetails)(s) unwrapped.Encode(e) } -// Decode decodes QueryProjectsTooManyRequests from json. -func (s *QueryProjectsTooManyRequests) Decode(d *jx.Decoder) error { +// Decode decodes QueryTeamsTooManyRequests from json. +func (s *QueryTeamsTooManyRequests) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode QueryProjectsTooManyRequests to nil") + return errors.New("invalid: unable to decode QueryTeamsTooManyRequests to nil") } var unwrapped ErrorDetails if err := func() error { @@ -19840,34 +20547,34 @@ func (s *QueryProjectsTooManyRequests) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "alias") } - *s = QueryProjectsTooManyRequests(unwrapped) + *s = QueryTeamsTooManyRequests(unwrapped) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s *QueryProjectsTooManyRequests) MarshalJSON() ([]byte, error) { +func (s *QueryTeamsTooManyRequests) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *QueryProjectsTooManyRequests) UnmarshalJSON(data []byte) error { +func (s *QueryTeamsTooManyRequests) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } -// Encode encodes QueryProjectsUnauthorized as json. -func (s *QueryProjectsUnauthorized) Encode(e *jx.Encoder) { +// Encode encodes QueryTeamsUnauthorized as json. +func (s *QueryTeamsUnauthorized) Encode(e *jx.Encoder) { unwrapped := (*ErrorDetails)(s) unwrapped.Encode(e) } -// Decode decodes QueryProjectsUnauthorized from json. -func (s *QueryProjectsUnauthorized) Decode(d *jx.Decoder) error { +// Decode decodes QueryTeamsUnauthorized from json. +func (s *QueryTeamsUnauthorized) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode QueryProjectsUnauthorized to nil") + return errors.New("invalid: unable to decode QueryTeamsUnauthorized to nil") } var unwrapped ErrorDetails if err := func() error { @@ -19878,19 +20585,19 @@ func (s *QueryProjectsUnauthorized) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "alias") } - *s = QueryProjectsUnauthorized(unwrapped) + *s = QueryTeamsUnauthorized(unwrapped) return nil } // MarshalJSON implements stdjson.Marshaler. -func (s *QueryProjectsUnauthorized) MarshalJSON() ([]byte, error) { +func (s *QueryTeamsUnauthorized) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *QueryProjectsUnauthorized) UnmarshalJSON(data []byte) error { +func (s *QueryTeamsUnauthorized) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } @@ -22043,6 +22750,44 @@ func (s *SubmitFlowEventNotFound) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode encodes TeamFilterField as json. +func (s TeamFilterField) Encode(e *jx.Encoder) { + e.Str(string(s)) +} + +// Decode decodes TeamFilterField from json. +func (s *TeamFilterField) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode TeamFilterField to nil") + } + v, err := d.StrBytes() + if err != nil { + return err + } + // Try to use constant string. + switch TeamFilterField(v) { + case TeamFilterFieldCreatedAt: + *s = TeamFilterFieldCreatedAt + default: + *s = TeamFilterField(v) + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s TeamFilterField) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *TeamFilterField) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode implements json.Marshaler. func (s *TeamResponse) Encode(e *jx.Encoder) { e.ObjStart() diff --git a/api/generated/oas_operations_gen.go b/api/generated/oas_operations_gen.go index 1a0abd8a4..e18a5bf50 100644 --- a/api/generated/oas_operations_gen.go +++ b/api/generated/oas_operations_gen.go @@ -52,6 +52,7 @@ const ( ListUsersOperation OperationName = "ListUsers" PatchProjectOperation OperationName = "PatchProject" QueryProjectsOperation OperationName = "QueryProjects" + QueryTeamsOperation OperationName = "QueryTeams" RevokeMySessionOperation OperationName = "RevokeMySession" RevokeSessionOperation OperationName = "RevokeSession" RevokeTokenOperation OperationName = "RevokeToken" diff --git a/api/generated/oas_parameters_gen.go b/api/generated/oas_parameters_gen.go index 69f589eb4..5b940b97e 100644 --- a/api/generated/oas_parameters_gen.go +++ b/api/generated/oas_parameters_gen.go @@ -5423,6 +5423,79 @@ func decodePatchProjectParams(args [1]string, argsEscaped bool, r *http.Request) return params, nil } +// QueryTeamsParams is parameters of queryTeams operation. +type QueryTeamsParams struct { + // The unique identifier of the project. + ProjectID ProjectID +} + +func unpackQueryTeamsParams(packed middleware.Parameters) (params QueryTeamsParams) { + { + key := middleware.ParameterKey{ + Name: "project_id", + In: "query", + } + params.ProjectID = packed[key].(ProjectID) + } + return params +} + +func decodeQueryTeamsParams(args [0]string, argsEscaped bool, r *http.Request) (params QueryTeamsParams, _ error) { + q := uri.NewQueryDecoder(r.URL.Query()) + // Decode query: project_id. + if err := func() error { + cfg := uri.QueryParameterDecodingConfig{ + Name: "project_id", + Style: uri.QueryStyleForm, + Explode: true, + } + + if err := q.HasParam(cfg); err == nil { + if err := q.DecodeParam(cfg, func(d uri.Decoder) error { + var paramsDotProjectIDVal string + if err := func() error { + val, err := d.DecodeValue() + if err != nil { + return err + } + + c, err := conv.ToString(val) + if err != nil { + return err + } + + paramsDotProjectIDVal = c + return nil + }(); err != nil { + return err + } + params.ProjectID = ProjectID(paramsDotProjectIDVal) + return nil + }); err != nil { + return err + } + if err := func() error { + if err := params.ProjectID.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } else { + return err + } + return nil + }(); err != nil { + return params, &ogenerrors.DecodeParamError{ + Name: "project_id", + In: "query", + Err: err, + } + } + return params, nil +} + // RevokeSessionParams is parameters of revokeSession operation. type RevokeSessionParams struct { // The unique identifier of the project. diff --git a/api/generated/oas_request_decoders_gen.go b/api/generated/oas_request_decoders_gen.go index 1aa2c8bb0..84cf03fde 100644 --- a/api/generated/oas_request_decoders_gen.go +++ b/api/generated/oas_request_decoders_gen.go @@ -1429,6 +1429,85 @@ func (s *Server) decodeQueryProjectsRequest(r *http.Request) ( } } +func (s *Server) decodeQueryTeamsRequest(r *http.Request) ( + req *QueryTeamsRequest, + rawBody []byte, + close func() error, + rerr error, +) { + var closers []func() error + close = func() error { + var merr error + // Close in reverse order, to match defer behavior. + for i := len(closers) - 1; i >= 0; i-- { + c := closers[i] + merr = errors.Join(merr, c()) + } + return merr + } + defer func() { + if rerr != nil { + rerr = errors.Join(rerr, close()) + } + }() + ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err != nil { + return req, rawBody, close, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + if r.ContentLength == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + buf, err := io.ReadAll(r.Body) + defer func() { + _ = r.Body.Close() + }() + if err != nil { + return req, rawBody, close, err + } + + // Reset the body to allow for downstream reading. + r.Body = io.NopCloser(bytes.NewBuffer(buf)) + + if len(buf) == 0 { + return req, rawBody, close, validate.ErrBodyRequired + } + + rawBody = append(rawBody, buf...) + d := jx.DecodeBytes(buf) + + var request QueryTeamsRequest + if err := func() error { + if err := request.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return req, rawBody, close, err + } + if err := func() error { + if err := request.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return req, rawBody, close, errors.Wrap(err, "validate") + } + return &request, rawBody, close, nil + default: + return req, rawBody, close, validate.InvalidContentType(ct) + } +} + func (s *Server) decodeRevokeTokenRequest(r *http.Request) ( req *RevokeRequest, rawBody []byte, diff --git a/api/generated/oas_request_encoders_gen.go b/api/generated/oas_request_encoders_gen.go index 086df7205..ac236b44e 100644 --- a/api/generated/oas_request_encoders_gen.go +++ b/api/generated/oas_request_encoders_gen.go @@ -379,6 +379,20 @@ func encodeQueryProjectsRequest( return nil } +func encodeQueryTeamsRequest( + req *QueryTeamsRequest, + r *http.Request, +) error { + const contentType = "application/json" + e := new(jx.Encoder) + { + req.Encode(e) + } + encoded := e.Bytes() + ht.SetBody(r, bytes.NewReader(encoded), contentType) + return nil +} + func encodeRevokeTokenRequest( req *RevokeRequest, r *http.Request, diff --git a/api/generated/oas_response_decoders_gen.go b/api/generated/oas_response_decoders_gen.go index 3f9b401a1..5bd5d3eaf 100644 --- a/api/generated/oas_response_decoders_gen.go +++ b/api/generated/oas_response_decoders_gen.go @@ -6725,6 +6725,273 @@ func decodeQueryProjectsResponse(resp *http.Response) (res QueryProjectsRes, _ e return res, nil } +func decodeQueryTeamsResponse(resp *http.Response) (res QueryTeamsRes, _ error) { + switch resp.StatusCode { + case 200: + // Code 200. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response QueryTeamsResponse + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + // Validate response. + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return res, errors.Wrap(err, "validate") + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 400: + // Code 400. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response QueryTeamsBadRequest + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 401: + // Code 401. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response QueryTeamsUnauthorized + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 403: + // Code 403. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response QueryTeamsForbidden + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 404: + // Code 404. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response QueryTeamsNotFound + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + case 429: + // Code 429. + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response QueryTeamsTooManyRequests + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &response, nil + default: + return res, validate.InvalidContentType(ct) + } + } + // Default response. + res, err := func() (res QueryTeamsRes, err error) { + ct, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return res, errors.Wrap(err, "parse media type") + } + switch { + case ct == "application/json": + buf, err := io.ReadAll(resp.Body) + if err != nil { + return res, err + } + d := jx.DecodeBytes(buf) + + var response ErrorDetails + if err := func() error { + if err := response.Decode(d); err != nil { + return err + } + if err := d.Skip(); err != io.EOF { + return errors.New("unexpected trailing data") + } + return nil + }(); err != nil { + err = &ogenerrors.DecodeBodyError{ + ContentType: ct, + Body: buf, + Err: err, + } + return res, err + } + return &ErrorDetailsStatusCode{ + StatusCode: resp.StatusCode, + Response: response, + }, nil + default: + return res, validate.InvalidContentType(ct) + } + }() + if err != nil { + return res, errors.Wrapf(err, "default (code %d)", resp.StatusCode) + } + return res, nil +} + func decodeRevokeMySessionResponse(resp *http.Response) (res RevokeMySessionRes, _ error) { switch resp.StatusCode { case 204: diff --git a/api/generated/oas_response_encoders_gen.go b/api/generated/oas_response_encoders_gen.go index ae71fcfee..49ea8d031 100644 --- a/api/generated/oas_response_encoders_gen.go +++ b/api/generated/oas_response_encoders_gen.go @@ -3239,6 +3239,124 @@ func encodeQueryProjectsResponse(response QueryProjectsRes, w http.ResponseWrite } } +func encodeQueryTeamsResponse(response QueryTeamsRes, w http.ResponseWriter, span trace.Span) error { + switch response := response.(type) { + case *QueryTeamsResponse: + if err := func() error { + if err := response.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "validate") + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + span.SetStatus(codes.Ok, http.StatusText(200)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *QueryTeamsBadRequest: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(400) + span.SetStatus(codes.Error, http.StatusText(400)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *QueryTeamsUnauthorized: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(401) + span.SetStatus(codes.Error, http.StatusText(401)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *QueryTeamsForbidden: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(403) + span.SetStatus(codes.Error, http.StatusText(403)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *QueryTeamsNotFound: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(404) + span.SetStatus(codes.Error, http.StatusText(404)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *QueryTeamsTooManyRequests: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(429) + span.SetStatus(codes.Error, http.StatusText(429)) + + e := new(jx.Encoder) + response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + return nil + + case *ErrorDetailsStatusCode: + w.Header().Set("Content-Type", "application/json; charset=utf-8") + code := response.StatusCode + if code == 0 { + // Set default status code. + code = http.StatusOK + } + w.WriteHeader(code) + if st := http.StatusText(code); code >= http.StatusBadRequest { + span.SetStatus(codes.Error, st) + } else { + span.SetStatus(codes.Ok, st) + } + + e := new(jx.Encoder) + response.Response.Encode(e) + if _, err := e.WriteTo(w); err != nil { + return errors.Wrap(err, "write") + } + + if code >= http.StatusInternalServerError { + return errors.Wrapf(ht.ErrInternalServerErrorResponse, "code: %d, message: %s", code, http.StatusText(code)) + } + return nil + + default: + return errors.Errorf("unexpected response type: %T", response) + } +} + func encodeRevokeMySessionResponse(response RevokeMySessionRes, w http.ResponseWriter, span trace.Span) error { switch response := response.(type) { case *RevokeMySessionNoContent: diff --git a/api/generated/oas_router_gen.go b/api/generated/oas_router_gen.go index 3dc538d2c..57a948350 100644 --- a/api/generated/oas_router_gen.go +++ b/api/generated/oas_router_gen.go @@ -20,7 +20,7 @@ var ( rn49AllowedHeaders = map[string]string{ "POST": "Authorization,Content-Type", } - rn54AllowedHeaders = map[string]string{ + rn55AllowedHeaders = map[string]string{ "POST": "Authorization,Content-Type", } rn47AllowedHeaders = map[string]string{ @@ -38,7 +38,7 @@ var ( rn51AllowedHeaders = map[string]string{ "POST": "Authorization,Content-Type", } - rn62AllowedHeaders = map[string]string{ + rn63AllowedHeaders = map[string]string{ "POST": "Authorization,Content-Type,Idempotency-Key", } rn15AllowedHeaders = map[string]string{ @@ -54,10 +54,10 @@ var ( rn11AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn57AllowedHeaders = map[string]string{ + rn58AllowedHeaders = map[string]string{ "POST": "Content-Type", } - rn59AllowedHeaders = map[string]string{ + rn60AllowedHeaders = map[string]string{ "POST": "Content-Type,Origin", } rn12AllowedHeaders = map[string]string{ @@ -106,6 +106,9 @@ var ( rn20AllowedHeaders = map[string]string{ "POST": "Authorization,Content-Type", } + rn54AllowedHeaders = map[string]string{ + "POST": "Authorization,Content-Type", + } rn46AllowedHeaders = map[string]string{ "GET": "Authorization", "PATCH": "Authorization,Content-Type", @@ -121,7 +124,7 @@ var ( rn52AllowedHeaders = map[string]string{ "GET": "Authorization", } - rn56AllowedHeaders = map[string]string{ + rn57AllowedHeaders = map[string]string{ "PUT": "Authorization,Content-Type", } ) @@ -367,7 +370,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn54AllowedHeaders, + allowedHeaders: rn55AllowedHeaders, acceptPost: "application/x-www-form-urlencoded", acceptPatch: "", }) @@ -565,7 +568,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn62AllowedHeaders, + allowedHeaders: rn63AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -766,7 +769,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn57AllowedHeaders, + allowedHeaders: rn58AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -793,7 +796,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "POST", - allowedHeaders: rn59AllowedHeaders, + allowedHeaders: rn60AllowedHeaders, acceptPost: "application/json", acceptPatch: "", }) @@ -1358,6 +1361,37 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { break } + if len(elem) == 0 { + break + } + switch elem[0] { + case 'q': // Prefix: "query" + origElem := elem + if l := len("query"); len(elem) >= l && elem[0:l] == "query" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch r.Method { + case "POST": + s.handleQueryTeamsRequest([0]string{}, elemIsEscaped, w, r) + default: + s.notAllowed(w, r, notAllowedParams{ + allowedMethods: "POST", + allowedHeaders: rn54AllowedHeaders, + acceptPost: "application/json", + acceptPatch: "", + }) + } + + return + } + + elem = origElem + } // Param: "team_id" // Leaf parameter, slashes are prohibited idx := strings.IndexByte(elem, '/') @@ -1545,7 +1579,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: s.notAllowed(w, r, notAllowedParams{ allowedMethods: "PUT", - allowedHeaders: rn56AllowedHeaders, + allowedHeaders: rn57AllowedHeaders, acceptPost: "", acceptPatch: "", }) @@ -2867,6 +2901,37 @@ func (s *Server) FindPath(method string, u *url.URL) (r Route, _ bool) { break } + if len(elem) == 0 { + break + } + switch elem[0] { + case 'q': // Prefix: "query" + origElem := elem + if l := len("query"); len(elem) >= l && elem[0:l] == "query" { + elem = elem[l:] + } else { + break + } + + if len(elem) == 0 { + // Leaf node. + switch method { + case "POST": + r.name = QueryTeamsOperation + r.summary = "Query teams" + r.operationID = "queryTeams" + r.operationGroup = "" + r.pathPattern = "/teams/query" + r.args = args + r.count = 0 + return r, true + default: + return + } + } + + elem = origElem + } // Param: "team_id" // Leaf parameter, slashes are prohibited idx := strings.IndexByte(elem, '/') diff --git a/api/generated/oas_schemas_gen.go b/api/generated/oas_schemas_gen.go index 94012a3ed..9ec1ef2de 100644 --- a/api/generated/oas_schemas_gen.go +++ b/api/generated/oas_schemas_gen.go @@ -2505,6 +2505,7 @@ func (*ErrorDetailsStatusCode) listUserPasskeysRes() {} func (*ErrorDetailsStatusCode) listUsersRes() {} func (*ErrorDetailsStatusCode) patchProjectRes() {} func (*ErrorDetailsStatusCode) queryProjectsRes() {} +func (*ErrorDetailsStatusCode) queryTeamsRes() {} func (*ErrorDetailsStatusCode) revokeMySessionRes() {} func (*ErrorDetailsStatusCode) revokeSessionRes() {} func (*ErrorDetailsStatusCode) revokeTokenRes() {} @@ -10007,6 +10008,52 @@ func (o OptQueryProjectsRequestSorting) Or(d QueryProjectsRequestSorting) QueryP return d } +// NewOptQueryTeamsRequestSorting returns new OptQueryTeamsRequestSorting with value set to v. +func NewOptQueryTeamsRequestSorting(v QueryTeamsRequestSorting) OptQueryTeamsRequestSorting { + return OptQueryTeamsRequestSorting{ + Value: v, + Set: true, + } +} + +// OptQueryTeamsRequestSorting is optional QueryTeamsRequestSorting. +type OptQueryTeamsRequestSorting struct { + Value QueryTeamsRequestSorting + Set bool +} + +// IsSet returns true if OptQueryTeamsRequestSorting was set. +func (o OptQueryTeamsRequestSorting) IsSet() bool { return o.Set } + +// Reset unsets value. +func (o *OptQueryTeamsRequestSorting) Reset() { + var v QueryTeamsRequestSorting + o.Value = v + o.Set = false +} + +// SetTo sets value to v. +func (o *OptQueryTeamsRequestSorting) SetTo(v QueryTeamsRequestSorting) { + o.Set = true + o.Value = v +} + +// Get returns value and boolean that denotes whether value was set. +func (o OptQueryTeamsRequestSorting) Get() (v QueryTeamsRequestSorting, ok bool) { + if !o.Set { + return v, false + } + return o.Value, true +} + +// Or returns value if set, or given parameter if does not. +func (o OptQueryTeamsRequestSorting) Or(d QueryTeamsRequestSorting) QueryTeamsRequestSorting { + if v, ok := o.Get(); ok { + return v + } + return d +} + // NewOptSchemaURI returns new OptSchemaURI with value set to v. func NewOptSchemaURI(v SchemaURI) OptSchemaURI { return OptSchemaURI{ @@ -11248,6 +11295,172 @@ type QueryProjectsUnauthorized ErrorDetails func (*QueryProjectsUnauthorized) queryProjectsRes() {} +type QueryTeamsBadRequest ErrorDetails + +func (*QueryTeamsBadRequest) queryTeamsRes() {} + +type QueryTeamsForbidden ErrorDetails + +func (*QueryTeamsForbidden) queryTeamsRes() {} + +type QueryTeamsNotFound ErrorDetails + +func (*QueryTeamsNotFound) queryTeamsRes() {} + +// Request to query the teams of a project. +// Ref: # +type QueryTeamsRequest struct { + Limit OptLimit `json:"limit"` + // Token to retrieve the next page of results. + PageToken OptNilPageToken `json:"page_token"` + Sorting OptQueryTeamsRequestSorting `json:"sorting"` + // Filter criteria for querying teams. + Filter []QueryTeamsRequestFilterItem `json:"filter"` +} + +// GetLimit returns the value of Limit. +func (s *QueryTeamsRequest) GetLimit() OptLimit { + return s.Limit +} + +// GetPageToken returns the value of PageToken. +func (s *QueryTeamsRequest) GetPageToken() OptNilPageToken { + return s.PageToken +} + +// GetSorting returns the value of Sorting. +func (s *QueryTeamsRequest) GetSorting() OptQueryTeamsRequestSorting { + return s.Sorting +} + +// GetFilter returns the value of Filter. +func (s *QueryTeamsRequest) GetFilter() []QueryTeamsRequestFilterItem { + return s.Filter +} + +// SetLimit sets the value of Limit. +func (s *QueryTeamsRequest) SetLimit(val OptLimit) { + s.Limit = val +} + +// SetPageToken sets the value of PageToken. +func (s *QueryTeamsRequest) SetPageToken(val OptNilPageToken) { + s.PageToken = val +} + +// SetSorting sets the value of Sorting. +func (s *QueryTeamsRequest) SetSorting(val OptQueryTeamsRequestSorting) { + s.Sorting = val +} + +// SetFilter sets the value of Filter. +func (s *QueryTeamsRequest) SetFilter(val []QueryTeamsRequestFilterItem) { + s.Filter = val +} + +type QueryTeamsRequestFilterItem struct { + // The field to filter by. + Field TeamFilterField `json:"field"` + Value OptFilterValue `json:"value"` + Operation FilterOperation `json:"operation"` +} + +// GetField returns the value of Field. +func (s *QueryTeamsRequestFilterItem) GetField() TeamFilterField { + return s.Field +} + +// GetValue returns the value of Value. +func (s *QueryTeamsRequestFilterItem) GetValue() OptFilterValue { + return s.Value +} + +// GetOperation returns the value of Operation. +func (s *QueryTeamsRequestFilterItem) GetOperation() FilterOperation { + return s.Operation +} + +// SetField sets the value of Field. +func (s *QueryTeamsRequestFilterItem) SetField(val TeamFilterField) { + s.Field = val +} + +// SetValue sets the value of Value. +func (s *QueryTeamsRequestFilterItem) SetValue(val OptFilterValue) { + s.Value = val +} + +// SetOperation sets the value of Operation. +func (s *QueryTeamsRequestFilterItem) SetOperation(val FilterOperation) { + s.Operation = val +} + +type QueryTeamsRequestSorting struct { + // The field to sort by. + Field TeamFilterField `json:"field"` + // The direction to sort by. + Direction SortDirection `json:"direction"` +} + +// GetField returns the value of Field. +func (s *QueryTeamsRequestSorting) GetField() TeamFilterField { + return s.Field +} + +// GetDirection returns the value of Direction. +func (s *QueryTeamsRequestSorting) GetDirection() SortDirection { + return s.Direction +} + +// SetField sets the value of Field. +func (s *QueryTeamsRequestSorting) SetField(val TeamFilterField) { + s.Field = val +} + +// SetDirection sets the value of Direction. +func (s *QueryTeamsRequestSorting) SetDirection(val SortDirection) { + s.Direction = val +} + +// Paginated list of teams. +// Ref: # +type QueryTeamsResponse struct { + Teams []TeamResponse `json:"teams"` + // Token to pass as `page_token` in the next request to fetch the following page. + // Absent when there are no more results. + NextPageToken OptNilPageToken `json:"next_page_token"` +} + +// GetTeams returns the value of Teams. +func (s *QueryTeamsResponse) GetTeams() []TeamResponse { + return s.Teams +} + +// GetNextPageToken returns the value of NextPageToken. +func (s *QueryTeamsResponse) GetNextPageToken() OptNilPageToken { + return s.NextPageToken +} + +// SetTeams sets the value of Teams. +func (s *QueryTeamsResponse) SetTeams(val []TeamResponse) { + s.Teams = val +} + +// SetNextPageToken sets the value of NextPageToken. +func (s *QueryTeamsResponse) SetNextPageToken(val OptNilPageToken) { + s.NextPageToken = val +} + +func (*QueryTeamsResponse) queryTeamsRes() {} + +type QueryTeamsTooManyRequests ErrorDetails + +func (*QueryTeamsTooManyRequests) queryTeamsRes() {} + +type QueryTeamsUnauthorized ErrorDetails + +func (*QueryTeamsUnauthorized) queryTeamsRes() {} + type RevokeMySessionConflict ErrorDetails func (*RevokeMySessionConflict) revokeMySessionRes() {} @@ -12181,6 +12394,41 @@ type SubmitFlowStepOK FlowResponseHeaders func (*SubmitFlowStepOK) submitFlowStepRes() {} +// Ref: # +type TeamFilterField string + +const ( + TeamFilterFieldCreatedAt TeamFilterField = "createdAt" +) + +// AllValues returns all TeamFilterField values. +func (TeamFilterField) AllValues() []TeamFilterField { + return []TeamFilterField{ + TeamFilterFieldCreatedAt, + } +} + +// MarshalText implements encoding.TextMarshaler. +func (s TeamFilterField) MarshalText() ([]byte, error) { + switch s { + case TeamFilterFieldCreatedAt: + return []byte(s), nil + default: + return nil, errors.Errorf("invalid value: %q", s) + } +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (s *TeamFilterField) UnmarshalText(data []byte) error { + switch TeamFilterField(data) { + case TeamFilterFieldCreatedAt: + *s = TeamFilterFieldCreatedAt + return nil + default: + return errors.Errorf("invalid value: %q", data) + } +} + type TeamID string // Details of a team. diff --git a/api/generated/oas_security_gen.go b/api/generated/oas_security_gen.go index 477cbdbcd..c18f4768e 100644 --- a/api/generated/oas_security_gen.go +++ b/api/generated/oas_security_gen.go @@ -162,6 +162,9 @@ var oauth2ScopesOAuth2 = map[string][]string{ QueryProjectsOperation: []string{ "project.write", }, + QueryTeamsOperation: []string{ + "team.read", + }, RevokeSessionOperation: []string{ "session.delete", }, diff --git a/api/generated/oas_server_gen.go b/api/generated/oas_server_gen.go index 2b3f7dafa..9e9cf8275 100644 --- a/api/generated/oas_server_gen.go +++ b/api/generated/oas_server_gen.go @@ -383,6 +383,13 @@ type Handler interface { // // POST /projects/query QueryProjects(ctx context.Context, req *QueryProjectsRequest) (QueryProjectsRes, error) + // QueryTeams implements queryTeams operation. + // + // Returns the teams of a project, paginated with a cursor. + // Teams of every lifecycle status are returned; each carries its `status`. + // + // POST /teams/query + QueryTeams(ctx context.Context, req *QueryTeamsRequest, params QueryTeamsParams) (QueryTeamsRes, error) // RevokeMySession implements revokeMySession operation. // // Revokes the session immediately (`state: revoked`). This is the logout operation. diff --git a/api/generated/oas_unimplemented_gen.go b/api/generated/oas_unimplemented_gen.go index 9b56d718e..232591855 100644 --- a/api/generated/oas_unimplemented_gen.go +++ b/api/generated/oas_unimplemented_gen.go @@ -526,6 +526,16 @@ func (UnimplementedHandler) QueryProjects(ctx context.Context, req *QueryProject return r, ht.ErrNotImplemented } +// QueryTeams implements queryTeams operation. +// +// Returns the teams of a project, paginated with a cursor. +// Teams of every lifecycle status are returned; each carries its `status`. +// +// POST /teams/query +func (UnimplementedHandler) QueryTeams(ctx context.Context, req *QueryTeamsRequest, params QueryTeamsParams) (r QueryTeamsRes, _ error) { + return r, ht.ErrNotImplemented +} + // RevokeMySession implements revokeMySession operation. // // Revokes the session immediately (`state: revoked`). This is the logout operation. diff --git a/api/generated/oas_validators_gen.go b/api/generated/oas_validators_gen.go index e00255a26..5fcb69f10 100644 --- a/api/generated/oas_validators_gen.go +++ b/api/generated/oas_validators_gen.go @@ -2756,6 +2756,205 @@ func (s *QueryProjectsResponse) Validate() error { return nil } +func (s *QueryTeamsRequest) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if value, ok := s.Limit.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "limit", + Error: err, + }) + } + if err := func() error { + if value, ok := s.Sorting.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "sorting", + Error: err, + }) + } + if err := func() error { + var failures []validate.FieldError + for i, elem := range s.Filter { + if err := func() error { + if err := elem.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: fmt.Sprintf("[%d]", i), + Error: err, + }) + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "filter", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *QueryTeamsRequestFilterItem) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Field.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "field", + Error: err, + }) + } + if err := func() error { + if value, ok := s.Value.Get(); ok { + if err := func() error { + if err := value.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + return err + } + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "value", + Error: err, + }) + } + if err := func() error { + if err := s.Operation.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "operation", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *QueryTeamsRequestSorting) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if err := s.Field.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "field", + Error: err, + }) + } + if err := func() error { + if err := s.Direction.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "direction", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + +func (s *QueryTeamsResponse) Validate() error { + if s == nil { + return validate.ErrNilPointer + } + + var failures []validate.FieldError + if err := func() error { + if s.Teams == nil { + return errors.New("nil is invalid value") + } + var failures []validate.FieldError + for i, elem := range s.Teams { + if err := func() error { + if err := elem.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: fmt.Sprintf("[%d]", i), + Error: err, + }) + } + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "teams", + Error: err, + }) + } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } + return nil +} + func (s SessionID) Validate() error { alias := (string)(s) if err := (validate.String{ @@ -3058,6 +3257,15 @@ func (s *SubmitFlowStepOK) Validate() error { return nil } +func (s TeamFilterField) Validate() error { + switch s { + case "createdAt": + return nil + default: + return errors.Errorf("invalid value: %v", s) + } +} + func (s TeamID) Validate() error { alias := (string)(s) if err := (validate.String{ diff --git a/api/openapi/endpoints/teams/query/methods.yaml b/api/openapi/endpoints/teams/query/methods.yaml new file mode 100644 index 000000000..0476fa69c --- /dev/null +++ b/api/openapi/endpoints/teams/query/methods.yaml @@ -0,0 +1,65 @@ +post: + operationId: queryTeams + summary: Query teams + description: | + Returns the teams of a project, paginated with a cursor. + Teams of every lifecycle status are returned; each carries its `status`. + tags: + - Teams + security: + - oauth2: + - team.read + parameters: + - $ref: ../../../components/parameters/project-id.yaml + requestBody: + description: Request to query the teams of a project. + required: true + content: + application/json: + schema: + $ref: query-teams-request.yaml + responses: + '200': + description: The list of teams was returned successfully + content: + application/json: + schema: + $ref: query-teams-response.yaml + '400': + description: Invalid request (e.g., invalid query parameters). + content: + application/json: + schema: + $ref: ../../../components/error-details.yaml + '401': + description: Missing or invalid credentials. + content: + application/json: + schema: + $ref: ../../../components/error-details.yaml + '403': + description: Insufficient permissions. + content: + application/json: + schema: + $ref: ../../../components/error-details.yaml + '404': + description: Project not found. + content: + application/json: + schema: + $ref: ../../../components/error-details.yaml + '429': + description: | + Too many requests, the client has sent too many requests in a given + amount of time. + content: + application/json: + schema: + $ref: ../../../components/error-details.yaml + default: + description: Unexpected error response. + content: + application/json: + schema: + $ref: ../../../components/error-details.yaml diff --git a/api/openapi/endpoints/teams/query/query-teams-request.yaml b/api/openapi/endpoints/teams/query/query-teams-request.yaml new file mode 100644 index 000000000..12b895237 --- /dev/null +++ b/api/openapi/endpoints/teams/query/query-teams-request.yaml @@ -0,0 +1,38 @@ +type: object +description: Request to query the teams of a project. +properties: + limit: + $ref: ../../../components/schemas/limit.yaml + page_token: + description: Token to retrieve the next page of results. + oneOf: + - $ref: ../../../components/schemas/page-token.yaml + - type: 'null' + sorting: + type: object + required: + - field + - direction + properties: + field: + $ref: team-filter-field.yaml + description: The field to sort by. + direction: + $ref: ../../../components/schemas/sort-direction.yaml + description: The direction to sort by. + filter: + type: array + description: Filter criteria for querying teams. + items: + type: object + required: + - field + - operation + properties: + field: + $ref: team-filter-field.yaml + description: The field to filter by. + value: + $ref: ../../../components/schemas/filter-value.yaml + operation: + $ref: ../../../components/schemas/filter-operation.yaml diff --git a/api/openapi/endpoints/teams/query/query-teams-response.yaml b/api/openapi/endpoints/teams/query/query-teams-response.yaml new file mode 100644 index 000000000..fc8ce29a1 --- /dev/null +++ b/api/openapi/endpoints/teams/query/query-teams-response.yaml @@ -0,0 +1,16 @@ +type: object +description: Paginated list of teams. +required: + - teams +properties: + teams: + type: array + items: + $ref: ../team-response.yaml + next_page_token: + description: | + Token to pass as `page_token` in the next request to fetch the following page. + Absent when there are no more results. + oneOf: + - $ref: ../../../components/schemas/page-token.yaml + - type: 'null' diff --git a/api/openapi/endpoints/teams/query/team-filter-field.yaml b/api/openapi/endpoints/teams/query/team-filter-field.yaml new file mode 100644 index 000000000..ab7685c84 --- /dev/null +++ b/api/openapi/endpoints/teams/query/team-filter-field.yaml @@ -0,0 +1,3 @@ +type: string +enum: + - createdAt diff --git a/api/openapi/openapi-spec.yaml b/api/openapi/openapi-spec.yaml index ac78a98c2..fbb6c092a 100644 --- a/api/openapi/openapi-spec.yaml +++ b/api/openapi/openapi-spec.yaml @@ -123,6 +123,8 @@ paths: $ref: endpoints/flow_definitions/lifecycle/deactivate/method.yaml /teams: $ref: endpoints/teams/methods.yaml + /teams/query: + $ref: endpoints/teams/query/methods.yaml /teams/{team_id}: $ref: endpoints/teams/by_id/methods.yaml /branding: diff --git a/internal/api/integration_test/authz_test.go b/internal/api/integration_test/authz_test.go index b2a892e5e..91180f1e2 100644 --- a/internal/api/integration_test/authz_test.go +++ b/internal/api/integration_test/authz_test.go @@ -238,6 +238,12 @@ func TestManagementAuthz(t *testing.T) { updateResp, err := foreign.UpdateTeam(t.Context(), &api.UpdateTeamRequest{Name: api.NewOptString(helpers.TeamName())}, api.UpdateTeamParams{ProjectID: victimID, TeamID: "team_irrelevant"}) require.NoError(t, err) assertAuthzError(t, updateResp, "team.project_not_found") + + // A read of a foreign project answers "nothing there", so a + // listing cannot be used to prove the project exists. + queryResp, err := foreign.QueryTeams(t.Context(), &api.QueryTeamsRequest{}, api.QueryTeamsParams{ProjectID: victimID}) + require.NoError(t, err) + assertAuthzError(t, queryResp, "team.team_not_found") }) t.Run("preview secret rejected", func(t *testing.T) { @@ -252,6 +258,11 @@ func TestManagementAuthz(t *testing.T) { require.NoError(t, err) require.IsType(t, &api.UpdateTeamForbidden{}, updateResp, helpers.MustMarshal(t, updateResp)) assertAuthzError(t, updateResp, "team.permission_denied") + + queryResp, err := preview.QueryTeams(t.Context(), &api.QueryTeamsRequest{}, api.QueryTeamsParams{ProjectID: victimID}) + require.NoError(t, err) + require.IsType(t, &api.QueryTeamsForbidden{}, queryResp, helpers.MustMarshal(t, queryResp)) + assertAuthzError(t, queryResp, "team.permission_denied") }) t.Run("unauthenticated create is a 401", func(t *testing.T) { diff --git a/internal/api/integration_test/team_test.go b/internal/api/integration_test/team_test.go index 95d8c3547..54d8f93db 100644 --- a/internal/api/integration_test/team_test.go +++ b/internal/api/integration_test/team_test.go @@ -9,6 +9,7 @@ import ( "net/url" "strings" "testing" + "time" "github.com/ogen-go/ogen/validate" "github.com/stretchr/testify/assert" @@ -522,6 +523,187 @@ func TestUpdateTeam(t *testing.T) { }) } +func TestQueryTeams(t *testing.T) { + t.Parallel() + + project, err := harness.EnsureProjectService(t).Create(t.Context(), helpers.ProjectName(), nil, true) + require.NoError(t, err) + t.Cleanup(func() { + // Teams have no delete statement; they cascade with their project. + _ = harness.EnsureServiceDB(t).Statements().DeleteProjectByID(context.Background(), project.ID) + }) + + client, err := helpers.NewApiClient(harness.EnsureTestServer(t).URL) + require.NoError(t, err) + harness.SetProjectSecretOnApiClient(t, client, project) + + params := api.QueryTeamsParams{ProjectID: api.ProjectID(project.ID)} + + // A team in a second project must never surface in this project's results. + otherProject, err := harness.EnsureProjectService(t).Create(t.Context(), helpers.ProjectName(), nil, true) + require.NoError(t, err) + t.Cleanup(func() { + _ = harness.EnsureServiceDB(t).Statements().DeleteProjectByID(context.Background(), otherProject.ID) + }) + _, err = harness.EnsureTeamService(t).Create(t.Context(), service.CreateTeamInput{ + ProjectID: otherProject.ID, + Name: helpers.TeamName(), + }) + require.NoError(t, err) + + active, err := harness.EnsureTeamService(t).Create(t.Context(), service.CreateTeamInput{ + ProjectID: project.ID, + Name: helpers.TeamName(), + }) + require.NoError(t, err) + + deactivated, err := harness.EnsureTeamService(t).Create(t.Context(), service.CreateTeamInput{ + ProjectID: project.ID, + Name: helpers.TeamName(), + }) + require.NoError(t, err) + require.NoError(t, harness.EnsureServiceDB(t).Statements().DeactivateTeam(t.Context(), project.ID, deactivated.ID)) + + queryTeams := func(t *testing.T, req *api.QueryTeamsRequest) *api.QueryTeamsResponse { + t.Helper() + resp, err := client.QueryTeams(t.Context(), req, params) + require.NoError(t, err) + require.IsType(t, &api.QueryTeamsResponse{}, resp, helpers.MustMarshal(t, resp)) + return resp.(*api.QueryTeamsResponse) + } + + byID := func(teams []api.TeamResponse) map[string]api.TeamResponse { + out := make(map[string]api.TeamResponse, len(teams)) + for _, team := range teams { + out[team.ID] = team + } + return out + } + + t.Run("returns the project's teams with their status", func(t *testing.T) { + got := byID(queryTeams(t, &api.QueryTeamsRequest{}).Teams) + + require.Contains(t, got, active.ID) + assert.Equal(t, active.Name, got[active.ID].Name) + assert.Equal(t, api.TeamStatusActive, got[active.ID].Status) + assert.NotEmpty(t, got[active.ID].CreatedAt) + assert.False(t, got[active.ID].UpdatedAt.Before(got[active.ID].CreatedAt)) + + // A deactivated team is a tombstone, not a deletion: it stays listed, + // its status tells it apart. + require.Contains(t, got, deactivated.ID) + assert.Equal(t, api.TeamStatusDeactivated, got[deactivated.ID].Status) + + // Only this project's teams: the other project created exactly one. + assert.Len(t, got, 2) + }) + + t.Run("filters and sorts by createdAt", func(t *testing.T) { + // A minute of slack absorbs the whole-second truncation RFC3339 applies. + before := active.CreatedAt.Add(-time.Minute).Format(time.RFC3339) + after := active.CreatedAt.Add(time.Minute).Format(time.RFC3339) + + createdAtFilter := func(op api.FilterOperation, value string) []api.QueryTeamsRequestFilterItem { + return []api.QueryTeamsRequestFilterItem{{ + Field: api.TeamFilterFieldCreatedAt, + Operation: op, + Value: api.NewOptFilterValue(api.NewStringFilterValue(value)), + }} + } + + matched := queryTeams(t, &api.QueryTeamsRequest{ + Filter: createdAtFilter(api.FilterOperationGreaterThan, before), + }) + assert.Len(t, matched.Teams, 2) + + none := queryTeams(t, &api.QueryTeamsRequest{ + Filter: createdAtFilter(api.FilterOperationGreaterThan, after), + }) + assert.Empty(t, none.Teams) + + asc := queryTeams(t, &api.QueryTeamsRequest{ + Sorting: api.NewOptQueryTeamsRequestSorting(api.QueryTeamsRequestSorting{ + Field: api.TeamFilterFieldCreatedAt, + Direction: api.SortDirectionAsc, + }), + }) + desc := queryTeams(t, &api.QueryTeamsRequest{ + Sorting: api.NewOptQueryTeamsRequestSorting(api.QueryTeamsRequestSorting{ + Field: api.TeamFilterFieldCreatedAt, + Direction: api.SortDirectionDesc, + }), + }) + require.Len(t, asc.Teams, 2) + require.Len(t, desc.Teams, 2) + assert.Equal(t, asc.Teams[0].ID, desc.Teams[1].ID) + assert.Equal(t, asc.Teams[1].ID, desc.Teams[0].ID) + }) + + t.Run("pages with the cursor token", func(t *testing.T) { + req := &api.QueryTeamsRequest{Limit: api.NewOptLimit(1)} + firstPage := queryTeams(t, req) + require.Len(t, firstPage.Teams, 1) + + pageToken, ok := firstPage.NextPageToken.Get() + require.True(t, ok, "a full page carries a cursor") + + req.PageToken = api.NewOptNilPageToken(pageToken) + secondPage := queryTeams(t, req) + require.Len(t, secondPage.Teams, 1) + assert.NotEqual(t, firstPage.Teams[0].ID, secondPage.Teams[0].ID) + + lastToken, ok := secondPage.NextPageToken.Get() + require.True(t, ok, "a full page carries a cursor even when it is the last one") + req.PageToken = api.NewOptNilPageToken(lastToken) + thirdPage := queryTeams(t, req) + assert.Empty(t, thirdPage.Teams) + assert.False(t, thirdPage.NextPageToken.IsSet()) + }) + + t.Run("error", func(t *testing.T) { + t.Parallel() + + requestInvalid := domain.ErrRequestInvalid() + + badRequests := []struct { + name string + req *api.QueryTeamsRequest + }{ + { + name: "unparseable createdAt value", + req: &api.QueryTeamsRequest{Filter: []api.QueryTeamsRequestFilterItem{{ + Field: api.TeamFilterFieldCreatedAt, + Operation: api.FilterOperationEquals, + Value: api.NewOptFilterValue(api.NewStringFilterValue("yesterday")), + }}}, + }, + { + name: "non-string createdAt value", + req: &api.QueryTeamsRequest{Filter: []api.QueryTeamsRequestFilterItem{{ + Field: api.TeamFilterFieldCreatedAt, + Operation: api.FilterOperationEquals, + Value: api.NewOptFilterValue(api.NewBoolFilterValue(true)), + }}}, + }, + { + name: "malformed page token", + req: &api.QueryTeamsRequest{PageToken: api.NewOptNilPageToken("not-a-cursor")}, + }, + } + + for _, tc := range badRequests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + resp, err := client.QueryTeams(t.Context(), tc.req, params) + require.NoError(t, err) + require.IsType(t, &api.QueryTeamsBadRequest{}, resp, helpers.MustMarshal(t, resp)) + assert.Equal(t, api.ErrorCode(requestInvalid.Code), resp.(*api.QueryTeamsBadRequest).Code) + }) + } + }) +} + // TestUpdateTeamRawRequest sends raw bodies (e.g., from a curl request or from a // non-generated SDK), which is the only way to reach the server-side length // checks: the generated client rejects these bodies before sending them. diff --git a/internal/api/team.go b/internal/api/team.go index 175b8ca6a..436299133 100644 --- a/internal/api/team.go +++ b/internal/api/team.go @@ -52,6 +52,53 @@ func (h *Handler) UpdateTeam(ctx context.Context, req *api.UpdateTeamRequest, pa return teamResponse(team), nil } +func (h *Handler) QueryTeams(ctx context.Context, req *api.QueryTeamsRequest, params api.QueryTeamsParams) (api.QueryTeamsRes, error) { + if err := requireProjectAccess(ctx, string(params.ProjectID), teamAccess, opRead); err != nil { + return nil, err + } + + listed, err := h.teamService.List(ctx, mapQueryTeamsToService(string(params.ProjectID), req)) + if err != nil { + return nil, err + } + + teams := make([]api.TeamResponse, 0, len(listed.Teams)) + for _, team := range listed.Teams { + teams = append(teams, *teamResponse(team)) + } + resp := &api.QueryTeamsResponse{Teams: teams} + if listed.NextPageToken != "" { + resp.NextPageToken = api.NewOptNilPageToken(api.PageToken(listed.NextPageToken)) + } + return resp, nil +} + +// ------------------ Converters --------------- + +func mapQueryTeamsToService(projectID string, req *api.QueryTeamsRequest) service.ListTeamsRequest { + svcReq := service.ListTeamsRequest{ + ProjectID: projectID, + Limit: int(req.Limit.Or(0)), // if not defined, set to default value in the service layer + PageToken: string(req.PageToken.Or("")), + } + if sorting, ok := req.Sorting.Get(); ok { + svcReq.Sorting = &service.Sorting{ + Field: string(sorting.Field), + Direction: string(sorting.Direction), + } + } + for _, filter := range req.Filter { + svcReq.Filters = append(svcReq.Filters, service.Filter{ + Field: string(filter.Field), + Operation: string(filter.Operation), + Value: filterValue(filter.Value), + }) + } + return svcReq +} + +// teamResponse is the shared team body: createTeam, getTeam, updateTeam, and +// every item in queryTeams answer with it. func teamResponse(team *domain.Team) *api.TeamResponse { return &api.TeamResponse{ ID: team.ID,