rename to mixtape

This commit is contained in:
2026-05-05 11:34:32 +02:00
parent 56a51c9ad0
commit 34cc74dbf7
271 changed files with 1022 additions and 1025 deletions
@@ -0,0 +1,32 @@
using System;
using System.Linq.Expressions;
using ServiceStack.OrmLite;
using Mixtape.Extensions;
namespace Mixtape.Sqlite;
public static class SqlExpressionExtensions
{
public static SqlExpression<T> Paging<T>(this SqlExpression<T> source, int pageNumber, int pageSize)
{
pageNumber = pageNumber.Limit(1, 10_000_000);
pageSize = pageSize.Limit(1, 1_000);
if (pageNumber <= 0 || pageSize <= 0)
{
throw new NotSupportedException("Both pageNumber and pageSize must be greater than z_ero");
}
return source.Skip((pageNumber - 1) * pageSize).Take(pageSize);
}
public static SqlExpression<T> WhereIf<T>(this SqlExpression<T> source, Expression<Func<T, bool>> predicate, bool condition, Expression<Func<T, bool>> elsePredicate = null)
{
if (condition)
{
return source.Where(predicate);
}
return elsePredicate != null ? source.Where(elsePredicate) : source;
}
}
+55
View File
@@ -0,0 +1,55 @@
using System;
using System.Data;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ServiceStack.Data;
using ServiceStack.OrmLite;
using Mixtape.Configuration;
using Mixtape.Models;
using Mixtape.Modules;
namespace Mixtape.Sqlite;
public static class MixtapeBuilderExtensions
{
public static MixtapeBuilder AddSqlite(this MixtapeBuilder builder)
{
builder.AddModule<MixtapeSqliteModule>();
return builder;
}
}
internal class MixtapeSqliteModule : MixtapeModule
{
public override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IDbConnectionFactory>(CreateDbConnectionFactory);
services.AddScoped<IDbConnection>(CreateDbConnection);
services.AddScoped<IDbOperations, DbOperations>();
services.AddScoped<StoreContext>();
services.AddScoped<IEntityModifiedHandler, EmptyEntityModifiedHandler>();
services.AddOptions<FlavorOptions>();
services.AddOptions<SqliteOptions>().Bind(configuration.GetSection("Mixtape:Sqlite"));
services.ConfigureOptions<ConfigureFlavorJsonOptions>();
}
protected IDbConnectionFactory CreateDbConnectionFactory(IServiceProvider services)
{
IMixtapeOptions options = services.GetService<IMixtapeOptions>();
SqliteOptions sqliteOptions = options.For<SqliteOptions>();
return new OrmLiteConnectionFactory(sqliteOptions.ConnectionString, SqliteDialect.Provider);
}
protected IDbConnection CreateDbConnection(IServiceProvider services)
{
IDbConnectionFactory factory = services.GetService<IDbConnectionFactory>();
IMixtapeOptions options = services.GetService<IMixtapeOptions>();
SqliteOptions sqliteOptions = options.For<SqliteOptions>();
IDbConnection db = factory.CreateDbConnection();
db.Open();
sqliteOptions.OnConnectionCreate?.Invoke(db);
return db;
}
}
@@ -0,0 +1,41 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ServiceStack.OrmLite;
using Mixtape.Models;
namespace Mixtape.Sqlite;
public partial class DbOperations : IDbOperations
{
/// <inheritdoc />
public virtual Task<Result<T>> Delete<T>(T model) where T : MixtapeIdEntity, new()
=> Delete<T>(model.Id);
/// <inheritdoc />
public virtual async Task<Result<T>> Delete<T>(string id) where T : MixtapeIdEntity, new()
{
T model = await Load<T>(id);
if (model == null)
{
Logger.LogWarning("Could not delete entity (model is null) for type {type}", typeof(T));
return Result<T>.Fail("@errors.ondelete.idnotfound");
}
if (model is ISupportsSoftDelete softDeleteModel)
{
softDeleteModel.IsDeleted = true;
}
else
{
await Db.DeleteByIdAsync<T>(model.Id);
}
Logger.LogInformation("{id} ({type}) successfully deleted", typeof(T), model.Id);
await EntityModifiedHandler.Deleted(model);
return Result<T>.Success();
}
}
@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using ServiceStack.OrmLite;
using Mixtape.Extensions;
using Mixtape.Models;
namespace Mixtape.Sqlite;
public partial class DbOperations : IDbOperations
{
/// <inheritdoc />
public virtual async Task<T> Load<T>(string id, string changeVector = null) where T : MixtapeIdEntity, new()
{
if (id.IsNullOrWhiteSpace())
{
return null;
}
if (!changeVector.IsNullOrEmpty())
{
//return WhenActive(await GetRevision(changeVector)); // TODO
}
return WhenActive(await Db.SingleByIdAsync<T>(id));
}
/// <inheritdoc />
public virtual async Task<Dictionary<string, T>> Load<T>(IEnumerable<string> ids) where T : MixtapeIdEntity, new()
{
ids = ids.Distinct().ToArray();
List<T> models = await Db.SelectByIdsAsync<T>(ids);
Dictionary<string, T> result = new();
foreach (string id in ids)
{
T model = models.FirstOrDefault(x => x.Id == id);
result.Add(id, WhenActive(model));
}
return result;
}
/// <inheritdoc />
public virtual async Task<List<T>> LoadAsList<T>(IEnumerable<string> ids) where T : MixtapeIdEntity, new()
{
ids = ids.Distinct().ToArray();
List<T> models = await Db.SelectByIdsAsync<T>(ids);
List<T> result = [];
foreach (string id in ids)
{
T model = models.FirstOrDefault(x => x.Id == id);
if (WhenActive(model) != null)
{
result.Add(model);
}
}
return result;
}
/// <inheritdoc />
public virtual async Task<bool> Any<T>(Expression<Func<T, bool>> querySelector = null) where T : MixtapeIdEntity, new()
{
return await Db.ExistsAsync(querySelector ?? (x => true));
}
/// <inheritdoc />
public virtual async Task<List<T>> Load<T>(Expression<Func<T, bool>> querySelector) where T : MixtapeIdEntity, new()
{
return await Db.SelectAsync(querySelector ?? (x => true));
}
/// <inheritdoc />
public virtual async Task<T> Find<T>(Expression<Func<T, bool>> querySelector) where T : MixtapeIdEntity, new()
{
return await Db.SingleAsync(querySelector);
}
/// <inheritdoc />
public virtual async Task<List<T>> LoadBySql<T>(Func<SqlExpression<T>, SqlExpression<T>> querySelector) where T : MixtapeIdEntity, new()
{
return await Db.SelectAsync(querySelector(Db.From<T>()));
}
/// <inheritdoc />
public virtual async Task<List<T>> LoadAll<T>() where T : MixtapeIdEntity, new()
{
return await Db.SelectAsync<T>(x => true);
}
}
@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentValidation.Results;
using Microsoft.Extensions.Logging;
using ServiceStack.OrmLite;
using Mixtape.Models;
using Mixtape.Extensions;
namespace Mixtape.Sqlite;
public partial class DbOperations : IDbOperations
{
/// <inheritdoc />
public virtual Task<Result<T>> Create<T>(T model, Func<T, Task<ValidationResult>> validate = null) where T : MixtapeIdEntity, new() => Save(model, validate);
/// <inheritdoc />
public virtual Task<Result<T>> Update<T>(T model, Func<T, Task<ValidationResult>> validate = null) where T : MixtapeIdEntity, new() => Save(model, validate, true);
/// <inheritdoc />
public virtual async Task<Result<T>> CreateOrUpdate<T>(T model, Func<T, Task<ValidationResult>> validate = null) where T : MixtapeIdEntity, new()
{
bool update = !model.Id.IsNullOrEmpty() && await Any<T>(x => x.Id == model.Id);
return await Save(model, validate, update);
}
/// <inheritdoc />
protected virtual async Task<Result<T>> Save<T>(T model, Func<T, Task<ValidationResult>> validate = null, bool update = false) where T : MixtapeIdEntity, new()
{
if (model == null)
{
Logger.LogWarning("Could not create/update entity (model is null) for type {type}", typeof(T));
return Result<T>.Fail("@errors.onsave.empty");
}
// check if the Id for a model already exists
if (!model.Id.IsNullOrEmpty())
{
T previousModel = await Db.SingleByIdAsync<T>(model.Id);
if (update && previousModel == null)
{
return Result<T>.Fail("@errors.onsave.noidmatch");
}
else if (!update && previousModel != null)
{
return Result<T>.Fail("@errors.oncreate.idmismatch");
}
}
// validate flavor
if (model is ISupportsFlavors flavorModel && !flavorModel.Flavor.IsNullOrEmpty())
{
if (!Flavors.Exists<T>(flavorModel.Flavor))
{
Logger.LogWarning("Flavor {flavor} not found for type {type}", flavorModel.Flavor, typeof(T));
return Result<T>.Fail("@errors.onsave.flavornotfound");
}
}
// prepare model
PrepareForSave(model);
// run validator
if (validate != null)
{
ValidationResult validation = await validate(model);
if (!validation.IsValid)
{
Logger.LogInformation("Validation failed for {id} ({errors})", model.Id, validation.Errors);
return Result<T>.Fail(validation);
}
}
// create ID before-hand so interceptors can use it
if (!update && !model.Id.HasValue())
{
model.Id = await GenerateId(model);
}
// store our model
await Db.SaveAsync(model);
string action = update ? "Updated" : "Created";
if (model is MixtapeEntity mixtapeEntity)
{
Logger.LogInformation(action + " {id} with name {name}", model.Id, mixtapeEntity.Name);
}
else
{
Logger.LogInformation(action + " {id}", model.Id);
}
await EntityModifiedHandler.Saved(model, update);
return Result<T>.Success(model);
}
/// <inheritdoc />
public virtual async Task Sort<T>(IEnumerable<string> ids) where T : MixtapeEntity, new()
{
List<T> items = await LoadAll<T>();
uint sort = 0;
foreach (string id in ids)
{
T item = items.FirstOrDefault(x => x.Id == id);
if (item != null)
{
sort += 10;
item.Sort = sort;
item.LastModifiedDate = DateTimeOffset.Now;
}
}
await Db.UpdateAllAsync(items);
foreach (T item in items)
{
await EntityModifiedHandler.Updated(item);
}
}
}
+216
View File
@@ -0,0 +1,216 @@
using System;
using System.Collections.Generic;
using System.Data;
using FluentValidation.Results;
using Microsoft.Extensions.DependencyInjection;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ServiceStack.OrmLite;
using Mixtape.Communication;
using Mixtape.Context;
using Mixtape.Models;
using Mixtape.Utils;
using Mixtape.Validation;
namespace Mixtape.Sqlite;
public partial class DbOperations : IDbOperations
{
protected IMixtapeContext Context { get; private set; }
protected FlavorOptions Flavors { get; }
protected IServiceProvider Services { get; }
protected IDbConnection Db { get; }
protected ILogger<IDbOperations> Logger { get; }
protected IEntityModifiedHandler EntityModifiedHandler { get; }
public DbOperations(StoreContext context, IDbConnection db, ILogger<IDbOperations> logger, IHandlerHolder handler)
{
Context = context.Context;
Services = context.Services;
Flavors = context.Options.For<FlavorOptions>();
Db = db;
Logger = logger;
EntityModifiedHandler = handler.Get<IEntityModifiedHandler>();
}
/// <inheritdoc />
public bool EnsureTableExists<T>() where T : MixtapeIdEntity
{
return Db.CreateTableIfNotExists<T>();
}
/// <inheritdoc />
public Task<string> GenerateId<T>(T model) where T : MixtapeIdEntity
{
return Task.FromResult(IdGenerator.Create(12));
}
/// <inheritdoc />
public T AutoSetIds<T>(T model)
{
return IdGenerator.Autofill(model);
}
/// <inheritdoc />
public T PrepareForSave<T>(T model) where T : MixtapeIdEntity
{
// set IDs
AutoSetIds(model);
if (model is not MixtapeEntity mixtapeModel)
{
return model;
}
// set default properties
if (mixtapeModel.CreatedDate == default)
{
mixtapeModel.CreatedDate = DateTimeOffset.Now;
}
// update name alias and last modified
mixtapeModel.Alias = Safenames.Alias(mixtapeModel.Name);
mixtapeModel.LastModifiedDate = DateTimeOffset.Now;
mixtapeModel.Hash ??= IdGenerator.Create();
return model;
}
/// <inheritdoc />
public async Task<ValidationResult> Validate<T>(T model) where T : MixtapeIdEntity, new()
{
IMixtapeMergedValidator<T> validator = Services.GetService<IMixtapeMergedValidator<T>>();
if (validator == null)
{
return new();
}
return await validator.ValidateAsync(model);
}
/// <inheritdoc />
public virtual T WhenActive<T>(T model) where T : MixtapeIdEntity, 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 MixtapeEntity || (model as MixtapeEntity).IsActive) && (model is not ISupportsSoftDelete || !(model as ISupportsSoftDelete).IsDeleted) ? model : default;
}
}
public interface IDbOperations
{
/// <summary>
/// Create a table if not existing
/// </summary>
bool EnsureTableExists<T>() where T : MixtapeIdEntity;
/// <summary>
/// Generate model Id by using configured document store conventions
/// </summary>
Task<string> GenerateId<T>(T model) where T : MixtapeIdEntity;
/// <summary>
/// Do only return the model when it is set to active or inactive entities are included with IncludeInactive()
/// </summary>
T WhenActive<T>(T model) where T : MixtapeIdEntity, new();
/// <summary>
/// Validates an entity
/// </summary>
Task<ValidationResult> Validate<T>(T model) where T : MixtapeIdEntity, new();
/// <summary>
/// Generate values for all properties (incl. nested) which contain the [GenerateId] attribute
/// </summary>
T AutoSetIds<T>(T model);
/// <summary>
/// Automatically fill base properties of a MixtapeEntity
/// </summary>
T PrepareForSave<T>(T model) where T : MixtapeIdEntity;
/// <summary>
/// Get an entity by Id
/// </summary>
Task<T> Load<T>(string id, string changeVector = null) where T : MixtapeIdEntity, new();
/// <summary>
/// Get entities by ids
/// </summary>
Task<Dictionary<string, T>> Load<T>(IEnumerable<string> ids) where T : MixtapeIdEntity, new();
/// <summary>
/// Get entities by ids
/// </summary>
Task<List<T>> LoadAsList<T>(IEnumerable<string> ids) where T : MixtapeIdEntity, new();
/// <summary>
/// Check if any items exist in this collection (with optional query)
/// </summary>
Task<bool> Any<T>(Expression<Func<T, bool>> querySelector = null) where T : MixtapeIdEntity, new();
/// <summary>
/// Get entities by query
/// </summary>
Task<List<T>> Load<T>(Expression<Func<T, bool>> querySelector) where T : MixtapeIdEntity, new();
/// <summary>
/// Find entity by query
/// </summary>
Task<T> Find<T>(Expression<Func<T, bool>> querySelector) where T : MixtapeIdEntity, new();
/// <summary>
/// Get entities by sql query
/// </summary>
Task<List<T>> LoadBySql<T>(Func<SqlExpression<T>, SqlExpression<T>> querySelector) where T : MixtapeIdEntity, new();
/// <summary>
/// Get all entities from this collection.
/// Warning: Don't use this method for large collections. Stream the results instead.
/// </summary>
Task<List<T>> LoadAll<T>() where T : MixtapeIdEntity, new();
/// <summary>
/// Creates an entity with an optional validator
/// </summary>
Task<Result<T>> Create<T>(T model, Func<T, Task<ValidationResult>> validate = null) where T : MixtapeIdEntity, new();
/// <summary>
/// Updates an entity with an optional validator
/// </summary>
Task<Result<T>> Update<T>(T model, Func<T, Task<ValidationResult>> validate = null) where T : MixtapeIdEntity, new();
/// <summary>
/// Checks if an entity exists (via ID) and creates or updates it afterwards accordingly
/// </summary>
Task<Result<T>> CreateOrUpdate<T>(T model, Func<T, Task<ValidationResult>> validate = null) where T : MixtapeIdEntity, new();
/// <summary>
/// Updates sorting of all items in a collection based on the given enumerable
/// </summary>
Task Sort<T>(IEnumerable<string> ids) where T : MixtapeEntity, new();
/// <summary>
/// Deletes an entity
/// </summary>
Task<Result<T>> Delete<T>(T model) where T : MixtapeIdEntity, new();
/// <summary>
/// Deletes an entity
/// </summary>
Task<Result<T>> Delete<T>(string id) where T : MixtapeIdEntity, new();
}
@@ -0,0 +1,21 @@
using System.Threading.Tasks;
using Mixtape.Communication;
using Mixtape.Models;
namespace Mixtape.Sqlite;
public interface IEntityModifiedHandler : IHandler
{
Task Saved<T>(T model, bool update) where T : MixtapeIdEntity, new() => update ? Updated(model) : Created(model);
Task Created<T>(T model) where T : MixtapeIdEntity, new();
Task Updated<T>(T model) where T : MixtapeIdEntity, new();
Task Deleted<T>(T model) where T : MixtapeIdEntity, new();
}
public class EmptyEntityModifiedHandler : IEntityModifiedHandler
{
public Task Created<T>(T model) where T : MixtapeIdEntity, new() => Task.CompletedTask;
public Task Updated<T>(T model) where T : MixtapeIdEntity, new() => Task.CompletedTask;
public Task Deleted<T>(T model) where T : MixtapeIdEntity, new() => Task.CompletedTask;
}
+26
View File
@@ -0,0 +1,26 @@
using System;
using Mixtape.Communication;
using Mixtape.Configuration;
using Mixtape.Context;
namespace Mixtape.Sqlite;
public class StoreContext
{
public IMixtapeContext Context { get; private set; }
public IMixtapeOptions Options { get; private set; }
public IServiceProvider Services { get; private set; }
public IMessageAggregator Messages { get; private set; }
public StoreContext(IMixtapeContext context, IServiceProvider serviceProvider, IMessageAggregator messages)
{
Options = context.Options;
Context = context;
Services = serviceProvider;
Messages = messages;
}
}
+11
View File
@@ -0,0 +1,11 @@
using System;
using System.Data;
namespace Mixtape.Sqlite;
public class SqliteOptions
{
public string ConnectionString { get; set; }
public Action<IDbConnection> OnConnectionCreate { get; set; }
}
+21
View File
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>Mixtape.Sqlite</PackageId>
<Version>1.0.0</Version>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
<RootNamespace>Mixtape.Sqlite</RootNamespace>
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="ServiceStack.OrmLite.Sqlite" Version="10.0.6" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Mixtape\Mixtape.csproj" />
</ItemGroup>
</Project>