Файлы, правки отображения

This commit is contained in:
Халимов Рустам
2026-04-01 17:42:29 +03:00
parent 2c6d6f831f
commit 249c344df8
24 changed files with 847 additions and 328 deletions

View File

@@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
namespace Knot.Host.Presentation.Endpoints;
namespace Knot.Modules.Admin.Presentation.Endpoints;
public record KlipyTestDto(
[property: JsonPropertyName("apiKey")] string ApiKey,

View File

@@ -5,7 +5,7 @@ namespace Knot.Modules.Conversations.Application.DTOs;
public record MediaDto(
Guid Id,
string Type,
string Url,
string? Url,
string? Filename,
long? Size
);

View File

@@ -1,7 +1,9 @@
using System.IO;
using Knot.Modules.Conversations.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.Configuration;
namespace Knot.Modules.Conversations.Infrastructure;
@@ -10,8 +12,24 @@ public class ChatsDbContextFactory : IDesignTimeDbContextFactory<ChatsDbContext>
{
public ChatsDbContext CreateDbContext(string[] args)
{
var basePath = Path.Combine(Directory.GetCurrentDirectory(), "..", "Host");
if (!Directory.Exists(basePath))
{
basePath = Directory.GetCurrentDirectory();
}
var configuration = new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables()
.Build();
var optionsBuilder = new DbContextOptionsBuilder<ChatsDbContext>();
optionsBuilder.UseNpgsql("Host=localhost;Database=knot_db;Username=knot;Password=knot_pass");
var connectionString = configuration.GetConnectionString("DefaultConnection")
?? configuration["DATABASE_URL"]
?? "Host=localhost;Port=5432;Database=knot_db;Username=knot;Password=knot_pass";
optionsBuilder.UseNpgsql(connectionString);
return new ChatsDbContext(optionsBuilder.Options);
}
}

View File

@@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
namespace Host.Endpoints;
namespace Knot.Modules.Federation.Presentation.Endpoints;
/// <summary>
/// Эндпоинты федерации для межсерверного взаимодействия.

View File

@@ -11,7 +11,7 @@ using System.Threading;
using System.Threading.Tasks;
using MediatR;
namespace Host.Application.Klipy.Queries;
namespace Knot.Modules.Klipy.Application.Klipy.Queries;
public record GetTrendingGifsQuery : IQuery<JsonElement?>;

View File

@@ -11,7 +11,7 @@ using System.Threading;
using System.Threading.Tasks;
using MediatR;
namespace Host.Application.Klipy.Queries;
namespace Knot.Modules.Klipy.Application.Klipy.Queries;
public record SearchGifsQuery(string Query) : IQuery<JsonElement?>;

View File

@@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Routing;
using System;
using System.Threading.Tasks;
namespace Host.Endpoints;
namespace Knot.Modules.Klipy.Presentation.Endpoints;
/// <summary>
/// Регистрация эндпоинтов для сервиса Klipy.
@@ -20,13 +20,13 @@ public static class KlipyEndpoints
group.MapGet("/trending", async (ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(new Host.Application.Klipy.Queries.GetTrendingGifsQuery(), ct);
var result = await sender.Send(new Knot.Modules.Klipy.Application.Klipy.Queries.GetTrendingGifsQuery(), ct);
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(new { error = result.Error.Description });
});
group.MapGet("/search", async ([FromQuery] string q, ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(new Host.Application.Klipy.Queries.SearchGifsQuery(q), ct);
var result = await sender.Send(new Knot.Modules.Klipy.Application.Klipy.Queries.SearchGifsQuery(q), ct);
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(new { error = result.Error.Description });
});
}

View File

@@ -15,7 +15,7 @@ public static class DependencyInjection
services.AddDbContext<RelationsDbContext>(options =>
options.UseNpgsql(connectionString));
services.AddScoped<IFriendshipRepository, FriendshipRepository>();
services.AddScoped<Knot.Contracts.Relations.Application.Abstractions.IFriendshipRepository, FriendshipRepository>();
services.AddMediatR(config =>
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));

View File

