From d38618a9e647902ce019396ff1c33973020bf797 Mon Sep 17 00:00:00 2001 From: Patrik Date: Wed, 17 Mar 2021 14:24:23 +0100 Subject: [PATCH] feat: REST patch relation tuples (#491) The new PATCH handler allows transactional changes similar to the already existing gRPC service. --- .schema/swagger.json | 141 ++++++++ .../write/patch_relation_tuples_parameters.go | 148 ++++++++ .../write/patch_relation_tuples_responses.go | 326 ++++++++++++++++++ .../httpclient/client/write/write_client.go | 40 +++ internal/httpclient/models/patch_action.go | 27 ++ internal/httpclient/models/patch_delta.go | 138 ++++++++ internal/relationtuple/definitions.go | 22 ++ internal/relationtuple/handler.go | 1 + internal/relationtuple/transact_server.go | 47 ++- .../relationtuple/transact_server_test.go | 77 +++++ 10 files changed, 964 insertions(+), 3 deletions(-) create mode 100644 internal/httpclient/client/write/patch_relation_tuples_parameters.go create mode 100644 internal/httpclient/client/write/patch_relation_tuples_responses.go create mode 100644 internal/httpclient/models/patch_action.go create mode 100644 internal/httpclient/models/patch_delta.go diff --git a/.schema/swagger.json b/.schema/swagger.json index 23a8e1650..5bd56ba89 100755 --- a/.schema/swagger.json +++ b/.schema/swagger.json @@ -804,6 +804,133 @@ } } } + }, + "patch": { + "description": "Patch multiple relation tuples", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "tags": [ + "write" + ], + "operationId": "patchRelationTuples", + "parameters": [ + { + "name": "Payload", + "in": "body", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/PatchDelta" + } + } + } + ], + "responses": { + "204": { + "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 201." + }, + "400": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + }, + "404": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + }, + "500": { + "description": "The standard error format", + "schema": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int64" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "request": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + } + } } }, "/version": { @@ -855,6 +982,17 @@ } } }, + "PatchDelta": { + "type": "object", + "properties": { + "action": { + "$ref": "#/definitions/patchAction" + }, + "relation_tuple": { + "$ref": "#/definitions/InternalRelationTuple" + } + } + }, "expandTree": { "type": "object", "required": [ @@ -934,6 +1072,9 @@ } } }, + "patchAction": { + "type": "string" + }, "subject": { "type": "string" }, diff --git a/internal/httpclient/client/write/patch_relation_tuples_parameters.go b/internal/httpclient/client/write/patch_relation_tuples_parameters.go new file mode 100644 index 000000000..c26d13545 --- /dev/null +++ b/internal/httpclient/client/write/patch_relation_tuples_parameters.go @@ -0,0 +1,148 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package write + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + + "github.com/ory/keto/internal/httpclient/models" +) + +// NewPatchRelationTuplesParams creates a new PatchRelationTuplesParams object, +// with the default timeout for this client. +// +// Default values are not hydrated, since defaults are normally applied by the API server side. +// +// To enforce default values in parameter, use SetDefaults or WithDefaults. +func NewPatchRelationTuplesParams() *PatchRelationTuplesParams { + return &PatchRelationTuplesParams{ + timeout: cr.DefaultTimeout, + } +} + +// NewPatchRelationTuplesParamsWithTimeout creates a new PatchRelationTuplesParams object +// with the ability to set a timeout on a request. +func NewPatchRelationTuplesParamsWithTimeout(timeout time.Duration) *PatchRelationTuplesParams { + return &PatchRelationTuplesParams{ + timeout: timeout, + } +} + +// NewPatchRelationTuplesParamsWithContext creates a new PatchRelationTuplesParams object +// with the ability to set a context for a request. +func NewPatchRelationTuplesParamsWithContext(ctx context.Context) *PatchRelationTuplesParams { + return &PatchRelationTuplesParams{ + Context: ctx, + } +} + +// NewPatchRelationTuplesParamsWithHTTPClient creates a new PatchRelationTuplesParams object +// with the ability to set a custom HTTPClient for a request. +func NewPatchRelationTuplesParamsWithHTTPClient(client *http.Client) *PatchRelationTuplesParams { + return &PatchRelationTuplesParams{ + HTTPClient: client, + } +} + +/* PatchRelationTuplesParams contains all the parameters to send to the API endpoint + for the patch relation tuples operation. + + Typically these are written to a http.Request. +*/ +type PatchRelationTuplesParams struct { + + // Payload. + Payload []*models.PatchDelta + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithDefaults hydrates default values in the patch relation tuples params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *PatchRelationTuplesParams) WithDefaults() *PatchRelationTuplesParams { + o.SetDefaults() + return o +} + +// SetDefaults hydrates default values in the patch relation tuples params (not the query body). +// +// All values with no default are reset to their zero value. +func (o *PatchRelationTuplesParams) SetDefaults() { + // no default values defined for this parameter +} + +// WithTimeout adds the timeout to the patch relation tuples params +func (o *PatchRelationTuplesParams) WithTimeout(timeout time.Duration) *PatchRelationTuplesParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the patch relation tuples params +func (o *PatchRelationTuplesParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the patch relation tuples params +func (o *PatchRelationTuplesParams) WithContext(ctx context.Context) *PatchRelationTuplesParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the patch relation tuples params +func (o *PatchRelationTuplesParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the patch relation tuples params +func (o *PatchRelationTuplesParams) WithHTTPClient(client *http.Client) *PatchRelationTuplesParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the patch relation tuples params +func (o *PatchRelationTuplesParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithPayload adds the payload to the patch relation tuples params +func (o *PatchRelationTuplesParams) WithPayload(payload []*models.PatchDelta) *PatchRelationTuplesParams { + o.SetPayload(payload) + return o +} + +// SetPayload adds the payload to the patch relation tuples params +func (o *PatchRelationTuplesParams) SetPayload(payload []*models.PatchDelta) { + o.Payload = payload +} + +// WriteToRequest writes these params to a swagger request +func (o *PatchRelationTuplesParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + if o.Payload != nil { + if err := r.SetBodyParam(o.Payload); err != nil { + return err + } + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/internal/httpclient/client/write/patch_relation_tuples_responses.go b/internal/httpclient/client/write/patch_relation_tuples_responses.go new file mode 100644 index 000000000..ad5525aa6 --- /dev/null +++ b/internal/httpclient/client/write/patch_relation_tuples_responses.go @@ -0,0 +1,326 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package write + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// PatchRelationTuplesReader is a Reader for the PatchRelationTuples structure. +type PatchRelationTuplesReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *PatchRelationTuplesReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 204: + result := NewPatchRelationTuplesNoContent() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewPatchRelationTuplesBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 404: + result := NewPatchRelationTuplesNotFound() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + case 500: + result := NewPatchRelationTuplesInternalServerError() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + return nil, runtime.NewAPIError("response status code does not match any response statuses defined for this endpoint in the swagger spec", response, response.Code()) + } +} + +// NewPatchRelationTuplesNoContent creates a PatchRelationTuplesNoContent with default headers values +func NewPatchRelationTuplesNoContent() *PatchRelationTuplesNoContent { + return &PatchRelationTuplesNoContent{} +} + +/* PatchRelationTuplesNoContent describes a response with status code 204, with default header values. + +Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 201. +*/ +type PatchRelationTuplesNoContent struct { +} + +func (o *PatchRelationTuplesNoContent) Error() string { + return fmt.Sprintf("[PATCH /relationtuple][%d] patchRelationTuplesNoContent ", 204) +} + +func (o *PatchRelationTuplesNoContent) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + return nil +} + +// NewPatchRelationTuplesBadRequest creates a PatchRelationTuplesBadRequest with default headers values +func NewPatchRelationTuplesBadRequest() *PatchRelationTuplesBadRequest { + return &PatchRelationTuplesBadRequest{} +} + +/* PatchRelationTuplesBadRequest describes a response with status code 400, with default header values. + +The standard error format +*/ +type PatchRelationTuplesBadRequest struct { + Payload *PatchRelationTuplesBadRequestBody +} + +func (o *PatchRelationTuplesBadRequest) Error() string { + return fmt.Sprintf("[PATCH /relationtuple][%d] patchRelationTuplesBadRequest %+v", 400, o.Payload) +} +func (o *PatchRelationTuplesBadRequest) GetPayload() *PatchRelationTuplesBadRequestBody { + return o.Payload +} + +func (o *PatchRelationTuplesBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(PatchRelationTuplesBadRequestBody) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewPatchRelationTuplesNotFound creates a PatchRelationTuplesNotFound with default headers values +func NewPatchRelationTuplesNotFound() *PatchRelationTuplesNotFound { + return &PatchRelationTuplesNotFound{} +} + +/* PatchRelationTuplesNotFound describes a response with status code 404, with default header values. + +The standard error format +*/ +type PatchRelationTuplesNotFound struct { + Payload *PatchRelationTuplesNotFoundBody +} + +func (o *PatchRelationTuplesNotFound) Error() string { + return fmt.Sprintf("[PATCH /relationtuple][%d] patchRelationTuplesNotFound %+v", 404, o.Payload) +} +func (o *PatchRelationTuplesNotFound) GetPayload() *PatchRelationTuplesNotFoundBody { + return o.Payload +} + +func (o *PatchRelationTuplesNotFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(PatchRelationTuplesNotFoundBody) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewPatchRelationTuplesInternalServerError creates a PatchRelationTuplesInternalServerError with default headers values +func NewPatchRelationTuplesInternalServerError() *PatchRelationTuplesInternalServerError { + return &PatchRelationTuplesInternalServerError{} +} + +/* PatchRelationTuplesInternalServerError describes a response with status code 500, with default header values. + +The standard error format +*/ +type PatchRelationTuplesInternalServerError struct { + Payload *PatchRelationTuplesInternalServerErrorBody +} + +func (o *PatchRelationTuplesInternalServerError) Error() string { + return fmt.Sprintf("[PATCH /relationtuple][%d] patchRelationTuplesInternalServerError %+v", 500, o.Payload) +} +func (o *PatchRelationTuplesInternalServerError) GetPayload() *PatchRelationTuplesInternalServerErrorBody { + return o.Payload +} + +func (o *PatchRelationTuplesInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(PatchRelationTuplesInternalServerErrorBody) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +/*PatchRelationTuplesBadRequestBody patch relation tuples bad request body +swagger:model PatchRelationTuplesBadRequestBody +*/ +type PatchRelationTuplesBadRequestBody struct { + + // code + Code int64 `json:"code,omitempty"` + + // details + Details []interface{} `json:"details"` + + // message + Message string `json:"message,omitempty"` + + // reason + Reason string `json:"reason,omitempty"` + + // request + Request string `json:"request,omitempty"` + + // status + Status string `json:"status,omitempty"` +} + +// Validate validates this patch relation tuples bad request body +func (o *PatchRelationTuplesBadRequestBody) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this patch relation tuples bad request body based on context it is used +func (o *PatchRelationTuplesBadRequestBody) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (o *PatchRelationTuplesBadRequestBody) MarshalBinary() ([]byte, error) { + if o == nil { + return nil, nil + } + return swag.WriteJSON(o) +} + +// UnmarshalBinary interface implementation +func (o *PatchRelationTuplesBadRequestBody) UnmarshalBinary(b []byte) error { + var res PatchRelationTuplesBadRequestBody + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *o = res + return nil +} + +/*PatchRelationTuplesInternalServerErrorBody patch relation tuples internal server error body +swagger:model PatchRelationTuplesInternalServerErrorBody +*/ +type PatchRelationTuplesInternalServerErrorBody struct { + + // code + Code int64 `json:"code,omitempty"` + + // details + Details []interface{} `json:"details"` + + // message + Message string `json:"message,omitempty"` + + // reason + Reason string `json:"reason,omitempty"` + + // request + Request string `json:"request,omitempty"` + + // status + Status string `json:"status,omitempty"` +} + +// Validate validates this patch relation tuples internal server error body +func (o *PatchRelationTuplesInternalServerErrorBody) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this patch relation tuples internal server error body based on context it is used +func (o *PatchRelationTuplesInternalServerErrorBody) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (o *PatchRelationTuplesInternalServerErrorBody) MarshalBinary() ([]byte, error) { + if o == nil { + return nil, nil + } + return swag.WriteJSON(o) +} + +// UnmarshalBinary interface implementation +func (o *PatchRelationTuplesInternalServerErrorBody) UnmarshalBinary(b []byte) error { + var res PatchRelationTuplesInternalServerErrorBody + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *o = res + return nil +} + +/*PatchRelationTuplesNotFoundBody patch relation tuples not found body +swagger:model PatchRelationTuplesNotFoundBody +*/ +type PatchRelationTuplesNotFoundBody struct { + + // code + Code int64 `json:"code,omitempty"` + + // details + Details []interface{} `json:"details"` + + // message + Message string `json:"message,omitempty"` + + // reason + Reason string `json:"reason,omitempty"` + + // request + Request string `json:"request,omitempty"` + + // status + Status string `json:"status,omitempty"` +} + +// Validate validates this patch relation tuples not found body +func (o *PatchRelationTuplesNotFoundBody) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this patch relation tuples not found body based on context it is used +func (o *PatchRelationTuplesNotFoundBody) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (o *PatchRelationTuplesNotFoundBody) MarshalBinary() ([]byte, error) { + if o == nil { + return nil, nil + } + return swag.WriteJSON(o) +} + +// UnmarshalBinary interface implementation +func (o *PatchRelationTuplesNotFoundBody) UnmarshalBinary(b []byte) error { + var res PatchRelationTuplesNotFoundBody + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *o = res + return nil +} diff --git a/internal/httpclient/client/write/write_client.go b/internal/httpclient/client/write/write_client.go index 1b5f9c03e..0dd6cf099 100644 --- a/internal/httpclient/client/write/write_client.go +++ b/internal/httpclient/client/write/write_client.go @@ -34,6 +34,8 @@ type ClientService interface { DeleteRelationTuple(params *DeleteRelationTupleParams, opts ...ClientOption) (*DeleteRelationTupleNoContent, error) + PatchRelationTuples(params *PatchRelationTuplesParams, opts ...ClientOption) (*PatchRelationTuplesNoContent, error) + SetTransport(transport runtime.ClientTransport) } @@ -113,6 +115,44 @@ func (a *Client) DeleteRelationTuple(params *DeleteRelationTupleParams, opts ... panic(msg) } +/* + PatchRelationTuples Patch multiple relation tuples +*/ +func (a *Client) PatchRelationTuples(params *PatchRelationTuplesParams, opts ...ClientOption) (*PatchRelationTuplesNoContent, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewPatchRelationTuplesParams() + } + op := &runtime.ClientOperation{ + ID: "patchRelationTuples", + Method: "PATCH", + PathPattern: "/relationtuple", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"http", "https"}, + Params: params, + Reader: &PatchRelationTuplesReader{formats: a.formats}, + Context: params.Context, + Client: params.HTTPClient, + } + for _, opt := range opts { + opt(op) + } + + result, err := a.transport.Submit(op) + if err != nil { + return nil, err + } + success, ok := result.(*PatchRelationTuplesNoContent) + if ok { + return success, nil + } + // unexpected success response + // safeguard: normally, absent a default response, unknown success responses return an error above: so this is a codegen issue + msg := fmt.Sprintf("unexpected success response for patchRelationTuples: API contract not enforced by server. Client expected to get an error, but got: %T", result) + panic(msg) +} + // SetTransport changes the transport on the client func (a *Client) SetTransport(transport runtime.ClientTransport) { a.transport = transport diff --git a/internal/httpclient/models/patch_action.go b/internal/httpclient/models/patch_action.go new file mode 100644 index 000000000..63a593869 --- /dev/null +++ b/internal/httpclient/models/patch_action.go @@ -0,0 +1,27 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/strfmt" +) + +// PatchAction patch action +// +// swagger:model patchAction +type PatchAction string + +// Validate validates this patch action +func (m PatchAction) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this patch action based on context it is used +func (m PatchAction) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} diff --git a/internal/httpclient/models/patch_delta.go b/internal/httpclient/models/patch_delta.go new file mode 100644 index 000000000..597724711 --- /dev/null +++ b/internal/httpclient/models/patch_delta.go @@ -0,0 +1,138 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// PatchDelta patch delta +// +// swagger:model PatchDelta +type PatchDelta struct { + + // action + Action PatchAction `json:"action,omitempty"` + + // relation tuple + RelationTuple *InternalRelationTuple `json:"relation_tuple,omitempty"` +} + +// Validate validates this patch delta +func (m *PatchDelta) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAction(formats); err != nil { + res = append(res, err) + } + + if err := m.validateRelationTuple(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *PatchDelta) validateAction(formats strfmt.Registry) error { + if swag.IsZero(m.Action) { // not required + return nil + } + + if err := m.Action.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("action") + } + return err + } + + return nil +} + +func (m *PatchDelta) validateRelationTuple(formats strfmt.Registry) error { + if swag.IsZero(m.RelationTuple) { // not required + return nil + } + + if m.RelationTuple != nil { + if err := m.RelationTuple.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("relation_tuple") + } + return err + } + } + + return nil +} + +// ContextValidate validate this patch delta based on the context it is used +func (m *PatchDelta) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateAction(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateRelationTuple(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *PatchDelta) contextValidateAction(ctx context.Context, formats strfmt.Registry) error { + + if err := m.Action.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("action") + } + return err + } + + return nil +} + +func (m *PatchDelta) contextValidateRelationTuple(ctx context.Context, formats strfmt.Registry) error { + + if m.RelationTuple != nil { + if err := m.RelationTuple.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("relation_tuple") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *PatchDelta) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *PatchDelta) UnmarshalBinary(b []byte) error { + var res PatchDelta + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/internal/relationtuple/definitions.go b/internal/relationtuple/definitions.go index 4afc6a7a2..b9977e586 100644 --- a/internal/relationtuple/definitions.go +++ b/internal/relationtuple/definitions.go @@ -113,6 +113,28 @@ var ( ErrNilSubject = errors.New("subject is nil") ) +// swagger:enum +type patchAction string + +const ( + ActionInsert patchAction = "insert" + ActionDelete patchAction = "delete" +) + +// The patch request payload +// +// swagger:parameters patchRelationTuples +// nolint:deadcode,unused +type patchPayload struct { + // in:body + Payload []*PatchDelta +} + +type PatchDelta struct { + Action patchAction `json:"action"` + RelationTuple *InternalRelationTuple `json:"relation_tuple"` +} + func SubjectFromString(s string) (Subject, error) { if strings.Contains(s, "#") { return (&SubjectSet{}).FromString(s) diff --git a/internal/relationtuple/handler.go b/internal/relationtuple/handler.go index 4e33d63b8..5d87d5cdc 100644 --- a/internal/relationtuple/handler.go +++ b/internal/relationtuple/handler.go @@ -43,6 +43,7 @@ func (h *handler) RegisterReadRoutes(r *x.ReadRouter) { func (h *handler) RegisterWriteRoutes(r *x.WriteRouter) { r.PUT(RouteBase, h.createRelation) r.DELETE(RouteBase, h.deleteRelation) + r.PATCH(RouteBase, h.patchRelations) } func (h *handler) RegisterReadGRPC(s *grpc.Server) { diff --git a/internal/relationtuple/transact_server.go b/internal/relationtuple/transact_server.go index 77689fbec..f5f522e1a 100644 --- a/internal/relationtuple/transact_server.go +++ b/internal/relationtuple/transact_server.go @@ -14,7 +14,7 @@ import ( var _ acl.WriteServiceServer = (*handler)(nil) -func tuplesWithAction(deltas []*acl.RelationTupleDelta, action acl.RelationTupleDelta_Action) (filtered []*InternalRelationTuple, err error) { +func protoTuplesWithAction(deltas []*acl.RelationTupleDelta, action acl.RelationTupleDelta_Action) (filtered []*InternalRelationTuple, err error) { for _, d := range deltas { if d.Action == action { it, err := (&InternalRelationTuple{}).FromDataProvider(d.RelationTuple) @@ -28,12 +28,12 @@ func tuplesWithAction(deltas []*acl.RelationTupleDelta, action acl.RelationTuple } func (h *handler) TransactRelationTuples(ctx context.Context, req *acl.TransactRelationTuplesRequest) (*acl.TransactRelationTuplesResponse, error) { - insertTuples, err := tuplesWithAction(req.RelationTupleDeltas, acl.RelationTupleDelta_INSERT) + insertTuples, err := protoTuplesWithAction(req.RelationTupleDeltas, acl.RelationTupleDelta_INSERT) if err != nil { return nil, err } - deleteTuples, err := tuplesWithAction(req.RelationTupleDeltas, acl.RelationTupleDelta_DELETE) + deleteTuples, err := protoTuplesWithAction(req.RelationTupleDeltas, acl.RelationTupleDelta_DELETE) if err != nil { return nil, err } @@ -127,3 +127,44 @@ func (h *handler) deleteRelation(w http.ResponseWriter, r *http.Request, _ httpr w.WriteHeader(http.StatusNoContent) } + +func internalTuplesWithAction(deltas []*PatchDelta, action patchAction) (filtered []*InternalRelationTuple) { + for _, d := range deltas { + if d.Action == action { + filtered = append(filtered, d.RelationTuple) + } + } + return +} + +// swagger:route PATCH /relationtuple write patchRelationTuples +// +// Patch multiple relation tuples +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 204: emptyResponse +// 400: genericError +// 404: genericError +// 500: genericError +func (h *handler) patchRelations(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + var deltas []*PatchDelta + if err := json.NewDecoder(r.Body).Decode(&deltas); err != nil { + h.d.Writer().WriteError(w, r, herodot.ErrBadRequest.WithError(err.Error())) + return + } + + if err := h.d.RelationTupleManager().TransactRelationTuples(r.Context(), internalTuplesWithAction(deltas, ActionInsert), internalTuplesWithAction(deltas, ActionDelete)); err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/relationtuple/transact_server_test.go b/internal/relationtuple/transact_server_test.go index 15206cee9..c23a6280c 100644 --- a/internal/relationtuple/transact_server_test.go +++ b/internal/relationtuple/transact_server_test.go @@ -107,4 +107,81 @@ func TestWriteHandlers(t *testing.T) { assert.Equal(t, []*relationtuple.InternalRelationTuple{}, actualRTs) }) }) + + t.Run("method=patch", func(t *testing.T) { + t.Run("case=create and delete", func(t *testing.T) { + deltas := []*relationtuple.PatchDelta{ + { + Action: relationtuple.ActionInsert, + RelationTuple: &relationtuple.InternalRelationTuple{ + Namespace: nspace.Name, + Object: "create obj", + Relation: t.Name(), + Subject: &relationtuple.SubjectID{ID: "create sub"}, + }, + }, + { + Action: relationtuple.ActionDelete, + RelationTuple: &relationtuple.InternalRelationTuple{ + Namespace: nspace.Name, + Object: "delete obj", + Relation: t.Name(), + Subject: &relationtuple.SubjectID{ID: "delete sub"}, + }, + }, + } + require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), deltas[1].RelationTuple)) + + body, err := json.Marshal(deltas) + require.NoError(t, err) + req, err := http.NewRequest(http.MethodPatch, ts.URL+relationtuple.RouteBase, bytes.NewBuffer(body)) + require.NoError(t, err) + resp, err := ts.Client().Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusNoContent, resp.StatusCode) + + actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(context.Background(), &relationtuple.RelationQuery{ + Namespace: nspace.Name, + Relation: t.Name(), + }) + require.NoError(t, err) + assert.Equal(t, []*relationtuple.InternalRelationTuple{deltas[0].RelationTuple}, actualRTs) + }) + + t.Run("case=ignores rest on err", func(t *testing.T) { + deltas := []*relationtuple.PatchDelta{ + { + Action: relationtuple.ActionInsert, + RelationTuple: &relationtuple.InternalRelationTuple{ + Namespace: nspace.Name, + Object: "create obj", + Relation: t.Name(), + Subject: &relationtuple.SubjectID{ID: "create sub"}, + }, + }, + { + Action: relationtuple.ActionDelete, + RelationTuple: &relationtuple.InternalRelationTuple{ + Namespace: "not " + nspace.Name, + Object: "o", + Relation: "r", + Subject: &relationtuple.SubjectID{ID: "s"}, + }, + }, + } + + body, err := json.Marshal(deltas) + require.NoError(t, err) + req, err := http.NewRequest(http.MethodPatch, ts.URL+relationtuple.RouteBase, bytes.NewBuffer(body)) + require.NoError(t, err) + resp, err := ts.Client().Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + + // set a size > 1 just to make sure it gets all + actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(context.Background(), (*relationtuple.RelationQuery)(deltas[0].RelationTuple), x.WithSize(10)) + require.NoError(t, err) + assert.Len(t, actualRTs, 0) + }) + }) }