Skip to content
Open
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
1,518 changes: 1,518 additions & 0 deletions Common/Migrations/20260612142230_RefactorPgEnums.Designer.cs

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions Common/Migrations/20260612142230_RefactorPgEnums.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace OpenShock.Common.Migrations
{
/// <inheritdoc />
public partial class RefactorPgEnums : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("ALTER TYPE shocker_model_type RENAME VALUE 'petTrainer' TO 'petrainer';");

migrationBuilder.AlterDatabase()
.Annotation("Npgsql:CollationDefinition:public.ndcoll", "und-u-ks-level2,und-u-ks-level2,icu,False")
.Annotation("Npgsql:Enum:configuration_value_type", "string,bool,int,float,json")
.Annotation("Npgsql:Enum:control_limit_mode", "clamp,lerp")
.Annotation("Npgsql:Enum:control_type", "stop,shock,vibrate,sound")
.Annotation("Npgsql:Enum:match_type_enum", "exact,contains")
.Annotation("Npgsql:Enum:ota_update_status", "started,running,finished,error,timeout")
.Annotation("Npgsql:Enum:password_encryption_type", "bcrypt_enhanced,pbkdf2")
.Annotation("Npgsql:Enum:permission_type", "shockers.use,shockers.edit,shockers.pause,devices.edit,devices.auth")
.Annotation("Npgsql:Enum:role_type", "support,staff,admin,system")
.Annotation("Npgsql:Enum:shocker_model_type", "caiXianlin,petrainer,petrainer998DR,wellturnT330")
.OldAnnotation("Npgsql:CollationDefinition:public.ndcoll", "und-u-ks-level2,und-u-ks-level2,icu,False")
.OldAnnotation("Npgsql:Enum:configuration_value_type", "string,bool,int,float,json")
.OldAnnotation("Npgsql:Enum:control_limit_mode", "clamp,lerp")
.OldAnnotation("Npgsql:Enum:control_type", "sound,vibrate,shock,stop")
.OldAnnotation("Npgsql:Enum:match_type_enum", "exact,contains")
.OldAnnotation("Npgsql:Enum:ota_update_status", "started,running,finished,error,timeout")
.OldAnnotation("Npgsql:Enum:password_encryption_type", "pbkdf2,bcrypt_enhanced")
.OldAnnotation("Npgsql:Enum:permission_type", "shockers.use,shockers.edit,shockers.pause,devices.edit,devices.auth")
.OldAnnotation("Npgsql:Enum:role_type", "support,staff,admin,system")
.OldAnnotation("Npgsql:Enum:shocker_model_type", "caiXianlin,petTrainer,petrainer998DR,wellturnT330");
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:CollationDefinition:public.ndcoll", "und-u-ks-level2,und-u-ks-level2,icu,False")
.Annotation("Npgsql:Enum:configuration_value_type", "string,bool,int,float,json")
.Annotation("Npgsql:Enum:control_limit_mode", "clamp,lerp")
.Annotation("Npgsql:Enum:control_type", "sound,vibrate,shock,stop")
.Annotation("Npgsql:Enum:match_type_enum", "exact,contains")
.Annotation("Npgsql:Enum:ota_update_status", "started,running,finished,error,timeout")
.Annotation("Npgsql:Enum:password_encryption_type", "pbkdf2,bcrypt_enhanced")
.Annotation("Npgsql:Enum:permission_type", "shockers.use,shockers.edit,shockers.pause,devices.edit,devices.auth")
.Annotation("Npgsql:Enum:role_type", "support,staff,admin,system")
.Annotation("Npgsql:Enum:shocker_model_type", "caiXianlin,petTrainer,petrainer998DR,wellturnT330")
.OldAnnotation("Npgsql:CollationDefinition:public.ndcoll", "und-u-ks-level2,und-u-ks-level2,icu,False")
.OldAnnotation("Npgsql:Enum:configuration_value_type", "string,bool,int,float,json")
.OldAnnotation("Npgsql:Enum:control_limit_mode", "clamp,lerp")
.OldAnnotation("Npgsql:Enum:control_type", "stop,shock,vibrate,sound")
.OldAnnotation("Npgsql:Enum:match_type_enum", "exact,contains")
.OldAnnotation("Npgsql:Enum:ota_update_status", "started,running,finished,error,timeout")
.OldAnnotation("Npgsql:Enum:password_encryption_type", "bcrypt_enhanced,pbkdf2")
.OldAnnotation("Npgsql:Enum:permission_type", "shockers.use,shockers.edit,shockers.pause,devices.edit,devices.auth")
.OldAnnotation("Npgsql:Enum:role_type", "support,staff,admin,system")
.OldAnnotation("Npgsql:Enum:shocker_model_type", "caiXianlin,petrainer,petrainer998DR,wellturnT330");

migrationBuilder.Sql("ALTER TYPE shocker_model_type RENAME VALUE 'petrainer' TO 'petTrainer';");
}
}
}
6 changes: 3 additions & 3 deletions Common/Migrations/OpenShockContextModelSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ protected override void BuildModel(ModelBuilder modelBuilder)

NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "configuration_value_type", new[] { "string", "bool", "int", "float", "json" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "control_limit_mode", new[] { "clamp", "lerp" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "control_type", new[] { "sound", "vibrate", "shock", "stop" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "control_type", new[] { "stop", "shock", "vibrate", "sound" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "match_type_enum", new[] { "exact", "contains" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "ota_update_status", new[] { "started", "running", "finished", "error", "timeout" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "password_encryption_type", new[] { "pbkdf2", "bcrypt_enhanced" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "password_encryption_type", new[] { "bcrypt_enhanced", "pbkdf2" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "permission_type", new[] { "shockers.use", "shockers.edit", "shockers.pause", "devices.edit", "devices.auth" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "role_type", new[] { "support", "staff", "admin", "system" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "shocker_model_type", new[] { "caiXianlin", "petTrainer", "petrainer998DR", "wellturnT330" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "shocker_model_type", new[] { "caiXianlin", "petrainer", "petrainer998DR", "wellturnT330" });
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);

modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
Expand Down
8 changes: 6 additions & 2 deletions Common/Models/ControlLimitMode.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
using NpgsqlTypes;
using OpenShock.Common.Utils;

namespace OpenShock.Common.Models;

/// <summary>
/// Determines how a per-token min/max limit is applied to an incoming control value.
/// </summary>
[PgEnum]
public enum ControlLimitMode
{
/// <summary>
/// Clamp the incoming value into the [min, max] range.
/// </summary>
Clamp = 0,
[PgName("clamp")] Clamp = 0,

/// <summary>
/// Linearly remap the full input range onto [min, max].
/// </summary>
Lerp = 1
[PgName("lerp")] Lerp = 1,
}
16 changes: 10 additions & 6 deletions Common/Models/ControlType.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
namespace OpenShock.Common.Models;
using NpgsqlTypes;
using OpenShock.Common.Utils;

namespace OpenShock.Common.Models;

[PgEnum]
public enum ControlType
{
Stop = 0,
Shock = 1,
Vibrate = 2,
Sound = 3
}
[PgName("stop")] Stop = 0,
[PgName("shock")] Shock = 1,
[PgName("vibrate")] Vibrate = 2,
[PgName("sound")] Sound = 3,
}
18 changes: 11 additions & 7 deletions Common/Models/OtaUpdateStatus.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
namespace OpenShock.Common.Models;
using NpgsqlTypes;
using OpenShock.Common.Utils;

namespace OpenShock.Common.Models;

[PgEnum]
public enum OtaUpdateStatus
{
Started,
Running,
Finished,
Error,
Timeout
}
[PgName("started")] Started,
[PgName("running")] Running,
[PgName("finished")] Finished,
[PgName("error")] Error,
[PgName("timeout")] Timeout,
}
12 changes: 8 additions & 4 deletions Common/Models/PasswordHashingAlgorithm.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
// ReSharper disable InconsistentNaming
// ReSharper disable InconsistentNaming
using NpgsqlTypes;
using OpenShock.Common.Utils;

namespace OpenShock.Common.Models;

[PgEnum("password_encryption_type")]
public enum PasswordHashingAlgorithm
{
Unknown = -1,
BCrypt = 0,
PBKDF2 = 1,
};
[PgName("bcrypt_enhanced")] BCrypt = 0,
[PgName("pbkdf2")] PBKDF2 = 1,
};
2 changes: 2 additions & 0 deletions Common/Models/PermissionType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
using System.Text.Json.Serialization;
using NpgsqlTypes;
using OpenShock.Common.JsonSerialization;
using OpenShock.Common.Utils;

// ReSharper disable InconsistentNaming

namespace OpenShock.Common.Models;

[PgEnum]
[JsonConverter(typeof(PermissionTypeConverter))]
public enum PermissionType
{
Expand Down
16 changes: 10 additions & 6 deletions Common/Models/RoleType.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
namespace OpenShock.Common.Models;
using NpgsqlTypes;
using OpenShock.Common.Utils;

namespace OpenShock.Common.Models;

[PgEnum]
public enum RoleType
{
Support,
Staff,
Admin,
System
}
[PgName("support")] Support,
[PgName("staff")] Staff,
[PgName("admin")] Admin,
[PgName("system")] System,
}
4 changes: 3 additions & 1 deletion Common/Models/ShockerModelType.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using NpgsqlTypes;
using OpenShock.Common.Utils;

namespace OpenShock.Common.Models;

[PgEnum]
public enum ShockerModelType
{
[PgName("caiXianlin")] CaiXianlin = 0,
[PgName("petTrainer")] PetTrainer = 1, // Misspelled, should be "petrainer",
[PgName("petrainer")] PetTrainer = 1,
[PgName("petrainer998DR")] Petrainer998DR = 2,
Comment on lines 9 to 11
[PgName("wellturnT330")] WellturnT330 = 3,
}
16 changes: 10 additions & 6 deletions Common/OpenShockDb/ConfigurationItem.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
namespace OpenShock.Common.OpenShockDb;
using NpgsqlTypes;
using OpenShock.Common.Utils;

namespace OpenShock.Common.OpenShockDb;

[PgEnum]
public enum ConfigurationValueType
{
String,
Bool,
Int,
Float,
Json
[PgName("string")] String,
[PgName("bool")] Bool,
[PgName("int")] Int,
[PgName("float")] Float,
[PgName("json")] Json,
}

public sealed class ConfigurationItem
Expand Down
69 changes: 69 additions & 0 deletions Common/OpenShockDb/NpgsqlEnumExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
using NpgsqlTypes;
using OpenShock.Common.Models;
using OpenShock.Common.Utils;

namespace OpenShock.Common.OpenShockDb;

public static partial class NpgsqlEnumExtensions
{
[GeneratedRegex("([a-z])([A-Z0-9])")]
private static partial Regex SnakeCaseRegex();

private static string ToSnakeCase(string name) =>
SnakeCaseRegex().Replace(name, "$1_$2").ToLowerInvariant();

private readonly record struct PgEnumInfo(
Action<NpgsqlDbContextOptionsBuilder> Map,
Action<ModelBuilder> Register);

private static PgEnumInfo BuildInfo<TEnum>() where TEnum : struct, Enum
{
var type = typeof(TEnum);
var attr = type.GetCustomAttribute<PgEnumAttribute>()
?? throw new InvalidOperationException($"{type.Name} is missing [PgEnum]");

var pgTypeName = attr.Name ?? ToSnakeCase(type.Name);

var members = type
.GetFields(BindingFlags.Public | BindingFlags.Static)
.Select(f => f.GetCustomAttribute<PgNameAttribute>()?.PgName)
.Where(n => n is not null)
.Cast<string>()
.ToArray();

return new PgEnumInfo(
Map: b => b.MapEnum<TEnum>(pgTypeName),
Register: m => m.HasPostgresEnum(attr.Schema, pgTypeName, members));
Comment on lines +38 to +40
}

private static readonly PgEnumInfo[] Enums =
[
BuildInfo<ControlType>(),
BuildInfo<ControlLimitMode>(),
BuildInfo<OtaUpdateStatus>(),
BuildInfo<PasswordHashingAlgorithm>(),
BuildInfo<PermissionType>(),
BuildInfo<RoleType>(),
BuildInfo<ShockerModelType>(),
BuildInfo<MatchTypeEnum>(),
BuildInfo<ConfigurationValueType>(),
];

public static NpgsqlDbContextOptionsBuilder MapPgEnums(this NpgsqlDbContextOptionsBuilder builder)
{
foreach (var info in Enums)
info.Map(builder);
return builder;
}

public static ModelBuilder RegisterPgEnums(this ModelBuilder modelBuilder)
{
foreach (var info in Enums)
info.Register(modelBuilder);
return modelBuilder;
}
}
23 changes: 2 additions & 21 deletions Common/OpenShockDb/OpenShockContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,7 @@ public OpenShockContext(DbContextOptions<OpenShockContext> options)
public static void ConfigureOptionsBuilder(DbContextOptionsBuilder optionsBuilder, string connectionString,
bool debug)
{
optionsBuilder.UseNpgsql(connectionString, npgsqlBuilder =>
{
npgsqlBuilder.MapEnum<RoleType>();
npgsqlBuilder.MapEnum<ControlType>();
npgsqlBuilder.MapEnum<ControlLimitMode>();
npgsqlBuilder.MapEnum<PermissionType>();
npgsqlBuilder.MapEnum<ShockerModelType>();
npgsqlBuilder.MapEnum<OtaUpdateStatus>();
npgsqlBuilder.MapEnum<MatchTypeEnum>();
npgsqlBuilder.MapEnum<ConfigurationValueType>();
});
optionsBuilder.UseNpgsql(connectionString, npgsqlBuilder => npgsqlBuilder.MapPgEnums());

if (debug)
{
Expand Down Expand Up @@ -141,16 +131,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.HasPostgresEnum("control_type", ["sound", "vibrate", "shock", "stop"])
.HasPostgresEnum("control_limit_mode", ["clamp", "lerp"])
.HasPostgresEnum("ota_update_status", ["started", "running", "finished", "error", "timeout"])
.HasPostgresEnum("password_encryption_type", ["pbkdf2", "bcrypt_enhanced"])
.HasPostgresEnum("permission_type",
["shockers.use", "shockers.edit", "shockers.pause", "devices.edit", "devices.auth"])
.HasPostgresEnum("role_type", ["support", "staff", "admin", "system"])
.HasPostgresEnum("shocker_model_type", ["caiXianlin", "petTrainer", "petrainer998DR", "wellturnT330"])
.HasPostgresEnum("match_type_enum", ["exact", "contains"])
.HasPostgresEnum("configuration_value_type", ["string", "bool", "int", "float", "json"])
.RegisterPgEnums()
.HasCollation("public", "ndcoll", "und-u-ks-level2", "icu", false); // Add case-insensitive, accent-sensitive comparison collation

modelBuilder.Entity<ApiToken>(entity =>
Expand Down
10 changes: 7 additions & 3 deletions Common/OpenShockDb/UserNameBlacklist.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
namespace OpenShock.Common.OpenShockDb;
using NpgsqlTypes;
using OpenShock.Common.Utils;

namespace OpenShock.Common.OpenShockDb;

[PgEnum]
public enum MatchTypeEnum
{
Exact,
Contains,
[PgName("exact")] Exact,
[PgName("contains")] Contains,
}

public sealed class UserNameBlacklist
Expand Down
16 changes: 16 additions & 0 deletions Common/Utils/PgEnumAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace OpenShock.Common.Utils;

/// <summary>
/// Marks an enum as a Postgres native enum type so it is automatically registered
/// with the EF Core model via <see cref="NpgsqlEnumExtensions.RegisterPgEnums"/>.

Check warning on line 5 in Common/Utils/PgEnumAttribute.cs

View workflow job for this annotation

GitHub Actions / Tests

XML comment has cref attribute 'RegisterPgEnums' that could not be resolved

Check warning on line 5 in Common/Utils/PgEnumAttribute.cs

View workflow job for this annotation

GitHub Actions / Tests

XML comment has cref attribute 'RegisterPgEnums' that could not be resolved
/// </summary>
Comment on lines +4 to +6
/// <param name="name">
/// Explicit Postgres type name. When omitted, the C# type name is converted to snake_case.
/// </param>
/// <param name="schema">Postgres schema; defaults to the model's default schema when null.</param>
[AttributeUsage(AttributeTargets.Enum)]
public sealed class PgEnumAttribute(string? name = null, string? schema = null) : Attribute
{
public string? Name { get; } = name;
public string? Schema { get; } = schema;
}
Loading