234 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			234 lines
		
	
	
		
			5.3 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)
 | 
						|
	}
 | 
						|
 | 
						|
	return s.InitFieldsOptions()
 | 
						|
}
 | 
						|
 | 
						|
// 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)
 | 
						|
}
 |