Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/WarehouseEngine.Api/Endpoints/WarehouseEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using WarehouseEngine.Api.Extensions.ErrorTypeExtensions;
using WarehouseEngine.Application.Dtos;
using WarehouseEngine.Application.Interfaces;

namespace WarehouseEngine.Api.Endpoints;

internal class WarehouseEndpoints
{
public static void Map(WebApplication app)
{
app.MapGet("/api/v{version:apiVersion}/warehouse/{id}", [Authorize] async (ILogger<WarehouseEndpoints> logger, IWarehouseService warehouseService, Guid id) =>
{
var warehouse = await warehouseService.GetByIdAsync(id);
return warehouse.Match<Results<Ok<WarehouseResponseDto>, ProblemHttpResult>>(
warehouse => TypedResults.Ok(warehouse),
invalidResult =>
{
logger.LogError("Record not found. {message}", invalidResult.GetMessage());
return TypedResults.Problem(statusCode: 404, detail: invalidResult.GetMessage());
});
})
.Produces<WarehouseResponseDto>(200)
.WithName("GetWarehouseById")
.WithTags("Warehouse");

app.MapGet("/api/v{version:apiVersion}/warehouse/count", [Authorize] async (IWarehouseService warehouseService) =>
{
var count = await warehouseService.GetCount();
return TypedResults.Ok(count);
})
.Produces<int>(200)
.WithName("GetWarehouseCount")
.WithTags("Warehouse");

app.MapPost("/api/v{version:apiVersion}/warehouse", [Authorize] async (ILogger<WarehouseEndpoints> logger, IWarehouseService warehouseService, PostWarehouseDto warehouseDto) =>
{
var warehouse = await warehouseService.AddAsync(warehouseDto);

return warehouse.Match<Results<Created, ProblemHttpResult>>(
warehouse => TypedResults.Created(),
entityExists =>
{
logger.LogWarning("Entity already exists. {message}", entityExists.GetMessage());

return TypedResults.Problem(statusCode: 409, detail: entityExists.GetMessage());
});
})
.Produces(201)
.ProducesProblem(409)
.WithName("CreateWarehouse")
.WithTags("Warehouse");
}
}
12 changes: 12 additions & 0 deletions src/WarehouseEngine.Api/Examples/WarehouseExamples.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using WarehouseEngine.Application.Dtos;

namespace WarehouseEngine.Api.Examples;

public static class WarehouseExamples
{
public static readonly WarehouseResponseDto WarehouseResponseDto = new()
{
Id = Guid.NewGuid(),
Name = "Main Warehouse"
};
}
3 changes: 2 additions & 1 deletion src/WarehouseEngine.Api/Examples/_ExampleDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public static class ExampleDictionary
{ typeof(CustomerResponseDto), JsonSerializer.SerializeToNode(CustomerExamples.CustomerResponseDto, JsonSerializerOptions.Web) },
{ typeof(VendorResponseDto), JsonSerializer.SerializeToNode(VendorExamples.VendorResponseDto, JsonSerializerOptions.Web) },
{ typeof(PositionResponseDto), JsonSerializer.SerializeToNode(PositionExamples.PositionResponseDto, JsonSerializerOptions.Web) },
{ typeof(AuthenticationResponse), JsonSerializer.SerializeToNode(AuthenticationExamples.AuthenticationResultExample, JsonSerializerOptions.Web) }
{ typeof(AuthenticationResponse), JsonSerializer.SerializeToNode(AuthenticationExamples.AuthenticationResultExample, JsonSerializerOptions.Web) },
{ typeof(WarehouseResponseDto), JsonSerializer.SerializeToNode(WarehouseExamples.WarehouseResponseDto, JsonSerializerOptions.Web) }
};
}
2 changes: 2 additions & 0 deletions src/WarehouseEngine.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public static async Task Main(string[] args)
services.AddScoped<ICustomerService, CustomerService>();
services.AddScoped<IVendorService, VendorService>();
services.AddScoped<IPositionService, PositionService>();
services.AddScoped<IWarehouseService, WarehouseService>();
services.AddTransient<IIdGenerator, SequentialIdGenerator>();

