Skip to content

Commit

Permalink
feature : catalog brand (#1057)
Browse files Browse the repository at this point in the history
* Add Brand domain model with events and exception handling

Introduced the `Brand` class in the `FSH.Starter.WebApi.Catalog.Domain` namespace, inheriting from `AuditableEntity` and implementing `IAggregateRoot`. Added properties for `Name` and `Description`, and methods for creating and updating brand instances. Queued domain events `BrandCreated` and `BrandUpdated` during these operations.

Added `BrandCreated` and `BrandUpdated` classes in the `FSH.Starter.WebApi.Catalog.Domain.Events` namespace, both inheriting from `DomainEvent` and including a property for the `Brand` instance.

Added `BrandNotFoundException` class in the `FSH.Starter.WebApi.Catalog.Domain.Exceptions` namespace, inheriting from `NotFoundException` to handle cases where a brand with a specified ID is not found.

* Add brand management commands and handlers

Added commands and handlers for brand management:
- CreateBrandCommand, CreateBrandCommandValidator, CreateBrandHandler, and CreateBrandResponse for creating brands.
- DeleteBrandCommand and DeleteBrandHandler for deleting brands.
- BrandCreatedEventHandler for handling brand creation events.
- BrandResponse and GetBrandHandler for retrieving brand details.
- GetBrandRequest for brand retrieval requests.
- SearchBrandSpecs, SearchBrandsCommand, and SearchBrandsHandler for searching brands with pagination.
- UpdateBrandCommand, UpdateBrandCommandValidator, UpdateBrandHandler, and UpdateBrandResponse for updating brand details.

* Add brand-related endpoints in API version 1

Introduce new endpoints for brand operations in the
FSH.Starter.WebApi.Catalog.Infrastructure.Endpoints.v1 namespace.
Endpoints include CreateBrand, DeleteBrand, GetBrand,
SearchBrands, and UpdateBrand, each secured with specific
permissions and mapped to appropriate routes. Handlers use
MediatR for request handling and response production.

* Add BrandConfiguration for EF Core in Catalog project

Introduce BrandConfiguration class to configure the Brand entity
for Entity Framework Core. This includes enabling multi-tenancy,
setting the primary key, and specifying maximum lengths for the
Name (100 characters) and Description (1000 characters) properties.

* Add permissions for managing Brands in FshPermissions

The changes introduce new permissions related to the "Brands" resource in the `FshPermissions` class. Specifically, the following permissions are added:
- View Brands (with `IsBasic` set to true)
- Search Brands (with `IsBasic` set to true)
- Create Brands
- Update Brands
- Delete Brands
- Export Brands

The "View Brands" and "Search Brands" permissions are marked as basic, indicating they might be available to users with basic access rights.

* Refactor product handling to include brand details

- Modified GetProductHandler to use specification pattern
- Added GetProductSpecs for flexible product querying
- Updated ProductResponse to include BrandResponse
- Enhanced SearchProductSpecs to include brand details
- Updated Product class to establish brand relationship
- Modified Create and Update methods to accept brandId

* Add brand management endpoints and services

Introduce new endpoints in CatalogModule for brand creation, retrieval, listing, updating, and deletion. Register scoped services for Brand in RegisterCatalogServices method. Add DbSet<Brand> property in CatalogDbContext for managing Brand entities.

* Add BrandId property to product commands and handlers

- Updated CreateProductCommand and UpdateProductCommand to include BrandId with a default value of null.
- Modified CreateProductHandler and UpdateProductHandler to pass BrandId when creating or updating a product.
- Added BrandId filter condition in SearchProductSpecs.
- Updated CatalogDbInitializer to include BrandId when seeding the database.

* Add Brands table and update Catalog schema

Removed old migration and added new migration to create Brands table alongside Products table. Updated Designer and DbContext snapshot to reflect new schema. Updated project file to include new Catalog folder.

* Add cancellation tokens, brand methods, and update runtime

Enhanced ApiClient with cancellation tokens and new brand methods.
Updated serialization to use JsonSerializerSettings. Upgraded
NJsonSchema and NSwag to 14.1.0.0. Changed runtime in nswag.json
from WinX64 to Net80.

* Add Brands management feature with navigation and CRUD

Introduced a new "Brands" section in the application:
- Added a navigation link for "Brands" in `NavMenu.razor`.
- Implemented permission checks for viewing Brands in `NavMenu.razor.cs`.
- Created `Brands.razor` page with route `/catalog/brands`.
- Set up `EntityTable` component for managing brands.
- Added `Brands` class and dependency injection in `Brands.razor.cs`.
- Defined `BrandViewModel` for CRUD operations in `Brands.razor.cs`.

* Add brand selection to Products component

Added a `MudSelect` component in `Products.razor` for brand selection, bound to `context.BrandId` and populated with a list of brands. Introduced a private `_brands` field in `Products.razor.cs` to store the list of brands. Modified `OnInitialized` to `OnInitializedAsync` and added `LoadBrandsAsync` to fetch brands from the server. Updated `EntityServerTableContext` initialization to include the brand name field.

* Add brand filter dropdown to advanced search

Added a dropdown (`MudSelect`) for selecting a brand in the advanced
search section of the `Products.razor` file, allowing users to filter
products by brand with an "All Brands" option. Updated the search
function in `Products.razor.cs` to include the selected brand ID
(`SearchBrandId`). Changed the type of `SearchBrandId` from `Guid` to
`Guid?` to support the nullable brand ID for the "All Brands" option.

* Remove Catalog folder reference from PostgreSQL.csproj

The `ItemGroup` containing the `<Folder Include="Catalog\" />` line was removed from the `PostgreSQL.csproj` file. This change eliminates the folder reference to `Catalog` from the project file.
  • Loading branch information
jacekmichalski authored Nov 22, 2024
1 parent 23a76cd commit cd770dc
Show file tree
Hide file tree
Showing 52 changed files with 1,700 additions and 180 deletions.
8 changes: 8 additions & 0 deletions src/Shared/Authorization/FshPermissions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ public static class FshPermissions
new("Delete Products", FshActions.Delete, FshResources.Products),
new("Export Products", FshActions.Export, FshResources.Products),

//brands
new("View Brands", FshActions.View, FshResources.Brands, IsBasic: true),
new("Search Brands", FshActions.Search, FshResources.Brands, IsBasic: true),
new("Create Brands", FshActions.Create, FshResources.Brands),
new("Update Brands", FshActions.Update, FshResources.Brands),
new("Delete Brands", FshActions.Delete, FshResources.Brands),
new("Export Brands", FshActions.Export, FshResources.Brands),

//todos
new("View Todos", FshActions.View, FshResources.Todos, IsBasic: true),
new("Search Todos", FshActions.Search, FshResources.Todos, IsBasic: true),
Expand Down

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace FSH.Starter.WebApi.Migrations.PostgreSQL.Catalog
{
/// <inheritdoc />
public partial class AddCatalogSchema : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "catalog");

migrationBuilder.CreateTable(
name: "Brands",
schema: "catalog",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
TenantId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Created = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
LastModified = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
LastModifiedBy = table.Column<Guid>(type: "uuid", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Brands", x => x.Id);
});

migrationBuilder.CreateTable(
name: "Products",
schema: "catalog",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
Price = table.Column<decimal>(type: "numeric", nullable: false),
BrandId = table.Column<Guid>(type: "uuid", nullable: true),
TenantId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Created = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
CreatedBy = table.Column<Guid>(type: "uuid", nullable: false),
LastModified = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
LastModifiedBy = table.Column<Guid>(type: "uuid", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Products", x => x.Id);
table.ForeignKey(
name: "FK_Products_Brands_BrandId",
column: x => x.BrandId,
principalSchema: "catalog",
principalTable: "Brands",
principalColumn: "Id");
});

migrationBuilder.CreateIndex(
name: "IX_Products_BrandId",
schema: "catalog",
table: "Products",
column: "BrandId");
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Products",
schema: "catalog");

