feat(graphql) Add upsertPageModule graphql endpoint for home page (#13981)

This commit is contained in:
Chris Collins 2025-07-09 12:17:33 -04:00 committed by GitHub
parent beb6233e8b
commit 0beebae081
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 886 additions and 0 deletions

View File

@ -139,6 +139,7 @@ import com.linkedin.datahub.graphql.resolvers.load.LoadableTypeResolver;
import com.linkedin.datahub.graphql.resolvers.load.OwnerTypeBatchResolver;
import com.linkedin.datahub.graphql.resolvers.load.OwnerTypeResolver;
import com.linkedin.datahub.graphql.resolvers.load.TimeSeriesAspectResolver;
import com.linkedin.datahub.graphql.resolvers.module.UpsertPageModuleResolver;
import com.linkedin.datahub.graphql.resolvers.mutate.AddLinkResolver;
import com.linkedin.datahub.graphql.resolvers.mutate.AddOwnerResolver;
import com.linkedin.datahub.graphql.resolvers.mutate.AddOwnersResolver;
@ -335,6 +336,7 @@ import com.linkedin.metadata.service.ERModelRelationshipService;
import com.linkedin.metadata.service.FormService;
import com.linkedin.metadata.service.LineageService;
import com.linkedin.metadata.service.OwnershipTypeService;
import com.linkedin.metadata.service.PageModuleService;
import com.linkedin.metadata.service.PageTemplateService;
import com.linkedin.metadata.service.QueryService;
import com.linkedin.metadata.service.SettingsService;
@ -412,6 +414,7 @@ public class GmsGraphQLEngine {
private final EntityVersioningService entityVersioningService;
private final ApplicationService applicationService;
private final PageTemplateService pageTemplateService;
private final PageModuleService pageModuleService;
private final BusinessAttributeService businessAttributeService;
private final FeatureFlags featureFlags;
@ -542,6 +545,7 @@ public class GmsGraphQLEngine {
this.dataProductService = args.dataProductService;
this.applicationService = args.applicationService;
this.pageTemplateService = args.pageTemplateService;
this.pageModuleService = args.pageModuleService;
this.formService = args.formService;
this.restrictedService = args.restrictedService;
this.connectionService = args.connectionService;
@ -1375,6 +1379,7 @@ public class GmsGraphQLEngine {
.dataFetcher("updateForm", new UpdateFormResolver(this.entityClient))
.dataFetcher(
"upsertPageTemplate", new UpsertPageTemplateResolver(this.pageTemplateService))
.dataFetcher("upsertPageModule", new UpsertPageModuleResolver(this.pageModuleService))
.dataFetcher(
"updateDocPropagationSettings",
new UpdateDocPropagationSettingsResolver(this.settingsService))

View File

@ -38,6 +38,7 @@ import com.linkedin.metadata.service.ERModelRelationshipService;
import com.linkedin.metadata.service.FormService;
import com.linkedin.metadata.service.LineageService;
import com.linkedin.metadata.service.OwnershipTypeService;
import com.linkedin.metadata.service.PageModuleService;
import com.linkedin.metadata.service.PageTemplateService;
import com.linkedin.metadata.service.QueryService;
import com.linkedin.metadata.service.SettingsService;
@ -100,6 +101,7 @@ public class GmsGraphQLEngineArgs {
EntityVersioningService entityVersioningService;
ApplicationService applicationService;
PageTemplateService pageTemplateService;
PageModuleService pageModuleService;
boolean systemTelemetryEnabled;
MetricUtils metricUtils;

View File

@ -0,0 +1,116 @@
package com.linkedin.datahub.graphql.resolvers.module;
import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
import com.linkedin.datahub.graphql.generated.DataHubPageModule;
import com.linkedin.datahub.graphql.generated.DataHubPageModuleType;
import com.linkedin.datahub.graphql.generated.PageModuleScope;
import com.linkedin.datahub.graphql.generated.UpsertPageModuleInput;
import com.linkedin.datahub.graphql.types.module.PageModuleMapper;
import com.linkedin.entity.EntityResponse;
import com.linkedin.metadata.service.PageModuleService;
import com.linkedin.module.DataHubPageModuleParams;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nonnull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequiredArgsConstructor
public class UpsertPageModuleResolver implements DataFetcher<CompletableFuture<DataHubPageModule>> {
private final PageModuleService _pageModuleService;
@Override
public CompletableFuture<DataHubPageModule> get(DataFetchingEnvironment environment)
throws Exception {
final QueryContext context = environment.getContext();
final UpsertPageModuleInput input =
bindArgument(environment.getArgument("input"), UpsertPageModuleInput.class);
String urn = input.getUrn();
String name = input.getName();
DataHubPageModuleType type = input.getType();
PageModuleScope scope = input.getScope();
com.linkedin.datahub.graphql.generated.PageModuleParamsInput paramsInput = input.getParams();
// TODO: check permissions if the scope is GLOBAL
return GraphQLConcurrencyUtils.supplyAsync(
() -> {
try {
// Map GraphQL input to GMS types
com.linkedin.module.DataHubPageModuleType gmsType =
com.linkedin.module.DataHubPageModuleType.valueOf(type.toString());
com.linkedin.module.PageModuleScope gmsScope =
com.linkedin.module.PageModuleScope.valueOf(scope.toString());
DataHubPageModuleParams gmsParams = mapParamsInput(paramsInput);
validateInput(gmsType, gmsParams);
final Urn moduleUrn =
_pageModuleService.upsertPageModule(
context.getOperationContext(), urn, name, gmsType, gmsScope, gmsParams);
EntityResponse response =
_pageModuleService.getPageModuleEntityResponse(
context.getOperationContext(), moduleUrn);
return PageModuleMapper.map(context, response);
} catch (Exception e) {
throw new RuntimeException(
String.format(
"Failed to perform upsert page module update against input %s", input),
e);
}
},
this.getClass().getSimpleName(),
"get");
}
@Nonnull
private DataHubPageModuleParams mapParamsInput(
com.linkedin.datahub.graphql.generated.PageModuleParamsInput paramsInput) {
DataHubPageModuleParams gmsParams = new DataHubPageModuleParams();
if (paramsInput.getLinkParams() != null) {
com.linkedin.module.LinkModuleParams linkParams = new com.linkedin.module.LinkModuleParams();
linkParams.setLinkUrn(UrnUtils.getUrn(paramsInput.getLinkParams().getLinkUrn()));
gmsParams.setLinkParams(linkParams);
}
if (paramsInput.getRichTextParams() != null) {
com.linkedin.module.RichTextModuleParams richTextParams =
new com.linkedin.module.RichTextModuleParams();
richTextParams.setContent(paramsInput.getRichTextParams().getContent());
gmsParams.setRichTextParams(richTextParams);
}
return gmsParams;
}
private void validateInput(
@Nonnull final com.linkedin.module.DataHubPageModuleType type,
@Nonnull final DataHubPageModuleParams params) {
// check if we provide the correct params given the type of module we're creating
if (type.equals(com.linkedin.module.DataHubPageModuleType.RICH_TEXT)) {
if (params.getRichTextParams() == null) {
throw new IllegalArgumentException("Did not provide rich text params for rich text module");
}
} else if (type.equals(com.linkedin.module.DataHubPageModuleType.LINK)) {
if (params.getLinkParams() == null) {
throw new IllegalArgumentException("Did not provide link params for link module");
}
} else {
// TODO: add more blocks to this check as we support creating more types of modules to this
// resolver
// If someone tries to create one of the default modules this error will be thrown
throw new IllegalArgumentException("Attempted to create an unsupported module type.");
}
}
}

View File

@ -23,6 +23,78 @@ type DataHubPageModule implements Entity {
relationships(input: RelationshipsInput!): EntityRelationshipsResult
}
extend type Mutation {
"""
Create or update a DataHub page module
"""
upsertPageModule(input: UpsertPageModuleInput!): DataHubPageModule!
}
"""
Input for creating or updating a DataHub page module
"""
input UpsertPageModuleInput {
"""
The URN of the page module to update. If not provided, a new module will be created.
"""
urn: String
"""
The display name of this module
"""
name: String!
"""
The type of this module
"""
type: DataHubPageModuleType!
"""
The scope of this module and who can use/see it
"""
scope: PageModuleScope!
"""
The specific parameters stored for this module
"""
params: PageModuleParamsInput!
}
"""
Input for the specific parameters stored for a module
"""
input PageModuleParamsInput {
"""
The params required if the module is type LINK
"""
linkParams: LinkModuleParamsInput
"""
The params required if the module is type RICH_TEXT
"""
richTextParams: RichTextModuleParamsInput
}
"""
Input for the params required if the module is type LINK
"""
input LinkModuleParamsInput {
"""
The URN of the Post entity containing the link
"""
linkUrn: String!
}
"""
Input for the params required if the module is type RICH_TEXT
"""
input RichTextModuleParamsInput {
"""
The content of the rich text module
"""
content: String!
}
"""
The main properties of a DataHub page module
"""

View File

@ -0,0 +1,295 @@
package com.linkedin.datahub.graphql.resolvers.module;
import static com.linkedin.datahub.graphql.TestUtils.getMockAllowContext;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertThrows;
import com.linkedin.common.AuditStamp;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.generated.DataHubPageModule;
import com.linkedin.datahub.graphql.generated.DataHubPageModuleType;
import com.linkedin.datahub.graphql.generated.LinkModuleParamsInput;
import com.linkedin.datahub.graphql.generated.PageModuleParamsInput;
import com.linkedin.datahub.graphql.generated.PageModuleScope;
import com.linkedin.datahub.graphql.generated.RichTextModuleParamsInput;
import com.linkedin.datahub.graphql.generated.UpsertPageModuleInput;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.EnvelopedAspect;
import com.linkedin.entity.EnvelopedAspectMap;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.service.PageModuleService;
import com.linkedin.module.DataHubPageModuleParams;
import com.linkedin.module.DataHubPageModuleProperties;
import com.linkedin.module.RichTextModuleParams;
import graphql.schema.DataFetchingEnvironment;
import java.util.concurrent.CompletableFuture;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class UpsertPageModuleResolverTest {
private static final String TEST_MODULE_URN = "urn:li:dataHubPageModule:test-module";
private static final String TEST_MODULE_NAME = "Test Module";
private static final String TEST_RICH_TEXT_CONTENT = "Test content";
private PageModuleService mockService;
private UpsertPageModuleResolver resolver;
private DataFetchingEnvironment mockEnvironment;
private QueryContext mockQueryContext;
@BeforeMethod
public void setUp() {
mockService = mock(PageModuleService.class);
resolver = new UpsertPageModuleResolver(mockService);
mockEnvironment = mock(DataFetchingEnvironment.class);
mockQueryContext = getMockAllowContext();
}
@Test
public void testUpsertPageModuleSuccessWithUrn() throws Exception {
// Arrange
UpsertPageModuleInput input = new UpsertPageModuleInput();
input.setUrn(TEST_MODULE_URN);
input.setName(TEST_MODULE_NAME);
input.setType(DataHubPageModuleType.RICH_TEXT);
input.setScope(PageModuleScope.PERSONAL);
RichTextModuleParamsInput richTextParams = new RichTextModuleParamsInput();
richTextParams.setContent(TEST_RICH_TEXT_CONTENT);
PageModuleParamsInput paramsInput = new PageModuleParamsInput();
paramsInput.setRichTextParams(richTextParams);
input.setParams(paramsInput);
Urn moduleUrn = UrnUtils.getUrn(TEST_MODULE_URN);
EntityResponse mockResponse = createMockEntityResponse(moduleUrn);
when(mockEnvironment.getArgument("input")).thenReturn(input);
when(mockEnvironment.getContext()).thenReturn(mockQueryContext);
when(mockService.upsertPageModule(any(), eq(TEST_MODULE_URN), any(), any(), any(), any()))
.thenReturn(moduleUrn);
when(mockService.getPageModuleEntityResponse(any(), eq(moduleUrn))).thenReturn(mockResponse);
// Act
CompletableFuture<DataHubPageModule> future = resolver.get(mockEnvironment);
DataHubPageModule result = future.get();
// Assert
assertNotNull(result);
assertEquals(result.getUrn(), TEST_MODULE_URN);
assertEquals(result.getType().toString(), "DATAHUB_PAGE_MODULE");
verify(mockService, times(1))
.upsertPageModule(any(), eq(TEST_MODULE_URN), any(), any(), any(), any());
verify(mockService, times(1)).getPageModuleEntityResponse(any(), eq(moduleUrn));
}
@Test
public void testUpsertPageModuleSuccessWithGeneratedUrn() throws Exception {
// Arrange
UpsertPageModuleInput input = new UpsertPageModuleInput();
input.setName(TEST_MODULE_NAME);
input.setType(DataHubPageModuleType.RICH_TEXT);
input.setScope(PageModuleScope.PERSONAL);
RichTextModuleParamsInput richTextParams = new RichTextModuleParamsInput();
richTextParams.setContent(TEST_RICH_TEXT_CONTENT);
PageModuleParamsInput paramsInput = new PageModuleParamsInput();
paramsInput.setRichTextParams(richTextParams);
input.setParams(paramsInput);
Urn moduleUrn = UrnUtils.getUrn(TEST_MODULE_URN);
EntityResponse mockResponse = createMockEntityResponse(moduleUrn);
when(mockEnvironment.getArgument("input")).thenReturn(input);
when(mockEnvironment.getContext()).thenReturn(mockQueryContext);
when(mockService.upsertPageModule(any(), eq(null), any(), any(), any(), any()))
.thenReturn(moduleUrn);
when(mockService.getPageModuleEntityResponse(any(), eq(moduleUrn))).thenReturn(mockResponse);
// Act
CompletableFuture<DataHubPageModule> future = resolver.get(mockEnvironment);
DataHubPageModule result = future.get();
// Assert
assertNotNull(result);
assertEquals(result.getUrn(), TEST_MODULE_URN);
assertEquals(result.getType().toString(), "DATAHUB_PAGE_MODULE");
verify(mockService, times(1)).upsertPageModule(any(), eq(null), any(), any(), any(), any());
verify(mockService, times(1)).getPageModuleEntityResponse(any(), eq(moduleUrn));
}
@Test
public void testUpsertPageModuleWithLinkParams() throws Exception {
// Arrange
UpsertPageModuleInput input = new UpsertPageModuleInput();
input.setName(TEST_MODULE_NAME);
input.setType(DataHubPageModuleType.LINK);
input.setScope(PageModuleScope.PERSONAL);
PageModuleParamsInput paramsInput = new PageModuleParamsInput();
paramsInput.setLinkParams(new LinkModuleParamsInput());
paramsInput.getLinkParams().setLinkUrn("urn:li:post:test-post");
input.setParams(paramsInput);
Urn moduleUrn = UrnUtils.getUrn(TEST_MODULE_URN);
EntityResponse mockResponse = createMockEntityResponse(moduleUrn);
when(mockEnvironment.getArgument("input")).thenReturn(input);
when(mockEnvironment.getContext()).thenReturn(mockQueryContext);
when(mockService.upsertPageModule(any(), any(), any(), any(), any(), any()))
.thenReturn(moduleUrn);
when(mockService.getPageModuleEntityResponse(any(), eq(moduleUrn))).thenReturn(mockResponse);
// Act
CompletableFuture<DataHubPageModule> future = resolver.get(mockEnvironment);
DataHubPageModule result = future.get();
// Assert
assertNotNull(result);
assertEquals(result.getUrn(), TEST_MODULE_URN);
verify(mockService, times(1)).upsertPageModule(any(), any(), any(), any(), any(), any());
verify(mockService, times(1)).getPageModuleEntityResponse(any(), eq(moduleUrn));
}
@Test
public void testUpsertPageModuleValidationFailureRichTextWithoutParams() throws Exception {
// Arrange
UpsertPageModuleInput input = new UpsertPageModuleInput();
input.setName(TEST_MODULE_NAME);
input.setType(DataHubPageModuleType.RICH_TEXT);
input.setScope(PageModuleScope.PERSONAL);
// Don't set rich text params
PageModuleParamsInput paramsInput = new PageModuleParamsInput();
input.setParams(paramsInput);
when(mockEnvironment.getArgument("input")).thenReturn(input);
when(mockEnvironment.getContext()).thenReturn(mockQueryContext);
// Act & Assert
assertThrows(RuntimeException.class, () -> resolver.get(mockEnvironment).join());
}
@Test
public void testUpsertPageModuleValidationFailureLinkWithoutParams() throws Exception {
// Arrange
UpsertPageModuleInput input = new UpsertPageModuleInput();
input.setName(TEST_MODULE_NAME);
input.setType(DataHubPageModuleType.LINK);
input.setScope(PageModuleScope.PERSONAL);
// Don't set link params
PageModuleParamsInput paramsInput = new PageModuleParamsInput();
input.setParams(paramsInput);
when(mockEnvironment.getArgument("input")).thenReturn(input);
when(mockEnvironment.getContext()).thenReturn(mockQueryContext);
// Act & Assert
assertThrows(RuntimeException.class, () -> resolver.get(mockEnvironment).join());
}
@Test
public void testUpsertPageModuleValidationFailureUnsupportedModuleType() throws Exception {
// Arrange
UpsertPageModuleInput input = new UpsertPageModuleInput();
input.setName(TEST_MODULE_NAME);
input.setType(DataHubPageModuleType.ASSET_COLLECTION); // Unsupported type
input.setScope(PageModuleScope.PERSONAL);
PageModuleParamsInput paramsInput = new PageModuleParamsInput();
input.setParams(paramsInput);
when(mockEnvironment.getArgument("input")).thenReturn(input);
when(mockEnvironment.getContext()).thenReturn(mockQueryContext);
// Act & Assert
assertThrows(RuntimeException.class, () -> resolver.get(mockEnvironment).join());
}
@Test
public void testUpsertPageModuleValidationFailureRichTextWithWrongParams() throws Exception {
// Arrange
UpsertPageModuleInput input = new UpsertPageModuleInput();
input.setName(TEST_MODULE_NAME);
input.setType(DataHubPageModuleType.RICH_TEXT);
input.setScope(PageModuleScope.PERSONAL);
// Set link params instead of rich text params
PageModuleParamsInput paramsInput = new PageModuleParamsInput();
paramsInput.setLinkParams(new LinkModuleParamsInput());
paramsInput.getLinkParams().setLinkUrn("urn:li:post:test-post");
input.setParams(paramsInput);
when(mockEnvironment.getArgument("input")).thenReturn(input);
when(mockEnvironment.getContext()).thenReturn(mockQueryContext);
// Act & Assert
assertThrows(RuntimeException.class, () -> resolver.get(mockEnvironment).join());
}
@Test
public void testUpsertPageModuleValidationFailureLinkWithWrongParams() throws Exception {
// Arrange
UpsertPageModuleInput input = new UpsertPageModuleInput();
input.setName(TEST_MODULE_NAME);
input.setType(DataHubPageModuleType.LINK);
input.setScope(PageModuleScope.PERSONAL);
// Set rich text params instead of link params
PageModuleParamsInput paramsInput = new PageModuleParamsInput();
RichTextModuleParamsInput richTextParams = new RichTextModuleParamsInput();
richTextParams.setContent(TEST_RICH_TEXT_CONTENT);
paramsInput.setRichTextParams(richTextParams);
input.setParams(paramsInput);
when(mockEnvironment.getArgument("input")).thenReturn(input);
when(mockEnvironment.getContext()).thenReturn(mockQueryContext);
// Act & Assert
assertThrows(RuntimeException.class, () -> resolver.get(mockEnvironment).join());
}
private EntityResponse createMockEntityResponse(Urn moduleUrn) {
DataHubPageModuleProperties properties = new DataHubPageModuleProperties();
properties.setName(TEST_MODULE_NAME);
properties.setType(com.linkedin.module.DataHubPageModuleType.RICH_TEXT);
com.linkedin.module.PageModuleScope gmsScope = com.linkedin.module.PageModuleScope.PERSONAL;
com.linkedin.module.DataHubPageModuleVisibility visibility =
new com.linkedin.module.DataHubPageModuleVisibility();
visibility.setScope(gmsScope);
properties.setVisibility(visibility);
DataHubPageModuleParams params = new DataHubPageModuleParams();
RichTextModuleParams richTextParams = new RichTextModuleParams();
richTextParams.setContent(TEST_RICH_TEXT_CONTENT);
params.setRichTextParams(richTextParams);
properties.setParams(params);
AuditStamp auditStamp = new AuditStamp();
auditStamp.setTime(System.currentTimeMillis());
auditStamp.setActor(UrnUtils.getUrn("urn:li:corpuser:test-user"));
properties.setCreated(auditStamp);
properties.setLastModified(auditStamp);
EntityResponse response = new EntityResponse();
response.setUrn(moduleUrn);
EnvelopedAspectMap aspectMap = new EnvelopedAspectMap();
EnvelopedAspect aspect = new EnvelopedAspect();
aspect.setValue(new com.linkedin.entity.Aspect(properties.data()));
aspectMap.put(Constants.DATAHUB_PAGE_MODULE_PROPERTIES_ASPECT_NAME, aspect);
response.setAspects(aspectMap);
return response;
}
}

View File

@ -41,6 +41,7 @@ import com.linkedin.metadata.service.ERModelRelationshipService;
import com.linkedin.metadata.service.FormService;
import com.linkedin.metadata.service.LineageService;
import com.linkedin.metadata.service.OwnershipTypeService;
import com.linkedin.metadata.service.PageModuleService;
import com.linkedin.metadata.service.PageTemplateService;
import com.linkedin.metadata.service.QueryService;
import com.linkedin.metadata.service.SettingsService;
@ -214,6 +215,10 @@ public class GraphQLEngineFactory {
@Qualifier("pageTemplateService")
private PageTemplateService pageTemplateService;
@Autowired
@Qualifier("pageModuleService")
private PageModuleService pageModuleService;
@Bean(name = "graphQLEngine")
@Nonnull
protected GraphQLEngine graphQLEngine(
@ -270,6 +275,7 @@ public class GraphQLEngineFactory {
args.setDataProductService(dataProductService);
args.setApplicationService(applicationService);
args.setPageTemplateService(pageTemplateService);
args.setPageModuleService(pageModuleService);
args.setGraphQLConfiguration(configProvider.getGraphQL());
args.setBusinessAttributeService(businessAttributeService);
args.setChromeExtensionConfiguration(configProvider.getChromeExtension());

View File

@ -0,0 +1,18 @@
package com.linkedin.gms.factory.module;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.service.PageModuleService;
import javax.annotation.Nonnull;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class PageModuleServiceFactory {
@Bean(name = "pageModuleService")
@Nonnull
protected PageModuleService getInstance(
@Qualifier("entityClient") @Nonnull final EntityClient entityClient) {
return new PageModuleService(entityClient);
}
}

View File

@ -0,0 +1,130 @@
package com.linkedin.metadata.service;
import com.linkedin.common.AuditStamp;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.entity.AspectUtils;
import com.linkedin.metadata.key.DataHubPageModuleKey;
import com.linkedin.metadata.utils.EntityKeyUtils;
import com.linkedin.module.DataHubPageModuleParams;
import com.linkedin.module.DataHubPageModuleProperties;
import com.linkedin.module.DataHubPageModuleType;
import com.linkedin.module.DataHubPageModuleVisibility;
import com.linkedin.module.PageModuleScope;
import com.linkedin.mxe.MetadataChangeProposal;
import io.datahubproject.metadata.context.OperationContext;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class PageModuleService {
private final EntityClient entityClient;
public PageModuleService(@Nonnull EntityClient entityClient) {
this.entityClient = entityClient;
}
/**
* Upserts a DataHub page module. If the page module with the provided urn already exists, then it
* will be overwritten.
*
* <p>This method assumes that authorization has already been verified at the calling layer.
*
* @return the URN of the new page module.
*/
public Urn upsertPageModule(
@Nonnull OperationContext opContext,
@Nullable final String urn,
@Nonnull final String name,
@Nonnull final DataHubPageModuleType type,
@Nonnull final PageModuleScope scope,
@Nonnull final DataHubPageModuleParams params) {
Objects.requireNonNull(name, "name must not be null");
Objects.requireNonNull(type, "type must not be null");
Objects.requireNonNull(scope, "scope must not be null");
Objects.requireNonNull(params, "params must not be null");
// 1. Optionally generate new page module urn
Urn moduleUrn = null;
if (urn != null) {
moduleUrn = UrnUtils.getUrn(urn);
} else {
final String moduleId = UUID.randomUUID().toString();
final DataHubPageModuleKey key = new DataHubPageModuleKey().setId(moduleId);
moduleUrn =
EntityKeyUtils.convertEntityKeyToUrn(key, Constants.DATAHUB_PAGE_MODULE_ENTITY_NAME);
}
final AuditStamp nowAuditStamp = opContext.getAuditStamp();
// 2. Build Page Module Properties
DataHubPageModuleProperties properties = new DataHubPageModuleProperties();
DataHubPageModuleProperties existingProperties = getPageModuleProperties(opContext, moduleUrn);
if (existingProperties != null) {
properties = existingProperties;
} else {
// if creating a new page module, set the created stamp
properties.setCreated(nowAuditStamp);
}
properties.setName(name);
properties.setType(type);
DataHubPageModuleVisibility visibility = new DataHubPageModuleVisibility();
visibility.setScope(scope);
properties.setVisibility(visibility);
properties.setParams(params);
properties.setLastModified(nowAuditStamp);
// 3. Write changes to GMS
try {
final List<MetadataChangeProposal> aspectsToIngest = new ArrayList<>();
aspectsToIngest.add(
AspectUtils.buildMetadataChangeProposal(
moduleUrn, Constants.DATAHUB_PAGE_MODULE_PROPERTIES_ASPECT_NAME, properties));
entityClient.batchIngestProposals(opContext, aspectsToIngest, false);
} catch (Exception e) {
throw new RuntimeException(
String.format("Failed to upsert PageModule with urn %s", moduleUrn), e);
}
return moduleUrn;
}
@Nullable
public DataHubPageModuleProperties getPageModuleProperties(
@Nonnull OperationContext opContext, @Nonnull final Urn moduleUrn) {
Objects.requireNonNull(moduleUrn, "moduleUrn must not be null");
final EntityResponse response = getPageModuleEntityResponse(opContext, moduleUrn);
if (response != null
&& response
.getAspects()
.containsKey(Constants.DATAHUB_PAGE_MODULE_PROPERTIES_ASPECT_NAME)) {
return new DataHubPageModuleProperties(
response
.getAspects()
.get(Constants.DATAHUB_PAGE_MODULE_PROPERTIES_ASPECT_NAME)
.getValue()
.data());
}
// No aspect found
return null;
}
@Nullable
public EntityResponse getPageModuleEntityResponse(
@Nonnull OperationContext opContext, @Nonnull final Urn moduleUrn) {
try {
return entityClient.getV2(
opContext, Constants.DATAHUB_PAGE_MODULE_ENTITY_NAME, moduleUrn, null, false);
} catch (Exception e) {
throw new RuntimeException(
String.format("Failed to retrieve PageModule with urn %s", moduleUrn), e);
}
}
}

View File

@ -0,0 +1,242 @@
package com.linkedin.metadata.service;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertThrows;
import com.linkedin.common.AuditStamp;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.EnvelopedAspect;
import com.linkedin.entity.EnvelopedAspectMap;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.Constants;
import com.linkedin.module.DataHubPageModuleParams;
import com.linkedin.module.DataHubPageModuleProperties;
import com.linkedin.module.DataHubPageModuleType;
import com.linkedin.module.DataHubPageModuleVisibility;
import com.linkedin.module.PageModuleScope;
import com.linkedin.module.RichTextModuleParams;
import io.datahubproject.metadata.context.OperationContext;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class PageModuleServiceTest {
private static final String TEST_MODULE_URN = "urn:li:dataHubPageModule:test-module";
private static final String TEST_MODULE_NAME = "Test Module";
private static final String TEST_RICH_TEXT_CONTENT = "Test content";
@Mock private EntityClient mockEntityClient;
@Mock private OperationContext mockOpContext;
private PageModuleService service;
@BeforeMethod
public void setUp() {
MockitoAnnotations.openMocks(this);
service = new PageModuleService(mockEntityClient);
}
@Test
public void testUpsertPageModuleSuccessWithUrn() throws Exception {
// Arrange
Urn moduleUrn = UrnUtils.getUrn(TEST_MODULE_URN);
DataHubPageModuleType type = DataHubPageModuleType.RICH_TEXT;
PageModuleScope scope = PageModuleScope.PERSONAL;
DataHubPageModuleParams params = createTestParams();
when(mockOpContext.getAuditStamp()).thenReturn(createTestAuditStamp());
when(mockEntityClient.batchIngestProposals(any(), any(), eq(false))).thenReturn(null);
// Act
Urn result =
service.upsertPageModule(
mockOpContext, TEST_MODULE_URN, TEST_MODULE_NAME, type, scope, params);
// Assert
assertEquals(result, moduleUrn);
verify(mockEntityClient, times(1)).batchIngestProposals(any(), any(), eq(false));
}
@Test
public void testUpsertPageModuleSuccessWithGeneratedUrn() throws Exception {
// Arrange
DataHubPageModuleType type = DataHubPageModuleType.RICH_TEXT;
PageModuleScope scope = PageModuleScope.PERSONAL;
DataHubPageModuleParams params = createTestParams();
when(mockOpContext.getAuditStamp()).thenReturn(createTestAuditStamp());
when(mockEntityClient.batchIngestProposals(any(), any(), eq(false))).thenReturn(null);
// Act
Urn result =
service.upsertPageModule(mockOpContext, null, TEST_MODULE_NAME, type, scope, params);
// Assert
assertNotNull(result);
assertEquals(result.getEntityType(), "dataHubPageModule");
verify(mockEntityClient, times(1)).batchIngestProposals(any(), any(), eq(false));
}
@Test
public void testUpsertPageModuleFailure() throws Exception {
// Arrange
DataHubPageModuleType type = DataHubPageModuleType.RICH_TEXT;
PageModuleScope scope = PageModuleScope.PERSONAL;
DataHubPageModuleParams params = createTestParams();
when(mockOpContext.getAuditStamp()).thenReturn(createTestAuditStamp());
when(mockEntityClient.batchIngestProposals(any(), any(), eq(false)))
.thenThrow(new RuntimeException("Test exception"));
// Act & Assert
assertThrows(
RuntimeException.class,
() -> {
service.upsertPageModule(
mockOpContext, TEST_MODULE_URN, TEST_MODULE_NAME, type, scope, params);
});
}
@Test
public void testGetPageModulePropertiesSuccess() throws Exception {
// Arrange
Urn moduleUrn = UrnUtils.getUrn(TEST_MODULE_URN);
DataHubPageModuleProperties expectedProperties = createTestModuleProperties();
EntityResponse mockResponse = createMockEntityResponse(moduleUrn, expectedProperties);
when(mockEntityClient.getV2(
any(),
eq(Constants.DATAHUB_PAGE_MODULE_ENTITY_NAME),
eq(moduleUrn),
eq(null),
eq(false)))
.thenReturn(mockResponse);
// Act
DataHubPageModuleProperties result = service.getPageModuleProperties(mockOpContext, moduleUrn);
// Assert
assertNotNull(result);
assertEquals(result.getName(), TEST_MODULE_NAME);
assertEquals(result.getType(), DataHubPageModuleType.RICH_TEXT);
}
@Test
public void testGetPageModulePropertiesNotFound() throws Exception {
// Arrange
Urn moduleUrn = UrnUtils.getUrn(TEST_MODULE_URN);
when(mockEntityClient.getV2(
any(),
eq(Constants.DATAHUB_PAGE_MODULE_ENTITY_NAME),
eq(moduleUrn),
eq(null),
eq(false)))
.thenReturn(null);
// Act
DataHubPageModuleProperties result = service.getPageModuleProperties(mockOpContext, moduleUrn);
// Assert
assertEquals(result, null);
}
@Test
public void testGetPageModuleEntityResponseSuccess() throws Exception {
// Arrange
Urn moduleUrn = UrnUtils.getUrn(TEST_MODULE_URN);
EntityResponse expectedResponse =
createMockEntityResponse(moduleUrn, createTestModuleProperties());
when(mockEntityClient.getV2(
any(),
eq(Constants.DATAHUB_PAGE_MODULE_ENTITY_NAME),
eq(moduleUrn),
eq(null),
eq(false)))
.thenReturn(expectedResponse);
// Act
EntityResponse result = service.getPageModuleEntityResponse(mockOpContext, moduleUrn);
// Assert
assertNotNull(result);
assertEquals(result.getUrn(), moduleUrn);
}
@Test
public void testGetPageModuleEntityResponseFailure() throws Exception {
// Arrange
Urn moduleUrn = UrnUtils.getUrn(TEST_MODULE_URN);
when(mockEntityClient.getV2(
any(),
eq(Constants.DATAHUB_PAGE_MODULE_ENTITY_NAME),
eq(moduleUrn),
eq(null),
eq(false)))
.thenThrow(new RuntimeException("Test exception"));
// Act & Assert
assertThrows(
RuntimeException.class,
() -> {
service.getPageModuleEntityResponse(mockOpContext, moduleUrn);
});
}
private DataHubPageModuleParams createTestParams() {
DataHubPageModuleParams params = new DataHubPageModuleParams();
RichTextModuleParams richTextParams = new RichTextModuleParams();
richTextParams.setContent(TEST_RICH_TEXT_CONTENT);
params.setRichTextParams(richTextParams);
return params;
}
private DataHubPageModuleProperties createTestModuleProperties() {
DataHubPageModuleProperties properties = new DataHubPageModuleProperties();
properties.setName(TEST_MODULE_NAME);
properties.setType(DataHubPageModuleType.RICH_TEXT);
DataHubPageModuleVisibility visibility = new DataHubPageModuleVisibility();
visibility.setScope(PageModuleScope.PERSONAL);
properties.setVisibility(visibility);
properties.setParams(createTestParams());
properties.setCreated(createTestAuditStamp());
properties.setLastModified(createTestAuditStamp());
return properties;
}
private AuditStamp createTestAuditStamp() {
AuditStamp auditStamp = new AuditStamp();
auditStamp.setTime(System.currentTimeMillis());
auditStamp.setActor(UrnUtils.getUrn("urn:li:corpuser:test-user"));
return auditStamp;
}
private EntityResponse createMockEntityResponse(
Urn moduleUrn, DataHubPageModuleProperties properties) {
EntityResponse response = new EntityResponse();
response.setUrn(moduleUrn);
EnvelopedAspectMap aspectMap = new EnvelopedAspectMap();
EnvelopedAspect aspect = new EnvelopedAspect();
aspect.setValue(new com.linkedin.entity.Aspect(properties.data()));
aspectMap.put(Constants.DATAHUB_PAGE_MODULE_PROPERTIES_ASPECT_NAME, aspect);
response.setAspects(aspectMap);
return response;
}
}