Kenric Nugteren пре 8 месеци
родитељ
комит
aab253c150

+ 50 - 0
prs.classes/Entities/Events/Event.cs

@@ -0,0 +1,50 @@
+using Expressive;
+using InABox.Clients;
+using InABox.Core;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+
+namespace Comal.Classes
+{
+    public enum EventType
+    {
+        AfterSave
+    }
+
+    public class Event : Entity, IRemotable, IPersistent, ILicense<CoreLicense>
+    {
+        [UniqueCodeEditor]
+        [EditorSequence(1)]
+        public string Code { get; set; } = "";
+
+        [EditorSequence(2)]
+        public EventType EventType { get; set; }
+
+        /// <summary>
+        /// Serialised event data.
+        /// </summary>
+        [NullEditor]
+        public string Data { get; set; } = "";
+
+        [ExpressionEditor(null)]
+        public string NotificationExpression { get; set; } = "";
+
+        static Event()
+        {
+            DefaultColumns.Add<Event>(x => x.Code);
+            DefaultColumns.Add<Event>(x => x.EventType);
+        }
+    }
+
+    public class EventLink : EntityLink<Event>, ILicense<CoreLicense>
+    {
+        [CodePopupEditor(typeof(Event))]
+        public override Guid ID { get; set; }
+
+        public string Code { get; set; }
+    }
+}

+ 21 - 0
prs.classes/Entities/Events/EventSubscriber.cs

@@ -0,0 +1,21 @@
+using InABox.Core;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Comal.Classes
+{
+    public class EventSubscriber : Entity, IRemotable, IPersistent, IManyToMany<Event, Employee>
+    {
+        public EventLink Event { get; set; }
+
+        public EmployeeLink Employee { get; set; }
+
+        static EventSubscriber()
+        {
+            DefaultColumns.Add<EventSubscriber>(x => x.Event.Code);
+            DefaultColumns.Add<EventSubscriber>(x => x.Employee.Code);
+            DefaultColumns.Add<EventSubscriber>(x => x.Employee.Name);
+        }
+    }
+}

+ 283 - 0
prs.shared/Events/Event.cs

