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:
parent
88d759ad19
commit
7fc66e74ad
16
codegen.sh
16
codegen.sh
@ -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
31
compose/db/schema/mysql/20190514090000.module_fields.up.sql
Normal file
31
compose/db/schema/mysql/20190514090000.module_fields.up.sql
Normal 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`);
|
||||
@ -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...)
|
||||
|
||||
55
compose/internal/repository/module_test.go
Normal file
55
compose/internal/repository/module_test.go
Normal 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
|
||||
})
|
||||
}
|
||||
@ -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"`
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user