Skip to content

Commit

Permalink
Implementing Language management. (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
Utar94 authored Dec 18, 2024
1 parent b1a577d commit a67601e
Show file tree
Hide file tree
Showing 51 changed files with 1,527 additions and 54 deletions.
Empty file removed .github/workflows/.gitkeep
Empty file.
40 changes: 40 additions & 0 deletions .github/workflows/backend-build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Build CMS Backend

on:
push:
branches:
- main
paths:
- 'backend/**'
pull_request:
branches:
- main
paths:
- 'backend/**'
workflow_dispatch:

defaults:
run:
working-directory: ./backend

jobs:
build:
name: Build CMS Backend
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Setup .NET9
uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.x

- name: Restore dependencies
run: dotnet restore

- name: Build Solution
run: dotnet build --no-restore

- name: Test Solution
run: dotnet test --no-build --verbosity normal --filter Category=Unit
71 changes: 17 additions & 54 deletions backend/Cms.sln
Original file line number Diff line number Diff line change
Expand Up @@ -12,75 +12,38 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logitar.Cms.Contracts", "src\Logitar.Cms.Contracts\Logitar.Cms.Contracts.csproj", "{02C0849F-005D-43A1-9EED-4CA0998C35FA}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logitar.Cms.Core", "src\Logitar.Cms.Core\Logitar.Cms.Core.csproj", "{382907CD-18AF-45DE-A1AB-263353F0A9C8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logitar.Cms.Core", "src\Logitar.Cms.Core\Logitar.Cms.Core.csproj", "{CABB3261-5723-4A0A-B373-DB4758092E97}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logitar.Cms.Web", "src\Logitar.Cms.Web\Logitar.Cms.Web.csproj", "{134EAA3D-D3A5-45F0-982B-D58BF1434B0F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C73FCDEC-8D2B-4E48-81BF-BD86ACBBB07C}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{49A3AE69-C12F-465C-8C4E-CCE766C2414E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logitar.Cms.Core.UnitTests", "tests\Logitar.Cms.Core.UnitTests\Logitar.Cms.Core.UnitTests.csproj", "{411F8113-64F4-4C79-B445-4F716E300144}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logitar.Cms.UnitTests", "tests\Logitar.Cms.UnitTests\Logitar.Cms.UnitTests.csproj", "{8392DAB5-9FEF-4FD9-8747-EBE684E06BE1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logitar.Cms.Infrastructure", "src\Logitar.Cms.Infrastructure\Logitar.Cms.Infrastructure.csproj", "{588FCFB1-B0FE-4598-8B31-B4F7494B94D4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logitar.Cms.EntityFrameworkCore", "src\Logitar.Cms.EntityFrameworkCore\Logitar.Cms.EntityFrameworkCore.csproj", "{6786E254-83CF-4FEC-B3A3-185AACAD9705}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logitar.Cms.EntityFrameworkCore.SqlServer", "src\Logitar.Cms.EntityFrameworkCore.SqlServer\Logitar.Cms.EntityFrameworkCore.SqlServer.csproj", "{5F740A3A-5598-4B0B-BFE5-DF7E0781232D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logitar.Cms", "src\Logitar.Cms\Logitar.Cms.csproj", "{C06B0081-9E08-4416-98A5-30538C18F1E7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logitar.Cms.Web", "src\Logitar.Cms.Web\Logitar.Cms.Web.csproj", "{0144EB23-8A46-4823-861B-8E67F3A154B7}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logitar.Cms.UnitTests", "tests\Logitar.Cms.UnitTests\Logitar.Cms.UnitTests.csproj", "{F367B008-1CAD-49A5-9EB1-5160661FF9CC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{02C0849F-005D-43A1-9EED-4CA0998C35FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{02C0849F-005D-43A1-9EED-4CA0998C35FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{02C0849F-005D-43A1-9EED-4CA0998C35FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{02C0849F-005D-43A1-9EED-4CA0998C35FA}.Release|Any CPU.Build.0 = Release|Any CPU
{CABB3261-5723-4A0A-B373-DB4758092E97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CABB3261-5723-4A0A-B373-DB4758092E97}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CABB3261-5723-4A0A-B373-DB4758092E97}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CABB3261-5723-4A0A-B373-DB4758092E97}.Release|Any CPU.Build.0 = Release|Any CPU
{411F8113-64F4-4C79-B445-4F716E300144}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{411F8113-64F4-4C79-B445-4F716E300144}.Debug|Any CPU.Build.0 = Debug|Any CPU
{411F8113-64F4-4C79-B445-4F716E300144}.Release|Any CPU.ActiveCfg = Release|Any CPU
{411F8113-64F4-4C79-B445-4F716E300144}.Release|Any CPU.Build.0 = Release|Any CPU
{8392DAB5-9FEF-4FD9-8747-EBE684E06BE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8392DAB5-9FEF-4FD9-8747-EBE684E06BE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8392DAB5-9FEF-4FD9-8747-EBE684E06BE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8392DAB5-9FEF-4FD9-8747-EBE684E06BE1}.Release|Any CPU.Build.0 = Release|Any CPU
{588FCFB1-B0FE-4598-8B31-B4F7494B94D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{588FCFB1-B0FE-4598-8B31-B4F7494B94D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{588FCFB1-B0FE-4598-8B31-B4F7494B94D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{588FCFB1-B0FE-4598-8B31-B4F7494B94D4}.Release|Any CPU.Build.0 = Release|Any CPU
{6786E254-83CF-4FEC-B3A3-185AACAD9705}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6786E254-83CF-4FEC-B3A3-185AACAD9705}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6786E254-83CF-4FEC-B3A3-185AACAD9705}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6786E254-83CF-4FEC-B3A3-185AACAD9705}.Release|Any CPU.Build.0 = Release|Any CPU
{5F740A3A-5598-4B0B-BFE5-DF7E0781232D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5F740A3A-5598-4B0B-BFE5-DF7E0781232D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F740A3A-5598-4B0B-BFE5-DF7E0781232D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F740A3A-5598-4B0B-BFE5-DF7E0781232D}.Release|Any CPU.Build.0 = Release|Any CPU
{C06B0081-9E08-4416-98A5-30538C18F1E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C06B0081-9E08-4416-98A5-30538C18F1E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C06B0081-9E08-4416-98A5-30538C18F1E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C06B0081-9E08-4416-98A5-30538C18F1E7}.Release|Any CPU.Build.0 = Release|Any CPU
{0144EB23-8A46-4823-861B-8E67F3A154B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0144EB23-8A46-4823-861B-8E67F3A154B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0144EB23-8A46-4823-861B-8E67F3A154B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0144EB23-8A46-4823-861B-8E67F3A154B7}.Release|Any CPU.Build.0 = Release|Any CPU
{382907CD-18AF-45DE-A1AB-263353F0A9C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{382907CD-18AF-45DE-A1AB-263353F0A9C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{382907CD-18AF-45DE-A1AB-263353F0A9C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{382907CD-18AF-45DE-A1AB-263353F0A9C8}.Release|Any CPU.Build.0 = Release|Any CPU
{134EAA3D-D3A5-45F0-982B-D58BF1434B0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{134EAA3D-D3A5-45F0-982B-D58BF1434B0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{134EAA3D-D3A5-45F0-982B-D58BF1434B0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{134EAA3D-D3A5-45F0-982B-D58BF1434B0F}.Release|Any CPU.Build.0 = Release|Any CPU
{F367B008-1CAD-49A5-9EB1-5160661FF9CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F367B008-1CAD-49A5-9EB1-5160661FF9CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F367B008-1CAD-49A5-9EB1-5160661FF9CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F367B008-1CAD-49A5-9EB1-5160661FF9CC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{411F8113-64F4-4C79-B445-4F716E300144} = {C73FCDEC-8D2B-4E48-81BF-BD86ACBBB07C}
{8392DAB5-9FEF-4FD9-8747-EBE684E06BE1} = {C73FCDEC-8D2B-4E48-81BF-BD86ACBBB07C}
{F367B008-1CAD-49A5-9EB1-5160661FF9CC} = {49A3AE69-C12F-465C-8C4E-CCE766C2414E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DA500B33-B3CA-431A-80D8-55CD95479A70}
Expand Down
Empty file removed backend/src/.gitkeep
Empty file.
10 changes: 10 additions & 0 deletions backend/src/Logitar.Cms.Core/BadRequestException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Logitar.Cms.Core.Errors;

namespace Logitar.Cms.Core;

public abstract class BadRequestException : ErrorException
{
protected BadRequestException(string? message, Exception? innerException = null) : base(message, innerException)
{
}
}
10 changes: 10 additions & 0 deletions backend/src/Logitar.Cms.Core/ConflictException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Logitar.Cms.Core.Errors;

namespace Logitar.Cms.Core;

public abstract class ConflictException : ErrorException
{
protected ConflictException(string? message, Exception? innerException = null) : base(message, innerException)
{
}
}
23 changes: 23 additions & 0 deletions backend/src/Logitar.Cms.Core/Errors/Error.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Logitar.Cms.Core.Errors;

public record Error
{
public string Code { get; init; } = string.Empty;
public string Message { get; init; } = string.Empty;
public IReadOnlyCollection<ErrorData> Data { get; init; } = [];

public Error()
{
}

public Error(string code, string message, IEnumerable<ErrorData>? data = null)
{
Code = code;
Message = message;

if (data != null)
{
Data = data.ToArray();
}
}
}
23 changes: 23 additions & 0 deletions backend/src/Logitar.Cms.Core/Errors/ErrorData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Logitar.Cms.Core.Errors;

public record ErrorData
{
public string Key { get; init; } = string.Empty;
public object? Value { get; init; }

public ErrorData()
{
}

public ErrorData(KeyValuePair<string, object?> pair)
{
Key = pair.Key;
Value = pair.Value;
}

public ErrorData(string key, object? value)
{
Key = key;
Value = value;
}
}
10 changes: 10 additions & 0 deletions backend/src/Logitar.Cms.Core/Errors/ErrorException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Logitar.Cms.Core.Errors;

public abstract class ErrorException : Exception
{
public abstract Error Error { get; }

protected ErrorException(string? message, Exception? innerException = null) : base(message, innerException)
{
}
}
8 changes: 8 additions & 0 deletions backend/src/Logitar.Cms.Core/IApplicationContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Logitar.EventSourcing;

namespace Logitar.Cms.Core;

public interface IApplicationContext
{
ActorId? ActorId { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using FluentValidation;
using Logitar.Cms.Core.Localization.Models;
using Logitar.Cms.Core.Localization.Validators;
using Logitar.EventSourcing;
using MediatR;

namespace Logitar.Cms.Core.Localization.Commands;

public record CreateOrReplaceLanguageResult(LanguageModel? Language = null, bool Created = false);

public record CreateOrReplaceLanguageCommand(Guid? Id, CreateOrReplaceLanguagePayload Payload, long? Version) : IRequest<CreateOrReplaceLanguageResult>;

internal class CreateOrReplaceLanguageCommandHandler : IRequestHandler<CreateOrReplaceLanguageCommand, CreateOrReplaceLanguageResult>
{
private readonly IApplicationContext _applicationContext;
private readonly ILanguageQuerier _languageQuerier;
private readonly ILanguageRepository _languageRepository;
private readonly IMediator _mediator;

public CreateOrReplaceLanguageCommandHandler(
IApplicationContext applicationContext,
ILanguageQuerier languageQuerier,
ILanguageRepository languageRepository,
IMediator mediator)
{
_applicationContext = applicationContext;
_languageQuerier = languageQuerier;
_languageRepository = languageRepository;
_mediator = mediator;
}

public async Task<CreateOrReplaceLanguageResult> Handle(CreateOrReplaceLanguageCommand command, CancellationToken cancellationToken)
{
CreateOrReplaceLanguagePayload payload = command.Payload;
new CreateOrReplaceLanguageValidator().ValidateAndThrow(payload);

ActorId? actorId = _applicationContext.ActorId;
Locale locale = new(payload.Locale);

LanguageId? languageId = null;
Language? language = null;
if (command.Id.HasValue)
{
languageId = new(command.Id.Value);
language = await _languageRepository.LoadAsync(languageId.Value, cancellationToken);
}

bool created = false;
if (language == null)
{
if (command.Version.HasValue)
{
return new CreateOrReplaceLanguageResult();
}

language = new(locale, isDefault: false, actorId, languageId);
created = true;
}

Language reference = (command.Version.HasValue
? await _languageRepository.LoadAsync(language.Id, command.Version.Value, cancellationToken)
: null) ?? language;

if (reference.Locale != locale)
{
language.Locale = locale;
}

language.Update(actorId);

await _mediator.Send(new SaveLanguageCommand(language), cancellationToken);

LanguageModel model = await _languageQuerier.ReadAsync(language, cancellationToken);
return new CreateOrReplaceLanguageResult(model, created);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Logitar.Cms.Core.Localization.Events;
using MediatR;

namespace Logitar.Cms.Core.Localization.Commands;

public record SaveLanguageCommand(Language Language) : IRequest;

internal class SaveLanguageCommandHandler : IRequestHandler<SaveLanguageCommand>
{
private readonly ILanguageQuerier _languageQuerier;
private readonly ILanguageRepository _languageRepository;

public SaveLanguageCommandHandler(ILanguageQuerier languageQuerier, ILanguageRepository languageRepository)
{
_languageQuerier = languageQuerier;
_languageRepository = languageRepository;
}

public async Task Handle(SaveLanguageCommand command, CancellationToken cancellationToken)
{
Language language = command.Language;

bool hasLocaleChanged = language.Changes.Any(change => change is LanguageCreated || change is LanguageUpdated updated && updated.Locale != null);
if (hasLocaleChanged)
{
LanguageId? conflictId = await _languageQuerier.FindIdAsync(language.Locale, cancellationToken);
if (conflictId != null && !conflictId.Equals(language.Id))
{
throw new LocaleAlreadyUsedException(language, conflictId.Value);
}
}

await _languageRepository.SaveAsync(language, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Logitar.Cms.Core.Localization.Models;
using Logitar.EventSourcing;
using MediatR;

namespace Logitar.Cms.Core.Localization.Commands;

public record SetDefaultLanguageCommand(Guid Id) : IRequest<LanguageModel?>;

internal class SetDefaultLanguageCommandHandler : IRequestHandler<SetDefaultLanguageCommand, LanguageModel?>
{
private readonly IApplicationContext _applicationContext;
private readonly ILanguageQuerier _languageQuerier;
private readonly ILanguageRepository _languageRepository;

public SetDefaultLanguageCommandHandler(IApplicationContext applicationContext, ILanguageQuerier languageQuerier, ILanguageRepository languageRepository)
{
_applicationContext = applicationContext;
_languageQuerier = languageQuerier;
_languageRepository = languageRepository;
}

public async Task<LanguageModel?> Handle(SetDefaultLanguageCommand command, CancellationToken cancellationToken)
{
LanguageId languageId = new(command.Id);
Language? language = await _languageRepository.LoadAsync(languageId, cancellationToken);
if (language == null)
{
return null;
}

if (!language.IsDefault)
{
LanguageId defaultId = await _languageQuerier.FindDefaultIdAsync(cancellationToken);
Language @default = await _languageRepository.LoadAsync(defaultId, cancellationToken)
?? throw new InvalidOperationException($"The default language 'Id={defaultId}' could not be loaded.");

ActorId? actorId = _applicationContext.ActorId;
@default.SetDefault(false, actorId);
language.SetDefault(true, actorId);

await _languageRepository.SaveAsync([@default, language], cancellationToken);
}

return await _languageQuerier.ReadAsync(language, cancellationToken);
}
}
Loading

0 comments on commit a67601e

Please sign in to comment.