Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add form data to service requests #77

Merged
merged 5 commits into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions backend/src/database/models/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ import (
"go.mongodb.org/mongo-driver/bson/primitive"
)

type FormFieldType string

const (
TextField FormFieldType = "TEXT"
DropdownField FormFieldType = "DROPDOWN"
OptionField FormFieldType = "OPTION"
CheckboxField FormFieldType = "CHECKBOX"
)

type FormField struct {
Name string `bson:"name" json:"name"`
Type FormFieldType `bson:"type" json:"type"`
IsRequired bool `bson:"is_required" json:"is_required"`
Placeholder string `bson:"placeholder" json:"placeholder"`
Description string `bson:"description" json:"description"`
Values []string `bson:"values" json:"values"`
}

type Form struct {
Fields []FormField `bson:"fields" json:"fields"`
}

type PipelineStepType string

const (
Expand Down Expand Up @@ -41,6 +63,7 @@ type PipelineModel struct {
FirstStepName string `bson:"first_step_name" json:"first_step_name"`
Steps []PipelineStepModel `bson:"steps" json:"steps"`
CreatedOn time.Time `bson:"created_on" json:"created_on"`
Form Form `bson:"form" json:"form"`
}

func (p *PipelineModel) GetPipelineStep(name string) *PipelineStepModel {
Expand Down
4 changes: 3 additions & 1 deletion backend/src/database/models/service_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const (
NotStarted ServiceRequestStatus = "Not Started"
)

type FormData map[string]any

type ServiceRequestModel struct {
Id primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
PipelineId string `bson:"pipeline_id" json:"pipeline_id"` // should we use primitive.ObjectID here?
Expand All @@ -25,5 +27,5 @@ type ServiceRequestModel struct {
CreatedOn time.Time `bson:"created_on" json:"created_on"`
LastUpdated time.Time `bson:"last_updated" json:"last_updated"`
Remarks string `bson:"remarks" json:"remarks"`
// FormData FormData `bson:"form_data"`
FormData FormData `bson:"form_data" json:"form_data"`
}
9 changes: 9 additions & 0 deletions backend/src/helper/strings.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,12 @@ func StringSliceEqual(a, b []string) bool {

return true
}

func StringInSlice(s string, sl []string) bool {
for _, v := range sl {
if v == s {
return true
}
}
return false
}
24 changes: 24 additions & 0 deletions backend/src/helper/strings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,27 @@ func TestStringSliceEqual(t *testing.T) {
})
}
}

func TestStringInSlice(t *testing.T) {
testCases := []struct {
s string
sl []string
expected bool
}{
{"a", []string{"a", "b", "c"}, true},
{"b", []string{"a", "b", "c"}, true},
{"c", []string{"a", "b", "c"}, true},
{"d", []string{"a", "b", "c"}, false},
{"", []string{"a", "b", "c"}, false},
{"", []string{}, false},
{"a", []string{}, false},
}

for _, tc := range testCases {
t.Run("", func(t *testing.T) {
if StringInSlice(tc.s, tc.sl) != tc.expected {
t.Errorf("Expected: %v, Got: %v", tc.expected, !tc.expected)
}
})
}
}
33 changes: 33 additions & 0 deletions backend/src/validation/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package validation

import (
"fmt"
"strings"
)

