repository attachment api

This commit is contained in:
efrilm 2025-10-15 21:58:44 +07:00
parent da2246d45a
commit 58128495a6
15 changed files with 606 additions and 64 deletions

View File

@ -53,6 +53,7 @@ func (a *App) Initialize(cfg *config.Config) error {
onlyOfficeHandler := handler.NewOnlyOfficeHandler(services.onlyOfficeService)
analyticsHandler := handler.NewAnalyticsHandler(services.analyticsService)
notificationHandler := handler.NewNotificationHandler(services.notificationService)
repositoryAttachmentHandler := handler.NewRepositoryAttachmentHandler(services.repositoryAttachmentService)
a.router = router.NewRouter(
cfg,
@ -70,6 +71,7 @@ func (a *App) Initialize(cfg *config.Config) error {
onlyOfficeHandler,
analyticsHandler,
notificationHandler,
repositoryAttachmentHandler,
)
return nil
@ -147,6 +149,7 @@ type repositories struct {
approvalFlowRepo *repository.ApprovalFlowRepository
letterOutgoingApprovalRepo *repository.LetterOutgoingApprovalRepository
analyticsRepo *repository.AnalyticsRepository
repositoryAttachmentRepo *repository.RepositoryAttachmentRepositoryImpl
}
func (a *App) initRepositories() *repositories {
@ -181,6 +184,7 @@ func (a *App) initRepositories() *repositories {
approvalFlowRepo: repository.NewApprovalFlowRepository(a.db),
letterOutgoingApprovalRepo: repository.NewLetterOutgoingApprovalRepository(a.db),
analyticsRepo: repository.NewAnalyticsRepository(a.db),
repositoryAttachmentRepo: repository.NewRepositoryAttachmentRepositoryImpl(a.db),
}
}
@ -204,6 +208,7 @@ type processors struct {
letterAttachmentProcessor processor.LetterAttachmentProcessor
letterOutgoingRecipientProcessor processor.LetterOutgoingRecipientProcessor
letterActivityProcessor processor.LetterActivityProcessor
repositoryAttachmentProcessor *processor.RepositoryAttachmentProcessorImpl
txManager *repository.TxManager
}
@ -327,6 +332,9 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
repos.letterRepo,
)
repositoryAttachmentProc := processor.NewRepositoryAttachmentProcessor(
repos.repositoryAttachmentRepo)
return &processors{
userProcessor: userProc,
cachedUserProcessor: cachedUserProc,
@ -347,6 +355,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
letterAttachmentProcessor: letterAttachmentProc,
letterOutgoingRecipientProcessor: letterOutgoingRecipientProc,
letterActivityProcessor: letterActivityProc,
repositoryAttachmentProcessor: repositoryAttachmentProc,
txManager: txMgr,
}
}
@ -364,6 +373,7 @@ type services struct {
onlyOfficeService *service.OnlyOfficeServiceImpl
analyticsService *service.AnalyticsServiceImpl
notificationService *service.NotificationServiceImpl
repositoryAttachmentService *service.RepositoryAttachmentServiceImpl
}
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
@ -423,6 +433,8 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
novuConfig := internalConfig.LoadNovuConfig(cfg)
notificationSvc := service.NewNotificationService(novuConfig, processors.userProcessor)
repositoryAttachmentSvc := service.NewRepositoryAttachmentService(processors.repositoryAttachmentProcessor)
return &services{
userService: userSvc,
authService: authService,
@ -436,6 +448,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
onlyOfficeService: onlyOfficeSvc,
analyticsService: analyticsSvc,
notificationService: notificationSvc,
repositoryAttachmentService: repositoryAttachmentSvc,
}
}

View File

@ -0,0 +1,35 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type CreateRepositoryAttachmentRequest struct {
FileURL string `json:"file_url" validate:"required"`
FileName string `json:"file_name" validate:"required"`
FileType string `json:"file_type" validate:"required"`
Category string `json:"category" validate:"required"`
}
type RepositoryAttachmentsResponse struct {
ID uuid.UUID `json:"id"`
FileURL string `json:"file_url"`
FileName string `json:"file_name"`
FileType string `json:"file_type"`
Category string `json:"category"`
UploadBy uuid.UUID `json:"upload_by"`
UploadAt time.Time `json:"upload_at"`
}
type ListRepositoryAttachmentsResponse struct {
Attachments []RepositoryAttachmentsResponse `json:"attachments"`
Pagination PaginationResponse `json:"pagination"`
}
type ListRepositoryAttachmentsRequest struct {
Page int `json:"page"`
Limit int `json:"limit"`
Search *string `json:"search,omitempty"`
}

