github.com/gocaveman/caveman@v0.0.0-20191211162744-0ddf99dbdf6e/gen/ctrl-api-crud.go (about)

     1  package gen
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"strings"
     7  
     8  	"github.com/spf13/pflag"
     9  )
    10  
    11  // basic crud
    12  
    13  // advanced find methods for specific fields (do we do this by
    14  // calling store.FindByFirstName or store.Find("first_name", ...))
    15  // should do something with LIKE vs exact field match
    16  
    17  // paging
    18  
    19  // basic detail view page load - so when we generate the view it doesn't
    20  // need a separate handler to loads its data - the common case of a single
    21  // record by ID is already handled here
    22  
    23  // update mulitple records with one PATCH/PUT call, so you can e.g. update a bunch of sequence numbers at once
    24  // See httpapi and fill stuff to properly implement PATCH
    25  
    26  // i18n for validation (and other?) error messages
    27  
    28  // callback methods, with interface and New method checks to see if we implement it -
    29  // looks like this is changing to having a ...Router and a ...Controller and a Default...Controller
    30  
    31  // handlerregistry integration
    32  
    33  // optional (but default) permissions code (crud perms at top, registered, and checks in code)
    34  
    35  // prefix for api calls
    36  
    37  // prefix/path for page data (will need the renderer stuff for this to work as expected)
    38  // what about login check for other pages (listing page), even though they don't necessarily
    39  // require data loading...
    40  
    41  func init() {
    42  	globalMapGenerator["ctrl-api-crud"] = GeneratorFunc(func(s *Settings, name string, args ...string) error {
    43  
    44  		fset := pflag.NewFlagSet("gen", pflag.ContinueOnError)
    45  		storeType := fset.String("store", "", "The type of the store to use for data access (defaults to '*store.Store' or '*Store', depending on package).")
    46  		modelName := fset.String("model", "", "The model object name, if not specified default will be deduced from file name.")
    47  		// TODO: responder code should be an option - it's a fair amount of cruft and people shouldn't
    48  		// be forced to have it if they don't need it; default to off
    49  		// TODO: option for permission stuff - default to on
    50  		renderer := fset.Bool("renderer", true, "Output renderer integration for view/edit page.")
    51  		tests := fset.Bool("tests", true, "Create test file with test(s) for this controller.")
    52  		targetFile, data, err := ParsePFlagsAndOneFile(s, fset, args)
    53  		if err != nil {
    54  			return err
    55  		}
    56  
    57  		_, targetFileName := filepath.Split(targetFile)
    58  
    59  		if *modelName == "" {
    60  			*modelName = NameSnakeToCamel(targetFileName, []string{"ctrl-"}, []string{"-api.go", ".go"})
    61  		}
    62  		data["ModelName"] = *modelName
    63  		// FIXME: this breaks on JSONThing -> jSONThing
    64  		data["ModelNameL"] = strings.ToLower((*modelName)[:1]) + (*modelName)[1:]
    65  
    66  		data["ModelPathPart"] = strings.TrimPrefix(
    67  			strings.TrimSuffix(strings.TrimSuffix(targetFileName, ".go"), "-api"),
    68  			"ctrl-")
    69  
    70  		if *storeType == "" {
    71  			if data["PackageName"].(string) == "main" {
    72  				*storeType = "*Store"
    73  				data["ModelTypeName"] = *modelName
    74  			} else {
    75  				*storeType = "*store.Store"
    76  				data["ModelTypeName"] = "store." + *modelName
    77  			}
    78  		}
    79  		data["StoreType"] = *storeType
    80  
    81  		data["Renderer"] = *renderer
    82  		data["Tests"] = *tests
    83  
    84  		err = OutputGoSrcTemplate(s, data, targetFile, `
    85  package {{.PackageName}}
    86  
    87  import (
    88  	"net/http"
    89  )
    90  
    91  const (
    92  	{{.ModelName}}CreatePerm = "{{.ModelName}}.Create"
    93  	{{.ModelName}}FetchPerm = "{{.ModelName}}.Fetch"
    94  	{{.ModelName}}SearchPerm = "{{.ModelName}}.Search"
    95  	{{.ModelName}}UpdatePerm = "{{.ModelName}}.Update"
    96  	{{.ModelName}}DeletePerm = "{{.ModelName}}.Delete"
    97  
    98  	{{/* TODO: figure out if we should provide some variations as options
    99  	{{.ModelName}}CreateAnyPerm = "{{.ModelName}}.CreateAny"
   100  	{{.ModelName}}FetchAnyPerm = "{{.ModelName}}.FetchAny"
   101  	{{.ModelName}}SearchAnyPerm = "{{.ModelName}}.SearchAny"
   102  	{{.ModelName}}UpdateAnyPerm = "{{.ModelName}}.UpdateAny"
   103  	{{.ModelName}}DeleteAnyPerm = "{{.ModelName}}.DeleteAny"
   104  	*/}}
   105  )
   106  
   107  func init() {
   108  	permregistry.MustAddPerm("admin", {{.ModelName}}CreatePerm)
   109  	permregistry.MustAddPerm("admin", {{.ModelName}}FetchPerm)
   110  	permregistry.MustAddPerm("admin", {{.ModelName}}SearchPerm)
   111  	permregistry.MustAddPerm("admin", {{.ModelName}}UpdatePerm)
   112  	permregistry.MustAddPerm("admin", {{.ModelName}}DeletePerm)
   113  }
   114  
   115  type {{.ModelName}}APIRouter struct {
   116  	APIPrefix string {{bq "autowire:\"api_prefix,optional\""}} // default: "/api" {{/* TODO: naming convention? */}}
   117  	ModelPrefix string // default: "/{{.ModelPathPart}}"
   118  	Controller *{{.ModelName}}APIController
   119  
   120  	{{/*
   121  	TODO: Renderer and path for data loading on detail page.
   122  	Hm, need to figure out if the data loading for detail page,
   123  	etc should be done here or in a different handler.  Or do we
   124  	just assume people won't need to do this... not sure, different
   125  	schools of thought on that
   126  	*/}}
   127  }
   128  
   129  type {{.ModelName}}APIController struct {
   130  	Store {{.StoreType}} {{bq "autowire:\"\""}}
   131  }
   132  
   133  func (h *{{.ModelName}}APIRouter) AfterWire() error {
   134  	if h.APIPrefix == "" {
   135  		h.APIPrefix = "/api"
   136  	}
   137  	if h.ModelPrefix == "" {
   138  		h.ModelPrefix = "/{{.ModelPathPart}}"
   139  	}
   140  	return nil
   141  }
   142  
   143  // search{{.ModelName}}Params is the criteria for a search,
   144  // corresponding to URL parameters
   145  type search{{.ModelName}}Params struct {
   146  	Criteria tmetautil.Criteria {{bq "json:\"criteria\""}}
   147  	OrderBy tmetautil.OrderByList {{bq "json:\"order_by\""}}
   148  	Limit int64 {{bq "json:\"limit\""}}
   149  	Offset int64 {{bq "json:\"offset\""}}
   150  	Related []string {{bq "json:\"related\""}}
   151  	ReturnCount bool {{bq "json:\"return_count\""}}
   152  }
   153  
   154  func (h *{{.ModelName}}APIRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   155  
   156  	var {{.ModelNameL}} {{.ModelTypeName}}
   157  	var {{.ModelNameL}}ID string
   158  	var mapData map[string]interface{}
   159  
   160  	ar := httpapi.NewRequest(r)
   161  
   162  	searchParams := search{{.ModelName}}Params {
   163  		Limit: 100,
   164  	}
   165  	var err error
   166  
   167  	switch {
   168  
   169  	/**
   170  	 * @api {post} /api/{{.ModelPathPart}} Create {{.ModelName}}
   171  	 * @apiGroup {{.ModelName}}
   172  	 * @apiName create-{{.ModelPathPart}}
   173  	 * @apiDescription Create a {{.ModelName}}
   174  	 *
   175  	 * @apiSuccessExample {json} Success-Response:
   176  	 *     HTTP/1.1 200 OK
   177  	 *     Content-Type: application/json
   178  	 *     Location: /api/{{.ModelPathPart}}/6cSAs4i2P3PsHq2s6PZi6V
   179  	 *     X-Id: 6cSAs4i2P3PsHq2s6PZi6V
   180  	 *
   181  	 *     [{
   182  	 *         // {{.ModelName}}
   183  	 *     }]
   184  	 */
   185  	case ar.ParseRESTObj("POST", &{{.ModelNameL}}, h.APIPrefix+h.ModelPrefix):
   186  		err = h.Controller.Create(w, r, ar, &{{.ModelNameL}})
   187  
   188  	/**
   189  	 * @api {get} /api/{{.ModelPathPart}} Search {{plural .ModelName}}
   190  	 * @apiGroup {{.ModelName}}
   191  	 * @apiName search-{{.ModelPathPart}}
   192  	 * @apiDescription List {{plural .ModelName}}
   193  	 *
   194  	 * @apiSuccessExample {json} Success-Response:
   195  	 *     HTTP/1.1 200 OK
   196  	 *     Content-Type: application/json
   197  	 *
   198  	 *     [{
   199  	 *         // {{.ModelName}}
   200  	 *     }]
   201  	 */
   202  	case ar.ParseRESTPath("GET", h.APIPrefix+h.ModelPrefix):
   203  		err = httpapi.FormUnmarshal(r.URL.Query(), &searchParams)
   204  		if err != nil {
   205  			break
   206  		}
   207  		err = h.Controller.Search(w, r, ar, searchParams)
   208  
   209  	/**
   210  	 * @api {get} /api/{{.ModelPathPart}}/:id Fetch {{.ModelName}}
   211  	 * @apiGroup {{.ModelName}}
   212  	 * @apiName fetch-{{.ModelPathPart}}
   213  	 * @apiDescription Get a {{.ModelName}} with the specified ID.  Will return an error
   214  	 * if the object cannot be found.
   215  	 *
   216  	 * @apiParam {String} id ID of the {{.ModelName}} to return
   217  	 *
   218  	 * @apiSuccessExample {json} Success-Response:
   219  	 *     HTTP/1.1 200 OK
   220  	 *     Content-Type: application/json
   221  	 *
   222  	 *     [{
   223  	 *         // {{.ModelName}}
   224  	 *     }]
   225  	 */
   226  	case ar.ParseRESTPath("GET", h.APIPrefix+h.ModelPrefix+"/%s", &{{.ModelNameL}}ID):
   227  		err = httpapi.FormUnmarshal(r.URL.Query(), &searchParams)
   228  		if err != nil {
   229  			break
   230  		}
   231  		err = h.Controller.Fetch(w, r, ar, {{.ModelNameL}}ID, searchParams.Related...)
   232  
   233  	/**
   234  	 * @api {patch} /api/{{.ModelPathPart}}/:id Update {{.ModelName}}
   235  	 * @apiGroup {{.ModelName}}
   236  	 * @apiName update-{{.ModelPathPart}}
   237  	 * @apiDescription Update a {{.ModelName}} by ID.  Will return an error
   238  	 * if the object cannot be found or validation error occurs.  The updated
   239  	 * object will be returned.
   240  	 *
   241  	 * @apiParam {String} id ID of the {{.ModelName}} to update
   242  	 *
   243  	 * @apiSuccessExample {json} Success-Response:
   244  	 *     HTTP/1.1 200 OK
   245  	 *     Content-Type: application/json
   246  	 *
   247  	 *     [{
   248  	 *         // {{.ModelName}}
   249  	 *     }]
   250  	 */
   251  	case ar.ParseRESTObjPath("PUT", &mapData, h.APIPrefix+h.ModelPrefix+"/%s", &{{.ModelNameL}}ID) ||
   252  		ar.ParseRESTObjPath("PATCH", &mapData, h.APIPrefix+h.ModelPrefix+"/%s", &{{.ModelNameL}}ID):
   253  		{{.ModelNameL}}.{{.ModelName}}ID = {{.ModelNameL}}ID
   254  		err = h.Controller.Update(w, r, ar, {{.ModelNameL}}ID, mapData)
   255  
   256  	/**
   257  	 * @api {delete} /api/{{.ModelPathPart}}/:id Delete {{.ModelName}}
   258  	 * @apiGroup {{.ModelName}}
   259  	 * @apiName delete-{{.ModelPathPart}}
   260  	 * @apiDescription Delete a {{.ModelName}} by ID.
   261  	 *
   262  	 * @apiParam {String} id ID of the {{.ModelName}} to delete
   263  	 *
   264  	 * @apiSuccessExample {json} Success-Response:
   265  	 *     HTTP/1.1 200 OK
   266  	 *     Content-Type: application/json
   267  	 *
   268  	 *     {"result":true}
   269  	 */
   270  	case ar.ParseRESTPath("DELETE", h.APIPrefix+h.ModelPrefix+"/%s", &{{.ModelNameL}}ID):
   271  		err = h.Controller.Delete(w, r, ar, {{.ModelNameL}}ID)
   272  
   273  	}
   274  
   275  	if err != nil {
   276  		ar.WriteErr(w, err)
   277  		return
   278  	}	
   279  
   280  }
   281  
   282  func (h *{{.ModelName}}APIController) Create(w http.ResponseWriter, r *http.Request, ar *httpapi.APIRequest, {{.ModelNameL}} *{{.ModelTypeName}}) error {
   283  
   284  	if !userctrl.ReqUserHasPerm(r, {{.ModelName}}CreatePerm) {
   285  		return &httpapi.ErrorDetail{Code: 403, Message: "access denied"}
   286  	}
   287  
   288  	// FIXME: we should probably be using httpapi.Fill() here to ensure
   289  	// consistent behavior with Update()
   290  
   291  	err := h.Store.Create{{.ModelName}}(r.Context(), {{.ModelNameL}})
   292  	if err != nil {
   293  		return err
   294  	}
   295  
   296  	w.Header().Set("Location", r.URL.Path + "/"+{{.ModelNameL}}.{{.ModelName}}ID)
   297  	w.Header().Set("X-Id", {{.ModelNameL}}.{{.ModelName}}ID)
   298  	ar.WriteResult(w, 201, {{.ModelNameL}})
   299  
   300  	return nil
   301  }
   302  
   303  // TODO: we need our fancy find, it should have:
   304  // * List of relations to return
   305  // * Make sure to set the deadline, and a default and a max limit on the records, offset for paging
   306  // * Where conditions expressed as nested struct (probably something that should go in dbutil pkg).
   307  //   There can also be something on that where struct that allows the Store to easily say
   308  //   "I need an exact match (optionally or LIKE prefix% with at least N chars) on
   309  //   at least one of these fields", to enforce that an index is being
   310  //   used (if desired - which it should be by default).
   311  // * What about rate limiting for security purposes, and API usage? (related but not necessarily the same)
   312  
   313  func (h *{{.ModelName}}APIController) Search(w http.ResponseWriter, r *http.Request, ar *httpapi.APIRequest, params search{{.ModelName}}Params) error {
   314  
   315  	if !userctrl.ReqUserHasPerm(r, {{.ModelName}}SearchPerm) {
   316  		return &httpapi.ErrorDetail{Code: 403, Message: "access denied"}
   317  	}
   318  
   319  	// make a separate context with timeout so search doesn't run too long
   320  	queryCtx, cancel := context.WithTimeout(context.Background(), time.Second*5)
   321  	defer cancel()
   322  
   323  	maxLimit := int64(5000) // never allow more than this many records
   324  	if params.Limit + params.Offset > maxLimit {
   325  		return fmt.Errorf("limit plus offset exceeded maximum value")
   326  	}
   327  
   328  	// check all of the field and relation names to prevent SQL injection or other unexpected behavior
   329      err := params.Criteria.CheckFieldNames(h.Store.Meta.For({{.ModelTypeName}}{}).SQLFields(true)...)
   330      if err != nil {
   331          return err
   332      }
   333      err = params.OrderBy.CheckFieldNames(h.Store.Meta.For({{.ModelTypeName}}{}).SQLFields(true)...)
   334      if err != nil {
   335          return err
   336      }
   337      err = h.Store.Meta.For({{.ModelTypeName}}{}).CheckRelationNames(params.Related...)
   338      if err != nil {
   339          return err
   340      }
   341  
   342  	ret := make(map[string]interface{}, 3)
   343  
   344  	if params.ReturnCount {
   345  		count, err := h.Store.Search{{.ModelName}}Count(queryCtx,
   346  			params.Criteria, params.OrderBy,
   347  			maxLimit)
   348  		if err != nil {
   349  			return err
   350  		}
   351  		ret["count"] = count
   352  	}
   353  
   354  	resultList, err := h.Store.Search{{.ModelName}}(queryCtx,
   355  		params.Criteria, params.OrderBy,
   356  		params.Limit, params.Offset,
   357  		params.Related...)
   358  	if err != nil {
   359  		return err
   360  	}
   361  	ret["result_list"] = resultList
   362  	ret["result_length"] = len(resultList)
   363  
   364  	{{/*
   365  	// TODO: It would be nice to sanely implement paging.  I think using
   366  	// headers to convey paging is a crappy way to go - harder for clients
   367  	// expecting JSON to handle.  The idea of "caller needs 10 on a page
   368  	// so ask for 11 and if you get it you know there's another page" should
   369  	// work fine but should be internalized here so that complexity isn't 
   370  	// pushed back to the caller.  So maybe it's just limit, offset that gets
   371  	// passed in and the result {result_list:...:has_more:true} or something. 
   372  	// Do more looking at other APIs and find someone doing it well.
   373  	// https://www.moesif.com/blog/technical/api-design/REST-API-Design-Filtering-Sorting-and-Pagination/
   374  
   375  	// We also should support pagination based on keys (i.e. "10 next records after X")
   376  	// so it doesn't choke on large datasets - I can see use cases for both approaches
   377  	*/}}
   378  
   379  	{{/* FIXME: so do we return this error or not? if we return it
   380  		then the caller will double-output the error, if not we just
   381  		hid the error - maybe we need a log statement specifically
   382  		for this case, think it through and drop it in */}}
   383  
   384  	ar.WriteResult(w, 200, ret)
   385  
   386  	return nil
   387  }
   388  
   389  func (h *{{.ModelName}}APIController) Fetch(w http.ResponseWriter, r *http.Request, ar *httpapi.APIRequest, {{.ModelNameL}}ID string, related ...string) error {
   390  
   391  	if !userctrl.ReqUserHasPerm(r, {{.ModelName}}FetchPerm) {
   392  		return &httpapi.ErrorDetail{Code: 403, Message: "access denied"}
   393  	}
   394  
   395  	var {{.ModelNameL}} {{.ModelTypeName}}
   396  	err := h.Store.Fetch{{.ModelName}}(r.Context(), &{{.ModelNameL}}, {{.ModelNameL}}ID, related...)
   397  	if webutil.IsNotFound(err) {
   398  		return &httpapi.ErrorDetail{Code: 404, Message: "not found"}
   399  	}
   400  	if err != nil {
   401  		return err
   402  	}
   403  
   404  	// TODO: sometimes return values need to be vetted/scrubbed, both
   405  	// here and in Search, before being returned, where should this
   406  	// call go?  Might be better actually to ensure at the store
   407  	// layer we can just avoid loading the fields we don't need...
   408  	// Although that might be a specific case for certain data types that we
   409  	// don't want to have in everything by default.
   410  
   411  	ar.WriteResult(w, 200, {{.ModelNameL}})
   412  
   413  	return nil
   414  }
   415  
   416  func (h *{{.ModelName}}APIController) Update(w http.ResponseWriter, r *http.Request, ar *httpapi.APIRequest, {{.ModelNameL}}ID string, mapData map[string]interface{}) error {
   417  
   418  	if !userctrl.ReqUserHasPerm(r, {{.ModelName}}UpdatePerm) {
   419  		return &httpapi.ErrorDetail{Code: 403, Message: "access denied"}
   420  	}
   421  
   422  	{{/*
   423  	// FIXME: THIS IS STILL NEEDED - ESPECIALLY THE ERRORS THAT ROLL UP TO THE UI
   424  	// TODO: Validation!
   425  	// Tags in model
   426  	// Check in store calls
   427  	// Additional validation in controller
   428  	// Errors that roll all the way up to the UI - look at responses codes, how to indicate what validation went wrong etc
   429  	// Translatable
   430  	*/}}
   431  
   432  	// TODO: add version number check, so we can do optimistic locking all the
   433  	// way up to the form/UI
   434  	var {{.ModelNameL}} {{.ModelTypeName}}
   435  	err := h.Store.Fetch{{.ModelName}}(r.Context(), &{{.ModelNameL}}, {{.ModelNameL}}ID)
   436  	if err != nil {
   437  		return err
   438  	}
   439  
   440  	// patch the fillable fields
   441  	err = httpapi.Fill(&{{.ModelNameL}}, mapData)
   442  	if err != nil {
   443  		return err
   444  	}
   445  
   446  	err = h.Store.Update{{.ModelName}}(r.Context(), &{{.ModelNameL}})
   447  	if webutil.IsNotFound(err) {
   448  		return &httpapi.ErrorDetail{Code: 404, Message: "not found"}
   449  	}
   450  	if err != nil {
   451  		return err
   452  	}
   453  
   454  	ar.WriteResult(w, 200, {{.ModelNameL}})
   455  
   456  	return nil
   457  }
   458  
   459  func (h *{{.ModelName}}APIController) Delete(w http.ResponseWriter, r *http.Request, ar *httpapi.APIRequest, {{.ModelNameL}}ID string) error {
   460  
   461  	if !userctrl.ReqUserHasPerm(r, {{.ModelName}}DeletePerm) {
   462  		return &httpapi.ErrorDetail{Code: 403, Message: "access denied"}
   463  	}
   464  
   465  	err := h.Store.Delete{{.ModelName}}(r.Context(), &{{.ModelTypeName}}{ {{.ModelName}}ID: {{.ModelNameL}}ID } )
   466  	if webutil.IsNotFound(err) {
   467  		return &httpapi.ErrorDetail{Code: 404, Message: "not found"}
   468  	}
   469  	if err != nil {
   470  		return err
   471  	}
   472  
   473  	ar.WriteResult(w, 200, true)
   474  
   475  	return nil
   476  }
   477  
   478  `, false)
   479  
   480  		if err != nil {
   481  			return err
   482  		}
   483  
   484  		if *tests {
   485  
   486  			testsTargetFile := strings.Replace(targetFile, ".go", "_test.go", 1)
   487  			if testsTargetFile == targetFile {
   488  				return fmt.Errorf("unable to determine test file name for %q", targetFile)
   489  			}
   490  
   491  			err = OutputGoSrcTemplate(s, data, testsTargetFile, `
   492  package {{.PackageName}}
   493  
   494  // TODO: write tests
   495  
   496  {{/*
   497  func Test{{.ModelName}}CRUD(t *testing.T) {
   498  
   499  	assert := assert.New(t)
   500  	_ = assert
   501  
   502  }
   503  */}}
   504  
   505  `, false)
   506  			if err != nil {
   507  				return err
   508  			}
   509  
   510  		}
   511  
   512  		return nil
   513  
   514  	})
   515  }