ocket8888 commented on code in PR #7734: URL: https://github.com/apache/trafficcontrol/pull/7734#discussion_r1303432251
########## CHANGELOG.md: ########## @@ -75,6 +75,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - [#7716](https://github.com/apache/trafficcontrol/pull/7716) *Apache Traffic Server* Use GCC 11 for building. ### Fixed +- [#7734](https://github.com/apache/trafficcontrol/pull/7734) *Traffic Ops* *Traffic Ops* Fixed `Profiles` V5 apis to respond with `RFC3339` date/time Format. Review Comment: Extra `*Traffic Ops*` also, the route is `/profiles` with a lowercase "p", "API" should be all caps, and "Format" need not start with a capital letter. ########## traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go: ########## @@ -2263,6 +2263,20 @@ func PhysLocationExists(tx *sql.Tx, id string) (bool, error) { return true, nil } +func ProfileExists(tx *sql.Tx, id string) (bool, error) { Review Comment: exported symbols should be described by a GoDoc comment ########## traffic_ops/traffic_ops_golang/profile/profiles.go: ########## @@ -360,3 +362,294 @@ type) VALUES ( func deleteQuery() string { return `DELETE FROM profile WHERE id = :id` } + +// Read gets list of Profiles for APIv5 +func Read(w http.ResponseWriter, r *http.Request) { + var runSecond bool + var maxTime time.Time + inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) + tx := inf.Tx + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + // Query Parameters to Database Query column mappings + queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{ + "cdn": {Column: "c.id", Checker: api.IsInt}, + "name": {Column: "prof.name", Checker: nil}, + "id": {Column: "prof.id", Checker: api.IsInt}, + "param": {Column: "pp.parameter", Checker: api.IsInt}, + } + + query := selectProfilesQuery() + if paramValue, ok := inf.Params["param"]; ok { + if len(paramValue) > 0 { + query += " LEFT JOIN profile_parameter pp ON prof.id = pp.profile" + } + } + + where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, queryParamsToQueryCols) + if len(errs) > 0 { + api.HandleErr(w, r, tx.Tx, http.StatusBadRequest, util.JoinErrs(errs), nil) + return + } + + if inf.Config.UseIMS { + runSecond, maxTime = ims.TryIfModifiedSinceQuery(tx, r.Header, queryValues, selectMaxLastUpdatedQuery(where)) + if !runSecond { + log.Debugln("IMS HIT") + api.AddLastModifiedHdr(w, maxTime) + w.WriteHeader(http.StatusNotModified) + return + } + log.Debugln("IMS MISS") + } else { + log.Debugln("Non IMS request") + } + + query += where + orderBy + pagination + rows, err := tx.NamedQuery(query, queryValues) + if err != nil { + api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("profile read: error getting profile(s): %w", err)) + return + } + defer log.Close(rows, "unable to close DB connection") + + profile := tc.ProfileV5{} + var profileList []tc.ProfileV5 + for rows.Next() { + if err = rows.Scan(&profile.Description, &profile.ID, &profile.LastUpdated, &profile.Name, &profile.RoutingDisabled, &profile.Type, &profile.CDNID, &profile.CDNName); err != nil { + api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("error getting profile(s): %w", err)) + return + } + profileList = append(profileList, profile) + } + rows.Close() + profileInterfaces := []interface{}{} + for _, p := range profileList { + // Attach Parameters if the 'param' parameter is sent + if _, ok := inf.Params["param"]; ok { + p.Parameters, err = ReadParameters(inf.Tx, inf.User, &p.ID) + if err != nil { + api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("profile read: error reading parameters for a profile: %w", err)) + return + } + } + profileInterfaces = append(profileInterfaces, p) + } + + api.WriteResp(w, r, profileInterfaces) + return +} + +// Create a Profile for APIv5 +func Create(w http.ResponseWriter, r *http.Request) { + inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + tx := inf.Tx.Tx + + profile, readValErr := readAndValidateJsonStruct(r) + if readValErr != nil { + api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil) + return + } + + //check if user can modify. + if len(strings.TrimSpace(profile.CDNName)) != 0 || profile.CDNID != 0 { + userErr, sysErr, statusCode := canProfileBeAlteredByCurrentUser(inf.User.UserName, inf.Tx.Tx, &profile.CDNName, &profile.CDNID) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx, statusCode, userErr, sysErr) + return + } + } + + // check if profile already exists + var count int + err := tx.QueryRow("SELECT count(*) from profile where name=$1", profile.Name).Scan(&count) + if err != nil { + api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("error: %w, when checking if profile '%s' exists", err, profile.Name)) + return + } + if count == 1 { + api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("profile:'%s' already exists", profile.Name), nil) + return + } + + // create profile + query := `INSERT INTO profile (name, cdn, type, routing_disabled, description) + VALUES ($1, $2, $3, $4, $5) + RETURNING id, last_updated, name, description, (select name FROM cdn where id = $2), cdn, routing_disabled, type` + + err = tx.QueryRow(query, profile.Name, profile.CDNID, profile.Type, profile.RoutingDisabled, profile.Description). + Scan(&profile.ID, &profile.LastUpdated, &profile.Name, &profile.Description, &profile.CDNName, &profile.CDNID, &profile.RoutingDisabled, &profile.Type) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + api.HandleErr(w, r, tx, http.StatusInternalServerError, fmt.Errorf("error: %w in creating profile:%s", err, profile.Name), nil) + return + } + usrErr, sysErr, code := api.ParseDBError(err) + api.HandleErr(w, r, tx, code, usrErr, sysErr) + return + } + + alerts := tc.CreateAlerts(tc.SuccessLevel, "profile was created.") + w.Header().Set("Location", fmt.Sprintf("/api/%d.%d/profiles?id=%d", inf.Version.Major, inf.Version.Minor, profile.ID)) Review Comment: Just FYI, using `fmt.Sprintf("%d.%d", Version.Major, Version.Minor)` is exactly equivalent to just `Version.String()`. Or, to use this exact example: ```go fmt.Sprintf("/api/%d.%d/profiles?id=%d", inf.Version.Major, inf.Version.Minor, profile.ID) // is the same as fmt.Sprintf("/api/%s/profiles?id=%d", inf.Version, profile.ID) ``` ########## traffic_ops/traffic_ops_golang/profile/profiles.go: ########## @@ -269,13 +271,13 @@ JOIN profile_parameter pp ON pp.parameter = p.id WHERE pp.profile = :profile_id` } -func (pr *TOProfile) checkIfProfileCanBeAlteredByCurrentUser() (error, error, int) { +func canProfileBeAlteredByCurrentUser(user string, tx *sql.Tx, cName *string, cdnID *int) (error, error, int) { var cdnName string - if pr.CDNName != nil { - cdnName = *pr.CDNName + if cName != nil { + cdnName = *cName } else { - if pr.CDNID != nil { - cdn, ok, err := dbhelpers.GetCDNNameFromID(pr.ReqInfo.Tx.Tx, int64(*pr.CDNID)) + if cdnID != nil { + cdn, ok, err := dbhelpers.GetCDNNameFromID(tx, int64(*cdnID)) Review Comment: can we instead just not allow these parameters to be passed as `nil` by making them non-reference values? ########## traffic_ops/traffic_ops_golang/profile/profiles.go: ########## @@ -226,10 +228,10 @@ LEFT JOIN cdn c ON prof.cdn = c.id` return query } -func ReadParameters(tx *sqlx.Tx, parameters map[string]string, user *auth.CurrentUser, profile tc.ProfileNullable) ([]tc.ParameterNullable, error) { +func ReadParameters(tx *sqlx.Tx, user *auth.CurrentUser, profileID *int) ([]tc.ParameterNullable, error) { privLevel := user.PrivLevel queryValues := make(map[string]interface{}) - queryValues["profile_id"] = *profile.ID + queryValues["profile_id"] = profileID Review Comment: why did we change the type of this query argument from `int` to `*int`? If it's `nil`, the query will always return zero results, which just means that it's a waste of a database call, so callers shouldn't use `ReadParameters` in that way. ########## lib/go-tc/profiles.go: ########## @@ -91,6 +92,28 @@ type Profile struct { Parameters []ParameterNullable `json:"params,omitempty"` } +// ProfilesResponseV5 is a list of profiles returned by GET requests. +type ProfilesResponseV5 struct { + Response []ProfileV5 `json:"response"` + Alerts +} + +// A ProfileV5 represents a set of configuration for a server or Delivery Service +// which may be reused to allow sharing configuration across the objects to +// which it is assigned. Note: Field LastUpdated represents RFC3339 +type ProfileV5 struct { + ID int `json:"id" db:"id"` + LastUpdated time.Time `json:"lastUpdated" db:"last_updated"` + Name string `json:"name" db:"name"` + Parameter string `json:"param"` Review Comment: What is this field used for? ########## traffic_ops/traffic_ops_golang/profile/profiles.go: ########## @@ -360,3 +362,294 @@ type) VALUES ( func deleteQuery() string { return `DELETE FROM profile WHERE id = :id` } + +// Read gets list of Profiles for APIv5 +func Read(w http.ResponseWriter, r *http.Request) { + var runSecond bool + var maxTime time.Time + inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) + tx := inf.Tx + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + // Query Parameters to Database Query column mappings + queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{ + "cdn": {Column: "c.id", Checker: api.IsInt}, + "name": {Column: "prof.name", Checker: nil}, + "id": {Column: "prof.id", Checker: api.IsInt}, + "param": {Column: "pp.parameter", Checker: api.IsInt}, + } + + query := selectProfilesQuery() + if paramValue, ok := inf.Params["param"]; ok { + if len(paramValue) > 0 { + query += " LEFT JOIN profile_parameter pp ON prof.id = pp.profile" + } + } + + where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, queryParamsToQueryCols) + if len(errs) > 0 { + api.HandleErr(w, r, tx.Tx, http.StatusBadRequest, util.JoinErrs(errs), nil) + return + } + + if inf.Config.UseIMS { + runSecond, maxTime = ims.TryIfModifiedSinceQuery(tx, r.Header, queryValues, selectMaxLastUpdatedQuery(where)) + if !runSecond { + log.Debugln("IMS HIT") + api.AddLastModifiedHdr(w, maxTime) + w.WriteHeader(http.StatusNotModified) + return + } + log.Debugln("IMS MISS") + } else { + log.Debugln("Non IMS request") + } + + query += where + orderBy + pagination + rows, err := tx.NamedQuery(query, queryValues) + if err != nil { + api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("profile read: error getting profile(s): %w", err)) + return + } + defer log.Close(rows, "unable to close DB connection") + + profile := tc.ProfileV5{} + var profileList []tc.ProfileV5 + for rows.Next() { + if err = rows.Scan(&profile.Description, &profile.ID, &profile.LastUpdated, &profile.Name, &profile.RoutingDisabled, &profile.Type, &profile.CDNID, &profile.CDNName); err != nil { + api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("error getting profile(s): %w", err)) + return + } + profileList = append(profileList, profile) + } + rows.Close() + profileInterfaces := []interface{}{} + for _, p := range profileList { + // Attach Parameters if the 'param' parameter is sent + if _, ok := inf.Params["param"]; ok { + p.Parameters, err = ReadParameters(inf.Tx, inf.User, &p.ID) + if err != nil { + api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("profile read: error reading parameters for a profile: %w", err)) + return + } + } + profileInterfaces = append(profileInterfaces, p) + } + + api.WriteResp(w, r, profileInterfaces) + return +} + +// Create a Profile for APIv5 +func Create(w http.ResponseWriter, r *http.Request) { + inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + tx := inf.Tx.Tx + + profile, readValErr := readAndValidateJsonStruct(r) + if readValErr != nil { + api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil) + return + } + + //check if user can modify. + if len(strings.TrimSpace(profile.CDNName)) != 0 || profile.CDNID != 0 { + userErr, sysErr, statusCode := canProfileBeAlteredByCurrentUser(inf.User.UserName, inf.Tx.Tx, &profile.CDNName, &profile.CDNID) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx, statusCode, userErr, sysErr) + return + } + } + + // check if profile already exists + var count int + err := tx.QueryRow("SELECT count(*) from profile where name=$1", profile.Name).Scan(&count) + if err != nil { + api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("error: %w, when checking if profile '%s' exists", err, profile.Name)) + return + } + if count == 1 { + api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("profile:'%s' already exists", profile.Name), nil) + return + } + + // create profile + query := `INSERT INTO profile (name, cdn, type, routing_disabled, description) + VALUES ($1, $2, $3, $4, $5) + RETURNING id, last_updated, name, description, (select name FROM cdn where id = $2), cdn, routing_disabled, type` + + err = tx.QueryRow(query, profile.Name, profile.CDNID, profile.Type, profile.RoutingDisabled, profile.Description). + Scan(&profile.ID, &profile.LastUpdated, &profile.Name, &profile.Description, &profile.CDNName, &profile.CDNID, &profile.RoutingDisabled, &profile.Type) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + api.HandleErr(w, r, tx, http.StatusInternalServerError, fmt.Errorf("error: %w in creating profile:%s", err, profile.Name), nil) + return + } + usrErr, sysErr, code := api.ParseDBError(err) + api.HandleErr(w, r, tx, code, usrErr, sysErr) + return + } + + alerts := tc.CreateAlerts(tc.SuccessLevel, "profile was created.") + w.Header().Set("Location", fmt.Sprintf("/api/%d.%d/profiles?id=%d", inf.Version.Major, inf.Version.Minor, profile.ID)) Review Comment: Please use [`github.com/apache/trafficcontrol/lib/go-rfc.Location`](https://pkg.go.dev/github.com/apache/[email protected]+incompatible/lib/go-rfc#pkg-constants) instead of an inline string. ########## traffic_ops/traffic_ops_golang/profile/profiles.go: ########## @@ -360,3 +362,294 @@ type) VALUES ( func deleteQuery() string { return `DELETE FROM profile WHERE id = :id` } + +// Read gets list of Profiles for APIv5 +func Read(w http.ResponseWriter, r *http.Request) { + var runSecond bool + var maxTime time.Time + inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) + tx := inf.Tx + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + // Query Parameters to Database Query column mappings + queryParamsToQueryCols := map[string]dbhelpers.WhereColumnInfo{ + "cdn": {Column: "c.id", Checker: api.IsInt}, + "name": {Column: "prof.name", Checker: nil}, + "id": {Column: "prof.id", Checker: api.IsInt}, + "param": {Column: "pp.parameter", Checker: api.IsInt}, + } + + query := selectProfilesQuery() + if paramValue, ok := inf.Params["param"]; ok { + if len(paramValue) > 0 { + query += " LEFT JOIN profile_parameter pp ON prof.id = pp.profile" + } + } + + where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(inf.Params, queryParamsToQueryCols) + if len(errs) > 0 { + api.HandleErr(w, r, tx.Tx, http.StatusBadRequest, util.JoinErrs(errs), nil) + return + } + + if inf.Config.UseIMS { + runSecond, maxTime = ims.TryIfModifiedSinceQuery(tx, r.Header, queryValues, selectMaxLastUpdatedQuery(where)) + if !runSecond { + log.Debugln("IMS HIT") + api.AddLastModifiedHdr(w, maxTime) + w.WriteHeader(http.StatusNotModified) + return + } + log.Debugln("IMS MISS") + } else { + log.Debugln("Non IMS request") + } + + query += where + orderBy + pagination + rows, err := tx.NamedQuery(query, queryValues) + if err != nil { + api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("profile read: error getting profile(s): %w", err)) + return + } + defer log.Close(rows, "unable to close DB connection") + + profile := tc.ProfileV5{} + var profileList []tc.ProfileV5 + for rows.Next() { + if err = rows.Scan(&profile.Description, &profile.ID, &profile.LastUpdated, &profile.Name, &profile.RoutingDisabled, &profile.Type, &profile.CDNID, &profile.CDNName); err != nil { + api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("error getting profile(s): %w", err)) + return + } + profileList = append(profileList, profile) + } + rows.Close() + profileInterfaces := []interface{}{} + for _, p := range profileList { + // Attach Parameters if the 'param' parameter is sent + if _, ok := inf.Params["param"]; ok { + p.Parameters, err = ReadParameters(inf.Tx, inf.User, &p.ID) + if err != nil { + api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("profile read: error reading parameters for a profile: %w", err)) + return + } + } + profileInterfaces = append(profileInterfaces, p) + } + + api.WriteResp(w, r, profileInterfaces) + return +} + +// Create a Profile for APIv5 +func Create(w http.ResponseWriter, r *http.Request) { + inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + tx := inf.Tx.Tx + + profile, readValErr := readAndValidateJsonStruct(r) + if readValErr != nil { + api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil) + return + } + + //check if user can modify. + if len(strings.TrimSpace(profile.CDNName)) != 0 || profile.CDNID != 0 { + userErr, sysErr, statusCode := canProfileBeAlteredByCurrentUser(inf.User.UserName, inf.Tx.Tx, &profile.CDNName, &profile.CDNID) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx, statusCode, userErr, sysErr) + return + } + } + + // check if profile already exists + var count int + err := tx.QueryRow("SELECT count(*) from profile where name=$1", profile.Name).Scan(&count) + if err != nil { + api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("error: %w, when checking if profile '%s' exists", err, profile.Name)) + return + } + if count == 1 { + api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("profile:'%s' already exists", profile.Name), nil) + return + } + + // create profile + query := `INSERT INTO profile (name, cdn, type, routing_disabled, description) + VALUES ($1, $2, $3, $4, $5) + RETURNING id, last_updated, name, description, (select name FROM cdn where id = $2), cdn, routing_disabled, type` + + err = tx.QueryRow(query, profile.Name, profile.CDNID, profile.Type, profile.RoutingDisabled, profile.Description). + Scan(&profile.ID, &profile.LastUpdated, &profile.Name, &profile.Description, &profile.CDNName, &profile.CDNID, &profile.RoutingDisabled, &profile.Type) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + api.HandleErr(w, r, tx, http.StatusInternalServerError, fmt.Errorf("error: %w in creating profile:%s", err, profile.Name), nil) + return + } + usrErr, sysErr, code := api.ParseDBError(err) + api.HandleErr(w, r, tx, code, usrErr, sysErr) + return + } + + alerts := tc.CreateAlerts(tc.SuccessLevel, "profile was created.") + w.Header().Set("Location", fmt.Sprintf("/api/%d.%d/profiles?id=%d", inf.Version.Major, inf.Version.Minor, profile.ID)) + api.WriteAlertsObj(w, r, http.StatusCreated, alerts, profile) + return +} + +// Update a profile for APIv5 +func Update(w http.ResponseWriter, r *http.Request) { + inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"}) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + tx := inf.Tx.Tx + profile, readValErr := readAndValidateJsonStruct(r) + if readValErr != nil { + api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil) + return + } + + //check if user can modify. + if len(strings.TrimSpace(profile.CDNName)) != 0 || profile.CDNID != 0 { + userErr, sysErr, statusCode := canProfileBeAlteredByCurrentUser(inf.User.UserName, inf.Tx.Tx, &profile.CDNName, &profile.CDNID) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx, statusCode, userErr, sysErr) + return + } + } + + requestedProfileId := inf.IntParams["id"] + // check if the entity was already updated + userErr, sysErr, errCode = api.CheckIfUnModified(r.Header, inf.Tx, requestedProfileId, "profile") + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx, errCode, userErr, sysErr) + return + } + + //update profile + query := `UPDATE profile SET + name = $2, + cdn = $3, + type = $4, + routing_disabled = $5, + description = $6 + WHERE id = $1 + RETURNING id, last_updated, name, description, (select name FROM cdn where id = $3), cdn, routing_disabled, type` + + err := tx.QueryRow(query, requestedProfileId, profile.Name, profile.CDNID, profile.Type, profile.RoutingDisabled, profile.Description). + Scan(&profile.ID, &profile.LastUpdated, &profile.Name, &profile.Description, &profile.CDNName, &profile.CDNID, &profile.RoutingDisabled, &profile.Type) + + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + api.HandleErr(w, r, tx, http.StatusBadRequest, fmt.Errorf("profile: %s not found", profile.Name), nil) + return + } + usrErr, sysErr, code := api.ParseDBError(err) + api.HandleErr(w, r, tx, code, usrErr, sysErr) + return + } + + alerts := tc.CreateAlerts(tc.SuccessLevel, "profile was updated") + api.WriteAlertsObj(w, r, http.StatusOK, alerts, profile) + return +} + +// Delete an profile for APIv5 +func Delete(w http.ResponseWriter, r *http.Request) { + inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) + tx := inf.Tx.Tx + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + id := inf.Params["id"] Review Comment: This should be a required parameter as passed into `api.NewInfo`. If that is done, then it can also be specified as an integral parameter, which will allow you to access it as `inf.IntParams["id"]`, meaning you can skip the `strconv.Atoi` step. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
