update tests for CRM with new database schema

This commit is contained in:
Tit Petric
2018-10-08 21:09:53 +00:00
parent 8b8684d3e5
commit 47c47dcfda
15 changed files with 306 additions and 146 deletions
+77 -12
View File
@@ -2,19 +2,28 @@ package repository
import (
"context"
"github.com/crusttech/crust/crm/types"
"strings"
"time"
"encoding/json"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"github.com/crusttech/crust/crm/types"
)
type (
Content interface {
With(ctx context.Context) Content
ContentRepository interface {
With(ctx context.Context, db *factory.DB) ContentRepository
FindByID(id uint64) (*types.Content, error)
Find() ([]*types.Content, error)
Create(mod *types.Content) (*types.Content, error)
Update(mod *types.Content) (*types.Content, error)
DeleteByID(id uint64) error
Fields(mod *types.Content) ([]*types.ContentColumn, error)
}
content struct {
@@ -22,13 +31,13 @@ type (
}
)
func NewContent(ctx context.Context) Content {
return (&content{}).With(ctx)
func Content(ctx context.Context, db *factory.DB) ContentRepository {
return (&content{}).With(ctx, db)
}
func (r *content) With(ctx context.Context) Content {
func (r *content) With(ctx context.Context, db *factory.DB) ContentRepository {
return &content{
repository: r.repository.With(ctx),
repository: r.repository.With(ctx, db),
}
}
@@ -36,24 +45,80 @@ func (r *content) With(ctx context.Context) Content {
func (r *content) FindByID(id uint64) (*types.Content, error) {
mod := &types.Content{}
return mod, r.db().Get(mod, "SELECT * FROM crm_module_content WHERE id=?", id)
return mod, r.db().Get(mod, "SELECT * FROM crm_content WHERE id=? and deleted_at IS NULL", id)
}
func (r *content) Find() ([]*types.Content, error) {
mod := make([]*types.Content, 0)
return mod, r.db().Select(&mod, "SELECT * FROM crm_module_content ORDER BY id DESC")
return mod, r.db().Select(&mod, "SELECT * FROM crm_content WHERE deleted_at IS NULL ORDER BY id DESC")
}
func (r *content) Create(mod *types.Content) (*types.Content, error) {
mod.ID = factory.Sonyflake.NextID()
return mod, r.db().Insert("crm_module_content", mod)
mod.CreatedAt = time.Now()
fields := make([]types.ContentColumn, 0)
if err := json.Unmarshal(mod.Fields, &fields); err != nil {
return nil, errors.Wrap(err, "No content")
}
for _, v := range fields {
v.ContentID = mod.ID
if err := r.db().Replace("crm_content_column", v); err != nil {
return nil, errors.Wrap(err, "Error adding columns")
}
}
return mod, r.db().Insert("crm_content", mod)
}
func (r *content) Update(mod *types.Content) (*types.Content, error) {
return mod, r.db().Replace("crm_module_content", mod)
now := time.Now()
mod.UpdatedAt = &now
fields := make([]types.ContentColumn, 0)
if err := json.Unmarshal(mod.Fields, &fields); err != nil {
return nil, errors.Wrap(err, "Error when saving content, no content")
}
for _, v := range fields {
v.ContentID = mod.ID
if err := r.db().Replace("crm_content_column", v); err != nil {
return nil, errors.Wrap(err, "Error adding columns to database")
}
}
return mod, r.db().Replace("crm_content", mod)
}
func (r *content) DeleteByID(id uint64) error {
_, err := r.db().Exec("DELETE FROM crm_module_content WHERE id=?", id)
_, err := r.db().Exec("update crm_content 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)
module := Module(r.ctx, r.db())
mod, err := module.FindByID(content.ModuleID)
if err != nil {
return result, err
}
fieldNames, err := module.FieldNames(mod)
if err != nil {
return result, err
}
if len(fieldNames) == 0 {
return result, errors.New("Module has no fields")
}
order := "FIELD(column_name" + strings.Repeat(",?", len(fieldNames)) + ")"
args := []interface{}{
content.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...)
}
+20 -23
View File
@@ -3,25 +3,15 @@ package repository
import (
"context"
"encoding/json"
"github.com/crusttech/crust/crm/types"
"testing"
"github.com/pkg/errors"
"github.com/crusttech/crust/crm/types"
)
type testContentRow struct {
Name string `db:"name"`
Value string `db:"value"`
}
func TestContent(t *testing.T) {
repository := NewContent(context.TODO()).With(context.Background())
// clean up tables
{
for _, name := range []string{"crm_module", "crm_module_content"} {
_, err := db().Exec("truncate " + name)
assert(t, err == nil, "Error when clearing "+name+": %+v", err)
}
}
repository := Content(context.TODO(), nil).With(context.Background(), nil)
fields, err := json.Marshal([]types.Field{
types.Field{
@@ -42,7 +32,7 @@ func TestContent(t *testing.T) {
// set up a module
{
_, err := NewModule(context.TODO()).With(context.Background()).Create(module)
_, err := Module(context.TODO(), nil).With(context.Background(), nil).Create(module)
assert(t, err == nil, "Error when creating module: %+v", err)
assert(t, module.ID > 0, "Expected auto generated ID")
}
@@ -51,9 +41,15 @@ func TestContent(t *testing.T) {
ModuleID: module.ID,
}
(&content.Fields).Scan(func() []byte {
b, _ := json.Marshal([]testContentRow{
testContentRow{"name", "Tit Petric"},
testContentRow{"email", "tit.petric@example.com"},
b, _ := json.Marshal([]types.ContentColumn{
types.ContentColumn{
Name: "name",
Value: "Tit Petric",
},
types.ContentColumn{
Name: "email",
Value: "tit.petric@example.com",
},
})
return b
}())
@@ -72,9 +68,10 @@ func TestContent(t *testing.T) {
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)
fields := make([]testContentRow, 0)
err = json.Unmarshal(ms.Fields, &fields)
assert(t, err == nil, "Didn't expect error when unmarshalling: %+v", err)
fields, err := repository.Fields(ms)
// fields := make([]testContentRow, 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) == 2, "Expected different field count: %d != %d", 2, len(fields))
assert(t, fields[0].Name == "name", "Expected field.0 type = name, got %s", fields[0].Name)
assert(t, fields[1].Name == "email", "Expected field.1 type = email, got %s", fields[1].Name)
@@ -112,7 +109,7 @@ func TestContent(t *testing.T) {
{
ms, err := repository.Find()
assert(t, err == nil, "Error when retrieving contents: %+v", err)
assert(t, len(ms) == 0, "Expected one content, got %d", len(ms))
assert(t, len(ms) == 0, "Expected no content, got %d", len(ms))
}
}
+11 -8
View File
@@ -2,12 +2,15 @@ package repository
import (
"context"
"github.com/titpetric/factory"
"github.com/crusttech/crust/crm/types"
)
type (
Field interface {
With(ctx context.Context) Field
FieldRepository interface {
With(ctx context.Context, db *factory.DB) FieldRepository
FindByType(t string) (*types.Field, error)
Find() ([]*types.Field, error)
@@ -18,24 +21,24 @@ type (
}
)
func NewField(ctx context.Context) Field {
return (&field{}).With(ctx)
func Field(ctx context.Context, db *factory.DB) FieldRepository {
return (&field{}).With(ctx, db)
}
func (r *field) With(ctx context.Context) Field {
func (r *field) With(ctx context.Context, db *factory.DB) FieldRepository {
return &field{
repository: r.repository.With(ctx),
repository: r.repository.With(ctx, db),
}
}
// FindByName returns field with a given name
func (f *field) FindByType(t string) (*types.Field, error) {
res := &types.Field{}
return res, f.db().Get(res, "SELECT * from crm_fields where field_type=?", t)
return res, f.db().Get(res, "SELECT * from crm_field where field_type=?", t)
}
// Find returns all known fields
func (f *field) Find() ([]*types.Field, error) {
mod := make([]*types.Field, 0)
return mod, f.db().Select(&mod, "SELECT * FROM crm_fields ORDER BY field_name ASC")
return mod, f.db().Select(&mod, "SELECT * FROM crm_field ORDER BY field_name ASC")
}
+1 -2
View File
@@ -6,8 +6,7 @@ import (
)
func TestField(t *testing.T) {
repository := NewField(context.TODO()).With(context.Background())
repository := Field(context.TODO(), nil).With(context.Background(), nil)
{
// fetch all fields
+10
View File
@@ -31,6 +31,16 @@ func TestMain(m *testing.M) {
db := factory.Database.MustGet()
db.Profiler = &factory.Database.ProfilerStdout
// clean up tables
{
for _, name := range []string{"crm_module", "crm_content", "crm_content_column"} {
_, err := db.Exec("truncate " + name)
if err != nil {
panic("Error when clearing "+name+": "+err.Error())
}
}
}
os.Exit(m.Run())
}
+60 -7
View File
@@ -2,19 +2,27 @@ package repository
import (
"context"
"github.com/crusttech/crust/crm/types"
"encoding/json"
"time"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"github.com/crusttech/crust/crm/types"
)
type (
Module interface {
With(ctx context.Context) Module
ModuleRepository interface {
With(ctx context.Context, db *factory.DB) ModuleRepository
FindByID(id uint64) (*types.Module, error)
Find() ([]*types.Module, error)
Create(mod *types.Module) (*types.Module, error)
Update(mod *types.Module) (*types.Module, error)
DeleteByID(id uint64) error
Fields(mod *types.Module) ([]*types.ModuleField, error)
FieldNames(mod *types.Module) ([]string, error)
}
module struct {
@@ -22,13 +30,13 @@ type (
}
)
func NewModule(ctx context.Context) Module {
return (&module{}).With(ctx)
func Module(ctx context.Context, db *factory.DB) ModuleRepository {
return (&module{}).With(ctx, db)
}
func (r *module) With(ctx context.Context) Module {
func (r *module) With(ctx context.Context, db *factory.DB) ModuleRepository {
return &module{
repository: r.repository.With(ctx),
repository: r.repository.With(ctx, db),
}
}
@@ -46,10 +54,37 @@ func (r *module) Find() ([]*types.Module, error) {
func (r *module) Create(mod *types.Module) (*types.Module, error) {
mod.ID = factory.Sonyflake.NextID()
mod.CreatedAt = time.Now()
fields := make([]types.ModuleField, 0)
if err := json.Unmarshal(mod.Fields, &fields); err != nil {
return nil, errors.Wrap(err, "No fields")
}
for _, v := range fields {
v.ModuleID = mod.ID
if err := r.db().Replace("crm_module_form", v); err != nil {
return nil, errors.Wrap(err, "Error adding module fields")
}
}
return mod, r.db().Insert("crm_module", mod)
}
func (r *module) Update(mod *types.Module) (*types.Module, error) {
now := time.Now()
mod.UpdatedAt = &now
fields := make([]types.ModuleField, 0)
if err := json.Unmarshal(mod.Fields, &fields); err != nil {
return nil, errors.Wrap(err, "No fields")
}
for _, v := range fields {
v.ModuleID = mod.ID
if err := r.db().Replace("crm_module_form", v); err != nil {
return nil, errors.Wrap(err, "Error adding module fields")
}
}
return mod, r.db().Replace("crm_module", mod)
}
@@ -57,3 +92,21 @@ func (r *module) DeleteByID(id uint64) error {
_, err := r.db().Exec("DELETE FROM crm_module WHERE id=?", id)
return err
}
func (r *module) Fields(mod *types.Module) ([]*types.ModuleField, error) {
fields := make([]*types.ModuleField, 0)
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
func (r *module) FieldNames(mod *types.Module) ([]string, error) {
if fields, err := r.Fields(mod); err != nil {
return []string{}, err
} else {
result := make([]string, len(fields))
for k, v := range fields {
result[k] = v.Name
}
return result, nil
}
}
+7 -11
View File
@@ -7,19 +7,15 @@ import (
)
func TestModule(t *testing.T) {
repository := NewModule(context.TODO()).With(context.Background())
// clean up tables
{
_, err := db().Exec("truncate crm_module")
assert(t, err == nil, "Error when clearing crm_module: %+v", err)
}
repository := Module(context.TODO(), nil).With(context.Background(), nil)
// the module object we're working with
module := &types.Module{
Name: "Test",
}
(&module.Fields).Scan([]byte("[]"))
prevModuleCount := 0
{
// create module
@@ -54,8 +50,8 @@ func TestModule(t *testing.T) {
{
ms, err := repository.Find()
assert(t, err == nil, "Error when retrieving modules: %+v", err)
assert(t, len(ms) == 1, "Expected one module, got %d", len(ms))
assert(t, ms[0].Name == m.Name, "Expected module name to match, %s != %s", m.Name, ms[0].Name)
assert(t, len(ms) >= 1, "Expected at least one module, got %d", len(ms))
prevModuleCount = len(ms)
}
// re-fetch module
@@ -68,7 +64,7 @@ func TestModule(t *testing.T) {
{
ms, err := repository.Find()
assert(t, err == nil, "Error when retrieving modules: %+v", err)
assert(t, len(ms) == 0, "Expected no modules, got %d", len(ms))
assert(t, len(ms) < prevModuleCount, "Expected modules count to decrease after deletion, %d < %d", len(ms), prevModuleCount)
}
}
+23 -24
View File
@@ -2,45 +2,44 @@ package repository
import (
"context"
"github.com/titpetric/factory"
"github.com/crusttech/crust/internal/auth"
)
type (
repository struct {
ctx context.Context
// Get database handle
dbh func(ctxs ...context.Context) *factory.DB
dbh *factory.DB
}
)
var _db *factory.DB
// DB returns a repository-wide singleton DB handle
func DB(ctxs ...context.Context) *factory.DB {
if _db == nil {
_db = factory.Database.MustGet()
}
for _, ctx := range ctxs {
_db = _db.With(ctx)
break
}
return _db
// DB produces a contextual DB handle
func DB(ctx context.Context) *factory.DB {
return factory.Database.MustGet().With(ctx)
}
// With updates repository and database contexts
func (r *repository) With(ctx context.Context) *repository {
res := &repository{
func Identity(ctx context.Context) uint64 {
return auth.GetIdentityFromContext(ctx).Identity()
}
func (r *repository) With(ctx context.Context, db *factory.DB) *repository {
return &repository{
ctx: ctx,
dbh: DB,
dbh: db,
}
if r != nil {
res.dbh = r.dbh
}
return res
}
// Context returns current active repository context
func (r *repository) Context() context.Context {
return r.ctx
}
// db returns context-aware db handle
func (r *repository) db() *factory.DB {
return r.dbh(r.ctx)
if r.dbh != nil {
return r.dbh
}
return DB(r.ctx)
}
+11
View File
@@ -0,0 +1,11 @@
package repository
import (
"context"
"testing"
)
func TestEvents(t *testing.T) {
repo := &repository{}
repo.With(context.Background(), nil)
}
+11 -5
View File
@@ -2,13 +2,18 @@ package service
import (
"context"
"github.com/titpetric/factory"
"github.com/crusttech/crust/crm/repository"
"github.com/crusttech/crust/crm/types"
)
type (
content struct {
repository repository.Content
db *factory.DB
ctx context.Context
repository repository.ContentRepository
}
ContentService interface {
@@ -24,14 +29,15 @@ type (
)
func Content() ContentService {
return &content{
repository: repository.NewContent(context.Background()),
}
return (&content{}).With(context.Background())
}
func (s *content) With(ctx context.Context) ContentService {
db := repository.DB(ctx)
return &content{
repository: s.repository.With(ctx),
db: db,
ctx: ctx,
repository: s.repository.With(ctx, db),
}
}
+8 -5
View File
@@ -8,7 +8,9 @@ import (
type (
field struct {
repository repository.Field
db *factory.DB
ctx context.Context
repository repository.FieldRepository
}
FieldService interface {
@@ -19,14 +21,15 @@ type (
)
func Field() FieldService {
return &field{
repository: repository.NewField(context.Background()),
}
return (&field{}).With(context.Background())
}
func (s *field) With(ctx context.Context) FieldService {
db := repository.DB(ctx)
return &field{
repository: s.repository.With(ctx),
db: db,
ctx: ctx,
repository: s.repository.With(ctx, db),
}
}
+7 -5
View File
@@ -8,7 +8,9 @@ import (
type (
module struct {
repository repository.Module
db *factory.DB
ctx context.Context
repository repository.ModuleRepository
}
ModuleService interface {
@@ -24,14 +26,14 @@ type (
)
func Module() ModuleService {
return &module{
repository: repository.NewModule(context.Background()),
}
return (&module{}).With(context.Background())
}
func (s *module) With(ctx context.Context) ModuleService {
return &module{
repository: s.repository.With(ctx),
db: db,
ctx: ctx,
repository: s.repository.With(ctx, db),
}
}
-12
View File
@@ -1,12 +0,0 @@
package types
/* If you want to edit this file by hand, remove codegen/[project]/types/index.php */
type (
// Fields - CRM input field definitions
Field struct {
Name string `json:"field_name" db:"field_name"`
Type string `json:"field_type" db:"field_type"`
Template string `json:"field_template,omitempty" db:"field_template"`
}
)
-32
View File
@@ -1,32 +0,0 @@
package types
/* If you want to edit this file by hand, remove codegen/[project]/types/index.php */
import (
"github.com/jmoiron/sqlx/types"
)
type (
// Modules - CRM module definitions
Module struct {
ID uint64 `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Fields types.JSONText `json:"fields" db:"json"`
}
// Modules - CRM module definitions
ModuleField struct {
Name string `json:"name" db:"name"`
Title string `json:"title" db:"title"`
Kind string `json:"kind" db:"kind"`
GDPR bool `json:"gdpr" db:"gdpr"`
Show bool `json:"show" db:"show"`
}
// Modules - CRM module definitions
Content struct {
ID uint64 `json:"id" db:"id"`
ModuleID uint64 `json:"module_id" db:"module_id"`
Fields types.JSONText `json:"json" db:"json"`
}
)
+60
View File
@@ -0,0 +1,60 @@
package types
import (
"time"
"github.com/jmoiron/sqlx/types"
)
type (
// Content is a stored row in the `content` table
Content struct {
ID uint64 `json:"id" db:"id"`
ModuleID uint64 `json:"moduleID" db:"module_id"`
Fields types.JSONText `json:"fields,omitempty" db:"-"`
CreatedAt time.Time `db:"created_at" json:"createdAt,omitempty"`
UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"`
DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"`
}
// ContentColumn is a stored row in the `content_column` table
ContentColumn struct {
ContentID uint64 `json:"contentID" db:"content_id"`
Name string `json:"name" db:"column_name"`
Value string `json:"value" db:"column_value"`
}
// Field - CRM input field definitions
Field struct {
Name string `json:"name" db:"field_name"`
Type string `json:"type" db:"field_type"`
Template string `json:"template,omitempty" db:"field_template"`
}
// Modules - CRM module definitions
Module struct {
ID uint64 `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Fields types.JSONText `json:"fields" db:"json"`
CreatedAt time.Time `db:"created_at" json:"createdAt,omitempty"`
UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"`
DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"`
}
// Modules - CRM module definitions
ModuleField struct {
ModuleID uint64 `json:"moduleID" db:"module_id"`
Place int `json:"-" db:"place"`
Kind string `json:"kind" db:"kind"`
Name string `json:"name" db:"name"`
Label string `json:"label" db:"label"`
HelpText string `json:"helpText,omitempty" db:"help_text"`
Default string `json:"defaultValue,omitempty" db:"default_value"`
MaxLength int `json:"maxLength" db:"max_length"`
Private bool `json:"isPrivate" db:"is_private"`
}
)