Feat(BE-36,37,38,39): master area, customer, kandang, location, warehouse

This commit is contained in:
Hafizh A. Y
2025-10-02 10:51:15 +07:00
parent dbc1f79a36
commit e8905be856
79 changed files with 3745 additions and 169 deletions
+34
View File
@@ -0,0 +1,34 @@
package repository
import (
"context"
"gorm.io/gorm"
)
// Exists reports whether a record with the given ID exists for type T.
func Exists[T any](ctx context.Context, db *gorm.DB, id uint) (bool, error) {
var count int64
if err := db.WithContext(ctx).
Model(new(T)).
Where("id = ?", id).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeID *uint) (bool, error) {
var count int64
q := db.WithContext(ctx).
Model(new(T)).
Where("name = ?", name).
Where("deleted_at IS NULL")
if excludeID != nil {
q = q.Where("id <> ?", *excludeID)
}
if err := q.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
+242
View File
@@ -0,0 +1,242 @@
package repository
import (
"context"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type BaseRepository[T any] interface {
GetAll(ctx context.Context, offset, limit int, modifier func(*gorm.DB) *gorm.DB) ([]T, int64, error)
GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*T, error)
GetByIDs(ctx context.Context, ids []uint, modifier func(*gorm.DB) *gorm.DB) ([]T, error)
CreateOne(ctx context.Context, entity *T, modifier func(*gorm.DB) *gorm.DB) error
CreateMany(ctx context.Context, entities []*T, modifier func(*gorm.DB) *gorm.DB) error
UpdateOne(ctx context.Context, id uint, entity *T, modifier func(*gorm.DB) *gorm.DB) error
UpdateMany(ctx context.Context, entities []*T, modifier func(*gorm.DB) *gorm.DB) error
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
DeleteOne(ctx context.Context, id uint) error
DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error
Upsert(ctx context.Context, entity *T, conflictColumns []clause.Column, modifier func(*gorm.DB) *gorm.DB) error
WithTx(tx *gorm.DB) BaseRepository[T]
DB() *gorm.DB
}
type BaseRepositoryImpl[T any] struct {
db *gorm.DB
}
func NewBaseRepository[T any](db *gorm.DB) *BaseRepositoryImpl[T] {
return &BaseRepositoryImpl[T]{db: db}
}
func (r *BaseRepositoryImpl[T]) GetAll(
ctx context.Context,
offset, limit int,
modifier func(*gorm.DB) *gorm.DB,
) ([]T, int64, error) {
var entities []T
var total int64
q := r.db.WithContext(ctx).Model(new(T))
if modifier != nil {
q = modifier(q)
}
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := q.Offset(offset).Limit(limit).Find(&entities).Error; err != nil {
return nil, 0, err
}
return entities, total, nil
}
func (r *BaseRepositoryImpl[T]) GetByID(
ctx context.Context,
id uint,
modifier func(*gorm.DB) *gorm.DB,
) (*T, error) {
entity := new(T)
q := r.db.WithContext(ctx)
if modifier != nil {
q = modifier(q)
}
if err := q.First(entity, id).Error; err != nil {
return nil, err
}
return entity, nil
}
func (r *BaseRepositoryImpl[T]) GetByIDs(
ctx context.Context,
ids []uint,
modifier func(*gorm.DB) *gorm.DB,
) ([]T, error) {
var entities []T
q := r.db.WithContext(ctx).Model(new(T))
if modifier != nil {
q = modifier(q)
}
if err := q.Where("id IN ?", ids).Find(&entities).Error; err != nil {
return nil, err
}
if len(entities) == 0 {
return nil, gorm.ErrRecordNotFound
}
return entities, nil
}
// ---- CREATE ----
func (r *BaseRepositoryImpl[T]) CreateOne(
ctx context.Context,
entity *T,
modifier func(*gorm.DB) *gorm.DB,
) error {
q := r.db.WithContext(ctx)
if modifier != nil {
q = modifier(q)
}
return q.Create(entity).Error
}
func (r *BaseRepositoryImpl[T]) CreateMany(
ctx context.Context,
entities []*T,
modifier func(*gorm.DB) *gorm.DB,
) error {
q := r.db.WithContext(ctx)
if modifier != nil {
q = modifier(q)
}
return q.Create(&entities).Error
}
// ---- UPDATE ----
func (r *BaseRepositoryImpl[T]) UpdateOne(
ctx context.Context,
id uint,
entity *T,
modifier func(*gorm.DB) *gorm.DB,
) error {
q := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id)
if modifier != nil {
q = modifier(q)
}
result := q.Updates(entity)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (r *BaseRepositoryImpl[T]) UpdateMany(
ctx context.Context,
entities []*T,
modifier func(*gorm.DB) *gorm.DB,
) error {
q := r.db.WithContext(ctx)
if modifier != nil {
q = modifier(q)
}
result := q.Save(&entities)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (r *BaseRepositoryImpl[T]) PatchOne(
ctx context.Context,
id uint,
updates map[string]any,
modifier func(*gorm.DB) *gorm.DB,
) error {
q := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id)
if modifier != nil {
q = modifier(q)
}
result := q.Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// ---- DELETE ----
func (r *BaseRepositoryImpl[T]) DeleteOne(ctx context.Context, id uint) error {
result := r.db.WithContext(ctx).Delete(new(T), id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
func (r *BaseRepositoryImpl[T]) DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error {
q := r.db.WithContext(ctx).Model(new(T))
if modifier != nil {
q = modifier(q)
}
result := q.Delete(new(T))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// ---- UPSERT ----
func (r *BaseRepositoryImpl[T]) Upsert(
ctx context.Context,
entity *T,
conflictColumns []clause.Column,
modifier func(*gorm.DB) *gorm.DB,
) error {
q := r.db.WithContext(ctx).Clauses(clause.OnConflict{
Columns: conflictColumns,
UpdateAll: true,
})
if modifier != nil {
q = modifier(q)
}
return q.Create(entity).Error
}
func (r *BaseRepositoryImpl[T]) WithTx(tx *gorm.DB) BaseRepository[T] {
return &BaseRepositoryImpl[T]{db: tx}
}
func (r *BaseRepositoryImpl[T]) DB() *gorm.DB {
return r.db
}
+41
View File
@@ -0,0 +1,41 @@
package service
import (
"context"
"fmt"
"strings"
"github.com/gofiber/fiber/v2"
)
// RelationCheck describes a foreign-key style dependency that must exist before processing.
type RelationCheck struct {
Name string
ID *uint
Exists func(context.Context, uint) (bool, error)
}
// EnsureRelations validates that each RelationCheck is satisfied, returning consistent Fiber errors.
func EnsureRelations(ctx context.Context, checks ...RelationCheck) error {
for _, check := range checks {
if check.ID == nil {
continue
}
exists, err := check.Exists(ctx, *check.ID)
if err != nil {
return fiber.NewError(
fiber.StatusInternalServerError,
fmt.Sprintf("Failed to check %s", strings.ToLower(check.Name)),
)
}
if !exists {
return fiber.NewError(
fiber.StatusNotFound,
fmt.Sprintf("%s with id %d not found", check.Name, *check.ID),
)
}
}
return nil
}
@@ -0,0 +1,83 @@
package validation
import (
"reflect"
"regexp"
"strings"
"github.com/go-playground/validator/v10"
)
var (
reUpper = regexp.MustCompile(`[A-Z]`)
reLower = regexp.MustCompile(`[a-z]`)
reDigit = regexp.MustCompile(`[0-9]`)
reSym = regexp.MustCompile(`[^A-Za-z0-9]`)
)
func Password(fl validator.FieldLevel) bool {
pw := fl.Field().String()
pw = strings.TrimSpace(pw)
if len(pw) < 8 {
return false
}
if !reUpper.MatchString(pw) {
return false
}
if !reLower.MatchString(pw) {
return false
}
if !reDigit.MatchString(pw) {
return false
}
if !reSym.MatchString(pw) {
return false
}
if strings.Contains(pw, " ") {
return false
}
parent := fl.Parent()
if parent.IsValid() && parent.Kind() == reflect.Struct {
emailField := parent.FieldByName("Email")
if emailField.IsValid() && emailField.Kind() == reflect.String {
if email := emailField.String(); email != "" {
if i := strings.IndexByte(email, '@'); i > 0 {
local := strings.ToLower(email[:i])
if local != "" && strings.Contains(strings.ToLower(pw), local) {
return false
}
}
}
}
}
return true
}
func RequiredStrict(fl validator.FieldLevel) bool {
field := fl.Field()
switch field.Kind() {
case reflect.String:
return field.String() != ""
case reflect.Ptr:
return !field.IsNil()
}
return field.IsValid() && !field.IsZero()
}
func OmitemptyStrict(fl validator.FieldLevel) bool {
field := fl.Field()
if !field.IsValid() || field.IsZero() {
return true
}
if field.Kind() == reflect.String {
return field.String() != ""
}
return true
}
+74
View File
@@ -0,0 +1,74 @@
package validation
import (
"errors"
"fmt"
"github.com/go-playground/validator/v10"
)
var customMessages = map[string]string{
"required": "Field %s is required",
"required_strict": "Field %s is required and cannot be null or empty",
"omitempty_strict": "Field %s cannot be null or empty when provided",
"email": "Invalid email address for field %s",
"min": "Field %s must have a minimum length of %s characters",
"max": "Field %s must have a maximum length of %s characters",
"len": "Field %s must be exactly %s characters long",
"number": "Field %s must be a number",
"positive": "Field %s must be a positive number",
"alphanum": "Field %s must contain only alphanumeric characters",
"oneof": "Invalid value for field %s",
"password": "Field %s must be at least 8 characters, contain uppercase, lowercase, number, and special character",
}
func CustomErrorMessages(err error) map[string]string {
var validationErrors validator.ValidationErrors
if errors.As(err, &validationErrors) {
return generateErrorMessages(validationErrors)
}
return nil
}
func generateErrorMessages(validationErrors validator.ValidationErrors) map[string]string {
errorsMap := make(map[string]string)
for _, err := range validationErrors {
fieldName := err.StructNamespace()
tag := err.Tag()
customMessage := customMessages[tag]
if customMessage != "" {
errorsMap[fieldName] = formatErrorMessage(customMessage, err, tag)
} else {
errorsMap[fieldName] = defaultErrorMessage(err)
}
}
return errorsMap
}
func formatErrorMessage(customMessage string, err validator.FieldError, tag string) string {
if tag == "min" || tag == "max" || tag == "len" {
return fmt.Sprintf(customMessage, err.Field(), err.Param())
}
return fmt.Sprintf(customMessage, err.Field())
}
func defaultErrorMessage(err validator.FieldError) string {
return fmt.Sprintf("Field validation for '%s' failed on the '%s' tag", err.Field(), err.Tag())
}
func Validator() *validator.Validate {
validate := validator.New()
if err := validate.RegisterValidation("password", Password); err != nil {
return nil
}
if err := validate.RegisterValidation("required_strict", RequiredStrict); err != nil {
return nil
}
if err := validate.RegisterValidation("omitempty_strict", OmitemptyStrict); err != nil {
return nil
}
return validate
}