Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,96 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/archive.go
package collection
import (
"encoding/json"
"errors"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type ArchiveCollectionHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_collection.ArchiveCollectionService
middleware middleware.Middleware
}
func NewArchiveCollectionHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_collection.ArchiveCollectionService,
middleware middleware.Middleware,
) *ArchiveCollectionHTTPHandler {
logger = logger.Named("ArchiveCollectionHTTPHandler")
return &ArchiveCollectionHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*ArchiveCollectionHTTPHandler) Pattern() string {
return "PUT /api/v1/collections/{id}/archive"
}
func (h *ArchiveCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *ArchiveCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract collection ID from the URL
collectionIDStr := r.PathValue("id")
if collectionIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
return
}
// Convert string ID to ObjectID
collectionID, err := gocql.ParseUUID(collectionIDStr)
if err != nil {
h.logger.Error("invalid collection ID format",
zap.String("collection_id", collectionIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
return
}
// Create request DTO
dtoReq := &svc_collection.ArchiveCollectionRequestDTO{
ID: collectionID,
}
resp, err := h.service.Execute(ctx, dtoReq)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,109 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/create.go
package collection
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type CreateCollectionHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_collection.CreateCollectionService
middleware middleware.Middleware
}
func NewCreateCollectionHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_collection.CreateCollectionService,
middleware middleware.Middleware,
) *CreateCollectionHTTPHandler {
logger = logger.Named("CreateCollectionHTTPHandler")
return &CreateCollectionHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*CreateCollectionHTTPHandler) Pattern() string {
return "POST /api/v1/collections"
}
func (h *CreateCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *CreateCollectionHTTPHandler) unmarshalRequest(
ctx context.Context,
r *http.Request,
) (*svc_collection.CreateCollectionRequestDTO, error) {
// Initialize our structure which will store the parsed request data
var requestData svc_collection.CreateCollectionRequestDTO
defer r.Body.Close()
var rawJSON bytes.Buffer
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
// Read the JSON string and convert it into our golang struct
err := json.NewDecoder(teeReader).Decode(&requestData)
if err != nil {
h.logger.Error("Failed to decode create collection request",
zap.Error(err),
zap.String("json", rawJSON.String()),
)
return nil, httperror.NewBadRequestError("Invalid request payload. Please check your collection data.")
}
return &requestData, nil
}
func (h *CreateCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := h.unmarshalRequest(ctx, r)
if err != nil {
h.logger.Error("Failed to unmarshal create collection request", zap.Error(err))
httperror.RespondWithError(w, r, err)
return
}
resp, err := h.service.Execute(ctx, req)
if err != nil {
h.logger.Error("Failed to create collection", zap.Error(err))
// Service returns RFC 9457 errors, use RespondWithError to handle them
httperror.RespondWithError(w, r, err)
return
}
if resp == nil {
h.logger.Error("No collection returned from service")
problem := httperror.NewInternalServerError("Failed to create collection. Please try again.").
WithInstance(r.URL.Path).
WithTraceID(httperror.ExtractRequestID(r))
httperror.RespondWithProblem(w, problem)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("Failed to encode collection response", zap.Error(err))
// At this point headers are already sent, log the error but can't send RFC 9457 response
return
}
}

View file