@@ -0,0 +1,283 @@
+using Comal.Classes;
+using Expressive;
+using InABox.Core;
+using InABox.Database;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace PRS.Shared.Events;
+
+public class EventData<T, TDataModel>
+    where T : IEvent<TDataModel>
+    where TDataModel : IEventDataModel
+{
+    public T Event { get; set; }
+
+    /// <summary>
+    /// A list of triggers for this event. If any of the triggers match, the event runs.
+    /// </summary>
+    public List<ITrigger<T, TDataModel>> Triggers { get; set; }
+
+    public List<IEventAction<T>> Actions { get; set; }
+
+    public EventData(T eventData)
+    {
+        Event = eventData;
+        Triggers = new List<ITrigger<T, TDataModel>>();
+        Actions = new List<IEventAction<T>>();
+    }
+
+    public Notification GenerateNotification(TDataModel model) => Event.GenerateNotification(model);
+}
+
+public static class EventUtils
+{
+    private static bool Check<T, TDataModel>(EventData<T, TDataModel> ev, TDataModel dataModel)
+        where T : IEvent<TDataModel>
+        where TDataModel : IEventDataModel
+    {
+        return ev.Triggers.Any(x => x.Check(dataModel));
+    }
+
+    public static void Run<T, TDataModel>(IStore store, Event ev, EventData<T, TDataModel> evData, TDataModel dataModel)
+        where T : IEvent<TDataModel>
+        where TDataModel : IEventDataModel
+    {
+        if (!Check(evData, dataModel)) return;
+
+        foreach(var action in evData.Actions)
+        {
+            IEvent.RunAction(evData.Event, dataModel, action);
+        }
+
+        NotifySubscribers(store, ev, evData, dataModel);
+    }
+
+    private static void NotifySubscribers<T, TDataModel>(IStore store, Event ev, EventData<T, TDataModel> evData, TDataModel dataModel)
+        where T : IEvent<TDataModel>
+        where TDataModel : IEventDataModel
+    {
+        string? description;
+        if (ev.NotificationExpression.IsNullOrWhiteSpace())
+        {
+            description = null;
+        }
+        else
+        {
+            var descriptionExpr = new CoreExpression(ev.NotificationExpression, typeof(string));
+            if(!descriptionExpr.TryEvaluate<string>(dataModel).Get(out description, out var error))
+            {
+                CoreUtils.LogException(store.UserID, error, extra: "Error notifying subscribers", store.Logger);
+                return;
+            }
+        }
+
+        var subscribers = store.Provider.Query(
+            new Filter<EventSubscriber>(x => x.Event.ID).IsEqualTo(ev.ID),
+            Columns.None<EventSubscriber>().Add(x => x.Employee.ID))
+            .ToArray<EventSubscriber>();
+
+        var notifications = new List<Notification>();
+        foreach(var subscriber in subscribers)
+        {
+            var notification = evData.GenerateNotification(dataModel);
+            notification.Employee.CopyFrom(subscriber.Employee);
+            if(description is not null)
+            {
+                notification.Description = description;
+            }
+            notifications.Add(notification);
+        }
+        store.Provider.Save(notifications);
+    }
+}
+
+#region DataModel Definition
+
+public interface IEventVariable
+{
+    string Name { get; set; }
+
+    Type Type { get; set; }
+}
+
+public class StandardEventVariable : IEventVariable
+{
+    public string Name { get; set; }
+
+    public Type Type { get; set; }
+
+    public StandardEventVariable(string name, Type type)
+    {
+        Name = name;
+        Type = type;
+    }
+}
+
+public class ListEventVariable : IEventVariable
+{
+    public string Name { get; set; }
+
+    public Type Type { get; set; }
+
+    public ListEventVariable(string name, Type type)
+    {
+        Name = name;
+        Type = type;
+    }
+}
+
+public interface IEventDataModelDefinition
+{
+    IEnumerable<IEventVariable> GetVariables();
+
+    IEventVariable? GetVariable(string name);
+}
+
+#endregion
+
+#region DataModel
+
+public interface IEventDataModel : IVariableProvider
+{
+    bool TryGetVariable(string name, out object? value);
+
+    bool IVariableProvider.TryGetValue(string variableName, out object? value)
+    {
+        return TryGetVariable(variableName, out value);
+    }
+
+    T RootModel<T>()
+        where T : IEventDataModel
+    {
+        if(this is IChildEventDataModel child)
+        {
+            return child.Parent.RootModel<T>();
+        }
+        else if(this is T root)
+        {
+            return root;
+        }
+        else
+        {
+            throw new Exception($"Root model of wrong type; expected {typeof(T).FullName}; got {GetType().FullName}");
+        }
+    }
+}
+
+public interface ITypedEventDataModel : IEventDataModel
+{
+    public Type EntityType { get; }
+}
+
+public interface IChildEventDataModel : IEventDataModel
+{
+    IEventDataModel Parent { get; }
+}
+
+public class ChildEventDataModel : IChildEventDataModel
+{
+    public IEventDataModel Parent { get; set; }
+
+    public Dictionary<string, object?> Values { get; set; } = new Dictionary<string, object?>();
+
+    public ChildEventDataModel(IEventDataModel parent)
+    {
+        Parent = parent;
+    }
+
+    public bool TryGetVariable(string name, out object? value)
+    {
+        return Values.TryGetValue(name, out value)
+            || Parent.TryGetVariable(name, out value);
+    }
+}
+
+#endregion
+
+public interface IEvent
+{
+    IEventDataModelDefinition DataModelDefinition();
+
+    public static void RunAction(IEvent ev, IEventDataModel model, IEventAction action)
+    {
+        var dataModelDef = ev.DataModelDefinition();
+
+        var values = new List<(string name, object? value)[]>() { Array.Empty<(string, object?)>() };
+
+        var vars = action.ReferencedVariables();
+        foreach(var variable in vars)
+        {
+            var varDef = dataModelDef.GetVariable(variable);
+            if(varDef is ListEventVariable)
+            {
+                if (model.TryGetVariable(varDef.Name, out var list) && list is IEnumerable enumerable)
+                {
+                    var oldValues = values;
+                    values = new List<(string name, object? value)[]>();
+                    foreach(var item in enumerable)
+                    {
+                        foreach(var valueList in oldValues)
+                        {
+                            values.Add(valueList.Concatenate(new (string name, object? value)[]
+                            {
+                                (varDef.Name, item)
+                            }));
+                        }
+                    }
+                }
+                else
+                {
+                    values.Clear();
+                    break;
+                }
+            }
+        }
+
+        if(values.Count > 0 && (values.Count > 1 || values[0].Length > 0))
+        {
+            var subModel = new ChildEventDataModel(model);
+            foreach(var valueSet in values)
+            {
+                subModel.Values.Clear();
+                foreach(var (name, value) in valueSet)
+                {
+                    subModel.Values[name] = value;
+                }
+                action.Execute(subModel);
+            }
+        }
+        else
+        {
+            action.Execute(model);
+        }
+    }
+}
+
+public interface IEvent<TDataModel> : IEvent
+{
+    Notification GenerateNotification(TDataModel model);
+}
+
+public interface ITrigger<TEvent, TDataModel>
+    where TEvent : IEvent<TDataModel>
+    where TDataModel : IEventDataModel
+{
+    bool Check(TDataModel dataModel);
+}
+
+public interface IEventAction
+{
+    IEnumerable<string> ReferencedVariables();
+
+    object? Execute(IEventDataModel dataModel);
+}
+
+public interface IEventAction<TEvent> : IEventAction
+    where TEvent : IEvent
+{
+}