type InvalidStepTypeError struct {
Expand Down Expand Up @@ -152,3 +153,35 @@ func NewCircularReferenceError(startStepName, endStepName string) *CircularRefer
func (e *CircularReferenceError) Error() string {
return fmt.Sprintf("circular reference detected between steps '%s' and '%s'", e.startStepName, e.endStepName)
}

type InvalidFormDataTypeError struct {
fieldName string
expectedType string
}

func NewInvalidFormDataTypeError(fieldName, expectedType string) *InvalidFormDataTypeError {
return &InvalidFormDataTypeError{
fieldName: fieldName,
expectedType: expectedType,
}
}

func (e *InvalidFormDataTypeError) Error() string {
return fmt.Sprintf("data provided for '%s' is not of type '%s'", e.fieldName, e.expectedType)
}

type InvalidSelectedFormDataError struct {
expectedValues []string
receivedValue string
}

func NewInvalidSelectedFormDataError(expectedValues []string, receivedValue string) *InvalidSelectedFormDataError {
return &InvalidSelectedFormDataError{
expectedValues: expectedValues,
receivedValue: receivedValue,
}
}

func (e *InvalidSelectedFormDataError) Error() string {
return fmt.Sprintf("expected selected value to be one of '%s', got '%s' instead", strings.Join(e.expectedValues, ","), e.receivedValue)
}
119 changes: 119 additions & 0 deletions backend/src/validation/validator.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package validation

import (
"fmt"

"github.com/joshtyf/flowforge/src/database/models"
"github.com/joshtyf/flowforge/src/helper"
)

func ValidatePipeline(pipeline *models.PipelineModel) error {
Expand Down Expand Up @@ -59,3 +62,119 @@ func ValidatePipeline(pipeline *models.PipelineModel) error {

return nil
}

// Validates a form field of a newly created pipeline
func ValidateFormField(f models.FormField) error {
if f.Name == "" {
return NewMissingRequiredFieldError("name")
}
if f.Type == "" {
return NewMissingRequiredFieldError("type")
}
if f.Type == models.DropdownField || f.Type == models.OptionField || f.Type == models.CheckboxField {
if f.Values == nil || len(f.Values) == 0 {
return NewMissingRequiredFieldError("values")
}
}
return nil
}

type FormFieldDataValidator func(models.FormField, any) error

func (f FormFieldDataValidator) validate(field models.FormField, data any) error {
return f(field, data)
}

// Default text field validator
func defaultTextFieldDataValidator(field models.FormField, data any) error {
if _, ok := data.(string); !ok {
return NewInvalidFormDataTypeError(field.Name, "string")
}

return nil
}

// Default dropdown field validator
func defaultDropdownFieldDataValidator(field models.FormField, data any) error {
dataStr, ok := data.(string)
if !ok {
return NewInvalidFormDataTypeError(field.Name, "string")
}
if !helper.StringInSlice(dataStr, field.Values) {
return NewInvalidSelectedFormDataError(field.Values, dataStr)
}

return nil
}

// Default option field validator
func defaultOptionFieldDataValidator(field models.FormField, data any) error {
dataStr, ok := data.(string)
if !ok {
return NewInvalidFormDataTypeError(field.Name, "string")
}
if !helper.StringInSlice(dataStr, field.Values) {
return NewInvalidSelectedFormDataError(field.Values, dataStr)
}

return nil
}

// Default checkbox field validator
func defaultCheckboxFieldDataValidator(field models.FormField, data any) error {
dataStrings, ok := data.([]string)
if !ok {
return NewInvalidFormDataTypeError(field.Name, "[]string")
}
for _, s := range dataStrings {
if !helper.StringInSlice(s, field.Values) {
return NewInvalidSelectedFormDataError(field.Values, s)
}
}
return nil
}

type FormDataValidator struct {
fieldDataValidators map[models.FormFieldType]FormFieldDataValidator
}

func NewFormDataValidator(customValidators *map[models.FormFieldType]FormFieldDataValidator) *FormDataValidator {
validator := &FormDataValidator{
// Initialize with default field validators
fieldDataValidators: map[models.FormFieldType]FormFieldDataValidator{
models.TextField: FormFieldDataValidator(defaultTextFieldDataValidator),
models.DropdownField: FormFieldDataValidator(defaultDropdownFieldDataValidator),
models.OptionField: FormFieldDataValidator(defaultOptionFieldDataValidator),
models.CheckboxField: FormFieldDataValidator(defaultCheckboxFieldDataValidator),
},
}
if customValidators != nil {
for fieldType, customFieldValidator := range *customValidators {
validator.fieldDataValidators[fieldType] = customFieldValidator
}
}
return validator
}

func (v *FormDataValidator) Validate(formData *models.FormData, form *models.Form) error {
for _, field := range form.Fields {
fieldData, ok := (*formData)[field.Name]
if !ok {
if field.IsRequired {
return NewMissingRequiredFieldError(field.Name)
} else {
continue
}
}

fieldDataValidator, ok := v.fieldDataValidators[field.Type]
if !ok {
return fmt.Errorf("no data validator defined for field '%s' of type '%s'", field.Name, field.Type)
}
err := fieldDataValidator.validate(field, fieldData)
if err != nil {
return err
}
}
return nil
}
Loading
Loading