diff --git a/backend/Makefile b/backend/Makefile index 4846ee1..18ee7cc 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -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" diff --git a/backend/internal/api/handlers/user_handler.go b/backend/internal/api/handlers/user_handler.go index ebd94c8..09e8c94 100644 --- a/backend/internal/api/handlers/user_handler.go +++ b/backend/internal/api/handlers/user_handler.go @@ -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{ diff --git a/backend/internal/dtos/common.go b/backend/internal/dtos/helper/nullstring.go similarity index 98% rename from backend/internal/dtos/common.go rename to backend/internal/dtos/helper/nullstring.go index 6dd0eec..fc33090 100644 --- a/backend/internal/dtos/common.go +++ b/backend/internal/dtos/helper/nullstring.go @@ -1,4 +1,4 @@ -package dto +package helper import "encoding/json" diff --git a/backend/internal/dtos/user_dto.go b/backend/internal/dtos/user_dto.go index ae61c83..9373a4a 100644 --- a/backend/internal/dtos/user_dto.go +++ b/backend/internal/dtos/user_dto.go @@ -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"` } diff --git a/backend/internal/types/nullable.go b/backend/internal/types/nullable.go new file mode 100644 index 0000000..74f131b --- /dev/null +++ b/backend/internal/types/nullable.go @@ -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 +} diff --git a/backend/scripts/fix_tygo.go b/backend/scripts/fix_tygo.go new file mode 100644 index 0000000..b4820dc --- /dev/null +++ b/backend/scripts/fix_tygo.go @@ -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 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 +} diff --git a/backend/tygo.yaml b/backend/tygo.yaml index 43d2d55..d0c1770 100644 --- a/backend/tygo.yaml +++ b/backend/tygo.yaml @@ -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 diff --git a/frontend/src/types/dto.ts b/frontend/src/types/dto.ts index bb63d59..fa7fdae 100644 --- a/frontend/src/types/dto.ts +++ b/frontend/src/types/dto.ts @@ -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; hourlyRate: number /* float64 */; } export interface UserUpdateDto { @@ -178,6 +179,6 @@ export interface UserUpdateDto { email?: string; password?: string; role?: string; - companyId?: string; + companyId?: Nullable; hourlyRate?: number /* float64 */; } diff --git a/frontend/src/types/nullable.ts b/frontend/src/types/nullable.ts new file mode 100644 index 0000000..aa32b2d --- /dev/null +++ b/frontend/src/types/nullable.ts @@ -0,0 +1 @@ +export type Nullable = T | null;