narasiahli-be/plan/code-templates.md

21 KiB

Code Templates for Narasi Ahli Implementation

Database Entity Template

Example: Education History Entity

package entity

import (
    "narasi-ahli-be/app/database/entity/users"
    "time"
)

type EducationHistory struct {
    ID               uint         `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
    UserID           uint         `json:"user_id" gorm:"type:int4;not null;index"`
    SchoolName       string       `json:"school_name" gorm:"type:varchar(255);not null"`
    Major            string       `json:"major" gorm:"type:varchar(255);not null"`
    EducationLevel   string       `json:"education_level" gorm:"type:varchar(100);not null"`
    GraduationYear   int          `json:"graduation_year" gorm:"type:int4;not null"`
    CertificateImage *string      `json:"certificate_image" gorm:"type:varchar(500)"`
    CreatedAt        time.Time    `json:"created_at" gorm:"default:now()"`
    UpdatedAt        time.Time    `json:"updated_at" gorm:"default:now()"`
    
    // Relationships
    User *users.Users `json:"user" gorm:"foreignKey:UserID;references:ID"`
}

func (EducationHistory) TableName() string {
    return "education_histories"
}

Module Structure Template

Module File Template

package education_history

import (
    "narasi-ahli-be/app/module/education_history/controller"
    "narasi-ahli-be/app/module/education_history/repository"
    "narasi-ahli-be/app/module/education_history/service"

    "github.com/gofiber/fiber/v2"
    "go.uber.org/fx"
)

type EducationHistoryRouter struct {
    App        fiber.Router
    Controller *controller.Controller
}

var NewEducationHistoryModule = fx.Options(
    fx.Provide(repository.NewEducationHistoryRepository),
    fx.Provide(service.NewEducationHistoryService),
    fx.Provide(controller.NewController),
    fx.Provide(NewEducationHistoryRouter),
)

func NewEducationHistoryRouter(fiber *fiber.App, controller *controller.Controller) *EducationHistoryRouter {
    return &EducationHistoryRouter{
        App:        fiber,
        Controller: controller,
    }
}

func (_i *EducationHistoryRouter) RegisterEducationHistoryRoutes() {
    educationController := _i.Controller.EducationHistory

    _i.App.Route("/education-history", func(router fiber.Router) {
        router.Get("/", educationController.All)
        router.Get("/:id", educationController.Show)
        router.Post("/", educationController.Save)
        router.Put("/:id", educationController.Update)
        router.Delete("/:id", educationController.Delete)
        router.Post("/:id/certificate", educationController.UploadCertificate)
    })
}

Controller Template

package controller

import (
    "narasi-ahli-be/app/middleware"
    "narasi-ahli-be/app/module/education_history/request"
    "narasi-ahli-be/app/module/education_history/service"
    "narasi-ahli-be/utils/paginator"
    utilRes "narasi-ahli-be/utils/response"
    utilVal "narasi-ahli-be/utils/validator"
    utilSvc "narasi-ahli-be/utils/service"
    "strconv"

    "github.com/gofiber/fiber/v2"
)

type educationHistoryController struct {
    educationHistoryService service.EducationHistoryService
}

type EducationHistoryController interface {
    All(c *fiber.Ctx) error
    Show(c *fiber.Ctx) error
    Save(c *fiber.Ctx) error
    Update(c *fiber.Ctx) error
    Delete(c *fiber.Ctx) error
    UploadCertificate(c *fiber.Ctx) error
}

func NewEducationHistoryController(educationHistoryService service.EducationHistoryService) EducationHistoryController {
    return &educationHistoryController{
        educationHistoryService: educationHistoryService,
    }
}

// All Education History
// @Summary      Get all Education History
// @Description  API for getting all Education History for authenticated user
// @Tags         Education History
// @Security     Bearer
// @Param        req query request.EducationHistoryQueryRequest false "query parameters"
// @Param        req query paginator.Pagination false "pagination parameters"
// @Success      200  {object}  response.Response
// @Failure      400  {object}  response.BadRequestError
// @Failure      401  {object}  response.UnauthorizedError
// @Failure      500  {object}  response.InternalServerError
// @Router       /education-history [get]
func (_i *educationHistoryController) All(c *fiber.Ctx) error {
    paginate, err := paginator.Paginate(c)
    if err != nil {
        return err
    }

    // Get user ID from auth token
    authHeader := c.Get("Authorization")
    userId := utilSvc.GetUserId(authHeader)
    if userId == nil {
        return utilRes.Resp(c, utilRes.Response{
            Success: false,
            Code:    401,
            Messages: utilRes.Messages{"Unauthorized"},
        })
    }

    reqContext := request.EducationHistoryQueryRequestContext{
        SchoolName:     c.Query("schoolName"),
        Major:          c.Query("major"),
        EducationLevel: c.Query("educationLevel"),
        GraduationYear: c.Query("graduationYear"),
    }
    req := reqContext.ToParamRequest()
    req.Pagination = paginate

    clientId := middleware.GetClientID(c)
    educationData, paging, err := _i.educationHistoryService.All(clientId, *userId, req)
    if err != nil {
        return err
    }

    return utilRes.Resp(c, utilRes.Response{
        Success:  true,
        Messages: utilRes.Messages{"Education history list successfully retrieved"},
        Data:     educationData,
        Meta:     paging,
    })
}

// Show Education History
// @Summary      Get one Education History
// @Description  API for getting one Education History
// @Tags         Education History
// @Security     Bearer
// @Param        id path int true "Education History ID"
// @Success      200  {object}  response.Response
// @Failure      400  {object}  response.BadRequestError
// @Failure      401  {object}  response.UnauthorizedError
// @Failure      500  {object}  response.InternalServerError
// @Router       /education-history/{id} [get]
func (_i *educationHistoryController) Show(c *fiber.Ctx) error {
    id, err := strconv.ParseUint(c.Params("id"), 10, 0)
    if err != nil {
        return err
    }

    authHeader := c.Get("Authorization")
    userId := utilSvc.GetUserId(authHeader)
    if userId == nil {
        return utilRes.Resp(c, utilRes.Response{
            Success: false,
            Code:    401,
            Messages: utilRes.Messages{"Unauthorized"},
        })
    }

    clientId := middleware.GetClientID(c)
    educationData, err := _i.educationHistoryService.Show(clientId, *userId, uint(id))
    if err != nil {
        return err
    }

    return utilRes.Resp(c, utilRes.Response{
        Success:  true,
        Messages: utilRes.Messages{"Education history successfully retrieved"},
        Data:     educationData,
    })
}

// Save Education History
// @Summary      Create Education History
// @Description  API for create Education History
// @Tags         Education History
// @Security     Bearer
// @Param        X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param        Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param        payload body request.EducationHistoryCreateRequest true "Required payload"
// @Success      200  {object}  response.Response
// @Failure      400  {object}  response.BadRequestError
// @Failure      401  {object}  response.UnauthorizedError
// @Failure      500  {object}  response.InternalServerError
// @Router       /education-history [post]
func (_i *educationHistoryController) Save(c *fiber.Ctx) error {
    req := new(request.EducationHistoryCreateRequest)
    if err := utilVal.ParseAndValidate(c, req); err != nil {
        return err
    }

    authHeader := c.Get("Authorization")
    userId := utilSvc.GetUserId(authHeader)
    if userId == nil {
        return utilRes.Resp(c, utilRes.Response{
            Success: false,
            Code:    401,
            Messages: utilRes.Messages{"Unauthorized"},
        })
    }

    clientId := middleware.GetClientID(c)
    dataResult, err := _i.educationHistoryService.Save(clientId, *userId, *req)
    if err != nil {
        return err
    }

    return utilRes.Resp(c, utilRes.Response{
        Success:  true,
        Messages: utilRes.Messages{"Education history successfully created"},
        Data:     dataResult,
    })
}

// Additional methods: Update, Delete, UploadCertificate...

Service Template

package service

import (
    "narasi-ahli-be/app/module/education_history/mapper"
    "narasi-ahli-be/app/module/education_history/repository"
    "narasi-ahli-be/app/module/education_history/request"
    "narasi-ahli-be/app/module/education_history/response"
    "narasi-ahli-be/utils/paginator"

    "github.com/google/uuid"
    "github.com/rs/zerolog"
)

type educationHistoryService struct {
    Repo repository.EducationHistoryRepository
    Log  zerolog.Logger
}

type EducationHistoryService interface {
    All(clientId *uuid.UUID, userId uint, req request.EducationHistoryQueryRequest) (educationHistories []*response.EducationHistoryResponse, paging paginator.Pagination, err error)
    Show(clientId *uuid.UUID, userId uint, id uint) (educationHistory *response.EducationHistoryResponse, err error)
    Save(clientId *uuid.UUID, userId uint, req request.EducationHistoryCreateRequest) (educationHistory *response.EducationHistoryResponse, err error)
    Update(clientId *uuid.UUID, userId uint, id uint, req request.EducationHistoryUpdateRequest) (err error)
    Delete(clientId *uuid.UUID, userId uint, id uint) error
    UploadCertificate(clientId *uuid.UUID, userId uint, id uint, filePath string) error
}

func NewEducationHistoryService(repo repository.EducationHistoryRepository, log zerolog.Logger) EducationHistoryService {
    return &educationHistoryService{
        Repo: repo,
        Log:  log,
    }
}

func (_i *educationHistoryService) All(clientId *uuid.UUID, userId uint, req request.EducationHistoryQueryRequest) (educationHistories []*response.EducationHistoryResponse, paging paginator.Pagination, err error) {
    results, paging, err := _i.Repo.GetAll(clientId, userId, req)
    if err != nil {
        return
    }

    for _, result := range results {
        educationHistories = append(educationHistories, mapper.EducationHistoryResponseMapper(result))
    }

    return
}

func (_i *educationHistoryService) Show(clientId *uuid.UUID, userId uint, id uint) (educationHistory *response.EducationHistoryResponse, err error) {
    result, err := _i.Repo.FindOneByUserAndId(clientId, userId, id)
    if err != nil {
        return nil, err
    }

    return mapper.EducationHistoryResponseMapper(result), nil
}

func (_i *educationHistoryService) Save(clientId *uuid.UUID, userId uint, req request.EducationHistoryCreateRequest) (educationHistory *response.EducationHistoryResponse, err error) {
    _i.Log.Info().Interface("data", req).Msg("Creating education history")

    entity := req.ToEntity()
    entity.UserID = userId

    result, err := _i.Repo.Create(clientId, entity)
    if err != nil {
        return nil, err
    }

    return mapper.EducationHistoryResponseMapper(result), nil
}

// Additional methods: Update, Delete, UploadCertificate...

Repository Template

package repository

import (
    "narasi-ahli-be/app/database/entity"
    "narasi-ahli-be/app/module/education_history/request"
    "narasi-ahli-be/utils/paginator"

    "github.com/google/uuid"
    "gorm.io/gorm"
)

type educationHistoryRepository struct {
    DB *gorm.DB
}

type EducationHistoryRepository interface {
    GetAll(clientId *uuid.UUID, userId uint, req request.EducationHistoryQueryRequest) (educationHistories []*entity.EducationHistory, paging paginator.Pagination, err error)
    FindOneByUserAndId(clientId *uuid.UUID, userId uint, id uint) (educationHistory *entity.EducationHistory, err error)
    Create(clientId *uuid.UUID, educationHistory *entity.EducationHistory) (result *entity.EducationHistory, err error)
    Update(clientId *uuid.UUID, userId uint, id uint, educationHistory *entity.EducationHistory) (err error)
    Delete(clientId *uuid.UUID, userId uint, id uint) (err error)
}

func NewEducationHistoryRepository(db *gorm.DB) EducationHistoryRepository {
    return &educationHistoryRepository{
        DB: db,
    }
}

func (_i *educationHistoryRepository) GetAll(clientId *uuid.UUID, userId uint, req request.EducationHistoryQueryRequest) (educationHistories []*entity.EducationHistory, paging paginator.Pagination, err error) {
    query := _i.DB.Where("user_id = ?", userId)

    // Apply filters
    if req.SchoolName != nil {
        query = query.Where("school_name ILIKE ?", "%"+*req.SchoolName+"%")
    }
    if req.Major != nil {
        query = query.Where("major ILIKE ?", "%"+*req.Major+"%")
    }
    if req.EducationLevel != nil {
        query = query.Where("education_level = ?", *req.EducationLevel)
    }
    if req.GraduationYear != nil {
        query = query.Where("graduation_year = ?", *req.GraduationYear)
    }

    // Include user relationship
    query = query.Preload("User")

    // Apply pagination
    paging = paginator.Paging(req.Pagination, query, &educationHistories)

    return
}

func (_i *educationHistoryRepository) FindOneByUserAndId(clientId *uuid.UUID, userId uint, id uint) (educationHistory *entity.EducationHistory, err error) {
    err = _i.DB.Where("user_id = ? AND id = ?", userId, id).Preload("User").First(&educationHistory).Error
    return
}

func (_i *educationHistoryRepository) Create(clientId *uuid.UUID, educationHistory *entity.EducationHistory) (result *entity.EducationHistory, err error) {
    err = _i.DB.Create(educationHistory).Error
    if err != nil {
        return nil, err
    }

    // Reload with relationships
    err = _i.DB.Preload("User").First(&result, educationHistory.ID).Error
    return
}

// Additional methods: Update, Delete...

Request DTO Template

package request

import (
    "narasi-ahli-be/app/database/entity"
    "narasi-ahli-be/utils/paginator"
    "strconv"
)

type EducationHistoryQueryRequest struct {
    SchoolName     *string               `json:"schoolName"`
    Major          *string               `json:"major"`
    EducationLevel *string               `json:"educationLevel"`
    GraduationYear *int                  `json:"graduationYear"`
    Pagination     *paginator.Pagination `json:"pagination"`
}

type EducationHistoryCreateRequest struct {
    SchoolName       string `json:"schoolName" validate:"required,min=2,max=255"`
    Major            string `json:"major" validate:"required,min=2,max=255"`
    EducationLevel   string `json:"educationLevel" validate:"required,min=2,max=100"`
    GraduationYear   int    `json:"graduationYear" validate:"required,min=1950,max=2030"`
    CertificateImage string `json:"certificateImage,omitempty"`
}

func (req EducationHistoryCreateRequest) ToEntity() *entity.EducationHistory {
    return &entity.EducationHistory{
        SchoolName:       req.SchoolName,
        Major:            req.Major,
        EducationLevel:   req.EducationLevel,
        GraduationYear:   req.GraduationYear,
        CertificateImage: &req.CertificateImage,
    }
}

type EducationHistoryUpdateRequest struct {
    SchoolName       string `json:"schoolName" validate:"required,min=2,max=255"`
    Major            string `json:"major" validate:"required,min=2,max=255"`
    EducationLevel   string `json:"educationLevel" validate:"required,min=2,max=100"`
    GraduationYear   int    `json:"graduationYear" validate:"required,min=1950,max=2030"`
    CertificateImage string `json:"certificateImage,omitempty"`
}

func (req EducationHistoryUpdateRequest) ToEntity() *entity.EducationHistory {
    return &entity.EducationHistory{
        SchoolName:       req.SchoolName,
        Major:            req.Major,
        EducationLevel:   req.EducationLevel,
        GraduationYear:   req.GraduationYear,
        CertificateImage: &req.CertificateImage,
    }
}

type EducationHistoryQueryRequestContext struct {
    SchoolName     string `json:"schoolName"`
    Major          string `json:"major"`
    EducationLevel string `json:"educationLevel"`
    GraduationYear string `json:"graduationYear"`
}

func (req EducationHistoryQueryRequestContext) ToParamRequest() EducationHistoryQueryRequest {
    var request EducationHistoryQueryRequest

    if schoolName := req.SchoolName; schoolName != "" {
        request.SchoolName = &schoolName
    }
    if major := req.Major; major != "" {
        request.Major = &major
    }
    if educationLevel := req.EducationLevel; educationLevel != "" {
        request.EducationLevel = &educationLevel
    }
    if graduationYearStr := req.GraduationYear; graduationYearStr != "" {
        graduationYear, err := strconv.Atoi(graduationYearStr)
        if err == nil {
            request.GraduationYear = &graduationYear
        }
    }

    return request
}

Response DTO Template

package response

import (
    "time"
)

type EducationHistoryResponse struct {
    ID               uint      `json:"id"`
    UserID           uint      `json:"userId"`
    SchoolName       string    `json:"schoolName"`
    Major            string    `json:"major"`
    EducationLevel   string    `json:"educationLevel"`
    GraduationYear   int       `json:"graduationYear"`
    CertificateImage *string   `json:"certificateImage"`
    CreatedAt        time.Time `json:"createdAt"`
    UpdatedAt        time.Time `json:"updatedAt"`
    
    // Nested user info (optional)
    User *UserBasicInfo `json:"user,omitempty"`
}

type UserBasicInfo struct {
    ID       uint   `json:"id"`
    Username string `json:"username"`
    Fullname string `json:"fullname"`
    Email    string `json:"email"`
}

Mapper Template

package mapper

import (
    "narasi-ahli-be/app/database/entity"
    "narasi-ahli-be/app/module/education_history/response"
)

func EducationHistoryResponseMapper(educationHistory *entity.EducationHistory) *response.EducationHistoryResponse {
    result := &response.EducationHistoryResponse{
        ID:               educationHistory.ID,
        UserID:           educationHistory.UserID,
        SchoolName:       educationHistory.SchoolName,
        Major:            educationHistory.Major,
        EducationLevel:   educationHistory.EducationLevel,
        GraduationYear:   educationHistory.GraduationYear,
        CertificateImage: educationHistory.CertificateImage,
        CreatedAt:        educationHistory.CreatedAt,
        UpdatedAt:        educationHistory.UpdatedAt,
    }

    // Include user info if loaded
    if educationHistory.User != nil {
        result.User = &response.UserBasicInfo{
            ID:       educationHistory.User.ID,
            Username: educationHistory.User.Username,
            Fullname: educationHistory.User.Fullname,
            Email:    educationHistory.User.Email,
        }
    }

    return result
}

File Upload Service Template

package service

import (
    "context"
    "fmt"
    "mime/multipart"
    "path/filepath"
    "strings"
    "time"

    "github.com/google/uuid"
    "github.com/minio/minio-go/v7"
)

type FileUploadService interface {
    UploadCertificate(file *multipart.FileHeader) (string, error)
    UploadChatFile(file *multipart.FileHeader, messageType string) (string, error)
    ValidateFile(file *multipart.FileHeader, allowedTypes []string, maxSize int64) error
}

type fileUploadService struct {
    minioClient *minio.Client
    bucketName  string
}

func NewFileUploadService(minioClient *minio.Client, bucketName string) FileUploadService {
    return &fileUploadService{
        minioClient: minioClient,
        bucketName:  bucketName,
    }
}

func (s *fileUploadService) UploadCertificate(file *multipart.FileHeader) (string, error) {
    // Validate file
    allowedTypes := []string{".jpg", ".jpeg", ".png", ".pdf"}
    maxSize := int64(5 * 1024 * 1024) // 5MB
    
    if err := s.ValidateFile(file, allowedTypes, maxSize); err != nil {
        return "", err
    }

    // Generate unique filename
    ext := filepath.Ext(file.Filename)
    filename := fmt.Sprintf("certificates/%s_%d%s", uuid.New().String(), time.Now().Unix(), ext)

    // Upload to MinIO
    src, err := file.Open()
    if err != nil {
        return "", err
    }
    defer src.Close()

    _, err = s.minioClient.PutObject(
        context.Background(),
        s.bucketName,
        filename,
        src,
        file.Size,
        minio.PutObjectOptions{ContentType: file.Header.Get("Content-Type")},
    )
    if err != nil {
        return "", err
    }

    return filename, nil
}

func (s *fileUploadService) ValidateFile(file *multipart.FileHeader, allowedTypes []string, maxSize int64) error {
    // Check file size
    if file.Size > maxSize {
        return fmt.Errorf("file size exceeds maximum allowed size")
    }

    // Check file type
    ext := strings.ToLower(filepath.Ext(file.Filename))
    allowed := false
    for _, allowedType := range allowedTypes {
        if ext == allowedType {
            allowed = true
            break
        }
    }
    if !allowed {
        return fmt.Errorf("file type not allowed")
    }

    return nil
}

These templates provide a solid foundation for implementing each module following the existing codebase patterns. Each module can be built by copying and adapting these templates with the specific field requirements for that module.