feat(notification): add admin rest management and deletion
Add revision-safe service and template APIs, channel mapping, and validated admin controls. Ref: IT-1601
This commit is contained in:
@@ -43,6 +43,93 @@ internal sealed class EmailTemplateRepository(NotificationDbContext db, Template
|
||||
return entity is null ? null : MapDefinition(entity);
|
||||
}
|
||||
|
||||
public async Task<EmailTemplateDefinition?> GetDefinitionAsync(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 : MapDefinition(entity);
|
||||
}
|
||||
|
||||
public async Task<EmailTemplateDefinition?> UpsertDefinitionAsync(EmailTemplateDefinitionPatch patch, CancellationToken ct = default)
|
||||
{
|
||||
EmailTemplateDefinitionEntity? saved = null;
|
||||
await mutations.ExecuteAsync(async () =>
|
||||
{
|
||||
var service = await db.Services.Include(s => s.Languages).SingleOrDefaultAsync(s => s.Id == patch.ServiceId, ct);
|
||||
if (service is null)
|
||||
return;
|
||||
ValidatePatch(patch, service);
|
||||
var entity = await LoadForUpsertAsync(patch, service, ct);
|
||||
entity.Variables = MapVariables(patch.Variables);
|
||||
var removed = patch.RemoveLanguageCodes.ToHashSet(StringComparer.Ordinal);
|
||||
db.Templates.RemoveRange(entity.Languages.Where(t => removed.Contains(t.ServiceLanguage.Code)));
|
||||
// Omitted translations are retained. Only explicitly supplied content is changed.
|
||||
UpsertLanguages(entity, patch.Translations);
|
||||
Touch(entity);
|
||||
saved = entity;
|
||||
}, ct);
|
||||
return saved is null ? null : MapDefinition(saved);
|
||||
}
|
||||
|
||||
private async Task<EmailTemplateDefinitionEntity> LoadForUpsertAsync(EmailTemplateDefinitionPatch patch, ServiceEntity service, CancellationToken ct)
|
||||
{
|
||||
var entity = await Definitions.SingleOrDefaultAsync(d => d.ServiceId == patch.ServiceId && d.Key == patch.Key, ct);
|
||||
if (patch.Revision is null)
|
||||
{
|
||||
if (entity is not null)
|
||||
throw new EmailTemplateWriteException("A definition with this service and key already exists. Read its revision before updating.");
|
||||
entity = new EmailTemplateDefinitionEntity { Service = service, ServiceId = service.Id, Key = patch.Key, Created = DateTimeOffset.UtcNow };
|
||||
db.TemplateDefinitions.Add(entity);
|
||||
return entity;
|
||||
}
|
||||
if (entity is null || entity.Revision != patch.Revision)
|
||||
throw new EmailTemplateWriteException("The definition is missing or its revision changed. Read it again before updating.");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static void ValidatePatch(EmailTemplateDefinitionPatch patch, ServiceEntity service)
|
||||
{
|
||||
EmailTemplateValidation.ValidateDefinition(new EmailTemplateDefinition
|
||||
{
|
||||
ServiceId = service.Id, ServiceName = service.Name, Key = patch.Key,
|
||||
Variables = patch.Variables, Languages = patch.Translations
|
||||
});
|
||||
if (patch.Revision == Guid.Empty)
|
||||
throw new EmailTemplateWriteException("Revision must be null for creation or a nonempty revision from the last read for update.", false);
|
||||
var removed = patch.RemoveLanguageCodes;
|
||||
if (removed.Any(string.IsNullOrWhiteSpace) || removed.Distinct(StringComparer.Ordinal).Count() != removed.Count)
|
||||
throw new EmailTemplateWriteException("removeLanguageCodes must contain unique nonblank language codes.", false);
|
||||
var supplied = patch.Translations.Select(t => t.LanguageCode).ToHashSet(StringComparer.Ordinal);
|
||||
if (removed.Any(supplied.Contains))
|
||||
throw new EmailTemplateWriteException("A language cannot be supplied and removed in the same request.", false);
|
||||
var supported = service.Languages.Select(l => l.Code).ToHashSet(StringComparer.Ordinal);
|
||||
if (supplied.Concat(removed).Any(code => !supported.Contains(code)))
|
||||
throw new EmailTemplateWriteException("Every supplied or removed language must exactly match a code in this service's catalog.", false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<EmailTemplateDefinition>> GetDefinitionsAsync(Guid serviceId, CancellationToken ct = default)
|
||||
{
|
||||
var entities = await Definitions.AsNoTracking().Where(d => d.ServiceId == serviceId).OrderBy(d => d.Key).ToListAsync(ct);
|
||||
return entities.Select(MapDefinition).ToList();
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteDefinitionAsync(Guid serviceId, string key, Guid revision, CancellationToken ct = default)
|
||||
{
|
||||
var removed = false;
|
||||
await mutations.ExecuteAsync(async () =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key) || key.Length > 100 || revision == Guid.Empty)
|
||||
throw new EmailTemplateWriteException("An exact key (at most 100 characters) and a nonempty revision are required.", false);
|
||||
var definition = await Definitions.SingleOrDefaultAsync(d => d.ServiceId == serviceId && d.Key == key, ct);
|
||||
if (definition is null)
|
||||
return;
|
||||
if (definition.Revision != revision)
|
||||
throw new EmailTemplateWriteException("The definition revision changed. Read it again before deleting.");
|
||||
db.TemplateDefinitions.Remove(definition);
|
||||
removed = true;
|
||||
}, ct);
|
||||
return removed;
|
||||
}
|
||||
|
||||
public Task AddAsync(EmailTemplate template, CancellationToken ct = default) => mutations.ExecuteAsync(async () =>
|
||||
{
|
||||
ValidateLegacy(template);
|
||||
@@ -113,6 +200,11 @@ internal sealed class EmailTemplateRepository(NotificationDbContext db, Template
|
||||
{
|
||||
var codes = languages.Select(t => t.LanguageCode).ToHashSet(StringComparer.Ordinal);
|
||||
db.Templates.RemoveRange(definition.Languages.Where(t => !codes.Contains(t.ServiceLanguage.Code)));
|
||||
UpsertLanguages(definition, languages);
|
||||
}
|
||||
|
||||
private void UpsertLanguages(EmailTemplateDefinitionEntity definition, IReadOnlyList<EmailTemplate> languages)
|
||||
{
|
||||
foreach (var model in languages)
|
||||
{
|
||||
var code = model.LanguageCode;
|
||||
|
||||
Reference in New Issue
Block a user