241 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			241 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			Go
		
	
	
	
| // 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.PseudorandomString(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.By(func(value any) error {
 | |
| 		fields := s.fields // use directly the schema value to avoid unnecessary interface casting
 | |
| 
 | |
| 		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)
 | |
| 	}
 | |
| 
 | |
| 	for _, field := range s.fields {
 | |
| 		if err := field.InitOptions(); err != nil {
 | |
| 			// ignore the error and remove the invalid field
 | |
| 			s.RemoveField(field.Id)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Value implements the [driver.Valuer] interface.
 | |
| func (s Schema) Value() (driver.Value, error) {
 | |
| 	if s.fields == nil {
 | |
| 		// initialize an empty slice to ensure that `[]` is returned
 | |
| 		s.fields = []*SchemaField{}
 | |
| 	}
 | |
| 
 | |
| 	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)
 | |
| }
 |