Merge branch 'p9-w1-t1-arch-lift' — BlazorBlocks data lift, project renames, initial Postgres migration

This commit is contained in:
Daniel Harvey
2026-05-18 22:30:11 -04:00
82 changed files with 511 additions and 600 deletions
+2 -2
View File
@@ -2,9 +2,9 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftCli", "DeepDrftCli\DeepDrftCli.csproj", "{84844B37-FD15-4AFC-850B-DD432AA33B4C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftContent.Services", "DeepDrftContent.Services\DeepDrftContent.Services.csproj", "{169D5D3E-DAEC-46BE-98EE-CC5EBF5E3E8A}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftContent.Data", "DeepDrftContent.Data\DeepDrftContent.Data.csproj", "{169D5D3E-DAEC-46BE-98EE-CC5EBF5E3E8A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftWeb.Services", "DeepDrftWeb.Services\DeepDrftWeb.Services.csproj", "{A3DA341B-589E-4705-AB66-6B22652A9B36}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftData", "DeepDrftData\DeepDrftData.csproj", "{A3DA341B-589E-4705-AB66-6B22652A9B36}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetBlocks", "C:\lib\NetBlocks\NetBlocks.csproj", "{41FC69D0-F60D-41B4-AA41-C2382C83DFE8}"
EndProject
+2 -2
View File
@@ -23,8 +23,8 @@
<ItemGroup>
<ProjectReference Include="..\DeepDrftModels\DeepDrftModels.csproj" />
<ProjectReference Include="..\DeepDrftWeb.Services\DeepDrftWeb.Services.csproj" />
<ProjectReference Include="..\DeepDrftContent.Services\DeepDrftContent.Services.csproj" />
<ProjectReference Include="..\DeepDrftData\DeepDrftData.csproj" />
<ProjectReference Include="..\DeepDrftContent.Data\DeepDrftContent.Data.csproj" />
</ItemGroup>
<ItemGroup>
+11 -8
View File
@@ -3,10 +3,11 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.EntityFrameworkCore;
using DeepDrftWeb.Services.Data;
using DeepDrftWeb.Services.Repositories;
using DeepDrftContent.Services.FileDatabase.Services;
using DeepDrftContent.Services.Processors;
using DeepDrftData;
using DeepDrftData.Data;
using DeepDrftData.Repositories;
using DeepDrftContent.Data.FileDatabase.Services;
using DeepDrftContent.Data.Processors;
using DeepDrftCli.Services;
using DeepDrftCli.Models;
using NetBlocks.Utilities.Environment;
@@ -49,11 +50,13 @@ builder.Services.AddSingleton<FileDatabase>(provider =>
}
});
// Add services
// Add services. TrackManager fronts the BlazorBlocks data layer and implements
// ITrackService for legacy consumers; same scoped instance backs both registrations.
builder.Services.AddScoped<TrackRepository>();
builder.Services.AddScoped<DeepDrftWeb.Services.ITrackService, DeepDrftWeb.Services.TrackService>();
builder.Services.AddScoped<TrackManager>();
builder.Services.AddScoped<ITrackService>(sp => sp.GetRequiredService<TrackManager>());
builder.Services.AddScoped<AudioProcessor>();
builder.Services.AddScoped<DeepDrftContent.Services.TrackService>();
builder.Services.AddScoped<DeepDrftContent.Data.TrackService>();
builder.Services.AddScoped<CliService>();
builder.Services.AddScoped<GuiService>();
@@ -72,4 +75,4 @@ else
// Run traditional CLI mode
var cliService = app.Services.GetRequiredService<CliService>();
await cliService.RunAsync(args);
}
}
+5 -5
View File
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using DeepDrftContent.Services;
using DeepDrftContent.Data;
using DeepDrftModels.Entities;
using NetBlocks.Models;
using DeepDrftCli.Utils;
@@ -13,13 +13,13 @@ namespace DeepDrftCli.Services;
public class CliService
{
private readonly ILogger<CliService> _logger;
private readonly DeepDrftWeb.Services.ITrackService _webTrackService;
private readonly DeepDrftContent.Services.TrackService _contentTrackService;
private readonly DeepDrftData.ITrackService _webTrackService;
private readonly DeepDrftContent.Data.TrackService _contentTrackService;
public CliService(
ILogger<CliService> logger,
DeepDrftWeb.Services.ITrackService webTrackService,
DeepDrftContent.Services.TrackService contentTrackService)
DeepDrftData.ITrackService webTrackService,
DeepDrftContent.Data.TrackService contentTrackService)
{
_logger = logger;
_webTrackService = webTrackService;
+4 -4
View File
@@ -11,8 +11,8 @@ namespace DeepDrftCli.Services;
public class GuiService
{
private readonly ILogger<GuiService> _logger;
private readonly DeepDrftWeb.Services.ITrackService _webTrackService;
private readonly DeepDrftContent.Services.TrackService _contentTrackService;
private readonly DeepDrftData.ITrackService _webTrackService;
private readonly DeepDrftContent.Data.TrackService _contentTrackService;
// GUI Components
private Window? _mainWindow;
@@ -24,8 +24,8 @@ public class GuiService
public GuiService(
ILogger<GuiService> logger,
DeepDrftWeb.Services.ITrackService webTrackService,
DeepDrftContent.Services.TrackService contentTrackService)
DeepDrftData.ITrackService webTrackService,
DeepDrftContent.Data.TrackService contentTrackService)
{
_logger = logger;
_webTrackService = webTrackService;
+1 -1
View File
@@ -21,7 +21,7 @@
<ItemGroup>
<ProjectReference Include="..\DeepDrftModels\DeepDrftModels.csproj" />
<ProjectReference Include="..\DeepDrftWeb.Services\DeepDrftWeb.Services.csproj" />
<ProjectReference Include="..\DeepDrftData\DeepDrftData.csproj" />
</ItemGroup>
</Project>
+1 -1
View File
@@ -3,7 +3,7 @@
@rendermode InteractiveServer
@using AuthBlocksWeb.HierarchicalAuthorize
@using AuthBlocksWeb.Services
@using DeepDrftWeb.Services
@using DeepDrftData
@using System.Net.Http.Headers
@using System.Net.Http.Json
@attribute [HierarchicalRoleAuthorize("Admin")]
+1 -1
View File
@@ -3,7 +3,7 @@
@using System.Net
@using System.Net.Http.Headers
@using AuthBlocksWeb.HierarchicalAuthorize
@using DeepDrftModels.Models
@using Models.Common
@attribute [HierarchicalRoleAuthorize("Admin")]
@inject ITrackService TrackService
@inject IHttpClientFactory HttpClientFactory
+2 -1
View File
@@ -9,5 +9,6 @@
@using Microsoft.JSInterop
@using DeepDrftCms
@using DeepDrftModels.Entities
@using DeepDrftWeb.Services
@using DeepDrftData
@using Models.Common
@using MudBlazor
@@ -1,6 +1,6 @@
using System.Text;
namespace DeepDrftContent.Services.Audio;
namespace DeepDrftContent.Data.Audio;
/// <summary>
/// Service for creating WAV audio streams starting from a byte offset.
@@ -1,4 +1,4 @@
namespace DeepDrftContent.Services.Constants;
namespace DeepDrftContent.Data.Constants;
/// <summary>
/// Constants for FileDatabase vault names
@@ -1,7 +1,7 @@
using DeepDrftContent.Services.FileDatabase.Models;
using IndexType = DeepDrftContent.Services.FileDatabase.Services.IndexType;
using DeepDrftContent.Data.FileDatabase.Models;
using IndexType = DeepDrftContent.Data.FileDatabase.Services.IndexType;
namespace DeepDrftContent.Services.FileDatabase.Abstractions;
namespace DeepDrftContent.Data.FileDatabase.Abstractions;
/// <summary>
/// Interface for creating index instances
@@ -1,7 +1,7 @@
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Services.FileDatabase.Services;
using DeepDrftContent.Data.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Services;
namespace DeepDrftContent.Services.FileDatabase.Abstractions;
namespace DeepDrftContent.Data.FileDatabase.Abstractions;
/// <summary>
/// Interface for registering media type factories
@@ -1,4 +1,4 @@
namespace DeepDrftContent.Services.FileDatabase.Models;
namespace DeepDrftContent.Data.FileDatabase.Models;
/// <summary>
/// Base interface for all index types - minimal contract
@@ -1,7 +1,7 @@
using DeepDrftContent.Services.FileDatabase.Utils;
using DeepDrftContent.Data.FileDatabase.Utils;
using System.Text.Json.Serialization;
namespace DeepDrftContent.Services.FileDatabase.Models;
namespace DeepDrftContent.Data.FileDatabase.Models;
/// <summary>
/// Base class for index data used in serialization
@@ -1,7 +1,7 @@
using DeepDrftContent.Services.FileDatabase.Abstractions;
using DeepDrftContent.Services.FileDatabase.Services;
using DeepDrftContent.Data.FileDatabase.Abstractions;
using DeepDrftContent.Data.FileDatabase.Services;
namespace DeepDrftContent.Services.FileDatabase.Models;
namespace DeepDrftContent.Data.FileDatabase.Models;
/// <summary>
/// Shared media type registry instance — one allocation for all factory classes in this file.
@@ -1,4 +1,4 @@
namespace DeepDrftContent.Services.FileDatabase.Models;
namespace DeepDrftContent.Data.FileDatabase.Models;
/// <summary>
/// Parameters for creating a FileBinary
@@ -1,4 +1,4 @@
namespace DeepDrftContent.Services.FileDatabase.Models;
namespace DeepDrftContent.Data.FileDatabase.Models;
/// <summary>
/// Enum representing different types of media vaults
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace DeepDrftContent.Services.FileDatabase.Models;
namespace DeepDrftContent.Data.FileDatabase.Models;
/// <summary>
/// Base metadata for media entries
@@ -1,8 +1,8 @@
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Services.FileDatabase.Utils;
using DeepDrftContent.Data.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Utils;
using Microsoft.Extensions.Logging;
namespace DeepDrftContent.Services.FileDatabase.Services;
namespace DeepDrftContent.Data.FileDatabase.Services;
/// <summary>
/// Main file database class that orchestrates multiple media vaults.
@@ -1,9 +1,9 @@
using DeepDrftContent.Services.FileDatabase.Abstractions;
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Services.FileDatabase.Utils;
using IndexType = DeepDrftContent.Services.FileDatabase.Services.IndexType;
using DeepDrftContent.Data.FileDatabase.Abstractions;
using DeepDrftContent.Data.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Utils;
using IndexType = DeepDrftContent.Data.FileDatabase.Services.IndexType;
namespace DeepDrftContent.Services.FileDatabase.Services;
namespace DeepDrftContent.Data.FileDatabase.Services;
/// <summary>
/// Factory service for creating and managing indexes
@@ -1,9 +1,9 @@
using DeepDrftContent.Services.FileDatabase.Abstractions;
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Services.FileDatabase.Utils;
using DeepDrftContent.Data.FileDatabase.Abstractions;
using DeepDrftContent.Data.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Utils;
using Microsoft.Extensions.Logging;
namespace DeepDrftContent.Services.FileDatabase.Services;
namespace DeepDrftContent.Data.FileDatabase.Services;
/// <summary>
/// Enum representing different types of indexes
@@ -1,7 +1,7 @@
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Models;
using Microsoft.Extensions.Logging;
namespace DeepDrftContent.Services.FileDatabase.Services;
namespace DeepDrftContent.Data.FileDatabase.Services;
/// <summary>
/// Watches index files for external modifications and triggers reloads.
@@ -1,8 +1,8 @@
using System.Text.RegularExpressions;
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Services.FileDatabase.Utils;
using DeepDrftContent.Data.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Utils;
namespace DeepDrftContent.Services.FileDatabase.Services;
namespace DeepDrftContent.Data.FileDatabase.Services;
/// <summary>
/// Abstract base class for media vaults that store and manage media files
@@ -1,6 +1,6 @@
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Models;
namespace DeepDrftContent.Services.FileDatabase.Services;
namespace DeepDrftContent.Data.FileDatabase.Services;
/// <summary>
/// Factory for creating media vaults
@@ -1,7 +1,7 @@
using DeepDrftContent.Services.FileDatabase.Abstractions;
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Abstractions;
using DeepDrftContent.Data.FileDatabase.Models;
namespace DeepDrftContent.Services.FileDatabase.Services;
namespace DeepDrftContent.Data.FileDatabase.Services;
/// <summary>
/// Simple dictionary-based registry for media type factories
@@ -1,7 +1,7 @@
using System.Text.Json;
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Models;
namespace DeepDrftContent.Services.FileDatabase.Utils;
namespace DeepDrftContent.Data.FileDatabase.Utils;
/// <summary>
/// Utility class for file I/O operations, matching the TypeScript file utilities
@@ -1,7 +1,7 @@
using System.Collections;
using System.Text.Json;
namespace DeepDrftContent.Services.FileDatabase.Utils;
namespace DeepDrftContent.Data.FileDatabase.Utils;
/// <summary>
/// A map implementation that uses structural equality for keys by serializing them to JSON.
@@ -1,7 +1,7 @@
using System.Collections;
using System.Text.Json;
namespace DeepDrftContent.Services.FileDatabase.Utils;
namespace DeepDrftContent.Data.FileDatabase.Utils;
/// <summary>
/// A set implementation that uses structural equality for values by serializing them to JSON.
@@ -1,6 +1,6 @@
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Models;
namespace DeepDrftContent.Services.Processors;
namespace DeepDrftContent.Data.Processors;
/// <summary>
/// Service for processing audio files and extracting metadata
@@ -1,9 +1,9 @@
using DeepDrftContent.Services.Constants;
using DeepDrftContent.Services.FileDatabase.Services;
using DeepDrftContent.Services.Processors;
using DeepDrftContent.Data.Constants;
using DeepDrftContent.Data.FileDatabase.Services;
using DeepDrftContent.Data.Processors;
using DeepDrftModels.Entities;
namespace DeepDrftContent.Services;
namespace DeepDrftContent.Data;
/// <summary>
/// Service for managing tracks in both SQL and FileDatabase
@@ -52,7 +52,7 @@ public class TrackService
// Ensure tracks vault exists
if (!_fileDatabase.HasVault(VaultConstants.Tracks))
{
await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, DeepDrftContent.Services.FileDatabase.Models.MediaVaultType.Audio);
await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, DeepDrftContent.Data.FileDatabase.Models.MediaVaultType.Audio);
}
// Store the audio in FileDatabase
@@ -87,9 +87,9 @@ public class TrackService
/// </summary>
/// <param name="trackId">Track ID (EntryKey)</param>
/// <returns>Audio binary or null if not found</returns>
public async Task<DeepDrftContent.Services.FileDatabase.Models.AudioBinary?> GetAudioBinaryAsync(string trackId)
public async Task<DeepDrftContent.Data.FileDatabase.Models.AudioBinary?> GetAudioBinaryAsync(string trackId)
{
return await _fileDatabase.LoadResourceAsync<DeepDrftContent.Services.FileDatabase.Models.AudioBinary>(VaultConstants.Tracks, trackId);
return await _fileDatabase.LoadResourceAsync<DeepDrftContent.Data.FileDatabase.Models.AudioBinary>(VaultConstants.Tracks, trackId);
}
/// <summary>
@@ -107,7 +107,7 @@ public class TrackService
{
if (!_fileDatabase.HasVault(VaultConstants.Tracks))
{
await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, DeepDrftContent.Services.FileDatabase.Models.MediaVaultType.Audio);
await _fileDatabase.CreateVaultAsync(VaultConstants.Tracks, DeepDrftContent.Data.FileDatabase.Models.MediaVaultType.Audio);
}
}
}
}
@@ -1,7 +1,7 @@
using DeepDrftContent.Services.Audio;
using DeepDrftContent.Services.Constants;
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Services.FileDatabase.Services;
using DeepDrftContent.Data.Audio;
using DeepDrftContent.Data.Constants;
using DeepDrftContent.Data.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Services;
using DeepDrftContent.Middleware;
using Microsoft.AspNetCore.Mvc;
@@ -11,7 +11,7 @@ namespace DeepDrftContent.Controllers;
[Route("api/[controller]")]
public class TrackController : ControllerBase
{
private readonly DeepDrftContent.Services.TrackService _trackService;
private readonly DeepDrftContent.Data.TrackService _trackService;
private readonly WavOffsetService _wavOffsetService;
private readonly ILogger<TrackController> _logger;
@@ -19,11 +19,11 @@ public class TrackController : ControllerBase
// AudioBinaryDto over the wire, not a WAV file path. TrackService.AddTrackFromWavAsync is
// file-path-oriented and not applicable here. If a file-upload flow is added in future,
// route it through TrackService instead.
private readonly DeepDrftContent.Services.FileDatabase.Services.FileDatabase _fileDatabase;
private readonly DeepDrftContent.Data.FileDatabase.Services.FileDatabase _fileDatabase;
public TrackController(
DeepDrftContent.Services.TrackService trackService,
DeepDrftContent.Services.FileDatabase.Services.FileDatabase fileDatabase,
DeepDrftContent.Data.TrackService trackService,
DeepDrftContent.Data.FileDatabase.Services.FileDatabase fileDatabase,
WavOffsetService wavOffsetService,
ILogger<TrackController> logger)
{
@@ -245,7 +245,7 @@ public class TrackController : ControllerBase
// Direct FileDatabase write: this endpoint receives an already-processed AudioBinaryDto,
// not a WAV file, so TrackService.AddTrackFromWavAsync does not apply. See constructor comment.
var success = await _fileDatabase.RegisterResourceAsync(
DeepDrftContent.Services.Constants.VaultConstants.Tracks, trackId, audioBinary);
DeepDrftContent.Data.Constants.VaultConstants.Tracks, trackId, audioBinary);
return success ? Ok() : BadRequest("Failed to store audio track");
}
+1 -1
View File
@@ -12,7 +12,7 @@
<ItemGroup>
<ProjectReference Include="..\DeepDrftModels\DeepDrftModels.csproj" />
<ProjectReference Include="..\DeepDrftContent.Services\DeepDrftContent.Services.csproj" />
<ProjectReference Include="..\DeepDrftContent.Data\DeepDrftContent.Data.csproj" />
</ItemGroup>
<ItemGroup>
+1 -1
View File
@@ -1,5 +1,5 @@
using DeepDrftContent;
using DeepDrftContent.Services.FileDatabase.Services;
using DeepDrftContent.Data.FileDatabase.Services;
using DeepDrftContent.Middleware;
using DeepDrftContent.Models;
using Microsoft.AspNetCore.HttpOverrides;
+6 -6
View File
@@ -1,9 +1,9 @@
using DeepDrftContent.Services;
using DeepDrftContent.Services.Audio;
using DeepDrftContent.Services.Constants;
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Services.FileDatabase.Services;
using DeepDrftContent.Services.Processors;
using DeepDrftContent.Data;
using DeepDrftContent.Data.Audio;
using DeepDrftContent.Data.Constants;
using DeepDrftContent.Data.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Services;
using DeepDrftContent.Data.Processors;
using DeepDrftContent.Models;
using Microsoft.Extensions.Logging;
using NetBlocks.Utilities.Environment;
@@ -0,0 +1,61 @@
using Data.Data.Configurations;
using DeepDrftModels.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DeepDrftData.Data.Configurations;
public class TrackConfiguration : BaseEntityConfiguration<TrackEntity>
{
public override void Configure(EntityTypeBuilder<TrackEntity> builder)
{
// Wires up Id PK + audit columns (CreatedAt, UpdatedAt, IsDeleted) and the IsDeleted index.
base.Configure(builder);
builder.ToTable("track");
// Map the base audit columns to the snake_case naming the rest of the schema uses.
builder.Property(e => e.Id).HasColumnName("id");
builder.Property(e => e.CreatedAt).HasColumnName("created_at");
builder.Property(e => e.UpdatedAt).HasColumnName("updated_at");
builder.Property(e => e.IsDeleted).HasColumnName("is_deleted");
builder.Property(e => e.EntryKey)
.IsRequired()
.HasMaxLength(100)
.HasColumnName("entry_key");
builder.Property(e => e.TrackName)
.IsRequired()
.HasMaxLength(200)
.HasColumnName("track_name");
builder.Property(e => e.Artist)
.IsRequired()
.HasMaxLength(200)
.HasColumnName("artist");
builder.Property(e => e.Album)
.HasMaxLength(200)
.HasColumnName("album");
builder.Property(e => e.Genre)
.HasMaxLength(100)
.HasColumnName("genre");
builder.Property(e => e.ReleaseDate)
.HasColumnName("release_date");
builder.Property(e => e.ImagePath)
.HasMaxLength(500)
.HasColumnName("image_path");
builder.Property(e => e.CreatedByUserId)
.HasColumnName("created_by_user_id");
// Explicit index on is_deleted so soft-delete global query filters are
// not full table scans. base.Configure may or may not add this depending
// on the BlazorBlocks.Data version; declaring it here guarantees it.
builder.HasIndex(e => e.IsDeleted).HasDatabaseName("IX_track_is_deleted");
}
}
@@ -1,8 +1,8 @@
using DeepDrftModels.Entities;
using DeepDrftWeb.Services.Data.Configurations;
using DeepDrftData.Data.Configurations;
using Microsoft.EntityFrameworkCore;
namespace DeepDrftWeb.Services.Data;
namespace DeepDrftData.Data;
public class DeepDrftContext : DbContext
{
@@ -15,7 +15,7 @@ public class DeepDrftContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfiguration(new TrackConfiguration());
}
}
}
@@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace DeepDrftWeb.Services.Data;
namespace DeepDrftData.Data;
public class DeepDrftContextFactory : IDesignTimeDbContextFactory<DeepDrftContext>
{
@@ -19,4 +19,4 @@ public class DeepDrftContextFactory : IDesignTimeDbContextFactory<DeepDrftContex
return new DeepDrftContext(optionsBuilder.Options);
}
}
}
@@ -18,6 +18,8 @@
</PackageReference>
<!-- Npgsql 10.0.1 requires Microsoft.EntityFrameworkCore >= 10.0.4; keep in sync -->
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
<PackageReference Include="Cerebellum.BlazorBlocks.Data" Version="10.3.30" />
<PackageReference Include="Cerebellum.BlazorBlocks.Data.Postgres" Version="10.3.30" />
</ItemGroup>
<ItemGroup>
@@ -1,8 +1,8 @@
using DeepDrftModels.Entities;
using DeepDrftModels.Models;
using Models.Common;
using NetBlocks.Models;
namespace DeepDrftWeb.Services;
namespace DeepDrftData;
public interface ITrackService
{
@@ -1,6 +1,6 @@
// <auto-generated />
using System;
using DeepDrftWeb.Services.Data;
using DeepDrftData.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@@ -9,11 +9,11 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace DeepDrftWeb.Migrations
namespace DeepDrftData.Migrations
{
[DbContext(typeof(DeepDrftContext))]
[Migration("20260518035137_AddCreatedByUserId")]
partial class AddCreatedByUserId
[Migration("20260519021400_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -45,6 +45,10 @@ namespace DeepDrftWeb.Migrations
.HasColumnType("character varying(200)")
.HasColumnName("artist");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<long?>("CreatedByUserId")
.HasColumnType("bigint")
.HasColumnName("created_by_user_id");
@@ -65,6 +69,12 @@ namespace DeepDrftWeb.Migrations
.HasColumnType("character varying(500)")
.HasColumnName("image_path");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<DateOnly?>("ReleaseDate")
.HasColumnType("date")
.HasColumnName("release_date");
@@ -75,8 +85,15 @@ namespace DeepDrftWeb.Migrations
.HasColumnType("character varying(200)")
.HasColumnName("track_name");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id");
b.HasIndex("IsDeleted")
.HasDatabaseName("IX_track_is_deleted");
b.ToTable("track", (string)null);
});
#pragma warning restore 612, 618
@@ -4,10 +4,10 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace DeepDrftWeb.Migrations
namespace DeepDrftData.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
@@ -24,12 +24,21 @@ namespace DeepDrftWeb.Migrations
album = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
genre = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
release_date = table.Column<DateOnly>(type: "date", nullable: true),
image_path = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true)
image_path = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
created_by_user_id = table.Column<long>(type: "bigint", nullable: true),
created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
updated_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
is_deleted = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false)
},
constraints: table =>
{
table.PrimaryKey("PK_track", x => x.id);
});
migrationBuilder.CreateIndex(
name: "IX_track_is_deleted",
table: "track",
column: "is_deleted");
}
/// <inheritdoc />
@@ -1,6 +1,6 @@
// <auto-generated />
using System;
using DeepDrftWeb.Services.Data;
using DeepDrftData.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@@ -8,7 +8,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace DeepDrftWeb.Migrations
namespace DeepDrftData.Migrations
{
[DbContext(typeof(DeepDrftContext))]
partial class DeepDrftContextModelSnapshot : ModelSnapshot
@@ -42,6 +42,10 @@ namespace DeepDrftWeb.Migrations
.HasColumnType("character varying(200)")
.HasColumnName("artist");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("created_at");
b.Property<long?>("CreatedByUserId")
.HasColumnType("bigint")
.HasColumnName("created_by_user_id");
@@ -62,6 +66,12 @@ namespace DeepDrftWeb.Migrations
.HasColumnType("character varying(500)")
.HasColumnName("image_path");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<DateOnly?>("ReleaseDate")
.HasColumnType("date")
.HasColumnName("release_date");
@@ -72,8 +82,15 @@ namespace DeepDrftWeb.Migrations
.HasColumnType("character varying(200)")
.HasColumnName("track_name");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated_at");
b.HasKey("Id");
b.HasIndex("IsDeleted")
.HasDatabaseName("IX_track_is_deleted");
b.ToTable("track", (string)null);
});
#pragma warning restore 612, 618
@@ -0,0 +1,31 @@
using Data.Data.Repositories;
using Data.Errors;
using DeepDrftData.Data;
using DeepDrftModels.Entities;
using Microsoft.Extensions.Logging;
namespace DeepDrftData.Repositories;
public class TrackRepository : Repository<DeepDrftContext, TrackEntity>
{
public TrackRepository(
DeepDrftContext context,
ILogger<Repository<DeepDrftContext, TrackEntity>> logger,
IDbExceptionClassifier? classifier = null)
: base(context, logger, classifier: classifier)
{
}
protected override void UpdateEntity(TrackEntity target, TrackEntity source)
{
base.UpdateEntity(target, source); // copies CreatedAt, UpdatedAt, IsDeleted
target.EntryKey = source.EntryKey;
target.TrackName = source.TrackName;
target.Artist = source.Artist;
target.Album = source.Album;
target.Genre = source.Genre;
target.ReleaseDate = source.ReleaseDate;
target.ImagePath = source.ImagePath;
target.CreatedByUserId = source.CreatedByUserId;
}
}
+44
View File
@@ -0,0 +1,44 @@
using DeepDrftModels.DTOs;
using DeepDrftModels.Entities;
using Models.Converters;
namespace DeepDrftData;
/// <summary>
/// Static entity ↔ DTO converter consumed by the BlazorBlocks Manager base class.
/// The DTO side mirrors the entity field-for-field; the audit columns
/// (CreatedAt, UpdatedAt) come from BaseEntity / BaseModel.
/// IsDeleted does not round-trip — soft-deleted rows are not exposed via the model.
/// </summary>
public class TrackConverter : IEntityToModelConverter<TrackEntity, TrackDto>
{
public static TrackDto Convert(TrackEntity entity) => new()
{
Id = entity.Id,
CreatedAt = entity.CreatedAt,
UpdatedAt = entity.UpdatedAt,
EntryKey = entity.EntryKey,
TrackName = entity.TrackName,
Artist = entity.Artist,
Album = entity.Album,
Genre = entity.Genre,
ReleaseDate = entity.ReleaseDate,
ImagePath = entity.ImagePath,
CreatedByUserId = entity.CreatedByUserId
};
public static TrackEntity Convert(TrackDto model) => new()
{
Id = model.Id,
CreatedAt = model.CreatedAt,
UpdatedAt = model.UpdatedAt,
EntryKey = model.EntryKey,
TrackName = model.TrackName,
Artist = model.Artist,
Album = model.Album,
Genre = model.Genre,
ReleaseDate = model.ReleaseDate,
ImagePath = model.ImagePath,
CreatedByUserId = model.CreatedByUserId
};
}
+137
View File
@@ -0,0 +1,137 @@
using Data.Managers;
using DeepDrftData.Repositories;
using DeepDrftModels.DTOs;
using DeepDrftModels.Entities;
using Microsoft.Extensions.Logging;
using Models.Common;
using NetBlocks.Models;
namespace DeepDrftData;
/// <summary>
/// SQL-side track orchestrator built on the BlazorBlocks Manager base. Two surfaces coexist:
///
/// - The DTO-shaped IManager surface (GetById → TrackDto, etc.) inherited from Manager&lt;&gt;.
/// - The entity-shaped ITrackService surface retained for backward compatibility with the
/// web host, CMS, and CLI — all existing controllers and pages inject ITrackService and
/// expect TrackEntity-typed results. The two GetById overloads conflict on signature, so
/// ITrackService.GetById is implemented explicitly; the base Manager.Delete satisfies
/// both interfaces because the signatures align.
/// </summary>
public class TrackManager
: Manager<TrackEntity, TrackDto, TrackRepository, TrackConverter>, ITrackService
{
public TrackManager(
TrackRepository repository,
ILogger<Manager<TrackEntity, TrackDto, TrackRepository, TrackConverter>> logger)
: base(repository, logger)
{
}
// --- ITrackService implementation (entity-space; calls Repository directly) ---
// Explicit interface implementation — IManager.GetById returns ResultContainer<TrackDto>
// (inherited from Manager<>), so this entity-typed overload cannot coexist as a public
// member with the same name. Callers always inject ITrackService, so the explicit impl
// resolves correctly at the call site.
async Task<ResultContainer<TrackEntity?>> ITrackService.GetById(long id)
{
try
{
var entity = await Repository.GetByIdAsync(id);
return ResultContainer<TrackEntity?>.CreatePassResult(entity);
}
catch (Exception e)
{
return ResultContainer<TrackEntity?>.CreateFailResult(e.Message);
}
}
public async Task<ResultContainer<List<TrackEntity>>> GetAll()
{
try
{
var entities = await Repository.GetAllAsync();
return ResultContainer<List<TrackEntity>>.CreatePassResult(entities.ToList());
}
catch (Exception e)
{
return ResultContainer<List<TrackEntity>>.CreateFailResult(e.Message);
}
}
public async Task<ResultContainer<PagedResult<TrackEntity>>> GetPaged(
int pageNumber,
int pageSize,
string? sortColumn,
bool sortDescending,
CancellationToken cancellationToken = default)
{
try
{
var parameters = new PagingParameters<TrackEntity>
{
Page = pageNumber,
PageSize = pageSize,
IsDescending = sortDescending,
OrderBy = sortColumn switch
{
"TrackName" => e => e.TrackName,
"Artist" => e => e.Artist,
"Album" => e => (object)(e.Album ?? string.Empty),
"Genre" => e => (object)(e.Genre ?? string.Empty),
"ReleaseDate" => e => (object)(e.ReleaseDate ?? DateOnly.MaxValue),
_ => e => e.Id
}
};
var page = await Repository.GetPagedAsync(parameters);
return ResultContainer<PagedResult<TrackEntity>>.CreatePassResult(page);
}
catch (Exception e)
{
return ResultContainer<PagedResult<TrackEntity>>.CreateFailResult(e.Message);
}
}
public async Task<ResultContainer<TrackEntity>> Create(TrackEntity newTrack)
{
try
{
var added = await Repository.AddAsync(newTrack);
return ResultContainer<TrackEntity>.CreatePassResult(added);
}
catch (Exception e)
{
return ResultContainer<TrackEntity>.CreateFailResult(e.Message);
}
}
// Manager<>.Update takes TrackDto and returns Result; this Update keeps the
// entity-typed contract callers expect and returns the post-update entity for the
// existing CMS edit flow that reads back the persisted values.
/// <summary>
/// Updates the track's metadata fields and returns the DB-authoritative entity.
/// The caller's <paramref name="track"/> object has its <c>UpdatedAt</c> field
/// mutated in place by <see cref="TrackRepository.UpdateAsync"/>; do not reuse it.
/// </summary>
public async Task<ResultContainer<TrackEntity>> Update(TrackEntity track)
{
try
{
await Repository.UpdateAsync(track);
var updated = await Repository.GetByIdAsync(track.Id);
return updated is not null
? ResultContainer<TrackEntity>.CreatePassResult(updated)
: ResultContainer<TrackEntity>.CreateFailResult("Track not found after update.");
}
catch (Exception e)
{
return ResultContainer<TrackEntity>.CreateFailResult(e.Message);
}
}
// Delete(long) is inherited from Manager<> — its Task<Result> signature already
// satisfies ITrackService.Delete, and the base implementation performs the soft delete
// via Repository.DeleteAsync. No override needed.
}
+2 -2
View File
@@ -14,9 +14,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftTests", "DeepDrftTe
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftCli", "DeepDrftCli\DeepDrftCli.csproj", "{E4C0ADD6-1264-47A5-98E1-0843AD14B7F9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftWeb.Services", "DeepDrftWeb.Services\DeepDrftWeb.Services.csproj", "{1D1CE905-DAD0-4E93-9B09-326E8EC05877}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftData", "DeepDrftData\DeepDrftData.csproj", "{1D1CE905-DAD0-4E93-9B09-326E8EC05877}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftContent.Services", "DeepDrftContent.Services\DeepDrftContent.Services.csproj", "{4D025326-7F27-4C42-BE0F-92C1E64E0696}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftContent.Data", "DeepDrftContent.Data\DeepDrftContent.Data.csproj", "{4D025326-7F27-4C42-BE0F-92C1E64E0696}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftCms", "DeepDrftCms\DeepDrftCms.csproj", "{81F1D47F-F892-45FB-9E35-D7775805FFD3}"
EndProject
+15 -7
View File
@@ -1,13 +1,21 @@
namespace DeepDrftModels.DTOs;
using Models.Models;
public class TrackDto
namespace DeepDrftModels.DTOs;
// Inherits Id, CreatedAt, UpdatedAt from BaseModel (Cerebellum.BlazorBlocks.Models).
// BlazorBlocks's Manager<> generic constraint requires `new()` on the model type, which
// disqualifies `required` properties (the `new()` constraint and required members do not
// compose). EntryKey/TrackName/Artist therefore drop `required` here — the TrackEntity
// side remains required, and TrackConverter assigns every field on the round-trip so an
// empty default is never observable in production code paths.
public class TrackDto : BaseModel
{
public long Id { get; set; }
public required string EntryKey { get; set; }
public required string TrackName { get; set; }
public required string Artist { get; set; }
public string EntryKey { get; set; } = string.Empty;
public string TrackName { get; set; } = string.Empty;
public string Artist { get; set; } = string.Empty;
public string? Album { get; set; }
public string? Genre { get; set; }
public DateOnly? ReleaseDate { get; set; }
public string? ImagePath { get; set; }
}
public long? CreatedByUserId { get; set; }
}
+2 -1
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
@@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Cerebellum.NetBlocks" Version="10.3.30" />
<PackageReference Include="Cerebellum.BlazorBlocks.Models" Version="10.3.30" />
</ItemGroup>
</Project>
+8 -4
View File
@@ -1,8 +1,12 @@
namespace DeepDrftModels.Entities;
using Models.Entities;
public class TrackEntity
namespace DeepDrftModels.Entities;
// Inherits Id, CreatedAt, UpdatedAt, IsDeleted from BaseEntity (Cerebellum.BlazorBlocks.Models).
// BaseEntity ships the audit columns but does not declare IEntity itself, so subclasses
// declare it explicitly to satisfy the generic constraints on Repository<>/Manager<>/etc.
public class TrackEntity : BaseEntity, IEntity
{
public long Id { get; set; }
public required string EntryKey { get; set; }
public required string TrackName { get; set; }
public required string Artist { get; set; }
@@ -11,4 +15,4 @@ public class TrackEntity
public DateOnly? ReleaseDate { get; set; }
public string? ImagePath { get; set; }
public long? CreatedByUserId { get; set; }
}
}
-35
View File
@@ -1,35 +0,0 @@
namespace DeepDrftModels.Models;
public class PagedResult<T>
{
public IEnumerable<T> Items { get; set; } = new List<T>();
public int TotalCount { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)TotalCount / PageSize) : 0;
public bool HasNextPage => Page < TotalPages;
public bool HasPreviousPage => Page > 1;
public PagedResult()
{
}
public static PagedResult<T> From<TOther>(PagedResult<TOther> other, IEnumerable<T> items)
{
return new PagedResult<T>()
{
Items = items.ToList(),
Page = other.Page,
PageSize = other.PageSize,
TotalCount = other.TotalCount,
};
}
public PagedResult(IEnumerable<T> items, int totalCount, int page, int pageSize)
{
Items = items.ToList();
TotalCount = totalCount;
Page = page;
PageSize = pageSize;
}
}
-25
View File
@@ -1,25 +0,0 @@
using System.Linq.Expressions;
namespace DeepDrftModels.Models;
public class PagingParameters
{
private const int _maxPageSize = 100;
private int _pageSize = 20;
public int Page { get; set; } = 1;
public int PageSize
{
get => _pageSize;
set => _pageSize = value > _maxPageSize ? _maxPageSize : value;
}
}
public class PagingParameters<T> : PagingParameters
{
public Expression<Func<T, object>>? OrderBy { get; set; }
public bool IsDescending { get; set; } = false;
public int Skip => (Page - 1) * PageSize;
}
+1 -1
View File
@@ -27,7 +27,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DeepDrftContent.Services\DeepDrftContent.Services.csproj" />
<ProjectReference Include="..\DeepDrftContent.Data\DeepDrftContent.Data.csproj" />
</ItemGroup>
</Project>
+2 -2
View File
@@ -1,5 +1,5 @@
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Services.FileDatabase.Services;
using DeepDrftContent.Data.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Services;
namespace DeepDrftTests;
+4 -4
View File
@@ -1,7 +1,7 @@
using DeepDrftContent.Services.FileDatabase.Abstractions;
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Services.FileDatabase.Services;
using DeepDrftContent.Services.FileDatabase.Utils;
using DeepDrftContent.Data.FileDatabase.Abstractions;
using DeepDrftContent.Data.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Services;
using DeepDrftContent.Data.FileDatabase.Utils;
namespace DeepDrftTests;
+2 -2
View File
@@ -1,5 +1,5 @@
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Services.FileDatabase.Services;
using DeepDrftContent.Data.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Services;
namespace DeepDrftTests;
+3 -3
View File
@@ -1,6 +1,6 @@
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Services.FileDatabase.Services;
using DeepDrftContent.Services.FileDatabase.Utils;
using DeepDrftContent.Data.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Services;
using DeepDrftContent.Data.FileDatabase.Utils;
namespace DeepDrftTests;
+1 -1
View File
@@ -1,4 +1,4 @@
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Models;
namespace DeepDrftTests;
@@ -1,5 +1,5 @@
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Services.FileDatabase.Services;
using DeepDrftContent.Data.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Services;
namespace DeepDrftTests;
+1 -1
View File
@@ -1,4 +1,4 @@
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Models;
namespace DeepDrftTests;
+2 -2
View File
@@ -1,5 +1,5 @@
using DeepDrftContent.Services.FileDatabase.Models;
using DeepDrftContent.Services.FileDatabase.Utils;
using DeepDrftContent.Data.FileDatabase.Models;
using DeepDrftContent.Data.FileDatabase.Utils;
namespace DeepDrftTests;
+1 -1
View File
@@ -1,5 +1,5 @@
using DeepDrftModels.Entities;
using DeepDrftModels.Models;
using Models.Common;
using NetBlocks.Models;
using System.Text.Json;
using System.Web;
+1 -1
View File
@@ -1,8 +1,8 @@
using DeepDrftModels.Entities;
using DeepDrftModels.Models;
using DeepDrftWeb.Client.Services;
using DeepDrftWeb.Client.ViewModels;
using Microsoft.AspNetCore.Components;
using Models.Common;
namespace DeepDrftWeb.Client.Pages;
@@ -1,6 +1,6 @@
using DeepDrftModels.Entities;
using DeepDrftModels.Models;
using DeepDrftWeb.Client.Clients;
using Models.Common;
namespace DeepDrftWeb.Client.ViewModels;
@@ -1,52 +0,0 @@
using DeepDrftModels.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DeepDrftWeb.Services.Data.Configurations;
public class TrackConfiguration : IEntityTypeConfiguration<TrackEntity>
{
public void Configure(EntityTypeBuilder<TrackEntity> builder)
{
builder.ToTable("track");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.HasColumnName("id")
.IsRequired();
builder.Property(x => x.EntryKey)
.HasColumnName("entry_key")
.IsRequired()
.HasMaxLength(100);
builder.Property(x => x.TrackName)
.HasColumnName("track_name")
.IsRequired()
.HasMaxLength(200);
builder.Property(x => x.Artist)
.HasColumnName("artist")
.IsRequired()
.HasMaxLength(200);
builder.Property(x => x.Album)
.HasColumnName("album")
.HasMaxLength(200);
builder.Property(x => x.Genre)
.HasColumnName("genre")
.HasMaxLength(100);
builder.Property(x => x.ReleaseDate)
.HasColumnName("release_date");
builder.Property(x => x.ImagePath)
.HasColumnName("image_path")
.HasMaxLength(500);
builder.Property(x => x.CreatedByUserId)
.HasColumnName("created_by_user_id");
}
}
@@ -1,81 +0,0 @@
// <auto-generated />
using System;
using DeepDrftWeb.Services.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace DeepDrftWeb.Migrations
{
[DbContext(typeof(DeepDrftContext))]
[Migration("20260518025102_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("DeepDrftModels.Entities.TrackEntity", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Album")
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("album");
b.Property<string>("Artist")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("artist");
b.Property<string>("EntryKey")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("entry_key");
b.Property<string>("Genre")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("genre");
b.Property<string>("ImagePath")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasColumnName("image_path");
b.Property<DateOnly?>("ReleaseDate")
.HasColumnType("date")
.HasColumnName("release_date");
b.Property<string>("TrackName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("track_name");
b.HasKey("Id");
b.ToTable("track", (string)null);
});
#pragma warning restore 612, 618
}
}
}
@@ -1,28 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DeepDrftWeb.Migrations
{
/// <inheritdoc />
public partial class AddCreatedByUserId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "created_by_user_id",
table: "track",
type: "bigint",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "created_by_user_id",
table: "track");
}
}
}
@@ -1,84 +0,0 @@
using DeepDrftModels.Entities;
using DeepDrftModels.Models;
using Microsoft.EntityFrameworkCore;
using DeepDrftWeb.Services.Data;
namespace DeepDrftWeb.Services.Repositories;
public class TrackRepository
{
private readonly DeepDrftContext _db;
public TrackRepository(DeepDrftContext db)
{
_db = db;
}
public async Task<TrackEntity?> GetById(long id)
{
return await _db.Tracks.FindAsync(id);
}
public async Task<List<TrackEntity>> GetAll()
{
return await _db.Tracks.ToListAsync();
}
public async Task<PagedResult<TrackEntity>> GetPage(PagingParameters<TrackEntity> pageParameters, CancellationToken cancellationToken = default)
{
// Two separate queries with no transaction: count and page can be momentarily inconsistent
// under concurrent writes. Acceptable — write volume is low and the UI is read-only.
// If filtering is added, the count query must be updated to apply the same filter.
var count = await _db.Tracks.CountAsync(cancellationToken);
var orderBy = pageParameters.OrderBy ?? (t => t.Id);
var ordered = pageParameters.IsDescending
? _db.Tracks.OrderByDescending(orderBy)
: _db.Tracks.OrderBy(orderBy);
var page = await ordered
.Skip(pageParameters.Skip)
.Take(pageParameters.PageSize)
.ToListAsync(cancellationToken);
return new PagedResult<TrackEntity>(page, count, pageParameters.Page, pageParameters.PageSize);
}
public async Task<TrackEntity> Create(TrackEntity newTrack)
{
var track = _db.Tracks.Add(newTrack);
await _db.SaveChangesAsync();
return track.Entity;
}
public async Task<TrackEntity> Update(TrackEntity track)
{
var trackEntity = await GetById(track.Id);
if (trackEntity == null)
{
throw new InvalidOperationException($"Track not found: {track.Id}");
}
trackEntity.Album = track.Album;
trackEntity.Artist = track.Artist;
trackEntity.Genre = track.Genre;
trackEntity.ImagePath = track.ImagePath;
trackEntity.EntryKey = track.EntryKey;
trackEntity.ReleaseDate = track.ReleaseDate;
trackEntity.TrackName = track.TrackName;
await _db.SaveChangesAsync();
return trackEntity;
}
public async Task Delete(long id)
{
var track = await GetById(id);
if (track != null)
{
_db.Tracks.Remove(track);
await _db.SaveChangesAsync();
}
}
}
-125
View File
@@ -1,125 +0,0 @@
using DeepDrftModels.Entities;
using DeepDrftModels.Models;
using DeepDrftWeb.Services.Data;
using DeepDrftWeb.Services.Repositories;
using NetBlocks.Models;
namespace DeepDrftWeb.Services;
public class TrackService : ITrackService
{
private readonly TrackRepository _repository;
public TrackService(TrackRepository repository)
{
_repository = repository;
}
public async Task<ResultContainer<TrackEntity?>> GetById(long id)
{
try
{
var track = await _repository.GetById(id);
return ResultContainer<TrackEntity?>.CreatePassResult(track);
}
catch (Exception e)
{
return ResultContainer<TrackEntity?>.CreateFailResult(e.Message);
}
}
public async Task<ResultContainer<List<TrackEntity>>> GetAll()
{
try
{
var tracks = await _repository.GetAll();
return ResultContainer<List<TrackEntity>>.CreatePassResult(tracks);
}
catch (Exception e)
{
return ResultContainer<List<TrackEntity>>.CreateFailResult(e.Message);
}
}
public async Task<ResultContainer<PagedResult<TrackEntity>>> GetPaged(int pageNumber, int pageSize, string? sortColumn, bool sortDescending, CancellationToken cancellationToken = default)
{
try
{
var parameters = new PagingParameters<TrackEntity>()
{
Page = pageNumber,
PageSize = pageSize,
IsDescending = sortDescending
};
if (sortColumn != null)
{
switch (sortColumn)
{
case "TrackName":
parameters.OrderBy = entity => entity.TrackName;
break;
case "Artist":
parameters.OrderBy = entity => entity.Artist;
break;
case "Album":
parameters.OrderBy = entity => entity.Album ?? "";
break;
case "ReleaseDate":
parameters.OrderBy = entity => entity.ReleaseDate ?? DateOnly.MaxValue;
break;
case "Genre":
parameters.OrderBy = entity => entity.Genre ?? "";
break;
}
}
var page = await _repository.GetPage(parameters, cancellationToken);
return ResultContainer<PagedResult<TrackEntity>>.CreatePassResult(page);
}
catch (Exception e)
{
return ResultContainer<PagedResult<TrackEntity>>.CreateFailResult(e.Message);
}
}
public async Task<ResultContainer<TrackEntity>> Create(TrackEntity newTrack)
{
try
{
var track = await _repository.Create(newTrack);
return ResultContainer<TrackEntity>.CreatePassResult(track);
}
catch (Exception e)
{
return ResultContainer<TrackEntity>.CreateFailResult(e.Message);
}
}
public async Task<ResultContainer<TrackEntity>> Update(TrackEntity track)
{
try
{
var updatedTrack = await _repository.Update(track);
return ResultContainer<TrackEntity>.CreatePassResult(updatedTrack);
}
catch (Exception e)
{
return ResultContainer<TrackEntity>.CreateFailResult(e.Message);
}
}
public async Task<Result> Delete(long id)
{
try
{
await _repository.Delete(id);
return Result.CreatePassResult();
}
catch (Exception e)
{
return Result.CreateFailResult(e.Message);
}
}
}
@@ -1,4 +1,4 @@
using DeepDrftWeb.Services;
using DeepDrftData;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+1 -1
View File
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
using DeepDrftData;
using DeepDrftModels.Entities;
using DeepDrftWeb.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NetBlocks.Models;
@@ -1,7 +1,7 @@
using System.Net.Http.Headers;
using System.Security.Claims;
using DeepDrftData;
using DeepDrftModels.Entities;
using DeepDrftWeb.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -10,7 +10,7 @@ namespace DeepDrftWeb.Controllers;
/// <summary>
/// CMS upload surface. Proxies a WAV + metadata multipart form to DeepDrftContent's
/// POST api/track/upload, then persists the returned unpersisted TrackEntity to SQL via
/// ITrackService.Create. DeepDrftWeb intentionally does not reference DeepDrftContent.Services
/// ITrackService.Create. DeepDrftWeb intentionally does not reference DeepDrftContent.Data
/// (CMS-PLAN §5, Option B) — all vault access is over HTTP.
/// </summary>
[ApiController]
+3 -3
View File
@@ -1,7 +1,7 @@
using DeepDrftModels.Entities;
using DeepDrftModels.Models;
using DeepDrftWeb.Services;
using DeepDrftData;
using DeepDrftModels.Entities;
using Microsoft.AspNetCore.Mvc;
using Models.Common;
using NetBlocks.Models;
namespace DeepDrftWeb.Controllers;
+1 -1
View File
@@ -25,7 +25,7 @@
<ProjectReference Include="..\DeepDrftCms\DeepDrftCms.csproj" />
<ProjectReference Include="..\DeepDrftModels\DeepDrftModels.csproj" />
<ProjectReference Include="..\DeepDrftWeb.Client\DeepDrftWeb.Client.csproj" />
<ProjectReference Include="..\DeepDrftWeb.Services\DeepDrftWeb.Services.csproj" />
<ProjectReference Include="..\DeepDrftData\DeepDrftData.csproj" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.7" />
</ItemGroup>
+17 -12
View File
@@ -1,6 +1,7 @@
using DeepDrftWeb.Services.Data;
using DeepDrftWeb.Services.Repositories;
using DeepDrftWeb.Services;
using DeepDrftData;
using DeepDrftData.Data;
using DeepDrftData.Repositories;
using DeepDrftWeb.Services; // DarkModeService namespace (within this host project)
using Microsoft.EntityFrameworkCore;
namespace DeepDrftWeb;
@@ -26,10 +27,14 @@ public static class Startup
.AddHttpContextAccessor()
.AddScoped<DarkModeService>();
// Add Track services
// Add Track services. TrackManager implements ITrackService for backward compatibility
// with controllers and CMS pages that inject the interface; resolving ITrackService
// returns the same scoped TrackManager instance so the manager surface (DTO-space)
// and the service surface (entity-space) share state.
builder.Services
.AddScoped<TrackRepository>()
.AddScoped<ITrackService, TrackService>();
.AddScoped<TrackManager>()
.AddScoped<ITrackService>(sp => sp.GetRequiredService<TrackManager>());
// CMS → DeepDrftContent client. The API key is required up front (no lazy resolution)
// so a misconfiguration surfaces at startup instead of on the first delete attempt.
@@ -44,13 +49,13 @@ public static class Startup
client.DefaultRequestHeaders.Add("ApiKey", contentApiKey);
});
}
public static string GetKestrelUrl(this WebApplicationBuilder builder)
{
// Check all the places Kestrel URL can be configured
var urls = builder.Configuration["ASPNETCORE_URLS"]
var urls = builder.Configuration["ASPNETCORE_URLS"]
?? builder.Configuration["urls"];
if (!string.IsNullOrEmpty(urls))
{
return urls.Split(';')[0].Trim();
@@ -60,15 +65,15 @@ public static class Startup
var kestrelSection = builder.Configuration.GetSection("Kestrel:Endpoints");
var firstEndpoint = kestrelSection.GetChildren().FirstOrDefault();
var endpointUrl = firstEndpoint?["Url"];
if (!string.IsNullOrEmpty(endpointUrl))
{
return endpointUrl;
}
// ASP.NET Core defaults
return builder.Environment.IsDevelopment()
? "https://localhost:5001"
return builder.Environment.IsDevelopment()
? "https://localhost:5001"
: "http://localhost:5000";
}
}
}
+1
View File
@@ -4,5 +4,6 @@
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<!-- Machine-specific local feed — update path if not at C:\Development\AuthBlocks\nupkgs\ -->
<add key="AuthBlocks-local" value="C:\Development\AuthBlocks\nupkgs\" />
<add key="BlazorBlocks-local" value="C:\Development\BlazorBlocks\nupkgs\" />
</packageSources>
</configuration>
+1 -1
View File
@@ -1,6 +1,6 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftContent.Services", "DeepDrftContent.Services\DeepDrftContent.Services.csproj", "{9C5D844B-46A6-4C70-B5A7-3A028795629C}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftContent.Data", "DeepDrftContent.Data\DeepDrftContent.Data.csproj", "{9C5D844B-46A6-4C70-B5A7-3A028795629C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftContent", "DeepDrftContent\DeepDrftContent.csproj", "{91F52E3A-D36D-47C3-924B-2A3CE3606B03}"
EndProject
+1 -1
View File
@@ -1,6 +1,6 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftWeb.Services", "DeepDrftWeb.Services\DeepDrftWeb.Services.csproj", "{0B1ABD2E-ACC4-40B2-A80E-CC47CF209C74}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftData", "DeepDrftData\DeepDrftData.csproj", "{0B1ABD2E-ACC4-40B2-A80E-CC47CF209C74}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeepDrftModels", "DeepDrftModels\DeepDrftModels.csproj", "{2507F960-A210-40A3-999D-368A6D067350}"
EndProject