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,