diff --git a/generator/templates/enums.gotpl b/generator/templates/enums.gotpl index f8ec5fc..301d9d8 100644 --- a/generator/templates/enums.gotpl +++ b/generator/templates/enums.gotpl @@ -18,4 +18,12 @@ var {{ $enum.Name }} = {{ lowercase $enum.Name }}Namespace{ {{ capitalize $val.Name }}: {{ $enum.Name }}Type{{ capitalize $val.Name }}, {{- end }} } + +func (e {{ $enum.Name }}Type) IsValid() bool { + switch e { + case {{ range $i, $val := $enum.ValueMap }}{{ if $i }}, {{ end }}{{ $enum.Name }}Type{{ capitalize $val.Name }}{{ end }}: + return true + } + return false +} {{- end }} diff --git a/generator/templates/model_create.gotpl b/generator/templates/model_create.gotpl index cc6d20a..1acd15f 100644 --- a/generator/templates/model_create.gotpl +++ b/generator/templates/model_create.gotpl @@ -24,6 +24,9 @@ func (d *{{ .Model.Name }}Delegate) Create(input {{ .Model.Name }}CreateInput) * } func (q *Queries) execute{{ .Model.Name }}Create(ctx context.Context, input {{ .Model.Name }}CreateInput, selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit) (*{{ .Model.Name }}, error) { + if err := input.Validate(); err != nil { + return nil, err + } m := q.{{ .Model.Name }}InputToMap(input) cols, vals := mapToColsVals(m, {{ .Model.Name }}ColOrder) @@ -130,6 +133,11 @@ func (q *Queries) execute{{ .Model.Name }}CreateMany(ctx context.Context, inputs if len(inputs) == 0 { return 0, nil } + for i, input := range inputs { + if err := input.Validate(); err != nil { + return 0, fmt.Errorf("validation failed at index %d: %w", i, err) + } + } if q.dialect.SupportsBulkInsert() { rowMaps := make([]map[string]any, len(inputs)) @@ -162,6 +170,11 @@ func (q *Queries) execute{{ .Model.Name }}CreateManyAndReturn(ctx context.Contex if len(inputs) == 0 { return nil, nil } + for i, input := range inputs { + if err := input.Validate(); err != nil { + return nil, fmt.Errorf("validation failed at index %d: %w", i, err) + } + } hasRelations := selects.hasAnyRelation() returningCols := q.select{{ .Model.Name }}Cols(selects, omits) diff --git a/generator/templates/model_structs.gotpl b/generator/templates/model_structs.gotpl index f13b4a1..4275267 100644 --- a/generator/templates/model_structs.gotpl +++ b/generator/templates/model_structs.gotpl @@ -83,3 +83,39 @@ func (q *Queries) select{{ .Model.Name }}Cols(selects *{{ .Model.Name }}Select, return cols } + +func (input {{ .Model.Name }}CreateInput) Validate() error { + {{- range $field := .Model.ScalarFields }} + {{- if $field.EnumRef }} + {{- if $field.IsArray }} + for _, val := range input.{{ capitalize $field.Name }} { + if !val.IsValid() { + return fmt.Errorf("invalid enum value %q for field {{ capitalize $field.Name }}", val) + } + } + {{- else if and (eq $field.Default nil) (not $field.Optional) }} + if input.{{ capitalize $field.Name }} == nil { + return fmt.Errorf("field {{ capitalize $field.Name }} is required") + } + if !input.{{ capitalize $field.Name }}.IsValid() { + return fmt.Errorf("invalid enum value %q for field {{ capitalize $field.Name }}", *input.{{ capitalize $field.Name }}) + } + {{- else }} + if input.{{ capitalize $field.Name }} != nil { + if !input.{{ capitalize $field.Name }}.IsValid() { + return fmt.Errorf("invalid enum value %q for field {{ capitalize $field.Name }}", *input.{{ capitalize $field.Name }}) + } + } + {{- end }} + {{- else }} + {{- if and (eq $field.Default nil) (not $field.Optional) (not $field.IsArray) }} + {{- if eq $field.GoType "string" }} + if input.{{ capitalize $field.Name }} == "" { + return fmt.Errorf("field {{ capitalize $field.Name }} is required") + } + {{- end }} + {{- end }} + {{- end }} + {{- end }} + return nil +} diff --git a/integration/create_many_test.go b/integration/create_many_test.go index 654aad3..5d3dfc5 100644 --- a/integration/create_many_test.go +++ b/integration/create_many_test.go @@ -49,7 +49,8 @@ func TestCreateMany(t *testing.T) { t.Run("CreateManyAndReturn works and supports Select", func(t *testing.T) { author, err := client.User.Create(valkyrie.UserCreateInput{ - Email: "author@example.com", + Email: "author@example.com", + PhoneNum: "+444", }).Exec(ctx) if err != nil { t.Fatalf("failed to create author: %v", err) diff --git a/integration/create_test.go b/integration/create_test.go index 61d57ad..ac3855b 100644 --- a/integration/create_test.go +++ b/integration/create_test.go @@ -132,3 +132,33 @@ func TestCreateWithCustomEnum(t *testing.T) { t.Errorf("expected role '%s', got '%s'", valkyrie.UserRole.Admin, u.Role) } } + +func TestCreateValidation(t *testing.T) { + db, cleanup := setupTestDB(t) + defer cleanup() + ctx := context.Background() + + _, err := db.User.Create(valkyrie.UserCreateInput{ + // no email + PhoneNum: "+123456789", + }).Exec(ctx) + if err == nil { + t.Fatal("expected error creating user with empty required email, got nil") + } + if !strings.Contains(err.Error(), "field Email is required") { + t.Errorf("expected error message to contain 'field Email is required', got: %v", err) + } + + invalidRole := valkyrie.UserRoleType("INVALID_ROLE") + _, err = db.User.Create(valkyrie.UserCreateInput{ + Email: "invalid_role@example.com", + PhoneNum: "+123456789", + Role: &invalidRole, + }).Exec(ctx) + if err == nil { + t.Fatal("expected error creating user with invalid enum role, got nil") + } + if !strings.Contains(err.Error(), "invalid enum value \"INVALID_ROLE\" for field Role") { + t.Errorf("expected error message to contain 'invalid enum value \"INVALID_ROLE\" for field Role', got: %v", err) + } +} diff --git a/integration/valkyrie/category.go b/integration/valkyrie/category.go index 71987ad..425f47a 100644 --- a/integration/valkyrie/category.go +++ b/integration/valkyrie/category.go @@ -88,6 +88,13 @@ func (q *Queries) selectCategoryCols(selects *CategorySelect, omits *CategoryOmi return cols } +func (input CategoryCreateInput) Validate() error { + if input.Name == "" { + return fmt.Errorf("field Name is required") + } + return nil +} + var CategoryColOrder = []string{ "id", "name", @@ -109,6 +116,9 @@ func (d *CategoryDelegate) Create(input CategoryCreateInput) *CreateBuilder[Cate } func (q *Queries) executeCategoryCreate(ctx context.Context, input CategoryCreateInput, selects *CategorySelect, omits *CategoryOmit) (*Category, error) { + if err := input.Validate(); err != nil { + return nil, err + } m := q.CategoryInputToMap(input) cols, vals := mapToColsVals(m, CategoryColOrder) @@ -173,6 +183,11 @@ func (q *Queries) executeCategoryCreateMany(ctx context.Context, inputs []Catego if len(inputs) == 0 { return 0, nil } + for i, input := range inputs { + if err := input.Validate(); err != nil { + return 0, fmt.Errorf("validation failed at index %d: %w", i, err) + } + } if q.dialect.SupportsBulkInsert() { rowMaps := make([]map[string]any, len(inputs)) @@ -205,6 +220,11 @@ func (q *Queries) executeCategoryCreateManyAndReturn(ctx context.Context, inputs if len(inputs) == 0 { return nil, nil } + for i, input := range inputs { + if err := input.Validate(); err != nil { + return nil, fmt.Errorf("validation failed at index %d: %w", i, err) + } + } hasRelations := selects.hasAnyRelation() returningCols := q.selectCategoryCols(selects, omits) diff --git a/integration/valkyrie/categoryToPost.go b/integration/valkyrie/categoryToPost.go index 83de630..288f440 100644 --- a/integration/valkyrie/categoryToPost.go +++ b/integration/valkyrie/categoryToPost.go @@ -91,6 +91,13 @@ func (q *Queries) selectCategoryToPostCols(selects *CategoryToPostSelect, omits return cols } +func (input CategoryToPostCreateInput) Validate() error { + if input.PostId == "" { + return fmt.Errorf("field PostId is required") + } + return nil +} + var CategoryToPostColOrder = []string{ "postId", "categoryId", @@ -112,6 +119,9 @@ func (d *CategoryToPostDelegate) Create(input CategoryToPostCreateInput) *Create } func (q *Queries) executeCategoryToPostCreate(ctx context.Context, input CategoryToPostCreateInput, selects *CategoryToPostSelect, omits *CategoryToPostOmit) (*CategoryToPost, error) { + if err := input.Validate(); err != nil { + return nil, err + } m := q.CategoryToPostInputToMap(input) cols, vals := mapToColsVals(m, CategoryToPostColOrder) @@ -173,6 +183,11 @@ func (q *Queries) executeCategoryToPostCreateMany(ctx context.Context, inputs [] if len(inputs) == 0 { return 0, nil } + for i, input := range inputs { + if err := input.Validate(); err != nil { + return 0, fmt.Errorf("validation failed at index %d: %w", i, err) + } + } if q.dialect.SupportsBulkInsert() { rowMaps := make([]map[string]any, len(inputs)) @@ -205,6 +220,11 @@ func (q *Queries) executeCategoryToPostCreateManyAndReturn(ctx context.Context, if len(inputs) == 0 { return nil, nil } + for i, input := range inputs { + if err := input.Validate(); err != nil { + return nil, fmt.Errorf("validation failed at index %d: %w", i, err) + } + } hasRelations := selects.hasAnyRelation() returningCols := q.selectCategoryToPostCols(selects, omits) diff --git a/integration/valkyrie/client.go b/integration/valkyrie/client.go index bee6c89..6be244b 100644 --- a/integration/valkyrie/client.go +++ b/integration/valkyrie/client.go @@ -64,6 +64,14 @@ var UserRole = userRoleNamespace{ Teacher: UserRoleTypeTeacher, } +func (e UserRoleType) IsValid() bool { + switch e { + case UserRoleTypeAdmin, UserRoleTypeStudent, UserRoleTypeTeacher: + return true + } + return false +} + type Dialect interface { Quote(ident string) string BindVar(idx int) string diff --git a/integration/valkyrie/comment.go b/integration/valkyrie/comment.go index 90eb1f2..107247f 100644 --- a/integration/valkyrie/comment.go +++ b/integration/valkyrie/comment.go @@ -131,6 +131,22 @@ func (q *Queries) selectCommentCols(selects *CommentSelect, omits *CommentOmit, return cols } +func (input CommentCreateInput) Validate() error { + if input.Dummy3 == "" { + return fmt.Errorf("field Dummy3 is required") + } + if input.Dummy2 == "" { + return fmt.Errorf("field Dummy2 is required") + } + if input.PostId == "" { + return fmt.Errorf("field PostId is required") + } + if input.AuthorId == "" { + return fmt.Errorf("field AuthorId is required") + } + return nil +} + var CommentColOrder = []string{ "id", "textify", @@ -157,6 +173,9 @@ func (d *CommentDelegate) Create(input CommentCreateInput) *CreateBuilder[Commen } func (q *Queries) executeCommentCreate(ctx context.Context, input CommentCreateInput, selects *CommentSelect, omits *CommentOmit) (*Comment, error) { + if err := input.Validate(); err != nil { + return nil, err + } m := q.CommentInputToMap(input) cols, vals := mapToColsVals(m, CommentColOrder) @@ -227,6 +246,11 @@ func (q *Queries) executeCommentCreateMany(ctx context.Context, inputs []Comment if len(inputs) == 0 { return 0, nil } + for i, input := range inputs { + if err := input.Validate(); err != nil { + return 0, fmt.Errorf("validation failed at index %d: %w", i, err) + } + } if q.dialect.SupportsBulkInsert() { rowMaps := make([]map[string]any, len(inputs)) @@ -259,6 +283,11 @@ func (q *Queries) executeCommentCreateManyAndReturn(ctx context.Context, inputs if len(inputs) == 0 { return nil, nil } + for i, input := range inputs { + if err := input.Validate(); err != nil { + return nil, fmt.Errorf("validation failed at index %d: %w", i, err) + } + } hasRelations := selects.hasAnyRelation() returningCols := q.selectCommentCols(selects, omits) diff --git a/integration/valkyrie/post.go b/integration/valkyrie/post.go index ab2cf4b..ded72f1 100644 --- a/integration/valkyrie/post.go +++ b/integration/valkyrie/post.go @@ -118,6 +118,16 @@ func (q *Queries) selectPostCols(selects *PostSelect, omits *PostOmit, forceCols return cols } +func (input PostCreateInput) Validate() error { + if input.Title == "" { + return fmt.Errorf("field Title is required") + } + if input.AuthorId == "" { + return fmt.Errorf("field AuthorId is required") + } + return nil +} + var PostColOrder = []string{ "id", "title", @@ -142,6 +152,9 @@ func (d *PostDelegate) Create(input PostCreateInput) *CreateBuilder[Post, PostCr } func (q *Queries) executePostCreate(ctx context.Context, input PostCreateInput, selects *PostSelect, omits *PostOmit) (*Post, error) { + if err := input.Validate(); err != nil { + return nil, err + } m := q.PostInputToMap(input) cols, vals := mapToColsVals(m, PostColOrder) @@ -214,6 +227,11 @@ func (q *Queries) executePostCreateMany(ctx context.Context, inputs []PostCreate if len(inputs) == 0 { return 0, nil } + for i, input := range inputs { + if err := input.Validate(); err != nil { + return 0, fmt.Errorf("validation failed at index %d: %w", i, err) + } + } if q.dialect.SupportsBulkInsert() { rowMaps := make([]map[string]any, len(inputs)) @@ -246,6 +264,11 @@ func (q *Queries) executePostCreateManyAndReturn(ctx context.Context, inputs []P if len(inputs) == 0 { return nil, nil } + for i, input := range inputs { + if err := input.Validate(); err != nil { + return nil, fmt.Errorf("validation failed at index %d: %w", i, err) + } + } hasRelations := selects.hasAnyRelation() returningCols := q.selectPostCols(selects, omits) diff --git a/integration/valkyrie/profile.go b/integration/valkyrie/profile.go index 261569b..f5f6cc1 100644 --- a/integration/valkyrie/profile.go +++ b/integration/valkyrie/profile.go @@ -96,6 +96,13 @@ func (q *Queries) selectProfileCols(selects *ProfileSelect, omits *ProfileOmit, return cols } +func (input ProfileCreateInput) Validate() error { + if input.UserId == "" { + return fmt.Errorf("field UserId is required") + } + return nil +} + var ProfileColOrder = []string{ "id", "bio", @@ -118,6 +125,9 @@ func (d *ProfileDelegate) Create(input ProfileCreateInput) *CreateBuilder[Profil } func (q *Queries) executeProfileCreate(ctx context.Context, input ProfileCreateInput, selects *ProfileSelect, omits *ProfileOmit) (*Profile, error) { + if err := input.Validate(); err != nil { + return nil, err + } m := q.ProfileInputToMap(input) cols, vals := mapToColsVals(m, ProfileColOrder) @@ -186,6 +196,11 @@ func (q *Queries) executeProfileCreateMany(ctx context.Context, inputs []Profile if len(inputs) == 0 { return 0, nil } + for i, input := range inputs { + if err := input.Validate(); err != nil { + return 0, fmt.Errorf("validation failed at index %d: %w", i, err) + } + } if q.dialect.SupportsBulkInsert() { rowMaps := make([]map[string]any, len(inputs)) @@ -218,6 +233,11 @@ func (q *Queries) executeProfileCreateManyAndReturn(ctx context.Context, inputs if len(inputs) == 0 { return nil, nil } + for i, input := range inputs { + if err := input.Validate(); err != nil { + return nil, fmt.Errorf("validation failed at index %d: %w", i, err) + } + } hasRelations := selects.hasAnyRelation() returningCols := q.selectProfileCols(selects, omits) diff --git a/integration/valkyrie/user.go b/integration/valkyrie/user.go index 58941c4..173b7fe 100644 --- a/integration/valkyrie/user.go +++ b/integration/valkyrie/user.go @@ -124,6 +124,21 @@ func (q *Queries) selectUserCols(selects *UserSelect, omits *UserOmit, forceCols return cols } +func (input UserCreateInput) Validate() error { + if input.Email == "" { + return fmt.Errorf("field Email is required") + } + if input.PhoneNum == "" { + return fmt.Errorf("field PhoneNum is required") + } + if input.Role != nil { + if !input.Role.IsValid() { + return fmt.Errorf("invalid enum value %q for field Role", *input.Role) + } + } + return nil +} + var UserColOrder = []string{ "id", "email", @@ -148,6 +163,9 @@ func (d *UserDelegate) Create(input UserCreateInput) *CreateBuilder[User, UserCr } func (q *Queries) executeUserCreate(ctx context.Context, input UserCreateInput, selects *UserSelect, omits *UserOmit) (*User, error) { + if err := input.Validate(); err != nil { + return nil, err + } m := q.UserInputToMap(input) cols, vals := mapToColsVals(m, UserColOrder) @@ -220,6 +238,11 @@ func (q *Queries) executeUserCreateMany(ctx context.Context, inputs []UserCreate if len(inputs) == 0 { return 0, nil } + for i, input := range inputs { + if err := input.Validate(); err != nil { + return 0, fmt.Errorf("validation failed at index %d: %w", i, err) + } + } if q.dialect.SupportsBulkInsert() { rowMaps := make([]map[string]any, len(inputs)) @@ -252,6 +275,11 @@ func (q *Queries) executeUserCreateManyAndReturn(ctx context.Context, inputs []U if len(inputs) == 0 { return nil, nil } + for i, input := range inputs { + if err := input.Validate(); err != nil { + return nil, fmt.Errorf("validation failed at index %d: %w", i, err) + } + } hasRelations := selects.hasAnyRelation() returningCols := q.selectUserCols(selects, omits)