Add automatic timestamps for created_at/updated_at

* Update and Insert
* Add --no-auto-timestamps flag
This commit is contained in:
Patrick O'brien 2016-08-29 00:12:37 +10:00
parent 677d45cef4
commit 96d40fcfe4
14 changed files with 201 additions and 60 deletions

View file

@ -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

View file

@ -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
View 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
}

View file

@ -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"`
} }

View file

@ -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"`,

View file

@ -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

View file

@ -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,
} }

View file

@ -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
}

View file

@ -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")
}
}

View file

@ -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,

View file

@ -22,3 +22,6 @@ type (
*boil.Query *boil.Query
} }
) )
// Force time package dependency for automated UpdatedAt/CreatedAt.
var _ = time.Second

View file

@ -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

View file

@ -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

View 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 -}}