Add automatic timestamps for created_at/updated_at
* Update and Insert * Add --no-auto-timestamps flag
This commit is contained in:
parent
677d45cef4
commit
96d40fcfe4
14 changed files with 201 additions and 60 deletions
14
README.md
14
README.md
|
@ -22,6 +22,7 @@ lifecycle.
|
||||||
- Easy workflow (models can always be regenerated, full auto-complete)
|
- Easy workflow (models can always be regenerated, full auto-complete)
|
||||||
- Strongly typed querying (usually no converting or binding to pointers)
|
- Strongly typed querying (usually no converting or binding to pointers)
|
||||||
- Hooks (Before/After Create/Update)
|
- Hooks (Before/After Create/Update)
|
||||||
|
- Automatic CreatedAt/UpdatedAt
|
||||||
- Relationships/Associations
|
- Relationships/Associations
|
||||||
- Eager loading
|
- Eager loading
|
||||||
- Transactions
|
- Transactions
|
||||||
|
@ -29,17 +30,20 @@ lifecycle.
|
||||||
- Compatibility tests (Run against your own DB schema)
|
- Compatibility tests (Run against your own DB schema)
|
||||||
- Debug logging
|
- Debug logging
|
||||||
|
|
||||||
#### Missing Features
|
|
||||||
|
|
||||||
- Automatic CreatedAt UpdatedAt (use Hooks instead)
|
|
||||||
- Nested eager loading
|
|
||||||
|
|
||||||
#### Supported Databases
|
#### Supported Databases
|
||||||
|
|
||||||
- PostgreSQL
|
- PostgreSQL
|
||||||
|
|
||||||
Note: Seeking contributors for other database engines.
|
Note: Seeking contributors for other database engines.
|
||||||
|
|
||||||
|
#### Automatic CreatedAt/UpdatedAt
|
||||||
|
|
||||||
|
If your generated SQLBoiler models package can find columns with the
|
||||||
|
names `created_at` or `updated_at` it will automatically set them
|
||||||
|
to `time.Now()` in your database, and update your object appropriately.
|
||||||
|
|
||||||
|
Note: You can set the timezone for this feature by calling `boil.SetLocation()`
|
||||||
|
|
||||||
#### Example Queries
|
#### Example Queries
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|
29
boil/db.go
29
boil/db.go
|
@ -1,14 +1,6 @@
|
||||||
package boil
|
package boil
|
||||||
|
|
||||||
import (
|
import "database/sql"
|
||||||
"database/sql"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// currentDB is a global database handle for the package
|
|
||||||
currentDB Executor
|
|
||||||
)
|
|
||||||
|
|
||||||
// Executor can perform SQL queries.
|
// Executor can perform SQL queries.
|
||||||
type Executor interface {
|
type Executor interface {
|
||||||
|
@ -30,15 +22,6 @@ type Beginner interface {
|
||||||
Begin() (*sql.Tx, error)
|
Begin() (*sql.Tx, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DebugMode is a flag controlling whether generated sql statements and
|
|
||||||
// debug information is outputted to the DebugWriter handle
|
|
||||||
//
|
|
||||||
// NOTE: This should be disabled in production to avoid leaking sensitive data
|
|
||||||
var DebugMode = false
|
|
||||||
|
|
||||||
// DebugWriter is where the debug output will be sent if DebugMode is true
|
|
||||||
var DebugWriter = os.Stdout
|
|
||||||
|
|
||||||
// Begin a transaction
|
// Begin a transaction
|
||||||
func Begin() (Transactor, error) {
|
func Begin() (Transactor, error) {
|
||||||
creator, ok := currentDB.(Beginner)
|
creator, ok := currentDB.(Beginner)
|
||||||
|
@ -48,13 +31,3 @@ func Begin() (Transactor, error) {
|
||||||
|
|
||||||
return creator.Begin()
|
return creator.Begin()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDB initializes the database handle for all template db interactions
|
|
||||||
func SetDB(db Executor) {
|
|
||||||
currentDB = db
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDB retrieves the global state database handle
|
|
||||||
func GetDB() Executor {
|
|
||||||
return currentDB
|
|
||||||
}
|
|
||||||
|
|
50
boil/global.go
Normal file
50
boil/global.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package boil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// currentDB is a global database handle for the package
|
||||||
|
currentDB Executor
|
||||||
|
// timestampLocation is the timezone used for the
|
||||||
|
// automated setting of created_at/updated_at columns
|
||||||
|
timestampLocation *time.Location
|
||||||
|
)
|
||||||
|
|
||||||
|
// DebugMode is a flag controlling whether generated sql statements and
|
||||||
|
// debug information is outputted to the DebugWriter handle
|
||||||
|
//
|
||||||
|
// NOTE: This should be disabled in production to avoid leaking sensitive data
|
||||||
|
var DebugMode = false
|
||||||
|
|
||||||
|
// DebugWriter is where the debug output will be sent if DebugMode is true
|
||||||
|
var DebugWriter = os.Stdout
|
||||||
|
|
||||||
|
// SetDB initializes the database handle for all template db interactions
|
||||||
|
func SetDB(db Executor) {
|
||||||
|
currentDB = db
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDB retrieves the global state database handle
|
||||||
|
func GetDB() Executor {
|
||||||
|
return currentDB
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLocation sets the global timestamp Location.
|
||||||
|
// This is the timezone used by the generated package for the
|
||||||
|
// automated setting of created_at and updated_at columns.
|
||||||
|
// If the package was generated with the --no-auto-timestamps flag
|
||||||
|
// then this function has no effect.
|
||||||
|
func SetLocation(loc *time.Location) {
|
||||||
|
timestampLocation = loc
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocation retrieves the global timestamp Location.
|
||||||
|
// This is the timezone used by the generated package for the
|
||||||
|
// automated setting of created_at and updated_at columns
|
||||||
|
// if the package was not generated with the --no-auto-timestamps flag.
|
||||||
|
func GetLocation() *time.Location {
|
||||||
|
return timestampLocation
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ type Config struct {
|
||||||
BaseDir string `toml:"base_dir"`
|
BaseDir string `toml:"base_dir"`
|
||||||
ExcludeTables []string `toml:"exclude"`
|
ExcludeTables []string `toml:"exclude"`
|
||||||
NoHooks bool `toml:"no_hooks"`
|
NoHooks bool `toml:"no_hooks"`
|
||||||
|
NoAutoTimestamps bool `toml:"no_auto_timestamps"`
|
||||||
|
|
||||||
Postgres PostgresConfig `toml:"postgres"`
|
Postgres PostgresConfig `toml:"postgres"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,6 +146,7 @@ var defaultTemplateImports = imports{
|
||||||
`"fmt"`,
|
`"fmt"`,
|
||||||
`"strings"`,
|
`"strings"`,
|
||||||
`"database/sql"`,
|
`"database/sql"`,
|
||||||
|
`"time"`,
|
||||||
},
|
},
|
||||||
thirdParty: importList{
|
thirdParty: importList{
|
||||||
`"github.com/pkg/errors"`,
|
`"github.com/pkg/errors"`,
|
||||||
|
|
2
main.go
2
main.go
|
@ -66,6 +66,7 @@ func main() {
|
||||||
rootCmd.PersistentFlags().StringSliceP("exclude", "x", nil, "Tables to be excluded from the generated package")
|
rootCmd.PersistentFlags().StringSliceP("exclude", "x", nil, "Tables to be excluded from the generated package")
|
||||||
rootCmd.PersistentFlags().BoolP("debug", "d", false, "Debug mode prints stack traces on error")
|
rootCmd.PersistentFlags().BoolP("debug", "d", false, "Debug mode prints stack traces on error")
|
||||||
rootCmd.PersistentFlags().BoolP("no-hooks", "", false, "Disable hooks feature for your models")
|
rootCmd.PersistentFlags().BoolP("no-hooks", "", false, "Disable hooks feature for your models")
|
||||||
|
rootCmd.PersistentFlags().BoolP("no-auto-timestamps", "", false, "Disable automatic timestamps for created_at/updated_at")
|
||||||
|
|
||||||
viper.SetDefault("postgres.sslmode", "require")
|
viper.SetDefault("postgres.sslmode", "require")
|
||||||
viper.SetDefault("postgres.port", "5432")
|
viper.SetDefault("postgres.port", "5432")
|
||||||
|
@ -106,6 +107,7 @@ func preRun(cmd *cobra.Command, args []string) error {
|
||||||
OutFolder: viper.GetString("output"),
|
OutFolder: viper.GetString("output"),
|
||||||
PkgName: viper.GetString("pkgname"),
|
PkgName: viper.GetString("pkgname"),
|
||||||
NoHooks: viper.GetBool("no-hooks"),
|
NoHooks: viper.GetBool("no-hooks"),
|
||||||
|
NoAutoTimestamps: viper.GetBool("no-auto-timestamps"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// BUG: https://github.com/spf13/viper/issues/200
|
// BUG: https://github.com/spf13/viper/issues/200
|
||||||
|
|
|
@ -82,6 +82,7 @@ func (s *State) Run(includeTests bool) error {
|
||||||
UseLastInsertID: s.Driver.UseLastInsertID(),
|
UseLastInsertID: s.Driver.UseLastInsertID(),
|
||||||
PkgName: s.Config.PkgName,
|
PkgName: s.Config.PkgName,
|
||||||
NoHooks: s.Config.NoHooks,
|
NoHooks: s.Config.NoHooks,
|
||||||
|
NoAutoTimestamps: s.Config.NoAutoTimestamps,
|
||||||
|
|
||||||
StringFuncs: templateStringMappers,
|
StringFuncs: templateStringMappers,
|
||||||
}
|
}
|
||||||
|
@ -112,6 +113,7 @@ func (s *State) Run(includeTests bool) error {
|
||||||
UseLastInsertID: s.Driver.UseLastInsertID(),
|
UseLastInsertID: s.Driver.UseLastInsertID(),
|
||||||
PkgName: s.Config.PkgName,
|
PkgName: s.Config.PkgName,
|
||||||
NoHooks: s.Config.NoHooks,
|
NoHooks: s.Config.NoHooks,
|
||||||
|
NoAutoTimestamps: s.Config.NoAutoTimestamps,
|
||||||
|
|
||||||
StringFuncs: templateStringMappers,
|
StringFuncs: templateStringMappers,
|
||||||
}
|
}
|
||||||
|
|
|
@ -411,3 +411,17 @@ func StringSliceMatch(a []string, b []string) bool {
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ContainsAny returns true if any of the passed in strings are
|
||||||
|
// found in the passed in string slice
|
||||||
|
func ContainsAny(a []string, finds ...string) bool {
|
||||||
|
for _, s := range a {
|
||||||
|
for _, find := range finds {
|
||||||
|
if s == find {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -378,3 +378,32 @@ func TestStringSliceMatch(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContainsAny(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
a := []string{"hello", "friend"}
|
||||||
|
if ContainsAny([]string{}, "x") {
|
||||||
|
t.Errorf("Should not contain x")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ContainsAny(a, "x") {
|
||||||
|
t.Errorf("Should not contain x")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ContainsAny(a, "hello") {
|
||||||
|
t.Errorf("Should contain hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ContainsAny(a, "friend") {
|
||||||
|
t.Errorf("Should contain friend")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ContainsAny(a, "hello", "friend") {
|
||||||
|
t.Errorf("Should contain hello and friend")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ContainsAny(a) {
|
||||||
|
t.Errorf("Should not return true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ type templateData struct {
|
||||||
UseLastInsertID bool
|
UseLastInsertID bool
|
||||||
PkgName string
|
PkgName string
|
||||||
NoHooks bool
|
NoHooks bool
|
||||||
|
NoAutoTimestamps bool
|
||||||
|
|
||||||
StringFuncs map[string]func(string) string
|
StringFuncs map[string]func(string) string
|
||||||
}
|
}
|
||||||
|
@ -127,6 +128,7 @@ var templateFunctions = template.FuncMap{
|
||||||
"joinSlices": strmangle.JoinSlices,
|
"joinSlices": strmangle.JoinSlices,
|
||||||
"stringMap": strmangle.StringMap,
|
"stringMap": strmangle.StringMap,
|
||||||
"prefixStringSlice": strmangle.PrefixStringSlice,
|
"prefixStringSlice": strmangle.PrefixStringSlice,
|
||||||
|
"containsAny": strmangle.ContainsAny,
|
||||||
|
|
||||||
// String Map ops
|
// String Map ops
|
||||||
"makeStringMap": strmangle.MakeStringMap,
|
"makeStringMap": strmangle.MakeStringMap,
|
||||||
|
|
|
@ -22,3 +22,6 @@ type (
|
||||||
*boil.Query
|
*boil.Query
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Force time package dependency for automated UpdatedAt/CreatedAt.
|
||||||
|
var _ = time.Second
|
||||||
|
|
|
@ -32,6 +32,8 @@ func (o *{{$tableNameSingular}}) Insert(exec boil.Executor, whitelist ... string
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
{{- template "timestamp_insert_helper" . }}
|
||||||
|
|
||||||
{{if eq .NoHooks false -}}
|
{{if eq .NoHooks false -}}
|
||||||
if err := o.doBeforeInsertHooks(); err != nil {
|
if err := o.doBeforeInsertHooks(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -35,6 +35,8 @@ func (o *{{$tableNameSingular}}) UpdateP(exec boil.Executor, whitelist ... strin
|
||||||
// Update does not automatically update the record in case of default values. Use .Reload()
|
// Update does not automatically update the record in case of default values. Use .Reload()
|
||||||
// to refresh the records.
|
// to refresh the records.
|
||||||
func (o *{{$tableNameSingular}}) Update(exec boil.Executor, whitelist ... string) error {
|
func (o *{{$tableNameSingular}}) Update(exec boil.Executor, whitelist ... string) error {
|
||||||
|
{{- template "timestamp_update_helper" . -}}
|
||||||
|
|
||||||
{{if eq .NoHooks false -}}
|
{{if eq .NoHooks false -}}
|
||||||
if err := o.doBeforeUpdateHooks(); err != nil {
|
if err := o.doBeforeUpdateHooks(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
56
templates/17_auto_timestamps.tpl
Normal file
56
templates/17_auto_timestamps.tpl
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{{- define "timestamp_insert_helper" -}}
|
||||||
|
{{- if eq .NoAutoTimestamps false -}}
|
||||||
|
{{- $colNames := .Table.Columns | columnNames -}}
|
||||||
|
{{if containsAny $colNames "created_at" "updated_at"}}
|
||||||
|
loc := boil.GetLocation()
|
||||||
|
currTime := time.Time{}
|
||||||
|
if loc != nil {
|
||||||
|
currTime = time.Now().In(boil.GetLocation())
|
||||||
|
} else {
|
||||||
|
currTime = time.Now()
|
||||||
|
}
|
||||||
|
{{range $ind, $col := .Table.Columns}}
|
||||||
|
{{- if eq $col.Name "created_at" -}}
|
||||||
|
{{- if $col.Nullable}}
|
||||||
|
o.CreatedAt.Time = currTime
|
||||||
|
o.CreatedAt.Valid = true
|
||||||
|
{{- else}}
|
||||||
|
o.CreatedAt = currTime
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- if eq $col.Name "updated_at" -}}
|
||||||
|
{{- if $col.Nullable}}
|
||||||
|
o.UpdatedAt.Time = currTime
|
||||||
|
o.UpdatedAt.Valid = true
|
||||||
|
{{- else}}
|
||||||
|
o.UpdatedAt = currTime
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{- end}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- define "timestamp_update_helper" -}}
|
||||||
|
{{- if eq .NoAutoTimestamps false -}}
|
||||||
|
{{- $colNames := .Table.Columns | columnNames -}}
|
||||||
|
{{if containsAny $colNames "updated_at"}}
|
||||||
|
loc := boil.GetLocation()
|
||||||
|
currTime := time.Time{}
|
||||||
|
if loc != nil {
|
||||||
|
currTime = time.Now().In(boil.GetLocation())
|
||||||
|
} else {
|
||||||
|
currTime = time.Now()
|
||||||
|
}
|
||||||
|
{{range $ind, $col := .Table.Columns}}
|
||||||
|
{{- if eq $col.Name "updated_at" -}}
|
||||||
|
{{- if $col.Nullable}}
|
||||||
|
o.UpdatedAt.Time = currTime
|
||||||
|
o.UpdatedAt.Valid = true
|
||||||
|
{{- else}}
|
||||||
|
o.UpdatedAt = currTime
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{- end}}
|
||||||
|
{{end -}}
|
Loading…
Add table
Add a link
Reference in a new issue