Updated settings service to support generic types with JSON serialization/deserialization, adjusted setup wizard checks and modified database schema for proper JSONB storage.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace Moonlight.Api.Database.Entities;
|
||||
|
||||
@@ -10,5 +11,6 @@ public class SettingsOption
|
||||
public required string Key { get; set; }
|
||||
|
||||
[MaxLength(4096)]
|
||||
public required string Value { get; set; }
|
||||
[Column(TypeName = "jsonb")]
|
||||
public required string ValueJson { get; set; }
|
||||
}
|
||||
251
Moonlight.Api/Database/Migrations/20260129134620_SwitchedToJsonForSettingsOption.Designer.cs
generated
Normal file
251
Moonlight.Api/Database/Migrations/20260129134620_SwitchedToJsonForSettingsOption.Designer.cs
generated
Normal file
@@ -0,0 +1,251 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Moonlight.Api.Database;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Moonlight.Api.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20260129134620_SwitchedToJsonForSettingsOption")]
|
||||
partial class SwitchedToJsonForSettingsOption
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("core")
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Moonlight.Api.Database.Entities.ApiKey", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Permissions")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ApiKeys", "core");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("Permissions")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Roles", "core");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("RoleId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("RoleMembers", "core");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.Api.Database.Entities.SettingsOption", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ValueJson")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("SettingsOptions", "core");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.Api.Database.Entities.Theme", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Author")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("CssContent")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20000)
|
||||
.HasColumnType("character varying(20000)");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Themes", "core");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(254)
|
||||
.HasColumnType("character varying(254)");
|
||||
|
||||
b.Property<DateTimeOffset>("InvalidateTimestamp")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users", "core");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.Api.Database.Entities.RoleMember", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.Api.Database.Entities.Role", "Role")
|
||||
.WithMany("Members")
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Moonlight.Api.Database.Entities.User", "User")
|
||||
.WithMany("RoleMemberships")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.Api.Database.Entities.Role", b =>
|
||||
{
|
||||
b.Navigation("Members");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.Api.Database.Entities.User", b =>
|
||||
{
|
||||
b.Navigation("RoleMemberships");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Moonlight.Api.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SwitchedToJsonForSettingsOption : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Value",
|
||||
schema: "core",
|
||||
table: "SettingsOptions");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ValueJson",
|
||||
schema: "core",
|
||||
table: "SettingsOptions",
|
||||
type: "jsonb",
|
||||
maxLength: 4096,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ValueJson",
|
||||
schema: "core",
|
||||
table: "SettingsOptions");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Value",
|
||||
schema: "core",
|
||||
table: "SettingsOptions",
|
||||
type: "character varying(4096)",
|
||||
maxLength: 4096,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,10 +136,10 @@ namespace Moonlight.Api.Database.Migrations
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
b.Property<string>("ValueJson")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)");
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
|
||||
@@ -34,12 +34,12 @@ public class SetupController : Controller
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult> GetSetupAsync()
|
||||
{
|
||||
var hasBeenSetup = await SettingsService.GetValueAsync(StateSettingsKey);
|
||||
var hasBeenSetup = await SettingsService.GetValueAsync<bool>(StateSettingsKey);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(hasBeenSetup) && hasBeenSetup.Equals("true", StringComparison.OrdinalIgnoreCase))
|
||||
if (hasBeenSetup)
|
||||
return Problem("This instance is already configured", statusCode: 405);
|
||||
|
||||
return Ok();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@@ -120,7 +120,7 @@ public class SetupController : Controller
|
||||
await UsersRepository.UpdateAsync(user);
|
||||
}
|
||||
|
||||
await SettingsService.SetValueAsync(StateSettingsKey, "true");
|
||||
await SettingsService.SetValueAsync(StateSettingsKey, true);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moonlight.Api.Configuration;
|
||||
@@ -26,31 +27,32 @@ public class SettingsService
|
||||
Options = options;
|
||||
}
|
||||
|
||||
public async Task<string?> GetValueAsync(string key)
|
||||
public async Task<T?> GetValueAsync<T>(string key)
|
||||
{
|
||||
var cacheKey = string.Format(CacheKey, key);
|
||||
|
||||
if (!Cache.TryGetValue<string>(cacheKey, out var value))
|
||||
{
|
||||
var retrievedValue = await Repository
|
||||
.Query()
|
||||
.Where(x => x.Key == key)
|
||||
.Select(o => o.Value)
|
||||
.FirstOrDefaultAsync();
|
||||
if (Cache.TryGetValue<string>(cacheKey, out var value))
|
||||
return JsonSerializer.Deserialize<T>(value!);
|
||||
|
||||
value = await Repository
|
||||
.Query()
|
||||
.Where(x => x.Key == key)
|
||||
.Select(o => o.ValueJson)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if(string.IsNullOrEmpty(value))
|
||||
return default;
|
||||
|
||||
Cache.Set(
|
||||
cacheKey,
|
||||
retrievedValue,
|
||||
TimeSpan.FromMinutes(Options.Value.CacheMinutes)
|
||||
);
|
||||
Cache.Set(
|
||||
cacheKey,
|
||||
value,
|
||||
TimeSpan.FromMinutes(Options.Value.CacheMinutes)
|
||||
);
|
||||
|
||||
return retrievedValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
return JsonSerializer.Deserialize<T>(value);
|
||||
}
|
||||
|
||||
public async Task SetValueAsync(string key, string value)
|
||||
public async Task SetValueAsync<T>(string key, T value)
|
||||
{
|
||||
var cacheKey = string.Format(CacheKey, key);
|
||||
|
||||
@@ -58,9 +60,11 @@ public class SettingsService
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Key == key);
|
||||
|
||||
var json = JsonSerializer.Serialize(value);
|
||||
|
||||
if (option != null)
|
||||
{
|
||||
option.Value = value;
|
||||
option.ValueJson = json;
|
||||
await Repository.UpdateAsync(option);
|
||||
}
|
||||
else
|
||||
@@ -68,7 +72,7 @@ public class SettingsService
|
||||
option = new SettingsOption()
|
||||
{
|
||||
Key = key,
|
||||
Value = value
|
||||
ValueJson = json
|
||||
};
|
||||
|
||||
await Repository.AddAsync(option);
|
||||
|
||||
Reference in New Issue
Block a user