From 39dbb94ec3ed8375bf71445bf61b3049cbf05493 Mon Sep 17 00:00:00 2001 From: Atonia Andall Date: Sun, 9 Jan 2022 02:00:34 -0400 Subject: [PATCH 01/20] WEOS-1256 updated projections to include a get by entity id and a get by keys function added projections get by entity id test --- projections/gorm.go | 24 +++++++ projections/projections.go | 2 +- projections/projections_test.go | 117 ++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) diff --git a/projections/gorm.go b/projections/gorm.go index 145e80d8..9ad7486c 100644 --- a/projections/gorm.go +++ b/projections/gorm.go @@ -2,6 +2,7 @@ package projections import ( "encoding/json" + "fmt" "strings" ds "github.com/ompluscator/dynamic-struct" @@ -9,6 +10,7 @@ import ( weos "github.com/wepala/weos-service/model" "golang.org/x/net/context" "gorm.io/gorm" + "gorm.io/gorm/clause" ) //GORMProjection interface struct @@ -19,6 +21,28 @@ type GORMProjection struct { Schema map[string]interface{} } +func (p *GORMProjection) GetByKey(contentType weosContext.ContentType, keys map[string]interface{}) error { + return nil +} + +func (p *GORMProjection) GetByEntityID(contentType weosContext.ContentType, id string) (map[string]interface{}, error) { + if scheme, ok := p.Schema[strings.Title(contentType.Name)]; ok { + result := p.db.Table(contentType.Name).Preload(clause.Associations).Where("weos_id = ?", id).Take(scheme) + if result.Error != nil { + return nil, result.Error + } + data, err := json.Marshal(scheme) + if err != nil { + return nil, err + } + val := map[string]interface{}{} + json.Unmarshal(data, &val) + return val, nil + } else { + return nil, weos.NewError(fmt.Sprintf("no content type '%s' exists", contentType.Name), nil) + } +} + //Persist save entity information in database func (p *GORMProjection) Persist(entities []weos.Entity) error { return nil diff --git a/projections/projections.go b/projections/projections.go index 5328aa6b..79e8ea99 100644 --- a/projections/projections.go +++ b/projections/projections.go @@ -11,6 +11,6 @@ type Projection interface { type DefaultProjection struct { WEOSID string `json:"weos_id" gorm:"unique"` - SequenceNo int64 + SequenceNo int64 `json:"sequence_no"` Table string `json:"table_alias"` } diff --git a/projections/projections_test.go b/projections/projections_test.go index af76ba85..84c1a93a 100644 --- a/projections/projections_test.go +++ b/projections/projections_test.go @@ -1077,6 +1077,123 @@ components: }) } +func TestProjections_GetContentTypeByEntityID(t *testing.T) { + openAPI := `openapi: 3.0.3 +info: + title: Blog + description: Blog example + version: 1.0.0 +servers: + - url: https://prod1.weos.sh/blog/dev + description: WeOS Dev + - url: https://prod1.weos.sh/blog/v1 +components: + schemas: + Post: + type: object + properties: + title: + type: string + description: blog title + description: + type: string + Blog: + type: object + properties: + title: + type: string + description: blog title + description: + type: string + posts: + type: array + items: + $ref: "#/components/schemas/Post" +` + + loader := openapi3.NewSwaggerLoader() + swagger, err := loader.LoadSwaggerFromData([]byte(openAPI)) + if err != nil { + t.Fatal(err) + } + + schemes := rest.CreateSchema(context.Background(), echo.New(), swagger) + p, err := projections.NewProjection(context.Background(), app, schemes) + if err != nil { + t.Fatal(err) + } + + err = p.Migrate(context.Background()) + if err != nil { + t.Fatal(err) + } + + gormDB := app.DB() + if !gormDB.Migrator().HasTable("Blog") { + t.Errorf("expected to get a table 'Blog'") + } + + if !gormDB.Migrator().HasTable("Post") { + t.Errorf("expected to get a table 'Post'") + } + + if !gormDB.Migrator().HasTable("blog_posts") { + t.Errorf("expected to get a table 'blog_posts'") + } + + columns, _ := gormDB.Migrator().ColumnTypes("blog_posts") + + found := false + found1 := false + for _, c := range columns { + if c.Name() == "id" { + found = true + } + if c.Name() == "post_id" { + found1 = true + } + } + + if !found1 || !found { + t.Fatal("not all fields found") + } + gormDB.Table("Post").Create(map[string]interface{}{"weos_id": "1234", "title": "hugs"}) + gormDB.Table("Blog").Create(map[string]interface{}{"weos_id": "5678", "title": "hugs"}) + result := gormDB.Table("blog_posts").Create(map[string]interface{}{ + "id": 1, + "post_id": 1, + }) + if result.Error != nil { + t.Errorf("expected to create a post with relationship, got err '%s'", result.Error) + } + + r, err := p.GetByEntityID(weosContext.ContentType{Name: "Blog"}, "5678") + if err != nil { + t.Errorf("error removing table '%s' '%s'", "Blog", err) + } + + if r["title"] != "hugs" { + t.Errorf("expected the blog title to be %s got %v", "hugs", r["titles"]) + } + + if len(r["posts"].([]interface{})) != 1 { + t.Errorf("expected to get %d posts, got %d", 1, len(r["posts"].([]interface{}))) + } + + err = gormDB.Migrator().DropTable("Blog") + if err != nil { + t.Errorf("error removing table '%s' '%s'", "Blog", err) + } + err = gormDB.Migrator().DropTable("Post") + if err != nil { + t.Errorf("error removing table '%s' '%s'", "Post", err) + } + err = gormDB.Migrator().DropTable("blog_posts") + if err != nil { + t.Errorf("error removing table '%s' '%s'", "blog_posts", err) + } +} + func TestProjections_GormOperations(t *testing.T) { t.Run("Basic Create using schema", func(t *testing.T) { openAPI := `openapi: 3.0.3 From 1eaff9a030d9c92f79e40d46847763563ba18afe Mon Sep 17 00:00:00 2001 From: Atonia Andall Date: Sun, 9 Jan 2022 04:06:36 -0400 Subject: [PATCH 02/20] WEOS-1256 - Create projection functions to get entity by keys and by weos id added functionality to omit sequence number and weos_id in relations when pulling the entire entity with relations from database --- projections/gorm.go | 19 +++++-------------- projections/projections.go | 4 ++-- projections/projections_test.go | 28 +++++++++++++++++++++++----- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/projections/gorm.go b/projections/gorm.go index 43bbad42..083e1ed8 100644 --- a/projections/gorm.go +++ b/projections/gorm.go @@ -21,13 +21,13 @@ type GORMProjection struct { Schema map[string]interface{} } -func (p *GORMProjection) GetByKey(contentType weosContext.ContentType, keys map[string]interface{}) error { - return nil +func (p *GORMProjection) GetByKey(ctxt context.Context, contentType weosContext.ContentType, identifier []interface{}) (interface{}, error) { + return nil, nil } -func (p *GORMProjection) GetByEntityID(contentType weosContext.ContentType, id string) (map[string]interface{}, error) { +func (p *GORMProjection) GetByEntityID(ctxt context.Context, contentType weosContext.ContentType, id string) (map[string]interface{}, error) { if scheme, ok := p.Schema[strings.Title(contentType.Name)]; ok { - result := p.db.Table(contentType.Name).Preload(clause.Associations).Where("weos_id = ?", id).Take(scheme) + result := p.db.Table(contentType.Name).Preload(clause.Associations, func(tx *gorm.DB) *gorm.DB { return tx.Omit("weos_id, sequence_no") }).Where("weos_id = ?", id).Take(scheme) if result.Error != nil { return nil, result.Error } @@ -39,7 +39,7 @@ func (p *GORMProjection) GetByEntityID(contentType weosContext.ContentType, id s json.Unmarshal(data, &val) return val, nil } else { - return nil, weos.NewError(fmt.Sprintf("no content type '%s' exists", contentType.Name), nil) + return nil, fmt.Errorf("no content type '%s' exists", contentType.Name) } } @@ -53,15 +53,6 @@ func (p *GORMProjection) Remove(entities []weos.Entity) error { return nil } -func (p *GORMProjection) GetByID(ctxt context.Context, contentType weosContext.ContentType, identifier []interface{}) (interface{}, error) { - - return nil, nil -} - -func (p *GORMProjection) GetByEntityID(ctxt context.Context, contentType weosContext.ContentType, id string) (interface{}, error) { - return nil, nil -} - func (p *GORMProjection) Migrate(ctx context.Context) error { //we may need to reorder the creation so that tables don't reference things that don't exist as yet. diff --git a/projections/projections.go b/projections/projections.go index 79e8ea99..9f1b61fa 100644 --- a/projections/projections.go +++ b/projections/projections.go @@ -10,7 +10,7 @@ type Projection interface { } type DefaultProjection struct { - WEOSID string `json:"weos_id" gorm:"unique"` - SequenceNo int64 `json:"sequence_no"` + WEOSID string `json:"weos_id,omitempty" gorm:"unique"` + SequenceNo int64 `json:"sequence_no,omitempty"` Table string `json:"table_alias"` } diff --git a/projections/projections_test.go b/projections/projections_test.go index 84c1a93a..53d47879 100644 --- a/projections/projections_test.go +++ b/projections/projections_test.go @@ -1157,8 +1157,8 @@ components: if !found1 || !found { t.Fatal("not all fields found") } - gormDB.Table("Post").Create(map[string]interface{}{"weos_id": "1234", "title": "hugs"}) - gormDB.Table("Blog").Create(map[string]interface{}{"weos_id": "5678", "title": "hugs"}) + gormDB.Table("Post").Create(map[string]interface{}{"weos_id": "1234", "sequence_no": 1, "title": "punches"}) + gormDB.Table("Blog").Create(map[string]interface{}{"weos_id": "5678", "sequence_no": 1, "title": "hugs"}) result := gormDB.Table("blog_posts").Create(map[string]interface{}{ "id": 1, "post_id": 1, @@ -1167,7 +1167,7 @@ components: t.Errorf("expected to create a post with relationship, got err '%s'", result.Error) } - r, err := p.GetByEntityID(weosContext.ContentType{Name: "Blog"}, "5678") + r, err := p.GetByEntityID(context.Background(), weosContext.ContentType{Name: "Blog"}, "5678") if err != nil { t.Errorf("error removing table '%s' '%s'", "Blog", err) } @@ -1176,8 +1176,26 @@ components: t.Errorf("expected the blog title to be %s got %v", "hugs", r["titles"]) } - if len(r["posts"].([]interface{})) != 1 { - t.Errorf("expected to get %d posts, got %d", 1, len(r["posts"].([]interface{}))) + posts := r["posts"].([]interface{}) + if len(posts) != 1 { + t.Errorf("expected to get %d posts, got %d", 1, len(posts)) + } + + pp := posts[0].(map[string]interface{}) + if pp["title"] != "punches" { + t.Errorf("expected the post title to be %s got %v", "punches", pp["title"]) + } + + if id, ok := pp["weos_id"]; ok { + if id != "" { + t.Errorf("there should be no weos_id value") + } + } + + if no, ok := pp["sequence_no"]; ok { + if no != 0 { + t.Errorf("there should be no sequence number value") + } } err = gormDB.Migrator().DropTable("Blog") From da58ed9a1df28871b1f32d3663f943ad1374e580 Mon Sep 17 00:00:00 2001 From: Atonia Andall Date: Sun, 9 Jan 2022 05:45:41 -0400 Subject: [PATCH 03/20] WEOS-1256 added a get by key function in projections and added projections test --- projections/gorm.go | 45 +++++++++- projections/projections_test.go | 154 +++++++++++++++++++++++++++++++- 2 files changed, 196 insertions(+), 3 deletions(-) diff --git a/projections/gorm.go b/projections/gorm.go index 083e1ed8..e7017519 100644 --- a/projections/gorm.go +++ b/projections/gorm.go @@ -21,8 +21,49 @@ type GORMProjection struct { Schema map[string]interface{} } -func (p *GORMProjection) GetByKey(ctxt context.Context, contentType weosContext.ContentType, identifier []interface{}) (interface{}, error) { - return nil, nil +func (p *GORMProjection) GetByKey(ctxt context.Context, contentType weosContext.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) { + if scheme, ok := p.Schema[strings.Title(contentType.Name)]; ok { + //pulling the primary keys from the schema in order to match with the keys given for searching + pks, _ := json.Marshal(contentType.Schema.Extensions["x-identifier"]) + primaryKeys := []string{} + json.Unmarshal(pks, &primaryKeys) + + if len(primaryKeys) == 0 { + primaryKeys = append(primaryKeys, "id") + } + + if len(primaryKeys) != len(identifiers) { + return nil, fmt.Errorf("%d keys provided for %s but there should be %d keys", len(identifiers), contentType.Name, len(primaryKeys)) + } + + for _, k := range primaryKeys { + found := false + for i, _ := range identifiers { + if k == i { + found = true + break + } + } + if !found { + return nil, fmt.Errorf("no value for %s %s found", contentType.Name, k) + } + } + + //gorm sqlite generates the query incorrectly for composite keys when preloading + result := p.db.Table(contentType.Name).Preload(clause.Associations, func(tx *gorm.DB) *gorm.DB { return tx.Omit("weos_id, sequence_no") }).First(scheme, identifiers) + if result.Error != nil { + return nil, result.Error + } + data, err := json.Marshal(scheme) + if err != nil { + return nil, err + } + val := map[string]interface{}{} + json.Unmarshal(data, &val) + return val, nil + } else { + return nil, fmt.Errorf("no content type '%s' exists", contentType.Name) + } } func (p *GORMProjection) GetByEntityID(ctxt context.Context, contentType weosContext.ContentType, id string) (map[string]interface{}, error) { diff --git a/projections/projections_test.go b/projections/projections_test.go index 53d47879..079893ba 100644 --- a/projections/projections_test.go +++ b/projections/projections_test.go @@ -1168,15 +1168,167 @@ components: } r, err := p.GetByEntityID(context.Background(), weosContext.ContentType{Name: "Blog"}, "5678") + if err != nil { + t.Fatalf("error querying '%s' '%s'", "Blog", err) + } + if r["title"] != "hugs" { + t.Errorf("expected the blog title to be %s got %v", "hugs", r["titles"]) + } + + posts := r["posts"].([]interface{}) + if len(posts) != 1 { + t.Errorf("expected to get %d posts, got %d", 1, len(posts)) + } + + pp := posts[0].(map[string]interface{}) + if pp["title"] != "punches" { + t.Errorf("expected the post title to be %s got %v", "punches", pp["title"]) + } + + if id, ok := pp["weos_id"]; ok { + if id != "" { + t.Errorf("there should be no weos_id value") + } + } + + if no, ok := pp["sequence_no"]; ok { + if no != 0 { + t.Errorf("there should be no sequence number value") + } + } + + err = gormDB.Migrator().DropTable("Blog") if err != nil { t.Errorf("error removing table '%s' '%s'", "Blog", err) } + err = gormDB.Migrator().DropTable("Post") + if err != nil { + t.Errorf("error removing table '%s' '%s'", "Post", err) + } + err = gormDB.Migrator().DropTable("blog_posts") + if err != nil { + t.Errorf("error removing table '%s' '%s'", "blog_posts", err) + } +} + +func TestProjections_GetContentTypeByKeys(t *testing.T) { + openAPI := `openapi: 3.0.3 +info: + title: Blog + description: Blog example + version: 1.0.0 +servers: + - url: https://prod1.weos.sh/blog/dev + description: WeOS Dev + - url: https://prod1.weos.sh/blog/v1 +components: + schemas: + Post: + type: object + properties: + title: + type: string + description: blog title + description: + type: string + Blog: + type: object + properties: + title: + type: string + description: blog title + author_id: + type: string + description: + type: string + posts: + type: array + items: + $ref: "#/components/schemas/Post" + x-identifier: + - title + - author_id +` + + loader := openapi3.NewSwaggerLoader() + swagger, err := loader.LoadSwaggerFromData([]byte(openAPI)) + if err != nil { + t.Fatal(err) + } + + schemes := rest.CreateSchema(context.Background(), echo.New(), swagger) + p, err := projections.NewProjection(context.Background(), app, schemes) + if err != nil { + t.Fatal(err) + } + + err = p.Migrate(context.Background()) + if err != nil { + t.Fatal(err) + } + + gormDB := app.DB() + if !gormDB.Migrator().HasTable("Blog") { + t.Errorf("expected to get a table 'Blog'") + } + + if !gormDB.Migrator().HasTable("Post") { + t.Errorf("expected to get a table 'Post'") + } + + if !gormDB.Migrator().HasTable("blog_posts") { + t.Errorf("expected to get a table 'blog_posts'") + } + + columns, _ := gormDB.Migrator().ColumnTypes("blog_posts") + + found := false + found1 := false + found2 := false + for _, c := range columns { + if c.Name() == "id" { + found = true + } + if c.Name() == "title" { + found1 = true + } + if c.Name() == "author_id" { + found2 = true + } + } + + if !found1 || !found || !found2 { + t.Fatal("not all fields found") + } + gormDB.Table("Post").Create(map[string]interface{}{"weos_id": "1234", "sequence_no": 1, "title": "punches"}) + gormDB.Table("Blog").Create(map[string]interface{}{"weos_id": "5678", "sequence_no": 1, "title": "hugs", "author_id": "kidding"}) + gormDB.Table("Blog").Create(map[string]interface{}{"weos_id": "9101", "sequence_no": 1, "title": "hugs 2 - the reckoning", "author_id": "kidding"}) + result := gormDB.Table("blog_posts").Create(map[string]interface{}{ + "author_id": "kidding", + "title": "hugs", + "id": 1, + }) + if result.Error != nil { + t.Errorf("expected to create a post with relationship, got err '%s'", result.Error) + } + + blogRef := swagger.Components.Schemas["Blog"] + r, err := p.GetByKey(context.Background(), weosContext.ContentType{Name: "Blog", Schema: blogRef.Value}, map[string]interface{}{ + "author_id": "kidding", + "title": "hugs", + }) + if err != nil { + t.Fatalf("error querying '%s' '%s'", "Blog", err) + } if r["title"] != "hugs" { t.Errorf("expected the blog title to be %s got %v", "hugs", r["titles"]) } - posts := r["posts"].([]interface{}) + posts, ok := r["posts"].([]interface{}) + if !ok { + t.Fatal("expected to get a posts array") + } if len(posts) != 1 { t.Errorf("expected to get %d posts, got %d", 1, len(posts)) } From 161f3df371df38de08c62d1cbac97783b005e418 Mon Sep 17 00:00:00 2001 From: Atonia Andall Date: Sun, 9 Jan 2022 06:13:53 -0400 Subject: [PATCH 04/20] WEOS-1256 updated projections get by entity ids and get by keys in order to facilitate gorm sqlite error --- projections/gorm.go | 19 +++++++-- projections/projections_test.go | 71 +++++++++++++++++---------------- 2 files changed, 53 insertions(+), 37 deletions(-) diff --git a/projections/gorm.go b/projections/gorm.go index e7017519..f996f34f 100644 --- a/projections/gorm.go +++ b/projections/gorm.go @@ -49,8 +49,14 @@ func (p *GORMProjection) GetByKey(ctxt context.Context, contentType weosContext. } } - //gorm sqlite generates the query incorrectly for composite keys when preloading - result := p.db.Table(contentType.Name).Preload(clause.Associations, func(tx *gorm.DB) *gorm.DB { return tx.Omit("weos_id, sequence_no") }).First(scheme, identifiers) + var result *gorm.DB + if p.db.Dialector.Name() == "sqlite" { + //gorm sqlite generates the query incorrectly if there are composite keys when preloading + //https://github.com/go-gorm/gorm/issues/3585 + result = p.db.Table(contentType.Name).First(scheme, identifiers) + } else { + result = p.db.Table(contentType.Name).Preload(clause.Associations, func(tx *gorm.DB) *gorm.DB { return tx.Omit("weos_id, sequence_no") }).First(scheme, identifiers) + } if result.Error != nil { return nil, result.Error } @@ -68,7 +74,14 @@ func (p *GORMProjection) GetByKey(ctxt context.Context, contentType weosContext. func (p *GORMProjection) GetByEntityID(ctxt context.Context, contentType weosContext.ContentType, id string) (map[string]interface{}, error) { if scheme, ok := p.Schema[strings.Title(contentType.Name)]; ok { - result := p.db.Table(contentType.Name).Preload(clause.Associations, func(tx *gorm.DB) *gorm.DB { return tx.Omit("weos_id, sequence_no") }).Where("weos_id = ?", id).Take(scheme) + var result *gorm.DB + if p.db.Dialector.Name() == "sqlite" { + //gorm sqlite generates the query incorrectly if there are composite keys when preloading + //https://github.com/go-gorm/gorm/issues/3585 + result = p.db.Table(contentType.Name).Where("weos_id = ?", id).Take(scheme) + } else { + result = p.db.Table(contentType.Name).Preload(clause.Associations, func(tx *gorm.DB) *gorm.DB { return tx.Omit("weos_id, sequence_no") }).Where("weos_id = ?", id).Take(scheme) + } if result.Error != nil { return nil, result.Error } diff --git a/projections/projections_test.go b/projections/projections_test.go index 079893ba..53b89947 100644 --- a/projections/projections_test.go +++ b/projections/projections_test.go @@ -1175,25 +1175,27 @@ components: t.Errorf("expected the blog title to be %s got %v", "hugs", r["titles"]) } - posts := r["posts"].([]interface{}) - if len(posts) != 1 { - t.Errorf("expected to get %d posts, got %d", 1, len(posts)) - } + if *driver != "sqlite3" { + posts := r["posts"].([]interface{}) + if len(posts) != 1 { + t.Errorf("expected to get %d posts, got %d", 1, len(posts)) + } - pp := posts[0].(map[string]interface{}) - if pp["title"] != "punches" { - t.Errorf("expected the post title to be %s got %v", "punches", pp["title"]) - } + pp := posts[0].(map[string]interface{}) + if pp["title"] != "punches" { + t.Errorf("expected the post title to be %s got %v", "punches", pp["title"]) + } - if id, ok := pp["weos_id"]; ok { - if id != "" { - t.Errorf("there should be no weos_id value") + if id, ok := pp["weos_id"]; ok { + if id != "" { + t.Errorf("there should be no weos_id value") + } } - } - if no, ok := pp["sequence_no"]; ok { - if no != 0 { - t.Errorf("there should be no sequence number value") + if no, ok := pp["sequence_no"]; ok { + if no != 0 { + t.Errorf("there should be no sequence number value") + } } } @@ -1325,31 +1327,32 @@ components: t.Errorf("expected the blog title to be %s got %v", "hugs", r["titles"]) } - posts, ok := r["posts"].([]interface{}) - if !ok { - t.Fatal("expected to get a posts array") - } - if len(posts) != 1 { - t.Errorf("expected to get %d posts, got %d", 1, len(posts)) - } + if *driver != "sqlite3" { + posts, ok := r["posts"].([]interface{}) + if !ok { + t.Fatal("expected to get a posts array") + } + if len(posts) != 1 { + t.Errorf("expected to get %d posts, got %d", 1, len(posts)) + } - pp := posts[0].(map[string]interface{}) - if pp["title"] != "punches" { - t.Errorf("expected the post title to be %s got %v", "punches", pp["title"]) - } + pp := posts[0].(map[string]interface{}) + if pp["title"] != "punches" { + t.Errorf("expected the post title to be %s got %v", "punches", pp["title"]) + } - if id, ok := pp["weos_id"]; ok { - if id != "" { - t.Errorf("there should be no weos_id value") + if id, ok := pp["weos_id"]; ok { + if id != "" { + t.Errorf("there should be no weos_id value") + } } - } - if no, ok := pp["sequence_no"]; ok { - if no != 0 { - t.Errorf("there should be no sequence number value") + if no, ok := pp["sequence_no"]; ok { + if no != 0 { + t.Errorf("there should be no sequence number value") + } } } - err = gormDB.Migrator().DropTable("Blog") if err != nil { t.Errorf("error removing table '%s' '%s'", "Blog", err) From ac2e1abe4003c10b5faf6c1c8faa648773ff4e73 Mon Sep 17 00:00:00 2001 From: Atonia Andall Date: Mon, 10 Jan 2022 00:22:58 -0400 Subject: [PATCH 05/20] WEOS-1256 added projections test for reassigning primary key --- projections/projections_test.go | 168 ++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/projections/projections_test.go b/projections/projections_test.go index 53b89947..702b9937 100644 --- a/projections/projections_test.go +++ b/projections/projections_test.go @@ -1504,3 +1504,171 @@ components: } }) } + +func TestProjections_ChangePrimaryKey(t *testing.T) { + + t.Run("Create basic many to one relationship", func(t *testing.T) { + openAPI := `openapi: 3.0.3 +info: + title: Blog + description: Blog example + version: 1.0.0 +servers: + - url: https://prod1.weos.sh/blog/dev + description: WeOS Dev + - url: https://prod1.weos.sh/blog/v1 +components: + schemas: + Blog: + type: object + properties: + title: + type: string + description: blog title + description: + type: string + Post: + type: object + properties: + title: + type: string + description: blog title + description: + type: string + blog: + $ref: "#/components/schemas/Blog" +` + + loader := openapi3.NewSwaggerLoader() + swagger, err := loader.LoadSwaggerFromData([]byte(openAPI)) + if err != nil { + t.Fatal(err) + } + + schemes := rest.CreateSchema(context.Background(), echo.New(), swagger) + p, err := projections.NewProjection(context.Background(), app, schemes) + if err != nil { + t.Fatal(err) + } + + err = p.Migrate(context.Background()) + if err != nil { + t.Fatal(err) + } + + gormDB := app.DB() + if !gormDB.Migrator().HasTable("Blog") { + t.Errorf("expected to get a table 'Blog'") + } + + if !gormDB.Migrator().HasTable("Post") { + t.Errorf("expected to get a table 'Post'") + } + + columns, _ := gormDB.Migrator().ColumnTypes("Post") + + found := false + found1 := false + found2 := false + found3 := false + for _, c := range columns { + if c.Name() == "id" { + found = true + } + if c.Name() == "title" { + found1 = true + } + if c.Name() == "description" { + found2 = true + } + if c.Name() == "blog_id" { + found3 = true + } + } + + if !found1 || !found2 || !found || !found3 { + t.Fatal("not all fields found") + } + + gormDB.Table("Blog").Create(map[string]interface{}{"title": "hugs"}) + result := gormDB.Table("Post").Create(map[string]interface{}{"title": "hugs", "blog_id": 1}) + if result.Error != nil { + t.Errorf("expected to create a post with relationship, got err '%s'", result.Error) + } + + result = gormDB.Table("Post").Create(map[string]interface{}{"title": "hugs"}) + if result.Error != nil { + t.Errorf("expected to create a post without relationship, got err '%s'", result.Error) + } + + result = gormDB.Table("Post").Create(map[string]interface{}{"title": "hugs", "blog_id": 5}) + if result.Error == nil { + t.Errorf("expected to be unable to create post with invalid reference to blog") + } + + openAPI = `openapi: 3.0.3 +info: + title: Blog + description: Blog example + version: 1.0.0 +servers: + - url: https://prod1.weos.sh/blog/dev + description: WeOS Dev + - url: https://prod1.weos.sh/blog/v1 +components: + schemas: + Post: + type: object + properties: + title: + type: string + description: blog title + description: + type: string + Blog: + type: object + properties: + title: + type: string + description: blog title + author_id: + type: string + description: + type: string + posts: + type: array + items: + $ref: "#/components/schemas/Post" + x-identifier: + - title + - author_id +` + loader = openapi3.NewSwaggerLoader() + swagger, err = loader.LoadSwaggerFromData([]byte(openAPI)) + if err != nil { + t.Fatal(err) + } + + schemes = rest.CreateSchema(context.Background(), echo.New(), swagger) + p, err = projections.NewProjection(context.Background(), app, schemes) + if err != nil { + t.Fatal(err) + } + + //fails because can't have empty values for primary key + err = p.Migrate(context.Background()) + if err != nil { + t.Fatal(err) + } + + err = gormDB.Migrator().DropTable("Blog") + if err != nil { + t.Errorf("error removing table '%s' '%s'", "Blog", err) + } + err = gormDB.Migrator().DropTable("Post") + if err != nil { + t.Errorf("error removing table '%s' '%s'", "Post", err) + } + }) + +} From 0f91fa64d6ce506900e152b7ed23566861bff9f6 Mon Sep 17 00:00:00 2001 From: Atonia Andall Date: Thu, 13 Jan 2022 12:36:11 -0400 Subject: [PATCH 06/20] WEOS-1256 made preload tables functionality a query modifier to be added to the scopes --- model/domain_service.go | 1 + projections/gorm.go | 37 +++++++++++++++++++++++-------------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/model/domain_service.go b/model/domain_service.go index 2e99a4c1..797c9623 100644 --- a/model/domain_service.go +++ b/model/domain_service.go @@ -2,6 +2,7 @@ package model import ( "encoding/json" + weosContext "github.com/wepala/weos-service/context" "golang.org/x/net/context" ) diff --git a/projections/gorm.go b/projections/gorm.go index 9a56d059..95a14261 100644 --- a/projections/gorm.go +++ b/projections/gorm.go @@ -50,13 +50,9 @@ func (p *GORMProjection) GetByKey(ctxt context.Context, contentType weosContext. } var result *gorm.DB - if p.db.Dialector.Name() == "sqlite" { - //gorm sqlite generates the query incorrectly if there are composite keys when preloading - //https://github.com/go-gorm/gorm/issues/3585 - result = p.db.Table(contentType.Name).First(scheme, identifiers) - } else { - result = p.db.Table(contentType.Name).Preload(clause.Associations, func(tx *gorm.DB) *gorm.DB { return tx.Omit("weos_id, sequence_no") }).First(scheme, identifiers) - } + + result = p.db.Table(contentType.Name).Scopes(ContentQuery()).First(scheme, identifiers) + if result.Error != nil { return nil, result.Error } @@ -75,13 +71,9 @@ func (p *GORMProjection) GetByKey(ctxt context.Context, contentType weosContext. func (p *GORMProjection) GetByEntityID(ctxt context.Context, contentType weosContext.ContentType, id string) (map[string]interface{}, error) { if scheme, ok := p.Schema[strings.Title(contentType.Name)]; ok { var result *gorm.DB - if p.db.Dialector.Name() == "sqlite" { - //gorm sqlite generates the query incorrectly if there are composite keys when preloading - //https://github.com/go-gorm/gorm/issues/3585 - result = p.db.Table(contentType.Name).Where("weos_id = ?", id).Take(scheme) - } else { - result = p.db.Table(contentType.Name).Preload(clause.Associations, func(tx *gorm.DB) *gorm.DB { return tx.Omit("weos_id, sequence_no") }).Where("weos_id = ?", id).Take(scheme) - } + + result = p.db.Table(contentType.Name).Scopes(ContentQuery()).Where("weos_id = ?", id).Take(scheme) + if result.Error != nil { return nil, result.Error } @@ -209,6 +201,11 @@ func (p *GORMProjection) GetContentEntity(ctx context.Context, weosID string) (* return newEntity, nil } +//query modifier for making queries to the database +type QueryModifier func() func(db *gorm.DB) *gorm.DB + +var ContentQuery QueryModifier + //NewProjection creates an instance of the projection func NewProjection(ctx context.Context, application weos.Service, schemas map[string]interface{}) (*GORMProjection, error) { @@ -218,5 +215,17 @@ func NewProjection(ctx context.Context, application weos.Service, schemas map[st Schema: schemas, } application.AddProjection(projection) + + ContentQuery = func() func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + if projection.db.Dialector.Name() == "sqlite" { + //gorm sqlite generates the query incorrectly if there are composite keys when preloading + //https://github.com/go-gorm/gorm/issues/3585 + return db + } else { + return db.Preload(clause.Associations, func(tx *gorm.DB) *gorm.DB { return tx.Omit("weos_id, sequence_no") }) + } + } + } return projection, nil } From a13209af3525e5cbda8213b1af0be4fa9cf3e571 Mon Sep 17 00:00:00 2001 From: Atonia Andall Date: Thu, 13 Jan 2022 12:45:27 -0400 Subject: [PATCH 07/20] WEOS-1245 updated sequence number check --- end2end_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/end2end_test.go b/end2end_test.go index 28425a73..209710c9 100644 --- a/end2end_test.go +++ b/end2end_test.go @@ -518,10 +518,14 @@ func theHeaderShouldBe(key, value string) error { if seqNoEtag == "" { return fmt.Errorf("expected the Etag to contain a sequence no, got %s", seqNoEtag) } + + if seqNoEtag != strings.Split(value, ".")[1] { + return fmt.Errorf("expected the Etag to contain a sequence no %s, got %s", strings.Split(value, ".")[1], seqNoEtag) + } return nil } - headers := rec.HeaderMap + headers := rec.Result().Header val := []string{} for k, v := range headers { From b29ebae60a1e8bd153200bcf1cc18dceeee686d9 Mon Sep 17 00:00:00 2001 From: RandyDeo Date: Thu, 13 Jan 2022 13:29:17 -0400 Subject: [PATCH 08/20] feature:WEOS-1132 - Added command - Added receiver and test - Added func for update in domain service --- model/command_standard.go | 16 ++++++++ model/domain_service.go | 11 ++++++ model/receiver.go | 15 ++++++++ model/receiver_test.go | 81 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 123 insertions(+) diff --git a/model/command_standard.go b/model/command_standard.go index 94d47808..97d2ad12 100644 --- a/model/command_standard.go +++ b/model/command_standard.go @@ -37,3 +37,19 @@ func CreateBatch(ctx context.Context, payload json.RawMessage, entityType string } return command } + +func Update(ctx context.Context, payload json.RawMessage, entityType string, entityID string) *Command { + + command := &Command{ + Type: "update", + Payload: payload, + Metadata: CommandMetadata{ + Version: 1, + UserID: weoscontext.GetUser(ctx), + AccountID: weoscontext.GetAccount(ctx), + EntityType: entityType, + EntityID: entityID, + }, + } + return command +} diff --git a/model/domain_service.go b/model/domain_service.go index 2e99a4c1..5fe21e2f 100644 --- a/model/domain_service.go +++ b/model/domain_service.go @@ -56,6 +56,17 @@ func (s *DomainService) CreateBatch(ctx context.Context, payload json.RawMessage } +//Update is used for a single payload. It gets an existing entity and updates it with the new payload +//TODO Add weosID/EntityID to cmd (when 1130 -> dev) +func (s *DomainService) Update(ctx context.Context, payload json.RawMessage, entityType string) (*ContentEntity, error) { + //TODO Check weosID if blank + //TODO call getEntity with id + //TODO check if entity is nil + //TODO call entity update + //TODO Return entity + return new(ContentEntity), nil +} + func NewDomainService(ctx context.Context, eventRepository EventRepository) *DomainService { return &DomainService{ eventRepository: eventRepository, diff --git a/model/receiver.go b/model/receiver.go index 3bd934b2..5bcb9fe2 100644 --- a/model/receiver.go +++ b/model/receiver.go @@ -43,6 +43,20 @@ func (r *Receiver) CreateBatch(ctx context.Context, command *Command) error { return nil } +//Update is used for a single payload. It takes in the command and context which is used to dispatch and updated the specified entity. +func (r *Receiver) Update(ctx context.Context, command *Command) error { + + updatedEntity, err := r.domainService.Update(ctx, command.Payload, command.Metadata.EntityType) + if err != nil { + return err + } + err = r.service.EventRepository().Persist(ctx, updatedEntity) + if err != nil { + return err + } + return nil +} + //Initialize sets up the command handlers func Initialize(service Service) error { var payload json.RawMessage @@ -51,6 +65,7 @@ func Initialize(service Service) error { //add command handlers to the application's command dispatcher service.Dispatcher().AddSubscriber(Create(context.Background(), payload, "", ""), receiver.Create) service.Dispatcher().AddSubscriber(CreateBatch(context.Background(), payload, ""), receiver.CreateBatch) + service.Dispatcher().AddSubscriber(Create(context.Background(), payload, "", ""), receiver.Update) //initialize any services receiver.domainService = NewDomainService(context.Background(), service.EventRepository()) diff --git a/model/receiver_test.go b/model/receiver_test.go index c04b612b..889d5317 100644 --- a/model/receiver_test.go +++ b/model/receiver_test.go @@ -124,3 +124,84 @@ func TestCreateContentType(t *testing.T) { } }) } + +func TestUpdateContentType(t *testing.T) { + swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile("../controllers/rest/fixtures/blog.yaml") + if err != nil { + t.Fatalf("unexpected error occured '%s'", err) + } + var contentType string + var contentTypeSchema *openapi3.SchemaRef + contentType = "Blog" + contentTypeSchema = swagger.Components.Schemas[contentType] + ctx := context.Background() + ctx = context.WithValue(ctx, weosContext.CONTENT_TYPE, &weosContext.ContentType{ + Name: contentType, + Schema: contentTypeSchema.Value, + }) + ctx = context.WithValue(ctx, weosContext.USER_ID, "123") + commandDispatcher := &model.DefaultCommandDispatcher{} + mockEventRepository := &EventRepositoryMock{ + PersistFunc: func(ctxt context.Context, entity model.AggregateInterface) error { + var event *model.Event + var ok bool + entities := entity.GetNewChanges() + if len(entities) != 1 { + t.Fatalf("expected %d event to be saved, got %d", 1, len(entities)) + } + + if event, ok = entities[0].(*model.Event); !ok { + t.Fatalf("the entity is not an event") + } + + if event.Type != "update" { + t.Errorf("expected event to be '%s', got '%s'", "create", event.Type) + } + if event.Meta.EntityType == "" { + t.Errorf("expected event to be '%s', got '%s'", "", event.Type) + } + + return nil + }, + AddSubscriberFunc: func(handler model.EventHandler) { + }, + } + application := &ApplicationMock{ + DispatcherFunc: func() model.Dispatcher { + return commandDispatcher + }, + EventRepositoryFunc: func() model.EventRepository { + return mockEventRepository + }, + ProjectionsFunc: func() []model.Projection { + return []model.Projection{} + }, + } + + err1 := model.Initialize(application) + if err1 != nil { + t.Fatalf("unexpected error setting up model '%s'", err1) + } + + t.Run("Testing basic update entity", func(t *testing.T) { + mockBlog := &Blog{ + ID: "123", + Title: "Test Blog", + Url: "ww.testingBlog.com", + } + entityType := "Blog" + reqBytes, err := json.Marshal(mockBlog) + if err != nil { + t.Fatalf("error converting content type to bytes %s", err) + } + + err1 := commandDispatcher.Dispatch(ctx, model.Update(ctx, reqBytes, entityType, "123")) + if err1 != nil { + t.Fatalf("unexpected error dispatching command '%s'", err1) + } + + if len(mockEventRepository.PersistCalls()) != 1 { + t.Fatalf("expected change events to be persisted '%d' got persisted '%d' times", 1, len(mockEventRepository.PersistCalls())) + } + }) +} From e1b20d5f6380f182d1f8e6e1412a37fafa4efa68 Mon Sep 17 00:00:00 2001 From: Atonia Andall Date: Thu, 13 Jan 2022 14:59:38 -0400 Subject: [PATCH 09/20] WEOS-1256 added functionality to get an entity by id and sequence number --- controllers/rest/api_test.go | 47 ++++++++++++++++++++++++++++++++++++ controllers/rest/utils.go | 17 ++++++++++--- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/controllers/rest/api_test.go b/controllers/rest/api_test.go index 04ac318f..d930092e 100644 --- a/controllers/rest/api_test.go +++ b/controllers/rest/api_test.go @@ -202,3 +202,50 @@ func TestRESTAPI_Initialize_RequiredField(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-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"}, + } + 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) + 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) + } + os.Remove("test.db") +} diff --git a/controllers/rest/utils.go b/controllers/rest/utils.go index 6fce7808..009e946d 100644 --- a/controllers/rest/utils.go +++ b/controllers/rest/utils.go @@ -4,14 +4,15 @@ import ( "bufio" "bytes" "fmt" - "github.com/labstack/echo/v4" - "github.com/labstack/gommon/log" - "github.com/wepala/weos-service/model" "io" "io/ioutil" "net/http" "strconv" "strings" + + "github.com/labstack/echo/v4" + "github.com/labstack/gommon/log" + "github.com/wepala/weos-service/model" ) //LoadHttpRequestFixture wrapper around the test helper to make it easier to use it with test table @@ -197,3 +198,13 @@ func SplitEtag(Etag string) (string, string) { seqNo := result[1] return weosID, seqNo } + +func GetContentBySequenceNumber(eventRepository model.EventRepository, id string, sequence_no int64) (*model.ContentEntity, error) { + entity := &model.ContentEntity{} + events, err := eventRepository.GetByAggregateAndSequenceRange(id, 0, sequence_no) + if err != nil { + return nil, err + } + err = entity.ApplyChanges(events) + return entity, err +} From baf3f71998f38f51810fdefbd1670ae501195501 Mon Sep 17 00:00:00 2001 From: RandyDeo Date: Thu, 13 Jan 2022 15:21:21 -0400 Subject: [PATCH 10/20] feature:WEOS-1132 - working on an update test --- model/domain_service_test.go | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/model/domain_service_test.go b/model/domain_service_test.go index e4935d2f..d46486dd 100644 --- a/model/domain_service_test.go +++ b/model/domain_service_test.go @@ -143,3 +143,69 @@ func TestDomainService_CreateBatch(t *testing.T) { } }) } + +func TestDomainService_Update(t *testing.T) { + + mockEventRepository := &EventRepositoryMock{ + PersistFunc: func(ctxt context.Context, entity model.AggregateInterface) error { + return nil + }, + } + //load open api spec + swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile("../controllers/rest/fixtures/blog.yaml") + if err != nil { + t.Fatalf("unexpected error occured '%s'", err) + } + var contentType string + var contentTypeSchema *openapi3.SchemaRef + contentType = "Blog" + contentTypeSchema = swagger.Components.Schemas[contentType] + newContext := context.Background() + newContext = context.WithValue(newContext, context2.CONTENT_TYPE, &context2.ContentType{ + Name: contentType, + Schema: contentTypeSchema.Value, + }) + + //mock existing entity + mockPayload := map[string]interface{}{"weos_id": "123456", "sequence_no": int64(1), "title": "Test Blog", "description": "testing"} + mockContentEntity := &model.ContentEntity{ + AggregateRoot: model.AggregateRoot{ + BasicEntity: model.BasicEntity{ + ID: "123456", + }, + SequenceNo: 1, + }, + Property: mockPayload, + } + + update := &Blog{ + Title: "First blog", + Description: "Description testing 1", + Url: "www.TestBlog.com", + } + entityType := "Blog" + + reqBytes, err := json.Marshal(update) + if err != nil { + t.Fatalf("error converting content type to bytes %s", err) + } + + dService := model.NewDomainService(newContext, mockEventRepository) + blog, err := dService.Create(newContext, reqBytes, entityType) + + if err != nil { + t.Fatalf("unexpected error creating content type '%s'", err) + } + if blog == nil { + t.Fatal("expected blog to be returned") + } + if blog.GetString("Title") != mockBlog.Title { + t.Fatalf("expected blog title to be %s got %s", mockBlog.Title, blog.GetString("Title")) + } + if blog.GetString("Description") != mockBlog.Description { + t.Fatalf("expected blog description to be %s got %s", mockBlog.Description, blog.GetString("Description")) + } + if blog.GetString("Url") != mockBlog.Url { + t.Fatalf("expected blog url to be %s got %s", mockBlog.Url, blog.GetString("Url")) + } +} From b3d8a0f9ee32cd53950e2624411768c872006f66 Mon Sep 17 00:00:00 2001 From: RandyDeo Date: Fri, 14 Jan 2022 09:30:23 -0400 Subject: [PATCH 11/20] feature:WEOS-1132 - Worked on the service test - Worked on the update service func - Added to the receiver --- model/domain_service.go | 26 ++++++++++++++++------ model/domain_service_test.go | 42 ++++++++++++++++++------------------ model/receiver.go | 6 +++++- projections/gorm.go | 1 + 4 files changed, 46 insertions(+), 29 deletions(-) diff --git a/model/domain_service.go b/model/domain_service.go index 5fe21e2f..f327b98c 100644 --- a/model/domain_service.go +++ b/model/domain_service.go @@ -7,6 +7,7 @@ import ( ) type DomainService struct { + Projection //not sure if its this simple Repository eventRepository EventRepository } @@ -57,14 +58,25 @@ func (s *DomainService) CreateBatch(ctx context.Context, payload json.RawMessage } //Update is used for a single payload. It gets an existing entity and updates it with the new payload -//TODO Add weosID/EntityID to cmd (when 1130 -> dev) func (s *DomainService) Update(ctx context.Context, payload json.RawMessage, entityType string) (*ContentEntity, error) { - //TODO Check weosID if blank - //TODO call getEntity with id - //TODO check if entity is nil - //TODO call entity update - //TODO Return entity - return new(ContentEntity), nil + + weosID, err := GetIDfromPayload(payload) + if err != nil { + return nil, NewDomainError("unexpected error unmarshalling payload to get weosID", entityType, "", err) + } + + if weosID == "" { + return nil, NewDomainError("no weosID provided", entityType, "", nil) + } + + existingEntity, err := s.GetContentEntity(ctx, weosID) + if err != nil { + return nil, NewDomainError("unexpected error fetching existing entity", entityType, weosID, err) + } + + //TODO create the update func on the content entity + updatedEntity := existingEntity.Update(payload) + return updatedEntity, nil } func NewDomainService(ctx context.Context, eventRepository EventRepository) *DomainService { diff --git a/model/domain_service_test.go b/model/domain_service_test.go index d46486dd..de699832 100644 --- a/model/domain_service_test.go +++ b/model/domain_service_test.go @@ -151,6 +151,7 @@ func TestDomainService_Update(t *testing.T) { return nil }, } + //load open api spec swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile("../controllers/rest/fixtures/blog.yaml") if err != nil { @@ -166,26 +167,15 @@ func TestDomainService_Update(t *testing.T) { Schema: contentTypeSchema.Value, }) - //mock existing entity - mockPayload := map[string]interface{}{"weos_id": "123456", "sequence_no": int64(1), "title": "Test Blog", "description": "testing"} - mockContentEntity := &model.ContentEntity{ - AggregateRoot: model.AggregateRoot{ - BasicEntity: model.BasicEntity{ - ID: "123456", - }, - SequenceNo: 1, - }, - Property: mockPayload, - } - - update := &Blog{ + //Create a blog + mockBlog := &Blog{ Title: "First blog", Description: "Description testing 1", Url: "www.TestBlog.com", } entityType := "Blog" - reqBytes, err := json.Marshal(update) + reqBytes, err := json.Marshal(mockBlog) if err != nil { t.Fatalf("error converting content type to bytes %s", err) } @@ -193,19 +183,29 @@ func TestDomainService_Update(t *testing.T) { dService := model.NewDomainService(newContext, mockEventRepository) blog, err := dService.Create(newContext, reqBytes, entityType) + //Update a blog - payload uses woesID and seq no from the created entity + updatedPayload := map[string]interface{}{"weos_id": blog.ID, "sequence_no": blog.SequenceNo, "title": "Update Blog", "description": "Description testing 2", "url": "www.TestBlog2.com"} + + reqBytes, err = json.Marshal(updatedPayload) + if err != nil { + t.Fatalf("error converting content type to bytes %s", err) + } + + updatedBlog, err := dService.Update(newContext, reqBytes, entityType) + if err != nil { t.Fatalf("unexpected error creating content type '%s'", err) } - if blog == nil { + if updatedBlog == nil { t.Fatal("expected blog to be returned") } - if blog.GetString("Title") != mockBlog.Title { - t.Fatalf("expected blog title to be %s got %s", mockBlog.Title, blog.GetString("Title")) + if updatedBlog.GetString("Title") != updatedPayload["title"] { + t.Fatalf("expected blog title to be %s got %s", updatedPayload["title"], updatedBlog.GetString("Title")) } - if blog.GetString("Description") != mockBlog.Description { - t.Fatalf("expected blog description to be %s got %s", mockBlog.Description, blog.GetString("Description")) + if updatedBlog.GetString("Description") != updatedPayload["description"] { + t.Fatalf("expected blog description to be %s got %s", updatedPayload["description"], updatedBlog.GetString("Description")) } - if blog.GetString("Url") != mockBlog.Url { - t.Fatalf("expected blog url to be %s got %s", mockBlog.Url, blog.GetString("Url")) + if updatedBlog.GetString("Url") != updatedPayload["url"] { + t.Fatalf("expected blog url to be %s got %s", updatedPayload["url"], updatedBlog.GetString("Url")) } } diff --git a/model/receiver.go b/model/receiver.go index 5bcb9fe2..3a69d4a0 100644 --- a/model/receiver.go +++ b/model/receiver.go @@ -45,8 +45,12 @@ func (r *Receiver) CreateBatch(ctx context.Context, command *Command) error { //Update is used for a single payload. It takes in the command and context which is used to dispatch and updated the specified entity. func (r *Receiver) Update(ctx context.Context, command *Command) error { + payload, err := AddIDToPayload(command.Payload, command.Metadata.EntityID) + if err != nil { + return err + } - updatedEntity, err := r.domainService.Update(ctx, command.Payload, command.Metadata.EntityType) + updatedEntity, err := r.domainService.Update(ctx, payload, command.Metadata.EntityType) if err != nil { return err } diff --git a/projections/gorm.go b/projections/gorm.go index 35f8a482..dc2c19ed 100644 --- a/projections/gorm.go +++ b/projections/gorm.go @@ -36,6 +36,7 @@ func (p *GORMProjection) GetByID(ctxt context.Context, contentType weosContext.C } func (p *GORMProjection) GetByEntityID(ctxt context.Context, contentType weosContext.ContentType, id string) (interface{}, error) { + return nil, nil } From 1d313e4fc26a53abd126f8e3e3337bb937aa67d8 Mon Sep 17 00:00:00 2001 From: RandyDeo Date: Fri, 14 Jan 2022 10:04:36 -0400 Subject: [PATCH 12/20] feature:WEOS-1132 - Added todos --- model/domain_service.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/model/domain_service.go b/model/domain_service.go index f327b98c..871b5c69 100644 --- a/model/domain_service.go +++ b/model/domain_service.go @@ -59,7 +59,8 @@ func (s *DomainService) CreateBatch(ctx context.Context, payload json.RawMessage //Update is used for a single payload. It gets an existing entity and updates it with the new payload func (s *DomainService) Update(ctx context.Context, payload json.RawMessage, entityType string) (*ContentEntity, error) { - + //TODO check for the ID(pk) they know + //TODO check for weosID weosID, err := GetIDfromPayload(payload) if err != nil { return nil, NewDomainError("unexpected error unmarshalling payload to get weosID", entityType, "", err) @@ -68,7 +69,8 @@ func (s *DomainService) Update(ctx context.Context, payload json.RawMessage, ent if weosID == "" { return nil, NewDomainError("no weosID provided", entityType, "", nil) } - + //TODO if blank get other ID + //TODO check if seq no = payload seq no existingEntity, err := s.GetContentEntity(ctx, weosID) if err != nil { return nil, NewDomainError("unexpected error fetching existing entity", entityType, weosID, err) From ee88035830afc8d0dd56d793328e2b4cc58958d8 Mon Sep 17 00:00:00 2001 From: RandyDeo Date: Fri, 14 Jan 2022 11:38:02 -0400 Subject: [PATCH 13/20] feature:WEOS-1132 - Updated command receiver and service --- model/command_standard.go | 3 +-- model/domain_service.go | 5 +++-- model/receiver.go | 6 +----- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/model/command_standard.go b/model/command_standard.go index 97d2ad12..3ade388f 100644 --- a/model/command_standard.go +++ b/model/command_standard.go @@ -38,7 +38,7 @@ func CreateBatch(ctx context.Context, payload json.RawMessage, entityType string return command } -func Update(ctx context.Context, payload json.RawMessage, entityType string, entityID string) *Command { +func Update(ctx context.Context, payload json.RawMessage, entityType string) *Command { command := &Command{ Type: "update", @@ -48,7 +48,6 @@ func Update(ctx context.Context, payload json.RawMessage, entityType string, ent UserID: weoscontext.GetUser(ctx), AccountID: weoscontext.GetAccount(ctx), EntityType: entityType, - EntityID: entityID, }, } return command diff --git a/model/domain_service.go b/model/domain_service.go index 871b5c69..9893c991 100644 --- a/model/domain_service.go +++ b/model/domain_service.go @@ -59,8 +59,9 @@ func (s *DomainService) CreateBatch(ctx context.Context, payload json.RawMessage //Update is used for a single payload. It gets an existing entity and updates it with the new payload func (s *DomainService) Update(ctx context.Context, payload json.RawMessage, entityType string) (*ContentEntity, error) { - //TODO check for the ID(pk) they know - //TODO check for weosID + //TODO check for the ID(pk) in the context + //TODO if ID, use getByKey func + //TODO check for weosID in payload weosID, err := GetIDfromPayload(payload) if err != nil { return nil, NewDomainError("unexpected error unmarshalling payload to get weosID", entityType, "", err) diff --git a/model/receiver.go b/model/receiver.go index 3a69d4a0..5bcb9fe2 100644 --- a/model/receiver.go +++ b/model/receiver.go @@ -45,12 +45,8 @@ func (r *Receiver) CreateBatch(ctx context.Context, command *Command) error { //Update is used for a single payload. It takes in the command and context which is used to dispatch and updated the specified entity. func (r *Receiver) Update(ctx context.Context, command *Command) error { - payload, err := AddIDToPayload(command.Payload, command.Metadata.EntityID) - if err != nil { - return err - } - updatedEntity, err := r.domainService.Update(ctx, payload, command.Metadata.EntityType) + updatedEntity, err := r.domainService.Update(ctx, command.Payload, command.Metadata.EntityType) if err != nil { return err } From 39f725b556d9c88fc4e90b5563654f8e4a4ffd36 Mon Sep 17 00:00:00 2001 From: RandyDeo Date: Fri, 14 Jan 2022 15:07:17 -0400 Subject: [PATCH 14/20] feature:WEOS-1132 - Updated Mocks - Added update to content entity - Tested content entity - Added service - Tested Service - Updated receiver - Testing receiver still - Updated utils to have a getSeqNo --- controllers/rest/weos_mocks_test.go | 56 +++++++++++++++++++ model/content_entity.go | 13 +++++ model/content_entity_test.go | 59 ++++++++++++++++++++ model/domain_service.go | 80 ++++++++++++++++++++++------ model/domain_service_test.go | 55 +++++++++++-------- model/event_repository_mocks_test.go | 58 ++++++++++++++++++++ model/interfaces.go | 6 ++- model/receiver.go | 2 +- model/receiver_test.go | 35 +++++++++--- model/utils.go | 17 ++++++ projections/gorm.go | 2 +- 11 files changed, 334 insertions(+), 49 deletions(-) diff --git a/controllers/rest/weos_mocks_test.go b/controllers/rest/weos_mocks_test.go index 356c2439..f46f972a 100644 --- a/controllers/rest/weos_mocks_test.go +++ b/controllers/rest/weos_mocks_test.go @@ -6,6 +6,7 @@ package rest_test import ( "context" "database/sql" + context2 "github.com/wepala/weos-service/context" weos "github.com/wepala/weos-service/model" "gorm.io/gorm" "net/http" @@ -1692,6 +1693,9 @@ var _ weos.Projection = &ProjectionMock{} // // // make and configure a mocked model.Projection // mockedProjection := &ProjectionMock{ +// GetByKeyFunc: func(ctxt context.Context, contentType *context2.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) { +// panic("mock out the GetByKey method") +// }, // GetContentEntityFunc: func(ctx context.Context, weosID string) (*model.ContentEntity, error) { // panic("mock out the GetContentEntity method") // }, @@ -1708,6 +1712,9 @@ var _ weos.Projection = &ProjectionMock{} // // } type ProjectionMock struct { + // GetByKeyFunc mocks the GetByKey method. + GetByKeyFunc func(ctxt context.Context, contentType *context2.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) + // GetContentEntityFunc mocks the GetContentEntity method. GetContentEntityFunc func(ctx context.Context, weosID string) (*weos.ContentEntity, error) @@ -1719,6 +1726,15 @@ type ProjectionMock struct { // calls tracks calls to the methods. calls struct { + // GetByKey holds details about calls to the GetByKey method. + GetByKey []struct { + // Ctxt is the ctxt argument value. + Ctxt context.Context + // ContentType is the contentType argument value. + ContentType *context2.ContentType + // Identifiers is the identifiers argument value. + Identifiers map[string]interface{} + } // GetContentEntity holds details about calls to the GetContentEntity method. GetContentEntity []struct { // Ctx is the ctx argument value. @@ -1735,11 +1751,51 @@ type ProjectionMock struct { Ctx context.Context } } + lockGetByKey sync.RWMutex lockGetContentEntity sync.RWMutex lockGetEventHandler sync.RWMutex lockMigrate sync.RWMutex } +// GetByKey calls GetByKeyFunc. +func (mock *ProjectionMock) GetByKey(ctxt context.Context, contentType *context2.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) { + if mock.GetByKeyFunc == nil { + panic("ProjectionMock.GetByKeyFunc: method is nil but Projection.GetByKey was just called") + } + callInfo := struct { + Ctxt context.Context + ContentType *context2.ContentType + Identifiers map[string]interface{} + }{ + Ctxt: ctxt, + ContentType: contentType, + Identifiers: identifiers, + } + mock.lockGetByKey.Lock() + mock.calls.GetByKey = append(mock.calls.GetByKey, callInfo) + mock.lockGetByKey.Unlock() + return mock.GetByKeyFunc(ctxt, contentType, identifiers) +} + +// GetByKeyCalls gets all the calls that were made to GetByKey. +// Check the length with: +// len(mockedProjection.GetByKeyCalls()) +func (mock *ProjectionMock) GetByKeyCalls() []struct { + Ctxt context.Context + ContentType *context2.ContentType + Identifiers map[string]interface{} +} { + var calls []struct { + Ctxt context.Context + ContentType *context2.ContentType + Identifiers map[string]interface{} + } + mock.lockGetByKey.RLock() + calls = mock.calls.GetByKey + mock.lockGetByKey.RUnlock() + return calls +} + // GetContentEntity calls GetContentEntityFunc. func (mock *ProjectionMock) GetContentEntity(ctx context.Context, weosID string) (*weos.ContentEntity, error) { if mock.GetContentEntityFunc == nil { diff --git a/model/content_entity.go b/model/content_entity.go index 4c73c04d..8211f07d 100644 --- a/model/content_entity.go +++ b/model/content_entity.go @@ -131,6 +131,13 @@ func (w *ContentEntity) FromSchemaWithValues(ctx context.Context, schema *openap return w, w.ApplyChanges([]*Event{event}) } +func (w *ContentEntity) Update(payload json.RawMessage) (*ContentEntity, error) { + + event := NewEntityEvent("update", w, w.ID, payload) + w.NewChange(event) + return w, w.ApplyChanges([]*Event{event}) +} + //GetString returns the string property value stored of a given the property name func (w *ContentEntity) GetString(name string) string { if w.Property == nil { @@ -210,6 +217,12 @@ func (w *ContentEntity) ApplyChanges(changes []*Event) error { return err } w.User.BasicEntity.ID = change.Meta.User + case "update": + err := json.Unmarshal(change.Payload, &w.Property) + if err != nil { + return err + } + w.User.BasicEntity.ID = change.Meta.User } } diff --git a/model/content_entity_test.go b/model/content_entity_test.go index be7a74a9..3f78bff1 100644 --- a/model/content_entity_test.go +++ b/model/content_entity_test.go @@ -116,3 +116,62 @@ func TestContentEntity_IsValid(t *testing.T) { } }) } + +func TestContentEntity_Update(t *testing.T) { + swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile("../controllers/rest/fixtures/blog.yaml") + if err != nil { + t.Fatalf("unexpected error occured '%s'", err) + } + var contentType string + var contentTypeSchema *openapi3.SchemaRef + contentType = "Blog" + contentTypeSchema = swagger.Components.Schemas[contentType] + ctx := context.Background() + ctx = context.WithValue(ctx, weosContext.CONTENT_TYPE, &weosContext.ContentType{ + Name: contentType, + Schema: contentTypeSchema.Value, + }) + ctx = context.WithValue(ctx, weosContext.USER_ID, "123") + + mockBlog := &Blog{ + Title: "test 1", + Description: "lorem ipsum", + Url: "www.ShaniahsBlog.com", + } + payload, err := json.Marshal(mockBlog) + if err != nil { + t.Fatalf("unexpected error marshalling payload '%s'", err) + } + + existingEntity, err := new(model.ContentEntity).FromSchemaWithValues(ctx, swagger.Components.Schemas["Blog"].Value, payload) + if err != nil { + t.Fatalf("unexpected error instantiating content entity '%s'", err) + } + + if existingEntity.GetString("Title") != "test 1" { + t.Errorf("expected the title to be '%s', got '%s'", "test 1", existingEntity.GetString("Title")) + } + + input := &Blog{ + Title: "updated title", + Description: "updated desc", + } + + updatedPayload, err := json.Marshal(input) + if err != nil { + t.Fatalf("unexpected error marshalling update payload '%s'", err) + } + + updatedEntity, err := existingEntity.Update(updatedPayload) + if err != nil { + t.Fatalf("unexpected error updating existing entity '%s'", err) + } + + if updatedEntity.GetString("Title") != "updated title" { + t.Errorf("expected the updated title to be '%s', got '%s'", "updated title", existingEntity.GetString("Title")) + } + + if updatedEntity.GetString("Description") != "updated desc" { + t.Errorf("expected the updated description to be '%s', got '%s'", "updated desc", existingEntity.GetString("Description")) + } +} diff --git a/model/domain_service.go b/model/domain_service.go index e9a0b88c..5f2f0176 100644 --- a/model/domain_service.go +++ b/model/domain_service.go @@ -2,13 +2,14 @@ package model import ( "encoding/json" + "strconv" weosContext "github.com/wepala/weos-service/context" "golang.org/x/net/context" ) type DomainService struct { - Projection //not sure if its this simple + Projection Repository eventRepository EventRepository } @@ -60,31 +61,78 @@ func (s *DomainService) CreateBatch(ctx context.Context, payload json.RawMessage //Update is used for a single payload. It gets an existing entity and updates it with the new payload func (s *DomainService) Update(ctx context.Context, payload json.RawMessage, entityType string) (*ContentEntity, error) { - //TODO check for the ID(pk) in the context - //TODO if ID, use getByKey func - //TODO check for weosID in payload + var existingEntity *ContentEntity + var updatedEntity *ContentEntity + var identifier map[string]interface{} + var weosID string + contentType := weosContext.GetContentType(ctx) + + //Fetch the weosID from the payload weosID, err := GetIDfromPayload(payload) if err != nil { return nil, NewDomainError("unexpected error unmarshalling payload to get weosID", entityType, "", err) } - if weosID == "" { - return nil, NewDomainError("no weosID provided", entityType, "", nil) - } - //TODO if blank get other ID - //TODO check if seq no = payload seq no - existingEntity, err := s.GetContentEntity(ctx, weosID) - if err != nil { - return nil, NewDomainError("unexpected error fetching existing entity", entityType, weosID, err) - } + //If there is a weosID present use this + if weosID != "" { + seqNo, err := GetSeqfromPayload(payload) + if err != nil { + return nil, NewDomainError("unexpected error unmarshalling payload to get sequence number", entityType, "", err) + } + + if seqNo == "" { + return nil, NewDomainError("no sequence number provided", entityType, "", nil) + } + + existingEntity, err := s.GetContentEntity(ctx, weosID) + if err != nil { + return nil, NewDomainError("unexpected error fetching existing entity", entityType, weosID, err) + } + + entitySeqNo := strconv.Itoa(int(existingEntity.SequenceNo)) + + if seqNo != entitySeqNo { + return nil, NewDomainError("error updating entity. This is a stale item", entityType, weosID, nil) + } + + updatedEntity, err = existingEntity.Update(payload) + if err != nil { + return nil, NewDomainError("unexpected error updating existingEntity", entityType, weosID, err) + } + + //If there is no weosID, use the id passed from the param + } else if weosID == "" { + paramID := ctx.Value("id") + + if paramID == "" { + return nil, NewDomainError("no ID provided", entityType, "", nil) + } + + identifier = map[string]interface{}{"id": paramID} + entityInterface, err := s.GetByKey(ctx, contentType, identifier) + + data, err := json.Marshal(entityInterface) + if err != nil { + return nil, NewDomainError("unexpected error marshalling existingEntity interface", entityType, paramID.(string), err) + } + + err = json.Unmarshal(data, &existingEntity) + if err != nil { + return nil, NewDomainError("unexpected error unmarshalling existingEntity", entityType, paramID.(string), err) + } - //TODO create the update func on the content entity - updatedEntity := existingEntity.Update(payload) + updatedEntity, err = existingEntity.Update(payload) + if err != nil { + return nil, NewDomainError("unexpected error updating existingEntity", entityType, paramID.(string), err) + } + + } return updatedEntity, nil } -func NewDomainService(ctx context.Context, eventRepository EventRepository) *DomainService { +func NewDomainService(ctx context.Context, eventRepository EventRepository, projections Projection) *DomainService { return &DomainService{ eventRepository: eventRepository, + Projection: projections, } } diff --git a/model/domain_service_test.go b/model/domain_service_test.go index de699832..54e311fd 100644 --- a/model/domain_service_test.go +++ b/model/domain_service_test.go @@ -1,6 +1,7 @@ package model_test import ( + context3 "context" "encoding/json" "github.com/getkin/kin-openapi/openapi3" context2 "github.com/wepala/weos-service/context" @@ -44,7 +45,7 @@ func TestDomainService_Create(t *testing.T) { t.Fatalf("error converting content type to bytes %s", err) } - dService := model.NewDomainService(newContext, mockEventRepository) + dService := model.NewDomainService(newContext, mockEventRepository, nil) blog, err := dService.Create(newContext, reqBytes, entityType) if err != nil { @@ -73,7 +74,7 @@ func TestDomainService_Create(t *testing.T) { if err != nil { t.Fatalf("error converting content type to bytes %s", err) } - dService := model.NewDomainService(newContext, mockEventRepository) + dService := model.NewDomainService(newContext, mockEventRepository, nil) blog, err := dService.Create(newContext, reqBytes, entityType) if err.Error() != "entity property title required" { @@ -123,7 +124,7 @@ func TestDomainService_CreateBatch(t *testing.T) { t.Fatalf("error converting content type to bytes %s", err) } - dService := model.NewDomainService(newContext, mockEventRepository) + dService := model.NewDomainService(newContext, mockEventRepository, nil) blogs, err := dService.CreateBatch(newContext, reqBytes, entityType) if err != nil { @@ -146,12 +147,6 @@ func TestDomainService_CreateBatch(t *testing.T) { func TestDomainService_Update(t *testing.T) { - mockEventRepository := &EventRepositoryMock{ - PersistFunc: func(ctxt context.Context, entity model.AggregateInterface) error { - return nil - }, - } - //load open api spec swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile("../controllers/rest/fixtures/blog.yaml") if err != nil { @@ -167,34 +162,50 @@ func TestDomainService_Update(t *testing.T) { Schema: contentTypeSchema.Value, }) - //Create a blog - mockBlog := &Blog{ - Title: "First blog", - Description: "Description testing 1", - Url: "www.TestBlog.com", - } entityType := "Blog" - reqBytes, err := json.Marshal(mockBlog) + existingPayload := map[string]interface{}{"weos_id": "dsafdsdfdsf", "sequence_no": int64(1), "title": "blog 1", "description": "Description testing 1", "url": "www.TestBlog1.com"} + reqBytes, err := json.Marshal(existingPayload) if err != nil { - t.Fatalf("error converting content type to bytes %s", err) + t.Fatalf("error converting payload to bytes %s", err) + } + + mockEventRepository := &EventRepositoryMock{ + PersistFunc: func(ctxt context.Context, entity model.AggregateInterface) error { + return nil + }, + } + + dService := model.NewDomainService(newContext, mockEventRepository, nil) + existingBlog, err := dService.Create(newContext, reqBytes, entityType) + + projectionMock := &ProjectionMock{ + GetContentEntityFunc: func(ctx context3.Context, weosID string) (*model.ContentEntity, error) { + return existingBlog, nil + }, + GetByKeyFunc: func(ctxt context3.Context, contentType *context2.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) { + return existingPayload, nil + }, } - dService := model.NewDomainService(newContext, mockEventRepository) - blog, err := dService.Create(newContext, reqBytes, entityType) + dService1 := model.NewDomainService(newContext, mockEventRepository, projectionMock) //Update a blog - payload uses woesID and seq no from the created entity - updatedPayload := map[string]interface{}{"weos_id": blog.ID, "sequence_no": blog.SequenceNo, "title": "Update Blog", "description": "Description testing 2", "url": "www.TestBlog2.com"} + updatedPayload := map[string]interface{}{"weos_id": "dsafdsdfdsf", "sequence_no": "1", "title": "Update Blog", "description": "Update Description", "url": "www.Updated!.com"} + updatedReqBytes, err := json.Marshal(updatedPayload) + if err != nil { + t.Fatalf("error converting payload to bytes %s", err) + } reqBytes, err = json.Marshal(updatedPayload) if err != nil { t.Fatalf("error converting content type to bytes %s", err) } - updatedBlog, err := dService.Update(newContext, reqBytes, entityType) + updatedBlog, err := dService1.Update(newContext, updatedReqBytes, entityType) if err != nil { - t.Fatalf("unexpected error creating content type '%s'", err) + t.Fatalf("unexpected error updating content type '%s'", err) } if updatedBlog == nil { t.Fatal("expected blog to be returned") diff --git a/model/event_repository_mocks_test.go b/model/event_repository_mocks_test.go index 32343cff..ba89115b 100644 --- a/model/event_repository_mocks_test.go +++ b/model/event_repository_mocks_test.go @@ -6,6 +6,7 @@ package model_test import ( "context" "database/sql" + context2 "github.com/wepala/weos-service/context" weos "github.com/wepala/weos-service/model" "gorm.io/gorm" "net/http" @@ -496,6 +497,9 @@ var _ weos.Projection = &ProjectionMock{} // // // make and configure a mocked model.Projection // mockedProjection := &ProjectionMock{ +// GetByKeyFunc: func(ctxt context.Context, contentType *context2.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) { +// panic("mock out the GetByKey method") +// }, // GetContentEntityFunc: func(ctx context.Context, weosID string) (*model.ContentEntity, error) { // panic("mock out the GetContentEntity method") // }, @@ -512,6 +516,9 @@ var _ weos.Projection = &ProjectionMock{} // // } type ProjectionMock struct { + // GetByKeyFunc mocks the GetByKey method. + GetByKeyFunc func(ctxt context.Context, contentType *context2.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) + // GetContentEntityFunc mocks the GetContentEntity method. GetContentEntityFunc func(ctx context.Context, weosID string) (*weos.ContentEntity, error) @@ -523,6 +530,15 @@ type ProjectionMock struct { // calls tracks calls to the methods. calls struct { + // GetByKey holds details about calls to the GetByKey method. + GetByKey []struct { + // Ctxt is the ctxt argument value. + Ctxt context.Context + // ContentType is the contentType argument value. + ContentType *context2.ContentType + // Identifiers is the identifiers argument value. + Identifiers map[string]interface{} + } // GetContentEntity holds details about calls to the GetContentEntity method. GetContentEntity []struct { // Ctx is the ctx argument value. @@ -539,11 +555,51 @@ type ProjectionMock struct { Ctx context.Context } } + lockGetByKey sync.RWMutex lockGetContentEntity sync.RWMutex lockGetEventHandler sync.RWMutex lockMigrate sync.RWMutex } +// GetByKey calls GetByKeyFunc. +func (mock *ProjectionMock) GetByKey(ctxt context.Context, contentType *context2.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) { + if mock.GetByKeyFunc == nil { + panic("ProjectionMock.GetByKeyFunc: method is nil but Projection.GetByKey was just called") + } + callInfo := struct { + Ctxt context.Context + ContentType *context2.ContentType + Identifiers map[string]interface{} + }{ + Ctxt: ctxt, + ContentType: contentType, + Identifiers: identifiers, + } + mock.lockGetByKey.Lock() + mock.calls.GetByKey = append(mock.calls.GetByKey, callInfo) + mock.lockGetByKey.Unlock() + return mock.GetByKeyFunc(ctxt, contentType, identifiers) +} + +// GetByKeyCalls gets all the calls that were made to GetByKey. +// Check the length with: +// len(mockedProjection.GetByKeyCalls()) +func (mock *ProjectionMock) GetByKeyCalls() []struct { + Ctxt context.Context + ContentType *context2.ContentType + Identifiers map[string]interface{} +} { + var calls []struct { + Ctxt context.Context + ContentType *context2.ContentType + Identifiers map[string]interface{} + } + mock.lockGetByKey.RLock() + calls = mock.calls.GetByKey + mock.lockGetByKey.RUnlock() + return calls +} + // GetContentEntity calls GetContentEntityFunc. func (mock *ProjectionMock) GetContentEntity(ctx context.Context, weosID string) (*weos.ContentEntity, error) { if mock.GetContentEntityFunc == nil { @@ -1833,3 +1889,5 @@ func (mock *ApplicationMock) TitleCalls() []struct { mock.lockTitle.RUnlock() return calls } + + diff --git a/model/interfaces.go b/model/interfaces.go index 33236364..3e9a4cac 100644 --- a/model/interfaces.go +++ b/model/interfaces.go @@ -1,7 +1,10 @@ package model //go:generate moq -out temp_mocks_test.go -pkg model_test . Projection -import "golang.org/x/net/context" +import ( + weosContext "github.com/wepala/weos-service/context" + "golang.org/x/net/context" +) type WeOSEntity interface { Entity @@ -64,4 +67,5 @@ type Projection interface { Datastore GetEventHandler() EventHandler GetContentEntity(ctx context.Context, weosID string) (*ContentEntity, error) + GetByKey(ctxt context.Context, contentType *weosContext.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) } diff --git a/model/receiver.go b/model/receiver.go index 5bcb9fe2..65306714 100644 --- a/model/receiver.go +++ b/model/receiver.go @@ -67,7 +67,7 @@ func Initialize(service Service) error { service.Dispatcher().AddSubscriber(CreateBatch(context.Background(), payload, ""), receiver.CreateBatch) service.Dispatcher().AddSubscriber(Create(context.Background(), payload, "", ""), receiver.Update) //initialize any services - receiver.domainService = NewDomainService(context.Background(), service.EventRepository()) + receiver.domainService = NewDomainService(context.Background(), service.EventRepository(), nil) if receiver.domainService == nil { return NewError("no projection provided", nil) diff --git a/model/receiver_test.go b/model/receiver_test.go index 889d5317..f006fd0d 100644 --- a/model/receiver_test.go +++ b/model/receiver_test.go @@ -1,6 +1,7 @@ package model_test import ( + context3 "context" "encoding/json" "github.com/getkin/kin-openapi/openapi3" weosContext "github.com/wepala/weos-service/context" @@ -166,6 +167,28 @@ func TestUpdateContentType(t *testing.T) { AddSubscriberFunc: func(handler model.EventHandler) { }, } + + existingPayload := map[string]interface{}{"weos_id": "dsafdsdfdsf", "sequence_no": int64(1), "title": "blog 1", "description": "Description testing 1", "url": "www.TestBlog1.com"} + existingBlog := &model.ContentEntity{ + AggregateRoot: model.AggregateRoot{ + BasicEntity: model.BasicEntity{ + ID: "dsafdsdfdsf", + }, + SequenceNo: int64(1), + //TODO Add Create Event + }, + Property: existingPayload, + } + + projectionMock := &ProjectionMock{ + GetContentEntityFunc: func(ctx context3.Context, weosID string) (*model.ContentEntity, error) { + return existingBlog, nil + }, + GetByKeyFunc: func(ctxt context3.Context, contentType *weosContext.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) { + return existingPayload, nil + }, + } + application := &ApplicationMock{ DispatcherFunc: func() model.Dispatcher { return commandDispatcher @@ -174,7 +197,7 @@ func TestUpdateContentType(t *testing.T) { return mockEventRepository }, ProjectionsFunc: func() []model.Projection { - return []model.Projection{} + return []model.Projection{projectionMock} }, } @@ -184,18 +207,14 @@ func TestUpdateContentType(t *testing.T) { } t.Run("Testing basic update entity", func(t *testing.T) { - mockBlog := &Blog{ - ID: "123", - Title: "Test Blog", - Url: "ww.testingBlog.com", - } + updatedPayload := map[string]interface{}{"weos_id": "dsafdsdfdsf", "sequence_no": "1", "title": "Update Blog", "description": "Update Description", "url": "www.Updated!.com"} entityType := "Blog" - reqBytes, err := json.Marshal(mockBlog) + reqBytes, err := json.Marshal(updatedPayload) if err != nil { t.Fatalf("error converting content type to bytes %s", err) } - err1 := commandDispatcher.Dispatch(ctx, model.Update(ctx, reqBytes, entityType, "123")) + err1 := commandDispatcher.Dispatch(ctx, model.Update(ctx, reqBytes, entityType)) if err1 != nil { t.Fatalf("unexpected error dispatching command '%s'", err1) } diff --git a/model/utils.go b/model/utils.go index 5f6a5193..57722634 100644 --- a/model/utils.go +++ b/model/utils.go @@ -47,3 +47,20 @@ func GetIDfromPayload(payload []byte) (string, error) { return weosID, nil } + +//GetSeqfromPayload: This returns the sequence number from payload +func GetSeqfromPayload(payload []byte) (string, error) { + var tempPayload map[string]interface{} + err := json.Unmarshal(payload, &tempPayload) + if err != nil { + return "", err + } + + if tempPayload["sequence_no"] == nil { + tempPayload["sequence_no"] = "" + } + + seqNo := tempPayload["sequence_no"].(string) + + return seqNo, nil +} diff --git a/projections/gorm.go b/projections/gorm.go index b600cecf..37dd5171 100644 --- a/projections/gorm.go +++ b/projections/gorm.go @@ -21,7 +21,7 @@ type GORMProjection struct { Schema map[string]interface{} } -func (p *GORMProjection) GetByKey(ctxt context.Context, contentType weosContext.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) { +func (p *GORMProjection) GetByKey(ctxt context.Context, contentType *weosContext.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) { if scheme, ok := p.Schema[strings.Title(contentType.Name)]; ok { //pulling the primary keys from the schema in order to match with the keys given for searching pks, _ := json.Marshal(contentType.Schema.Extensions["x-identifier"]) From 6a5f7b2ce5a3a0f5ae3c307cf7a2a15b93327f59 Mon Sep 17 00:00:00 2001 From: RandyDeo Date: Mon, 17 Jan 2022 06:47:17 -0400 Subject: [PATCH 15/20] feature:WEOS-1132 - Updated Receiver and receiver test --- model/receiver.go | 2 +- model/receiver_test.go | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/model/receiver.go b/model/receiver.go index 65306714..61914935 100644 --- a/model/receiver.go +++ b/model/receiver.go @@ -65,7 +65,7 @@ func Initialize(service Service) error { //add command handlers to the application's command dispatcher service.Dispatcher().AddSubscriber(Create(context.Background(), payload, "", ""), receiver.Create) service.Dispatcher().AddSubscriber(CreateBatch(context.Background(), payload, ""), receiver.CreateBatch) - service.Dispatcher().AddSubscriber(Create(context.Background(), payload, "", ""), receiver.Update) + service.Dispatcher().AddSubscriber(Update(context.Background(), payload, ""), receiver.Update) //initialize any services receiver.domainService = NewDomainService(context.Background(), service.EventRepository(), nil) diff --git a/model/receiver_test.go b/model/receiver_test.go index f006fd0d..2e0d677b 100644 --- a/model/receiver_test.go +++ b/model/receiver_test.go @@ -156,7 +156,7 @@ func TestUpdateContentType(t *testing.T) { } if event.Type != "update" { - t.Errorf("expected event to be '%s', got '%s'", "create", event.Type) + t.Errorf("expected event to be '%s', got '%s'", "update", event.Type) } if event.Meta.EntityType == "" { t.Errorf("expected event to be '%s', got '%s'", "", event.Type) @@ -174,11 +174,12 @@ func TestUpdateContentType(t *testing.T) { BasicEntity: model.BasicEntity{ ID: "dsafdsdfdsf", }, - SequenceNo: int64(1), - //TODO Add Create Event + SequenceNo: int64(0), }, Property: existingPayload, } + event := model.NewEntityEvent("update", existingBlog, existingBlog.ID, existingPayload) + existingBlog.NewChange(event) projectionMock := &ProjectionMock{ GetContentEntityFunc: func(ctx context3.Context, weosID string) (*model.ContentEntity, error) { From 453fb2d00ea1825b91e912bb7977aae4a23c9789 Mon Sep 17 00:00:00 2001 From: RandyDeo Date: Mon, 17 Jan 2022 06:59:29 -0400 Subject: [PATCH 16/20] feature:WEOS-1132 - Added & to projections test --- projections/projections_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projections/projections_test.go b/projections/projections_test.go index 5b23a330..35c9c959 100644 --- a/projections/projections_test.go +++ b/projections/projections_test.go @@ -1316,7 +1316,7 @@ components: } blogRef := swagger.Components.Schemas["Blog"] - r, err := p.GetByKey(context.Background(), weosContext.ContentType{Name: "Blog", Schema: blogRef.Value}, map[string]interface{}{ + r, err := p.GetByKey(context.Background(), &weosContext.ContentType{Name: "Blog", Schema: blogRef.Value}, map[string]interface{}{ "author_id": "kidding", "title": "hugs", }) From bb6106c6590abb73ddfbdd1bd966c32b614d3f59 Mon Sep 17 00:00:00 2001 From: RandyDeo Date: Mon, 17 Jan 2022 09:15:02 -0400 Subject: [PATCH 17/20] feature:WEOS-1123 - Fixed Receiver --- model/receiver.go | 6 ++++++ model/receiver_test.go | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/model/receiver.go b/model/receiver.go index 61914935..764ef44e 100644 --- a/model/receiver.go +++ b/model/receiver.go @@ -69,6 +69,12 @@ func Initialize(service Service) error { //initialize any services receiver.domainService = NewDomainService(context.Background(), service.EventRepository(), nil) + for _, projection := range service.Projections() { + if projections, ok := projection.(Projection); ok { + receiver.domainService = NewDomainService(context.Background(), service.EventRepository(), projections) + } + } + if receiver.domainService == nil { return NewError("no projection provided", nil) } diff --git a/model/receiver_test.go b/model/receiver_test.go index 2e0d677b..7a1b6d74 100644 --- a/model/receiver_test.go +++ b/model/receiver_test.go @@ -147,8 +147,8 @@ func TestUpdateContentType(t *testing.T) { var event *model.Event var ok bool entities := entity.GetNewChanges() - if len(entities) != 1 { - t.Fatalf("expected %d event to be saved, got %d", 1, len(entities)) + if len(entities) != 2 { + t.Fatalf("expected %d event to be saved, got %d", 2, len(entities)) } if event, ok = entities[0].(*model.Event); !ok { From 522153392d8c1a2eba59d44d5eee451e9b582fef Mon Sep 17 00:00:00 2001 From: RandyDeo Date: Mon, 17 Jan 2022 09:59:18 -0400 Subject: [PATCH 18/20] feature:WEOS-1132 - Added "invalid:" tags to errors --- model/domain_service.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/model/domain_service.go b/model/domain_service.go index 5f2f0176..99776c1f 100644 --- a/model/domain_service.go +++ b/model/domain_service.go @@ -70,23 +70,23 @@ func (s *DomainService) Update(ctx context.Context, payload json.RawMessage, ent //Fetch the weosID from the payload weosID, err := GetIDfromPayload(payload) if err != nil { - return nil, NewDomainError("unexpected error unmarshalling payload to get weosID", entityType, "", err) + return nil, NewDomainError("invalid: unexpected error unmarshalling payload to get weosID", entityType, "", err) } //If there is a weosID present use this if weosID != "" { seqNo, err := GetSeqfromPayload(payload) if err != nil { - return nil, NewDomainError("unexpected error unmarshalling payload to get sequence number", entityType, "", err) + return nil, NewDomainError("invalid: unexpected error unmarshalling payload to get sequence number", entityType, "", err) } if seqNo == "" { - return nil, NewDomainError("no sequence number provided", entityType, "", nil) + return nil, NewDomainError("invalid: no sequence number provided", entityType, "", nil) } existingEntity, err := s.GetContentEntity(ctx, weosID) if err != nil { - return nil, NewDomainError("unexpected error fetching existing entity", entityType, weosID, err) + return nil, NewDomainError("invalid: unexpected error fetching existing entity", entityType, weosID, err) } entitySeqNo := strconv.Itoa(int(existingEntity.SequenceNo)) @@ -97,7 +97,7 @@ func (s *DomainService) Update(ctx context.Context, payload json.RawMessage, ent updatedEntity, err = existingEntity.Update(payload) if err != nil { - return nil, NewDomainError("unexpected error updating existingEntity", entityType, weosID, err) + return nil, NewDomainError("invalid: unexpected error updating existingEntity", entityType, weosID, err) } //If there is no weosID, use the id passed from the param @@ -105,7 +105,7 @@ func (s *DomainService) Update(ctx context.Context, payload json.RawMessage, ent paramID := ctx.Value("id") if paramID == "" { - return nil, NewDomainError("no ID provided", entityType, "", nil) + return nil, NewDomainError("invalid: no ID provided", entityType, "", nil) } identifier = map[string]interface{}{"id": paramID} @@ -113,17 +113,17 @@ func (s *DomainService) Update(ctx context.Context, payload json.RawMessage, ent data, err := json.Marshal(entityInterface) if err != nil { - return nil, NewDomainError("unexpected error marshalling existingEntity interface", entityType, paramID.(string), err) + return nil, NewDomainError("invalid: unexpected error marshalling existingEntity interface", entityType, paramID.(string), err) } err = json.Unmarshal(data, &existingEntity) if err != nil { - return nil, NewDomainError("unexpected error unmarshalling existingEntity", entityType, paramID.(string), err) + return nil, NewDomainError("invalid: unexpected error unmarshalling existingEntity", entityType, paramID.(string), err) } updatedEntity, err = existingEntity.Update(payload) if err != nil { - return nil, NewDomainError("unexpected error updating existingEntity", entityType, paramID.(string), err) + return nil, NewDomainError("invalid: unexpected error updating existingEntity", entityType, paramID.(string), err) } } From 71a506e8c857dec314b437026845ce2a0831144b Mon Sep 17 00:00:00 2001 From: RandyDeo Date: Mon, 17 Jan 2022 12:38:14 -0400 Subject: [PATCH 19/20] feature:WEOS-1132 - Made requested changes on the pr - Revisited errors being returned --- model/content_entity.go | 2 +- model/domain_service.go | 51 +++++++++++++++-------- model/domain_service_test.go | 80 ++++++++++++++++++++++++------------ 3 files changed, 88 insertions(+), 45 deletions(-) diff --git a/model/content_entity.go b/model/content_entity.go index 8211f07d..39b5b6fb 100644 --- a/model/content_entity.go +++ b/model/content_entity.go @@ -220,7 +220,7 @@ func (w *ContentEntity) ApplyChanges(changes []*Event) error { case "update": err := json.Unmarshal(change.Payload, &w.Property) if err != nil { - return err + return NewDomainError("invalid: unable to get ID from payload", change.Meta.EntityType, w.ID, err) } w.User.BasicEntity.ID = change.Meta.User diff --git a/model/domain_service.go b/model/domain_service.go index 99776c1f..c9cee37f 100644 --- a/model/domain_service.go +++ b/model/domain_service.go @@ -63,25 +63,20 @@ func (s *DomainService) CreateBatch(ctx context.Context, payload json.RawMessage func (s *DomainService) Update(ctx context.Context, payload json.RawMessage, entityType string) (*ContentEntity, error) { var existingEntity *ContentEntity var updatedEntity *ContentEntity - var identifier map[string]interface{} var weosID string contentType := weosContext.GetContentType(ctx) //Fetch the weosID from the payload weosID, err := GetIDfromPayload(payload) if err != nil { - return nil, NewDomainError("invalid: unexpected error unmarshalling payload to get weosID", entityType, "", err) + return nil, err } //If there is a weosID present use this if weosID != "" { seqNo, err := GetSeqfromPayload(payload) if err != nil { - return nil, NewDomainError("invalid: unexpected error unmarshalling payload to get sequence number", entityType, "", err) - } - - if seqNo == "" { - return nil, NewDomainError("invalid: no sequence number provided", entityType, "", nil) + return nil, err } existingEntity, err := s.GetContentEntity(ctx, weosID) @@ -91,39 +86,59 @@ func (s *DomainService) Update(ctx context.Context, payload json.RawMessage, ent entitySeqNo := strconv.Itoa(int(existingEntity.SequenceNo)) - if seqNo != entitySeqNo { - return nil, NewDomainError("error updating entity. This is a stale item", entityType, weosID, nil) + if seqNo != "" { + if seqNo != entitySeqNo { + return nil, NewDomainError("error updating entity. This is a stale item", entityType, weosID, nil) + } } updatedEntity, err = existingEntity.Update(payload) if err != nil { - return nil, NewDomainError("invalid: unexpected error updating existingEntity", entityType, weosID, err) + return nil, err } //If there is no weosID, use the id passed from the param } else if weosID == "" { - paramID := ctx.Value("id") + var primaryKeys []string + identifiers := map[string]interface{}{} + + if contentType.Schema.Extensions["x-identifier"] != nil { + identifiersFromSchema := contentType.Schema.Extensions["x-identifier"].(json.RawMessage) + json.Unmarshal(identifiersFromSchema, &primaryKeys) + } + + if len(primaryKeys) == 0 { + primaryKeys = append(primaryKeys, "id") + } + + for _, pk := range primaryKeys { + ctxtIdentifier := ctx.Value(pk) - if paramID == "" { - return nil, NewDomainError("invalid: no ID provided", entityType, "", nil) + if ctxtIdentifier == "" { + return nil, NewDomainError("invalid: no value provided for primary key", entityType, "", nil) + } + + identifiers[pk] = pk } - identifier = map[string]interface{}{"id": paramID} - entityInterface, err := s.GetByKey(ctx, contentType, identifier) + entityInterface, err := s.GetByKey(ctx, contentType, identifiers) + if err != nil { + return nil, NewDomainError("invalid: unexpected error fetching existing entity", entityType, "", err) + } data, err := json.Marshal(entityInterface) if err != nil { - return nil, NewDomainError("invalid: unexpected error marshalling existingEntity interface", entityType, paramID.(string), err) + return nil, err } err = json.Unmarshal(data, &existingEntity) if err != nil { - return nil, NewDomainError("invalid: unexpected error unmarshalling existingEntity", entityType, paramID.(string), err) + return nil, err } updatedEntity, err = existingEntity.Update(payload) if err != nil { - return nil, NewDomainError("invalid: unexpected error updating existingEntity", entityType, paramID.(string), err) + return nil, err } } diff --git a/model/domain_service_test.go b/model/domain_service_test.go index 54e311fd..2e585fd3 100644 --- a/model/domain_service_test.go +++ b/model/domain_service_test.go @@ -188,35 +188,63 @@ func TestDomainService_Update(t *testing.T) { }, } + t.Run("Testing with valid ID,Title and Description", func(t *testing.T) { + dService1 := model.NewDomainService(newContext, mockEventRepository, projectionMock) + + //Update a blog - payload uses woesID and seq no from the created entity + updatedPayload := map[string]interface{}{"weos_id": "dsafdsdfdsf", "sequence_no": "1", "title": "Update Blog", "description": "Update Description", "url": "www.Updated!.com"} + updatedReqBytes, err := json.Marshal(updatedPayload) + if err != nil { + t.Fatalf("error converting payload to bytes %s", err) + } + + reqBytes, err = json.Marshal(updatedPayload) + if err != nil { + t.Fatalf("error converting content type to bytes %s", err) + } + + updatedBlog, err := dService1.Update(newContext, updatedReqBytes, entityType) + + if err != nil { + t.Fatalf("unexpected error updating content type '%s'", err) + } + if updatedBlog == nil { + t.Fatal("expected blog to be returned") + } + if updatedBlog.GetString("Title") != updatedPayload["title"] { + t.Fatalf("expected blog title to be %s got %s", updatedPayload["title"], updatedBlog.GetString("Title")) + } + if updatedBlog.GetString("Description") != updatedPayload["description"] { + t.Fatalf("expected blog description to be %s got %s", updatedPayload["description"], updatedBlog.GetString("Description")) + } + if updatedBlog.GetString("Url") != updatedPayload["url"] { + t.Fatalf("expected blog url to be %s got %s", updatedPayload["url"], updatedBlog.GetString("Url")) + } + }) + dService1 := model.NewDomainService(newContext, mockEventRepository, projectionMock) - //Update a blog - payload uses woesID and seq no from the created entity - updatedPayload := map[string]interface{}{"weos_id": "dsafdsdfdsf", "sequence_no": "1", "title": "Update Blog", "description": "Update Description", "url": "www.Updated!.com"} - updatedReqBytes, err := json.Marshal(updatedPayload) - if err != nil { - t.Fatalf("error converting payload to bytes %s", err) - } + t.Run("Testing with stale sequence number", func(t *testing.T) { - reqBytes, err = json.Marshal(updatedPayload) - if err != nil { - t.Fatalf("error converting content type to bytes %s", err) - } + //Update a blog - payload uses woesID and seq no from the created entity + updatedPayload := map[string]interface{}{"weos_id": "dsafdsdfdsf", "sequence_no": "3", "title": "Update Blog", "description": "Update Description", "url": "www.Updated!.com"} + updatedReqBytes, err := json.Marshal(updatedPayload) + if err != nil { + t.Fatalf("error converting payload to bytes %s", err) + } - updatedBlog, err := dService1.Update(newContext, updatedReqBytes, entityType) + reqBytes, err = json.Marshal(updatedPayload) + if err != nil { + t.Fatalf("error converting content type to bytes %s", err) + } - if err != nil { - t.Fatalf("unexpected error updating content type '%s'", err) - } - if updatedBlog == nil { - t.Fatal("expected blog to be returned") - } - if updatedBlog.GetString("Title") != updatedPayload["title"] { - t.Fatalf("expected blog title to be %s got %s", updatedPayload["title"], updatedBlog.GetString("Title")) - } - if updatedBlog.GetString("Description") != updatedPayload["description"] { - t.Fatalf("expected blog description to be %s got %s", updatedPayload["description"], updatedBlog.GetString("Description")) - } - if updatedBlog.GetString("Url") != updatedPayload["url"] { - t.Fatalf("expected blog url to be %s got %s", updatedPayload["url"], updatedBlog.GetString("Url")) - } + updatedBlog, err := dService1.Update(newContext, updatedReqBytes, entityType) + + if err == nil { + t.Fatalf("expected error updating content type '%s'", err) + } + if updatedBlog != nil { + t.Fatal("expected no blog to be returned") + } + }) } From c1433c2f33bd8e3ba3bddbac5908954ac402fec4 Mon Sep 17 00:00:00 2001 From: RandyDeo Date: Mon, 17 Jan 2022 20:52:04 -0400 Subject: [PATCH 20/20] feature:WEOS-1132 - Added Validation to service - Found some issues with the service and added code to address that - Added tests for compound keys - Add yaml files for testing - Updated update func in content entity. - Fixed content entity test --- .../rest/fixtures/blog-pk-guid-title.yaml | 582 ++++++++++++++++++ controllers/rest/fixtures/blog-pk-id.yaml | 581 +++++++++++++++++ model/content_entity.go | 16 +- model/content_entity_test.go | 7 +- model/domain_service.go | 23 +- model/domain_service_test.go | 241 +++++++- 6 files changed, 1439 insertions(+), 11 deletions(-) create mode 100644 controllers/rest/fixtures/blog-pk-guid-title.yaml create mode 100644 controllers/rest/fixtures/blog-pk-id.yaml diff --git a/controllers/rest/fixtures/blog-pk-guid-title.yaml b/controllers/rest/fixtures/blog-pk-guid-title.yaml new file mode 100644 index 00000000..410096c3 --- /dev/null +++ b/controllers/rest/fixtures/blog-pk-guid-title.yaml @@ -0,0 +1,582 @@ +openapi: 3.0.3 +info: + title: Blog + description: Blog example + version: 1.0.0 +servers: + - url: https://prod1.weos.sh/blog/dev + description: WeOS Dev + - url: https://prod1.weos.sh/blog/v1 +x-weos-config: + event-source: + - title: default + driver: service + endpoint: https://prod1.weos.sh/events/v1 + - title: event + driver: sqlite3 + database: test.db + database: + driver: sqlite3 + database: test.db + databases: + - title: default + driver: sqlite3 + database: test.db + rest: + middleware: + - RequestID + - Recover + - ZapLogger +components: + schemas: + Category: + type: object + properties: + title: + type: string + description: + type: string + required: + - title + x-identifier: + - title + Author: + type: object + properties: + id: + type: string + format: ksuid + firstName: + type: string + lastName: + type: string + email: + type: string + format: email + required: + - firstName + - lastName + x-identifier: + - id + - email + Blog: + type: object + properties: + id: + type: string + url: + type: string + format: uri + title: + type: string + description: + type: string + status: + type: string + nullable: true + enum: + - null + - unpublished + - published + image: + type: string + format: byte + categories: + type: array + items: + $ref: "#/components/schemas/Post" + posts: + type: array + items: + $ref: "#/components/schemas/Category" + lastUpdated: + type: string + format: date-time + created: + type: string + format: date-time + required: + - title + - url + x-identifier: + - guid + - title + Post: + type: object + properties: + title: + type: string + description: + type: string + author: + $ref: "#/components/schemas/Author" + created: + type: string + format: date-time +paths: + /health: + summary: Health Check + get: + x-controller: HealthCheck + responses: + 200: + description: Health Response + 500: + description: API Internal Error + /blogs: + parameters: + - in: header + name: someHeader + schema: + type: string + - in: header + name: someOtherHeader + schema: + type: string + x-context-name: soh + - in: header + name: X-Account-Id + schema: + type: string + x-context-name: AccountID + - in: query + name: q + schema: + type: string + post: + operationId: Add Blog + summary: Create Blog + requestBody: + description: Blog info that is submitted + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Blog" + responses: + 201: + description: Add Blog to Aggregator + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + get: + operationId: Get Blogs + summary: Get List of Blogs + parameters: + - in: query + name: filters + schema: + type: array + items: + type: object + properties: + field: + type: string + operator: + type: string + values: + type: array + items: + type: string + + required: false + description: query string + x-context: + filters: + - field: status + operator: eq + values: + - Active + - field: lastUpdated + operator: between + values: + - 2021-12-17 15:46:00 + - 2021-12-18 15:46:00 + - field: categories + operator: in + values: + - Technology + - Javascript + sorts: + - field: title + order: asc + page: 1 + limit: 10 + responses: + 200: + description: List of blogs + content: + application/json: + schema: + type: object + properties: + total: + type: integer + page: + type: integer + items: + $ref: "#/components/schemas/Blog" + /blogs/{id}: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + description: blog id + summary: Get Blog by id + operationId: Get Blog + responses: + 200: + description: Blog details without any supporting collections + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + put: + parameters: + - in: path + name: id + schema: + type: string + required: true + description: blog id + summary: Update blog details + operationId: Update Blog + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + responses: + 200: + description: Update Blog + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + delete: + parameters: + - in: path + name: id + schema: + type: string + required: true + description: blog id + summary: Delete blog + operationId: Delete Blog + responses: + 200: + description: Blog Deleted + + /posts/: + post: + operationId: Create Blog Post + summary: Create Blog Post + requestBody: + description: Post details + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Post" + responses: + 201: + description: Post + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + put: + operationId: Import Blog Posts + summary: Import Blog Posts + requestBody: + description: List of posts to import + required: true + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Post" + application/x-www-form-urlencoded: + schema: + type: array + items: + $ref: "#/components/schemas/Post" + responses: + 201: + description: Post + get: + operationId: Get Posts + summary: Get a blog's list of posts + parameters: + - in: query + name: q + schema: + type: string + required: false + description: query string + responses: + 200: + description: List of blog posts + content: + application/json: + schema: + type: object + properties: + total: + type: integer + page: + type: integer + items: + $ref: "#/components/schemas/Post" + + /posts/{id}: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Get blog post by id + responses: + 200: + description: Get blog post information + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + put: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Update post + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + responses: + 200: + description: Get blog post information + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + delete: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Delete post + responses: + 200: + description: Delete post + + + /categories/: + post: + operationId: Create Blog Category + summary: Create Blog Category + requestBody: + description: Post details + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Category" + responses: + 201: + description: Post + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + get: + operationId: Get Categories + summary: Get a blog's list of categories + parameters: + - in: query + name: q + schema: + type: string + required: false + description: query string + responses: + 200: + description: List of blog categories + content: + application/json: + schema: + type: object + properties: + total: + type: integer + page: + type: integer + items: + $ref: "#/components/schemas/Category" + + /categories/{id}: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Get blog category by id + responses: + 200: + description: Get blog category information + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + put: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Update category + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + responses: + 200: + description: Get blog category information + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + delete: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Delete category + responses: + 200: + description: Delete category + + /authors/: + post: + operationId: Create Blog Author + summary: Create Blog Author + requestBody: + description: Author details + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Author" + responses: + 201: + description: Post + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + get: + operationId: Get Authors + summary: Get a blog's list of authors + parameters: + - in: query + name: q + schema: + type: string + required: false + description: query string + responses: + 200: + description: List of blog authors + content: + application/json: + schema: + type: object + properties: + total: + type: integer + page: + type: integer + items: + $ref: "#/components/schemas/Author" + + /authors/{id}: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Get Author by id + responses: + 200: + description: Get author information + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + put: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Update Author details + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + responses: + 200: + description: Author details + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + delete: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Delete author + responses: + 200: + description: Delete author diff --git a/controllers/rest/fixtures/blog-pk-id.yaml b/controllers/rest/fixtures/blog-pk-id.yaml new file mode 100644 index 00000000..7116f94e --- /dev/null +++ b/controllers/rest/fixtures/blog-pk-id.yaml @@ -0,0 +1,581 @@ +openapi: 3.0.3 +info: + title: Blog + description: Blog example + version: 1.0.0 +servers: + - url: https://prod1.weos.sh/blog/dev + description: WeOS Dev + - url: https://prod1.weos.sh/blog/v1 +x-weos-config: + event-source: + - title: default + driver: service + endpoint: https://prod1.weos.sh/events/v1 + - title: event + driver: sqlite3 + database: test.db + database: + driver: sqlite3 + database: test.db + databases: + - title: default + driver: sqlite3 + database: test.db + rest: + middleware: + - RequestID + - Recover + - ZapLogger +components: + schemas: + Category: + type: object + properties: + title: + type: string + description: + type: string + required: + - title + x-identifier: + - title + Author: + type: object + properties: + id: + type: string + format: ksuid + firstName: + type: string + lastName: + type: string + email: + type: string + format: email + required: + - firstName + - lastName + x-identifier: + - id + - email + Blog: + type: object + properties: + id: + type: string + url: + type: string + format: uri + title: + type: string + description: + type: string + status: + type: string + nullable: true + enum: + - null + - unpublished + - published + image: + type: string + format: byte + categories: + type: array + items: + $ref: "#/components/schemas/Post" + posts: + type: array + items: + $ref: "#/components/schemas/Category" + lastUpdated: + type: string + format: date-time + created: + type: string + format: date-time + required: + - title + - url + x-identifier: + - id + Post: + type: object + properties: + title: + type: string + description: + type: string + author: + $ref: "#/components/schemas/Author" + created: + type: string + format: date-time +paths: + /health: + summary: Health Check + get: + x-controller: HealthCheck + responses: + 200: + description: Health Response + 500: + description: API Internal Error + /blogs: + parameters: + - in: header + name: someHeader + schema: + type: string + - in: header + name: someOtherHeader + schema: + type: string + x-context-name: soh + - in: header + name: X-Account-Id + schema: + type: string + x-context-name: AccountID + - in: query + name: q + schema: + type: string + post: + operationId: Add Blog + summary: Create Blog + requestBody: + description: Blog info that is submitted + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Blog" + responses: + 201: + description: Add Blog to Aggregator + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + get: + operationId: Get Blogs + summary: Get List of Blogs + parameters: + - in: query + name: filters + schema: + type: array + items: + type: object + properties: + field: + type: string + operator: + type: string + values: + type: array + items: + type: string + + required: false + description: query string + x-context: + filters: + - field: status + operator: eq + values: + - Active + - field: lastUpdated + operator: between + values: + - 2021-12-17 15:46:00 + - 2021-12-18 15:46:00 + - field: categories + operator: in + values: + - Technology + - Javascript + sorts: + - field: title + order: asc + page: 1 + limit: 10 + responses: + 200: + description: List of blogs + content: + application/json: + schema: + type: object + properties: + total: + type: integer + page: + type: integer + items: + $ref: "#/components/schemas/Blog" + /blogs/{id}: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + description: blog id + summary: Get Blog by id + operationId: Get Blog + responses: + 200: + description: Blog details without any supporting collections + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + put: + parameters: + - in: path + name: id + schema: + type: string + required: true + description: blog id + summary: Update blog details + operationId: Update Blog + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + responses: + 200: + description: Update Blog + content: + application/json: + schema: + $ref: "#/components/schemas/Blog" + delete: + parameters: + - in: path + name: id + schema: + type: string + required: true + description: blog id + summary: Delete blog + operationId: Delete Blog + responses: + 200: + description: Blog Deleted + + /posts/: + post: + operationId: Create Blog Post + summary: Create Blog Post + requestBody: + description: Post details + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Post" + responses: + 201: + description: Post + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + put: + operationId: Import Blog Posts + summary: Import Blog Posts + requestBody: + description: List of posts to import + required: true + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Post" + application/x-www-form-urlencoded: + schema: + type: array + items: + $ref: "#/components/schemas/Post" + responses: + 201: + description: Post + get: + operationId: Get Posts + summary: Get a blog's list of posts + parameters: + - in: query + name: q + schema: + type: string + required: false + description: query string + responses: + 200: + description: List of blog posts + content: + application/json: + schema: + type: object + properties: + total: + type: integer + page: + type: integer + items: + $ref: "#/components/schemas/Post" + + /posts/{id}: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Get blog post by id + responses: + 200: + description: Get blog post information + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + put: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Update post + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + responses: + 200: + description: Get blog post information + content: + application/json: + schema: + $ref: "#/components/schemas/Post" + delete: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Delete post + responses: + 200: + description: Delete post + + + /categories/: + post: + operationId: Create Blog Category + summary: Create Blog Category + requestBody: + description: Post details + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Category" + responses: + 201: + description: Post + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + get: + operationId: Get Categories + summary: Get a blog's list of categories + parameters: + - in: query + name: q + schema: + type: string + required: false + description: query string + responses: + 200: + description: List of blog categories + content: + application/json: + schema: + type: object + properties: + total: + type: integer + page: + type: integer + items: + $ref: "#/components/schemas/Category" + + /categories/{id}: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Get blog category by id + responses: + 200: + description: Get blog category information + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + put: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Update category + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + responses: + 200: + description: Get blog category information + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + delete: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Delete category + responses: + 200: + description: Delete category + + /authors/: + post: + operationId: Create Blog Author + summary: Create Blog Author + requestBody: + description: Author details + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/Author" + responses: + 201: + description: Post + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + get: + operationId: Get Authors + summary: Get a blog's list of authors + parameters: + - in: query + name: q + schema: + type: string + required: false + description: query string + responses: + 200: + description: List of blog authors + content: + application/json: + schema: + type: object + properties: + total: + type: integer + page: + type: integer + items: + $ref: "#/components/schemas/Author" + + /authors/{id}: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Get Author by id + responses: + 200: + description: Get author information + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + put: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Update Author details + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + responses: + 200: + description: Author details + content: + application/json: + schema: + $ref: "#/components/schemas/Author" + delete: + parameters: + - in: path + name: id + schema: + type: string + required: true + summary: Delete author + responses: + 200: + description: Delete author diff --git a/model/content_entity.go b/model/content_entity.go index 39b5b6fb..34b05075 100644 --- a/model/content_entity.go +++ b/model/content_entity.go @@ -131,9 +131,21 @@ func (w *ContentEntity) FromSchemaWithValues(ctx context.Context, schema *openap return w, w.ApplyChanges([]*Event{event}) } -func (w *ContentEntity) Update(payload json.RawMessage) (*ContentEntity, error) { +func (w *ContentEntity) Update(ctx context.Context, existingPayload json.RawMessage, updatedPayload json.RawMessage) (*ContentEntity, error) { + contentType := weosContext.GetContentType(ctx) - event := NewEntityEvent("update", w, w.ID, payload) + w.FromSchema(ctx, contentType.Schema) + + err := json.Unmarshal(existingPayload, &w.BasicEntity) + if err != nil { + return nil, err + } + err = json.Unmarshal(existingPayload, &w.Property) + if err != nil { + return nil, err + } + + event := NewEntityEvent("update", w, w.ID, updatedPayload) w.NewChange(event) return w, w.ApplyChanges([]*Event{event}) } diff --git a/model/content_entity_test.go b/model/content_entity_test.go index 3f78bff1..683f0f39 100644 --- a/model/content_entity_test.go +++ b/model/content_entity_test.go @@ -148,6 +148,11 @@ func TestContentEntity_Update(t *testing.T) { t.Fatalf("unexpected error instantiating content entity '%s'", err) } + existingEntityPayload, err := json.Marshal(existingEntity) + if err != nil { + t.Fatalf("unexpected error marshalling content entity '%s'", err) + } + if existingEntity.GetString("Title") != "test 1" { t.Errorf("expected the title to be '%s', got '%s'", "test 1", existingEntity.GetString("Title")) } @@ -162,7 +167,7 @@ func TestContentEntity_Update(t *testing.T) { t.Fatalf("unexpected error marshalling update payload '%s'", err) } - updatedEntity, err := existingEntity.Update(updatedPayload) + updatedEntity, err := existingEntity.Update(ctx, existingEntityPayload, updatedPayload) if err != nil { t.Fatalf("unexpected error updating existing entity '%s'", err) } diff --git a/model/domain_service.go b/model/domain_service.go index c9cee37f..421d30c9 100644 --- a/model/domain_service.go +++ b/model/domain_service.go @@ -61,8 +61,8 @@ func (s *DomainService) CreateBatch(ctx context.Context, payload json.RawMessage //Update is used for a single payload. It gets an existing entity and updates it with the new payload func (s *DomainService) Update(ctx context.Context, payload json.RawMessage, entityType string) (*ContentEntity, error) { - var existingEntity *ContentEntity var updatedEntity *ContentEntity + existingEntity := &ContentEntity{} var weosID string contentType := weosContext.GetContentType(ctx) @@ -92,11 +92,20 @@ func (s *DomainService) Update(ctx context.Context, payload json.RawMessage, ent } } - updatedEntity, err = existingEntity.Update(payload) + existingEntityPayload, err := json.Marshal(existingEntity) if err != nil { return nil, err } + updatedEntity, err = existingEntity.Update(ctx, existingEntityPayload, payload) + if err != nil { + return nil, err + } + + if ok := updatedEntity.IsValid(); !ok { + return nil, NewDomainError("unexpected error entity is invalid", entityType, updatedEntity.ID, nil) + } + //If there is no weosID, use the id passed from the param } else if weosID == "" { var primaryKeys []string @@ -114,11 +123,11 @@ func (s *DomainService) Update(ctx context.Context, payload json.RawMessage, ent for _, pk := range primaryKeys { ctxtIdentifier := ctx.Value(pk) - if ctxtIdentifier == "" { + if ctxtIdentifier == nil { return nil, NewDomainError("invalid: no value provided for primary key", entityType, "", nil) } - identifiers[pk] = pk + identifiers[pk] = ctxtIdentifier } entityInterface, err := s.GetByKey(ctx, contentType, identifiers) @@ -136,11 +145,15 @@ func (s *DomainService) Update(ctx context.Context, payload json.RawMessage, ent return nil, err } - updatedEntity, err = existingEntity.Update(payload) + updatedEntity, err = existingEntity.Update(ctx, data, payload) if err != nil { return nil, err } + if ok := updatedEntity.IsValid(); !ok { + return nil, NewDomainError("unexpected error entity is invalid", entityType, updatedEntity.ID, nil) + } + } return updatedEntity, nil } diff --git a/model/domain_service_test.go b/model/domain_service_test.go index 2e585fd3..27a0935f 100644 --- a/model/domain_service_test.go +++ b/model/domain_service_test.go @@ -188,8 +188,9 @@ func TestDomainService_Update(t *testing.T) { }, } + dService1 := model.NewDomainService(newContext, mockEventRepository, projectionMock) + t.Run("Testing with valid ID,Title and Description", func(t *testing.T) { - dService1 := model.NewDomainService(newContext, mockEventRepository, projectionMock) //Update a blog - payload uses woesID and seq no from the created entity updatedPayload := map[string]interface{}{"weos_id": "dsafdsdfdsf", "sequence_no": "1", "title": "Update Blog", "description": "Update Description", "url": "www.Updated!.com"} @@ -222,8 +223,6 @@ func TestDomainService_Update(t *testing.T) { } }) - dService1 := model.NewDomainService(newContext, mockEventRepository, projectionMock) - t.Run("Testing with stale sequence number", func(t *testing.T) { //Update a blog - payload uses woesID and seq no from the created entity @@ -247,4 +246,240 @@ func TestDomainService_Update(t *testing.T) { t.Fatal("expected no blog to be returned") } }) + + t.Run("Testing with invalid data", func(t *testing.T) { + + //Update a blog - payload uses woesID and seq no from the created entity + updatedPayload := map[string]interface{}{"weos_id": "dsafdsdfdsf", "sequence_no": "1", "title": nil, "description": "Update Description", "url": "www.Updated!.com"} + updatedReqBytes, err := json.Marshal(updatedPayload) + if err != nil { + t.Fatalf("error converting payload to bytes %s", err) + } + + reqBytes, err = json.Marshal(updatedPayload) + if err != nil { + t.Fatalf("error converting content type to bytes %s", err) + } + + updatedBlog, err := dService1.Update(newContext, updatedReqBytes, entityType) + + if err == nil { + t.Fatalf("expected error updating content type '%s'", err) + } + if updatedBlog != nil { + t.Fatal("expected no blog to be returned") + } + }) +} + +func TestDomainService_UpdateCompoundPrimaryKeyID(t *testing.T) { + //load open api spec + swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile("../controllers/rest/fixtures/blog-pk-id.yaml") + if err != nil { + t.Fatalf("unexpected error occured '%s'", err) + } + var contentType string + var contentTypeSchema *openapi3.SchemaRef + contentType = "Blog" + contentTypeSchema = swagger.Components.Schemas[contentType] + newContext := context.Background() + newContext = context.WithValue(newContext, context2.CONTENT_TYPE, &context2.ContentType{ + Name: contentType, + Schema: contentTypeSchema.Value, + }) + + newContext1 := newContext + + //Adds primary key ID to context + newContext = context.WithValue(newContext, "id", "1") + + entityType := "Blog" + + existingPayload := map[string]interface{}{"weos_id": "dsafdsdfdsf", "sequence_no": int64(1), "title": "blog 1", "description": "Description testing 1", "url": "www.TestBlog1.com"} + reqBytes, err := json.Marshal(existingPayload) + if err != nil { + t.Fatalf("error converting payload to bytes %s", err) + } + + mockEventRepository := &EventRepositoryMock{ + PersistFunc: func(ctxt context.Context, entity model.AggregateInterface) error { + return nil + }, + } + + dService := model.NewDomainService(newContext, mockEventRepository, nil) + existingBlog, err := dService.Create(newContext, reqBytes, entityType) + + projectionMock := &ProjectionMock{ + GetContentEntityFunc: func(ctx context3.Context, weosID string) (*model.ContentEntity, error) { + return existingBlog, nil + }, + GetByKeyFunc: func(ctxt context3.Context, contentType *context2.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) { + return existingPayload, nil + }, + } + + t.Run("Testing with compound PK - ID", func(t *testing.T) { + dService1 := model.NewDomainService(newContext, mockEventRepository, projectionMock) + + updatedPayload := map[string]interface{}{"title": "Update Blog", "description": "Update Description", "url": "www.Updated!.com"} + updatedReqBytes, err := json.Marshal(updatedPayload) + if err != nil { + t.Fatalf("error converting payload to bytes %s", err) + } + + reqBytes, err = json.Marshal(updatedPayload) + if err != nil { + t.Fatalf("error converting content type to bytes %s", err) + } + + updatedBlog, err := dService1.Update(newContext, updatedReqBytes, entityType) + + if err != nil { + t.Fatalf("unexpected error updating content type '%s'", err) + } + if updatedBlog == nil { + t.Fatal("expected blog to be returned") + } + if updatedBlog.GetString("Title") != updatedPayload["title"] { + t.Fatalf("expected blog title to be %s got %s", updatedPayload["title"], updatedBlog.GetString("Title")) + } + if updatedBlog.GetString("Description") != updatedPayload["description"] { + t.Fatalf("expected blog description to be %s got %s", updatedPayload["description"], updatedBlog.GetString("Description")) + } + if updatedBlog.GetString("Url") != updatedPayload["url"] { + t.Fatalf("expected blog url to be %s got %s", updatedPayload["url"], updatedBlog.GetString("Url")) + } + }) + + t.Run("Testing without compound PK - ID", func(t *testing.T) { + dService1 := model.NewDomainService(newContext1, mockEventRepository, projectionMock) + + updatedPayload := map[string]interface{}{"title": "Update Blog", "description": "Update Description", "url": "www.Updated!.com"} + updatedReqBytes, err := json.Marshal(updatedPayload) + if err != nil { + t.Fatalf("error converting payload to bytes %s", err) + } + + reqBytes, err = json.Marshal(updatedPayload) + if err != nil { + t.Fatalf("error converting content type to bytes %s", err) + } + + updatedBlog, err := dService1.Update(newContext1, updatedReqBytes, entityType) + + if err == nil { + t.Fatalf("expected error updating content type '%s'", err) + } + if updatedBlog != nil { + t.Fatal("expected blog to not be returned") + } + }) +} + +func TestDomainService_UpdateCompoundPrimaryKeyGuidTitle(t *testing.T) { + //load open api spec + swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile("../controllers/rest/fixtures/blog-pk-guid-title.yaml") + if err != nil { + t.Fatalf("unexpected error occured '%s'", err) + } + var contentType string + var contentTypeSchema *openapi3.SchemaRef + contentType = "Blog" + contentTypeSchema = swagger.Components.Schemas[contentType] + newContext := context.Background() + newContext = context.WithValue(newContext, context2.CONTENT_TYPE, &context2.ContentType{ + Name: contentType, + Schema: contentTypeSchema.Value, + }) + + newContext1 := newContext + + //Adds primary key ID to context + newContext = context.WithValue(newContext, "guid", "1") + newContext = context.WithValue(newContext, "title", "blog 1") + + entityType := "Blog" + + existingPayload := map[string]interface{}{"weos_id": "dsafdsdfdsf", "sequence_no": int64(1), "title": "blog 1", "description": "Description testing 1", "url": "www.TestBlog1.com"} + reqBytes, err := json.Marshal(existingPayload) + if err != nil { + t.Fatalf("error converting payload to bytes %s", err) + } + + mockEventRepository := &EventRepositoryMock{ + PersistFunc: func(ctxt context.Context, entity model.AggregateInterface) error { + return nil + }, + } + + dService := model.NewDomainService(newContext, mockEventRepository, nil) + existingBlog, err := dService.Create(newContext, reqBytes, entityType) + + projectionMock := &ProjectionMock{ + GetContentEntityFunc: func(ctx context3.Context, weosID string) (*model.ContentEntity, error) { + return existingBlog, nil + }, + GetByKeyFunc: func(ctxt context3.Context, contentType *context2.ContentType, identifiers map[string]interface{}) (map[string]interface{}, error) { + return existingPayload, nil + }, + } + + t.Run("Testing with compound PK - GUID, Title", func(t *testing.T) { + + dService1 := model.NewDomainService(newContext, mockEventRepository, projectionMock) + + updatedPayload := map[string]interface{}{"title": "Update Blog", "description": "Update Description", "url": "www.Updated!.com"} + updatedReqBytes, err := json.Marshal(updatedPayload) + if err != nil { + t.Fatalf("error converting payload to bytes %s", err) + } + + reqBytes, err = json.Marshal(updatedPayload) + if err != nil { + t.Fatalf("error converting content type to bytes %s", err) + } + + updatedBlog, err := dService1.Update(newContext, updatedReqBytes, entityType) + + if err != nil { + t.Fatalf("unexpected error updating content type '%s'", err) + } + if updatedBlog == nil { + t.Fatal("expected blog to be returned") + } + if updatedBlog.GetString("Title") != updatedPayload["title"] { + t.Fatalf("expected blog title to be %s got %s", updatedPayload["title"], updatedBlog.GetString("Title")) + } + if updatedBlog.GetString("Description") != updatedPayload["description"] { + t.Fatalf("expected blog description to be %s got %s", updatedPayload["description"], updatedBlog.GetString("Description")) + } + if updatedBlog.GetString("Url") != updatedPayload["url"] { + t.Fatalf("expected blog url to be %s got %s", updatedPayload["url"], updatedBlog.GetString("Url")) + } + }) + + t.Run("Testing without compound PK - GUID, Title", func(t *testing.T) { + dService1 := model.NewDomainService(newContext1, mockEventRepository, projectionMock) + + updatedPayload := map[string]interface{}{"title": "Update Blog", "description": "Update Description", "url": "www.Updated!.com"} + updatedReqBytes, err := json.Marshal(updatedPayload) + if err != nil { + t.Fatalf("error converting payload to bytes %s", err) + } + + reqBytes, err = json.Marshal(updatedPayload) + if err != nil { + t.Fatalf("error converting content type to bytes %s", err) + } + + updatedBlog, err := dService1.Update(newContext1, updatedReqBytes, entityType) + + if err == nil { + t.Fatalf("expected error updating content type '%s'", err) + } + if updatedBlog != nil { + t.Fatal("expected blog to not be returned") + } + }) }