feat: backend service
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
package backendclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"galaxy/gateway/internal/downstream"
|
||||
usermodel "galaxy/model/user"
|
||||
"galaxy/transcoder"
|
||||
)
|
||||
|
||||
const (
|
||||
userCommandResultCodeOK = "ok"
|
||||
defaultUserErrorCode = "internal_error"
|
||||
)
|
||||
|
||||
var stableUserErrorMessages = map[string]string{
|
||||
"invalid_request": "request is invalid",
|
||||
"subject_not_found": "subject not found",
|
||||
"conflict": "request conflicts with current state",
|
||||
defaultUserErrorCode: "internal server error",
|
||||
}
|
||||
|
||||
// ExecuteUserCommand routes one authenticated user-surface command into
|
||||
// backend's `/api/v1/user/*` endpoints. The function is registered for
|
||||
// the message types listed in `galaxy/model/user`.
|
||||
func (c *RESTClient) ExecuteUserCommand(ctx context.Context, command downstream.AuthenticatedCommand) (downstream.UnaryResult, error) {
|
||||
if c == nil || c.httpClient == nil {
|
||||
return downstream.UnaryResult{}, errors.New("backendclient: execute user command: nil client")
|
||||
}
|
||||
if ctx == nil {
|
||||
return downstream.UnaryResult{}, errors.New("backendclient: execute user command: nil context")
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return downstream.UnaryResult{}, err
|
||||
}
|
||||
if strings.TrimSpace(command.UserID) == "" {
|
||||
return downstream.UnaryResult{}, errors.New("backendclient: execute user command: user_id must not be empty")
|
||||
}
|
||||
|
||||
switch command.MessageType {
|
||||
case usermodel.MessageTypeGetMyAccount:
|
||||
if _, err := transcoder.PayloadToGetMyAccountRequest(command.PayloadBytes); err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute user command %q: %w", command.MessageType, err)
|
||||
}
|
||||
return c.executeUserAccountGet(ctx, command.UserID)
|
||||
case usermodel.MessageTypeUpdateMyProfile:
|
||||
req, err := transcoder.PayloadToUpdateMyProfileRequest(command.PayloadBytes)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute user command %q: %w", command.MessageType, err)
|
||||
}
|
||||
return c.executeUserAccountUpdateProfile(ctx, command.UserID, req)
|
||||
case usermodel.MessageTypeUpdateMySettings:
|
||||
req, err := transcoder.PayloadToUpdateMySettingsRequest(command.PayloadBytes)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute user command %q: %w", command.MessageType, err)
|
||||
}
|
||||
return c.executeUserAccountUpdateSettings(ctx, command.UserID, req)
|
||||
default:
|
||||
return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute user command: unsupported message type %q", command.MessageType)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *RESTClient) executeUserAccountGet(ctx context.Context, userID string) (downstream.UnaryResult, error) {
|
||||
body, status, err := c.do(ctx, http.MethodGet, c.baseURL+"/api/v1/user/account", userID, nil)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("execute user.account.get: %w", err)
|
||||
}
|
||||
return projectUserResponse(status, body)
|
||||
}
|
||||
|
||||
func (c *RESTClient) executeUserAccountUpdateProfile(ctx context.Context, userID string, req *usermodel.UpdateMyProfileRequest) (downstream.UnaryResult, error) {
|
||||
body, status, err := c.do(ctx, http.MethodPatch, c.baseURL+"/api/v1/user/account/profile", userID, req)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("execute user.profile.update: %w", err)
|
||||
}
|
||||
return projectUserResponse(status, body)
|
||||
}
|
||||
|
||||
func (c *RESTClient) executeUserAccountUpdateSettings(ctx context.Context, userID string, req *usermodel.UpdateMySettingsRequest) (downstream.UnaryResult, error) {
|
||||
body, status, err := c.do(ctx, http.MethodPatch, c.baseURL+"/api/v1/user/account/settings", userID, req)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("execute user.settings.update: %w", err)
|
||||
}
|
||||
return projectUserResponse(status, body)
|
||||
}
|
||||
|
||||
func projectUserResponse(statusCode int, payload []byte) (downstream.UnaryResult, error) {
|
||||
switch {
|
||||
case statusCode == http.StatusOK:
|
||||
var response usermodel.AccountResponse
|
||||
if err := decodeStrictJSON(payload, &response); err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("decode success response: %w", err)
|
||||
}
|
||||
payloadBytes, err := transcoder.AccountResponseToPayload(&response)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("encode success response payload: %w", err)
|
||||
}
|
||||
return downstream.UnaryResult{
|
||||
ResultCode: userCommandResultCodeOK,
|
||||
PayloadBytes: payloadBytes,
|
||||
}, nil
|
||||
case statusCode == http.StatusServiceUnavailable:
|
||||
return downstream.UnaryResult{}, downstream.ErrDownstreamUnavailable
|
||||
case statusCode >= 400 && statusCode <= 599:
|
||||
errResp, err := decodeUserError(statusCode, payload)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("decode error response: %w", err)
|
||||
}
|
||||
payloadBytes, err := transcoder.ErrorResponseToPayload(errResp)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("encode error response payload: %w", err)
|
||||
}
|
||||
return downstream.UnaryResult{
|
||||
ResultCode: errResp.Error.Code,
|
||||
PayloadBytes: payloadBytes,
|
||||
}, nil
|
||||
default:
|
||||
return downstream.UnaryResult{}, fmt.Errorf("unexpected HTTP status %d", statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeUserError(statusCode int, payload []byte) (*usermodel.ErrorResponse, error) {
|
||||
var response usermodel.ErrorResponse
|
||||
if err := decodeStrictJSON(payload, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response.Error.Code = normalizeUserErrorCode(statusCode, response.Error.Code)
|
||||
response.Error.Message = normalizeUserErrorMessage(response.Error.Code, response.Error.Message)
|
||||
if strings.TrimSpace(response.Error.Code) == "" {
|
||||
return nil, errors.New("missing error code")
|
||||
}
|
||||
if strings.TrimSpace(response.Error.Message) == "" {
|
||||
return nil, errors.New("missing error message")
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func normalizeUserErrorCode(statusCode int, code string) string {
|
||||
if trimmed := strings.TrimSpace(code); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
switch statusCode {
|
||||
case http.StatusBadRequest:
|
||||
return "invalid_request"
|
||||
case http.StatusNotFound:
|
||||
return "subject_not_found"
|
||||
case http.StatusConflict:
|
||||
return "conflict"
|
||||
default:
|
||||
return defaultUserErrorCode
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeUserErrorMessage(code, message string) string {
|
||||
if trimmed := strings.TrimSpace(message); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
if stable, ok := stableUserErrorMessages[code]; ok {
|
||||
return stable
|
||||
}
|
||||
return stableUserErrorMessages[defaultUserErrorCode]
|
||||
}
|
||||
Reference in New Issue
Block a user