1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2024-12-01 02:56:49 +02:00
pocketbase/models/record.go

305 lines
8.0 KiB
Go

package models
import (
"encoding/json"
"fmt"
"log"
"strings"
"time"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/types"
"github.com/spf13/cast"
)
var _ Model = (*Record)(nil)
var _ ColumnValueMapper = (*Record)(nil)
type Record struct {
BaseModel
collection *Collection
data map[string]any
expand map[string]any
}
// NewRecord initializes a new empty Record model.
func NewRecord(collection *Collection) *Record {
return &Record{
collection: collection,
data: map[string]any{},
}
}
// NewRecordFromNullStringMap initializes a single new Record model
// with data loaded from the provided NullStringMap.
func NewRecordFromNullStringMap(collection *Collection, data dbx.NullStringMap) *Record {
resultMap := map[string]any{}
for _, field := range collection.Schema.Fields() {
var rawValue any
nullString, ok := data[field.Name]
if !ok || !nullString.Valid {
rawValue = nil
} else {
rawValue = nullString.String
}
resultMap[field.Name] = rawValue
}
record := NewRecord(collection)
// load base mode fields
resultMap[schema.ReservedFieldNameId] = data[schema.ReservedFieldNameId].String
resultMap[schema.ReservedFieldNameCreated] = data[schema.ReservedFieldNameCreated].String
resultMap[schema.ReservedFieldNameUpdated] = data[schema.ReservedFieldNameUpdated].String
if err := record.Load(resultMap); err != nil {
log.Println("Failed to unmarshal record:", err)
}
return record
}
// NewRecordsFromNullStringMaps initializes a new Record model for
// each row in the provided NullStringMap slice.
func NewRecordsFromNullStringMaps(collection *Collection, rows []dbx.NullStringMap) []*Record {
result := []*Record{}
for _, row := range rows {
result = append(result, 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
}
// GetExpand returns a shallow copy of the optional `expand` data
// attached to the current Record model.
func (m *Record) GetExpand() map[string]any {
return shallowCopy(m.expand)
}
// SetExpand assigns the provided data to `record.expand`.
func (m *Record) SetExpand(data map[string]any) {
m.expand = shallowCopy(data)
}
// Data returns a shallow copy of the currently loaded record's data.
func (m *Record) Data() map[string]any {
return shallowCopy(m.data)
}
// SetDataValue sets the provided key-value data pair for the current Record model.
//
// This method does nothing if the record doesn't have a `key` field.
func (m *Record) SetDataValue(key string, value any) {
if m.data == nil {
m.data = map[string]any{}
}
field := m.Collection().Schema.GetFieldByName(key)
if field != nil {
m.data[key] = field.PrepareValue(value)
}
}
// GetDataValue returns the current record's data value for `key`.
//
// Returns nil if data value with `key` is not found or set.
func (m *Record) GetDataValue(key string) any {
return m.data[key]
}
// GetBoolDataValue returns the data value for `key` as a bool.
func (m *Record) GetBoolDataValue(key string) bool {
return cast.ToBool(m.GetDataValue(key))
}
// GetStringDataValue returns the data value for `key` as a string.
func (m *Record) GetStringDataValue(key string) string {
return cast.ToString(m.GetDataValue(key))
}
// GetIntDataValue returns the data value for `key` as an int.
func (m *Record) GetIntDataValue(key string) int {
return cast.ToInt(m.GetDataValue(key))
}
// GetFloatDataValue returns the data value for `key` as a float64.
func (m *Record) GetFloatDataValue(key string) float64 {
return cast.ToFloat64(m.GetDataValue(key))
}
// GetTimeDataValue returns the data value for `key` as a [time.Time] instance.
func (m *Record) GetTimeDataValue(key string) time.Time {
return cast.ToTime(m.GetDataValue(key))
}
// GetDateTimeDataValue returns the data value for `key` as a DateTime instance.
func (m *Record) GetDateTimeDataValue(key string) types.DateTime {
d, _ := types.ParseDateTime(m.GetDataValue(key))
return d
}
// GetStringSliceDataValue returns the data value for `key` as a slice of unique strings.
func (m *Record) GetStringSliceDataValue(key string) []string {
return list.ToUniqueStringSlice(m.GetDataValue(key))
}
// 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.GetStringSliceDataValue(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) error {
if data[schema.ReservedFieldNameId] != nil {
id, err := cast.ToStringE(data[schema.ReservedFieldNameId])
if err != nil {
return err
}
m.Id = id
}
if data[schema.ReservedFieldNameCreated] != nil {
m.Created, _ = types.ParseDateTime(data[schema.ReservedFieldNameCreated])
}
if data[schema.ReservedFieldNameUpdated] != nil {
m.Updated, _ = types.ParseDateTime(data[schema.ReservedFieldNameUpdated])
}
for k, v := range data {
m.SetDataValue(k, v)
}
return nil
}
// ColumnValueMap implements [ColumnValueMapper] interface.
func (m *Record) ColumnValueMap() map[string]any {
result := map[string]any{}
for key := range m.data {
result[key] = m.normalizeDataValueForDB(key)
}
// set base model fields
result[schema.ReservedFieldNameId] = m.Id
result[schema.ReservedFieldNameCreated] = m.Created
result[schema.ReservedFieldNameUpdated] = m.Updated
return result
}
// PublicExport exports only the record fields that are safe to be public.
//
// This method also skips the "hidden" fields, aka. fields prefixed with `#`.
func (m *Record) PublicExport() map[string]any {
result := skipHiddenFields(m.data)
// set base model fields
result[schema.ReservedFieldNameId] = m.Id
result[schema.ReservedFieldNameCreated] = m.Created
result[schema.ReservedFieldNameUpdated] = m.Updated
// add helper collection fields
result["@collectionId"] = m.collection.Id
result["@collectionName"] = m.collection.Name
// add expand (if set)
if m.expand != nil {
result["@expand"] = m.expand
}
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
}
return m.Load(result)
}
// normalizeDataValueForDB returns the `key` data value formatted for db storage.
func (m *Record) normalizeDataValueForDB(key string) any {
val := m.GetDataValue(key)
switch ids := val.(type) {
case []string:
// encode strings slice
return append(types.JsonArray{}, list.ToInterfaceSlice(ids)...)
case []any:
// encode interfaces slice
return append(types.JsonArray{}, ids...)
default:
// no changes
return val
}
}
// shallowCopy shallow copy data into a new map.
func shallowCopy(data map[string]any) map[string]any {
result := map[string]any{}
for k, v := range data {
result[k] = v
}
return result
}
// skipHiddenFields returns a new data map without the "#" prefixed fields.
func skipHiddenFields(data map[string]any) map[string]any {
result := map[string]any{}
for key, val := range data {
// ignore "#" prefixed fields
if strings.HasPrefix(key, "#") {
continue
}
result[key] = val
}
return result
}