878 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			878 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Go
		
	
	
	
| package models
 | |
| 
 | |
| import (
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"regexp"
 | |
| 	"strconv"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/pocketbase/dbx"
 | |
| 	"github.com/pocketbase/pocketbase/models/schema"
 | |
| 	"github.com/pocketbase/pocketbase/tools/list"
 | |
| 	"github.com/pocketbase/pocketbase/tools/security"
 | |
| 	"github.com/pocketbase/pocketbase/tools/store"
 | |
| 	"github.com/pocketbase/pocketbase/tools/types"
 | |
| 	"github.com/spf13/cast"
 | |
| 	"golang.org/x/crypto/bcrypt"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	_ Model             = (*Record)(nil)
 | |
| 	_ ColumnValueMapper = (*Record)(nil)
 | |
| 	_ FilesManager      = (*Record)(nil)
 | |
| )
 | |
| 
 | |
| type Record struct {
 | |
| 	BaseModel
 | |
| 
 | |
| 	collection *Collection
 | |
| 
 | |
| 	exportUnknown         bool // whether to export unknown fields
 | |
| 	ignoreEmailVisibility bool // whether to ignore the emailVisibility flag for auth collections
 | |
| 	loaded                bool
 | |
| 	originalData          map[string]any    // the original (aka. first loaded) model data
 | |
| 	expand                *store.Store[any] // expanded relations
 | |
| 	data                  *store.Store[any] // any custom data in addition to the base model fields
 | |
| }
 | |
| 
 | |
| // NewRecord initializes a new empty Record model.
 | |
