diff --git a/api/crm/spec.json b/api/crm/spec.json index 6c21cb91c..0ef173b08 100644 --- a/api/crm/spec.json +++ b/api/crm/spec.json @@ -454,6 +454,12 @@ "type": "int", "required": false, "title": "Returned items per page (default 50)" + }, + { + "name": "sort", + "type": "string", + "required": false, + "title": "Sort field (default id desc)" } ] } diff --git a/api/crm/spec/module.json b/api/crm/spec/module.json index 8287993d2..92c3ab21b 100644 --- a/api/crm/spec/module.json +++ b/api/crm/spec/module.json @@ -218,6 +218,12 @@ "required": false, "title": "Returned items per page (default 50)", "type": "int" + }, + { + "name": "sort", + "required": false, + "title": "Sort field (default id desc)", + "type": "string" } ], "path": [ diff --git a/crm/repository/content.go b/crm/repository/content.go index 1ebbb45e5..c1af1d44e 100644 --- a/crm/repository/content.go +++ b/crm/repository/content.go @@ -20,7 +20,7 @@ type ( FindByID(id uint64) (*types.Content, error) - Find(moduleID uint64, query string, page int, perPage int) (*FindResponse, error) + Find(moduleID uint64, query string, page int, perPage int, sort string) (*FindResponse, error) Create(mod *types.Content) (*types.Content, error) Update(mod *types.Content) (*types.Content, error) @@ -34,6 +34,7 @@ type ( Page int `json:"page"` PerPage int `json:"perPage"` Count int `json:"count"` + Sort string `json:"sort"` } FindResponse struct { @@ -66,7 +67,7 @@ func (r *content) FindByID(id uint64) (*types.Content, error) { return mod, nil } -func (r *content) Find(moduleID uint64, query string, page int, perPage int) (*FindResponse, error) { +func (r *content) Find(moduleID uint64, query string, page int, perPage int, sort string) (*FindResponse, error) { if page < 0 { page = 0 } @@ -84,6 +85,7 @@ func (r *content) Find(moduleID uint64, query string, page int, perPage int) (*F Page: page, PerPage: perPage, Query: query, + Sort: sort, }, Contents: make([]*types.Content, 0), } @@ -93,10 +95,87 @@ func (r *content) Find(moduleID uint64, query string, page int, perPage int) (*F sqlSelect := "SELECT * FROM crm_content" sqlCount := "SELECT count(*) FROM crm_content" sqlWhere := "WHERE module_id=? and deleted_at IS NULL" - sqlOrder := "ORDER BY id DESC" sqlLimit := fmt.Sprintf("LIMIT %d, %d", page*perPage, perPage) + chuncks := strings.Split(sort, ",") + if len(chuncks) > 0 { + + // Ger module fields. + modulRepo := Module(r.Context(), r.db()) + mod, err := modulRepo.FindByID(moduleID) + if err != nil { + return nil, err + } + modFields, err := modulRepo.FieldNames(mod) + if err != nil { + return nil, err + } + fieldMap := make(map[string]bool) + for i := 0; i < len(modFields); i++ { + fieldMap[modFields[i]] = true + } + + orderFields := make([]string, 0) + for _, c := range chuncks { + args := strings.Split(c, " ") + + var field string + if _, ok := fieldMap[args[0]]; ok { + field = "JSON_UNQUOTE(JSON_EXTRACT(json, REPLACE(JSON_UNQUOTE(JSON_SEARCH(json, 'one', '" + args[0] + "')), '.name', '.value')))" + } else { + switch args[0] { + case "moduleId": + field = "module_id" + case "userId": + field = "user_id" + case "createdAt": + field = "created_at" + case "updatedAt": + field = "updated_at" + case "deletedAt": + field = "deleted_at" + default: + field = "id" + } + } + + // Check for second order parameter or use default value ASC. + order := "ASC" + if len(args) == 2 { + order = strings.ToUpper(args[1]) + switch order { + case "DESC": + order = "DESC" + default: + order = "ASC" + } + } + + // We skip batch of parameters if there are more then 2 values. + if len(args) > 2 { + continue + } + + // Add field and order to sort order fields. + orderFields = append(orderFields, field+" "+order) + } + + sqlOrder = "ORDER BY " + strings.Join(orderFields, ", ") + } + + // One possibility to order by field value without JSON, is query written bellow with FIELD over column names and order by value: + // SELECT * FROM crm_content + // LEFT JOIN crm_content_column ON crm_content.id = crm_content_column.content_id" + // WHERE column_name in ('name', 'email') + // ORDER BY FIELD(column_name, 'email', 'name'), column_value; + + // Possibility to order with JSON: + // SELECT *, + // JSON_UNQUOTE(JSON_EXTRACT(json, REPLACE(JSON_UNQUOTE(JSON_SEARCH(json, 'all', 'email')), '.name', '.value'))) as emailField + // FROM crm_content + // ORDER by emailField asc; + switch true { case query != "": sqlWhere = sqlWhere + " AND id in (select distinct content_id from crm_content_column where column_value like ?)" @@ -110,7 +189,7 @@ func (r *content) Find(moduleID uint64, query string, page int, perPage int) (*F if err := r.db().Get(&response.Meta.Count, sqlCount+" "+sqlWhere, moduleID); err != nil { return nil, err } - if err := r.db().Select(&response.Contents, fmt.Sprintf("SELECT * FROM crm_content WHERE module_id=? and deleted_at IS NULL ORDER BY id DESC LIMIT %d, %d", page, perPage), moduleID); err != nil { + if err := r.db().Select(&response.Contents, sqlSelect+" "+sqlWhere+" "+sqlOrder+" "+sqlLimit, moduleID); err != nil { return nil, err } } diff --git a/crm/rest/module.go b/crm/rest/module.go index 83086985e..8ce31b11c 100644 --- a/crm/rest/module.go +++ b/crm/rest/module.go @@ -55,7 +55,7 @@ func (s *Module) Edit(ctx context.Context, r *request.ModuleEdit) (interface{}, } func (s *Module) ContentList(ctx context.Context, r *request.ModuleContentList) (interface{}, error) { - return s.content.With(ctx).Find(r.ModuleID, r.Query, r.Page, r.PerPage) + return s.content.With(ctx).Find(r.ModuleID, r.Query, r.Page, r.PerPage, r.Sort) } func (s *Module) ContentRead(ctx context.Context, r *request.ModuleContentRead) (interface{}, error) { diff --git a/crm/rest/request/module.go b/crm/rest/request/module.go index 19474e2e4..659960ade 100644 --- a/crm/rest/request/module.go +++ b/crm/rest/request/module.go @@ -358,6 +358,7 @@ type ModuleContentList struct { Query string Page int PerPage int + Sort string ModuleID uint64 `json:",string"` } @@ -404,6 +405,10 @@ func (m *ModuleContentList) Fill(r *http.Request) (err error) { m.PerPage = parseInt(val) } + if val, ok := get["sort"]; ok { + + m.Sort = val + } m.ModuleID = parseUInt64(chi.URLParam(r, "moduleID")) return err diff --git a/crm/service/content.go b/crm/service/content.go index fa98cf0fc..5fea48051 100644 --- a/crm/service/content.go +++ b/crm/service/content.go @@ -28,7 +28,7 @@ type ( FindByID(contentID uint64) (*types.Content, error) - Find(moduleID uint64, query string, page int, perPage int) (*repository.FindResponse, error) + Find(moduleID uint64, query string, page int, perPage int, sort string) (*repository.FindResponse, error) Create(content *types.Content) (*types.Content, error) Update(content *types.Content) (*types.Content, error) @@ -63,8 +63,8 @@ func (s *content) FindByID(id uint64) (*types.Content, error) { return response, s.preload(response, "page", "user", "fields") } -func (s *content) Find(moduleID uint64, query string, page int, perPage int) (*repository.FindResponse, error) { - response, err := s.repository.Find(moduleID, query, page, perPage) +func (s *content) Find(moduleID uint64, query string, page int, perPage int, sort string) (*repository.FindResponse, error) { + response, err := s.repository.Find(moduleID, query, page, perPage, sort) if err != nil { return nil, err } @@ -84,7 +84,7 @@ func (s *content) Create(mod *types.Content) (*types.Content, error) { func (s *content) Update(mod *types.Content) (*types.Content, error) { if mod.ID == 0 { - return nil, errors.New("Error when savig content, invalid ID") + return nil, errors.New("Error when saving content, invalid ID") } return s.repository.Update(mod) } diff --git a/crm/service/content_test.go b/crm/service/content_test.go index d1f4cdcc0..28d8e710e 100644 --- a/crm/service/content_test.go +++ b/crm/service/content_test.go @@ -48,6 +48,10 @@ func TestContent(t *testing.T) { Name: "options", Kind: "select_multi", }, + types.ModuleField{ + Name: "description", + Kind: "text", + }, }, } @@ -71,37 +75,75 @@ func TestContent(t *testing.T) { Name: "options", Related: []string{"1", "2", "3"}, }, + types.ContentColumn{ + Name: "description", + Value: "jack of all trades", + }, } - content := &types.Content{ + content1 := &types.Content{ ModuleID: module.ID, } - (&content.Fields).Scan(func() []byte { + (&content1.Fields).Scan(func() []byte { b, _ := json.Marshal(columns) return b }()) + columns2 := []types.ContentColumn{ + types.ContentColumn{ + Name: "name", + Value: "Marko Novak", + }, + types.ContentColumn{ + Name: "email", + Value: "marko.n@example.com", + }, + types.ContentColumn{ + Name: "options", + Related: []string{"1", "2", "3"}, + }, + types.ContentColumn{ + Name: "description", + Value: "persona non grata", + }, + } + + content2 := &types.Content{ + ModuleID: module.ID, + } + (&content2.Fields).Scan(func() []byte { + b, _ := json.Marshal(columns2) + return b + }()) + // now work with content { { - m, err := repository.Update(content) - assert(t, m == nil, "Expected empty return for ivalid update, got %#v", m) + m, err := repository.Update(content1) + assert(t, m == nil, "Expected empty return for invalid update, got %#v", m) assert(t, err != nil, "Expected error when updating invalid content") } // create content - m, err := repository.Create(content) + m1, err := repository.Create(content1) assert(t, err == nil, "Error when creating content: %+v", err) - assert(t, m.ID > 0, "Expected auto generated ID") - assert(t, m.User != nil, "Expected non-nil user when creating content") - assert(t, m.User.Username == "TestUser", "Expected 'TestUser' as username, got '%s'", m.User.Username) + assert(t, m1.ID > 0, "Expected auto generated ID") + assert(t, m1.User != nil, "Expected non-nil user when creating content") + assert(t, m1.User.Username == "TestUser", "Expected 'TestUser' as username, got '%s'", m1.User.Username) + + // create content + m2, err := repository.Create(content2) + assert(t, err == nil, "Error when creating content: %+v", err) + assert(t, m2.ID > 0, "Expected auto generated ID") + assert(t, m2.User != nil, "Expected non-nil user when creating content") + assert(t, m2.User.Username == "TestUser", "Expected 'TestUser' as username, got '%s'", m2.User.Username) // fetch created content { - ms, err := repository.FindByID(m.ID) + ms, err := repository.FindByID(m1.ID) assert(t, err == nil, "Error when retrieving content by id: %+v", err) - assert(t, ms.ID == m.ID, "Expected ID from database to match, %d != %d", m.ID, ms.ID) - assert(t, ms.ModuleID == m.ModuleID, "Expected Module ID from database to match, %d != %d", m.ModuleID, ms.ModuleID) + assert(t, ms.ID == m1.ID, "Expected ID from database to match, %d != %d", m1.ID, ms.ID) + assert(t, ms.ModuleID == m1.ModuleID, "Expected Module ID from database to match, %d != %d", m1.ModuleID, ms.ModuleID) { fields, err := repository.Fields(ms) @@ -126,57 +168,84 @@ func TestContent(t *testing.T) { // update created content { - _, err := repository.Update(m) + _, err := repository.Update(m1) assert(t, err == nil, "Error when updating content, %+v", err) } // re-fetch content { - ms, err := repository.FindByID(m.ID) + ms, err := repository.FindByID(m1.ID) assert(t, err == nil, "Error when retrieving content by id: %+v", err) - assert(t, ms.ID == m.ID, "Expected ID from database to match, %d != %d", m.ID, ms.ID) - assert(t, ms.ModuleID == m.ModuleID, "Expected ID from database to match, %d != %d", m.ModuleID, ms.ModuleID) + assert(t, ms.ID == m1.ID, "Expected ID from database to match, %d != %d", m1.ID, ms.ID) + assert(t, ms.ModuleID == m1.ModuleID, "Expected ID from database to match, %d != %d", m1.ModuleID, ms.ModuleID) } // fetch all contents { - mr, err := repository.Find(module.ID, "", 0, 20) + mr, err := repository.Find(module.ID, "", 0, 20, "id desc") assert(t, err == nil, "Error when retrieving contents: %+v", err) - assert(t, len(mr.Contents) == 1, "Expected one content, got %d", len(mr.Contents)) - assert(t, mr.Meta.Count == 1, "Expected Meta.Count == 1, got %d", mr.Meta.Count) - assert(t, mr.Contents[0].ModuleID == m.ModuleID, "Expected content module to match, %d != %d", m.ModuleID, mr.Contents[0].ModuleID) + assert(t, len(mr.Contents) == 2, "Expected two content, got %d", len(mr.Contents)) + assert(t, mr.Meta.Count == 2, "Expected Meta.Count == 2, got %d", mr.Meta.Count) + assert(t, mr.Meta.Sort == "id desc", "Expected Meta.Sort == id desc, got '%s'", mr.Meta.Sort) + assert(t, mr.Contents[0].ModuleID == m1.ModuleID, "Expected content module to match, %d != %d", m1.ModuleID, mr.Contents[0].ModuleID) + assert(t, mr.Contents[0].ID > mr.Contents[1].ID, "Expected order to be descending") + } + + // fetch all contents + { + mr, err := repository.Find(module.ID, "", 0, 20, "name asc, email desc") + assert(t, err == nil, "Error when retrieving contents: %+v", err) + assert(t, len(mr.Contents) == 2, "Expected two content, got %d", len(mr.Contents)) + assert(t, mr.Meta.Count == 2, "Expected Meta.Count == 2, got %d", mr.Meta.Count) + assert(t, mr.Meta.Sort == "name asc, email desc", "Expected Meta.Sort == 'name asc, email desc' '%s'", mr.Meta.Sort) + assert(t, mr.Contents[0].ModuleID == m1.ModuleID, "Expected content module to match, %d != %d", m1.ModuleID, mr.Contents[0].ModuleID) + assert(t, mr.Contents[0].ID > mr.Contents[1].ID, "Expected order to be ascending") + } + + // fetch all contents + { + mr, err := repository.Find(module.ID, "", 0, 20, "created_at desc") + assert(t, err == nil, "Error when retrieving contents: %+v", err) + assert(t, len(mr.Contents) == 2, "Expected two content, got %d", len(mr.Contents)) + assert(t, mr.Meta.Count == 2, "Expected Meta.Count == 2, got %d", mr.Meta.Count) + assert(t, mr.Meta.Sort == "created_at desc", "Expected Meta.Sort == created_at desc, got '%s'", mr.Meta.Sort) + assert(t, mr.Contents[0].ModuleID == m1.ModuleID, "Expected content module to match, %d != %d", m1.ModuleID, mr.Contents[0].ModuleID) + assert(t, mr.Contents[0].ID > mr.Contents[1].ID, "Expected order to be ascending") } // fetch all contents by query { - mr, err := repository.Find(module.ID, "petric", 0, 20) + mr, err := repository.Find(module.ID, "petric", 0, 20, "id desc") assert(t, err == nil, "Error when retrieving contents: %+v", err) assert(t, len(mr.Contents) == 1, "Expected one content, got %d", len(mr.Contents)) assert(t, mr.Meta.Count == 1, "Expected Meta.Count == 1, got %d", mr.Meta.Count) assert(t, mr.Meta.Page == 0, "Expected Meta.Page == 0, got %d", mr.Meta.Page) assert(t, mr.Meta.PerPage == 20, "Expected Meta.PerPage == 20, got %d", mr.Meta.PerPage) assert(t, mr.Meta.Query == "petric", "Expected Meta.Query == petric, got '%s'", mr.Meta.Query) + assert(t, mr.Meta.Sort == "id desc", "Expected Meta.Sort == id desc, got '%s'", mr.Meta.Sort) } // fetch all contents by query { - mr, err := repository.Find(module.ID, "niall", 0, 20) + mr, err := repository.Find(module.ID, "niall", 0, 20, "id asc") assert(t, err == nil, "Error when retrieving contents: %+v", err) assert(t, len(mr.Contents) == 0, "Expected no contents, got %d", len(mr.Contents)) } - // re-fetch content + // delete content { - err := repository.DeleteByID(m.ID) + err := repository.DeleteByID(m1.ID) + assert(t, err == nil, "Error when retrieving content by id: %+v", err) + + err = repository.DeleteByID(m2.ID) assert(t, err == nil, "Error when retrieving content by id: %+v", err) } // fetch all contents { - mr, err := repository.Find(module.ID, "", 0, 20) + mr, err := repository.Find(module.ID, "", 0, 20, "") assert(t, err == nil, "Error when retrieving contents: %+v", err) assert(t, len(mr.Contents) == 0, "Expected no content, got %d", len(mr.Contents)) } } - } diff --git a/crm/service/page_test.go b/crm/service/page_test.go index d25d04515..cf3e35635 100644 --- a/crm/service/page_test.go +++ b/crm/service/page_test.go @@ -25,7 +25,7 @@ func TestPage(t *testing.T) { { { m, err := repository.Update(page) - assert(t, m == nil, "Expected empty return for ivalid update, got %#v", m) + assert(t, m == nil, "Expected empty return for invalid update, got %#v", m) assert(t, err != nil, "Expected error when updating invalid content") } diff --git a/docs/crm/README.md b/docs/crm/README.md index 636870319..0adb935b0 100644 --- a/docs/crm/README.md +++ b/docs/crm/README.md @@ -333,6 +333,7 @@ Example bar chart with number of leads per country: select lead.country from lea | query | string | GET | Search query | N/A | NO | | page | int | GET | Page number (0 based) | N/A | NO | | perPage | int | GET | Returned items per page (default 50) | N/A | NO | +| sort | string | GET | Sort field (default id desc) | N/A | NO | | moduleID | uint64 | PATH | Module ID | N/A | YES | ## List/read contents from module section