Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public async IAsyncEnumerable<AuditEntry> StreamEntries(AuditEntrySearch search,
var bounded = new AuditEntrySearch {
FromUtc = search.FromUtc,
ToUtc = search.ToUtc == null || search.ToUtc > now ? now : search.ToUtc,
ActorEmail = search.ActorEmail,
SearchText = search.SearchText,
Source = search.Source,
Kind = search.Kind,
EntityType = search.EntityType,
Expand Down Expand Up @@ -108,10 +108,11 @@ private IQueryable<AuditEntry> Query(AuditEntrySearch search)
query = query.Where(x => x.CreatedOnUtc >= search.FromUtc.Value);
if (search.ToUtc.HasValue)
query = query.Where(x => x.CreatedOnUtc <= search.ToUtc.Value);
if (!string.IsNullOrEmpty(search.ActorEmail))
if (!string.IsNullOrEmpty(search.SearchText))
{
var email = search.ActorEmail.ToLowerInvariant();
query = query.Where(x => x.ActorEmail != null && x.ActorEmail.ToLower().Contains(email));
var text = search.SearchText.ToLowerInvariant();
query = query.Where(x => (x.ActorEmail != null && x.ActorEmail.ToLower().Contains(text))
|| (x.EntityName != null && x.EntityName.ToLower().Contains(text)));
}

if (search.Source.HasValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ public class AuditEntrySearch
{
public DateTime? FromUtc { get; set; }
public DateTime? ToUtc { get; set; }
public string ActorEmail { get; set; }
/// <summary>
/// Part of the staff member's e-mail or of the record's name, any case
/// </summary>
public string SearchText { get; set; }
public AuditSource? Source { get; set; }
public AuditKind? Kind { get; set; }
public string EntityType { get; set; }
Expand Down
47 changes: 38 additions & 9 deletions src/Core/Grand.Data/Audit/AuditSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ public static class AuditSnapshot

public static IDictionary<string, string> Take<T>(T entity) where T : BaseEntity
{
return Flatten(entity.ToBsonDocument(), typeof(T) == typeof(Setting));
return Take(entity, typeof(T));
}

public static IDictionary<string, string> Take(object entity, Type entityType)
{
return Flatten(entity.ToBsonDocument(entityType), entityType == typeof(Setting));
}

public static IDictionary<string, string> Flatten(BsonDocument document, bool isSetting)
Expand Down Expand Up @@ -56,7 +61,13 @@ private static void Flatten(string path, BsonValue value, Dictionary<string, str
break;
case BsonArray array when array.Any(v => v.IsBsonDocument || v.IsBsonArray):
for (var i = 0; i < array.Count; i++)
Flatten($"{path}[{ElementKey(array[i], i)}]", array[i], leaves);
{
//the key of a user field is already in its path; repeating it as a field shows a change of nothing
var element = array[i] is BsonDocument document && IsKeyed(document)
? new BsonDocument(document.Where(e => e.Name is not ("Key" or "StoreId")))
: array[i];
Flatten($"{path}[{ElementKey(array[i], i)}]", element, leaves);
}
break;
default:
//a scalar, or a list of scalars kept as one value; when element ids repeat, the first wins
Expand All @@ -69,13 +80,25 @@ private static string ElementKey(BsonValue element, int index)
{
if (element is not BsonDocument document)
return index.ToString();
if ((document.TryGetValue("_id", out var id) || document.TryGetValue("Id", out id)) && !id.IsBsonNull)
if (HasId(document, out var id))
return $"id={id}";
if (!document.TryGetValue("Key", out var key) || !key.IsString || key.AsString == "")
if (!IsKeyed(document))
return index.ToString();
var key = document["Key"].AsString;
return document.TryGetValue("StoreId", out var storeId) && storeId.IsString && storeId.AsString != ""
? $"{key.AsString}@{storeId.AsString}"
: key.AsString;
? $"{key}@{storeId.AsString}"
: key;
}

private static bool HasId(BsonDocument document, out BsonValue id)
{
return (document.TryGetValue("_id", out id) || document.TryGetValue("Id", out id)) && !id.IsBsonNull;
}

//a key/value pair such as a user field: matched by its key (once per store) instead of its position
private static bool IsKeyed(BsonDocument document)
{
return !HasId(document, out _) && document.TryGetValue("Key", out var key) && key.IsString && key.AsString != "";
}

private static BsonDocument TryParse(string json)
Expand All @@ -94,10 +117,16 @@ private static string Format(BsonValue value)
{
if (value.IsString)
return value.AsString;
if (value.IsBsonBinaryData)
return Binary;
//a Guid is stored as binary (CSharpLegacy, see MongoDBStartupBase) but is a value like any other
if (value is BsonBinaryData binary)
return binary.SubType switch {
BsonBinarySubType.UuidLegacy => GuidConverter.FromBytes(binary.Bytes, GuidRepresentation.CSharpLegacy).ToString(),
BsonBinarySubType.UuidStandard => GuidConverter.FromBytes(binary.Bytes, GuidRepresentation.Standard).ToString(),
_ => Binary
};
//to the second: a form that posts a date back drops its milliseconds, which is no change to a person
if (value.IsValidDateTime)
return value.ToUniversalTime().ToString("o");
return value.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'");
if (value.IsBsonArray)
return value.ToJson();
return value.ToString();
Expand Down
14 changes: 14 additions & 0 deletions src/Core/Grand.Data/Audit/IAuditRecordReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Grand.Data.Audit;

/// <summary>
/// Reads one stored record as an audit snapshot, to name the record a changed field refers to
/// </summary>
public interface IAuditRecordReader
{
/// <summary>
/// The record of <paramref name="entityType" /> whose <paramref name="key" /> (the id when null) is
/// <paramref name="value" />, as "path → value", or null when there is none. With <paramref name="field" />,
/// the snapshot may hold only that top-level field.
/// </summary>
Task<IDictionary<string, string>> Read(Type entityType, string value, string key = null, string field = null);
}
25 changes: 25 additions & 0 deletions src/Core/Grand.Data/LiteDb/LiteDBAuditRecordReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Grand.Data.Audit;
using LiteDB;

namespace Grand.Data.LiteDb;

/// <summary>
/// Resolves the database only when a record is read, so it can be registered before installation
/// </summary>
public class LiteDBAuditRecordReader(Func<LiteDatabase> database) : IAuditRecordReader
{
//the embedded database reads the whole document either way, so the field is not projected
public Task<IDictionary<string, string>> Read(Type entityType, string value, string key = null,
string field = null)
{
var db = database();
var collection = db.GetCollection(entityType.Name);
//a key other than the id may hold a number, stored as one
var document = key == null
? collection.FindById(new BsonValue(value))
: collection.FindOne(Query.EQ(key, int.TryParse(value, out var number) ? new BsonValue(number) : new BsonValue(value)));
//through the entity, so the snapshot has the same shape as on MongoDB
var entity = document == null ? null : db.Mapper.ToObject(entityType, document);
return Task.FromResult(entity == null ? null : AuditSnapshot.Take(entity, entityType));
}
}
24 changes: 24 additions & 0 deletions src/Core/Grand.Data/Mongo/MongoAuditRecordReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Grand.Data.Audit;
using Grand.Domain.Configuration;
using MongoDB.Bson;
using MongoDB.Driver;

namespace Grand.Data.Mongo;

/// <summary>
/// Resolves the database only when a record is read, so it can be registered before installation
/// </summary>
public class MongoAuditRecordReader(Func<IMongoDatabase> database) : IAuditRecordReader
{
public async Task<IDictionary<string, string>> Read(Type entityType, string value, string key = null,
string field = null)
{
//a key other than the id may hold a number, stored as one
BsonValue match = key != null && int.TryParse(value, out var number) ? number : value;
var find = database().GetCollection<BsonDocument>(entityType.Name).Find(new BsonDocument(key ?? "_id", match));
var document = field == null
? await find.FirstOrDefaultAsync()
: await find.Project(Builders<BsonDocument>.Projection.Include(field)).FirstOrDefaultAsync();
return AuditSnapshot.Flatten(document, entityType == typeof(Setting));
}
}
25 changes: 25 additions & 0 deletions src/Core/Grand.Domain/Audit/AuditReferenceAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Grand.Domain.Audit;

/// <summary>
/// Marks a property that refers to another record (or holds an enum value as a number), so the audit log shows
/// "name (id)" instead of a bare id. <paramref name="target" /> is the referenced entity or the enum;
/// <paramref name="list" /> names the list inside the target that holds the referenced element, whose owning
/// target record is given by the sibling property named in <see cref="Owner" />.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class AuditReferenceAttribute(Type target, string list = null) : Attribute
{
public Type Target { get; } = target;

public string List { get; } = list;

/// <summary>
/// The sibling property holding the id of the target record that contains <see cref="List" />
/// </summary>
public string Owner { get; set; }

/// <summary>
/// The property of the target that the value matches, when it is not the id (an order status number)
/// </summary>
public string Key { get; set; }
}
4 changes: 3 additions & 1 deletion src/Core/Grand.Domain/Blogs/BlogCategory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Grand.Domain.Localization;
using Grand.Domain.Audit;
using Grand.Domain.Localization;
using Grand.Domain.Stores;

namespace Grand.Domain.Blogs;
Expand Down Expand Up @@ -27,6 +28,7 @@ public class BlogCategory : BaseEntity, IStoreLinkEntity, ITranslationEntity
/// </summary>
public bool LimitedToStores { get; set; }

[AuditReference(typeof(Store))]
public IList<string> Stores { get; set; } = new List<string>();

/// <summary>
Expand Down
5 changes: 4 additions & 1 deletion src/Core/Grand.Domain/Blogs/BlogCategoryPost.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
namespace Grand.Domain.Blogs;
using Grand.Domain.Audit;

namespace Grand.Domain.Blogs;

public class BlogCategoryPost : SubBaseEntity
{
[AuditReference(typeof(BlogPost))]
public string BlogPostId { get; set; }
}
7 changes: 7 additions & 0 deletions src/Core/Grand.Domain/Blogs/BlogComment.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
using Grand.Domain.Stores;
using Grand.Domain.Customers;
using Grand.Domain.Audit;

namespace Grand.Domain.Blogs;

/// <summary>
Expand All @@ -8,11 +12,13 @@ public class BlogComment : BaseEntity
/// <summary>
/// Gets or sets the customer identifier
/// </summary>
[AuditReference(typeof(Customer))]
public string CustomerId { get; set; }

/// <summary>
/// Gets or sets the store identifier
/// </summary>
[AuditReference(typeof(Store))]
public string StoreId { get; set; }

/// <summary>
Expand All @@ -28,5 +34,6 @@ public class BlogComment : BaseEntity
/// <summary>
/// Gets or sets the blog post identifier
/// </summary>
[AuditReference(typeof(BlogPost))]
public string BlogPostId { get; set; }
}
1 change: 1 addition & 0 deletions src/Core/Grand.Domain/Blogs/BlogPost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public class BlogPost : BaseEntity, ISlugEntity, IStoreLinkEntity, ITranslationE
/// </summary>
public virtual bool LimitedToStores { get; set; }

[AuditReference(typeof(Store))]
public IList<string> Stores { get; set; } = new List<string>();

/// <summary>
Expand Down
5 changes: 4 additions & 1 deletion src/Core/Grand.Domain/Blogs/BlogProduct.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
namespace Grand.Domain.Blogs;
using Grand.Domain.Audit;

namespace Grand.Domain.Blogs;

public class BlogProduct : BaseEntity
{
[AuditReference(typeof(BlogPost))]
public string BlogPostId { get; set; }
public string ProductId { get; set; }
public int DisplayOrder { get; set; }
Expand Down
13 changes: 12 additions & 1 deletion src/Core/Grand.Domain/Catalog/Bid.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
namespace Grand.Domain.Catalog;
using Grand.Domain.Shipping;
using Grand.Domain.Stores;
using Grand.Domain.Orders;
using Grand.Domain.Customers;
using Grand.Domain.Audit;

namespace Grand.Domain.Catalog;

/// <summary>
/// Represents a auction bid
Expand All @@ -8,11 +14,13 @@ public class Bid : BaseEntity
/// <summary>
/// Gets or sets the product identifier
/// </summary>
[AuditReference(typeof(Product))]
public string ProductId { get; set; }

/// <summary>
/// Gets or sets the warehouse identifier
/// </summary>
[AuditReference(typeof(Warehouse))]
public string WarehouseId { get; set; }

/// <summary>
Expand All @@ -23,6 +31,7 @@ public class Bid : BaseEntity
/// <summary>
/// Gets or sets the customer id
/// </summary>
[AuditReference(typeof(Customer))]
public string CustomerId { get; set; }

/// <summary>
Expand All @@ -33,12 +42,14 @@ public class Bid : BaseEntity
/// <summary>
/// Gets or sets Order Id
/// </summary>
[AuditReference(typeof(Order))]
public string OrderId { get; set; }

/// <summary>
/// /
/// Gets or sets Store Id
/// </summary>
[AuditReference(typeof(Store))]
public string StoreId { get; set; }

/// <summary>
Expand Down
7 changes: 7 additions & 0 deletions src/Core/Grand.Domain/Catalog/Brand.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using Grand.Domain.Customers;
using Grand.Domain.Discounts;
using Grand.Domain.Audit;
using Grand.Domain.Localization;
using Grand.Domain.Permissions;
using Grand.Domain.Seo;
Expand Down Expand Up @@ -30,6 +33,7 @@ public class Brand : BaseEntity, ITranslationEntity, ISlugEntity, IGroupLinkEnti
/// <summary>
/// Gets or sets a value of used brand layout identifier
/// </summary>
[AuditReference(typeof(BrandLayout))]
public string BrandLayoutId { get; set; }

/// <summary>
Expand Down Expand Up @@ -105,6 +109,7 @@ public class Brand : BaseEntity, ITranslationEntity, ISlugEntity, IGroupLinkEnti
/// <summary>
/// Gets or sets the collection of applied discounts
/// </summary>
[AuditReference(typeof(Discount))]
public virtual ICollection<string> AppliedDiscounts {
get => _appliedDiscounts ??= new List<string>();
protected set => _appliedDiscounts = value;
Expand All @@ -115,6 +120,7 @@ public virtual ICollection<string> AppliedDiscounts {
/// </summary>
public bool LimitedToGroups { get; set; }

[AuditReference(typeof(CustomerGroup))]
public IList<string> CustomerGroups { get; set; } = new List<string>();

/// <summary>
Expand All @@ -127,6 +133,7 @@ public virtual ICollection<string> AppliedDiscounts {
/// </summary>
public bool LimitedToStores { get; set; }

[AuditReference(typeof(Store))]
public IList<string> Stores { get; set; } = new List<string>();

/// <summary>
Expand Down
3 changes: 3 additions & 0 deletions src/Core/Grand.Domain/Catalog/BundleProduct.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Grand.Domain.Audit;

namespace Grand.Domain.Catalog;

/// <summary>
Expand All @@ -8,6 +10,7 @@ public class BundleProduct : SubBaseEntity
/// <summary>
/// Gets or sets the product identifier
/// </summary>
[AuditReference(typeof(Product))]
public string ProductId { get; set; }

/// <summary>
Expand Down
Loading
Loading