3
0

Cleanup & enhance compose module & fields

- Add module field ID
 - Rename db table (compose_module_form => compose_module_field)
 - Add id, created_at, updated_at, deleted_at db columns
 - Rename json to options, module_id to rel_module
 - Fix primary keys (now just ID), add unique indexes (mod+place, mod+name)
 - Add foreign key from fields to modules
 - module repo Update() func no longer does REPLACE but UPDATE
 - in updateFields(), fields are removed more precisely (only missing fields are removed)
 - Add integration tests for module/field updates
This commit is contained in:
Denis Arh 2019-05-14 11:39:32 +02:00
parent 88d759ad19
commit 7fc66e74ad
8 changed files with 195 additions and 24 deletions

View File

@ -22,15 +22,15 @@ function types {
CGO_ENABLED=0 go build -o ./build/gen-type-set codegen/v2/type-set.go
fi
./build/gen-type-set --types Namespace --output compose/types/namespace.gen.go
./build/gen-type-set --types Attachment --output compose/types/attachment.gen.go
./build/gen-type-set --types Module --output compose/types/module.gen.go
./build/gen-type-set --types Page --output compose/types/page.gen.go
./build/gen-type-set --types Chart --output compose/types/chart.gen.go
./build/gen-type-set --types Trigger --output compose/types/trigger.gen.go
./build/gen-type-set --types Record --output compose/types/record.gen.go
./build/gen-type-set --types Namespace --output compose/types/namespace.gen.go
./build/gen-type-set --types Attachment --output compose/types/attachment.gen.go
./build/gen-type-set --types Module --output compose/types/module.gen.go
./build/gen-type-set --types Page --output compose/types/page.gen.go
./build/gen-type-set --types Chart --output compose/types/chart.gen.go
./build/gen-type-set --types Trigger --output compose/types/trigger.gen.go
./build/gen-type-set --types Record --output compose/types/record.gen.go
./build/gen-type-set --types ModuleField --output compose/types/module_field.gen.go
./build/gen-type-set --with-primary-key=false --types ModuleField --output compose/types/module_field.gen.go
./build/gen-type-set --with-primary-key=false --types RecordValue --output compose/types/record_value.gen.go
./build/gen-type-set --types MessageAttachment --output messaging/types/attachment.gen.go

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,31 @@
ALTER TABLE compose_module_form
RENAME TO compose_module_field;
-- Remove orphaned and invalid fields
DELETE FROM `compose_module_field` WHERE `module_id` NOT IN (SELECT `id` FROM `compose_module`) OR `name` = '';
-- Order and consistency.
ALTER TABLE `compose_module_field`
ADD COLUMN `id` BIGINT UNSIGNED NOT NULL FIRST,
ADD COLUMN `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN `updated_at` DATETIME DEFAULT NULL,
ADD COLUMN `deleted_at` DATETIME DEFAULT NULL,
RENAME COLUMN `module_id` TO `rel_module`,
RENAME COLUMN `json` TO `options`;
-- Generate IDs for the new field, use module, offset by one (just to start with a different ID)
-- and use place (0 based, +1 for every field, expecting to be unique per module because of the existing pkey)
UPDATE `compose_module_field` SET id = rel_module + 1 + place;
-- Drop old primary key (module_id, place)
ALTER TABLE `compose_module_field` DROP PRIMARY KEY, ADD PRIMARY KEY(`id`);
-- Foreign key
ALTER TABLE `compose_module_field`
ADD CONSTRAINT `compose_module`
FOREIGN KEY (`rel_module`)
REFERENCES `compose_module` (`id`);
-- And unique indexes for module+place/name combos.
CREATE UNIQUE INDEX uid_compose_module_field_place ON compose_module_field (`rel_module`, `place`);
CREATE UNIQUE INDEX uid_compose_module_field_name ON compose_module_field (`rel_module`, `name`);

View File

@ -2,6 +2,7 @@ package repository
import (
"context"
"fmt"
"time"
"github.com/jmoiron/sqlx"
@ -47,6 +48,10 @@ func (r module) table() string {
return "compose_module"
}
func (r module) tableFields() string {
return "compose_module_field"
}
func (r module) columns() []string {
return []string{
"id", "rel_namespace", "name", "json",
@ -104,14 +109,20 @@ func (r module) Find(filter types.ModuleFilter) (set types.ModuleSet, f types.Mo
}
func (r module) Create(mod *types.Module) (*types.Module, error) {
var err error
mod.ID = factory.Sonyflake.NextID()
mod.CreatedAt = time.Now()
if err := r.updateFields(mod.ID, mod.Fields); err != nil {
if err = r.db().Insert(r.table(), mod); err != nil {
return nil, err
}
return mod, r.db().Insert(r.table(), mod)
if err = r.updateFields(mod.ID, mod.Fields); err != nil {
return nil, err
}
return mod, nil
}
func (r module) Update(mod *types.Module) (*types.Module, error) {
@ -122,29 +133,62 @@ func (r module) Update(mod *types.Module) (*types.Module, error) {
return nil, err
}
return mod, r.db().Replace(r.table(), mod)
return mod, r.db().Update(r.table(), mod, "id")
}
func (r module) updateFields(moduleID uint64, ff types.ModuleFieldSet) error {
// @todo be more selective when deleting
if _, err := r.db().Exec("DELETE FROM compose_module_form WHERE module_id = ?", moduleID); err != nil {
return errors.Wrap(err, "Error updating module fields")
if existing, err := r.FindFields(moduleID); err != nil {
return err
} else {
// Remove fields that do not exist anymore
err = existing.Walk(func(e *types.ModuleField) error {
if ff.FindByID(e.ID) == nil {
return r.deleteFieldByID(moduleID, e.ID)
}
return nil
})
if err != nil {
return err
}
}
for idx, v := range ff {
v.ModuleID = moduleID
v.Place = idx
if err := r.db().Replace("compose_module_form", v); err != nil {
now := time.Now()
for idx, f := range ff {
if f.ID == 0 {
f.ID = factory.Sonyflake.NextID()
f.CreatedAt = now
} else {
f.UpdatedAt = &now
}
f.ModuleID = moduleID
f.Place = idx
if err := r.db().Replace(r.tableFields(), f); err != nil {
return errors.Wrap(err, "Error updating module fields")
}
}
return nil
}
func (r module) deleteFieldByID(moduleID, fieldID uint64) error {
_, err := r.db().Exec(
fmt.Sprintf("DELETE FROM %s WHERE rel_module = ? AND id = ?", r.tableFields()),
moduleID,
fieldID,
)
return err
}
func (r module) DeleteByID(namespaceID, moduleID uint64) error {
_, err := r.db().Exec(
"UPDATE "+r.table()+" SET deleted_at = NOW() WHERE rel_namespace = ? AND id = ?",
fmt.Sprintf("UPDATE %s SET deleted_at = NOW() WHERE rel_namespace = ? AND id = ?", r.table()),
namespaceID,
moduleID,
)
@ -157,7 +201,17 @@ func (r module) FindFields(moduleIDs ...uint64) (ff types.ModuleFieldSet, err er
return
}
if sql, args, err := sqlx.In("SELECT * FROM compose_module_form WHERE module_id IN (?) ORDER BY module_id AND place", moduleIDs); err != nil {
query := `SELECT id, rel_module, place,
kind, name, label, options,
is_private, is_required, is_visible, is_multi
FROM %s
WHERE rel_module IN (?)
AND deleted_at IS NULL
ORDER BY rel_module, place`
query = fmt.Sprintf(query, r.tableFields())
if sql, args, err := sqlx.In(query, moduleIDs); err != nil {
return nil, err
} else {
return ff, r.db().Select(&ff, sql, args...)

View File

@ -0,0 +1,55 @@
// +build unit integration
package repository
import (
"context"
"testing"
"github.com/crusttech/crust/compose/types"
"github.com/crusttech/crust/internal/test"
"github.com/titpetric/factory"
)
func TestModule_updateFields(t *testing.T) {
tx(t, func(ctx context.Context, db *factory.DB, ns *types.Namespace) (err error) {
var (
m *types.Module
repo = Module(ctx, db)
)
m, err = repo.Create(&types.Module{
NamespaceID: ns.ID,
Name: "test-module",
})
test.NoError(t, err, "unexpected error on module creation")
test.Assert(t, len(m.Fields) == 0, "unexpected fields found in the fresh module")
m, err = repo.Create(&types.Module{
NamespaceID: ns.ID,
Name: "test-module",
Fields: types.ModuleFieldSet{
&types.ModuleField{Name: "one"},
&types.ModuleField{Name: "two"},
},
})
test.NoError(t, err, "unexpected error on module creation")
test.Assert(t, len(m.Fields) == 2, "expecting to find two fields in the new module")
m.Fields[0].Name = "one-v2"
m.Fields[1] = &types.ModuleField{Name: "three"}
m, err = repo.Update(m)
test.NoError(t, err, "unexpected error on module update")
test.Assert(t, len(m.Fields) == 2, "expecting to find two fields in the new module")
test.Assert(t, m.Fields[0].Name == "one-v2", "expecting to find field 'one'")
test.Assert(t, m.Fields[0].Place == 0, "expecting Place=0")
test.Assert(t, m.Fields[1].Name == "three", "expecting to find field 'three'")
test.Assert(t, m.Fields[1].Place == 1, "expecting Place=1")
return
})
}

View File

@ -13,7 +13,6 @@ type (
Name string `json:"name" db:"name"`
Meta types.JSONText `json:"meta" db:"json"`
Fields ModuleFieldSet `json:"fields" db:"-"`
Page *Page `json:"page,omitempty"`
NamespaceID uint64 `json:"namespaceID,string" db:"rel_namespace"`

View File

@ -39,3 +39,29 @@ func (set ModuleFieldSet) Filter(f func(*ModuleField) (bool, error)) (out Module
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set ModuleFieldSet) FindByID(ID uint64) *ModuleField {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set ModuleFieldSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}

View File

@ -3,6 +3,7 @@ package types
import (
"database/sql/driver"
"encoding/json"
"time"
"github.com/jmoiron/sqlx/types"
)
@ -10,19 +11,24 @@ import (
type (
// Modules - CRM module definitions
ModuleField struct {
ModuleID uint64 `json:"moduleID,string" db:"module_id"`
ID uint64 `json:"fieldID,string" db:"id"`
ModuleID uint64 `json:"moduleID,string" db:"rel_module"`
Place int `json:"-" db:"place"`
Kind string `json:"kind" db:"kind"`
Name string `json:"name" db:"name"`
Label string `json:"label" db:"label"`
Options types.JSONText `json:"options" db:"json"`
Options types.JSONText `json:"options" db:"options"`
Private bool `json:"isPrivate" db:"is_private"`
Required bool `json:"isRequired" db:"is_required"`
Visible bool `json:"isVisible" db:"is_visible"`
Multi bool `json:"isMulti" db:"is_multi"`
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"`
}
)