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)