diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/NotificationTemplateRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/NotificationTemplateRepository.java index 5f18092ae9d..a9d1109579b 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/NotificationTemplateRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/NotificationTemplateRepository.java @@ -13,28 +13,50 @@ package org.openmetadata.service.jdbi3; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.Invocation; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Set; +import java.util.UUID; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.openmetadata.schema.alert.type.EmailAlertConfig; +import org.openmetadata.schema.api.events.NotificationTemplateRenderRequest; +import org.openmetadata.schema.api.events.NotificationTemplateRenderResponse; +import org.openmetadata.schema.api.events.NotificationTemplateSendRequest; import org.openmetadata.schema.api.events.NotificationTemplateValidationRequest; import org.openmetadata.schema.api.events.NotificationTemplateValidationResponse; +import org.openmetadata.schema.api.events.TemplateRenderResult; import org.openmetadata.schema.entity.events.EventSubscription; import org.openmetadata.schema.entity.events.NotificationTemplate; +import org.openmetadata.schema.entity.events.SubscriptionDestination; +import org.openmetadata.schema.type.ChangeEvent; import org.openmetadata.schema.type.EntityReference; import org.openmetadata.schema.type.Include; import org.openmetadata.schema.type.ProviderType; import org.openmetadata.schema.type.Relationship; +import org.openmetadata.schema.type.Webhook; import org.openmetadata.schema.type.change.ChangeSource; import org.openmetadata.schema.utils.JsonUtils; import org.openmetadata.service.Entity; +import org.openmetadata.service.notifications.HandlebarsNotificationMessageEngine; +import org.openmetadata.service.notifications.channels.NotificationMessage; +import org.openmetadata.service.notifications.channels.email.EmailMessage; import org.openmetadata.service.notifications.template.NotificationTemplateProcessor; import org.openmetadata.service.notifications.template.handlebars.HandlebarsNotificationTemplateProcessor; +import org.openmetadata.service.notifications.template.testing.EntityFixtureLoader; +import org.openmetadata.service.notifications.template.testing.MockChangeEventFactory; +import org.openmetadata.service.notifications.template.testing.MockChangeEventRegistry; import org.openmetadata.service.resources.events.NotificationTemplateResource; import org.openmetadata.service.util.EntityUtil; import org.openmetadata.service.util.EntityUtil.Fields; +import org.openmetadata.service.util.SubscriptionUtil; +import org.openmetadata.service.util.email.EmailUtil; import org.openmetadata.service.util.resourcepath.ResourcePathResolver; import org.openmetadata.service.util.resourcepath.providers.NotificationTemplateResourcePathProvider; @@ -45,6 +67,8 @@ public class NotificationTemplateRepository extends EntityRepository errors = new ArrayList<>(); - if (response.getTemplateBody() != null && !response.getTemplateBody().getPassed()) { - errors.add("Template body: " + response.getTemplateBody().getError()); - } - if (response.getTemplateSubject() != null && !response.getTemplateSubject().getPassed()) { - errors.add("Template subject: " + response.getTemplateSubject().getError()); - } - - if (!errors.isEmpty()) { + if (!response.getIsValid()) { + List errors = new ArrayList<>(); + if (response.getSubjectError() != null) { + errors.add("Template subject: " + response.getSubjectError()); + } + if (response.getBodyError() != null) { + errors.add("Template body: " + response.getBodyError()); + } throw new IllegalArgumentException("Invalid template: " + String.join("; ", errors)); } } @@ -258,6 +288,130 @@ public class NotificationTemplateRepository extends EntityRepository destinations) { + for (SubscriptionDestination dest : destinations) { + if (dest.getCategory() != SubscriptionDestination.SubscriptionCategory.EXTERNAL) { + throw new IllegalArgumentException( + "Only external destinations (Email, Slack, Teams, GChat, Webhook) are supported."); + } + } + } + + private void sendToDestination( + ChangeEvent event, + EventSubscription subscription, + SubscriptionDestination destination, + NotificationTemplate template) { + + NotificationMessage message = + messageEngine.generateMessageWithTemplate(event, subscription, destination, template); + + switch (destination.getType()) { + case EMAIL -> sendEmailNotification((EmailMessage) message, destination); + case SLACK, MS_TEAMS, G_CHAT, WEBHOOK -> sendWebhookNotification(message, destination); + default -> throw new IllegalArgumentException( + "Unsupported destination type: " + destination.getType()); + } + } + + private void sendEmailNotification( + EmailMessage emailMessage, SubscriptionDestination destination) { + EmailAlertConfig emailConfig = + JsonUtils.convertValue(destination.getConfig(), EmailAlertConfig.class); + Set receivers = emailConfig.getReceivers(); + + for (String receiver : receivers) { + EmailUtil.sendNotificationEmail( + receiver, emailMessage.getSubject(), emailMessage.getHtmlContent()); + } + } + + private void sendWebhookNotification( + NotificationMessage message, SubscriptionDestination destination) { + Webhook webhook = JsonUtils.convertValue(destination.getConfig(), Webhook.class); + String json = JsonUtils.pojoToJsonIgnoreNull(message); + + try (Client client = + SubscriptionUtil.getClient(destination.getTimeout(), destination.getReadTimeout())) { + Invocation.Builder target = SubscriptionUtil.getTarget(client, webhook, json); + + try (Response response = + target.post(jakarta.ws.rs.client.Entity.entity(json, MediaType.APPLICATION_JSON_TYPE))) { + if (response.getStatus() >= 300) { + throw new RuntimeException("Webhook failed with status: " + response.getStatus()); + } + } + } + } + public class NotificationTemplateUpdater extends EntityUpdater { public NotificationTemplateUpdater( NotificationTemplate original, NotificationTemplate updated, Operation operation) { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/notifications/HandlebarsNotificationMessageEngine.java b/openmetadata-service/src/main/java/org/openmetadata/service/notifications/HandlebarsNotificationMessageEngine.java index 5e2a13c7a02..2bbaaf45c5c 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/notifications/HandlebarsNotificationMessageEngine.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/notifications/HandlebarsNotificationMessageEngine.java @@ -74,6 +74,16 @@ public class HandlebarsNotificationMessageEngine implements NotificationMessageE // Resolve the template for this event NotificationTemplate template = resolveTemplate(event, subscription); + return generateMessageWithTemplate(event, subscription, destination, template); + } + + @Override + public NotificationMessage generateMessageWithTemplate( + ChangeEvent event, + EventSubscription subscription, + SubscriptionDestination destination, + NotificationTemplate template) { + // Create deep copy of event to avoid modifying the original ChangeEvent eventCopy = JsonUtils.deepCopy(event, ChangeEvent.class); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/notifications/NotificationMessageEngine.java b/openmetadata-service/src/main/java/org/openmetadata/service/notifications/NotificationMessageEngine.java index fac50a239cf..4e636c8dea9 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/notifications/NotificationMessageEngine.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/notifications/NotificationMessageEngine.java @@ -13,6 +13,7 @@ package org.openmetadata.service.notifications; import org.openmetadata.schema.entity.events.EventSubscription; +import org.openmetadata.schema.entity.events.NotificationTemplate; import org.openmetadata.schema.entity.events.SubscriptionDestination; import org.openmetadata.schema.type.ChangeEvent; import org.openmetadata.service.notifications.channels.NotificationMessage; @@ -32,4 +33,22 @@ public interface NotificationMessageEngine { */ NotificationMessage generateMessage( ChangeEvent event, EventSubscription subscription, SubscriptionDestination destination); + + /** + * Generate notification message using a specific template (for testing). + * Bypasses template resolution and uses the provided template directly. + * This method is primarily used for testing notification templates without storing them in the + * database. + * + * @param event The ChangeEvent to render + * @param subscription The EventSubscription context + * @param destination The target destination + * @param template The NotificationTemplate to use (not resolved from DB) + * @return Rendered NotificationMessage for the destination type + */ + NotificationMessage generateMessageWithTemplate( + ChangeEvent event, + EventSubscription subscription, + SubscriptionDestination destination, + NotificationTemplate template); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/notifications/template/handlebars/HandlebarsNotificationTemplateProcessor.java b/openmetadata-service/src/main/java/org/openmetadata/service/notifications/template/handlebars/HandlebarsNotificationTemplateProcessor.java index 0be8299c2ae..458c2bc73e3 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/notifications/template/handlebars/HandlebarsNotificationTemplateProcessor.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/notifications/template/handlebars/HandlebarsNotificationTemplateProcessor.java @@ -5,7 +5,6 @@ import com.github.jknack.handlebars.HandlebarsException; import com.github.jknack.handlebars.Template; import java.util.Map; import lombok.extern.slf4j.Slf4j; -import org.openmetadata.schema.api.events.FieldValidation; import org.openmetadata.schema.api.events.NotificationTemplateValidationRequest; import org.openmetadata.schema.api.events.NotificationTemplateValidationResponse; import org.openmetadata.service.notifications.template.NotificationTemplateProcessor; @@ -32,27 +31,25 @@ public class HandlebarsNotificationTemplateProcessor implements NotificationTemp @Override public NotificationTemplateValidationResponse validate( NotificationTemplateValidationRequest request) { - NotificationTemplateValidationResponse response = new NotificationTemplateValidationResponse(); - - // Validate template body - if (request.getTemplateBody() != null && !request.getTemplateBody().isEmpty()) { - FieldValidation bodyValidation = new FieldValidation(); - String error = validateTemplateString(request.getTemplateBody()); - bodyValidation.setPassed(error == null); - bodyValidation.setError(error); - response.setTemplateBody(bodyValidation); - } + String subjectError = null; + String bodyError = null; // Validate template subject if (request.getTemplateSubject() != null && !request.getTemplateSubject().isEmpty()) { - FieldValidation subjectValidation = new FieldValidation(); - String error = validateTemplateString(request.getTemplateSubject()); - subjectValidation.setPassed(error == null); - subjectValidation.setError(error); - response.setTemplateSubject(subjectValidation); + subjectError = validateTemplateString(request.getTemplateSubject()); } - return response; + // Validate template body + if (request.getTemplateBody() != null && !request.getTemplateBody().isEmpty()) { + bodyError = validateTemplateString(request.getTemplateBody()); + } + + boolean isValid = (subjectError == null) && (bodyError == null); + + return new NotificationTemplateValidationResponse() + .withIsValid(isValid) + .withSubjectError(subjectError) + .withBodyError(bodyError); } private String validateTemplateString(String templateString) { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/notifications/template/testing/EntityFixtureLoader.java b/openmetadata-service/src/main/java/org/openmetadata/service/notifications/template/testing/EntityFixtureLoader.java new file mode 100644 index 00000000000..31e4ef2475d --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/notifications/template/testing/EntityFixtureLoader.java @@ -0,0 +1,107 @@ +package org.openmetadata.service.notifications.template.testing; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Loads entity fixture JSON files from classpath with caching. + * Encapsulates all knowledge about fixture file paths and structure. + * Provides fallback to generic fixtures when specific fixtures aren't found. + */ +public class EntityFixtureLoader { + private static final String FIXTURES_BASE_PATH = "json/data/notifications/fixtures/"; + private static final String BASE_FIXTURE_PATH_FORMAT = FIXTURES_BASE_PATH + "%s/base.json"; + private static final String SCENARIO_FIXTURE_PATH_FORMAT = + FIXTURES_BASE_PATH + "%s/scenarios/%s.json"; + + private final Map> cache = new ConcurrentHashMap<>(); + private final ObjectMapper json = new ObjectMapper(); + + /** + * Generic base fixture with EntityInterface fields. + * Used as fallback when resource-specific fixture doesn't exist. + */ + private static final Map GENERIC_BASE_FIXTURE = + Map.of( + "id", UUID.randomUUID().toString(), + "name", "generic_entity", + "displayName", "Generic Entity", + "fullyQualifiedName", "generic.entity", + "description", "Generic entity for notification template testing", + "version", 1.0, + "updatedAt", System.currentTimeMillis(), + "updatedBy", "admin", + "deleted", false); + + /** + * Load base entity fixture for a resource. + * Convention: {FIXTURES_BASE_PATH}/{resource}/base.json + * Falls back to GENERIC_BASE_FIXTURE if resource-specific fixture not found. + * + * @param resource Resource type (e.g., "table", "dashboard") + * @return Entity fixture data as map (resource-specific or generic fallback) + */ + public Map loadBaseFixture(String resource) { + String path = String.format(BASE_FIXTURE_PATH_FORMAT, resource); + Map fixture = load(path); + + if (fixture.isEmpty()) { + return GENERIC_BASE_FIXTURE; + } + + return fixture; + } + + /** + * Load scenario fixture for a resource and event type. + * Convention: {FIXTURES_BASE_PATH}/{resource}/scenarios/{eventType}.json + * Returns empty map if scenario fixture not found (caller will use base entity only). + * + * @param resource Resource type (e.g., "table", "dashboard") + * @param eventType Event type (e.g., "entityCreated", "entityUpdated") + * @return Scenario fixture data as map (empty if not found) + */ + public Map loadScenarioFixture(String resource, String eventType) { + String path = String.format(SCENARIO_FIXTURE_PATH_FORMAT, resource, eventType); + Map fixture = load(path); + + if (fixture.isEmpty()) { + return Map.of(); + } + + return fixture; + } + + /** + * Load a JSON file from classpath with caching. + * Generic method for loading any JSON file by full path. + * + * @param path Full classpath path to the JSON file + * @return Map representation of the JSON file, or empty map if not found + */ + private Map load(String path) { + return cache.computeIfAbsent(path, this::loadFromClasspath); + } + + @SuppressWarnings("unchecked") + private Map loadFromClasspath(String path) { + try (InputStream is = getClass().getClassLoader().getResourceAsStream(path)) { + if (is == null) { + return Map.of(); // Return empty for missing files + } + return json.readValue(is, Map.class); + } catch (IOException e) { + throw new UncheckedIOException( + String.format( + "Failed to parse JSON file at classpath location: %s. " + + "Verify the file contains valid JSON syntax.", + path), + e); + } + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/notifications/template/testing/MockChangeEventFactory.java b/openmetadata-service/src/main/java/org/openmetadata/service/notifications/template/testing/MockChangeEventFactory.java new file mode 100644 index 00000000000..77a4464703e --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/notifications/template/testing/MockChangeEventFactory.java @@ -0,0 +1,76 @@ +package org.openmetadata.service.notifications.template.testing; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import javax.annotation.Nullable; +import org.openmetadata.schema.type.ChangeEvent; +import org.openmetadata.schema.type.EventType; +import org.openmetadata.schema.utils.JsonUtils; +import org.openmetadata.service.notifications.template.testing.MockChangeEventRegistry.EntityScenario; +import org.openmetadata.service.notifications.template.testing.MockChangeEventRegistry.Scenario; + +/** + * Factory for creating mock ChangeEvents for notification template testing. + */ +public final class MockChangeEventFactory { + private static final String TEST_USER = "test-user"; + + private final MockChangeEventRegistry registry; + + /** + * Create factory for generating mock ChangeEvents. + * + * @param registry Mock registry with builders and appliers + */ + public MockChangeEventFactory(MockChangeEventRegistry registry) { + this.registry = Objects.requireNonNull(registry, "registry must not be null"); + } + + /** + * Create a mock ChangeEvent for the given resource and eventType. + * Falls back to generic fixture for unknown resources. + * + * @param resource Resource type (e.g., "table", "dashboard") + * @param eventType Optional eventType (null for neutral mode) + * @return Fully populated ChangeEvent with mock data + */ + public ChangeEvent create(String resource, @Nullable EventType eventType) { + Objects.requireNonNull(resource, "resource must not be null"); + String type = resource.trim(); + + Map entity = new HashMap<>(); + registry.builder(type).build(entity); + + UUID entityId = UUID.fromString(String.valueOf(entity.get("id"))); + String fqn = "test.namespace.sample_" + type; + + ChangeEvent event = + new ChangeEvent() + .withId(UUID.randomUUID()) + .withEntityId(entityId) + .withEntityType(type) + .withEntity(JsonUtils.pojoToJson(entity)) + .withUserName(TEST_USER) + .withTimestamp(Instant.now().toEpochMilli()) + .withEntityFullyQualifiedName(fqn); + + Optional.ofNullable(eventType) + .ifPresentOrElse( + typeEvent -> { + Scenario scenario = new EntityScenario(type, typeEvent); + registry.scenarioApplier(scenario).apply(event, entity); + }, + () -> + event + .withEventType(null) + .withPreviousVersion(1.0) + .withCurrentVersion(1.1) + .withChangeDescription(null)); + + return event; + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/notifications/template/testing/MockChangeEventRegistry.java b/openmetadata-service/src/main/java/org/openmetadata/service/notifications/template/testing/MockChangeEventRegistry.java new file mode 100644 index 00000000000..0a3209b4ccd --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/notifications/template/testing/MockChangeEventRegistry.java @@ -0,0 +1,180 @@ +package org.openmetadata.service.notifications.template.testing; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.openmetadata.schema.type.ChangeDescription; +import org.openmetadata.schema.type.ChangeEvent; +import org.openmetadata.schema.type.EventType; +import org.openmetadata.schema.type.FieldChange; + +/** + * Registry for entity fixture builders and event scenario appliers. + * Provides DSL-like API for registering mock generation strategies. + */ +public final class MockChangeEventRegistry { + + /** + * Functional interface for building entity fixture data from loaded JSON. + */ + @FunctionalInterface + public interface EntityFixtureBuilder { + void build(Map entity); + } + + /** + * Functional interface for applying event scenario data to ChangeEvents. + */ + @FunctionalInterface + public interface EntityFixtureEventApplier { + void apply(ChangeEvent event, Map entity); + } + + /** + * Represents a test scenario for notification template testing. + * Used as a key to look up event scenario appliers in the registry. + */ + public sealed interface Scenario + permits MockChangeEventRegistry.EntityScenario, MockChangeEventRegistry.NeutralScenario { + String resource(); + + Optional eventType(); + } + + /** + * Scenario for testing with a specific eventType (e.g., entityUpdated). + */ + public record EntityScenario(String resource, EventType type) implements Scenario { + @Override + public Optional eventType() { + return Optional.of(type); + } + } + + /** + * Scenario for testing without eventType (neutral mode). + * Tests only entity field access, not event-specific logic. + */ + public record NeutralScenario(String resource) implements Scenario { + @Override + public Optional eventType() { + return Optional.empty(); + } + } + + private final Map builders = new HashMap<>(); + private final Map appliers = new HashMap<>(); + private final EntityFixtureLoader fixtures; + + public MockChangeEventRegistry(EntityFixtureLoader fixtures) { + this.fixtures = fixtures; + } + + /** + * Get builder for a resource using convention-based loading. + * Automatically loads from {resource}/base.json if not explicitly registered. + * Falls back to generic fixture if resource-specific fixture not found. + * Uses lazy loading with automatic caching. + * + * @param resource Resource type (e.g., "table", "dashboard") + * @return Builder for the resource + */ + public EntityFixtureBuilder builder(String resource) { + return builders.computeIfAbsent( + resource, r -> entity -> entity.putAll(fixtures.loadBaseFixture(r))); + } + + /** + * Get applier for a scenario using convention-based loading. + * Automatically loads from {resource}/scenarios/{eventType}.json if not explicitly registered. + * Gracefully handles missing scenario fixtures (uses base entity only). + * Uses lazy loading with automatic caching. + * + * @param scenario Scenario containing resource and optional event type + * @return Applier for the scenario + */ + public EntityFixtureEventApplier scenarioApplier(Scenario scenario) { + return appliers.computeIfAbsent( + scenario, + s -> { + if (s.eventType().isEmpty()) { + // Neutral scenario - no fixture loading needed + return (event, entity) -> {}; + } + + String eventTypeValue = s.eventType().get().value(); + + return (event, entity) -> { + Map scenarioData = + fixtures.loadScenarioFixture(s.resource(), eventTypeValue); + applyScenario(event, scenarioData); + }; + }); + } + + /** + * Apply scenario fixture to ChangeEvent. + * Gracefully handles empty scenarios (when fixture not found). + */ + private void applyScenario(ChangeEvent event, Map scenario) { + if (scenario.isEmpty()) { + // No scenario fixture found - skip scenario application + return; + } + + String eventTypeStr = (String) scenario.get("eventType"); + event + .withEventType(EventType.fromValue(eventTypeStr)) + .withPreviousVersion((Double) scenario.get("previousVersion")) + .withCurrentVersion((Double) scenario.get("currentVersion")); + + @SuppressWarnings("unchecked") + Map cdMap = (Map) scenario.get("changeDescription"); + if (cdMap != null) { + ChangeDescription cd = buildChangeDescription(cdMap); + event.withChangeDescription(cd); + } + } + + /** + * Build ChangeDescription from fixture map. + */ + @SuppressWarnings("unchecked") + private ChangeDescription buildChangeDescription(Map cdMap) { + ChangeDescription cd = + new ChangeDescription().withPreviousVersion((Double) cdMap.get("previousVersion")); + + List> added = (List>) cdMap.get("fieldsAdded"); + if (added != null) { + cd.withFieldsAdded( + added.stream() + .map(m -> new FieldChange().withName(m.get("name")).withNewValue(m.get("newValue"))) + .collect(Collectors.toList())); + } + + List> updated = (List>) cdMap.get("fieldsUpdated"); + if (updated != null) { + cd.withFieldsUpdated( + updated.stream() + .map( + m -> + new FieldChange() + .withName(m.get("name")) + .withOldValue(m.get("oldValue")) + .withNewValue(m.get("newValue"))) + .collect(Collectors.toList())); + } + + List> deleted = (List>) cdMap.get("fieldsDeleted"); + if (deleted != null) { + cd.withFieldsDeleted( + deleted.stream() + .map(m -> new FieldChange().withName(m.get("name")).withOldValue(m.get("oldValue"))) + .collect(Collectors.toList())); + } + + return cd; + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/events/NotificationTemplateResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/events/NotificationTemplateResource.java index d9fecf79952..4851fbefda9 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/events/NotificationTemplateResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/events/NotificationTemplateResource.java @@ -47,6 +47,9 @@ import java.util.UUID; import lombok.extern.slf4j.Slf4j; import org.openmetadata.schema.api.data.RestoreEntity; import org.openmetadata.schema.api.events.CreateNotificationTemplate; +import org.openmetadata.schema.api.events.NotificationTemplateRenderRequest; +import org.openmetadata.schema.api.events.NotificationTemplateRenderResponse; +import org.openmetadata.schema.api.events.NotificationTemplateSendRequest; import org.openmetadata.schema.api.events.NotificationTemplateValidationRequest; import org.openmetadata.schema.api.events.NotificationTemplateValidationResponse; import org.openmetadata.schema.entity.events.NotificationTemplate; @@ -614,9 +617,9 @@ public class NotificationTemplateResource @Path("/validate") @Operation( operationId = "validateNotificationTemplate", - summary = "Validate a notification template", + summary = "Validate notification template syntax", description = - "Validates the syntax and structure of a notification template without saving it. " + "Validates only the Handlebars syntax of template subject and body without generating mock data or sending. " + "Requires CREATE or EDIT_ALL permission for notification templates.", responses = { @ApiResponse( @@ -627,11 +630,7 @@ public class NotificationTemplateResource mediaType = "application/json", schema = @Schema(implementation = NotificationTemplateValidationResponse.class))), - @ApiResponse(responseCode = "400", description = "Invalid request"), - @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse( - responseCode = "403", - description = "Forbidden - CREATE or EDIT_ALL permission required") + @ApiResponse(responseCode = "400", description = "Bad request") }) public Response validateTemplate( @Context UriInfo uriInfo, @@ -655,4 +654,82 @@ public class NotificationTemplateResource return Response.ok(response).build(); } + + @POST + @Path("/render") + @Operation( + operationId = "renderNotificationTemplate", + summary = "Render notification template with mock data", + description = + "Generates mock ChangeEvent data for the specified resource and eventType, then renders the template. " + + "Returns the rendered subject and body for preview. Does not send to any destination. " + + "Requires CREATE or EDIT_ALL permission for notification templates.", + responses = { + @ApiResponse( + responseCode = "200", + description = "Rendering result with validation and render outputs", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = NotificationTemplateRenderResponse.class))), + @ApiResponse(responseCode = "400", description = "Bad request") + }) + public Response renderTemplate( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Valid NotificationTemplateRenderRequest request) { + + List authRequests = + List.of( + new AuthRequest( + new OperationContext(entityType, MetadataOperation.CREATE), getResourceContext()), + new AuthRequest( + new OperationContext(entityType, MetadataOperation.EDIT_ALL), + getResourceContext())); + authorizer.authorizeRequests(securityContext, authRequests, AuthorizationLogic.ANY); + + NotificationTemplateRenderResponse response = repository.render(request); + + return Response.ok(response).build(); + } + + @POST + @Path("/send") + @Operation( + operationId = "sendNotificationTemplateTest", + summary = "Validate and send notification template to external destinations", + description = + "Validates template syntax, generates mock ChangeEvent data, and sends to specified external destinations. " + + "Returns validation status only. Delivery errors are logged server-side. " + + "Only external destinations (Email, Slack, Teams, GChat, Webhook) are supported. " + + "Requires CREATE or EDIT_ALL permission for notification templates.", + responses = { + @ApiResponse( + responseCode = "200", + description = "Validation result", + content = + @Content( + mediaType = "application/json", + schema = + @Schema(implementation = NotificationTemplateValidationResponse.class))), + @ApiResponse(responseCode = "400", description = "Bad request or validation failure") + }) + public Response sendTemplate( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Valid NotificationTemplateSendRequest request) { + + List authRequests = + List.of( + new AuthRequest( + new OperationContext(entityType, MetadataOperation.CREATE), getResourceContext()), + new AuthRequest( + new OperationContext(entityType, MetadataOperation.EDIT_ALL), + getResourceContext())); + authorizer.authorizeRequests(securityContext, authRequests, AuthorizationLogic.ANY); + + NotificationTemplateValidationResponse response = repository.send(request); + + return Response.ok(response).build(); + } } diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/chart/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/chart/base.json new file mode 100644 index 00000000000..4fa8e76b609 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/chart/base.json @@ -0,0 +1,49 @@ +{ + "id": "7f8c3e4d-5a6b-4c9d-8e7f-6a5b4c3d2e1f", + "name": "sample_chart", + "displayName": "Sample Chart", + "fullyQualifiedName": "sample-dashboard-service.sample_chart", + "description": "Mock chart for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "chartType": "Bar", + "sourceUrl": "/charts/sample_chart", + "href": "http://localhost:8585/api/v1/charts/7f8c3e4d-5a6b-4c9d-8e7f-6a5b4c3d2e1f", + "service": { + "id": "9e8d7c6b-5a4f-3e2d-1c0b-9a8f7e6d5c4b", + "type": "dashboardService", + "name": "sample-dashboard-service", + "fullyQualifiedName": "sample-dashboard-service", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/dashboardServices/9e8d7c6b-5a4f-3e2d-1c0b-9a8f7e6d5c4b" + }, + "serviceType": "Superset", + "dashboards": [ + { + "id": "a1b2c3d4-1111-2222-3333-444444444444", + "type": "dashboard", + "name": "sample_dashboard", + "fullyQualifiedName": "sample-dashboard-service.sample_dashboard", + "displayName": "Sample Dashboard" + } + ], + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "domains": [ + { + "id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", + "type": "domain", + "name": "Engineering", + "fullyQualifiedName": "Engineering" + } + ], + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/container/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/container/base.json new file mode 100644 index 00000000000..63c8bb305f3 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/container/base.json @@ -0,0 +1,45 @@ +{ + "id": "cf7d6b5a-4f3e-2d1c-0b9a-8f7e6d5c4b3a", + "name": "sample_container", + "displayName": "Sample Container", + "fullyQualifiedName": "sample-storage-service.sample_container", + "description": "Mock container for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "dataModel": { + "isPartitioned": false + }, + "prefix": "/data/", + "numberOfObjects": 150, + "size": 1073741824, + "fileFormats": ["parquet", "json"], + "href": "http://localhost:8585/api/v1/containers/cf7d6b5a-4f3e-2d1c-0b9a-8f7e6d5c4b3a", + "service": { + "id": "5a4f3e2d-1c0b-9a8f-7e6d-5c4b3a2f1e0d", + "type": "storageService", + "name": "sample-storage-service", + "fullyQualifiedName": "sample-storage-service", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/storageServices/5a4f3e2d-1c0b-9a8f-7e6d-5c4b3a2f1e0d" + }, + "serviceType": "S3", + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "domains": [ + { + "id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", + "type": "domain", + "name": "Engineering", + "fullyQualifiedName": "Engineering" + } + ], + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/dashboard/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/dashboard/base.json new file mode 100644 index 00000000000..1e3aed207a2 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/dashboard/base.json @@ -0,0 +1,63 @@ +{ + "id": "a1b2c3d4-1111-2222-3333-444444444444", + "name": "sample_dashboard", + "displayName": "Sample Dashboard", + "fullyQualifiedName": "sample-dashboard-service.sample_dashboard", + "description": "Mock dashboard for notification template testing", + "version": 1.0, + "updatedAt": 1234567890000, + "updatedBy": "admin", + "href": "http://localhost:8585/api/v1/dashboards/a1b2c3d4-1111-2222-3333-444444444444", + "dashboardType": "Dashboard", + "charts": [ + { + "id": "b2c3d4e5-2222-3333-4444-555555555555", + "type": "chart", + "name": "revenue_chart", + "fullyQualifiedName": "sample-dashboard-service.revenue_chart", + "displayName": "Revenue Overview" + }, + { + "id": "c3d4e5f6-3333-4444-5555-666666666666", + "type": "chart", + "name": "users_chart", + "fullyQualifiedName": "sample-dashboard-service.users_chart", + "displayName": "Active Users" + } + ], + "service": { + "id": "d4e5f6a7-4444-5555-6666-777777777777", + "type": "dashboardService", + "name": "sample-dashboard-service", + "fullyQualifiedName": "sample-dashboard-service", + "displayName": "Sample Dashboard Service" + }, + "serviceType": "Superset", + "dataModels": [ + { + "id": "e5f6a7b8-5555-6666-7777-888888888888", + "type": "dashboardDataModel", + "name": "sample_data_model", + "fullyQualifiedName": "sample-dashboard-service.sample_data_model", + "displayName": "Sample Data Model" + } + ], + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "domains": [ + { + "id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", + "type": "domain", + "name": "Engineering", + "fullyQualifiedName": "Engineering" + } + ], + "tags": [], + "deleted": false +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/dashboardDataModel/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/dashboardDataModel/base.json new file mode 100644 index 00000000000..abede522c74 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/dashboardDataModel/base.json @@ -0,0 +1,52 @@ +{ + "id": "8e7d6c5b-4a3f-2e1d-0c9b-8a7f6e5d4c3b", + "name": "sample_data_model", + "displayName": "Sample Data Model", + "fullyQualifiedName": "sample-dashboard-service.sample_dashboard.datamodel.sample_data_model", + "description": "Mock dashboard data model for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "dataModelType": "SupersetDataModel", + "sourceUrl": "/datamodels/sample_data_model", + "href": "http://localhost:8585/api/v1/dashboardDataModels/8e7d6c5b-4a3f-2e1d-0c9b-8a7f6e5d4c3b", + "service": { + "id": "9e8d7c6b-5a4f-3e2d-1c0b-9a8f7e6d5c4b", + "type": "dashboardService", + "name": "sample-dashboard-service", + "fullyQualifiedName": "sample-dashboard-service", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/dashboardServices/9e8d7c6b-5a4f-3e2d-1c0b-9a8f7e6d5c4b" + }, + "serviceType": "Superset", + "columns": [ + { + "name": "user_id", + "dataType": "NUMERIC", + "description": "User identifier" + }, + { + "name": "revenue", + "dataType": "NUMERIC", + "description": "Total revenue" + } + ], + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "domains": [ + { + "id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", + "type": "domain", + "name": "Engineering", + "fullyQualifiedName": "Engineering" + } + ], + "tags": [] +} diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/dashboardService/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/dashboardService/base.json new file mode 100644 index 00000000000..a9c43c5b8de --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/dashboardService/base.json @@ -0,0 +1,28 @@ +{ + "id": "9e8d7c6b-5a4f-3e2d-1c0b-9a8f7e6d5c4b", + "name": "sample-dashboard-service", + "displayName": "Sample Dashboard Service", + "fullyQualifiedName": "sample-dashboard-service", + "description": "Mock dashboard service for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/dashboardServices/9e8d7c6b-5a4f-3e2d-1c0b-9a8f7e6d5c4b", + "serviceType": "Superset", + "connection": { + "config": { + "type": "Superset", + "hostPort": "http://localhost:8088" + } + }, + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/dataContract/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/dataContract/base.json new file mode 100644 index 00000000000..5a58fdcc1c7 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/dataContract/base.json @@ -0,0 +1,90 @@ +{ + "id": "df6c5b4a-3f2e-1d0c-9b8a-7f6e5d4c3b2a", + "name": "sample_data_contract", + "displayName": "Sample Data Contract", + "fullyQualifiedName": "sample_data_contract", + "description": "Mock data contract for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/dataContracts/df6c5b4a-3f2e-1d0c-9b8a-7f6e5d4c3b2a", + "contractType": "SLA", + "entity": { + "id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", + "type": "table", + "name": "sample_table", + "fullyQualifiedName": "sample-service.sample-db.public.sample_table", + "deleted": false + }, + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "reviewers": [ + { + "id": "b2c3d4e5-f678-90ab-cdef-123456789012", + "type": "user", + "name": "jane.smith", + "fullyQualifiedName": "jane.smith" + } + ], + "entityStatus": "Approved", + "schema": { + "columns": [ + { + "name": "id", + "dataType": "BIGINT", + "constraint": "NOT NULL" + }, + { + "name": "email", + "dataType": "VARCHAR", + "constraint": "NOT NULL" + } + ] + }, + "qualityExpectations": { + "completeness": { + "threshold": 95.0 + }, + "accuracy": { + "threshold": 99.0 + } + }, + "contractUpdates": [ + { + "timestamp": 1699564800000, + "updatedBy": "admin", + "changes": "Initial contract creation" + } + ], + "semantics": { + "purpose": "Customer data management", + "classifications": ["PII", "GDPR"] + }, + "termsOfUse": "Data must be used in accordance with privacy policies", + "security": { + "encryption": "AES-256", + "accessControl": "RBAC" + }, + "sla": { + "freshness": { + "maxAge": "PT24H" + }, + "availability": { + "uptime": 99.9 + } + }, + "latestResult": { + "timestamp": 1699564800000, + "status": "Passed", + "violations": 0 + }, + "dataQualityTests": [], + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/database/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/database/base.json new file mode 100644 index 00000000000..9f3b1a629ee --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/database/base.json @@ -0,0 +1,39 @@ +{ + "id": "ef5b4a3f-2e1d-0c9b-8a7f-6e5d4c3b2a1f", + "name": "sample_database", + "displayName": "Sample Database", + "fullyQualifiedName": "sample-database-service.sample_database", + "description": "Mock database for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/databases/ef5b4a3f-2e1d-0c9b-8a7f-6e5d4c3b2a1f", + "service": { + "id": "4a3f2e1d-0c9b-8a7f-6e5d-4c3b2a1f0e9d", + "type": "databaseService", + "name": "sample-database-service", + "fullyQualifiedName": "sample-database-service", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/databaseServices/4a3f2e1d-0c9b-8a7f-6e5d-4c3b2a1f0e9d" + }, + "serviceType": "Postgres", + "default": false, + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "domains": [ + { + "id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", + "type": "domain", + "name": "Engineering", + "fullyQualifiedName": "Engineering" + } + ], + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/databaseSchema/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/databaseSchema/base.json new file mode 100644 index 00000000000..cbf3e1c3619 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/databaseSchema/base.json @@ -0,0 +1,47 @@ +{ + "id": "ff4a3f2e-1d0c-9b8a-7f6e-5d4c3b2a1f0e", + "name": "public", + "displayName": "Public Schema", + "fullyQualifiedName": "sample-database-service.sample_database.public", + "description": "Mock database schema for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/databaseSchemas/ff4a3f2e-1d0c-9b8a-7f6e-5d4c3b2a1f0e", + "service": { + "id": "4a3f2e1d-0c9b-8a7f-6e5d-4c3b2a1f0e9d", + "type": "databaseService", + "name": "sample-database-service", + "fullyQualifiedName": "sample-database-service", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/databaseServices/4a3f2e1d-0c9b-8a7f-6e5d-4c3b2a1f0e9d" + }, + "serviceType": "Postgres", + "database": { + "id": "ef5b4a3f-2e1d-0c9b-8a7f-6e5d4c3b2a1f", + "type": "database", + "name": "sample_database", + "fullyQualifiedName": "sample-database-service.sample_database", + "deleted": false, + "href": "http://localhost:8585/api/v1/databases/ef5b4a3f-2e1d-0c9b-8a7f-6e5d4c3b2a1f" + }, + "retentionPeriod": "P30D", + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "domains": [ + { + "id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", + "type": "domain", + "name": "Engineering", + "fullyQualifiedName": "Engineering" + } + ], + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/databaseService/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/databaseService/base.json new file mode 100644 index 00000000000..b0d82d36b5e --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/databaseService/base.json @@ -0,0 +1,31 @@ +{ + "id": "4a3f2e1d-0c9b-8a7f-6e5d-4c3b2a1f0e9d", + "name": "sample-database-service", + "displayName": "Sample Database Service", + "fullyQualifiedName": "sample-database-service", + "description": "Mock database service for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/databaseServices/4a3f2e1d-0c9b-8a7f-6e5d-4c3b2a1f0e9d", + "serviceType": "Postgres", + "connection": { + "config": { + "type": "Postgres", + "scheme": "postgresql+psycopg2", + "username": "openmetadata_user", + "hostPort": "localhost:5432", + "database": "sample_database" + } + }, + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/glossary/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/glossary/base.json new file mode 100644 index 00000000000..c03742652c2 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/glossary/base.json @@ -0,0 +1,14 @@ +{ + "id": "1e2d1c0b-9a8f-7e6d-5c4b-3a2f1e0d9c8b", + "name": "sample_glossary", + "displayName": "Sample Glossary", + "fullyQualifiedName": "sample_glossary", + "description": "Mock glossary for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/glossaries/1e2d1c0b-9a8f-7e6d-5c4b-3a2f1e0d9c8b", + "mutuallyExclusive": false, + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/glossaryTerm/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/glossaryTerm/base.json new file mode 100644 index 00000000000..34258078345 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/glossaryTerm/base.json @@ -0,0 +1,53 @@ +{ + "id": "2d1c0b9a-8f7e-6d5c-4b3a-2f1e0d9c8b7a", + "name": "sample_term", + "displayName": "Sample Term", + "fullyQualifiedName": "sample_glossary.sample_term", + "description": "Mock glossary term for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/glossaryTerms/2d1c0b9a-8f7e-6d5c-4b3a-2f1e0d9c8b7a", + "glossary": { + "id": "1e2d1c0b-9a8f-7e6d-5c4b-3a2f1e0d9c8b", + "type": "glossary", + "name": "sample_glossary", + "fullyQualifiedName": "sample_glossary", + "deleted": false, + "href": "http://localhost:8585/api/v1/glossaries/1e2d1c0b-9a8f-7e6d-5c4b-3a2f1e0d9c8b" + }, + "parent": null, + "synonyms": [], + "relatedTerms": [], + "references": [], + "style": { + "color": "#7147E8", + "iconURL": "https://example.com/icons/glossary.svg" + }, + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "domains": [ + { + "id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", + "type": "domain", + "name": "Engineering", + "fullyQualifiedName": "Engineering" + } + ], + "reviewers": [ + { + "id": "b2c3d4e5-f678-90ab-cdef-123456789012", + "type": "user", + "name": "jane.smith", + "fullyQualifiedName": "jane.smith" + } + ], + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/ingestionPipeline/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/ingestionPipeline/base.json new file mode 100644 index 00000000000..8342658ce99 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/ingestionPipeline/base.json @@ -0,0 +1,40 @@ +{ + "id": "2d1c0b9a-8f7e-6d5c-4b3a-2f1e0d9c8b7a", + "name": "sample_ingestion_pipeline", + "displayName": "Sample Ingestion Pipeline", + "fullyQualifiedName": "sample-database-service.sample_ingestion_pipeline", + "description": "Mock ingestion pipeline for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/ingestionPipelines/2d1c0b9a-8f7e-6d5c-4b3a-2f1e0d9c8b7a", + "pipelineType": "metadata", + "service": { + "id": "4a3f2e1d-0c9b-8a7f-6e5d-4c3b2a1f0e9d", + "type": "databaseService", + "name": "sample-database-service", + "fullyQualifiedName": "sample-database-service", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/databaseServices/4a3f2e1d-0c9b-8a7f-6e5d-4c3b2a1f0e9d" + }, + "sourceConfig": { + "config": { + "type": "DatabaseMetadata" + } + }, + "airflowConfig": { + "scheduleInterval": "0 0 * * *" + }, + "enabled": true, + "deployed": true, + "loggerLevel": "INFO", + "processingEngine": { + "type": "Default", + "configuration": {} + }, + "pipelineStatuses": { + "timestamp": 1699564800000, + "pipelineState": "success" + } +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/location/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/location/base.json new file mode 100644 index 00000000000..a221553f381 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/location/base.json @@ -0,0 +1,24 @@ +{ + "id": "0f3e2d1c-0b9a-8f7e-6d5c-4b3a2f1e0d9c", + "name": "sample_location", + "displayName": "Sample Location", + "fullyQualifiedName": "sample-storage-service.sample_location", + "description": "Mock location for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "locationType": "Bucket", + "path": "s3://sample-bucket/data", + "href": "http://localhost:8585/api/v1/locations/0f3e2d1c-0b9a-8f7e-6d5c-4b3a2f1e0d9c", + "service": { + "id": "5a4f3e2d-1c0b-9a8f-7e6d-5c4b3a2f1e0d", + "type": "storageService", + "name": "sample-storage-service", + "fullyQualifiedName": "sample-storage-service", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/storageServices/5a4f3e2d-1c0b-9a8f-7e6d-5c4b3a2f1e0d" + }, + "serviceType": "S3", + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/messagingService/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/messagingService/base.json new file mode 100644 index 00000000000..cfa0549663c --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/messagingService/base.json @@ -0,0 +1,28 @@ +{ + "id": "8d7c6b5a-4f3e-2d1c-0b9a-8f7e6d5c4b3a", + "name": "sample-messaging-service", + "displayName": "Sample Messaging Service", + "fullyQualifiedName": "sample-messaging-service", + "description": "Mock messaging service for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/messagingServices/8d7c6b5a-4f3e-2d1c-0b9a-8f7e6d5c4b3a", + "serviceType": "Kafka", + "connection": { + "config": { + "type": "Kafka", + "bootstrapServers": "localhost:9092" + } + }, + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/metadataService/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/metadataService/base.json new file mode 100644 index 00000000000..247852737d5 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/metadataService/base.json @@ -0,0 +1,28 @@ +{ + "id": "3e2d1c0b-9a8f-7e6d-5c4b-3a2f1e0d9c8b", + "name": "sample-metadata-service", + "displayName": "Sample Metadata Service", + "fullyQualifiedName": "sample-metadata-service", + "description": "Mock metadata service for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/metadataServices/3e2d1c0b-9a8f-7e6d-5c4b-3a2f1e0d9c8b", + "serviceType": "Amundsen", + "connection": { + "config": { + "type": "Amundsen", + "hostPort": "http://localhost:5000" + } + }, + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/mlmodel/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/mlmodel/base.json new file mode 100644 index 00000000000..086e0bdd069 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/mlmodel/base.json @@ -0,0 +1,69 @@ +{ + "id": "bf8e7d6c-5a4f-3e2d-1c0b-9a8f7e6d5c4b", + "name": "sample_ml_model", + "displayName": "Sample ML Model", + "fullyQualifiedName": "sample-mlmodel-service.sample_ml_model", + "description": "Mock ML model for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "algorithm": "RandomForest", + "href": "http://localhost:8585/api/v1/mlmodels/bf8e7d6c-5a4f-3e2d-1c0b-9a8f7e6d5c4b", + "service": { + "id": "6b5a4f3e-2d1c-0b9a-8f7e-6d5c4b3a2f1e", + "type": "mlmodelService", + "name": "sample-mlmodel-service", + "fullyQualifiedName": "sample-mlmodel-service", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/mlmodelServices/6b5a4f3e-2d1c-0b9a-8f7e-6d5c4b3a2f1e" + }, + "serviceType": "Mlflow", + "mlFeatures": [ + { + "name": "age", + "dataType": "numerical", + "description": "Customer age" + }, + { + "name": "category", + "dataType": "categorical", + "description": "Product category" + } + ], + "mlHyperParameters": [ + { + "name": "n_estimators", + "value": "100" + }, + { + "name": "max_depth", + "value": "10" + } + ], + "target": "purchase_probability", + "dashboard": { + "id": "a1b2c3d4-1111-2222-3333-444444444444", + "type": "dashboard", + "name": "sample_dashboard", + "fullyQualifiedName": "sample-dashboard-service.sample_dashboard", + "displayName": "Sample Dashboard" + }, + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "domains": [ + { + "id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", + "type": "domain", + "name": "Engineering", + "fullyQualifiedName": "Engineering" + } + ], + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/mlmodelService/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/mlmodelService/base.json new file mode 100644 index 00000000000..1d64ba7864c --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/mlmodelService/base.json @@ -0,0 +1,28 @@ +{ + "id": "6b5a4f3e-2d1c-0b9a-8f7e-6d5c4b3a2f1e", + "name": "sample-mlmodel-service", + "displayName": "Sample ML Model Service", + "fullyQualifiedName": "sample-mlmodel-service", + "description": "Mock ML model service for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/mlmodelServices/6b5a4f3e-2d1c-0b9a-8f7e-6d5c4b3a2f1e", + "serviceType": "Mlflow", + "connection": { + "config": { + "type": "Mlflow", + "trackingUri": "http://localhost:5000" + } + }, + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/pipeline/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/pipeline/base.json new file mode 100644 index 00000000000..f1c12681d1b --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/pipeline/base.json @@ -0,0 +1,60 @@ +{ + "id": "af9e8d7c-6b5a-4f3e-2d1c-0b9a8f7e6d5c", + "name": "sample_pipeline", + "displayName": "Sample Pipeline", + "fullyQualifiedName": "sample-pipeline-service.sample_pipeline", + "description": "Mock pipeline for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "sourceUrl": "http://localhost:8080/pipelines/sample_pipeline", + "href": "http://localhost:8585/api/v1/pipelines/af9e8d7c-6b5a-4f3e-2d1c-0b9a8f7e6d5c", + "service": { + "id": "7c6b5a4f-3e2d-1c0b-9a8f-7e6d5c4b3a2f", + "type": "pipelineService", + "name": "sample-pipeline-service", + "fullyQualifiedName": "sample-pipeline-service", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/pipelineServices/7c6b5a4f-3e2d-1c0b-9a8f-7e6d5c4b3a2f" + }, + "serviceType": "Airflow", + "pipelineStatus": { + "timestamp": 1699564800000, + "executionStatus": "Successful" + }, + "tasks": [ + { + "name": "extract_task", + "displayName": "Extract Data", + "description": "Extract data from source" + }, + { + "name": "transform_task", + "displayName": "Transform Data", + "description": "Transform extracted data" + }, + { + "name": "load_task", + "displayName": "Load Data", + "description": "Load transformed data" + } + ], + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "domains": [ + { + "id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", + "type": "domain", + "name": "Engineering", + "fullyQualifiedName": "Engineering" + } + ], + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/pipelineService/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/pipelineService/base.json new file mode 100644 index 00000000000..571b36c4bdf --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/pipelineService/base.json @@ -0,0 +1,28 @@ +{ + "id": "7c6b5a4f-3e2d-1c0b-9a8f-7e6d5c4b3a2f", + "name": "sample-pipeline-service", + "displayName": "Sample Pipeline Service", + "fullyQualifiedName": "sample-pipeline-service", + "description": "Mock pipeline service for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/pipelineServices/7c6b5a4f-3e2d-1c0b-9a8f-7e6d5c4b3a2f", + "serviceType": "Airflow", + "connection": { + "config": { + "type": "Airflow", + "hostPort": "http://localhost:8080" + } + }, + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/storageService/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/storageService/base.json new file mode 100644 index 00000000000..81eba64c08d --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/storageService/base.json @@ -0,0 +1,28 @@ +{ + "id": "5a4f3e2d-1c0b-9a8f-7e6d-5c4b3a2f1e0d", + "name": "sample-storage-service", + "displayName": "Sample Storage Service", + "fullyQualifiedName": "sample-storage-service", + "description": "Mock storage service for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/storageServices/5a4f3e2d-1c0b-9a8f-7e6d-5c4b3a2f1e0d", + "serviceType": "S3", + "connection": { + "config": { + "type": "S3", + "awsRegion": "us-east-1" + } + }, + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/table/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/table/base.json new file mode 100644 index 00000000000..20bb3e2e659 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/table/base.json @@ -0,0 +1,97 @@ +{ + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "name": "sample_table", + "displayName": "Sample Table", + "fullyQualifiedName": "sample-service.sample-db.public.sample_table", + "description": "Mock table for notification template testing", + "version": 1.0, + "updatedAt": 1234567890000, + "updatedBy": "admin", + "href": "http://localhost:8585/api/v1/tables/3fa85f64-5717-4562-b3fc-2c963f66afa6", + "tableType": "Regular", + "columns": [ + { + "name": "id", + "dataType": "BIGINT", + "dataTypeDisplay": "bigint", + "description": "Unique identifier", + "fullyQualifiedName": "sample-service.sample-db.public.sample_table.id", + "ordinalPosition": 1 + }, + { + "name": "name", + "dataType": "VARCHAR", + "dataTypeDisplay": "varchar(255)", + "description": "User name", + "fullyQualifiedName": "sample-service.sample-db.public.sample_table.name", + "ordinalPosition": 2 + }, + { + "name": "email", + "dataType": "VARCHAR", + "dataTypeDisplay": "varchar(255)", + "description": "Contact email address", + "fullyQualifiedName": "sample-service.sample-db.public.sample_table.email", + "ordinalPosition": 3 + } + ], + "databaseSchema": { + "id": "7c7f9a8b-1234-5678-9abc-def012345678", + "type": "databaseSchema", + "name": "public", + "fullyQualifiedName": "sample-service.sample-db.public", + "description": "Public database schema", + "displayName": "Public" + }, + "database": { + "id": "8d8e0b9c-2345-6789-0bcd-ef1234567890", + "type": "database", + "name": "sample-db", + "fullyQualifiedName": "sample-service.sample-db", + "description": "Sample database", + "displayName": "Sample DB" + }, + "service": { + "id": "9e9f1cad-3456-789a-1cde-f23456789012", + "type": "databaseService", + "name": "sample-service", + "fullyQualifiedName": "sample-service", + "displayName": "Sample Service" + }, + "serviceType": "Postgres", + "tableConstraints": [ + { + "constraintType": "PRIMARY_KEY", + "columns": ["id"] + } + ], + "tablePartition": { + "columns": ["created_date"], + "intervalType": "TIME-UNIT" + }, + "dataModel": { + "id": "4f5a6b7c-8d9e-0f1a-2b3c-4d5e6f7a8b9c", + "type": "dashboardDataModel", + "name": "sample_data_model", + "fullyQualifiedName": "sample-dashboard-service.sample_data_model" + }, + "sourceUrl": "https://github.com/organization/repo/blob/main/schema/sample_table.sql", + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "domains": [ + { + "id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", + "type": "domain", + "name": "Engineering", + "fullyQualifiedName": "Engineering" + } + ], + "tags": [], + "deleted": false +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/tag/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/tag/base.json new file mode 100644 index 00000000000..f07d92d5863 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/tag/base.json @@ -0,0 +1,24 @@ +{ + "id": "3c0b9a8f-7e6d-5c4b-3a2f-1e0d9c8b7a6f", + "name": "sample_tag", + "displayName": "Sample Tag", + "fullyQualifiedName": "sample_classification.sample_tag", + "description": "Mock tag for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "deprecated": false, + "href": "http://localhost:8585/api/v1/tags/3c0b9a8f-7e6d-5c4b-3a2f-1e0d9c8b7a6f", + "classification": { + "id": "4b9a8f7e-6d5c-4b3a-2f1e-0d9c8b7a6f5e", + "type": "classification", + "name": "sample_classification", + "fullyQualifiedName": "sample_classification", + "deleted": false + }, + "style": { + "color": "#2196F3", + "iconURL": "" + } +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/tagCategory/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/tagCategory/base.json new file mode 100644 index 00000000000..69c38deccdf --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/tagCategory/base.json @@ -0,0 +1,15 @@ +{ + "id": "4b9a8f7e-6d5c-4b3a-2f1e-0d9c8b7a6f5e", + "name": "sample_classification", + "displayName": "Sample Classification", + "fullyQualifiedName": "sample_classification", + "description": "Mock classification (tag category) for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/classifications/4b9a8f7e-6d5c-4b3a-2f1e-0d9c8b7a6f5e", + "mutuallyExclusive": false, + "disabled": false, + "provider": "User" +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/testCase/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/testCase/base.json new file mode 100644 index 00000000000..98d5500be4a --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/testCase/base.json @@ -0,0 +1,63 @@ +{ + "id": "1c0b9a8f-7e6d-5c4b-3a2f-1e0d9c8b7a6f", + "name": "sample_test_case", + "displayName": "Sample Test Case", + "fullyQualifiedName": "sample_test_suite.sample_test_case", + "description": "Mock test case for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/dataQuality/testCases/1c0b9a8f-7e6d-5c4b-3a2f-1e0d9c8b7a6f", + "testSuite": { + "id": "0b9a8f7e-6d5c-4b3a-2f1e-0d9c8b7a6f5e", + "type": "testSuite", + "name": "sample_test_suite", + "fullyQualifiedName": "sample_test_suite", + "deleted": false, + "href": "http://localhost:8585/api/v1/dataQuality/testSuites/0b9a8f7e-6d5c-4b3a-2f1e-0d9c8b7a6f5e" + }, + "entityLink": "<#E::table::sample-service.sample-db.public.sample_table>", + "testDefinition": { + "id": "9a8f7e6d-5c4b-3a2f-1e0d-9c8b7a6f5e4d", + "type": "testDefinition", + "name": "columnValuesToBeNotNull", + "fullyQualifiedName": "columnValuesToBeNotNull", + "deleted": false + }, + "parameterValues": [ + { + "name": "columnName", + "value": "id" + } + ], + "testCaseResult": { + "timestamp": 1699564800000, + "testCaseStatus": "Success", + "result": "Test passed successfully" + }, + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "testSuites": [ + { + "id": "0b9a8f7e-6d5c-4b3a-2f1e-0d9c8b7a6f5e", + "type": "testSuite", + "name": "sample_test_suite", + "fullyQualifiedName": "sample_test_suite" + } + ], + "domains": [ + { + "id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", + "type": "domain", + "name": "Engineering", + "fullyQualifiedName": "Engineering" + } + ] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/testSuite/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/testSuite/base.json new file mode 100644 index 00000000000..ea6c4a2bb45 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/testSuite/base.json @@ -0,0 +1,33 @@ +{ + "id": "0b9a8f7e-6d5c-4b3a-2f1e-0d9c8b7a6f5e", + "name": "sample_test_suite", + "displayName": "Sample Test Suite", + "fullyQualifiedName": "sample_test_suite", + "description": "Mock test suite for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/dataQuality/testSuites/0b9a8f7e-6d5c-4b3a-2f1e-0d9c8b7a6f5e", + "executableEntityReference": { + "id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", + "type": "table", + "name": "sample_table", + "fullyQualifiedName": "sample-service.sample-db.public.sample_table", + "deleted": false + }, + "summary": { + "success": 8, + "failed": 2, + "aborted": 0, + "total": 10 + }, + "tests": [ + { + "id": "1c0b9a8f-7e6d-5c4b-3a2f-1e0d9c8b7a6f", + "type": "testCase", + "name": "sample_test_case", + "fullyQualifiedName": "sample_test_suite.sample_test_case" + } + ] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/topic/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/topic/base.json new file mode 100644 index 00000000000..96b7c81bfa6 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/topic/base.json @@ -0,0 +1,46 @@ +{ + "id": "9f8e7d6c-5b4a-3f2e-1d0c-9b8a7f6e5d4c", + "name": "sample_topic", + "displayName": "Sample Topic", + "fullyQualifiedName": "sample-messaging-service.sample_topic", + "description": "Mock topic for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "partitions": 3, + "replicationFactor": 1, + "maximumMessageSize": 1048576, + "cleanupPolicies": ["delete"], + "href": "http://localhost:8585/api/v1/topics/9f8e7d6c-5b4a-3f2e-1d0c-9b8a7f6e5d4c", + "service": { + "id": "8d7c6b5a-4f3e-2d1c-0b9a-8f7e6d5c4b3a", + "type": "messagingService", + "name": "sample-messaging-service", + "fullyQualifiedName": "sample-messaging-service", + "deleted": false, + "href": "http://localhost:8585/api/v1/services/messagingServices/8d7c6b5a-4f3e-2d1c-0b9a-8f7e6d5c4b3a" + }, + "serviceType": "Kafka", + "messageSchema": { + "schemaType": "Avro", + "schemaText": "{\"type\":\"record\",\"name\":\"SampleEvent\",\"fields\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"timestamp\",\"type\":\"long\"}]}" + }, + "owners": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "type": "user", + "name": "john.doe", + "fullyQualifiedName": "john.doe" + } + ], + "domains": [ + { + "id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", + "type": "domain", + "name": "Engineering", + "fullyQualifiedName": "Engineering" + } + ], + "tags": [] +} \ No newline at end of file diff --git a/openmetadata-service/src/main/resources/json/data/notifications/fixtures/user/base.json b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/user/base.json new file mode 100644 index 00000000000..f8429434e42 --- /dev/null +++ b/openmetadata-service/src/main/resources/json/data/notifications/fixtures/user/base.json @@ -0,0 +1,72 @@ +{ + "id": "9a8f7e6d-5c4b-3a2f-1e0d-9c8b7a6f5e4d", + "name": "sample.user", + "displayName": "Sample User", + "fullyQualifiedName": "sample.user", + "description": "Mock user for notification template testing", + "version": 1.0, + "updatedAt": 1699564800000, + "updatedBy": "admin", + "deleted": false, + "href": "http://localhost:8585/api/v1/users/9a8f7e6d-5c4b-3a2f-1e0d-9c8b7a6f5e4d", + "email": "sample.user@example.com", + "timezone": "America/Los_Angeles", + "isBot": false, + "isAdmin": false, + "authenticationMechanism": { + "authType": "JWT", + "config": {} + }, + "roles": [ + { + "id": "8f7e6d5c-4b3a-2f1e-0d9c-8b7a6f5e4d3c", + "type": "role", + "name": "DataConsumer", + "fullyQualifiedName": "DataConsumer", + "deleted": false + } + ], + "teams": [ + { + "id": "7e6d5c4b-3a2f-1e0d-9c8b-7a6f5e4d3c2b", + "type": "team", + "name": "Data", + "fullyQualifiedName": "Data", + "deleted": false + } + ], + "isEmailVerified": true, + "profile": { + "images": { + "image": "https://example.com/images/users/sample-user.jpg", + "image192": "https://example.com/images/users/sample-user-192.jpg", + "image512": "https://example.com/images/users/sample-user-512.jpg" + } + }, + "personas": [ + { + "id": "d4e5f6a7-8901-2345-6789-0abcdef12345", + "type": "persona", + "name": "DataAnalyst", + "fullyQualifiedName": "DataAnalyst" + } + ], + "defaultPersona": { + "id": "d4e5f6a7-8901-2345-6789-0abcdef12345", + "type": "persona", + "name": "DataAnalyst", + "fullyQualifiedName": "DataAnalyst" + }, + "domains": [ + { + "id": "c3d4e5f6-7890-abcd-ef12-34567890abcd", + "type": "domain", + "name": "Engineering", + "fullyQualifiedName": "Engineering" + } + ], + "personaPreferences": { + "landingPage": "myData", + "assistantEnabled": true + } +} \ No newline at end of file diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/events/EventSubscriptionResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/events/EventSubscriptionResourceTest.java index 3b9f8fee8c1..ea813a4d30f 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/events/EventSubscriptionResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/events/EventSubscriptionResourceTest.java @@ -2539,32 +2539,6 @@ public class EventSubscriptionResourceTest templateTest.deleteEntity(template.getId(), ADMIN_AUTH_HEADERS); } - @Test - void test_deleteTemplatePreservesSubscription(TestInfo test) throws IOException { - NotificationTemplateResourceTest templateTest = new NotificationTemplateResourceTest(); - CreateNotificationTemplate createTemplate = - templateTest - .createRequest("template-to-delete-" + test.getDisplayName()) - .withTemplateBody("
Will be deleted
"); - NotificationTemplate template = templateTest.createEntity(createTemplate, ADMIN_AUTH_HEADERS); - - EntityReference templateRef = - new EntityReference().withId(template.getId()).withType(Entity.NOTIFICATION_TEMPLATE); - - CreateEventSubscription createSub = - createRequest("sub-survives-" + test.getDisplayName()) - .withNotificationTemplate(templateRef); - EventSubscription subscription = createEntity(createSub, ADMIN_AUTH_HEADERS); - - templateTest.deleteEntity(template.getId(), ADMIN_AUTH_HEADERS); - - EventSubscription afterDelete = getEntity(subscription.getId(), ADMIN_AUTH_HEADERS); - assertNotNull(afterDelete, "Subscription should exist after template deletion"); - assertNull(afterDelete.getNotificationTemplate(), "Template reference should be null"); - - deleteEntity(subscription.getId(), ADMIN_AUTH_HEADERS); - } - @Test void test_querySubscriptionsByTemplate(TestInfo test) throws IOException { NotificationTemplateResourceTest templateTest = new NotificationTemplateResourceTest(); @@ -2624,67 +2598,4 @@ public class EventSubscriptionResourceTest templateTest.deleteEntity(template1.getId(), ADMIN_AUTH_HEADERS); templateTest.deleteEntity(template2.getId(), ADMIN_AUTH_HEADERS); } - - @Test - void test_compositeFlow_TemplateLifecycle(TestInfo test) throws IOException { - NotificationTemplateResourceTest templateTest = new NotificationTemplateResourceTest(); - - CreateNotificationTemplate createTemplate1 = - templateTest - .createRequest("composite-template1-" + test.getDisplayName()) - .withTemplateBody("
Composite Template 1
"); - NotificationTemplate template1 = templateTest.createEntity(createTemplate1, ADMIN_AUTH_HEADERS); - - EntityReference template1Ref = - new EntityReference().withId(template1.getId()).withType(Entity.NOTIFICATION_TEMPLATE); - - CreateEventSubscription createSub1 = - createRequest("composite-sub1-" + test.getDisplayName()) - .withNotificationTemplate(template1Ref); - EventSubscription sub1 = createEntity(createSub1, ADMIN_AUTH_HEADERS); - - CreateEventSubscription createSub2 = - createRequest("composite-sub2-" + test.getDisplayName()) - .withNotificationTemplate(template1Ref); - EventSubscription sub2 = createEntity(createSub2, ADMIN_AUTH_HEADERS); - - Map params = new HashMap<>(); - params.put("notificationTemplate", template1.getId().toString()); - ResultList results = listEntities(params, ADMIN_AUTH_HEADERS); - assertEquals(2, results.getData().size(), "Should have 2 subscriptions with template1"); - - CreateNotificationTemplate createTemplate2 = - templateTest - .createRequest("composite-template2-" + test.getDisplayName()) - .withTemplateBody("
Composite Template 2
"); - NotificationTemplate template2 = templateTest.createEntity(createTemplate2, ADMIN_AUTH_HEADERS); - - EntityReference template2Ref = - new EntityReference().withId(template2.getId()).withType(Entity.NOTIFICATION_TEMPLATE); - - updateEntity(createSub1.withNotificationTemplate(template2Ref), OK, ADMIN_AUTH_HEADERS); - - params.put("notificationTemplate", template1.getId().toString()); - results = listEntities(params, ADMIN_AUTH_HEADERS); - assertEquals(1, results.getData().size(), "Should have 1 subscription with template1"); - - params.put("notificationTemplate", template2.getId().toString()); - results = listEntities(params, ADMIN_AUTH_HEADERS); - assertEquals(1, results.getData().size(), "Should have 1 subscription with template2"); - - deleteEntity(sub2.getId(), ADMIN_AUTH_HEADERS); - - params.put("notificationTemplate", template1.getId().toString()); - results = listEntities(params, ADMIN_AUTH_HEADERS); - assertEquals(0, results.getData().size(), "Should have 0 subscriptions with template1"); - - templateTest.deleteEntity(template1.getId(), ADMIN_AUTH_HEADERS); - - EventSubscription sub1After = getEntity(sub1.getId(), ADMIN_AUTH_HEADERS); - assertNotNull(sub1After, "Subscription should still exist"); - assertEquals(template2.getId(), sub1After.getNotificationTemplate().getId()); - - deleteEntity(sub1.getId(), ADMIN_AUTH_HEADERS); - templateTest.deleteEntity(template2.getId(), ADMIN_AUTH_HEADERS); - } } diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/events/NotificationTemplateResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/events/NotificationTemplateResourceTest.java index 5c3f5897475..f32c06b88de 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/events/NotificationTemplateResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/events/NotificationTemplateResourceTest.java @@ -22,6 +22,7 @@ import static jakarta.ws.rs.core.Response.Status.OK; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.openmetadata.service.util.EntityUtil.fieldUpdated; import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS; @@ -34,8 +35,10 @@ import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.io.IOException; +import java.net.URI; import java.net.URISyntaxException; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -45,12 +48,21 @@ import org.apache.http.client.HttpResponseException; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; +import org.openmetadata.schema.alert.type.EmailAlertConfig; +import org.openmetadata.schema.api.events.CreateEventSubscription; import org.openmetadata.schema.api.events.CreateNotificationTemplate; +import org.openmetadata.schema.api.events.NotificationTemplateRenderRequest; +import org.openmetadata.schema.api.events.NotificationTemplateRenderResponse; +import org.openmetadata.schema.api.events.NotificationTemplateSendRequest; import org.openmetadata.schema.api.events.NotificationTemplateValidationRequest; import org.openmetadata.schema.api.events.NotificationTemplateValidationResponse; +import org.openmetadata.schema.entity.events.EventSubscription; import org.openmetadata.schema.entity.events.NotificationTemplate; +import org.openmetadata.schema.entity.events.SubscriptionDestination; import org.openmetadata.schema.type.ChangeDescription; +import org.openmetadata.schema.type.EntityReference; import org.openmetadata.schema.type.ProviderType; +import org.openmetadata.schema.type.Webhook; import org.openmetadata.schema.utils.JsonUtils; import org.openmetadata.schema.utils.ResultList; import org.openmetadata.service.Entity; @@ -239,6 +251,175 @@ public class NotificationTemplateResourceTest authHeaders); } + public final NotificationTemplateRenderResponse renderTemplate( + NotificationTemplateRenderRequest request, Map authHeaders) + throws HttpResponseException { + WebTarget target = getCollection().path("/render"); + return TestUtils.post( + target, request, NotificationTemplateRenderResponse.class, OK.getStatusCode(), authHeaders); + } + + public final NotificationTemplateValidationResponse sendTemplate( + NotificationTemplateSendRequest request, Map authHeaders) + throws HttpResponseException { + WebTarget target = getCollection().path("/send"); + return TestUtils.post( + target, + request, + NotificationTemplateValidationResponse.class, + OK.getStatusCode(), + authHeaders); + } + + public final Response sendTemplateRaw( + NotificationTemplateSendRequest request, Map authHeaders) { + WebTarget target = getCollection().path("/send"); + return SecurityUtil.addHeaders(target, authHeaders) + .post(entity(request, MediaType.APPLICATION_JSON)); + } + + private SubscriptionDestination createEmailDestination(String... receivers) { + EmailAlertConfig config = new EmailAlertConfig().withReceivers(Set.of(receivers)); + return new SubscriptionDestination() + .withType(SubscriptionDestination.SubscriptionType.EMAIL) + .withCategory(SubscriptionDestination.SubscriptionCategory.EXTERNAL) + .withConfig(JsonUtils.valueToTree(config)); + } + + private SubscriptionDestination createWebhookDestination( + SubscriptionDestination.SubscriptionType type, String endpoint) { + Webhook config = new Webhook().withEndpoint(URI.create(endpoint)); + return new SubscriptionDestination() + .withType(type) + .withCategory(SubscriptionDestination.SubscriptionCategory.EXTERNAL) + .withConfig(JsonUtils.valueToTree(config)) + .withTimeout(5000) + .withReadTimeout(5000); + } + + private SubscriptionDestination createInternalDestination( + SubscriptionDestination.SubscriptionType type) { + return new SubscriptionDestination() + .withType(type) + .withCategory(SubscriptionDestination.SubscriptionCategory.TEAMS); + } + + private NotificationTemplateSendRequest createSendRequest( + String templateSubject, + String templateBody, + String resource, + SubscriptionDestination... destinations) { + NotificationTemplateRenderRequest renderRequest = + new NotificationTemplateRenderRequest() + .withTemplateSubject(templateSubject) + .withTemplateBody(templateBody) + .withResource(resource); + + return new NotificationTemplateSendRequest() + .withRenderRequest(renderRequest) + .withDestinations(List.of(destinations)); + } + + /** + * Helper method to build the expected email with the full envelope structure. + * This wraps the provided content in the complete OpenMetadata email envelope. + * + * @param content The notification content (without envelope) + * @return Complete HTML email with envelope + */ + private String createExpectedEmailWithEnvelope(String content) { + // Build the complete HTML envelope that matches the system template + String envelopeHtml = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "OpenMetadata · Change Event" + + "" + + "" + + "" + + "" + + "
Fresh activity spotted in your OpenMetadata environment.
" + + "
" + + "" + + "" + + "
" + + "" + + "" + + "" + + "" + + "" + + "
" + + "" + + "" + + "\"OpenMetadata" + + "" + + "
" + + "" + + "
" + + "Heads up! Some fresh updates just landed in your OpenMetadata environment. Take a quick look at what's new below:" + + "
" + + "" + + "" + + "
 " + + "" + + "
" + + "
" + + content + + "
" + + "
" + + "" + + "
" + + "Happy exploring!
Thanks,
OpenMetadata Team" + + "
" + + "" + + "" + + "
" + + "You're receiving this message as part of your OpenMetadata change notification subscription." + + "
" + + "Need a hand? The OpenMetadata docs and community are ready to help." + + "
" + + "" + + "
" + + "" + + "
"; + return envelopeHtml; + } + + private void assertRenderEquals( + String expectedSubject, + String expectedHtmlBody, + NotificationTemplateRenderResponse response) { + assertNotNull(response); + assertNotNull(response.getValidation()); + assertTrue(response.getValidation().getIsValid(), "Validation should pass"); + assertNotNull(response.getRender()); + + assertEquals(expectedSubject, response.getRender().getSubject(), "Subject should match"); + assertEquals(expectedHtmlBody, response.getRender().getBody(), "HTML body should match"); + } + @Test void post_validNotificationTemplate_200(TestInfo test) throws IOException { CreateNotificationTemplate create = @@ -768,10 +949,8 @@ public class NotificationTemplateResourceTest NotificationTemplateValidationResponse validationResponse = validateTemplate(request, ADMIN_AUTH_HEADERS); - assertNotNull(validationResponse.getTemplateBody()); - assertTrue(validationResponse.getTemplateBody().getPassed()); - assertNotNull(validationResponse.getTemplateSubject()); - assertTrue(validationResponse.getTemplateSubject().getPassed()); + assertTrue(validationResponse.getIsValid(), "Validation should pass"); + assertNotNull(validationResponse); } @Test @@ -785,19 +964,15 @@ public class NotificationTemplateResourceTest NotificationTemplateValidationResponse validationResponse = validateTemplate(request, ADMIN_AUTH_HEADERS); - // Template body should fail validation - assertNotNull(validationResponse.getTemplateBody()); - assertFalse(validationResponse.getTemplateBody().getPassed()); - assertNotNull(validationResponse.getTemplateBody().getError()); + assertFalse(validationResponse.getIsValid(), "Validation should fail"); + assertNotNull(validationResponse.getBodyError()); assertTrue( - validationResponse.getTemplateBody().getError().contains("Template validation failed")); - - // Template subject should also fail validation - assertNotNull(validationResponse.getTemplateSubject()); - assertFalse(validationResponse.getTemplateSubject().getPassed()); - assertNotNull(validationResponse.getTemplateSubject().getError()); + validationResponse.getBodyError().contains("Template validation failed"), + "Body error should contain validation message"); + assertNotNull(validationResponse.getSubjectError()); assertTrue( - validationResponse.getTemplateSubject().getError().contains("Template validation failed")); + validationResponse.getSubjectError().contains("Template validation failed"), + "Subject error should contain validation message"); } @Test @@ -827,4 +1002,297 @@ public class NotificationTemplateResourceTest .post(entity(request, MediaType.APPLICATION_JSON)); assertEquals(FORBIDDEN.getStatusCode(), response.getStatus()); } + + @Test + void test_renderTemplate_table_200() throws HttpResponseException { + NotificationTemplateRenderRequest request = + new NotificationTemplateRenderRequest() + .withTemplateSubject("Table Update: {{entity.name}}") + .withTemplateBody( + "

Table {{entity.name}} has been updated

" + + "

FQN: {{entity.fullyQualifiedName}}

" + + "

Description: {{entity.description}}

" + + "
    " + + "{{#each entity.columns as |column|}}" + + "
  • {{column.name}} - {{column.dataType}}
  • " + + "{{/each}}" + + "
") + .withResource("table"); + + NotificationTemplateRenderResponse response = renderTemplate(request, ADMIN_AUTH_HEADERS); + + String expectedSubject = "Table Update: sample_table"; + String expectedContent = + "

Table sample_table has been updated

" + + "

FQN: sample-service.sample-db.public.sample_table

" + + "

Description: Mock table for notification template testing

" + + "
    " + + "
  • id - BIGINT
  • " + + "
  • name - VARCHAR
  • " + + "
  • email - VARCHAR
  • " + + "
"; + + String expectedBody = createExpectedEmailWithEnvelope(expectedContent); + + assertRenderEquals(expectedSubject, expectedBody, response); + } + + @Test + void test_renderTemplate_dashboard_200() throws HttpResponseException { + NotificationTemplateRenderRequest request = + new NotificationTemplateRenderRequest() + .withTemplateSubject("Dashboard Update: {{entity.displayName}}") + .withTemplateBody( + "

Dashboard {{entity.displayName}}

" + + "

Name: {{entity.name}}

" + + "

FQN: {{entity.fullyQualifiedName}}

" + + "{{#if entity.description}}" + + "

Description: {{entity.description}}

" + + "{{/if}}") + .withResource("dashboard"); + + NotificationTemplateRenderResponse response = renderTemplate(request, ADMIN_AUTH_HEADERS); + + String expectedSubject = "Dashboard Update: Sample Dashboard"; + String expectedContent = + "

Dashboard Sample Dashboard

" + + "

Name: sample_dashboard

" + + "

FQN: sample-dashboard-service.sample_dashboard

" + + "

Description: Mock dashboard for notification template testing

"; + + String expectedBody = createExpectedEmailWithEnvelope(expectedContent); + + assertRenderEquals(expectedSubject, expectedBody, response); + } + + @Test + void test_renderTemplate_unknownResource_usesGenericFixture() throws HttpResponseException { + // Unknown resources now use generic EntityInterface fixture instead of throwing errors + NotificationTemplateRenderRequest request = + new NotificationTemplateRenderRequest() + .withTemplateSubject("Unknown Entity: {{entity.name}}") + .withTemplateBody( + "

Entity {{entity.name}}

" + + "

FQN: {{entity.fullyQualifiedName}}

" + + "

Description: {{entity.description}}

") + .withResource("unknown-resource-type"); + + NotificationTemplateRenderResponse response = renderTemplate(request, ADMIN_AUTH_HEADERS); + + // Should succeed with generic fixture values + assertNotNull(response); + assertNotNull(response.getValidation()); + assertTrue(response.getValidation().getIsValid(), "Validation should pass"); + assertNotNull(response.getRender()); + + String expectedSubject = "Unknown Entity: generic_entity"; + assertEquals(expectedSubject, response.getRender().getSubject()); + + // Body should contain the generic fixture values + assertTrue(response.getRender().getBody().contains("generic_entity")); + assertTrue(response.getRender().getBody().contains("generic.entity")); + assertTrue( + response + .getRender() + .getBody() + .contains("Generic entity for notification template testing")); + } + + @Test + void test_renderTemplate_allResource_usesGenericFixture() throws HttpResponseException { + // "all" resource now uses generic EntityInterface fixture instead of throwing errors + NotificationTemplateRenderRequest request = + new NotificationTemplateRenderRequest() + .withTemplateSubject("All Entities: {{entity.name}}") + .withTemplateBody( + "

Entity {{entity.name}}

" + "

Display Name: {{entity.displayName}}

") + .withResource("all"); + + NotificationTemplateRenderResponse response = renderTemplate(request, ADMIN_AUTH_HEADERS); + + // Should succeed with generic fixture values + assertNotNull(response); + assertNotNull(response.getValidation()); + assertTrue(response.getValidation().getIsValid(), "Validation should pass"); + assertNotNull(response.getRender()); + + String expectedSubject = "All Entities: generic_entity"; + assertEquals(expectedSubject, response.getRender().getSubject()); + + // Body should contain the generic fixture values + assertTrue(response.getRender().getBody().contains("Generic Entity")); + } + + @Test + void test_renderTemplate_invalidSyntax_validationFailure() throws HttpResponseException { + NotificationTemplateRenderRequest request = + new NotificationTemplateRenderRequest() + .withTemplateSubject("Valid Subject") + .withTemplateBody("{{#if entity.name}} Missing end if tag") + .withResource("table"); + + NotificationTemplateRenderResponse response = renderTemplate(request, ADMIN_AUTH_HEADERS); + + assertNotNull(response); + assertNotNull(response.getValidation()); + assertFalse(response.getValidation().getIsValid(), "Validation should fail"); + assertNotNull(response.getValidation().getBodyError()); + assertTrue( + response.getValidation().getBodyError().contains("Template validation failed"), + "Body error should contain validation message"); + } + + @Test + void test_renderTemplate_403_forbidden() { + NotificationTemplateRenderRequest request = + new NotificationTemplateRenderRequest() + .withTemplateSubject("Table Update: {{entity.name}}") + .withTemplateBody("

{{entity.name}}

") + .withResource("table"); + + WebTarget target = getCollection().path("/render"); + Response response = + SecurityUtil.addHeaders(target, TEST_AUTH_HEADERS) + .post(entity(request, MediaType.APPLICATION_JSON)); + assertEquals(FORBIDDEN.getStatusCode(), response.getStatus()); + } + + @Test + void test_sendTemplate_emailDestination_200() throws HttpResponseException { + SubscriptionDestination emailDest = createEmailDestination("test@example.com"); + + NotificationTemplateSendRequest request = + createSendRequest( + "Test Email: {{entity.name}}", + "

Entity {{entity.name}} was updated

", + "table", + emailDest); + + NotificationTemplateValidationResponse response = sendTemplate(request, ADMIN_AUTH_HEADERS); + + assertNotNull(response); + assertTrue(response.getIsValid(), "Validation should pass"); + } + + @Test + void test_sendTemplate_webhookDestination_200() throws HttpResponseException { + SubscriptionDestination webhookDest = + createWebhookDestination( + SubscriptionDestination.SubscriptionType.SLACK, "http://localhost:9999/webhook"); + + NotificationTemplateSendRequest request = + createSendRequest( + "Test Webhook: {{entity.name}}", + "

Entity {{entity.name}} was updated

", + "table", + webhookDest); + + NotificationTemplateValidationResponse response = sendTemplate(request, ADMIN_AUTH_HEADERS); + + assertNotNull(response); + assertTrue(response.getIsValid(), "Validation should pass"); + } + + @Test + void test_sendTemplate_multipleDestinations_200() throws HttpResponseException { + SubscriptionDestination emailDest = createEmailDestination("test@example.com"); + SubscriptionDestination webhookDest = + createWebhookDestination( + SubscriptionDestination.SubscriptionType.SLACK, "http://localhost:9999/webhook"); + + NotificationTemplateSendRequest request = + createSendRequest( + "Test Multiple: {{entity.name}}", + "

Entity {{entity.name}} was updated

", + "table", + emailDest, + webhookDest); + + NotificationTemplateValidationResponse response = sendTemplate(request, ADMIN_AUTH_HEADERS); + + assertNotNull(response); + assertTrue(response.getIsValid(), "Validation should pass"); + } + + @Test + void test_sendTemplate_internalDestination_400() { + SubscriptionDestination internalDest = + createInternalDestination(SubscriptionDestination.SubscriptionType.EMAIL); + + NotificationTemplateSendRequest request = + createSendRequest( + "Test Internal: {{entity.name}}", + "

Entity {{entity.name}} was updated

", + "table", + internalDest); + + Response response = sendTemplateRaw(request, ADMIN_AUTH_HEADERS); + assertEquals(BAD_REQUEST.getStatusCode(), response.getStatus()); + + String responseBody = response.readEntity(String.class); + assertTrue( + responseBody.contains("Only external destinations"), + "Error message should indicate only external destinations are supported"); + } + + @Test + void test_sendTemplate_invalidTemplate_validationFailure() throws HttpResponseException { + SubscriptionDestination emailDest = createEmailDestination("test@example.com"); + + NotificationTemplateSendRequest request = + createSendRequest( + "Valid Subject", "{{#if entity.name}} Missing end if tag", "table", emailDest); + + NotificationTemplateValidationResponse response = sendTemplate(request, ADMIN_AUTH_HEADERS); + + assertNotNull(response); + assertFalse(response.getIsValid(), "Validation should fail"); + assertNotNull(response.getBodyError()); + assertTrue( + response.getBodyError().contains("Template validation failed"), + "Body error should contain validation message"); + } + + @Test + void test_sendTemplate_403_forbidden() { + SubscriptionDestination emailDest = createEmailDestination("test@example.com"); + + NotificationTemplateSendRequest request = + createSendRequest( + "Test Email: {{entity.name}}", + "

Entity {{entity.name}} was updated

", + "table", + emailDest); + + Response response = sendTemplateRaw(request, TEST_AUTH_HEADERS); + assertEquals(FORBIDDEN.getStatusCode(), response.getStatus()); + } + + @Test + void test_deleteTemplatePreservesSubscription(TestInfo test) throws IOException { + EventSubscriptionResourceTest subscriptionTest = new EventSubscriptionResourceTest(); + + CreateNotificationTemplate createTemplate = + createRequest("template-to-delete-" + test.getDisplayName()) + .withTemplateBody("
Will be deleted
"); + NotificationTemplate template = createEntity(createTemplate, ADMIN_AUTH_HEADERS); + + EntityReference templateRef = + new EntityReference().withId(template.getId()).withType(Entity.NOTIFICATION_TEMPLATE); + + CreateEventSubscription createSub = + subscriptionTest + .createRequest("sub-survives-" + test.getDisplayName()) + .withNotificationTemplate(templateRef); + EventSubscription subscription = subscriptionTest.createEntity(createSub, ADMIN_AUTH_HEADERS); + + deleteEntity(template.getId(), ADMIN_AUTH_HEADERS); + + EventSubscription afterDelete = + subscriptionTest.getEntity(subscription.getId(), ADMIN_AUTH_HEADERS); + assertNotNull(afterDelete, "Subscription should exist after template deletion"); + assertNull(afterDelete.getNotificationTemplate(), "Template reference should be null"); + + subscriptionTest.deleteEntity(subscription.getId(), ADMIN_AUTH_HEADERS); + } } diff --git a/openmetadata-spec/src/main/resources/json/schema/api/events/notificationTemplateRenderRequest.json b/openmetadata-spec/src/main/resources/json/schema/api/events/notificationTemplateRenderRequest.json new file mode 100644 index 00000000000..57d4558f045 --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/api/events/notificationTemplateRenderRequest.json @@ -0,0 +1,34 @@ +{ + "$id": "https://open-metadata.org/schema/api/events/notificationTemplateRenderRequest.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NotificationTemplateRenderRequest", + "description": "Request to render a notification template with mock data for preview.", + "type": "object", + "javaType": "org.openmetadata.schema.api.events.NotificationTemplateRenderRequest", + "properties": { + "templateSubject": { + "description": "Handlebars template for notification subject line", + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "templateBody": { + "description": "Handlebars template content for rendering notifications", + "type": "string", + "minLength": 1, + "maxLength": 10240 + }, + "resource": { + "type": "string", + "description": "Entity type to mock (e.g., 'table', 'dashboard', 'task'). Falls back to generic fixture if not found.", + "minLength": 1, + "maxLength": 128 + }, + "eventType": { + "$ref": "../../type/changeEventType.json", + "description": "Optional. Specific event type to test (e.g., 'entityUpdated'). When omitted, generates a neutral ChangeEvent with no event-specific fields." + } + }, + "required": ["templateSubject", "templateBody", "resource"], + "additionalProperties": false +} \ No newline at end of file diff --git a/openmetadata-spec/src/main/resources/json/schema/api/events/notificationTemplateRenderResponse.json b/openmetadata-spec/src/main/resources/json/schema/api/events/notificationTemplateRenderResponse.json new file mode 100644 index 00000000000..c9aef98a5e1 --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/api/events/notificationTemplateRenderResponse.json @@ -0,0 +1,39 @@ +{ + "$id": "https://open-metadata.org/schema/api/events/notificationTemplateRenderResponse.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NotificationTemplateRenderResponse", + "description": "Response from rendering a notification template with mock data, including validation and rendered output.", + "type": "object", + "javaType": "org.openmetadata.schema.api.events.NotificationTemplateRenderResponse", + "definitions": { + "templateRenderResult": { + "type": "object", + "description": "Rendered template output with subject and body strings.", + "javaType": "org.openmetadata.schema.api.events.TemplateRenderResult", + "properties": { + "subject": { + "type": "string", + "description": "Rendered template subject." + }, + "body": { + "type": "string", + "description": "Rendered template body." + } + }, + "required": ["subject", "body"], + "additionalProperties": false + } + }, + "properties": { + "validation": { + "$ref": "./notificationTemplateValidationResponse.json", + "description": "Syntax validation results for the template." + }, + "render": { + "$ref": "#/definitions/templateRenderResult", + "description": "Actual rendering results with mock data. Null if validation failed." + } + }, + "required": ["validation"], + "additionalProperties": false +} \ No newline at end of file diff --git a/openmetadata-spec/src/main/resources/json/schema/api/events/notificationTemplateSendRequest.json b/openmetadata-spec/src/main/resources/json/schema/api/events/notificationTemplateSendRequest.json new file mode 100644 index 00000000000..c7357bb8aa8 --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/api/events/notificationTemplateSendRequest.json @@ -0,0 +1,24 @@ +{ + "$id": "https://open-metadata.org/schema/api/events/notificationTemplateSendRequest.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NotificationTemplateSendRequest", + "description": "Request to render and send a notification template with mock data to external destinations.", + "type": "object", + "javaType": "org.openmetadata.schema.api.events.NotificationTemplateSendRequest", + "properties": { + "renderRequest": { + "$ref": "./notificationTemplateRenderRequest.json", + "description": "Template rendering parameters (template content, resource, eventType)" + }, + "destinations": { + "type": "array", + "description": "External-only destinations for sending test notifications (Slack, Teams, GChat, Email, Webhook).", + "items": { + "$ref": "../../events/eventSubscription.json#/definitions/destination" + }, + "minItems": 1 + } + }, + "required": ["renderRequest", "destinations"], + "additionalProperties": false +} \ No newline at end of file diff --git a/openmetadata-spec/src/main/resources/json/schema/api/events/notificationTemplateValidationResponse.json b/openmetadata-spec/src/main/resources/json/schema/api/events/notificationTemplateValidationResponse.json index 618aa64f5ca..c507a2fef5f 100644 --- a/openmetadata-spec/src/main/resources/json/schema/api/events/notificationTemplateValidationResponse.json +++ b/openmetadata-spec/src/main/resources/json/schema/api/events/notificationTemplateValidationResponse.json @@ -5,35 +5,20 @@ "description": "Response from notification template validation", "type": "object", "javaType": "org.openmetadata.schema.api.events.NotificationTemplateValidationResponse", - "definitions": { - "fieldValidation": { - "description": "Validation result for a template field", - "type": "object", - "javaType": "org.openmetadata.schema.api.events.FieldValidation", - "properties": { - "passed": { - "description": "Whether the field validation passed", - "type": "boolean" - }, - "error": { - "description": "Error message if validation failed", - "type": "string", - "default": null - } - }, - "required": ["passed"], - "additionalProperties": false - } - }, "properties": { - "templateBody": { - "description": "Validation result for template body", - "$ref": "#/definitions/fieldValidation" + "isValid": { + "description": "Whether both subject and body passed validation", + "type": "boolean" }, - "templateSubject": { - "description": "Validation result for template subject", - "$ref": "#/definitions/fieldValidation" + "subjectError": { + "description": "Error message if subject validation failed, null otherwise", + "type": ["string", "null"] + }, + "bodyError": { + "description": "Error message if body validation failed, null otherwise", + "type": ["string", "null"] } }, + "required": ["isValid"], "additionalProperties": false } \ No newline at end of file diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/api/events/notificationTemplateRenderRequest.ts b/openmetadata-ui/src/main/resources/ui/src/generated/api/events/notificationTemplateRenderRequest.ts new file mode 100644 index 00000000000..395d31f37ef --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/generated/api/events/notificationTemplateRenderRequest.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2025 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Request to render a notification template with mock data for preview. + */ +export interface NotificationTemplateRenderRequest { + /** + * Optional. Specific event type to test (e.g., 'entityUpdated'). When omitted, generates a + * neutral ChangeEvent with no event-specific fields. + */ + eventType?: EventType; + /** + * Entity type to mock (e.g., 'table', 'dashboard', 'task'). Falls back to generic fixture + * if not found. + */ + resource: string; + /** + * Handlebars template content for rendering notifications + */ + templateBody: string; + /** + * Handlebars template for notification subject line + */ + templateSubject: string; +} + +/** + * Optional. Specific event type to test (e.g., 'entityUpdated'). When omitted, generates a + * neutral ChangeEvent with no event-specific fields. + * + * Type of event. + */ +export enum EventType { + EntityCreated = "entityCreated", + EntityDeleted = "entityDeleted", + EntityFieldsChanged = "entityFieldsChanged", + EntityNoChange = "entityNoChange", + EntityRestored = "entityRestored", + EntitySoftDeleted = "entitySoftDeleted", + EntityUpdated = "entityUpdated", + LogicalTestCaseAdded = "logicalTestCaseAdded", + PostCreated = "postCreated", + PostUpdated = "postUpdated", + SuggestionAccepted = "suggestionAccepted", + SuggestionCreated = "suggestionCreated", + SuggestionDeleted = "suggestionDeleted", + SuggestionRejected = "suggestionRejected", + SuggestionUpdated = "suggestionUpdated", + TaskClosed = "taskClosed", + TaskResolved = "taskResolved", + ThreadCreated = "threadCreated", + ThreadUpdated = "threadUpdated", +} diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/api/events/notificationTemplateRenderResponse.ts b/openmetadata-ui/src/main/resources/ui/src/generated/api/events/notificationTemplateRenderResponse.ts new file mode 100644 index 00000000000..2898ac84ef8 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/generated/api/events/notificationTemplateRenderResponse.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2025 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Response from rendering a notification template with mock data, including validation and + * rendered output. + */ +export interface NotificationTemplateRenderResponse { + /** + * Actual rendering results with mock data. Null if validation failed. + */ + render?: TemplateRenderResult; + /** + * Syntax validation results for the template. + */ + validation: NotificationTemplateValidationResponse; +} + +/** + * Actual rendering results with mock data. Null if validation failed. + * + * Rendered template output with subject and body strings. + */ +export interface TemplateRenderResult { + /** + * Rendered template body. + */ + body: string; + /** + * Rendered template subject. + */ + subject: string; +} + +/** + * Syntax validation results for the template. + * + * Response from notification template validation + */ +export interface NotificationTemplateValidationResponse { + /** + * Error message if body validation failed, null otherwise + */ + bodyError?: null | string; + /** + * Whether both subject and body passed validation + */ + isValid: boolean; + /** + * Error message if subject validation failed, null otherwise + */ + subjectError?: null | string; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/api/events/notificationTemplateSendRequest.ts b/openmetadata-ui/src/main/resources/ui/src/generated/api/events/notificationTemplateSendRequest.ts new file mode 100644 index 00000000000..a9778132f6e --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/generated/api/events/notificationTemplateSendRequest.ts @@ -0,0 +1,305 @@ +/* + * Copyright 2025 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Request to render and send a notification template with mock data to external + * destinations. + */ +export interface NotificationTemplateSendRequest { + /** + * External-only destinations for sending test notifications (Slack, Teams, GChat, Email, + * Webhook). + */ + destinations: Destination[]; + /** + * Template rendering parameters (template content, resource, eventType) + */ + renderRequest: NotificationTemplateRenderRequest; +} + +/** + * Subscription which has a type and the config. + */ +export interface Destination { + category: SubscriptionCategory; + config?: Webhook; + /** + * Maximum depth for downstream stakeholder notification traversal. If null, traverses + * without depth limit (with cycle protection). + */ + downstreamDepth?: number | null; + /** + * Is the subscription enabled. + */ + enabled?: boolean; + /** + * Unique identifier that identifies this Event Subscription. + */ + id?: string; + /** + * Enable notification of downstream entity stakeholders. When true, notifications will + * traverse lineage to include stakeholders of entities that consume data from the affected + * entity. + */ + notifyDownstream?: boolean; + /** + * Read timeout in seconds. (Default 12s). + */ + readTimeout?: number; + statusDetails?: TionStatus; + /** + * Connection timeout in seconds. (Default 10s). + */ + timeout?: number; + type: SubscriptionType; +} + +/** + * Subscription Endpoint Type. + */ +export enum SubscriptionCategory { + Admins = "Admins", + Assignees = "Assignees", + External = "External", + Followers = "Followers", + Mentions = "Mentions", + Owners = "Owners", + Teams = "Teams", + Users = "Users", +} + +/** + * This schema defines webhook for receiving events from OpenMetadata. + * + * This schema defines email config for receiving events from OpenMetadata. + * + * A generic map that can be deserialized later. + */ +export interface Webhook { + /** + * Endpoint to receive the webhook events over POST requests. + */ + endpoint?: string; + /** + * Custom headers to be sent with the webhook request. + */ + headers?: { [key: string]: any }; + /** + * HTTP operation to send the webhook request. Supports POST or PUT. + */ + httpMethod?: HTTPMethod; + /** + * Query parameters to be added to the webhook request URL. + */ + queryParams?: { [key: string]: any }; + /** + * List of receivers to send mail to + */ + receivers?: string[]; + /** + * Secret set by the webhook client used for computing HMAC SHA256 signature of webhook + * payload and sent in `X-OM-Signature` header in POST requests to publish the events. + */ + secretKey?: string; + /** + * Send the Event to Admins + * + * Send the Mails to Admins + */ + sendToAdmins?: boolean; + /** + * Send the Event to Followers + * + * Send the Mails to Followers + */ + sendToFollowers?: boolean; + /** + * Send the Event to Owners + * + * Send the Mails to Owners + */ + sendToOwners?: boolean; + [property: string]: any; +} + +/** + * HTTP operation to send the webhook request. Supports POST or PUT. + */ +export enum HTTPMethod { + Post = "POST", + Put = "PUT", +} + +/** + * Current status of the subscription, including details on the last successful and failed + * attempts, and retry information. + * + * Detailed status of the destination during a test operation, including HTTP response + * information. + */ +export interface TionStatus { + /** + * Timestamp of the last failed callback in UNIX UTC epoch time in milliseconds. + */ + lastFailedAt?: number; + /** + * Detailed reason for the last failure received during callback. + */ + lastFailedReason?: string; + /** + * HTTP status code received during the last failed callback attempt. + */ + lastFailedStatusCode?: number; + /** + * Timestamp of the last successful callback in UNIX UTC epoch time in milliseconds. + */ + lastSuccessfulAt?: number; + /** + * Timestamp for the next retry attempt in UNIX epoch time in milliseconds. Only valid if + * `status` is `awaitingRetry`. + */ + nextAttempt?: number; + /** + * Status is `disabled` when the event subscription was created with `enabled` set to false + * and it never started publishing events. Status is `active` when the event subscription is + * functioning normally and a 200 OK response was received for the callback notification. + * Status is `failed` when a bad callback URL, connection failures, or `1xx` or `3xx` + * response was received for the callback notification. Status is `awaitingRetry` when the + * previous attempt at callback timed out or received a `4xx` or `5xx` response. Status is + * `retryLimitReached` after all retries fail. + * + * Overall test status, indicating if the test operation succeeded or failed. + */ + status?: Status; + /** + * Current timestamp of this status in UNIX epoch time in milliseconds. + * + * Timestamp when the response was received, in UNIX epoch time milliseconds. + */ + timestamp?: number; + /** + * Body of the HTTP response, if any, returned by the server. + */ + entity?: string; + /** + * HTTP headers returned in the response as a map of header names to values. + */ + headers?: any; + /** + * URL location if the response indicates a redirect or newly created resource. + */ + location?: string; + /** + * Media type of the response entity, if specified (e.g., application/json). + */ + mediaType?: string; + /** + * Detailed reason for failure if the test did not succeed. + */ + reason?: string; + /** + * HTTP status code of the response (e.g., 200 for OK, 404 for Not Found). + */ + statusCode?: number; + /** + * HTTP status reason phrase associated with the status code (e.g., 'Not Found'). + */ + statusInfo?: string; +} + +/** + * Status is `disabled` when the event subscription was created with `enabled` set to false + * and it never started publishing events. Status is `active` when the event subscription is + * functioning normally and a 200 OK response was received for the callback notification. + * Status is `failed` when a bad callback URL, connection failures, or `1xx` or `3xx` + * response was received for the callback notification. Status is `awaitingRetry` when the + * previous attempt at callback timed out or received a `4xx` or `5xx` response. Status is + * `retryLimitReached` after all retries fail. + * + * Overall test status, indicating if the test operation succeeded or failed. + */ +export enum Status { + Active = "active", + AwaitingRetry = "awaitingRetry", + Disabled = "disabled", + Failed = "failed", + RetryLimitReached = "retryLimitReached", + StatusFailed = "Failed", + Success = "Success", +} + +/** + * Subscription Endpoint Type. + */ +export enum SubscriptionType { + ActivityFeed = "ActivityFeed", + Email = "Email", + GChat = "GChat", + GovernanceWorkflowChangeEvent = "GovernanceWorkflowChangeEvent", + MSTeams = "MsTeams", + Slack = "Slack", + Webhook = "Webhook", +} + +/** + * Template rendering parameters (template content, resource, eventType) + * + * Request to render a notification template with mock data for preview. + */ +export interface NotificationTemplateRenderRequest { + /** + * Optional. Specific event type to test (e.g., 'entityUpdated'). When omitted, generates a + * neutral ChangeEvent with no event-specific fields. + */ + eventType?: EventType; + /** + * Entity type to mock (e.g., 'table', 'dashboard', 'task'). Falls back to generic fixture + * if not found. + */ + resource: string; + /** + * Handlebars template content for rendering notifications + */ + templateBody: string; + /** + * Handlebars template for notification subject line + */ + templateSubject: string; +} + +/** + * Optional. Specific event type to test (e.g., 'entityUpdated'). When omitted, generates a + * neutral ChangeEvent with no event-specific fields. + * + * Type of event. + */ +export enum EventType { + EntityCreated = "entityCreated", + EntityDeleted = "entityDeleted", + EntityFieldsChanged = "entityFieldsChanged", + EntityNoChange = "entityNoChange", + EntityRestored = "entityRestored", + EntitySoftDeleted = "entitySoftDeleted", + EntityUpdated = "entityUpdated", + LogicalTestCaseAdded = "logicalTestCaseAdded", + PostCreated = "postCreated", + PostUpdated = "postUpdated", + SuggestionAccepted = "suggestionAccepted", + SuggestionCreated = "suggestionCreated", + SuggestionDeleted = "suggestionDeleted", + SuggestionRejected = "suggestionRejected", + SuggestionUpdated = "suggestionUpdated", + TaskClosed = "taskClosed", + TaskResolved = "taskResolved", + ThreadCreated = "threadCreated", + ThreadUpdated = "threadUpdated", +} diff --git a/openmetadata-ui/src/main/resources/ui/src/generated/api/events/notificationTemplateValidationResponse.ts b/openmetadata-ui/src/main/resources/ui/src/generated/api/events/notificationTemplateValidationResponse.ts index 02286bb0e68..1040c0fcc72 100644 --- a/openmetadata-ui/src/main/resources/ui/src/generated/api/events/notificationTemplateValidationResponse.ts +++ b/openmetadata-ui/src/main/resources/ui/src/generated/api/events/notificationTemplateValidationResponse.ts @@ -15,29 +15,15 @@ */ export interface NotificationTemplateValidationResponse { /** - * Validation result for template body + * Error message if body validation failed, null otherwise */ - templateBody?: FieldValidation; + bodyError?: null | string; /** - * Validation result for template subject + * Whether both subject and body passed validation */ - templateSubject?: FieldValidation; -} - -/** - * Validation result for template body - * - * Validation result for a template field - * - * Validation result for template subject - */ -export interface FieldValidation { - /** - * Error message if validation failed - */ - error?: string; - /** - * Whether the field validation passed - */ - passed: boolean; + isValid: boolean; + /** + * Error message if subject validation failed, null otherwise + */ + subjectError?: null | string; }