Skip to content

Commit

Permalink
Miscellaneous fixes. (#15)
Browse files Browse the repository at this point in the history
* Implemented exception handling.

* Fixed empty roster bug.

* Allow to receive regions and types as unique names.

* Implementing an edit modal.

* Fixed SearchNumber.

* Using library components.

* Using library components.

* Using library components.
  • Loading branch information
Utar94 authored Jan 5, 2024
1 parent 5be09a7 commit 575dab9
Show file tree
Hide file tree
Showing 38 changed files with 777 additions and 389 deletions.
2 changes: 1 addition & 1 deletion backend/PokeData.sln
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PokeData.EntityFrameworkCor
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PokeData", "src\PokeData\PokeData.csproj", "{1448F506-73E9-4664-A419-002B79A8C276}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PokeData.Contracts", "src\PokeData.Contracts\PokeData.Contracts.csproj", "{6F2213F6-F202-4C6E-B5DA-5957213C917E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PokeData.Contracts", "src\PokeData.Contracts\PokeData.Contracts.csproj", "{6F2213F6-F202-4C6E-B5DA-5957213C917E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using Logitar;
using PokeData.Contracts.Errors;

namespace PokeData.Application;

public class AggregateNotFoundException : Exception
public class AggregateNotFoundException : Exception, IErrorException
{
public const string ErrorMessage = "The specified aggregate could not be found.";

Expand All @@ -22,6 +23,12 @@ public string? PropertyName
private set => Data[nameof(PropertyName)] = value;
}

public Error Error => new(this.GetErrorCode(), ErrorMessage, new ErrorData[]
{
new(nameof(AggregateId), AggregateId),
new(nameof(PropertyName), PropertyName)
});

public AggregateNotFoundException(Type type, string aggregateId, string? propertyName = null) : base(BuildMessage(type, aggregateId, propertyName))
{
TypeName = type.GetNamespaceQualifiedName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ public async Task<Unit> Handle(SaveRosterItemCommand command, CancellationToken
PokemonType primaryType = await _typeRepository.LoadAsync(payload.PrimaryType, cancellationToken)
?? throw new AggregateNotFoundException<PokemonType>(payload.PrimaryType.ToString(), nameof(payload.PrimaryType));
PokemonType? secondaryType = null;
if (payload.SecondaryType.HasValue)
if (!string.IsNullOrWhiteSpace(payload.SecondaryType))
{
secondaryType = await _typeRepository.LoadAsync(payload.SecondaryType.Value, cancellationToken)
?? throw new AggregateNotFoundException<PokemonType>(payload.SecondaryType.Value.ToString(), nameof(payload.SecondaryType));
secondaryType = await _typeRepository.LoadAsync(payload.SecondaryType, cancellationToken)
?? throw new AggregateNotFoundException<PokemonType>(payload.SecondaryType, nameof(payload.SecondaryType));
}

PokemonRoster roster = new()
Expand Down
7 changes: 7 additions & 0 deletions backend/src/PokeData.Application/TooManyResultsException.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Logitar;
using PokeData.Contracts.Errors;

namespace PokeData.Application;

Expand All @@ -22,6 +23,12 @@ public int ActualCount
private set => Data[nameof(ActualCount)] = value;
}

public Error Error => new(this.GetErrorCode(), ErrorMessage, new ErrorData[]
{
new(nameof(ExpectedCount), ExpectedCount),
new(nameof(ActualCount), ActualCount)
});

public TooManyResultsException(Type type, int expectedCount, int actualCount) : base(BuildMessage(type, expectedCount, actualCount))
{
TypeName = type.GetNamespaceQualifiedName();
Expand Down
18 changes: 18 additions & 0 deletions backend/src/PokeData.Contracts/Errors/Error.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace PokeData.Contracts.Errors;

public record Error
{
public string Code { get; set; }
public string Message { get; set; }
public List<ErrorData>? Data { get; set; }

public Error() : this(string.Empty, string.Empty)
{
}
public Error(string code, string message, IEnumerable<ErrorData>? data = null)
{
Code = code;
Message = message;
Data = data?.ToList();
}
}
16 changes: 16 additions & 0 deletions backend/src/PokeData.Contracts/Errors/ErrorData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace PokeData.Contracts.Errors;

public record ErrorData
{
public string Key { get; set; }
public string? Value { get; set; }

public ErrorData() : this(string.Empty)
{
}
public ErrorData(string key, object? value = null)
{
Key = key;
Value = value?.ToString();
}
}
6 changes: 6 additions & 0 deletions backend/src/PokeData.Contracts/Errors/IErrorException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace PokeData.Contracts.Errors;

public interface IErrorException
{
Error Error { get; }
}
2 changes: 1 addition & 1 deletion backend/src/PokeData.Contracts/Roster/RosterStatistic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ public record RosterStatistic
{
Key = key,
Count = count,
Percentage = count / (double)total
Percentage = total == 0 ? 0 : count / (double)total
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ public record SaveRosterItemPayload
public string Name { get; set; } = string.Empty;
public string? Category { get; set; }

public byte Region { get; set; }
public byte PrimaryType { get; set; }
public byte? SecondaryType { get; set; }
public string Region { get; set; } = string.Empty;
public string PrimaryType { get; set; } = string.Empty;
public string? SecondaryType { get; set; }

public bool IsBaby { get; set; }
public bool IsLegendary { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion backend/src/PokeData.Domain/Regions/IRegionRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

public interface IRegionRepository
{
Task<RegionAggregate?> LoadAsync(byte id, CancellationToken cancellationToken = default);
Task<RegionAggregate?> LoadAsync(string idOrUniqueName, CancellationToken cancellationToken = default);
Task SaveAsync(RegionAggregate region, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

public interface IPokemonTypeRepository
{
Task<PokemonType?> LoadAsync(byte id, CancellationToken cancellationToken = default);
Task<PokemonType?> LoadAsync(string idOrUniqueName, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,23 @@ public PokemonTypeRepository(PokemonContext context)
_context = context;
}

public async Task<PokemonType?> LoadAsync(byte id, CancellationToken cancellationToken)
public async Task<PokemonType?> LoadAsync(string idOrUniqueName, CancellationToken cancellationToken)
{
PokemonTypeEntity? type = await _context.PokemonTypes.AsNoTracking()
.SingleOrDefaultAsync(x => x.PokemonTypeId == id, cancellationToken);
PokemonTypeEntity? type = null;

if (byte.TryParse(idOrUniqueName, out byte id))
{
type = await _context.PokemonTypes.AsNoTracking()
.SingleOrDefaultAsync(x => x.PokemonTypeId == id, cancellationToken);
}

if (type == null)
{
string uniqueNameNormalized = idOrUniqueName.Trim().ToUpper();

type = await _context.PokemonTypes.AsNoTracking()
.SingleOrDefaultAsync(x => x.UniqueNameNormalized == uniqueNameNormalized, cancellationToken);
}

return type == null ? null : DomainMapper.ToPokemonType(type);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,27 @@ public RegionRepository(PokemonContext context)
_context = context;
}

public async Task<RegionAggregate?> LoadAsync(byte id, CancellationToken cancellationToken)
public async Task<RegionAggregate?> LoadAsync(string idOrUniqueName, CancellationToken cancellationToken)
{
RegionEntity? entity = await _context.Regions.AsNoTracking()
.Include(x => x.MainGeneration)
.SingleOrDefaultAsync(x => x.RegionId == id, cancellationToken);
RegionEntity? region = null;

return entity == null ? null : DomainMapper.ToRegion(entity);
if (byte.TryParse(idOrUniqueName, out byte id))
{
region = await _context.Regions.AsNoTracking()
.Include(x => x.MainGeneration)
.SingleOrDefaultAsync(x => x.RegionId == id, cancellationToken);
}

if (region == null)
{
string uniqueNameNormalized = idOrUniqueName.Trim().ToUpper();

region = await _context.Regions.AsNoTracking()
.Include(x => x.MainGeneration)
.SingleOrDefaultAsync(x => x.UniqueNameNormalized == uniqueNameNormalized, cancellationToken);
}

return region == null ? null : DomainMapper.ToRegion(region);
}

public async Task SaveAsync(RegionAggregate region, CancellationToken cancellationToken)
Expand Down
43 changes: 43 additions & 0 deletions backend/src/PokeData/Filters/PokemonExceptionFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using PokeData.Application;

namespace PokeData.Filters;

internal class PokemonExceptionFilter : ExceptionFilterAttribute
{
private readonly Dictionary<Type, Func<ExceptionContext, IActionResult>> _handlers = new()
{
[typeof(ValidationException)] = HandleValidationException
};

public override void OnException(ExceptionContext context)
{
if (_handlers.TryGetValue(context.Exception.GetType(), out Func<ExceptionContext, IActionResult>? handler))
{
context.Result = handler(context);
context.ExceptionHandled = true;
}
else if (context.Exception is AggregateNotFoundException aggregateNotFound)
{
context.Result = new NotFoundObjectResult(aggregateNotFound.Error);
context.ExceptionHandled = true;
}
else if (context.Exception is TooManyResultsException tooManyResults)
{
context.Result = new BadRequestObjectResult(tooManyResults.Error);
context.ExceptionHandled = true;
}
else
{
base.OnException(context);
}
}

private static BadRequestObjectResult HandleValidationException(ExceptionContext context)
{
ValidationException exception = (ValidationException)context.Exception;
return new BadRequestObjectResult(new { exception.Errors });
}
}
3 changes: 2 additions & 1 deletion backend/src/PokeData/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using PokeData.EntityFrameworkCore.Relational;
using PokeData.EntityFrameworkCore.SqlServer;
using PokeData.Extensions;
using PokeData.Filters;
using PokeData.Infrastructure.PokeApiClient;
using PokeData.Settings;

Expand All @@ -22,7 +23,7 @@ public override void ConfigureServices(IServiceCollection services)
{
base.ConfigureServices(services);

services.AddControllers()
services.AddControllers(options => options.Filters.Add<PokemonExceptionFilter>())
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ services:
restart: unless-stopped
environment:
ASPNETCORE_Environment: Development
SQLCONNSTR_Master: Server=pokedata_mssql,27945;Database=Pokemon;User Id=SA;Password=m7tPnE6dB5TQxYCW;Persist Security Info=False;Encrypt=False;
SQLCONNSTR_Master: Server=pokedata_mssql;Database=Pokemon;User Id=SA;Password=m7tPnE6dB5TQxYCW;Persist Security Info=False;Encrypt=False;
ports:
- 43551:8080
4 changes: 2 additions & 2 deletions frontend/.env.development
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
VITE_APP_API_BASE_URL="http://localhost:43551/"
__VITE_APP_API_BASE_URL="https://localhost:32768/"
__VITE_APP_API_BASE_URL="http://localhost:43551/"
VITE_APP_API_BASE_URL="https://localhost:32770/"
48 changes: 48 additions & 0 deletions frontend/src/components/PokemonCategoryInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script setup lang="ts">
import { TarInput, type InputSize } from "logitar-vue3-ui";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
disabled?: boolean;
floating?: boolean;
id?: string;
label?: string;
max?: number;
min?: number;
modelValue?: string;
placeholder?: string;
required?: boolean;
size?: InputSize;
}>(),
{
floating: true,
id: "category",
label: "Category",
max: 128,
},
);
const inputPlaceholder = computed<string | undefined>(() => (props.floating ? props.placeholder ?? props.label : props.placeholder));
defineEmits<{
(e: "update:model-value", value?: string): void;
}>();
</script>

<template>
<TarInput
:disabled="disabled"
:floating="floating"
:id="id"
:label="label"
:max="max"
:min="min"
:model-value="modelValue"
:placeholder="inputPlaceholder"
:required="required"
:size="size"
type="text"
@update:model-value="$emit('update:model-value', $event)"
/>
</template>
48 changes: 48 additions & 0 deletions frontend/src/components/PokemonNameInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script setup lang="ts">
import { TarInput, type InputSize } from "logitar-vue3-ui";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
disabled?: boolean;
floating?: boolean;
id?: string;
label?: string;
max?: number;
min?: number;
modelValue?: string;
placeholder?: string;
required?: boolean;
size?: InputSize;
}>(),
{
floating: true,
id: "name",
label: "Name",
max: 128,
},
);
const inputPlaceholder = computed<string | undefined>(() => (props.floating ? props.placeholder ?? props.label : props.placeholder));
defineEmits<{
(e: "update:model-value", value?: string): void;
}>();
</script>

<template>
<TarInput
:disabled="disabled"
:floating="floating"
:id="id"
:label="label"
:max="max"
:min="min"
:model-value="modelValue"
:placeholder="inputPlaceholder"
:required="required"
:size="size"
type="text"
@update:model-value="$emit('update:model-value', $event)"
/>
</template>
Loading

0 comments on commit 575dab9

Please sign in to comment.