feat: Introduce Nullable type for optional fields and update user DTOs accordingly
This commit is contained in:
		
							parent
							
								
									da115dc3f6
								
							
						
					
					
						commit
						233f3cdb5c
					
				| @ -99,6 +99,6 @@ swagger: | |||||||
| # Generate TypeScript types
 | # Generate TypeScript types
 | ||||||
| generate-ts: | generate-ts: | ||||||
| 	@echo "Generating TypeScript types..." | 	@echo "Generating TypeScript types..." | ||||||
| 	@tygo generate | 	@go run scripts/fix_tygo.go | ||||||
| 	@echo "TypeScript types generated" | 	@echo "TypeScript types generated" | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -182,12 +182,17 @@ func (h *UserHandler) UpdateUser(c *gin.Context) { | |||||||
| 		update.Role = userUpdateDTO.Role | 		update.Role = userUpdateDTO.Role | ||||||
| 	} | 	} | ||||||
| 	if userUpdateDTO.CompanyID != nil { | 	if userUpdateDTO.CompanyID != nil { | ||||||
| 		companyID, err := models.ULIDWrapperFromString(*userUpdateDTO.CompanyID) | 		if userUpdateDTO.CompanyID.Valid { | ||||||
|  | 			companyID, err := models.ULIDWrapperFromString(*userUpdateDTO.CompanyID.Value) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				utils.BadRequestResponse(c, "Invalid company ID format") | 				utils.BadRequestResponse(c, "Invalid company ID format") | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| 			update.CompanyID = &companyID | 			update.CompanyID = &companyID | ||||||
|  | 		} else { | ||||||
|  | 			update.CompanyID = nil | ||||||
|  | 
 | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	if userUpdateDTO.HourlyRate != nil { | 	if userUpdateDTO.HourlyRate != nil { | ||||||
| 		update.HourlyRate = userUpdateDTO.HourlyRate | 		update.HourlyRate = userUpdateDTO.HourlyRate | ||||||
| @ -393,8 +398,8 @@ func convertUserToDTO(user *models.User) dto.UserDto { | |||||||
| 
 | 
 | ||||||
| func convertCreateDTOToModel(dto dto.UserCreateDto) models.UserCreate { | func convertCreateDTOToModel(dto dto.UserCreateDto) models.UserCreate { | ||||||
| 	var companyID models.ULIDWrapper | 	var companyID models.ULIDWrapper | ||||||
| 	if dto.CompanyID != nil { | 	if dto.CompanyID != nil && dto.CompanyID.Valid { | ||||||
| 		companyID, _ = models.ULIDWrapperFromString(*dto.CompanyID) // Ignoring error, validation happens in the model | 		companyID, _ = models.ULIDWrapperFromString(*dto.CompanyID.Value) // Ignoring error, validation happens in the model | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return models.UserCreate{ | 	return models.UserCreate{ | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| package dto | package helper | ||||||
| 
 | 
 | ||||||
| import "encoding/json" | import "encoding/json" | ||||||
| 
 | 
 | ||||||
| @ -2,6 +2,8 @@ package dto | |||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"time" | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/timetracker/backend/internal/types" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type UserDto struct { | type UserDto struct { | ||||||
| @ -19,7 +21,7 @@ type UserCreateDto struct { | |||||||
| 	Email      string                  `json:"email" example:"test@example.com"` | 	Email      string                  `json:"email" example:"test@example.com"` | ||||||
| 	Password   string                  `json:"password" example:"password123"` | 	Password   string                  `json:"password" example:"password123"` | ||||||
| 	Role       string                  `json:"role" example:"admin"` | 	Role       string                  `json:"role" example:"admin"` | ||||||
| 	CompanyID  *string `json:"companyId" example:"01HGW2BBG0000000000000000"` | 	CompanyID  *types.Nullable[string] `json:"companyId" example:"01HGW2BBG0000000000000000"` | ||||||
| 	HourlyRate float64                 `json:"hourlyRate" example:"50.00"` | 	HourlyRate float64                 `json:"hourlyRate" example:"50.00"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -31,6 +33,6 @@ type UserUpdateDto struct { | |||||||
| 	Email        *string                 `json:"email" example:"test@example.com"` | 	Email        *string                 `json:"email" example:"test@example.com"` | ||||||
| 	Password     *string                 `json:"password" example:"password123"` | 	Password     *string                 `json:"password" example:"password123"` | ||||||
| 	Role         *string                 `json:"role" example:"admin"` | 	Role         *string                 `json:"role" example:"admin"` | ||||||
| 	CompanyID    *string    `json:"companyId" example:"01HGW2BBG0000000000000000"` | 	CompanyID    *types.Nullable[string] `json:"companyId" example:"01HGW2BBG0000000000000000"` | ||||||
| 	HourlyRate   *float64                `json:"hourlyRate" example:"50.00"` | 	HourlyRate   *float64                `json:"hourlyRate" example:"50.00"` | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										48
									
								
								backend/internal/types/nullable.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								backend/internal/types/nullable.go
									
									
									
									
									
										Normal 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 | ||||||
|  | } | ||||||
							
								
								
									
										62
									
								
								backend/scripts/fix_tygo.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								backend/scripts/fix_tygo.go
									
									
									
									
									
										Normal 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 | ||||||
|  | } | ||||||
| @ -3,5 +3,5 @@ packages: | |||||||
|     type_mappings: |     type_mappings: | ||||||
|       "time.Time": "string" |       "time.Time": "string" | ||||||
|       "ulid.ULID": "string" |       "ulid.ULID": "string" | ||||||
|       "null.String": "null | string" |       "types.Nullable": "Nullable" | ||||||
|     output_path: ../frontend/src/types/dto.ts |     output_path: ../frontend/src/types/dto.ts | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | import { Nullable } from "./nullable"; | ||||||
| // Code generated by tygo. DO NOT EDIT.
 | // Code generated by tygo. DO NOT EDIT.
 | ||||||
| 
 | 
 | ||||||
| //////////
 | //////////
 | ||||||
| @ -167,7 +168,7 @@ export interface UserCreateDto { | |||||||
|   email: string; |   email: string; | ||||||
|   password: string; |   password: string; | ||||||
|   role: string; |   role: string; | ||||||
|   companyId?: string; |   companyId?: Nullable<string>; | ||||||
|   hourlyRate: number /* float64 */; |   hourlyRate: number /* float64 */; | ||||||
| } | } | ||||||
| export interface UserUpdateDto { | export interface UserUpdateDto { | ||||||
| @ -178,6 +179,6 @@ export interface UserUpdateDto { | |||||||
|   email?: string; |   email?: string; | ||||||
|   password?: string; |   password?: string; | ||||||
|   role?: string; |   role?: string; | ||||||
|   companyId?: string; |   companyId?: Nullable<string>; | ||||||
|   hourlyRate?: number /* float64 */; |   hourlyRate?: number /* float64 */; | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								frontend/src/types/nullable.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/types/nullable.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | export type Nullable<T> = T | null; | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user