Add initial project structure with core functionality for book and stationery uploads

- Created main application entry point in `main.go`.
- Added configuration management in `config/config.go` and tests in `config/config_test.go`.
- Implemented handlers for book and stationery uploads in `handlers/book.go` and `handlers/stationery.go`, including validation logic.
- Established database connection and services in `services/database.go` and `services/book_service.go`.
- Defined models for books and stationery in `models/book.go` and `models/stationery.go`.
- Set up Firebase integration for image uploads in `services/firebase.go`.
- Created migration scripts for database schema in `migrations/001_create_tables.sql` and subsequent updates.
- Added CORS and error handling middleware.
- Included comprehensive tests for handlers, services, and utilities.
- Documented API endpoints and usage in `README.md` and migration instructions in `migrations/README.md`.
- Introduced `.gitignore` to exclude unnecessary files and directories.
- Added Go module support with `go.mod` and `go.sum` files.
- Implemented utility functions for slug generation and validation in `utils/slug.go` and `utils/validation.go`.
This commit is contained in:
ianshaloom
2025-11-21 08:50:27 +03:00
commit ebeae34e01
39 changed files with 4044 additions and 0 deletions

182
utils/validation.go Normal file
View File

@@ -0,0 +1,182 @@
package utils
import (
"fmt"
"strconv"
"strings"
)
// ValidationError represents a validation error with field name
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// ValidateBookFields validates all book form fields
func ValidateBookFields(bookName, cost, price, discount, quantity, publisherAuthor, category, description string) []*ValidationError {
var errors []*ValidationError
// Book Name
bookName = strings.TrimSpace(bookName)
if bookName == "" {
errors = append(errors, &ValidationError{Field: "book_name", Message: "is required"})
} else if len(bookName) > 200 {
errors = append(errors, &ValidationError{Field: "book_name", Message: "exceeds 200 characters"})
}
// Cost
if cost == "" {
errors = append(errors, &ValidationError{Field: "cost", Message: "is required"})
} else {
costVal, err := strconv.ParseFloat(cost, 64)
if err != nil || costVal <= 0 {
errors = append(errors, &ValidationError{Field: "cost", Message: "must be a positive number"})
}
}
// Price
if price == "" {
errors = append(errors, &ValidationError{Field: "price", Message: "is required"})
} else {
priceVal, err := strconv.ParseFloat(price, 64)
if err != nil || priceVal <= 0 {
errors = append(errors, &ValidationError{Field: "price", Message: "must be a positive number"})
}
}
// Discount (optional)
if discount != "" {
discountVal, err := strconv.ParseFloat(discount, 64)
if err != nil || discountVal < 0 {
errors = append(errors, &ValidationError{Field: "discount", Message: "must be a non-negative number"})
}
}
// Quantity
if quantity == "" {
errors = append(errors, &ValidationError{Field: "quantity", Message: "is required"})
} else {
quantityVal, err := strconv.Atoi(quantity)
if err != nil || quantityVal <= 0 {
errors = append(errors, &ValidationError{Field: "quantity", Message: "must be a positive integer"})
}
}
// Publisher/Author
publisherAuthor = strings.TrimSpace(publisherAuthor)
if publisherAuthor == "" {
errors = append(errors, &ValidationError{Field: "publisher_author", Message: "is required"})
} else if len(publisherAuthor) > 200 {
errors = append(errors, &ValidationError{Field: "publisher_author", Message: "exceeds 200 characters"})
}
// Category
category = strings.TrimSpace(category)
if category == "" {
errors = append(errors, &ValidationError{Field: "category", Message: "is required"})
} else if len(category) > 100 {
errors = append(errors, &ValidationError{Field: "category", Message: "exceeds 100 characters"})
}
// Description (optional)
if description != "" && len(description) > 1000 {
errors = append(errors, &ValidationError{Field: "description", Message: "exceeds 1000 characters"})
}
return errors
}
// ValidateStationeryFields validates all stationery form fields
func ValidateStationeryFields(stationeryName, cost, price, discount, quantity, category, description string) []*ValidationError {
var errors []*ValidationError
// Stationery Name
stationeryName = strings.TrimSpace(stationeryName)
if stationeryName == "" {
errors = append(errors, &ValidationError{Field: "stationery_name", Message: "is required"})
} else if len(stationeryName) > 200 {
errors = append(errors, &ValidationError{Field: "stationery_name", Message: "exceeds 200 characters"})
}
// Cost
if cost == "" {
errors = append(errors, &ValidationError{Field: "cost", Message: "is required"})
} else {
costVal, err := strconv.ParseFloat(cost, 64)
if err != nil || costVal <= 0 {
errors = append(errors, &ValidationError{Field: "cost", Message: "must be a positive number"})
}
}
// Price
if price == "" {
errors = append(errors, &ValidationError{Field: "price", Message: "is required"})
} else {
priceVal, err := strconv.ParseFloat(price, 64)
if err != nil || priceVal <= 0 {
errors = append(errors, &ValidationError{Field: "price", Message: "must be a positive number"})
}
}
// Discount (optional)
if discount != "" {
discountVal, err := strconv.ParseFloat(discount, 64)
if err != nil || discountVal < 0 {
errors = append(errors, &ValidationError{Field: "discount", Message: "must be a non-negative number"})
}
}
// Quantity
if quantity == "" {
errors = append(errors, &ValidationError{Field: "quantity", Message: "is required"})
} else {
quantityVal, err := strconv.Atoi(quantity)
if err != nil || quantityVal <= 0 {
errors = append(errors, &ValidationError{Field: "quantity", Message: "must be a positive integer"})
}
}
// Category
category = strings.TrimSpace(category)
if category == "" {
errors = append(errors, &ValidationError{Field: "category", Message: "is required"})
} else if len(category) > 100 {
errors = append(errors, &ValidationError{Field: "category", Message: "exceeds 100 characters"})
}
// Description (optional)
if description != "" && len(description) > 1000 {
errors = append(errors, &ValidationError{Field: "description", Message: "exceeds 1000 characters"})
}
return errors
}
// ValidateFileType checks if file type is allowed
func ValidateFileType(contentType string) error {
allowedTypes := map[string]bool{
"image/png": true,
"image/jpeg": true,
"image/jpg": true,
"image/webp": true,
}
if !allowedTypes[contentType] {
return fmt.Errorf("invalid file type. Only PNG, JPEG, and WEBP are allowed")
}
return nil
}
// ValidateFileSize checks if file size is within limit
func ValidateFileSize(size, maxSize int64) error {
if size > maxSize {
return fmt.Errorf("file size exceeds %dMB limit", maxSize/(1024*1024))
}
return nil
}