feat(templates): add localized push templates and actions
Share template keys across email and push with independent editors and revisions. Render localized actions and preserve version-one delivery. Ref: IT-1627
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
using HrynCo.NotificationService.DAL.Abstract.Repositories;
|
||||
using HrynCo.NotificationService.DAL.Abstract.Templates;
|
||||
using HrynCo.NotificationService.DAL.EF.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HrynCo.NotificationService.DAL.EF.Repositories;
|
||||
|
||||
internal sealed class PushTemplateDefinitionRepository(NotificationDbContext db, TemplateMutationRunner mutations)
|
||||
: IPushTemplateDefinitionRepository
|
||||
{
|
||||
private IQueryable<PushTemplateDefinitionEntity> Definitions => db.PushTemplateDefinitions
|
||||
.Include(d => d.Languages).ThenInclude(l => l.ServiceLanguage);
|
||||
|
||||
public async Task<IReadOnlyList<PushTemplateDefinition>> GetDefinitionsAsync(Guid serviceId, CancellationToken ct = default) =>
|
||||
(await Definitions.AsNoTracking().Where(d => d.ServiceId == serviceId).OrderBy(d => d.Key).ToListAsync(ct)).Select(Map).ToList();
|
||||
|
||||
public async Task<PushTemplateDefinition?> GetAsync(Guid serviceId, string key, CancellationToken ct = default)
|
||||
{
|
||||
var entity = await Definitions.AsNoTracking().SingleOrDefaultAsync(d => d.ServiceId == serviceId && d.Key == key, ct);
|
||||
return entity is null ? null : Map(entity);
|
||||
}
|
||||
|
||||
public Task SaveAsync(PushTemplateDefinition definition, bool isNew, CancellationToken ct = default) => mutations.ExecuteAsync(async () =>
|
||||
{
|
||||
PushTemplateValidation.Validate(definition);
|
||||
var service = await db.Services.Include(s => s.Languages).SingleOrDefaultAsync(s => s.Id == definition.ServiceId, ct)
|
||||
?? throw new EmailTemplateWriteException("Service no longer exists. Reload services.");
|
||||
if (definition.Languages.Any(l => !service.Languages.Any(s => s.Code == l.LanguageCode)))
|
||||
throw new EmailTemplateWriteException("Every language must exactly match this service's catalog.", false);
|
||||
var entity = await LoadForSaveAsync(definition, service, isNew, ct);
|
||||
entity.Variables = definition.Variables.Select(v => new PushTemplateVariableData { Name = v.Name, Required = v.Required }).ToList();
|
||||
SynchronizeLanguages(entity, service, definition.Languages);
|
||||
entity.Revision = Guid.NewGuid();
|
||||
entity.Updated = DateTimeOffset.UtcNow;
|
||||
definition.Id = entity.Id;
|
||||
definition.Revision = entity.Revision;
|
||||
}, ct);
|
||||
|
||||
private async Task<PushTemplateDefinitionEntity> LoadForSaveAsync(PushTemplateDefinition model, ServiceEntity service, bool isNew, CancellationToken ct)
|
||||
{
|
||||
var entity = await Definitions.SingleOrDefaultAsync(d => d.ServiceId == service.Id && d.Key == model.Key, ct);
|
||||
if (!isNew)
|
||||
{
|
||||
if (entity is null || entity.Id != model.Id || entity.Revision != model.Revision)
|
||||
throw new EmailTemplateWriteException("This push template changed after you opened it. Reload before saving; your entered values are retained.");
|
||||
return entity;
|
||||
}
|
||||
if (entity is not null)
|
||||
throw new EmailTemplateWriteException("A push template with this key already exists.");
|
||||
var template = await db.TemplateCatalog.SingleOrDefaultAsync(t => t.ServiceId == service.Id && t.Key == model.Key, ct)
|
||||
?? new TemplateEntity { Service = service, ServiceId = service.Id, Key = model.Key, Created = DateTimeOffset.UtcNow };
|
||||
entity = new PushTemplateDefinitionEntity { Template = template, ServiceId = service.Id, Key = model.Key, Created = DateTimeOffset.UtcNow };
|
||||
db.PushTemplateDefinitions.Add(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private void SynchronizeLanguages(PushTemplateDefinitionEntity definition, ServiceEntity service, IReadOnlyList<PushTemplateTranslation> translations)
|
||||
{
|
||||
var codes = translations.Select(l => l.LanguageCode).ToHashSet(StringComparer.Ordinal);
|
||||
db.PushTemplates.RemoveRange(definition.Languages.Where(l => !codes.Contains(l.ServiceLanguage.Code)));
|
||||
foreach (var translation in translations)
|
||||
{
|
||||
var entity = definition.Languages.SingleOrDefault(l => l.ServiceLanguage.Code == translation.LanguageCode);
|
||||
if (entity is null)
|
||||
{
|
||||
entity = new PushTemplateEntity
|
||||
{
|
||||
Definition = definition, ServiceId = service.Id,
|
||||
ServiceLanguage = service.Languages.Single(l => l.Code == translation.LanguageCode),
|
||||
Title = translation.Title, Body = translation.Body, Created = DateTimeOffset.UtcNow
|
||||
};
|
||||
definition.Languages.Add(entity);
|
||||
}
|
||||
entity.Title = translation.Title;
|
||||
entity.Body = translation.Body;
|
||||
entity.ActionsJson = translation.ActionsJson;
|
||||
entity.Updated = DateTimeOffset.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid serviceId, string key, Guid revision, CancellationToken ct = default)
|
||||
{
|
||||
var removed = false;
|
||||
await mutations.ExecuteAsync(async () =>
|
||||
{
|
||||
var entity = await Definitions.SingleOrDefaultAsync(d => d.ServiceId == serviceId && d.Key == key, ct);
|
||||
if (entity is null)
|
||||
return;
|
||||
if (revision == Guid.Empty || entity.Revision != revision)
|
||||
throw new EmailTemplateWriteException("The push template revision changed. Reload before deleting.");
|
||||
db.PushTemplateDefinitions.Remove(entity);
|
||||
if (!await db.TemplateDefinitions.AnyAsync(d => d.ServiceId == serviceId && d.Key == key, ct))
|
||||
db.TemplateCatalog.Remove(await db.TemplateCatalog.SingleAsync(t => t.ServiceId == serviceId && t.Key == key, ct));
|
||||
removed = true;
|
||||
}, ct);
|
||||
return removed;
|
||||
}
|
||||
|
||||
private static PushTemplateDefinition Map(PushTemplateDefinitionEntity entity) => new()
|
||||
{
|
||||
Id = entity.Id, ServiceId = entity.ServiceId, Key = entity.Key, Revision = entity.Revision,
|
||||
Variables = entity.Variables.Select(v => new EmailTemplateVariable { Name = v.Name, Required = v.Required }).ToList(),
|
||||
Languages = entity.Languages.OrderBy(l => l.ServiceLanguage.Code).Select(l => new PushTemplateTranslation
|
||||
{
|
||||
LanguageCode = l.ServiceLanguage.Code, Title = l.Title, Body = l.Body, ActionsJson = l.ActionsJson
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user