View File

@ -0,0 +1,19 @@
package entities
import (
"time"
"github.com/google/uuid"
)
type RepositoryAttachment struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
FileURL string `gorm:"not null" json:"file_url"`
FileName string `gorm:"not null" json:"file_name"`
FileType string `gorm:"not null" json:"file_type"`
Category string `gorm:"not null" json:"category"`
UploadedBy *uuid.UUID `json:"uploaded_by,omitempty"`
UploadedAt time.Time `gorm:"autoCreateTime" json:"uploaded_at"`
}
func (RepositoryAttachment) TableName() string { return "repository_attachments" }

View File

@ -0,0 +1,137 @@
package handler
import (
"eslogad-be/internal/constants"
"eslogad-be/internal/contract"
"eslogad-be/internal/logger"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type RepositoryAttachmentHandler struct {
attachmentService RepositoryAttachmentService
}
func NewRepositoryAttachmentHandler(attachmentService RepositoryAttachmentService) *RepositoryAttachmentHandler {
return &RepositoryAttachmentHandler{
attachmentService: attachmentService,
}
}
func (h *RepositoryAttachmentHandler) CreateAttachment(c *gin.Context) {
var req contract.CreateRepositoryAttachmentRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.FromContext(c).WithError(err).Error("UserHandler::CreateAttachment -> request binding failed")
h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode)
return
}
userResponse, err := h.attachmentService.CreateAttachment(c.Request.Context(), &req)
if err != nil {
logger.FromContext(c).WithError(err).Error("UserHandler::CreateAttachment -> Failed to create user from service")
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
logger.FromContext(c).Infof("UserHandler::CreateUser -> Successfully created repository attachment = %+v", userResponse)
c.JSON(http.StatusOK, contract.BuildSuccessResponse(userResponse))
}
func (h *RepositoryAttachmentHandler) DeleteAttachment(c *gin.Context) {
attachmentIDStr := c.Param("id")
attachmentID, err := uuid.Parse(attachmentIDStr)
if err != nil {
logger.FromContext(c).WithError(err).Error("UserHandler::DeleteAttachment -> Invalid attachment id")
h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode)
return
}
err = h.attachmentService.DeleteAttachment(c.Request.Context(), attachmentID)
if err != nil {
logger.FromContext(c).WithError(err).Error("UserHandler::DeleteAttachment -> Failed to delete attachment from service")
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
logger.FromContext(c).Info("UserHandler::DeleteAttachment -> Successfully deleted attachment")
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "User deleted successfully"})
}
func (h *RepositoryAttachmentHandler) GetAttachment(c *gin.Context) {
attachmentIDStr := c.Param("id")
attachmentID, err := uuid.Parse(attachmentIDStr)
if err != nil {
logger.FromContext(c).WithError(err).Error("UserHandler::GetAttachment -> Invalid attachment ID")
h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode)
return
}
attachmentResponse, err := h.attachmentService.GetById(c.Request.Context(), attachmentID)
if err != nil {
logger.FromContext(c).WithError(err).Error("UserHandler::GetAttachment -> Failed to get attachment from service")
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
logger.FromContext(c).Infof("UserHandler::GetAttachment -> Successfully retrieved attachment = %+v", attachmentResponse)
c.JSON(http.StatusOK, attachmentResponse)
}
func (h *RepositoryAttachmentHandler) ListAttachment(c *gin.Context) {
ctx := c.Request.Context()
req := &contract.ListRepositoryAttachmentsRequest{
Page: 1,
Limit: 10,
}
if page := c.Query("page"); page != "" {
if p, err := strconv.Atoi(page); err == nil {
req.Page = p
}
}
if limit := c.Query("limit"); limit != "" {
if l, err := strconv.Atoi(limit); err == nil {
req.Limit = l
}
}
attachmentsResponse, err := h.attachmentService.ListAttachment(ctx, req)
if err != nil {
logger.FromContext(c).WithError(err).Error("UserHandler::ListUsers -> Failed to list users from service")
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
logger.FromContext(c).Infof("UserHandler::ListUsers -> Successfully listed users = %+v", attachmentsResponse)
c.JSON(http.StatusOK, contract.BuildSuccessResponse(attachmentsResponse))
}
func (h *RepositoryAttachmentHandler) sendValidationErrorResponse(c *gin.Context, message string, errorCode string) {
statusCode := constants.HttpErrorMap[errorCode]
if statusCode == 0 {
statusCode = http.StatusBadRequest
}
errorResponse := &contract.ErrorResponse{
Error: message,
Code: statusCode,
Details: map[string]interface{}{
"error_code": errorCode,
"entity": constants.UserValidatorEntity,
},
}
c.JSON(statusCode, errorResponse)
}
func (h *RepositoryAttachmentHandler) sendErrorResponse(c *gin.Context, message string, statusCode int) {
errorResponse := &contract.ErrorResponse{
Error: message,
Code: statusCode,
Details: map[string]interface{}{},
}
c.JSON(statusCode, errorResponse)
}

View File

@ -0,0 +1,15 @@
package handler
import (
"context"
"eslogad-be/internal/contract"
"github.com/google/uuid"
)
type RepositoryAttachmentService interface {
CreateAttachment(ctx context.Context, req *contract.CreateRepositoryAttachmentRequest) (*contract.RepositoryAttachmentsResponse, error)
DeleteAttachment(ctx context.Context, id uuid.UUID) error
GetById(ctx context.Context, id uuid.UUID) (*contract.RepositoryAttachmentsResponse, error)
ListAttachment(ctx context.Context, req *contract.ListRepositoryAttachmentsRequest) (*contract.ListRepositoryAttachmentsResponse, error)
}

View File

@ -0,0 +1,78 @@
package processor
import (
"context"
"eslogad-be/internal/appcontext"
"eslogad-be/internal/contract"
"eslogad-be/internal/transformer"
"fmt"
"github.com/google/uuid"
)
type RepositoryAttachmentProcessorImpl struct {
attachmentRepo RepositoryAttachmentRepository
}
func NewRepositoryAttachmentProcessor(attachmentRepo RepositoryAttachmentRepository) *RepositoryAttachmentProcessorImpl {
return &RepositoryAttachmentProcessorImpl{
attachmentRepo: attachmentRepo,
}
}
func (p *RepositoryAttachmentProcessorImpl) CreateAttachment(ctx context.Context, req *contract.CreateRepositoryAttachmentRequest) (*contract.RepositoryAttachmentsResponse, error) {
userID := getUserIDFromContext(ctx)
attachmentEntity := transformer.CreateRepositoryAttachmentRequestToEntity(req, userID)
err := p.attachmentRepo.Create(ctx, attachmentEntity)
if err != nil {
return nil, fmt.Errorf("failed to create repository attachment: %w", err)
}
return transformer.RepositoryAttachmentEntityToContract(attachmentEntity), nil
}
func getUserIDFromContext(ctx context.Context) uuid.UUID {
appCtx := appcontext.FromGinContext(ctx)
if appCtx != nil {
return appCtx.UserID
}
return uuid.New()
}
func (p *RepositoryAttachmentProcessorImpl) DeleteAttachment(ctx context.Context, id uuid.UUID) error {
_, err := p.attachmentRepo.GetByID(ctx, id)
if err != nil {
return fmt.Errorf("repository attachment not found: %w", err)
}
err = p.attachmentRepo.Delete(ctx, id)
if err != nil {
return fmt.Errorf("failed to delete repository attachment: %w", err)
}
return nil
}
func (p *RepositoryAttachmentProcessorImpl) GetById(ctx context.Context, id uuid.UUID) (*contract.RepositoryAttachmentsResponse, error) {
attachment, err := p.attachmentRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("repository attachment not found: %w", err)
}
resp := transformer.RepositoryAttachmentEntityToContract(attachment)
return resp, nil
}
func (p *RepositoryAttachmentProcessorImpl) ListAttachment(ctx context.Context, search *string, limit, offset int) ([]contract.RepositoryAttachmentsResponse, int, error) {
attachments, totalCount, err := p.attachmentRepo.List(ctx, search, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("failed to get users: %w", err)
}
responses := transformer.RepositoryAttachmentEntityToContracts(attachments)
return responses, int(totalCount), nil
}

