package service import ( "context" "errors" "fmt" "github.com/cortezaproject/corteza-server/compose/service/event" "github.com/cortezaproject/corteza-server/compose/types" "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/handle" "github.com/cortezaproject/corteza-server/store" "sort" "strconv" ) type ( module struct { ctx context.Context actionlog actionlog.Recorder ac moduleAccessController eventbus eventDispatcher store store.Storer } moduleAccessController interface { CanReadNamespace(context.Context, *types.Namespace) bool CanCreateModule(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 { With(ctx context.Context) ModuleService FindByID(namespaceID, moduleID uint64) (*types.Module, error) FindByName(namespaceID uint64, name string) (*types.Module, error) FindByHandle(namespaceID uint64, handle string) (*types.Module, error) FindByAny(namespaceID uint64, identifier interface{}) (*types.Module, error) Find(filter types.ModuleFilter) (set types.ModuleSet, f types.ModuleFilter, err error) Create(module *types.Module) (*types.Module, error) Update(module *types.Module) (*types.Module, error) DeleteByID(namespaceID, moduleID uint64) error } moduleUpdateHandler func(ctx context.Context, ns *types.Namespace, c *types.Module) (bool, bool, error) ) func Module() ModuleService { return (&module{ ctx: context.Background(), ac: DefaultAccessControl, eventbus: eventbus.Service(), }).With(context.Background()) } func (svc module) With(ctx context.Context) ModuleService { return &module{ ctx: ctx, actionlog: DefaultActionlog, ac: svc.ac, eventbus: svc.eventbus, store: DefaultStore, } } func (svc module) Find(filter types.ModuleFilter) (set types.ModuleSet, f types.ModuleFilter, err error) { var ( 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(svc.ctx, res) { return false, nil } return true, nil } err = func() error { if ns, err := loadNamespace(svc.ctx, svc.store, filter.NamespaceID); err != nil { return err } else { aProps.setNamespace(ns) } if set, f, err = store.SearchComposeModules(svc.ctx, svc.store, filter); err != nil { return err } return loadModuleFields(svc.ctx, svc.store, set...) }() return set, f, svc.recordAction(svc.ctx, aProps, ModuleActionSearch, err) } // FindByID tries to find module by ID func (svc module) FindByID(namespaceID, moduleID uint64) (m *types.Module, err error) { return svc.lookup(namespaceID, func(aProps *moduleActionProps) (*types.Module, error) { if moduleID == 0 { return nil, ModuleErrInvalidID() } aProps.module.ID = moduleID return store.LookupComposeModuleByID(svc.ctx, svc.store, moduleID) }) } // FindByName tries to find module by name func (svc module) FindByName(namespaceID uint64, name string) (m *types.Module, err error) { return svc.lookup(namespaceID, func(aProps *moduleActionProps) (*types.Module, error) { aProps.module.Name = name return store.LookupComposeModuleByNamespaceIDName(svc.ctx, svc.store, namespaceID, name) }) } // FindByHandle tries to find module by handle func (svc module) FindByHandle(namespaceID uint64, h string) (m *types.Module, err error) { return svc.lookup(namespaceID, func(aProps *moduleActionProps) (*types.Module, error) { if !handle.IsValid(h) { return nil, ModuleErrInvalidHandle() } aProps.module.Handle = h return store.LookupComposeModuleByNamespaceIDHandle(svc.ctx, svc.store, namespaceID, h) }) } // FindByAny tries to find module in a particular namespace by id, handle or name func (svc module) FindByAny(namespaceID uint64, identifier interface{}) (m *types.Module, err error) { if ID, ok := identifier.(uint64); ok { m, err = svc.FindByID(namespaceID, ID) } else if strIdentifier, ok := identifier.(string); ok { if ID, _ := strconv.ParseUint(strIdentifier, 10, 64); ID > 0 { m, err = svc.FindByID(namespaceID, ID) } else { m, err = svc.FindByHandle(namespaceID, strIdentifier) if err == nil && m.ID == 0 { m, err = svc.FindByName(namespaceID, strIdentifier) } } } else { // force invalid ID error // we do that to wrap error with lookup action context _, err = svc.FindByID(namespaceID, 0) } if err != nil { return nil, err } return m, nil } func (svc module) Create(new *types.Module) (*types.Module, error) { var ( ns *types.Namespace aProps = &moduleActionProps{changed: new} ) err := store.Tx(svc.ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) { if !handle.IsValid(new.Handle) { return ModuleErrInvalidHandle() } if ns, err = loadNamespace(ctx, s, new.NamespaceID); err != nil { return err } if !svc.ac.CanCreateModule(ctx, ns) { return ModuleErrNotAllowedToCreate() } aProps.setNamespace(ns) // Calling before-create scripts if err = svc.eventbus.WaitFor(ctx, event.ModuleBeforeCreate(new, nil, ns)); err != nil { return err } if err = svc.uniqueCheck(new); err != nil { return err } new.ID = nextID() new.CreatedAt = *now() new.UpdatedAt = nil new.DeletedAt = nil if new.Fields != nil { _ = new.Fields.Walk(func(f *types.ModuleField) error { f.ModuleID = new.ID f.CreatedAt = *now() f.UpdatedAt = nil f.DeletedAt = nil return nil }) } 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 } _ = svc.eventbus.WaitFor(ctx, event.ModuleAfterCreate(new, nil, ns)) return nil }) return new, svc.recordAction(svc.ctx, aProps, ModuleActionCreate, err) } func (svc module) Update(upd *types.Module) (c *types.Module, err error) { return svc.updater(upd.NamespaceID, upd.ID, ModuleActionUpdate, svc.handleUpdate(upd)) } func (svc module) DeleteByID(namespaceID, moduleID uint64) error { return trim1st(svc.updater(namespaceID, moduleID, ModuleActionDelete, svc.handleDelete)) } func (svc module) UndeleteByID(namespaceID, moduleID uint64) error { return trim1st(svc.updater(namespaceID, moduleID, ModuleActionUndelete, svc.handleUndelete)) } func (svc module) updater(namespaceID, moduleID uint64, action func(...*moduleActionProps) *moduleAction, fn moduleUpdateHandler) (*types.Module, error) { var ( moduleChanged, fieldsChanged bool ns *types.Namespace m, old *types.Module aProps = &moduleActionProps{module: &types.Module{ID: moduleID, NamespaceID: namespaceID}} err error ) err = store.Tx(svc.ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) { ns, m, err = loadModuleWithNamespace(svc.ctx, s, namespaceID, moduleID) if err != nil { return } old = m.Clone() aProps.setNamespace(ns) aProps.setChanged(m) if m.DeletedAt == nil { err = svc.eventbus.WaitFor(svc.ctx, event.ModuleBeforeUpdate(m, old, ns)) } else { err = svc.eventbus.WaitFor(svc.ctx, event.ModuleBeforeDelete(m, old, ns)) } if err != nil { return } if moduleChanged, fieldsChanged, err = fn(svc.ctx, ns, m); err != nil { return err } if moduleChanged { if err = store.UpdateComposeModule(svc.ctx, svc.store, m); err != nil { return err } } if fieldsChanged { var ( hasRecords bool set types.RecordSet ) if set, _, err = store.SearchComposeRecords(ctx, s, m, types.RecordFilter{Paging: filter.Paging{Limit: 1}}); err != nil { return err } hasRecords = len(set) > 0 if err = updateModuleFields(ctx, s, m, m.Fields, hasRecords); err != nil { } } if m.DeletedAt == nil { err = svc.eventbus.WaitFor(svc.ctx, event.ModuleAfterUpdate(m, old, ns)) } else { err = svc.eventbus.WaitFor(svc.ctx, event.ModuleAfterDelete(nil, old, ns)) } return err }) return m, svc.recordAction(svc.ctx, aProps, action, err) } // lookup fn() orchestrates module lookup, namespace preload and check, module reading... func (svc module) lookup(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(svc.ctx, svc.store, namespaceID); err != nil { return err } else { aProps.setNamespace(ns) } if m, err = lookup(aProps); errors.Is(err, store.ErrNotFound) { return ModuleErrNotFound() } else if err != nil { return err } aProps.setModule(m) if !svc.ac.CanReadModule(svc.ctx, m) { return ModuleErrNotAllowedToRead() } return loadModuleFields(svc.ctx, svc.store, m) }() return m, svc.recordAction(svc.ctx, aProps, ModuleActionLookup, err) } func (svc module) uniqueCheck(m *types.Module) (err error) { if m.Handle != "" { if e, _ := store.LookupComposeModuleByNamespaceIDHandle(svc.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(svc.ctx, svc.store, m.NamespaceID, m.Name); e != nil && e.ID > 0 && e.ID != m.ID { return ModuleErrNameNotUnique() } } return nil } func (svc module) handleUpdate(upd *types.Module) moduleUpdateHandler { return func(ctx context.Context, ns *types.Namespace, m *types.Module) (mch bool, fch bool, err error) { if isStale(upd.UpdatedAt, m.UpdatedAt, m.CreatedAt) { return false, false, ModuleErrStaleData() } if upd.Handle != m.Handle && !handle.IsValid(upd.Handle) { return false, false, ModuleErrInvalidHandle() } if err = svc.uniqueCheck(upd); err != nil { return false, false, err } if !svc.ac.CanUpdateModule(svc.ctx, m) { return false, false, ModuleErrNotAllowedToUpdate() } if m.Name != upd.Name { mch = true m.Name = upd.Name } if m.Handle != upd.Handle { mch = true m.Handle = upd.Handle } if m.Meta.String() != upd.Meta.String() { mch = true m.Meta = upd.Meta } // @todo make field-change detection more optimal if len(upd.Fields) > 0 { fch = true m.Fields = upd.Fields } if mch { m.UpdatedAt = now() } // for now, we assume that return mch, fch, nil } } func (svc module) handleDelete(ctx context.Context, ns *types.Namespace, m *types.Module) (bool, bool, error) { if !svc.ac.CanDeleteModule(ctx, m) { return false, false, ModuleErrNotAllowedToDelete() } if m.DeletedAt != nil { // module already deleted return false, false, nil } m.DeletedAt = now() return true, false, nil } func (svc module) handleUndelete(ctx context.Context, ns *types.Namespace, m *types.Module) (bool, bool, error) { if !svc.ac.CanDeleteModule(ctx, m) { return false, false, ModuleErrNotAllowedToUndelete() } if m.DeletedAt == nil { // module not deleted return false, false, nil } m.DeletedAt = nil return true, false, 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, m *types.Module, newFields types.ModuleFieldSet, hasRecords bool) (err error) { for _, f := range newFields { // Set module ID to all new fields if f.ModuleID == 0 { f.ModuleID = m.ID } // Make sure all updating fields belong here if f.ModuleID != m.ID { return fmt.Errorf("module id of field %q does not match the module", f.Name) } } if err = loadModuleFields(ctx, s, m); err != nil { return } for _, ef := range m.Fields { f := newFields.FindByID(ef.ID) if f != nil || f.DeletedAt == nil { continue } ef.DeletedAt = now() err = store.UpdateComposeModuleField(ctx, s, ef) if err != nil { return err } } for idx, f := range newFields { f.Place = idx f.DeletedAt = nil if e := m.Fields.FindByID(f.ID); e != nil { f.CreatedAt = e.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 = e.Name f.Kind = e.Kind } f.UpdatedAt = now() err = store.UpdateComposeModuleField(ctx, s, f) } else { f.ID = nextID() f.CreatedAt = *now() err = store.CreateComposeModuleField(ctx, s, f) } if err != nil { return err } } return 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) sort.Sort(m.Fields) } return } // loads record module with fields and namespace func loadModuleWithNamespace(ctx context.Context, s store.Storer, namespaceID, moduleID uint64) (ns *types.Namespace, m *types.Module, err error) { if moduleID == 0 { return nil, nil, ModuleErrInvalidID() } if ns, err = loadNamespace(ctx, s, namespaceID); err == nil { m, err = loadModule(ctx, s, moduleID) } if err != nil { return nil, nil, err } if namespaceID != m.NamespaceID { // Make sure chart belongs to the right namespace return nil, nil, ModuleErrNotFound() } return } func loadModule(ctx context.Context, s store.Storer, moduleID uint64) (m *types.Module, err error) { if moduleID == 0 { return nil, ModuleErrInvalidID() } if m, err = store.LookupComposeModuleByID(ctx, s, moduleID); errors.Is(err, store.ErrNotFound) { err = ModuleErrNotFound() } if err == nil { err = loadModuleFields(ctx, s, m) } if err != nil { return nil, err } return }