diff --git a/cypress/e2e/03_features.cy.ts b/cypress/e2e/03_features.cy.ts index 3a9becb7c..dd2c66424 100644 --- a/cypress/e2e/03_features.cy.ts +++ b/cypress/e2e/03_features.cy.ts @@ -11,10 +11,12 @@ describe('Create Features for Workspace', () => { url: `${HostName}/features`, headers: { 'x-jwt': `${value}` }, body: Features[i] - }).its('body').should('have.property', 'name', Features[i].name.trim()) - .its('body').should('have.property', 'brief', Features[i].brief.trim()) - .its('body').should('have.property', 'requirements', Features[i].requirements.trim()) - .its('body').should('have.property', 'architecture', Features[i].architecture.trim()) + }).its('body').then(body => { + expect(body).to.have.property('name').and.equal(Features[i].name.trim()); + expect(body).to.have.property('brief').and.equal(Features[i].brief.trim()); + expect(body).to.have.property('requirements').and.equal(Features[i].requirements.trim()); + expect(body).to.have.property('architecture').and.equal(Features[i].architecture.trim()); + }); } }) }) @@ -32,10 +34,12 @@ describe('Modify name for Feature', () => { uuid: Features[i].uuid, name: Features[i].name + "_addtext" } - }).its('body').should('have.property', 'name', Features[i].name.trim() + "_addtext") - .its('body').should('have.property', 'brief', Features[i].brief.trim()) - .its('body').should('have.property', 'requirements', Features[i].requirements.trim()) - .its('body').should('have.property', 'architecture', Features[i].architecture.trim()) + }).its('body').then(body => { + expect(body).to.have.property('name').and.equal(Features[i].name.trim() + " _addtext"); + expect(body).to.have.property('brief').and.equal(Features[i].brief.trim()); + expect(body).to.have.property('requirements').and.equal(Features[i].requirements.trim()); + expect(body).to.have.property('architecture').and.equal(Features[i].architecture.trim()); + }); } }) }) @@ -53,10 +57,12 @@ describe('Modify brief for Feature', () => { uuid: Features[i].uuid, brief: Features[i].brief + "_addtext" } - }).its('body').should('have.property', 'name', Features[i].name.trim() + "_addtext") - .its('body').should('have.property', 'brief', Features[i].brief.trim() + "_addtext") - .its('body').should('have.property', 'requirements', Features[i].requirements.trim()) - .its('body').should('have.property', 'architecture', Features[i].architecture.trim()) + }).its('body').then(body => { + expect(body).to.have.property('name').and.equal(Features[i].name.trim() + " _addtext"); + expect(body).to.have.property('brief').and.equal(Features[i].brief.trim() + " _addtext"); + expect(body).to.have.property('requirements').and.equal(Features[i].requirements.trim()); + expect(body).to.have.property('architecture').and.equal(Features[i].architecture.trim()); + }); } }) }) @@ -74,10 +80,12 @@ describe('Modify requirements for Feature', () => { uuid: Features[i].uuid, requirements: Features[i].requirements + "_addtext" } - }).its('body').should('have.property', 'name', Features[i].name.trim() + "_addtext") - .its('body').should('have.property', 'brief', Features[i].brief.trim() + "_addtext") - .its('body').should('have.property', 'requirements', Features[i].requirements.trim() + "_addtext") - .its('body').should('have.property', 'architecture', Features[i].architecture.trim()) + }).its('body').then(body => { + expect(body).to.have.property('name').and.equal(Features[i].name.trim() + " _addtext"); + expect(body).to.have.property('brief').and.equal(Features[i].brief.trim() + " _addtext"); + expect(body).to.have.property('requirements').and.equal(Features[i].requirements.trim() + " _addtext"); + expect(body).to.have.property('architecture').and.equal(Features[i].architecture.trim()); + }); } }) }) @@ -95,10 +103,12 @@ describe('Modify architecture for Feature', () => { uuid: Features[i].uuid, architecture: Features[i].architecture + "_addtext" } - }).its('body').should('have.property', 'name', Features[i].name.trim() + "_addtext") - .its('body').should('have.property', 'brief', Features[i].brief.trim() + "_addtext") - .its('body').should('have.property', 'requirements', Features[i].requirements.trim() + "_addtext") - .its('body').should('have.property', 'architecture', Features[i].architecture.trim() + "_addtext") + }).its('body').then(body => { + expect(body).to.have.property('name').and.equal(Features[i].name.trim() + " _addtext"); + expect(body).to.have.property('brief').and.equal(Features[i].brief.trim() + " _addtext"); + expect(body).to.have.property('requirements').and.equal(Features[i].requirements.trim() + " _addtext"); + expect(body).to.have.property('architecture').and.equal(Features[i].architecture.trim() + " _addtext"); + }); } }) }) @@ -116,10 +126,10 @@ describe('Get Features for Workspace', () => { }).then((resp) => { expect(resp.status).to.eq(200) for(let i = 0; i <= 2; i++) { - expect(resp.body[i]).to.have.property('name', Features[i].name.trim() + "_addtext") - expect(resp.body[i]).to.have.property('brief', Features[i].brief.trim() + "_addtext") - expect(resp.body[i]).to.have.property('requirements', Features[i].requirements.trim() + "_addtext") - expect(resp.body[i]).to.have.property('architecture', Features[i].architecture.trim() + "_addtext") + expect(resp.body[i]).to.have.property('name', Features[i].name.trim() + " _addtext") + expect(resp.body[i]).to.have.property('brief', Features[i].brief.trim() + " _addtext") + expect(resp.body[i]).to.have.property('requirements', Features[i].requirements.trim() + " _addtext") + expect(resp.body[i]).to.have.property('architecture', Features[i].architecture.trim() + " _addtext") } }) }) @@ -137,10 +147,10 @@ describe('Get Feature by uuid', () => { body: {} }).then((resp) => { expect(resp.status).to.eq(200) - expect(resp.body).to.have.property('name', Features[i].name.trim() + "_addtext") - expect(resp.body).to.have.property('brief', Features[i].brief.trim() + "_addtext") - expect(resp.body).to.have.property('requirements', Features[i].requirements.trim() + "_addtext") - expect(resp.body).to.have.property('architecture', Features[i].architecture.trim() + "_addtext") + expect(resp.body).to.have.property('name', Features[i].name.trim() + " _addtext") + expect(resp.body).to.have.property('brief', Features[i].brief.trim() + " _addtext") + expect(resp.body).to.have.property('requirements', Features[i].requirements.trim() + " _addtext") + expect(resp.body).to.have.property('architecture', Features[i].architecture.trim() + " _addtext") }) } }) diff --git a/cypress/support/objects/objects.ts b/cypress/support/objects/objects.ts index c1cfa4af0..bcbe8215d 100644 --- a/cypress/support/objects/objects.ts +++ b/cypress/support/objects/objects.ts @@ -100,7 +100,7 @@ export const Features = [ ' the base from which every technical decition relays on.
' + 'We are going to leverage AI to help the PM write better definitions.
' + 'The fields that would benefit form AI assistance are: mission, tactics, ' + - 'feature brief and feature user stories', + 'feature brief and feature user stories ', requirements: ' Create a new page for a conversation format between the PM and the LLM
' + 'Rely as much as possible on stakwork workflows
' + 'Have history of previous definitions ', @@ -110,7 +110,7 @@ export const Features = [ 'Front

', }, { - uuid: 'com1l5on1e49tucv350g', + uuid: 'com1l5on1e49tucv350h', workspace_uuid: 'cohob00n1e4808utqel0', name: ' AI Assited relation between text fields ', priority: 2, @@ -118,9 +118,9 @@ export const Features = [ 'feature brief, user stories, requirements and architecture should have some ' + 'relation between each other.
' + 'One way to do that is to leverage an LLM ' + 'to discern the parts of the defintion that have a connection to other definitions.
' + - 'The UI will need to show the user how each definition is related to other defintions.', + 'The UI will need to show the user how each definition is related to other defintions. ', requirements: 'Create a new process after a Feature text has changed. It should use the LLM to ' + - 'determine de relationship between parts of the text.', + 'determine de relationship between parts of the text. ', architecture: 'Describe the architecture of the feature with the following sections:' + '

Wireframes

Visual Schematics

Object Definition

' + 'DB Schema Changes

UX

CI/CD

Changes

Endpoints

' + diff --git a/db/config.go b/db/config.go index 4b50a4502..f07e89d70 100644 --- a/db/config.go +++ b/db/config.go @@ -67,6 +67,7 @@ func InitDB() { db.AutoMigrate(&ConnectionCodes{}) db.AutoMigrate(&BountyRoles{}) db.AutoMigrate(&UserInvoiceData{}) + db.AutoMigrate(&WorkspaceFeatures{}) DB.MigrateTablesWithOrgUuid() DB.MigrateOrganizationToWorkspace() diff --git a/db/features.go b/db/features.go new file mode 100644 index 000000000..c978124be --- /dev/null +++ b/db/features.go @@ -0,0 +1,41 @@ +package db + +import ( + "strings" + "time" +) + +func (db database) GetFeaturesByWorkspaceUuid(uuid string) []WorkspaceFeatures { + ms := []WorkspaceFeatures{} + + db.db.Model(&WorkspaceFeatures{}).Where("workspace_uuid = ?", uuid).Order("Created").Find(&ms) + + return ms +} + +func (db database) GetFeatureByUuid(uuid string) WorkspaceFeatures { + ms := WorkspaceFeatures{} + + db.db.Model(&WorkspaceFeatures{}).Where("uuid = ?", uuid).Find(&ms) + + return ms +} + +func (db database) CreateOrEditFeature(m WorkspaceFeatures) (WorkspaceFeatures, error) { + m.Name = strings.TrimSpace(m.Name) + m.Brief = strings.TrimSpace(m.Brief) + m.Requirements = strings.TrimSpace(m.Requirements) + m.Architecture = strings.TrimSpace(m.Architecture) + + now := time.Now() + m.Updated = &now + + if db.db.Model(&m).Where("uuid = ?", m.Uuid).Updates(&m).RowsAffected == 0 { + m.Created = &now + db.db.Create(&m) + } + + db.db.Model(&WorkspaceFeatures{}).Where("uuid = ?", m.Uuid).Find(&m) + + return m, nil +} diff --git a/db/interface.go b/db/interface.go index e3cc95163..757c7f542 100644 --- a/db/interface.go +++ b/db/interface.go @@ -139,4 +139,7 @@ type Database interface { PersonUniqueNameFromName(name string) (string, error) ProcessAlerts(p Person) UserHasAccess(pubKeyFromAuth string, uuid string, role string) bool + CreateOrEditFeature(m WorkspaceFeatures) (WorkspaceFeatures, error) + GetFeaturesByWorkspaceUuid(uuid string) []WorkspaceFeatures + GetFeatureByUuid(uuid string) WorkspaceFeatures } diff --git a/db/structs.go b/db/structs.go index e6a25a7de..caaa56a4c 100644 --- a/db/structs.go +++ b/db/structs.go @@ -547,6 +547,20 @@ type WorkspaceUsersData struct { Person } +type WorkspaceFeatures struct { + ID uint `json:"id"` + Uuid string `json:"uuid"` + WorkspaceUuid string `json:"workspace_uuid"` + Name string `json:"name"` + Brief string `json:"brief"` + Requirements string `json:"requirements"` + Architecture string `json:"architecture"` + Created *time.Time `json:"created"` + Updated *time.Time `json:"updated"` + CreatedBy string `json:"created_by"` + UpdatedBy string `json:"updated_by"` +} + type BountyRoles struct { Name string `json:"name"` } diff --git a/handlers/features.go b/handlers/features.go new file mode 100644 index 000000000..9c4f9cd68 --- /dev/null +++ b/handlers/features.go @@ -0,0 +1,95 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/go-chi/chi" + "github.com/stakwork/sphinx-tribes/auth" + "github.com/stakwork/sphinx-tribes/db" +) + +type featureHandler struct { + db db.Database +} + +func NewFeatureHandler(database db.Database) *featureHandler { + return &featureHandler{ + db: database, + } +} + +func (oh *featureHandler) CreateOrEditFeatures(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) + if pubKeyFromAuth == "" { + fmt.Println("no pubkey from auth") + w.WriteHeader(http.StatusUnauthorized) + return + } + + features := db.WorkspaceFeatures{} + body, _ := io.ReadAll(r.Body) + r.Body.Close() + err := json.Unmarshal(body, &features) + + if err != nil { + fmt.Println(err) + w.WriteHeader(http.StatusNotAcceptable) + return + } + + features.CreatedBy = pubKeyFromAuth + + // Validate struct data + err = db.Validate.Struct(features) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + msg := fmt.Sprintf("Error: did not pass validation test : %s", err) + json.NewEncoder(w).Encode(msg) + return + } + + p, err := oh.db.CreateOrEditFeature(features) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(p) +} + +func (oh *featureHandler) GetFeaturesByWorkspaceUuid(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) + if pubKeyFromAuth == "" { + fmt.Println("no pubkey from auth") + w.WriteHeader(http.StatusUnauthorized) + return + } + + uuid := chi.URLParam(r, "uuid") + workspaceFeatures := oh.db.GetFeaturesByWorkspaceUuid(uuid) + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(workspaceFeatures) +} + +func (oh *featureHandler) GetFeatureByUuid(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) + if pubKeyFromAuth == "" { + fmt.Println("no pubkey from auth") + w.WriteHeader(http.StatusUnauthorized) + return + } + + uuid := chi.URLParam(r, "uuid") + workspaceFeature := oh.db.GetFeatureByUuid(uuid) + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(workspaceFeature) +} diff --git a/mocks/Database.go b/mocks/Database.go index 784a2492a..974d81e38 100644 --- a/mocks/Database.go +++ b/mocks/Database.go @@ -927,6 +927,62 @@ func (_c *Database_CreateOrEditBounty_Call) RunAndReturn(run func(db.NewBounty) return _c } +// CreateOrEditFeature provides a mock function with given fields: m +func (_m *Database) CreateOrEditFeature(m db.WorkspaceFeatures) (db.WorkspaceFeatures, error) { + ret := _m.Called(m) + + if len(ret) == 0 { + panic("no return value specified for CreateOrEditFeature") + } + + var r0 db.WorkspaceFeatures + var r1 error + if rf, ok := ret.Get(0).(func(db.WorkspaceFeatures) (db.WorkspaceFeatures, error)); ok { + return rf(m) + } + if rf, ok := ret.Get(0).(func(db.WorkspaceFeatures) db.WorkspaceFeatures); ok { + r0 = rf(m) + } else { + r0 = ret.Get(0).(db.WorkspaceFeatures) + } + + if rf, ok := ret.Get(1).(func(db.WorkspaceFeatures) error); ok { + r1 = rf(m) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Database_CreateOrEditFeature_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateOrEditFeature' +type Database_CreateOrEditFeature_Call struct { + *mock.Call +} + +// CreateOrEditFeature is a helper method to define mock.On call +// - m db.WorkspaceFeatures +func (_e *Database_Expecter) CreateOrEditFeature(m interface{}) *Database_CreateOrEditFeature_Call { + return &Database_CreateOrEditFeature_Call{Call: _e.mock.On("CreateOrEditFeature", m)} +} + +func (_c *Database_CreateOrEditFeature_Call) Run(run func(m db.WorkspaceFeatures)) *Database_CreateOrEditFeature_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(db.WorkspaceFeatures)) + }) + return _c +} + +func (_c *Database_CreateOrEditFeature_Call) Return(_a0 db.WorkspaceFeatures, _a1 error) *Database_CreateOrEditFeature_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Database_CreateOrEditFeature_Call) RunAndReturn(run func(db.WorkspaceFeatures) (db.WorkspaceFeatures, error)) *Database_CreateOrEditFeature_Call { + _c.Call.Return(run) + return _c +} + // CreateOrEditPerson provides a mock function with given fields: m func (_m *Database) CreateOrEditPerson(m db.Person) (db.Person, error) { ret := _m.Called(m) @@ -2520,6 +2576,100 @@ func (_c *Database_GetCreatedBounties_Call) RunAndReturn(run func(*http.Request) return _c } +// GetFeatureByUuid provides a mock function with given fields: uuid +func (_m *Database) GetFeatureByUuid(uuid string) db.WorkspaceFeatures { + ret := _m.Called(uuid) + + if len(ret) == 0 { + panic("no return value specified for GetFeatureByUuid") + } + + var r0 db.WorkspaceFeatures + if rf, ok := ret.Get(0).(func(string) db.WorkspaceFeatures); ok { + r0 = rf(uuid) + } else { + r0 = ret.Get(0).(db.WorkspaceFeatures) + } + + return r0 +} + +// Database_GetFeatureByUuid_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetFeatureByUuid' +type Database_GetFeatureByUuid_Call struct { + *mock.Call +} + +// GetFeatureByUuid is a helper method to define mock.On call +// - uuid string +func (_e *Database_Expecter) GetFeatureByUuid(uuid interface{}) *Database_GetFeatureByUuid_Call { + return &Database_GetFeatureByUuid_Call{Call: _e.mock.On("GetFeatureByUuid", uuid)} +} + +func (_c *Database_GetFeatureByUuid_Call) Run(run func(uuid string)) *Database_GetFeatureByUuid_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Database_GetFeatureByUuid_Call) Return(_a0 db.WorkspaceFeatures) *Database_GetFeatureByUuid_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Database_GetFeatureByUuid_Call) RunAndReturn(run func(string) db.WorkspaceFeatures) *Database_GetFeatureByUuid_Call { + _c.Call.Return(run) + return _c +} + +// GetFeaturesByWorkspaceUuid provides a mock function with given fields: uuid +func (_m *Database) GetFeaturesByWorkspaceUuid(uuid string) []db.WorkspaceFeatures { + ret := _m.Called(uuid) + + if len(ret) == 0 { + panic("no return value specified for GetFeaturesByWorkspaceUuid") + } + + var r0 []db.WorkspaceFeatures + if rf, ok := ret.Get(0).(func(string) []db.WorkspaceFeatures); ok { + r0 = rf(uuid) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]db.WorkspaceFeatures) + } + } + + return r0 +} + +// Database_GetFeaturesByWorkspaceUuid_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetFeaturesByWorkspaceUuid' +type Database_GetFeaturesByWorkspaceUuid_Call struct { + *mock.Call +} + +// GetFeaturesByWorkspaceUuid is a helper method to define mock.On call +// - uuid string +func (_e *Database_Expecter) GetFeaturesByWorkspaceUuid(uuid interface{}) *Database_GetFeaturesByWorkspaceUuid_Call { + return &Database_GetFeaturesByWorkspaceUuid_Call{Call: _e.mock.On("GetFeaturesByWorkspaceUuid", uuid)} +} + +func (_c *Database_GetFeaturesByWorkspaceUuid_Call) Run(run func(uuid string)) *Database_GetFeaturesByWorkspaceUuid_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Database_GetFeaturesByWorkspaceUuid_Call) Return(_a0 []db.WorkspaceFeatures) *Database_GetFeaturesByWorkspaceUuid_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Database_GetFeaturesByWorkspaceUuid_Call) RunAndReturn(run func(string) []db.WorkspaceFeatures) *Database_GetFeaturesByWorkspaceUuid_Call { + _c.Call.Return(run) + return _c +} + // GetFilterStatusCount provides a mock function with given fields: func (_m *Database) GetFilterStatusCount() db.FilterStattuCount { ret := _m.Called() diff --git a/routes/features.go b/routes/features.go new file mode 100644 index 000000000..0b2449399 --- /dev/null +++ b/routes/features.go @@ -0,0 +1,21 @@ +package routes + +import ( + "github.com/go-chi/chi" + "github.com/stakwork/sphinx-tribes/auth" + "github.com/stakwork/sphinx-tribes/db" + "github.com/stakwork/sphinx-tribes/handlers" +) + +func FeatureRoutes() chi.Router { + r := chi.NewRouter() + featureHandlers := handlers.NewFeatureHandler(db.DB) + r.Group(func(r chi.Router) { + r.Use(auth.PubKeyContext) + + r.Post("/", featureHandlers.CreateOrEditFeatures) + r.Get("/forworkspace/{uuid}", featureHandlers.GetFeaturesByWorkspaceUuid) + r.Get("/{uuid}", featureHandlers.GetFeatureByUuid) + }) + return r +} diff --git a/routes/index.go b/routes/index.go index 98dc0c162..22090938c 100644 --- a/routes/index.go +++ b/routes/index.go @@ -36,6 +36,7 @@ func NewRouter() *http.Server { r.Mount("/gobounties", BountyRoutes()) r.Mount("/workspaces", WorkspaceRoutes()) r.Mount("/metrics", MetricsRoutes()) + r.Mount("/features", FeatureRoutes()) r.Group(func(r chi.Router) { r.Get("/tribe_by_feed", tribeHandlers.GetFirstTribeByFeed)