Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing Language management. #43

Merged
merged 3 commits into from
Dec 18, 2024
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
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
Loading