1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-01-10 00:43:36 +02:00
pocketbase/models/schema/schema.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)
}