@@ -1,12 +1,12 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Knot.Modules.Relations.Domain;
using Knot.Shared.Kernel;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Knot.Modules.Relations.Domain;
using Knot.Shared.Kernel;
using System.Linq;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Knot.Modules.Relations.Infrastructure.Persistence;
@@ -22,6 +22,7 @@ public sealed class RelationsDbContext : DbContext
public DbSet<Contact> Contacts => Set<Contact>();
public DbSet<UserReplica> UserReplicas => Set<UserReplica>();
public DbSet<Friendship> Friendships => Set<Friendship>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -40,6 +41,14 @@ public sealed class RelationsDbContext : DbContext
builder.ToTable("UserReplicas");
builder.HasKey(r => r.Id);
});
modelBuilder.Entity<Friendship>(builder =>
{
builder.ToTable("Friendships");
builder.HasKey(f => f.Id);
builder.Property(f => f.Status).HasConversion<string>();
builder.HasIndex(f => new { f.UserId, f.FriendId }).IsUnique();
});
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)

View File

@@ -0,0 +1,33 @@
using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
namespace Knot.Modules.Relations.Infrastructure.Persistence;
public class RelationsDbContextFactory : IDesignTimeDbContextFactory<RelationsDbContext>
{
public RelationsDbContext CreateDbContext(string[] args)
{
var basePath = Path.Combine(Directory.GetCurrentDirectory(), "..", "Host");
if (!Directory.Exists(basePath))
{
basePath = Directory.GetCurrentDirectory();
}
var configuration = new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables()
.Build();
var optionsBuilder = new DbContextOptionsBuilder<RelationsDbContext>();
var connectionString = configuration.GetConnectionString("DefaultConnection")
?? configuration["DATABASE_URL"]
?? "Host=localhost;Port=5432;Database=knot_db;Username=knot;Password=knot_pass";
optionsBuilder.UseNpgsql(connectionString);
return new RelationsDbContext(optionsBuilder.Options, null!);
}
}

View File

