mirror of
https://github.com/pocketbase/pocketbase.git
synced 2025-01-10 00:43:36 +02:00
237 lines
5.4 KiB
Go
237 lines
5.4 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.Required, validation.By(func(value any) error {
|
|
fields := s.fields // use directly the schema value to avoid unnecessary 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)
|
|
}
|