| func NewRecord(collection *Collection) *Record {
 | |
| 	return &Record{
 | |
| 		collection: collection,
 | |
| 		data:       store.New[any](nil),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // nullStringMapValue returns the raw string value if it exist and
 | |
| // its not NULL, otherwise - nil.
 | |
| func nullStringMapValue(data dbx.NullStringMap, key string) any {
 | |
| 	nullString, ok := data[key]
 | |
| 
 | |
| 	if ok && nullString.Valid {
 | |
| 		return nullString.String
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // NewRecordFromNullStringMap initializes a single new Record model
 | |
| // with data loaded from the provided NullStringMap.
 | |
| //
 | |
| // Note that this method is intended to load and Scan data from a database
 | |
| // result and calls PostScan() which marks the record as "not new".
 | |
| func NewRecordFromNullStringMap(collection *Collection, data dbx.NullStringMap) *Record {
 | |
| 	resultMap := make(map[string]any, len(data))
 | |
| 
 | |
| 	// load schema fields
 | |
| 	for _, field := range collection.Schema.Fields() {
 | |
| 		resultMap[field.Name] = nullStringMapValue(data, field.Name)
 | |
| 	}
 | |
| 
 | |
| 	// load base model fields
 | |
| 	for _, name := range schema.BaseModelFieldNames() {
 | |
| 		resultMap[name] = nullStringMapValue(data, name)
 | |
| 	}
 | |
| 
 | |
| 	// load auth fields
 | |
| 	if collection.IsAuth() {
 | |
| 		for _, name := range schema.AuthFieldNames() {
 | |
| 			resultMap[name] = nullStringMapValue(data, name)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	record := NewRecord(collection)
 | |
| 
 | |
| 	record.Load(resultMap)
 | |
| 	record.PostScan()
 | |
| 
 | |
| 	return record
 | |
| }
 | |
| 
 | |
| // NewRecordsFromNullStringMaps initializes a new Record model for
 | |
| // each row in the provided NullStringMap slice.
 | |
| //
 | |
| // Note that this method is intended to load and Scan data from a database
 | |
| // result and calls PostScan() for each record marking them as "not new".
 | |
| func NewRecordsFromNullStringMaps(collection *Collection, rows []dbx.NullStringMap) []*Record {
 | |
| 	result := make([]*Record, len(rows))
 | |
| 
 | |
| 	for i, row := range rows {
 | |
| 		result[i] = NewRecordFromNullStringMap(collection, row)
 | |
| 	}
 | |
| 
 | |
| 	return result
 | |
| }
 | |
| 
 | |
| // TableName returns the table name associated to the current Record model.
 | |
| func (m *Record) TableName() string {
 | |
| 	return m.collection.Name
 | |
| }
 | |
| 
 | |
| // Collection returns the Collection model associated to the current Record model.
 | |
| func (m *Record) Collection() *Collection {
 | |
| 	return m.collection
 | |
| }
 | |
| 
 | |
| // OriginalCopy returns a copy of the current record model populated
 | |
| // with its ORIGINAL data state (aka. the initially loaded) and
 | |
| // everything else reset to the defaults.
 | |
| func (m *Record) OriginalCopy() *Record {
 | |
| 	newRecord := NewRecord(m.collection)
 | |
| 	newRecord.Load(m.originalData)
 | |
| 
 | |
| 	if m.IsNew() {
 | |
| 		newRecord.MarkAsNew()
 | |
| 	} else {
 | |
| 		newRecord.MarkAsNotNew()
 | |
| 	}
 | |
| 
 | |
| 	return newRecord
 | |
| }
 | |
| 
 | |
| // CleanCopy returns a copy of the current record model populated only
 | |
| // with its LATEST data state and everything else reset to the defaults.
 | |
| func (m *Record) CleanCopy() *Record {
 | |
| 	newRecord := NewRecord(m.collection)
 | |
| 	newRecord.Load(m.data.GetAll())
 | |
| 	newRecord.Id = m.Id
 | |
| 	newRecord.Created = m.Created
 | |
| 	newRecord.Updated = m.Updated
 | |
| 
 | |
| 	if m.IsNew() {
 | |
| 		newRecord.MarkAsNew()
 | |
| 	} else {
 | |
| 		newRecord.MarkAsNotNew()
 | |
| 	}
 | |
| 
 | |
| 	return newRecord
 | |
| }
 | |
| 
 | |
| // Expand returns a shallow copy of the current Record model expand data.
 | |
| func (m *Record) Expand() map[string]any {
 | |
| 	if m.expand == nil {
 | |
| 		m.expand = store.New[any](nil)
 | |
| 	}
 | |
| 
 | |
| 	return m.expand.GetAll()
 | |
| }
 | |
| 
 | |
| // SetExpand shallow copies the provided data to the current Record model's expand.
 | |
| func (m *Record) SetExpand(expand map[string]any) {
 | |
| 	if m.expand == nil {
 | |
| 		m.expand = store.New[any](nil)
 | |
| 	}
 | |
| 
 | |
| 	m.expand.Reset(expand)
 | |
| }
 | |
| 
 | |
| // MergeExpand merges recursively the provided expand data into
 | |
| // the current model's expand (if any).
 | |
| //
 | |
| // Note that if an expanded prop with the same key is a slice (old or new expand)
 | |
| // then both old and new records will be merged into a new slice (aka. a :merge: [b,c] => [a,b,c]).
 | |
| // Otherwise the "old" expanded record will be replace with the "new" one (aka. a :merge: aNew => aNew).
 | |
| func (m *Record) MergeExpand(expand map[string]any) {
 | |
| 	// nothing to merge
 | |
| 	if len(expand) == 0 {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// no old expand
 | |
| 	if m.expand == nil {
 | |
| 		m.expand = store.New(expand)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	oldExpand := m.expand.GetAll()
 | |
| 
 | |
| 	for key, new := range expand {
 | |
| 		old, ok := oldExpand[key]
 | |
| 		if !ok {
 | |
| 			oldExpand[key] = new
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		var wasOldSlice bool
 | |
| 		var oldSlice []*Record
 | |
| 		switch v := old.(type) {
 | |
| 		case *Record:
 | |
| 			oldSlice = []*Record{v}
 | |
| 		case []*Record:
 | |
| 			wasOldSlice = true
 | |
| 			oldSlice = v
 | |
| 		default:
 | |
| 			// invalid old expand data -> assign directly the new
 | |
| 			// (no matter whether new is valid or not)
 | |
| 			oldExpand[key] = new
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		var wasNewSlice bool
 | |
| 		var newSlice []*Record
 | |
| 		switch v := new.(type) {
 | |
| 		case *Record:
 | |
| 			newSlice = []*Record{v}
 | |
| 		case []*Record:
 | |
| 			wasNewSlice = true
 | |
| 			newSlice = v
 | |
| 		default:
 | |
| 			// invalid new expand data -> skip
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		oldIndexed := make(map[string]*Record, len(oldSlice))
 | |
| 		for _, oldRecord := range oldSlice {
 | |
| 			oldIndexed[oldRecord.Id] = oldRecord
 | |
| 		}
 | |
| 
 | |
| 		for _, newRecord := range newSlice {
 | |
| 			oldRecord := oldIndexed[newRecord.Id]
 | |
| 			if oldRecord != nil {
 | |
| 				// note: there is no need to update oldSlice since oldRecord is a reference
 | |
| 				oldRecord.MergeExpand(newRecord.Expand())
 | |
| 			} else {
 | |
| 				// missing new entry
 | |
| 				oldSlice = append(oldSlice, newRecord)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if wasOldSlice || wasNewSlice || len(oldSlice) == 0 {
 | |
| 			oldExpand[key] = oldSlice
 | |
| 		} else {
 | |
| 			oldExpand[key] = oldSlice[0]
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	m.expand.Reset(oldExpand)
 | |
| }
 | |
| 
 | |
| // SchemaData returns a shallow copy ONLY of the defined record schema fields data.
 | |
| func (m *Record) SchemaData() map[string]any {
 | |
| 	result := make(map[string]any, len(m.collection.Schema.Fields()))
 | |
| 
 | |
| 	data := m.data.GetAll()
 | |
| 
 | |
| 	for _, field := range m.collection.Schema.Fields() {
 | |
| 		if v, ok := data[field.Name]; ok {
 | |
| 			result[field.Name] = v
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return result
 | |
| }
 | |
| 
 | |
| // UnknownData returns a shallow copy ONLY of the unknown record fields data,
 | |
| // aka. fields that are neither one of the base and special system ones,
 | |
| // nor defined by the collection schema.
 | |
| func (m *Record) UnknownData() map[string]any {
 | |
| 	if m.data == nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	return m.extractUnknownData(m.data.GetAll())
 | |
| }
 | |
| 
 | |
| // IgnoreEmailVisibility toggles the flag to ignore the auth record email visibility check.
 | |
| func (m *Record) IgnoreEmailVisibility(state bool) {
 | |
| 	m.ignoreEmailVisibility = state
 | |
| }
 | |
| 
 | |
| // WithUnknownData toggles the export/serialization of unknown data fields
 | |
| // (false by default).
 | |
| func (m *Record) WithUnknownData(state bool) {
 | |
| 	m.exportUnknown = state
 | |
| }
 | |
| 
 | |
| // Set sets the provided key-value data pair for the current Record model.
 | |
| //
 | |
| // If the record collection has field with name matching the provided "key",
 | |
| // the value will be further normalized according to the field rules.
 | |
| func (m *Record) Set(key string, value any) {
 | |
| 	switch key {
 | |
| 	case schema.FieldNameId:
 | |
| 		m.Id = cast.ToString(value)
 | |
| 	case schema.FieldNameCreated:
 | |
| 		m.Created, _ = types.ParseDateTime(value)
 | |
| 	case schema.FieldNameUpdated:
 | |
| 		m.Updated, _ = types.ParseDateTime(value)
 | |
| 	case schema.FieldNameExpand:
 | |
| 		m.SetExpand(cast.ToStringMap(value))
 | |
| 	default:
 | |
| 		var v = value
 | |
| 
 | |
| 		if field := m.Collection().Schema.GetFieldByName(key); field != nil {
 | |
| 			v = field.PrepareValue(value)
 | |
| 		} else if m.collection.IsAuth() {
 | |
| 			// normalize auth fields
 | |
| 			switch key {
 | |
| 			case schema.FieldNameEmailVisibility, schema.FieldNameVerified:
 | |
| 				v = cast.ToBool(value)
 | |
| 			case schema.FieldNameLastResetSentAt, schema.FieldNameLastVerificationSentAt:
 | |
| 				v, _ = types.ParseDateTime(value)
 | |
| 			case schema.FieldNameUsername, schema.FieldNameEmail, schema.FieldNameTokenKey, schema.FieldNamePasswordHash:
 | |
| 				v = cast.ToString(value)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if m.data == nil {
 | |
| 			m.data = store.New[any](nil)
 | |
| 		}
 | |
| 
 | |
| 		m.data.Set(key, v)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Get returns a single record model data value for "key".
 | |
| func (m *Record) Get(key string) any {
 | |
| 	switch key {
 | |
| 	case schema.FieldNameId:
 | |
| 		return m.Id
 | |
| 	case schema.FieldNameCreated:
 | |
| 		return m.Created
 | |
| 	case schema.FieldNameUpdated:
 | |
| 		return m.Updated
 | |
| 	default:
 | |
| 		if m.data == nil {
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		return m.data.Get(key)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // GetBool returns the data value for "key" as a bool.
 | |
| func (m *Record) GetBool(key string) bool {
 | |
| 	return cast.ToBool(m.Get(key))
 | |
| }
 | |
| 
 | |
| // GetString returns the data value for "key" as a string.
 | |
| func (m *Record) GetString(key string) string {
 | |
| 	return cast.ToString(m.Get(key))
 | |
| }
 | |
| 
 | |
| // GetInt returns the data value for "key" as an int.
 | |
| func (m *Record) GetInt(key string) int {
 | |
| 	return cast.ToInt(m.Get(key))
 | |
| }
 | |
| 
 | |
| // GetFloat returns the data value for "key" as a float64.
 | |
| func (m *Record) GetFloat(key string) float64 {
 | |
| 	return cast.ToFloat64(m.Get(key))
 | |
| }
 | |
| 
 | |
| // GetTime returns the data value for "key" as a [time.Time] instance.
 | |
| func (m *Record) GetTime(key string) time.Time {
 | |
| 	return cast.ToTime(m.Get(key))
 | |
| }
 | |
| 
 | |
| // GetDateTime returns the data value for "key" as a DateTime instance.
 | |
| func (m *Record) GetDateTime(key string) types.DateTime {
 | |
| 	d, _ := types.ParseDateTime(m.Get(key))
 | |
| 	return d
 | |
| }
 | |
| 
 | |
| // GetStringSlice returns the data value for "key" as a slice of unique strings.
 | |
| func (m *Record) GetStringSlice(key string) []string {
 | |
| 	return list.ToUniqueStringSlice(m.Get(key))
 | |
| }
 | |
| 
 | |
| // Retrieves the "key" json field value and unmarshals it into "result".
 | |
| //
 | |
| // Example
 | |
| //
 | |
| //	result := struct {
 | |
| //	    FirstName string `json:"first_name"`
 | |
| //	}{}
 | |
| //	err := m.UnmarshalJSONField("my_field_name", &result)
 | |
| func (m *Record) UnmarshalJSONField(key string, result any) error {
 | |
| 	return json.Unmarshal([]byte(m.GetString(key)), &result)
 | |
| }
 | |
| 
 | |
| // BaseFilesPath returns the storage dir path used by the record.
 | |
| func (m *Record) BaseFilesPath() string {
 | |
| 	return fmt.Sprintf("%s/%s", m.Collection().BaseFilesPath(), m.Id)
 | |
| }
 | |
| 
 | |
| // FindFileFieldByFile returns the first file type field for which
 | |
| // any of the record's data contains the provided filename.
 | |
| func (m *Record) FindFileFieldByFile(filename string) *schema.SchemaField {
 | |
| 	for _, field := range m.Collection().Schema.Fields() {
 | |
| 		if field.Type == schema.FieldTypeFile {
 | |
| 			names := m.GetStringSlice(field.Name)
 | |
| 			if list.ExistInSlice(filename, names) {
 | |
| 				return field
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Load bulk loads the provided data into the current Record model.
 | |
| func (m *Record) Load(data map[string]any) {
 | |
| 	if !m.loaded {
 | |
| 		m.loaded = true
 | |
| 		m.originalData = data
 | |
| 	}
 | |
| 
 | |
| 	for k, v := range data {
 | |
| 		m.Set(k, v)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // ColumnValueMap implements [ColumnValueMapper] interface.
 | |
| func (m *Record) ColumnValueMap() map[string]any {
 | |
| 	result := make(map[string]any, len(m.collection.Schema.Fields())+3)
 | |
| 
 | |
| 	// export schema field values
 | |
| 	for _, field := range m.collection.Schema.Fields() {
 | |
| 		result[field.Name] = m.getNormalizeDataValueForDB(field.Name)
 | |
| 	}
 | |
| 
 | |
| 	// export auth collection fields
 | |
| 	if m.collection.IsAuth() {
 | |
| 		for _, name := range schema.AuthFieldNames() {
 | |
| 			result[name] = m.getNormalizeDataValueForDB(name)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// export base model fields
 | |
| 	result[schema.FieldNameId] = m.getNormalizeDataValueForDB(schema.FieldNameId)
 | |
| 	result[schema.FieldNameCreated] = m.getNormalizeDataValueForDB(schema.FieldNameCreated)
 | |
| 	result[schema.FieldNameUpdated] = m.getNormalizeDataValueForDB(schema.FieldNameUpdated)
 | |
| 
 | |
| 	return result
 | |
| }
 | |
| 
 | |
| // PublicExport exports only the record fields that are safe to be public.
 | |
| //
 | |
| // Fields marked as hidden will be exported only if `m.IgnoreEmailVisibility(true)` is set.
 | |
| func (m *Record) PublicExport() map[string]any {
 | |
| 	result := make(map[string]any, len(m.collection.Schema.Fields())+5)
 | |
| 
 | |
| 	// export unknown data fields if allowed
 | |
| 	if m.exportUnknown {
 | |
| 		for k, v := range m.UnknownData() {
 | |
| 			result[k] = v
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// export schema field values
 | |
| 	for _, field := range m.collection.Schema.Fields() {
 | |
| 		result[field.Name] = m.Get(field.Name)
 | |
| 	}
 | |
| 
 | |
| 	// export some of the safe auth collection fields
 | |
| 	if m.collection.IsAuth() {
 | |
| 		result[schema.FieldNameVerified] = m.Verified()
 | |
| 		result[schema.FieldNameUsername] = m.Username()
 | |
| 		result[schema.FieldNameEmailVisibility] = m.EmailVisibility()
 | |
| 		if m.ignoreEmailVisibility || m.EmailVisibility() {
 | |
| 			result[schema.FieldNameEmail] = m.Email()
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// export base model fields
 | |
| 	result[schema.FieldNameId] = m.GetId()
 | |
| 	if created := m.GetCreated(); !m.Collection().IsView() || !created.IsZero() {
 | |
| 		result[schema.FieldNameCreated] = created
 | |
| 	}
 | |
| 	if updated := m.GetUpdated(); !m.Collection().IsView() || !updated.IsZero() {
 | |
| 		result[schema.FieldNameUpdated] = updated
 | |
| 	}
 | |
| 
 | |
| 	// add helper collection reference fields
 | |
| 	result[schema.FieldNameCollectionId] = m.collection.Id
 | |
| 	result[schema.FieldNameCollectionName] = m.collection.Name
 | |
| 
 | |
| 	// add expand (if set)
 | |
| 	if m.expand != nil && m.expand.Length() > 0 {
 | |
| 		result[schema.FieldNameExpand] = m.expand.GetAll()
 | |
| 	}
 | |
| 
 | |
| 	return result
 | |
| }
 | |
| 
 | |
| // MarshalJSON implements the [json.Marshaler] interface.
 | |
| //
 | |
| // Only the data exported by `PublicExport()` will be serialized.
 | |
| func (m Record) MarshalJSON() ([]byte, error) {
 | |
| 	return json.Marshal(m.PublicExport())
 | |
| }
 | |
| 
 | |
| // UnmarshalJSON implements the [json.Unmarshaler] interface.
 | |
| func (m *Record) UnmarshalJSON(data []byte) error {
 | |
| 	result := map[string]any{}
 | |
| 
 | |
| 	if err := json.Unmarshal(data, &result); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	m.Load(result)
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // ReplaceModifers returns a new map with applied modifier
 | |
| // values based on the current record and the specified data.
 | |
| //
 | |
| // The resolved modifier keys will be removed.
 | |
| //
 | |
| // Multiple modifiers will be applied one after another,
 | |
| // while reusing the previous base key value result (eg. 1; -5; +2 => -2).
 | |
| //
 | |
| // Example usage:
 | |
| //
 | |
| //	 newData := record.ReplaceModifers(data)
 | |
| //		// record:  {"field": 10}
 | |
| //		// data:    {"field+": 5}
 | |
| //		// newData: {"field": 15}
 | |
| func (m *Record) ReplaceModifers(data map[string]any) map[string]any {
 | |
| 	var clone = shallowCopy(data)
 | |
| 	if len(clone) == 0 {
 | |
| 		return clone
 | |
| 	}
 | |
| 
 | |
| 	var recordDataCache map[string]any
 | |
| 
 | |
| 	// export recordData lazily
 | |
| 	recordData := func() map[string]any {
 | |
| 		if recordDataCache == nil {
 | |
| 			recordDataCache = m.SchemaData()
 | |
| 		}
 | |
| 		return recordDataCache
 | |
| 	}
 | |
| 
 | |
| 	modifiers := schema.FieldValueModifiers()
 | |
| 
 | |
| 	for _, field := range m.Collection().Schema.Fields() {
 | |
| 		key := field.Name
 | |
| 
 | |
| 		for _, m := range modifiers {
 | |
| 			if mv, mOk := clone[key+m]; mOk {
 | |
| 				if _, ok := clone[key]; !ok {
 | |
| 					// get base value from the merged data
 | |
| 					clone[key] = recordData()[key]
 | |
| 				}
 | |
| 
 | |
| 				clone[key] = field.PrepareValueWithModifier(clone[key], m, mv)
 | |
| 				delete(clone, key+m)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if field.Type != schema.FieldTypeFile {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// -----------------------------------------------------------
 | |
| 		// legacy file field modifiers (kept for backward compatibility)
 | |
| 		// -----------------------------------------------------------
 | |
| 
 | |
| 		var oldNames []string
 | |
| 		var toDelete []string
 | |
| 		if _, ok := clone[key]; ok {
 | |
| 			oldNames = list.ToUniqueStringSlice(clone[key])
 | |
| 		} else {
 | |
| 			// get oldNames from the model
 | |
| 			oldNames = list.ToUniqueStringSlice(recordData()[key])
 | |
| 		}
 | |
| 
 | |
| 		// search for individual file name to delete (eg. "file.test.png = null")
 | |
| 		for _, name := range oldNames {
 | |
| 			suffixedKey := key + "." + name
 | |
| 			if v, ok := clone[suffixedKey]; ok && cast.ToString(v) == "" {
 | |
| 				toDelete = append(toDelete, name)
 | |
| 				delete(clone, suffixedKey)
 | |
| 				continue
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// search for individual file index to delete (eg. "file.0 = null")
 | |
| 		keyExp, _ := regexp.Compile(`^` + regexp.QuoteMeta(key) + `\.\d+$`)
 | |
| 		for indexedKey := range clone {
 | |
| 			if keyExp.MatchString(indexedKey) && cast.ToString(clone[indexedKey]) == "" {
 | |
| 				index, indexErr := strconv.Atoi(indexedKey[len(key)+1:])
 | |
| 				if indexErr != nil || index < 0 || index >= len(oldNames) {
 | |
| 					continue
 | |
| 				}
 | |
| 				toDelete = append(toDelete, oldNames[index])
 | |
| 				delete(clone, indexedKey)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if toDelete != nil {
 | |
| 			clone[key] = field.PrepareValue(list.SubtractSlice(oldNames, toDelete))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return clone
 | |
| }
 | |
| 
 | |
| // getNormalizeDataValueForDB returns the "key" data value formatted for db storage.
 | |
| func (m *Record) getNormalizeDataValueForDB(key string) any {
 | |
| 	var val any
 | |
| 
 | |
| 	// normalize auth fields
 | |
| 	if m.collection.IsAuth() {
 | |
| 		switch key {
 | |
| 		case schema.FieldNameEmailVisibility, schema.FieldNameVerified:
 | |
| 			return m.GetBool(key)
 | |
| 		case schema.FieldNameLastResetSentAt, schema.FieldNameLastVerificationSentAt:
 | |
| 			return m.GetDateTime(key)
 | |
| 		case schema.FieldNameUsername, schema.FieldNameEmail, schema.FieldNameTokenKey, schema.FieldNamePasswordHash:
 | |
| 			return m.GetString(key)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	val = m.Get(key)
 | |
| 
 | |
| 	switch ids := val.(type) {
 | |
| 	case []string:
 | |
| 		// encode string slice
 | |
| 		return append(types.JsonArray[string]{}, ids...)
 | |
| 	case []int:
 | |
| 		// encode int slice
 | |
| 		return append(types.JsonArray[int]{}, ids...)
 | |
| 	case []float64:
 | |
| 		// encode float64 slice
 | |
| 		return append(types.JsonArray[float64]{}, ids...)
 | |
| 	case []any:
 | |
| 		// encode interface slice
 | |
| 		return append(types.JsonArray[any]{}, ids...)
 | |
| 	default:
 | |
| 		// no changes
 | |
| 		return val
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // shallowCopy shallow copy data into a new map.
 | |
| func shallowCopy(data map[string]any) map[string]any {
 | |
| 	result := make(map[string]any, len(data))
 | |
| 
 | |
| 	for k, v := range data {
 | |
| 		result[k] = v
 | |
| 	}
 | |
| 
 | |
| 	return result
 | |
| }
 | |
| 
 | |
| func (m *Record) extractUnknownData(data map[string]any) map[string]any {
 | |
| 	knownFields := map[string]struct{}{}
 | |
| 
 | |
| 	for _, name := range schema.SystemFieldNames() {
 | |
| 		knownFields[name] = struct{}{}
 | |
| 	}
 | |
| 	for _, name := range schema.BaseModelFieldNames() {
 | |
| 		knownFields[name] = struct{}{}
 | |
| 	}
 | |
| 
 | |
| 	for _, f := range m.collection.Schema.Fields() {
 | |
| 		knownFields[f.Name] = struct{}{}
 | |
| 	}
 | |
| 
 | |
| 	if m.collection.IsAuth() {
 | |
| 		for _, name := range schema.AuthFieldNames() {
 | |
| 			knownFields[name] = struct{}{}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	result := map[string]any{}
 | |
| 
 | |
| 	for k, v := range data {
 | |
| 		if _, ok := knownFields[k]; !ok {
 | |
| 			result[k] = v
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return result
 | |
| }
 | |
| 
 | |
| // -------------------------------------------------------------------
 | |
| // Auth helpers
 | |
| // -------------------------------------------------------------------
 | |
| 
 | |
| var notAuthRecordErr = errors.New("Not an auth collection record.")
 | |
| 
 | |
| // Username returns the "username" auth record data value.
 | |
| func (m *Record) Username() string {
 | |
| 	return m.GetString(schema.FieldNameUsername)
 | |
| }
 | |
| 
 | |
| // SetUsername sets the "username" auth record data value.
 | |
| //
 | |
| // This method doesn't check whether the provided value is a valid username.
 | |
| //
 | |
| // Returns an error if the record is not from an auth collection.
 | |
| func (m *Record) SetUsername(username string) error {
 | |
| 	if !m.collection.IsAuth() {
 | |
| 		return notAuthRecordErr
 | |
| 	}
 | |
| 
 | |
| 	m.Set(schema.FieldNameUsername, username)
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Email returns the "email" auth record data value.
 | |
| func (m *Record) Email() string {
 | |
| 	return m.GetString(schema.FieldNameEmail)
 | |
| }
 | |
| 
 | |
| // SetEmail sets the "email" auth record data value.
 | |
| //
 | |
| // This method doesn't check whether the provided value is a valid email.
 | |
| //
 | |
| // Returns an error if the record is not from an auth collection.
 | |
| func (m *Record) SetEmail(email string) error {
 | |
| 	if !m.collection.IsAuth() {
 | |
| 		return notAuthRecordErr
 | |
| 	}
 | |
| 
 | |
| 	m.Set(schema.FieldNameEmail, email)
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Verified returns the "emailVisibility" auth record data value.
 | |
| func (m *Record) EmailVisibility() bool {
 | |
| 	return m.GetBool(schema.FieldNameEmailVisibility)
 | |
| }
 | |
| 
 | |
| // SetEmailVisibility sets the "emailVisibility" auth record data value.
 | |
| //
 | |
| // Returns an error if the record is not from an auth collection.
 | |
| func (m *Record) SetEmailVisibility(visible bool) error {
 | |
| 	if !m.collection.IsAuth() {
 | |
| 		return notAuthRecordErr
 | |
| 	}
 | |
| 
 | |
| 	m.Set(schema.FieldNameEmailVisibility, visible)
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Verified returns the "verified" auth record data value.
 | |
| func (m *Record) Verified() bool {
 | |
| 	return m.GetBool(schema.FieldNameVerified)
 | |
| }
 | |
| 
 | |
| // SetVerified sets the "verified" auth record data value.
 | |
| //
 | |
| // Returns an error if the record is not from an auth collection.
 | |
| func (m *Record) SetVerified(verified bool) error {
 | |
| 	if !m.collection.IsAuth() {
 | |
| 		return notAuthRecordErr
 | |
| 	}
 | |
| 
 | |
| 	m.Set(schema.FieldNameVerified, verified)
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // TokenKey returns the "tokenKey" auth record data value.
 | |
| func (m *Record) TokenKey() string {
 | |
| 	return m.GetString(schema.FieldNameTokenKey)
 | |
| }
 | |
| 
 | |
| // SetTokenKey sets the "tokenKey" auth record data value.
 | |
| //
 | |
| // Returns an error if the record is not from an auth collection.
 | |
| func (m *Record) SetTokenKey(key string) error {
 | |
| 	if !m.collection.IsAuth() {
 | |
| 		return notAuthRecordErr
 | |
| 	}
 | |
| 
 | |
| 	m.Set(schema.FieldNameTokenKey, key)
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // RefreshTokenKey generates and sets new random auth record "tokenKey".
 | |
| //
 | |
| // Returns an error if the record is not from an auth collection.
 | |
| func (m *Record) RefreshTokenKey() error {
 | |
| 	return m.SetTokenKey(security.RandomString(50))
 | |
| }
 | |
| 
 | |
| // LastResetSentAt returns the "lastResentSentAt" auth record data value.
 | |
| func (m *Record) LastResetSentAt() types.DateTime {
 | |
| 	return m.GetDateTime(schema.FieldNameLastResetSentAt)
 | |
| }
 | |
| 
 | |
| // SetLastResetSentAt sets the "lastResentSentAt" auth record data value.
 | |
| //
 | |
| // Returns an error if the record is not from an auth collection.
 | |
| func (m *Record) SetLastResetSentAt(dateTime types.DateTime) error {
 | |
| 	if !m.collection.IsAuth() {
 | |
| 		return notAuthRecordErr
 | |
| 	}
 | |
| 
 | |
| 	m.Set(schema.FieldNameLastResetSentAt, dateTime)
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // LastVerificationSentAt returns the "lastVerificationSentAt" auth record data value.
 | |
| func (m *Record) LastVerificationSentAt() types.DateTime {
 | |
| 	return m.GetDateTime(schema.FieldNameLastVerificationSentAt)
 | |
| }
 | |
| 
 | |
| // SetLastVerificationSentAt sets an "lastVerificationSentAt" auth record data value.
 | |
| //
 | |
| // Returns an error if the record is not from an auth collection.
 | |
| func (m *Record) SetLastVerificationSentAt(dateTime types.DateTime) error {
 | |
| 	if !m.collection.IsAuth() {
 | |
| 		return notAuthRecordErr
 | |
| 	}
 | |
| 
 | |
| 	m.Set(schema.FieldNameLastVerificationSentAt, dateTime)
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // PasswordHash returns the "passwordHash" auth record data value.
 | |
| func (m *Record) PasswordHash() string {
 | |
| 	return m.GetString(schema.FieldNamePasswordHash)
 | |
| }
 | |
| 
 | |
| // ValidatePassword validates a plain password against the auth record password.
 | |
| //
 | |
| // Returns false if the password is incorrect or record is not from an auth collection.
 | |
| func (m *Record) ValidatePassword(password string) bool {
 | |
| 	if !m.collection.IsAuth() {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	err := bcrypt.CompareHashAndPassword([]byte(m.PasswordHash()), []byte(password))
 | |
| 
 | |
| 	return err == nil
 | |
| }
 | |
| 
 | |
| // SetPassword sets cryptographically secure string to the auth record "password" field.
 | |
| // This method also resets the "lastResetSentAt" and the "tokenKey" fields.
 | |
| //
 | |
| // Returns an error if the record is not from an auth collection or
 | |
| // an empty password is provided.
 | |
| func (m *Record) SetPassword(password string) error {
 | |
| 	if !m.collection.IsAuth() {
 | |
| 		return notAuthRecordErr
 | |
| 	}
 | |
| 
 | |
| 	if password == "" {
 | |
| 		return errors.New("The provided plain password is empty")
 | |
| 	}
 | |
| 
 | |
| 	// hash the password
 | |
| 	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	m.Set(schema.FieldNamePasswordHash, string(hashedPassword))
 | |
| 	m.Set(schema.FieldNameLastResetSentAt, types.DateTime{})
 | |
| 
 | |
| 	// invalidate previously issued tokens
 | |
| 	return m.RefreshTokenKey()
 | |
| }
 |