@@ -0,0 +1,114 @@
// <auto-generated />
using System;
using Knot.Modules.Relations.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Knot.Modules.Relations.Migrations
{
[DbContext(typeof(RelationsDbContext))]
[Migration("20260401112051_AddFriendships")]
partial class AddFriendships
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("relations")
.HasAnnotation("ProductVersion", "10.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Knot.Modules.Relations.Domain.Contact", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ContactId")
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId", "ContactId")
.IsUnique();
b.ToTable("Contacts", "relations");
});
modelBuilder.Entity("Knot.Modules.Relations.Domain.Friendship", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("FriendId")
.HasColumnType("uuid");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId", "FriendId")
.IsUnique();
b.ToTable("Friendships", "relations");
});
modelBuilder.Entity("Knot.Modules.Relations.Domain.UserReplica", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Avatar")
.IsRequired()
.HasColumnType("text");
b.Property<string>("DisplayName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Domain")
.HasColumnType("text");
b.Property<bool>("IsExternal")
.HasColumnType("boolean");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("UserReplicas", "relations");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,97 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Knot.Modules.Relations.Migrations
{
/// <inheritdoc />
public partial class AddFriendships : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "relations");
migrationBuilder.CreateTable(
name: "Contacts",
schema: "relations",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
ContactId = table.Column<Guid>(type: "uuid", nullable: false),
Status = table.Column<string>(type: "text", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Contacts", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Friendships",
schema: "relations",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
FriendId = table.Column<Guid>(type: "uuid", nullable: false),
Status = table.Column<string>(type: "text", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Friendships", x => x.Id);
});
migrationBuilder.CreateTable(
name: "UserReplicas",
schema: "relations",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Username = table.Column<string>(type: "text", nullable: false),
DisplayName = table.Column<string>(type: "text", nullable: false),
Avatar = table.Column<string>(type: "text", nullable: false),
IsExternal = table.Column<bool>(type: "boolean", nullable: false),
Domain = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_UserReplicas", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Contacts_UserId_ContactId",
schema: "relations",
table: "Contacts",
columns: new[] { "UserId", "ContactId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Friendships_UserId_FriendId",
schema: "relations",
table: "Friendships",
columns: new[] { "UserId", "FriendId" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Contacts",
schema: "relations");
migrationBuilder.DropTable(
name: "Friendships",
schema: "relations");
migrationBuilder.DropTable(
name: "UserReplicas",
schema: "relations");
}
}
}

View File

@@ -0,0 +1,111 @@
// <auto-generated />
using System;
using Knot.Modules.Relations.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Knot.Modules.Relations.Migrations
{
[DbContext(typeof(RelationsDbContext))]
partial class RelationsDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("relations")
.HasAnnotation("ProductVersion", "10.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Knot.Modules.Relations.Domain.Contact", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ContactId")
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId", "ContactId")
.IsUnique();
b.ToTable("Contacts", "relations");
});
modelBuilder.Entity("Knot.Modules.Relations.Domain.Friendship", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("FriendId")
.HasColumnType("uuid");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId", "FriendId")
.IsUnique();
b.ToTable("Friendships", "relations");
});
modelBuilder.Entity("Knot.Modules.Relations.Domain.UserReplica", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Avatar")
.IsRequired()
.HasColumnType("text");
b.Property<string>("DisplayName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Domain")
.HasColumnType("text");
b.Property<bool>("IsExternal")
.HasColumnType("boolean");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("UserReplicas", "relations");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -9,17 +9,43 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Caching.Memory;
namespace Knot.Host.Presentation.Endpoints;
namespace Knot.Modules.Storage.Presentation.Endpoints;
public static class FilesEndpoints
{
public static void MapFilesEndpoints(this WebApplication app)
{
var group = app.MapGroup("api/files");
group.MapGet("{id}", async (string id, [FromQuery] bool download, [FromServices] IFileStorageService fileStorage, [FromServices] IMemoryCache cache, CancellationToken ct) =>
// Federation remote endpoint - most specific, must be first
app.MapGet("/api/files/remote/{domain}/{id}", async (string domain, string id, HttpClient httpClient, ISettingsService settingsService, CancellationToken ct) =>
{
var settings = await settingsService.GetSettingsAsync(ct);
if (!settings.Federation.Enabled) return Results.Forbid();
var targetUrl = $"{domain.TrimEnd('/')}/api/federation/v1/proxy/{id}";
var request = new HttpRequestMessage(HttpMethod.Get, targetUrl);
request.Headers.Add("X-Knot-Domain", settings.System.DomainUrl);
using (var rsa = RSA.Create())
{
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(settings.Federation.PrivateKey!), out _);
var dataToSign = Encoding.UTF8.GetBytes(id + settings.System.DomainUrl);
var signature = Convert.ToBase64String(rsa.SignData(dataToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
request.Headers.Add("X-Knot-Signature", signature);
}
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
if (!response.IsSuccessStatusCode) return Results.NotFound();
var remoteStream = await response.Content.ReadAsStreamAsync(ct);
var contentType = response.Content.Headers.ContentType?.ToString() ?? "application/octet-stream";
return Results.Stream(remoteStream, contentType, enableRangeProcessing: true);
});
// Main file download endpoint - without group prefix to avoid routing issues
app.MapGet("/api/files/{id}", async ([FromRoute] string id, [FromServices] IFileStorageService fileStorage, [FromServices] IMemoryCache cache, HttpContext httpContext, [FromQuery] bool download = false, CancellationToken ct = default) =>
{
// (Текущая логика локальных файлов остается без изменений)
try
{
var cacheKey = $"file_{id}";
@@ -50,39 +76,9 @@ public static class FilesEndpoints
}
catch (Exception ex)
{
return Results.NotFound(new { error = "Файл не найден или ошибка доступа.", message = ex.Message });
return Results.NotFound(new { error = "Файл не найден", message = ex.Message });
}
});
// Новый эндпоинт для удаленных (федеративных) файлов
group.MapGet("/remote/{domain}/{id}", async (string domain, string id, HttpClient httpClient, ISettingsService settingsService, CancellationToken ct) =>
{
var settings = await settingsService.GetSettingsAsync(ct);
if (!settings.Federation.Enabled) return Results.Forbid();
// Формируем подписанный запрос к удаленному серверу
var targetUrl = $"{domain.TrimEnd('/')}/api/federation/v1/proxy/{id}";
var request = new HttpRequestMessage(HttpMethod.Get, targetUrl);
request.Headers.Add("X-Knot-Domain", settings.System.DomainUrl);
// Генерируем подпись запроса своим приватным ключом
using (var rsa = RSA.Create())
{
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(settings.Federation.PrivateKey!), out _);
var dataToSign = Encoding.UTF8.GetBytes(id + settings.System.DomainUrl);
var signature = Convert.ToBase64String(rsa.SignData(dataToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
request.Headers.Add("X-Knot-Signature", signature);
}
var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
if (!response.IsSuccessStatusCode) return Results.NotFound();
var remoteStream = await response.Content.ReadAsStreamAsync(ct);
var contentType = response.Content.Headers.ContentType?.ToString() ?? "application/octet-stream";
return Results.Stream(remoteStream, contentType, enableRangeProcessing: true);
});
}
}

View File

@@ -1,6 +1,7 @@
using System.Net.Http;
using Knot.Contracts.Stories.Infrastructure.Persistence;
using Knot.Modules.Stories.Application.Abstractions;
using Knot.Modules.Stories.Infrastructure.Database;
using Knot.Modules.Stories.Infrastructure.Persistence.Mongo;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@@ -14,6 +15,7 @@ public static class DependencyInjection
public static IServiceCollection AddStoriesModule(this IServiceCollection services, IConfiguration configuration)
{
services.AddScoped<IStoryRepository, StoryRepository>();
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<Knot.Contracts.Stories.Infrastructure.Persistence.IStoryCollection, StoryCollection>();
var connectionString = configuration.GetConnectionString("DefaultConnection");

View File

@@ -0,0 +1,33 @@
using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
namespace Knot.Modules.Stories.Infrastructure.Database;
public class StoriesDbContextFactory : IDesignTimeDbContextFactory<StoriesDbContext>
{
public StoriesDbContext CreateDbContext(string[] args)
{
var basePath = Path.Combine(Directory.GetCurrentDirectory(), "..", "Host");
if (!Directory.Exists(basePath))
{
basePath = Directory.GetCurrentDirectory();
}
var configuration = new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables()
.Build();
var optionsBuilder = new DbContextOptionsBuilder<StoriesDbContext>();
var connectionString = configuration.GetConnectionString("DefaultConnection")
?? configuration["DATABASE_URL"]
?? "Host=localhost;Port=5432;Database=knot_db;Username=knot;Password=knot_pass";
optionsBuilder.UseNpgsql(connectionString);
return new StoriesDbContext(optionsBuilder.Options);
}
}

View File

@@ -0,0 +1,42 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Profiles.Domain;
using Knot.Contracts.Relations.Domain;
using Knot.Modules.Stories.Application.Abstractions;
namespace Knot.Modules.Stories.Infrastructure.Database;
internal sealed class UserRepository : IUserRepository
{
private readonly IProfileRepository _profileRepository;
public UserRepository(IProfileRepository profileRepository)
{
_profileRepository = profileRepository;
}
public async Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default)
{
var profile = await _profileRepository.GetAsync(id, cancellationToken);
return profile != null;
}
public async Task<UserReplica?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
var profile = await _profileRepository.GetAsync(id, cancellationToken);
if (profile == null)
{
return null;
}
return new UserReplica(
profile.UserId,
profile.Username ?? string.Empty,
profile.DisplayName ?? string.Empty,
profile.Avatar ?? string.Empty,
false,
null
);
}
}

View File

@@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Contracts\Profiles\Knot.Contracts.Profiles.csproj" />
<ProjectReference Include="..\..\Contracts\Relations\Knot.Contracts.Relations.csproj" />
<ProjectReference Include="..\..\Contracts\Conversations\Knot.Contracts.Conversations.csproj" />
<ProjectReference Include="..\..\Contracts\Settings\Knot.Contracts.Settings.csproj" />

View File

@@ -0,0 +1,54 @@
// <auto-generated />
using System;
using Knot.Modules.Stories.Infrastructure.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Knot.Modules.Stories.Migrations
{
[DbContext(typeof(StoriesDbContext))]
[Migration("20260401112239_InitialStories")]
partial class InitialStories
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("stories")
.HasAnnotation("ProductVersion", "10.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Knot.Modules.Stories.Domain.StoryViewer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("StoryId")
.HasColumnType("uuid");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<DateTime>("ViewedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("StoryId", "UserId")
.IsUnique();
b.ToTable("StoryViewers", "stories");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Knot.Modules.Stories.Migrations
{
/// <inheritdoc />
public partial class InitialStories : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "stories");
migrationBuilder.CreateTable(
name: "StoryViewers",
schema: "stories",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
StoryId = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
ViewedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StoryViewers", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_StoryViewers_StoryId_UserId",
schema: "stories",
table: "StoryViewers",
columns: new[] { "StoryId", "UserId" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "StoryViewers",
schema: "stories");
}
}
}

View File

@@ -0,0 +1,51 @@
// <auto-generated />
using System;
using Knot.Modules.Stories.Infrastructure.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Knot.Modules.Stories.Migrations
{
[DbContext(typeof(StoriesDbContext))]
partial class StoriesDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("stories")
.HasAnnotation("ProductVersion", "10.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Knot.Modules.Stories.Domain.StoryViewer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("StoryId")
.HasColumnType("uuid");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<DateTime>("ViewedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("StoryId", "UserId")
.IsUnique();
b.ToTable("StoryViewers", "stories");
});
#pragma warning restore 612, 618
}
}
}