Skip to content

Commit

Permalink
Merge pull request #47 from wepala/WEOS-1246
Browse files Browse the repository at this point in the history
Weos 1246 - Add api  functionality for viewing content type and refactor projections
  • Loading branch information
RandyDeo authored Jan 18, 2022
2 parents 64dad41 + d35fc06 commit bbd6a6f
Show file tree
Hide file tree
Showing 13 changed files with 757 additions and 479 deletions.
14 changes: 9 additions & 5 deletions controllers/rest/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import (
"github.com/labstack/echo/v4/middleware"

"github.com/getkin/kin-openapi/openapi3"
"github.com/wepala/weos-service/model"
"github.com/wepala/weos-service/projections"

"github.com/labstack/echo/v4"
"github.com/labstack/gommon/log"
ds "github.com/ompluscator/dynamic-struct"
"github.com/wepala/weos-service/model"
"github.com/wepala/weos-service/projections"
)

//RESTAPI is used to manage the API
Expand All @@ -35,7 +35,7 @@ type RESTAPI struct {
Config *APIConfig
e *echo.Echo
PathConfigs map[string]*PathConfig
Schemas map[string]interface{}
Schemas map[string]ds.Builder
middlewares map[string]Middleware
controllers map[string]Controller
}
Expand Down Expand Up @@ -117,7 +117,11 @@ func (p *RESTAPI) GetController(name string) (Controller, error) {
}

func (p *RESTAPI) GetSchemas() (map[string]interface{}, error) {
return p.projection.Schema, nil
schemes := map[string]interface{}{}
for name, s := range p.Schemas {
schemes[name] = s.Build().New()
}
return schemes, nil
}

//Initialize and setup configurations for RESTAPI
Expand Down
95 changes: 55 additions & 40 deletions controllers/rest/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,69 +227,56 @@ func TestRESTAPI_Initialize_UpdateAddedToPut(t *testing.T) {
os.Remove("test.db")
}

