2022-07-06 23:19:05 +02:00
package daos
import (
"database/sql"
"errors"
"fmt"
"log"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/security"
)
// UserQuery returns a new User model select query.
func ( dao * Dao ) UserQuery ( ) * dbx . SelectQuery {
return dao . ModelQuery ( & models . User { } )
}
// LoadProfile loads the profile record associated to the provided user.
func ( dao * Dao ) LoadProfile ( user * models . User ) error {
collection , err := dao . FindCollectionByNameOrId ( models . ProfileCollectionName )
if err != nil {
return err
}
profile , err := dao . FindFirstRecordByData ( collection , models . ProfileCollectionUserFieldName , user . Id )
if err != nil && err != sql . ErrNoRows {
return err
}
user . Profile = profile
return nil
}
// LoadProfiles loads the profile records associated to the provied users list.
func ( dao * Dao ) LoadProfiles ( users [ ] * models . User ) error {
collection , err := dao . FindCollectionByNameOrId ( models . ProfileCollectionName )
if err != nil {
return err
}
// extract user ids
2022-07-14 18:26:08 +02:00
ids := make ( [ ] string , len ( users ) )
2022-07-06 23:19:05 +02:00
usersMap := map [ string ] * models . User { }
2022-07-14 18:26:08 +02:00
for i , user := range users {
ids [ i ] = user . Id
2022-07-06 23:19:05 +02:00
usersMap [ user . Id ] = user
}
profiles , err := dao . FindRecordsByExpr ( collection , dbx . HashExp {
models . ProfileCollectionUserFieldName : list . ToInterfaceSlice ( ids ) ,
} )
if err != nil {
return err
}
// populate each user.Profile member
for _ , profile := range profiles {
userId := profile . GetStringDataValue ( models . ProfileCollectionUserFieldName )
user , ok := usersMap [ userId ]
if ! ok {
continue
}
user . Profile = profile
}
return nil
}
// FindUserById finds a single User model by its id.
//
// This method also auto loads the related user profile record
// into the found model.
func ( dao * Dao ) FindUserById ( id string ) ( * models . User , error ) {
model := & models . User { }
err := dao . UserQuery ( ) .
AndWhere ( dbx . HashExp { "id" : id } ) .
Limit ( 1 ) .
One ( model )
if err != nil {
return nil , err
}
// try to load the user profile (if exist)
if err := dao . LoadProfile ( model ) ; err != nil {
log . Println ( err )
}
return model , nil
}
// FindUserByEmail finds a single User model by its email address.
//
// This method also auto loads the related user profile record
// into the found model.
func ( dao * Dao ) FindUserByEmail ( email string ) ( * models . User , error ) {
model := & models . User { }
err := dao . UserQuery ( ) .
AndWhere ( dbx . HashExp { "email" : email } ) .
Limit ( 1 ) .
One ( model )
if err != nil {
return nil , err
}
// try to load the user profile (if exist)
if err := dao . LoadProfile ( model ) ; err != nil {
log . Println ( err )
}
return model , nil
}
// FindUserByToken finds the user associated with the provided JWT token.
// Returns an error if the JWT token is invalid or expired.
//
// This method also auto loads the related user profile record
// into the found model.
func ( dao * Dao ) FindUserByToken ( token string , baseTokenKey string ) ( * models . User , error ) {
unverifiedClaims , err := security . ParseUnverifiedJWT ( token )
if err != nil {
return nil , err
}
// check required claims
id , _ := unverifiedClaims [ "id" ] . ( string )
if id == "" {
return nil , errors . New ( "Missing or invalid token claims." )
}
user , err := dao . FindUserById ( id )
if err != nil || user == nil {
return nil , err
}
verificationKey := user . TokenKey + baseTokenKey
// verify token signature
if _ , err := security . ParseJWT ( token , verificationKey ) ; err != nil {
return nil , err
}
return user , nil
}
// IsUserEmailUnique checks if the provided email address is not
// already in use by other users.
func ( dao * Dao ) IsUserEmailUnique ( email string , excludeId string ) bool {
if email == "" {
return false
}
var exists bool
err := dao . UserQuery ( ) .
Select ( "count(*)" ) .
AndWhere ( dbx . Not ( dbx . HashExp { "id" : excludeId } ) ) .
AndWhere ( dbx . HashExp { "email" : email } ) .
Limit ( 1 ) .
Row ( & exists )
return err == nil && ! exists
}
// DeleteUser deletes the provided User model.
//
// This method will also cascade the delete operation to all
// Record models that references the provided User model
// (delete or set to NULL, depending on the related user shema field settings).
//
// The delete operation may fail if the user is part of a required
// reference in another Record model (aka. cannot be deleted or set to NULL).
func ( dao * Dao ) DeleteUser ( user * models . User ) error {
// fetch related records
// note: the select is outside of the transaction to prevent SQLITE_LOCKED error when mixing read&write in a single transaction
relatedRecords , err := dao . FindUserRelatedRecords ( user )
if err != nil {
return err
}
return dao . RunInTransaction ( func ( txDao * Dao ) error {
// check if related records has to be deleted (if `CascadeDelete` is set)
// OR
// just unset the user related fields (if they are not required)
// -----------------------------------------------------------
recordsLoop :
for _ , record := range relatedRecords {
var needSave bool
for _ , field := range record . Collection ( ) . Schema . Fields ( ) {
if field . Type != schema . FieldTypeUser {
continue // not a user field
}
ids := record . GetStringSliceDataValue ( field . Name )
// unset the user id
for i := len ( ids ) - 1 ; i >= 0 ; i -- {
if ids [ i ] == user . Id {
ids = append ( ids [ : i ] , ids [ i + 1 : ] ... )
break
}
}
options , _ := field . Options . ( * schema . UserOptions )
// cascade delete
// (only if there are no other user references in case of multiple select)
if options . CascadeDelete && len ( ids ) == 0 {
if err := txDao . DeleteRecord ( record ) ; err != nil {
return err
}
// no need to further iterate the user fields (the record is deleted)
continue recordsLoop
}
if field . Required && len ( ids ) == 0 {
return fmt . Errorf ( "Failed delete the user because a record exist with required user reference to the current model (%q, %q)." , record . Id , record . Collection ( ) . Name )
}
// apply the reference changes
record . SetDataValue ( field . Name , field . PrepareValue ( ids ) )
needSave = true
}
if needSave {
if err := txDao . SaveRecord ( record ) ; err != nil {
return err
}
}
}
// -----------------------------------------------------------
return txDao . Delete ( user )
} )
}
// SaveUser upserts the provided User model.
//
// An empty profile record will be created if the user
// doesn't have a profile record set yet.
func ( dao * Dao ) SaveUser ( user * models . User ) error {
profileCollection , err := dao . FindCollectionByNameOrId ( models . ProfileCollectionName )
if err != nil {
return err
}
// fetch the related user profile record (if exist)
var userProfile * models . Record
if user . HasId ( ) {
userProfile , _ = dao . FindFirstRecordByData (
profileCollection ,
models . ProfileCollectionUserFieldName ,
user . Id ,
)
}
return dao . RunInTransaction ( func ( txDao * Dao ) error {
if err := txDao . Save ( user ) ; err != nil {
return err
}
// create default/empty profile record if doesn't exist
if userProfile == nil {
userProfile = models . NewRecord ( profileCollection )
userProfile . SetDataValue ( models . ProfileCollectionUserFieldName , user . Id )
if err := txDao . Save ( userProfile ) ; err != nil {
return err
}
user . Profile = userProfile
}
return nil
} )
}