Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Weos 1246 - Add api functionality for viewing content type and refactor projections #47

Merged
merged 20 commits into from
Jan 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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