mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-04 11:33:07 +00:00
Enable notification template preview and test send (#24229)
* Add NotificationTemplate sending and rendering endpoints with mock data * Setup entity fixtures for mock notifications * Update generated TypeScript types --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
679ad3daf8
commit
9ad6783a99
@ -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<Notificatio
|
||||
static final String UPDATE_FIELDS = "templateBody,templateSubject";
|
||||
|
||||
private final NotificationTemplateProcessor templateProcessor;
|
||||
private final MockChangeEventFactory mockChangeEventFactory;
|
||||
private final HandlebarsNotificationMessageEngine messageEngine;
|
||||
|
||||
public NotificationTemplateRepository() {
|
||||
super(
|
||||
@ -57,6 +81,14 @@ public class NotificationTemplateRepository extends EntityRepository<Notificatio
|
||||
|
||||
// Initialize template processor
|
||||
this.templateProcessor = new HandlebarsNotificationTemplateProcessor();
|
||||
|
||||
// Initialize mock factory for template testing
|
||||
EntityFixtureLoader fixtureLoader = new EntityFixtureLoader();
|
||||
MockChangeEventRegistry mockRegistry = new MockChangeEventRegistry(fixtureLoader);
|
||||
this.mockChangeEventFactory = new MockChangeEventFactory(mockRegistry);
|
||||
|
||||
// Initialize message engine for template rendering
|
||||
this.messageEngine = new HandlebarsNotificationMessageEngine(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -73,16 +105,14 @@ public class NotificationTemplateRepository extends EntityRepository<Notificatio
|
||||
|
||||
NotificationTemplateValidationResponse response = templateProcessor.validate(request);
|
||||
|
||||
// Check for validation errors
|
||||
List<String> 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<String> 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<Notificatio
|
||||
return templateProcessor.validate(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a template with mock data using HandlebarsNotificationMessageEngine.
|
||||
* Called by the REST endpoint for rendering preview.
|
||||
*
|
||||
* @param request The render request with template and resource info
|
||||
* @return The render response with validation and rendering results
|
||||
*/
|
||||
public NotificationTemplateRenderResponse render(NotificationTemplateRenderRequest request) {
|
||||
NotificationTemplateValidationResponse validationResponse =
|
||||
templateProcessor.validate(
|
||||
new NotificationTemplateValidationRequest()
|
||||
.withTemplateBody(request.getTemplateBody())
|
||||
.withTemplateSubject(request.getTemplateSubject()));
|
||||
|
||||
if (!validationResponse.getIsValid()) {
|
||||
return new NotificationTemplateRenderResponse()
|
||||
.withValidation(validationResponse)
|
||||
.withRender(null);
|
||||
}
|
||||
|
||||
ChangeEvent mockEvent =
|
||||
mockChangeEventFactory.create(request.getResource(), request.getEventType());
|
||||
|
||||
NotificationTemplate testTemplate =
|
||||
new NotificationTemplate()
|
||||
.withId(UUID.randomUUID())
|
||||
.withName("test-template")
|
||||
.withTemplateSubject(request.getTemplateSubject())
|
||||
.withTemplateBody(request.getTemplateBody());
|
||||
|
||||
EventSubscription testSubscription =
|
||||
new EventSubscription()
|
||||
.withId(UUID.randomUUID())
|
||||
.withName("test-subscription")
|
||||
.withDisplayName("Test Notification");
|
||||
|
||||
SubscriptionDestination emailDestination =
|
||||
new SubscriptionDestination().withType(SubscriptionDestination.SubscriptionType.EMAIL);
|
||||
|
||||
TemplateRenderResult renderResult =
|
||||
renderWithMessageEngine(mockEvent, testSubscription, emailDestination, testTemplate);
|
||||
|
||||
return new NotificationTemplateRenderResponse()
|
||||
.withValidation(validationResponse)
|
||||
.withRender(renderResult);
|
||||
}
|
||||
|
||||
private TemplateRenderResult renderWithMessageEngine(
|
||||
ChangeEvent event,
|
||||
EventSubscription subscription,
|
||||
SubscriptionDestination destination,
|
||||
NotificationTemplate template) {
|
||||
try {
|
||||
EmailMessage emailMessage =
|
||||
(EmailMessage)
|
||||
messageEngine.generateMessageWithTemplate(event, subscription, destination, template);
|
||||
|
||||
return new TemplateRenderResult()
|
||||
.withSubject(emailMessage.getSubject())
|
||||
.withBody(emailMessage.getHtmlContent());
|
||||
|
||||
} catch (Exception e) {
|
||||
String errorMessage = "Failed to render template: " + e.getMessage();
|
||||
LOG.error(errorMessage, e);
|
||||
return new TemplateRenderResult().withSubject("").withBody("");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and sends a template to specified destinations.
|
||||
* Called by the REST endpoint for send testing.
|
||||
*
|
||||
* @param request The send request with template, resource, eventType, and destinations
|
||||
* @return The validation response (delivery errors logged server-side only)
|
||||
*/
|
||||
public NotificationTemplateValidationResponse send(NotificationTemplateSendRequest request) {
|
||||
NotificationTemplateRenderRequest renderRequest = request.getRenderRequest();
|
||||
|
||||
NotificationTemplateValidationRequest validationRequest =
|
||||
new NotificationTemplateValidationRequest()
|
||||
.withTemplateBody(renderRequest.getTemplateBody())
|
||||
.withTemplateSubject(renderRequest.getTemplateSubject());
|
||||
|
||||
NotificationTemplateValidationResponse validation =
|
||||
templateProcessor.validate(validationRequest);
|
||||
|
||||
if (!validation.getIsValid()) {
|
||||
return validation;
|
||||
}
|
||||
|
||||
validateExternalDestinations(request.getDestinations());
|
||||
|
||||
ChangeEvent mockEvent =
|
||||
mockChangeEventFactory.create(renderRequest.getResource(), renderRequest.getEventType());
|
||||
|
||||
NotificationTemplate testTemplate =
|
||||
new NotificationTemplate()
|
||||
.withId(UUID.randomUUID())
|
||||
.withName("test-template")
|
||||
.withTemplateSubject(renderRequest.getTemplateSubject())
|
||||
.withTemplateBody(renderRequest.getTemplateBody());
|
||||
|
||||
EventSubscription testSubscription =
|
||||
new EventSubscription()
|
||||
.withId(UUID.randomUUID())
|
||||
.withName("test-notification")
|
||||
.withDisplayName("Test Notification Template");
|
||||
|
||||
for (SubscriptionDestination dest : request.getDestinations()) {
|
||||
try {
|
||||
sendToDestination(mockEvent, testSubscription, dest, testTemplate);
|
||||
LOG.info("Successfully sent test notification to {} destination", dest.getType());
|
||||
} catch (Exception e) {
|
||||
LOG.error(
|
||||
"Failed to send test notification to {} destination: {}",
|
||||
dest.getType(),
|
||||
e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
return validation;
|
||||
}
|
||||
|
||||
private NotificationTemplate getDefaultTemplateFromSeed(String fqn) throws IOException {
|
||||
String seedPath =
|
||||
ResourcePathResolver.getResourcePath(NotificationTemplateResourcePathProvider.class);
|
||||
@ -281,6 +435,62 @@ public class NotificationTemplateRepository extends EntityRepository<Notificatio
|
||||
return EntityUtil.hash(content);
|
||||
}
|
||||
|
||||
private void validateExternalDestinations(List<SubscriptionDestination> 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<String> 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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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<String, Map<String, Object>> 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<String, Object> 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<String, Object> loadBaseFixture(String resource) {
|
||||
String path = String.format(BASE_FIXTURE_PATH_FORMAT, resource);
|
||||
Map<String, Object> 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<String, Object> loadScenarioFixture(String resource, String eventType) {
|
||||
String path = String.format(SCENARIO_FIXTURE_PATH_FORMAT, resource, eventType);
|
||||
Map<String, Object> 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<String, Object> load(String path) {
|
||||
return cache.computeIfAbsent(path, this::loadFromClasspath);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
@ -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<String, Object> entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional interface for applying event scenario data to ChangeEvents.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface EntityFixtureEventApplier {
|
||||
void apply(ChangeEvent event, Map<String, Object> 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> eventType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario for testing with a specific eventType (e.g., entityUpdated).
|
||||
*/
|
||||
public record EntityScenario(String resource, EventType type) implements Scenario {
|
||||
@Override
|
||||
public Optional<EventType> 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> eventType() {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<String, EntityFixtureBuilder> builders = new HashMap<>();
|
||||
private final Map<Scenario, EntityFixtureEventApplier> 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<String, Object> 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<String, Object> 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<String, Object> cdMap = (Map<String, Object>) scenario.get("changeDescription");
|
||||
if (cdMap != null) {
|
||||
ChangeDescription cd = buildChangeDescription(cdMap);
|
||||
event.withChangeDescription(cd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build ChangeDescription from fixture map.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private ChangeDescription buildChangeDescription(Map<String, Object> cdMap) {
|
||||
ChangeDescription cd =
|
||||
new ChangeDescription().withPreviousVersion((Double) cdMap.get("previousVersion"));
|
||||
|
||||
List<Map<String, String>> added = (List<Map<String, String>>) 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<Map<String, String>> updated = (List<Map<String, String>>) 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<Map<String, String>> deleted = (List<Map<String, String>>) 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;
|
||||
}
|
||||
}
|
||||
@ -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<AuthRequest> 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<AuthRequest> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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": []
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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": ""
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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": []
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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("<div>Will be deleted</div>");
|
||||
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("<div>Composite Template 1</div>");
|
||||
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<String, String> params = new HashMap<>();
|
||||
params.put("notificationTemplate", template1.getId().toString());
|
||||
ResultList<EventSubscription> 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("<div>Composite Template 2</div>");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String, String> 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<String, String> 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<String, String> 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 =
|
||||
"<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">"
|
||||
+ "<html lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\">"
|
||||
+ "<head>"
|
||||
+ "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">"
|
||||
+ "<meta name=\"x-apple-disable-message-reformatting\">"
|
||||
+ "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
|
||||
+ "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">"
|
||||
+ "<title>OpenMetadata · Change Event</title>"
|
||||
+ "<!--[if mso]><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->"
|
||||
+ "<style type=\"text/css\">"
|
||||
+ "html, body { margin:0!important; padding:0!important; height:100%!important; width:100%!important; } "
|
||||
+ "* { -ms-text-size-adjust:100%; -webkit-text-size-adjust:100%; } "
|
||||
+ "table, td { mso-table-lspace:0pt; mso-table-rspace:0pt; } "
|
||||
+ "table { border-collapse:collapse!important; } "
|
||||
+ "img { -ms-interpolation-mode:bicubic; border:0; outline:none; text-decoration:none; display:block; } "
|
||||
+ "a { text-decoration:none; } "
|
||||
+ ".ExternalClass { width:100%; } "
|
||||
+ ".ExternalClass, .ExternalClass * { line-height:100%; } "
|
||||
+ "a[x-apple-data-detectors] { color:inherit!important; text-decoration:none!important; } "
|
||||
+ "u + #body .full-wrap { width:100%!important; width:100vw!important; } "
|
||||
+ ".preheader { display:none!important; visibility:hidden; opacity:0; color:transparent; height:0; width:0; overflow:hidden; mso-hide:all; } "
|
||||
+ ".container { max-width:600px!important; table-layout:fixed!important; } "
|
||||
+ ".content-card { word-break:break-word; overflow-wrap:anywhere; -ms-word-break:break-all; white-space:normal; max-width:100%!important; } "
|
||||
+ ".content-scroll { display:block; max-width:100%!important; overflow-x:auto; -webkit-overflow-scrolling:touch; } "
|
||||
+ "pre, code { white-space:pre-wrap!important; word-break:break-word!important; } "
|
||||
+ "blockquote { margin:10px 0!important; padding:12px 16px!important; background-color:#f8f9fa!important; border-left:3px solid #6b7280!important; border-radius:3px!important; font-style:italic!important; color:#1f2937!important; } "
|
||||
+ "@media (max-width:600px){ .container { width:100%!important; } .p-sm { padding-left:16px!important; padding-right:16px!important; } }"
|
||||
+ "</style>"
|
||||
+ "</head>"
|
||||
+ "<body id=\"body\" style=\"margin:0; padding:0; background-color:#f6f7fb;\">"
|
||||
+ "<div class=\"preheader\">Fresh activity spotted in your OpenMetadata environment.</div>"
|
||||
+ "<center role=\"presentation\" style=\"width:100%; background-color:#f6f7fb;\">"
|
||||
+ "<!--[if (gte mso 9)|(IE)]><table role=\"presentation\" width=\"100%\" bgcolor=\"#f6f7fb\"><tr><td align=\"center\"><![endif]-->"
|
||||
+ "<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" class=\"full-wrap\" style=\"width:100%; background-color:#f6f7fb;\">"
|
||||
+ "<tr><td align=\"center\" style=\"padding:24px 12px;\">"
|
||||
+ "<!--[if (gte mso 9)|(IE)]><table role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\"><tr><td><![endif]-->"
|
||||
+ "<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" class=\"container\" bgcolor=\"#ffffff\" style=\"max-width:600px; border-radius:14px; background-color:#ffffff;\">"
|
||||
+ "<tr><td style=\"padding:0; border-top-left-radius:14px; border-top-right-radius:14px;\">"
|
||||
+ "<!--[if mso]><v:rect xmlns:v=\"urn:schemas-microsoft-com:vml\" fill=\"true\" stroke=\"false\" style=\"width:600px;height:96px;\"><v:fill type=\"frame\" src=\"https://i.imgur.com/7fn1VBe.png\" /><v:textbox inset=\"0,0,0,0\"></v:textbox></v:rect><![endif]-->"
|
||||
+ "<!--[if !mso]><!-- -->"
|
||||
+ "<img src=\"https://i.imgur.com/7fn1VBe.png\" width=\"600\" alt=\"OpenMetadata · Change Event\" style=\"width:100%; height:auto; border-top-left-radius:14px; border-top-right-radius:14px;\">"
|
||||
+ "<!--<![endif]-->"
|
||||
+ "</td></tr>"
|
||||
+ "<tr><td class=\"p-sm\" style=\"padding:20px 24px 8px 24px;\">"
|
||||
+ "<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">"
|
||||
+ "<tr><td align=\"left\" style=\"font-family:-apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; font-size:14px; line-height:22px; color:#1f2937;\">"
|
||||
+ "<strong>Heads up!</strong> Some fresh updates just landed in your OpenMetadata environment. Take a quick look at what's new below:"
|
||||
+ "</td></tr></table></td></tr>"
|
||||
+ "<tr><td class=\"p-sm\" style=\"padding:8px 24px 24px 24px;\">"
|
||||
+ "<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"border-radius:12px;\">"
|
||||
+ "<tr><td width=\"6\" style=\"background-color:#7247e8; border-top-left-radius:12px; border-bottom-left-radius:12px;\"> </td>"
|
||||
+ "<td bgcolor=\"#ffffff\" style=\"background-color:#ffffff; padding:16px; border-top-right-radius:12px; border-bottom-right-radius:12px;\">"
|
||||
+ "<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">"
|
||||
+ "<tr><td align=\"left\" style=\"font-family:-apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; font-size:14px; line-height:22px; color:#1f2937;\">"
|
||||
+ "<div class=\"content-card\"><div class=\"content-scroll\">"
|
||||
+ content
|
||||
+ "</div></div>"
|
||||
+ "</td></tr></table></td></tr></table></td></tr>"
|
||||
+ "<tr><td class=\"p-sm\" style=\"padding:0 24px 24px 24px;\">"
|
||||
+ "<table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">"
|
||||
+ "<tr><td align=\"left\" style=\"font-family:-apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; font-size:14px; line-height:22px; color:#1f2937;\">"
|
||||
+ "<span style=\"font-weight:550;\">Happy exploring!</span><br>Thanks,<br><span style=\"color:#9ca3af; font-style:italic;\">OpenMetadata Team</span>"
|
||||
+ "</td></tr></table></td></tr></table>"
|
||||
+ "<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" class=\"container\" style=\"max-width:600px;\">"
|
||||
+ "<tr><td class=\"p-sm\" align=\"left\" style=\"padding:16px 24px 6px 24px; font-family:-apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; font-size:12px; line-height:18px; color:#6b7280;\">"
|
||||
+ "You're receiving this message as part of your OpenMetadata change notification subscription."
|
||||
+ "</td></tr>"
|
||||
+ "<tr><td class=\"p-sm\" align=\"left\" style=\"padding:0 24px 32px 24px; font-family:-apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; font-size:12px; line-height:18px; color:#6b7280;\">"
|
||||
+ "Need a hand? The <a href=\"https://docs.open-metadata.org\" target=\"_blank\" style=\"color:#6b4cf6; text-decoration:underline;\">OpenMetadata docs</a> and <a href=\"https://open-metadata.org/community\" target=\"_blank\" style=\"color:#6b4cf6; text-decoration:underline;\">community</a> are ready to help."
|
||||
+ "</td></tr></table>"
|
||||
+ "<!--[if (gte mso 9)|(IE)]></td></tr></table><![endif]-->"
|
||||
+ "</td></tr></table>"
|
||||
+ "<!--[if (gte mso 9)|(IE)]></td></tr></table><![endif]-->"
|
||||
+ "</center></body></html>";
|
||||
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(
|
||||
"<h3>Table {{entity.name}} has been updated</h3>"
|
||||
+ "<p>FQN: {{entity.fullyQualifiedName}}</p>"
|
||||
+ "<p>Description: {{entity.description}}</p>"
|
||||
+ "<ul>"
|
||||
+ "{{#each entity.columns as |column|}}"
|
||||
+ "<li>{{column.name}} - {{column.dataType}}</li>"
|
||||
+ "{{/each}}"
|
||||
+ "</ul>")
|
||||
.withResource("table");
|
||||
|
||||
NotificationTemplateRenderResponse response = renderTemplate(request, ADMIN_AUTH_HEADERS);
|
||||
|
||||
String expectedSubject = "Table Update: sample_table";
|
||||
String expectedContent =
|
||||
"<h3>Table sample_table has been updated</h3>"
|
||||
+ "<p>FQN: sample-service.sample-db.public.sample_table</p>"
|
||||
+ "<p>Description: Mock table for notification template testing</p>"
|
||||
+ "<ul>"
|
||||
+ "<li>id - BIGINT</li>"
|
||||
+ "<li>name - VARCHAR</li>"
|
||||
+ "<li>email - VARCHAR</li>"
|
||||
+ "</ul>";
|
||||
|
||||
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(
|
||||
"<h3>Dashboard {{entity.displayName}}</h3>"
|
||||
+ "<p>Name: {{entity.name}}</p>"
|
||||
+ "<p>FQN: {{entity.fullyQualifiedName}}</p>"
|
||||
+ "{{#if entity.description}}"
|
||||
+ "<p>Description: {{entity.description}}</p>"
|
||||
+ "{{/if}}")
|
||||
.withResource("dashboard");
|
||||
|
||||
NotificationTemplateRenderResponse response = renderTemplate(request, ADMIN_AUTH_HEADERS);
|
||||
|
||||
String expectedSubject = "Dashboard Update: Sample Dashboard";
|
||||
String expectedContent =
|
||||
"<h3>Dashboard Sample Dashboard</h3>"
|
||||
+ "<p>Name: sample_dashboard</p>"
|
||||
+ "<p>FQN: sample-dashboard-service.sample_dashboard</p>"
|
||||
+ "<p>Description: Mock dashboard for notification template testing</p>";
|
||||
|
||||
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(
|
||||
"<h3>Entity {{entity.name}}</h3>"
|
||||
+ "<p>FQN: {{entity.fullyQualifiedName}}</p>"
|
||||
+ "<p>Description: {{entity.description}}</p>")
|
||||
.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(
|
||||
"<h3>Entity {{entity.name}}</h3>" + "<p>Display Name: {{entity.displayName}}</p>")
|
||||
.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("<p>{{entity.name}}</p>")
|
||||
.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}}",
|
||||
"<p>Entity {{entity.name}} was updated</p>",
|
||||
"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}}",
|
||||
"<p>Entity {{entity.name}} was updated</p>",
|
||||
"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}}",
|
||||
"<p>Entity {{entity.name}} was updated</p>",
|
||||
"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}}",
|
||||
"<p>Entity {{entity.name}} was updated</p>",
|
||||
"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}}",
|
||||
"<p>Entity {{entity.name}} was updated</p>",
|
||||
"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("<div>Will be deleted</div>");
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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",
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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",
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user