Merge branch 'content-to-record'

This commit is contained in:
Denis Arh
2018-12-24 10:22:51 +01:00
24 changed files with 560 additions and 551 deletions
+24 -24
View File
@@ -658,7 +658,7 @@
}
},
{
"name": "content/report",
"name": "record/report",
"method": "GET",
"title": "Generates report from module records",
"path": "/{moduleID}/report",
@@ -688,10 +688,10 @@
}
},
{
"name": "content/list",
"name": "record/list",
"method": "GET",
"title": "List/read contents from module section",
"path": "/{moduleID}/content",
"title": "List/read records from module section",
"path": "/{moduleID}/record",
"parameters": {
"path": [
{
@@ -730,10 +730,10 @@
}
},
{
"name": "content/create",
"name": "record/create",
"method": "POST",
"title": "List/read contents from module section",
"path": "/{moduleID}/content",
"title": "Create record in module section",
"path": "/{moduleID}/record",
"parameters": {
"path": [
{
@@ -748,16 +748,16 @@
"type": "sqlxTypes.JSONText",
"name": "fields",
"required": true,
"title": "Content JSON"
"title": "Record JSON"
}
]
}
},
{
"name": "content/read",
"name": "record/read",
"method": "GET",
"title": "Read contents by ID from module section",
"path": "/{moduleID}/content/{contentID}",
"title": "Read records by ID from module section",
"path": "/{moduleID}/record/{recordID}",
"parameters": {
"path": [
{
@@ -768,18 +768,18 @@
},
{
"type": "uint64",
"name": "contentID",
"name": "recordID",
"required": true,
"title": "Content ID"
"title": "Record ID"
}
]
}
},
{
"name": "content/edit",
"name": "record/edit",
"method": "POST",
"title": "Add/update contents in module section",
"path": "/{moduleID}/content/{contentID}",
"title": "Add/update records in module section",
"path": "/{moduleID}/record/{recordID}",
"parameters": {
"path": [
{
@@ -790,9 +790,9 @@
},
{
"type": "uint64",
"name": "contentID",
"name": "recordID",
"required": true,
"title": "Content ID"
"title": "Record ID"
}
],
"post": [
@@ -800,16 +800,16 @@
"type": "sqlxTypes.JSONText",
"name": "fields",
"required": true,
"title": "Content JSON"
"title": "Record JSON"
}
]
}
},
{
"name": "content/delete",
"name": "record/delete",
"method": "DELETE",
"title": "Delete content row from module section",
"path": "/{moduleID}/content/{contentID}",
"title": "Delete record row from module section",
"path": "/{moduleID}/record/{recordID}",
"parameters": {
"path": [
{
@@ -820,9 +820,9 @@
},
{
"type": "uint64",
"name": "contentID",
"name": "recordID",
"required": true,
"title": "Content ID"
"title": "Record ID"
}
]
}
+24 -24
View File
@@ -117,7 +117,7 @@
}
},
{
"Name": "content/report",
"Name": "record/report",
"Method": "GET",
"Title": "Generates report from module records",
"Path": "/{moduleID}/report",
@@ -147,10 +147,10 @@
}
},
{
"Name": "content/list",
"Name": "record/list",
"Method": "GET",
"Title": "List/read contents from module section",
"Path": "/{moduleID}/content",
"Title": "List/read records from module section",
"Path": "/{moduleID}/record",
"Parameters": {
"get": [
{
@@ -189,10 +189,10 @@
}
},
{
"Name": "content/create",
"Name": "record/create",
"Method": "POST",
"Title": "List/read contents from module section",
"Path": "/{moduleID}/content",
"Title": "Create record in module section",
"Path": "/{moduleID}/record",
"Parameters": {
"path": [
{
@@ -206,17 +206,17 @@
{
"name": "fields",
"required": true,
"title": "Content JSON",
"title": "Record JSON",
"type": "sqlxTypes.JSONText"
}
]
}
},
{
"Name": "content/read",
"Name": "record/read",
"Method": "GET",
"Title": "Read contents by ID from module section",
"Path": "/{moduleID}/content/{contentID}",
"Title": "Read records by ID from module section",
"Path": "/{moduleID}/record/{recordID}",
"Parameters": {
"path": [
{
@@ -226,19 +226,19 @@
"type": "uint64"
},
{
"name": "contentID",
"name": "recordID",
"required": true,
"title": "Content ID",
"title": "Record ID",
"type": "uint64"
}
]
}
},
{
"Name": "content/edit",
"Name": "record/edit",
"Method": "POST",
"Title": "Add/update contents in module section",
"Path": "/{moduleID}/content/{contentID}",
"Title": "Add/update records in module section",
"Path": "/{moduleID}/record/{recordID}",
"Parameters": {
"path": [
{
@@ -248,9 +248,9 @@
"type": "uint64"
},
{
"name": "contentID",
"name": "recordID",
"required": true,
"title": "Content ID",
"title": "Record ID",
"type": "uint64"
}
],
@@ -258,17 +258,17 @@
{
"name": "fields",
"required": true,
"title": "Content JSON",
"title": "Record JSON",
"type": "sqlxTypes.JSONText"
}
]
}
},
{
"Name": "content/delete",
"Name": "record/delete",
"Method": "DELETE",
"Title": "Delete content row from module section",
"Path": "/{moduleID}/content/{contentID}",
"Title": "Delete record row from module section",
"Path": "/{moduleID}/record/{recordID}",
"Parameters": {
"path": [
{
@@ -278,9 +278,9 @@
"type": "uint64"
},
{
"name": "contentID",
"name": "recordID",
"required": true,
"title": "Content ID",
"title": "Record ID",
"type": "uint64"
}
]
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
ALTER TABLE `crm_content` RENAME TO `crm_record`;
ALTER TABLE `crm_record` MODIFY COLUMN `json` json DEFAULT NULL COMMENT 'Records in JSON format.';
ALTER TABLE `crm_content_column` RENAME TO `crm_record_column`;
ALTER TABLE `crm_record_column` CHANGE COLUMN `content_id` `record_id` bigint(20);
ALTER TABLE `crm_content_links` RENAME TO `crm_record_links`;
ALTER TABLE `crm_record_links` CHANGE COLUMN `content_id` `record_id` bigint(20) unsigned;
ALTER TABLE `crm_record_links` CHANGE COLUMN `rel_content_id` `rel_record_id` bigint(20) unsigned;
+1 -1
View File
@@ -98,7 +98,7 @@ func (r *module) Fields(mod *types.Module) ([]*types.ModuleField, error) {
return fields, r.db().Select(&fields, "select * from crm_module_form where module_id=? order by place asc", mod.ID)
}
// FieldNames returns a slice of field names, used for ordering content row columns
// FieldNames returns a slice of field names, used for ordering record row columns
func (r *module) FieldNames(mod *types.Module) ([]string, error) {
if fields, err := r.Fields(mod); err != nil {
return []string{}, err
@@ -14,19 +14,19 @@ import (
)
type (
ContentRepository interface {
With(ctx context.Context, db *factory.DB) ContentRepository
RecordRepository interface {
With(ctx context.Context, db *factory.DB) RecordRepository
FindByID(id uint64) (*types.Content, error)
FindByID(id uint64) (*types.Record, error)
Report(moduleID uint64, params *types.ContentReport) (results interface{}, err error)
Report(moduleID uint64, params *types.RecordReport) (results interface{}, err 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)
Create(mod *types.Record) (*types.Record, error)
Update(mod *types.Record) (*types.Record, error)
DeleteByID(id uint64) error
Fields(mod *types.Content) ([]*types.ContentColumn, error)
Fields(mod *types.Record) ([]*types.RecordColumn, error)
}
FindResponseMeta struct {
@@ -38,37 +38,37 @@ type (
}
FindResponse struct {
Meta FindResponseMeta `json:"meta"`
Contents []*types.Content `json:"contents"`
Meta FindResponseMeta `json:"meta"`
Records []*types.Record `json:"records"`
}
content struct {
record struct {
*repository
}
)
func Content(ctx context.Context, db *factory.DB) ContentRepository {
return (&content{}).With(ctx, db)
func Record(ctx context.Context, db *factory.DB) RecordRepository {
return (&record{}).With(ctx, db)
}
func (r *content) With(ctx context.Context, db *factory.DB) ContentRepository {
return &content{
func (r *record) With(ctx context.Context, db *factory.DB) RecordRepository {
return &record{
repository: r.repository.With(ctx, db),
}
}
// @todo: update to accepted DeletedAt column semantics from SAM
func (r *content) FindByID(id uint64) (*types.Content, error) {
mod := &types.Content{}
if err := r.db().Get(mod, "SELECT * FROM crm_content WHERE id=? and deleted_at IS NULL", id); err != nil {
func (r *record) FindByID(id uint64) (*types.Record, error) {
mod := &types.Record{}
if err := r.db().Get(mod, "SELECT * FROM crm_record WHERE id=? and deleted_at IS NULL", id); err != nil {
return nil, err
}
return mod, nil
}
func (r *content) Report(moduleID uint64, params *types.ContentReport) (results interface{}, err error) {
crb := NewContentReportBuilder(moduleID, params)
func (r *record) Report(moduleID uint64, params *types.RecordReport) (results interface{}, err error) {
crb := NewRecordReportBuilder(moduleID, params)
var result = make([]map[string]interface{}, 0)
@@ -85,7 +85,7 @@ func (r *content) Report(moduleID uint64, params *types.ContentReport) (results
}
}
func (r *content) Find(moduleID uint64, query string, page int, perPage int, sort string) (*FindResponse, error) {
func (r *record) Find(moduleID uint64, query string, page int, perPage int, sort string) (*FindResponse, error) {
if page < 0 {
page = 0
}
@@ -105,13 +105,13 @@ func (r *content) Find(moduleID uint64, query string, page int, perPage int, sor
Query: query,
Sort: sort,
},
Contents: make([]*types.Content, 0),
Records: make([]*types.Record, 0),
}
query = "%" + query + "%"
sqlSelect := "SELECT * FROM crm_content"
sqlCount := "SELECT count(*) FROM crm_content"
sqlSelect := "SELECT * FROM crm_record"
sqlCount := "SELECT count(*) FROM crm_record"
sqlWhere := "WHERE module_id=? and deleted_at IS NULL"
sqlOrder := "ORDER BY id DESC"
sqlLimit := fmt.Sprintf("LIMIT %d, %d", page*perPage, perPage)
@@ -183,31 +183,31 @@ func (r *content) Find(moduleID uint64, query string, page int, perPage int, sor
}
// 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"
// SELECT * FROM crm_record
// LEFT JOIN crm_record ON crm_record.id = crm_record_column.record_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
// FROM crm_record
// 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 ?)"
sqlWhere = sqlWhere + " AND id in (select distinct record_id from crm_record_column where column_value like ?)"
if err := r.db().Get(&response.Meta.Count, sqlCount+" "+sqlWhere, moduleID, query); err != nil {
return nil, err
}
if err := r.db().Select(&response.Contents, sqlSelect+" "+sqlWhere+" "+sqlOrder+" "+sqlLimit, moduleID, query); err != nil {
if err := r.db().Select(&response.Records, sqlSelect+" "+sqlWhere+" "+sqlOrder+" "+sqlLimit, moduleID, query); err != nil {
return nil, err
}
default:
if err := r.db().Get(&response.Meta.Count, sqlCount+" "+sqlWhere, moduleID); err != nil {
return nil, err
}
if err := r.db().Select(&response.Contents, sqlSelect+" "+sqlWhere+" "+sqlOrder+" "+sqlLimit, moduleID); err != nil {
if err := r.db().Select(&response.Records, sqlSelect+" "+sqlWhere+" "+sqlOrder+" "+sqlLimit, moduleID); err != nil {
return nil, err
}
}
@@ -215,80 +215,80 @@ func (r *content) Find(moduleID uint64, query string, page int, perPage int, sor
return response, nil
}
func (r *content) Create(mod *types.Content) (*types.Content, error) {
func (r *record) Create(mod *types.Record) (*types.Record, error) {
mod.ID = factory.Sonyflake.NextID()
mod.CreatedAt = time.Now()
mod.UserID = Identity(r.Context())
fields := make([]types.ContentColumn, 0)
fields := make([]types.RecordColumn, 0)
if err := json.Unmarshal(mod.Fields, &fields); err != nil {
return nil, errors.Wrap(err, "No content")
}
r.db().Exec("delete from crm_content_links where content_id=?", mod.ID)
r.db().Exec("delete from crm_record_links where record_id=?", mod.ID)
for _, v := range fields {
v.ContentID = mod.ID
if err := r.db().Replace("crm_content_column", v); err != nil {
v.RecordID = mod.ID
if err := r.db().Replace("crm_record_column", v); err != nil {
return nil, errors.Wrap(err, "Error adding columns")
}
for _, related := range v.Related {
row := types.Related{
ContentID: v.ContentID,
Name: v.Name,
RelatedContentID: related,
RecordID: v.RecordID,
Name: v.Name,
RelatedRecordID: related,
}
if err := r.db().Replace("crm_content_links", row); err != nil {
if err := r.db().Replace("crm_record_links", row); err != nil {
return nil, errors.Wrap(err, "Error adding column links")
}
}
}
if err := r.db().Insert("crm_content", mod); err != nil {
if err := r.db().Insert("crm_record", mod); err != nil {
return nil, err
}
return mod, nil
}
func (r *content) Update(mod *types.Content) (*types.Content, error) {
func (r *record) Update(mod *types.Record) (*types.Record, error) {
now := time.Now()
mod.UpdatedAt = &now
fields := make([]types.ContentColumn, 0)
fields := make([]types.RecordColumn, 0)
if err := json.Unmarshal(mod.Fields, &fields); err != nil {
return nil, errors.Wrap(err, "Error when saving content, no content")
return nil, errors.Wrap(err, "Error when saving record, no content")
}
r.db().Exec("delete from crm_content_links where content_id=?", mod.ID)
r.db().Exec("delete from crm_record_links where record_id=?", mod.ID)
for _, v := range fields {
v.ContentID = mod.ID
if err := r.db().Replace("crm_content_column", v); err != nil {
v.RecordID = mod.ID
if err := r.db().Replace("crm_record_column", v); err != nil {
return nil, errors.Wrap(err, "Error adding columns to database")
}
for _, related := range v.Related {
row := types.Related{
ContentID: v.ContentID,
Name: v.Name,
RelatedContentID: related,
RecordID: v.RecordID,
Name: v.Name,
RelatedRecordID: related,
}
if err := r.db().Replace("crm_content_links", row); err != nil {
if err := r.db().Replace("crm_record_links", row); err != nil {
return nil, errors.Wrap(err, "Error adding column links")
}
}
}
return mod, r.db().Replace("crm_content", mod)
return mod, r.db().Replace("crm_record", mod)
}
func (r *content) DeleteByID(id uint64) error {
_, err := r.db().Exec("update crm_content set deleted_at=? where id=?", time.Now(), id)
func (r *record) DeleteByID(id uint64) error {
_, err := r.db().Exec("update crm_record set deleted_at=? where id=?", time.Now(), id)
return err
}
func (r *content) Fields(content *types.Content) ([]*types.ContentColumn, error) {
result := make([]*types.ContentColumn, 0)
func (r *record) Fields(record *types.Record) ([]*types.RecordColumn, error) {
result := make([]*types.RecordColumn, 0)
module := Module(r.ctx, r.db())
mod, err := module.FindByID(content.ModuleID)
mod, err := module.FindByID(record.ModuleID)
if err != nil {
return result, err
}
@@ -303,10 +303,10 @@ func (r *content) Fields(content *types.Content) ([]*types.ContentColumn, error)
order := "FIELD(column_name" + strings.Repeat(",?", len(fieldNames)) + ")"
args := []interface{}{
content.ID,
record.ID,
}
for _, v := range fieldNames {
args = append(args, v)
}
return result, r.db().Select(&result, "select * FROM crm_content_column where content_id=? order by "+order, args...)
return result, r.db().Select(&result, "select * FROM crm_record_column where record_id=? order by "+order, args...)
}
@@ -14,27 +14,27 @@ import (
)
type (
contentReportBuilder struct {
recordReportBuilder struct {
jsonField string
moduleID uint64
params *types.ContentReport
params *types.RecordReport
}
)
var (
contentReportExprMatch = regexp.MustCompile(`^\s*(\w+)\((.+)\)\s*$`)
recordReportExprMatch = regexp.MustCompile(`^\s*(\w+)\((.+)\)\s*$`)
)
func NewContentReportBuilder(moduleID uint64, params *types.ContentReport) *contentReportBuilder {
return &contentReportBuilder{
func NewRecordReportBuilder(moduleID uint64, params *types.RecordReport) *recordReportBuilder {
return &recordReportBuilder{
moduleID: moduleID,
params: params,
jsonField: `JSON_UNQUOTE(JSON_EXTRACT(json, REPLACE(JSON_UNQUOTE(JSON_SEARCH(json, 'one', ?)), '.name', '.value')))`,
}
}
func (b contentReportBuilder) field(name string) squirrel.Sqlizer {
func (b recordReportBuilder) field(name string) squirrel.Sqlizer {
switch name {
case "created_at", "updated_at":
return squirrel.Expr(name)
@@ -43,7 +43,7 @@ func (b contentReportBuilder) field(name string) squirrel.Sqlizer {
}
}
func (b contentReportBuilder) alias(col squirrel.Sqlizer, alias, fallback string) (squirrel.Sqlizer, string) {
func (b recordReportBuilder) alias(col squirrel.Sqlizer, alias, fallback string) (squirrel.Sqlizer, string) {
if alias != "" {
return squirrel.Alias(col, alias), alias
}
@@ -51,7 +51,7 @@ func (b contentReportBuilder) alias(col squirrel.Sqlizer, alias, fallback string
return squirrel.Alias(col, fallback), fallback
}
func (b contentReportBuilder) wrapInModifiers(col squirrel.Sqlizer, mm ...string) squirrel.Sqlizer {
func (b recordReportBuilder) wrapInModifiers(col squirrel.Sqlizer, mm ...string) squirrel.Sqlizer {
for _, m := range mm {
switch strings.ToUpper(m) {
case "WEEKDAY":
@@ -72,8 +72,8 @@ func (b contentReportBuilder) wrapInModifiers(col squirrel.Sqlizer, mm ...string
return col
}
func (b contentReportBuilder) parseExpression(exp string) squirrel.Sqlizer {
res := contentReportExprMatch.FindStringSubmatch(exp)
func (b recordReportBuilder) parseExpression(exp string) squirrel.Sqlizer {
res := recordReportExprMatch.FindStringSubmatch(exp)
if len(res) > 0 {
aggrFuncName := strings.ToUpper(res[1])
aggrFuncArgs := b.parseExpression(res[2])
@@ -92,15 +92,15 @@ func (b contentReportBuilder) parseExpression(exp string) squirrel.Sqlizer {
return nil
}
func (b *contentReportBuilder) Build() (sql string, args []interface{}, err error) {
func (b *recordReportBuilder) Build() (sql string, args []interface{}, err error) {
report := squirrel.
Select().
Column(squirrel.Alias(squirrel.Expr("COUNT(*)"), "count")).
From("crm_content").
From("crm_record").
Where("module_id = ?", b.moduleID)
if b.params == nil {
return "", nil, errors.New("Can not generate report without parameters")
return "", nil, errors.New("can not generate report without parameters")
}
for i, m := range b.params.Metrics {
@@ -129,7 +129,7 @@ func (b *contentReportBuilder) Build() (sql string, args []interface{}, err erro
return report.ToSql()
}
func (b contentReportBuilder) Cast(row sqlx.ColScanner) map[string]interface{} {
func (b recordReportBuilder) Cast(row sqlx.ColScanner) map[string]interface{} {
out := map[string]interface{}{}
sqlx.MapScan(row, out)
for k, v := range out {
@@ -4,8 +4,8 @@ import (
"testing"
)
func TestContentReportBuilder_parseExpression(t *testing.T) {
b := contentReportBuilder{jsonField: "JSONFIELD"}
func TestRecordReportBuilder_parseExpression(t *testing.T) {
b := recordReportBuilder{jsonField: "JSONFIELD"}
tc := []struct {
exp string
+41 -41
View File
@@ -32,27 +32,27 @@ type ModuleAPI interface {
Read(context.Context, *request.ModuleRead) (interface{}, error)
Edit(context.Context, *request.ModuleEdit) (interface{}, error)
Delete(context.Context, *request.ModuleDelete) (interface{}, error)
ContentReport(context.Context, *request.ModuleContentReport) (interface{}, error)
ContentList(context.Context, *request.ModuleContentList) (interface{}, error)
ContentCreate(context.Context, *request.ModuleContentCreate) (interface{}, error)
ContentRead(context.Context, *request.ModuleContentRead) (interface{}, error)
ContentEdit(context.Context, *request.ModuleContentEdit) (interface{}, error)
ContentDelete(context.Context, *request.ModuleContentDelete) (interface{}, error)
RecordReport(context.Context, *request.ModuleRecordReport) (interface{}, error)
RecordList(context.Context, *request.ModuleRecordList) (interface{}, error)
RecordCreate(context.Context, *request.ModuleRecordCreate) (interface{}, error)
RecordRead(context.Context, *request.ModuleRecordRead) (interface{}, error)
RecordEdit(context.Context, *request.ModuleRecordEdit) (interface{}, error)
RecordDelete(context.Context, *request.ModuleRecordDelete) (interface{}, error)
}
// HTTP API interface
type Module struct {
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Edit func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
ContentReport func(http.ResponseWriter, *http.Request)
ContentList func(http.ResponseWriter, *http.Request)
ContentCreate func(http.ResponseWriter, *http.Request)
ContentRead func(http.ResponseWriter, *http.Request)
ContentEdit func(http.ResponseWriter, *http.Request)
ContentDelete func(http.ResponseWriter, *http.Request)
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Edit func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
RecordReport func(http.ResponseWriter, *http.Request)
RecordList func(http.ResponseWriter, *http.Request)
RecordCreate func(http.ResponseWriter, *http.Request)
RecordRead func(http.ResponseWriter, *http.Request)
RecordEdit func(http.ResponseWriter, *http.Request)
RecordDelete func(http.ResponseWriter, *http.Request)
}
func NewModule(mh ModuleAPI) *Module {
@@ -92,46 +92,46 @@ func NewModule(mh ModuleAPI) *Module {
return mh.Delete(r.Context(), params)
})
},
ContentReport: func(w http.ResponseWriter, r *http.Request) {
RecordReport: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleContentReport()
params := request.NewModuleRecordReport()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.ContentReport(r.Context(), params)
return mh.RecordReport(r.Context(), params)
})
},
ContentList: func(w http.ResponseWriter, r *http.Request) {
RecordList: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleContentList()
params := request.NewModuleRecordList()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.ContentList(r.Context(), params)
return mh.RecordList(r.Context(), params)
})
},
ContentCreate: func(w http.ResponseWriter, r *http.Request) {
RecordCreate: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleContentCreate()
params := request.NewModuleRecordCreate()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.ContentCreate(r.Context(), params)
return mh.RecordCreate(r.Context(), params)
})
},
ContentRead: func(w http.ResponseWriter, r *http.Request) {
RecordRead: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleContentRead()
params := request.NewModuleRecordRead()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.ContentRead(r.Context(), params)
return mh.RecordRead(r.Context(), params)
})
},
ContentEdit: func(w http.ResponseWriter, r *http.Request) {
RecordEdit: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleContentEdit()
params := request.NewModuleRecordEdit()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.ContentEdit(r.Context(), params)
return mh.RecordEdit(r.Context(), params)
})
},
ContentDelete: func(w http.ResponseWriter, r *http.Request) {
RecordDelete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewModuleContentDelete()
params := request.NewModuleRecordDelete()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.ContentDelete(r.Context(), params)
return mh.RecordDelete(r.Context(), params)
})
},
}
@@ -146,12 +146,12 @@ func (mh *Module) MountRoutes(r chi.Router, middlewares ...func(http.Handler) ht
r.Get("/{moduleID}", mh.Read)
r.Post("/{moduleID}", mh.Edit)
r.Delete("/{moduleID}", mh.Delete)
r.Get("/{moduleID}/report", mh.ContentReport)
r.Get("/{moduleID}/content", mh.ContentList)
r.Post("/{moduleID}/content", mh.ContentCreate)
r.Get("/{moduleID}/content/{contentID}", mh.ContentRead)
r.Post("/{moduleID}/content/{contentID}", mh.ContentEdit)
r.Delete("/{moduleID}/content/{contentID}", mh.ContentDelete)
r.Get("/{moduleID}/report", mh.RecordReport)
r.Get("/{moduleID}/record", mh.RecordList)
r.Post("/{moduleID}/record", mh.RecordCreate)
r.Get("/{moduleID}/record/{recordID}", mh.RecordRead)
r.Post("/{moduleID}/record/{recordID}", mh.RecordEdit)
r.Delete("/{moduleID}/record/{recordID}", mh.RecordDelete)
})
})
}
+14 -14
View File
@@ -14,14 +14,14 @@ import (
type (
Module struct {
module service.ModuleService
content service.ContentService
content service.RecordService
}
)
func (Module) New() *Module {
return &Module{
module: service.DefaultModule,
content: service.DefaultContent,
content: service.DefaultRecord,
}
}
@@ -54,8 +54,8 @@ func (s *Module) Edit(ctx context.Context, r *request.ModuleEdit) (interface{},
return s.module.With(ctx).Update(item)
}
func (s *Module) ContentReport(ctx context.Context, r *request.ModuleContentReport) (interface{}, error) {
reportParams := &types.ContentReport{}
func (s *Module) RecordReport(ctx context.Context, r *request.ModuleRecordReport) (interface{}, error) {
reportParams := &types.RecordReport{}
if strings.TrimSpace(r.Metrics) != "" {
reportParams.ScanMetrics(strings.Split(r.Metrics, ",")...)
@@ -68,31 +68,31 @@ func (s *Module) ContentReport(ctx context.Context, r *request.ModuleContentRepo
return s.content.With(ctx).Report(r.ModuleID, reportParams)
}
func (s *Module) ContentList(ctx context.Context, r *request.ModuleContentList) (interface{}, error) {
func (s *Module) RecordList(ctx context.Context, r *request.ModuleRecordList) (interface{}, error) {
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) {
return s.content.With(ctx).FindByID(r.ContentID)
func (s *Module) RecordRead(ctx context.Context, r *request.ModuleRecordRead) (interface{}, error) {
return s.content.With(ctx).FindByID(r.RecordID)
}
func (s *Module) ContentCreate(ctx context.Context, r *request.ModuleContentCreate) (interface{}, error) {
item := &types.Content{
func (s *Module) RecordCreate(ctx context.Context, r *request.ModuleRecordCreate) (interface{}, error) {
item := &types.Record{
ModuleID: r.ModuleID,
Fields: r.Fields,
}
return s.content.With(ctx).Create(item)
}
func (s *Module) ContentEdit(ctx context.Context, r *request.ModuleContentEdit) (interface{}, error) {
item := &types.Content{
ID: r.ContentID,
func (s *Module) RecordEdit(ctx context.Context, r *request.ModuleRecordEdit) (interface{}, error) {
item := &types.Record{
ID: r.RecordID,
ModuleID: r.ModuleID,
Fields: r.Fields,
}
return s.content.With(ctx).Update(item)
}
func (s *Module) ContentDelete(ctx context.Context, r *request.ModuleContentDelete) (interface{}, error) {
return resputil.OK(), s.content.With(ctx).DeleteByID(r.ContentID)
func (s *Module) RecordDelete(ctx context.Context, r *request.ModuleRecordDelete) (interface{}, error) {
return resputil.OK(), s.content.With(ctx).DeleteByID(r.RecordID)
}
+46 -46
View File
@@ -260,18 +260,18 @@ func (m *ModuleDelete) Fill(r *http.Request) (err error) {
var _ RequestFiller = NewModuleDelete()
// Module content/report request parameters
type ModuleContentReport struct {
// Module record/report request parameters
type ModuleRecordReport struct {
Metrics string
Dimensions string
ModuleID uint64 `json:",string"`
}
func NewModuleContentReport() *ModuleContentReport {
return &ModuleContentReport{}
func NewModuleRecordReport() *ModuleRecordReport {
return &ModuleRecordReport{}
}
func (m *ModuleContentReport) Fill(r *http.Request) (err error) {
func (m *ModuleRecordReport) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(m)
@@ -311,10 +311,10 @@ func (m *ModuleContentReport) Fill(r *http.Request) (err error) {
return err
}
var _ RequestFiller = NewModuleContentReport()
var _ RequestFiller = NewModuleRecordReport()
// Module content/list request parameters
type ModuleContentList struct {
// Module record/list request parameters
type ModuleRecordList struct {
Query string
Page int
PerPage int
@@ -322,11 +322,11 @@ type ModuleContentList struct {
ModuleID uint64 `json:",string"`
}
func NewModuleContentList() *ModuleContentList {
return &ModuleContentList{}
func NewModuleRecordList() *ModuleRecordList {
return &ModuleRecordList{}
}
func (m *ModuleContentList) Fill(r *http.Request) (err error) {
func (m *ModuleRecordList) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(m)
@@ -374,19 +374,19 @@ func (m *ModuleContentList) Fill(r *http.Request) (err error) {
return err
}
var _ RequestFiller = NewModuleContentList()
var _ RequestFiller = NewModuleRecordList()
// Module content/create request parameters
type ModuleContentCreate struct {
// Module record/create request parameters
type ModuleRecordCreate struct {
ModuleID uint64 `json:",string"`
Fields sqlxTypes.JSONText
}
func NewModuleContentCreate() *ModuleContentCreate {
return &ModuleContentCreate{}
func NewModuleRecordCreate() *ModuleRecordCreate {
return &ModuleRecordCreate{}
}
func (m *ModuleContentCreate) Fill(r *http.Request) (err error) {
func (m *ModuleRecordCreate) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(m)
@@ -424,19 +424,19 @@ func (m *ModuleContentCreate) Fill(r *http.Request) (err error) {
return err
}
var _ RequestFiller = NewModuleContentCreate()
var _ RequestFiller = NewModuleRecordCreate()
// Module content/read request parameters
type ModuleContentRead struct {
ModuleID uint64 `json:",string"`
ContentID uint64 `json:",string"`
// Module record/read request parameters
type ModuleRecordRead struct {
ModuleID uint64 `json:",string"`
RecordID uint64 `json:",string"`
}
func NewModuleContentRead() *ModuleContentRead {
return &ModuleContentRead{}
func NewModuleRecordRead() *ModuleRecordRead {
return &ModuleRecordRead{}
}
func (m *ModuleContentRead) Fill(r *http.Request) (err error) {
func (m *ModuleRecordRead) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(m)
@@ -464,25 +464,25 @@ func (m *ModuleContentRead) Fill(r *http.Request) (err error) {
}
m.ModuleID = parseUInt64(chi.URLParam(r, "moduleID"))
m.ContentID = parseUInt64(chi.URLParam(r, "contentID"))
m.RecordID = parseUInt64(chi.URLParam(r, "recordID"))
return err
}
var _ RequestFiller = NewModuleContentRead()
var _ RequestFiller = NewModuleRecordRead()
// Module content/edit request parameters
type ModuleContentEdit struct {
ModuleID uint64 `json:",string"`
ContentID uint64 `json:",string"`
Fields sqlxTypes.JSONText
// Module record/edit request parameters
type ModuleRecordEdit struct {
ModuleID uint64 `json:",string"`
RecordID uint64 `json:",string"`
Fields sqlxTypes.JSONText
}
func NewModuleContentEdit() *ModuleContentEdit {
return &ModuleContentEdit{}
func NewModuleRecordEdit() *ModuleRecordEdit {
return &ModuleRecordEdit{}
}
func (m *ModuleContentEdit) Fill(r *http.Request) (err error) {
func (m *ModuleRecordEdit) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(m)
@@ -510,7 +510,7 @@ func (m *ModuleContentEdit) Fill(r *http.Request) (err error) {
}
m.ModuleID = parseUInt64(chi.URLParam(r, "moduleID"))
m.ContentID = parseUInt64(chi.URLParam(r, "contentID"))
m.RecordID = parseUInt64(chi.URLParam(r, "recordID"))
if val, ok := post["fields"]; ok {
if m.Fields, err = parseJSONTextWithErr(val); err != nil {
@@ -521,19 +521,19 @@ func (m *ModuleContentEdit) Fill(r *http.Request) (err error) {
return err
}
var _ RequestFiller = NewModuleContentEdit()
var _ RequestFiller = NewModuleRecordEdit()
// Module content/delete request parameters
type ModuleContentDelete struct {
ModuleID uint64 `json:",string"`
ContentID uint64 `json:",string"`
// Module record/delete request parameters
type ModuleRecordDelete struct {
ModuleID uint64 `json:",string"`
RecordID uint64 `json:",string"`
}
func NewModuleContentDelete() *ModuleContentDelete {
return &ModuleContentDelete{}
func NewModuleRecordDelete() *ModuleRecordDelete {
return &ModuleRecordDelete{}
}
func (m *ModuleContentDelete) Fill(r *http.Request) (err error) {
func (m *ModuleRecordDelete) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(m)
@@ -561,9 +561,9 @@ func (m *ModuleContentDelete) Fill(r *http.Request) (err error) {
}
m.ModuleID = parseUInt64(chi.URLParam(r, "moduleID"))
m.ContentID = parseUInt64(chi.URLParam(r, "contentID"))
m.RecordID = parseUInt64(chi.URLParam(r, "recordID"))
return err
}
var _ RequestFiller = NewModuleContentDelete()
var _ RequestFiller = NewModuleRecordDelete()
-119
View File
@@ -1,119 +0,0 @@
package service
import (
"context"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"github.com/crusttech/crust/crm/repository"
"github.com/crusttech/crust/crm/types"
systemService "github.com/crusttech/crust/system/service"
)
type (
content struct {
db *factory.DB
ctx context.Context
repository repository.ContentRepository
pageRepo repository.PageRepository
userSvc systemService.UserService
}
ContentService interface {
With(ctx context.Context) ContentService
FindByID(contentID uint64) (*types.Content, error)
Report(moduleID uint64, params *types.ContentReport) (interface{}, 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)
DeleteByID(contentID uint64) error
Fields(mod *types.Content) ([]*types.ContentColumn, error)
}
)
func Content() ContentService {
return (&content{
userSvc: systemService.DefaultUser,
}).With(context.Background())
}
func (s *content) With(ctx context.Context) ContentService {
db := repository.DB(ctx)
return &content{
db: db,
ctx: ctx,
repository: repository.Content(ctx, db),
pageRepo: repository.Page(ctx, db),
userSvc: s.userSvc.With(ctx),
}
}
func (s *content) FindByID(id uint64) (*types.Content, error) {
response, err := s.repository.FindByID(id)
if err != nil {
return nil, err
}
return response, s.preload(response, "page", "user", "fields")
}
func (s *content) Report(moduleID uint64, params *types.ContentReport) (interface{}, error) {
return s.repository.Report(moduleID, params)
}
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
}
if err := s.preloadAll(response.Contents, "user", "fields"); err != nil {
return nil, err
}
return response, nil
}
func (s *content) Create(mod *types.Content) (*types.Content, error) {
response, err := s.repository.Create(mod)
if err != nil {
return nil, err
}
return response, s.preload(response, "user", "fields")
}
func (s *content) Update(content *types.Content) (c *types.Content, err error) {
validate := func() error {
if content.ID == 0 {
return errors.New("Error updating content: invalid ID")
} else if c, err = s.repository.FindByID(content.ID); err != nil {
return errors.Wrap(err, "Error while loading content for update")
} else {
content.CreatedAt = c.CreatedAt
}
return nil
}
if err = validate(); err != nil {
return nil, err
}
return c, s.db.Transaction(func() (err error) {
c, err = s.repository.Update(content)
return
})
}
func (s *content) Fields(mod *types.Content) ([]*types.ContentColumn, error) {
return s.repository.Fields(mod)
}
func (s *content) DeleteByID(id uint64) error {
return s.repository.DeleteByID(id)
}
-46
View File
@@ -1,46 +0,0 @@
package service
import (
"encoding/json"
"github.com/crusttech/crust/crm/types"
)
func (r *content) preloadAll(contents []*types.Content, fields ...string) error {
for _, content := range contents {
if err := r.preload(content, fields...); err != nil {
return err
}
}
return nil
}
func (r *content) preload(content *types.Content, fields ...string) (err error) {
for _, field := range fields {
switch field {
case "fields":
fields, err := r.Fields(content)
if err != nil {
return err
}
json, err := json.Marshal(fields)
if err != nil {
return err
}
if err := (&content.Fields).Scan(json); err != nil {
return err
}
case "page":
if content.Page, err = r.pageRepo.FindByModuleID(content.ModuleID); err != nil {
return
}
case "user":
if content.UserID > 0 {
if content.User, err = r.userSvc.FindByID(content.UserID); err != nil {
return
}
}
}
}
return
}
+1 -1
View File
@@ -50,7 +50,7 @@ func TestMain(m *testing.M) {
// clean up tables
{
for _, name := range []string{"crm_module", "crm_module_form", "crm_content", "crm_content_column", "crm_page", "sys_user"} {
for _, name := range []string{"crm_module", "crm_module_form", "crm_record", "crm_record_column", "crm_page", "sys_user"} {
_, err := db.Exec("truncate " + name)
if err != nil {
panic("Error when clearing " + name + ": " + err.Error())
+1 -1
View File
@@ -38,7 +38,7 @@ func TestModule(t *testing.T) {
{
{
m, err := repository.Update(module)
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")
}
+119
View File
@@ -0,0 +1,119 @@
package service
import (
"context"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"github.com/crusttech/crust/crm/repository"
"github.com/crusttech/crust/crm/types"
systemService "github.com/crusttech/crust/system/service"
)
type (
record struct {
db *factory.DB
ctx context.Context
repository repository.RecordRepository
pageRepo repository.PageRepository
userSvc systemService.UserService
}
RecordService interface {
With(ctx context.Context) RecordService
FindByID(recordID uint64) (*types.Record, error)
Report(moduleID uint64, params *types.RecordReport) (interface{}, error)
Find(moduleID uint64, query string, page int, perPage int, sort string) (*repository.FindResponse, error)
Create(record *types.Record) (*types.Record, error)
Update(record *types.Record) (*types.Record, error)
DeleteByID(recordID uint64) error
Fields(mod *types.Record) ([]*types.RecordColumn, error)
}
)
func Record() RecordService {
return (&record{
userSvc: systemService.DefaultUser,
}).With(context.Background())
}
func (s *record) With(ctx context.Context) RecordService {
db := repository.DB(ctx)
return &record{
db: db,
ctx: ctx,
repository: repository.Record(ctx, db),
pageRepo: repository.Page(ctx, db),
userSvc: s.userSvc.With(ctx),
}
}
func (s *record) FindByID(id uint64) (*types.Record, error) {
response, err := s.repository.FindByID(id)
if err != nil {
return nil, err
}
return response, s.preload(response, "page", "user", "fields")
}
func (s *record) Report(moduleID uint64, params *types.RecordReport) (interface{}, error) {
return s.repository.Report(moduleID, params)
}
func (s *record) 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
}
if err := s.preloadAll(response.Records, "user", "fields"); err != nil {
return nil, err
}
return response, nil
}
func (s *record) Create(mod *types.Record) (*types.Record, error) {
response, err := s.repository.Create(mod)
if err != nil {
return nil, err
}
return response, s.preload(response, "user", "fields")
}
func (s *record) Update(record *types.Record) (c *types.Record, err error) {
validate := func() error {
if record.ID == 0 {
return errors.New("Error updating record: invalid ID")
} else if c, err = s.repository.FindByID(record.ID); err != nil {
return errors.Wrap(err, "Error while loading record for update")
} else {
record.CreatedAt = c.CreatedAt
}
return nil
}
if err = validate(); err != nil {
return nil, err
}
return c, s.db.Transaction(func() (err error) {
c, err = s.repository.Update(record)
return
})
}
func (s *record) Fields(mod *types.Record) ([]*types.RecordColumn, error) {
return s.repository.Fields(mod)
}
func (s *record) DeleteByID(id uint64) error {
return s.repository.DeleteByID(id)
}
@@ -14,7 +14,7 @@ import (
systemTypes "github.com/crusttech/crust/system/types"
)
func TestContent(t *testing.T) {
func TestRecord(t *testing.T) {
user := &systemTypes.User{
ID: 1337,
Username: "TestUser",
@@ -31,7 +31,7 @@ func TestContent(t *testing.T) {
}
ctx := auth.SetIdentityToContext(context.Background(), auth.NewIdentity(user.Identity()))
repository := Content().With(ctx)
repository := Record().With(ctx)
module := &types.Module{
Name: "Test",
@@ -62,92 +62,92 @@ func TestContent(t *testing.T) {
assert(t, module.ID > 0, "Expected auto generated ID")
}
columns := []types.ContentColumn{
types.ContentColumn{
columns := []types.RecordColumn{
types.RecordColumn{
Name: "name",
Value: "Tit Petric",
},
types.ContentColumn{
types.RecordColumn{
Name: "email",
Value: "tit.petric@example.com",
},
types.ContentColumn{
types.RecordColumn{
Name: "options",
Related: []string{"1", "2", "3"},
},
types.ContentColumn{
types.RecordColumn{
Name: "description",
Value: "jack of all trades",
},
}
content1 := &types.Content{
record1 := &types.Record{
ModuleID: module.ID,
}
(&content1.Fields).Scan(func() []byte {
(&record1.Fields).Scan(func() []byte {
b, _ := json.Marshal(columns)
return b
}())
columns2 := []types.ContentColumn{
types.ContentColumn{
columns2 := []types.RecordColumn{
types.RecordColumn{
Name: "name",
Value: "Marko Novak",
},
types.ContentColumn{
types.RecordColumn{
Name: "email",
Value: "marko.n@example.com",
},
types.ContentColumn{
types.RecordColumn{
Name: "options",
Related: []string{"1", "2", "3"},
},
types.ContentColumn{
types.RecordColumn{
Name: "description",
Value: "persona non grata",
},
}
content2 := &types.Content{
record2 := &types.Record{
ModuleID: module.ID,
}
(&content2.Fields).Scan(func() []byte {
(&record2.Fields).Scan(func() []byte {
b, _ := json.Marshal(columns2)
return b
}())
// now work with content
// now work with records
{
{
m, err := repository.Update(content1)
m, err := repository.Update(record1)
assert(t, m == nil, "Expected empty return for invalid update, got %#v", m)
assert(t, err != nil, "Expected error when updating invalid content")
assert(t, err != nil, "Expected error when updating invalid record")
}
// create content
m1, err := repository.Create(content1)
assert(t, err == nil, "Error when creating content: %+v", err)
// create record
m1, err := repository.Create(record1)
assert(t, err == nil, "Error when creating record: %+v", err)
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 != nil, "Expected non-nil user when creating record")
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)
// create record
m2, err := repository.Create(record2)
assert(t, err == nil, "Error when creating record: %+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 != nil, "Expected non-nil user when creating record")
assert(t, m2.User.Username == "TestUser", "Expected 'TestUser' as username, got '%s'", m2.User.Username)
// fetch created content
// fetch created record
{
ms, err := repository.FindByID(m1.ID)
assert(t, err == nil, "Error when retrieving content by id: %+v", err)
assert(t, err == nil, "Error when retrieving record by id: %+v", err)
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)
// fields := make([]testContentRow, 0)
// fields := make([]testRecordRow, 0)
// err = json.Unmarshal(ms.Fields, &fields)
assert(t, err == nil, "%+v", errors.Wrap(err, "Didn't expect error when unmarshalling"))
assert(t, len(fields) == len(columns), "Expected different field count: %d != %d", 2, len(fields))
@@ -156,7 +156,7 @@ func TestContent(t *testing.T) {
}
}
{
fields := make([]types.ContentColumn, 0)
fields := make([]types.RecordColumn, 0)
err := json.Unmarshal(ms.Fields, &fields)
assert(t, err == nil, "%+v", errors.Wrap(err, "Didn't expect error when unmarshalling"))
assert(t, len(fields) == len(columns), "Expected different field count: %d != %d", 2, len(fields))
@@ -166,58 +166,58 @@ func TestContent(t *testing.T) {
}
}
// update created content
// update created record
{
_, err := repository.Update(m1)
assert(t, err == nil, "Error when updating content, %+v", err)
assert(t, err == nil, "Error when updating record, %+v", err)
}
// re-fetch content
// re-fetch record
{
ms, err := repository.FindByID(m1.ID)
assert(t, err == nil, "Error when retrieving content by id: %+v", err)
assert(t, err == nil, "Error when retrieving record by id: %+v", err)
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
// fetch all records
{
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) == 2, "Expected two content, got %d", len(mr.Contents))
assert(t, err == nil, "Error when retrieving records: %+v", err)
assert(t, len(mr.Records) == 2, "Expected two record, got %d", len(mr.Records))
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")
assert(t, mr.Records[0].ModuleID == m1.ModuleID, "Expected record module to match, %d != %d", m1.ModuleID, mr.Records[0].ModuleID)
assert(t, mr.Records[0].ID > mr.Records[1].ID, "Expected order to be descending")
}
// fetch all contents
// fetch all records
{
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, err == nil, "Error when retrieving records: %+v", err)
assert(t, len(mr.Records) == 2, "Expected two record, got %d", len(mr.Records))
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")
assert(t, mr.Records[0].ModuleID == m1.ModuleID, "Expected record module to match, %d != %d", m1.ModuleID, mr.Records[0].ModuleID)
assert(t, mr.Records[0].ID > mr.Records[1].ID, "Expected order to be ascending")
}
// fetch all contents
// fetch all records
{
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, err == nil, "Error when retrieving records: %+v", err)
assert(t, len(mr.Records) == 2, "Expected two record, got %d", len(mr.Records))
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")
assert(t, mr.Records[0].ModuleID == m1.ModuleID, "Expected record module to match, %d != %d", m1.ModuleID, mr.Records[0].ModuleID)
assert(t, mr.Records[0].ID > mr.Records[1].ID, "Expected order to be ascending")
}
// fetch all contents by query
// fetch all records by query
{
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, err == nil, "Error when retrieving records: %+v", err)
assert(t, len(mr.Records) == 1, "Expected one record, got %d", len(mr.Records))
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)
@@ -225,27 +225,27 @@ func TestContent(t *testing.T) {
assert(t, mr.Meta.Sort == "id desc", "Expected Meta.Sort == id desc, got '%s'", mr.Meta.Sort)
}
// fetch all contents by query
// fetch all records by query
{
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))
assert(t, err == nil, "Error when retrieving records: %+v", err)
assert(t, len(mr.Records) == 0, "Expected no records, got %d", len(mr.Records))
}
// delete content
// delete record
{
err := repository.DeleteByID(m1.ID)
assert(t, err == nil, "Error when retrieving content by id: %+v", err)
assert(t, err == nil, "Error when retrieving record by id: %+v", err)
err = repository.DeleteByID(m2.ID)
assert(t, err == nil, "Error when retrieving content by id: %+v", err)
assert(t, err == nil, "Error when retrieving record by id: %+v", err)
}
// fetch all contents
// fetch all records
{
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))
assert(t, err == nil, "Error when retrieving records: %+v", err)
assert(t, len(mr.Records) == 0, "Expected no record, got %d", len(mr.Records))
}
}
}
+46
View File
@@ -0,0 +1,46 @@
package service
import (
"encoding/json"
"github.com/crusttech/crust/crm/types"
)
func (r *record) preloadAll(records []*types.Record, fields ...string) error {
for _, record := range records {
if err := r.preload(record, fields...); err != nil {
return err
}
}
return nil
}
func (r *record) preload(record *types.Record, fields ...string) (err error) {
for _, field := range fields {
switch field {
case "fields":
fields, err := r.Fields(record)
if err != nil {
return err
}
json, err := json.Marshal(fields)
if err != nil {
return err
}
if err := (&record.Fields).Scan(json); err != nil {
return err
}
case "page":
if record.Page, err = r.pageRepo.FindByModuleID(record.ModuleID); err != nil {
return
}
case "user":
if record.UserID > 0 {
if record.User, err = r.userSvc.FindByID(record.UserID); err != nil {
return
}
}
}
}
return
}
+2 -2
View File
@@ -6,7 +6,7 @@ import (
var (
o sync.Once
DefaultContent ContentService
DefaultRecord RecordService
DefaultField FieldService
DefaultModule ModuleService
DefaultChart ChartService
@@ -17,7 +17,7 @@ var (
func Init() {
o.Do(func() {
DefaultContent = Content()
DefaultRecord = Record()
DefaultField = Field()
DefaultModule = Module()
DefaultPage = Page()
-66
View File
@@ -1,66 +0,0 @@
package types
import (
"regexp"
"strings"
)
type (
ContentReport struct {
Metrics []ContentReportMetric
Dimensions []ContentReportDimensions
}
ContentReportMetric struct {
Alias string
Expression string
}
ContentReportDimensions struct {
Alias string
Field string
Modifiers []string
}
)
var (
contentReportMetricScanRE *regexp.Regexp
contentReportDimensionScanRE *regexp.Regexp
)
func init() {
contentReportMetricScanRE = regexp.MustCompile("^(?:(\\w+):)?(.+)$")
contentReportDimensionScanRE = regexp.MustCompile("^(?:(\\w+):)?(\\w+)((?:\\|?\\w+)+)?$")
}
func (r *ContentReport) ScanMetrics(metrics ...string) {
r.Metrics = make([]ContentReportMetric, len(metrics))
for i := 0; i < len(metrics); i++ {
r.Metrics[i].Scan(metrics[i])
}
}
func (r *ContentReport) ScanDimensions(dimensions ...string) {
r.Dimensions = make([]ContentReportDimensions, len(dimensions))
for i := 0; i < len(dimensions); i++ {
r.Dimensions[i].Scan(dimensions[i])
}
}
func (m *ContentReportMetric) Scan(metric string) {
if match := contentReportMetricScanRE.FindStringSubmatch(metric); len(match) == 3 {
m.Alias = match[1]
m.Expression = match[2]
}
}
func (d *ContentReportDimensions) Scan(dimension string) {
if match := contentReportDimensionScanRE.FindStringSubmatch(dimension); len(match) == 4 {
d.Alias = match[1]
d.Field = match[2]
if len(match[3]) > 0 {
d.Modifiers = strings.Split(match[3][1:], "|")
}
}
}
+66
View File
@@ -0,0 +1,66 @@
package types
import (
"regexp"
"strings"
)
type (
RecordReport struct {
Metrics []RecordReportMetric
Dimensions []RecordReportDimensions
}
RecordReportMetric struct {
Alias string
Expression string
}
RecordReportDimensions struct {
Alias string
Field string
Modifiers []string
}
)
var (
recordReportMetricScanRE *regexp.Regexp
recordReportDimensionScanRE *regexp.Regexp
)
func init() {
recordReportMetricScanRE = regexp.MustCompile("^(?:(\\w+):)?(.+)$")
recordReportDimensionScanRE = regexp.MustCompile("^(?:(\\w+):)?(\\w+)((?:\\|?\\w+)+)?$")
}
func (r *RecordReport) ScanMetrics(metrics ...string) {
r.Metrics = make([]RecordReportMetric, len(metrics))
for i := 0; i < len(metrics); i++ {
r.Metrics[i].Scan(metrics[i])
}
}
func (r *RecordReport) ScanDimensions(dimensions ...string) {
r.Dimensions = make([]RecordReportDimensions, len(dimensions))
for i := 0; i < len(dimensions); i++ {
r.Dimensions[i].Scan(dimensions[i])
}
}
func (m *RecordReportMetric) Scan(metric string) {
if match := recordReportMetricScanRE.FindStringSubmatch(metric); len(match) == 3 {
m.Alias = match[1]
m.Expression = match[2]
}
}
func (d *RecordReportDimensions) Scan(dimension string) {
if match := recordReportDimensionScanRE.FindStringSubmatch(dimension); len(match) == 4 {
d.Alias = match[1]
d.Field = match[2]
if len(match[3]) > 0 {
d.Modifiers = strings.Split(match[3][1:], "|")
}
}
}
@@ -4,8 +4,8 @@ import (
"testing"
)
func TestContentReport(t *testing.T) {
r := &ContentReport{}
func TestRecordReport(t *testing.T) {
r := &RecordReport{}
r.ScanMetrics("alias:exp")
if len(r.Metrics) == 0 {
t.Log("No metrics scanned")
+13 -13
View File
@@ -12,9 +12,9 @@ import (
)
type (
// Content is a stored row in the `content` table
Content struct {
ID uint64 `json:"contentID,string" db:"id"`
// Record is a stored row in the `record` table
Record struct {
ID uint64 `json:"recordID,string" db:"id"`
ModuleID uint64 `json:"moduleID,string" db:"module_id"`
User *systemTypes.User `json:"user,omitempty" db:"-"`
@@ -29,20 +29,20 @@ type (
DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"`
}
// ContentColumn is a stored row in the `content_column` table
ContentColumn struct {
ContentID uint64 `json:"-" db:"content_id"`
Name string `json:"name" db:"column_name"`
Value string `json:"value" db:"column_value"`
Related []string `json:"related" db:"-"`
// RecordColumn is a stored row in the `record_column` table
RecordColumn struct {
RecordID uint64 `json:"-" db:"record_id"`
Name string `json:"name" db:"column_name"`
Value string `json:"value" db:"column_value"`
Related []string `json:"related" db:"-"`
}
Related struct {
ContentID uint64 `json:"-" db:"content_id"`
Name string `json:"-" db:"column_name"`
RecordID uint64 `json:"-" db:"record_id"`
Name string `json:"-" db:"column_name"`
// RelatedContentID isn't necessarily a content ID (multiple-select anything goes options)
RelatedContentID string `json:"-" db:"rel_content_id"`
// RelatedRecordID isn't necessarily a record ID (multiple-select anything goes options)
RelatedRecordID string `json:"-" db:"rel_record_id"`
}
// Field - CRM input field definitions
+15 -15
View File
@@ -314,13 +314,13 @@ CRM module definitions
| dimensions | string | GET | Dimensions (syntax: alias:field|modifier|modifier2;...) | N/A | YES |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## List/read contents from module section
## List/read records from module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/content` | HTTP/S | GET | |
| `/module/{moduleID}/record` | HTTP/S | GET | |
#### Request parameters
@@ -332,66 +332,66 @@ CRM module definitions
| 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
## Create record in module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/content` | HTTP/S | POST | |
| `/module/{moduleID}/record` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
| fields | sqlxTypes.JSONText | POST | Content JSON | N/A | YES |
| fields | sqlxTypes.JSONText | POST | Record JSON | N/A | YES |
## Read contents by ID from module section
## Read records by ID from module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/content/{contentID}` | HTTP/S | GET | |
| `/module/{moduleID}/record/{recordID}` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
| contentID | uint64 | PATH | Content ID | N/A | YES |
| recordID | uint64 | PATH | Record ID | N/A | YES |
## Add/update contents in module section
## Add/update records in module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/content/{contentID}` | HTTP/S | POST | |
| `/module/{moduleID}/record/{recordID}` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
| contentID | uint64 | PATH | Content ID | N/A | YES |
| fields | sqlxTypes.JSONText | POST | Content JSON | N/A | YES |
| recordID | uint64 | PATH | Record ID | N/A | YES |
| fields | sqlxTypes.JSONText | POST | Record JSON | N/A | YES |
## Delete content row from module section
## Delete record row from module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/content/{contentID}` | HTTP/S | DELETE | |
| `/module/{moduleID}/record/{recordID}` | HTTP/S | DELETE | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
| contentID | uint64 | PATH | Content ID | N/A | YES |
| recordID | uint64 | PATH | Record ID | N/A | YES |