services.AddAuthentication(options =>
Expand Down Expand Up @@ -177,6 +178,7 @@ [new OpenApiSecuritySchemeReference("Bearer", document)] = []
ItemEndpoints.Map(app);
PositionEndpoints.Map(app);
VendorEndpoints.Map(app);
WarehouseEndpoints.Map(app);

#if DEBUG
app.UseCors("localhost");
Expand Down
53 changes: 53 additions & 0 deletions src/WarehouseEngine.Application/Dtos/Warehouse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using WarehouseEngine.Domain.Entities;
using WarehouseEngine.Domain.Exceptions;

namespace WarehouseEngine.Application.Dtos;

public class WarehouseResponseDto
{
public required Guid Id { get; init; }

[Required]
public required string Name { get; init; }

public static explicit operator WarehouseResponseDto(Warehouse warehouse)
{
return new WarehouseResponseDto
{
Id = warehouse.Id,
Name = warehouse.Name
};
}
}

public class PostWarehouseDto
{
/// <summary>
/// This should be null when deserialized from a request
/// </summary>
[JsonIgnore]
public Guid? Id { get; set; }

[Required]
public required string Name { get; init; }

public static explicit operator Warehouse(PostWarehouseDto dto)
{
if (!dto.Id.HasValue)
{
throw new EntityConversionException<Warehouse, PostWarehouseDto>("Id is null");
}
if (dto.Id.Value == Guid.Empty)
{
throw new EntityConversionException<Warehouse, PostWarehouseDto>("Id is empty");
}

return new Warehouse
{
Id = dto.Id.Value,
Name = dto.Name
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Microsoft.EntityFrameworkCore;
using OneOf;
using WarehouseEngine.Application.Dtos;
using WarehouseEngine.Application.Interfaces;
using WarehouseEngine.Domain.Entities;
using WarehouseEngine.Domain.ErrorTypes;

namespace WarehouseEngine.Application.Implementations;

public class WarehouseService : IWarehouseService
{
private readonly IWarehouseEngineContext _context;
private readonly IIdGenerator _idGenerator;

public WarehouseService(IWarehouseEngineContext context, IIdGenerator idGenerator)
{
_context = context;
_idGenerator = idGenerator;
}

public async Task<OneOf<WarehouseResponseDto, EntityErrorType>> GetByIdAsync(Guid id)
{
Warehouse? entity = await _context.Warehouse
.AsNoTracking()
.SingleOrDefaultAsync(w => w.Id == id);

return entity is not null
? (WarehouseResponseDto)entity
: new EntityDoesNotExist();
}

public async Task<int> GetCount()
{
return await _context.Warehouse.CountAsync();
}

public async Task<OneOf<WarehouseResponseDto, EntityAlreadyExists>> AddAsync(PostWarehouseDto warehouse)
{
if (warehouse.Id is not null)
{
// this is exceptional because this is internal logic
throw new InvalidOperationException("Warehouse should not have new id when created");
}
warehouse.Id = _idGenerator.NewId();

var entity = (Warehouse)warehouse;

if (await _context.Warehouse.AnyAsync(w => entity.Id == w.Id))
return new EntityAlreadyExists();

await _context.Warehouse.AddAsync(entity);
await _context.SaveChangesAsync();

return (WarehouseResponseDto)entity;
}
}
12 changes: 12 additions & 0 deletions src/WarehouseEngine.Application/Interfaces/IWarehouseService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using OneOf;
using WarehouseEngine.Application.Dtos;
using WarehouseEngine.Domain.ErrorTypes;

namespace WarehouseEngine.Application.Interfaces;

public interface IWarehouseService
{
Task<OneOf<WarehouseResponseDto, EntityAlreadyExists>> AddAsync(PostWarehouseDto warehouse);
Task<OneOf<WarehouseResponseDto, EntityErrorType>> GetByIdAsync(Guid id);
Task<int> GetCount();
}
13 changes: 12 additions & 1 deletion src/WarehouseEngine.UI/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"version": "0.2.0",
"configurations": [
{
"type": "edge",
"type": "msedge",
"request": "launch",
"name": "localhost (Edge)",
"url": "http://localhost:4201",
Expand All @@ -14,6 +14,17 @@
"name": "localhost (Chrome)",
"url": "http://localhost:4201",
"webRoot": "${workspaceFolder}"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html",
"webRoot": "${workspaceFolder}",
"sourceMapPathOverrides": {
"*": "${webRoot}/*"
}
}
]
}
14 changes: 7 additions & 7 deletions src/WarehouseEngine.UI/.vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "server started at"
}
}
}
}
Expand Down
30 changes: 2 additions & 28 deletions src/WarehouseEngine.UI/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,36 +94,10 @@
}
},
"test": {
"builder": "@angular-devkit/build-angular:web-test-runner",
"builder": "@angular/build:unit-test",
"options": {
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"],
"styles": [
{
"inject": false,
"input": "src/styles/custom-themes/pink-bluegrey.scss",
"bundleName": "pink-bluegrey"
},
{
"inject": false,
"input": "src/styles/custom-themes/deeppurple-amber.scss",
"bundleName": "deeppurple-amber"
},
{
"inject": false,
"input": "src/styles/custom-themes/indigo-pink.scss",
"bundleName": "indigo-pink"
},
{
"inject": false,
"input": "src/styles/custom-themes/purple-green.scss",
"bundleName": "purple-green"
},
"src/styles.scss"
],
"scripts": []
"browsers": ["chromium"]
}
},
"lint": {
Expand Down
Loading
Loading