2024-09-29 19:23:19 +03:00
package core
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"maps"
"slices"
"sort"
"strings"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core/validators"
"github.com/pocketbase/pocketbase/tools/filesystem"
"github.com/pocketbase/pocketbase/tools/hook"
"github.com/pocketbase/pocketbase/tools/inflector"
"github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/store"
"github.com/pocketbase/pocketbase/tools/types"
"github.com/spf13/cast"
)
// used as a workaround by some fields for persisting local state between various events
// (for now is kept private and cannot be changed or cloned outside of the core package)
const internalCustomFieldKeyPrefix = "@pbInternal"
var (
_ Model = ( * Record ) ( nil )
_ HookTagger = ( * Record ) ( nil )
_ DBExporter = ( * Record ) ( nil )
_ FilesManager = ( * Record ) ( nil )
)
type Record struct {
collection * Collection
originalData map [ string ] any
customVisibility * store . Store [ bool ]
data * store . Store [ any ]
expand * store . Store [ any ]
BaseModel
exportCustomData bool
ignoreEmailVisibility bool
ignoreUnchangedFields bool
}
const systemHookIdRecord = "__pbRecordSystemHook__"
func ( app * BaseApp ) registerRecordHooks ( ) {
app . OnModelValidate ( ) . Bind ( & hook . Handler [ * ModelEvent ] {
Id : systemHookIdRecord ,
Func : func ( me * ModelEvent ) error {
if re , ok := newRecordEventFromModelEvent ( me ) ; ok {
return me . App . OnRecordValidate ( ) . Trigger ( re , func ( re * RecordEvent ) error {
syncModelEventWithRecordEvent ( me , re )
return me . Next ( )
} )
}
return me . Next ( )
} ,
Priority : - 99 ,
} )
app . OnModelCreate ( ) . Bind ( & hook . Handler [ * ModelEvent ] {
Id : systemHookIdRecord ,
Func : func ( me * ModelEvent ) error {
if re , ok := newRecordEventFromModelEvent ( me ) ; ok {
return me . App . OnRecordCreate ( ) . Trigger ( re , func ( re * RecordEvent ) error {
syncModelEventWithRecordEvent ( me , re )
return me . Next ( )
} )
}
return me . Next ( )
} ,
Priority : - 99 ,
} )
app . OnModelCreateExecute ( ) . Bind ( & hook . Handler [ * ModelEvent ] {
Id : systemHookIdRecord ,
Func : func ( me * ModelEvent ) error {
if re , ok := newRecordEventFromModelEvent ( me ) ; ok {
return me . App . OnRecordCreateExecute ( ) . Trigger ( re , func ( re * RecordEvent ) error {
syncModelEventWithRecordEvent ( me , re )
return me . Next ( )
} )
}
return me . Next ( )
} ,
Priority : - 99 ,
} )
app . OnModelAfterCreateSuccess ( ) . Bind ( & hook . Handler [ * ModelEvent ] {
Id : systemHookIdRecord ,
Func : func ( me * ModelEvent ) error {
if re , ok := newRecordEventFromModelEvent ( me ) ; ok {
return me . App . OnRecordAfterCreateSuccess ( ) . Trigger ( re , func ( re * RecordEvent ) error {
syncModelEventWithRecordEvent ( me , re )
return me . Next ( )
} )
}
return me . Next ( )
} ,
Priority : - 99 ,
} )
app . OnModelAfterCreateError ( ) . Bind ( & hook . Handler [ * ModelErrorEvent ] {
Id : systemHookIdRecord ,
Func : func ( me * ModelErrorEvent ) error {
if re , ok := newRecordErrorEventFromModelErrorEvent ( me ) ; ok {
return me . App . OnRecordAfterCreateError ( ) . Trigger ( re , func ( re * RecordErrorEvent ) error {
syncModelErrorEventWithRecordErrorEvent ( me , re )
return me . Next ( )
} )
}
return me . Next ( )
} ,
Priority : - 99 ,
} )
app . OnModelUpdate ( ) . Bind ( & hook . Handler [ * ModelEvent ] {
Id : systemHookIdRecord ,
Func : func ( me * ModelEvent ) error {
if re , ok := newRecordEventFromModelEvent ( me ) ; ok {
return me . App . OnRecordUpdate ( ) . Trigger ( re , func ( re * RecordEvent ) error {
syncModelEventWithRecordEvent ( me , re )
return me . Next ( )
} )
}
return me . Next ( )
} ,
Priority : - 99 ,
} )
app . OnModelUpdateExecute ( ) . Bind ( & hook . Handler [ * ModelEvent ] {
Id : systemHookIdRecord ,
Func : func ( me * ModelEvent ) error {
if re , ok := newRecordEventFromModelEvent ( me ) ; ok {
return me . App . OnRecordUpdateExecute ( ) . Trigger ( re , func ( re * RecordEvent ) error {
syncModelEventWithRecordEvent ( me , re )
return me . Next ( )
} )
}
return me . Next ( )
} ,
Priority : - 99 ,
} )
app . OnModelAfterUpdateSuccess ( ) . Bind ( & hook . Handler [ * ModelEvent ] {
Id : systemHookIdRecord ,
Func : func ( me * ModelEvent ) error {
if re , ok := newRecordEventFromModelEvent ( me ) ; ok {
return me . App . OnRecordAfterUpdateSuccess ( ) . Trigger ( re , func ( re * RecordEvent ) error {
syncModelEventWithRecordEvent ( me , re )
return me . Next ( )
} )
}
return me . Next ( )
} ,
Priority : - 99 ,
} )
app . OnModelAfterUpdateError ( ) . Bind ( & hook . Handler [ * ModelErrorEvent ] {
Id : systemHookIdRecord ,
Func : func ( me * ModelErrorEvent ) error {
if re , ok := newRecordErrorEventFromModelErrorEvent ( me ) ; ok {
return me . App . OnRecordAfterUpdateError ( ) . Trigger ( re , func ( re * RecordErrorEvent ) error {
syncModelErrorEventWithRecordErrorEvent ( me , re )
return me . Next ( )
} )
}
return me . Next ( )
} ,
Priority : - 99 ,
} )
app . OnModelDelete ( ) . Bind ( & hook . Handler [ * ModelEvent ] {
Id : systemHookIdRecord ,
Func : func ( me * ModelEvent ) error {
if re , ok := newRecordEventFromModelEvent ( me ) ; ok {
return me . App . OnRecordDelete ( ) . Trigger ( re , func ( re * RecordEvent ) error {
syncModelEventWithRecordEvent ( me , re )
return me . Next ( )
} )
}
return me . Next ( )
} ,
Priority : - 99 ,
} )
app . OnModelDeleteExecute ( ) . Bind ( & hook . Handler [ * ModelEvent ] {
Id : systemHookIdRecord ,
Func : func ( me * ModelEvent ) error {
if re , ok := newRecordEventFromModelEvent ( me ) ; ok {
return me . App . OnRecordDeleteExecute ( ) . Trigger ( re , func ( re * RecordEvent ) error {
syncModelEventWithRecordEvent ( me , re )
return me . Next ( )
} )
}
return me . Next ( )
} ,
Priority : - 99 ,
} )
app . OnModelAfterDeleteSuccess ( ) . Bind ( & hook . Handler [ * ModelEvent ] {
Id : systemHookIdRecord ,
Func : func ( me * ModelEvent ) error {
if re , ok := newRecordEventFromModelEvent ( me ) ; ok {
return me . App . OnRecordAfterDeleteSuccess ( ) . Trigger ( re , func ( re * RecordEvent ) error {
syncModelEventWithRecordEvent ( me , re )
return me . Next ( )
} )
}
return me . Next ( )
} ,
Priority : - 99 ,
} )
app . OnModelAfterDeleteError ( ) . Bind ( & hook . Handler [ * ModelErrorEvent ] {
Id : systemHookIdRecord ,
Func : func ( me * ModelErrorEvent ) error {
if re , ok := newRecordErrorEventFromModelErrorEvent ( me ) ; ok {
return me . App . OnRecordAfterDeleteError ( ) . Trigger ( re , func ( re * RecordErrorEvent ) error {
syncModelErrorEventWithRecordErrorEvent ( me , re )
return me . Next ( )
} )
}
return me . Next ( )
} ,
Priority : - 99 ,
} )
// ---------------------------------------------------------------
app . OnRecordValidate ( ) . Bind ( & hook . Handler [ * RecordEvent ] {
Id : systemHookIdRecord ,
Func : func ( e * RecordEvent ) error {
return e . Record . callFieldInterceptors (
e . Context ,
e . App ,
InterceptorActionValidate ,
func ( ) error {
return onRecordValidate ( e )
} ,
)
} ,
Priority : 99 ,
} )
app . OnRecordCreate ( ) . Bind ( & hook . Handler [ * RecordEvent ] {
Id : systemHookIdRecord ,
Func : func ( e * RecordEvent ) error {
return e . Record . callFieldInterceptors (
e . Context ,
e . App ,
InterceptorActionCreate ,
e . Next ,
)
} ,
Priority : - 99 ,
} )
app . OnRecordCreateExecute ( ) . Bind ( & hook . Handler [ * RecordEvent ] {
Id : systemHookIdRecord ,
Func : func ( e * RecordEvent ) error {
return e . Record . callFieldInterceptors (
e . Context ,
e . App ,
InterceptorActionCreateExecute ,
func ( ) error {
return onRecordSaveExecute ( e )
} ,
)
} ,
Priority : 99 ,
} )
app . OnRecordAfterCreateSuccess ( ) . Bind ( & hook . Handler [ * RecordEvent ] {
Id : systemHookIdRecord ,
Func : func ( e * RecordEvent ) error {
return e . Record . callFieldInterceptors (
e . Context ,
e . App ,
InterceptorActionAfterCreate ,
e . Next ,
)
} ,
Priority : - 99 ,
} )
app . OnRecordAfterCreateError ( ) . Bind ( & hook . Handler [ * RecordErrorEvent ] {
Id : systemHookIdRecord ,
Func : func ( e * RecordErrorEvent ) error {
return e . Record . callFieldInterceptors (
e . Context ,
e . App ,
InterceptorActionAfterCreateError ,
e . Next ,
)
} ,
Priority : - 99 ,
} )
app . OnRecordUpdate ( ) . Bind ( & hook . Handler [ * RecordEvent ] {
Id : systemHookIdRecord ,
Func : func ( e * RecordEvent ) error {
return e . Record . callFieldInterceptors (
e . Context ,
e . App ,
InterceptorActionUpdate ,
e . Next ,
)
} ,
Priority : - 99 ,
} )
app . OnRecordUpdateExecute ( ) . Bind ( & hook . Handler [ * RecordEvent ] {
Id : systemHookIdRecord ,
Func : func ( e * RecordEvent ) error {
return e . Record . callFieldInterceptors (
e . Context ,
e . App ,
InterceptorActionUpdateExecute ,
func ( ) error {
return onRecordSaveExecute ( e )
} ,
)
} ,
Priority : 99 ,
} )
app . OnRecordAfterUpdateSuccess ( ) . Bind ( & hook . Handler [ * RecordEvent ] {
Id : systemHookIdRecord ,
Func : func ( e * RecordEvent ) error {
return e . Record . callFieldInterceptors (
e . Context ,
e . App ,
InterceptorActionAfterUpdate ,
e . Next ,
)
} ,
Priority : - 99 ,
} )
app . OnRecordAfterUpdateError ( ) . Bind ( & hook . Handler [ * RecordErrorEvent ] {
Id : systemHookIdRecord ,
Func : func ( e * RecordErrorEvent ) error {
return e . Record . callFieldInterceptors (
e . Context ,
e . App ,
InterceptorActionAfterUpdateError ,
e . Next ,
)
} ,
Priority : - 99 ,
} )
app . OnRecordDelete ( ) . Bind ( & hook . Handler [ * RecordEvent ] {
Id : systemHookIdRecord ,
Func : func ( e * RecordEvent ) error {
return e . Record . callFieldInterceptors (
e . Context ,
e . App ,
InterceptorActionDelete ,
2024-10-09 11:51:03 +03:00
func ( ) error {
if e . Record . Collection ( ) . IsView ( ) {
return errors . New ( "view records cannot be deleted" )
}
return e . Next ( )
} ,
2024-09-29 19:23:19 +03:00
)
} ,
Priority : - 99 ,
} )
app . OnRecordDeleteExecute ( ) . Bind ( & hook . Handler [ * RecordEvent ] {
Id : systemHookIdRecord ,
Func : func ( e * RecordEvent ) error {
return e . Record . callFieldInterceptors (
e . Context ,
e . App ,
InterceptorActionDeleteExecute ,
func ( ) error {
return onRecordDeleteExecute ( e )
} ,
)
} ,
Priority : 99 ,
} )
app . OnRecordAfterDeleteSuccess ( ) . Bind ( & hook . Handler [ * RecordEvent ] {
Id : systemHookIdRecord ,
Func : func ( e * RecordEvent ) error {
return e . Record . callFieldInterceptors (
e . Context ,
e . App ,
InterceptorActionAfterDelete ,
e . Next ,
)
} ,
Priority : - 99 ,
} )
app . OnRecordAfterDeleteError ( ) . Bind ( & hook . Handler [ * RecordErrorEvent ] {
Id : systemHookIdRecord ,
Func : func ( e * RecordErrorEvent ) error {
return e . Record . callFieldInterceptors (
e . Context ,
e . App ,
InterceptorActionAfterDeleteError ,
e . Next ,
)
} ,
Priority : - 99 ,
} )
}
// -------------------------------------------------------------------
// 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 row result.
func newRecordFromNullStringMap ( collection * Collection , data dbx . NullStringMap ) ( * Record , error ) {
record := NewRecord ( collection )
var fieldName string
for _ , field := range collection . Fields {
fieldName = field . GetName ( )
nullString , ok := data [ fieldName ]
var value any
var err error
if ok && nullString . Valid {
value , err = field . PrepareValue ( record , nullString . String )
} else {
value , err = field . PrepareValue ( record , nil )
}
if err != nil {
return nil , err
}
// we load only the original data to avoid unnecessary copying the same data into the record.data store
// (it is also the reason why we don't invoke PostScan on the record itself)
record . originalData [ fieldName ] = value
if fieldName == FieldNameId {
record . Id = cast . ToString ( value )
}
}
record . BaseModel . PostScan ( )
return record , nil
}
// 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 rows result.
func newRecordsFromNullStringMaps ( collection * Collection , rows [ ] dbx . NullStringMap ) ( [ ] * Record , error ) {
result := make ( [ ] * Record , len ( rows ) )
var err error
for i , row := range rows {
result [ i ] , err = newRecordFromNullStringMap ( collection , row )
if err != nil {
return nil , err
}
}
return result , nil
}
// -------------------------------------------------------------------
// NewRecord initializes a new empty Record model.
func NewRecord ( collection * Collection ) * Record {
record := & Record {
collection : collection ,
data : store . New [ any ] ( nil ) ,
customVisibility : store . New [ bool ] ( nil ) ,
originalData : make ( map [ string ] any , len ( collection . Fields ) ) ,
}
// initialize default field values
2024-11-29 11:30:54 +02:00
var fieldName string
2024-09-29 19:23:19 +03:00
for _ , field := range collection . Fields {
2024-11-29 11:30:54 +02:00
fieldName = field . GetName ( )
if fieldName == FieldNameId {
2024-09-29 19:23:19 +03:00
continue
}
2024-11-29 11:30:54 +02:00
2024-09-29 19:23:19 +03:00
value , _ := field . PrepareValue ( record , nil )
2024-11-29 11:30:54 +02:00
record . originalData [ fieldName ] = value
2024-09-29 19:23:19 +03:00
}
return record
}
// Collection returns the Collection model associated with the current Record model.
//
// NB! The returned collection is only for read purposes and it shouldn't be modified
// because it could have unintended side-effects on other Record models from the same collection.
func ( m * Record ) Collection ( ) * Collection {
return m . collection
}
// TableName returns the table name associated with the current Record model.
func ( m * Record ) TableName ( ) string {
return m . collection . Name
}
// PostScan implements the [dbx.PostScanner] interface.
//
// It essentially refreshes/updates the current Record original state
// as if the model was fetched from the databases for the first time.
//
// Or in other words, it means that m.Original().FieldsData() will have
// the same values as m.Record().FieldsData().
func ( m * Record ) PostScan ( ) error {
if m . Id == "" {
return errors . New ( "missing record primary key" )
}
if err := m . BaseModel . PostScan ( ) ; err != nil {
return err
}
m . originalData = m . FieldsData ( )
return nil
}
// HookTags returns the hook tags associated with the current record.
func ( m * Record ) HookTags ( ) [ ] string {
return [ ] string { m . collection . Name , m . collection . Id }
}
// BaseFilesPath returns the storage dir path used by the record.
func ( m * Record ) BaseFilesPath ( ) string {
id := cast . ToString ( m . LastSavedPK ( ) )
if id == "" {
id = m . Id
}
return m . collection . BaseFilesPath ( ) + "/" + id
}
// Original returns a shallow copy of the current record model populated
// with its ORIGINAL db data state (aka. right after PostScan())
// and everything else reset to the defaults.
//
// If record was created using NewRecord() the original will be always
// a blank record (until PostScan() is invoked).
func ( m * Record ) Original ( ) * Record {
newRecord := NewRecord ( m . collection )
newRecord . originalData = maps . Clone ( m . originalData )
if newRecord . originalData [ FieldNameId ] != nil {
newRecord . lastSavedPK = cast . ToString ( newRecord . originalData [ FieldNameId ] )
newRecord . Id = newRecord . lastSavedPK
}
return newRecord
}
// Fresh returns a shallow copy of the current record model populated
// with its LATEST data state and everything else reset to the defaults
// (aka. no expand, no unknown fields and with default visibility flags).
func ( m * Record ) Fresh ( ) * Record {
newRecord := m . Original ( )
2024-11-28 23:06:03 +02:00
// note: this will also load the Id field through m.GetRaw
2024-11-29 11:30:54 +02:00
var fieldName string
2024-11-28 23:06:03 +02:00
for _ , field := range m . collection . Fields {
2024-11-29 11:30:54 +02:00
fieldName = field . GetName ( )
newRecord . SetRaw ( fieldName , m . GetRaw ( fieldName ) )
2024-11-28 23:06:03 +02:00
}
2024-09-29 19:23:19 +03:00
return newRecord
}
// Clone returns a shallow copy of the current record model with all of
// its collection and unknown fields data, expand and flags copied.
//
// use [Record.Fresh()] instead if you want a copy with only the latest
// collection fields data and everything else reset to the defaults.
func ( m * Record ) Clone ( ) * Record {
newRecord := m . Original ( )
newRecord . Id = m . Id
newRecord . exportCustomData = m . exportCustomData
newRecord . ignoreEmailVisibility = m . ignoreEmailVisibility
newRecord . ignoreUnchangedFields = m . ignoreUnchangedFields
newRecord . customVisibility . Reset ( m . customVisibility . GetAll ( ) )
2024-11-28 23:06:03 +02:00
data := m . data . GetAll ( )
for k , v := range data {
newRecord . SetRaw ( k , v )
}
2024-09-29 19:23:19 +03:00
if m . expand != nil {
newRecord . SetExpand ( m . expand . GetAll ( ) )
}
return newRecord
}
// Expand returns a shallow copy of the current Record model expand data (if any).
func ( m * Record ) Expand ( ) map [ string ] any {
if m . expand == nil {
// return a dummy initialized map to avoid assignment to nil map errors
return map [ string ] any { }
}
return m . expand . GetAll ( )
}
// SetExpand replaces the current Record's expand with the provided expand arg data (shallow copied).
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 )
}
// FieldsData returns a shallow copy ONLY of the collection's fields record's data.
func ( m * Record ) FieldsData ( ) map [ string ] any {
result := make ( map [ string ] any , len ( m . collection . Fields ) )
2024-11-29 11:30:54 +02:00
var fieldName string
2024-09-29 19:23:19 +03:00
for _ , field := range m . collection . Fields {
2024-11-29 11:30:54 +02:00
fieldName = field . GetName ( )
result [ fieldName ] = m . Get ( fieldName )
2024-09-29 19:23:19 +03:00
}
return result
}
// CustomData returns a shallow copy ONLY of the custom record fields data,
// aka. fields that are neither defined by the collection, nor special system ones.
//
// Note that custom fields prefixed with "@pbInternal" are always skipped.
func ( m * Record ) CustomData ( ) map [ string ] any {
if m . data == nil {
return nil
}
fields := m . Collection ( ) . Fields
knownFields := make ( map [ string ] struct { } , len ( fields ) )
for _ , f := range fields {
knownFields [ f . GetName ( ) ] = struct { } { }
}
result := map [ string ] any { }
rawData := m . data . GetAll ( )
for k , v := range rawData {
if _ , ok := knownFields [ k ] ; ! ok {
// skip internal custom fields
if strings . HasPrefix ( k , internalCustomFieldKeyPrefix ) {
continue
}
result [ k ] = v
}
}
return result
}
// WithCustomData toggles the export/serialization of custom data fields
// (false by default).
func ( m * Record ) WithCustomData ( state bool ) * Record {
m . exportCustomData = state
return m
}
// IgnoreEmailVisibility toggles the flag to ignore the auth record email visibility check.
func ( m * Record ) IgnoreEmailVisibility ( state bool ) * Record {
m . ignoreEmailVisibility = state
return m
}
// IgnoreUnchangedFields toggles the flag to ignore the unchanged fields
// from the DB export for the UPDATE SQL query.
//
// This could be used if you want to save only the record fields that you've changed
// without overwrite other untouched fields in case of concurrent update.
func ( m * Record ) IgnoreUnchangedFields ( state bool ) * Record {
m . ignoreUnchangedFields = state
return m
}
// Set sets the provided key-value data pair into the current Record
// model directly as it is WITHOUT NORMALIZATIONS.
//
// See also [Record.Set].
func ( m * Record ) SetRaw ( key string , value any ) {
if key == FieldNameId {
m . Id = cast . ToString ( value )
}
m . data . Set ( key , value )
}
// SetIfFieldExists sets the provided key-value data pair into the current Record model
// ONLY if key is existing Collection field name/modifier.
//
// This method does nothing if key is not a known Collection field name/modifier.
//
// On success returns the matched Field, otherwise - nil.
//
// To set any key-value, including custom/unknown fields, use the [Record.Set] method.
func ( m * Record ) SetIfFieldExists ( key string , value any ) Field {
for _ , field := range m . Collection ( ) . Fields {
ff , ok := field . ( SetterFinder )
if ok {
setter := ff . FindSetter ( key )
if setter != nil {
setter ( m , value )
return field
}
}
// fallback to the default field PrepareValue method for direct match
if key == field . GetName ( ) {
value , _ = field . PrepareValue ( m , value )
m . SetRaw ( key , value )
return field
}
}
return nil
}
// Set sets the provided key-value data pair into 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 setter(s).
func ( m * Record ) Set ( key string , value any ) {
switch key {
case FieldNameExpand : // for backward-compatibility with earlier versions
m . SetExpand ( cast . ToStringMap ( value ) )
default :
field := m . SetIfFieldExists ( key , value )
if field == nil {
// custom key - set it without any transformations
m . SetRaw ( key , value )
}
}
}
func ( m * Record ) GetRaw ( key string ) any {
if key == FieldNameId {
return m . Id
}
if v , ok := m . data . GetOk ( key ) ; ok {
return v
}
return m . originalData [ key ]
}
// Get returns a normalized single record model data value for "key".
func ( m * Record ) Get ( key string ) any {
switch key {
case FieldNameExpand : // for backward-compatibility with earlier versions
return m . Expand ( )
default :
for _ , field := range m . Collection ( ) . Fields {
gm , ok := field . ( GetterFinder )
if ! ok {
continue // no custom getters
}
getter := gm . FindGetter ( key )
if getter != nil {
return getter ( m )
}
}
return m . GetRaw ( key )
}
}
// Load bulk loads the provided data into the current Record model.
func ( m * Record ) Load ( data map [ string ] any ) {
for k , v := range data {
m . Set ( k , v )
}
}
// 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 ) )
}
// 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 non-zero unique strings.
func ( m * Record ) GetStringSlice ( key string ) [ ] string {
return list . ToUniqueStringSlice ( m . Get ( key ) )
}
// GetUploadedFiles returns the uploaded files for the provided "file" field key,
// (aka. the current [*filesytem.File] values) so that you can apply further
// validations or modifications (including changing the file name or content before persisting).
//
// Example:
//
// files := record.GetUploadedFiles("documents")
// for _, f := range files {
// f.Name = "doc_" + f.Name // add a prefix to each file name
// }
// app.Save(record) // the files are pointers so the applied changes will transparently reflect on the record value
func ( m * Record ) GetUploadedFiles ( key string ) [ ] * filesystem . File {
if ! strings . HasSuffix ( key , ":uploaded" ) {
key += ":uploaded"
}
values , _ := m . Get ( key ) . ( [ ] * filesystem . File )
return values
}
// 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 )
}
// ExpandedOne retrieves a single relation Record from the already
// loaded expand data of the current model.
//
// If the requested expand relation is multiple, this method returns
// only first available Record from the expanded relation.
//
// Returns nil if there is no such expand relation loaded.
func ( m * Record ) ExpandedOne ( relField string ) * Record {
if m . expand == nil {
return nil
}
rel := m . expand . Get ( relField )
switch v := rel . ( type ) {
case * Record :
return v
case [ ] * Record :
if len ( v ) > 0 {
return v [ 0 ]
}
}
return nil
}
// ExpandedAll retrieves a slice of relation Records from the already
// loaded expand data of the current model.
//
// If the requested expand relation is single, this method normalizes
// the return result and will wrap the single model as a slice.
//
// Returns nil slice if there is no such expand relation loaded.
func ( m * Record ) ExpandedAll ( relField string ) [ ] * Record {
if m . expand == nil {
return nil
}
rel := m . expand . Get ( relField )
switch v := rel . ( type ) {
case * Record :
return [ ] * Record { v }
case [ ] * Record :
return v
}
return nil
}
// 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 ) * FileField {
for _ , field := range m . Collection ( ) . Fields {
if field . Type ( ) != FieldTypeFile {
continue
}
f , ok := field . ( * FileField )
if ! ok {
continue
}
filenames := m . GetStringSlice ( f . GetName ( ) )
if slices . Contains ( filenames , filename ) {
return f
}
}
return nil
}
// DBExport implements the [DBExporter] interface and returns a key-value
// map with the data to be persisted when saving the Record in the database.
func ( m * Record ) DBExport ( app App ) ( map [ string ] any , error ) {
result , err := m . dbExport ( )
if err != nil {
return nil , err
}
// remove exported fields that haven't changed
// (with exception of the id column)
if ! m . IsNew ( ) && m . ignoreUnchangedFields {
oldResult , err := m . Original ( ) . dbExport ( )
if err != nil {
return nil , err
}
for oldK , oldV := range oldResult {
if oldK == idColumn {
continue
}
newV , ok := result [ oldK ]
if ok && areValuesEqual ( newV , oldV ) {
delete ( result , oldK )
}
}
}
return result , nil
}
func ( m * Record ) dbExport ( ) ( map [ string ] any , error ) {
fields := m . Collection ( ) . Fields
result := make ( map [ string ] any , len ( fields ) )
2024-11-29 11:30:54 +02:00
var fieldName string
2024-09-29 19:23:19 +03:00
for _ , field := range fields {
2024-11-29 11:30:54 +02:00
fieldName = field . GetName ( )
2024-09-29 19:23:19 +03:00
if f , ok := field . ( DriverValuer ) ; ok {
v , err := f . DriverValue ( m )
if err != nil {
return nil , err
}
2024-11-29 11:30:54 +02:00
result [ fieldName ] = v
2024-09-29 19:23:19 +03:00
} else {
2024-11-29 11:30:54 +02:00
result [ fieldName ] = m . GetRaw ( fieldName )
2024-09-29 19:23:19 +03:00
}
}
return result , nil
}
func areValuesEqual ( a any , b any ) bool {
switch av := a . ( type ) {
case string :
bv , ok := b . ( string )
return ok && bv == av
case bool :
bv , ok := b . ( bool )
return ok && bv == av
case float32 :
bv , ok := b . ( float32 )
return ok && bv == av
case float64 :
bv , ok := b . ( float64 )
return ok && bv == av
case uint :
bv , ok := b . ( uint )
return ok && bv == av
case uint8 :
bv , ok := b . ( uint8 )
return ok && bv == av
case uint16 :
bv , ok := b . ( uint16 )
return ok && bv == av
case uint32 :
bv , ok := b . ( uint32 )
return ok && bv == av
case uint64 :
bv , ok := b . ( uint64 )
return ok && bv == av
case int :
bv , ok := b . ( int )
return ok && bv == av
case int8 :
bv , ok := b . ( int8 )
return ok && bv == av
case int16 :
bv , ok := b . ( int16 )
return ok && bv == av
case int32 :
bv , ok := b . ( int32 )
return ok && bv == av
case int64 :
bv , ok := b . ( int64 )
return ok && bv == av
case [ ] byte :
bv , ok := b . ( [ ] byte )
return ok && bytes . Equal ( av , bv )
case [ ] string :
bv , ok := b . ( [ ] string )
return ok && slices . Equal ( av , bv )
case [ ] int :
bv , ok := b . ( [ ] int )
return ok && slices . Equal ( av , bv )
case [ ] int32 :
bv , ok := b . ( [ ] int32 )
return ok && slices . Equal ( av , bv )
case [ ] int64 :
bv , ok := b . ( [ ] int64 )
return ok && slices . Equal ( av , bv )
case [ ] float32 :
bv , ok := b . ( [ ] float32 )
return ok && slices . Equal ( av , bv )
case [ ] float64 :
bv , ok := b . ( [ ] float64 )
return ok && slices . Equal ( av , bv )
case types . JSONArray [ string ] :
bv , ok := b . ( types . JSONArray [ string ] )
return ok && slices . Equal ( av , bv )
case types . JSONRaw :
bv , ok := b . ( types . JSONRaw )
return ok && bytes . Equal ( av , bv )
default :
aRaw , err := json . Marshal ( a )
if err != nil {
return false
}
bRaw , err := json . Marshal ( b )
if err != nil {
return false
}
return bytes . Equal ( aRaw , bRaw )
}
}
// Hide hides the specified fields from the public safe serialization of the record.
func ( record * Record ) Hide ( fieldNames ... string ) * Record {
for _ , name := range fieldNames {
record . customVisibility . Set ( name , false )
}
return record
}
// Unhide forces to unhide the specified fields from the public safe serialization
// of the record (even when the collection field itself is marked as hidden).
func ( record * Record ) Unhide ( fieldNames ... string ) * Record {
for _ , name := range fieldNames {
record . customVisibility . Set ( name , true )
}
return record
}
// PublicExport exports only the record fields that are safe to be public.
//
// To export unknown data fields you need to set record.WithCustomData(true).
//
// For auth records, to force the export of the email field you need to set
// record.IgnoreEmailVisibility(true).
func ( record * Record ) PublicExport ( ) map [ string ] any {
export := make ( map [ string ] any , len ( record . collection . Fields ) + 3 )
var isVisible , hasCustomVisibility bool
customVisibility := record . customVisibility . GetAll ( )
// export schema fields
2024-11-29 11:30:54 +02:00
var fieldName string
2024-09-29 19:23:19 +03:00
for _ , f := range record . collection . Fields {
2024-11-29 11:30:54 +02:00
fieldName = f . GetName ( )
isVisible , hasCustomVisibility = customVisibility [ fieldName ]
2024-09-29 19:23:19 +03:00
if ! hasCustomVisibility {
isVisible = ! f . GetHidden ( )
}
if ! isVisible {
continue
}
2024-11-29 11:30:54 +02:00
export [ fieldName ] = record . Get ( fieldName )
2024-09-29 19:23:19 +03:00
}
// export custom fields
if record . exportCustomData {
for k , v := range record . CustomData ( ) {
isVisible , hasCustomVisibility = customVisibility [ k ]
if ! hasCustomVisibility || isVisible {
export [ k ] = v
}
}
}
if record . Collection ( ) . IsAuth ( ) {
// always hide the password and tokenKey fields
delete ( export , FieldNamePassword )
delete ( export , FieldNameTokenKey )
if ! record . ignoreEmailVisibility && ! record . GetBool ( FieldNameEmailVisibility ) {
delete ( export , FieldNameEmail )
}
}
// add helper collection reference fields
isVisible , hasCustomVisibility = customVisibility [ FieldNameCollectionId ]
if ! hasCustomVisibility || isVisible {
export [ FieldNameCollectionId ] = record . collection . Id
}
isVisible , hasCustomVisibility = customVisibility [ FieldNameCollectionName ]
if ! hasCustomVisibility || isVisible {
export [ FieldNameCollectionName ] = record . collection . Name
}
// add expand (if non-nil)
isVisible , hasCustomVisibility = customVisibility [ FieldNameExpand ]
if ( ! hasCustomVisibility || isVisible ) && record . expand != nil {
export [ FieldNameExpand ] = record . expand . GetAll ( )
}
return export
}
// 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
}
// ReplaceModifiers 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 (ex. 1; -5; +2 => -2).
//
// Note that because Go doesn't guaranteed the iteration order of maps,
// we would explicitly apply shorter keys first for a more consistent and reproducible behavior.
//
// Example usage:
//
// newData := record.ReplaceModifiers(data)
// // record: {"field": 10}
// // data: {"field+": 5}
// // result: {"field": 15}
func ( m * Record ) ReplaceModifiers ( data map [ string ] any ) map [ string ] any {
if len ( data ) == 0 {
return data
}
dataCopy := maps . Clone ( data )
recordCopy := m . Fresh ( )
// key orders is not guaranteed so
sortedDataKeys := make ( [ ] string , 0 , len ( data ) )
for k := range data {
sortedDataKeys = append ( sortedDataKeys , k )
}
sort . SliceStable ( sortedDataKeys , func ( i int , j int ) bool {
return len ( sortedDataKeys [ i ] ) < len ( sortedDataKeys [ j ] )
} )
for _ , k := range sortedDataKeys {
field := recordCopy . SetIfFieldExists ( k , data [ k ] )
if field != nil {
// delete the original key in case it is with a modifer (ex. "items+")
delete ( dataCopy , k )
// store the transformed value under the field name
dataCopy [ field . GetName ( ) ] = recordCopy . Get ( field . GetName ( ) )
}
}
return dataCopy
}
// -------------------------------------------------------------------
func ( m * Record ) callFieldInterceptors (
ctx context . Context ,
app App ,
actionName string ,
actionFunc func ( ) error ,
) error {
// the firing order of the fields doesn't matter
for _ , field := range m . Collection ( ) . Fields {
if f , ok := field . ( RecordInterceptor ) ; ok {
oldfn := actionFunc
actionFunc = func ( ) error {
return f . Intercept ( ctx , app , m , actionName , oldfn )
}
}
}
return actionFunc ( )
}
func onRecordValidate ( e * RecordEvent ) error {
errs := validation . Errors { }
for _ , f := range e . Record . Collection ( ) . Fields {
if err := f . ValidateValue ( e . Context , e . App , e . Record ) ; err != nil {
errs [ f . GetName ( ) ] = err
}
}
if len ( errs ) > 0 {
return errs
}
return e . Next ( )
}
func onRecordSaveExecute ( e * RecordEvent ) error {
if e . Record . Collection ( ) . IsAuth ( ) {
// ensure that the token key is different on password change
old := e . Record . Original ( )
if ! e . Record . IsNew ( ) &&
old . TokenKey ( ) == e . Record . TokenKey ( ) &&
old . Get ( FieldNamePassword ) != e . Record . Get ( FieldNamePassword ) {
e . Record . RefreshTokenKey ( )
}
// cross-check that the auth record id is unique across all auth collections.
authCollections , err := e . App . FindAllCollections ( CollectionTypeAuth )
if err != nil {
return fmt . Errorf ( "unable to fetch the auth collections for cross-id unique check: %w" , err )
}
for _ , collection := range authCollections {
if e . Record . Collection ( ) . Id == collection . Id {
continue // skip current collection (sqlite will do the check for us)
}
record , _ := e . App . FindRecordById ( collection , e . Record . Id )
if record != nil {
return validation . Errors {
FieldNameId : validation . NewError ( "validation_invalid_auth_id" , "Invalid or duplicated auth record id." ) ,
}
}
}
}
err := e . Next ( )
if err == nil {
return nil
}
return validators . NormalizeUniqueIndexError (
err ,
e . Record . Collection ( ) . Name ,
e . Record . Collection ( ) . Fields . FieldNames ( ) ,
)
}
func onRecordDeleteExecute ( e * RecordEvent ) error {
// fetch rel references (if any)
//
// note: the select is outside of the transaction to minimize
// SQLITE_BUSY errors when mixing read&write in a single transaction
refs , err := e . App . FindCollectionReferences ( e . Record . Collection ( ) )
if err != nil {
return err
}
originalApp := e . App
txErr := e . App . RunInTransaction ( func ( txApp App ) error {
e . App = txApp
// delete the record before the relation references to ensure that there
// will be no "A<->B" relations to prevent deadlock when calling DeleteRecord recursively
if err := e . Next ( ) ; err != nil {
return err
}
return cascadeRecordDelete ( txApp , e . Record , refs )
} )
e . App = originalApp
return txErr
}
// cascadeRecordDelete triggers cascade deletion for the provided references.
//
// NB! This method is expected to be called from inside of a transaction.
func cascadeRecordDelete ( app App , mainRecord * Record , refs map [ * Collection ] [ ] Field ) error {
// Sort the refs keys to ensure that the cascade events firing order is always the same.
// This is not necessary for the operation to function correctly but it helps having deterministic output during testing.
sortedRefKeys := make ( [ ] * Collection , 0 , len ( refs ) )
for k := range refs {
sortedRefKeys = append ( sortedRefKeys , k )
}
sort . Slice ( sortedRefKeys , func ( i , j int ) bool {
return sortedRefKeys [ i ] . Name < sortedRefKeys [ j ] . Name
} )
for _ , refCollection := range sortedRefKeys {
fields , ok := refs [ refCollection ]
if refCollection . IsView ( ) || ! ok {
continue // skip missing or view collections
}
for _ , field := range fields {
recordTableName := inflector . Columnify ( refCollection . Name )
prefixedFieldName := recordTableName + "." + inflector . Columnify ( field . GetName ( ) )
query := app . RecordQuery ( refCollection )
if opt , ok := field . ( MultiValuer ) ; ! ok || ! opt . IsMultiple ( ) {
query . AndWhere ( dbx . HashExp { prefixedFieldName : mainRecord . Id } )
} else {
query . AndWhere ( dbx . Exists ( dbx . NewExp ( fmt . Sprintf (
` SELECT 1 FROM json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END) {{ __je__ }} WHERE [[__je__.value]]= { :jevalue} ` ,
prefixedFieldName , prefixedFieldName , prefixedFieldName ,
) , dbx . Params {
"jevalue" : mainRecord . Id ,
} ) ) )
}
if refCollection . Id == mainRecord . Collection ( ) . Id {
query . AndWhere ( dbx . Not ( dbx . HashExp { recordTableName + ".id" : mainRecord . Id } ) )
}
// trigger cascade for each batchSize rel items until there is none
batchSize := 4000
rows := make ( [ ] * Record , 0 , batchSize )
for {
if err := query . Limit ( int64 ( batchSize ) ) . All ( & rows ) ; err != nil {
return err
}
total := len ( rows )
if total == 0 {
break
}
err := deleteRefRecords ( app , mainRecord , rows , field )
if err != nil {
return err
}
if total < batchSize {
break // no more items
}
rows = rows [ : 0 ] // keep allocated memory
}
}
}
return nil
}
// deleteRefRecords checks if related records has to be deleted (if `CascadeDelete` is set)
// OR
// just unset the record id from any relation field values (if they are not required).
//
// NB! This method is expected to be called from inside of a transaction.
func deleteRefRecords ( app App , mainRecord * Record , refRecords [ ] * Record , field Field ) error {
relField , _ := field . ( * RelationField )
if relField == nil {
return errors . New ( "only RelationField is supported at the moment, got " + field . Type ( ) )
}
for _ , refRecord := range refRecords {
ids := refRecord . GetStringSlice ( relField . Name )
// unset the record id
for i := len ( ids ) - 1 ; i >= 0 ; i -- {
if ids [ i ] == mainRecord . Id {
ids = append ( ids [ : i ] , ids [ i + 1 : ] ... )
break
}
}
// cascade delete the reference
// (only if there are no other active references in case of multiple select)
if relField . CascadeDelete && len ( ids ) == 0 {
if err := app . Delete ( refRecord ) ; err != nil {
return err
}
// no further actions are needed (the reference is deleted)
continue
}
if relField . Required && len ( ids ) == 0 {
return fmt . Errorf ( "the record cannot be deleted because it is part of a required reference in record %s (%s collection)" , refRecord . Id , refRecord . Collection ( ) . Name )
}
// save the reference changes
// (without validation because it is possible that another relation field to have a reference to a previous deleted record)
refRecord . Set ( relField . Name , ids )
if err := app . SaveNoValidate ( refRecord ) ; err != nil {
return err
}
}
return nil
}