View File

@ -0,0 +1,16 @@
package processor
import (
"context"
"eslogad-be/internal/entities"
"github.com/google/uuid"
)
type RepositoryAttachmentRepository interface {
Create(ctx context.Context, user *entities.RepositoryAttachment) error
GetByID(ctx context.Context, id uuid.UUID) (*entities.RepositoryAttachment, error)
Update(ctx context.Context, user *entities.RepositoryAttachment) error
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, search *string, limit, offset int) ([]*entities.RepositoryAttachment, int64, error)
}

View File

@ -0,0 +1,74 @@
package repository
import (
"context"
"eslogad-be/internal/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
type RepositoryAttachmentRepositoryImpl struct {
b *gorm.DB
}
func NewRepositoryAttachmentRepositoryImpl(db *gorm.DB) *RepositoryAttachmentRepositoryImpl {
return &RepositoryAttachmentRepositoryImpl{
b: db,
}
}
func (r *RepositoryAttachmentRepositoryImpl) Create(ctx context.Context, user *entities.RepositoryAttachment) error {
return r.b.WithContext(ctx).Create(user).Error
}
func (r *RepositoryAttachmentRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.RepositoryAttachment, error) {
var attachment entities.RepositoryAttachment
err := r.b.WithContext(ctx).
First(&attachment, "id = ?", id).Error
if err != nil {
return nil, err
}
return &attachment, nil
}
func (r *RepositoryAttachmentRepositoryImpl) Update(ctx context.Context, user *entities.RepositoryAttachment) error {
return r.b.WithContext(ctx).Save(user).Error
}
func (r *RepositoryAttachmentRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
return r.b.WithContext(ctx).Delete(&entities.RepositoryAttachment{}, "id = ?", id).Error
}
func (r *RepositoryAttachmentRepositoryImpl) List(ctx context.Context, search *string, limit, offset int) ([]*entities.RepositoryAttachment, int64, error) {
var attachments []*entities.RepositoryAttachment
var total int64
baseQuery := r.b.WithContext(ctx).Model(&entities.RepositoryAttachment{})
if search != nil && *search != "" {
like := "%" + *search + "%"
baseQuery = baseQuery.Where("name ILIKE ? OR email ILIKE ?", like, like)
}
countQuery := baseQuery
if err := countQuery.Count(&total).Error; err != nil {
return nil, 0, err
}
dataQuery := r.b.WithContext(ctx).Model(&entities.RepositoryAttachment{})
if search != nil && *search != "" {
like := "%" + *search + "%"
dataQuery = dataQuery.Where("name ILIKE ? OR category ILIKE ?", like, like)
}
if err := dataQuery.
Limit(limit).
Offset(offset).
Find(&attachments).Error; err != nil {
return nil, 0, err
}
return attachments, total, nil
}

View File

@ -180,3 +180,10 @@ type NotificationHandler interface {
GetCurrentUserSubscriber(c *gin.Context)
UpdateCurrentUserSubscriberChannel(c *gin.Context)
}
type RepositoryAttachmentHandler interface {
CreateAttachment(c *gin.Context)
DeleteAttachment(c *gin.Context)
GetAttachment(c *gin.Context)
ListAttachment(c *gin.Context)
}

View File

@ -23,6 +23,7 @@ type Router struct {
onlyOfficeHandler OnlyOfficeHandler
analyticsHandler AnalyticsHandler
notificationHandler NotificationHandler
repositoryAttachmentHandler RepositoryAttachmentHandler
}
func NewRouter(
@ -41,6 +42,7 @@ func NewRouter(
onlyOfficeHandler OnlyOfficeHandler,
analyticsHandler AnalyticsHandler,
notificationHandler NotificationHandler,
repositoryAttachmentHandler RepositoryAttachmentHandler,
) *Router {
return &Router{
config: cfg,
@ -58,6 +60,7 @@ func NewRouter(
onlyOfficeHandler: onlyOfficeHandler,
analyticsHandler: analyticsHandler,
notificationHandler: notificationHandler,
repositoryAttachmentHandler: repositoryAttachmentHandler,
}
}
@ -236,6 +239,15 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
droutes.PUT("/:id/active", r.dispRouteHandler.SetActive)
}
repoattachsch := v1.Group("/repository-attachments")
repoattachsch.Use(r.authMiddleware.RequireAuth())
{
repoattachsch.POST("", r.repositoryAttachmentHandler.CreateAttachment)
repoattachsch.GET("", r.repositoryAttachmentHandler.ListAttachment)
repoattachsch.DELETE("/:id", r.repositoryAttachmentHandler.DeleteAttachment)
repoattachsch.GET("/:id", r.repositoryAttachmentHandler.GetAttachment)
}
admin := v1.Group("/setting")
admin.Use(r.authMiddleware.RequireAuth())
{

View File

@ -0,0 +1,15 @@
package service
import (
"context"
"eslogad-be/internal/contract"
"github.com/google/uuid"
)
type RepositoryAttachmentProcessor interface {
CreateAttachment(ctx context.Context, req *contract.CreateRepositoryAttachmentRequest) (*contract.RepositoryAttachmentsResponse, error)
DeleteAttachment(ctx context.Context, id uuid.UUID) error
GetById(ctx context.Context, id uuid.UUID) (*contract.RepositoryAttachmentsResponse, error)
ListAttachment(ctx context.Context, search *string, limit, offset int) ([]contract.RepositoryAttachmentsResponse, int, error)
}

View File

@ -0,0 +1,59 @@
package service
import (
"context"
"eslogad-be/internal/contract"
"eslogad-be/internal/transformer"
"github.com/google/uuid"
)
type RepositoryAttachmentServiceImpl struct {
attachmentProcessor RepositoryAttachmentProcessor
}
func NewRepositoryAttachmentService(attachmentProcessor RepositoryAttachmentProcessor) *RepositoryAttachmentServiceImpl {
return &RepositoryAttachmentServiceImpl{
attachmentProcessor: attachmentProcessor,
}
}
func (s *RepositoryAttachmentServiceImpl) CreateAttachment(ctx context.Context, req *contract.CreateRepositoryAttachmentRequest) (*contract.RepositoryAttachmentsResponse, error) {
return s.attachmentProcessor.CreateAttachment(ctx, req)
}
func (s *RepositoryAttachmentServiceImpl) DeleteAttachment(ctx context.Context, id uuid.UUID) error {
return s.attachmentProcessor.DeleteAttachment(ctx, id)
}
func (s *RepositoryAttachmentServiceImpl) GetById(ctx context.Context, id uuid.UUID) (*contract.RepositoryAttachmentsResponse, error) {
return s.attachmentProcessor.GetById(ctx, id)
}
func (s *RepositoryAttachmentServiceImpl) ListAttachment(ctx context.Context, req *contract.ListRepositoryAttachmentsRequest) (*contract.ListRepositoryAttachmentsResponse, error) {
page := req.Page
if page <= 0 {
page = 1
}
limit := req.Limit
if limit <= 0 {
limit = 10
}
if limit > 100 {
limit = 100 // Max limit to prevent performance issues
}
offset := (page - 1) * limit
// Pass calculated offset and limit to processor
attachmentResponses, totalCount, err := s.attachmentProcessor.ListAttachment(ctx, req.Search, limit, offset)
if err != nil {
return nil, err
}
return &contract.ListRepositoryAttachmentsResponse{
Attachments: attachmentResponses,
Pagination: transformer.CreatePaginationResponse(totalCount, page, limit),
}, nil
}

View File

@ -0,0 +1,49 @@
package transformer
import (
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
"github.com/google/uuid"
)
func CreateRepositoryAttachmentRequestToEntity(req *contract.CreateRepositoryAttachmentRequest, userId uuid.UUID) *entities.RepositoryAttachment {
if req == nil {
return nil
}
return &entities.RepositoryAttachment{
FileName: req.FileName,
FileType: req.FileType,
FileURL: req.FileURL,
UploadedBy: &userId,
Category: req.Category,
}
}
func RepositoryAttachmentEntityToContract(entity *entities.RepositoryAttachment) *contract.RepositoryAttachmentsResponse {
resp := &contract.RepositoryAttachmentsResponse{
ID: entity.ID,
FileName: entity.FileName,
FileType: entity.FileType,
FileURL: entity.FileURL,
Category: entity.Category,
UploadBy: *entity.UploadedBy,
UploadAt: entity.UploadedAt,
}
return resp
}
func RepositoryAttachmentEntityToContracts(attachments []*entities.RepositoryAttachment) []contract.RepositoryAttachmentsResponse {
if attachments == nil {
return nil
}
responses := make([]contract.RepositoryAttachmentsResponse, len(attachments))
for i, u := range attachments {
resp := RepositoryAttachmentEntityToContract(u)
if resp != nil {
responses[i] = *resp
}
}
return responses
}

View File

@ -0,0 +1,13 @@
BEGIN;
CREATE TABLE IF NOT EXISTS repository_attachments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
file_url TEXT NOT NULL,
file_name TEXT NOT NULL,
file_type TEXT NOT NULL,
category TEXT NOT NULL,
uploaded_by UUID REFERENCES users(id) ON DELETE SET NULL,
uploaded_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
COMMIT;