feat: Introduce Nullable type for optional fields and update user DTOs accordingly

This commit is contained in:
Jean Jacques Avril 2025-03-12 08:56:44 +00:00
parent da115dc3f6
commit 233f3cdb5c
9 changed files with 145 additions and 26 deletions

View File

@ -99,6 +99,6 @@ swagger:
# Generate TypeScript types
generate-ts:
@echo "Generating TypeScript types..."
@tygo generate
@go run scripts/fix_tygo.go
@echo "TypeScript types generated"

View File

@ -182,12 +182,17 @@ func (h *UserHandler) UpdateUser(c *gin.Context) {
update.Role = userUpdateDTO.Role
}
if userUpdateDTO.CompanyID != nil {
companyID, err := models.ULIDWrapperFromString(*userUpdateDTO.CompanyID)
if err != nil {
utils.BadRequestResponse(c, "Invalid company ID format")
return
if userUpdateDTO.CompanyID.Valid {
companyID, err := models.ULIDWrapperFromString(*userUpdateDTO.CompanyID.Value)
if err != nil {
utils.BadRequestResponse(c, "Invalid company ID format")
return
}
update.CompanyID = &companyID
} else {
update.CompanyID = nil
}
update.CompanyID = &companyID
}
if userUpdateDTO.HourlyRate != nil {
update.HourlyRate = userUpdateDTO.HourlyRate
@ -393,8 +398,8 @@ func convertUserToDTO(user *models.User) dto.UserDto {
func convertCreateDTOToModel(dto dto.UserCreateDto) models.UserCreate {
var companyID models.ULIDWrapper
if dto.CompanyID != nil {
companyID, _ = models.ULIDWrapperFromString(*dto.CompanyID) // Ignoring error, validation happens in the model
if dto.CompanyID != nil && dto.CompanyID.Valid {
companyID, _ = models.ULIDWrapperFromString(*dto.CompanyID.Value) // Ignoring error, validation happens in the model
}
return models.UserCreate{

View File

@ -1,4 +1,4 @@
package dto
package helper
import "encoding/json"

View File

@ -2,6 +2,8 @@ package dto
import (
"time"
"github.com/timetracker/backend/internal/types"
)
type UserDto struct {
@ -16,21 +18,21 @@ type UserDto struct {
}
type UserCreateDto struct {
Email string `json:"email" example:"test@example.com"`
Password string `json:"password" example:"password123"`
Role string `json:"role" example:"admin"`
CompanyID *string `json:"companyId" example:"01HGW2BBG0000000000000000"`
HourlyRate float64 `json:"hourlyRate" example:"50.00"`
Email string `json:"email" example:"test@example.com"`
Password string `json:"password" example:"password123"`
Role string `json:"role" example:"admin"`
CompanyID *types.Nullable[string] `json:"companyId" example:"01HGW2BBG0000000000000000"`
HourlyRate float64 `json:"hourlyRate" example:"50.00"`
}
type UserUpdateDto struct {
ID string `json:"id" example:"01HGW2BBG0000000000000000"`
CreatedAt *time.Time `json:"createdAt" example:"2024-01-01T00:00:00Z"`
UpdatedAt *time.Time `json:"updatedAt" example:"2024-01-01T00:00:00Z"`
LastEditorID *string `json:"lastEditorID" example:"01HGW2BBG0000000000000000"`
Email *string `json:"email" example:"test@example.com"`
Password *string `json:"password" example:"password123"`
Role *string `json:"role" example:"admin"`
CompanyID *string `json:"companyId" example:"01HGW2BBG0000000000000000"`
HourlyRate *float64 `json:"hourlyRate" example:"50.00"`
ID string `json:"id" example:"01HGW2BBG0000000000000000"`
CreatedAt *time.Time `json:"createdAt" example:"2024-01-01T00:00:00Z"`
UpdatedAt *time.Time `json:"updatedAt" example:"2024-01-01T00:00:00Z"`
LastEditorID *string `json:"lastEditorID" example:"01HGW2BBG0000000000000000"`
Email *string `json:"email" example:"test@example.com"`
Password *string `json:"password" example:"password123"`
Role *string `json:"role" example:"admin"`
CompanyID *types.Nullable[string] `json:"companyId" example:"01HGW2BBG0000000000000000"`
HourlyRate *float64 `json:"hourlyRate" example:"50.00"`
}

View File

@ -0,0 +1,48 @@
package types
import (
"encoding/json"
"fmt"
)
// Nullable[T] - Generischer Typ für optionale Werte (nullable fields)
type Nullable[T any] struct {
Value *T // Der tatsächliche Wert (kann nil sein)
Valid bool // Gibt an, ob der Wert gesetzt wurde
}
// NewNullable erstellt eine gültige Nullable-Instanz
func NewNullable[T any](value T) Nullable[T] {
return Nullable[T]{Value: &value, Valid: true}
}
// Null erstellt eine leere Nullable-Instanz (ungesetzt)
func Null[T any]() Nullable[T] {
return Nullable[T]{Valid: false}
}
// MarshalJSON - Serialisiert `Nullable[T]` korrekt ins JSON-Format
func (n Nullable[T]) MarshalJSON() ([]byte, error) {
if !n.Valid {
return []byte("null"), nil // Wenn nicht valid, dann NULL
}
return json.Marshal(n.Value) // Serialisiert den tatsächlichen Wert
}
// UnmarshalJSON - Deserialisiert JSON in `Nullable[T]`
func (n *Nullable[T]) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
n.Valid = true // Wert wurde gesetzt, aber auf NULL
n.Value = nil // Explizit NULL setzen
return nil
}
var v T
if err := json.Unmarshal(data, &v); err != nil {
return fmt.Errorf("invalid JSON for Nullable: %w", err)
}
n.Value = &v
n.Valid = true
return nil
}

View File

@ -0,0 +1,62 @@
package main
import (
"fmt"
"os"
"os/exec"
"strings"
)
const (
dtoFilePath = "../frontend/src/types/dto.ts"
importStatement = `import { Nullable } from "./nullable";`
)
func main() {
// Run Tygo first
fmt.Println("🔄 Running tygo...")
if err := runTygo(); err != nil {
fmt.Println("❌ Error running tygo:", err)
os.Exit(1)
}
// Read dto.ts file
content, err := os.ReadFile(dtoFilePath)
if err != nil {
fmt.Println("❌ Could not read dto.ts:", err)
os.Exit(1)
}
// Convert to string
dtoContent := string(content)
// Check if import already exists
if strings.Contains(dtoContent, importStatement) {
fmt.Println(" Import already exists in dto.ts, skipping.")
return
}
// Add import statement at the beginning
newContent := importStatement + "\n" + dtoContent
if err := os.WriteFile(dtoFilePath, []byte(newContent), 0644); err != nil {
fmt.Println("❌ Error writing dto.ts:", err)
os.Exit(1)
}
fmt.Println("✅ Successfully added Nullable<T> import to dto.ts")
}
// Runs Tygo command
func runTygo() error {
cmd := "tygo"
output, err := exec.Command(cmd, "generate").CombinedOutput()
if err != nil {
fmt.Println("Tygo output:", string(output))
return err
}
if len(output) > 0 {
fmt.Println("Tygo output:", string(output))
return nil
}
return nil
}

View File

@ -3,5 +3,5 @@ packages:
type_mappings:
"time.Time": "string"
"ulid.ULID": "string"
"null.String": "null | string"
"types.Nullable": "Nullable"
output_path: ../frontend/src/types/dto.ts

View File

@ -1,3 +1,4 @@
import { Nullable } from "./nullable";
// Code generated by tygo. DO NOT EDIT.
//////////
@ -167,7 +168,7 @@ export interface UserCreateDto {
email: string;
password: string;
role: string;
companyId?: string;
companyId?: Nullable<string>;
hourlyRate: number /* float64 */;
}
export interface UserUpdateDto {
@ -178,6 +179,6 @@ export interface UserUpdateDto {
email?: string;
password?: string;
role?: string;
companyId?: string;
companyId?: Nullable<string>;
hourlyRate?: number /* float64 */;
}

View File

@ -0,0 +1 @@
export type Nullable<T> = T | null;