This is an automated email from the ASF dual-hosted git repository.

zrhoffman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new 6cab395c8c TO Fixes Region V5 apis to respond with RFC3339 date/time 
Format (#7698)
6cab395c8c is described below

commit 6cab395c8c2ca56147a274721ebe79ad71ed2a9b
Author: Kannan.G.B <[email protected]>
AuthorDate: Wed Aug 9 22:29:16 2023 +0530

    TO Fixes Region V5 apis to respond with RFC3339 date/time Format (#7698)
    
    * region v5 api
    
    * change log and doc
    
    * region comments addresed
    
    * response error fix
    
    * comments addressed
    
    * comment addressed
    
    * removed  stray pipe
    
    ---------
    
    Co-authored-by: Zach Hoffman <[email protected]>
---
 CHANGELOG.md                                       |   1 +
 docs/source/api/v5/regions.rst                     |  25 +-
 docs/source/api/v5/regions_id.rst                  |  15 +-
 lib/go-tc/regions.go                               |  24 ++
 traffic_ops/testing/api/v5/regions_test.go         |  38 ++-
 traffic_ops/testing/api/v5/traffic_control_test.go |   2 +-
 traffic_ops/traffic_ops_golang/region/regions.go   | 264 +++++++++++++++++++++
 traffic_ops/traffic_ops_golang/routing/routes.go   |   8 +-
 traffic_ops/v5-client/region.go                    |   8 +-
 9 files changed, 330 insertions(+), 55 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f985538b42..cf9ab93930 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -145,6 +145,7 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
 - [#7628](https://github.com/apache/trafficcontrol/pull/7628) *Traffic Ops* 
Fixes an issue where certificate chain validation failed based on leading or 
trailing whitespace.
 - [#7596](https://github.com/apache/trafficcontrol/pull/7596) *Traffic Ops* 
Fixes `federation_resolvers` v5 apis to respond with `RFC3339` date/time Format.
 - [#7660](https://github.com/apache/trafficcontrol/pull/7660) *Traffic Ops* 
Fixes `deliveryServices` v5 apis to respond with `RFC3339` date/time Format.
+- [#7698](https://github.com/apache/trafficcontrol/pull/7698) *Traffic Ops* 
Fixes `region` v5 apis to respond with `RFC3339` date/time Format.
 - [#7686](https://github.com/apache/trafficcontrol/pull/7686) *Traffic Ops* 
Fixes secured parameters being visible when role has proper permissions.
 - [#7697](https://github.com/apache/trafficcontrol/pull/7697) *Traffic Ops* 
Fixes `iloPassword` and `xmppPassword` checking for priv-level instead of using 
permissions.
 
diff --git a/docs/source/api/v5/regions.rst b/docs/source/api/v5/regions.rst
index 6f13234871..13a8984168 100644
--- a/docs/source/api/v5/regions.rst
+++ b/docs/source/api/v5/regions.rst
@@ -41,8 +41,7 @@ Request Structure
        
+-----------+----------+---------------------------------------------------------------------------------------------------------------+
        | name      | no       | Filter :term:`Regions` by name                 
                                                               |
        
+-----------+----------+---------------------------------------------------------------------------------------------------------------+
-       | orderby   | no       | Choose the ordering of the results - must be 
the name of one of the fields of the objects in the ``response`` |
-       |           |          | array                                          
                                                               |
+       | orderby   | no       | Choose the ordering of the results - either 
one of them "division", "id", "name"                              |
        
+-----------+----------+---------------------------------------------------------------------------------------------------------------+
        | sortOrder | no       | Changes the order of sorting. Either ascending 
(default or "asc") or descending ("desc")                      |
        
+-----------+----------+---------------------------------------------------------------------------------------------------------------+
@@ -67,9 +66,9 @@ Request Structure
 Response Structure
 -------------------
 :divisionName: The name of the division which contains this region
-:divisionId:   The integral, unique identifier of the division which contains 
this region
+:division:   The integral, unique identifier of the division which contains 
this region
 :id:           An integral, unique identifier for this region
-:lastUpdated:  The date and time at which this region was last updated, in 
:ref:`non-rfc-datetime`
+:lastUpdated:  The date and time at which this region was last updated in 
:rfc:`3339` Format
 :name:         The region name
 
 .. code-block:: http
@@ -92,7 +91,7 @@ Response Structure
                        "divisionName": "Quebec",
                        "division": 1,
                        "id": 2,
-                       "lastUpdated": "2018-12-05 17:50:58+00",
+                       "lastUpdated": "2023-05-25T15:59:33.7096-06:00",
                        "name": "Montreal"
                }
        ]}
@@ -110,8 +109,7 @@ Creates a new region
 
 Request Structure
 -----------------
-:division:     The integral, unique identifier of the division which shall 
contain the new region\ [1]_
-:divisionName: The name of the division which shall contain the new region\ 
[1]_
+:division:     The integral, unique identifier of the division which shall 
contain the new region
 :name:         The name of the region
 
 .. code-block:: http
@@ -126,19 +124,16 @@ Request Structure
        Content-Type: application/json
 
        {
-               "name": "Manchester",
-               "division": "4",
-               "divisionName": "England"
+               "division": 4,
+               "name": "Manchester"
        }
 
-.. [1] The only "division" key that actually matters in the request body is 
``division``; ``divisionName`` is not validated and has no effect - 
particularly not the effect of re-naming the division - beyond changing the 
name in the API response to this request. Subsequent requests will reveal the 
true name of the division. Note that if ``divisionName`` is not present in the 
request body it will be ``null`` in the response, but again further requests 
will show the true division name (prov [...]
-
 Response Structure
 ------------------
 :divisionName: The name of the division which contains this region
-:divisionId:   The integral, unique identifier of the division which contains 
this region
+:division:   The integral, unique identifier of the division which contains 
this region
 :id:           An integral, unique identifier for this region
-:lastUpdated:  The date and time at which this region was last updated, in 
:ref:`non-rfc-datetime`
+:lastUpdated:  The date and time at which this region was last updated in 
:rfc:`3339` Format
 :name:         The region name
 
 .. code-block:: http
@@ -166,7 +161,7 @@ Response Structure
                "divisionName": "England",
                "division": 3,
                "id": 5,
-               "lastUpdated": "2018-12-06 02:14:45+00",
+               "lastUpdated": "2023-05-25T15:59:33.7096-06:00",
                "name": "Manchester"
        }}
 
diff --git a/docs/source/api/v5/regions_id.rst 
b/docs/source/api/v5/regions_id.rst
index ed92a29d8c..ca8e8a81d1 100644
--- a/docs/source/api/v5/regions_id.rst
+++ b/docs/source/api/v5/regions_id.rst
@@ -38,8 +38,7 @@ Request Structure
        |  ID  | The integral, unique identifier of the region to update |
        +------+---------------------------------------------------------+
 
-:division:     The new integral, unique identifier of the division which shall 
contain the region\ [1]_
-:divisionName: The new name of the division which shall contain the region\ 
[1]_
+:division:     The new integral, unique identifier of the division which shall 
contain the region
 :name:         The new name of the region
 
 .. code-block:: http
@@ -54,20 +53,16 @@ Request Structure
        Content-Type: application/json
 
        {
-               "name": "Leeds",
                "division": 3,
-               "divisionName": "England"
+               "name": "Leeds"
        }
 
-.. [1] The only "division" key that actually matters in the request body is 
``division``; ``divisionName`` is not validated and has no effect - 
particularly not the effect of re-naming the division - beyond changing the 
name in the API response to this request. Subsequent requests will reveal the 
true name of the division. Note that if ``divisionName`` is not present in the 
request body it will be ``null`` in the response, but again further requests 
will show the true division name (prov [...]
-
-
 Response Structure
 ------------------
 :divisionName: The name of the division which contains this region
-:divisionId:   The integral, unique identifier of the division which contains 
this region
+:division:   The integral, unique identifier of the division which contains 
this region
 :id:           An integral, unique identifier for this region
-:lastUpdated:  The date and time at which this region was last updated, in 
:ref:`non-rfc-datetime`
+:lastUpdated:  The date and time at which this region was last updated in 
:rfc:`3339` Format
 :name:         The region name
 
 .. code-block:: http
@@ -95,6 +90,6 @@ Response Structure
                "divisionName": "England",
                "division": 3,
                "id": 5,
-               "lastUpdated": "2018-12-06 02:23:40+00",
+               "lastUpdated": "2023-05-26T15:59:33.7096-06:00",
                "name": "Leeds"
        }}
diff --git a/lib/go-tc/regions.go b/lib/go-tc/regions.go
index fd38082335..5d3a290501 100644
--- a/lib/go-tc/regions.go
+++ b/lib/go-tc/regions.go
@@ -21,6 +21,9 @@ package tc
 
 // RegionsResponse is the type of responses from Traffic Ops to GET requests
 // made to its /regions API endpoint.
+
+import "time"
+
 type RegionsResponse struct {
        Response []Region `json:"response"`
        Alerts
@@ -35,6 +38,27 @@ type Region struct {
        Name         string    `json:"name" db:"name"`
 }
 
+// RegionsResponseV5 is an alias for the latest minor version of the major 
version 5.
+type RegionsResponseV5 = RegionsResponseV50
+
+// RegionsResponseV50 is response made from /regions API endpoint - in the 
latest minor version APIv50.
+type RegionsResponseV50 struct {
+       Response []RegionV5 `json:"response"`
+       Alerts
+}
+
+// RegionV5 is an alias for the latest minor version of the major version 5.
+type RegionV5 = RegionV50
+
+// RegionV50 is a named collection of Physical Locations within a Division in 
the latest minor version APIv50.
+type RegionV50 struct {
+       DivisionName string    `json:"divisionName"`
+       Division     int       `json:"division" db:"division"`
+       ID           int       `json:"id" db:"id"`
+       LastUpdated  time.Time `json:"lastUpdated" db:"last_updated"`
+       Name         string    `json:"name" db:"name"`
+}
+
 // RegionName is a response to a request to get a region by its name. It
 // includes the division that the region is in.
 type RegionName struct {
diff --git a/traffic_ops/testing/api/v5/regions_test.go 
b/traffic_ops/testing/api/v5/regions_test.go
index e30e7b86fe..f031e542fd 100644
--- a/traffic_ops/testing/api/v5/regions_test.go
+++ b/traffic_ops/testing/api/v5/regions_test.go
@@ -38,7 +38,7 @@ func TestRegions(t *testing.T) {
                currentTimeRFC := currentTime.Format(time.RFC1123)
                tomorrow := currentTime.AddDate(0, 0, 1).Format(time.RFC1123)
 
-               methodTests := utils.TestCase[client.Session, 
client.RequestOptions, tc.Region]{
+               methodTests := utils.TestCase[client.Session, 
client.RequestOptions, tc.RegionV5]{
                        "GET": {
                                "NOT MODIFIED when NO CHANGES made": {
                                        ClientSession: TOSession,
@@ -121,10 +121,9 @@ func TestRegions(t *testing.T) {
                        "POST": {
                                "NOT FOUND when DIVISION DOESNT EXIST": {
                                        ClientSession: TOSession,
-                                       RequestBody: tc.Region{
-                                               Name:         "invalidDivision",
-                                               Division:     99999999,
-                                               DivisionName: "doesntexist",
+                                       RequestBody: tc.RegionV5{
+                                               Name:     "invalidDivision",
+                                               Division: 99999999,
                                        },
                                        Expectations: 
utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusNotFound)),
                                },
@@ -133,10 +132,9 @@ func TestRegions(t *testing.T) {
                                "OK when VALID request": {
                                        EndpointID:    GetRegionID(t, 
"cdn-region2"),
                                        ClientSession: TOSession,
-                                       RequestBody: tc.Region{
-                                               Name:         "newName",
-                                               Division:     GetDivisionID(t, 
"cdn-div2")(),
-                                               DivisionName: "cdn-div2",
+                                       RequestBody: tc.RegionV5{
+                                               Name:     "newName",
+                                               Division: GetDivisionID(t, 
"cdn-div2")(),
                                        },
                                        Expectations: 
utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK),
                                                
validateRegionsUpdateCreateFields("newName", map[string]interface{}{"Name": 
"newName"})),
@@ -145,20 +143,18 @@ func TestRegions(t *testing.T) {
                                        EndpointID:    GetRegionID(t, 
"region1"),
                                        ClientSession: TOSession,
                                        RequestOpts:   
client.RequestOptions{Header: http.Header{rfc.IfUnmodifiedSince: 
{currentTimeRFC}}},
-                                       RequestBody: tc.Region{
-                                               Name:         "newName",
-                                               Division:     GetDivisionID(t, 
"division1")(),
-                                               DivisionName: "division1",
+                                       RequestBody: tc.RegionV5{
+                                               Name:     "newName",
+                                               Division: GetDivisionID(t, 
"division1")(),
                                        },
                                        Expectations: 
utils.CkRequest(utils.HasError(), 
utils.HasStatus(http.StatusPreconditionFailed)),
                                },
                                "PRECONDITION FAILED when updating with IFMATCH 
ETAG Header": {
                                        EndpointID:    GetRegionID(t, 
"region1"),
                                        ClientSession: TOSession,
-                                       RequestBody: tc.Region{
-                                               Name:         "newName",
-                                               Division:     GetDivisionID(t, 
"division1")(),
-                                               DivisionName: "division1",
+                                       RequestBody: tc.RegionV5{
+                                               Name:     "newName",
+                                               Division: GetDivisionID(t, 
"division1")(),
                                        },
                                        RequestOpts:  
client.RequestOptions{Header: http.Header{rfc.IfMatch: 
{rfc.ETag(currentTime)}}},
                                        Expectations: 
utils.CkRequest(utils.HasError(), 
utils.HasStatus(http.StatusPreconditionFailed)),
@@ -229,7 +225,7 @@ func TestRegions(t *testing.T) {
 func validateRegionsFields(expectedResp map[string]interface{}) 
utils.CkReqFunc {
        return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ 
tc.Alerts, _ error) {
                assert.RequireNotNil(t, resp, "Expected Regions response to not 
be nil.")
-               regionResp := resp.([]tc.Region)
+               regionResp := resp.([]tc.RegionV5)
                for field, expected := range expectedResp {
                        for _, region := range regionResp {
                                switch field {
@@ -264,7 +260,7 @@ func validateRegionsSort() utils.CkReqFunc {
        return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, 
alerts tc.Alerts, _ error) {
                assert.RequireNotNil(t, resp, "Expected Regions response to not 
be nil.")
                var regionNames []string
-               regionResp := resp.([]tc.Region)
+               regionResp := resp.([]tc.RegionV5)
                for _, region := range regionResp {
                        regionNames = append(regionNames, region.Name)
                }
@@ -275,7 +271,7 @@ func validateRegionsSort() utils.CkReqFunc {
 func validateRegionsDescSort() utils.CkReqFunc {
        return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, 
alerts tc.Alerts, _ error) {
                assert.RequireNotNil(t, resp, "Expected Regions response to not 
be nil.")
-               regionDescResp := resp.([]tc.Region)
+               regionDescResp := resp.([]tc.RegionV5)
                var descSortedList []string
                var ascSortedList []string
                assert.RequireGreaterOrEqual(t, len(regionDescResp), 2, "Need 
at least 2 Regions in Traffic Ops to test desc sort, found: %d", 
len(regionDescResp))
@@ -298,7 +294,7 @@ func validateRegionsDescSort() utils.CkReqFunc {
 
 func validateRegionsPagination(paginationParam string) utils.CkReqFunc {
        return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ 
tc.Alerts, _ error) {
-               paginationResp := resp.([]tc.Region)
+               paginationResp := resp.([]tc.RegionV5)
 
                opts := client.NewRequestOptions()
                opts.QueryParameters.Set("orderby", "id")
diff --git a/traffic_ops/testing/api/v5/traffic_control_test.go 
b/traffic_ops/testing/api/v5/traffic_control_test.go
index 1ed3ad0b05..4263f50faa 100644
--- a/traffic_ops/testing/api/v5/traffic_control_test.go
+++ b/traffic_ops/testing/api/v5/traffic_control_test.go
@@ -43,7 +43,7 @@ type TrafficControl struct {
        Parameters                                        []tc.Parameter        
                  `json:"parameters"`
        ProfileParameters                                 []tc.ProfileParameter 
                  `json:"profileParameters"`
        PhysLocations                                     []tc.PhysLocationV5   
                  `json:"physLocations"`
-       Regions                                           []tc.Region           
                  `json:"regions"`
+       Regions                                           []tc.RegionV5         
                  `json:"regions"`
        Roles                                             []tc.RoleV4           
                  `json:"roles"`
        Servers                                           []tc.ServerV4         
                  `json:"servers"`
        ServerServerCapabilities                          
[]tc.ServerServerCapability             `json:"serverServerCapabilities"`
diff --git a/traffic_ops/traffic_ops_golang/region/regions.go 
b/traffic_ops/traffic_ops_golang/region/regions.go
index 79fd9ccbe4..5ce193057d 100644
--- a/traffic_ops/traffic_ops_golang/region/regions.go
+++ b/traffic_ops/traffic_ops_golang/region/regions.go
@@ -20,14 +20,22 @@ package region
  */
 
 import (
+       "database/sql"
+       "encoding/json"
        "errors"
        "fmt"
        "net/http"
        "time"
 
+       "github.com/apache/trafficcontrol/lib/go-log"
        "github.com/apache/trafficcontrol/lib/go-tc"
+       "github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
+       "github.com/apache/trafficcontrol/lib/go-util"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
+       
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/ims"
+
+       validation "github.com/go-ozzo/ozzo-validation"
 )
 
 // we need a type alias to define functions on
@@ -192,3 +200,259 @@ func deleteQuery() string {
        query := deleteQueryBase() + ` WHERE id=:id`
        return query
 }
+
+// Read is the handler for GET requests to Regions of APIv5
+func Read(w http.ResponseWriter, r *http.Request) {
+       var maxTime time.Time
+       var runSecond bool
+
+       inf, sysErr, userErr, errCode := api.NewInfo(r, nil, nil)
+       tx := inf.Tx
+       if sysErr != nil || userErr != 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{
+               "division": {Column: "r.division"},
+               "id":       {Column: "r.id", Checker: api.IsInt},
+               "name":     {Column: "r.name"},
+       }
+       if _, ok := inf.Params["orderby"]; !ok {
+               inf.Params["orderby"] = "name"
+       }
+       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 := `SELECT r.division, d.name as divisionname, r.id, 
r.last_updated, r.name FROM region r
+                               JOIN division d ON r.division = d.id` + where + 
orderBy + pagination
+       rows, err := tx.NamedQuery(query, queryValues)
+       if err != nil {
+               api.HandleErr(w, r, tx.Tx, http.StatusInternalServerError, nil, 
fmt.Errorf("region get: error getting region(s): %w", err))
+       }
+       defer log.Close(rows, "unable to close DB connection")
+
+       typ := tc.RegionV5{}
+       regionList := []tc.RegionV5{}
+       for rows.Next() {
+               if err = rows.Scan(&typ.Division, &typ.DivisionName, &typ.ID, 
&typ.LastUpdated, &typ.Name); err != nil {
+                       api.HandleErr(w, r, tx.Tx, 
http.StatusInternalServerError, nil, fmt.Errorf("error getting region(s): %w", 
err))
+               }
+               regionList = append(regionList, typ)
+       }
+
+       api.WriteResp(w, r, regionList)
+       return
+}
+
+// Create region with the passed data 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
+       rg, readValErr := readAndValidateJsonStruct(r)
+       if readValErr != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil)
+               return
+       }
+
+       var exists bool
+       existErr := tx.QueryRow(`SELECT EXISTS(SELECT * from region where name 
= $1)`, rg.Name).Scan(&exists)
+
+       if existErr != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
fmt.Errorf("error: %w, when checking if region with name %s exists", existErr, 
rg.Name))
+               return
+       }
+       if exists {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, 
fmt.Errorf("region name '%s' already exists", rg.Name), nil)
+               return
+       }
+
+       query := `INSERT INTO region ( division, name) VALUES ( $1, $2 ) 
+                               RETURNING 
+                                       id,last_updated,
+                                       (SELECT d.name FROM division d WHERE 
d.id = region.division)`
+       err := tx.QueryRow(query, rg.Division, rg.Name).Scan(&rg.ID, 
&rg.LastUpdated, &rg.DivisionName)
+       if err != nil {
+               if errors.Is(err, sql.ErrNoRows) {
+                       api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("error: %w in creating region with name: %s", err, rg.Name), nil)
+                       return
+               }
+               usrErr, sysErr, code := api.ParseDBError(err)
+               api.HandleErr(w, r, tx, code, usrErr, sysErr)
+               return
+       }
+       alerts := tc.CreateAlerts(tc.SuccessLevel, "region is created.")
+       api.WriteAlertsObj(w, r, http.StatusCreated, alerts, rg)
+       return
+}
+
+// Update a Region 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
+       rg, readValErr := readAndValidateJsonStruct(r)
+       if readValErr != nil {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, readValErr, nil)
+               return
+       }
+
+       requestedRegionId := inf.IntParams["id"]
+       // check if the entity was already updated
+       userErr, sysErr, errCode = api.CheckIfUnModified(r.Header, inf.Tx, 
requestedRegionId, "region")
+       if userErr != nil || sysErr != nil {
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+
+       query := `UPDATE region SET division= $1, name= $2 WHERE id= $3 
+                               RETURNING 
+                                       id,last_updated,
+                                       (SELECT d.name FROM division d WHERE 
d.id = region.division)`
+
+       err := tx.QueryRow(query, rg.Division, rg.Name, 
requestedRegionId).Scan(&rg.ID, &rg.LastUpdated, &rg.DivisionName)
+       if err != nil {
+               if errors.Is(err, sql.ErrNoRows) {
+                       api.HandleErr(w, r, tx, http.StatusBadRequest, 
fmt.Errorf("region: %d not found", requestedRegionId), nil)
+                       return
+               }
+               usrErr, sysErr, code := api.ParseDBError(err)
+               api.HandleErr(w, r, tx, code, usrErr, sysErr)
+               return
+       }
+
+       alerts := tc.CreateAlerts(tc.SuccessLevel, "region was updated")
+       api.WriteAlertsObj(w, r, http.StatusOK, alerts, rg)
+       return
+}
+
+// Delete an Region 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()
+
+       requestedRegionId := inf.Params["id"]
+       requestedRegionName := inf.Params["name"]
+
+       var exists bool
+       var noExistsErrMsg string
+
+       if requestedRegionId == "" && requestedRegionName == "" {
+               api.HandleErr(w, r, tx, http.StatusBadRequest, 
fmt.Errorf("refusing to delete all resources of type Region"), nil)
+               return
+       } else if requestedRegionId != "" && requestedRegionName != "" { // 
checking if both id and name are passed
+               existErr := tx.QueryRow(`SELECT EXISTS(SELECT * from region 
where id = $1 AND name = $2)`, requestedRegionId, 
requestedRegionName).Scan(&exists)
+               noExistsErrMsg = fmt.Sprintf("no region exists by id: %s and 
name: %s", requestedRegionId, requestedRegionName)
+               if existErr != nil {
+                       api.HandleErr(w, r, tx, http.StatusInternalServerError, 
nil, fmt.Errorf("error: %w, when checking if region with id %s exists", 
existErr, requestedRegionId))
+                       return
+               }
+       } else if requestedRegionId != "" {
+               existErr := tx.QueryRow(`SELECT EXISTS(SELECT * from region 
where id = $1)`, requestedRegionId).Scan(&exists)
+               noExistsErrMsg = fmt.Sprintf("no region exists by id: %s", 
requestedRegionId)
+               if existErr != nil {
+                       api.HandleErr(w, r, tx, http.StatusInternalServerError, 
nil, fmt.Errorf("error: %w, when checking if region with id %s exists", 
existErr, requestedRegionId))
+                       return
+               }
+       } else if requestedRegionName != "" {
+               existErr := tx.QueryRow(`SELECT EXISTS(SELECT * from region 
where name = $1)`, requestedRegionName).Scan(&exists)
+               noExistsErrMsg = fmt.Sprintf("no region exists by name: %s", 
requestedRegionName)
+               if existErr != nil {
+                       api.HandleErr(w, r, tx, http.StatusInternalServerError, 
nil, fmt.Errorf("error: %w, when checking if region with name %s exists", 
existErr, requestedRegionName))
+                       return
+               }
+               if exists {
+                       existErr := tx.QueryRow(`SELECT id from region where 
name = $1`, requestedRegionName).Scan(&requestedRegionId)
+                       if existErr != nil {
+                               api.HandleErr(w, r, tx, 
http.StatusInternalServerError, nil, fmt.Errorf("error: %w, when checking if 
region with name %s exists", existErr, requestedRegionName))
+                               return
+                       }
+               }
+       }
+
+       if !exists {
+               api.HandleErr(w, r, tx, http.StatusNotFound, 
fmt.Errorf(noExistsErrMsg), nil)
+               return
+       }
+
+       res, err := tx.Exec("DELETE FROM region WHERE id = $1", 
requestedRegionId)
+
+       if err != nil {
+               userErr, sysErr, errCode := api.ParseDBError(err)
+               api.HandleErr(w, r, tx, errCode, userErr, sysErr)
+               return
+       }
+       rowsAffected, err := res.RowsAffected()
+       if err != nil {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, 
fmt.Errorf("determining rows affected for delete region: %w", err))
+               return
+       }
+       if rowsAffected == 0 {
+               api.HandleErr(w, r, tx, http.StatusInternalServerError, 
fmt.Errorf("no rows deleted for region"), nil)
+               return
+       }
+       alerts := tc.CreateAlerts(tc.SuccessLevel, "region was deleted.")
+       api.WriteAlerts(w, r, http.StatusOK, alerts)
+       return
+}
+
+// readAndValidateJsonStruct reads json body and validates json fields
+func readAndValidateJsonStruct(r *http.Request) (tc.RegionV5, error) {
+       var region tc.RegionV5
+       if err := json.NewDecoder(r.Body).Decode(&region); err != nil {
+               userErr := fmt.Errorf("error decoding POST request body into 
RegionV5 struct %w", err)
+               return region, userErr
+       }
+
+       // validate JSON body
+       errs := tovalidate.ToErrors(validation.Errors{
+               "division": validation.Validate(region.Division, 
validation.NotNil, validation.Min(0)),
+               "name":     validation.Validate(region.Name, 
validation.Required),
+       })
+       if len(errs) > 0 {
+               userErr := util.JoinErrs(errs)
+               return region, userErr
+       }
+       return region, nil
+}
+
+// SelectMaxLastUpdatedQuery used for TryIfModifiedSinceQuery()
+func SelectMaxLastUpdatedQuery(where string) string {
+       return `SELECT max(t) from (
+               SELECT max(r.last_updated) as t FROM region r
+JOIN division d ON r.division = d.id ` + where +
+               ` UNION ALL
+       select max(last_updated) as t from last_deleted l where 
l.table_name='region') as res`
+}
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go 
b/traffic_ops/traffic_ops_golang/routing/routes.go
index f62a77db47..26b5a0c171 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -278,10 +278,10 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
                {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodPost, Path: `profiles/name/{new_profile}/copy/{existing_profile}`, 
Handler: profile.CopyProfileHandler, RequiredPrivLevel: 
auth.PrivLevelOperations, RequiredPermissions: []string{"PROFILE:CREATE", 
"PROFILE:READ", "PARAMETER:READ"}, Authenticated: Authenticated, Middlewares: 
nil, ID: 40614320931},
 
                //Region: CRUDs
-               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodGet, Path: `regions/?$`, Handler: 
api.ReadHandler(&region.TORegion{}), RequiredPrivLevel: auth.PrivLevelReadOnly, 
RequiredPermissions: []string{"REGION:READ"}, Authenticated: Authenticated, 
Middlewares: nil, ID: 41003708531},
-               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodPut, Path: `regions/{id}$`, Handler: 
api.UpdateHandler(&region.TORegion{}), RequiredPrivLevel: 
auth.PrivLevelOperations, RequiredPermissions: []string{"REGION:UPDATE", 
"REGION:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 
42230822431},
-               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodPost, Path: `regions/?$`, Handler: 
api.CreateHandler(&region.TORegion{}), RequiredPrivLevel: 
auth.PrivLevelOperations, RequiredPermissions: []string{"REGION:CREATE", 
"REGION:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 
428833448831},
-               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodDelete, Path: `regions/?$`, Handler: 
api.DeleteHandler(&region.TORegion{}), RequiredPrivLevel: 
auth.PrivLevelOperations, RequiredPermissions: []string{"REGION:DELETE"}, 
Authenticated: Authenticated, Middlewares: nil, ID: 423262675831},
+               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodGet, Path: `regions/?$`, Handler: region.Read, RequiredPrivLevel: 
auth.PrivLevelReadOnly, RequiredPermissions: []string{"REGION:READ"}, 
Authenticated: Authenticated, Middlewares: nil, ID: 41003708531},
+               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodPut, Path: `regions/{id}$`, Handler: region.Update, 
RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: 
[]string{"REGION:UPDATE", "REGION:READ"}, Authenticated: Authenticated, 
Middlewares: nil, ID: 42230822431},
+               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodPost, Path: `regions/?$`, Handler: region.Create, RequiredPrivLevel: 
auth.PrivLevelOperations, RequiredPermissions: []string{"REGION:CREATE", 
"REGION:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 
428833448831},
+               {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodDelete, Path: `regions/?$`, Handler: region.Delete, 
RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: 
[]string{"REGION:DELETE"}, Authenticated: Authenticated, Middlewares: nil, ID: 
423262675831},
 
                {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodPost, Path: `topologies/?$`, Handler: 
api.CreateHandler(&topology.TOTopology{}), RequiredPrivLevel: 
auth.PrivLevelOperations, RequiredPermissions: []string{"TOPOLOGY:CREATE", 
"TOPOLOGY:READ", "CACHE-GROUP:READ"}, Authenticated: Authenticated, 
Middlewares: nil, ID: 48714522211},
                {Version: api.Version{Major: 5, Minor: 0}, Method: 
http.MethodGet, Path: `topologies/?$`, Handler: 
api.ReadHandler(&topology.TOTopology{}), RequiredPrivLevel: 
auth.PrivLevelReadOnly, RequiredPermissions: []string{"TOPOLOGY:READ", 
"CACHE-GROUP:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 
48714522221},
diff --git a/traffic_ops/v5-client/region.go b/traffic_ops/v5-client/region.go
index 7ef85216bf..e7c2edaa12 100644
--- a/traffic_ops/v5-client/region.go
+++ b/traffic_ops/v5-client/region.go
@@ -28,7 +28,7 @@ import (
 const apiRegions = "/regions"
 
 // CreateRegion creates the given Region.
-func (to *Session) CreateRegion(region tc.Region, opts RequestOptions) 
(tc.Alerts, toclientlib.ReqInf, error) {
+func (to *Session) CreateRegion(region tc.RegionV5, opts RequestOptions) 
(tc.Alerts, toclientlib.ReqInf, error) {
        if region.Division == 0 && region.DivisionName != "" {
                divisionOpts := NewRequestOptions()
                divisionOpts.QueryParameters.Set("name", region.DivisionName)
@@ -47,7 +47,7 @@ func (to *Session) CreateRegion(region tc.Region, opts 
RequestOptions) (tc.Alert
 }
 
 // UpdateRegion replaces the Region identified by ID with the one provided.
-func (to *Session) UpdateRegion(id int, region tc.Region, opts RequestOptions) 
(tc.Alerts, toclientlib.ReqInf, error) {
+func (to *Session) UpdateRegion(id int, region tc.RegionV5, opts 
RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) {
        route := fmt.Sprintf("%s/%d", apiRegions, id)
        var alerts tc.Alerts
        reqInf, err := to.put(route, opts, region, &alerts)
@@ -55,8 +55,8 @@ func (to *Session) UpdateRegion(id int, region tc.Region, 
opts RequestOptions) (
 }
 
 // GetRegions returns all Regions in Traffic Ops.
-func (to *Session) GetRegions(opts RequestOptions) (tc.RegionsResponse, 
toclientlib.ReqInf, error) {
-       var data tc.RegionsResponse
+func (to *Session) GetRegions(opts RequestOptions) (tc.RegionsResponseV5, 
toclientlib.ReqInf, error) {
+       var data tc.RegionsResponseV5
        reqInf, err := to.get(apiRegions, opts, &data)
        return data, reqInf, err
 }

Reply via email to