github.com/gocaveman/caveman@v0.0.0-20191211162744-0ddf99dbdf6e/gen/store-crud.go (about) 1 package gen 2 3 import ( 4 "fmt" 5 "path" 6 "strings" 7 8 "github.com/spf13/pflag" 9 ) 10 11 func init() { 12 globalMapGenerator["store-crud"] = GeneratorFunc(func(s *Settings, name string, args ...string) error { 13 14 fset := pflag.NewFlagSet("gen", pflag.ContinueOnError) 15 storeName := fset.String("store", "Store", "The name of the store struct to add methods to.") 16 modelName := fset.String("model", "", "The model object name, if not specified default will be deduced from file name.") 17 genericMode := fset.Bool("generic", false, "Generic mode outputs methods that use interface{} instead of the specific type and can have the underlying model object swapped out.") 18 tests := fset.Bool("tests", true, "Create test file with test(s) for these store methods.") 19 targetFile, data, err := ParsePFlagsAndOneFile(s, fset, args) 20 if err != nil { 21 return err 22 } 23 24 data["StoreName"] = *storeName 25 data["GenericMode"] = *genericMode 26 27 modelNameFixed := *modelName 28 if modelNameFixed == "" { 29 _, fname := path.Split(targetFile) 30 modelNameFixed = NameSnakeToCamel(fname, []string{"store-"}, nil) 31 } 32 data["ModelName"] = modelNameFixed 33 34 err = OutputGoSrcTemplate(s, data, targetFile, ` 35 package {{.PackageName}} 36 37 import ( 38 "context" 39 40 "github.com/bradleypeabody/gouuidv6" 41 "github.com/gocaveman/caveman/valid" 42 "github.com/gocaveman/tmeta/tmetadbr" 43 "github.com/gocaveman/tmeta/tmetautil" 44 "github.com/gocraft/dbr" 45 ) 46 47 // Create{{.ModelName}} inserts the record into the database. 48 func (s *{{.StoreName}}) Create{{.ModelName}}(ctx context.Context, o {{if .GenericMode}}interface{}{{else}}*{{.ModelName}}{{end}}) error { 49 50 tx, err := s.dbrc.NewSession(s.EventReceiver).BeginTx(ctx, nil) 51 if err != nil { 52 return err 53 } 54 defer tx.RollbackUnlessCommitted() 55 56 err = valid.Obj(o, nil) 57 if err != nil { 58 return err 59 } 60 61 b := tmetadbr.New(tx, s.Meta) 62 _, err = b.MustInsert(o).Exec() 63 if err != nil { 64 return err 65 } 66 67 return tx.Commit() 68 } 69 70 // Update{{.ModelName}} updates this record in the database. 71 func (s *{{.StoreName}}) Update{{.ModelName}}(ctx context.Context, o {{if .GenericMode}}interface{}{{else}}*{{.ModelName}}{{end}}) error { 72 73 tx, err := s.dbrc.NewSession(s.EventReceiver).BeginTx(ctx, nil) 74 if err != nil { 75 return err 76 } 77 defer tx.RollbackUnlessCommitted() 78 79 err = valid.Obj(o, nil) 80 if err != nil { 81 return err 82 } 83 84 b := tmetadbr.New(tx, s.Meta) 85 err = b.ResultWithOneUpdate(b.MustUpdateByID(o).Exec()) 86 if err != nil { 87 return err 88 } 89 90 return tx.Commit() 91 } 92 93 // Delete{{.ModelName}} deletes this record in the database. 94 func (s *{{.StoreName}}) Delete{{.ModelName}}(ctx context.Context, o {{if .GenericMode}}interface{}{{else}}*{{.ModelName}}{{end}}) error { 95 96 tx, err := s.dbrc.NewSession(s.EventReceiver).BeginTx(ctx, nil) 97 if err != nil { 98 return err 99 } 100 defer tx.RollbackUnlessCommitted() 101 102 b := tmetadbr.New(tx, s.Meta) 103 err = b.ResultWithOneUpdate(b.MustDeleteByID(o).Exec()) 104 if err != nil { 105 return err 106 } 107 108 return tx.Commit() 109 } 110 111 // Fetch{{.ModelName}} get a record in the database by ID. 112 func (s *{{.StoreName}}) Fetch{{.ModelName}}(ctx context.Context, o {{if .GenericMode}}interface{}{{else}}*{{.ModelName}}{{end}}, id string, related ...string) error { 113 114 tx, err := s.dbrc.NewSession(s.EventReceiver).BeginTx(ctx, nil) 115 if err != nil { 116 return err 117 } 118 defer tx.RollbackUnlessCommitted() 119 120 b := tmetadbr.New(tx, s.Meta) 121 err = b.MustSelectByID(o, id).LoadOne(o) 122 if err != nil { 123 return err 124 } 125 126 ti := s.Meta.For(o) 127 for _, r := range related { 128 rstmt, err := b.SelectRelation(o, r) 129 if err != nil { 130 return err 131 } 132 _, err = rstmt.Load( 133 ti.RelationTargetPtr(o, r)) 134 if err != nil { 135 return err 136 } 137 } 138 139 return tx.Commit() 140 } 141 142 {{/* FIXME: security goes in controller - so move the strict check of the 143 fields on criteria and orderby and related etc up up there 144 */}} 145 146 func (s *{{.StoreName}}) Search{{.ModelName}}Count(ctx context.Context, criteria tmetautil.Criteria, orderBy tmetautil.OrderByList, maxRows int64) (int64, error) { 147 148 tx, err := s.dbrc.NewSession(s.EventReceiver).BeginTx(ctx, nil) 149 if err != nil { 150 return 0, err 151 } 152 defer tx.RollbackUnlessCommitted() 153 154 ti := s.Meta.For({{.ModelName}}{}) 155 156 stmt := tx.Select(ti.SQLPKFields()...).From(ti.SQLName()) 157 158 whereSql, args, err := criteria.SQL() 159 if err != nil { 160 return 0, err 161 } 162 if len(whereSql) > 0 { 163 stmt = stmt.Where(whereSql, args...) 164 } 165 166 for _, o := range orderBy { 167 stmt = stmt.OrderDir(o.Field, !o.Desc) 168 } 169 170 if maxRows >= 0 { 171 stmt = stmt.Limit(uint64(maxRows)) 172 } 173 174 buf := dbr.NewBuffer() 175 err = stmt.Build(s.dbrc.Dialect, buf) 176 if err != nil { 177 return 0, err 178 } 179 innerSQL := buf.String() 180 args = buf.Value() 181 182 var c int64 183 err = tx.SelectBySql("SELECT count(1) c FROM ("+innerSQL+") t", args...).LoadOne(&c) 184 if err != nil { 185 return 0, err 186 } 187 188 return c, tx.Commit() 189 } 190 191 // Search{{.ModelName}} builds and runs a select statement from the input you provide. 192 func (s *{{.StoreName}}) Search{{.ModelName}}(ctx context.Context, criteria tmetautil.Criteria, orderBy tmetautil.OrderByList, limit, offset int64, related ...string) ([]{{.ModelName}}, error) { 193 194 tx, err := s.dbrc.NewSession(s.EventReceiver).BeginTx(ctx, nil) 195 if err != nil { 196 return nil, err 197 } 198 defer tx.RollbackUnlessCommitted() 199 200 b := tmetadbr.New(tx, s.Meta) 201 202 ti := s.Meta.For({{.ModelName}}{}) 203 204 stmt := tx.Select(ti.SQLFields(true)...).From(ti.SQLName()) 205 206 whereSql, args, err := criteria.SQL() 207 if err != nil { 208 return nil, err 209 } 210 if len(whereSql) > 0 { 211 stmt = stmt.Where(whereSql, args...) 212 } 213 214 for _, o := range orderBy { 215 stmt = stmt.OrderDir(o.Field, !o.Desc) 216 } 217 218 if offset > 0 { 219 stmt = stmt.Offset(uint64(offset)) 220 } 221 if limit >= 0 { 222 stmt = stmt.Limit(uint64(limit)) 223 } 224 225 // empty set should return zero length slice instead of nil for proper JSON output and semantic correctness 226 ret := make([]{{.ModelName}}, 0) 227 _, err = stmt.Load(&ret) 228 if err != nil { 229 return nil, err 230 } 231 232 if len(related) > 0 { 233 for i := range ret { 234 for _, r := range related { 235 rstmt, err := b.SelectRelation(&ret[i], r) 236 if err != nil { 237 return nil, err 238 } 239 _, err = rstmt.Load( 240 ti.RelationTargetPtr(&ret[i], r)) 241 if err != nil { 242 return nil, err 243 } 244 } 245 } 246 } 247 248 return ret, tx.Commit() 249 } 250 251 252 {{/* NOTE: the paging/limits here are all based on the idea of not overloading the database server and putting sensible limits on how 253 much data a client can ask for. However it is completely feasible for clients to page through an arbitrarily large data set 254 by asking for the next N records where KEY > LAST_KEY, and clients don't need any special tools for that particularly 255 - this should definitely be documented somewhere */}} 256 257 258 // TODO: figure out Find... methods... (think about listing page); also make sure empty 259 // list returns zero length slice and not nil, so JSON marshaling etc work as expected 260 // (it's also, while less efficient, semantically correct - the search didn't return "nothing" 261 // it returned zero of the specified element - and this difference also shows up in 262 // the resulting JSON) 263 264 // TODO: upsert? - maybe it's an option to add an example if desired. 265 266 // TODO: related/joins; also specifically look at having methods that say "set the 267 // list of this type of join to exactly X set as one call in one transaction", rather 268 // than having to delete and re-write. 269 270 `, false) 271 if err != nil { 272 return err 273 } 274 275 // TODO: disable tests for now until we fill this in 276 if false && *tests { 277 278 testsTargetFile := strings.Replace(targetFile, ".go", "_test.go", 1) 279 if testsTargetFile == targetFile { 280 return fmt.Errorf("unable to determine test file name for %q", targetFile) 281 } 282 283 err = OutputGoSrcTemplate(s, data, testsTargetFile, ` 284 package {{.PackageName}} 285 286 func Test{{.ModelName}}CRUD(t *testing.T) { 287 288 assert := assert.New(t) 289 290 dbDriver := "sqlite3" 291 dbDsn := "file:Test{{.ModelName}}CRUD?mode=memory&cache=shared" 292 293 ml := DefaultStoreMigrations.WithDriverName(dbDriver).Sorted() 294 versioner, err := migratedbr.New(dbDriver, dbDsn) 295 assert.NoError(err) 296 runner := migrate.NewRunner(dbDriver, dbDsn, versioner, ml) 297 err = runner.RunAllUpToLatest() 298 assert.NoError(err) 299 300 conn, err := dbr.Open(dbDriver, dbDsn, nil) 301 assert.NoError(err) 302 store := NewStore(dbrobj.NewConfig().NewConnector(conn, nil)) 303 304 err = store.AfterWire() 305 assert.NoError(err) 306 307 o := &{{.ModelName}}{ 308 // TODO: populate with valid data 309 // Category: "testcat", 310 // Title: "Clean the Kitchen", 311 // Description: "It's gross", 312 } 313 err = store.Create{{.ModelName}}(o) 314 assert.NoError(err) 315 // TODO: o.Title = "Deep Clean the Kitchen" 316 err = store.Update{{.ModelName}}(o) 317 assert.NoError(err) 318 o2 := &{{.ModelName}}{} 319 err = store.Fetch{{.ModelName}}(o2, o.{{.ModelName}}ID) 320 assert.NoError(err) 321 // TODO: assert.Equal("Deep Clean the Kitchen", o2.Title) 322 err = store.Delete{{.ModelName}}(o2) 323 assert.NoError(err) 324 err = store.Fetch{{.ModelName}}(o2) 325 assert.Error(err) 326 327 } 328 329 `, false) 330 if err != nil { 331 return err 332 } 333 334 } 335 336 return nil 337 }) 338 }