From 9446e5716f974f48f7c9d1d8a9cea51f31543064 Mon Sep 17 00:00:00 2001 From: Tobias Klika Date: Thu, 4 Sep 2025 16:05:41 +0200 Subject: [PATCH] new sqlite ops --- zero.Sqlite/Operations/DbOperations.Delete.cs | 36 ++++ zero.Sqlite/Operations/DbOperations.Read.cs | 191 ++++++++++++++++++ zero.Sqlite/Operations/DbOperations.Write.cs | 79 ++++++++ zero.Sqlite/Operations/DbOperations.cs | 73 ++++++- zero.Sqlite/SqliteOptions.cs | 7 +- zero.Sqlite/ZeroSqliteModule.cs | 8 +- 6 files changed, 389 insertions(+), 5 deletions(-) create mode 100644 zero.Sqlite/Operations/DbOperations.Delete.cs create mode 100644 zero.Sqlite/Operations/DbOperations.Read.cs create mode 100644 zero.Sqlite/Operations/DbOperations.Write.cs diff --git a/zero.Sqlite/Operations/DbOperations.Delete.cs b/zero.Sqlite/Operations/DbOperations.Delete.cs new file mode 100644 index 00000000..75dd9495 --- /dev/null +++ b/zero.Sqlite/Operations/DbOperations.Delete.cs @@ -0,0 +1,36 @@ + +using System.Threading.Tasks; +using ServiceStack.OrmLite; +using zero.Models; + +namespace zero.Sqlite; + +public partial class DbOperations : IDbOperations +{ + /// + public virtual Task> Delete(T model) where T : ZeroIdEntity, new() + => Delete(model.Id); + + + /// + public virtual async Task> Delete(string id) where T : ZeroIdEntity, new() + { + T model = await Load(id); + + if (model == null) + { + return Result.Fail("@errors.ondelete.idnotfound"); + } + + if (model is ISupportsSoftDelete softDeleteModel) + { + softDeleteModel.IsDeleted = true; + } + else + { + await Db.DeleteByIdAsync(model.Id); + } + + return Result.Success(); + } +} \ No newline at end of file diff --git a/zero.Sqlite/Operations/DbOperations.Read.cs b/zero.Sqlite/Operations/DbOperations.Read.cs new file mode 100644 index 00000000..19e9d5ee --- /dev/null +++ b/zero.Sqlite/Operations/DbOperations.Read.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using ServiceStack.OrmLite; +using zero.Extensions; +using zero.Models; +using zero.Utils; + +namespace zero.Sqlite; + +public partial class DbOperations : IDbOperations +{ + /// + public virtual async Task Load(string id, string changeVector = null) where T : ZeroIdEntity, new() + { + if (id.IsNullOrWhiteSpace()) + { + return null; + } + if (!changeVector.IsNullOrEmpty()) + { + //return WhenActive(await GetRevision(changeVector)); // TODO + } + + return WhenActive(await Db.SingleByIdAsync(id)); + } + + + /// + public virtual async Task> Load(IEnumerable ids) where T : ZeroIdEntity, new() + { + ids = ids.Distinct().ToArray(); + + List models = await Db.SelectByIdsAsync(ids); + Dictionary result = new(); + + foreach (string id in ids) + { + T model = models.FirstOrDefault(x => x.Id == id); + result.Add(id, WhenActive(model)); + } + + return result; + } + + + /// + public virtual async Task> LoadAsList(IEnumerable ids) where T : ZeroIdEntity, new() + { + ids = ids.Distinct().ToArray(); + + List models = await Db.SelectByIdsAsync(ids); + List result = []; + + foreach (string id in ids) + { + T model = models.FirstOrDefault(x => x.Id == id); + if (WhenActive(model) != null) + { + result.Add(model); + } + } + + return result; + } + + + // /// + // public virtual async Task Any(Func, IQueryable> querySelector = default) where T : ZeroIdEntity, new() + // { + // querySelector ??= x => x; + // return await querySelector(Session.Query()).AnyAsync(); + // } + // + // + // /// + // public virtual async Task> Load(int pageNumber, int pageSize, Func, IQueryable> querySelector = default) where T : ZeroIdEntity, new() + // { + // IRavenQueryable queryable = Session.Query().Statistics(out QueryStatistics statistics); + // querySelector ??= x => x; + // + // List result = await querySelector(queryable).Paging(pageNumber, pageSize).ToListAsync(); + // return new Paged(result, statistics.TotalResults, pageNumber, pageSize); + // } + // + // + // /// + // public virtual async Task> Load(int pageNumber, int pageSize, Func, IQueryable> querySelector = default) + // where T : ZeroIdEntity, new() + // where TIndex : AbstractCommonApiForIndexes, new() + // { + // IRavenQueryable queryable = Session.Query().Statistics(out QueryStatistics statistics); + // querySelector ??= x => x; + // + // List result = await querySelector(queryable).Paging(pageNumber, pageSize).ToListAsync(); + // return new Paged(result, statistics.TotalResults, pageNumber, pageSize); + // } + // + // + // /// + // public virtual async Task> Load(Func, IQueryable> querySelector) where T : ZeroIdEntity, new() + // { + // IRavenQueryable queryable = Session.Query(); + // querySelector ??= x => x; + // + // return await querySelector(queryable).ToListAsync(); + // } + // + // + // /// + // public virtual async Task> Load(Func, IQueryable> querySelector) + // where T : ZeroIdEntity, new() + // where TIndex : AbstractCommonApiForIndexes, new() + // { + // IRavenQueryable queryable = Session.Query(); + // querySelector ??= x => x; + // + // return await querySelector(queryable).ToListAsync(); + // } + // + // + // /// + // public virtual async Task> Load(Expression> predicate) where T : ZeroIdEntity, new() + // { + // return await Session.Query().Where(predicate).ToListAsync(); + // } + // + // + // /// + // public virtual async Task> Load(Expression> predicate) + // where T : ZeroIdEntity, new() + // where TIndex : AbstractCommonApiForIndexes, new() + // { + // return await Session.Query().Where(predicate).ToListAsync(); + // } + // + // + // /// + // public virtual async Task> Load(int pageNumber, int pageSize, Func, IQueryable> querySelector = default) + // where T : ZeroIdEntity, new() + // where TProjection : ZeroIdEntity, new() + // where TIndex : AbstractCommonApiForIndexes, new() + // { + // IRavenQueryable queryable = Session.Query().Statistics(out QueryStatistics statistics); + // querySelector ??= x => x; + // + // List result = await querySelector(queryable).ProjectInto().Paging(pageNumber, pageSize).ToListAsync(); + // return new Paged(result, statistics.TotalResults, pageNumber, pageSize); + // } + // + // + // /// + // public virtual async Task> LoadAll() where T : ZeroIdEntity, new() + // { + // List items = new(); + // + // await foreach (T item in Stream(null)) + // { + // items.Add(item); + // } + // + // return items; + // } + // + // + // /// + // public virtual async IAsyncEnumerable Stream(Func, IQueryable> expression) where T : ZeroIdEntity, new() + // { + // IRavenQueryable query = Session.Query(); + // IQueryable queryable = query; + // + // if (expression != null) + // { + // queryable = expression(query); + // } + // + // var stream = await Session.Advanced.StreamAsync(queryable); + // + // while (await stream.MoveNextAsync()) + // { + // if (WhenActive(stream.Current.Document) == default) + // { + // continue; + // } + // + // yield return stream.Current.Document; + // } + // } +} \ No newline at end of file diff --git a/zero.Sqlite/Operations/DbOperations.Write.cs b/zero.Sqlite/Operations/DbOperations.Write.cs new file mode 100644 index 00000000..200fd681 --- /dev/null +++ b/zero.Sqlite/Operations/DbOperations.Write.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentValidation.Results; +using ServiceStack.OrmLite; +using zero.Models; +using zero.Extensions; + +namespace zero.Sqlite; + +public partial class DbOperations : IDbOperations +{ + /// + public virtual Task> Create(T model, Func> validate = null) where T : ZeroIdEntity, new() => Save(model, validate); + + /// + public virtual Task> Update(T model, Func> validate = null) where T : ZeroIdEntity, new() => Save(model, validate, true); + + /// + protected virtual async Task> Save(T model, Func> validate = null, bool update = false) where T : ZeroIdEntity, new() + { + if (model == null) + { + return Result.Fail("@errors.onsave.empty"); + } + + T previousModel = null; + + // check if the Id for a model already exists + if (!model.Id.IsNullOrEmpty()) + { + previousModel = await Db.SingleByIdAsync(model.Id); + + if (update && previousModel == null) + { + return Result.Fail("@errors.onsave.noidmatch"); + } + else if (!update && previousModel != null) + { + return Result.Fail("@errors.oncreate.idmismatch"); + } + } + + // validate flavor + if (model is ISupportsFlavors flavorModel && !flavorModel.Flavor.IsNullOrEmpty()) + { + if (!Flavors.Exists(flavorModel.Flavor)) + { + return Result.Fail("@errors.onsave.flavornotfound"); + } + } + + // prepare model + PrepareForSave(model); + + // run validator + if (validate != null) + { + ValidationResult validation = await validate(model); + + if (!validation.IsValid) + { + return Result.Fail(validation); + } + } + + // create ID before-hand so interceptors can use it + if (!update) + { + model.Id = await GenerateId(model); + } + + // store our model + await Db.SaveAsync(model); + + return Result.Success(model); + } +} \ No newline at end of file diff --git a/zero.Sqlite/Operations/DbOperations.cs b/zero.Sqlite/Operations/DbOperations.cs index ae30704e..419b98b4 100644 --- a/zero.Sqlite/Operations/DbOperations.cs +++ b/zero.Sqlite/Operations/DbOperations.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; +using System.Data; using FluentValidation.Results; using Microsoft.Extensions.DependencyInjection; using System.Linq.Expressions; using System.Security.Claims; using System.Threading.Tasks; +using ServiceStack.OrmLite; using zero.Context; using zero.Models; using zero.Utils; @@ -19,12 +22,22 @@ public partial class DbOperations : IDbOperations protected IServiceProvider Services { get; private set; } + protected IDbConnection Db { get; private set; } - public DbOperations(StoreContext context) + + public DbOperations(StoreContext context, IDbConnection db) { Context = context.Context; Services = context.Services; Flavors = context.Options.For(); + Db = db; + } + + + /// + public bool EnsureTableExists() where T : ZeroIdEntity + { + return Db.CreateTableIfNotExists(); } @@ -80,16 +93,39 @@ public partial class DbOperations : IDbOperations return await validator.ValidateAsync(model); } + + + /// + public virtual T WhenActive(T model) where T : ZeroIdEntity, new() + { + return model; // TODO should we really use this? I tried to get data in a custom backend but couldn't because of this + //return model != null && (model is not ZeroEntity || (model as ZeroEntity).IsActive) && (model is not ISupportsSoftDelete || !(model as ISupportsSoftDelete).IsDeleted) ? model : default; + } } public interface IDbOperations { + /// + /// Create a table if not existing + /// + bool EnsureTableExists() where T : ZeroIdEntity; + /// /// Generate model Id by using configured document store conventions /// Task GenerateId(T model) where T : ZeroIdEntity; + /// + /// Do only return the model when it is set to active or inactive entities are included with IncludeInactive() + /// + T WhenActive(T model) where T : ZeroIdEntity, new(); + + /// + /// Validates an entity + /// + Task Validate(T model) where T : ZeroIdEntity, new(); + /// /// Generate values for all properties (incl. nested) which contain the [GenerateId] attribute /// @@ -99,4 +135,39 @@ public interface IDbOperations /// Automatically fill base properties of a ZeroEntity /// T PrepareForSave(T model) where T : ZeroIdEntity; + + /// + /// Get an entity by Id + /// + Task Load(string id, string changeVector = null) where T : ZeroIdEntity, new(); + + /// + /// Get entities by ids + /// + Task> Load(IEnumerable ids) where T : ZeroIdEntity, new(); + + /// + /// Get entities by ids + /// + Task> LoadAsList(IEnumerable ids) where T : ZeroIdEntity, new(); + + /// + /// Creates an entity with an optional validator + /// + Task> Create(T model, Func> validate = null) where T : ZeroIdEntity, new(); + + /// + /// Updates an entity with an optional validator + /// + Task> Update(T model, Func> validate = null) where T : ZeroIdEntity, new(); + + /// + /// Deletes an entity + /// + Task> Delete(T model) where T : ZeroIdEntity, new(); + + /// + /// Deletes an entity + /// + Task> Delete(string id) where T : ZeroIdEntity, new(); } \ No newline at end of file diff --git a/zero.Sqlite/SqliteOptions.cs b/zero.Sqlite/SqliteOptions.cs index 0c52c95c..a905a49a 100644 --- a/zero.Sqlite/SqliteOptions.cs +++ b/zero.Sqlite/SqliteOptions.cs @@ -1,6 +1,11 @@ -namespace zero.Sqlite; +using System; +using System.Data; + +namespace zero.Sqlite; public class SqliteOptions { public string ConnectionString { get; set; } + + public Action OnConnectionCreate { get; set; } } \ No newline at end of file diff --git a/zero.Sqlite/ZeroSqliteModule.cs b/zero.Sqlite/ZeroSqliteModule.cs index 410b648d..ca22f561 100644 --- a/zero.Sqlite/ZeroSqliteModule.cs +++ b/zero.Sqlite/ZeroSqliteModule.cs @@ -36,17 +36,19 @@ internal class ZeroSqliteModule : ZeroModule protected IDbConnectionFactory CreateDbConnectionFactory(IServiceProvider services) { IZeroOptions options = services.GetService(); - SqliteOptions ravenOptions = options.For(); - return new OrmLiteConnectionFactory(ravenOptions.ConnectionString, SqliteDialect.Provider); + SqliteOptions sqliteOptions = options.For(); + return new OrmLiteConnectionFactory(sqliteOptions.ConnectionString, SqliteDialect.Provider); } protected IDbConnection CreateDbConnection(IServiceProvider services) { IDbConnectionFactory factory = services.GetService(); + IZeroOptions options = services.GetService(); + SqliteOptions sqliteOptions = options.For(); IDbConnection db = factory.CreateDbConnection(); db.Open(); - //db.CreateTableIfNotExists(); + sqliteOptions.OnConnectionCreate?.Invoke(db); return db; } } \ No newline at end of file