initial public commit
This commit is contained in:
503
models/schema/schema_field.go
Normal file
503
models/schema/schema_field.go
Normal file
@@ -0,0 +1,503 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"regexp"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
var schemaFieldNameRegex = regexp.MustCompile(`^\#?\w+$`)
|
||||
|
||||
// reserved internal field names
|
||||
const (
|
||||
ReservedFieldNameId = "id"
|
||||
ReservedFieldNameCreated = "created"
|
||||
ReservedFieldNameUpdated = "updated"
|
||||
)
|
||||
|
||||
// ReservedFieldNames returns slice with reserved/system field names.
|
||||
func ReservedFieldNames() []string {
|
||||
return []string{
|
||||
ReservedFieldNameId,
|
||||
ReservedFieldNameCreated,
|
||||
ReservedFieldNameUpdated,
|
||||
}
|
||||
}
|
||||
|
||||
// All valid field types
|
||||
const (
|
||||
FieldTypeText string = "text"
|
||||
FieldTypeNumber string = "number"
|
||||
FieldTypeBool string = "bool"
|
||||
FieldTypeEmail string = "email"
|
||||
FieldTypeUrl string = "url"
|
||||
FieldTypeDate string = "date"
|
||||
FieldTypeSelect string = "select"
|
||||
FieldTypeJson string = "json"
|
||||
FieldTypeFile string = "file"
|
||||
FieldTypeRelation string = "relation"
|
||||
FieldTypeUser string = "user"
|
||||
)
|
||||
|
||||
// FieldTypes returns slice with all supported field types.
|
||||
func FieldTypes() []string {
|
||||
return []string{
|
||||
FieldTypeText,
|
||||
FieldTypeNumber,
|
||||
FieldTypeBool,
|
||||
FieldTypeEmail,
|
||||
FieldTypeUrl,
|
||||
FieldTypeDate,
|
||||
FieldTypeSelect,
|
||||
FieldTypeJson,
|
||||
FieldTypeFile,
|
||||
FieldTypeRelation,
|
||||
FieldTypeUser,
|
||||
}
|
||||
}
|
||||
|
||||
// ArraybleFieldTypes returns slice with all array value supported field types.
|
||||
func ArraybleFieldTypes() []string {
|
||||
return []string{
|
||||
FieldTypeSelect,
|
||||
FieldTypeFile,
|
||||
FieldTypeRelation,
|
||||
FieldTypeUser,
|
||||
}
|
||||
}
|
||||
|
||||
// SchemaField defines a single schema field structure.
|
||||
type SchemaField struct {
|
||||
System bool `form:"system" json:"system"`
|
||||
Id string `form:"id" json:"id"`
|
||||
Name string `form:"name" json:"name"`
|
||||
Type string `form:"type" json:"type"`
|
||||
Required bool `form:"required" json:"required"`
|
||||
Unique bool `form:"unique" json:"unique"`
|
||||
Options any `form:"options" json:"options"`
|
||||
}
|
||||
|
||||
// ColDefinition returns the field db column type definition as string.
|
||||
func (f *SchemaField) ColDefinition() string {
|
||||
switch f.Type {
|
||||
case FieldTypeNumber:
|
||||
return "REAL DEFAULT 0"
|
||||
case FieldTypeBool:
|
||||
return "Boolean DEFAULT FALSE"
|
||||
case FieldTypeJson:
|
||||
return "JSON DEFAULT NULL"
|
||||
default:
|
||||
return "TEXT DEFAULT ''"
|
||||
}
|
||||
}
|
||||
|
||||
// String serializes and returns the current field as string.
|
||||
func (f SchemaField) String() string {
|
||||
data, _ := f.MarshalJSON()
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// MarshalJSON implements the [json.Marshaler] interface.
|
||||
func (f SchemaField) MarshalJSON() ([]byte, error) {
|
||||
type alias SchemaField // alias to prevent recursion
|
||||
|
||||
f.InitOptions()
|
||||
|
||||
return json.Marshal(alias(f))
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the [json.Unmarshaler] interface.
|
||||
//
|
||||
// The schema field options are auto initialized on success.
|
||||
func (f *SchemaField) UnmarshalJSON(data []byte) error {
|
||||
type alias *SchemaField // alias to prevent recursion
|
||||
|
||||
a := alias(f)
|
||||
|
||||
if err := json.Unmarshal(data, a); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return f.InitOptions()
|
||||
}
|
||||
|
||||
// Validate makes `SchemaField` validatable by implementing [validation.Validatable] interface.
|
||||
func (f SchemaField) Validate() error {
|
||||
// init field options (if not already)
|
||||
f.InitOptions()
|
||||
|
||||
// add commonly used filter literals to the exlude names list
|
||||
excludeNames := ReservedFieldNames()
|
||||
excludeNames = append(excludeNames, "null", "true", "false")
|
||||
|
||||
return validation.ValidateStruct(&f,
|
||||
validation.Field(&f.Options, validation.Required, validation.By(f.checkOptions)),
|
||||
validation.Field(&f.Id, validation.Required, validation.Length(5, 255)),
|
||||
validation.Field(
|
||||
&f.Name,
|
||||
validation.Required,
|
||||
validation.Length(1, 255),
|
||||
validation.Match(schemaFieldNameRegex),
|
||||
validation.NotIn(list.ToInterfaceSlice(excludeNames)...),
|
||||
),
|
||||
validation.Field(&f.Type, validation.Required, validation.In(list.ToInterfaceSlice(FieldTypes())...)),
|
||||
// currently file fields cannot be unique because a proper
|
||||
// hash/content check could cause performance issues
|
||||
validation.Field(&f.Unique, validation.When(f.Type == FieldTypeFile, validation.Empty)),
|
||||
)
|
||||
}
|
||||
|
||||
func (f *SchemaField) checkOptions(value any) error {
|
||||
v, ok := value.(FieldOptions)
|
||||
if !ok {
|
||||
return validation.NewError("validation_invalid_options", "Failed to initialize field options")
|
||||
}
|
||||
|
||||
return v.Validate()
|
||||
}
|
||||
|
||||
// InitOptions initializes the current field options based on its type.
|
||||
//
|
||||
// Returns error on unknown field type.
|
||||
func (f *SchemaField) InitOptions() error {
|
||||
if _, ok := f.Options.(FieldOptions); ok {
|
||||
return nil // already inited
|
||||
}
|
||||
|
||||
serialized, err := json.Marshal(f.Options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var options any
|
||||
switch f.Type {
|
||||
case FieldTypeText:
|
||||
options = &TextOptions{}
|
||||
case FieldTypeNumber:
|
||||
options = &NumberOptions{}
|
||||
case FieldTypeBool:
|
||||
options = &BoolOptions{}
|
||||
case FieldTypeEmail:
|
||||
options = &EmailOptions{}
|
||||
case FieldTypeUrl:
|
||||
options = &UrlOptions{}
|
||||
case FieldTypeDate:
|
||||
options = &DateOptions{}
|
||||
case FieldTypeSelect:
|
||||
options = &SelectOptions{}
|
||||
case FieldTypeJson:
|
||||
options = &JsonOptions{}
|
||||
case FieldTypeFile:
|
||||
options = &FileOptions{}
|
||||
case FieldTypeRelation:
|
||||
options = &RelationOptions{}
|
||||
case FieldTypeUser:
|
||||
options = &UserOptions{}
|
||||
default:
|
||||
return errors.New("Missing or unknown field field type.")
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(serialized, options); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.Options = options
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrepareValue returns normalized and properly formatted field value.
|
||||
func (f *SchemaField) PrepareValue(value any) any {
|
||||
// init field options (if not already)
|
||||
f.InitOptions()
|
||||
|
||||
switch f.Type {
|
||||
case FieldTypeText, FieldTypeEmail, FieldTypeUrl: // string
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return cast.ToString(value)
|
||||
case FieldTypeJson: // string
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
val, _ := types.ParseJsonRaw(value)
|
||||
return val
|
||||
case FieldTypeNumber: // nil, int or float
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return cast.ToFloat64(value)
|
||||
case FieldTypeBool: // bool
|
||||
return cast.ToBool(value)
|
||||
case FieldTypeDate: // string, DateTime or time.Time
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
val, _ := types.ParseDateTime(value)
|
||||
return val
|
||||
case FieldTypeSelect: // nil, string or slice of strings
|
||||
val := list.ToUniqueStringSlice(value)
|
||||
|
||||
options, _ := f.Options.(*SelectOptions)
|
||||
if options.MaxSelect <= 1 {
|
||||
if len(val) > 0 {
|
||||
return val[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return val
|
||||
case FieldTypeFile: // nil, string or slice of strings
|
||||
val := list.ToUniqueStringSlice(value)
|
||||
|
||||
options, _ := f.Options.(*FileOptions)
|
||||
if options.MaxSelect <= 1 {
|
||||
if len(val) > 0 {
|
||||
return val[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return val
|
||||
case FieldTypeRelation: // nil, string or slice of strings
|
||||
ids := list.ToUniqueStringSlice(value)
|
||||
|
||||
options, _ := f.Options.(*RelationOptions)
|
||||
if options.MaxSelect <= 1 {
|
||||
if len(ids) > 0 {
|
||||
return ids[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return ids
|
||||
case FieldTypeUser: // nil, string or slice of strings
|
||||
ids := list.ToUniqueStringSlice(value)
|
||||
|
||||
options, _ := f.Options.(*UserOptions)
|
||||
if options.MaxSelect <= 1 {
|
||||
if len(ids) > 0 {
|
||||
return ids[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return ids
|
||||
default:
|
||||
return value // unmodified
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// FieldOptions interfaces that defines common methods that every field options struct has.
|
||||
type FieldOptions interface {
|
||||
Validate() error
|
||||
}
|
||||
|
||||
type TextOptions struct {
|
||||
Min *int `form:"min" json:"min"`
|
||||
Max *int `form:"max" json:"max"`
|
||||
Pattern string `form:"pattern" json:"pattern"`
|
||||
}
|
||||
|
||||
func (o TextOptions) Validate() error {
|
||||
minVal := 0
|
||||
if o.Min != nil {
|
||||
minVal = *o.Min
|
||||
}
|
||||
|
||||
return validation.ValidateStruct(&o,
|
||||
validation.Field(&o.Min, validation.Min(0)),
|
||||
validation.Field(&o.Max, validation.Min(minVal)),
|
||||
validation.Field(&o.Pattern, validation.By(o.checkRegex)),
|
||||
)
|
||||
}
|
||||
|
||||
func (o *TextOptions) checkRegex(value any) error {
|
||||
v, _ := value.(string)
|
||||
if v == "" {
|
||||
return nil // nothing to check
|
||||
}
|
||||
|
||||
if _, err := regexp.Compile(v); err != nil {
|
||||
return validation.NewError("validation_invalid_regex", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type NumberOptions struct {
|
||||
Min *float64 `form:"min" json:"min"`
|
||||
Max *float64 `form:"max" json:"max"`
|
||||
}
|
||||
|
||||
func (o NumberOptions) Validate() error {
|
||||
var maxRules []validation.Rule
|
||||
if o.Min != nil && o.Max != nil {
|
||||
maxRules = append(maxRules, validation.Min(*o.Min))
|
||||
}
|
||||
|
||||
return validation.ValidateStruct(&o,
|
||||
validation.Field(&o.Max, maxRules...),
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type BoolOptions struct {
|
||||
}
|
||||
|
||||
func (o BoolOptions) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type EmailOptions struct {
|
||||
ExceptDomains []string `form:"exceptDomains" json:"exceptDomains"`
|
||||
OnlyDomains []string `form:"onlyDomains" json:"onlyDomains"`
|
||||
}
|
||||
|
||||
func (o EmailOptions) Validate() error {
|
||||
return validation.ValidateStruct(&o,
|
||||
validation.Field(
|
||||
&o.ExceptDomains,
|
||||
validation.When(len(o.OnlyDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)),
|
||||
),
|
||||
validation.Field(
|
||||
&o.OnlyDomains,
|
||||
validation.When(len(o.ExceptDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type UrlOptions struct {
|
||||
ExceptDomains []string `form:"exceptDomains" json:"exceptDomains"`
|
||||
OnlyDomains []string `form:"onlyDomains" json:"onlyDomains"`
|
||||
}
|
||||
|
||||
func (o UrlOptions) Validate() error {
|
||||
return validation.ValidateStruct(&o,
|
||||
validation.Field(
|
||||
&o.ExceptDomains,
|
||||
validation.When(len(o.OnlyDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)),
|
||||
),
|
||||
validation.Field(
|
||||
&o.OnlyDomains,
|
||||
validation.When(len(o.ExceptDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type DateOptions struct {
|
||||
Min types.DateTime `form:"min" json:"min"`
|
||||
Max types.DateTime `form:"max" json:"max"`
|
||||
}
|
||||
|
||||
func (o DateOptions) Validate() error {
|
||||
return validation.ValidateStruct(&o,
|
||||
validation.Field(&o.Max, validation.By(o.checkRange(o.Min, o.Max))),
|
||||
)
|
||||
}
|
||||
|
||||
func (o *DateOptions) checkRange(min types.DateTime, max types.DateTime) validation.RuleFunc {
|
||||
return func(value any) error {
|
||||
v, _ := value.(types.DateTime)
|
||||
|
||||
if v.IsZero() || min.IsZero() || max.IsZero() {
|
||||
return nil // nothing to check
|
||||
}
|
||||
|
||||
return validation.Date(types.DefaultDateLayout).
|
||||
Min(min.Time()).
|
||||
Max(max.Time()).
|
||||
Validate(v.String())
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type SelectOptions struct {
|
||||
MaxSelect int `form:"maxSelect" json:"maxSelect"`
|
||||
Values []string `form:"values" json:"values"`
|
||||
}
|
||||
|
||||
func (o SelectOptions) Validate() error {
|
||||
return validation.ValidateStruct(&o,
|
||||
validation.Field(&o.Values, validation.Required),
|
||||
validation.Field(
|
||||
&o.MaxSelect,
|
||||
validation.Required,
|
||||
validation.Min(1),
|
||||
validation.Max(len(o.Values)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type JsonOptions struct {
|
||||
}
|
||||
|
||||
func (o JsonOptions) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type FileOptions struct {
|
||||
MaxSelect int `form:"maxSelect" json:"maxSelect"`
|
||||
MaxSize int `form:"maxSize" json:"maxSize"` // in bytes
|
||||
MimeTypes []string `form:"mimeTypes" json:"mimeTypes"`
|
||||
Thumbs []string `form:"thumbs" json:"thumbs"`
|
||||
}
|
||||
|
||||
func (o FileOptions) Validate() error {
|
||||
return validation.ValidateStruct(&o,
|
||||
validation.Field(&o.MaxSelect, validation.Required, validation.Min(1)),
|
||||
validation.Field(&o.MaxSize, validation.Required, validation.Min(1)),
|
||||
validation.Field(&o.Thumbs, validation.Each(validation.Match(regexp.MustCompile(`^[1-9]\d*x[1-9]\d*$`)))),
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type RelationOptions struct {
|
||||
MaxSelect int `form:"maxSelect" json:"maxSelect"`
|
||||
CollectionId string `form:"collectionId" json:"collectionId"`
|
||||
CascadeDelete bool `form:"cascadeDelete" json:"cascadeDelete"`
|
||||
}
|
||||
|
||||
func (o RelationOptions) Validate() error {
|
||||
return validation.ValidateStruct(&o,
|
||||
validation.Field(&o.MaxSelect, validation.Required, validation.Min(1)),
|
||||
validation.Field(&o.CollectionId, validation.Required),
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type UserOptions struct {
|
||||
MaxSelect int `form:"maxSelect" json:"maxSelect"`
|
||||
CascadeDelete bool `form:"cascadeDelete" json:"cascadeDelete"`
|
||||
}
|
||||
|
||||
func (o UserOptions) Validate() error {
|
||||
return validation.ValidateStruct(&o,
|
||||
validation.Field(&o.MaxSelect, validation.Required, validation.Min(1)),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user