Реализован модуль каталога, миграции, сваггер

This commit is contained in:
Халимов Рустам
2026-02-10 15:32:40 +03:00
parent 4c9e44e992
commit 9d3999f690
40 changed files with 1665 additions and 4 deletions

4
.gitignore vendored
View File

@@ -136,3 +136,7 @@ src/Modules/Collaboration/Domain/bin/
src/Modules/Collaboration/Presentation/bin/
src/Modules/Identity/Tests/obj/
src/Modules/Catalog/Tests/bin/
src/Modules/Catalog/Tests/obj/

View File

@@ -11,6 +11,10 @@
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.76.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -2,6 +2,8 @@ using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Nashel.Modules.Catalog.Infrastructure;
using Nashel.Modules.Catalog.Presentation.Endpoints;
using Nashel.Modules.Identity.Infrastructure;
using Nashel.Modules.Identity.Presentation.Endpoints;
@@ -41,7 +43,6 @@ builder.Services.AddSwaggerGen(options =>
var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml");
foreach (var xmlFile in xmlFiles)
{
// Исключаем системные сборки, берем только наши, если нужно, или просто все
options.IncludeXmlComments(xmlFile);
}
});
@@ -49,6 +50,9 @@ builder.Services.AddSwaggerGen(options =>
// Регистрация модуля Identity
builder.Services.AddIdentityModule(builder.Configuration);
// Регистрация модуля Catalog
builder.Services.AddCatalogModule(builder.Configuration);
// Настройка аутентификации
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
@@ -83,7 +87,8 @@ if (app.Environment.IsDevelopment())
app.UseAuthentication();
app.UseAuthorization();
// Маппинг эндпоинтов модуля Identity
// Маппинг эндпоинтов
app.MapIdentityEndpoints();
app.MapCatalogEndpoints();
app.Run();

View File

@@ -0,0 +1,75 @@
using System.Text.Json;
using MediatR;
using Nashel.Modules.Catalog.Domain.Aggregates;
using Nashel.Modules.Catalog.Domain.Repositories;
namespace Nashel.Modules.Catalog.Application.Commands;
/// <summary>
/// Команда создания категории.
/// </summary>
public record CreateCategoryCommand : IRequest<Guid>
{
/// <summary>
/// Название категории.
/// </summary>
public string Title { get; init; }
/// <summary>
/// URL-friendly идентификатор (слаг).
/// </summary>
public string Slug { get; init; }
/// <summary>
/// ID родительской категории (null для корневых).
/// </summary>
public Guid? ParentId { get; init; }
/// <summary>
/// Шаблон характеристик (JSON).
/// </summary>
public JsonDocument? AttributeSchema { get; init; }
public CreateCategoryCommand(string title, string slug, Guid? parentId, JsonDocument? attributeSchema)
{
Title = title;
Slug = slug;
ParentId = parentId;
AttributeSchema = attributeSchema;
}
public CreateCategoryCommand() { } // For deserialization
}
public class CreateCategoryCommandHandler : IRequestHandler<CreateCategoryCommand, Guid>
{
private readonly ICategoryRepository _repository;
public CreateCategoryCommandHandler(ICategoryRepository repository)
{
_repository = repository;
}
public async Task<Guid> Handle(CreateCategoryCommand request, CancellationToken cancellationToken)
{
if (request.ParentId.HasValue)
{
var parentCategory = await _repository.GetByIdAsync(request.ParentId.Value, cancellationToken);
if (parentCategory == null)
{
throw new ApplicationException($"Родительская категория с ID {request.ParentId} не найдена.");
}
}
var category = new Category(
request.Title,
request.Slug,
request.ParentId,
request.AttributeSchema
);
await _repository.AddAsync(category, cancellationToken);
return category.Id;
}
}

View File

@@ -0,0 +1,81 @@
using System.Text.Json;
using MediatR;
using Nashel.Modules.Catalog.Domain.Aggregates;
using Nashel.Modules.Catalog.Domain.Repositories;
using Nashel.Modules.Catalog.Domain.ValueObjects;
namespace Nashel.Modules.Catalog.Application.Commands;
/// <summary>
/// Команда создания услуги (оффера).
/// </summary>
public record CreateOfferCommand : IRequest<Guid>
{
/// <summary>
/// ID владельца (пользователя/исполнителя).
/// </summary>
public Guid OwnerId { get; init; }
/// <summary>
/// ID категории услуги.
/// </summary>
public Guid CategoryId { get; init; }
/// <summary>
/// Заголовок объявления.
/// </summary>
public string Title { get; init; } = default!;
/// <summary>
/// Полное описание услуги.
/// </summary>
public string Description { get; init; } = default!;
/// <summary>
/// Цена услуги.
/// </summary>
public Price Price { get; init; } = default!;
/// <summary>
/// Характеристики услуги (JSON).
/// </summary>
public JsonDocument? Attributes { get; init; }
public CreateOfferCommand(Guid ownerId, Guid categoryId, string title, string description, Price price, JsonDocument? attributes)
{
OwnerId = ownerId;
CategoryId = categoryId;
Title = title;
Description = description;
Price = price;
Attributes = attributes;
}
public CreateOfferCommand() { }
}
public class CreateOfferCommandHandler : IRequestHandler<CreateOfferCommand, Guid>
{
private readonly IOfferRepository _repository;
public CreateOfferCommandHandler(IOfferRepository repository)
{
_repository = repository;
}
public async Task<Guid> Handle(CreateOfferCommand request, CancellationToken cancellationToken)
{
var offer = new Offer(
request.OwnerId,
request.CategoryId,
request.Title,
request.Description,
request.Price,
request.Attributes
);
await _repository.AddAsync(offer, cancellationToken);
return offer.Id;
}
}

View File

@@ -0,0 +1,29 @@
using System.Text.Json;
using Nashel.Modules.Catalog.Domain.ValueObjects;
namespace Nashel.Modules.Catalog.Application.Common;
/// <summary>
/// DTO категории.
/// </summary>
public record CategoryDto(
Guid Id,
string Title,
string Slug,
Guid? ParentId,
JsonDocument? AttributeSchema
);
/// <summary>
/// DTO услуги/оффера.
/// </summary>
public record OfferDto(
Guid Id,
Guid OwnerId,
Guid CategoryId,
string Title,
string Description,
Price Price,
JsonDocument? Attributes,
bool IsActive
);

View File

@@ -11,5 +11,7 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,38 @@
using System.Text.Json;
using MediatR;
using Nashel.Modules.Catalog.Application.Common;
using Nashel.Modules.Catalog.Domain.Repositories;
namespace Nashel.Modules.Catalog.Application.Queries;
/// <summary>
/// Запрос дерева категорий.
/// </summary>
public record GetCategoriesQuery() : IRequest<List<CategoryDto>>;
public class GetCategoriesQueryHandler : IRequestHandler<GetCategoriesQuery, List<CategoryDto>>
{
private readonly ICategoryRepository _repository;
public GetCategoriesQueryHandler(ICategoryRepository repository)
{
_repository = repository;
}
public async Task<List<CategoryDto>> Handle(GetCategoriesQuery request, CancellationToken cancellationToken)
{
var rawCategories = await _repository.GetAllAsync(cancellationToken);
// Преобразование в DTO (для дерева логика нужна сложнее, но пока плоский список для старта)
// Если нужно дерево: нужно иметь DTO с List<CategoryDto> Children
// Для MVP возвращаем плоский список, фронтенд сам строит дерево по ParentId
return rawCategories.Select(c => new CategoryDto(
c.Id,
c.Title,
c.Slug,
c.ParentId,
c.AttributeSchema
)).ToList();
}
}

View File

@@ -0,0 +1,37 @@
using MediatR;
using Nashel.Modules.Catalog.Application.Common;
using Nashel.Modules.Catalog.Domain.Repositories;
namespace Nashel.Modules.Catalog.Application.Queries;
/// <summary>
/// Запрос деталей услуги по ID.
/// </summary>
public record GetOfferByIdQuery(Guid Id) : IRequest<OfferDto?>;
public class GetOfferByIdQueryHandler : IRequestHandler<GetOfferByIdQuery, OfferDto?>
{
private readonly IOfferRepository _repository;
public GetOfferByIdQueryHandler(IOfferRepository repository)
{
_repository = repository;
}
public async Task<OfferDto?> Handle(GetOfferByIdQuery request, CancellationToken cancellationToken)
{
var offer = await _repository.GetByIdAsync(request.Id, cancellationToken);
if (offer == null) return null;
return new OfferDto(
offer.Id,
offer.OwnerId,
offer.CategoryId,
offer.Title,
offer.Description,
offer.Price,
offer.Attributes,
offer.IsActive
);
}
}

View File

@@ -0,0 +1,50 @@
using System.Text.Json;
using System.Text.Json.Nodes;
namespace Nashel.Modules.Catalog.Domain.Aggregates;
/// <summary>
/// Категория услуг.
/// </summary>
public class Category
{
/// <summary>
/// Уникальный идентификатор.
/// </summary>
public Guid Id { get; private set; }
/// <summary>
/// Название категории.
/// </summary>
public string Title { get; private set; }
/// <summary>
/// URL-friendly идентификатор (слаг).
/// </summary>
public string Slug { get; private set; }
/// <summary>
/// ID родительской категории (null для корневых).
/// </summary>
public Guid? ParentId { get; private set; }
/// <summary>
/// Шаблон характеристик (JSON).
/// </summary>
public JsonDocument? AttributeSchema { get; private set; }
// Конструктор по умолчанию для EF Core
private Category() { }
/// <summary>
/// Создает новую категорию.
/// </summary>
public Category(string title, string slug, Guid? parentId, JsonDocument? attributeSchema = null)
{
Id = Guid.NewGuid();
Title = title;
Slug = slug;
ParentId = parentId;
AttributeSchema = attributeSchema;
}
}

View File

@@ -0,0 +1,68 @@
using System.Text.Json;
using Nashel.Modules.Catalog.Domain.ValueObjects;
namespace Nashel.Modules.Catalog.Domain.Aggregates;
/// <summary>
/// Услуга или оффер исполнителя.
/// </summary>
public class Offer
{
/// <summary>
/// Уникальный идентификатор оффера.
/// </summary>
public Guid Id { get; private set; }
/// <summary>
/// ID владельца (пользователя/исполнителя).
/// </summary>
public Guid OwnerId { get; private set; }
/// <summary>
/// ID услуги/категории.
/// </summary>
public Guid CategoryId { get; private set; }
/// <summary>
/// Заголовок объявления.
/// </summary>
public string Title { get; private set; }
/// <summary>
/// Полное описание.
/// </summary>
public string? Description { get; private set; }
/// <summary>
/// Цена (сумма, валюта, тип оплаты).
/// </summary>
public Price Price { get; private set; }
/// <summary>
/// Характеристики (JSONB).
/// </summary>
public JsonDocument? Attributes { get; private set; }
/// <summary>
/// Активно ли объявление.
/// </summary>
public bool IsActive { get; private set; }
// Конструктор по умолчанию для EF Core
private Offer() { }
/// <summary>
/// Создает новый оффер.
/// </summary>
public Offer(Guid ownerId, Guid categoryId, string title, string description, Price price, JsonDocument? attributes = null)
{
Id = Guid.NewGuid();
OwnerId = ownerId;
CategoryId = categoryId;
Title = title;
Description = description;
Price = price;
Attributes = attributes;
IsActive = true;
}
}

View File

@@ -0,0 +1,22 @@
namespace Nashel.Modules.Catalog.Domain.Enums;
/// <summary>
/// Тип оплаты услуги.
/// </summary>
public enum OfferType
{
/// <summary>
/// Фиксированная цена.
/// </summary>
Fixed,
/// <summary>
/// Почасовая оплата.
/// </summary>
Hourly,
/// <summary>
/// Договорная цена.
/// </summary>
Negotiable
}

View File

@@ -9,6 +9,8 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,10 @@
using Nashel.Modules.Catalog.Domain.Aggregates;
namespace Nashel.Modules.Catalog.Domain.Repositories;
public interface ICategoryRepository
{
Task<Category?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<List<Category>> GetAllAsync(CancellationToken cancellationToken = default);
Task AddAsync(Category category, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,9 @@
using Nashel.Modules.Catalog.Domain.Aggregates;
namespace Nashel.Modules.Catalog.Domain.Repositories;
public interface IOfferRepository
{
Task<Offer?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task AddAsync(Offer offer, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,35 @@
using System.Text.Json.Serialization;
using Nashel.Modules.Catalog.Domain.Enums;
namespace Nashel.Modules.Catalog.Domain.ValueObjects;
/// <summary>
/// Значение цены услуги.
/// </summary>
public record Price
{
/// <summary>
/// Сумма.
/// </summary>
public decimal Amount { get; init; }
/// <summary>
/// Валюта (по умолчанию RUB).
/// </summary>
public string Currency { get; init; } = "RUB";
/// <summary>
/// Тип оплаты (фиксированная, почасовая, договорная).
/// </summary>
public OfferType Type { get; init; }
// Конструктор по умолчанию для EF Core и сериализации
public Price() { }
public Price(decimal amount, OfferType type, string currency = "RUB")
{
Amount = amount;
Type = type;
Currency = currency;
}
}

View File

@@ -0,0 +1,34 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Nashel.Modules.Catalog.Application.Commands;
using Nashel.Modules.Catalog.Domain.Repositories;
using Nashel.Modules.Catalog.Infrastructure.Persistence;
using Nashel.Modules.Catalog.Infrastructure.Repositories;
namespace Nashel.Modules.Catalog.Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddCatalogModule(this IServiceCollection services, IConfiguration configuration)
{
// 1. Регистрация DbContext
services.AddDbContext<CatalogDbContext>(options =>
{
options.UseNpgsql(configuration.GetConnectionString("DefaultConnection"));
});
// 2. Репозитории
services.AddScoped<ICategoryRepository, CategoryRepository>();
services.AddScoped<IOfferRepository, OfferRepository>();
// 3. MediatR (сканируем сборку Application)
services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(CreateCategoryCommand).Assembly);
});
return services;
}
}

View File

@@ -0,0 +1,55 @@
using Microsoft.EntityFrameworkCore;
using Nashel.Modules.Catalog.Domain.Aggregates;
namespace Nashel.Modules.Catalog.Infrastructure.Persistence;
public class CatalogDbContext : DbContext
{
public DbSet<Category> Categories { get; set; } = null!;
public DbSet<Offer> Offers { get; set; } = null!;
public CatalogDbContext(DbContextOptions<CatalogDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasPostgresExtension("postgis"); // Если нужно для чего-то еще, но вообще JSONB встроен.
// Но лучше просто не трогать расширения если не уверены.
// Для JSONB ничего особенного не нужно, кроме HasColumnType("jsonb").
modelBuilder.Entity<Category>(entity =>
{
entity.ToTable("Categories", "catalog");
entity.HasKey(e => e.Id);
entity.HasIndex(e => e.Slug).IsUnique();
// Self-referencing
entity.HasOne<Category>()
.WithMany() // Навигационное свойство Children не добавлено в доменную модель явно, но связь есть
.HasForeignKey(e => e.ParentId)
.OnDelete(DeleteBehavior.Restrict);
entity.Property(e => e.AttributeSchema)
.HasColumnType("jsonb");
});
modelBuilder.Entity<Offer>(entity =>
{
entity.ToTable("Offers", "catalog");
entity.HasKey(e => e.Id);
entity.Property(e => e.Attributes)
.HasColumnType("jsonb");
// GIN Index
entity.HasIndex(e => e.Attributes)
.HasMethod("gin");
entity.OwnsOne(e => e.Price, price =>
{
price.Property(p => p.Amount).HasColumnName("PriceAmount");
price.Property(p => p.Currency).HasColumnName("PriceCurrency").HasMaxLength(3);
price.Property(p => p.Type).HasColumnName("PriceType"); // Enum as int by default
});
});
}
}

View File

@@ -0,0 +1,137 @@
// <auto-generated />
using System;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Nashel.Modules.Catalog.Infrastructure.Persistence;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Nashel.Modules.Catalog.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(CatalogDbContext))]
[Migration("20260210114511_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Category", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<JsonDocument>("AttributeSchema")
.HasColumnType("jsonb");
b.Property<Guid?>("ParentId")
.HasColumnType("uuid");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ParentId");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Categories", "catalog");
});
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Offer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<JsonDocument>("Attributes")
.HasColumnType("jsonb");
b.Property<Guid>("CategoryId")
.HasColumnType("uuid");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<Guid>("OwnerId")
.HasColumnType("uuid");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Attributes");
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Attributes"), "gin");
b.ToTable("Offers", "catalog");
});
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Category", b =>
{
b.HasOne("Nashel.Modules.Catalog.Domain.Aggregates.Category", null)
.WithMany()
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Offer", b =>
{
b.OwnsOne("Nashel.Modules.Catalog.Domain.ValueObjects.Price", "Price", b1 =>
{
b1.Property<Guid>("OfferId")
.HasColumnType("uuid");
b1.Property<decimal>("Amount")
.HasColumnType("numeric")
.HasColumnName("PriceAmount");
b1.Property<string>("Currency")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("character varying(3)")
.HasColumnName("PriceCurrency");
b1.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("PriceType");
b1.HasKey("OfferId");
b1.ToTable("Offers", "catalog");
b1.WithOwner()
.HasForeignKey("OfferId");
});
b.Navigation("Price")
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,98 @@
using System;
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Nashel.Modules.Catalog.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "catalog");
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:PostgresExtension:postgis", ",,");
migrationBuilder.CreateTable(
name: "Categories",
schema: "catalog",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Title = table.Column<string>(type: "text", nullable: false),
Slug = table.Column<string>(type: "text", nullable: false),
ParentId = table.Column<Guid>(type: "uuid", nullable: true),
AttributeSchema = table.Column<JsonDocument>(type: "jsonb", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Categories", x => x.Id);
table.ForeignKey(
name: "FK_Categories_Categories_ParentId",
column: x => x.ParentId,
principalSchema: "catalog",
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Offers",
schema: "catalog",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
OwnerId = table.Column<Guid>(type: "uuid", nullable: false),
CategoryId = table.Column<Guid>(type: "uuid", nullable: false),
Title = table.Column<string>(type: "text", nullable: false),
Description = table.Column<string>(type: "text", nullable: true),
PriceAmount = table.Column<decimal>(type: "numeric", nullable: false),
PriceCurrency = table.Column<string>(type: "character varying(3)", maxLength: 3, nullable: false),
PriceType = table.Column<int>(type: "integer", nullable: false),
Attributes = table.Column<JsonDocument>(type: "jsonb", nullable: true),
IsActive = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Offers", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Categories_ParentId",
schema: "catalog",
table: "Categories",
column: "ParentId");
migrationBuilder.CreateIndex(
name: "IX_Categories_Slug",
schema: "catalog",
table: "Categories",
column: "Slug",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Offers_Attributes",
schema: "catalog",
table: "Offers",
column: "Attributes")
.Annotation("Npgsql:IndexMethod", "gin");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Categories",
schema: "catalog");
migrationBuilder.DropTable(
name: "Offers",
schema: "catalog");
}
}
}