migrationBuilder.DropTable(
name: "Brands",
schema: "catalog");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,59 @@ protected override void BuildModel(ModelBuilder modelBuilder)
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("catalog")
.HasAnnotation("ProductVersion", "8.0.6")
.HasAnnotation("ProductVersion", "8.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);

NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);

modelBuilder.Entity("FSH.Starter.WebApi.Catalog.Domain.Brand", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");

b.Property<DateTimeOffset>("Created")
.HasColumnType("timestamp with time zone");

b.Property<Guid>("CreatedBy")
.HasColumnType("uuid");

b.Property<string>("Description")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");

b.Property<DateTimeOffset>("LastModified")
.HasColumnType("timestamp with time zone");

b.Property<Guid?>("LastModifiedBy")
.HasColumnType("uuid");

b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");

b.Property<string>("TenantId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");

b.HasKey("Id");

b.ToTable("Brands", "catalog");

b.HasAnnotation("Finbuckle:MultiTenant", true);
});

modelBuilder.Entity("FSH.Starter.WebApi.Catalog.Domain.Product", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");

b.Property<Guid?>("BrandId")
.HasColumnType("uuid");

b.Property<DateTimeOffset>("Created")
.HasColumnType("timestamp with time zone");

Expand Down Expand Up @@ -60,10 +102,21 @@ protected override void BuildModel(ModelBuilder modelBuilder)

b.HasKey("Id");

b.HasIndex("BrandId");

b.ToTable("Products", "catalog");

b.HasAnnotation("Finbuckle:MultiTenant", true);
});

