3
0
Files
corteza/compose/service/module.go
2022-07-28 12:58:40 +02:00

1469 lines
40 KiB
Go

package service
import (
"context"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"github.com/cortezaproject/corteza-server/pkg/dal"
"github.com/cortezaproject/corteza-server/pkg/dal/capabilities"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/compose/dalutils"
"github.com/cortezaproject/corteza-server/compose/service/event"
"github.com/cortezaproject/corteza-server/compose/service/values"
"github.com/cortezaproject/corteza-server/compose/types"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/pkg/eventbus"
"github.com/cortezaproject/corteza-server/pkg/handle"
"github.com/cortezaproject/corteza-server/pkg/label"
"github.com/cortezaproject/corteza-server/pkg/locale"
"github.com/cortezaproject/corteza-server/pkg/slice"
"github.com/cortezaproject/corteza-server/store"
systemTypes "github.com/cortezaproject/corteza-server/system/types"
)
type (
module struct {
actionlog actionlog.Recorder
ac moduleAccessController
eventbus eventDispatcher
store store.Storer
locale ResourceTranslationsManagerService
dal dalModelManager
}
moduleAccessController interface {
CanManageResourceTranslations(ctx context.Context) bool
CanSearchModulesOnNamespace(context.Context, *types.Namespace) bool
CanReadNamespace(context.Context, *types.Namespace) bool
CanCreateModuleOnNamespace(context.Context, *types.Namespace) bool
CanReadModule(context.Context, *types.Module) bool
CanUpdateModule(context.Context, *types.Module) bool
CanDeleteModule(context.Context, *types.Module) bool
}
ModuleService interface {
FindByID(ctx context.Context, namespaceID, moduleID uint64) (*types.Module, error)
FindByName(ctx context.Context, namespaceID uint64, name string) (*types.Module, error)
FindByHandle(ctx context.Context, namespaceID uint64, handle string) (*types.Module, error)
FindByAny(ctx context.Context, namespaceID uint64, identifier interface{}) (*types.Module, error)
Find(ctx context.Context, filter types.ModuleFilter) (set types.ModuleSet, f types.ModuleFilter, err error)
FindSensitive(ctx context.Context, filter types.PrivacyModuleFilter) (set []types.PrivacyModule, f types.PrivacyModuleFilter, err error)
Create(ctx context.Context, module *types.Module) (*types.Module, error)
Update(ctx context.Context, module *types.Module) (*types.Module, error)
DeleteByID(ctx context.Context, namespaceID, moduleID uint64) error
// @note probably temporary just so tests are easier
ReloadDALModels(ctx context.Context) error
}
moduleUpdateHandler func(ctx context.Context, ns *types.Namespace, c *types.Module) (moduleChanges, error)
moduleChanges uint8
// Model management on DAL Service
dalModelManager interface {
GetConnectionMeta(ctx context.Context, ID uint64) (cm dal.ConnectionMeta, err error)
Search(ctx context.Context, m dal.ModelRef, capabilities capabilities.Set, f filter.Filter) (dal.Iterator, error)
ReplaceModel(context.Context, *dal.Model) error
RemoveModel(ctx context.Context, connectionID, ID uint64) error
ReplaceModelAttribute(ctx context.Context, model *dal.Model, old, new *dal.Attribute, trans ...dal.TransformationFunction) (err error)
SearchModelIssues(connectionID, ID uint64) []error
}
dalIdentFormatter interface {
Format(ctx context.Context, template string) (out string, ok bool)
}
)
const (
moduleUnchanged moduleChanges = 0
moduleChanged moduleChanges = 1
moduleLabelsChanged moduleChanges = 2
moduleFieldsChanged moduleChanges = 4
)
const (
// https://www.rfc-editor.org/errata/eid1690
emailLength = 254
// Generally the upper most limit
urlLength = 2048
sysID = "ID"
sysNamespaceID = "namespaceID"
sysModuleID = "moduleID"
sysCreatedAt = "createdAt"
sysCreatedBy = "createdBy"
sysUpdatedAt = "updatedAt"
sysUpdatedBy = "updatedBy"
sysDeletedAt = "deletedAt"
sysDeletedBy = "deletedBy"
sysOwnedBy = "ownedBy"
colSysID = "id"
colSysNamespaceID = "rel_namespace"
colSysModuleID = "module_id"
colSysCreatedAt = "created_at"
colSysCreatedBy = "created_by"
colSysUpdatedAt = "updated_at"
colSysUpdatedBy = "updated_by"
colSysDeletedAt = "deleted_at"
colSysDeletedBy = "deleted_by"
colSysOwnedBy = "owned_by"
)
var (
systemFields = slice.ToStringBoolMap([]string{
"recordID",
"ownedBy",
"createdBy",
"createdAt",
"updatedBy",
"updatedAt",
"deletedBy",
"deletedAt",
})
)
func Module() *module {
return &module{
ac: DefaultAccessControl,
eventbus: eventbus.Service(),
actionlog: DefaultActionlog,
store: DefaultStore,
locale: DefaultResourceTranslation,
dal: dal.Service(),
}
}
func (svc module) Find(ctx context.Context, filter types.ModuleFilter) (set types.ModuleSet, f types.ModuleFilter, err error) {
var (
ns *types.Namespace
aProps = &moduleActionProps{filter: &filter}
)
// For each fetched item, store backend will check if it is valid or not
filter.Check = func(res *types.Module) (bool, error) {
if !svc.ac.CanReadModule(ctx, res) {
return false, nil
}
return true, nil
}
err = func() error {
ns, err = loadNamespace(ctx, svc.store, filter.NamespaceID)
if err != nil {
return err
}
aProps.setNamespace(ns)
if !svc.ac.CanSearchModulesOnNamespace(ctx, ns) {
return ModuleErrNotAllowedToSearch()
}
if len(filter.Labels) > 0 {
filter.LabeledIDs, err = label.Search(
ctx,
svc.store,
types.Module{}.LabelResourceKind(),
filter.Labels,
filter.ModuleID...,
)
if err != nil {
return err
}
// labels specified but no labeled resources found
if len(filter.LabeledIDs) == 0 {
return nil
}
}
if set, f, err = store.SearchComposeModules(ctx, svc.store, filter); err != nil {
return err
}
if err = loadModuleLabels(ctx, svc.store, set...); err != nil {
return err
}
err = loadModuleFields(ctx, svc.store, set...)
if err != nil {
return err
}
set.Walk(func(m *types.Module) error {
svc.proc(ctx, m)
return nil
})
return nil
}()
return set, f, svc.recordAction(ctx, aProps, ModuleActionSearch, err)
}
// FindByID tries to find module by ID
func (svc module) FindByID(ctx context.Context, namespaceID, moduleID uint64) (m *types.Module, err error) {
return svc.lookup(ctx, namespaceID, func(aProps *moduleActionProps) (*types.Module, error) {
if moduleID == 0 {
return nil, ModuleErrInvalidID()
}
aProps.module.ID = moduleID
return store.LookupComposeModuleByID(ctx, svc.store, moduleID)
})
}
// FindByName tries to find module by name
func (svc module) FindByName(ctx context.Context, namespaceID uint64, name string) (m *types.Module, err error) {
return svc.lookup(ctx, namespaceID, func(aProps *moduleActionProps) (*types.Module, error) {
aProps.module.Name = name
return store.LookupComposeModuleByNamespaceIDName(ctx, svc.store, namespaceID, name)
})
}
// FindByHandle tries to find module by handle
func (svc module) FindByHandle(ctx context.Context, namespaceID uint64, h string) (m *types.Module, err error) {
return svc.lookup(ctx, namespaceID, func(aProps *moduleActionProps) (*types.Module, error) {
if !handle.IsValid(h) {
return nil, ModuleErrInvalidHandle()
}
aProps.module.Handle = h
return store.LookupComposeModuleByNamespaceIDHandle(ctx, svc.store, namespaceID, h)
})
}
// FindByAny tries to find module in a particular namespace by id, handle or name
func (svc module) FindByAny(ctx context.Context, namespaceID uint64, identifier interface{}) (m *types.Module, err error) {
if ID, ok := identifier.(uint64); ok {
m, err = svc.FindByID(ctx, namespaceID, ID)
} else if strIdentifier, ok := identifier.(string); ok {
if ID, _ := strconv.ParseUint(strIdentifier, 10, 64); ID > 0 {
m, err = svc.FindByID(ctx, namespaceID, ID)
} else {
m, err = svc.FindByHandle(ctx, namespaceID, strIdentifier)
if err == nil && m.ID == 0 {
m, err = svc.FindByName(ctx, namespaceID, strIdentifier)
}
}
} else {
// force invalid ID error
// we do that to wrap error with lookup action context
_, err = svc.FindByID(ctx, namespaceID, 0)
}
if err != nil {
return nil, err
}
return m, nil
}
func (svc module) proc(ctx context.Context, m *types.Module) {
svc.procLocale(ctx, m)
svc.procDal(m)
}
func (svc module) procLocale(ctx context.Context, m *types.Module) {
if svc.locale == nil || svc.locale.Locale() == nil {
return
}
tag := locale.GetAcceptLanguageFromContext(ctx)
m.DecodeTranslations(svc.locale.Locale().ResourceTranslations(tag, m.ResourceTranslation()))
m.Fields.Walk(func(mf *types.ModuleField) error {
mf.DecodeTranslations(svc.locale.Locale().ResourceTranslations(tag, mf.ResourceTranslation()))
return nil
})
}
func (svc module) procDal(m *types.Module) {
if svc.dal == nil {
return
}
ii := svc.dal.SearchModelIssues(m.ModelConfig.ConnectionID, m.ID)
if len(ii) == 0 {
m.ModelConfig.Issues = nil
return
}
m.ModelConfig.Issues = make([]string, len(ii))
for i, err := range ii {
m.ModelConfig.Issues[i] = err.Error()
}
}
func (svc module) Create(ctx context.Context, new *types.Module) (*types.Module, error) {
var (
ns *types.Namespace
aProps = &moduleActionProps{changed: new}
)
err := store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
if !handle.IsValid(new.Handle) {
return ModuleErrInvalidHandle()
}
for _, f := range new.Fields {
if systemFields[f.Name] {
return ModuleErrFieldNameReserved()
}
}
if err != nil {
}
if ns, err = loadNamespace(ctx, s, new.NamespaceID); err != nil {
return err
}
aProps.setNamespace(ns)
if !svc.ac.CanCreateModuleOnNamespace(ctx, ns) {
return ModuleErrNotAllowedToCreate()
}
// Calling before-create scripts
if err = svc.eventbus.WaitFor(ctx, event.ModuleBeforeCreate(new, nil, ns)); err != nil {
return err
}
if err = svc.uniqueCheck(ctx, new); err != nil {
return err
}
new.ID = nextID()
new.CreatedAt = *now()
new.UpdatedAt = nil
new.DeletedAt = nil
if new.Fields != nil {
err = new.Fields.Walk(func(f *types.ModuleField) error {
f.ID = nextID()
f.ModuleID = new.ID
f.NamespaceID = new.NamespaceID
f.CreatedAt = *now()
f.UpdatedAt = nil
f.DeletedAt = nil
// Assure validatorID
for i, v := range f.Expressions.Validators {
v.ValidatorID = uint64(i) + 1
f.Expressions.Validators[i] = v
}
if !handle.IsValid(f.Name) {
return ModuleErrInvalidHandle()
}
return nil
})
if err != nil {
return
}
}
aProps.setModule(new)
if err = store.CreateComposeModule(ctx, s, new); err != nil {
return err
}
if err = store.CreateComposeModuleField(ctx, s, new.Fields...); err != nil {
return err
}
tt := new.EncodeTranslations()
for _, f := range new.Fields {
tt = append(tt, f.EncodeTranslations()...)
}
if err = updateTranslations(ctx, svc.ac, svc.locale, tt...); err != nil {
return
}
if err = label.Create(ctx, s, new); err != nil {
return
}
if err = DalModelReplace(ctx, svc.dal, ns, new); err != nil {
return err
}
_ = svc.eventbus.WaitFor(ctx, event.ModuleAfterCreate(new, nil, ns))
svc.procDal(new)
return nil
})
return new, svc.recordAction(ctx, aProps, ModuleActionCreate, err)
}
func (svc module) Update(ctx context.Context, upd *types.Module) (c *types.Module, err error) {
return svc.updater(ctx, upd.NamespaceID, upd.ID, ModuleActionUpdate, svc.handleUpdate(ctx, upd))
}
func (svc module) DeleteByID(ctx context.Context, namespaceID, moduleID uint64) error {
return trim1st(svc.updater(ctx, namespaceID, moduleID, ModuleActionDelete, svc.handleDelete))
}
func (svc module) UndeleteByID(ctx context.Context, namespaceID, moduleID uint64) error {
return trim1st(svc.updater(ctx, namespaceID, moduleID, ModuleActionUndelete, svc.handleUndelete))
}
// ReloadDALModels reconstructs the DAL's data model based on the store.Storer
//
// Directly using store so we don't spam the action log
func (svc *module) ReloadDALModels(ctx context.Context) (err error) {
return DalModelReload(ctx, svc.store, svc.dal)
}
// FindSensitive will list all module with at least one private module field
func (svc module) FindSensitive(ctx context.Context, filter types.PrivacyModuleFilter) (set []types.PrivacyModule, f types.PrivacyModuleFilter, err error) {
var (
mm types.ModuleSet
reqConnes = make(map[uint64]bool)
hasReqConnes = len(filter.ConnectionID) > 0
)
for _, connectionID := range filter.ConnectionID {
reqConnes[connectionID] = true
}
err = func() error {
mm, _, err = svc.Find(ctx, types.ModuleFilter{NamespaceID: filter.NamespaceID})
if err != nil {
return err
}
for _, m := range mm {
cMeta, err := svc.dal.GetConnectionMeta(ctx, m.ModelConfig.ConnectionID)
if err != nil {
return err
}
connID := cMeta.ConnectionID
if hasReqConnes && !reqConnes[connID] {
continue
}
isSensitive := false
for _, f := range m.Fields {
isSensitive = isSensitive || f.IsSensitive()
}
tag := locale.GetAcceptLanguageFromContext(ctx)
m.DecodeTranslations(svc.locale.Locale().ResourceTranslations(tag, m.ResourceTranslation()))
if isSensitive && m != nil {
pm := types.PrivacyModule{
Module: types.PrivacyModuleMeta{
ID: m.ID,
Name: m.Name,
Handle: m.Handle,
Fields: m.Fields,
},
ConnectionID: connID,
}
set = append(set, pm)
}
}
return nil
}()
return set, filter, err
}
func (svc module) updater(ctx context.Context, namespaceID, moduleID uint64, action func(...*moduleActionProps) *moduleAction, fn moduleUpdateHandler) (*types.Module, error) {
var (
changes moduleChanges
ns *types.Namespace
m, old *types.Module
aProps = &moduleActionProps{module: &types.Module{ID: moduleID, NamespaceID: namespaceID}}
err error
)
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
ns, m, err = loadModuleCombo(ctx, s, namespaceID, moduleID)
if err != nil {
return
}
if err = loadModuleLabels(ctx, svc.store, m); err != nil {
return err
}
old = m.Clone()
// so we can get issues
svc.procDal(old)
aProps.setNamespace(ns)
aProps.setChanged(m)
if m.DeletedAt == nil {
err = svc.eventbus.WaitFor(ctx, event.ModuleBeforeUpdate(m, old, ns))
} else {
err = svc.eventbus.WaitFor(ctx, event.ModuleBeforeDelete(m, old, ns))
}
if err != nil {
return
}
if changes, err = fn(ctx, ns, m); err != nil {
return err
}
if changes&moduleChanged > 0 {
if old.ModelConfig.ConnectionID != m.ModelConfig.ConnectionID {
return fmt.Errorf("unable to switch connection for existing models: run data migration")
}
if err = store.UpdateComposeModule(ctx, svc.store, m); err != nil {
return err
}
}
if changes&moduleFieldsChanged > 0 {
var (
hasRecords bool
set types.RecordSet
)
// @todo rethink how model issues and attempted module update with records should interact.
// this is a temporary solution but should be re-thinked.
modelIssues := svc.dal.SearchModelIssues(m.ModelConfig.ConnectionID, m.ID)
if len(modelIssues) == 0 {
if set, _, err = dalutils.ComposeRecordsList(ctx, svc.dal, m, types.RecordFilter{Paging: filter.Paging{Limit: 1}, Check: func(r *types.Record) (bool, error) { return true, nil }}); err != nil {
return err
}
hasRecords = len(set) > 0
} else {
hasRecords = false
}
if err = updateModuleFields(ctx, s, m, old, hasRecords); err != nil {
return err
}
}
// i18n
tt := m.EncodeTranslations()
for _, f := range m.Fields {
tt = append(tt, f.EncodeTranslations()...)
}
if err = updateTranslations(ctx, svc.ac, svc.locale, tt...); err != nil {
return
}
if changes&moduleLabelsChanged > 0 {
if err = label.Update(ctx, s, m); err != nil {
return
}
}
if m.DeletedAt == nil {
if err = svc.eventbus.WaitFor(ctx, event.ModuleAfterUpdate(m, old, ns)); err != nil {
return err
}
if err = DalModelReplace(ctx, svc.dal, ns, old, m); err != nil {
return err
}
if err = dalAttributeReplace(ctx, svc.dal, ns, old, m); err != nil {
return err
}
} else {
if err = svc.eventbus.WaitFor(ctx, event.ModuleAfterDelete(nil, old, ns)); err != nil {
return
}
if err = DalModelRemove(ctx, svc.dal, m); err != nil {
return err
}
}
svc.procDal(m)
return err
})
return m, svc.recordAction(ctx, aProps, action, err)
}
// lookup fn() orchestrates module lookup, namespace preload and check, module reading...
func (svc module) lookup(ctx context.Context, namespaceID uint64, lookup func(*moduleActionProps) (*types.Module, error)) (m *types.Module, err error) {
var aProps = &moduleActionProps{module: &types.Module{NamespaceID: namespaceID}}
err = func() error {
if ns, err := loadNamespace(ctx, svc.store, namespaceID); err != nil {
return err
} else {
aProps.setNamespace(ns)
}
if m, err = lookup(aProps); errors.IsNotFound(err) {
return ModuleErrNotFound()
} else if err != nil {
return err
}
aProps.setModule(m)
if !svc.ac.CanReadModule(ctx, m) {
return ModuleErrNotAllowedToRead()
}
if err = loadModuleLabels(ctx, svc.store, m); err != nil {
return err
}
if err = loadModuleFields(ctx, svc.store, m); err != nil {
return err
}
svc.proc(ctx, m)
return nil
}()
return m, svc.recordAction(ctx, aProps, ModuleActionLookup, err)
}
func (svc module) uniqueCheck(ctx context.Context, m *types.Module) (err error) {
if m.Handle != "" {
if e, _ := store.LookupComposeModuleByNamespaceIDHandle(ctx, svc.store, m.NamespaceID, m.Handle); e != nil && e.ID > 0 && e.ID != m.ID {
return ModuleErrHandleNotUnique()
}
}
if m.Name != "" {
if e, _ := store.LookupComposeModuleByNamespaceIDName(ctx, svc.store, m.NamespaceID, m.Name); e != nil && e.ID > 0 && e.ID != m.ID {
return ModuleErrNameNotUnique()
}
}
return nil
}
func (svc module) handleUpdate(ctx context.Context, upd *types.Module) moduleUpdateHandler {
return func(ctx context.Context, ns *types.Namespace, res *types.Module) (changes moduleChanges, err error) {
if isStale(upd.UpdatedAt, res.UpdatedAt, res.CreatedAt) {
return moduleUnchanged, ModuleErrStaleData()
}
if upd.Handle != res.Handle && !handle.IsValid(upd.Handle) {
return moduleUnchanged, ModuleErrInvalidHandle()
}
if err = svc.uniqueCheck(ctx, upd); err != nil {
return moduleUnchanged, err
}
if !svc.ac.CanUpdateModule(ctx, res) {
return moduleUnchanged, ModuleErrNotAllowedToUpdate()
}
// Get max validatorID for later use
vvID := make(map[uint64]uint64)
for _, f := range res.Fields {
for _, v := range f.Expressions.Validators {
if vvID[f.ID] < v.ValidatorID {
vvID[f.ID] = v.ValidatorID
}
}
}
if res.Name != upd.Name {
changes |= moduleChanged
res.Name = upd.Name
}
if res.Handle != upd.Handle {
changes |= moduleChanged
res.Handle = upd.Handle
}
{
oldMeta := res.Meta.String()
if oldMeta == "{}" {
oldMeta = ""
}
newMeta := upd.Meta.String()
if newMeta == "{}" {
newMeta = ""
}
if oldMeta != newMeta {
changes |= moduleChanged
res.Meta = upd.Meta
}
}
if !reflect.DeepEqual(res.ModelConfig, upd.ModelConfig) {
changes |= moduleChanged
res.ModelConfig = upd.ModelConfig
}
if !reflect.DeepEqual(res.Privacy, upd.Privacy) {
changes |= moduleChanged
res.Privacy = upd.Privacy
}
// @todo make field-change detection more optimal
if !reflect.DeepEqual(res.Fields, upd.Fields) {
changes |= moduleFieldsChanged
res.Fields = upd.Fields
}
// Assure validatorIDs
for _, f := range res.Fields {
for j, v := range f.Expressions.Validators {
if v.ValidatorID == 0 {
vvID[f.ID] += 1
v.ValidatorID = vvID[f.ID]
f.Expressions.Validators[j] = v
changes |= moduleFieldsChanged
}
}
}
if upd.Labels != nil {
if label.Changed(res.Labels, upd.Labels) {
changes |= moduleLabelsChanged
res.Labels = upd.Labels
}
}
if changes&moduleChanged > 0 {
res.UpdatedAt = now()
}
// for now, we assume that
return
}
}
func (svc module) handleDelete(ctx context.Context, ns *types.Namespace, m *types.Module) (moduleChanges, error) {
if !svc.ac.CanDeleteModule(ctx, m) {
return moduleUnchanged, ModuleErrNotAllowedToDelete()
}
if m.DeletedAt != nil {
// module already deleted
return moduleUnchanged, nil
}
m.DeletedAt = now()
return moduleChanged, nil
}
func (svc module) handleUndelete(ctx context.Context, ns *types.Namespace, m *types.Module) (moduleChanges, error) {
if !svc.ac.CanDeleteModule(ctx, m) {
return moduleUnchanged, ModuleErrNotAllowedToUndelete()
}
if m.DeletedAt == nil {
// module not deleted
return moduleUnchanged, nil
}
m.DeletedAt = nil
return moduleChanged, nil
}
// updates module fields
// expecting to receive all module fields, as it deletes the rest
// also, sort order of the fields is also important as this fn stores and updates field's place as send
func updateModuleFields(ctx context.Context, s store.Storer, new, old *types.Module, hasRecords bool) (err error) {
// Go over new to assure field integrity
for _, f := range new.Fields {
if f.ModuleID == 0 {
f.ModuleID = new.ID
}
if f.NamespaceID == 0 {
f.NamespaceID = new.NamespaceID
}
if systemFields[f.Name] && !old.Fields.HasName(f.Name) {
// make sure we're backward compatible, or better:
// if, by some weird case, someone managed to get invalid field name into
// the store, we'll turn a blind eye.
return ModuleErrFieldNameReserved()
}
// backward compatible; we didn't check for valid handle.
// if a field already existed and the handle is invalid we ignore the error.
if !handle.IsValid(f.Name) && old.Fields.FindByName(f.Name) == nil {
return ModuleErrInvalidHandle()
}
if f.ModuleID != new.ID {
return fmt.Errorf("module id of field %q does not match the module", f.Name)
}
}
// Delete any missing module fields
n := now()
ff := make(types.ModuleFieldSet, 0, len(old.Fields))
for _, of := range old.Fields {
nf := new.Fields.FindByID(of.ID)
if nf == nil {
of.DeletedAt = n
ff = append(ff, of)
} else if nf.DeletedAt != nil {
of.DeletedAt = n
ff = append(ff, of)
}
}
if len(ff) > 0 {
err = store.DeleteComposeModuleField(ctx, s, ff...)
if err != nil {
return err
}
}
// Next preproc any default values
new.Fields, err = moduleFieldDefaultPreparer(ctx, s, new, new.Fields)
if err != nil {
return err
}
// Assure; create/update remaining fields
idx := 0
ff = make(types.ModuleFieldSet, 0, len(old.Fields))
for _, f := range new.Fields {
if f.DeletedAt != nil {
continue
}
f.Place = idx
if of := old.Fields.FindByID(f.ID); of != nil {
f.CreatedAt = of.CreatedAt
// We do not have any other code in place that would handle changes of field name and kind, so we need
// to reset any changes made to the field.
// @todo remove when we are able to handle field rename & type change
if hasRecords {
f.Name = of.Name
f.Kind = of.Kind
}
f.UpdatedAt = now()
err = store.UpdateComposeModuleField(ctx, s, f)
if err != nil {
return err
}
if label.Changed(f.Labels, of.Labels) {
if err = label.Update(ctx, s, f); err != nil {
return
}
}
ff = append(ff, f)
} else {
f.ID = nextID()
f.CreatedAt = *now()
if err = store.CreateComposeModuleField(ctx, s, f); err != nil {
return err
}
if err = label.Update(ctx, s, f); err != nil {
return
}
ff = append(ff, f)
}
idx++
}
sort.Sort(ff)
new.Fields = ff
return nil
}
func moduleFieldDefaultPreparer(ctx context.Context, s store.Storer, m *types.Module, newFields types.ModuleFieldSet) (types.ModuleFieldSet, error) {
var err error
// prepare an auxiliary module to perform isolated validations on
auxm := &types.Module{
Handle: "aux_module",
NamespaceID: m.NamespaceID,
Fields: types.ModuleFieldSet{nil},
}
for _, f := range newFields {
if f.DefaultValue == nil || len(f.DefaultValue) == 0 {
continue
}
auxm.Fields[0] = f
vv := f.DefaultValue
vv.SetUpdatedFlag(true)
// Module field default values should not have a field name, so let's temporarily add it
vv.Walk(func(rv *types.RecordValue) error {
rv.Name = f.Name
return nil
})
if err = RecordValueSanitization(auxm, vv); err != nil {
return nil, err
}
vv = values.Sanitizer().Run(auxm, vv)
r := &types.Record{
Values: vv,
}
rve := defaultValidator(DefaultRecord).Run(ctx, s, auxm, r)
if !rve.IsValid() {
return nil, rve
}
vv = values.Formatter().Run(auxm, vv)
// Module field default values should not have a field name, so let's remove it
vv.Walk(func(rv *types.RecordValue) error {
rv.Name = ""
return nil
})
f.DefaultValue = vv
}
return newFields, nil
}
func loadModuleFields(ctx context.Context, s store.Storer, mm ...*types.Module) (err error) {
if len(mm) == 0 {
return nil
}
var (
ff types.ModuleFieldSet
mff = types.ModuleFieldFilter{ModuleID: types.ModuleSet(mm).IDs()}
)
if ff, _, err = store.SearchComposeModuleFields(ctx, s, mff); err != nil {
return
}
for _, m := range mm {
m.Fields = ff.FilterByModule(m.ID)
m.Fields.Walk(func(f *types.ModuleField) error {
f.NamespaceID = m.NamespaceID
return nil
})
sort.Sort(m.Fields)
}
return
}
// loads record module with fields and namespace
func loadModuleCombo(ctx context.Context, s store.Storer, namespaceID, moduleID uint64) (ns *types.Namespace, m *types.Module, err error) {
ns, err = loadNamespace(ctx, s, namespaceID)
if err != nil {
return
}
m, err = loadModule(ctx, s, namespaceID, moduleID)
return
}
func loadModule(ctx context.Context, s store.Storer, namespaceID, moduleID uint64) (res *types.Module, err error) {
if moduleID == 0 {
return nil, ModuleErrInvalidID()
}
if res, err = store.LookupComposeModuleByID(ctx, s, moduleID); errors.IsNotFound(err) {
err = ModuleErrNotFound()
}
if err == nil && namespaceID != res.NamespaceID {
// Make sure chart belongs to the right namespace
return nil, ModuleErrNotFound()
}
if err == nil {
err = loadModuleFields(ctx, s, res)
}
return
}
func loadModuleField(ctx context.Context, s store.Storer, namespaceID, moduleID, fieldID uint64) (res *types.ModuleField, err error) {
if moduleID == 0 {
return nil, ModuleErrInvalidID()
}
if res, err = store.LookupComposeModuleFieldByID(ctx, s, fieldID); errors.IsNotFound(err) {
err = ModuleErrNotFound()
}
if err == nil && (namespaceID != res.NamespaceID || moduleID != res.ModuleID) {
// Make sure chart belongs to the right namespace
return nil, ModuleErrNotFound()
}
return
}
// loadLabeledModules loads labels on one or more modules and their fields
//
func loadModuleLabels(ctx context.Context, s store.Labels, set ...*types.Module) error {
if len(set) == 0 {
return nil
}
mll := make([]label.LabeledResource, 0, len(set))
fll := make([]label.LabeledResource, 0, len(set)*10)
for i := range set {
mll = append(mll, set[i])
for j := range set[i].Fields {
fll = append(fll, set[i].Fields[j])
}
}
if err := label.Load(ctx, s, mll...); err != nil {
return err
}
if err := label.Load(ctx, s, fll...); err != nil {
return err
}
return nil
}
// DalModelReload reloads all defined compose modules into the DAL
func DalModelReload(ctx context.Context, s store.Storer, dmm dalModelManager) (err error) {
// Get all available namespaces
nn, _, err := store.SearchComposeNamespaces(ctx, s, types.NamespaceFilter{})
if err != nil {
return
}
// Get all available connections
mm, _, err := store.SearchComposeModules(ctx, s, types.ModuleFilter{})
if err != nil {
return
}
err = loadModuleFields(ctx, s, mm...)
if err != nil {
return err
}
// Reload!
for _, ns := range nn {
err = DalModelReplace(ctx, dmm, ns, modulesForNamespace(ns, mm)...)
if err != nil {
return
}
}
return
}
// modulesForNamespace returns all of the modules belonging to that namespace
// @todo implement some indexing at an earlier step for faster processing; will do for now
func modulesForNamespace(ns *types.Namespace, mm types.ModuleSet) (out types.ModuleSet) {
out = make(types.ModuleSet, 0, len(mm))
for _, m := range mm {
if m.NamespaceID == ns.ID {
out = append(out, m)
}
}
return
}
// Replaces all given connections
func DalModelReplace(ctx context.Context, dmm dalModelManager, ns *types.Namespace, modules ...*types.Module) (err error) {
var (
models dal.ModelSet
)
models, err = moduleToModel(ctx, dmm, ns, modules...)
if err != nil {
return
}
for _, m := range models {
err = dmm.ReplaceModel(ctx, m)
if err != nil {
return
}
}
return
}
func dalAttributeReplace(ctx context.Context, dmm dalModelManager, ns *types.Namespace, old, new *types.Module) (err error) {
oldModel, err := moduleToModel(ctx, dmm, ns, old)
if err != nil {
return
}
newModel, err := moduleToModel(ctx, dmm, ns, new)
if err != nil {
return
}
diff := oldModel[0].Diff(newModel[0])
for _, d := range diff {
if err = dmm.ReplaceModelAttribute(ctx, oldModel[0], d.Original, d.Asserted); err != nil {
return
}
}
return
}
// Removes a connection from DAL service
func DalModelRemove(ctx context.Context, dmm dalModelManager, mm ...*types.Module) (err error) {
for _, m := range mm {
if err = dmm.RemoveModel(ctx, m.ModelConfig.ConnectionID, m.ID); err != nil {
return err
}
}
return
}
func moduleToModel(ctx context.Context, dmm dalModelManager, ns *types.Namespace, modules ...*types.Module) (out dal.ModelSet, err error) {
var (
cm dal.ConnectionMeta
attrAux dal.AttributeSet
ok bool
)
for connectionID, modules := range modulesByConnection(modules...) {
// Get the connection meta
cm, err = dmm.GetConnectionMeta(ctx, connectionID)
if err != nil {
return
}
// Prepare the ident formatter for this connection
ff := dal.IdentFormatter(formatterNamespaceParams(ns)...)
if cm.PartitionValidator != "" {
ff, err = ff.WithValidationE(cm.PartitionValidator, nil)
if err != nil {
return
}
}
// Convert all modules to models
for _, mod := range modules {
// - base params
model := &dal.Model{
ConnectionID: connectionID,
Label: mod.Handle,
Resource: mod.RbacResource(),
ResourceID: mod.ID,
ResourceType: types.ModuleResourceType,
SensitivityLevel: mod.Privacy.SensitivityLevel,
Capabilities: mod.ModelConfig.Capabilities,
}
// - make the model ident
ident := cm.DefaultModelIdent
if mod.ModelConfig.Partitioned {
tpl := mod.ModelConfig.PartitionFormat
if tpl == "" {
tpl = cm.DefaultPartitionFormat
}
ident, ok = ff.Format(ctx, tpl, formatterModuleParams(mod)...)
if !ok {
err = fmt.Errorf("invalid model ident generated: %s", ident)
return
}
}
model.Ident = ident
// Convert user-defined fields to attributes
attrAux, err = moduleFieldsToAttributes(ctx, cm, ns, mod)
if err != nil {
return
}
model.Attributes = append(model.Attributes, attrAux...)
// Convert system fields to attribute
attrAux, err = moduleSystemFieldsToAttributes(ctx, cm, ns, mod)
if err != nil {
return
}
model.Attributes = append(model.Attributes, attrAux...)
out = append(out, model)
}
}
return
}
// moduleFieldsToAttributes converts all user-defined module fields to attributes
func moduleFieldsToAttributes(ctx context.Context, cm dal.ConnectionMeta, ns *types.Namespace, mod *types.Module) (out dal.AttributeSet, err error) {
out = make(dal.AttributeSet, 0, len(mod.Fields))
var (
attr *dal.Attribute
)
for _, f := range mod.Fields {
attr, err = moduleFieldToAttribute(ctx, cm, mod, f)
if err != nil {
return
}
out = append(out, attr)
}
return
}
// moduleSystemFieldsToAttributes converts all system-defined module fields to attributes
func moduleSystemFieldsToAttributes(ctx context.Context, cm dal.ConnectionMeta, ns *types.Namespace, mod *types.Module) (out dal.AttributeSet, err error) {
if mod.ModelConfig.Partitioned {
return partitionedModuleSystemFieldsToAttributes(cm, mod), nil
}
return defaultModuleSystemFieldsToAttributes(), nil
}
// partitionedModuleSystemFieldsToAttributes converts all system-defined module fields to attributes
// keeping user-defined codec in mind
func partitionedModuleSystemFieldsToAttributes(cm dal.ConnectionMeta, mod *types.Module) (out dal.AttributeSet) {
sysEnc := mod.ModelConfig.SystemFieldEncoding
if sysEnc.ID != nil {
out = append(out, dal.PrimaryAttribute(sysID, modelFieldCodec(cm, mod, &types.ModuleField{Name: sysID, EncodingStrategy: *sysEnc.ID})))
}
if sysEnc.ModuleID != nil {
out = append(out, dal.FullAttribute(sysModuleID, &dal.TypeID{}, modelFieldCodec(cm, mod, &types.ModuleField{Name: sysModuleID, EncodingStrategy: *sysEnc.ModuleID})))
}
if sysEnc.NamespaceID != nil {
out = append(out, dal.FullAttribute(sysNamespaceID, &dal.TypeID{}, modelFieldCodec(cm, mod, &types.ModuleField{Name: sysNamespaceID, EncodingStrategy: *sysEnc.NamespaceID})))
}
if sysEnc.OwnedBy != nil {
out = append(out, dal.FullAttribute(sysOwnedBy, &dal.TypeID{}, modelFieldCodec(cm, mod, &types.ModuleField{Name: sysOwnedBy, EncodingStrategy: *sysEnc.OwnedBy})))
}
if sysEnc.CreatedAt != nil {
out = append(out, dal.FullAttribute(sysCreatedAt, &dal.TypeTimestamp{}, modelFieldCodec(cm, mod, &types.ModuleField{Name: sysCreatedAt, EncodingStrategy: *sysEnc.CreatedAt})))
}
if sysEnc.CreatedBy != nil {
out = append(out, dal.FullAttribute(sysCreatedBy, &dal.TypeID{}, modelFieldCodec(cm, mod, &types.ModuleField{Name: sysCreatedBy, EncodingStrategy: *sysEnc.CreatedBy})))
}
if sysEnc.UpdatedAt != nil {
out = append(out, dal.FullAttribute(sysUpdatedAt, &dal.TypeTimestamp{Nullable: true}, modelFieldCodec(cm, mod, &types.ModuleField{Name: sysUpdatedAt, EncodingStrategy: *sysEnc.UpdatedAt})))
}
if sysEnc.UpdatedBy != nil {
out = append(out, dal.FullAttribute(sysUpdatedBy, &dal.TypeID{Nullable: true}, modelFieldCodec(cm, mod, &types.ModuleField{Name: sysUpdatedBy, EncodingStrategy: *sysEnc.UpdatedBy})))
}
if sysEnc.DeletedAt != nil {
out = append(out, dal.FullAttribute(sysDeletedAt, &dal.TypeTimestamp{Nullable: true}, modelFieldCodec(cm, mod, &types.ModuleField{Name: sysDeletedAt, EncodingStrategy: *sysEnc.DeletedAt})))
}
if sysEnc.DeletedBy != nil {
out = append(out, dal.FullAttribute(sysDeletedBy, &dal.TypeID{Nullable: true}, modelFieldCodec(cm, mod, &types.ModuleField{Name: sysDeletedBy, EncodingStrategy: *sysEnc.DeletedBy})))
}
return
}
// defaultModuleSystemFieldsToAttributes converts all system-defined module fields to attributes
// assuming no user-defined codec provided
func defaultModuleSystemFieldsToAttributes() dal.AttributeSet {
return dal.AttributeSet{
dal.PrimaryAttribute(sysID, &dal.CodecAlias{Ident: colSysID}),
dal.FullAttribute(sysModuleID, &dal.TypeID{}, &dal.CodecAlias{Ident: colSysModuleID}),
dal.FullAttribute(sysNamespaceID, &dal.TypeID{}, &dal.CodecAlias{Ident: colSysNamespaceID}),
dal.FullAttribute(sysOwnedBy, &dal.TypeID{}, &dal.CodecAlias{Ident: colSysOwnedBy}),
dal.FullAttribute(sysCreatedAt, &dal.TypeTimestamp{}, &dal.CodecAlias{Ident: colSysCreatedAt}),
dal.FullAttribute(sysCreatedBy, &dal.TypeID{}, &dal.CodecAlias{Ident: colSysCreatedBy}),
dal.FullAttribute(sysUpdatedAt, &dal.TypeTimestamp{Nullable: true}, &dal.CodecAlias{Ident: colSysUpdatedAt}),
dal.FullAttribute(sysUpdatedBy, &dal.TypeID{Nullable: true}, &dal.CodecAlias{Ident: colSysUpdatedBy}),
dal.FullAttribute(sysDeletedAt, &dal.TypeTimestamp{Nullable: true}, &dal.CodecAlias{Ident: colSysDeletedAt}),
dal.FullAttribute(sysDeletedBy, &dal.TypeID{Nullable: true}, &dal.CodecAlias{Ident: colSysDeletedBy}),
}
}
// moduleFieldToAttribute converts the given module field to a DAL attribute
func moduleFieldToAttribute(ctx context.Context, cm dal.ConnectionMeta, mod *types.Module, f *types.ModuleField) (out *dal.Attribute, err error) {
kind := f.Kind
if kind == "" {
kind = "String"
}
switch strings.ToLower(kind) {
case "bool", "boolean":
at := &dal.TypeBoolean{}
out = dal.FullAttribute(f.Name, at, modelFieldCodec(cm, mod, f))
case "datetime":
switch {
case f.IsDateOnly():
at := &dal.TypeDate{}
out = dal.FullAttribute(f.Name, at, modelFieldCodec(cm, mod, f))
case f.IsTimeOnly():
at := &dal.TypeTime{}
out = dal.FullAttribute(f.Name, at, modelFieldCodec(cm, mod, f))
default:
at := &dal.TypeTimestamp{}
out = dal.FullAttribute(f.Name, at, modelFieldCodec(cm, mod, f))
}
case "email":
at := &dal.TypeText{Length: emailLength}
out = dal.FullAttribute(f.Name, at, modelFieldCodec(cm, mod, f))
case "file":
at := &dal.TypeRef{
RefModel: &dal.Model{Resource: "corteza::system:attachment"},
RefAttribute: &dal.Attribute{Ident: "id"},
}
out = dal.FullAttribute(f.Name, at, modelFieldCodec(cm, mod, f))
case "number":
at := &dal.TypeNumber{
Precision: f.Options.Precision(),
}
out = dal.FullAttribute(f.Name, at, modelFieldCodec(cm, mod, f))
case "record":
at := &dal.TypeRef{
RefModel: &dal.Model{
ResourceID: f.Options.UInt64("moduleID"),
ResourceType: types.ModuleResourceType,
},
RefAttribute: &dal.Attribute{
Ident: "id",
},
}
out = dal.FullAttribute(f.Name, at, modelFieldCodec(cm, mod, f))
case "select":
at := &dal.TypeEnum{
Values: f.SelectOptions(),
}
out = dal.FullAttribute(f.Name, at, modelFieldCodec(cm, mod, f))
case "string":
at := &dal.TypeText{
Length: 0,
}
out = dal.FullAttribute(f.Name, at, modelFieldCodec(cm, mod, f))
case "url":
at := &dal.TypeText{
Length: urlLength,
}
out = dal.FullAttribute(f.Name, at, modelFieldCodec(cm, mod, f))
case "user":
at := &dal.TypeRef{
RefModel: &dal.Model{
ResourceType: systemTypes.UserResourceType,
},
RefAttribute: &dal.Attribute{
Ident: "id",
},
}
out = dal.FullAttribute(f.Name, at, modelFieldCodec(cm, mod, f))
default:
return nil, fmt.Errorf("invalid field %s: kind %s not supported", f.Name, f.Kind)
}
out.SensitivityLevel = f.Privacy.SensitivityLevel
out.Label = f.Name
out.MultiValue = f.Multi
return
}
// modulesByConnection groups given modules by the common connectionID
func modulesByConnection(modules ...*types.Module) map[uint64]types.ModuleSet {
out := make(map[uint64]types.ModuleSet)
for _, mod := range modules {
out[mod.ModelConfig.ConnectionID] = append(out[mod.ModelConfig.ConnectionID], mod)
}
return out
}
// formatterNamespaceParams returns the base namespace params used for ident formatting
func formatterNamespaceParams(ns *types.Namespace) []string {
nsHandle, _ := handle.Cast(nil, ns.Slug, strconv.FormatUint(ns.ID, 10))
return []string{
"namespace", nsHandle,
}
}
// formatterModuleParams returns the base module params used for ident formatting
func formatterModuleParams(mod *types.Module) []string {
modHandle, _ := handle.Cast(nil, mod.Handle, strconv.FormatUint(mod.ID, 10))
return []string{
"module", modHandle,
}
}
// modelFieldCodec returns the DAL codec the given module field should use
func modelFieldCodec(cm dal.ConnectionMeta, mod *types.Module, f *types.ModuleField) (c dal.Codec) {
c = baseModelFieldCodec(cm, mod, f)
switch {
case f.EncodingStrategy.EncodingStrategyAlias != nil:
c = &dal.CodecAlias{
Ident: f.EncodingStrategy.EncodingStrategyAlias.Ident,
}
case f.EncodingStrategy.EncodingStrategyJSON != nil:
c = &dal.CodecRecordValueSetJSON{
Ident: f.EncodingStrategy.EncodingStrategyJSON.Ident,
}
}
return
}
// baseModelFieldCodec returns the DAL codec the given module field should use by default
func baseModelFieldCodec(cm dal.ConnectionMeta, mod *types.Module, f *types.ModuleField) dal.Codec {
if mod.ModelConfig.Partitioned {
return &dal.CodecPlain{}
}
ident := cm.DefaultAttributeIdent
if ident == "" {
// @todo put in configs or something
ident = "values"
}
return &dal.CodecRecordValueSetJSON{
Ident: ident,
}
}