View File

@@ -0,0 +1,134 @@
// <auto-generated />
using System;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Nashel.Modules.Catalog.Infrastructure.Persistence;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Nashel.Modules.Catalog.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(CatalogDbContext))]
partial class CatalogDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Category", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<JsonDocument>("AttributeSchema")
.HasColumnType("jsonb");
b.Property<Guid?>("ParentId")
.HasColumnType("uuid");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ParentId");
b.HasIndex("Slug")
.IsUnique();
b.ToTable("Categories", "catalog");
});
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Offer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<JsonDocument>("Attributes")
.HasColumnType("jsonb");
b.Property<Guid>("CategoryId")
.HasColumnType("uuid");
b.Property<string>("Description")
.HasColumnType("text");
b.Property<bool>("IsActive")
.HasColumnType("boolean");
b.Property<Guid>("OwnerId")
.HasColumnType("uuid");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Attributes");
NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Attributes"), "gin");
b.ToTable("Offers", "catalog");
});
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Category", b =>
{
b.HasOne("Nashel.Modules.Catalog.Domain.Aggregates.Category", null)
.WithMany()
.HasForeignKey("ParentId")
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity("Nashel.Modules.Catalog.Domain.Aggregates.Offer", b =>
{
b.OwnsOne("Nashel.Modules.Catalog.Domain.ValueObjects.Price", "Price", b1 =>
{
b1.Property<Guid>("OfferId")
.HasColumnType("uuid");
b1.Property<decimal>("Amount")
.HasColumnType("numeric")
.HasColumnName("PriceAmount");
b1.Property<string>("Currency")
.IsRequired()
.HasMaxLength(3)
.HasColumnType("character varying(3)")
.HasColumnName("PriceCurrency");
b1.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("PriceType");
b1.HasKey("OfferId");
b1.ToTable("Offers", "catalog");
b1.WithOwner()
.HasForeignKey("OfferId");
});
b.Navigation("Price")
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore;
using Nashel.Modules.Catalog.Domain.Aggregates;
using Nashel.Modules.Catalog.Domain.Repositories;
using Nashel.Modules.Catalog.Infrastructure.Persistence;
namespace Nashel.Modules.Catalog.Infrastructure.Repositories;
public class CategoryRepository : ICategoryRepository
{
private readonly CatalogDbContext _context;
public CategoryRepository(CatalogDbContext context)
{
_context = context;
}
public async Task<Category?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
return await _context.Categories
.Include(c => c.AttributeSchema) // Not needed as it's a property now, but sometimes needed if navigation property (JsonDocument is property)
.FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
}
public async Task<List<Category>> GetAllAsync(CancellationToken cancellationToken = default)
{
return await _context.Categories.ToListAsync(cancellationToken);
}
public async Task AddAsync(Category category, CancellationToken cancellationToken = default)
{
await _context.Categories.AddAsync(category, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}
}

View File

@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore;
using Nashel.Modules.Catalog.Domain.Aggregates;
using Nashel.Modules.Catalog.Domain.Repositories;
using Nashel.Modules.Catalog.Infrastructure.Persistence;
namespace Nashel.Modules.Catalog.Infrastructure.Repositories;
public class OfferRepository : IOfferRepository
{
private readonly CatalogDbContext _context;
public OfferRepository(CatalogDbContext context)
{
_context = context;
}
public async Task<Offer?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
return await _context.Offers
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
}
public async Task AddAsync(Offer offer, CancellationToken cancellationToken = default)
{
await _context.Offers.AddAsync(offer, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
}
}

View File

@@ -0,0 +1,56 @@
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Nashel.Modules.Catalog.Application.Commands;
using Nashel.Modules.Catalog.Application.Queries;
namespace Nashel.Modules.Catalog.Presentation.Endpoints;
/// <summary>
/// API Каталога (Категории и Услуги).
/// </summary>
public static class CatalogEndpoints
{
public static void MapCatalogEndpoints(this IEndpointRouteBuilder app)
{
var catalogGroup = app.MapGroup("/api/catalog").WithTags("Catalog");
// --- Категории ---
catalogGroup.MapPost("/categories", async ([FromBody] CreateCategoryCommand command, ISender sender) =>
{
var id = await sender.Send(command);
return Results.Ok(id);
})
.WithName("CreateCategory")
.WithSummary("Создать категорию (Admin)");
catalogGroup.MapGet("/categories", async (ISender sender) =>
{
var result = await sender.Send(new GetCategoriesQuery());
return Results.Ok(result);
})
.WithName("GetCategories")
.WithSummary("Получить все категории (плоский список с ParentId)");
// --- Услуги (Offers) ---
catalogGroup.MapPost("/offers", async ([FromBody] CreateOfferCommand command, ISender sender) =>
{
var id = await sender.Send(command);
return Results.Ok(id);
})
.WithName("CreateOffer")
.WithSummary("Создать оффер/услугу");
catalogGroup.MapGet("/offers/{id:guid}", async (Guid id, ISender sender) =>
{
var result = await sender.Send(new GetOfferByIdQuery(id));
return result is not null ? Results.Ok(result) : Results.NotFound();
})
.WithName("GetOfferById")
.WithSummary("Получить детали услуги по ID");
}
}

View File

@@ -7,5 +7,10 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,44 @@
using FluentAssertions;
using Moq;
using Nashel.Modules.Catalog.Application.Commands;
using Nashel.Modules.Catalog.Domain.Aggregates;
using Nashel.Modules.Catalog.Domain.Repositories;
using Xunit;
namespace Nashel.Modules.Catalog.Tests.Application;
public class CreateCategoryCommandHandlerTests
{
private readonly Mock<ICategoryRepository> _categoryRepositoryMock;
private readonly CreateCategoryCommandHandler _handler;
public CreateCategoryCommandHandlerTests()
{
_categoryRepositoryMock = new Mock<ICategoryRepository>();
_handler = new CreateCategoryCommandHandler(_categoryRepositoryMock.Object);
}
[Fact]
public async Task Handle_ShouldCreateCategory_WhenCommandIsValid()
{
// Arrange
var command = new CreateCategoryCommand(
"Test Category",
"test-category",
null,
null
);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Should().Be(result); // Guid should be returned
_categoryRepositoryMock.Verify(r => r.AddAsync(It.Is<Category>(c =>
c.Title == command.Title &&
c.Slug == command.Slug &&
c.ParentId == command.ParentId &&
c.AttributeSchema == command.AttributeSchema
), It.IsAny<CancellationToken>()), Times.Once);
}
}

View File

@@ -0,0 +1,53 @@
using FluentAssertions;
using Moq;
using Nashel.Modules.Catalog.Application.Commands;
using Nashel.Modules.Catalog.Domain.Aggregates;
using Nashel.Modules.Catalog.Domain.Enums;
using Nashel.Modules.Catalog.Domain.Repositories;
using Nashel.Modules.Catalog.Domain.ValueObjects;
using System.Text.Json;
using Xunit;
namespace Nashel.Modules.Catalog.Tests.Application;
public class CreateOfferCommandHandlerTests
{
private readonly Mock<IOfferRepository> _offerRepositoryMock;
private readonly CreateOfferCommandHandler _handler;
public CreateOfferCommandHandlerTests()
{
_offerRepositoryMock = new Mock<IOfferRepository>();
_handler = new CreateOfferCommandHandler(_offerRepositoryMock.Object);
}
[Fact]
public async Task Handle_ShouldCreateOffer_WhenCommandIsValid()
{
// Arrange
var command = new CreateOfferCommand(
Guid.NewGuid(),
Guid.NewGuid(),
"Test Offer",
"Test Description",
new Price(100, OfferType.Fixed, "RUB"),
JsonDocument.Parse("{}")
);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Should().Be(result); // Guid should be returned
_offerRepositoryMock.Verify(r => r.AddAsync(It.Is<Offer>(o =>
o.Title == command.Title &&
o.OwnerId == command.OwnerId &&
o.CategoryId == command.CategoryId &&
o.Description == command.Description &&
o.Price.Amount == command.Price.Amount &&
o.Price.Type == command.Price.Type &&
o.Price.Currency == command.Price.Currency &&
o.Attributes == command.Attributes
), It.IsAny<CancellationToken>()), Times.Once);
}
}

View File

@@ -0,0 +1,67 @@
using FluentAssertions;
using Moq;
using Nashel.Modules.Catalog.Application.Common;
using Nashel.Modules.Catalog.Application.Queries;
using Nashel.Modules.Catalog.Domain.Aggregates;
using Nashel.Modules.Catalog.Domain.Repositories;
using System.Text.Json;
using Xunit;
namespace Nashel.Modules.Catalog.Tests.Application;
public class GetCategoriesQueryHandlerTests
{
private readonly Mock<ICategoryRepository> _categoryRepositoryMock;
private readonly GetCategoriesQueryHandler _handler;
public GetCategoriesQueryHandlerTests()
{
_categoryRepositoryMock = new Mock<ICategoryRepository>();
_handler = new GetCategoriesQueryHandler(_categoryRepositoryMock.Object);
}
[Fact]
public async Task Handle_ShouldReturnCategories_WhenCategoriesExist()
{
// Arrange
var categories = new List<Category>
{
new Category("Category 1", "category-1", null),
new Category("Category 2", "category-2", null)
};
_categoryRepositoryMock.Setup(r => r.GetAllAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(categories);
var query = new GetCategoriesQuery();
// Act
var result = await _handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Should().HaveCount(2);
var expectedDto1 = new CategoryDto(categories[0].Id, categories[0].Title, categories[0].Slug, categories[0].ParentId, categories[0].AttributeSchema);
var expectedDto2 = new CategoryDto(categories[1].Id, categories[1].Title, categories[1].Slug, categories[1].ParentId, categories[1].AttributeSchema);
result.Should().ContainEquivalentOf(expectedDto1);
result.Should().ContainEquivalentOf(expectedDto2);
}
[Fact]
public async Task Handle_ShouldReturnEmptyList_WhenNoCategoriesExist()
{
// Arrange
_categoryRepositoryMock.Setup(r => r.GetAllAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<Category>());
var query = new GetCategoriesQuery();
// Act
var result = await _handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Should().BeEmpty();
}
}

View File

@@ -0,0 +1,79 @@
using FluentAssertions;
using Moq;
using Nashel.Modules.Catalog.Application.Common;
using Nashel.Modules.Catalog.Application.Queries;
using Nashel.Modules.Catalog.Domain.Aggregates;
using Nashel.Modules.Catalog.Domain.Repositories;
using Nashel.Modules.Catalog.Domain.ValueObjects;
using Nashel.Modules.Catalog.Domain.Enums;
using System.Text.Json;
using Xunit;
namespace Nashel.Modules.Catalog.Tests.Application;
public class GetOfferByIdQueryHandlerTests
{
private readonly Mock<IOfferRepository> _offerRepositoryMock;
private readonly GetOfferByIdQueryHandler _handler;
public GetOfferByIdQueryHandlerTests()
{
_offerRepositoryMock = new Mock<IOfferRepository>();
_handler = new GetOfferByIdQueryHandler(_offerRepositoryMock.Object);
}
[Fact]
public async Task Handle_ShouldReturnOffer_WhenOfferExists()
{
// Arrange
var offerId = Guid.NewGuid();
var offer = new Offer(
Guid.NewGuid(),
Guid.NewGuid(),
"Test Offer",
"Test Description",
new Price(100, OfferType.Fixed, "RUB"),
JsonDocument.Parse("{}")
);
// Reflection hack to set the ID as it's private set in constructor (Guid.NewGuid())
// But wait, the constructor sets a new GUID.
// We need to mock the repository to return the offer when GetByIdAsync is called with the offer's generated ID.
// Or cleaner: modify the repository setup to ignore the specific ID argument or capture it?
// No, let's just use the ID generated by the offer.
_offerRepositoryMock.Setup(r => r.GetByIdAsync(offer.Id, It.IsAny<CancellationToken>()))
.ReturnsAsync(offer);
var query = new GetOfferByIdQuery(offer.Id);
// Act
var result = await _handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Id.Should().Be(offer.Id);
result.Title.Should().Be("Test Offer");
result.Description.Should().Be("Test Description");
result.Price.Should().BeEquivalentTo(offer.Price);
result.OwnerId.Should().Be(offer.OwnerId);
result.CategoryId.Should().Be(offer.CategoryId);
result.Attributes.Should().Be(offer.Attributes);
}
[Fact]
public async Task Handle_ShouldReturnNull_WhenOfferDoesNotExist()
{
// Arrange
_offerRepositoryMock.Setup(r => r.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((Offer?)null);
var query = new GetOfferByIdQuery(Guid.NewGuid());
// Act
var result = await _handler.Handle(query, CancellationToken.None);
// Assert
result.Should().BeNull();
}
}

View File

@@ -0,0 +1,40 @@
using FluentAssertions;
using Nashel.Modules.Catalog.Domain.Aggregates;
using System.Text.Json;
using Xunit;
namespace Nashel.Modules.Catalog.Tests.Domain;
public class CategoryTests
{
[Fact]
public void Constructor_ShouldSetPropertiesCorrectly()
{
// Assemble
var title = "Test Category";
var slug = "test-category";
var parentId = Guid.NewGuid();
var jsonDoc = JsonDocument.Parse("{}");
// Act
var category = new Category(title, slug, parentId, jsonDoc);
// Assert
category.Id.Should().NotBeEmpty();
category.Title.Should().Be(title);
category.Slug.Should().Be(slug);
category.ParentId.Should().Be(parentId);
category.AttributeSchema.Should().Be(jsonDoc);
}
[Fact]
public void Constructor_ShouldSetDefaultValues_WhenOptionalParametersAreNull()
{
// Act
var category = new Category("Test", "test", null, null);
// Assert
category.ParentId.Should().BeNull();
category.AttributeSchema.Should().BeNull();
}
}

View File

@@ -0,0 +1,36 @@
using FluentAssertions;
using Nashel.Modules.Catalog.Domain.Aggregates;
using Nashel.Modules.Catalog.Domain.ValueObjects;
using Nashel.Modules.Catalog.Domain.Enums;
using System.Text.Json;
using Xunit;
namespace Nashel.Modules.Catalog.Tests.Domain;
public class OfferTests
{
[Fact]
public void Constructor_ShouldSetPropertiesCorrectly()
{
// Assemble
var ownerId = Guid.NewGuid();
var categoryId = Guid.NewGuid();
var title = "Test Offer";
var description = "Test Description";
var price = new Price(100, OfferType.Fixed, "RUB");
var attributes = JsonDocument.Parse("{}");
// Act
var offer = new Offer(ownerId, categoryId, title, description, price, attributes);
// Assert
offer.Id.Should().NotBeEmpty();
offer.OwnerId.Should().Be(ownerId);
offer.CategoryId.Should().Be(categoryId);
offer.Title.Should().Be(title);
offer.Description.Should().Be(description);
offer.Price.Should().Be(price);
offer.Attributes.Should().Be(attributes);
offer.IsActive.Should().BeTrue();
}
}

View File

@@ -0,0 +1,58 @@
using FluentAssertions;
using Nashel.Modules.Catalog.Domain.Enums;
using Nashel.Modules.Catalog.Domain.ValueObjects;
using Xunit;
namespace Nashel.Modules.Catalog.Tests.Domain;
public class PriceTests
{
[Fact]
public void Constructor_ShouldSetPropertiesCorrectly()
{
// Assemble
var amount = 100m;
var type = OfferType.Fixed;
var currency = "USD";
// Act
var price = new Price(amount, type, currency);
// Assert
price.Amount.Should().Be(amount);
price.Type.Should().Be(type);
price.Currency.Should().Be(currency);
}
[Fact]
public void Constructor_ShouldSetDefaultCurrency_WhenNotProvided()
{
// Act
var price = new Price(100m, OfferType.Fixed);
// Assert
price.Currency.Should().Be("RUB");
}
[Fact]
public void Equality_ShouldBeTrue_WhenValuesAreSame()
{
// Arrange
var price1 = new Price(100m, OfferType.Fixed, "USD");
var price2 = new Price(100m, OfferType.Fixed, "USD");
// Act & Assert
price1.Should().Be(price2);
}
[Fact]
public void Equality_ShouldBeFalse_WhenValuesAreDifferent()
{
// Arrange
var price1 = new Price(100m, OfferType.Fixed, "USD");
var price2 = new Price(200m, OfferType.Fixed, "USD");
// Act & Assert
price1.Should().NotBe(price2);
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Moq" Version="4.20.70" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Domain\Nashel.Modules.Catalog.Domain.csproj" />
<ProjectReference Include="..\Application\Nashel.Modules.Catalog.Application.csproj" />
<ProjectReference Include="..\..\..\BuildingBlocks\Nashel.BuildingBlocks.csproj" />
</ItemGroup>
</Project>

View File

@@ -6,7 +6,29 @@ using Nashel.Modules.Identity.Domain.Services;
namespace Nashel.Modules.Identity.Application.Commands;
public record LoginCommand(string Phone, string Password) : IRequest<string>; // Возвращает JWT
/// <summary>
/// Команда входа пользователя.
/// </summary>
public record LoginCommand : IRequest<string>
{
/// <summary>
/// Номер телефона.
/// </summary>
public string Phone { get; init; } = default!;
/// <summary>
/// Пароль.
/// </summary>
public string Password { get; init; } = default!;
public LoginCommand(string phone, string password)
{
Phone = phone;
Password = password;
}
public LoginCommand() { }
} // Возвращает JWT
public class LoginCommandHandler : IRequestHandler<LoginCommand, string>
{

View File

@@ -5,7 +5,29 @@ using Nashel.Modules.Identity.Domain.Repositories;
namespace Nashel.Modules.Identity.Application.Commands;
public record RegisterUserCommand(string Phone, string Password) : IRequest<Guid>;
/// <summary>
/// Команда регистрации нового пользователя.
/// </summary>
public record RegisterUserCommand : IRequest<Guid>
{
/// <summary>
/// Номер телефона пользователя.
/// </summary>
public string Phone { get; init; } = default!;
/// <summary>
/// Пароль пользователя.
/// </summary>
public string Password { get; init; } = default!;
public RegisterUserCommand(string phone, string password)
{
Phone = phone;
Password = password;
}
public RegisterUserCommand() { }
}
public class RegisterUserCommandHandler : IRequestHandler<RegisterUserCommand, Guid>
{

View File

@@ -11,5 +11,7 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
</Project>

View File

@@ -9,6 +9,8 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,57 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Nashel.Modules.Identity.Infrastructure.Persistence;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(IdentityDbContext))]
[Migration("20260210114447_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.Account", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("Roles")
.IsRequired()
.HasColumnType("jsonb");
b.HasKey("Id");
b.HasIndex("Phone")
.IsUnique();
b.ToTable("Accounts", "identity");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,48 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "identity");
migrationBuilder.CreateTable(
name: "Accounts",
schema: "identity",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Phone = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: false),
Roles = table.Column<string>(type: "jsonb", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Accounts", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Accounts_Phone",
schema: "identity",
table: "Accounts",
column: "Phone",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Accounts",
schema: "identity");
}
}
}

View File

@@ -0,0 +1,54 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Nashel.Modules.Identity.Infrastructure.Persistence;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(IdentityDbContext))]
partial class IdentityDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.2")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.Account", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Phone")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("Roles")
.IsRequired()
.HasColumnType("jsonb");
b.HasKey("Id");
b.HasIndex("Phone")
.IsUnique();
b.ToTable("Accounts", "identity");
});
#pragma warning restore 612, 618
}
}
}