using Comal.Classes; using InABox.Core; using InABox.Database; using InABox.Scripting; using Inflector; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; namespace PRS.Shared.Events; public class SaveEvent : IEvent>, IEntityEvent where T : Entity, new() { public Type Entity => typeof(T); public IEventDataModelDefinition DataModelDefinition() { return new SaveEventDataModelDefinition(this); } public Notification GenerateNotification(SaveEventDataModel 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 void Init(IStore store, Event ev, IEventData evData, SaveEventDataModel model) { if (model.Entity.ID != Guid.Empty) { var loadCols = Columns.None(); IEnumerable refVars = evData.ReferencedVariables; if (!ev.NotificationExpression.IsNullOrWhiteSpace()) { var notificationExpression = new CoreExpression(ev.NotificationExpression); refVars = refVars.Concat(notificationExpression.ReferencedVariables); } var prefix = $"{typeof(T).Name}."; foreach (var variable in refVars) { if (variable.StartsWith(prefix)) { var varName = variable[prefix.Length..]; if (!model.Entity.HasOriginalValue(varName)) { loadCols.Add(varName); } } } var data = store.Provider.Query( new Filter(x => x.ID).IsEqualTo(model.Entity.ID), loadCols); if(data.Rows.Count > 0) { data.Rows[0].FillObject(model.Entity); } } } } public class SaveEventDataModelDefinition(SaveEvent ev) : IEventDataModelDefinition where T : Entity, new() { private IEventVariable[]? variables; public IEnumerable GetVariables() { if(variables is null) { variables = DatabaseSchema.AllProperties(ev.Entity).Select(x => new StandardEventVariable($"{typeof(T).Name}.{x.Name}", x.PropertyType)).ToArray(); variables.SortBy(x => x.Name); } return variables; } public IEventVariable? GetVariable(string name) { if(variables is null) { var prefix = $"{typeof(T).Name}."; if (name.StartsWith(prefix)) { name = name[prefix.Length..]; var prop = DatabaseSchema.Property(ev.Entity, name); if(prop is null) { return null; } else { return new StandardEventVariable(prop.Name, prop.PropertyType); } } else { return null; } } else { return variables.FirstOrDefault(x => x.Name == name); } } } public class SaveEventDataModel(T entity, IStore store) : IEventDataModel, ITypedEventDataModel where T : Entity, new() { public T Entity { get; set; } = entity; public Type EntityType => typeof(T); public IStore Store { get; } = store; public bool TryGetVariable(string name, out object? value) { var prefix = $"{typeof(T).Name}."; if (name.StartsWith(prefix)) { name = name[prefix.Length..]; var prop = DatabaseSchema.Property(typeof(T), name); if(prop != null) { value = prop.Getter()(Entity); return true; } } value = null; return false; } } #region Triggers [Caption("New Record")] public class CreatedSaveEventTrigger : IEventTrigger, SaveEventDataModel> where T : Entity, new() { public string Description => "New Record"; public IEnumerable ReferencedVariables => []; public bool Check(SaveEventDataModel dataModel) { return dataModel.Entity.HasOriginalValue(x => x.ID); } } [Caption("Property Changed")] public class PropertyChangedSaveEventTrigger : IEventTrigger, SaveEventDataModel> where T : Entity, new() { [JsonIgnore] public IProperty? TriggerProperty { get; set; } [JsonProperty(PropertyName = "TriggerProperty")] private string? _property { get => TriggerProperty?.Name; set { TriggerProperty = value is null ? null : DatabaseSchema.PropertyStrict(typeof(T), value); } } public string Description => TriggerProperty is null ? $"{typeof(T).GetCaption()} changed" : $"{typeof(T).GetCaption()}.{TriggerProperty.Name} changed"; public IEnumerable ReferencedVariables => []; public bool Check(SaveEventDataModel dataModel) { if(TriggerProperty is null) { return false; } if (!dataModel.Entity.HasOriginalValue(TriggerProperty.Name)) { return false; } return true; } } [Caption("Filter")] public class FilterSaveEventTrigger : IEventTrigger, SaveEventDataModel> where T : Entity, new() { public Filter? Filter { get; set; } public IEnumerable ReferencedVariables => GetReferencedProperties(Filter).Select(x => $"{typeof(T).Name}.{x}"); public string Description => Filter?.ToString() ?? "Blank Filter"; public bool Check(SaveEventDataModel dataModel) { return Filter.Match(dataModel.Entity, dataModel.Store.GetQueryProviderFactory()); } private IEnumerable GetReferencedProperties(Filter? filter) { if (filter is null) yield break; if(filter.Operator != Operator.All && filter.Operator != Operator.None && !filter.Property.IsNullOrWhiteSpace()) { yield return filter.Property; } foreach(var prop in filter.Ands.Concat(filter.Ors).SelectMany(GetReferencedProperties)) { yield return prop; } } } [Caption("Custom Script")] public class ScriptSaveEventTrigger : IEventTrigger, SaveEventDataModel> where T : Entity, new() { public string Description => "Custom Script"; private ScriptDocument? _scriptDocument; private ScriptDocument? ScriptDocument { get { if(_scriptDocument is null && Script is not null) { _scriptDocument = new(Script); _scriptDocument.Compile(); } return _scriptDocument; } } private string? _script; public string? Script { get => _script; set { if(_script != value) { _script = value; _scriptDocument = null; } } } public IEnumerable ReferencedVariables { get { var method = ScriptDocument?.GetMethod(methodName: "RequiredColumns"); if(method is not null) { var cols = Columns.None(); method.Invoke(ScriptDocument!.GetObject(), [cols]); return cols.ColumnNames().Select(x => $"{typeof(T).Name}.{x}"); } else { return []; } } } public string DefaultScript() { return @"using PRS.Shared.Events; public class Module { public void RequiredColumns(Columns<" + typeof(T).Name + @"> columns) { // Modify 'columns' as required to get the required columns for the 'model.Entity'. If you don't provide these, // the data you require in 'Execute' may not be present. Example: // columns.Add(x => x.ID); } 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 dataModel) { if (ScriptDocument is null) return false; return ScriptDocument.Execute(methodname: "Check", parameters: [dataModel]); } } #endregion #region Actions [Caption("Custom Script")] public class ScriptSaveEventAction : IEventAction> where T : Entity, new() { private ScriptDocument? _scriptDocument; private ScriptDocument? ScriptDocument { get { if(_scriptDocument is null && Script is not null) { _scriptDocument = new(Script); _scriptDocument.SetValue("Result", null); _scriptDocument.Compile(); } return _scriptDocument; } } private string? _script; public string? Script { get => _script; set { if(_script != value) { _script = value; _scriptDocument = null; } } } public IEnumerable ReferencedVariables { get { var method = ScriptDocument?.GetMethod(methodName: "RequiredColumns"); if(method is not null) { var cols = Columns.None(); method.Invoke(ScriptDocument!.GetObject(), [cols]); return cols.ColumnNames().Select(x => $"{typeof(T).Name}.{x}"); } else { return []; } } } public string Description => "Custom Script"; public string DefaultScript() { return @"using PRS.Shared.Events; public class Module { public object? Result { get; set; } public void RequiredColumns(Columns<" + typeof(T).Name + @"> columns) { // Modify 'columns' as required to get the required columns for the 'model.Entity'. If you don't provide these, // the data you require in 'Execute' may not be present. Example: // columns.Add(x => x.ID); } 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 (ScriptDocument is null) return null; var model = dataModel.RootModel>(); if(ScriptDocument.Execute(methodname: "Execute", parameters: [model])) { return ScriptDocument.GetValue("Result"); } else { return null; } } } [Caption("Create Entity")] public class CreateEntitySaveEventAction : IEventAction> where T : Entity, new() { [JsonProperty(Order = 1)] public Type? EntityType { get; set; } [JsonIgnore] public List Initializers { get; set; } = new List(); private ScriptDocument? _scriptDocument; private ScriptDocument? ScriptDocument { get { if(_scriptDocument is null && Script is not null) { _scriptDocument = new(Script); _scriptDocument.Compile(); } return _scriptDocument; } } private string? _script; public string? Script { get => _script; set { if(_script != value) { _script = value; _scriptDocument = null; } } } private IEnumerable ScriptReferencedVariables { get { var method = ScriptDocument?.GetMethod(methodName: "RequiredColumns"); if(method is not null) { var cols = Columns.None(); method.Invoke(ScriptDocument!.GetObject(), [cols]); return cols.ColumnNames().Select(x => $"{typeof(T).Name}.{x}"); } else { return []; } } } public string DefaultScript() { if (EntityType is null) return "Please select an entity type first."; return @"using PRS.Shared.Events; public class Module { public void RequiredColumns(Columns<" + typeof(T).Name + @"> columns) { // Modify 'columns' as required to get the required columns for the 'model.Entity'. If you don't provide these, // the data you require in 'Execute' may not be present. Example: // columns.Add(x => x.ID); } public void Execute(SaveEventDataModel<" + typeof(T).Name + @"> model, " + EntityType.Name + " new" + EntityType.Name + @") { // Modify new" + EntityType.Name + @" as you wish, using model.Entity to get the required data. This method runs after // the property initializers (in the previous screen) for the Create Entity action have run. } }"; } public string Description => $"Create New {EntityType?.Name ?? "Entity"}"; public IEnumerable ReferencedVariables => Initializers.SelectMany(x => x.ReferencedVariables).Concat(ScriptReferencedVariables); 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); } if(ScriptDocument is not null) { var model = dataModel.RootModel>(); ScriptDocument.Execute(methodname: "Execute", parameters: [model, entity]); } DbFactory.FindStore(EntityType, Guid.Empty, "", default, "", Logger.Main).Save(entity, ""); return entity; } #region Serialization Stuff [JsonProperty(Order = 2, PropertyName = "Initializers")] private PropertyInitializerSerializationItem[] _initializers { get => Initializers.ToArray(x => new PropertyInitializerSerializationItem { Property = x.Property.Name, Value = x.Value }); set { Initializers.Clear(); Initializers.AddRange(value.Select(x => new PropertyInitializer(EntityType!, x))); } } #endregion } internal class PropertyInitializerSerializationItem { public string Property { get; set; } public string Value { get; set; } } 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 IEnumerable ReferencedVariables => ValueExpression.ReferencedVariables; internal PropertyInitializer(Type entityType, PropertyInitializerSerializationItem item) { Value = item.Value; Property = DatabaseSchema.PropertyStrict(entityType, item.Property); } public PropertyInitializer(IProperty property, string value) { Property = property; Value = value; } public void Execute(Entity entity, IEventDataModel dataModel) { Property.Setter()(entity, ValueExpression.Evaluate(dataModel)); } } #endregion