mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-13 09:23:45 +00:00
NotificationTemplate Schema and Repository Improvements (#23769)
* NotificationTemplate Schema and Repository Improvements * Update generated TypeScript types --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Aniket Katkar <aniketkatkar97@gmail.com>
This commit is contained in:
parent
13ab5af508
commit
0b107569d7
@ -14,6 +14,7 @@
|
|||||||
package org.openmetadata.service.jdbi3;
|
package org.openmetadata.service.jdbi3;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -24,6 +25,7 @@ import org.openmetadata.schema.entity.events.NotificationTemplate;
|
|||||||
import org.openmetadata.schema.type.Include;
|
import org.openmetadata.schema.type.Include;
|
||||||
import org.openmetadata.schema.type.ProviderType;
|
import org.openmetadata.schema.type.ProviderType;
|
||||||
import org.openmetadata.schema.type.change.ChangeSource;
|
import org.openmetadata.schema.type.change.ChangeSource;
|
||||||
|
import org.openmetadata.schema.utils.JsonUtils;
|
||||||
import org.openmetadata.service.Entity;
|
import org.openmetadata.service.Entity;
|
||||||
import org.openmetadata.service.resources.events.NotificationTemplateResource;
|
import org.openmetadata.service.resources.events.NotificationTemplateResource;
|
||||||
import org.openmetadata.service.template.NotificationTemplateProcessor;
|
import org.openmetadata.service.template.NotificationTemplateProcessor;
|
||||||
@ -62,29 +64,23 @@ public class NotificationTemplateRepository extends EntityRepository<Notificatio
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void prepare(NotificationTemplate entity, boolean update) {
|
public void prepare(NotificationTemplate entity, boolean update) {
|
||||||
// Validate template if body is present
|
NotificationTemplateValidationRequest request = new NotificationTemplateValidationRequest();
|
||||||
if (entity.getTemplateBody() != null) {
|
request.setTemplateBody(entity.getTemplateBody());
|
||||||
NotificationTemplateValidationRequest request = new NotificationTemplateValidationRequest();
|
request.setTemplateSubject(entity.getTemplateSubject());
|
||||||
request.setTemplateBody(entity.getTemplateBody());
|
|
||||||
request.setTemplateSubject(entity.getTemplateSubject());
|
|
||||||
|
|
||||||
NotificationTemplateValidationResponse response = templateProcessor.validate(request);
|
NotificationTemplateValidationResponse response = templateProcessor.validate(request);
|
||||||
|
|
||||||
// Check for validation errors
|
// Check for validation errors
|
||||||
StringBuilder errors = new StringBuilder();
|
List<String> errors = new ArrayList<>();
|
||||||
if (response.getTemplateBody() != null && !response.getTemplateBody().getPassed()) {
|
if (response.getTemplateBody() != null && !response.getTemplateBody().getPassed()) {
|
||||||
errors.append("Template body: ").append(response.getTemplateBody().getError());
|
errors.add("Template body: " + response.getTemplateBody().getError());
|
||||||
}
|
}
|
||||||
if (response.getTemplateSubject() != null && !response.getTemplateSubject().getPassed()) {
|
if (response.getTemplateSubject() != null && !response.getTemplateSubject().getPassed()) {
|
||||||
if (errors.length() > 0) {
|
errors.add("Template subject: " + response.getTemplateSubject().getError());
|
||||||
errors.append("; ");
|
}
|
||||||
}
|
|
||||||
errors.append("Template subject: ").append(response.getTemplateSubject().getError());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length() > 0) {
|
if (!errors.isEmpty()) {
|
||||||
throw new IllegalArgumentException("Invalid template: " + errors);
|
throw new IllegalArgumentException("Invalid template: " + String.join("; ", errors));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,70 +115,80 @@ public class NotificationTemplateRepository extends EntityRepository<Notificatio
|
|||||||
for (NotificationTemplate seedTemplate : seedTemplates) {
|
for (NotificationTemplate seedTemplate : seedTemplates) {
|
||||||
String fqn = seedTemplate.getFullyQualifiedName();
|
String fqn = seedTemplate.getFullyQualifiedName();
|
||||||
NotificationTemplate existing = findByNameOrNull(fqn, Include.ALL);
|
NotificationTemplate existing = findByNameOrNull(fqn, Include.ALL);
|
||||||
String seedChecksum = calculateTemplateChecksum(seedTemplate);
|
|
||||||
|
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
seedTemplate.withIsModifiedFromDefault(false).withDefaultTemplateChecksum(seedChecksum);
|
createSystemTemplateFromSeed(seedTemplate, fqn);
|
||||||
initializeEntity(seedTemplate);
|
} else if (shouldUpdateSystemTemplate(existing, seedTemplate)) {
|
||||||
LOG.info("Created new system template {}", fqn);
|
updateSystemTemplateFromSeed(existing, seedTemplate, fqn);
|
||||||
continue;
|
} else {
|
||||||
|
LOG.debug("Skipping template {} - user template or modified system template", fqn);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ProviderType.SYSTEM.equals(existing.getProvider())
|
private void createSystemTemplateFromSeed(NotificationTemplate seedTemplate, String fqn)
|
||||||
&& Boolean.FALSE.equals(existing.getIsModifiedFromDefault())
|
throws IOException {
|
||||||
&& !Objects.equals(seedChecksum, existing.getDefaultTemplateChecksum())) {
|
seedTemplate.withIsModifiedFromDefault(false);
|
||||||
// Only update functional fields for unmodified templates (hybrid approach)
|
try {
|
||||||
// This preserves any user-customized displayName or description
|
initializeEntity(seedTemplate);
|
||||||
existing
|
} catch (IllegalArgumentException e) {
|
||||||
.withTemplateBody(seedTemplate.getTemplateBody())
|
throw new IOException(
|
||||||
.withTemplateSubject(seedTemplate.getTemplateSubject())
|
String.format("Failed to validate seed template '%s': %s", fqn, e.getMessage()), e);
|
||||||
.withDefaultTemplateChecksum(seedChecksum);
|
}
|
||||||
store(existing, true);
|
}
|
||||||
LOG.info("Updated system template {} to new version", existing.getFullyQualifiedName());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG.debug(
|
private boolean shouldUpdateSystemTemplate(
|
||||||
"Skipping template {} - either user template or modified system template",
|
NotificationTemplate existing, NotificationTemplate seedTemplate) {
|
||||||
existing.getFullyQualifiedName());
|
// Update only if system template has not been update and seed content differs from current
|
||||||
|
// content
|
||||||
|
return ProviderType.SYSTEM.equals(existing.getProvider())
|
||||||
|
&& Boolean.FALSE.equals(existing.getIsModifiedFromDefault())
|
||||||
|
&& !Objects.equals(
|
||||||
|
calculateTemplateChecksum(seedTemplate), calculateTemplateChecksum(existing));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSystemTemplateFromSeed(
|
||||||
|
NotificationTemplate existing, NotificationTemplate seedTemplate, String fqn)
|
||||||
|
throws IOException {
|
||||||
|
existing
|
||||||
|
.withTemplateBody(seedTemplate.getTemplateBody())
|
||||||
|
.withTemplateSubject(seedTemplate.getTemplateSubject());
|
||||||
|
try {
|
||||||
|
prepare(existing, true);
|
||||||
|
store(existing, true);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new IOException(
|
||||||
|
String.format("Failed to validate seed template '%s': %s", fqn, e.getMessage()), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resetToDefault(String fqn) {
|
public void resetToDefault(String fqn) {
|
||||||
try {
|
try {
|
||||||
NotificationTemplate template = getByName(null, fqn, getFields("*"));
|
NotificationTemplate original = getByName(null, fqn, getFields("*"));
|
||||||
if (template == null) {
|
if (original == null) {
|
||||||
throw new IllegalArgumentException("NotificationTemplate not found: " + fqn);
|
throw new IllegalArgumentException("NotificationTemplate not found: " + fqn);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ProviderType.SYSTEM.equals(template.getProvider())) {
|
if (!ProviderType.SYSTEM.equals(original.getProvider())) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Cannot reset template: only SYSTEM templates can be reset to default");
|
"Cannot reset template: only SYSTEM templates can be reset to default");
|
||||||
}
|
}
|
||||||
|
|
||||||
String seedPath =
|
NotificationTemplate defaultTemplate = getDefaultTemplateFromSeed(fqn);
|
||||||
ResourcePathResolver.getResourcePath(NotificationTemplateResourcePathProvider.class);
|
NotificationTemplate updated = JsonUtils.deepCopy(original, NotificationTemplate.class);
|
||||||
List<NotificationTemplate> defaultTemplates = getEntitiesFromSeedData(seedPath);
|
updated
|
||||||
NotificationTemplate defaultTemplate =
|
|
||||||
defaultTemplates.stream()
|
|
||||||
.filter(t -> fqn.equals(t.getFullyQualifiedName()))
|
|
||||||
.findFirst()
|
|
||||||
.orElseThrow(
|
|
||||||
() ->
|
|
||||||
new IllegalArgumentException(
|
|
||||||
"Default template not found in seed data: " + fqn));
|
|
||||||
|
|
||||||
template
|
|
||||||
.withTemplateBody(defaultTemplate.getTemplateBody())
|
.withTemplateBody(defaultTemplate.getTemplateBody())
|
||||||
.withTemplateSubject(defaultTemplate.getTemplateSubject())
|
.withTemplateSubject(defaultTemplate.getTemplateSubject())
|
||||||
.withDescription(defaultTemplate.getDescription())
|
.withDescription(defaultTemplate.getDescription())
|
||||||
.withDisplayName(defaultTemplate.getDisplayName())
|
.withDisplayName(defaultTemplate.getDisplayName());
|
||||||
.withIsModifiedFromDefault(false)
|
|
||||||
.withDefaultTemplateChecksum(calculateTemplateChecksum(defaultTemplate));
|
|
||||||
|
|
||||||
store(template, true);
|
EntityUpdater entityUpdater = getUpdater(original, updated, Operation.PUT, null);
|
||||||
|
entityUpdater.update();
|
||||||
|
|
||||||
LOG.info("Reset NotificationTemplate {} to default", fqn);
|
LOG.info("Reset NotificationTemplate {} to default", fqn);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
LOG.error("Failed to reset template: {}", e.getMessage(), e);
|
||||||
|
throw e;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.error("Failed to load seed data for reset operation", e);
|
LOG.error("Failed to load seed data for reset operation", e);
|
||||||
throw new RuntimeException("Failed to reset template due to seed data loading error", e);
|
throw new RuntimeException("Failed to reset template due to seed data loading error", e);
|
||||||
@ -201,6 +207,17 @@ public class NotificationTemplateRepository extends EntityRepository<Notificatio
|
|||||||
return templateProcessor.validate(request);
|
return templateProcessor.validate(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private NotificationTemplate getDefaultTemplateFromSeed(String fqn) throws IOException {
|
||||||
|
String seedPath =
|
||||||
|
ResourcePathResolver.getResourcePath(NotificationTemplateResourcePathProvider.class);
|
||||||
|
List<NotificationTemplate> defaultTemplates = getEntitiesFromSeedData(seedPath);
|
||||||
|
return defaultTemplates.stream()
|
||||||
|
.filter(t -> fqn.equals(t.getFullyQualifiedName()))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(
|
||||||
|
() -> new IllegalArgumentException("Default template not found in seed data: " + fqn));
|
||||||
|
}
|
||||||
|
|
||||||
private String calculateTemplateChecksum(NotificationTemplate template) {
|
private String calculateTemplateChecksum(NotificationTemplate template) {
|
||||||
// Only include functional fields in checksum
|
// Only include functional fields in checksum
|
||||||
// This allows users to customize displayName and description without triggering "modified"
|
// This allows users to customize displayName and description without triggering "modified"
|
||||||
@ -221,18 +238,21 @@ public class NotificationTemplateRepository extends EntityRepository<Notificatio
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void entitySpecificUpdate(boolean consolidatingChanges) {
|
protected void entitySpecificUpdate(boolean consolidatingChanges) {
|
||||||
// Record template-specific content changes for versioning and rollback
|
|
||||||
recordChange("templateBody", original.getTemplateBody(), updated.getTemplateBody());
|
recordChange("templateBody", original.getTemplateBody(), updated.getTemplateBody());
|
||||||
recordChange("templateSubject", original.getTemplateSubject(), updated.getTemplateSubject());
|
recordChange("templateSubject", original.getTemplateSubject(), updated.getTemplateSubject());
|
||||||
|
|
||||||
// Update modification tracking WITHOUT recording (metadata management)
|
|
||||||
if (ProviderType.SYSTEM.equals(original.getProvider())) {
|
if (ProviderType.SYSTEM.equals(original.getProvider())) {
|
||||||
// Calculate current template checksum
|
try {
|
||||||
String currentChecksum = calculateTemplateChecksum(updated);
|
NotificationTemplate defaultTemplate =
|
||||||
String defaultChecksum = original.getDefaultTemplateChecksum();
|
getDefaultTemplateFromSeed(original.getFullyQualifiedName());
|
||||||
|
|
||||||
// Set modification status based on checksum comparison
|
String currentChecksum = calculateTemplateChecksum(updated);
|
||||||
updated.setIsModifiedFromDefault(!Objects.equals(currentChecksum, defaultChecksum));
|
String defaultChecksum = calculateTemplateChecksum(defaultTemplate);
|
||||||
|
|
||||||
|
updated.setIsModifiedFromDefault(!Objects.equals(currentChecksum, defaultChecksum));
|
||||||
|
} catch (IOException | IllegalArgumentException e) {
|
||||||
|
LOG.warn("Failed to load seed data for modification tracking", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package org.openmetadata.service.resources.events;
|
|||||||
|
|
||||||
import org.openmetadata.schema.api.events.CreateNotificationTemplate;
|
import org.openmetadata.schema.api.events.CreateNotificationTemplate;
|
||||||
import org.openmetadata.schema.entity.events.NotificationTemplate;
|
import org.openmetadata.schema.entity.events.NotificationTemplate;
|
||||||
|
import org.openmetadata.schema.type.ProviderType;
|
||||||
import org.openmetadata.service.mapper.EntityMapper;
|
import org.openmetadata.service.mapper.EntityMapper;
|
||||||
|
|
||||||
public class NotificationTemplateMapper
|
public class NotificationTemplateMapper
|
||||||
@ -11,6 +12,7 @@ public class NotificationTemplateMapper
|
|||||||
public NotificationTemplate createToEntity(CreateNotificationTemplate create, String user) {
|
public NotificationTemplate createToEntity(CreateNotificationTemplate create, String user) {
|
||||||
return copy(new NotificationTemplate(), create, user)
|
return copy(new NotificationTemplate(), create, user)
|
||||||
.withTemplateSubject(create.getTemplateSubject())
|
.withTemplateSubject(create.getTemplateSubject())
|
||||||
.withTemplateBody(create.getTemplateBody());
|
.withTemplateBody(create.getTemplateBody())
|
||||||
|
.withProvider(ProviderType.USER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -586,7 +586,6 @@ public class NotificationTemplateResourceTest
|
|||||||
"system-notification-entity-deleted",
|
"system-notification-entity-deleted",
|
||||||
"system-notification-entity-soft-deleted",
|
"system-notification-entity-soft-deleted",
|
||||||
"system-notification-entity-default",
|
"system-notification-entity-default",
|
||||||
"system-notification-test-result",
|
|
||||||
"system-notification-logical-test-case-added",
|
"system-notification-logical-test-case-added",
|
||||||
"system-notification-task-closed",
|
"system-notification-task-closed",
|
||||||
"system-notification-task-resolved"
|
"system-notification-task-resolved"
|
||||||
@ -646,8 +645,6 @@ public class NotificationTemplateResourceTest
|
|||||||
assertFalse(
|
assertFalse(
|
||||||
systemTemplate.getIsModifiedFromDefault(),
|
systemTemplate.getIsModifiedFromDefault(),
|
||||||
"Fresh template should not be marked as modified");
|
"Fresh template should not be marked as modified");
|
||||||
assertNotNull(
|
|
||||||
systemTemplate.getDefaultTemplateChecksum(), "Template should have a default checksum");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -661,7 +658,6 @@ public class NotificationTemplateResourceTest
|
|||||||
assertFalse(
|
assertFalse(
|
||||||
systemTemplate.getIsModifiedFromDefault(),
|
systemTemplate.getIsModifiedFromDefault(),
|
||||||
"Fresh template should not be marked as modified");
|
"Fresh template should not be marked as modified");
|
||||||
String originalChecksum = systemTemplate.getDefaultTemplateChecksum();
|
|
||||||
|
|
||||||
// User modifies the template using PATCH (following existing test patterns)
|
// User modifies the template using PATCH (following existing test patterns)
|
||||||
String newBody = "<div>User modified template: {{entity.name}}</div>";
|
String newBody = "<div>User modified template: {{entity.name}}</div>";
|
||||||
@ -678,10 +674,6 @@ public class NotificationTemplateResourceTest
|
|||||||
modifiedTemplate.getIsModifiedFromDefault(),
|
modifiedTemplate.getIsModifiedFromDefault(),
|
||||||
"Template should be marked as modified after change");
|
"Template should be marked as modified after change");
|
||||||
assertEquals(newBody, modifiedTemplate.getTemplateBody());
|
assertEquals(newBody, modifiedTemplate.getTemplateBody());
|
||||||
assertEquals(
|
|
||||||
originalChecksum,
|
|
||||||
modifiedTemplate.getDefaultTemplateChecksum(),
|
|
||||||
"Default checksum should be preserved");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -731,8 +723,6 @@ public class NotificationTemplateResourceTest
|
|||||||
assertEquals(originalSubject, resetResult.getTemplateSubject());
|
assertEquals(originalSubject, resetResult.getTemplateSubject());
|
||||||
assertEquals(originalDescription, resetResult.getDescription());
|
assertEquals(originalDescription, resetResult.getDescription());
|
||||||
assertEquals(originalDisplayName, resetResult.getDisplayName());
|
assertEquals(originalDisplayName, resetResult.getDisplayName());
|
||||||
assertNotNull(
|
|
||||||
resetResult.getDefaultTemplateChecksum(), "Template should have checksum after reset");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -79,15 +79,12 @@
|
|||||||
"description": "Indicates if this system template has been modified from its default version. Only applicable to system templates.",
|
"description": "Indicates if this system template has been modified from its default version. Only applicable to system templates.",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false
|
"default": false
|
||||||
},
|
|
||||||
"defaultTemplateChecksum": {
|
|
||||||
"description": "Checksum of the default template to detect if updates are available. Only applicable to system templates.",
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
|
"templateSubject",
|
||||||
"templateBody"
|
"templateBody"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
@ -19,11 +19,6 @@ export interface NotificationTemplate {
|
|||||||
* Change that lead to this version of the template.
|
* Change that lead to this version of the template.
|
||||||
*/
|
*/
|
||||||
changeDescription?: ChangeDescription;
|
changeDescription?: ChangeDescription;
|
||||||
/**
|
|
||||||
* Checksum of the default template to detect if updates are available. Only applicable to
|
|
||||||
* system templates.
|
|
||||||
*/
|
|
||||||
defaultTemplateChecksum?: string;
|
|
||||||
/**
|
/**
|
||||||
* When `true` indicates the template has been soft deleted.
|
* When `true` indicates the template has been soft deleted.
|
||||||
*/
|
*/
|
||||||
@ -74,7 +69,7 @@ export interface NotificationTemplate {
|
|||||||
/**
|
/**
|
||||||
* Handlebars template for the email subject line with placeholders.
|
* Handlebars template for the email subject line with placeholders.
|
||||||
*/
|
*/
|
||||||
templateSubject?: string;
|
templateSubject: string;
|
||||||
/**
|
/**
|
||||||
* Last update time corresponding to the new version of the template.
|
* Last update time corresponding to the new version of the template.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user