initial v0.8 pre-release
This commit is contained in:
+57
-3
@@ -1,13 +1,67 @@
|
||||
package models
|
||||
|
||||
var _ Model = (*Admin)(nil)
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Model = (*Admin)(nil)
|
||||
)
|
||||
|
||||
type Admin struct {
|
||||
BaseAccount
|
||||
BaseModel
|
||||
|
||||
Avatar int `db:"avatar" json:"avatar"`
|
||||
Avatar int `db:"avatar" json:"avatar"`
|
||||
Email string `db:"email" json:"email"`
|
||||
TokenKey string `db:"tokenKey" json:"-"`
|
||||
PasswordHash string `db:"passwordHash" json:"-"`
|
||||
LastResetSentAt types.DateTime `db:"lastResetSentAt" json:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the Admin model SQL table name.
|
||||
func (m *Admin) TableName() string {
|
||||
return "_admins"
|
||||
}
|
||||
|
||||
// ValidatePassword validates a plain password against the model's password.
|
||||
func (m *Admin) ValidatePassword(password string) bool {
|
||||
bytePassword := []byte(password)
|
||||
bytePasswordHash := []byte(m.PasswordHash)
|
||||
|
||||
// comparing the password with the hash
|
||||
err := bcrypt.CompareHashAndPassword(bytePasswordHash, bytePassword)
|
||||
|
||||
// nil means it is a match
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// SetPassword sets cryptographically secure string to `model.Password`.
|
||||
//
|
||||
// Additionally this method also resets the LastResetSentAt and the TokenKey fields.
|
||||
func (m *Admin) SetPassword(password string) error {
|
||||
if password == "" {
|
||||
return errors.New("The provided plain password is empty")
|
||||
}
|
||||
|
||||
// hash the password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 13)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.PasswordHash = string(hashedPassword)
|
||||
m.LastResetSentAt = types.DateTime{} // reset
|
||||
|
||||
// invalidate previously issued tokens
|
||||
return m.RefreshTokenKey()
|
||||
}
|
||||
|
||||
// RefreshTokenKey generates and sets new random token key.
|
||||
func (m *Admin) RefreshTokenKey() error {
|
||||
m.TokenKey = security.RandomString(50)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func TestAdminTableName(t *testing.T) {
|
||||
@@ -12,3 +13,92 @@ func TestAdminTableName(t *testing.T) {
|
||||
t.Fatalf("Unexpected table name, got %q", m.TableName())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminValidatePassword(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
admin models.Admin
|
||||
password string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
// empty passwordHash + empty pass
|
||||
models.Admin{},
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
// empty passwordHash + nonempty pass
|
||||
models.Admin{},
|
||||
"123456",
|
||||
false,
|
||||
},
|
||||
{
|
||||
// nonempty passwordHash + empty pass
|
||||
models.Admin{PasswordHash: "$2a$10$SKk/Y/Yc925PBtsSYBvq3Ous9Jy18m4KTn6b/PQQ.Y9QVjy3o/Fv."},
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
// nonempty passwordHash + wrong pass
|
||||
models.Admin{PasswordHash: "$2a$10$SKk/Y/Yc925PBtsSYBvq3Ous9Jy18m4KTn6b/PQQ.Y9QVjy3o/Fv."},
|
||||
"654321",
|
||||
false,
|
||||
},
|
||||
{
|
||||
// nonempty passwordHash + correct pass
|
||||
models.Admin{PasswordHash: "$2a$10$SKk/Y/Yc925PBtsSYBvq3Ous9Jy18m4KTn6b/PQQ.Y9QVjy3o/Fv."},
|
||||
"123456",
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
result := s.admin.ValidatePassword(s.password)
|
||||
if result != s.expected {
|
||||
t.Errorf("(%d) Expected %v, got %v", i, s.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminSetPassword(t *testing.T) {
|
||||
m := models.Admin{
|
||||
// 123456
|
||||
PasswordHash: "$2a$10$SKk/Y/Yc925PBtsSYBvq3Ous9Jy18m4KTn6b/PQQ.Y9QVjy3o/Fv.",
|
||||
LastResetSentAt: types.NowDateTime(),
|
||||
TokenKey: "test",
|
||||
}
|
||||
|
||||
// empty pass
|
||||
err1 := m.SetPassword("")
|
||||
if err1 == nil {
|
||||
t.Fatal("Expected empty password error")
|
||||
}
|
||||
|
||||
err2 := m.SetPassword("654321")
|
||||
if err2 != nil {
|
||||
t.Fatalf("Expected nil, got error %v", err2)
|
||||
}
|
||||
|
||||
if !m.ValidatePassword("654321") {
|
||||
t.Fatalf("Password is invalid")
|
||||
}
|
||||
|
||||
if m.TokenKey == "test" {
|
||||
t.Fatalf("Expected TokenKey to change, got %v", m.TokenKey)
|
||||
}
|
||||
|
||||
if !m.LastResetSentAt.IsZero() {
|
||||
t.Fatalf("Expected LastResetSentAt to be zero datetime, got %v", m.LastResetSentAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminRefreshTokenKey(t *testing.T) {
|
||||
m := models.Admin{TokenKey: "test"}
|
||||
|
||||
m.RefreshTokenKey()
|
||||
|
||||
// empty pass
|
||||
if m.TokenKey == "" || m.TokenKey == "test" {
|
||||
t.Fatalf("Expected TokenKey to change, got %q", m.TokenKey)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-57
@@ -2,11 +2,8 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -103,6 +100,9 @@ func (m *BaseModel) GetUpdated() types.DateTime {
|
||||
//
|
||||
// The generated id is a cryptographically random 15 characters length string.
|
||||
func (m *BaseModel) RefreshId() {
|
||||
if m.Id == "" { // no previous id
|
||||
m.MarkAsNew()
|
||||
}
|
||||
m.Id = security.RandomStringWithAlphabet(DefaultIdLength, DefaultIdAlphabet)
|
||||
}
|
||||
|
||||
@@ -115,57 +115,3 @@ func (m *BaseModel) RefreshCreated() {
|
||||
func (m *BaseModel) RefreshUpdated() {
|
||||
m.Updated = types.NowDateTime()
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// BaseAccount
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// BaseAccount defines common fields and methods used by auth models (aka. users and admins).
|
||||
type BaseAccount struct {
|
||||
BaseModel
|
||||
|
||||
Email string `db:"email" json:"email"`
|
||||
TokenKey string `db:"tokenKey" json:"-"`
|
||||
PasswordHash string `db:"passwordHash" json:"-"`
|
||||
LastResetSentAt types.DateTime `db:"lastResetSentAt" json:"lastResetSentAt"`
|
||||
}
|
||||
|
||||
// ValidatePassword validates a plain password against the model's password.
|
||||
func (m *BaseAccount) ValidatePassword(password string) bool {
|
||||
bytePassword := []byte(password)
|
||||
bytePasswordHash := []byte(m.PasswordHash)
|
||||
|
||||
// comparing the password with the hash
|
||||
err := bcrypt.CompareHashAndPassword(bytePasswordHash, bytePassword)
|
||||
|
||||
// nil means it is a match
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// SetPassword sets cryptographically secure string to `model.Password`.
|
||||
//
|
||||
// Additionally this method also resets the LastResetSentAt and the TokenKey fields.
|
||||
func (m *BaseAccount) SetPassword(password string) error {
|
||||
if password == "" {
|
||||
return errors.New("The provided plain password is empty")
|
||||
}
|
||||
|
||||
// hash the password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 13)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.PasswordHash = string(hashedPassword)
|
||||
m.LastResetSentAt = types.DateTime{} // reset
|
||||
|
||||
// invalidate previously issued tokens
|
||||
m.RefreshTokenKey()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshTokenKey generates and sets new random token key.
|
||||
func (m *BaseAccount) RefreshTokenKey() {
|
||||
m.TokenKey = security.RandomString(50)
|
||||
}
|
||||
|
||||
+4
-94
@@ -4,7 +4,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func TestBaseModelHasId(t *testing.T) {
|
||||
@@ -65,6 +64,9 @@ func TestBaseModelIsNew(t *testing.T) {
|
||||
m5 := models.BaseModel{Id: "test"}
|
||||
m5.MarkAsNew()
|
||||
m5.UnmarkAsNew()
|
||||
// check if MarkAsNew will be called on initial RefreshId()
|
||||
m6 := models.BaseModel{}
|
||||
m6.RefreshId()
|
||||
|
||||
scenarios := []struct {
|
||||
model models.BaseModel
|
||||
@@ -76,6 +78,7 @@ func TestBaseModelIsNew(t *testing.T) {
|
||||
{m3, true},
|
||||
{m4, true},
|
||||
{m5, false},
|
||||
{m6, true},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
@@ -113,96 +116,3 @@ func TestBaseModelUpdated(t *testing.T) {
|
||||
t.Fatalf("Expected non-zero datetime, got %v", m.GetUpdated())
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// BaseAccount tests
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
func TestBaseAccountValidatePassword(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
account models.BaseAccount
|
||||
password string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
// empty passwordHash + empty pass
|
||||
models.BaseAccount{},
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
// empty passwordHash + nonempty pass
|
||||
models.BaseAccount{},
|
||||
"123456",
|
||||
false,
|
||||
},
|
||||
{
|
||||
// nonempty passwordHash + empty pass
|
||||
models.BaseAccount{PasswordHash: "$2a$10$SKk/Y/Yc925PBtsSYBvq3Ous9Jy18m4KTn6b/PQQ.Y9QVjy3o/Fv."},
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
// nonempty passwordHash + wrong pass
|
||||
models.BaseAccount{PasswordHash: "$2a$10$SKk/Y/Yc925PBtsSYBvq3Ous9Jy18m4KTn6b/PQQ.Y9QVjy3o/Fv."},
|
||||
"654321",
|
||||
false,
|
||||
},
|
||||
{
|
||||
// nonempty passwordHash + correct pass
|
||||
models.BaseAccount{PasswordHash: "$2a$10$SKk/Y/Yc925PBtsSYBvq3Ous9Jy18m4KTn6b/PQQ.Y9QVjy3o/Fv."},
|
||||
"123456",
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
result := s.account.ValidatePassword(s.password)
|
||||
if result != s.expected {
|
||||
t.Errorf("(%d) Expected %v, got %v", i, s.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseAccountSetPassword(t *testing.T) {
|
||||
m := models.BaseAccount{
|
||||
// 123456
|
||||
PasswordHash: "$2a$10$SKk/Y/Yc925PBtsSYBvq3Ous9Jy18m4KTn6b/PQQ.Y9QVjy3o/Fv.",
|
||||
LastResetSentAt: types.NowDateTime(),
|
||||
TokenKey: "test",
|
||||
}
|
||||
|
||||
// empty pass
|
||||
err1 := m.SetPassword("")
|
||||
if err1 == nil {
|
||||
t.Fatal("Expected empty password error")
|
||||
}
|
||||
|
||||
err2 := m.SetPassword("654321")
|
||||
if err2 != nil {
|
||||
t.Fatalf("Expected nil, got error %v", err2)
|
||||
}
|
||||
|
||||
if !m.ValidatePassword("654321") {
|
||||
t.Fatalf("Password is invalid")
|
||||
}
|
||||
|
||||
if m.TokenKey == "test" {
|
||||
t.Fatalf("Expected TokenKey to change, got %v", m.TokenKey)
|
||||
}
|
||||
|
||||
if !m.LastResetSentAt.IsZero() {
|
||||
t.Fatalf("Expected LastResetSentAt to be zero datetime, got %v", m.LastResetSentAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseAccountRefreshTokenKey(t *testing.T) {
|
||||
m := models.BaseAccount{TokenKey: "test"}
|
||||
|
||||
m.RefreshTokenKey()
|
||||
|
||||
// empty pass
|
||||
if m.TokenKey == "" || m.TokenKey == "test" {
|
||||
t.Fatalf("Expected TokenKey to change, got %q", m.TokenKey)
|
||||
}
|
||||
}
|
||||
|
||||
+169
-11
@@ -1,23 +1,43 @@
|
||||
package models
|
||||
|
||||
import "github.com/pocketbase/pocketbase/models/schema"
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
var _ Model = (*Collection)(nil)
|
||||
var _ FilesManager = (*Collection)(nil)
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/go-ozzo/ozzo-validation/v4/is"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Model = (*Collection)(nil)
|
||||
_ FilesManager = (*Collection)(nil)
|
||||
)
|
||||
|
||||
const (
|
||||
CollectionTypeBase = "base"
|
||||
CollectionTypeAuth = "auth"
|
||||
)
|
||||
|
||||
type Collection struct {
|
||||
BaseModel
|
||||
|
||||
Name string `db:"name" json:"name"`
|
||||
System bool `db:"system" json:"system"`
|
||||
Schema schema.Schema `db:"schema" json:"schema"`
|
||||
ListRule *string `db:"listRule" json:"listRule"`
|
||||
ViewRule *string `db:"viewRule" json:"viewRule"`
|
||||
CreateRule *string `db:"createRule" json:"createRule"`
|
||||
UpdateRule *string `db:"updateRule" json:"updateRule"`
|
||||
DeleteRule *string `db:"deleteRule" json:"deleteRule"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Type string `db:"type" json:"type"`
|
||||
System bool `db:"system" json:"system"`
|
||||
Schema schema.Schema `db:"schema" json:"schema"`
|
||||
|
||||
// rules
|
||||
ListRule *string `db:"listRule" json:"listRule"`
|
||||
ViewRule *string `db:"viewRule" json:"viewRule"`
|
||||
CreateRule *string `db:"createRule" json:"createRule"`
|
||||
UpdateRule *string `db:"updateRule" json:"updateRule"`
|
||||
DeleteRule *string `db:"deleteRule" json:"deleteRule"`
|
||||
|
||||
Options types.JsonMap `db:"options" json:"options"`
|
||||
}
|
||||
|
||||
// TableName returns the Collection model SQL table name.
|
||||
func (m *Collection) TableName() string {
|
||||
return "_collections"
|
||||
}
|
||||
@@ -26,3 +46,141 @@ func (m *Collection) TableName() string {
|
||||
func (m *Collection) BaseFilesPath() string {
|
||||
return m.Id
|
||||
}
|
||||
|
||||
// IsBase checks if the current collection has "base" type.
|
||||
func (m *Collection) IsBase() bool {
|
||||
return m.Type == CollectionTypeBase
|
||||
}
|
||||
|
||||
// IsBase checks if the current collection has "auth" type.
|
||||
func (m *Collection) IsAuth() bool {
|
||||
return m.Type == CollectionTypeAuth
|
||||
}
|
||||
|
||||
// MarshalJSON implements the [json.Marshaler] interface.
|
||||
func (m Collection) MarshalJSON() ([]byte, error) {
|
||||
type alias Collection // prevent recursion
|
||||
|
||||
m.NormalizeOptions()
|
||||
|
||||
return json.Marshal(alias(m))
|
||||
}
|
||||
|
||||
// BaseOptions decodes the current collection options and returns them
|
||||
// as new [CollectionBaseOptions] instance.
|
||||
func (m *Collection) BaseOptions() CollectionBaseOptions {
|
||||
result := CollectionBaseOptions{}
|
||||
m.DecodeOptions(&result)
|
||||
return result
|
||||
}
|
||||
|
||||
// AuthOptions decodes the current collection options and returns them
|
||||
// as new [CollectionAuthOptions] instance.
|
||||
func (m *Collection) AuthOptions() CollectionAuthOptions {
|
||||
result := CollectionAuthOptions{}
|
||||
m.DecodeOptions(&result)
|
||||
return result
|
||||
}
|
||||
|
||||
// NormalizeOptions updates the current collection options with a
|
||||
// new normalized state based on the collection type.
|
||||
func (m *Collection) NormalizeOptions() error {
|
||||
var typedOptions any
|
||||
switch m.Type {
|
||||
case CollectionTypeAuth:
|
||||
typedOptions = m.AuthOptions()
|
||||
default:
|
||||
typedOptions = m.BaseOptions()
|
||||
}
|
||||
|
||||
// serialize
|
||||
raw, err := json.Marshal(typedOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// load into a new JsonMap
|
||||
m.Options = types.JsonMap{}
|
||||
if err := json.Unmarshal(raw, &m.Options); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecodeOptions decodes the current collection options into the
|
||||
// provided "result" (must be a pointer).
|
||||
func (m *Collection) DecodeOptions(result any) error {
|
||||
// raw serialize
|
||||
raw, err := json.Marshal(m.Options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// decode into the provided result
|
||||
if err := json.Unmarshal(raw, result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetOptions normalizes and unmarshals the specified options into m.Options.
|
||||
func (m *Collection) SetOptions(typedOptions any) error {
|
||||
// serialize
|
||||
raw, err := json.Marshal(typedOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Options = types.JsonMap{}
|
||||
if err := json.Unmarshal(raw, &m.Options); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.NormalizeOptions()
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// CollectionAuthOptions defines the "base" Collection.Options fields.
|
||||
type CollectionBaseOptions struct {
|
||||
}
|
||||
|
||||
// Validate implements [validation.Validatable] interface.
|
||||
func (o CollectionBaseOptions) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CollectionAuthOptions defines the "auth" Collection.Options fields.
|
||||
type CollectionAuthOptions struct {
|
||||
ManageRule *string `form:"manageRule" json:"manageRule"`
|
||||
AllowOAuth2Auth bool `form:"allowOAuth2Auth" json:"allowOAuth2Auth"`
|
||||
AllowUsernameAuth bool `form:"allowUsernameAuth" json:"allowUsernameAuth"`
|
||||
AllowEmailAuth bool `form:"allowEmailAuth" json:"allowEmailAuth"`
|
||||
RequireEmail bool `form:"requireEmail" json:"requireEmail"`
|
||||
ExceptEmailDomains []string `form:"exceptEmailDomains" json:"exceptEmailDomains"`
|
||||
OnlyEmailDomains []string `form:"onlyEmailDomains" json:"onlyEmailDomains"`
|
||||
MinPasswordLength int `form:"minPasswordLength" json:"minPasswordLength"`
|
||||
}
|
||||
|
||||
// Validate implements [validation.Validatable] interface.
|
||||
func (o CollectionAuthOptions) Validate() error {
|
||||
return validation.ValidateStruct(&o,
|
||||
validation.Field(&o.ManageRule, validation.NilOrNotEmpty),
|
||||
validation.Field(
|
||||
&o.ExceptEmailDomains,
|
||||
validation.When(len(o.OnlyEmailDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)),
|
||||
),
|
||||
validation.Field(
|
||||
&o.OnlyEmailDomains,
|
||||
validation.When(len(o.ExceptEmailDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)),
|
||||
),
|
||||
validation.Field(
|
||||
&o.MinPasswordLength,
|
||||
validation.When(o.AllowUsernameAuth || o.AllowEmailAuth, validation.Required),
|
||||
validation.Min(5),
|
||||
validation.Max(72),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package models_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func TestCollectionTableName(t *testing.T) {
|
||||
@@ -23,3 +27,370 @@ func TestCollectionBaseFilesPath(t *testing.T) {
|
||||
t.Fatalf("Expected path %s, got %s", expected, m.BaseFilesPath())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionIsBase(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
collection models.Collection
|
||||
expected bool
|
||||
}{
|
||||
{models.Collection{}, false},
|
||||
{models.Collection{Type: "unknown"}, false},
|
||||
{models.Collection{Type: models.CollectionTypeBase}, true},
|
||||
{models.Collection{Type: models.CollectionTypeAuth}, false},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
result := s.collection.IsBase()
|
||||
if result != s.expected {
|
||||
t.Errorf("(%d) Expected %v, got %v", i, s.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionIsAuth(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
collection models.Collection
|
||||
expected bool
|
||||
}{
|
||||
{models.Collection{}, false},
|
||||
{models.Collection{Type: "unknown"}, false},
|
||||
{models.Collection{Type: models.CollectionTypeBase}, false},
|
||||
{models.Collection{Type: models.CollectionTypeAuth}, true},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
result := s.collection.IsAuth()
|
||||
if result != s.expected {
|
||||
t.Errorf("(%d) Expected %v, got %v", i, s.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionMarshalJSON(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
collection models.Collection
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"no type",
|
||||
models.Collection{Name: "test"},
|
||||
`{"id":"","created":"","updated":"","name":"test","type":"","system":false,"schema":[],"listRule":null,"viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`,
|
||||
},
|
||||
{
|
||||
"unknown type + non empty options",
|
||||
models.Collection{Name: "test", Type: "unknown", ListRule: types.Pointer("test_list"), Options: types.JsonMap{"test": 123}},
|
||||
`{"id":"","created":"","updated":"","name":"test","type":"unknown","system":false,"schema":[],"listRule":"test_list","viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`,
|
||||
},
|
||||
{
|
||||
"base type + non empty options",
|
||||
models.Collection{Name: "test", Type: models.CollectionTypeBase, ListRule: types.Pointer("test_list"), Options: types.JsonMap{"test": 123}},
|
||||
`{"id":"","created":"","updated":"","name":"test","type":"base","system":false,"schema":[],"listRule":"test_list","viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{}}`,
|
||||
},
|
||||
{
|
||||
"auth type + non empty options",
|
||||
models.Collection{BaseModel: models.BaseModel{Id: "test"}, Type: models.CollectionTypeAuth, Options: types.JsonMap{"test": 123, "allowOAuth2Auth": true, "minPasswordLength": 4}},
|
||||
`{"id":"test","created":"","updated":"","name":"","type":"auth","system":false,"schema":[],"listRule":null,"viewRule":null,"createRule":null,"updateRule":null,"deleteRule":null,"options":{"allowEmailAuth":false,"allowOAuth2Auth":true,"allowUsernameAuth":false,"exceptEmailDomains":null,"manageRule":null,"minPasswordLength":4,"onlyEmailDomains":null,"requireEmail":false}}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
result, err := s.collection.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Errorf("(%s) Unexpected error %v", s.name, err)
|
||||
continue
|
||||
}
|
||||
if string(result) != s.expected {
|
||||
t.Errorf("(%s) Expected\n%v \ngot \n%v", s.name, s.expected, string(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionBaseOptions(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
collection models.Collection
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"no type",
|
||||
models.Collection{Options: types.JsonMap{"test": 123}},
|
||||
"{}",
|
||||
},
|
||||
{
|
||||
"unknown type",
|
||||
models.Collection{Type: "anything", Options: types.JsonMap{"test": 123}},
|
||||
"{}",
|
||||
},
|
||||
{
|
||||
"different type",
|
||||
models.Collection{Type: models.CollectionTypeAuth, Options: types.JsonMap{"test": 123, "minPasswordLength": 4}},
|
||||
"{}",
|
||||
},
|
||||
{
|
||||
"base type",
|
||||
models.Collection{Type: models.CollectionTypeBase, Options: types.JsonMap{"test": 123}},
|
||||
"{}",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
result := s.collection.BaseOptions()
|
||||
|
||||
encoded, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if strEncoded := string(encoded); strEncoded != s.expected {
|
||||
t.Errorf("(%s) Expected \n%v \ngot \n%v", s.name, s.expected, strEncoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionAuthOptions(t *testing.T) {
|
||||
options := types.JsonMap{"test": 123, "minPasswordLength": 4}
|
||||
expectedSerialization := `{"manageRule":null,"allowOAuth2Auth":false,"allowUsernameAuth":false,"allowEmailAuth":false,"requireEmail":false,"exceptEmailDomains":null,"onlyEmailDomains":null,"minPasswordLength":4}`
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
collection models.Collection
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"no type",
|
||||
models.Collection{Options: options},
|
||||
expectedSerialization,
|
||||
},
|
||||
{
|
||||
"unknown type",
|
||||
models.Collection{Type: "anything", Options: options},
|
||||
expectedSerialization,
|
||||
},
|
||||
{
|
||||
"different type",
|
||||
models.Collection{Type: models.CollectionTypeBase, Options: options},
|
||||
expectedSerialization,
|
||||
},
|
||||
{
|
||||
"auth type",
|
||||
models.Collection{Type: models.CollectionTypeAuth, Options: options},
|
||||
expectedSerialization,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
result := s.collection.AuthOptions()
|
||||
|
||||
encoded, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if strEncoded := string(encoded); strEncoded != s.expected {
|
||||
t.Errorf("(%s) Expected \n%v \ngot \n%v", s.name, s.expected, strEncoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeOptions(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
collection models.Collection
|
||||
expected string // serialized options
|
||||
}{
|
||||
{
|
||||
"unknown type",
|
||||
models.Collection{Type: "unknown", Options: types.JsonMap{"test": 123, "minPasswordLength": 4}},
|
||||
"{}",
|
||||
},
|
||||
{
|
||||
"base type",
|
||||
models.Collection{Type: models.CollectionTypeBase, Options: types.JsonMap{"test": 123, "minPasswordLength": 4}},
|
||||
"{}",
|
||||
},
|
||||
{
|
||||
"auth type",
|
||||
models.Collection{Type: models.CollectionTypeAuth, Options: types.JsonMap{"test": 123, "minPasswordLength": 4}},
|
||||
`{"allowEmailAuth":false,"allowOAuth2Auth":false,"allowUsernameAuth":false,"exceptEmailDomains":null,"manageRule":null,"minPasswordLength":4,"onlyEmailDomains":null,"requireEmail":false}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
if err := s.collection.NormalizeOptions(); err != nil {
|
||||
t.Errorf("(%s) Unexpected error %v", s.name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(s.collection.Options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if strEncoded := string(encoded); strEncoded != s.expected {
|
||||
t.Errorf("(%s) Expected \n%v \ngot \n%v", s.name, s.expected, strEncoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeOptions(t *testing.T) {
|
||||
m := models.Collection{
|
||||
Options: types.JsonMap{"test": 123},
|
||||
}
|
||||
|
||||
result := struct {
|
||||
Test int
|
||||
}{}
|
||||
|
||||
if err := m.DecodeOptions(&result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if result.Test != 123 {
|
||||
t.Fatalf("Expected %v, got %v", 123, result.Test)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetOptions(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
collection models.Collection
|
||||
options any
|
||||
expected string // serialized options
|
||||
}{
|
||||
{
|
||||
"no type",
|
||||
models.Collection{},
|
||||
map[string]any{},
|
||||
"{}",
|
||||
},
|
||||
{
|
||||
"unknown type + non empty options",
|
||||
models.Collection{Type: "unknown", Options: types.JsonMap{"test": 123}},
|
||||
map[string]any{"test": 456, "minPasswordLength": 4},
|
||||
"{}",
|
||||
},
|
||||
{
|
||||
"base type",
|
||||
models.Collection{Type: models.CollectionTypeBase, Options: types.JsonMap{"test": 123}},
|
||||
map[string]any{"test": 456, "minPasswordLength": 4},
|
||||
"{}",
|
||||
},
|
||||
{
|
||||
"auth type",
|
||||
models.Collection{Type: models.CollectionTypeAuth, Options: types.JsonMap{"test": 123}},
|
||||
map[string]any{"test": 456, "minPasswordLength": 4},
|
||||
`{"allowEmailAuth":false,"allowOAuth2Auth":false,"allowUsernameAuth":false,"exceptEmailDomains":null,"manageRule":null,"minPasswordLength":4,"onlyEmailDomains":null,"requireEmail":false}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
if err := s.collection.SetOptions(s.options); err != nil {
|
||||
t.Errorf("(%s) Unexpected error %v", s.name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(s.collection.Options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if strEncoded := string(encoded); strEncoded != s.expected {
|
||||
t.Errorf("(%s) Expected \n%v \ngot \n%v", s.name, s.expected, strEncoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionBaseOptionsValidate(t *testing.T) {
|
||||
opt := models.CollectionBaseOptions{}
|
||||
if err := opt.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionAuthOptionsValidate(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
options models.CollectionAuthOptions
|
||||
expectedErrors []string
|
||||
}{
|
||||
{
|
||||
"empty",
|
||||
models.CollectionAuthOptions{},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"empty string ManageRule",
|
||||
models.CollectionAuthOptions{ManageRule: types.Pointer("")},
|
||||
[]string{"manageRule"},
|
||||
},
|
||||
{
|
||||
"minPasswordLength < 5",
|
||||
models.CollectionAuthOptions{MinPasswordLength: 3},
|
||||
[]string{"minPasswordLength"},
|
||||
},
|
||||
{
|
||||
"minPasswordLength > 72",
|
||||
models.CollectionAuthOptions{MinPasswordLength: 73},
|
||||
[]string{"minPasswordLength"},
|
||||
},
|
||||
{
|
||||
"both OnlyDomains and ExceptDomains set",
|
||||
models.CollectionAuthOptions{
|
||||
OnlyEmailDomains: []string{"example.com", "test.com"},
|
||||
ExceptEmailDomains: []string{"example.com", "test.com"},
|
||||
},
|
||||
[]string{"onlyEmailDomains", "exceptEmailDomains"},
|
||||
},
|
||||
{
|
||||
"only OnlyDomains set",
|
||||
models.CollectionAuthOptions{
|
||||
OnlyEmailDomains: []string{"example.com", "test.com"},
|
||||
},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"only ExceptEmailDomains set",
|
||||
models.CollectionAuthOptions{
|
||||
ExceptEmailDomains: []string{"example.com", "test.com"},
|
||||
},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"all fields with valid data",
|
||||
models.CollectionAuthOptions{
|
||||
ManageRule: types.Pointer("test"),
|
||||
AllowOAuth2Auth: true,
|
||||
AllowUsernameAuth: true,
|
||||
AllowEmailAuth: true,
|
||||
RequireEmail: true,
|
||||
ExceptEmailDomains: []string{"example.com", "test.com"},
|
||||
OnlyEmailDomains: nil,
|
||||
MinPasswordLength: 5,
|
||||
},
|
||||
[]string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
result := s.options.Validate()
|
||||
|
||||
// parse errors
|
||||
errs, ok := result.(validation.Errors)
|
||||
if !ok && result != nil {
|
||||
t.Errorf("(%s) Failed to parse errors %v", s.name, result)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(errs) != len(s.expectedErrors) {
|
||||
t.Errorf("(%s) Expected error keys %v, got errors \n%v", s.name, s.expectedErrors, result)
|
||||
continue
|
||||
}
|
||||
|
||||
for key := range errs {
|
||||
if !list.ExistInSlice(key, s.expectedErrors) {
|
||||
t.Errorf("(%s) Unexpected error key %q in \n%v", s.name, key, errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@ var _ Model = (*ExternalAuth)(nil)
|
||||
type ExternalAuth struct {
|
||||
BaseModel
|
||||
|
||||
UserId string `db:"userId" json:"userId"`
|
||||
Provider string `db:"provider" json:"provider"`
|
||||
ProviderId string `db:"providerId" json:"providerId"`
|
||||
CollectionId string `db:"collectionId" json:"collectionId"`
|
||||
RecordId string `db:"recordId" json:"recordId"`
|
||||
Provider string `db:"provider" json:"provider"`
|
||||
ProviderId string `db:"providerId" json:"providerId"`
|
||||
}
|
||||
|
||||
func (m *ExternalAuth) TableName() string {
|
||||
|
||||
+441
-117
@@ -2,28 +2,34 @@ package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"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/security"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"github.com/spf13/cast"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var _ Model = (*Record)(nil)
|
||||
var _ ColumnValueMapper = (*Record)(nil)
|
||||
var _ FilesManager = (*Record)(nil)
|
||||
var (
|
||||
_ Model = (*Record)(nil)
|
||||
_ ColumnValueMapper = (*Record)(nil)
|
||||
_ FilesManager = (*Record)(nil)
|
||||
)
|
||||
|
||||
type Record struct {
|
||||
BaseModel
|
||||
|
||||
collection *Collection
|
||||
data map[string]any
|
||||
expand map[string]any
|
||||
|
||||
exportUnknown bool // whether to export unknown fields
|
||||
ignoreEmailVisibility bool // whether to ignore the emailVisibility flag for auth collections
|
||||
data map[string]any // any custom data in addition to the base model fields
|
||||
expand map[string]any // expanded relations
|
||||
}
|
||||
|
||||
// NewRecord initializes a new empty Record model.
|
||||
@@ -34,34 +40,43 @@ func NewRecord(collection *Collection) *Record {
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
func NewRecordFromNullStringMap(collection *Collection, data dbx.NullStringMap) *Record {
|
||||
resultMap := map[string]any{}
|
||||
|
||||
// load schema fields
|
||||
for _, field := range collection.Schema.Fields() {
|
||||
var rawValue any
|
||||
resultMap[field.Name] = nullStringMapValue(data, field.Name)
|
||||
}
|
||||
|
||||
nullString, ok := data[field.Name]
|
||||
if !ok || !nullString.Valid {
|
||||
rawValue = nil
|
||||
} else {
|
||||
rawValue = nullString.String
|
||||
// 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
record.Load(resultMap)
|
||||
|
||||
return record
|
||||
}
|
||||
@@ -88,77 +103,150 @@ func (m *Record) Collection() *Collection {
|
||||
return m.collection
|
||||
}
|
||||
|
||||
// GetExpand returns a shallow copy of the optional `expand` data
|
||||
// Expand returns a shallow copy of the record.expand data
|
||||
// attached to the current Record model.
|
||||
func (m *Record) GetExpand() map[string]any {
|
||||
func (m *Record) Expand() 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)
|
||||
// SetExpand assigns the provided data to record.expand.
|
||||
func (m *Record) SetExpand(expand map[string]any) {
|
||||
m.expand = shallowCopy(expand)
|
||||
}
|
||||
|
||||
// Data returns a shallow copy of the currently loaded record's data.
|
||||
func (m *Record) Data() map[string]any {
|
||||
return shallowCopy(m.data)
|
||||
}
|
||||
// SchemaData returns a shallow copy ONLY of the defined record schema fields data.
|
||||
func (m *Record) SchemaData() map[string]any {
|
||||
result := map[string]any{}
|
||||
|
||||
// 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{}
|
||||
for _, field := range m.collection.Schema.Fields() {
|
||||
if v, ok := m.data[field.Name]; ok {
|
||||
result[field.Name] = v
|
||||
}
|
||||
}
|
||||
|
||||
field := m.Collection().Schema.GetFieldByName(key)
|
||||
if field != nil {
|
||||
m.data[key] = field.PrepareValue(value)
|
||||
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 {
|
||||
return m.extractUnknownData(m.data)
|
||||
}
|
||||
|
||||
// IgnoreEmailVisibility toggles the flag to ignore the auth record email visibility check.
|
||||
func (m *Record) IgnoreEmailVisibility(state bool) {
|
||||
m.ignoreEmailVisibility = state
|
||||
}
|
||||
|
||||
// WithUnkownData toggles the export/serialization of unknown data fields
|
||||
// (false by default).
|
||||
func (m *Record) WithUnkownData(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 = map[string]any{}
|
||||
}
|
||||
|
||||
m.data[key] = v
|
||||
}
|
||||
}
|
||||
|
||||
// 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]
|
||||
// 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 v, ok := m.data[key]; ok {
|
||||
return v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetBoolDataValue returns the data value for `key` as a bool.
|
||||
func (m *Record) GetBoolDataValue(key string) bool {
|
||||
return cast.ToBool(m.GetDataValue(key))
|
||||
// GetBool returns the data value for "key" as a bool.
|
||||
func (m *Record) GetBool(key string) bool {
|
||||
return cast.ToBool(m.Get(key))
|
||||
}
|
||||
|
||||
// GetStringDataValue returns the data value for `key` as a string.
|
||||
func (m *Record) GetStringDataValue(key string) string {
|
||||
return cast.ToString(m.GetDataValue(key))
|
||||
// GetString returns the data value for "key" as a string.
|
||||
func (m *Record) GetString(key string) string {
|
||||
return cast.ToString(m.Get(key))
|
||||
}
|
||||
|
||||
// GetIntDataValue returns the data value for `key` as an int.
|
||||
func (m *Record) GetIntDataValue(key string) int {
|
||||
return cast.ToInt(m.GetDataValue(key))
|
||||
// GetInt returns the data value for "key" as an int.
|
||||
func (m *Record) GetInt(key string) int {
|
||||
return cast.ToInt(m.Get(key))
|
||||
}
|
||||
|
||||
// GetFloatDataValue returns the data value for `key` as a float64.
|
||||
func (m *Record) GetFloatDataValue(key string) float64 {
|
||||
return cast.ToFloat64(m.GetDataValue(key))
|
||||
// GetFloat returns the data value for "key" as a float64.
|
||||
func (m *Record) GetFloat(key string) float64 {
|
||||
return cast.ToFloat64(m.Get(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))
|
||||
// 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))
|
||||
}
|
||||
|
||||
// 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))
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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))
|
||||
// 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.
|
||||
@@ -171,7 +259,7 @@ func (m *Record) BaseFilesPath() string {
|
||||
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)
|
||||
names := m.GetStringSlice(field.Name)
|
||||
if list.ExistInSlice(filename, names) {
|
||||
return field
|
||||
}
|
||||
@@ -181,63 +269,76 @@ func (m *Record) FindFileFieldByFile(filename string) *schema.SchemaField {
|
||||
}
|
||||
|
||||
// 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])
|
||||
}
|
||||
|
||||
func (m *Record) Load(data map[string]any) {
|
||||
for k, v := range data {
|
||||
m.SetDataValue(k, v)
|
||||
m.Set(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)
|
||||
|
||||
// export schema field values
|
||||
for _, field := range m.collection.Schema.Fields() {
|
||||
result[field.Name] = m.getNormalizeDataValueForDB(field.Name)
|
||||
}
|
||||
|
||||
// set base model fields
|
||||
result[schema.ReservedFieldNameId] = m.Id
|
||||
result[schema.ReservedFieldNameCreated] = m.Created
|
||||
result[schema.ReservedFieldNameUpdated] = m.Updated
|
||||
// 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.
|
||||
//
|
||||
// This method also skips the "hidden" fields, aka. fields prefixed with `#`.
|
||||
// Fields marked as hidden will be exported only if `m.IgnoreEmailVisibility(true)` is set.
|
||||
func (m *Record) PublicExport() map[string]any {
|
||||
result := skipHiddenFields(m.data)
|
||||
result := map[string]any{}
|
||||
|
||||
// set base model fields
|
||||
result[schema.ReservedFieldNameId] = m.Id
|
||||
result[schema.ReservedFieldNameCreated] = m.Created
|
||||
result[schema.ReservedFieldNameUpdated] = m.Updated
|
||||
// export unknown data fields if allowed
|
||||
if m.exportUnknown {
|
||||
for k, v := range m.UnknownData() {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// add helper collection fields
|
||||
result["@collectionId"] = m.collection.Id
|
||||
result["@collectionName"] = m.collection.Name
|
||||
// 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()
|
||||
result[schema.FieldNameCreated] = m.GetCreated()
|
||||
result[schema.FieldNameUpdated] = m.GetUpdated()
|
||||
|
||||
// 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 {
|
||||
result["@expand"] = m.expand
|
||||
result[schema.FieldNameExpand] = m.expand
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -258,19 +359,41 @@ func (m *Record) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.Load(result)
|
||||
m.Load(result)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeDataValueForDB returns the `key` data value formatted for db storage.
|
||||
func (m *Record) normalizeDataValueForDB(key string) any {
|
||||
val := m.GetDataValue(key)
|
||||
// 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 strings slice
|
||||
// encode string slice
|
||||
return append(types.JsonArray{}, list.ToInterfaceSlice(ids)...)
|
||||
case []int:
|
||||
// encode int slice
|
||||
return append(types.JsonArray{}, list.ToInterfaceSlice(ids)...)
|
||||
case []float64:
|
||||
// encode float64 slice
|
||||
return append(types.JsonArray{}, list.ToInterfaceSlice(ids)...)
|
||||
case []any:
|
||||
// encode interfaces slice
|
||||
// encode interface slice
|
||||
return append(types.JsonArray{}, ids...)
|
||||
default:
|
||||
// no changes
|
||||
@@ -289,17 +412,218 @@ func shallowCopy(data map[string]any) map[string]any {
|
||||
return result
|
||||
}
|
||||
|
||||
// skipHiddenFields returns a new data map without the "#" prefixed fields.
|
||||
func skipHiddenFields(data map[string]any) map[string]any {
|
||||
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 key, val := range data {
|
||||
// ignore "#" prefixed fields
|
||||
if strings.HasPrefix(key, "#") {
|
||||
continue
|
||||
for k, v := range m.data {
|
||||
if _, ok := knownFields[k]; !ok {
|
||||
result[k] = v
|
||||
}
|
||||
result[key] = val
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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.GetString(schema.FieldNamePasswordHash)),
|
||||
[]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), 13)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Set(schema.FieldNamePasswordHash, string(hashedPassword))
|
||||
m.Set(schema.FieldNameLastResetSentAt, types.DateTime{})
|
||||
|
||||
// invalidate previously issued tokens
|
||||
return m.RefreshTokenKey()
|
||||
}
|
||||
|
||||
+1104
-284
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -6,9 +6,9 @@ var _ Model = (*Request)(nil)
|
||||
|
||||
// list with the supported values for `Request.Auth`
|
||||
const (
|
||||
RequestAuthGuest = "guest"
|
||||
RequestAuthUser = "user"
|
||||
RequestAuthAdmin = "admin"
|
||||
RequestAuthGuest = "guest"
|
||||
RequestAuthAdmin = "admin"
|
||||
RequestAuthRecord = "auth_record"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
|
||||
@@ -13,21 +13,55 @@ import (
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
var schemaFieldNameRegex = regexp.MustCompile(`^\#?\w+$`)
|
||||
var schemaFieldNameRegex = regexp.MustCompile(`^\w+$`)
|
||||
|
||||
// reserved internal field names
|
||||
// commonly used field names
|
||||
const (
|
||||
ReservedFieldNameId = "id"
|
||||
ReservedFieldNameCreated = "created"
|
||||
ReservedFieldNameUpdated = "updated"
|
||||
FieldNameId = "id"
|
||||
FieldNameCreated = "created"
|
||||
FieldNameUpdated = "updated"
|
||||
FieldNameCollectionId = "collectionId"
|
||||
FieldNameCollectionName = "collectionName"
|
||||
FieldNameExpand = "expand"
|
||||
FieldNameUsername = "username"
|
||||
FieldNameEmail = "email"
|
||||
FieldNameEmailVisibility = "emailVisibility"
|
||||
FieldNameVerified = "verified"
|
||||
FieldNameTokenKey = "tokenKey"
|
||||
FieldNamePasswordHash = "passwordHash"
|
||||
FieldNameLastResetSentAt = "lastResetSentAt"
|
||||
FieldNameLastVerificationSentAt = "lastVerificationSentAt"
|
||||
)
|
||||
|
||||
// ReservedFieldNames returns slice with reserved/system field names.
|
||||
func ReservedFieldNames() []string {
|
||||
// BaseModelFieldNames returns the field names that all models have (id, created, updated).
|
||||
func BaseModelFieldNames() []string {
|
||||
return []string{
|
||||
ReservedFieldNameId,
|
||||
ReservedFieldNameCreated,
|
||||
ReservedFieldNameUpdated,
|
||||
FieldNameId,
|
||||
FieldNameCreated,
|
||||
FieldNameUpdated,
|
||||
}
|
||||
}
|
||||
|
||||
// SystemFields returns special internal field names that are usually readonly.
|
||||
func SystemFieldNames() []string {
|
||||
return []string{
|
||||
FieldNameCollectionId,
|
||||
FieldNameCollectionName,
|
||||
FieldNameExpand,
|
||||
}
|
||||
}
|
||||
|
||||
// AuthFieldNames returns the reserved "auth" collection auth field names.
|
||||
func AuthFieldNames() []string {
|
||||
return []string{
|
||||
FieldNameUsername,
|
||||
FieldNameEmail,
|
||||
FieldNameEmailVisibility,
|
||||
FieldNameVerified,
|
||||
FieldNameTokenKey,
|
||||
FieldNamePasswordHash,
|
||||
FieldNameLastResetSentAt,
|
||||
FieldNameLastVerificationSentAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +77,9 @@ const (
|
||||
FieldTypeJson string = "json"
|
||||
FieldTypeFile string = "file"
|
||||
FieldTypeRelation string = "relation"
|
||||
FieldTypeUser string = "user"
|
||||
|
||||
// Deprecated: Will be removed in v0.9!
|
||||
FieldTypeUser string = "user"
|
||||
)
|
||||
|
||||
// FieldTypes returns slice with all supported field types.
|
||||
@@ -59,7 +95,6 @@ func FieldTypes() []string {
|
||||
FieldTypeJson,
|
||||
FieldTypeFile,
|
||||
FieldTypeRelation,
|
||||
FieldTypeUser,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +104,6 @@ func ArraybleFieldTypes() []string {
|
||||
FieldTypeSelect,
|
||||
FieldTypeFile,
|
||||
FieldTypeRelation,
|
||||
FieldTypeUser,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +124,7 @@ func (f *SchemaField) ColDefinition() string {
|
||||
case FieldTypeNumber:
|
||||
return "REAL DEFAULT 0"
|
||||
case FieldTypeBool:
|
||||
return "Boolean DEFAULT FALSE"
|
||||
return "BOOLEAN DEFAULT FALSE"
|
||||
case FieldTypeJson:
|
||||
return "JSON DEFAULT NULL"
|
||||
default:
|
||||
@@ -133,9 +167,11 @@ func (f SchemaField) Validate() error {
|
||||
// init field options (if not already)
|
||||
f.InitOptions()
|
||||
|
||||
// add commonly used filter literals to the exclude names list
|
||||
excludeNames := ReservedFieldNames()
|
||||
excludeNames := BaseModelFieldNames()
|
||||
// exclude filter literals
|
||||
excludeNames = append(excludeNames, "null", "true", "false")
|
||||
// exclude system literals
|
||||
excludeNames = append(excludeNames, SystemFieldNames()...)
|
||||
|
||||
return validation.ValidateStruct(&f,
|
||||
validation.Field(&f.Options, validation.Required, validation.By(f.checkOptions)),
|
||||
@@ -198,8 +234,11 @@ func (f *SchemaField) InitOptions() error {
|
||||
options = &FileOptions{}
|
||||
case FieldTypeRelation:
|
||||
options = &RelationOptions{}
|
||||
|
||||
// Deprecated: Will be removed in v0.9!
|
||||
case FieldTypeUser:
|
||||
options = &UserOptions{}
|
||||
|
||||
default:
|
||||
return errors.New("Missing or unknown field field type.")
|
||||
}
|
||||
@@ -259,19 +298,7 @@ func (f *SchemaField) PrepareValue(value any) any {
|
||||
ids := list.ToUniqueStringSlice(value)
|
||||
|
||||
options, _ := f.Options.(*RelationOptions)
|
||||
if options.MaxSelect <= 1 {
|
||||
if len(ids) > 0 {
|
||||
return ids[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
return ids
|
||||
case FieldTypeUser:
|
||||
ids := list.ToUniqueStringSlice(value)
|
||||
|
||||
options, _ := f.Options.(*UserOptions)
|
||||
if options.MaxSelect <= 1 {
|
||||
if options.MaxSelect != nil && *options.MaxSelect <= 1 {
|
||||
if len(ids) > 0 {
|
||||
return ids[0]
|
||||
}
|
||||
@@ -426,13 +453,18 @@ type SelectOptions struct {
|
||||
}
|
||||
|
||||
func (o SelectOptions) Validate() error {
|
||||
max := len(o.Values)
|
||||
if max == 0 {
|
||||
max = 1
|
||||
}
|
||||
|
||||
return validation.ValidateStruct(&o,
|
||||
validation.Field(&o.Values, validation.Required),
|
||||
validation.Field(
|
||||
&o.MaxSelect,
|
||||
validation.Required,
|
||||
validation.Min(1),
|
||||
validation.Max(len(o.Values)),
|
||||
validation.Max(max),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -469,27 +501,27 @@ func (o FileOptions) Validate() error {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type RelationOptions struct {
|
||||
MaxSelect int `form:"maxSelect" json:"maxSelect"`
|
||||
MaxSelect *int `form:"maxSelect" json:"maxSelect"`
|
||||
CollectionId string `form:"collectionId" json:"collectionId"`
|
||||
CascadeDelete bool `form:"cascadeDelete" json:"cascadeDelete"`
|
||||
}
|
||||
|
||||
func (o RelationOptions) Validate() error {
|
||||
return validation.ValidateStruct(&o,
|
||||
validation.Field(&o.MaxSelect, validation.Required, validation.Min(1)),
|
||||
validation.Field(&o.CollectionId, validation.Required),
|
||||
validation.Field(&o.MaxSelect, validation.NilOrNotEmpty, validation.Min(1)),
|
||||
)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Deprecated: Will be removed in v0.9!
|
||||
type UserOptions struct {
|
||||
MaxSelect int `form:"maxSelect" json:"maxSelect"`
|
||||
CascadeDelete bool `form:"cascadeDelete" json:"cascadeDelete"`
|
||||
}
|
||||
|
||||
// Deprecated: Will be removed in v0.9!
|
||||
func (o UserOptions) Validate() error {
|
||||
return validation.ValidateStruct(&o,
|
||||
validation.Field(&o.MaxSelect, validation.Required, validation.Min(1)),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
+116
-132
@@ -11,27 +11,48 @@ import (
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func TestReservedFieldNames(t *testing.T) {
|
||||
result := schema.ReservedFieldNames()
|
||||
func TestBaseModelFieldNames(t *testing.T) {
|
||||
result := schema.BaseModelFieldNames()
|
||||
expected := 3
|
||||
|
||||
if len(result) != 3 {
|
||||
t.Fatalf("Expected %d names, got %d (%v)", 3, len(result), result)
|
||||
if len(result) != expected {
|
||||
t.Fatalf("Expected %d field names, got %d (%v)", expected, len(result), result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemFieldNames(t *testing.T) {
|
||||
result := schema.SystemFieldNames()
|
||||
expected := 3
|
||||
|
||||
if len(result) != expected {
|
||||
t.Fatalf("Expected %d field names, got %d (%v)", expected, len(result), result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthFieldNames(t *testing.T) {
|
||||
result := schema.AuthFieldNames()
|
||||
expected := 8
|
||||
|
||||
if len(result) != expected {
|
||||
t.Fatalf("Expected %d auth field names, got %d (%v)", expected, len(result), result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldTypes(t *testing.T) {
|
||||
result := schema.FieldTypes()
|
||||
expected := 10
|
||||
|
||||
if len(result) != 11 {
|
||||
t.Fatalf("Expected %d types, got %d (%v)", 3, len(result), result)
|
||||
if len(result) != expected {
|
||||
t.Fatalf("Expected %d types, got %d (%v)", expected, len(result), result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArraybleFieldTypes(t *testing.T) {
|
||||
result := schema.ArraybleFieldTypes()
|
||||
expected := 3
|
||||
|
||||
if len(result) != 4 {
|
||||
t.Fatalf("Expected %d types, got %d (%v)", 3, len(result), result)
|
||||
if len(result) != expected {
|
||||
t.Fatalf("Expected %d arrayble types, got %d (%v)", expected, len(result), result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +71,7 @@ func TestSchemaFieldColDefinition(t *testing.T) {
|
||||
},
|
||||
{
|
||||
schema.SchemaField{Type: schema.FieldTypeBool, Name: "test"},
|
||||
"Boolean DEFAULT FALSE",
|
||||
"BOOLEAN DEFAULT FALSE",
|
||||
},
|
||||
{
|
||||
schema.SchemaField{Type: schema.FieldTypeEmail, Name: "test"},
|
||||
@@ -80,10 +101,6 @@ func TestSchemaFieldColDefinition(t *testing.T) {
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation, Name: "test"},
|
||||
"TEXT DEFAULT ''",
|
||||
},
|
||||
{
|
||||
schema.SchemaField{Type: schema.FieldTypeUser, Name: "test"},
|
||||
"TEXT DEFAULT ''",
|
||||
},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
@@ -297,7 +314,7 @@ func TestSchemaFieldValidate(t *testing.T) {
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeText,
|
||||
Id: "1234567890",
|
||||
Name: schema.ReservedFieldNameId,
|
||||
Name: schema.FieldNameId,
|
||||
},
|
||||
[]string{"name"},
|
||||
},
|
||||
@@ -306,7 +323,7 @@ func TestSchemaFieldValidate(t *testing.T) {
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeText,
|
||||
Id: "1234567890",
|
||||
Name: schema.ReservedFieldNameCreated,
|
||||
Name: schema.FieldNameCreated,
|
||||
},
|
||||
[]string{"name"},
|
||||
},
|
||||
@@ -315,7 +332,34 @@ func TestSchemaFieldValidate(t *testing.T) {
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeText,
|
||||
Id: "1234567890",
|
||||
Name: schema.ReservedFieldNameUpdated,
|
||||
Name: schema.FieldNameUpdated,
|
||||
},
|
||||
[]string{"name"},
|
||||
},
|
||||
{
|
||||
"reserved name (collectionId)",
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeText,
|
||||
Id: "1234567890",
|
||||
Name: schema.FieldNameCollectionId,
|
||||
},
|
||||
[]string{"name"},
|
||||
},
|
||||
{
|
||||
"reserved name (collectionName)",
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeText,
|
||||
Id: "1234567890",
|
||||
Name: schema.FieldNameCollectionName,
|
||||
},
|
||||
[]string{"name"},
|
||||
},
|
||||
{
|
||||
"reserved name (expand)",
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeText,
|
||||
Id: "1234567890",
|
||||
Name: schema.FieldNameExpand,
|
||||
},
|
||||
[]string{"name"},
|
||||
},
|
||||
@@ -456,7 +500,7 @@ func TestSchemaFieldInitOptions(t *testing.T) {
|
||||
{
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation},
|
||||
false,
|
||||
`{"system":false,"id":"","name":"","type":"relation","required":false,"unique":false,"options":{"maxSelect":0,"collectionId":"","cascadeDelete":false}}`,
|
||||
`{"system":false,"id":"","name":"","type":"relation","required":false,"unique":false,"options":{"maxSelect":null,"collectionId":"","cascadeDelete":false}}`,
|
||||
},
|
||||
{
|
||||
schema.SchemaField{Type: schema.FieldTypeUser},
|
||||
@@ -548,8 +592,9 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
|
||||
{schema.SchemaField{Type: schema.FieldTypeDate}, nil, `""`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeDate}, "", `""`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeDate}, "test", `""`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeDate}, 1641024040, `"2022-01-01 08:00:40.000"`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeDate}, "2022-01-01 11:27:10.123", `"2022-01-01 11:27:10.123"`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeDate}, 1641024040, `"2022-01-01 08:00:40.000Z"`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeDate}, "2022-01-01 11:27:10.123", `"2022-01-01 11:27:10.123Z"`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeDate}, "2022-01-01 11:27:10.123Z", `"2022-01-01 11:27:10.123Z"`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeDate}, types.DateTime{}, `""`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeDate}, time.Time{}, `""`},
|
||||
|
||||
@@ -697,13 +742,48 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
|
||||
},
|
||||
|
||||
// relation (single)
|
||||
{schema.SchemaField{Type: schema.FieldTypeRelation}, nil, `""`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeRelation}, "", `""`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeRelation}, 123, `"123"`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeRelation}, "abc", `"abc"`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeRelation}, "1ba88b4f-e9da-42f0-9764-9a55c953e724", `"1ba88b4f-e9da-42f0-9764-9a55c953e724"`},
|
||||
{
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation},
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeRelation,
|
||||
Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)},
|
||||
},
|
||||
nil,
|
||||
`""`,
|
||||
},
|
||||
{
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeRelation,
|
||||
Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)},
|
||||
},
|
||||
"",
|
||||
`""`,
|
||||
},
|
||||
{
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeRelation,
|
||||
Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)},
|
||||
},
|
||||
123,
|
||||
`"123"`,
|
||||
},
|
||||
{
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeRelation,
|
||||
Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)},
|
||||
},
|
||||
"abc",
|
||||
`"abc"`,
|
||||
},
|
||||
{
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeRelation,
|
||||
Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)},
|
||||
},
|
||||
"1ba88b4f-e9da-42f0-9764-9a55c953e724",
|
||||
`"1ba88b4f-e9da-42f0-9764-9a55c953e724"`,
|
||||
},
|
||||
{
|
||||
schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
|
||||
[]string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724"},
|
||||
`"1ba88b4f-e9da-42f0-9764-9a55c953e724"`,
|
||||
},
|
||||
@@ -711,7 +791,7 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
|
||||
{
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeRelation,
|
||||
Options: &schema.RelationOptions{MaxSelect: 2},
|
||||
Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)},
|
||||
},
|
||||
nil,
|
||||
`[]`,
|
||||
@@ -719,7 +799,7 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
|
||||
{
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeRelation,
|
||||
Options: &schema.RelationOptions{MaxSelect: 2},
|
||||
Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)},
|
||||
},
|
||||
"",
|
||||
`[]`,
|
||||
@@ -727,7 +807,7 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
|
||||
{
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeRelation,
|
||||
Options: &schema.RelationOptions{MaxSelect: 2},
|
||||
Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)},
|
||||
},
|
||||
[]string{},
|
||||
`[]`,
|
||||
@@ -735,7 +815,7 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
|
||||
{
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeRelation,
|
||||
Options: &schema.RelationOptions{MaxSelect: 2},
|
||||
Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)},
|
||||
},
|
||||
123,
|
||||
`["123"]`,
|
||||
@@ -743,7 +823,7 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
|
||||
{
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeRelation,
|
||||
Options: &schema.RelationOptions{MaxSelect: 2},
|
||||
Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)},
|
||||
},
|
||||
[]string{"", "abc"},
|
||||
`["abc"]`,
|
||||
@@ -752,7 +832,7 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
|
||||
// no values validation
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeRelation,
|
||||
Options: &schema.RelationOptions{MaxSelect: 2},
|
||||
Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)},
|
||||
},
|
||||
[]string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724"},
|
||||
`["1ba88b4f-e9da-42f0-9764-9a55c953e724","2ba88b4f-e9da-42f0-9764-9a55c953e724"]`,
|
||||
@@ -761,77 +841,7 @@ func TestSchemaFieldPrepareValue(t *testing.T) {
|
||||
// duplicated values
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeRelation,
|
||||
Options: &schema.RelationOptions{MaxSelect: 2},
|
||||
},
|
||||
[]string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724", "1ba88b4f-e9da-42f0-9764-9a55c953e724"},
|
||||
`["1ba88b4f-e9da-42f0-9764-9a55c953e724","2ba88b4f-e9da-42f0-9764-9a55c953e724"]`,
|
||||
},
|
||||
|
||||
// user (single)
|
||||
{schema.SchemaField{Type: schema.FieldTypeUser}, nil, `""`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeUser}, "", `""`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeUser}, 123, `"123"`},
|
||||
{schema.SchemaField{Type: schema.FieldTypeUser}, "1ba88b4f-e9da-42f0-9764-9a55c953e724", `"1ba88b4f-e9da-42f0-9764-9a55c953e724"`},
|
||||
{
|
||||
schema.SchemaField{Type: schema.FieldTypeUser},
|
||||
[]string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724"},
|
||||
`"1ba88b4f-e9da-42f0-9764-9a55c953e724"`,
|
||||
},
|
||||
// user (multiple)
|
||||
{
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeUser,
|
||||
Options: &schema.UserOptions{MaxSelect: 2},
|
||||
},
|
||||
nil,
|
||||
`[]`,
|
||||
},
|
||||
{
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeUser,
|
||||
Options: &schema.UserOptions{MaxSelect: 2},
|
||||
},
|
||||
"",
|
||||
`[]`,
|
||||
},
|
||||
{
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeUser,
|
||||
Options: &schema.UserOptions{MaxSelect: 2},
|
||||
},
|
||||
[]string{},
|
||||
`[]`,
|
||||
},
|
||||
{
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeUser,
|
||||
Options: &schema.UserOptions{MaxSelect: 2},
|
||||
},
|
||||
123,
|
||||
`["123"]`,
|
||||
},
|
||||
{
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeUser,
|
||||
Options: &schema.UserOptions{MaxSelect: 2},
|
||||
},
|
||||
[]string{"", "abc"},
|
||||
`["abc"]`,
|
||||
},
|
||||
{
|
||||
// no values validation
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeUser,
|
||||
Options: &schema.UserOptions{MaxSelect: 2},
|
||||
},
|
||||
[]string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724"},
|
||||
`["1ba88b4f-e9da-42f0-9764-9a55c953e724","2ba88b4f-e9da-42f0-9764-9a55c953e724"]`,
|
||||
},
|
||||
{
|
||||
// duplicated values
|
||||
schema.SchemaField{
|
||||
Type: schema.FieldTypeUser,
|
||||
Options: &schema.UserOptions{MaxSelect: 2},
|
||||
Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)},
|
||||
},
|
||||
[]string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724", "1ba88b4f-e9da-42f0-9764-9a55c953e724"},
|
||||
`["1ba88b4f-e9da-42f0-9764-9a55c953e724","2ba88b4f-e9da-42f0-9764-9a55c953e724"]`,
|
||||
@@ -1277,13 +1287,13 @@ func TestRelationOptionsValidate(t *testing.T) {
|
||||
{
|
||||
"empty",
|
||||
schema.RelationOptions{},
|
||||
[]string{"maxSelect", "collectionId"},
|
||||
[]string{"collectionId"},
|
||||
},
|
||||
{
|
||||
"empty CollectionId",
|
||||
schema.RelationOptions{
|
||||
CollectionId: "",
|
||||
MaxSelect: 1,
|
||||
MaxSelect: types.Pointer(1),
|
||||
},
|
||||
[]string{"collectionId"},
|
||||
},
|
||||
@@ -1291,7 +1301,7 @@ func TestRelationOptionsValidate(t *testing.T) {
|
||||
"MaxSelect <= 0",
|
||||
schema.RelationOptions{
|
||||
CollectionId: "abc",
|
||||
MaxSelect: 0,
|
||||
MaxSelect: types.Pointer(0),
|
||||
},
|
||||
[]string{"maxSelect"},
|
||||
},
|
||||
@@ -1299,33 +1309,7 @@ func TestRelationOptionsValidate(t *testing.T) {
|
||||
"MaxSelect > 0 && non-empty CollectionId",
|
||||
schema.RelationOptions{
|
||||
CollectionId: "abc",
|
||||
MaxSelect: 1,
|
||||
},
|
||||
[]string{},
|
||||
},
|
||||
}
|
||||
|
||||
checkFieldOptionsScenarios(t, scenarios)
|
||||
}
|
||||
|
||||
func TestUserOptionsValidate(t *testing.T) {
|
||||
scenarios := []fieldOptionsScenario{
|
||||
{
|
||||
"empty",
|
||||
schema.UserOptions{},
|
||||
[]string{"maxSelect"},
|
||||
},
|
||||
{
|
||||
"MaxSelect <= 0",
|
||||
schema.UserOptions{
|
||||
MaxSelect: 0,
|
||||
},
|
||||
[]string{"maxSelect"},
|
||||
},
|
||||
{
|
||||
"MaxSelect > 0",
|
||||
schema.UserOptions{
|
||||
MaxSelect: 1,
|
||||
MaxSelect: types.Pointer(1),
|
||||
},
|
||||
[]string{},
|
||||
},
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
var _ Model = (*User)(nil)
|
||||
|
||||
const (
|
||||
// ProfileCollectionName is the name of the system user profiles collection.
|
||||
ProfileCollectionName = "profiles"
|
||||
|
||||
// ProfileCollectionUserFieldName is the name of the user field from the system user profiles collection.
|
||||
ProfileCollectionUserFieldName = "userId"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
BaseAccount
|
||||
|
||||
Verified bool `db:"verified" json:"verified"`
|
||||
LastVerificationSentAt types.DateTime `db:"lastVerificationSentAt" json:"lastVerificationSentAt"`
|
||||
|
||||
// profile rel
|
||||
Profile *Record `db:"-" json:"profile"`
|
||||
}
|
||||
|
||||
func (m *User) TableName() string {
|
||||
return "_users"
|
||||
}
|
||||
|
||||
// AsMap returns the current user data as a plain map
|
||||
// (including the profile relation, if loaded).
|
||||
func (m *User) AsMap() (map[string]any, error) {
|
||||
userBytes, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := map[string]any{}
|
||||
if err := json.Unmarshal(userBytes, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package models_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func TestUserTableName(t *testing.T) {
|
||||
m := models.User{}
|
||||
if m.TableName() != "_users" {
|
||||
t.Fatalf("Unexpected table name, got %q", m.TableName())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserAsMap(t *testing.T) {
|
||||
date, _ := types.ParseDateTime("2022-01-01 01:12:23.456")
|
||||
|
||||
m := models.User{}
|
||||
m.Id = "210a896c-1e32-4c94-ae06-90c25fcf6791"
|
||||
m.Email = "test@example.com"
|
||||
m.PasswordHash = "test"
|
||||
m.LastResetSentAt = date
|
||||
m.Updated = date
|
||||
m.RefreshTokenKey()
|
||||
|
||||
result, err := m.AsMap()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := `{"created":"","email":"test@example.com","id":"210a896c-1e32-4c94-ae06-90c25fcf6791","lastResetSentAt":"2022-01-01 01:12:23.456","lastVerificationSentAt":"","profile":null,"updated":"2022-01-01 01:12:23.456","verified":false}`
|
||||
if string(encoded) != expected {
|
||||
t.Errorf("Expected %s, got %s", expected, string(encoded))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user