diff --git a/controllers/rest/api.go b/controllers/rest/api.go index 658e5a0c..27d0b1e3 100644 --- a/controllers/rest/api.go +++ b/controllers/rest/api.go @@ -5,6 +5,13 @@ import ( "database/sql" "errors" "fmt" + "net/http" + "os" + "reflect" + "strconv" + "strings" + "time" + weoscontext "github.com/wepala/weos/context" "github.com/wepala/weos/projections/dialects" "gorm.io/driver/clickhouse" @@ -13,12 +20,6 @@ import ( "gorm.io/driver/sqlite" "gorm.io/driver/sqlserver" "gorm.io/gorm" - "net/http" - "os" - "reflect" - "strconv" - "strings" - "time" "github.com/getkin/kin-openapi/openapi3" "github.com/labstack/echo/v4" @@ -301,7 +302,7 @@ func (p *RESTAPI) Initialize(ctxt context.Context) error { //get the database schema schemas = CreateSchema(ctxt, p.EchoInstance(), p.Swagger) p.Schemas = schemas - err = defaultProjection.Migrate(ctxt, schemas) + err = defaultProjection.Migrate(ctxt, schemas, p.Swagger.Components.Schemas) if err != nil { p.EchoInstance().Logger.Error(err) return err diff --git a/controllers/rest/fixtures/blog-delete-content-field.yaml b/controllers/rest/fixtures/blog-delete-content-field.yaml new file mode 100644 index 00000000..47a62a52 --- /dev/null +++ b/controllers/rest/fixtures/blog-delete-content-field.yaml @@ -0,0 +1,642 @@ +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 + x-remove: + - email + Blog: + type: object + properties: + 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 + 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 + - in: query + name: cost + schema: + type: number + - in: query + name: leverage + schema: + type: number + format: double + 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 + blogs: + type: array + x-alias: items + items: + $ref: "#/components/schemas/Blog" + /blogs/{id}: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + description: blog id + - in: header + name: If-Match + schema: + type: string + required: false + - in: query + name: cost + schema: + type: number + - in: query + name: leverage + schema: + type: number + format: double + 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 + - in: header + name: If-Match + schema: + type: string + - in: query + name: cost + schema: + type: number + - in: query + name: leverage + schema: + type: number + format: double + 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: + type: array + 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: + type: array + items: + $ref: "#/components/schemas/Category" + + /categories/{title}: + get: + parameters: + - in: path + name: title + schema: + type: string + required: true + summary: Get blog category by title + responses: + 200: + description: Get blog category information + content: + application/json: + schema: + $ref: "#/components/schemas/Category" + put: + parameters: + - in: path + name: title + 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: title + 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: + type: array + items: + $ref: "#/components/schemas/Author" + + /authors/{id}: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + - in: header + name: email + schema: + type: string + format: email + required: true + summary: Get Author by email and 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 + - in: header + name: email + schema: + type: string + format: email + 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 + - in: header + name: email + schema: + type: string + format: email + required: true + summary: Delete author + responses: + 200: + description: Delete author \ No newline at end of file diff --git a/controllers/rest/middleware_initialize.go b/controllers/rest/middleware_initialize.go index 3b434035..4bb2a63f 100644 --- a/controllers/rest/middleware_initialize.go +++ b/controllers/rest/middleware_initialize.go @@ -46,9 +46,27 @@ func CreateSchema(ctx context.Context, e *echo.Echo, s *openapi3.Swagger) map[st //creates a new schema interface instance func newSchema(ref *openapi3.Schema, logger echo.Logger) (ds.Builder, map[string]string, []string) { pks, _ := json.Marshal(ref.Extensions["x-identifier"]) + dfs, _ := json.Marshal(ref.Extensions["x-remove"]) primaryKeys := []string{} + deletedFields := []string{} + json.Unmarshal(pks, &primaryKeys) + json.Unmarshal(dfs, &deletedFields) + + //was a primary key removed but not removed in the x-identifier fields? + for i, k := range primaryKeys { + for _, d := range deletedFields { + if strings.EqualFold(k, d) { + if len(primaryKeys) == 1 { + primaryKeys = []string{} + } else { + primaryKeys[i] = primaryKeys[len(primaryKeys)-1] + primaryKeys = primaryKeys[:len(primaryKeys)-1] + } + } + } + } if len(primaryKeys) == 0 { primaryKeys = append(primaryKeys, "id") @@ -58,6 +76,18 @@ func newSchema(ref *openapi3.Schema, logger echo.Logger) (ds.Builder, map[string relations := make(map[string]string) for name, p := range ref.Properties { + found := false + + for _, n := range deletedFields { + if strings.EqualFold(n, name) { + found = true + } + } + //this field should not be added to the schema + if found { + break + } + tagString := `json:"` + utils.SnakeCase(name) + `"` var gormParts []string for _, req := range ref.Required { diff --git a/controllers/rest/middleware_initialize_test.go b/controllers/rest/middleware_initialize_test.go index 41b8ad7b..2b7e2b9a 100644 --- a/controllers/rest/middleware_initialize_test.go +++ b/controllers/rest/middleware_initialize_test.go @@ -55,3 +55,50 @@ func TestCreateSchema(t *testing.T) { } }) } + +func TestXRemove(t *testing.T) { + t.Run("table name is set correctly", func(t *testing.T) { + content, err := ioutil.ReadFile("./fixtures/blog-delete-content-field.yaml") + if err != nil { + t.Fatalf("error loading api specification '%s'", err) + } + //change the $ref to another marker so that it doesn't get considered an environment variable WECON-1 + tempFile := strings.ReplaceAll(string(content), "$ref", "__ref__") + //replace environment variables in file + tempFile = os.ExpandEnv(string(tempFile)) + tempFile = strings.ReplaceAll(string(tempFile), "__ref__", "$ref") + //update path so that the open api way of specifying url parameters is change to the echo style of url parameters + re := regexp.MustCompile(`\{([a-zA-Z0-9\-_]+?)\}`) + tempFile = re.ReplaceAllString(tempFile, `:$1`) + content = []byte(tempFile) + loader := openapi3.NewSwaggerLoader() + swagger, err := loader.LoadSwaggerFromData(content) + if err != nil { + t.Fatalf("error loading api specification '%s'", err) + } + //instantiate api + e := echo.New() + + result := rest.CreateSchema(context.Background(), e, swagger) + //loop through and confirm each has a table name set + for _, table := range result { + if table.GetField("Table") == nil { + t.Fatalf("expected a table field") + } + } + + //check keys on table Author + authorTable, ok := result["Author"] + if !ok { + t.Fatalf("expected to find a table Author") + } + + if !authorTable.HasField("Id") { + t.Errorf("expected the struct to have field '%s'", "Id") + } + + if authorTable.HasField("Email") { + t.Errorf("expected the struct to not have field '%s'", "Email") + } + }) +} diff --git a/controllers/rest/weos_mocks_test.go b/controllers/rest/weos_mocks_test.go index 68f3de34..28cc031f 100644 --- a/controllers/rest/weos_mocks_test.go +++ b/controllers/rest/weos_mocks_test.go @@ -494,34 +494,34 @@ var _ model.Projection = &ProjectionMock{} // ProjectionMock is a mock implementation of model.Projection. // -// func TestSomethingThatUsesProjection(t *testing.T) { +// func TestSomethingThatUsesProjection(t *testing.T) { // -// // make and configure a mocked model.Projection -// mockedProjection := &ProjectionMock{ -// GetByEntityIDFunc: func(ctxt context.Context, entityFactory model.EntityFactory, id string) (map[string]interface{}, error) { -// panic("mock out the GetByEntityID method") -// }, -// GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { -// panic("mock out the GetByKey method") -// }, -// GetContentEntitiesFunc: func(ctx context.Context, entityFactory model.EntityFactory, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]map[string]interface{}, int64, error) { -// panic("mock out the GetContentEntities method") -// }, -// GetContentEntityFunc: func(ctx context.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { -// panic("mock out the GetContentEntity method") -// }, -// GetEventHandlerFunc: func() model.EventHandler { -// panic("mock out the GetEventHandler method") -// }, -// MigrateFunc: func(ctx context.Context, builders map[string]ds.Builder) error { -// panic("mock out the Migrate method") -// }, -// } +// // make and configure a mocked model.Projection +// mockedProjection := &ProjectionMock{ +// GetByEntityIDFunc: func(ctxt context.Context, entityFactory model.EntityFactory, id string) (map[string]interface{}, error) { +// panic("mock out the GetByEntityID method") +// }, +// GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { +// panic("mock out the GetByKey method") +// }, +// GetContentEntitiesFunc: func(ctx context.Context, entityFactory model.EntityFactory, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]map[string]interface{}, int64, error) { +// panic("mock out the GetContentEntities method") +// }, +// GetContentEntityFunc: func(ctx context.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { +// panic("mock out the GetContentEntity method") +// }, +// GetEventHandlerFunc: func() model.EventHandler { +// panic("mock out the GetEventHandler method") +// }, +// MigrateFunc: func(ctx context.Context, builders map[string]ds.Builder, refs map[string]*openapi3.SchemaRef) error { +// panic("mock out the Migrate method") +// }, +// } // -// // use mockedProjection in code that requires model.Projection -// // and then make assertions. +// // use mockedProjection in code that requires model.Projection +// // and then make assertions. // -// } +// } type ProjectionMock struct { // GetByEntityIDFunc mocks the GetByEntityID method. GetByEntityIDFunc func(ctxt context.Context, entityFactory model.EntityFactory, id string) (map[string]interface{}, error) @@ -539,7 +539,7 @@ type ProjectionMock struct { GetEventHandlerFunc func() model.EventHandler // MigrateFunc mocks the Migrate method. - MigrateFunc func(ctx context.Context, builders map[string]ds.Builder) error + MigrateFunc func(ctx context.Context, builders map[string]ds.Builder, refs map[string]*openapi3.SchemaRef) error // calls tracks calls to the methods. calls struct { @@ -596,6 +596,8 @@ type ProjectionMock struct { Ctx context.Context // Builders is the builders argument value. Builders map[string]ds.Builder + // Refs is the refs argument value. + Refs map[string]*openapi3.SchemaRef } } lockGetByEntityID sync.RWMutex @@ -805,21 +807,23 @@ func (mock *ProjectionMock) GetEventHandlerCalls() []struct { } // Migrate calls MigrateFunc. -func (mock *ProjectionMock) Migrate(ctx context.Context, builders map[string]ds.Builder) error { +func (mock *ProjectionMock) Migrate(ctx context.Context, builders map[string]ds.Builder, refs map[string]*openapi3.SchemaRef) error { if mock.MigrateFunc == nil { panic("ProjectionMock.MigrateFunc: method is nil but Projection.Migrate was just called") } callInfo := struct { Ctx context.Context Builders map[string]ds.Builder + Refs map[string]*openapi3.SchemaRef }{ Ctx: ctx, Builders: builders, + Refs: refs, } mock.lockMigrate.Lock() mock.calls.Migrate = append(mock.calls.Migrate, callInfo) mock.lockMigrate.Unlock() - return mock.MigrateFunc(ctx, builders) + return mock.MigrateFunc(ctx, builders, refs) } // MigrateCalls gets all the calls that were made to Migrate. @@ -828,10 +832,12 @@ func (mock *ProjectionMock) Migrate(ctx context.Context, builders map[string]ds. func (mock *ProjectionMock) MigrateCalls() []struct { Ctx context.Context Builders map[string]ds.Builder + Refs map[string]*openapi3.SchemaRef } { var calls []struct { Ctx context.Context Builders map[string]ds.Builder + Refs map[string]*openapi3.SchemaRef } mock.lockMigrate.RLock() calls = mock.calls.Migrate @@ -839,6 +845,7 @@ func (mock *ProjectionMock) MigrateCalls() []struct { return calls } + // Ensure, that LogMock does implement model.Log. // If this is not the case, regenerate this file with moq. var _ model.Log = &LogMock{} diff --git a/end2end_test.go b/end2end_test.go index 693b505d..a468425e 100644 --- a/end2end_test.go +++ b/end2end_test.go @@ -7,8 +7,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/testcontainers/testcontainers-go" - "github.com/wepala/weos/projections" "mime/multipart" "net/http" "net/http/httptest" @@ -20,6 +18,10 @@ import ( "testing" "time" + "github.com/getkin/kin-openapi/openapi3" + "github.com/testcontainers/testcontainers-go" + "github.com/wepala/weos/projections" + "github.com/cucumber/godog" "github.com/labstack/echo/v4" ds "github.com/ompluscator/dynamic-struct" @@ -41,6 +43,7 @@ var header http.Header var resp *http.Response var db *sql.DB var requests map[string]map[string]interface{} +var responseBody map[string]interface{} var currScreen string var contentTypeID map[string]bool var dockerEndpoint string @@ -55,6 +58,7 @@ var page int var contentType string var result api.ListApiResponse var scenarioContext context.Context +var blogfixtures []interface{} type User struct { Name string @@ -81,6 +85,7 @@ func InitializeSuite(ctx *godog.TestSuiteContext) { contentTypeID = map[string]bool{} Developer = &User{} result = api.ListApiResponse{} + blogfixtures = []interface{}{} os.Remove("e2e.db") openAPI = `openapi: 3.0.3 info: @@ -142,6 +147,7 @@ func reset(ctx context.Context, sc *godog.Scenario) (context.Context, error) { header = make(http.Header) rec = httptest.NewRecorder() resp = nil + blogfixtures = []interface{}{} os.Remove("e2e.db") var err error db, err = sql.Open("sqlite3", "e2e.db") @@ -348,49 +354,14 @@ func blogsInTheApi(details *godog.Table) error { for i := 1; i < len(details.Rows); i++ { req := make(map[string]interface{}) - seq := 0 for n, cell := range details.Rows[i].Cells { - if (head[n].Value) != "sequence_no" { - req[head[n].Value] = cell.Value - } else { - seq, _ = strconv.Atoi(cell.Value) - } + req[head[n].Value] = cell.Value } - reqBytes, _ := json.Marshal(req) - body := bytes.NewReader(reqBytes) - var request *http.Request - request = httptest.NewRequest("POST", "/blog", body) - - request = request.WithContext(context.TODO()) - header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - request.Header = header - request.Close = true - rec = httptest.NewRecorder() - e.ServeHTTP(rec, request) - if rec.Code != http.StatusCreated { - return fmt.Errorf("expected the status to be %d got %d", http.StatusCreated, rec.Code) - } - - if seq > 1 { - - for i := 1; i < seq; i++ { - reqBytes, _ := json.Marshal(req) - body := bytes.NewReader(reqBytes) - request = httptest.NewRequest("PUT", "/blogs/"+req["id"].(string), body) - request = request.WithContext(context.TODO()) - header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) - request.Header = header - request.Close = true - rec = httptest.NewRecorder() - e.ServeHTTP(rec, request) - if rec.Code != http.StatusOK { - return fmt.Errorf("expected the status to be %d got %d", http.StatusOK, rec.Code) - } - } - } + blogfixtures = append(blogfixtures, req) } + return nil } @@ -672,6 +643,51 @@ func theServiceIsRunning() error { return err } e = API.EchoInstance() + + if len(blogfixtures) != 0 { + for _, r := range blogfixtures { + req := r.(map[string]interface{}) + sequence := req["sequence_no"] + delete(req, "sequence_no") + reqBytes, _ := json.Marshal(req) + body := bytes.NewReader(reqBytes) + var request *http.Request + seq := 0 + if _, ok := sequence.(string); ok { + seq, _ = strconv.Atoi(sequence.(string)) + } + request = httptest.NewRequest("POST", "/blog", body) + + request = request.WithContext(context.TODO()) + header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + request.Header = header + request.Close = true + rec = httptest.NewRecorder() + e.ServeHTTP(rec, request) + if rec.Code != http.StatusCreated { + return fmt.Errorf("expected the status to be %d got %d", http.StatusCreated, rec.Code) + } + + if seq > 1 { + + for i := 1; i < seq; i++ { + reqBytes, _ := json.Marshal(req) + body := bytes.NewReader(reqBytes) + request = httptest.NewRequest("PUT", "/blogs/"+req["id"].(string), body) + request = request.WithContext(context.TODO()) + header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + request.Header = header + request.Close = true + rec = httptest.NewRecorder() + e.ServeHTTP(rec, request) + if rec.Code != http.StatusOK { + return fmt.Errorf("expected the status to be %d got %d", http.StatusOK, rec.Code) + } + } + } + + } + } return nil } @@ -793,6 +809,7 @@ func aBlogShouldBeReturned(details *godog.Table) error { } } + responseBody = contentEntity return nil } @@ -971,6 +988,165 @@ func theTotalResultsShouldBe(totalResult int) error { return nil } +func aWarningShouldBeOutputToTheLogsTellingTheDeveloperThePropertyDoesntExist() error { + if !strings.Contains(buf.String(), "property does not exist") { + } + return nil +} + +func addsTheAttributeToTheFieldOnTheContentType(user, attribute, field, contentType string) error { + loader := openapi3.NewSwaggerLoader() + swagger, err := loader.LoadSwaggerFromData([]byte(openAPI)) + if err != nil { + return err + } + + schemas := swagger.Components.Schemas + + attributes := schemas[contentType].Value.Extensions[attribute] + deletedFields := []string{} + bytes, _ := json.Marshal(attributes) + json.Unmarshal(bytes, &deletedFields) + deletedFields = append(deletedFields, field) + schemas[contentType].Value.Extensions[attribute] = deletedFields + + swagger.Components.Schemas = schemas + + bytes, err = swagger.MarshalJSON() + if err != nil { + return err + } + openAPI = string(bytes) + return nil +} + +func addsTheFieldToTheContentType(user, field, fieldType, contentType string) error { + loader := openapi3.NewSwaggerLoader() + swagger, err := loader.LoadSwaggerFromData([]byte(openAPI)) + if err != nil { + return err + } + + schemas := swagger.Components.Schemas + switch fieldType { + case "string": + schemas[contentType].Value.Properties[field] = &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: "string", + }, + } + default: + fmt.Errorf("no logic for adding field type %s", fieldType) + } + swagger.Components.Schemas = schemas + + bytes, err := swagger.MarshalJSON() + if err != nil { + return err + } + openAPI = string(bytes) + return nil +} + +func anErrorShouldShowLettingTheDeveloperKnowThatIsPartOfAForeignKeyReference() error { + if errs == nil { + fmt.Errorf("expected there to be an error on migrating") + return fmt.Errorf("expected error on migrating") + } + //TODO: add checks fo the speicific error + return nil +} + +func removedTheFieldFromTheContentType(user, field, contentType string) error { + loader := openapi3.NewSwaggerLoader() + swagger, err := loader.LoadSwaggerFromData([]byte(openAPI)) + if err != nil { + return err + } + + schemas := swagger.Components.Schemas + + delete(schemas[contentType].Value.Properties, strings.ToLower(field)) + + pks, _ := json.Marshal(schemas[contentType].Value.Extensions["x-identifier"]) + primayKeys := []string{} + json.Unmarshal(pks, &primayKeys) + for i, k := range primayKeys { + if strings.EqualFold(k, field) { + primayKeys[i] = primayKeys[len(primayKeys)-1] + primayKeys = primayKeys[:len(primayKeys)-1] + } + } + + schemas[contentType].Value.Extensions["x-identifier"] = primayKeys + + swagger.Components.Schemas = schemas + + bytes, err := swagger.MarshalJSON() + if err != nil { + return err + } + openAPI = string(bytes) + return nil +} + +func theFieldShouldBeRemovedFromTheTable(field, table string) error { + if errs != nil { + return errs + } + apiProjection, err := API.GetProjection("Default") + if err != nil { + return fmt.Errorf("unexpected error getting projection: %s", err) + } + apiProjection1 := apiProjection.(*projections.GORMProjection) + gormDB := apiProjection1.DB() + if !gormDB.Migrator().HasTable(table) { + return fmt.Errorf("expected there to be a table %s", table) + } + columns, err := gormDB.Migrator().ColumnTypes(table) + if err != nil { + return err + } + + for _, c := range columns { + if strings.EqualFold(c.Name(), field) { + return fmt.Errorf("there should be no column %s", field) + } + } + return nil +} + +func theServiceIsReset() error { + tapi, err := api.New(openAPI) + if err != nil { + return err + } + API = *tapi + e = API.EchoInstance() + buf = bytes.Buffer{} + e.Logger.SetOutput(&buf) + errs = API.Initialize(scenarioContext) + return nil +} + +func aBlogShouldBeReturnedWithoutField(field string) error { + if len(responseBody) == 0 { + err := json.NewDecoder(rec.Body).Decode(&responseBody) + if err != nil { + return err + } + } + + if _, ok := responseBody[field]; ok { + return fmt.Errorf("expected to not find field %s", field) + } + return nil +} + +func theServiceIsStopped() error { + return nil +} + func InitializeScenario(ctx *godog.ScenarioContext) { ctx.Before(reset) //add context steps @@ -1024,6 +1200,14 @@ func InitializeScenario(ctx *godog.ScenarioContext) { ctx.Step(`^the page no\. is (\d+)$`, thePageNoIs) ctx.Step(`^the search button is hit$`, theSearchButtonIsHit) ctx.Step(`^the total results should be (\d+)$`, theTotalResultsShouldBe) + ctx.Step(`^a warning should be output to the logs telling the developer the property doesn\'t exist$`, aWarningShouldBeOutputToTheLogsTellingTheDeveloperThePropertyDoesntExist) + ctx.Step(`^"([^"]*)" adds the "([^"]*)" attribute to the "([^"]*)" field on the "([^"]*)" content type$`, addsTheAttributeToTheFieldOnTheContentType) + ctx.Step(`^"([^"]*)" adds the field "([^"]*)" type "([^"]*)" to the "([^"]*)" content type$`, addsTheFieldToTheContentType) + ctx.Step(`^an error should show letting the developer know that is part of a foreign key reference$`, anErrorShouldShowLettingTheDeveloperKnowThatIsPartOfAForeignKeyReference) + ctx.Step(`^"([^"]*)" removed the "([^"]*)" field from the "([^"]*)" content type$`, removedTheFieldFromTheContentType) + ctx.Step(`^the "([^"]*)" field should be removed from the "([^"]*)" table$`, theFieldShouldBeRemovedFromTheTable) + ctx.Step(`^a blog should be returned without field "([^"]*)"$`, aBlogShouldBeReturnedWithoutField) + ctx.Step(`^the service is reset$`, theServiceIsReset) } func TestBDD(t *testing.T) { @@ -1033,7 +1217,7 @@ func TestBDD(t *testing.T) { TestSuiteInitializer: InitializeSuite, Options: &godog.Options{ Format: "pretty", - Tags: "~skipped && ~long", + Tags: "~long && ~skipped", //Tags: "WEOS-1176", //Tags: "WEOS-1110 && ~skipped", }, diff --git a/features/create-content.feature b/features/create-content.feature index fccb1dad..3cee74d8 100644 --- a/features/create-content.feature +++ b/features/create-content.feature @@ -149,12 +149,11 @@ Feature: Create content 400: description: Invalid Category submitted """ - And the service is running And blogs in the api | id | weos_id | sequence_no | title | description | | 1 | 24Kj3zfpocMlmFNV2KwkFfP2bgf | 1 | Blog 1 | Some Blog | | 2 | 24Kj7ExtIFvuGgTOTLBgpZgCl0n | 1 | Blog 2 | Some Blog 2 | - + And the service is running Scenario: Create a basic item diff --git a/features/edit-content.feature b/features/edit-content.feature index 238ddd1c..deffd2e6 100644 --- a/features/edit-content.feature +++ b/features/edit-content.feature @@ -182,12 +182,12 @@ Feature: Edit content 200: description: Blog Deleted """ - And the service is running And blogs in the api | id | weos_id | sequence_no | title | description | | 1234 | 986888285 | 1 | Blog 1 | Some Blog | | 4567 | 5uhq85nal | 1 | Blog 2 | Some Blog 2 | - + And the service is running + Scenario: Edit item Updating an item leads to a new sequence no. being created and returned diff --git a/features/list-content.feature b/features/list-content.feature index ab2a8c35..dfb086db 100644 --- a/features/list-content.feature +++ b/features/list-content.feature @@ -234,7 +234,6 @@ Feature: List content 200: description: Blog Deleted """ - And the service is running And blogs in the api | id | weos_id | sequence_no | title | description | | 1 | 24Kj7ExtIFvuGgTOTLBgpZgCl0n | 2 | Blog 1 | Some Blog | @@ -245,6 +244,7 @@ Feature: List content | 5 | 24KjLAP17p3KvTy5YCMWUIRlOSS | 1 | Blog 5 | Some Blog 5 | | 890 | 24KjMP9uTPxW5Xuhziv1balYskX | 1 | Blog 7 | Some Blog 7 | | 1237 | 24KjNifBFHrIQcfEe2QCaiHXd22 | 1 | Blog 8 | Some Blog 8 | + And the service is running @WEOS-1133 Scenario: Get list of items diff --git a/features/remove-content-type-field.feature b/features/remove-content-type-field.feature index 1e764662..ceace47f 100644 --- a/features/remove-content-type-field.feature +++ b/features/remove-content-type-field.feature @@ -1,4 +1,3 @@ -@skipped @WEOS-1125 Feature: Remove field from content type @@ -17,11 +16,41 @@ Feature: Remove field from content type title: Blog Aggregator Rest API version: 0.1.0 description: REST API for interacting with the Blog Aggregator + servers: + - url: https://prod1.weos.sh/blog/dev + description: WeOS Dev + - url: https://prod1.weos.sh/blog/v1 + x-weos-config: + logger: + level: warn + report-caller: true + formatter: json + database: + driver: sqlite3 + database: e2e.db + event-source: + - title: default + driver: service + endpoint: https://prod1.weos.sh/events/v1 + - title: event + driver: sqlite3 + database: e2e.db + databases: + - title: default + driver: sqlite3 + database: e2e.db + rest: + middleware: + - RequestID + - Recover + - ZapLogger components: schemas: Blog: type: object properties: + id: + type: string title: type: string description: blog title @@ -31,6 +60,8 @@ Feature: Remove field from content type type: string required: - title + x-identifier: + - id Post: type: object properties: @@ -106,10 +137,6 @@ Feature: Remove field from content type $ref: "#/components/schemas/Blog" 400: description: Invalid blog submitted - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorResponse" /blogs/{id}: get: parameters: @@ -173,43 +200,47 @@ Feature: Remove field from content type | id | entity id | sequence no | title | description | | 1234 | 22xu1Xa5CS3DK1Om2tB7OBDfWAF | 2 | Blog 1 | Some Blog | | 4567 | 22xu4iw0bWMwxqbrUvjqEqu5dof | 1 | Blog 2 | Some Blog 2 | + And the service is running Scenario: Remove a field that has no data Because the url field has been removed it should not be returned in the response Given "Sojourner" removed the "url" field from the "Blog" content type - And the service is running - When the "GET" endpoint "/blog/1234" is hit + And the service is reset + When the "GET" endpoint "/blogs/1234" is hit Then a 200 response should be returned And a blog should be returned | id | title | description | | 1234 | Blog 1 | Some Blog | + And a blog should be returned without field "url" Scenario: Remove a field that has data If a field that is removed is added back it should still have the contents that was there before Given "Sojourner" removed the "description" field from the "Blog" content type - And the service is running - And the "GET" endpoint "/blog/1234" is hit + And the service is reset + And the "GET" endpoint "/blogs/1234" is hit And a 200 response should be returned And a blog should be returned | id | title | | 1234 | Blog 1 | - And "Sojourner" adds the field "description" to the "Blog" content type - When the "GET" endpoint "/blog/1234" is hit + And a blog should be returned without field "description" + And "Sojourner" adds the field "description" type "string" to the "Blog" content type + And the service is reset + When the "GET" endpoint "/blogs/1234" is hit Then a 200 response should be returned And a blog should be returned | id | title | description | | 1234 | Blog 1 | Some Blog | - + Scenario: Permanently remove a field In order to permanently remove a field the "x-remove" extension should be used Given "Sojourner" adds the "x-remove" attribute to the "description" field on the "Blog" content type - When the service is running + When the service is reset Then the "description" field should be removed from the "Blog" table Scenario: Remove a field that has already been removed @@ -217,32 +248,34 @@ Feature: Remove field from content type If the field was already removed (maybe because of previous run) just show a warning Given "Sojourner" adds the "x-remove" attribute to the "description" field on the "Blog" content type - And the service is running + And the service is reset And the "description" field should be removed from the "Blog" table - And the service is stopped - When the service is running + And the service is reset Then a warning should be output to the logs telling the developer the property doesn't exist - + +@skipped + #this behaviour is not consistent across databases and after discussion, was decided would be handled later Scenario: Remove a field that is an identifier It's fine to remove an identifier Given "Sojourner" adds the "x-remove" attribute to the "guid" field on the "Tag" content type Given "Sojourner" adds the "x-remove" attribute to the "title" field on the "Tag" content type - When the service is running + When the service is reset Then the "title" field should be removed from the "Tag" table And the "guid" field should be removed from the "Tag" table + Scenario: Remove a field that is part of an identifier It's fine to remove a part of an identifier Given "Sojourner" adds the "x-remove" attribute to the "guid" field on the "Tag" content type - When the service is running - Then the "title" field should be removed from the "Tag" table + When the service is reset + Then the "guid" field should be removed from the "Tag" table Scenario: Remove a field that is part of a foreign key reference Given "Sojourner" adds the "x-remove" attribute to the "title" field on the "Category" content type - When the service is running + When the service is reset Then an error should show letting the developer know that is part of a foreign key reference \ No newline at end of file diff --git a/features/view-content.feature b/features/view-content.feature index a96f375f..2c8765c9 100644 --- a/features/view-content.feature +++ b/features/view-content.feature @@ -181,11 +181,11 @@ Feature: View content 200: description: Blog Deleted """ - And the service is running And blogs in the api | id | weos_id | sequence_no | title | description | | 1234 | 22xu1Xa5CS3DK1Om2tB7OBDfWAF | 2 | Blog 1 | Some Blog | | 4567 | 22xu4iw0bWMwxqbrUvjqEqu5dof | 1 | Blog 2 | Some Blog 2 | + And the service is running Scenario: Get blog details diff --git a/go.mod b/go.mod index 2143c146..d686961b 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/cucumber/godog v0.12.2 github.com/getkin/kin-openapi v0.15.0 + github.com/jinzhu/inflection v1.0.0 github.com/kr/text v0.2.0 // indirect github.com/labstack/echo/v4 v4.5.0 github.com/labstack/gommon v0.3.0 @@ -14,7 +15,6 @@ require ( github.com/ory/dockertest/v3 v3.6.0 github.com/proullon/ramsql v0.0.0-20181213202341-817cee58a244 github.com/segmentio/ksuid v1.0.3 - github.com/sirupsen/logrus v1.8.1 github.com/testcontainers/testcontainers-go v0.12.0 go.uber.org/zap v1.13.0 golang.org/x/net v0.0.0-20211108170745-6635138e15ea diff --git a/go.sum b/go.sum index 17d4ca96..1df06ecc 100644 --- a/go.sum +++ b/go.sum @@ -41,6 +41,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ClickHouse/clickhouse-go v1.4.5 h1:FfhyEnv6/BaWldyjgT2k4gDDmeNwJ9C4NbY/MXxJlXk= github.com/ClickHouse/clickhouse-go v1.4.5/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= +github.com/Flaque/filet v0.0.0-20201012163910-45f684403088 h1:PnnQln5IGbhLeJOi6hVs+lCeF+B1dRfFKPGXUAez0Ww= github.com/Flaque/filet v0.0.0-20201012163910-45f684403088/go.mod h1:TK+jB3mBs+8ZMWhU5BqZKnZWJ1MrLo8etNVg51ueTBo= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= @@ -206,6 +207,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cucumber/gherkin-go/v19 v19.0.3 h1:mMSKu1077ffLbTJULUfM5HPokgeBcIGboyeNUof1MdE= github.com/cucumber/gherkin-go/v19 v19.0.3/go.mod h1:jY/NP6jUtRSArQQJ5h1FXOUgk5fZK24qtE7vKi776Vw= @@ -289,6 +291,7 @@ github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL9 github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= @@ -375,6 +378,7 @@ github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3i github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I= github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -703,6 +707,7 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9 github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= diff --git a/model/interfaces.go b/model/interfaces.go index 36671f23..78214c56 100644 --- a/model/interfaces.go +++ b/model/interfaces.go @@ -2,6 +2,7 @@ package model //go:generate moq -out temp_mocks_test.go -pkg model_test . Projection import ( + "github.com/getkin/kin-openapi/openapi3" ds "github.com/ompluscator/dynamic-struct" "golang.org/x/net/context" ) @@ -71,7 +72,7 @@ type EventRepository interface { } type Datastore interface { - Migrate(ctx context.Context, builders map[string]ds.Builder) error + Migrate(ctx context.Context, builders map[string]ds.Builder, refs map[string]*openapi3.SchemaRef) error } type Projection interface { diff --git a/model/mocks_test.go b/model/mocks_test.go index debf1e3d..f2568675 100644 --- a/model/mocks_test.go +++ b/model/mocks_test.go @@ -7,7 +7,7 @@ import ( "context" "database/sql" "github.com/getkin/kin-openapi/openapi3" - ds "github.com/ompluscator/dynamic-struct" + "github.com/ompluscator/dynamic-struct" "github.com/wepala/weos/model" "gorm.io/gorm" "net/http" @@ -20,46 +20,46 @@ var _ model.EventRepository = &EventRepositoryMock{} // EventRepositoryMock is a mock implementation of model.EventRepository. // -// func TestSomethingThatUsesEventRepository(t *testing.T) { +// func TestSomethingThatUsesEventRepository(t *testing.T) { // -// // make and configure a mocked model.EventRepository -// mockedEventRepository := &EventRepositoryMock{ -// AddSubscriberFunc: func(handler model.EventHandler) { -// panic("mock out the AddSubscriber method") -// }, -// FlushFunc: func() error { -// panic("mock out the Flush method") -// }, -// GetAggregateSequenceNumberFunc: func(ID string) (int64, error) { -// panic("mock out the GetAggregateSequenceNumber method") -// }, -// GetByAggregateFunc: func(ID string) ([]*model.Event, error) { -// panic("mock out the GetByAggregate method") -// }, -// GetByAggregateAndSequenceRangeFunc: func(ID string, start int64, end int64) ([]*model.Event, error) { -// panic("mock out the GetByAggregateAndSequenceRange method") -// }, -// GetByAggregateAndTypeFunc: func(ID string, entityType string) ([]*model.Event, error) { -// panic("mock out the GetByAggregateAndType method") -// }, -// GetByEntityAndAggregateFunc: func(entityID string, entityType string, rootID string) ([]*model.Event, error) { -// panic("mock out the GetByEntityAndAggregate method") -// }, -// GetSubscribersFunc: func() ([]model.EventHandler, error) { -// panic("mock out the GetSubscribers method") -// }, -// MigrateFunc: func(ctx context.Context) error { -// panic("mock out the Migrate method") -// }, -// PersistFunc: func(ctxt context.Context, entity model.AggregateInterface) error { -// panic("mock out the Persist method") -// }, -// } +// // make and configure a mocked model.EventRepository +// mockedEventRepository := &EventRepositoryMock{ +// AddSubscriberFunc: func(handler model.EventHandler) { +// panic("mock out the AddSubscriber method") +// }, +// FlushFunc: func() error { +// panic("mock out the Flush method") +// }, +// GetAggregateSequenceNumberFunc: func(ID string) (int64, error) { +// panic("mock out the GetAggregateSequenceNumber method") +// }, +// GetByAggregateFunc: func(ID string) ([]*model.Event, error) { +// panic("mock out the GetByAggregate method") +// }, +// GetByAggregateAndSequenceRangeFunc: func(ID string, start int64, end int64) ([]*model.Event, error) { +// panic("mock out the GetByAggregateAndSequenceRange method") +// }, +// GetByAggregateAndTypeFunc: func(ID string, entityType string) ([]*model.Event, error) { +// panic("mock out the GetByAggregateAndType method") +// }, +// GetByEntityAndAggregateFunc: func(entityID string, entityType string, rootID string) ([]*model.Event, error) { +// panic("mock out the GetByEntityAndAggregate method") +// }, +// GetSubscribersFunc: func() ([]model.EventHandler, error) { +// panic("mock out the GetSubscribers method") +// }, +// MigrateFunc: func(ctx context.Context) error { +// panic("mock out the Migrate method") +// }, +// PersistFunc: func(ctxt context.Context, entity model.AggregateInterface) error { +// panic("mock out the Persist method") +// }, +// } // -// // use mockedEventRepository in code that requires model.EventRepository -// // and then make assertions. +// // use mockedEventRepository in code that requires model.EventRepository +// // and then make assertions. // -// } +// } type EventRepositoryMock struct { // AddSubscriberFunc mocks the AddSubscriber method. AddSubscriberFunc func(handler model.EventHandler) @@ -494,34 +494,34 @@ var _ model.Projection = &ProjectionMock{} // ProjectionMock is a mock implementation of model.Projection. // -// func TestSomethingThatUsesProjection(t *testing.T) { +// func TestSomethingThatUsesProjection(t *testing.T) { // -// // make and configure a mocked model.Projection -// mockedProjection := &ProjectionMock{ -// GetByEntityIDFunc: func(ctxt context.Context, entityFactory model.EntityFactory, id string) (map[string]interface{}, error) { -// panic("mock out the GetByEntityID method") -// }, -// GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { -// panic("mock out the GetByKey method") -// }, -// GetContentEntitiesFunc: func(ctx context.Context, entityFactory model.EntityFactory, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]map[string]interface{}, int64, error) { -// panic("mock out the GetContentEntities method") -// }, -// GetContentEntityFunc: func(ctx context.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { -// panic("mock out the GetContentEntity method") -// }, -// GetEventHandlerFunc: func() model.EventHandler { -// panic("mock out the GetEventHandler method") -// }, -// MigrateFunc: func(ctx context.Context, builders map[string]ds.Builder) error { -// panic("mock out the Migrate method") -// }, -// } +// // make and configure a mocked model.Projection +// mockedProjection := &ProjectionMock{ +// GetByEntityIDFunc: func(ctxt context.Context, entityFactory model.EntityFactory, id string) (map[string]interface{}, error) { +// panic("mock out the GetByEntityID method") +// }, +// GetByKeyFunc: func(ctxt context.Context, entityFactory model.EntityFactory, identifiers map[string]interface{}) (map[string]interface{}, error) { +// panic("mock out the GetByKey method") +// }, +// GetContentEntitiesFunc: func(ctx context.Context, entityFactory model.EntityFactory, page int, limit int, query string, sortOptions map[string]string, filterOptions map[string]interface{}) ([]map[string]interface{}, int64, error) { +// panic("mock out the GetContentEntities method") +// }, +// GetContentEntityFunc: func(ctx context.Context, entityFactory model.EntityFactory, weosID string) (*model.ContentEntity, error) { +// panic("mock out the GetContentEntity method") +// }, +// GetEventHandlerFunc: func() model.EventHandler { +// panic("mock out the GetEventHandler method") +// }, +// MigrateFunc: func(ctx context.Context, builders map[string]dynamicstruct.Builder, refs map[string]*openapi3.SchemaRef) error { +// panic("mock out the Migrate method") +// }, +// } // -// // use mockedProjection in code that requires model.Projection -// // and then make assertions. +// // use mockedProjection in code that requires model.Projection +// // and then make assertions. // -// } +// } type ProjectionMock struct { // GetByEntityIDFunc mocks the GetByEntityID method. GetByEntityIDFunc func(ctxt context.Context, entityFactory model.EntityFactory, id string) (map[string]interface{}, error) @@ -539,7 +539,7 @@ type ProjectionMock struct { GetEventHandlerFunc func() model.EventHandler // MigrateFunc mocks the Migrate method. - MigrateFunc func(ctx context.Context, builders map[string]ds.Builder) error + MigrateFunc func(ctx context.Context, builders map[string]dynamicstruct.Builder, refs map[string]*openapi3.SchemaRef) error // calls tracks calls to the methods. calls struct { @@ -595,7 +595,9 @@ type ProjectionMock struct { // Ctx is the ctx argument value. Ctx context.Context // Builders is the builders argument value. - Builders map[string]ds.Builder + Builders map[string]dynamicstruct.Builder + // Refs is the refs argument value. + Refs map[string]*openapi3.SchemaRef } } lockGetByEntityID sync.RWMutex @@ -805,21 +807,23 @@ func (mock *ProjectionMock) GetEventHandlerCalls() []struct { } // Migrate calls MigrateFunc. -func (mock *ProjectionMock) Migrate(ctx context.Context, builders map[string]ds.Builder) error { +func (mock *ProjectionMock) Migrate(ctx context.Context, builders map[string]dynamicstruct.Builder, refs map[string]*openapi3.SchemaRef) error { if mock.MigrateFunc == nil { panic("ProjectionMock.MigrateFunc: method is nil but Projection.Migrate was just called") } callInfo := struct { Ctx context.Context - Builders map[string]ds.Builder + Builders map[string]dynamicstruct.Builder + Refs map[string]*openapi3.SchemaRef }{ Ctx: ctx, Builders: builders, + Refs: refs, } mock.lockMigrate.Lock() mock.calls.Migrate = append(mock.calls.Migrate, callInfo) mock.lockMigrate.Unlock() - return mock.MigrateFunc(ctx, builders) + return mock.MigrateFunc(ctx, builders, refs) } // MigrateCalls gets all the calls that were made to Migrate. @@ -827,11 +831,13 @@ func (mock *ProjectionMock) Migrate(ctx context.Context, builders map[string]ds. // len(mockedProjection.MigrateCalls()) func (mock *ProjectionMock) MigrateCalls() []struct { Ctx context.Context - Builders map[string]ds.Builder + Builders map[string]dynamicstruct.Builder + Refs map[string]*openapi3.SchemaRef } { var calls []struct { Ctx context.Context - Builders map[string]ds.Builder + Builders map[string]dynamicstruct.Builder + Refs map[string]*openapi3.SchemaRef } mock.lockMigrate.RLock() calls = mock.calls.Migrate @@ -839,58 +845,59 @@ func (mock *ProjectionMock) MigrateCalls() []struct { return calls } + // Ensure, that LogMock does implement model.Log. // If this is not the case, regenerate this file with moq. var _ model.Log = &LogMock{} // LogMock is a mock implementation of model.Log. // -// func TestSomethingThatUsesLog(t *testing.T) { +// func TestSomethingThatUsesLog(t *testing.T) { // -// // make and configure a mocked model.Log -// mockedLog := &LogMock{ -// DebugFunc: func(args ...interface{}) { -// panic("mock out the Debug method") -// }, -// DebugfFunc: func(format string, args ...interface{}) { -// panic("mock out the Debugf method") -// }, -// ErrorFunc: func(args ...interface{}) { -// panic("mock out the Error method") -// }, -// ErrorfFunc: func(format string, args ...interface{}) { -// panic("mock out the Errorf method") -// }, -// FatalFunc: func(args ...interface{}) { -// panic("mock out the Fatal method") -// }, -// FatalfFunc: func(format string, args ...interface{}) { -// panic("mock out the Fatalf method") -// }, -// InfoFunc: func(args ...interface{}) { -// panic("mock out the Info method") -// }, -// InfofFunc: func(format string, args ...interface{}) { -// panic("mock out the Infof method") -// }, -// PanicFunc: func(args ...interface{}) { -// panic("mock out the Panic method") -// }, -// PanicfFunc: func(format string, args ...interface{}) { -// panic("mock out the Panicf method") -// }, -// PrintFunc: func(args ...interface{}) { -// panic("mock out the Print method") -// }, -// PrintfFunc: func(format string, args ...interface{}) { -// panic("mock out the Printf method") -// }, -// } +// // make and configure a mocked model.Log +// mockedLog := &LogMock{ +// DebugFunc: func(args ...interface{}) { +// panic("mock out the Debug method") +// }, +// DebugfFunc: func(format string, args ...interface{}) { +// panic("mock out the Debugf method") +// }, +// ErrorFunc: func(args ...interface{}) { +// panic("mock out the Error method") +// }, +// ErrorfFunc: func(format string, args ...interface{}) { +// panic("mock out the Errorf method") +// }, +// FatalFunc: func(args ...interface{}) { +// panic("mock out the Fatal method") +// }, +// FatalfFunc: func(format string, args ...interface{}) { +// panic("mock out the Fatalf method") +// }, +// InfoFunc: func(args ...interface{}) { +// panic("mock out the Info method") +// }, +// InfofFunc: func(format string, args ...interface{}) { +// panic("mock out the Infof method") +// }, +// PanicFunc: func(args ...interface{}) { +// panic("mock out the Panic method") +// }, +// PanicfFunc: func(format string, args ...interface{}) { +// panic("mock out the Panicf method") +// }, +// PrintFunc: func(args ...interface{}) { +// panic("mock out the Print method") +// }, +// PrintfFunc: func(format string, args ...interface{}) { +// panic("mock out the Printf method") +// }, +// } // -// // use mockedLog in code that requires model.Log -// // and then make assertions. +// // use mockedLog in code that requires model.Log +// // and then make assertions. // -// } +// } type LogMock struct { // DebugFunc mocks the Debug method. DebugFunc func(args ...interface{}) @@ -1419,25 +1426,25 @@ var _ model.CommandDispatcher = &CommandDispatcherMock{} // CommandDispatcherMock is a mock implementation of model.CommandDispatcher. // -// func TestSomethingThatUsesCommandDispatcher(t *testing.T) { +// func TestSomethingThatUsesCommandDispatcher(t *testing.T) { // -// // make and configure a mocked model.CommandDispatcher -// mockedCommandDispatcher := &CommandDispatcherMock{ -// AddSubscriberFunc: func(command *model.Command, handler model.CommandHandler) map[string][]model.CommandHandler { -// panic("mock out the AddSubscriber method") -// }, -// DispatchFunc: func(ctx context.Context, command *model.Command, eventStore model.EventRepository, projection model.Projection, logger model.Log) error { -// panic("mock out the Dispatch method") -// }, -// GetSubscribersFunc: func() map[string][]model.CommandHandler { -// panic("mock out the GetSubscribers method") -// }, -// } +// // make and configure a mocked model.CommandDispatcher +// mockedCommandDispatcher := &CommandDispatcherMock{ +// AddSubscriberFunc: func(command *model.Command, handler model.CommandHandler) map[string][]model.CommandHandler { +// panic("mock out the AddSubscriber method") +// }, +// DispatchFunc: func(ctx context.Context, command *model.Command, eventStore model.EventRepository, projection model.Projection, logger model.Log) error { +// panic("mock out the Dispatch method") +// }, +// GetSubscribersFunc: func() map[string][]model.CommandHandler { +// panic("mock out the GetSubscribers method") +// }, +// } // -// // use mockedCommandDispatcher in code that requires model.CommandDispatcher -// // and then make assertions. +// // use mockedCommandDispatcher in code that requires model.CommandDispatcher +// // and then make assertions. // -// } +// } type CommandDispatcherMock struct { // AddSubscriberFunc mocks the AddSubscriber method. AddSubscriberFunc func(command *model.Command, handler model.CommandHandler) map[string][]model.CommandHandler @@ -1593,52 +1600,52 @@ var _ model.Service = &ServiceMock{} // ServiceMock is a mock implementation of model.Service. // -// func TestSomethingThatUsesService(t *testing.T) { +// func TestSomethingThatUsesService(t *testing.T) { // -// // make and configure a mocked model.Service -// mockedService := &ServiceMock{ -// AddProjectionFunc: func(projection model.Projection) error { -// panic("mock out the AddProjection method") -// }, -// ConfigFunc: func() *model.ServiceConfig { -// panic("mock out the Config method") -// }, -// DBFunc: func() *gorm.DB { -// panic("mock out the DB method") -// }, -// DBConnectionFunc: func() *sql.DB { -// panic("mock out the DBConnection method") -// }, -// DispatcherFunc: func() model.CommandDispatcher { -// panic("mock out the Dispatcher method") -// }, -// EventRepositoryFunc: func() model.EventRepository { -// panic("mock out the EventRepository method") -// }, -// HTTPClientFunc: func() *http.Client { -// panic("mock out the HTTPClient method") -// }, -// IDFunc: func() string { -// panic("mock out the ID method") -// }, -// LoggerFunc: func() model.Log { -// panic("mock out the Logger method") -// }, -// MigrateFunc: func(ctx context.Context, builders map[string]ds.Builder) error { -// panic("mock out the Migrate method") -// }, -// ProjectionsFunc: func() []model.Projection { -// panic("mock out the Projections method") -// }, -// TitleFunc: func() string { -// panic("mock out the Title method") -// }, -// } +// // make and configure a mocked model.Service +// mockedService := &ServiceMock{ +// AddProjectionFunc: func(projection model.Projection) error { +// panic("mock out the AddProjection method") +// }, +// ConfigFunc: func() *model.ServiceConfig { +// panic("mock out the Config method") +// }, +// DBFunc: func() *gorm.DB { +// panic("mock out the DB method") +// }, +// DBConnectionFunc: func() *sql.DB { +// panic("mock out the DBConnection method") +// }, +// DispatcherFunc: func() model.CommandDispatcher { +// panic("mock out the Dispatcher method") +// }, +// EventRepositoryFunc: func() model.EventRepository { +// panic("mock out the EventRepository method") +// }, +// HTTPClientFunc: func() *http.Client { +// panic("mock out the HTTPClient method") +// }, +// IDFunc: func() string { +// panic("mock out the ID method") +// }, +// LoggerFunc: func() model.Log { +// panic("mock out the Logger method") +// }, +// MigrateFunc: func(ctx context.Context, builders map[string]dynamicstruct.Builder) error { +// panic("mock out the Migrate method") +// }, +// ProjectionsFunc: func() []model.Projection { +// panic("mock out the Projections method") +// }, +// TitleFunc: func() string { +// panic("mock out the Title method") +// }, +// } // -// // use mockedService in code that requires model.Service -// // and then make assertions. +// // use mockedService in code that requires model.Service +// // and then make assertions. // -// } +// } type ServiceMock struct { // AddProjectionFunc mocks the AddProjection method. AddProjectionFunc func(projection model.Projection) error @@ -1668,7 +1675,7 @@ type ServiceMock struct { LoggerFunc func() model.Log // MigrateFunc mocks the Migrate method. - MigrateFunc func(ctx context.Context, builders map[string]ds.Builder) error + MigrateFunc func(ctx context.Context, builders map[string]dynamicstruct.Builder) error // ProjectionsFunc mocks the Projections method. ProjectionsFunc func() []model.Projection @@ -1712,7 +1719,7 @@ type ServiceMock struct { // Ctx is the ctx argument value. Ctx context.Context // Builders is the builders argument value. - Builders map[string]ds.Builder + Builders map[string]dynamicstruct.Builder } // Projections holds details about calls to the Projections method. Projections []struct { @@ -1975,13 +1982,13 @@ func (mock *ServiceMock) LoggerCalls() []struct { } // Migrate calls MigrateFunc. -func (mock *ServiceMock) Migrate(ctx context.Context, builders map[string]ds.Builder) error { +func (mock *ServiceMock) Migrate(ctx context.Context, builders map[string]dynamicstruct.Builder) error { if mock.MigrateFunc == nil { panic("ServiceMock.MigrateFunc: method is nil but Service.Migrate was just called") } callInfo := struct { Ctx context.Context - Builders map[string]ds.Builder + Builders map[string]dynamicstruct.Builder }{ Ctx: ctx, Builders: builders, @@ -1997,11 +2004,11 @@ func (mock *ServiceMock) Migrate(ctx context.Context, builders map[string]ds.Bui // len(mockedService.MigrateCalls()) func (mock *ServiceMock) MigrateCalls() []struct { Ctx context.Context - Builders map[string]ds.Builder + Builders map[string]dynamicstruct.Builder } { var calls []struct { Ctx context.Context - Builders map[string]ds.Builder + Builders map[string]dynamicstruct.Builder } mock.lockMigrate.RLock() calls = mock.calls.Migrate @@ -2067,40 +2074,40 @@ var _ model.EntityFactory = &EntityFactoryMock{} // EntityFactoryMock is a mock implementation of model.EntityFactory. // -// func TestSomethingThatUsesEntityFactory(t *testing.T) { +// func TestSomethingThatUsesEntityFactory(t *testing.T) { // -// // make and configure a mocked model.EntityFactory -// mockedEntityFactory := &EntityFactoryMock{ -// DynamicStructFunc: func(ctx context.Context) ds.DynamicStruct { -// panic("mock out the DynamicStruct method") -// }, -// FromSchemaAndBuilderFunc: func(s string, schema *openapi3.Schema, builder ds.Builder) model.EntityFactory { -// panic("mock out the FromSchemaAndBuilder method") -// }, -// NameFunc: func() string { -// panic("mock out the Name method") -// }, -// NewEntityFunc: func(ctx context.Context) (*model.ContentEntity, error) { -// panic("mock out the NewEntity method") -// }, -// SchemaFunc: func() *openapi3.Schema { -// panic("mock out the Schema method") -// }, -// TableNameFunc: func() string { -// panic("mock out the TableName method") -// }, -// } +// // make and configure a mocked model.EntityFactory +// mockedEntityFactory := &EntityFactoryMock{ +// DynamicStructFunc: func(ctx context.Context) dynamicstruct.DynamicStruct { +// panic("mock out the DynamicStruct method") +// }, +// FromSchemaAndBuilderFunc: func(in1 string, in2 *openapi3.Schema, in3 dynamicstruct.Builder) model.EntityFactory { +// panic("mock out the FromSchemaAndBuilder method") +// }, +// NameFunc: func() string { +// panic("mock out the Name method") +// }, +// NewEntityFunc: func(ctx context.Context) (*model.ContentEntity, error) { +// panic("mock out the NewEntity method") +// }, +// SchemaFunc: func() *openapi3.Schema { +// panic("mock out the Schema method") +// }, +// TableNameFunc: func() string { +// panic("mock out the TableName method") +// }, +// } // -// // use mockedEntityFactory in code that requires model.EntityFactory -// // and then make assertions. +// // use mockedEntityFactory in code that requires model.EntityFactory +// // and then make assertions. // -// } +// } type EntityFactoryMock struct { // DynamicStructFunc mocks the DynamicStruct method. - DynamicStructFunc func(ctx context.Context) ds.DynamicStruct + DynamicStructFunc func(ctx context.Context) dynamicstruct.DynamicStruct // FromSchemaAndBuilderFunc mocks the FromSchemaAndBuilder method. - FromSchemaAndBuilderFunc func(s string, schema *openapi3.Schema, builder ds.Builder) model.EntityFactory + FromSchemaAndBuilderFunc func(in1 string, in2 *openapi3.Schema, in3 dynamicstruct.Builder) model.EntityFactory // NameFunc mocks the Name method. NameFunc func() string @@ -2123,12 +2130,12 @@ type EntityFactoryMock struct { } // FromSchemaAndBuilder holds details about calls to the FromSchemaAndBuilder method. FromSchemaAndBuilder []struct { - // S is the s argument value. - S string - // Schema is the schema argument value. - Schema *openapi3.Schema - // Builder is the builder argument value. - Builder ds.Builder + // In1 is the in1 argument value. + In1 string + // In2 is the in2 argument value. + In2 *openapi3.Schema + // In3 is the in3 argument value. + In3 dynamicstruct.Builder } // Name holds details about calls to the Name method. Name []struct { @@ -2154,7 +2161,7 @@ type EntityFactoryMock struct { } // DynamicStruct calls DynamicStructFunc. -func (mock *EntityFactoryMock) DynamicStruct(ctx context.Context) ds.DynamicStruct { +func (mock *EntityFactoryMock) DynamicStruct(ctx context.Context) dynamicstruct.DynamicStruct { if mock.DynamicStructFunc == nil { panic("EntityFactoryMock.DynamicStructFunc: method is nil but EntityFactory.DynamicStruct was just called") } @@ -2185,37 +2192,37 @@ func (mock *EntityFactoryMock) DynamicStructCalls() []struct { } // FromSchemaAndBuilder calls FromSchemaAndBuilderFunc. -func (mock *EntityFactoryMock) FromSchemaAndBuilder(s string, schema *openapi3.Schema, builder ds.Builder) model.EntityFactory { +func (mock *EntityFactoryMock) FromSchemaAndBuilder(in1 string, in2 *openapi3.Schema, in3 dynamicstruct.Builder) model.EntityFactory { if mock.FromSchemaAndBuilderFunc == nil { panic("EntityFactoryMock.FromSchemaAndBuilderFunc: method is nil but EntityFactory.FromSchemaAndBuilder was just called") } callInfo := struct { - S string - Schema *openapi3.Schema - Builder ds.Builder + In1 string + In2 *openapi3.Schema + In3 dynamicstruct.Builder }{ - S: s, - Schema: schema, - Builder: builder, + In1: in1, + In2: in2, + In3: in3, } mock.lockFromSchemaAndBuilder.Lock() mock.calls.FromSchemaAndBuilder = append(mock.calls.FromSchemaAndBuilder, callInfo) mock.lockFromSchemaAndBuilder.Unlock() - return mock.FromSchemaAndBuilderFunc(s, schema, builder) + return mock.FromSchemaAndBuilderFunc(in1, in2, in3) } // FromSchemaAndBuilderCalls gets all the calls that were made to FromSchemaAndBuilder. // Check the length with: // len(mockedEntityFactory.FromSchemaAndBuilderCalls()) func (mock *EntityFactoryMock) FromSchemaAndBuilderCalls() []struct { - S string - Schema *openapi3.Schema - Builder ds.Builder + In1 string + In2 *openapi3.Schema + In3 dynamicstruct.Builder } { var calls []struct { - S string - Schema *openapi3.Schema - Builder ds.Builder + In1 string + In2 *openapi3.Schema + In3 dynamicstruct.Builder } mock.lockFromSchemaAndBuilder.RLock() calls = mock.calls.FromSchemaAndBuilder diff --git a/model/module_test.go b/model/module_test.go index 21cb9012..be48f849 100644 --- a/model/module_test.go +++ b/model/module_test.go @@ -5,6 +5,7 @@ import ( "os" "testing" + "github.com/getkin/kin-openapi/openapi3" dynamicstruct "github.com/ompluscator/dynamic-struct" _ "github.com/proullon/ramsql/driver" "github.com/wepala/weos/controllers/rest" @@ -223,7 +224,7 @@ func TestWeOSApp_AddProjection(t *testing.T) { } }, - MigrateFunc: func(ctx context.Context, builders map[string]dynamicstruct.Builder) error { + MigrateFunc: func(ctx context.Context, builders map[string]dynamicstruct.Builder, refs map[string]*openapi3.SchemaRef) error { return nil }, } diff --git a/model/service.go b/model/service.go index 890e329d..b1882a3a 100644 --- a/model/service.go +++ b/model/service.go @@ -4,10 +4,11 @@ package model import ( "database/sql" - ds "github.com/ompluscator/dynamic-struct" "net/http" "time" + ds "github.com/ompluscator/dynamic-struct" + _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" "golang.org/x/net/context" @@ -118,7 +119,7 @@ func (w *BaseService) DB() *gorm.DB { func (w *BaseService) Migrate(ctx context.Context, builders map[string]ds.Builder) error { w.logger.Infof("preparing to migrate %d projections", len(w.projections)) for _, projection := range w.projections { - err := projection.Migrate(ctx, nil) + err := projection.Migrate(ctx, nil, nil) if err != nil { return err } diff --git a/projections/gorm.go b/projections/gorm.go index 2e556278..95f3c566 100644 --- a/projections/gorm.go +++ b/projections/gorm.go @@ -4,9 +4,13 @@ import ( "encoding/json" "fmt" "strings" + "time" + "github.com/getkin/kin-openapi/openapi3" + "github.com/jinzhu/inflection" ds "github.com/ompluscator/dynamic-struct" weos "github.com/wepala/weos/model" + "github.com/wepala/weos/utils" "golang.org/x/net/context" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -99,7 +103,7 @@ func (p *GORMProjection) Remove(entities []weos.Entity) error { return nil } -func (p *GORMProjection) Migrate(ctx context.Context, builders map[string]ds.Builder) error { +func (p *GORMProjection) Migrate(ctx context.Context, builders map[string]ds.Builder, refs map[string]*openapi3.SchemaRef) error { //we may need to reorder the creation so that tables don't reference things that don't exist as yet. var err error @@ -116,6 +120,155 @@ func (p *GORMProjection) Migrate(ctx context.Context, builders map[string]ds.Bui return err } tables = append(tables, instance) + + dfs, _ := json.Marshal(refs[name].Value.Extensions["x-remove"]) + deletedFields := []string{} + json.Unmarshal(dfs, &deletedFields) + + for i, f := range deletedFields { + deletedFields[i] = utils.SnakeCase(f) + } + + columns, err := p.db.Migrator().ColumnTypes(instance) + if err != nil { + p.logger.Errorf("unable to get columns from table %s with error '%s'", name, err) + } + if len(columns) != 0 { + reader := ds.NewReader(instance) + readerFields := reader.GetAllFields() + jsonFields := []string{} + for _, r := range readerFields { + jsonFields = append(jsonFields, utils.SnakeCase(r.Name())) + } + + builder := ds.ExtendStruct(instance) + for _, c := range columns { + if !utils.Contains(jsonFields, c.Name()) && !utils.Contains(deletedFields, c.Name()) { + if !utils.Contains(deletedFields, c.Name()) { + var val interface{} + dType := strings.ToLower(c.DatabaseTypeName()) + jsonString := `json:"` + c.Name() + `"` + switch dType { + case "text", "varchar", "char", "longtext": + var strings *string + val = strings + jsonString += `gorm:"size:512"` + case "integer", "int8", "int", "smallint", "bigint": + val = 0 + case "real", "float8", "numeric", "float4", "double", "decimal": + val = 0.0 + case "bool", "boolean": + val = false + case "timetz", "timestamptz", "date", "datetime", "timestamp": + val = time.Time{} + } + builder.AddField(strings.Title(c.Name()), val, jsonString) + } + } + } + + var deleteConstraintError error + b := builder.Build().New() + json.Unmarshal([]byte(`{ + "table_alias": "`+name+`" + }`), &b) + + //drop columns with x-remove tag + columns, err := p.db.Migrator().ColumnTypes(instance) + if err != nil { + p.logger.Errorf("unable to get columns from table %s with error '%s'", name, err) + } + + for _, f := range deletedFields { + if p.db.Migrator().HasColumn(b, f) { + + deleteConstraintError = p.db.Migrator().DropColumn(b, f) + if deleteConstraintError != nil { + p.logger.Errorf("unable to drop column %s from table %s with error '%s'", f, name, err) + } + } else { + p.logger.Errorf("unable to drop column %s from table %s. property does not exist", f, name) + } + } + + //if column exists in table but not in new schema, alter column + + //get columns after db drop + columns, err = p.db.Migrator().ColumnTypes(instance) + if err != nil { + p.logger.Errorf("unable to get columns from table %s with error '%s'", name, err) + } + if deleteConstraintError == nil { + for _, c := range columns { + if !utils.Contains(jsonFields, c.Name()) { + deleteConstraintError = p.db.Debug().Migrator().AlterColumn(b, c.Name()) + if deleteConstraintError != nil { + p.logger.Errorf("got error updating constraint %s", err) + } + + if deleteConstraintError != nil { + break + } + } + } + } + + //remake table if primary key constraints are changed + if deleteConstraintError != nil { + + //check if changing primary key affects relationship tables + tables, err := p.db.Migrator().GetTables() + if err != nil { + p.logger.Errorf("got error getting current tables %s", err) + return err + } + + //check foreign keys + for _, t := range tables { + pluralName := strings.ToLower(inflection.Plural(name)) + + if strings.Contains(strings.ToLower(t), name+"_") || strings.Contains(t, "_"+pluralName) { + return weos.NewError(fmt.Sprintf("a relationship exists that uses constraints from table %s", name), fmt.Errorf("a relationship exists that uses constraints from table %s", name)) + } + } + + b := builder.Build().New() + err = json.Unmarshal([]byte(`{ + "table_alias": "temp" + }`), &b) + if err != nil { + p.logger.Errorf("unable to set the table name '%s'", err) + return err + } + err = p.db.Migrator().CreateTable(b) + if err != nil { + p.logger.Errorf("got error creating temporary table %s", err) + return err + } + tableVals := []map[string]interface{}{} + p.db.Table(name).Find(&tableVals) + if len(tableVals) != 0 { + db := p.db.Table("temp").Create(&tableVals) + if db.Error != nil { + p.logger.Errorf("got error transfering table values %s", db.Error) + return err + } + } + + err = p.db.Migrator().DropTable(name) + if err != nil { + p.logger.Errorf("got error dropping table%s", err) + return err + } + err = p.db.Migrator().RenameTable("temp", name) + if err != nil { + p.logger.Errorf("got error renaming temporary table %s", err) + return err + } + + } + } + } err = p.db.Migrator().AutoMigrate(tables...) @@ -146,7 +299,7 @@ func (p *GORMProjection) GetEventHandler() weos.EventHandler { if err != nil { p.logger.Errorf("error unmarshalling event '%s'", err) } - db := p.db.Table(entityFactory.Name()).Create(eventPayload) + db := p.db.Debug().Table(entityFactory.Name()).Create(eventPayload) if db.Error != nil { p.logger.Errorf("error creating %s, got %s", entityFactory.Name(), db.Error) } diff --git a/projections/projections_test.go b/projections/projections_test.go index 54e290d3..7aff50e5 100644 --- a/projections/projections_test.go +++ b/projections/projections_test.go @@ -234,12 +234,12 @@ components: } schemes := rest.CreateSchema(context.Background(), echo.New(), api.Swagger) - p, err := projections.NewProjection(context.Background(), gormDB, nil) + p, err := projections.NewProjection(context.Background(), gormDB, api.EchoInstance().Logger) if err != nil { t.Fatal(err) } - err = p.Migrate(context.Background(), schemes) + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) if err != nil { t.Fatal(err) } @@ -376,12 +376,12 @@ components: t.Fatalf("error loading api config '%s'", err) } schemes := rest.CreateSchema(context.Background(), echo.New(), api.Swagger) - p, err := projections.NewProjection(context.Background(), gormDB, nil) + p, err := projections.NewProjection(context.Background(), gormDB, api.EchoInstance().Logger) if err != nil { t.Fatal(err) } - err = p.Migrate(context.Background(), schemes) + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) if err != nil { t.Fatal(err) } @@ -480,12 +480,12 @@ components: } schemes := rest.CreateSchema(context.Background(), echo.New(), api.Swagger) - p, err := projections.NewProjection(context.Background(), gormDB, nil) + p, err := projections.NewProjection(context.Background(), gormDB, api.EchoInstance().Logger) if err != nil { t.Fatal(err) } - err = p.Migrate(context.Background(), schemes) + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) if err != nil { t.Fatal(err) } @@ -592,12 +592,12 @@ components: t.Fatalf("error loading api config '%s'", err) } schemes := rest.CreateSchema(context.Background(), echo.New(), api.Swagger) - p, err := projections.NewProjection(context.Background(), gormDB, nil) + p, err := projections.NewProjection(context.Background(), gormDB, api.EchoInstance().Logger) if err != nil { t.Fatal(err) } - err = p.Migrate(context.Background(), schemes) + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) if err != nil { t.Fatal(err) } @@ -738,12 +738,12 @@ components: } schemes := rest.CreateSchema(context.Background(), echo.New(), api.Swagger) - p, err := projections.NewProjection(context.Background(), gormDB, nil) + p, err := projections.NewProjection(context.Background(), gormDB, api.EchoInstance().Logger) if err != nil { t.Fatal(err) } - err = p.Migrate(context.Background(), schemes) + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) if err != nil { t.Fatal(err) } @@ -852,7 +852,7 @@ components: t.Fatal(err) } - err = p.Migrate(context.Background(), schemes) + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) if err != nil { t.Fatal(err) } @@ -1010,12 +1010,12 @@ components: } schemes := rest.CreateSchema(context.Background(), echo.New(), api.Swagger) - p, err := projections.NewProjection(context.Background(), gormDB, nil) + p, err := projections.NewProjection(context.Background(), gormDB, api.EchoInstance().Logger) if err != nil { t.Fatal(err) } - err = p.Migrate(context.Background(), schemes) + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) if err != nil { t.Fatal(err) } @@ -1102,12 +1102,12 @@ components: } schemes := rest.CreateSchema(context.Background(), echo.New(), api.Swagger) - p, err := projections.NewProjection(context.Background(), gormDB, nil) + p, err := projections.NewProjection(context.Background(), gormDB, api.EchoInstance().Logger) if err != nil { t.Fatal(err) } - err = p.Migrate(context.Background(), schemes) + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) if err != nil { t.Fatal(err) } @@ -1263,12 +1263,12 @@ components: t.Fatalf("error loading api config '%s'", err) } schemes := rest.CreateSchema(context.Background(), echo.New(), api.Swagger) - p, err := projections.NewProjection(context.Background(), gormDB, nil) + p, err := projections.NewProjection(context.Background(), gormDB, api.EchoInstance().Logger) if err != nil { t.Fatal(err) } - err = p.Migrate(context.Background(), schemes) + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) if err != nil { t.Fatal(err) } @@ -1413,7 +1413,7 @@ components: t.Fatal(err) } - err = p.Migrate(context.Background(), schemes) + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) if err != nil { t.Fatal(err) } @@ -1557,12 +1557,12 @@ components: } schemes := rest.CreateSchema(context.Background(), echo.New(), api.Swagger) - p, err := projections.NewProjection(context.Background(), gormDB, nil) + p, err := projections.NewProjection(context.Background(), gormDB, api.EchoInstance().Logger) if err != nil { t.Fatal(err) } - err = p.Migrate(context.Background(), schemes) + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) if err != nil { t.Fatal(err) } @@ -1714,7 +1714,7 @@ components: t.Fatal(err) } - err = p.Migrate(context.Background(), schemes) + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) if err != nil { t.Fatal(err) } @@ -1844,7 +1844,7 @@ components: t.Fatal(err) } - err = p.Migrate(context.Background(), schemes) + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) if err != nil { t.Fatal(err) } @@ -1978,7 +1978,7 @@ components: t.Fatal(err) } - err = p.Migrate(context.Background(), schemes) + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) if err != nil { t.Fatal(err) } @@ -2081,7 +2081,7 @@ components: t.Fatal(err) } - err = p.Migrate(context.Background(), schemes) + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) if err != nil { t.Fatal(err) } @@ -2199,3 +2199,1003 @@ components: }) } + +func TestProjections_InitializeContentRemove(t *testing.T) { + + t.Run("Remove non-primary key without x-remove", 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 +x-weos-config: + logger: + level: warn + report-caller: true + formatter: json + database: + driver: sqlite3 + database: test.db + event-source: + - title: default + driver: service + endpoint: https://prod1.weos.sh/events/v1 + - title: event + driver: sqlite3 + database: test.db + databases: + - title: default + driver: sqlite3 + database: test.db + rest: + middleware: + - RequestID + - Recover + - ZapLogger +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 +` + + api, err := rest.New(openAPI) + if err != nil { + t.Fatalf("error loading api config '%s'", err) + } + + schemes := rest.CreateSchema(context.Background(), echo.New(), api.Swagger) + p, err := projections.NewProjection(context.Background(), gormDB, api.EchoInstance().Logger) + if err != nil { + t.Fatal(err) + } + + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) + if err != nil { + t.Fatal(err) + } + + 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("Blog") + + 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() == "description" { + found2 = true + } + } + + if !found1 || !found2 || !found { + t.Fatal("not all fields found") + } + + 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 +x-weos-config: + logger: + level: warn + report-caller: true + formatter: json + database: + driver: sqlite3 + database: test.db + event-source: + - title: default + driver: service + endpoint: https://prod1.weos.sh/events/v1 + - title: event + driver: sqlite3 + database: test.db + databases: + - title: default + driver: sqlite3 + database: test.db + rest: + middleware: + - RequestID + - Recover + - ZapLogger +components: + schemas: + Blog: + type: object + properties: + title: + type: string + description: blog title + Post: + type: object + properties: + title: + type: string + description: blog title + description: + type: string +` + api, err = rest.New(openAPI) + if err != nil { + t.Fatalf("error loading api config '%s'", err) + } + + schemes = rest.CreateSchema(context.Background(), echo.New(), api.Swagger) + p, err = projections.NewProjection(context.Background(), gormDB, api.EchoInstance().Logger) + if err != nil { + t.Fatal(err) + } + + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) + if err != nil { + t.Fatal(err) + } + + if !gormDB.Migrator().HasTable("Post") { + t.Errorf("expected to get a table 'Post'") + } + + columns, _ = gormDB.Migrator().ColumnTypes("Blog") + + //expect all columns to still be in the database + 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() == "description" { + found2 = true + } + } + + if !found1 || !found2 || !found { + t.Fatal("not all fields found") + } + + err = gormDB.Migrator().DropTable("blog_posts") + if err != nil { + t.Errorf("error removing table '%s' '%s'", "blog_posts", 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) + } + }) + + t.Run("Remove non primary key with x-remove", 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 +x-weos-config: + logger: + level: warn + report-caller: true + formatter: json + database: + driver: sqlite3 + database: test.db + event-source: + - title: default + driver: service + endpoint: https://prod1.weos.sh/events/v1 + - title: event + driver: sqlite3 + database: test.db + databases: + - title: default + driver: sqlite3 + database: test.db + rest: + middleware: + - RequestID + - Recover + - ZapLogger +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 +` + + api, err := rest.New(openAPI) + if err != nil { + t.Fatalf("error loading api config '%s'", err) + } + + schemes := rest.CreateSchema(context.Background(), echo.New(), api.Swagger) + p, err := projections.NewProjection(context.Background(), gormDB, api.EchoInstance().Logger) + if err != nil { + t.Fatal(err) + } + + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) + if err != nil { + t.Fatal(err) + } + 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("Blog") + + 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() == "description" { + found2 = true + } + } + + if !found1 || !found2 || !found { + t.Fatal("not all fields found") + } + + 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 +x-weos-config: + logger: + level: warn + report-caller: true + formatter: json + database: + driver: sqlite3 + database: test.db + event-source: + - title: default + driver: service + endpoint: https://prod1.weos.sh/events/v1 + - title: event + driver: sqlite3 + database: test.db + databases: + - title: default + driver: sqlite3 + database: test.db + rest: + middleware: + - RequestID + - Recover + - ZapLogger +components: + schemas: + Blog: + type: object + properties: + title: + type: string + description: blog title + description: + type: string + x-remove: + - description + Post: + type: object + properties: + title: + type: string + description: blog title + description: + type: string +` + + api, err = rest.New(openAPI) + if err != nil { + t.Fatalf("error loading api config '%s'", err) + } + + schemes = rest.CreateSchema(context.Background(), echo.New(), api.Swagger) + p, err = projections.NewProjection(context.Background(), gormDB, api.EchoInstance().Logger) + if err != nil { + t.Fatal(err) + } + + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) + if err != nil { + t.Fatal(err) + } + + if !gormDB.Migrator().HasTable("Post") { + t.Errorf("expected to get a table 'Post'") + } + + columns, _ = gormDB.Migrator().ColumnTypes("Blog") + + //expect all columns to still be in the database + 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() == "description" { + found2 = true + } + } + + if !found1 || !found { + t.Fatal("not all fields found") + } + + if found2 { + t.Fatal("expected there to be no description field") + } + + err = gormDB.Migrator().DropTable("blog_posts") + if err != nil { + t.Errorf("error removing table '%s' '%s'", "blog_posts", 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) + } + }) + + t.Run("Remove primary key with x-remove", 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 +x-weos-config: + logger: + level: warn + report-caller: true + formatter: json + database: + driver: sqlite3 + database: test.db + event-source: + - title: default + driver: service + endpoint: https://prod1.weos.sh/events/v1 + - title: event + driver: sqlite3 + database: test.db + databases: + - title: default + driver: sqlite3 + database: test.db + rest: + middleware: + - RequestID + - Recover + - ZapLogger +components: + schemas: + Blog: + type: object + properties: + guid: + type: string + title: + type: string + description: blog title + description: + type: string + x-identifier: + - guid +` + + api, err := rest.New(openAPI) + if err != nil { + t.Fatalf("error loading api config '%s'", err) + } + + schemes := rest.CreateSchema(context.Background(), echo.New(), api.Swagger) + p, err := projections.NewProjection(context.Background(), gormDB, api.EchoInstance().Logger) + if err != nil { + t.Fatal(err) + } + + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) + if err != nil { + t.Fatal(err) + } + + if !gormDB.Migrator().HasTable("Blog") { + t.Fatal("expected to get a table 'Blog'") + } + + columns, _ := gormDB.Migrator().ColumnTypes("Blog") + + found := false + found1 := false + found2 := false + for _, c := range columns { + if c.Name() == "guid" { + found = true + } + if c.Name() == "title" { + found1 = true + } + if c.Name() == "description" { + found2 = true + } + } + + if !found1 || !found2 || !found { + t.Fatal("not all fields found") + } + + 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 +x-weos-config: + logger: + level: warn + report-caller: true + formatter: json + database: + driver: sqlite3 + database: test.db + event-source: + - title: default + driver: service + endpoint: https://prod1.weos.sh/events/v1 + - title: event + driver: sqlite3 + database: test.db + databases: + - title: default + driver: sqlite3 + database: test.db + rest: + middleware: + - RequestID + - Recover + - ZapLogger +components: + schemas: + Blog: + type: object + properties: + guid: + type: string + title: + type: string + description: blog title + description: + type: string + x-remove: + - guid + x-identifier: + - guid +` + + api, err = rest.New(openAPI) + if err != nil { + t.Fatalf("error loading api config '%s'", err) + } + + schemes = rest.CreateSchema(context.Background(), echo.New(), api.Swagger) + p, err = projections.NewProjection(context.Background(), gormDB, api.EchoInstance().Logger) + if err != nil { + t.Fatal(err) + } + + err = p.Migrate(context.Background(), schemes, api.Swagger.Components.Schemas) + if err != nil { + t.Fatal(err) + } + + columns, _ = gormDB.Migrator().ColumnTypes("Blog") + + //expect all columns to still be in the database + found = false + found1 = false + found2 = false + for _, c := range columns { + if c.Name() == "guid" { + found = true + } + //id should be auto created + if c.Name() == "id" { + found1 = true + } + if c.Name() == "description" { + found2 = true + } + } + + if !found1 || !found2 { + t.Fatal("not all fields found") + } + + if found { + t.Fatal("there should be no guid field") + } + //create using auto id and not guid + tresult := gormDB.Table("Blog").Create(map[string]interface{}{"title": "hugs2"}) + if tresult.Error != nil { + t.Errorf("expected to be able to create with new primary key") + } + + err = gormDB.Migrator().DropTable("Blog") + if err != nil { + t.Errorf("error removing table '%s' '%s'", "Blog", err) + } + + }) + + //TODO: Required field removal functionality without x-remove inconsistent across databases + // t.Run("Remove primary key without x-remove", 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 + // x-weos-config: + // logger: + // level: warn + // report-caller: true + // formatter: json + // database: + // driver: sqlite3 + // database: test.db + // event-source: + // - title: default + // driver: service + // endpoint: https://prod1.weos.sh/events/v1 + // - title: event + // driver: sqlite3 + // database: test.db + // databases: + // - title: default + // driver: sqlite3 + // database: test.db + // rest: + // middleware: + // - RequestID + // - Recover + // - ZapLogger + // components: + // schemas: + // Blog: + // type: object + // properties: + // guid: + // type: string + // title: + // type: string + // description: blog title + // description: + // type: string + // x-identifier: + // - guid + // ` + + // 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.Fatal("expected to get a table 'Blog'") + // } + + // columns, _ := gormDB.Migrator().ColumnTypes("Blog") + + // found := false + // found1 := false + // found2 := false + // for _, c := range columns { + // if c.Name() == "guid" { + // found = true + // } + // if c.Name() == "title" { + // found1 = true + // } + // if c.Name() == "description" { + // found2 = true + // } + // } + + // if !found1 || !found2 || !found { + // t.Fatal("not all fields found") + // } + + // 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 + // x-weos-config: + // logger: + // level: warn + // report-caller: true + // formatter: json + // database: + // driver: sqlite3 + // database: test.db + // event-source: + // - title: default + // driver: service + // endpoint: https://prod1.weos.sh/events/v1 + // - title: event + // driver: sqlite3 + // database: test.db + // databases: + // - title: default + // driver: sqlite3 + // database: test.db + // rest: + // middleware: + // - RequestID + // - Recover + // - ZapLogger + // components: + // schemas: + // Blog: + // type: object + // properties: + // title: + // type: string + // description: blog title + // description: + // type: string + // ` + // 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) + // } + + // columns, _ = gormDB.Migrator().ColumnTypes("Blog") + + // //expect all columns to still be in the database + // found = false + // found1 = false + // found2 = false + // for _, c := range columns { + // if c.Name() == "guid" { + // found = true + // } + // //id should be auto created + // if c.Name() == "id" { + // found1 = true + // } + // if c.Name() == "description" { + // found2 = true + // } + // } + + // if !found1 || !found2 || !found { + // t.Fatal("not all fields found") + // } + + // //create using auto id and not guid + // tresult := gormDB.Table("Blog").Create(map[string]interface{}{"title": "hugs2"}) + // if tresult.Error != nil { + // t.Errorf("expected to be able to create with new primary key") + // } + + // err = gormDB.Migrator().DropTable("Blog") + // if err != nil { + // t.Errorf("error removing table '%s' '%s'", "Blog", err) + // } + + // }) + + // t.Run("Remove required key without x-remove", 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 + // x-weos-config: + // logger: + // level: warn + // report-caller: true + // formatter: json + // database: + // driver: sqlite3 + // database: test.db + // event-source: + // - title: default + // driver: service + // endpoint: https://prod1.weos.sh/events/v1 + // - title: event + // driver: sqlite3 + // database: test.db + // databases: + // - title: default + // driver: sqlite3 + // database: test.db + // rest: + // middleware: + // - RequestID + // - Recover + // - ZapLogger + // components: + // schemas: + // Blog: + // type: object + // properties: + // title: + // type: string + // description: blog title + // description: + // type: string + // required: + // - title + // ` + + // 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'") + // } + + // columns, _ := gormDB.Migrator().ColumnTypes("Blog") + + // 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() == "description" { + // found2 = true + // } + // } + + // if !found1 || !found2 || !found { + // t.Fatal("not all fields found") + // } + + // 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 + // x-weos-config: + // logger: + // level: warn + // report-caller: true + // formatter: json + // database: + // driver: sqlite3 + // database: test.db + // event-source: + // - title: default + // driver: service + // endpoint: https://prod1.weos.sh/events/v1 + // - title: event + // driver: sqlite3 + // database: test.db + // databases: + // - title: default + // driver: sqlite3 + // database: test.db + // rest: + // middleware: + // - RequestID + // - Recover + // - ZapLogger + // components: + // schemas: + // Blog: + // type: object + // properties: + // description: + // type: string + // ` + + // 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) + // } + + // if !gormDB.Migrator().HasTable("Blog") { + // t.Errorf("expected to get a table 'Blog'") + // } + + // columns, _ = gormDB.Migrator().ColumnTypes("Blog") + + // //expect all columns to still be in the database + // 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() == "description" { + // found2 = true + // } + // } + + // if !found1 || !found2 || !found { + // t.Fatal("not all fields found") + // } + + // //create using without title + // tresult := gormDB.Table("Blog").Create(map[string]interface{}{"description": "hugs2"}) + // if tresult.Error != nil { + // t.Errorf("expected to be able to create without title, got error %s", tresult.Error) + // } + // err = gormDB.Migrator().DropTable("Blog") + // if err != nil { + // t.Errorf("error removing table '%s' '%s'", "Blog", err) + // } + // }) +} diff --git a/projections/test.db b/projections/test.db new file mode 100644 index 00000000..e69de29b diff --git a/tasks.db b/tasks.db new file mode 100644 index 00000000..8cc6c5f4 Binary files /dev/null and b/tasks.db differ diff --git a/utils/utils.go b/utils/utils.go index 11ee6220..bc9beedc 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -14,3 +14,12 @@ func SnakeCase(s string) string { } return strings.Join(split, "_") } + +func Contains(arr []string, s string) bool { + for _, a := range arr { + if s == a { + return true + } + } + return false +}