[#2992] fixed zero-default value not being used if the field is not explicitly set when manually creating records

This commit is contained in:
Gani Georgiev 2023-07-25 20:35:29 +03:00
parent 34fe55686d
commit 1330e2e1e7
11 changed files with 6241 additions and 6054 deletions

View File

@ -110,6 +110,10 @@
- Added new utility `github.com/pocketbase/pocketbase/tools/template` package to assist with rendering HTML templates using the standard Go `html/template` and `text/template` syntax. - Added new utility `github.com/pocketbase/pocketbase/tools/template` package to assist with rendering HTML templates using the standard Go `html/template` and `text/template` syntax.
- Fixed zero-default value not being used if the field is not explicitly set when manually creating records ([#2992](https://github.com/pocketbase/pocketbase/issues/2992)).
Additionally, `record.Get(field)` will now always return normalized value (the same as in the json serialization) for consistency and to avoid ambiguities what is stored in the related DB table.
The schema fields columns `DEFAULT` definition was also updated for new collections to ensure that `NULL` values can't be accidentally inserted.
## v0.16.10 ## v0.16.10

View File

@ -173,6 +173,15 @@ func (dao *Dao) normalizeSingleVsMultipleFieldChanges(newCollection, oldCollecti
} }
return dao.RunInTransaction(func(txDao *Dao) error { return dao.RunInTransaction(func(txDao *Dao) error {
// temporary disable the schema error checks to prevent view and trigger errors
// when "altering" (aka. deleting and recreating) the non-normalized columns
if _, err := txDao.DB().NewQuery("PRAGMA writable_schema = ON").Execute(); err != nil {
return err
}
// executed with defer to make sure that the pragma is always reverted
// in case of an error and when nested transactions are used
defer txDao.DB().NewQuery("PRAGMA writable_schema = RESET").Execute()
for _, newField := range newCollection.Schema.Fields() { for _, newField := range newCollection.Schema.Fields() {
// allow to continue even if there is no old field for the cases // allow to continue even if there is no old field for the cases
// when a new field is added and there are already inserted data // when a new field is added and there are already inserted data
@ -192,11 +201,26 @@ func (dao *Dao) normalizeSingleVsMultipleFieldChanges(newCollection, oldCollecti
continue // no change continue // no change
} }
var updateQuery *dbx.Query // update the column definition by:
// 1. inserting a new column with the new definition
// 2. copy normalized values from the original column to the new one
// 3. drop the original column
// 4. rename the new column to the original column
// -------------------------------------------------------
originalName := newField.Name
tempName := "_" + newField.Name + security.PseudorandomString(5)
_, err := txDao.DB().AddColumn(newCollection.Name, tempName, newField.ColDefinition()).Execute()
if err != nil {
return err
}
var copyQuery *dbx.Query
if !isOldMultiple && isNewMultiple { if !isOldMultiple && isNewMultiple {
// single -> multiple (convert to array) // single -> multiple (convert to array)
updateQuery = txDao.DB().NewQuery(fmt.Sprintf( copyQuery = txDao.DB().NewQuery(fmt.Sprintf(
`UPDATE {{%s}} set [[%s]] = ( `UPDATE {{%s}} set [[%s]] = (
CASE CASE
WHEN COALESCE([[%s]], '') = '' WHEN COALESCE([[%s]], '') = ''
@ -211,19 +235,19 @@ func (dao *Dao) normalizeSingleVsMultipleFieldChanges(newCollection, oldCollecti
END END
)`, )`,
newCollection.Name, newCollection.Name,
newField.Name, tempName,
newField.Name, originalName,
newField.Name, originalName,
newField.Name, originalName,
newField.Name, originalName,
newField.Name, originalName,
)) ))
} else { } else {
// multiple -> single (keep only the last element) // multiple -> single (keep only the last element)
// //
// note: for file fields the actual files are not deleted // note: for file fields the actual file objects are not
// allowing additional custom handling via migration. // deleted allowing additional custom handling via migration
updateQuery = txDao.DB().NewQuery(fmt.Sprintf( copyQuery = txDao.DB().NewQuery(fmt.Sprintf(
`UPDATE {{%s}} set [[%s]] = ( `UPDATE {{%s}} set [[%s]] = (
CASE CASE
WHEN COALESCE([[%s]], '[]') = '[]' WHEN COALESCE([[%s]], '[]') = '[]'
@ -238,21 +262,35 @@ func (dao *Dao) normalizeSingleVsMultipleFieldChanges(newCollection, oldCollecti
END END
)`, )`,
newCollection.Name, newCollection.Name,
newField.Name, tempName,
newField.Name, originalName,
newField.Name, originalName,
newField.Name, originalName,
newField.Name, originalName,
newField.Name, originalName,
)) ))
} }
if _, err := updateQuery.Execute(); err != nil { // copy the normalized values
if _, err := copyQuery.Execute(); err != nil {
return err
}
// drop the original column
if _, err := txDao.DB().DropColumn(newCollection.Name, originalName).Execute(); err != nil {
return err
}
// rename the new column back to the original
if _, err := txDao.DB().RenameColumn(newCollection.Name, tempName, originalName).Execute(); err != nil {
return err return err
} }
} }
return nil // revert the pragma and reload the schema
_, revertErr := txDao.DB().NewQuery("PRAGMA writable_schema = RESET").Execute()
return revertErr
}) })
} }

View File

@ -188,7 +188,51 @@ func TestSingleVsMultipleValuesNormalization(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
type expectation struct { // ensures that the writable schema was reverted to its expected default
var writableSchema bool
app.Dao().DB().NewQuery("PRAGMA writable_schema").Row(&writableSchema)
if writableSchema == true {
t.Fatalf("Expected writable_schema to be OFF, got %v", writableSchema)
}
// check whether the columns DEFAULT definition was updated
// ---------------------------------------------------------------
tableInfo, err := app.Dao().TableInfo(collection.Name)
if err != nil {
t.Fatal(err)
}
tableInfoExpectations := map[string]string{
"select_one": `'[]'`,
"select_many": `''`,
"file_one": `'[]'`,
"file_many": `''`,
"rel_one": `'[]'`,
"rel_many": `''`,
"new_multiple": `'[]'`,
}
for col, dflt := range tableInfoExpectations {
t.Run("check default for "+col, func(t *testing.T) {
var row *models.TableInfoRow
for _, r := range tableInfo {
if r.Name == col {
row = r
break
}
}
if row == nil {
t.Fatalf("Missing info for column %q", col)
}
if v := row.DefaultValue.String(); v != dflt {
t.Fatalf("Expected default value %q, got %q", dflt, v)
}
})
}
// check whether the values were normalized
// ---------------------------------------------------------------
type fieldsExpectation struct {
SelectOne string `db:"select_one"` SelectOne string `db:"select_one"`
SelectMany string `db:"select_many"` SelectMany string `db:"select_many"`
FileOne string `db:"file_one"` FileOne string `db:"file_one"`
@ -198,13 +242,13 @@ func TestSingleVsMultipleValuesNormalization(t *testing.T) {
NewMultiple string `db:"new_multiple"` NewMultiple string `db:"new_multiple"`
} }
scenarios := []struct { fieldsScenarios := []struct {
recordId string recordId string
expected expectation expected fieldsExpectation
}{ }{
{ {
"imy661ixudk5izi", "imy661ixudk5izi",
expectation{ fieldsExpectation{
SelectOne: `[]`, SelectOne: `[]`,
SelectMany: ``, SelectMany: ``,
FileOne: `[]`, FileOne: `[]`,
@ -216,7 +260,7 @@ func TestSingleVsMultipleValuesNormalization(t *testing.T) {
}, },
{ {
"al1h9ijdeojtsjy", "al1h9ijdeojtsjy",
expectation{ fieldsExpectation{
SelectOne: `["optionB"]`, SelectOne: `["optionB"]`,
SelectMany: `optionB`, SelectMany: `optionB`,
FileOne: `["300_Jsjq7RdBgA.png"]`, FileOne: `["300_Jsjq7RdBgA.png"]`,
@ -228,7 +272,7 @@ func TestSingleVsMultipleValuesNormalization(t *testing.T) {
}, },
{ {
"84nmscqy84lsi1t", "84nmscqy84lsi1t",
expectation{ fieldsExpectation{
SelectOne: `["optionB"]`, SelectOne: `["optionB"]`,
SelectMany: `optionC`, SelectMany: `optionC`,
FileOne: `["test_d61b33QdDU.txt"]`, FileOne: `["test_d61b33QdDU.txt"]`,
@ -240,8 +284,9 @@ func TestSingleVsMultipleValuesNormalization(t *testing.T) {
}, },
} }
for _, s := range scenarios { for _, s := range fieldsScenarios {
result := new(expectation) t.Run("check fields for record "+s.recordId, func(t *testing.T) {
result := new(fieldsExpectation)
err := app.Dao().DB().Select( err := app.Dao().DB().Select(
"select_one", "select_one",
@ -253,24 +298,22 @@ func TestSingleVsMultipleValuesNormalization(t *testing.T) {
"new_multiple", "new_multiple",
).From(collection.Name).Where(dbx.HashExp{"id": s.recordId}).One(result) ).From(collection.Name).Where(dbx.HashExp{"id": s.recordId}).One(result)
if err != nil { if err != nil {
t.Errorf("[%s] Failed to load record: %v", s.recordId, err) t.Fatalf("Failed to load record: %v", err)
continue
} }
encodedResult, err := json.Marshal(result) encodedResult, err := json.Marshal(result)
if err != nil { if err != nil {
t.Errorf("[%s] Failed to encode result: %v", s.recordId, err) t.Fatalf("Failed to encode result: %v", err)
continue
} }
encodedExpectation, err := json.Marshal(s.expected) encodedExpectation, err := json.Marshal(s.expected)
if err != nil { if err != nil {
t.Errorf("[%s] Failed to encode expectation: %v", s.recordId, err) t.Fatalf("Failed to encode expectation: %v", err)
continue
} }
if !bytes.EqualFold(encodedExpectation, encodedResult) { if !bytes.EqualFold(encodedExpectation, encodedResult) {
t.Errorf("[%s] Expected \n%s, \ngot \n%s", s.recordId, encodedExpectation, encodedResult) t.Fatalf("Expected \n%s, \ngot \n%s", encodedExpectation, encodedResult)
} }
})
} }
} }

5
go.sum
View File

@ -12,8 +12,6 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDe
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.44.306 h1:H487V/1N09BDxeGR7oR+LloC2uUpmf4atmqJaBgQOIs=
github.com/aws/aws-sdk-go v1.44.306/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go v1.44.307 h1:2R0/EPgpZcFSUwZhYImq/srjaOrOfLv5MNRzrFyAM38= github.com/aws/aws-sdk-go v1.44.307 h1:2R0/EPgpZcFSUwZhYImq/srjaOrOfLv5MNRzrFyAM38=
github.com/aws/aws-sdk-go v1.44.307/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go v1.44.307/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go-v2 v1.19.0 h1:klAT+y3pGFBU/qVf1uzwttpBbiuozJYWzNLHioyDJ+k= github.com/aws/aws-sdk-go-v2 v1.19.0 h1:klAT+y3pGFBU/qVf1uzwttpBbiuozJYWzNLHioyDJ+k=
@ -168,8 +166,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198 h1:lFz33AOOXwTpqOiHvrN8nmTdkxSfuNLHLPjgQ1muPpU=
github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198/go.mod h1:uh3YlzsEJj7OG57rDWj6c3WEkOF1ZHGBQkDuUZw3rE8=
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 h1:FwuzbVh87iLiUQj1+uQUsuw9x5t9m5n5g7rG7o4svW4= github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61 h1:FwuzbVh87iLiUQj1+uQUsuw9x5t9m5n5g7rG7o4svW4=
github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61/go.mod h1:paQfF1YtHe+GrGg5fOgjsjoCX/UKDr9bc1DoWpZfns8= github.com/labstack/echo/v5 v5.0.0-20230722203903-ec5b858dab61/go.mod h1:paQfF1YtHe+GrGg5fOgjsjoCX/UKDr9bc1DoWpZfns8=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@ -210,7 +206,6 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=

View File

@ -324,7 +324,7 @@ func (m *Record) Set(key string, value any) {
} }
} }
// Get returns a single record model data value for "key". // Get returns a normalized single record model data value for "key".
func (m *Record) Get(key string) any { func (m *Record) Get(key string) any {
switch key { switch key {
case schema.FieldNameId: case schema.FieldNameId:
@ -334,11 +334,27 @@ func (m *Record) Get(key string) any {
case schema.FieldNameUpdated: case schema.FieldNameUpdated:
return m.Updated return m.Updated
default: default:
if m.data == nil { var v any
return nil if m.data != nil {
v = m.data.Get(key)
} }
return m.data.Get(key) // normalize the field value in case it is missing or an incorrect type was set
// to ensure that the DB will always have normalized columns value.
if field := m.Collection().Schema.GetFieldByName(key); field != nil {
v = field.PrepareValue(v)
} else if m.collection.IsAuth() {
switch key {
case schema.FieldNameEmailVisibility, schema.FieldNameVerified:
v = cast.ToBool(v)
case schema.FieldNameLastResetSentAt, schema.FieldNameLastVerificationSentAt:
v, _ = types.ParseDateTime(v)
case schema.FieldNameUsername, schema.FieldNameEmail, schema.FieldNameTokenKey, schema.FieldNamePasswordHash:
v = cast.ToString(v)
}
}
return v
} }
} }

View File

@ -729,6 +729,22 @@ func TestRecordSetAndGet(t *testing.T) {
Name: "field2", Name: "field2",
Type: schema.FieldTypeNumber, Type: schema.FieldTypeNumber,
}, },
// fields that are not explicitly set to check
// the default retrieval value (single and multiple)
&schema.SchemaField{
Name: "field3",
Type: schema.FieldTypeBool,
},
&schema.SchemaField{
Name: "field4",
Type: schema.FieldTypeSelect,
Options: &schema.SelectOptions{MaxSelect: 2},
},
&schema.SchemaField{
Name: "field5",
Type: schema.FieldTypeRelation,
Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)},
},
), ),
} }
@ -741,28 +757,40 @@ func TestRecordSetAndGet(t *testing.T) {
m.Set("unknown", 456) // undefined fields are allowed but not exported by default m.Set("unknown", 456) // undefined fields are allowed but not exported by default
m.Set("expand", map[string]any{"test": 123}) // should store the value in m.expand m.Set("expand", map[string]any{"test": 123}) // should store the value in m.expand
if m.Get("id") != "test_id" { if v := m.Get("id"); v != "test_id" {
t.Fatalf("Expected id %q, got %q", "test_id", m.Get("id")) t.Fatalf("Expected id %q, got %q", "test_id", v)
} }
if m.GetString("created") != "2022-09-15 00:00:00.123Z" { if v := m.GetString("created"); v != "2022-09-15 00:00:00.123Z" {
t.Fatalf("Expected created %q, got %q", "2022-09-15 00:00:00.123Z", m.GetString("created")) t.Fatalf("Expected created %q, got %q", "2022-09-15 00:00:00.123Z", v)
} }
if m.GetString("updated") != "" { if v := m.GetString("updated"); v != "" {
t.Fatalf("Expected updated to be empty, got %q", m.GetString("updated")) t.Fatalf("Expected updated to be empty, got %q", v)
} }
if m.Get("field1") != "123" { if v, ok := m.Get("field1").(string); !ok || v != "123" {
t.Fatalf("Expected field1 %q, got %v", "123", m.Get("field1")) t.Fatalf("Expected field1 %#v, got %#v", "123", m.Get("field1"))
} }
if m.Get("field2") != 0.0 { if v, ok := m.Get("field2").(float64); !ok || v != 0.0 {
t.Fatalf("Expected field2 %v, got %v", 0.0, m.Get("field2")) t.Fatalf("Expected field2 %#v, got %#v", 0.0, m.Get("field2"))
} }
if m.Get("unknown") != 456 { if v, ok := m.Get("field3").(bool); !ok || v != false {
t.Fatalf("Expected unknown %v, got %v", 456, m.Get("unknown")) t.Fatalf("Expected field3 %#v, got %#v", false, m.Get("field3"))
}
if v, ok := m.Get("field4").([]string); !ok || len(v) != 0 {
t.Fatalf("Expected field4 %#v, got %#v", "[]", m.Get("field4"))
}
if v, ok := m.Get("field5").(string); !ok || len(v) != 0 {
t.Fatalf("Expected field5 %#v, got %#v", "", m.Get("field5"))
}
if v := m.Get("unknown"); v != 456 {
t.Fatalf("Expected unknown %v, got %v", 456, v)
} }
if m.Expand()["test"] != 123 { if m.Expand()["test"] != 123 {

View File

@ -143,13 +143,17 @@ type SchemaField struct {
func (f *SchemaField) ColDefinition() string { func (f *SchemaField) ColDefinition() string {
switch f.Type { switch f.Type {
case FieldTypeNumber: case FieldTypeNumber:
return "NUMERIC DEFAULT 0" return "NUMERIC DEFAULT 0 NOT NULL"
case FieldTypeBool: case FieldTypeBool:
return "BOOLEAN DEFAULT FALSE" return "BOOLEAN DEFAULT FALSE NOT NULL"
case FieldTypeJson: case FieldTypeJson:
return "JSON DEFAULT NULL" return "JSON DEFAULT NULL"
default: default:
return "TEXT DEFAULT ''" if opt, ok := f.Options.(MultiValuer); ok && opt.IsMultiple() {
return "JSON DEFAULT '[]' NOT NULL"
}
return "TEXT DEFAULT '' NOT NULL"
} }
} }

View File

@ -63,47 +63,59 @@ func TestSchemaFieldColDefinition(t *testing.T) {
}{ }{
{ {
schema.SchemaField{Type: schema.FieldTypeText, Name: "test"}, schema.SchemaField{Type: schema.FieldTypeText, Name: "test"},
"TEXT DEFAULT ''", "TEXT DEFAULT '' NOT NULL",
}, },
{ {
schema.SchemaField{Type: schema.FieldTypeNumber, Name: "test"}, schema.SchemaField{Type: schema.FieldTypeNumber, Name: "test"},
"NUMERIC DEFAULT 0", "NUMERIC DEFAULT 0 NOT NULL",
}, },
{ {
schema.SchemaField{Type: schema.FieldTypeBool, Name: "test"}, schema.SchemaField{Type: schema.FieldTypeBool, Name: "test"},
"BOOLEAN DEFAULT FALSE", "BOOLEAN DEFAULT FALSE NOT NULL",
}, },
{ {
schema.SchemaField{Type: schema.FieldTypeEmail, Name: "test"}, schema.SchemaField{Type: schema.FieldTypeEmail, Name: "test"},
"TEXT DEFAULT ''", "TEXT DEFAULT '' NOT NULL",
}, },
{ {
schema.SchemaField{Type: schema.FieldTypeUrl, Name: "test"}, schema.SchemaField{Type: schema.FieldTypeUrl, Name: "test"},
"TEXT DEFAULT ''", "TEXT DEFAULT '' NOT NULL",
}, },
{ {
schema.SchemaField{Type: schema.FieldTypeEditor, Name: "test"}, schema.SchemaField{Type: schema.FieldTypeEditor, Name: "test"},
"TEXT DEFAULT ''", "TEXT DEFAULT '' NOT NULL",
}, },
{ {
schema.SchemaField{Type: schema.FieldTypeDate, Name: "test"}, schema.SchemaField{Type: schema.FieldTypeDate, Name: "test"},
"TEXT DEFAULT ''", "TEXT DEFAULT '' NOT NULL",
},
{
schema.SchemaField{Type: schema.FieldTypeSelect, Name: "test"},
"TEXT DEFAULT ''",
}, },
{ {
schema.SchemaField{Type: schema.FieldTypeJson, Name: "test"}, schema.SchemaField{Type: schema.FieldTypeJson, Name: "test"},
"JSON DEFAULT NULL", "JSON DEFAULT NULL",
}, },
{ {
schema.SchemaField{Type: schema.FieldTypeFile, Name: "test"}, schema.SchemaField{Type: schema.FieldTypeSelect, Name: "test"},
"TEXT DEFAULT ''", "TEXT DEFAULT '' NOT NULL",
}, },
{ {
schema.SchemaField{Type: schema.FieldTypeRelation, Name: "test"}, schema.SchemaField{Type: schema.FieldTypeSelect, Name: "test_multiple", Options: &schema.SelectOptions{MaxSelect: 2}},
"TEXT DEFAULT ''", "JSON DEFAULT '[]' NOT NULL",
},
{
schema.SchemaField{Type: schema.FieldTypeFile, Name: "test"},
"TEXT DEFAULT '' NOT NULL",
},
{
schema.SchemaField{Type: schema.FieldTypeFile, Name: "test_multiple", Options: &schema.FileOptions{MaxSelect: 2}},
"JSON DEFAULT '[]' NOT NULL",
},
{
schema.SchemaField{Type: schema.FieldTypeRelation, Name: "test", Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
"TEXT DEFAULT '' NOT NULL",
},
{
schema.SchemaField{Type: schema.FieldTypeRelation, Name: "test_multiple", Options: &schema.RelationOptions{MaxSelect: nil}},
"JSON DEFAULT '[]' NOT NULL",
}, },
} }

File diff suppressed because it is too large Load Diff

View File

@ -141,6 +141,13 @@ declare function routerPre(...middlewares: Array<string|echo.MiddlewareFunc>): v
// baseBinds // baseBinds
// ------------------------------------------------------------------- // -------------------------------------------------------------------
/**
* Global helper variable that contains the absolute path to the app pb_hooks directory.
*
* @group PocketBase
*/
declare var __hooks: string
// skip on* hook methods as they are registered via the global on* method // skip on* hook methods as they are registered via the global on* method
type appWithoutHooks = Omit<pocketbase.PocketBase, ` + "`on${string}`" + `> type appWithoutHooks = Omit<pocketbase.PocketBase, ` + "`on${string}`" + `>
@ -148,6 +155,8 @@ type appWithoutHooks = Omit<pocketbase.PocketBase, ` + "`on${string}`" + `>
* ` + "`$app`" + ` is the current running PocketBase instance that is globally * ` + "`$app`" + ` is the current running PocketBase instance that is globally
* available in each .pb.js file. * available in each .pb.js file.
* *
* _Note that this variable is available only in pb_hooks context._
*
* @namespace * @namespace
* @group PocketBase * @group PocketBase
*/ */
@ -162,14 +171,10 @@ declare var $app: appWithoutHooks
* Example: * Example:
* *
* ` + "```" + `js * ` + "```" + `js
* routerAdd("get", "/hello", (c) => {
* const html = $template.loadFiles( * const html = $template.loadFiles(
* "views/layout.html", * "views/layout.html",
* "views/content.html", * "views/content.html",
* ).render({"name": "John"}) * ).render({"name": "John"})
*
* return c.html(200, html)
* })
* ` + "```" + ` * ` + "```" + `
* *
* _Note that this method is available only in pb_hooks context._ * _Note that this method is available only in pb_hooks context._

View File

@ -201,6 +201,11 @@ func (p *plugin) registerHooks() error {
return nil return nil
} }
absHooksDir, err := filepath.Abs(p.config.HooksDir)
if err != nil {
return err
}
p.app.OnBeforeServe().Add(func(e *core.ServeEvent) error { p.app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
e.Router.HTTPErrorHandler = p.normalizeServeExceptions(e.Router.HTTPErrorHandler) e.Router.HTTPErrorHandler = p.normalizeServeExceptions(e.Router.HTTPErrorHandler)
return nil return nil
@ -225,6 +230,7 @@ func (p *plugin) registerHooks() error {
apisBinds(vm) apisBinds(vm)
vm.Set("$app", p.app) vm.Set("$app", p.app)
vm.Set("$template", templateRegistry) vm.Set("$template", templateRegistry)
vm.Set("__hooks", absHooksDir)
} }
// initiliaze the executor vms // initiliaze the executor vms