@ -0,0 +1,97 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/find_by_parent.go
package collection
import (
"encoding/json"
"errors"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type FindCollectionsByParentHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_collection.FindCollectionsByParentService
middleware middleware.Middleware
}
func NewFindCollectionsByParentHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_collection.FindCollectionsByParentService,
middleware middleware.Middleware,
) *FindCollectionsByParentHTTPHandler {
logger = logger.Named("FindCollectionsByParentHTTPHandler")
return &FindCollectionsByParentHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*FindCollectionsByParentHTTPHandler) Pattern() string {
return "GET /api/v1/collections/parent/{parent_id}"
}
func (h *FindCollectionsByParentHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *FindCollectionsByParentHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract parent ID from URL parameters
parentIDStr := r.PathValue("parent_id")
if parentIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("parent_id", "Parent ID is required"))
return
}
// Convert string ID to ObjectID
parentID, err := gocql.ParseUUID(parentIDStr)
if err != nil {
h.logger.Error("invalid parent ID format",
zap.String("parent_id", parentIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("parent_id", "Invalid parent ID format"))
return
}
// Create request DTO
req := &svc_collection.FindByParentRequestDTO{
ParentID: parentID,
}
// Call service
resp, err := h.service.Execute(ctx, req)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,74 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/find_root_collections.go
package collection
import (
"encoding/json"
"net/http"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type FindRootCollectionsHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_collection.FindRootCollectionsService
middleware middleware.Middleware
}
func NewFindRootCollectionsHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_collection.FindRootCollectionsService,
middleware middleware.Middleware,
) *FindRootCollectionsHTTPHandler {
logger = logger.Named("FindRootCollectionsHTTPHandler")
return &FindRootCollectionsHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*FindRootCollectionsHTTPHandler) Pattern() string {
return "GET /api/v1/collections/root"
}
func (h *FindRootCollectionsHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *FindRootCollectionsHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
resp, err := h.service.Execute(ctx)
if err != nil {
h.logger.Error("Failed to find root collections", zap.Error(err))
// Service returns RFC 9457 errors, use RespondWithError to handle them
httperror.RespondWithError(w, r, err)
return
}
if resp == nil {
h.logger.Error("No collections returned from service")
problem := httperror.NewInternalServerError("Failed to retrieve collections. Please try again.").
WithInstance(r.URL.Path).
WithTraceID(httperror.ExtractRequestID(r))
httperror.RespondWithProblem(w, problem)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("Failed to encode collections response", zap.Error(err))
// At this point headers are already sent, log the error but can't send RFC 9457 response
return
}
}

View file

@ -0,0 +1,91 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/get.go
package collection
import (
"encoding/json"
"errors"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type GetCollectionHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_collection.GetCollectionService
middleware middleware.Middleware
}
func NewGetCollectionHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_collection.GetCollectionService,
middleware middleware.Middleware,
) *GetCollectionHTTPHandler {
logger = logger.Named("GetCollectionHTTPHandler")
return &GetCollectionHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*GetCollectionHTTPHandler) Pattern() string {
return "GET /api/v1/collections/{id}"
}
func (h *GetCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *GetCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract collection ID from URL parameters
// Assuming Go 1.22+ where r.PathValue is available for patterns like "/items/{id}"
collectionIDStr := r.PathValue("id")
if collectionIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
return
}
// Convert string ID to ObjectID
collectionID, err := gocql.ParseUUID(collectionIDStr)
if err != nil {
h.logger.Error("invalid collection ID format",
zap.String("collection_id", collectionIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
return
}
resp, err := h.service.Execute(ctx, collectionID)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,124 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/get_filtered.go
package collection
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type GetFilteredCollectionsHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_collection.GetFilteredCollectionsService
middleware middleware.Middleware
}
func NewGetFilteredCollectionsHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_collection.GetFilteredCollectionsService,
middleware middleware.Middleware,
) *GetFilteredCollectionsHTTPHandler {
logger = logger.Named("GetFilteredCollectionsHTTPHandler")
return &GetFilteredCollectionsHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*GetFilteredCollectionsHTTPHandler) Pattern() string {
return "GET /api/v1/collections/filtered"
}
func (h *GetFilteredCollectionsHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *GetFilteredCollectionsHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Parse query parameters for filter options
req, err := h.parseFilterOptions(r)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
resp, err := h.service.Execute(ctx, req)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}
// parseFilterOptions parses the query parameters to create the request DTO
func (h *GetFilteredCollectionsHTTPHandler) parseFilterOptions(r *http.Request) (*svc_collection.GetFilteredCollectionsRequestDTO, error) {
req := &svc_collection.GetFilteredCollectionsRequestDTO{
IncludeOwned: true, // Default to including owned collections
IncludeShared: false, // Default to not including shared collections
}
// Parse include_owned parameter
if includeOwnedStr := r.URL.Query().Get("include_owned"); includeOwnedStr != "" {
includeOwned, err := strconv.ParseBool(includeOwnedStr)
if err != nil {
h.logger.Warn("Invalid include_owned parameter",
zap.String("value", includeOwnedStr),
zap.Error(err))
return nil, httperror.NewForBadRequestWithSingleField("include_owned", "Invalid boolean value for include_owned parameter")
}
req.IncludeOwned = includeOwned
}
// Parse include_shared parameter
if includeSharedStr := r.URL.Query().Get("include_shared"); includeSharedStr != "" {
includeShared, err := strconv.ParseBool(includeSharedStr)
if err != nil {
h.logger.Warn("Invalid include_shared parameter",
zap.String("value", includeSharedStr),
zap.Error(err))
return nil, httperror.NewForBadRequestWithSingleField("include_shared", "Invalid boolean value for include_shared parameter")
}
req.IncludeShared = includeShared
}
// Validate that at least one option is enabled
if !req.IncludeOwned && !req.IncludeShared {
return nil, httperror.NewForBadRequestWithSingleField("filter_options", "At least one filter option (include_owned or include_shared) must be enabled")
}
h.logger.Debug("Parsed filter options",
zap.Bool("include_owned", req.IncludeOwned),
zap.Bool("include_shared", req.IncludeShared))
return req, nil
}

