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