diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index 618158c69b5b4..ad6809439b434 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -592,6 +592,7 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) { .dataFetcher("updateDashboard", new AuthenticatedResolver<>(new MutableTypeResolver<>(dashboardType))) .dataFetcher("updateDataJob", new AuthenticatedResolver<>(new MutableTypeResolver<>(dataJobType))) .dataFetcher("updateDataFlow", new AuthenticatedResolver<>(new MutableTypeResolver<>(dataFlowType))) + .dataFetcher("updateCorpUserProperties", new AuthenticatedResolver<>(new MutableTypeResolver<>(corpUserType))) .dataFetcher("addTag", new AuthenticatedResolver<>(new AddTagResolver(entityService))) .dataFetcher("removeTag", new AuthenticatedResolver<>(new RemoveTagResolver(entityService))) .dataFetcher("addTerm", new AuthenticatedResolver<>(new AddTermResolver(entityService))) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/corpuser/CorpUserType.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/corpuser/CorpUserType.java index ac802e9497ce5..993f2060459c2 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/corpuser/CorpUserType.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/corpuser/CorpUserType.java @@ -1,10 +1,15 @@ package com.linkedin.datahub.graphql.types.corpuser; +import com.linkedin.common.url.Url; import com.linkedin.common.urn.CorpuserUrn; import com.linkedin.common.urn.Urn; +import com.linkedin.data.template.RecordTemplate; +import com.linkedin.data.template.StringArray; import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.CorpUserUpdateInput; import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.types.MutableType; import com.linkedin.datahub.graphql.types.SearchableEntityType; import com.linkedin.datahub.graphql.generated.AutoCompleteResults; import com.linkedin.datahub.graphql.generated.CorpUser; @@ -15,10 +20,16 @@ import com.linkedin.datahub.graphql.types.mappers.UrnSearchResultsMapper; import com.linkedin.entity.Entity; import com.linkedin.entity.client.EntityClient; +import com.linkedin.events.metadata.ChangeType; +import com.linkedin.identity.CorpUserEditableInfo; +import com.linkedin.metadata.Constants; import com.linkedin.metadata.query.AutoCompleteResult; import com.linkedin.metadata.search.SearchResult; +import com.linkedin.metadata.utils.GenericAspectUtils; +import com.linkedin.mxe.MetadataChangeProposal; import graphql.execution.DataFetcherResult; +import java.util.Optional; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.net.URISyntaxException; @@ -29,7 +40,7 @@ import java.util.Map; import java.util.stream.Collectors; -public class CorpUserType implements SearchableEntityType { +public class CorpUserType implements SearchableEntityType, MutableType { private final EntityClient _entityClient; @@ -99,4 +110,59 @@ private CorpuserUrn getCorpUserUrn(final String urnStr) { throw new RuntimeException(String.format("Failed to retrieve user with urn %s, invalid urn", urnStr)); } } + + public Class inputClass() { + return CorpUserUpdateInput.class; + } + + @Override + public CorpUser update(@Nonnull String urn, @Nonnull CorpUserUpdateInput input, @Nonnull QueryContext context) throws Exception { + final CorpuserUrn actor = CorpuserUrn.createFromString(context.getAuthentication().getActor().toUrnStr()); + + // Get existing editable info to merge with + Optional existingCorpUserEditableInfo = + _entityClient.getVersionedAspect(urn, Constants.CORP_USER_EDITABLE_INFO_NAME, 0L, CorpUserEditableInfo.class, + context.getAuthentication()); + + // Create the MCP + final MetadataChangeProposal proposal = new MetadataChangeProposal(); + proposal.setEntityUrn(Urn.createFromString(urn)); + proposal.setEntityType(Constants.CORP_USER_ENTITY_NAME); + proposal.setAspectName(Constants.CORP_USER_EDITABLE_INFO_NAME); + proposal.setAspect(GenericAspectUtils.serializeAspect(mapCorpUserEditableInfo(input, existingCorpUserEditableInfo))); + proposal.setChangeType(ChangeType.UPSERT); + _entityClient.ingestProposal(proposal, context.getAuthentication()); + + return load(urn, context).getData(); + } + + private RecordTemplate mapCorpUserEditableInfo(CorpUserUpdateInput input, Optional existing) { + CorpUserEditableInfo result = existing.orElseGet(() -> new CorpUserEditableInfo()); + if (input.getAboutMe() != null) { + result.setAboutMe(input.getAboutMe()); + } + if (input.getPictureLink() != null) { + result.setPictureLink(new Url(input.getPictureLink())); + } + if (input.getAboutMe() != null) { + result.setAboutMe(input.getAboutMe()); + } + if (input.getSkills() != null) { + result.setSkills(new StringArray(input.getSkills())); + } + if (input.getTeams() != null) { + result.setTeams(new StringArray(input.getTeams())); + } + if (input.getPhone() != null) { + result.setPhone(input.getPhone()); + } + if (input.getSlack() != null) { + result.setSlack(input.getSlack()); + } + if (input.getEmail() != null) { + result.setEmail(input.getEmail()); + } + + return result; + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/corpuser/mappers/CorpUserEditableInfoMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/corpuser/mappers/CorpUserEditableInfoMapper.java index 1e5c5b8a90388..8e6ddfcbaab6b 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/corpuser/mappers/CorpUserEditableInfoMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/corpuser/mappers/CorpUserEditableInfoMapper.java @@ -1,6 +1,6 @@ package com.linkedin.datahub.graphql.types.corpuser.mappers; -import com.linkedin.datahub.graphql.generated.CorpUserEditableInfo; +import com.linkedin.datahub.graphql.generated.CorpUserEditableProperties; import com.linkedin.datahub.graphql.types.mappers.ModelMapper; import javax.annotation.Nonnull; @@ -10,20 +10,23 @@ * * To be replaced by auto-generated mappers implementations */ -public class CorpUserEditableInfoMapper implements ModelMapper { +public class CorpUserEditableInfoMapper implements ModelMapper { public static final CorpUserEditableInfoMapper INSTANCE = new CorpUserEditableInfoMapper(); - public static CorpUserEditableInfo map(@Nonnull final com.linkedin.identity.CorpUserEditableInfo info) { + public static CorpUserEditableProperties map(@Nonnull final com.linkedin.identity.CorpUserEditableInfo info) { return INSTANCE.apply(info); } @Override - public CorpUserEditableInfo apply(@Nonnull final com.linkedin.identity.CorpUserEditableInfo info) { - final CorpUserEditableInfo result = new CorpUserEditableInfo(); + public CorpUserEditableProperties apply(@Nonnull final com.linkedin.identity.CorpUserEditableInfo info) { + final CorpUserEditableProperties result = new CorpUserEditableProperties(); result.setAboutMe(info.getAboutMe()); result.setSkills(info.getSkills()); result.setTeams(info.getTeams()); + result.setEmail(info.getEmail()); + result.setPhone(info.getPhone()); + result.setSlack(info.getSlack()); if (info.hasPictureLink()) { result.setPictureLink(info.getPictureLink().toString()); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/corpuser/mappers/CorpUserSnapshotMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/corpuser/mappers/CorpUserSnapshotMapper.java index c7639c97c9600..d69e7d0d897ed 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/corpuser/mappers/CorpUserSnapshotMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/corpuser/mappers/CorpUserSnapshotMapper.java @@ -38,7 +38,7 @@ public CorpUser apply(@Nonnull final CorpUserSnapshot corpUser) { result.setProperties(CorpUserPropertiesMapper.map(CorpUserInfo.class.cast(aspect))); result.setInfo(CorpUserInfoMapper.map(CorpUserInfo.class.cast(aspect))); } else if (aspect instanceof CorpUserEditableInfo) { - result.setEditableInfo(CorpUserEditableInfoMapper.map(CorpUserEditableInfo.class.cast(aspect))); + result.setEditableProperties(CorpUserEditableInfoMapper.map(CorpUserEditableInfo.class.cast(aspect))); } else if (aspect instanceof GlobalTags) { result.setGlobalTags(GlobalTagsMapper.map(GlobalTags.class.cast(aspect))); } else if (aspect instanceof CorpUserStatus) { diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index 5df6877023c6d..08c10bbb85c17 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -260,6 +260,11 @@ type Mutation { Sets the Domain for a Dataset, Chart, Dashboard, Data Flow (Pipeline), or Data Job (Task). Returns true if the Domain was successfully removed, or was already removed. Requires the Edit Domains privilege for an asset. """ unsetDomain(entityUrn: String!): Boolean + + """ + Update a particular Corp User's editable properties + """ + updateCorpUserProperties(urn: String!, input: CorpUserUpdateInput!): CorpUser } """ @@ -2234,6 +2239,62 @@ type CorpUserEditableProperties { A URL which points to a picture which user wants to set as a profile photo """ pictureLink: String + + """ + The slack handle of the user + """ + slack: String + + """ + Phone number for the user + """ + phone: String + + """ + Email address for the user + """ + email: String +} + + +""" +Arguments provided to update a CorpUser Entity +""" +input CorpUserUpdateInput { + """ + About me section of the user + """ + aboutMe: String + + """ + Teams that the user belongs to + """ + teams: [String!] + + """ + Skills that the user possesses + """ + skills: [String!] + + """ + A URL which points to a picture which user wants to set as a profile photo + """ + pictureLink: String + + """ + The slack handle of the user + """ + slack: String + + """ + Phone number for the user + """ + phone: String + + """ + Email address for the user + """ + email: String } """ @@ -5552,4 +5613,4 @@ type ListDomainsResult { The Domains themselves """ domains: [Domain!]! -} \ No newline at end of file +} diff --git a/datahub-web-react/src/graphql/user.graphql b/datahub-web-react/src/graphql/user.graphql index e2ab701935035..c4ccd10f82a38 100644 --- a/datahub-web-react/src/graphql/user.graphql +++ b/datahub-web-react/src/graphql/user.graphql @@ -109,3 +109,9 @@ mutation removeUser($urn: String!) { mutation updateUserStatus($urn: String!, $status: CorpUserStatus!) { updateUserStatus(urn: $urn, status: $status) } + +mutation updateCorpUserProperties($urn: String!, $input: CorpUserUpdateInput!) { + updateCorpUserProperties(urn: $urn, input: $input) { + urn + } +} diff --git a/metadata-models/src/main/pegasus/com/linkedin/identity/CorpUserEditableInfo.pdl b/metadata-models/src/main/pegasus/com/linkedin/identity/CorpUserEditableInfo.pdl index 63c00acad7b42..782a2723075a5 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/identity/CorpUserEditableInfo.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/identity/CorpUserEditableInfo.pdl @@ -40,4 +40,19 @@ record CorpUserEditableInfo { * A URL which points to a picture which user wants to set as a profile photo */ pictureLink: Url = "https://raw.githubusercontent.com/linkedin/datahub/master/datahub-web-react/src/images/default_avatar.png" + + /** + * Slack handle for the user + */ + slack: optional string + + /** + * Phone number to contact the user + */ + phone: optional string + + /** + * Email address to contact the user + */ + email: optional string } diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json index abb6ad6fd2efc..10ddf1ba30a79 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.aspects.snapshot.json @@ -1680,6 +1680,21 @@ "type" : "com.linkedin.common.Url", "doc" : "A URL which points to a picture which user wants to set as a profile photo", "default" : "https://raw.githubusercontent.com/linkedin/datahub/master/datahub-web-react/src/images/default_avatar.png" + }, { + "name" : "slack", + "type" : "string", + "doc" : "Slack handle for the user", + "optional" : true + }, { + "name" : "phone", + "type" : "string", + "doc" : "Phone number to contact the user", + "optional" : true + }, { + "name" : "email", + "type" : "string", + "doc" : "Email address to contact the user", + "optional" : true } ], "Aspect" : { "EntityUrns" : [ "com.linkedin.common.CorpuserUrn" ], diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json index dd7ec570e84fb..3496452a34bba 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.entities.snapshot.json @@ -2097,6 +2097,21 @@ "type" : "com.linkedin.common.Url", "doc" : "A URL which points to a picture which user wants to set as a profile photo", "default" : "https://raw.githubusercontent.com/linkedin/datahub/master/datahub-web-react/src/images/default_avatar.png" + }, { + "name" : "slack", + "type" : "string", + "doc" : "Slack handle for the user", + "optional" : true + }, { + "name" : "phone", + "type" : "string", + "doc" : "Phone number to contact the user", + "optional" : true + }, { + "name" : "email", + "type" : "string", + "doc" : "Email address to contact the user", + "optional" : true } ], "Aspect" : { "EntityUrns" : [ "com.linkedin.common.CorpuserUrn" ], diff --git a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json index 7fdee5457b52e..ab5006e3bc189 100644 --- a/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json +++ b/metadata-service/restli-api/src/main/snapshot/com.linkedin.entity.runs.snapshot.json @@ -1437,6 +1437,21 @@ "type" : "com.linkedin.common.Url", "doc" : "A URL which points to a picture which user wants to set as a profile photo", "default" : "https://raw.githubusercontent.com/linkedin/datahub/master/datahub-web-react/src/images/default_avatar.png" + }, { + "name" : "slack", + "type" : "string", + "doc" : "Slack handle for the user", + "optional" : true + }, { + "name" : "phone", + "type" : "string", + "doc" : "Phone number to contact the user", + "optional" : true + }, { + "name" : "email", + "type" : "string", + "doc" : "Email address to contact the user", + "optional" : true } ], "Aspect" : { "EntityUrns" : [ "com.linkedin.common.CorpuserUrn" ], diff --git a/metadata-utils/src/main/java/com/linkedin/metadata/Constants.java b/metadata-utils/src/main/java/com/linkedin/metadata/Constants.java index 67cfb8d220956..27a21e0399e32 100644 --- a/metadata-utils/src/main/java/com/linkedin/metadata/Constants.java +++ b/metadata-utils/src/main/java/com/linkedin/metadata/Constants.java @@ -45,6 +45,7 @@ public class Constants { // User public static final String CORP_USER_KEY_ASPECT_NAME = "corpUserKey"; + public static final String CORP_USER_EDITABLE_INFO_NAME = "corpUserEditableInfo"; public static final String GROUP_MEMBERSHIP_ASPECT_NAME = "groupMembership"; public static final String CORP_USER_STATUS_ASPECT_NAME = "corpUserStatus";