View file

@ -0,0 +1,73 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/list_by_user.go
package collection
import (
"encoding/json"
"errors"
"net/http"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type ListUserCollectionsHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_collection.ListUserCollectionsService
middleware middleware.Middleware
}
func NewListUserCollectionsHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_collection.ListUserCollectionsService,
middleware middleware.Middleware,
) *ListUserCollectionsHTTPHandler {
logger = logger.Named("ListUserCollectionsHTTPHandler")
return &ListUserCollectionsHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*ListUserCollectionsHTTPHandler) Pattern() string {
return "GET /api/v1/collections"
}
func (h *ListUserCollectionsHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *ListUserCollectionsHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
resp, err := h.service.Execute(ctx)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,73 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/list_shared_with_user.go
package collection
import (
"encoding/json"
"errors"
"net/http"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type ListSharedCollectionsHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_collection.ListSharedCollectionsService
middleware middleware.Middleware
}
func NewListSharedCollectionsHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_collection.ListSharedCollectionsService,
middleware middleware.Middleware,
) *ListSharedCollectionsHTTPHandler {
logger = logger.Named("ListSharedCollectionsHTTPHandler")
return &ListSharedCollectionsHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*ListSharedCollectionsHTTPHandler) Pattern() string {
return "GET /api/v1/collections/shared"
}
func (h *ListSharedCollectionsHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *ListSharedCollectionsHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Call service
resp, err := h.service.Execute(ctx)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,129 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/move_collection.go
package collection
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type MoveCollectionHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_collection.MoveCollectionService
middleware middleware.Middleware
}
func NewMoveCollectionHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_collection.MoveCollectionService,
middleware middleware.Middleware,
) *MoveCollectionHTTPHandler {
logger = logger.Named("MoveCollectionHTTPHandler")
return &MoveCollectionHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*MoveCollectionHTTPHandler) Pattern() string {
return "PUT /api/v1/collections/{id}/move"
}
func (h *MoveCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *MoveCollectionHTTPHandler) unmarshalRequest(
ctx context.Context,
r *http.Request,
collectionID gocql.UUID,
) (*svc_collection.MoveCollectionRequestDTO, error) {
// Initialize our structure which will store the parsed request data
var requestData svc_collection.MoveCollectionRequestDTO
defer r.Body.Close()
var rawJSON bytes.Buffer
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
// Read the JSON string and convert it into our golang struct
err := json.NewDecoder(teeReader).Decode(&requestData)
if err != nil {
h.logger.Error("decoding error",
zap.Any("err", err),
zap.String("json", rawJSON.String()),
)
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
}
// Set the collection ID from the URL parameter
requestData.CollectionID = collectionID
return &requestData, nil
}
func (h *MoveCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract collection ID from URL parameters
collectionIDStr := r.PathValue("id")
if collectionIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
return
}
// Convert string ID to ObjectID
collectionID, err := gocql.ParseUUID(collectionIDStr)
if err != nil {
h.logger.Error("invalid collection ID format",
zap.String("collection_id", collectionIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
return
}
req, err := h.unmarshalRequest(ctx, r, collectionID)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
resp, err := h.service.Execute(ctx, req)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,146 @@
package collection
import (
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
)
// Wire providers for collection HTTP handlers
func ProvideCreateCollectionHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_collection.CreateCollectionService,
mw middleware.Middleware,
) *CreateCollectionHTTPHandler {
return NewCreateCollectionHTTPHandler(cfg, logger, service, mw)
}
func ProvideGetCollectionHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_collection.GetCollectionService,
mw middleware.Middleware,
) *GetCollectionHTTPHandler {
return NewGetCollectionHTTPHandler(cfg, logger, service, mw)
}
func ProvideListUserCollectionsHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_collection.ListUserCollectionsService,
mw middleware.Middleware,
) *ListUserCollectionsHTTPHandler {
return NewListUserCollectionsHTTPHandler(cfg, logger, service, mw)
}
func ProvideUpdateCollectionHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_collection.UpdateCollectionService,
mw middleware.Middleware,
) *UpdateCollectionHTTPHandler {
return NewUpdateCollectionHTTPHandler(cfg, logger, service, mw)
}
func ProvideSoftDeleteCollectionHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_collection.SoftDeleteCollectionService,
mw middleware.Middleware,
) *SoftDeleteCollectionHTTPHandler {
return NewSoftDeleteCollectionHTTPHandler(cfg, logger, service, mw)
}
func ProvideArchiveCollectionHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_collection.ArchiveCollectionService,
mw middleware.Middleware,
) *ArchiveCollectionHTTPHandler {
return NewArchiveCollectionHTTPHandler(cfg, logger, service, mw)
}
func ProvideRestoreCollectionHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_collection.RestoreCollectionService,
mw middleware.Middleware,
) *RestoreCollectionHTTPHandler {
return NewRestoreCollectionHTTPHandler(cfg, logger, service, mw)
}
func ProvideListSharedCollectionsHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_collection.ListSharedCollectionsService,
mw middleware.Middleware,
) *ListSharedCollectionsHTTPHandler {
return NewListSharedCollectionsHTTPHandler(cfg, logger, service, mw)
}
func ProvideFindRootCollectionsHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_collection.FindRootCollectionsService,
mw middleware.Middleware,
) *FindRootCollectionsHTTPHandler {
return NewFindRootCollectionsHTTPHandler(cfg, logger, service, mw)
}
func ProvideFindCollectionsByParentHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_collection.FindCollectionsByParentService,
mw middleware.Middleware,
) *FindCollectionsByParentHTTPHandler {
return NewFindCollectionsByParentHTTPHandler(cfg, logger, service, mw)
}
func ProvideCollectionSyncHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_collection.GetCollectionSyncDataService,
mw middleware.Middleware,
) *CollectionSyncHTTPHandler {
return NewCollectionSyncHTTPHandler(cfg, logger, service, mw)
}
func ProvideMoveCollectionHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_collection.MoveCollectionService,
mw middleware.Middleware,
) *MoveCollectionHTTPHandler {
return NewMoveCollectionHTTPHandler(cfg, logger, service, mw)
}
func ProvideGetFilteredCollectionsHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_collection.GetFilteredCollectionsService,
mw middleware.Middleware,
) *GetFilteredCollectionsHTTPHandler {
return NewGetFilteredCollectionsHTTPHandler(cfg, logger, service, mw)
}
func ProvideShareCollectionHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_collection.ShareCollectionService,
mw middleware.Middleware,
) *ShareCollectionHTTPHandler {
return NewShareCollectionHTTPHandler(cfg, logger, service, mw)
}
func ProvideRemoveMemberHTTPHandler(
cfg *config.Configuration,
logger *zap.Logger,
service svc_collection.RemoveMemberService,
mw middleware.Middleware,
) *RemoveMemberHTTPHandler {
return NewRemoveMemberHTTPHandler(cfg, logger, service, mw)
}

