mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-11-13 01:13:39 +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
|
-- Fetch updated searchSettings
|
||||||
DELETE FROM openmetadata_settings WHERE configType = '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
|
-- 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);
|
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
|
-- Fetch updated searchSettings
|
||||||
DELETE FROM openmetadata_settings WHERE configType = '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
|
-- 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);
|
ALTER TABLE ACT_RU_EVENT_SUBSCR ALTER COLUMN ACTIVITY_ID_ TYPE varchar(255);
|
||||||
|
|
||||||
|
|||||||
@ -917,6 +917,11 @@
|
|||||||
<artifactId>client-java</artifactId>
|
<artifactId>client-java</artifactId>
|
||||||
<version>${kubernetes-client.version}</version>
|
<version>${kubernetes-client.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Handlebars Template Engine -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.jknack</groupId>
|
||||||
|
<artifactId>handlebars</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.slack.api</groupId>
|
<groupId>com.slack.api</groupId>
|
||||||
<artifactId>bolt-servlet</artifactId>
|
<artifactId>bolt-servlet</artifactId>
|
||||||
|
|||||||
@ -236,6 +236,7 @@ public final class Entity {
|
|||||||
//
|
//
|
||||||
// Other entities
|
// Other entities
|
||||||
public static final String EVENT_SUBSCRIPTION = "eventsubscription";
|
public static final String EVENT_SUBSCRIPTION = "eventsubscription";
|
||||||
|
public static final String NOTIFICATION_TEMPLATE = "notificationTemplate";
|
||||||
public static final String THREAD = "THREAD";
|
public static final String THREAD = "THREAD";
|
||||||
public static final String SUGGESTION = "SUGGESTION";
|
public static final String SUGGESTION = "SUGGESTION";
|
||||||
public static final String WORKFLOW = "workflow";
|
public static final String WORKFLOW = "workflow";
|
||||||
|
|||||||
@ -284,6 +284,11 @@ public class CachedCollectionDAO implements CollectionDAO {
|
|||||||
return delegate.eventSubscriptionDAO();
|
return delegate.eventSubscriptionDAO();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NotificationTemplateDAO notificationTemplateDAO() {
|
||||||
|
return delegate.notificationTemplateDAO();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IngestionPipelineDAO ingestionPipelineDAO() {
|
public IngestionPipelineDAO ingestionPipelineDAO() {
|
||||||
return delegate.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.EventSubscription;
|
||||||
import org.openmetadata.schema.entity.events.FailedEvent;
|
import org.openmetadata.schema.entity.events.FailedEvent;
|
||||||
import org.openmetadata.schema.entity.events.FailedEventResponse;
|
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.policies.Policy;
|
||||||
import org.openmetadata.schema.entity.services.ApiService;
|
import org.openmetadata.schema.entity.services.ApiService;
|
||||||
import org.openmetadata.schema.entity.services.DashboardService;
|
import org.openmetadata.schema.entity.services.DashboardService;
|
||||||
@ -297,6 +298,9 @@ public interface CollectionDAO {
|
|||||||
@CreateSqlObject
|
@CreateSqlObject
|
||||||
EventSubscriptionDAO eventSubscriptionDAO();
|
EventSubscriptionDAO eventSubscriptionDAO();
|
||||||
|
|
||||||
|
@CreateSqlObject
|
||||||
|
NotificationTemplateDAO notificationTemplateDAO();
|
||||||
|
|
||||||
@CreateSqlObject
|
@CreateSqlObject
|
||||||
PolicyDAO policyDAO();
|
PolicyDAO policyDAO();
|
||||||
|
|
||||||
@ -2971,6 +2975,23 @@ public interface CollectionDAO {
|
|||||||
@Bind("eventSubscriptionId") String eventSubscriptionId);
|
@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> {
|
interface ChartDAO extends EntityDAO<Chart> {
|
||||||
@Override
|
@Override
|
||||||
default String getTableName() {
|
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",
|
"pipeline", "data",
|
||||||
"votes", "data",
|
"votes", "data",
|
||||||
"dataProduct", "domains",
|
"dataProduct", "domains",
|
||||||
"domain", "domains");
|
"domain", "domains",
|
||||||
|
"notificationTemplate", "events");
|
||||||
return entityTypeToSubdirectory.getOrDefault(entityType, "data");
|
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,
|
sourcemap: true,
|
||||||
minify: 'terser',
|
minify: 'esbuild',
|
||||||
target: 'es2020'
|
target: 'es2020'
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|||||||
@ -95,7 +95,6 @@ test.describe('Explore Sort Order Filter', () => {
|
|||||||
await page.getByTestId('update-btn').click();
|
await page.getByTestId('update-btn').click();
|
||||||
|
|
||||||
await selectSortOrder(page, 'Name');
|
await selectSortOrder(page, 'Name');
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
await verifyEntitiesAreSorted(page);
|
await verifyEntitiesAreSorted(page);
|
||||||
|
|
||||||
const clearFilters = page.getByTestId('clear-filters');
|
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}"]`, {
|
await page.waitForSelector(`role=menuitem[name="${sortOrder}"]`, {
|
||||||
state: 'visible',
|
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 page.getByRole('menuitem', { name: sortOrder }).click();
|
||||||
|
await nameFilter;
|
||||||
|
|
||||||
await expect(page.getByTestId('sorting-dropdown-label')).toHaveText(
|
await expect(page.getByTestId('sorting-dropdown-label')).toHaveText(
|
||||||
sortOrder
|
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 page.getByTestId('sort-order-button').click();
|
||||||
|
await ascSortOrder;
|
||||||
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyEntitiesAreSorted = async (page: Page) => {
|
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(
|
const entityNames = await page.$$eval(
|
||||||
'[data-testid="search-results"] .explore-search-card [data-testid="entity-link"]',
|
'[data-testid="search-results"] .explore-search-card [data-testid="entity-link"]',
|
||||||
(elements) => elements.map((el) => el.textContent?.trim() ?? '')
|
(elements) => elements.map((el) => el.textContent?.trim() ?? '')
|
||||||
);
|
);
|
||||||
|
|
||||||
// Normalize for case insensitivity, but retain punctuation
|
// Elasticsearch keyword field with case-insensitive sorting
|
||||||
const normalize = (str: string) => str.toLowerCase().trim();
|
|
||||||
|
|
||||||
// Sort using ASCII-based string comparison (ES behavior)
|
|
||||||
const sortedEntityNames = [...entityNames].sort((a, b) => {
|
const sortedEntityNames = [...entityNames].sort((a, b) => {
|
||||||
const normA = normalize(a);
|
const aLower = a.toLowerCase();
|
||||||
const normB = normalize(b);
|
const bLower = b.toLowerCase();
|
||||||
|
|
||||||
return normA < normB ? -1 : normA > normB ? 1 : 0;
|
return aLower < bLower ? -1 : aLower > bLower ? 1 : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(entityNames).toEqual(sortedEntityNames);
|
expect(entityNames).toEqual(sortedEntityNames);
|
||||||
|
|||||||
6
pom.xml
6
pom.xml
@ -117,6 +117,7 @@
|
|||||||
<log4j.version>2.21.0</log4j.version>
|
<log4j.version>2.21.0</log4j.version>
|
||||||
<org.junit.jupiter.version>5.9.3</org.junit.jupiter.version>
|
<org.junit.jupiter.version>5.9.3</org.junit.jupiter.version>
|
||||||
<dropwizard-health.version>4.0.14</dropwizard-health.version>
|
<dropwizard-health.version>4.0.14</dropwizard-health.version>
|
||||||
|
<handlebars.version>4.3.1</handlebars.version>
|
||||||
<fernet.version>1.5.0</fernet.version>
|
<fernet.version>1.5.0</fernet.version>
|
||||||
<antlr.version>4.13.2</antlr.version>
|
<antlr.version>4.13.2</antlr.version>
|
||||||
|
|
||||||
@ -608,6 +609,11 @@
|
|||||||
<groupId>com.github.java-json-tools</groupId>
|
<groupId>com.github.java-json-tools</groupId>
|
||||||
<artifactId>json-patch</artifactId>
|
<artifactId>json-patch</artifactId>
|
||||||
<version>${json-patch.version}</version>
|
<version>${json-patch.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.jknack</groupId>
|
||||||
|
<artifactId>handlebars</artifactId>
|
||||||
|
<version>${handlebars.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.microsoft.azure</groupId>
|
<groupId>com.microsoft.azure</groupId>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user