first commit
Made-with: Cursor
This commit is contained in:
59
internal/utils/pagination.go
Normal file
59
internal/utils/pagination.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultLimit = 50
|
||||
MaxLimit = 200
|
||||
)
|
||||
|
||||
type CursorParams struct {
|
||||
CursorTime *time.Time
|
||||
CursorID *uuid.UUID
|
||||
Limit int32
|
||||
}
|
||||
|
||||
func ParseCursor(cursor string) (*time.Time, *uuid.UUID, error) {
|
||||
if cursor == "" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
raw, err := base64.RawURLEncoding.DecodeString(cursor)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid cursor encoding")
|
||||
}
|
||||
parts := strings.SplitN(string(raw), "|", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, nil, fmt.Errorf("invalid cursor format")
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339Nano, parts[0])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid cursor time")
|
||||
}
|
||||
id, err := uuid.Parse(parts[1])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid cursor id")
|
||||
}
|
||||
return &t, &id, nil
|
||||
}
|
||||
|
||||
func EncodeCursor(t time.Time, id uuid.UUID) string {
|
||||
raw := fmt.Sprintf("%s|%s", t.Format(time.RFC3339Nano), id.String())
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(raw))
|
||||
}
|
||||
|
||||
func ClampLimit(limit int) int32 {
|
||||
if limit <= 0 {
|
||||
return DefaultLimit
|
||||
}
|
||||
if limit > MaxLimit {
|
||||
return MaxLimit
|
||||
}
|
||||
return int32(limit)
|
||||
}
|
||||
88
internal/utils/pgtype.go
Normal file
88
internal/utils/pgtype.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func ToPgUUID(id uuid.UUID) pgtype.UUID {
|
||||
return pgtype.UUID{Bytes: id, Valid: true}
|
||||
}
|
||||
|
||||
func FromPgUUID(id pgtype.UUID) uuid.UUID {
|
||||
if !id.Valid {
|
||||
return uuid.Nil
|
||||
}
|
||||
return uuid.UUID(id.Bytes)
|
||||
}
|
||||
|
||||
func ToPgTimestamptz(t time.Time) pgtype.Timestamptz {
|
||||
return pgtype.Timestamptz{Time: t, Valid: true}
|
||||
}
|
||||
|
||||
func ToPgTimestamptzPtr(t *time.Time) pgtype.Timestamptz {
|
||||
if t == nil {
|
||||
return pgtype.Timestamptz{Valid: false}
|
||||
}
|
||||
return pgtype.Timestamptz{Time: *t, Valid: true}
|
||||
}
|
||||
|
||||
func FromPgTimestamptz(t pgtype.Timestamptz) time.Time {
|
||||
if !t.Valid {
|
||||
return time.Time{}
|
||||
}
|
||||
return t.Time
|
||||
}
|
||||
|
||||
func FromPgTimestamptzPtr(t pgtype.Timestamptz) *time.Time {
|
||||
if !t.Valid {
|
||||
return nil
|
||||
}
|
||||
return &t.Time
|
||||
}
|
||||
|
||||
func ToPgText(s string) pgtype.Text {
|
||||
if s == "" {
|
||||
return pgtype.Text{Valid: false}
|
||||
}
|
||||
return pgtype.Text{String: s, Valid: true}
|
||||
}
|
||||
|
||||
func ToPgTextPtr(s *string) pgtype.Text {
|
||||
if s == nil {
|
||||
return pgtype.Text{Valid: false}
|
||||
}
|
||||
return pgtype.Text{String: *s, Valid: true}
|
||||
}
|
||||
|
||||
func FromPgText(t pgtype.Text) *string {
|
||||
if !t.Valid {
|
||||
return nil
|
||||
}
|
||||
return &t.String
|
||||
}
|
||||
|
||||
func FromPgTextValue(t pgtype.Text) string {
|
||||
if !t.Valid {
|
||||
return ""
|
||||
}
|
||||
return t.String
|
||||
}
|
||||
|
||||
func ToPgBool(b bool) pgtype.Bool {
|
||||
return pgtype.Bool{Bool: b, Valid: true}
|
||||
}
|
||||
|
||||
func NullPgUUID() pgtype.UUID {
|
||||
return pgtype.UUID{Valid: false}
|
||||
}
|
||||
|
||||
func NullPgTimestamptz() pgtype.Timestamptz {
|
||||
return pgtype.Timestamptz{Valid: false}
|
||||
}
|
||||
|
||||
func NullPgText() pgtype.Text {
|
||||
return pgtype.Text{Valid: false}
|
||||
}
|
||||
44
internal/utils/response.go
Normal file
44
internal/utils/response.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/calendarapi/internal/models"
|
||||
)
|
||||
|
||||
func WriteJSON(w http.ResponseWriter, status int, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func WriteError(w http.ResponseWriter, err error) {
|
||||
if appErr, ok := models.IsAppError(err); ok {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(appErr.Status)
|
||||
json.NewEncoder(w).Encode(appErr)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(models.ErrInternal)
|
||||
}
|
||||
|
||||
func WriteOK(w http.ResponseWriter) {
|
||||
WriteJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
||||
}
|
||||
|
||||
func WriteList(w http.ResponseWriter, items interface{}, page models.PageInfo) {
|
||||
WriteJSON(w, http.StatusOK, models.ListResponse{Items: items, Page: page})
|
||||
}
|
||||
|
||||
func DecodeJSON(r *http.Request, dst interface{}) error {
|
||||
if r.Body == nil {
|
||||
return models.NewValidationError("request body required")
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(dst); err != nil {
|
||||
return models.NewValidationError("invalid JSON: " + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
97
internal/utils/validation.go
Normal file
97
internal/utils/validation.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/calendarapi/internal/models"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var hexColorRegex = regexp.MustCompile(`^#[0-9A-Fa-f]{6}$`)
|
||||
|
||||
func ValidateEmail(email string) error {
|
||||
if email == "" {
|
||||
return models.NewValidationError("email is required")
|
||||
}
|
||||
if _, err := mail.ParseAddress(email); err != nil {
|
||||
return models.NewValidationError("invalid email format")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidatePassword(password string) error {
|
||||
if len(password) < 10 {
|
||||
return models.NewValidationError("password must be at least 10 characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateTimezone(tz string) error {
|
||||
if tz == "" {
|
||||
return nil
|
||||
}
|
||||
if _, err := time.LoadLocation(tz); err != nil {
|
||||
return models.NewValidationError("invalid IANA timezone: " + tz)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateCalendarName(name string) error {
|
||||
if len(name) < 1 || len(name) > 80 {
|
||||
return models.NewValidationError("calendar name must be 1-80 characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateColor(color string) error {
|
||||
if color == "" {
|
||||
return nil
|
||||
}
|
||||
if !hexColorRegex.MatchString(color) {
|
||||
return models.NewValidationError("color must be hex format #RRGGBB")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateEventTitle(title string) error {
|
||||
if len(title) < 1 || len(title) > 140 {
|
||||
return models.NewValidationError("event title must be 1-140 characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateTimeRange(start, end time.Time) error {
|
||||
if !end.After(start) {
|
||||
return models.NewValidationError("end_time must be after start_time")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateReminderMinutes(minutes int32) error {
|
||||
if minutes < 0 || minutes > 10080 {
|
||||
return models.NewValidationError("minutes_before must be 0-10080")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateUUID(s string) (uuid.UUID, error) {
|
||||
id, err := uuid.Parse(s)
|
||||
if err != nil {
|
||||
return uuid.Nil, models.NewValidationError("invalid UUID: " + s)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func NormalizeEmail(email string) string {
|
||||
return strings.ToLower(strings.TrimSpace(email))
|
||||
}
|
||||
|
||||
func ValidateRecurrenceRangeLimit(start, end time.Time) error {
|
||||
if end.Sub(start) > 366*24*time.Hour {
|
||||
return models.NewValidationError("date range cannot exceed 1 year")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user