Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(posts): add posts feature to DataHub #6110

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions datahub-frontend/play.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ play {
platform {
playVersion = '2.7.6'
scalaVersion = '2.12'
javaVersion = JavaVersion.VERSION_1_8
javaVersion = JavaVersion.VERSION_11
}

injectedRoutesGenerator = true
Expand All @@ -81,7 +81,7 @@ play {
model {
components {
play {
platform play: '2.7.6', scala: '2.12', java: '1.8'
platform play: '2.7.6', scala: '2.12', java: '11'
injectedRoutesGenerator = true

binaries.all {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.datahub.authentication.AuthenticationConfiguration;
import com.datahub.authentication.group.GroupService;
import com.datahub.authentication.invite.InviteTokenService;
import com.datahub.authentication.post.PostService;
import com.datahub.authentication.token.StatefulTokenService;
import com.datahub.authentication.user.NativeUserService;
import com.datahub.authorization.AuthorizationConfiguration;
Expand Down Expand Up @@ -175,6 +176,8 @@
import com.linkedin.datahub.graphql.resolvers.policy.GetGrantedPrivilegesResolver;
import com.linkedin.datahub.graphql.resolvers.policy.ListPoliciesResolver;
import com.linkedin.datahub.graphql.resolvers.policy.UpsertPolicyResolver;
import com.linkedin.datahub.graphql.resolvers.post.CreatePostResolver;
import com.linkedin.datahub.graphql.resolvers.post.ListPostsResolver;
import com.linkedin.datahub.graphql.resolvers.recommendation.ListRecommendationsResolver;
import com.linkedin.datahub.graphql.resolvers.role.AcceptRoleResolver;
import com.linkedin.datahub.graphql.resolvers.role.BatchAssignRoleResolver;
Expand Down Expand Up @@ -310,6 +313,7 @@ public class GmsGraphQLEngine {
private final GroupService groupService;
private final RoleService roleService;
private final InviteTokenService inviteTokenService;
private final PostService postService;

private final FeatureFlags featureFlags;

Expand Down Expand Up @@ -386,7 +390,7 @@ public GmsGraphQLEngine(final EntityClient entityClient, final GraphClient graph
final VisualConfiguration visualConfiguration, final TelemetryConfiguration telemetryConfiguration,
final TestsConfiguration testsConfiguration, final DatahubConfiguration datahubConfiguration,
final SiblingGraphService siblingGraphService, final GroupService groupService, final RoleService roleService,
final InviteTokenService inviteTokenService, final FeatureFlags featureFlags) {
final InviteTokenService inviteTokenService, final PostService postService, final FeatureFlags featureFlags) {

this.entityClient = entityClient;
this.graphClient = graphClient;
Expand All @@ -407,6 +411,7 @@ public GmsGraphQLEngine(final EntityClient entityClient, final GraphClient graph
this.groupService = groupService;
this.roleService = roleService;
this.inviteTokenService = inviteTokenService;
this.postService = postService;

this.ingestionConfiguration = Objects.requireNonNull(ingestionConfiguration);
this.authenticationConfiguration = Objects.requireNonNull(authenticationConfiguration);
Expand Down Expand Up @@ -676,6 +681,7 @@ private void configureQueryResolvers(final RuntimeWiring.Builder builder) {
.dataFetcher("entities", getEntitiesResolver())
.dataFetcher("listRoles", new ListRolesResolver(this.entityClient))
.dataFetcher("getInviteToken", new GetInviteTokenResolver(this.inviteTokenService))
.dataFetcher("listPosts", new ListPostsResolver(this.entityClient))
);
}

Expand Down Expand Up @@ -798,7 +804,7 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) {
.dataFetcher("batchAssignRole", new BatchAssignRoleResolver(this.roleService))
.dataFetcher("createInviteToken", new CreateInviteTokenResolver(this.inviteTokenService))
.dataFetcher("acceptRole", new AcceptRoleResolver(this.roleService, this.inviteTokenService))

.dataFetcher("createPost", new CreatePostResolver(this.postService))
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ public static boolean canEditGroupMembers(@Nonnull String groupUrnStr, @Nonnull
groupUrnStr, orPrivilegeGroups);
}

public static boolean canCreateGlobalAnnouncements(@Nonnull QueryContext context) {
return isAuthorized(context, Optional.empty(), PoliciesConfig.CREATE_GLOBAL_ANNOUNCEMENTS_PRIVILEGE);
}

public static boolean isAuthorized(
@Nonnull QueryContext context,
@Nonnull Optional<ResourceSpec> resourceSpec,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.linkedin.datahub.graphql.resolvers.post;

import com.datahub.authentication.Authentication;
import com.datahub.authentication.post.PostService;
import com.linkedin.common.Media;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.CreatePostInput;
import com.linkedin.datahub.graphql.generated.PostContentType;
import com.linkedin.datahub.graphql.generated.PostType;
import com.linkedin.datahub.graphql.generated.UpdateMediaInput;
import com.linkedin.datahub.graphql.generated.UpdatePostContentInput;
import com.linkedin.post.PostContent;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.concurrent.CompletableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*;


@Slf4j
@RequiredArgsConstructor
public class CreatePostResolver implements DataFetcher<CompletableFuture<Boolean>> {
private final PostService _postService;

@Override
public CompletableFuture<Boolean> get(final DataFetchingEnvironment environment) throws Exception {
final QueryContext context = environment.getContext();

if (!AuthorizationUtils.canCreateGlobalAnnouncements(context)) {
throw new AuthorizationException(
"Unauthorized to create posts. Please contact your DataHub administrator if this needs corrective action.");
}

final CreatePostInput input = bindArgument(environment.getArgument("input"), CreatePostInput.class);
final PostType type = input.getPostType();
final UpdatePostContentInput content = input.getContent();
final PostContentType contentType = content.getContentType();
final String title = content.getTitle();
final String link = content.getLink();
final String description = content.getDescription();
final UpdateMediaInput updateMediaInput = content.getMedia();
final Authentication authentication = context.getAuthentication();

Media media = updateMediaInput == null ? null
: _postService.mapMedia(updateMediaInput.getType().toString(), updateMediaInput.getLocation());
PostContent postContent = _postService.mapPostContent(contentType.toString(), title, description, link, media);

return CompletableFuture.supplyAsync(() -> {
try {
return _postService.createPost(type.toString(), postContent, authentication);
} catch (Exception e) {
throw new RuntimeException("Failed to create a new post", e);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.linkedin.datahub.graphql.resolvers.post;

import com.datahub.authentication.Authentication;
import com.linkedin.common.urn.Urn;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.generated.ListPostsInput;
import com.linkedin.datahub.graphql.generated.ListPostsResult;
import com.linkedin.datahub.graphql.types.post.PostMapper;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.query.filter.SortCriterion;
import com.linkedin.metadata.query.filter.SortOrder;
import com.linkedin.metadata.search.SearchEntity;
import com.linkedin.metadata.search.SearchResult;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*;
import static com.linkedin.metadata.Constants.*;


@Slf4j
@RequiredArgsConstructor
public class ListPostsResolver implements DataFetcher<CompletableFuture<ListPostsResult>> {
private static final Integer DEFAULT_START = 0;
private static final Integer DEFAULT_COUNT = 20;
private static final String DEFAULT_QUERY = "";

private final EntityClient _entityClient;

@Override
public CompletableFuture<ListPostsResult> get(final DataFetchingEnvironment environment) throws Exception {
final QueryContext context = environment.getContext();
final Authentication authentication = context.getAuthentication();

final ListPostsInput input = bindArgument(environment.getArgument("input"), ListPostsInput.class);
final Integer start = input.getStart() == null ? DEFAULT_START : input.getStart();
final Integer count = input.getCount() == null ? DEFAULT_COUNT : input.getCount();
final String query = input.getQuery() == null ? DEFAULT_QUERY : input.getQuery();

return CompletableFuture.supplyAsync(() -> {
try {
final SortCriterion sortCriterion =
new SortCriterion().setField(LAST_MODIFIED_FIELD_NAME).setOrder(SortOrder.DESCENDING);

// First, get all Post Urns.
final SearchResult gmsResult = _entityClient.search(POST_ENTITY_NAME, query, null, sortCriterion, start, count,
context.getAuthentication());

// Then, get and hydrate all Posts.
final Map<Urn, EntityResponse> entities = _entityClient.batchGetV2(POST_ENTITY_NAME,
new HashSet<>(gmsResult.getEntities().stream().map(SearchEntity::getEntity).collect(Collectors.toList())),
null, authentication);

final ListPostsResult result = new ListPostsResult();
result.setStart(gmsResult.getFrom());
result.setCount(gmsResult.getPageSize());
result.setTotal(gmsResult.getNumEntities());
result.setPosts(entities.values().stream().map(PostMapper::map).collect(Collectors.toList()));
return result;
} catch (Exception e) {
throw new RuntimeException("Failed to list posts", e);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.linkedin.datahub.graphql.types.post;

import com.linkedin.data.DataMap;
import com.linkedin.datahub.graphql.generated.AuditStamp;
import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.generated.Media;
import com.linkedin.datahub.graphql.generated.MediaType;
import com.linkedin.datahub.graphql.generated.Post;
import com.linkedin.datahub.graphql.generated.PostContent;
import com.linkedin.datahub.graphql.generated.PostContentType;
import com.linkedin.datahub.graphql.generated.PostType;
import com.linkedin.datahub.graphql.types.common.mappers.util.MappingHelper;
import com.linkedin.datahub.graphql.types.mappers.ModelMapper;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.EnvelopedAspectMap;
import com.linkedin.post.PostInfo;
import javax.annotation.Nonnull;

import static com.linkedin.metadata.Constants.*;


public class PostMapper implements ModelMapper<EntityResponse, Post> {

public static final PostMapper INSTANCE = new PostMapper();

public static Post map(@Nonnull final EntityResponse entityResponse) {
return INSTANCE.apply(entityResponse);
}

@Override
public Post apply(@Nonnull final EntityResponse entityResponse) {
final Post result = new Post();

result.setUrn(entityResponse.getUrn().toString());
result.setType(EntityType.POST);
EnvelopedAspectMap aspectMap = entityResponse.getAspects();
MappingHelper<Post> mappingHelper = new MappingHelper<>(aspectMap, result);
mappingHelper.mapToResult(POST_INFO_ASPECT_NAME, this::mapPostInfo);
return mappingHelper.getResult();
}

private void mapPostInfo(@Nonnull Post post, @Nonnull DataMap dataMap) {
PostInfo postInfo = new PostInfo(dataMap);
post.setPostType(PostType.valueOf(postInfo.getType().toString()));
post.setContent(mapPostContent(postInfo.getContent()));
AuditStamp lastModified = new AuditStamp();
lastModified.setTime(postInfo.getLastModified());
post.setLastModified(lastModified);
}

@Nonnull
private com.linkedin.datahub.graphql.generated.PostContent mapPostContent(
@Nonnull com.linkedin.post.PostContent postContent) {
PostContent result = new PostContent();
result.setContentType(PostContentType.valueOf(postContent.getType().toString()));
result.setTitle(postContent.getTitle());
if (postContent.hasDescription()) {
result.setDescription(postContent.getDescription());
}
if (postContent.hasLink()) {
result.setLink(postContent.getLink().toString());
}
if (postContent.hasMedia()) {
result.setMedia(mapPostMedia(postContent.getMedia()));
}
return result;
}

@Nonnull
private Media mapPostMedia(@Nonnull com.linkedin.common.Media postMedia) {
Media result = new Media();
result.setType(MediaType.valueOf(postMedia.getType().toString()));
result.setLocation(postMedia.getLocation().toString());
return result;
}
}
Loading