modelBuilder.Entity("FSH.Starter.WebApi.Catalog.Domain.Product", b =>
{
b.HasOne("FSH.Starter.WebApi.Catalog.Domain.Brand", "Brand")
.WithMany()
.HasForeignKey("BrandId");

b.Navigation("Brand");
});
#pragma warning restore 612, 618
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.ComponentModel;
using MediatR;

namespace FSH.Starter.WebApi.Catalog.Application.Brands.Create.v1;
public sealed record CreateBrandCommand(
[property: DefaultValue("Sample Brand")] string? Name,
[property: DefaultValue("Descriptive Description")] string? Description = null) : IRequest<CreateBrandResponse>;

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using FluentValidation;

namespace FSH.Starter.WebApi.Catalog.Application.Brands.Create.v1;
public class CreateBrandCommandValidator : AbstractValidator<CreateBrandCommand>
{
public CreateBrandCommandValidator()
{
RuleFor(b => b.Name).NotEmpty().MinimumLength(2).MaximumLength(100);
RuleFor(b => b.Description).MaximumLength(1000);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using FSH.Framework.Core.Persistence;
using FSH.Starter.WebApi.Catalog.Domain;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace FSH.Starter.WebApi.Catalog.Application.Brands.Create.v1;
public sealed class CreateBrandHandler(
ILogger<CreateBrandHandler> logger,
[FromKeyedServices("catalog:brands")] IRepository<Brand> repository)
: IRequestHandler<CreateBrandCommand, CreateBrandResponse>
{
public async Task<CreateBrandResponse> Handle(CreateBrandCommand request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var brand = Brand.Create(request.Name!, request.Description);
await repository.AddAsync(brand, cancellationToken);
logger.LogInformation("brand created {BrandId}", brand.Id);
return new CreateBrandResponse(brand.Id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace FSH.Starter.WebApi.Catalog.Application.Brands.Create.v1;

public sealed record CreateBrandResponse(Guid? Id);

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using MediatR;

namespace FSH.Starter.WebApi.Catalog.Application.Brands.Delete.v1;
public sealed record DeleteBrandCommand(
Guid Id) : IRequest;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using FSH.Framework.Core.Persistence;
using FSH.Starter.WebApi.Catalog.Domain;
using FSH.Starter.WebApi.Catalog.Domain.Exceptions;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace FSH.Starter.WebApi.Catalog.Application.Brands.Delete.v1;
public sealed class DeleteBrandHandler(
ILogger<DeleteBrandHandler> logger,
[FromKeyedServices("catalog:brands")] IRepository<Brand> repository)
: IRequestHandler<DeleteBrandCommand>
{
public async Task Handle(DeleteBrandCommand request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var brand = await repository.GetByIdAsync(request.Id, cancellationToken);
_ = brand ?? throw new BrandNotFoundException(request.Id);
await repository.DeleteAsync(brand, cancellationToken);
logger.LogInformation("Brand with id : {BrandId} deleted", brand.Id);
}
}
Loading

0 comments on commit cd770dc

Please sign in to comment.