mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-02 19:48:17 +00:00
Merge branch 'refs/heads/main' into feature/custom-workflows
# Conflicts: # bootstrap/sql/migrations/native/1.10.0/mysql/schemaChanges.sql # bootstrap/sql/migrations/native/1.10.0/postgres/schemaChanges.sql
This commit is contained in:
commit
3dafd6f104
@ -28,6 +28,23 @@ CREATE INDEX idx_metric_custom_unit ON metric_entity(customUnitOfMeasurement);
|
||||
-- Fetch updated searchSettings
|
||||
DELETE FROM openmetadata_settings WHERE configType = 'searchSettings';
|
||||
|
||||
-- Create notification_template_entity table following OpenMetadata patterns
|
||||
CREATE TABLE IF NOT EXISTS notification_template_entity (
|
||||
id VARCHAR(36) GENERATED ALWAYS AS (json_unquote(json_extract(json, '$.id'))) STORED NOT NULL,
|
||||
name VARCHAR(256) GENERATED ALWAYS AS (json_unquote(json_extract(json, '$.name'))) VIRTUAL NOT NULL,
|
||||
fqnHash VARCHAR(768) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
json JSON NOT NULL,
|
||||
updatedAt BIGINT UNSIGNED GENERATED ALWAYS AS (json_unquote(json_extract(json, '$.updatedAt'))) VIRTUAL NOT NULL,
|
||||
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json_unquote(json_extract(json, '$.updatedBy'))) VIRTUAL NOT NULL,
|
||||
deleted TINYINT(1) GENERATED ALWAYS AS (json_extract(json, '$.deleted')) VIRTUAL,
|
||||
provider VARCHAR(32) GENERATED ALWAYS AS (json_unquote(json_extract(json, '$.provider'))) VIRTUAL,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY fqnHash (fqnHash),
|
||||
INDEX idx_notification_template_name (name),
|
||||
INDEX idx_notification_template_provider (provider)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- Increase Flowable ACTIVITY_ID_ column size to support longer user-defined workflow node names
|
||||
ALTER TABLE ACT_RU_EVENT_SUBSCR MODIFY ACTIVITY_ID_ varchar(255);
|
||||
|
||||
|
||||
@ -30,6 +30,24 @@ CREATE INDEX idx_metric_custom_unit ON metric_entity(customUnitOfMeasurement);
|
||||
-- Fetch updated searchSettings
|
||||
DELETE FROM openmetadata_settings WHERE configType = 'searchSettings';
|
||||
|
||||
-- Create notification_template_entity table following OpenMetadata patterns
|
||||
CREATE TABLE IF NOT EXISTS notification_template_entity (
|
||||
id VARCHAR(36) GENERATED ALWAYS AS (json ->> 'id') STORED NOT NULL,
|
||||
name VARCHAR(256) GENERATED ALWAYS AS (json ->> 'name') STORED NOT NULL,
|
||||
fqnHash VARCHAR(768) NOT NULL,
|
||||
json JSONB NOT NULL,
|
||||
updatedAt BIGINT GENERATED ALWAYS AS ((json ->> 'updatedAt')::bigint) STORED NOT NULL,
|
||||
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> 'updatedBy') STORED NOT NULL,
|
||||
deleted BOOLEAN GENERATED ALWAYS AS ((json ->> 'deleted')::boolean) STORED,
|
||||
provider VARCHAR(32) GENERATED ALWAYS AS (json ->> 'provider') STORED,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (fqnHash)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_notification_template_name ON notification_template_entity(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_notification_template_provider ON notification_template_entity(provider);
|
||||
|
||||
-- Increase Flowable ACTIVITY_ID_ column size to support longer user-defined workflow node names
|
||||
ALTER TABLE ACT_RU_EVENT_SUBSCR ALTER COLUMN ACTIVITY_ID_ TYPE varchar(255);
|
||||
|
||||
|
||||
@ -917,6 +917,11 @@
|
||||
<artifactId>client-java</artifactId>
|
||||
<version>${kubernetes-client.version}</version>
|
||||
</dependency>
|
||||
<!-- Handlebars Template Engine -->
|
||||
<dependency>
|
||||
<groupId>com.github.jknack</groupId>
|
||||
<artifactId>handlebars</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.slack.api</groupId>
|
||||
<artifactId>bolt-servlet</artifactId>
|
||||
|
||||
@ -236,6 +236,7 @@ public final class Entity {
|
||||
//
|
||||
// Other entities
|
||||
public static final String EVENT_SUBSCRIPTION = "eventsubscription";
|
||||
public static final String NOTIFICATION_TEMPLATE = "notificationTemplate";
|
||||
public static final String THREAD = "THREAD";
|
||||
public static final String SUGGESTION = "SUGGESTION";
|
||||
public static final String WORKFLOW = "workflow";
|
||||
|
||||
@ -284,6 +284,11 @@ public class CachedCollectionDAO implements CollectionDAO {
|
||||
return delegate.eventSubscriptionDAO();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NotificationTemplateDAO notificationTemplateDAO() {
|
||||
return delegate.notificationTemplateDAO();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IngestionPipelineDAO ingestionPipelineDAO() {
|
||||
return delegate.ingestionPipelineDAO();
|
||||
|
||||
@ -116,6 +116,7 @@ import org.openmetadata.schema.entity.domains.Domain;
|
||||
import org.openmetadata.schema.entity.events.EventSubscription;
|
||||
import org.openmetadata.schema.entity.events.FailedEvent;
|
||||
import org.openmetadata.schema.entity.events.FailedEventResponse;
|
||||
import org.openmetadata.schema.entity.events.NotificationTemplate;
|
||||
import org.openmetadata.schema.entity.policies.Policy;
|
||||
import org.openmetadata.schema.entity.services.ApiService;
|
||||
import org.openmetadata.schema.entity.services.DashboardService;
|
||||
@ -297,6 +298,9 @@ public interface CollectionDAO {
|
||||
@CreateSqlObject
|
||||
EventSubscriptionDAO eventSubscriptionDAO();
|
||||
|
||||
@CreateSqlObject
|
||||
NotificationTemplateDAO notificationTemplateDAO();
|
||||
|
||||
@CreateSqlObject
|
||||
PolicyDAO policyDAO();
|
||||
|
||||
@ -2971,6 +2975,23 @@ public interface CollectionDAO {
|
||||
@Bind("eventSubscriptionId") String eventSubscriptionId);
|
||||
}
|
||||
|
||||
interface NotificationTemplateDAO extends EntityDAO<NotificationTemplate> {
|
||||
@Override
|
||||
default String getTableName() {
|
||||
return "notification_template_entity";
|
||||
}
|
||||
|
||||
@Override
|
||||
default Class<NotificationTemplate> getEntityClass() {
|
||||
return NotificationTemplate.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
default String getNameHashColumn() {
|
||||
return "fqnHash";
|
||||
}
|
||||
}
|
||||
|
||||
interface ChartDAO extends EntityDAO<Chart> {
|
||||
@Override
|
||||
default String getTableName() {
|
||||
|
||||
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2024 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.
|
||||
*/
|
||||
|
||||
package org.openmetadata.service.jdbi3;
|
||||
|
||||
import com.github.jknack.handlebars.Handlebars;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.openmetadata.schema.entity.events.NotificationTemplate;
|
||||
import org.openmetadata.schema.type.change.ChangeSource;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.resources.events.NotificationTemplateResource;
|
||||
import org.openmetadata.service.util.EntityUtil.Fields;
|
||||
|
||||
@Slf4j
|
||||
public class NotificationTemplateRepository extends EntityRepository<NotificationTemplate> {
|
||||
|
||||
static final String PATCH_FIELDS = "templateBody,description,displayName";
|
||||
static final String UPDATE_FIELDS = "templateBody,description,displayName";
|
||||
|
||||
public NotificationTemplateRepository() {
|
||||
super(
|
||||
NotificationTemplateResource.COLLECTION_PATH,
|
||||
Entity.NOTIFICATION_TEMPLATE,
|
||||
NotificationTemplate.class,
|
||||
Entity.getCollectionDAO().notificationTemplateDAO(),
|
||||
PATCH_FIELDS,
|
||||
UPDATE_FIELDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFields(NotificationTemplate entity, Fields fields) {
|
||||
/* Nothing to do */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearFields(NotificationTemplate entity, Fields fields) {
|
||||
/* Nothing to do */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(NotificationTemplate entity, boolean update) {
|
||||
// Validate template syntax
|
||||
if (entity.getTemplateBody() != null) {
|
||||
validateTemplateBody(entity.getTemplateBody());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeEntity(NotificationTemplate entity, boolean update) {
|
||||
// Store the entity using the standard mechanism
|
||||
store(entity, update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeRelationships(NotificationTemplate entity) {
|
||||
// No relationships to store beyond what is stored in the super class
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityRepository<NotificationTemplate>.EntityUpdater getUpdater(
|
||||
NotificationTemplate original,
|
||||
NotificationTemplate updated,
|
||||
Operation operation,
|
||||
ChangeSource changeSource) {
|
||||
return new NotificationTemplateUpdater(original, updated, operation);
|
||||
}
|
||||
|
||||
private void validateTemplateBody(String templateBody) {
|
||||
try {
|
||||
// Use Handlebars to validate the template syntax
|
||||
Handlebars handlebars = new Handlebars();
|
||||
handlebars.compileInline(templateBody);
|
||||
} catch (Exception e) {
|
||||
// Provide clean user message (detailed error available in server logs)
|
||||
throw new IllegalArgumentException("Invalid template syntax");
|
||||
}
|
||||
}
|
||||
|
||||
public class NotificationTemplateUpdater extends EntityUpdater {
|
||||
public NotificationTemplateUpdater(
|
||||
NotificationTemplate original, NotificationTemplate updated, Operation operation) {
|
||||
super(original, updated, operation);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void entitySpecificUpdate(boolean consolidatingChanges) {
|
||||
// Only record changes for fields specific to NotificationTemplate
|
||||
// Description and displayName are handled by the parent class
|
||||
recordChange("templateBody", original.getTemplateBody(), updated.getTemplateBody());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package org.openmetadata.service.resources.events;
|
||||
|
||||
import org.openmetadata.schema.api.events.CreateNotificationTemplate;
|
||||
import org.openmetadata.schema.entity.events.NotificationTemplate;
|
||||
import org.openmetadata.service.mapper.EntityMapper;
|
||||
|
||||
public class NotificationTemplateMapper
|
||||
implements EntityMapper<NotificationTemplate, CreateNotificationTemplate> {
|
||||
|
||||
@Override
|
||||
public NotificationTemplate createToEntity(CreateNotificationTemplate create, String user) {
|
||||
return copy(new NotificationTemplate(), create, user)
|
||||
.withTemplateBody(create.getTemplateBody());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,528 @@
|
||||
/*
|
||||
* Copyright 2021 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.
|
||||
*/
|
||||
|
||||
package org.openmetadata.service.resources.events;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.ExampleObject;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.json.JsonPatch;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.PATCH;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.SecurityContext;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import java.util.List;
|
||||
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.entity.events.NotificationTemplate;
|
||||
import org.openmetadata.schema.type.EntityHistory;
|
||||
import org.openmetadata.schema.type.Include;
|
||||
import org.openmetadata.schema.type.MetadataOperation;
|
||||
import org.openmetadata.schema.type.ProviderType;
|
||||
import org.openmetadata.schema.type.change.ChangeSource;
|
||||
import org.openmetadata.schema.utils.ResultList;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.jdbi3.ListFilter;
|
||||
import org.openmetadata.service.jdbi3.NotificationTemplateRepository;
|
||||
import org.openmetadata.service.limits.Limits;
|
||||
import org.openmetadata.service.resources.Collection;
|
||||
import org.openmetadata.service.resources.EntityResource;
|
||||
import org.openmetadata.service.security.Authorizer;
|
||||
|
||||
@Slf4j
|
||||
@Path("/v1/notificationTemplates")
|
||||
@Tag(
|
||||
name = "Notification Templates",
|
||||
description = "Notification templates for customizing event notifications")
|
||||
@Collection(name = "notificationTemplates")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class NotificationTemplateResource
|
||||
extends EntityResource<NotificationTemplate, NotificationTemplateRepository> {
|
||||
|
||||
public static final String COLLECTION_PATH = "/v1/notificationTemplates";
|
||||
public static final String FIELDS = "";
|
||||
|
||||
// Mapper for converting DTOs to entities
|
||||
private final NotificationTemplateMapper mapper = new NotificationTemplateMapper();
|
||||
|
||||
public NotificationTemplateResource(Authorizer authorizer, Limits limits) {
|
||||
super(Entity.NOTIFICATION_TEMPLATE, authorizer, limits);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<MetadataOperation> getEntitySpecificOperations() {
|
||||
addViewOperation("templateBody", MetadataOperation.VIEW_BASIC);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class NotificationTemplateList extends ResultList<NotificationTemplate> {
|
||||
/* Required for serde */
|
||||
}
|
||||
|
||||
@GET
|
||||
@Operation(
|
||||
operationId = "listNotificationTemplates",
|
||||
summary = "List notification templates",
|
||||
description = "Get a list of notification templates",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "List of notification templates",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = NotificationTemplateList.class)))
|
||||
})
|
||||
public ResultList<NotificationTemplate> list(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@Parameter(
|
||||
description = "Fields requested in the returned resource",
|
||||
schema = @Schema(type = "string", example = FIELDS))
|
||||
@QueryParam("fields")
|
||||
String fieldsParam,
|
||||
@Parameter(
|
||||
description = "Filter templates by provider type (SYSTEM or USER)",
|
||||
schema = @Schema(implementation = ProviderType.class))
|
||||
@QueryParam("provider")
|
||||
ProviderType provider,
|
||||
@Parameter(description = "Limit the number of results. (1 to 1000000, default = 10)")
|
||||
@DefaultValue("10")
|
||||
@QueryParam("limit")
|
||||
@Min(0)
|
||||
@Max(1000000)
|
||||
int limitParam,
|
||||
@Parameter(
|
||||
description = "Returns list of entities before this cursor",
|
||||
schema = @Schema(type = "string"))
|
||||
@QueryParam("before")
|
||||
String before,
|
||||
@Parameter(
|
||||
description = "Returns list of entities after this cursor",
|
||||
schema = @Schema(type = "string"))
|
||||
@QueryParam("after")
|
||||
String after,
|
||||
@Parameter(
|
||||
description = "Include all, deleted, or non-deleted entities.",
|
||||
schema = @Schema(implementation = Include.class))
|
||||
@QueryParam("include")
|
||||
@DefaultValue("non-deleted")
|
||||
Include include) {
|
||||
ListFilter filter = new ListFilter(include);
|
||||
if (provider != null) {
|
||||
filter.addQueryParam("provider", provider.value());
|
||||
}
|
||||
return super.listInternal(
|
||||
uriInfo, securityContext, fieldsParam, filter, limitParam, before, after);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
@Operation(
|
||||
operationId = "getNotificationTemplateById",
|
||||
summary = "Get a notification template by Id",
|
||||
description = "Get a notification template by `Id`.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "The notification template",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = NotificationTemplate.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "Notification template for instance {id} is not found")
|
||||
})
|
||||
public NotificationTemplate get(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@Parameter(description = "Id of the notification template", schema = @Schema(type = "UUID"))
|
||||
@PathParam("id")
|
||||
UUID id,
|
||||
@Parameter(
|
||||
description = "Fields requested in the returned resource",
|
||||
schema = @Schema(type = "string", example = FIELDS))
|
||||
@QueryParam("fields")
|
||||
String fieldsParam,
|
||||
@Parameter(
|
||||
description = "Include all, deleted, or non-deleted entities.",
|
||||
schema = @Schema(implementation = Include.class))
|
||||
@QueryParam("include")
|
||||
@DefaultValue("non-deleted")
|
||||
Include include) {
|
||||
return getInternal(uriInfo, securityContext, id, fieldsParam, include);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/name/{fqn}")
|
||||
@Operation(
|
||||
operationId = "getNotificationTemplateByFQN",
|
||||
summary = "Get a notification template by fully qualified name",
|
||||
description = "Get a notification template by `fullyQualifiedName`.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "The notification template",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = NotificationTemplate.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "Notification template for instance {fqn} is not found")
|
||||
})
|
||||
public NotificationTemplate getByName(
|
||||
@Context UriInfo uriInfo,
|
||||
@Parameter(
|
||||
description = "Fully qualified name of the notification template",
|
||||
schema = @Schema(type = "string"))
|
||||
@PathParam("fqn")
|
||||
String fqn,
|
||||
@Context SecurityContext securityContext,
|
||||
@Parameter(
|
||||
description = "Fields requested in the returned resource",
|
||||
schema = @Schema(type = "string", example = FIELDS))
|
||||
@QueryParam("fields")
|
||||
String fieldsParam,
|
||||
@Parameter(
|
||||
description = "Include all, deleted, or non-deleted entities.",
|
||||
schema = @Schema(implementation = Include.class))
|
||||
@QueryParam("include")
|
||||
@DefaultValue("non-deleted")
|
||||
Include include) {
|
||||
return getByNameInternal(uriInfo, securityContext, fqn, fieldsParam, include);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{id}/versions")
|
||||
@Operation(
|
||||
operationId = "listAllNotificationTemplateVersions",
|
||||
summary = "List notification template versions",
|
||||
description = "Get a list of all the versions of a notification template identified by `Id`",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "List of notification template versions",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = EntityHistory.class)))
|
||||
})
|
||||
public EntityHistory listVersions(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@Parameter(description = "Id of the notification template", schema = @Schema(type = "UUID"))
|
||||
@PathParam("id")
|
||||
UUID id) {
|
||||
return super.listVersionsInternal(securityContext, id);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{id}/versions/{version}")
|
||||
@Operation(
|
||||
operationId = "getSpecificNotificationTemplateVersion",
|
||||
summary = "Get a version of the notification template",
|
||||
description = "Get a version of the notification template by given `Id`",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "notification template",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = NotificationTemplate.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description =
|
||||
"Notification template for instance {id} and version {version} is not found")
|
||||
})
|
||||
public NotificationTemplate getVersion(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@Parameter(description = "Id of the notification template", schema = @Schema(type = "UUID"))
|
||||
@PathParam("id")
|
||||
UUID id,
|
||||
@Parameter(
|
||||
description = "Notification template version number in the form `major`.`minor`",
|
||||
schema = @Schema(type = "string", example = "0.1 or 1.1"))
|
||||
@PathParam("version")
|
||||
String version) {
|
||||
return super.getVersionInternal(securityContext, id, version);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Operation(
|
||||
operationId = "createNotificationTemplate",
|
||||
summary = "Create a notification template",
|
||||
description = "Create a new notification template",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "The notification template",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = NotificationTemplate.class))),
|
||||
@ApiResponse(responseCode = "400", description = "Bad request")
|
||||
})
|
||||
public Response create(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@Valid CreateNotificationTemplate create) {
|
||||
NotificationTemplate template =
|
||||
mapper.createToEntity(create, securityContext.getUserPrincipal().getName());
|
||||
return create(uriInfo, securityContext, template);
|
||||
}
|
||||
|
||||
@PATCH
|
||||
@Path("/{id}")
|
||||
@Operation(
|
||||
operationId = "patchNotificationTemplate",
|
||||
summary = "Update a notification template",
|
||||
description = "Update an existing notification template using JsonPatch.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Successfully updated the notification template",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = NotificationTemplate.class))),
|
||||
@ApiResponse(responseCode = "400", description = "Bad request"),
|
||||
@ApiResponse(responseCode = "404", description = "Notification template not found")
|
||||
})
|
||||
@Consumes(MediaType.APPLICATION_JSON_PATCH_JSON)
|
||||
public Response patch(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@Parameter(description = "Id of the notification template", schema = @Schema(type = "UUID"))
|
||||
@PathParam("id")
|
||||
UUID id,
|
||||
@RequestBody(
|
||||
description = "JsonPatch with array of operations",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_PATCH_JSON,
|
||||
examples = {
|
||||
@ExampleObject(
|
||||
"[{\"op\":\"replace\",\"path\":\"/description\",\"value\":\"new description\"}]")
|
||||
}))
|
||||
JsonPatch patch) {
|
||||
return patchInternal(uriInfo, securityContext, id, patch, ChangeSource.MANUAL);
|
||||
}
|
||||
|
||||
@PATCH
|
||||
@Path("/name/{fqn}")
|
||||
@Operation(
|
||||
operationId = "patchNotificationTemplateByFQN",
|
||||
summary = "Update a notification template by name",
|
||||
description = "Update an existing notification template using JsonPatch.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Successfully updated the notification template",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = NotificationTemplate.class))),
|
||||
@ApiResponse(responseCode = "400", description = "Bad request"),
|
||||
@ApiResponse(responseCode = "404", description = "Notification template not found")
|
||||
})
|
||||
@Consumes(MediaType.APPLICATION_JSON_PATCH_JSON)
|
||||
public Response patch(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@Parameter(
|
||||
description = "Fully qualified name of the notification template",
|
||||
schema = @Schema(type = "string"))
|
||||
@PathParam("fqn")
|
||||
String fqn,
|
||||
@RequestBody(
|
||||
description = "JsonPatch with array of operations",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = MediaType.APPLICATION_JSON_PATCH_JSON,
|
||||
examples = {
|
||||
@ExampleObject(
|
||||
"[{\"op\":\"replace\",\"path\":\"/description\",\"value\":\"new description\"}]")
|
||||
}))
|
||||
JsonPatch patch) {
|
||||
return patchInternal(uriInfo, securityContext, fqn, patch, ChangeSource.MANUAL);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Operation(
|
||||
operationId = "createOrUpdateNotificationTemplate",
|
||||
summary = "Create or update a notification template",
|
||||
description =
|
||||
"Create a notification template, if it does not exist or update an existing notification template.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "The notification template",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = NotificationTemplate.class))),
|
||||
@ApiResponse(responseCode = "400", description = "Bad request")
|
||||
})
|
||||
public Response createOrUpdate(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@Valid CreateNotificationTemplate create) {
|
||||
NotificationTemplate template =
|
||||
mapper.createToEntity(create, securityContext.getUserPrincipal().getName());
|
||||
return createOrUpdate(uriInfo, securityContext, template);
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/async/{id}")
|
||||
@Operation(
|
||||
operationId = "deleteNotificationTemplateAsync",
|
||||
summary = "Asynchronously delete a notification template by Id",
|
||||
description = "Asynchronously delete a notification template by `Id`.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "OK"),
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "Notification template for instance {id} is not found")
|
||||
})
|
||||
public Response deleteByIdAsync(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@Parameter(description = "Hard delete the entity. (Default = `false`)")
|
||||
@QueryParam("hardDelete")
|
||||
@DefaultValue("false")
|
||||
boolean hardDelete,
|
||||
@Parameter(
|
||||
description = "Recursively delete this entity and it's children. (Default `false`)")
|
||||
@QueryParam("recursive")
|
||||
@DefaultValue("false")
|
||||
boolean recursive,
|
||||
@Parameter(description = "Id of the notification template", schema = @Schema(type = "UUID"))
|
||||
@PathParam("id")
|
||||
UUID id) {
|
||||
return deleteByIdAsync(uriInfo, securityContext, id, recursive, hardDelete);
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
@Operation(
|
||||
operationId = "deleteNotificationTemplate",
|
||||
summary = "Delete a notification template by Id",
|
||||
description = "Delete a notification template by `Id`. System templates cannot be deleted.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "OK"),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Bad request - System templates cannot be deleted"),
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "Notification template for instance {id} is not found")
|
||||
})
|
||||
public Response delete(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@Parameter(description = "Id of the notification template", schema = @Schema(type = "UUID"))
|
||||
@PathParam("id")
|
||||
UUID id,
|
||||
@Parameter(
|
||||
description = "Recursively delete this entity and it's children. (Default `false`)")
|
||||
@QueryParam("recursive")
|
||||
@DefaultValue("false")
|
||||
boolean recursive,
|
||||
@Parameter(description = "Hard delete the entity. (Default = `false`)")
|
||||
@QueryParam("hardDelete")
|
||||
@DefaultValue("false")
|
||||
boolean hardDelete) {
|
||||
return super.delete(uriInfo, securityContext, id, recursive, hardDelete);
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/name/{fqn}")
|
||||
@Operation(
|
||||
operationId = "deleteNotificationTemplateByFQN",
|
||||
summary = "Delete a notification template by fully qualified name",
|
||||
description =
|
||||
"Delete a notification template by `fullyQualifiedName`. System templates cannot be deleted.",
|
||||
responses = {
|
||||
@ApiResponse(responseCode = "200", description = "OK"),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Bad request - System templates cannot be deleted"),
|
||||
@ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "Notification template for instance {fqn} is not found")
|
||||
})
|
||||
public Response delete(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@Parameter(
|
||||
description = "Fully qualified name of the notification template",
|
||||
schema = @Schema(type = "string"))
|
||||
@PathParam("fqn")
|
||||
String fqn,
|
||||
@Parameter(
|
||||
description = "Recursively delete this entity and it's children. (Default `false`)")
|
||||
@QueryParam("recursive")
|
||||
@DefaultValue("false")
|
||||
boolean recursive,
|
||||
@Parameter(description = "Hard delete the entity. (Default = `false`)")
|
||||
@QueryParam("hardDelete")
|
||||
@DefaultValue("false")
|
||||
boolean hardDelete) {
|
||||
return deleteByName(uriInfo, securityContext, fqn, recursive, hardDelete);
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/restore")
|
||||
@Operation(
|
||||
operationId = "restore",
|
||||
summary = "Restore a soft deleted notification template",
|
||||
description = "Restore a soft deleted notification template.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Successfully restored the notification template")
|
||||
})
|
||||
public Response restoreEntity(
|
||||
@Context UriInfo uriInfo,
|
||||
@Context SecurityContext securityContext,
|
||||
@Valid RestoreEntity restore) {
|
||||
return restoreEntity(uriInfo, securityContext, restore.getId());
|
||||
}
|
||||
}
|
||||
@ -594,7 +594,8 @@ public class SchemaFieldExtractor {
|
||||
"pipeline", "data",
|
||||
"votes", "data",
|
||||
"dataProduct", "domains",
|
||||
"domain", "domains");
|
||||
"domain", "domains",
|
||||
"notificationTemplate", "events");
|
||||
return entityTypeToSubdirectory.getOrDefault(entityType, "data");
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,353 @@
|
||||
/*
|
||||
* Copyright 2021 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.
|
||||
*/
|
||||
|
||||
package org.openmetadata.service.resources.events;
|
||||
|
||||
import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
|
||||
import static jakarta.ws.rs.core.Response.Status.CONFLICT;
|
||||
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.assertTrue;
|
||||
import static org.openmetadata.service.util.EntityUtil.fieldUpdated;
|
||||
import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS;
|
||||
import static org.openmetadata.service.util.TestUtils.UpdateType.MINOR_UPDATE;
|
||||
import static org.openmetadata.service.util.TestUtils.assertResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.http.client.HttpResponseException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.openmetadata.schema.api.events.CreateNotificationTemplate;
|
||||
import org.openmetadata.schema.entity.events.NotificationTemplate;
|
||||
import org.openmetadata.schema.type.ChangeDescription;
|
||||
import org.openmetadata.schema.type.ProviderType;
|
||||
import org.openmetadata.schema.utils.JsonUtils;
|
||||
import org.openmetadata.schema.utils.ResultList;
|
||||
import org.openmetadata.service.Entity;
|
||||
import org.openmetadata.service.resources.EntityResourceTest;
|
||||
|
||||
@Slf4j
|
||||
public class NotificationTemplateResourceTest
|
||||
extends EntityResourceTest<NotificationTemplate, CreateNotificationTemplate> {
|
||||
|
||||
public NotificationTemplateResourceTest() {
|
||||
super(
|
||||
Entity.NOTIFICATION_TEMPLATE,
|
||||
NotificationTemplate.class,
|
||||
NotificationTemplateResource.NotificationTemplateList.class,
|
||||
"notificationTemplates",
|
||||
NotificationTemplateResource.FIELDS);
|
||||
supportsFieldsQueryParam = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreateNotificationTemplate createRequest(String name) {
|
||||
return new CreateNotificationTemplate()
|
||||
.withName(name)
|
||||
.withDisplayName(name != null ? "Display " + name : null)
|
||||
.withDescription(name != null ? "Template for " + name : null)
|
||||
.withTemplateBody("<div>{{entity.name}} has been updated by {{updatedBy}}</div>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateCreatedEntity(
|
||||
NotificationTemplate template,
|
||||
CreateNotificationTemplate createRequest,
|
||||
Map<String, String> authHeaders) {
|
||||
assertEquals(createRequest.getName(), template.getName());
|
||||
assertEquals(createRequest.getDisplayName(), template.getDisplayName());
|
||||
assertEquals(createRequest.getDescription(), template.getDescription());
|
||||
assertEquals(createRequest.getTemplateBody(), template.getTemplateBody());
|
||||
assertNotNull(template.getVersion());
|
||||
assertNotNull(template.getUpdatedAt());
|
||||
assertNotNull(template.getUpdatedBy());
|
||||
assertNotNull(template.getHref());
|
||||
assertNotNull(template.getFullyQualifiedName());
|
||||
// FQN may be quoted if name contains special characters
|
||||
String expectedFqn = createRequest.getName();
|
||||
String actualFqn = template.getFullyQualifiedName();
|
||||
// Check if FQN matches, handling potential quoting
|
||||
assertTrue(
|
||||
actualFqn.equals(expectedFqn) || actualFqn.equals("\"" + expectedFqn + "\""),
|
||||
"FQN mismatch: expected " + expectedFqn + " or quoted version, got " + actualFqn);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void compareEntities(
|
||||
NotificationTemplate expected,
|
||||
NotificationTemplate updated,
|
||||
Map<String, String> authHeaders) {
|
||||
assertEquals(expected.getName(), updated.getName());
|
||||
assertEquals(expected.getFullyQualifiedName(), updated.getFullyQualifiedName());
|
||||
assertEquals(expected.getDescription(), updated.getDescription());
|
||||
assertEquals(expected.getDisplayName(), updated.getDisplayName());
|
||||
assertEquals(expected.getProvider(), updated.getProvider());
|
||||
assertEquals(expected.getTemplateBody(), updated.getTemplateBody());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertFieldChange(String fieldName, Object expected, Object actual) {
|
||||
if (expected == actual) {
|
||||
return;
|
||||
}
|
||||
switch (fieldName) {
|
||||
case "templateBody":
|
||||
case "description":
|
||||
case "displayName":
|
||||
assertEquals(expected, actual);
|
||||
break;
|
||||
default:
|
||||
assertCommonFieldChange(fieldName, expected, actual);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public NotificationTemplate validateGetWithDifferentFields(
|
||||
NotificationTemplate template, boolean byName) throws HttpResponseException {
|
||||
String fields = "";
|
||||
template =
|
||||
byName
|
||||
? getEntityByName(template.getFullyQualifiedName(), fields, ADMIN_AUTH_HEADERS)
|
||||
: getEntity(template.getId(), fields, ADMIN_AUTH_HEADERS);
|
||||
assertNotNull(template.getName());
|
||||
assertNotNull(template.getFullyQualifiedName());
|
||||
assertNotNull(template.getDescription());
|
||||
assertNotNull(template.getProvider());
|
||||
assertNotNull(template.getTemplateBody());
|
||||
return template;
|
||||
}
|
||||
|
||||
@Test
|
||||
void post_validNotificationTemplate_200(TestInfo test) throws IOException {
|
||||
CreateNotificationTemplate create =
|
||||
createRequest(getEntityName(test))
|
||||
.withTemplateBody(
|
||||
"<h3>Pipeline {{entity.name}} Status Update</h3>"
|
||||
+ "<p>Status: {{entity.pipelineStatus}}</p>");
|
||||
NotificationTemplate template = createEntity(create, ADMIN_AUTH_HEADERS);
|
||||
|
||||
assertEquals(getEntityName(test), template.getFullyQualifiedName().replaceAll("^\"|\"$", ""));
|
||||
assertEquals(ProviderType.USER, template.getProvider());
|
||||
}
|
||||
|
||||
@Test
|
||||
void post_invalidHandlebarsTemplate_400(TestInfo test) {
|
||||
CreateNotificationTemplate create =
|
||||
createRequest(getEntityName(test)).withTemplateBody("{{#if entity.name}} Missing end if");
|
||||
|
||||
assertResponse(
|
||||
() -> createEntity(create, ADMIN_AUTH_HEADERS), BAD_REQUEST, "Invalid template syntax");
|
||||
}
|
||||
|
||||
@Test
|
||||
void post_duplicateNotificationTemplate_409(TestInfo test) throws IOException {
|
||||
CreateNotificationTemplate create = createRequest(getEntityName(test));
|
||||
createEntity(create, ADMIN_AUTH_HEADERS);
|
||||
|
||||
assertResponse(
|
||||
() -> createEntity(create, ADMIN_AUTH_HEADERS), CONFLICT, "Entity already exists");
|
||||
}
|
||||
|
||||
@Test
|
||||
void patch_notificationTemplateAttributes_200(TestInfo test) throws IOException {
|
||||
NotificationTemplate template =
|
||||
createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS);
|
||||
|
||||
String origTemplateBody = template.getTemplateBody();
|
||||
String origDescription = template.getDescription();
|
||||
String origDisplayName = template.getDisplayName();
|
||||
|
||||
String newTemplateBody = "<div class='notification'>{{entity.name}} - Updated Template</div>";
|
||||
String newDescription = "Updated description";
|
||||
String newDisplayName = "Updated Display Name";
|
||||
|
||||
String json =
|
||||
String.format(
|
||||
"[{\"op\":\"replace\",\"path\":\"/templateBody\",\"value\":%s}]",
|
||||
JsonUtils.pojoToJson(newTemplateBody));
|
||||
template = patchEntity(template.getId(), JsonUtils.readTree(json), ADMIN_AUTH_HEADERS);
|
||||
assertEquals(newTemplateBody, template.getTemplateBody());
|
||||
|
||||
String json2 =
|
||||
String.format(
|
||||
"[{\"op\":\"replace\",\"path\":\"/description\",\"value\":%s}]",
|
||||
JsonUtils.pojoToJson(newDescription));
|
||||
template = patchEntity(template.getId(), JsonUtils.readTree(json2), ADMIN_AUTH_HEADERS);
|
||||
assertEquals(newDescription, template.getDescription());
|
||||
|
||||
String json3 =
|
||||
String.format(
|
||||
"[{\"op\":\"replace\",\"path\":\"/displayName\",\"value\":%s}]",
|
||||
JsonUtils.pojoToJson(newDisplayName));
|
||||
template = patchEntity(template.getId(), JsonUtils.readTree(json3), ADMIN_AUTH_HEADERS);
|
||||
assertEquals(newDisplayName, template.getDisplayName());
|
||||
|
||||
ChangeDescription change = getChangeDescription(template, MINOR_UPDATE);
|
||||
fieldUpdated(change, "templateBody", origTemplateBody, newTemplateBody);
|
||||
fieldUpdated(change, "description", origDescription, newDescription);
|
||||
fieldUpdated(change, "displayName", origDisplayName, newDisplayName);
|
||||
}
|
||||
|
||||
@Test
|
||||
void patch_invalidTemplateBody_400(TestInfo test) throws IOException {
|
||||
NotificationTemplate template =
|
||||
createEntity(createRequest(getEntityName(test)), ADMIN_AUTH_HEADERS);
|
||||
|
||||
String invalidTemplateBody = "{{#each items}} Missing end each";
|
||||
assertResponse(
|
||||
() -> {
|
||||
String json =
|
||||
String.format(
|
||||
"[{\"op\":\"replace\",\"path\":\"/templateBody\",\"value\":%s}]",
|
||||
JsonUtils.pojoToJson(invalidTemplateBody));
|
||||
patchEntity(template.getId(), JsonUtils.readTree(json), ADMIN_AUTH_HEADERS);
|
||||
},
|
||||
BAD_REQUEST,
|
||||
"Invalid template syntax");
|
||||
}
|
||||
|
||||
@Test
|
||||
void put_updateNotificationTemplate_200(TestInfo test) throws IOException {
|
||||
CreateNotificationTemplate request = createRequest(getEntityName(test));
|
||||
NotificationTemplate template = createEntity(request, ADMIN_AUTH_HEADERS);
|
||||
|
||||
String newTemplateBody = "<h1>Updated: {{entity.name}}</h1>";
|
||||
String newDescription = "Updated via PUT";
|
||||
|
||||
CreateNotificationTemplate updateRequest =
|
||||
createRequest(template.getName())
|
||||
.withDisplayName(template.getDisplayName())
|
||||
.withDescription(newDescription)
|
||||
.withTemplateBody(newTemplateBody);
|
||||
|
||||
NotificationTemplate updatedTemplate = updateEntity(updateRequest, OK, ADMIN_AUTH_HEADERS);
|
||||
assertEquals(newTemplateBody, updatedTemplate.getTemplateBody());
|
||||
assertEquals(newDescription, updatedTemplate.getDescription());
|
||||
assertEquals(template.getId(), updatedTemplate.getId());
|
||||
|
||||
ChangeDescription change = getChangeDescription(updatedTemplate, MINOR_UPDATE);
|
||||
fieldUpdated(change, "templateBody", template.getTemplateBody(), newTemplateBody);
|
||||
fieldUpdated(change, "description", template.getDescription(), newDescription);
|
||||
}
|
||||
|
||||
@Test
|
||||
void get_notificationTemplateByFQN_200(TestInfo test) throws IOException {
|
||||
CreateNotificationTemplate create = createRequest(getEntityName(test));
|
||||
NotificationTemplate template = createEntity(create, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Use the actual FQN from the created template for fetching
|
||||
NotificationTemplate fetched =
|
||||
getEntityByName(template.getFullyQualifiedName(), ADMIN_AUTH_HEADERS);
|
||||
assertEquals(template.getId(), fetched.getId());
|
||||
assertEquals(template.getFullyQualifiedName(), fetched.getFullyQualifiedName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_multipleTemplates(TestInfo test) throws IOException {
|
||||
// Test creating multiple templates
|
||||
String[] templateNames = {"entity_change", "test_change", "custom_alert"};
|
||||
|
||||
for (String templateName : templateNames) {
|
||||
String name = getEntityName(test) + "_" + templateName;
|
||||
CreateNotificationTemplate create =
|
||||
createRequest(name).withTemplateBody("<p>Template for {{entity.name}}</p>");
|
||||
|
||||
NotificationTemplate template = createEntity(create, ADMIN_AUTH_HEADERS);
|
||||
assertEquals(name, template.getFullyQualifiedName().replaceAll("^\"|\"$", ""));
|
||||
assertEquals(ProviderType.USER, template.getProvider());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_templateValidationWithComplexHandlebars(TestInfo test) throws IOException {
|
||||
String complexTemplate =
|
||||
"{{#if entity.owner}}"
|
||||
+ "<p>Owner: {{entity.owner.name}}</p>"
|
||||
+ "{{else}}"
|
||||
+ "<p>No owner assigned</p>"
|
||||
+ "{{/if}}"
|
||||
+ "{{#each entity.tags as |tag|}}"
|
||||
+ "<span class='tag'>{{tag.tagFQN}}</span>"
|
||||
+ "{{/each}}";
|
||||
|
||||
CreateNotificationTemplate create =
|
||||
createRequest(getEntityName(test)).withTemplateBody(complexTemplate);
|
||||
|
||||
NotificationTemplate template = createEntity(create, ADMIN_AUTH_HEADERS);
|
||||
assertEquals(complexTemplate, template.getTemplateBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_listFilterByProvider(TestInfo test) throws IOException {
|
||||
// Create multiple templates with USER provider
|
||||
String userTemplate1 = getEntityName(test) + "_user1";
|
||||
String userTemplate2 = getEntityName(test) + "_user2";
|
||||
|
||||
CreateNotificationTemplate create1 =
|
||||
createRequest(userTemplate1).withTemplateBody("<p>User template 1</p>");
|
||||
CreateNotificationTemplate create2 =
|
||||
createRequest(userTemplate2).withTemplateBody("<p>User template 2</p>");
|
||||
|
||||
NotificationTemplate template1 = createEntity(create1, ADMIN_AUTH_HEADERS);
|
||||
NotificationTemplate template2 = createEntity(create2, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Verify both templates have USER provider
|
||||
assertEquals(ProviderType.USER, template1.getProvider());
|
||||
assertEquals(ProviderType.USER, template2.getProvider());
|
||||
|
||||
// List all templates (no filter)
|
||||
Map<String, String> params = new HashMap<>();
|
||||
ResultList<NotificationTemplate> allTemplates = listEntities(params, ADMIN_AUTH_HEADERS);
|
||||
assertTrue(allTemplates.getData().size() >= 2);
|
||||
|
||||
// List only USER templates (use lowercase value as stored in JSON)
|
||||
params.put("provider", ProviderType.USER.value());
|
||||
params.put("limit", "1000"); // Increase limit to ensure we get all templates
|
||||
ResultList<NotificationTemplate> userTemplates = listEntities(params, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Verify all returned templates are USER provider
|
||||
for (NotificationTemplate template : userTemplates.getData()) {
|
||||
assertEquals(ProviderType.USER, template.getProvider());
|
||||
}
|
||||
|
||||
// Verify our created templates are in the results
|
||||
boolean found1 =
|
||||
userTemplates.getData().stream().anyMatch(t -> t.getId().equals(template1.getId()));
|
||||
boolean found2 =
|
||||
userTemplates.getData().stream().anyMatch(t -> t.getId().equals(template2.getId()));
|
||||
|
||||
assertTrue(found1, "Template1 should be in USER filtered results");
|
||||
assertTrue(found2, "Template2 should be in USER filtered results");
|
||||
|
||||
// List only SYSTEM templates (use lowercase value as stored in JSON)
|
||||
params.put("provider", "system");
|
||||
ResultList<NotificationTemplate> systemTemplates = listEntities(params, ADMIN_AUTH_HEADERS);
|
||||
|
||||
// Verify all returned templates are SYSTEM provider
|
||||
for (NotificationTemplate template : systemTemplates.getData()) {
|
||||
assertEquals(ProviderType.SYSTEM, template.getProvider());
|
||||
}
|
||||
|
||||
// Verify our USER templates are NOT in SYSTEM results
|
||||
assertFalse(
|
||||
systemTemplates.getData().stream().anyMatch(t -> t.getId().equals(template1.getId())));
|
||||
assertFalse(
|
||||
systemTemplates.getData().stream().anyMatch(t -> t.getId().equals(template2.getId())));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
{
|
||||
"$id": "https://open-metadata.org/schema/api/events/createNotificationTemplate.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "CreateNotificationTemplate",
|
||||
"description": "Create request for Notification Template",
|
||||
"type": "object",
|
||||
"javaType": "org.openmetadata.schema.api.events.CreateNotificationTemplate",
|
||||
"javaInterfaces": ["org.openmetadata.schema.CreateEntity"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name that uniquely identifies this notification template (e.g., 'entity_change', 'test_change')",
|
||||
"$ref": "../../type/basic.json#/definitions/entityName"
|
||||
},
|
||||
"displayName": {
|
||||
"description": "Display name for this notification template",
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Description of this notification template",
|
||||
"$ref": "../../type/basic.json#/definitions/markdown"
|
||||
},
|
||||
"templateBody": {
|
||||
"description": "Handlebars template content for rendering notifications",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 10240
|
||||
},
|
||||
"owners": {
|
||||
"description": "Owners of this template",
|
||||
"$ref": "../../type/entityReferenceList.json",
|
||||
"default": null
|
||||
},
|
||||
"domains": {
|
||||
"description": "Fully qualified names of the domains the template belongs to",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "templateBody"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
{
|
||||
"$id": "https://open-metadata.org/schema/entity/events/notificationTemplate.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "NotificationTemplate",
|
||||
"$comment": "@om-entity-type",
|
||||
"description": "A NotificationTemplate defines the default formatting template for notifications of a specific entity type.",
|
||||
"type": "object",
|
||||
"javaType": "org.openmetadata.schema.entity.events.NotificationTemplate",
|
||||
"javaInterfaces": [
|
||||
"org.openmetadata.schema.EntityInterface"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Unique identifier of this template instance.",
|
||||
"$ref": "../../type/basic.json#/definitions/uuid"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name for the notification template (e.g., 'Default Table Template', 'Custom Pipeline Alerts').",
|
||||
"$ref": "../../type/basic.json#/definitions/entityName"
|
||||
},
|
||||
"displayName": {
|
||||
"description": "Display Name that identifies this template.",
|
||||
"type": "string"
|
||||
},
|
||||
"fullyQualifiedName": {
|
||||
"description": "Fully qualified name for the template.",
|
||||
"$ref": "../../type/basic.json#/definitions/fullyQualifiedEntityName"
|
||||
},
|
||||
"description": {
|
||||
"description": "Description of the template purpose and usage.",
|
||||
"$ref": "../../type/basic.json#/definitions/markdown"
|
||||
},
|
||||
"version": {
|
||||
"description": "Metadata version of the template.",
|
||||
"$ref": "../../type/entityHistory.json#/definitions/entityVersion"
|
||||
},
|
||||
"updatedAt": {
|
||||
"description": "Last update time corresponding to the new version of the template.",
|
||||
"$ref": "../../type/basic.json#/definitions/timestamp"
|
||||
},
|
||||
"updatedBy": {
|
||||
"description": "User who made the update.",
|
||||
"type": "string"
|
||||
},
|
||||
"href": {
|
||||
"description": "Link to this template resource.",
|
||||
"$ref": "../../type/basic.json#/definitions/href"
|
||||
},
|
||||
"templateBody": {
|
||||
"description": "Handlebars HTML template body with placeholders.",
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 10240
|
||||
},
|
||||
"provider": {
|
||||
"description": "Provider of the template. System templates are pre-loaded and cannot be deleted. User templates are created by users and can be deleted.",
|
||||
"$ref": "../../type/basic.json#/definitions/providerType"
|
||||
},
|
||||
"changeDescription": {
|
||||
"description": "Change that lead to this version of the template.",
|
||||
"$ref": "../../type/entityHistory.json#/definitions/changeDescription"
|
||||
},
|
||||
"incrementalChangeDescription": {
|
||||
"description": "Change that lead to this version of the entity.",
|
||||
"$ref": "../../type/entityHistory.json#/definitions/changeDescription"
|
||||
},
|
||||
"deleted": {
|
||||
"description": "When `true` indicates the template has been soft deleted.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"templateBody"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
@ -44,7 +44,7 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
sourcemap: true,
|
||||
minify: 'terser',
|
||||
minify: 'esbuild',
|
||||
target: 'es2020'
|
||||
},
|
||||
resolve: {
|
||||
|
||||
@ -95,7 +95,6 @@ test.describe('Explore Sort Order Filter', () => {
|
||||
await page.getByTestId('update-btn').click();
|
||||
|
||||
await selectSortOrder(page, 'Name');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await verifyEntitiesAreSorted(page);
|
||||
|
||||
const clearFilters = page.getByTestId('clear-filters');
|
||||
|
||||
@ -257,31 +257,42 @@ export const selectSortOrder = async (page: Page, sortOrder: string) => {
|
||||
await page.waitForSelector(`role=menuitem[name="${sortOrder}"]`, {
|
||||
state: 'visible',
|
||||
});
|
||||
const nameFilter = page.waitForResponse(
|
||||
`/api/v1/search/query?q=&index=dataAsset&*sort_field=displayName.keyword&sort_order=desc*`
|
||||
);
|
||||
await page.getByRole('menuitem', { name: sortOrder }).click();
|
||||
await nameFilter;
|
||||
|
||||
await expect(page.getByTestId('sorting-dropdown-label')).toHaveText(
|
||||
sortOrder
|
||||
);
|
||||
|
||||
const ascSortOrder = page.waitForResponse(
|
||||
`/api/v1/search/query?q=&index=dataAsset&*sort_field=displayName.keyword&sort_order=asc*`
|
||||
);
|
||||
await page.getByTestId('sort-order-button').click();
|
||||
await ascSortOrder;
|
||||
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
||||
};
|
||||
|
||||
export const verifyEntitiesAreSorted = async (page: Page) => {
|
||||
// Wait for search results to be stable after sort
|
||||
await page.waitForSelector('[data-testid="search-results"]', {
|
||||
state: 'visible',
|
||||
});
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const entityNames = await page.$$eval(
|
||||
'[data-testid="search-results"] .explore-search-card [data-testid="entity-link"]',
|
||||
(elements) => elements.map((el) => el.textContent?.trim() ?? '')
|
||||
);
|
||||
|
||||
// Normalize for case insensitivity, but retain punctuation
|
||||
const normalize = (str: string) => str.toLowerCase().trim();
|
||||
|
||||
// Sort using ASCII-based string comparison (ES behavior)
|
||||
// Elasticsearch keyword field with case-insensitive sorting
|
||||
const sortedEntityNames = [...entityNames].sort((a, b) => {
|
||||
const normA = normalize(a);
|
||||
const normB = normalize(b);
|
||||
const aLower = a.toLowerCase();
|
||||
const bLower = b.toLowerCase();
|
||||
|
||||
return normA < normB ? -1 : normA > normB ? 1 : 0;
|
||||
return aLower < bLower ? -1 : aLower > bLower ? 1 : 0;
|
||||
});
|
||||
|
||||
expect(entityNames).toEqual(sortedEntityNames);
|
||||
|
||||
6
pom.xml
6
pom.xml
@ -117,6 +117,7 @@
|
||||
<log4j.version>2.21.0</log4j.version>
|
||||
<org.junit.jupiter.version>5.9.3</org.junit.jupiter.version>
|
||||
<dropwizard-health.version>4.0.14</dropwizard-health.version>
|
||||
<handlebars.version>4.3.1</handlebars.version>
|
||||
<fernet.version>1.5.0</fernet.version>
|
||||
<antlr.version>4.13.2</antlr.version>
|
||||
|
||||
@ -608,6 +609,11 @@
|
||||
<groupId>com.github.java-json-tools</groupId>
|
||||
<artifactId>json-patch</artifactId>
|
||||
<version>${json-patch.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.jknack</groupId>
|
||||
<artifactId>handlebars</artifactId>
|
||||
<version>${handlebars.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.azure</groupId>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user