func TestRESTAPI_Initialize_GetEntityBySequenceNuber(t *testing.T) {
func TestRESTAPI_Initialize_UpdateAddedToPatch(t *testing.T) {
os.Remove("test.db")
time.Sleep(1 * time.Second)
e := echo.New()
tapi := api.RESTAPI{}
_, err := api.Initialize(e, &tapi, "./fixtures/blog-create-batch.yaml")
if err != nil {
t.Fatalf("unexpected error '%s'", err)
}
mockBlog := &[3]Blog{
{ID: "1asdas3", Title: "Blog 1", Url: "www.testBlog1.com"},
{ID: "2gf233", Title: "Blog 2", Url: "www.testBlog2.com"},
{ID: "3dgff3", Title: "Blog 3", Url: "www.testBlog3.com"},
}
mockBlog := &Blog{ID: "1246dg", Title: "Test Blog", Url: "www.testBlog.com"}
reqBytes, err := json.Marshal(mockBlog)
if err != nil {
t.Fatalf("error setting up request %s", err)
}
body := bytes.NewReader(reqBytes)
resp := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/blogs", body)
req := httptest.NewRequest(http.MethodPatch, "/blogs/"+mockBlog.ID, body)
e.ServeHTTP(resp, req)
//confirm that the response is 201
if resp.Result().StatusCode != http.StatusCreated {
t.Errorf("expected the response code to be %d, got %d", http.StatusCreated, resp.Result().StatusCode)
}

blogEntity, err := api.GetContentBySequenceNumber(tapi.Application.EventRepository(), "3dgff3", 4)
if err != nil {
t.Fatal(err)
}

mapEntity, ok := blogEntity.Property.(map[string]interface{})

if !ok {
t.Fatal("expected the properties of the blog entity to be mapable")
}
if mapEntity["title"] != "Blog 3" {
t.Errorf("expected the title to be %s got %s", "Blog 3", mapEntity["title"])
}

if blogEntity.SequenceNo != int64(1) {
t.Errorf("expected the sequence number to be %d got %d", blogEntity.SequenceNo, 1)
//confirm that the response is 200
if resp.Result().StatusCode != http.StatusOK {
t.Errorf("expected the response code to be %d, got %d", http.StatusOK, resp.Result().StatusCode)
}
os.Remove("test.db")
}

func TestRESTAPI_Initialize_UpdateAddedToPatch(t *testing.T) {
func TestRESTAPI_Initialize_ViewAddedToGet(t *testing.T) {
os.Remove("test.db")
e := echo.New()
tapi := api.RESTAPI{}
_, err := api.Initialize(e, &tapi, "./fixtures/blog-create-batch.yaml")
_, err := api.Initialize(e, &tapi, "./fixtures/blog.yaml")
if err != nil {
t.Fatalf("unexpected error '%s'", err)
}
mockBlog := &Blog{ID: "1246dg", Title: "Test Blog", Url: "www.testBlog.com"}

mockID := "1246dg"
mockBlog := &Blog{ID: mockID, Title: "Test Blog", Url: "www.testBlog.com"}
reqBytes, err := json.Marshal(mockBlog)
if err != nil {
t.Fatalf("error setting up request %s", err)
}
body := bytes.NewReader(reqBytes)
resp := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPatch, "/blogs/"+mockBlog.ID, body)
req := httptest.NewRequest(http.MethodPost, "/blogs", body)
e.ServeHTTP(resp, req)
//confirm that the response is 200
if resp.Result().StatusCode != http.StatusCreated {
t.Fatalf("expected the response code to be %d, got %d", http.StatusCreated, resp.Result().StatusCode)
}

resp = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "/blogs/1", nil)
e.ServeHTTP(resp, req)
//confirm that the response is 200
if resp.Result().StatusCode != http.StatusOK {
Expand All @@ -298,21 +285,49 @@ func TestRESTAPI_Initialize_UpdateAddedToPatch(t *testing.T) {
os.Remove("test.db")
}

func TestRESTAPI_Initialize_ViewAddedToGet(t *testing.T) {
func TestRESTAPI_Initialize_GetEntityBySequenceNuber(t *testing.T) {
os.Remove("test.db")
time.Sleep(1 * time.Second)
e := echo.New()
tapi := api.RESTAPI{}
_, err := api.Initialize(e, &tapi, "./fixtures/blog.yaml")
_, err := api.Initialize(e, &tapi, "./fixtures/blog-create-batch.yaml")
if err != nil {
t.Fatalf("unexpected error '%s'", err)
}
mockID := "1246dg"
mockBlog := &[3]Blog{
{ID: "1asdas3", Title: "Blog 1", Url: "www.testBlog1.com"},
{ID: "2gf233", Title: "Blog 2", Url: "www.testBlog2.com"},
{ID: "3dgff3", Title: "Blog 3", Url: "www.testBlog3.com"},
}
reqBytes, err := json.Marshal(mockBlog)
if err != nil {
t.Fatalf("error setting up request %s", err)
}
body := bytes.NewReader(reqBytes)
resp := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/blogs/"+mockID, nil)
req := httptest.NewRequest(http.MethodPost, "/blogs", body)
e.ServeHTTP(resp, req)
//confirm that the response is 200
if resp.Result().StatusCode != http.StatusOK {
t.Errorf("expected the response code to be %d, got %d", http.StatusOK, resp.Result().StatusCode)
//confirm that the response is 201
if resp.Result().StatusCode != http.StatusCreated {
t.Errorf("expected the response code to be %d, got %d", http.StatusCreated, resp.Result().StatusCode)
}

blogEntity, err := api.GetContentBySequenceNumber(tapi.Application.EventRepository(), "3dgff3", 4)
if err != nil {
t.Fatal(err)
}

mapEntity, ok := blogEntity.Property.(map[string]interface{})

if !ok {
t.Fatal("expected the properties of the blog entity to be mapable")
}
if mapEntity["title"] != "Blog 3" {
t.Errorf("expected the title to be %s got %s", "Blog 3", mapEntity["title"])
}

if blogEntity.SequenceNo != int64(1) {
t.Errorf("expected the sequence number to be %d got %d", blogEntity.SequenceNo, 1)
}
os.Remove("test.db")
}
Expand Down
134 changes: 132 additions & 2 deletions controllers/rest/controller_standard.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package rest

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"

"github.com/segmentio/ksuid"
context2 "github.com/wepala/weos-service/context"
"golang.org/x/net/context"
"gorm.io/gorm"

"github.com/getkin/kin-openapi/openapi3"
"github.com/labstack/echo/v4"
Expand Down Expand Up @@ -42,7 +47,20 @@ func (c *StandardControllers) Create(app model.Service, spec *openapi3.Swagger,
}
//reads the request body
payload, _ := ioutil.ReadAll(ctxt.Request().Body)
weosID := ksuid.New().String()

//for inserting weos_id during testing
payMap := map[string]interface{}{}
var weosID string

json.Unmarshal(payload, &payMap)
if v, ok := payMap["weos_id"]; ok {
if val, ok := v.(string); ok {
weosID = val
}
}
if weosID == "" {
weosID = ksuid.New().String()
}

err := app.Dispatcher().Dispatch(newContext, model.Create(newContext, payload, contentType, weosID))
if err != nil {
Expand Down Expand Up @@ -153,9 +171,121 @@ func (c *StandardControllers) BulkUpdate(app model.Service, spec *openapi3.Swagg
}

func (c *StandardControllers) View(app model.Service, spec *openapi3.Swagger, path *openapi3.PathItem, operation *openapi3.Operation) echo.HandlerFunc {
var contentType string
var contentTypeSchema *openapi3.SchemaRef
//get the entity information based on the Content Type associated with this operation
for _, requestContent := range operation.Responses.Get(http.StatusOK).Value.Content {
//use the first schema ref to determine the entity type
if requestContent.Schema.Ref != "" {
contentType = strings.Replace(requestContent.Schema.Ref, "#/components/schemas/", "", -1)
//get the schema details from the swagger file
contentTypeSchema = spec.Components.Schemas[contentType]
break
}
}

return func(ctxt echo.Context) error {
cType := &context2.ContentType{}
if contentType != "" && contentTypeSchema.Value != nil {
cType = &context2.ContentType{
Name: contentType,
Schema: contentTypeSchema.Value,
}
}

pks, _ := json.Marshal(cType.Schema.Extensions["x-identifier"])
primaryKeys := []string{}
json.Unmarshal(pks, &primaryKeys)

if len(primaryKeys) == 0 {
primaryKeys = append(primaryKeys, "id")
}

newContext := ctxt.Request().Context()

identifiers := map[string]interface{}{}

for _, p := range primaryKeys {
identifiers[p] = newContext.Value(p)
}

sequenceString, _ := newContext.Value("sequence_no").(string)
etag, _ := newContext.Value("If-None-Match").(string)
entityID, _ := newContext.Value("use_entity_id").(string)

var sequence int
if sequenceString != "" {
sequence, _ = strconv.Atoi(sequenceString)
}
var result map[string]interface{}
var err error
if sequence == 0 && etag == "" && entityID != "true" {
for _, projection := range app.Projections() {
if projection != nil {
result, err = projection.GetByKey(ctxt.Request().Context(), *cType, identifiers)
}
}
} else if etag != "" {
tag, seq := SplitEtag(etag)
seqInt, er := strconv.Atoi(seq)
if er != nil {
return NewControllerError("Invalid sequence number", err, http.StatusBadRequest)
}
r, er := GetContentBySequenceNumber(app.EventRepository(), tag, int64(seqInt))
err = er
if r.SequenceNo == 0 {
return NewControllerError("No entity found", err, http.StatusNotFound)
}
if err == nil && r.SequenceNo < int64(seqInt) {
return ctxt.JSON(http.StatusNotModified, r.Property)
}
} else {
//get first identifider
id := ""
for _, i := range identifiers {
id = i.(string)
if id != "" {
break
}
}
if sequence != 0 {
r, er := GetContentBySequenceNumber(app.EventRepository(), id, int64(sequence))
if r != nil && r.ID != "" {
result = r.Property.(map[string]interface{})
}
err = er
} else {
for _, projection := range app.Projections() {
if projection != nil {
result, err = projection.GetByEntityID(ctxt.Request().Context(), *cType, id)
}
}
}
}
weos_id, ok := result["weos_id"].(string)
if errors.Is(err, gorm.ErrRecordNotFound) || (len(result) == 0) || !ok || weos_id == "" {
return NewControllerError("No entity found", err, http.StatusNotFound)
} else if err != nil {
return NewControllerError(err.Error(), err, http.StatusBadRequest)
}

sequenceString = fmt.Sprint(result["sequence_no"])
sequenceNo, _ := strconv.Atoi(sequenceString)

etag = NewEtag(&model.ContentEntity{
AggregateRoot: model.AggregateRoot{
SequenceNo: int64(sequenceNo),
BasicEntity: model.BasicEntity{ID: weos_id},
},
})

//remove sequence number and weos_id from response
delete(result, "weos_id")
delete(result, "sequence_no")

return ctxt.JSON(http.StatusOK, "View Item")
//set etag
ctxt.Response().Header().Set("Etag", etag)
return ctxt.JSON(http.StatusOK, result)
}
}

Expand Down
Loading

0 comments on commit bbd6a6f

Please sign in to comment.