+ 328 - 0
prs.shared/Events/SaveEvent.cs

@@ -0,0 +1,328 @@
+using Comal.Classes;
+using InABox.Core;
+using InABox.Scripting;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text;
+
+namespace PRS.Shared.Events;
+
+public class SaveEvent<T> : IEvent<SaveEventDataModel<T>>
+    where T : Entity
+{
+    public Type Entity => typeof(T);
+
+    public IEventDataModelDefinition DataModelDefinition()
+    {
+        return new SaveEventDataModelDefinition<T>(this);
+    }
+
+    public Notification GenerateNotification(SaveEventDataModel<T> model)
+    {
+        var notification = new Notification();
+        notification.Title = $"Updated {typeof(T).Name}";
+        notification.Description = $"Updated {typeof(T).Name}";
+
+        if(model.Entity.ID != Guid.Empty)
+        {
+            notification.EntityType = CoreUtils.EntityName(model.EntityType);
+            notification.EntityID = model.Entity.ID;
+        }
+        return notification;
+    }
+}
+
+public class SaveEventDataModelDefinition<T>(SaveEvent<T> ev) : IEventDataModelDefinition
+    where T : Entity
+{
+    private IEventVariable[]? variables;
+
+    public SaveEvent<T> Event { get; set; } = ev;
+
+    public IEnumerable<IEventVariable> GetVariables()
+    {
+        if(variables is null)
+        {
+            variables = DatabaseSchema.AllProperties(Event.Entity).Select(x => new StandardEventVariable(x.Name, x.PropertyType)).ToArray();
+            variables.SortBy(x => x.Name);
+        }
+        return variables;
+    }
+
+    public IEventVariable? GetVariable(string name)
+    {
+        if(variables is null)
+        {
+            var prop = DatabaseSchema.Property(Event.Entity, name);
+            if(prop is null)
+            {
+                return null;
+            }
+            else
+            {
+                return new StandardEventVariable(prop.Name, prop.PropertyType);
+            }
+        }
+        else
+        {
+            return variables.FirstOrDefault(x => x.Name == name);
+        }
+    }
+}
+
+public class SaveEventDataModel<T>(T entity) : IEventDataModel, ITypedEventDataModel
+    where T : Entity
+{
+    public T Entity { get; set; } = entity;
+
+    public Type EntityType => typeof(T);
+
+    public bool TryGetVariable(string name, out object? value)
+    {
+        var prop = DatabaseSchema.Property(typeof(T), name);
+        if(prop != null)
+        {
+            value = prop.Getter()(Entity);
+            return true;
+        }
+        else
+        {
+            value = null;
+            return false;
+        }
+    }
+}
+
+#region Triggers
+
+public class CreatedSaveEventTrigger<T> : ITrigger<SaveEvent<T>, SaveEventDataModel<T>>
+    where T : Entity
+{
+    public bool Check(SaveEventDataModel<T> dataModel)
+    {
+        return dataModel.Entity.HasOriginalValue(x => x.ID);
+    }
+}
+
+public class PropertyChangedSaveEventTrigger<T> : ITrigger<SaveEvent<T>, SaveEventDataModel<T>>
+    where T : Entity
+{
+    public IProperty? TriggerProperty { get; set; }
+
+    public object? OldValue { get; set; }
+
+    public object? NewValue { get; set; }
+
+    public bool Check(SaveEventDataModel<T> dataModel)
+    {
+        if(TriggerProperty is null)
+        {
+            return false;
+        }
+        if (!dataModel.Entity.HasOriginalValue(TriggerProperty.Name))
+        {
+            return false;
+        }
+        if(OldValue is not null && !object.Equals(dataModel.Entity.OriginalValueList[TriggerProperty.Name], OldValue))
+        {
+            return false;
+        }
+        if(NewValue is not null && !object.Equals(TriggerProperty.Getter()(dataModel.Entity), NewValue))
+        {
+            return false;
+        }
+        return true;
+    }
+}
+
+public class ScriptSaveEventTrigger<T> : ITrigger<SaveEvent<T>, SaveEventDataModel<T>>
+    where T : Entity
+{
+    private ScriptDocument? _scriptDocument;
+
+    private string? _script;
+    public string? Script
+    {
+        get => _script;
+        set
+        {
+            if(_script != value)
+            {
+                _script = value;
+                _scriptDocument = null;
+            }
+        }
+    }
+
+    public string DefaultScript()
+    {
+        return @"
+using Comal.Classes;
+
+public class Module
+{
+    public bool Check(SaveEventDataModel<" + typeof(T).Name + @"> model)
+    {
+        // Return true if model.Entity meets the requirements for this event trigger.
+        return true;
+    }
+}";
+    }
+
+    public bool Check(SaveEventDataModel<T> dataModel)
+    {
+        if (Script is null) return false;
+
+        if(_scriptDocument is null)
+        {
+            _scriptDocument = new(Script);
+            _scriptDocument.Compile();
+        }
+        return _scriptDocument.Execute(methodname: "Check", parameters: [dataModel]);
+    }
+}
+
+#endregion
+
+#region Actions
+
+public class ScriptSaveEventAction<T> : IEventAction<SaveEvent<T>>
+    where T : Entity
+{
+    private ScriptDocument? _scriptDocument;
+
+    private string? _script;
+    public string? Script
+    {
+        get => _script;
+        set
+        {
+            if(_script != value)
+            {
+                _script = value;
+                _scriptDocument = null;
+            }
+        }
+    }
+
+    public string DefaultScript()
+    {
+        return @"
+using Comal.Classes;
+
+public class Module
+{
+    public object? Result { get; set; }
+
+    public bool Execute(SaveEventDataModel<" + typeof(T).Name + @"> model)
+    {
+        // Do anything you want with model.Entity, and then save return-value to 'Result', or leave it as 'null' if no return value is needed.
+        return true;
+    }
+}";
+    }
+
+
+    public object? Execute(IEventDataModel dataModel)
+    {
+        if (Script is null) return null;
+
+        if(_scriptDocument is null)
+        {
+            _scriptDocument = new(Script);
+            _scriptDocument.SetValue("Result", null);
+            _scriptDocument.Compile();
+        }
+        var model = dataModel.RootModel<SaveEventDataModel<T>>();
+        if(_scriptDocument.Execute(methodname: "Execute", parameters: [model]))
+        {
+            return _scriptDocument.GetValue("Result");
+        }
+        else
+        {
+            return null;
+        }
+    }
+
+    public IEnumerable<string> ReferencedVariables()
+    {
+        yield break;
+    }
+}
+
+public class CreateEntityAction<T> : IEventAction<SaveEvent<T>>
+    where T : Entity
+{
+    public Type? EntityType { get; set; }
+
+    public List<PropertyInitializer> Initializers { get; set; } = new List<PropertyInitializer>();
+
+    public object? Execute(IEventDataModel dataModel)
+    {
+        if(EntityType is null)
+        {
+            return null;
+        }
+        var entity = (Activator.CreateInstance(EntityType) as Entity)!;
+        foreach(var initializer in Initializers)
+        {
+            initializer.Execute(entity, dataModel);
+        }
+        return entity;
+    }
+
+    public IEnumerable<string> ReferencedVariables()
+    {
+        return Initializers.SelectMany(x => x.ReferencedVariables());
+    }
+}
+
+public class PropertyInitializer
+{
+    public IProperty Property { get; set; }
+
+    private CoreExpression? _valueExpression;
+    private CoreExpression ValueExpression
+    {
+        get
+        {
+            _valueExpression ??= new CoreExpression(Value, Property.PropertyType);
+            return _valueExpression;
+        }
+    }
+
+    private string _value;
+    public string Value
+    {
+        get => _value;
+        [MemberNotNull(nameof(_value))]
+        set
+        {
+            if(value != _value)
+            {
+                _value = value;
+                _valueExpression = null;
+            }
+        }
+    }
+
+    public PropertyInitializer(IProperty property, string value)
+    {
+        Property = property;
+        Value = value;
+    }
+
+    public void Execute(Entity entity, IEventDataModel dataModel)
+    {
+        Property.Setter()(entity, ValueExpression.Evaluate(dataModel));
+    }
+
+    public IEnumerable<string> ReferencedVariables()
+    {
+        return ValueExpression.ReferencedVariables;
+    }
+}
+
+#endregion