View file

@ -0,0 +1,148 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/remove_member.go
package collection
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type RemoveMemberHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_collection.RemoveMemberService
middleware middleware.Middleware
}
func NewRemoveMemberHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_collection.RemoveMemberService,
middleware middleware.Middleware,
) *RemoveMemberHTTPHandler {
logger = logger.Named("RemoveMemberHTTPHandler")
return &RemoveMemberHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*RemoveMemberHTTPHandler) Pattern() string {
return "DELETE /api/v1/collections/{id}/members/{user_id}"
}
func (h *RemoveMemberHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *RemoveMemberHTTPHandler) unmarshalRequest(
ctx context.Context,
r *http.Request,
collectionID gocql.UUID,
recipientID gocql.UUID,
) (*svc_collection.RemoveMemberRequestDTO, error) {
// Initialize our structure which will store the parsed request data
var requestData svc_collection.RemoveMemberRequestDTO
defer r.Body.Close()
var rawJSON bytes.Buffer
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
// Read the JSON string and convert it into our golang struct
err := json.NewDecoder(teeReader).Decode(&requestData)
if err != nil {
h.logger.Error("decoding error",
zap.Any("err", err),
zap.String("json", rawJSON.String()),
)
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
}
// Set the collection ID and recipient ID from the URL parameters
requestData.CollectionID = collectionID
requestData.RecipientID = recipientID
return &requestData, nil
}
func (h *RemoveMemberHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract collection ID from URL parameters
collectionIDStr := r.PathValue("id")
if collectionIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
return
}
// Extract user ID from URL parameters
userIDStr := r.PathValue("user_id")
if userIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("user_id", "User ID is required"))
return
}
// Convert collection ID string to UUID
collectionID, err := gocql.ParseUUID(collectionIDStr)
if err != nil {
h.logger.Error("invalid collection ID format",
zap.String("collection_id", collectionIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
return
}
// Convert user ID string to UUID
userID, err := gocql.ParseUUID(userIDStr)
if err != nil {
h.logger.Error("invalid user ID format",
zap.String("user_id", userIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("user_id", "Invalid user ID format"))
return
}
req, err := h.unmarshalRequest(ctx, r, collectionID, userID)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
resp, err := h.service.Execute(ctx, req)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,96 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/restore.go
package collection
import (
"encoding/json"
"errors"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type RestoreCollectionHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_collection.RestoreCollectionService
middleware middleware.Middleware
}
func NewRestoreCollectionHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_collection.RestoreCollectionService,
middleware middleware.Middleware,
) *RestoreCollectionHTTPHandler {
logger = logger.Named("RestoreCollectionHTTPHandler")
return &RestoreCollectionHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*RestoreCollectionHTTPHandler) Pattern() string {
return "PUT /api/v1/collections/{id}/restore"
}
func (h *RestoreCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *RestoreCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract collection ID from the URL
collectionIDStr := r.PathValue("id")
if collectionIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
return
}
// Convert string ID to ObjectID
collectionID, err := gocql.ParseUUID(collectionIDStr)
if err != nil {
h.logger.Error("invalid collection ID format",
zap.String("collection_id", collectionIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
return
}
// Create request DTO
dtoReq := &svc_collection.RestoreCollectionRequestDTO{
ID: collectionID,
}
resp, err := h.service.Execute(ctx, dtoReq)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,167 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/share_collection.go
package collection
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
)
type ShareCollectionHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_collection.ShareCollectionService
middleware middleware.Middleware
}
func NewShareCollectionHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_collection.ShareCollectionService,
middleware middleware.Middleware,
) *ShareCollectionHTTPHandler {
logger = logger.Named("ShareCollectionHTTPHandler")
return &ShareCollectionHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*ShareCollectionHTTPHandler) Pattern() string {
return "POST /api/v1/collections/{id}/share"
}
func (h *ShareCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *ShareCollectionHTTPHandler) unmarshalRequest(
ctx context.Context,
r *http.Request,
collectionID gocql.UUID,
) (*svc_collection.ShareCollectionRequestDTO, error) {
// Initialize our structure which will store the parsed request data
var requestData svc_collection.ShareCollectionRequestDTO
defer r.Body.Close()
var rawJSON bytes.Buffer
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
// Read the JSON string and convert it into our golang struct
err := json.NewDecoder(teeReader).Decode(&requestData)
if err != nil {
h.logger.Error("JSON decoding error",
zap.Any("err", err),
zap.String("raw_json", rawJSON.String()),
)
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
}
// Log the decoded request for debugging (PII masked for security)
h.logger.Debug("decoded share collection request",
zap.String("collection_id_from_url", collectionID.String()),
zap.String("collection_id_from_body", requestData.CollectionID.String()),
zap.String("recipient_id", requestData.RecipientID.String()),
zap.String("recipient_email", validation.MaskEmail(requestData.RecipientEmail)),
zap.String("permission_level", requestData.PermissionLevel),
zap.Int("encrypted_key_length", len(requestData.EncryptedCollectionKey)),
zap.Bool("share_with_descendants", requestData.ShareWithDescendants))
// CRITICAL: Check if encrypted collection key is present in the request
if len(requestData.EncryptedCollectionKey) == 0 {
h.logger.Error("FRONTEND BUG: encrypted_collection_key is missing from request",
zap.String("collection_id", collectionID.String()),
zap.String("recipient_id", requestData.RecipientID.String()),
zap.String("recipient_email", validation.MaskEmail(requestData.RecipientEmail)))
// Log raw JSON at debug level only to avoid PII exposure in production logs
h.logger.Debug("raw request body for debugging",
zap.String("collection_id", collectionID.String()),
zap.String("raw_json", rawJSON.String()))
} else {
h.logger.Debug("encrypted_collection_key found in request",
zap.String("collection_id", collectionID.String()),
zap.String("recipient_id", requestData.RecipientID.String()),
zap.Int("encrypted_key_length", len(requestData.EncryptedCollectionKey)))
}
// Set the collection ID from the URL parameter
requestData.CollectionID = collectionID
return &requestData, nil
}
func (h *ShareCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract collection ID from URL parameters
collectionIDStr := r.PathValue("id")
if collectionIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
return
}
// Convert string ID to ObjectID
collectionID, err := gocql.ParseUUID(collectionIDStr)
if err != nil {
h.logger.Error("invalid collection ID format",
zap.String("collection_id", collectionIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
return
}
h.logger.Info("processing share collection request",
zap.String("collection_id", collectionID.String()),
zap.String("method", r.Method),
zap.String("content_type", r.Header.Get("Content-Type")))
req, err := h.unmarshalRequest(ctx, r, collectionID)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Call service
resp, err := h.service.Execute(ctx, req)
if err != nil {
h.logger.Error("share collection service failed",
zap.String("collection_id", collectionID.String()),
zap.String("recipient_id", req.RecipientID.String()),
zap.Error(err))
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,96 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/softdelete.go
package collection
import (
"encoding/json"
"errors"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type SoftDeleteCollectionHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_collection.SoftDeleteCollectionService
middleware middleware.Middleware
}
func NewSoftDeleteCollectionHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_collection.SoftDeleteCollectionService,
middleware middleware.Middleware,
) *SoftDeleteCollectionHTTPHandler {
logger = logger.Named("SoftDeleteCollectionHTTPHandler")
return &SoftDeleteCollectionHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*SoftDeleteCollectionHTTPHandler) Pattern() string {
return "DELETE /api/v1/collections/{id}"
}
func (h *SoftDeleteCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *SoftDeleteCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract collection ID from the URL
collectionIDStr := r.PathValue("id")
if collectionIDStr == "" {
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
return
}
// Convert string ID to ObjectID
collectionID, err := gocql.ParseUUID(collectionIDStr)
if err != nil {
h.logger.Error("invalid collection ID format",
zap.String("collection_id", collectionIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
return
}
// Create request DTO
dtoReq := &svc_collection.SoftDeleteCollectionRequestDTO{
ID: collectionID,
}
resp, err := h.service.Execute(ctx, dtoReq)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("no result")
httperror.RespondWithError(w, r, err)
return
}
}

View file

@ -0,0 +1,127 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/sync.go
package collection
import (
"encoding/json"
"net/http"
"strconv"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/constants"
dom_sync "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type CollectionSyncHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_collection.GetCollectionSyncDataService
middleware middleware.Middleware
}
func NewCollectionSyncHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_collection.GetCollectionSyncDataService,
middleware middleware.Middleware,
) *CollectionSyncHTTPHandler {
logger = logger.Named("CollectionSyncHTTPHandler")
return &CollectionSyncHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*CollectionSyncHTTPHandler) Pattern() string {
return "POST /api/v1/collections/sync"
}
func (h *CollectionSyncHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *CollectionSyncHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Get user ID from context
userID, ok := ctx.Value(constants.SessionUserID).(gocql.UUID)
if !ok {
h.logger.Error("Failed getting user ID from context")
httperror.RespondWithError(w, r, httperror.NewForInternalServerErrorWithSingleField("message", "Authentication context error"))
return
}
// Parse query parameters
queryParams := r.URL.Query()
// Parse limit parameter (default: 1000, max: 5000)
limit := int64(1000)
if limitStr := queryParams.Get("limit"); limitStr != "" {
if parsedLimit, err := strconv.ParseInt(limitStr, 10, 64); err == nil {
if parsedLimit > 0 && parsedLimit <= 5000 {
limit = parsedLimit
} else {
h.logger.Warn("Invalid limit parameter, using default",
zap.String("limit", limitStr),
zap.Int64("default", limit))
}
} else {
h.logger.Warn("Failed to parse limit parameter, using default",
zap.String("limit", limitStr),
zap.Error(err))
}
}
// Parse cursor parameter
var cursor *dom_sync.CollectionSyncCursor
if cursorStr := queryParams.Get("cursor"); cursorStr != "" {
var parsedCursor dom_sync.CollectionSyncCursor
if err := json.Unmarshal([]byte(cursorStr), &parsedCursor); err != nil {
h.logger.Error("Failed to parse cursor parameter",
zap.String("cursor", cursorStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("cursor", "Invalid cursor format"))
return
}
cursor = &parsedCursor
}
h.logger.Debug("Processing collection sync request",
zap.Any("user_id", userID),
zap.Int64("limit", limit),
zap.Any("cursor", cursor))
// Call service to get sync data
response, err := h.service.Execute(ctx, userID, cursor, limit, "all")
if err != nil {
h.logger.Error("Failed to get collection sync data",
zap.Any("user_id", userID),
zap.Error(err))
httperror.RespondWithError(w, r, err)
return
}
// Encode and return response
if err := json.NewEncoder(w).Encode(response); err != nil {
h.logger.Error("Failed to encode collection sync response",
zap.Error(err))
httperror.RespondWithError(w, r, err)
return
}
h.logger.Info("Successfully served collection sync data",
zap.Any("user_id", userID),
zap.Int("collections_count", len(response.Collections)),
zap.Bool("has_more", response.HasMore))
}

View file

@ -0,0 +1,136 @@
// monorepo/cloud/backend/internal/maplefile/interface/http/collection/update.go
package collection
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"go.uber.org/zap"
"github.com/gocql/gocql"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/interface/http/middleware"
svc_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/collection"
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
)
type UpdateCollectionHTTPHandler struct {
config *config.Configuration
logger *zap.Logger
service svc_collection.UpdateCollectionService
middleware middleware.Middleware
}
func NewUpdateCollectionHTTPHandler(
config *config.Configuration,
logger *zap.Logger,
service svc_collection.UpdateCollectionService,
middleware middleware.Middleware,
) *UpdateCollectionHTTPHandler {
logger = logger.Named("UpdateCollectionHTTPHandler")
return &UpdateCollectionHTTPHandler{
config: config,
logger: logger,
service: service,
middleware: middleware,
}
}
func (*UpdateCollectionHTTPHandler) Pattern() string {
return "PUT /api/v1/collections/{id}"
}
func (h *UpdateCollectionHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Apply middleware before handling the request
h.middleware.Attach(h.Execute)(w, req)
}
func (h *UpdateCollectionHTTPHandler) unmarshalRequest(
ctx context.Context,
r *http.Request,
collectionID gocql.UUID,
) (*svc_collection.UpdateCollectionRequestDTO, error) {
// Initialize our structure which will store the parsed request data
var requestData svc_collection.UpdateCollectionRequestDTO
defer r.Body.Close()
var rawJSON bytes.Buffer
teeReader := io.TeeReader(r.Body, &rawJSON) // TeeReader allows you to read the JSON and capture it
// Read the JSON string and convert it into our golang struct
err := json.NewDecoder(teeReader).Decode(&requestData)
if err != nil {
h.logger.Error("decoding error",
zap.Any("err", err),
zap.String("json", rawJSON.String()),
)
return nil, httperror.NewForSingleField(http.StatusBadRequest, "non_field_error", "payload structure is wrong")
}
// Set the collection ID from the URL parameter
requestData.ID = collectionID
return &requestData, nil
}
func (h *UpdateCollectionHTTPHandler) Execute(w http.ResponseWriter, r *http.Request) {
// Set response content type
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
// Extract collection ID from the URL path parameter
// This assumes the router is net/http (Go 1.22+) and the pattern was registered like "PUT /path/{id}"
collectionIDStr := r.PathValue("id")
if collectionIDStr == "" {
h.logger.Warn("collection_id not found in path parameters or is empty",
zap.String("path", r.URL.Path),
zap.String("method", r.Method),
)
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Collection ID is required"))
return
}
// Convert string ID to ObjectID
collectionID, err := gocql.ParseUUID(collectionIDStr)
if err != nil {
h.logger.Error("invalid collection ID format",
zap.String("collection_id", collectionIDStr),
zap.Error(err))
httperror.RespondWithError(w, r, httperror.NewForBadRequestWithSingleField("collection_id", "Invalid collection ID format"))
return
}
req, err := h.unmarshalRequest(ctx, r, collectionID)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Call service
resp, err := h.service.Execute(ctx, req)
if err != nil {
httperror.RespondWithError(w, r, err)
return
}
// Encode response
if resp != nil {
if err := json.NewEncoder(w).Encode(resp); err != nil {
h.logger.Error("failed to encode response",
zap.Any("error", err))
httperror.RespondWithError(w, r, err)
return
}
} else {
err := errors.New("transaction completed with no result") // Clarified error message
h.logger.Error("transaction completed with no result", zap.Any("request_payload", req))
httperror.RespondWithError(w, r, err)
return
}
}