3
0
2023-03-17 10:58:46 +01:00

1624 lines
42 KiB
Go

package service
import (
"context"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"github.com/cortezaproject/corteza/server/compose/dalutils"
"github.com/cortezaproject/corteza/server/pkg/logger"
"go.uber.org/zap"
"github.com/cortezaproject/corteza/server/pkg/revisions"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/filter"
"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)
SearchSensitive(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 {
GetConnectionByID(ID uint64) *dal.ConnectionWrap
Search(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, 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, diff *dal.ModelDiff, hasRecords bool, trans ...dal.TransformationFunction) (err error)
SearchModelIssues(ID uint64) []error
}
)
const (
moduleUnchanged moduleChanges = 0
moduleChanged moduleChanges = 1
moduleLabelsChanged moduleChanges = 2
moduleFieldsChanged moduleChanges = 4
recordTable = "compose_record"
recordFieldID = "ID"
recordFieldModuleID = "moduleID"
recordFieldNamespaceID = "namespaceID"
)
const (
// https://www.rfc-editor.org/errata/eid1690
emailLength = 254
// Generally the upper most limit
urlLength = 2048
sysID = "ID"
sysNamespaceID = "namespaceID"
sysModuleID = "moduleID"
sysRevision = "revision"
sysMeta = "meta"
sysCreatedAt = "createdAt"
sysCreatedBy = "createdBy"
sysUpdatedAt = "updatedAt"
sysUpdatedBy = "updatedBy"
sysDeletedAt = "deletedAt"
sysDeletedBy = "deletedBy"
sysOwnedBy = "ownedBy"
colSysID = "id"
colSysNamespaceID = "rel_namespace"
colSysModuleID = "rel_module"
colSysRevision = "revision"
colSysMeta = "meta"
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",
"revision",
"meta",
"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.ID)
if len(ii) == 0 {
m.Issues = nil
return
}
m.Issues = make([]string, len(ii))
for i, err := range ii {
m.Issues[i] = err.Error()
}
}
func (svc module) Create(ctx context.Context, new *types.Module) (*types.Module, error) {
var (
ns *types.Namespace
aProps = &moduleActionProps{module: 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
}
}
// Verify dal system field mappings
_ = handleDalSysFieldEncodingUpdate(new)
aProps.setChanged(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)
}
// SearchSensitive will list all module with at least one private module field
func (svc module) SearchSensitive(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 {
conn := svc.dal.GetConnectionByID(m.Config.DAL.ConnectionID)
if err != nil {
return err
}
connID := conn.ID
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
defConn *dal.ConnectionWrap
hasRecords bool
)
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 {
{
// properly resolve connection ID 0 to the actual ID of the default connection
if defConn = svc.dal.GetConnectionByID(0); defConn == nil {
return fmt.Errorf("could not find default DAL connection")
}
if old.Config.DAL.ConnectionID == 0 {
old.Config.DAL.ConnectionID = defConn.ID
}
if m.Config.DAL.ConnectionID == 0 {
m.Config.DAL.ConnectionID = defConn.ID
}
}
// we'll allow connection change on an existing module for now
//
//if old.Config.DAL.ConnectionID != m.Config.DAL.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 (
set types.RecordSet
recFilter = types.RecordFilter{
Paging: filter.Paging{Limit: 1},
Check: func(r *types.Record) (bool, error) { return true, nil },
}
)
if modelIssues := svc.dal.SearchModelIssues(m.ID); len(modelIssues) == 0 {
if set, _, err = dalutils.ComposeRecordsList(ctx, svc.dal, m, recFilter); err != nil {
// we should not really abort the update here.
//
// if we do, in case of a misconfigured module
// a model issue is raised and the module cannot be updated.
//
// a solution similar to soft(warning)/hard(error) issues that
// we introduced on record could be used here.
logger.Default().Warn("could not list records due to DAL model issues", zap.Error(err))
err = nil
}
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, hasRecords); 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
}
}
// Verify dal system field mappings
_ = handleDalSysFieldEncodingUpdate(upd)
if !reflect.DeepEqual(res.Config, upd.Config) {
changes |= moduleChanged
res.Config = upd.Config
}
// check by size first in case if one is nil and other len(0)
// @todo make field-change detection more optimal
if (len(upd.Fields) > 0 || len(res.Fields) > 0) && !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 = ModulesToModelSet(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, hasRecords bool) (err error) {
oldModel, err := ModulesToModelSet(dmm, ns, old)
if err != nil {
return
}
newModel, err := ModulesToModelSet(dmm, ns, new)
if err != nil {
return
}
diff := oldModel[0].Diff(newModel[0])
// TODO handle the fact that diff is a list of changes so the same field could be present more than once.
for _, d := range diff {
if err = dmm.ReplaceModelAttribute(ctx, oldModel[0], d, hasRecords); 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.Config.DAL.ConnectionID, m.ID); err != nil {
return err
}
}
return
}
// ModulesToModelSet takes a modules for a namespace and converts all of them
// into a model set for the DAL
//
// Ident partition placeholders are replaced here as well alongside
// with the revision models where revisions are enabled
func ModulesToModelSet(dmm dalModelManager, ns *types.Namespace, mm ...*types.Module) (out dal.ModelSet, err error) {
var (
conn *dal.ConnectionWrap
model *dal.Model
// partition replace pairs
modPartition []string
// namespace partition replacement pairs
// {{namespace}} is replaced with the namespace handle (slug)
nsPartition = []string{"{{namespace}}", ns.Slug}
defConnID uint64
defConn = dmm.GetConnectionByID(0)
)
if defConn != nil {
defConnID = defConn.ID
}
for connectionID, modules := range modulesByConnection(defConnID, mm...) {
// Get the connection meta
conn = dmm.GetConnectionByID(connectionID)
// Convert all modules to models
for _, mod := range modules {
if conn == nil {
// construct a simplified model w/o attributes, connection
// this will allow us to manage model's issues within
// the DAL service
model = &dal.Model{
Label: mod.Handle,
Resource: mod.RbacResource(),
ResourceID: mod.ID,
}
out = append(out, model)
continue
}
// convert each module to model
model, err = ModuleToModel(ns, mod, conn.Config.ModelIdent)
if err != nil {
return
}
// construct partition replacement pairs from namespace & module handles
// {{module}} is replaced with module handle
modPartition = append(nsPartition, "{{module}}", mod.Handle)
// replace all partition replacement pairs
model.Ident = strings.NewReplacer(modPartition...).Replace(model.Ident)
// @todo validate ident with connection's ident validator
model.Constraints = modelBaseConstraints(model, mod)
model.ConnectionID = connectionID
out = append(out, model)
if mod.Config.RecordRevisions.Enabled {
rModel := revisions.Model()
// reuse the connection from the module
rModel.ConnectionID = connectionID
rModel.Resource = model.Resource
rModel.ResourceID = nextID()
if rModel.Ident = mod.Config.RecordRevisions.Ident; rModel.Ident == "" {
rModel.Ident = "compose_record_revisions"
}
rModel.Ident = strings.NewReplacer(modPartition...).Replace(rModel.Ident)
// @todo validate ident with connection's ident validator
out = append(out, rModel)
}
}
}
return
}
func modelBaseConstraints(model *dal.Model, mod *types.Module) (out map[string][]any) {
// If we're writting to the default table apply additional constraints
// @todo there should be more logic here, but for now this is what we had
// elsewhere.
if model.Ident == recordTable {
out = map[string][]any{
recordFieldModuleID: {mod.ID},
recordFieldNamespaceID: {mod.NamespaceID},
}
}
return
}
// ModuleToModel converts a module with fields to DAL model and attributes
//
// note: this function does not do any partition placeholder replacements
func ModuleToModel(ns *types.Namespace, mod *types.Module, inhIdent string) (model *dal.Model, err error) {
var (
attrAux dal.AttributeSet
)
model = &dal.Model{
Label: mod.Handle,
Resource: mod.RbacResource(),
ResourceID: mod.ID,
ResourceType: types.ModuleResourceType,
SensitivityLevelID: mod.Config.Privacy.SensitivityLevelID,
}
userDefinedFieldIdents := make(map[string]bool)
if model.Ident = mod.Config.DAL.Ident; model.Ident == "" {
// try with explicitly set ident on module's DAL config
// and fallback connection's default if it is empty
model.Ident = inhIdent
}
// Refs for lookups
var (
nsSlug = ""
nsID = uint64(0)
)
if ns != nil {
nsSlug = ns.Slug
nsID = ns.ID
}
model.Refs = map[string]any{
"module": mod.Handle,
"moduleID": mod.ID,
"namespace": nsSlug,
"namespaceID": nsID,
}
// Convert user-defined fields to attributes
attrAux, err = moduleFieldsToAttributes(mod)
if err != nil {
return
}
for _, attr := range attrAux {
userDefinedFieldIdents[attr.Ident] = true
}
model.Attributes = append(model.Attributes, attrAux...)
// Convert system fields to attribute
attrAux, err = moduleSystemFieldsToAttributes(mod)
if err != nil {
return
}
for _, attr := range attrAux {
ok, _ := userDefinedFieldIdents[attr.Ident]
if !ok {
// make sure we're backward compatible:
// if, by some weird case, someone managed to get a system field name into
// the store, we'll turn a blind eye. We need to make sure not to include the field twice in this situation.
model.Attributes = append(model.Attributes, attr)
}
}
return
}
// moduleFieldsToAttributes converts all user-defined module fields to attributes
func moduleFieldsToAttributes(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(f)
if err != nil {
return
}
if attr == nil {
// when instructed to omit the attribute
// by field's encoding strategy
continue
}
out = append(out, attr)
}
return
}
// moduleSystemFieldsToAttributes converts all system-defined module fields to attributes
func moduleSystemFieldsToAttributes(mod *types.Module) (out dal.AttributeSet, err error) {
var (
sysEnc = mod.Config.DAL.SystemFieldEncoding
// generate dal.Codec for each attribute
// using encoding strategy for that attribute
// with failsafe on CodecAlias
mfc = func(defStoreIdent string, es *types.EncodingStrategy) dal.Codec {
switch {
case es != nil && es.EncodingStrategyAlias != nil:
return &dal.CodecAlias{
Ident: es.EncodingStrategyAlias.Ident,
}
case es != nil && es.EncodingStrategyJSON != nil:
return &dal.CodecRecordValueSetJSON{
Ident: es.EncodingStrategyJSON.Ident,
}
case es != nil:
// assuming omit!
return nil
default:
return &dal.CodecAlias{
Ident: defStoreIdent,
}
}
}
// takes a slice of attributes removes one with nil store codec
filterSkippedAttribtues = func(in ...*dal.Attribute) (out dal.AttributeSet) {
for _, attr := range in {
if attr.Store != nil {
out = append(out, attr)
}
}
return
}
)
aa := filterSkippedAttribtues(
dal.PrimaryAttribute(sysID, mfc(colSysID, sysEnc.ID)),
dal.FullAttribute(sysModuleID, &dal.TypeID{}, mfc(colSysModuleID, sysEnc.ModuleID)),
dal.FullAttribute(sysDeletedBy, &dal.TypeRef{RefModel: &dal.ModelRef{ResourceType: "corteza::system:user"}, Nullable: true}, mfc(colSysDeletedBy, sysEnc.DeletedBy)),
dal.FullAttribute(sysNamespaceID, &dal.TypeID{}, mfc(colSysNamespaceID, sysEnc.NamespaceID)),
dal.FullAttribute(sysRevision, &dal.TypeID{}, mfc(colSysRevision, sysEnc.Revision)),
dal.FullAttribute(sysMeta, &dal.TypeJSON{}, mfc(colSysMeta, sysEnc.Meta)),
dal.FullAttribute(sysOwnedBy, &dal.TypeRef{RefModel: &dal.ModelRef{ResourceType: "corteza::system:user"}}, mfc(colSysOwnedBy, sysEnc.OwnedBy)),
dal.FullAttribute(sysCreatedAt, &dal.TypeTimestamp{}, mfc(colSysCreatedAt, sysEnc.CreatedAt)),
dal.FullAttribute(sysCreatedBy, &dal.TypeRef{RefModel: &dal.ModelRef{ResourceType: "corteza::system:user"}}, mfc(colSysCreatedBy, sysEnc.CreatedBy)),
dal.FullAttribute(sysUpdatedAt, &dal.TypeTimestamp{Nullable: true}, mfc(colSysUpdatedAt, sysEnc.UpdatedAt)),
dal.FullAttribute(sysUpdatedBy, &dal.TypeRef{RefModel: &dal.ModelRef{ResourceType: "corteza::system:user"}, Nullable: true}, mfc(colSysUpdatedBy, sysEnc.UpdatedBy)),
dal.FullAttribute(sysDeletedAt, &dal.TypeTimestamp{Nullable: true}, mfc(colSysDeletedAt, sysEnc.DeletedAt)),
)
for _, a := range aa {
a.System = true
}
return append(out, aa...), nil
}
// moduleFieldToAttribute converts the given module field to a DAL attribute
func moduleFieldToAttribute(f *types.ModuleField) (out *dal.Attribute, err error) {
var (
// generate dal.Codec for each attribute
// using encoding strategy for that attribute
// with failsafe on JSON RVS.
codec = func(f *types.ModuleField) dal.Codec {
var es = f.Config.DAL.EncodingStrategy
switch {
case es != nil && es.EncodingStrategyPlain != nil:
return &dal.CodecPlain{}
case es != nil && es.EncodingStrategyAlias != nil:
return &dal.CodecAlias{
Ident: es.EncodingStrategyAlias.Ident,
}
case es != nil && es.EncodingStrategyJSON != nil:
return &dal.CodecRecordValueSetJSON{
Ident: es.EncodingStrategyJSON.Ident,
}
// Only omit if explicitly told to; for module fields, default to JSON
case es != nil && es.Omit:
return nil
default:
// defaulting to RecordValueSetJSON with
// default attribute ident from connection
return &dal.CodecRecordValueSetJSON{
// ensure JSON encoded record values always have
// "values" as col ident as a failsafe
Ident: "values",
}
}
}(f)
)
if codec == nil {
return
}
switch strings.ToLower(f.Kind) {
case "bool", "boolean":
at := &dal.TypeBoolean{
Nullable: !f.Required,
}
out = dal.FullAttribute(f.Name, at, codec)
case "datetime":
switch {
case f.IsDateOnly():
at := &dal.TypeDate{
Nullable: !f.Required,
}
out = dal.FullAttribute(f.Name, at, codec)
case f.IsTimeOnly():
at := &dal.TypeTime{
Nullable: !f.Required,
}
out = dal.FullAttribute(f.Name, at, codec)
default:
at := &dal.TypeTimestamp{
Nullable: !f.Required,
}
out = dal.FullAttribute(f.Name, at, codec)
}
case "email":
at := &dal.TypeText{
Length: emailLength,
Nullable: !f.Required,
}
out = dal.FullAttribute(f.Name, at, codec)
case "file":
at := &dal.TypeRef{
RefModel: &dal.ModelRef{Resource: "corteza::system:attachment"},
Nullable: !f.Required,
}
out = dal.FullAttribute(f.Name, at, codec)
case "number":
at := &dal.TypeNumber{
Precision: int(f.Options.Precision()),
Nullable: !f.Required,
}
out = dal.FullAttribute(f.Name, at, codec)
case "record":
at := &dal.TypeRef{
RefModel: &dal.ModelRef{
ResourceID: f.Options.UInt64("moduleID"),
ResourceType: types.ModuleResourceType,
},
Nullable: !f.Required,
}
out = dal.FullAttribute(f.Name, at, codec)
case "select":
at := &dal.TypeEnum{
Values: f.SelectOptions(),
Nullable: !f.Required,
}
out = dal.FullAttribute(f.Name, at, codec)
case "url":
at := &dal.TypeText{
Length: urlLength,
Nullable: !f.Required,
}
out = dal.FullAttribute(f.Name, at, codec)
case "user":
at := &dal.TypeRef{
RefModel: &dal.ModelRef{
ResourceType: systemTypes.UserResourceType,
},
Nullable: !f.Required,
}
out = dal.FullAttribute(f.Name, at, codec)
case "geometry":
at := &dal.TypeGeometry{
Nullable: !f.Required,
}
out = dal.FullAttribute(f.Name, at, codec)
out.Filterable = false
out.Sortable = false
default:
at := &dal.TypeText{
Nullable: !f.Required,
}
out = dal.FullAttribute(f.Name, at, codec)
}
out.SensitivityLevelID = f.Config.Privacy.SensitivityLevelID
out.Label = f.Label
out.MultiValue = f.Multi
return
}
// modulesByConnection groups given modules by the common connectionID
func modulesByConnection(defConnID uint64, modules ...*types.Module) map[uint64]types.ModuleSet {
var (
id uint64
out = make(map[uint64]types.ModuleSet)
)
for _, mod := range modules {
if id = mod.Config.DAL.ConnectionID; id == 0 {
// connection not explicitly set on module
// use default
id = defConnID
}
out[id] = append(out[id], mod)
}
return out
}
// handleDalSysFieldEncodingUpdate prevents the mapping from being disabled for certain system field
// IE. `recordID` -> `Module.Config.DAL.SystemFieldEncoding.ID`
func handleDalSysFieldEncodingUpdate(mod *types.Module) error {
if mod.Config.DAL.SystemFieldEncoding.ID != nil && mod.Config.DAL.SystemFieldEncoding.ID.Omit {
mod.Config.DAL.SystemFieldEncoding.ID.Omit = false
}
return nil
}