initial public commit

This commit is contained in:
Gani Georgiev
2022-07-07 00:19:05 +03:00
commit 3d07f0211d
484 changed files with 92412 additions and 0 deletions
+236
View File
@@ -0,0 +1,236 @@
// Package schema implements custom Schema and SchemaField datatypes
// for handling the Collection schema definitions.
package schema
import (
"database/sql/driver"
"encoding/json"
"fmt"
"strconv"
"strings"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/security"
)
// NewSchema creates a new Schema instance with the provided fields.
func NewSchema(fields ...*SchemaField) Schema {
s := Schema{}
for _, f := range fields {
s.AddField(f)
}
return s
}
// Schema defines a dynamic db schema as a slice of `SchemaField`s.
type Schema struct {
fields []*SchemaField
}
// Fields returns the registered schema fields.
func (s *Schema) Fields() []*SchemaField {
return s.fields
}
// InitFieldsOptions calls `InitOptions()` for all schema fields.
func (s *Schema) InitFieldsOptions() error {
for _, field := range s.Fields() {
if err := field.InitOptions(); err != nil {
return err
}
}
return nil
}
// Clone creates a deep clone of the current schema.
func (s *Schema) Clone() (*Schema, error) {
copyRaw, err := json.Marshal(s)
if err != nil {
return nil, err
}
result := &Schema{}
if err := json.Unmarshal(copyRaw, result); err != nil {
return nil, err
}
return result, nil
}
// AsMap returns a map with all registered schema field.
// The returned map is indexed with each field name.
func (s *Schema) AsMap() map[string]*SchemaField {
result := map[string]*SchemaField{}
for _, field := range s.fields {
result[field.Name] = field
}
return result
}
// GetFieldById returns a single field by its id.
func (s *Schema) GetFieldById(id string) *SchemaField {
for _, field := range s.fields {
if field.Id == id {
return field
}
}
return nil
}
// GetFieldByName returns a single field by its name.
func (s *Schema) GetFieldByName(name string) *SchemaField {
for _, field := range s.fields {
if field.Name == name {
return field
}
}
return nil
}
// RemoveField removes a single schema field by its id.
//
// This method does nothing if field with `id` doesn't exist.
func (s *Schema) RemoveField(id string) {
for i, field := range s.fields {
if field.Id == id {
s.fields = append(s.fields[:i], s.fields[i+1:]...)
return
}
}
}
// AddField registers the provided newField to the current schema.
//
// If field with `newField.Id` already exist, the existing field is
// replaced with the new one.
//
// Otherwise the new field is appended to the other schema fields.
func (s *Schema) AddField(newField *SchemaField) {
if newField.Id == "" {
// set default id
newField.Id = strings.ToLower(security.RandomString(8))
}
for i, field := range s.fields {
// replace existing
if field.Id == newField.Id {
s.fields[i] = newField
return
}
}
// add new field
s.fields = append(s.fields, newField)
}
// Validate makes Schema validatable by implementing [validation.Validatable] interface.
//
// Internally calls each individual field's validator and additionally
// checks for invalid renamed fields and field name duplications.
func (s Schema) Validate() error {
return validation.Validate(&s.fields, validation.Required, validation.By(func(value any) error {
fields := s.fields // use directly the schema value to avoid unnecesary interface casting
if len(fields) == 0 {
return validation.NewError("validation_invalid_schema", "Invalid schema format.")
}
ids := []string{}
names := []string{}
for i, field := range fields {
if list.ExistInSlice(field.Id, ids) {
return validation.Errors{
strconv.Itoa(i): validation.Errors{
"id": validation.NewError(
"validation_duplicated_field_id",
"Duplicated or invalid schema field id",
),
},
}
}
// field names are used as db columns and should be case insensitive
nameLower := strings.ToLower(field.Name)
if list.ExistInSlice(nameLower, names) {
return validation.Errors{
strconv.Itoa(i): validation.Errors{
"name": validation.NewError(
"validation_duplicated_field_name",
"Duplicated or invalid schema field name",
),
},
}
}
ids = append(ids, field.Id)
names = append(names, nameLower)
}
return nil
}))
}
// MarshalJSON implements the [json.Marshaler] interface.
func (s Schema) MarshalJSON() ([]byte, error) {
if s.fields == nil {
s.fields = []*SchemaField{}
}
return json.Marshal(s.fields)
}
// UnmarshalJSON implements the [json.Unmarshaler] interface.
//
// On success, all schema field options are auto initialized.
func (s *Schema) UnmarshalJSON(data []byte) error {
fields := []*SchemaField{}
if err := json.Unmarshal(data, &fields); err != nil {
return err
}
s.fields = []*SchemaField{}
for _, f := range fields {
s.AddField(f)
}
return s.InitFieldsOptions()
}
// Value implements the [driver.Valuer] interface.
func (s Schema) Value() (driver.Value, error) {
if len(s.fields) == 0 {
return nil, nil
}
data, err := json.Marshal(s.fields)
return string(data), err
}
// Scan implements [sql.Scanner] interface to scan the provided value
// into the current Schema instance.
func (s *Schema) Scan(value any) error {
var data []byte
switch v := value.(type) {
case nil:
// no cast needed
case []byte:
data = v
case string:
data = []byte(v)
default:
return fmt.Errorf("Failed to unmarshal Schema value %q.", value)
}
if len(data) == 0 {
data = []byte("[]")
}
return s.UnmarshalJSON(data)
}
+503
View 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)),
)
}
File diff suppressed because it is too large Load Diff
+414
View File
@@ -0,0 +1,414 @@
package schema_test
import (
"testing"
"github.com/pocketbase/pocketbase/models/schema"
)
func TestNewSchemaAndFields(t *testing.T) {
testSchema := schema.NewSchema(
&schema.SchemaField{Id: "id1", Name: "test1"},
&schema.SchemaField{Name: "test2"},
&schema.SchemaField{Id: "id1", Name: "test1_new"}, // should replace the original id1 field
)
fields := testSchema.Fields()
if len(fields) != 2 {
t.Fatalf("Expected 2 fields, got %d (%v)", len(fields), fields)
}
for _, f := range fields {
if f.Id == "" {
t.Fatalf("Expected field id to be set, found empty id for field %v", f)
}
}
if fields[0].Name != "test1_new" {
t.Fatalf("Expected field with name test1_new, got %s", fields[0].Name)
}
if fields[1].Name != "test2" {
t.Fatalf("Expected field with name test2, got %s", fields[1].Name)
}
}
func TestSchemaInitFieldsOptions(t *testing.T) {
f0 := &schema.SchemaField{Name: "test1", Type: "unknown"}
schema0 := schema.NewSchema(f0)
err0 := schema0.InitFieldsOptions()
if err0 == nil {
t.Fatalf("Expected unknown field schema to fail, got nil")
}
// ---
f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText}
f2 := &schema.SchemaField{Name: "test2", Type: schema.FieldTypeEmail}
schema1 := schema.NewSchema(f1, f2)
err1 := schema1.InitFieldsOptions()
if err1 != nil {
t.Fatal(err1)
}
if _, ok := f1.Options.(*schema.TextOptions); !ok {
t.Fatalf("Failed to init f1 options")
}
if _, ok := f2.Options.(*schema.EmailOptions); !ok {
t.Fatalf("Failed to init f2 options")
}
}
func TestSchemaClone(t *testing.T) {
f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText}
f2 := &schema.SchemaField{Name: "test2", Type: schema.FieldTypeEmail}
s1 := schema.NewSchema(f1, f2)
s2, err := s1.Clone()
if err != nil {
t.Fatal(err)
}
s1Encoded, _ := s1.MarshalJSON()
s2Encoded, _ := s2.MarshalJSON()
if string(s1Encoded) != string(s2Encoded) {
t.Fatalf("Expected the cloned schema to be equal, got %v VS\n %v", s1, s2)
}
// change in one schema shouldn't result to change in the other
// (aka. check if it is a deep clone)
s1.Fields()[0].Name = "test1_update"
if s2.Fields()[0].Name != "test1" {
t.Fatalf("Expected s2 field name to not change, got %q", s2.Fields()[0].Name)
}
}
func TestSchemaAsMap(t *testing.T) {
f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText}
f2 := &schema.SchemaField{Name: "test2", Type: schema.FieldTypeEmail}
testSchema := schema.NewSchema(f1, f2)
result := testSchema.AsMap()
if len(result) != 2 {
t.Fatalf("Expected 2 map elements, got %d (%v)", len(result), result)
}
expectedIndexes := []string{f1.Name, f2.Name}
for _, index := range expectedIndexes {
if _, ok := result[index]; !ok {
t.Fatalf("Missing index %q", index)
}
}
}
func TestSchemaGetFieldByName(t *testing.T) {
f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText}
f2 := &schema.SchemaField{Name: "test2", Type: schema.FieldTypeText}
testSchema := schema.NewSchema(f1, f2)
// missing field
result1 := testSchema.GetFieldByName("missing")
if result1 != nil {
t.Fatalf("Found unexpected field %v", result1)
}
// existing field
result2 := testSchema.GetFieldByName("test1")
if result2 == nil || result2.Name != "test1" {
t.Fatalf("Cannot find field with Name 'test1', got %v ", result2)
}
}
func TestSchemaGetFieldById(t *testing.T) {
f1 := &schema.SchemaField{Id: "id1", Name: "test1", Type: schema.FieldTypeText}
f2 := &schema.SchemaField{Id: "id2", Name: "test2", Type: schema.FieldTypeText}
testSchema := schema.NewSchema(f1, f2)
// missing field id
result1 := testSchema.GetFieldById("test1")
if result1 != nil {
t.Fatalf("Found unexpected field %v", result1)
}
// existing field id
result2 := testSchema.GetFieldById("id2")
if result2 == nil || result2.Id != "id2" {
t.Fatalf("Cannot find field with id 'id2', got %v ", result2)
}
}
func TestSchemaRemoveField(t *testing.T) {
f1 := &schema.SchemaField{Id: "id1", Name: "test1", Type: schema.FieldTypeText}
f2 := &schema.SchemaField{Id: "id2", Name: "test2", Type: schema.FieldTypeText}
f3 := &schema.SchemaField{Id: "id3", Name: "test3", Type: schema.FieldTypeText}
testSchema := schema.NewSchema(f1, f2, f3)
testSchema.RemoveField("id2")
testSchema.RemoveField("test3") // should do nothing
expected := []string{"test1", "test3"}
if len(testSchema.Fields()) != len(expected) {
t.Fatalf("Expected %d, got %d (%v)", len(expected), len(testSchema.Fields()), testSchema)
}
for _, name := range expected {
if f := testSchema.GetFieldByName(name); f == nil {
t.Fatalf("Missing field %q", name)
}
}
}
func TestSchemaAddField(t *testing.T) {
f1 := &schema.SchemaField{Name: "test1", Type: schema.FieldTypeText}
f2 := &schema.SchemaField{Id: "f2Id", Name: "test2", Type: schema.FieldTypeText}
f3 := &schema.SchemaField{Id: "f3Id", Name: "test3", Type: schema.FieldTypeText}
testSchema := schema.NewSchema(f1, f2, f3)
f2New := &schema.SchemaField{Id: "f2Id", Name: "test2_new", Type: schema.FieldTypeEmail}
f4 := &schema.SchemaField{Name: "test4", Type: schema.FieldTypeUrl}
testSchema.AddField(f2New)
testSchema.AddField(f4)
if len(testSchema.Fields()) != 4 {
t.Fatalf("Expected %d, got %d (%v)", 4, len(testSchema.Fields()), testSchema)
}
// check if each field has id
for _, f := range testSchema.Fields() {
if f.Id == "" {
t.Fatalf("Expected field id to be set, found empty id for field %v", f)
}
}
// check if f2 field was replaced
if f := testSchema.GetFieldById("f2Id"); f == nil || f.Type != schema.FieldTypeEmail {
t.Fatalf("Expected f2 field to be replaced, found %v", f)
}
// check if f4 was added
if f := testSchema.GetFieldByName("test4"); f == nil || f.Name != "test4" {
t.Fatalf("Expected f4 field to be added, found %v", f)
}
}
func TestSchemaValidate(t *testing.T) {
// emulate duplicated field ids
duplicatedIdsSchema := schema.NewSchema(
&schema.SchemaField{Id: "id1", Name: "test1", Type: schema.FieldTypeText},
&schema.SchemaField{Id: "id2", Name: "test2", Type: schema.FieldTypeText},
)
duplicatedIdsSchema.Fields()[1].Id = "id1" // manually set existing id
scenarios := []struct {
schema schema.Schema
expectError bool
}{
// no fields
{
schema.NewSchema(),
true,
},
// duplicated field ids
{
duplicatedIdsSchema,
true,
},
// duplicated field names (case insensitive)
{
schema.NewSchema(
&schema.SchemaField{Name: "test", Type: schema.FieldTypeText},
&schema.SchemaField{Name: "TeSt", Type: schema.FieldTypeText},
),
true,
},
// failure - base individual fields validation
{
schema.NewSchema(
&schema.SchemaField{Name: "", Type: schema.FieldTypeText},
),
true,
},
// success - base individual fields validation
{
schema.NewSchema(
&schema.SchemaField{Name: "test", Type: schema.FieldTypeText},
),
false,
},
// failure - individual field options validation
{
schema.NewSchema(
&schema.SchemaField{Name: "test", Type: schema.FieldTypeFile},
),
true,
},
// success - individual field options validation
{
schema.NewSchema(
&schema.SchemaField{Name: "test", Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1, MaxSize: 1}},
),
false,
},
}
for i, s := range scenarios {
err := s.schema.Validate()
hasErr := err != nil
if hasErr != s.expectError {
t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, err)
continue
}
}
}
func TestSchemaMarshalJSON(t *testing.T) {
f1 := &schema.SchemaField{Id: "f1id", Name: "test1", Type: schema.FieldTypeText}
f2 := &schema.SchemaField{
Id: "f2id",
Name: "test2",
Type: schema.FieldTypeText,
Options: &schema.TextOptions{Pattern: "test"},
}
testSchema := schema.NewSchema(f1, f2)
result, err := testSchema.MarshalJSON()
if err != nil {
t.Fatal(err)
}
expected := `[{"system":false,"id":"f1id","name":"test1","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}},{"system":false,"id":"f2id","name":"test2","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}]`
if string(result) != expected {
t.Fatalf("Expected %s, got %s", expected, string(result))
}
}
func TestSchemaUnmarshalJSON(t *testing.T) {
encoded := `[{"system":false,"id":"fid1", "name":"test1","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}},{"system":false,"name":"test2","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}]`
testSchema := schema.Schema{}
testSchema.AddField(&schema.SchemaField{Name: "tempField", Type: schema.FieldTypeUrl})
err := testSchema.UnmarshalJSON([]byte(encoded))
if err != nil {
t.Fatal(err)
}
fields := testSchema.Fields()
if len(fields) != 2 {
t.Fatalf("Expected 2 fields, found %v", fields)
}
f1 := testSchema.GetFieldByName("test1")
if f1 == nil {
t.Fatal("Expected to find field 'test1', got nil")
}
if f1.Id != "fid1" {
t.Fatalf("Expected fid1 id, got %s", f1.Id)
}
_, ok := f1.Options.(*schema.TextOptions)
if !ok {
t.Fatal("'test1' field options are not inited.")
}
f2 := testSchema.GetFieldByName("test2")
if f2 == nil {
t.Fatal("Expected to find field 'test2', got nil")
}
if f2.Id == "" {
t.Fatal("Expected f2 id to be set, got empty string")
}
o2, ok := f2.Options.(*schema.TextOptions)
if !ok {
t.Fatal("'test2' field options are not inited.")
}
if o2.Pattern != "test" {
t.Fatalf("Expected pattern to be %q, got %q", "test", o2.Pattern)
}
}
func TestSchemaValue(t *testing.T) {
// empty schema
s1 := schema.Schema{}
v1, err := s1.Value()
if err != nil {
t.Fatal(err)
}
if v1 != nil {
t.Fatalf("Expected nil, got %v", v1)
}
// schema with fields
f1 := &schema.SchemaField{Id: "f1id", Name: "test1", Type: schema.FieldTypeText}
s2 := schema.NewSchema(f1)
v2, err := s2.Value()
if err != nil {
t.Fatal(err)
}
expected := `[{"system":false,"id":"f1id","name":"test1","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}]`
if v2 != expected {
t.Fatalf("Expected %v, got %v", expected, v2)
}
}
func TestSchemaScan(t *testing.T) {
scenarios := []struct {
data any
expectError bool
expectJson string
}{
{nil, false, "[]"},
{"", false, "[]"},
{[]byte{}, false, "[]"},
{"[]", false, "[]"},
{"invalid", true, "[]"},
{123, true, "[]"},
// no field type
{`[{}]`, true, `[]`},
// unknown field type
{
`[{"system":false,"id":"123","name":"test1","type":"unknown","required":false,"unique":false}]`,
true,
`[]`,
},
// without options
{
`[{"system":false,"id":"123","name":"test1","type":"text","required":false,"unique":false}]`,
false,
`[{"system":false,"id":"123","name":"test1","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}]`,
},
// with options
{
`[{"system":false,"id":"123","name":"test1","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}]`,
false,
`[{"system":false,"id":"123","name":"test1","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}]`,
},
}
for i, s := range scenarios {
testSchema := schema.Schema{}
err := testSchema.Scan(s.data)
hasErr := err != nil
if hasErr != s.expectError {
t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, err)
continue
}
json, _ := testSchema.MarshalJSON()
if string(json) != s.expectJson {
t.Errorf("(%d) Expected json %v, got %v", i, s.expectJson, string(json))
}
}
}