Fix #6462 Backend: Add support for filter by 'teamType' in teams API (#6531)

This commit is contained in:
Vivek Ratnavel Subramanian 2022-08-04 09:08:51 -07:00 committed by GitHub
parent 1a42428e42
commit 3dd8929567
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 182 additions and 47 deletions

View File

@ -13,6 +13,7 @@
package org.openmetadata.catalog.jdbi3;
import static org.openmetadata.catalog.Entity.ORGANIZATION_NAME;
import static org.openmetadata.catalog.jdbi3.locator.ConnectionType.MYSQL;
import static org.openmetadata.catalog.jdbi3.locator.ConnectionType.POSTGRES;
@ -80,6 +81,7 @@ import org.openmetadata.catalog.type.UsageStats;
import org.openmetadata.catalog.type.Webhook;
import org.openmetadata.catalog.util.EntitiesCount;
import org.openmetadata.catalog.util.EntityUtil;
import org.openmetadata.catalog.util.FullyQualifiedName;
import org.openmetadata.catalog.util.ServicesCount;
import org.openmetadata.common.utils.CommonUtil;
@ -1818,49 +1820,133 @@ public interface CollectionDAO {
return "name";
}
default List<String> listUsersWithoutParents() {
return listUsersWithoutParents(Relationship.HAS.ordinal());
@Override
default int listCount(ListFilter filter) {
String parentTeam = filter.getQueryParam("parentTeam");
String condition = filter.getCondition();
if (parentTeam != null) {
// validate parent team
Team team = findEntityByName(parentTeam);
if (ORGANIZATION_NAME.equals(team.getName())) {
// All the parentless teams should come under "organization" team
condition =
String.format(
"%s AND id NOT IN ( (SELECT '%s') UNION (SELECT toId FROM entity_relationship WHERE fromId!='%s' AND fromEntity='team' AND toEntity='team' AND relation=%d) )",
condition, team.getId(), team.getId(), Relationship.PARENT_OF.ordinal());
} else {
condition =
String.format(
"%s AND id IN (SELECT toId FROM entity_relationship WHERE fromId='%s' AND fromEntity='team' AND toEntity='team' AND relation=%d)",
condition, team.getId(), Relationship.PARENT_OF.ordinal());
}
}
return listCount(getTableName(), getNameColumn(), condition);
}
default List<String> listTeamsWithoutParents() {
return listTeamsWithoutParents(Relationship.PARENT_OF.ordinal());
@Override
default List<String> listBefore(ListFilter filter, int limit, String before) {
String parentTeam = filter.getQueryParam("parentTeam");
String condition = filter.getCondition();
if (parentTeam != null) {
// validate parent team
Team team = findEntityByName(parentTeam);
if (ORGANIZATION_NAME.equals(team.getName())) {
// All the parentless teams should come under "organization" team
condition =
String.format(
"%s AND id NOT IN ( (SELECT '%s') UNION (SELECT toId FROM entity_relationship WHERE fromId!='%s' AND fromEntity='team' AND toEntity='team' AND relation=%d) )",
condition, team.getId(), team.getId(), Relationship.PARENT_OF.ordinal());
} else {
condition =
String.format(
"%s AND id IN (SELECT toId FROM entity_relationship WHERE fromId='%s' AND fromEntity='team' AND toEntity='team' AND relation=%d)",
condition, team.getId(), Relationship.PARENT_OF.ordinal());
}
}
// Quoted name is stored in fullyQualifiedName column and not in the name column
before = getNameColumn().equals("name") ? FullyQualifiedName.unquoteName(before) : before;
return listBefore(getTableName(), getNameColumn(), condition, limit, before);
}
@ConnectionAwareSqlQuery(
value =
"SELECT ue.id "
+ "FROM user_entity ue "
+ "WHERE ue.id NOT IN "
+ "(SELECT toId FROM entity_relationship "
+ "WHERE fromEntity = `team` AND relation = :relation AND toEntity = `user`)",
connectionType = MYSQL)
@ConnectionAwareSqlQuery(
value =
"SELECT ue.id "
+ "FROM user_entity ue "
+ "WHERE ue.id NOT IN "
+ "(SELECT toId FROM entity_relationship "
+ "WHERE fromEntity = `team` AND relation = :relation AND toEntity = `user`)",
connectionType = POSTGRES)
List<String> listUsersWithoutParents(@Bind("relation") int relation);
@Override
default List<String> listAfter(ListFilter filter, int limit, String after) {
String parentTeam = filter.getQueryParam("parentTeam");
String condition = filter.getCondition();
if (parentTeam != null) {
// validate parent team
Team team = findEntityByName(parentTeam);
if (ORGANIZATION_NAME.equals(team.getName())) {
// All the parentless teams should come under "organization" team
condition =
String.format(
"%s AND id NOT IN ( (SELECT '%s') UNION (SELECT toId FROM entity_relationship WHERE fromId!='%s' AND fromEntity='team' AND toEntity='team' AND relation=%d) )",
condition, team.getId(), team.getId(), Relationship.PARENT_OF.ordinal());
} else {
condition =
String.format(
"%s AND id IN (SELECT toId FROM entity_relationship WHERE fromId='%s' AND fromEntity='team' AND toEntity='team' AND relation=%d)",
condition, team.getId(), Relationship.PARENT_OF.ordinal());
}
}
@ConnectionAwareSqlQuery(
value =
"SELECT te.id "
+ "FROM team_entity te "
+ "WHERE te.id NOT IN "
+ "(SELECT toId FROM entity_relationship "
+ "WHERE fromEntity = 'team' AND relation = :relation AND toEntity = 'user')",
connectionType = MYSQL)
@ConnectionAwareSqlQuery(
value =
"SELECT te.id "
+ "FROM team_entity te "
+ "WHERE te.id NOT IN "
+ "(SELECT toId FROM entity_relationship "
+ "WHERE fromEntity = 'team' AND relation = :relation AND toEntity = 'user')",
connectionType = POSTGRES)
List<String> listTeamsWithoutParents(@Bind("relation") int relation);
// Quoted name is stored in fullyQualifiedName column and not in the name column
after = getNameColumn().equals("name") ? FullyQualifiedName.unquoteName(after) : after;
return listAfter(getTableName(), getNameColumn(), condition, limit, after);
}
@SqlQuery("SELECT count(*) FROM <table> <cond>")
int listCount(@Define("table") String table, @Define("nameColumn") String nameColumn, @Define("cond") String cond);
@SqlQuery(
"SELECT json FROM ("
+ "SELECT <nameColumn>, json FROM <table> <cond> AND "
+ "<nameColumn> < :before "
+ // Pagination by entity fullyQualifiedName or name (when entity does not have fqn)
"ORDER BY <nameColumn> DESC "
+ // Pagination ordering by entity fullyQualifiedName or name (when entity does not have fqn)
"LIMIT :limit"
+ ") last_rows_subquery ORDER BY <nameColumn>")
List<String> listBefore(
@Define("table") String table,
@Define("nameColumn") String nameColumn,
@Define("cond") String cond,
@Bind("limit") int limit,
@Bind("before") String before);
@SqlQuery(
"SELECT json FROM <table> <cond> AND " + "<nameColumn> > :after " + "ORDER BY <nameColumn> " + "LIMIT :limit")
List<String> listAfter(
@Define("table") String table,
@Define("nameColumn") String nameColumn,
@Define("cond") String cond,
@Bind("limit") int limit,
@Bind("after") String after);
default List<String> listUsersUnderOrganization(String teamId) {
return listUsersUnderOrganization(teamId, Relationship.HAS.ordinal());
}
default List<String> listTeamsUnderOrganization(String teamId) {
return listTeamsUnderOrganization(teamId, Relationship.PARENT_OF.ordinal());
}
@SqlQuery(
"SELECT ue.id "
+ "FROM user_entity ue "
+ "WHERE ue.id NOT IN (SELECT :teamId) UNION "
+ "(SELECT toId FROM entity_relationship "
+ "WHERE fromId != :teamId AND fromEntity = `team` AND relation = :relation AND toEntity = `user`)")
List<String> listUsersUnderOrganization(@Bind("teamId") String teamId, @Bind("relation") int relation);
@SqlQuery(
"SELECT te.id "
+ "FROM team_entity te "
+ "WHERE te.id NOT IN (SELECT :teamId) UNION "
+ "(SELECT toId FROM entity_relationship "
+ "WHERE fromId != :teamId AND fromEntity = 'team' AND relation = :relation AND toEntity = 'team')")
List<String> listTeamsUnderOrganization(@Bind("teamId") String teamId, @Bind("relation") int relation);
}
interface TopicDAO extends EntityDAO<Topic> {

View File

@ -66,8 +66,10 @@ public class TeamRepository extends EntityRepository<Team> {
team.setDefaultRoles(fields.contains("defaultRoles") ? getDefaultRoles(team) : null);
team.setOwner(fields.contains(FIELD_OWNER) ? getOwner(team) : null);
team.setParents(fields.contains("parents") ? getParents(team) : null);
team.setChildren(fields.contains("children") ? getChildren(team) : null);
team.setChildren(fields.contains("children") ? getChildren(team.getId()) : null);
team.setPolicies(fields.contains("policies") ? getPolicies(team) : null);
team.setChildrenCount(fields.contains("childrenCount") ? getChildrenCount(team) : null);
team.setUserCount(fields.contains("userCount") ? getUserCount(team.getId()) : null);
return team;
}
@ -168,6 +170,16 @@ public class TeamRepository extends EntityRepository<Team> {
return EntityUtil.populateEntityReferences(userIds, Entity.USER);
}
private Integer getUserCount(UUID teamId) throws IOException {
List<EntityRelationshipRecord> userIds = findTo(teamId, TEAM, Relationship.HAS, Entity.USER);
int userCount = userIds.size();
List<EntityReference> children = getChildren(teamId);
for (EntityReference child : children) {
userCount += getUserCount(child.getId());
}
return userCount;
}
private List<EntityReference> getOwns(Team team) throws IOException {
// Compile entities owned by the team
return EntityUtil.getEntityReferences(
@ -187,15 +199,19 @@ public class TeamRepository extends EntityRepository<Team> {
return EntityUtil.populateEntityReferences(parents, TEAM);
}
private List<EntityReference> getChildren(Team team) throws IOException {
if (team.getId().equals(organization.getId())) { // For organization all the parentless teams are children
List<String> children = daoCollection.teamDAO().listTeamsWithoutParents();
private List<EntityReference> getChildren(UUID teamId) throws IOException {
if (teamId.equals(organization.getId())) { // For organization all the parentless teams are children
List<String> children = daoCollection.teamDAO().listTeamsUnderOrganization(teamId.toString());
return EntityUtil.populateEntityReferencesById(children, Entity.TEAM);
}
List<EntityRelationshipRecord> children = findTo(team.getId(), TEAM, Relationship.PARENT_OF, TEAM);
List<EntityRelationshipRecord> children = findTo(teamId, TEAM, Relationship.PARENT_OF, TEAM);
return EntityUtil.populateEntityReferences(children, TEAM);
}
private Integer getChildrenCount(Team team) throws IOException {
return getChildren(team.getId()).size();
}
private List<EntityReference> getPolicies(Team team) throws IOException {
List<EntityRelationshipRecord> policies = findTo(team.getId(), TEAM, Relationship.HAS, POLICY);
return EntityUtil.populateEntityReferences(policies, POLICY);

View File

@ -101,7 +101,8 @@ public class TeamResource extends EntityResource<Team, TeamRepository> {
}
}
static final String FIELDS = "owner,profile,users,owns,defaultRoles,parents,children,policies";
static final String FIELDS =
"owner,profile,users,owns,defaultRoles,parents,children,policies,userCount,childrenCount";
@GET
@Valid
@ -139,6 +140,9 @@ public class TeamResource extends EntityResource<Team, TeamRepository> {
@Parameter(description = "Returns list of teams after this cursor", schema = @Schema(type = "string"))
@QueryParam("after")
String after,
@Parameter(description = "Filter the results by parent team name", schema = @Schema(type = "string"))
@QueryParam("parentTeam")
String parentTeam,
@Parameter(
description = "Include all, deleted, or non-deleted entities.",
schema = @Schema(implementation = Include.class))
@ -146,7 +150,7 @@ public class TeamResource extends EntityResource<Team, TeamRepository> {
@DefaultValue("non-deleted")
Include include)
throws IOException {
ListFilter filter = new ListFilter(include);
ListFilter filter = new ListFilter(include).addQueryParam("parentTeam", parentTeam);
return super.listInternal(uriInfo, securityContext, fieldsParam, filter, limitParam, before, after);
}

View File

@ -76,6 +76,14 @@
"$ref": "../../type/entityReference.json#/definitions/entityReferenceList",
"default": null
},
"childrenCount": {
"description" : "Total count of Children teams.",
"type": "integer"
},
"userCount": {
"description": "Total count of users that are part of the team.",
"type": "integer"
},
"owns": {
"description": "List of entities owned by the team.",
"$ref": "../../type/entityReference.json#/definitions/entityReferenceList"

View File

@ -19,6 +19,8 @@ import static javax.ws.rs.core.Response.Status.OK;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.openmetadata.catalog.Entity.ORGANIZATION_NAME;
import static org.openmetadata.catalog.api.teams.CreateTeam.TeamType.BUSINESS_UNIT;
import static org.openmetadata.catalog.api.teams.CreateTeam.TeamType.DEPARTMENT;
import static org.openmetadata.catalog.api.teams.CreateTeam.TeamType.DIVISION;
@ -41,6 +43,7 @@ import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
@ -77,6 +80,7 @@ import org.openmetadata.catalog.type.PolicyType;
import org.openmetadata.catalog.type.Profile;
import org.openmetadata.catalog.util.EntityUtil;
import org.openmetadata.catalog.util.JsonUtils;
import org.openmetadata.catalog.util.ResultList;
import org.openmetadata.catalog.util.TestUtils;
import org.openmetadata.catalog.util.TestUtils.UpdateType;
@ -94,13 +98,13 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
TEAM1 = teamResourceTest.createEntity(teamResourceTest.createRequest(test), ADMIN_AUTH_HEADERS);
TEAM_OWNER1 = TEAM1.getEntityReference();
ORG_TEAM = teamResourceTest.getEntityByName(Entity.ORGANIZATION_NAME, "", ADMIN_AUTH_HEADERS);
ORG_TEAM = teamResourceTest.getEntityByName(ORGANIZATION_NAME, "", ADMIN_AUTH_HEADERS);
}
@Test
void test_organization() throws HttpResponseException {
// Ensure getting organization from team hierarchy is successful
Team org = getEntityByName(Entity.ORGANIZATION_NAME, "", ADMIN_AUTH_HEADERS);
Team org = getEntityByName(ORGANIZATION_NAME, "", ADMIN_AUTH_HEADERS);
// Organization can't be deleted
assertResponse(
@ -356,6 +360,23 @@ public class TeamResourceTest extends EntityResourceTest<Team, CreateTeam> {
"t1", BUSINESS_UNIT, bu11.getEntityReference(), div12.getEntityReference(), dep13.getEntityReference());
assertEntityReferencesContain(t1.getParents(), ORG_TEAM.getEntityReference());
// assert children count for the newly created bu team
Map<String, String> queryParams = new HashMap<>();
queryParams.put("parentTeam", "t1");
queryParams.put("fields", "childrenCount,userCount");
ResultList<Team> teams = listEntities(queryParams, ADMIN_AUTH_HEADERS);
assertEquals(3, teams.getData().size());
assertEquals(3, teams.getPaging().getTotal());
assertEquals(0, teams.getData().get(0).getChildrenCount());
assertEquals(0, teams.getData().get(0).getUserCount());
queryParams.put("parentTeam", ORGANIZATION_NAME);
teams = listEntities(queryParams, ADMIN_AUTH_HEADERS);
assertTrue(teams.getData().stream().anyMatch(t -> t.getName().equals("t1")));
t1 = teams.getData().stream().filter(t -> t.getName().equals("t1")).collect(Collectors.toList()).get(0);
assertEquals(3, t1.getChildrenCount());
assertEquals(0, t1.getUserCount());
//
// Creating a parent with invalid children type is not allowed
// Department can't have Business unit as a child