diff --git a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java index d1afe7b9df82..fbd079e6a619 100644 --- a/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java +++ b/openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java @@ -2570,4 +2570,232 @@ void test_glossaryTermEntityStatusFiltering(TestNamespace ns) { assertEquals( 2, ourMultiStatusTerms.size(), "Both terms should be returned with multi-status filter"); } + + // =================================================================== + // ASSET ENDPOINT TESTS WITH PARENT FILTER + // =================================================================== + + @Test + void get_assetsCountsWithParentFilter(TestNamespace ns) { + OpenMetadataClient client = SdkClients.adminClient(); + + // Create two glossaries with terms + CreateGlossary createGlossary1 = + new CreateGlossary() + .withName(ns.prefix("asset_count_glossary1")) + .withDescription("Glossary 1 for asset count test"); + Glossary glossary1 = client.glossaries().create(createGlossary1); + + CreateGlossary createGlossary2 = + new CreateGlossary() + .withName(ns.prefix("asset_count_glossary2")) + .withDescription("Glossary 2 for asset count test"); + Glossary glossary2 = client.glossaries().create(createGlossary2); + + // Create terms in glossary1 + CreateGlossaryTerm termReq1 = + new CreateGlossaryTerm() + .withName(ns.prefix("count_term1")) + .withGlossary(glossary1.getFullyQualifiedName()) + .withDescription("Term 1 in glossary 1"); + GlossaryTerm term1 = createEntity(termReq1); + + CreateGlossaryTerm termReq2 = + new CreateGlossaryTerm() + .withName(ns.prefix("count_term2")) + .withGlossary(glossary1.getFullyQualifiedName()) + .withDescription("Term 2 in glossary 1"); + GlossaryTerm term2 = createEntity(termReq2); + + // Create term in glossary2 + CreateGlossaryTerm termReq3 = + new CreateGlossaryTerm() + .withName(ns.prefix("count_term3")) + .withGlossary(glossary2.getFullyQualifiedName()) + .withDescription("Term in glossary 2"); + GlossaryTerm term3 = createEntity(termReq3); + + // Get counts with parent=glossary1 — should only include glossary1's terms + String countsWithParent = getAssetCounts(client, glossary1.getFullyQualifiedName()); + assertNotNull(countsWithParent); + assertFalse( + countsWithParent.contains(term3.getFullyQualifiedName()), + "Should not contain terms from glossary2 when filtering by glossary1"); + + // Get counts without parent — should include all terms + String countsWithoutParent = getAssetCounts(client, null); + assertNotNull(countsWithoutParent); + } + + @Test + void get_assetsCountsWithNoParent(TestNamespace ns) { + OpenMetadataClient client = SdkClients.adminClient(); + + // Calling without parent should succeed and return a response + String counts = getAssetCounts(client, null); + assertNotNull(counts); + } + + @Test + void get_termAssetsById_withParentFilter(TestNamespace ns) { + OpenMetadataClient client = SdkClients.adminClient(); + + CreateGlossary createGlossary = + new CreateGlossary() + .withName(ns.prefix("id_assets_glossary")) + .withDescription("Glossary for assets by id test"); + Glossary glossary = client.glossaries().create(createGlossary); + + // Create parent term and child term + CreateGlossaryTerm parentReq = + new CreateGlossaryTerm() + .withName(ns.prefix("id_parent")) + .withGlossary(glossary.getFullyQualifiedName()) + .withDescription("Parent term"); + GlossaryTerm parentTerm = createEntity(parentReq); + + CreateGlossaryTerm childReq = + new CreateGlossaryTerm() + .withName(ns.prefix("id_child")) + .withGlossary(glossary.getFullyQualifiedName()) + .withParent(parentTerm.getFullyQualifiedName()) + .withDescription("Child term"); + GlossaryTerm childTerm = createEntity(childReq); + + // Query child term assets with parent filter matching its parent — should succeed + String result = + getTermAssetsById(client, childTerm.getId().toString(), parentTerm.getFullyQualifiedName()); + assertNotNull(result); + + // Query parent term assets with parent filter = glossary — should succeed + String parentResult = + getTermAssetsById(client, parentTerm.getId().toString(), glossary.getFullyQualifiedName()); + assertNotNull(parentResult); + + // Query child term assets with a non-matching parent — should return empty + String emptyResult = + getTermAssetsById(client, childTerm.getId().toString(), "nonexistent.glossary"); + assertNotNull(emptyResult); + } + + @Test + void get_termAssetsByName_withParentFilter(TestNamespace ns) { + OpenMetadataClient client = SdkClients.adminClient(); + + CreateGlossary createGlossary = + new CreateGlossary() + .withName(ns.prefix("name_assets_glossary")) + .withDescription("Glossary for assets by name test"); + Glossary glossary = client.glossaries().create(createGlossary); + + CreateGlossaryTerm parentReq = + new CreateGlossaryTerm() + .withName(ns.prefix("name_parent")) + .withGlossary(glossary.getFullyQualifiedName()) + .withDescription("Parent term"); + GlossaryTerm parentTerm = createEntity(parentReq); + + CreateGlossaryTerm childReq = + new CreateGlossaryTerm() + .withName(ns.prefix("name_child")) + .withGlossary(glossary.getFullyQualifiedName()) + .withParent(parentTerm.getFullyQualifiedName()) + .withDescription("Child term"); + GlossaryTerm childTerm = createEntity(childReq); + + // Query child term assets by name with parent filter matching — should succeed + String result = + getTermAssetsByName( + client, childTerm.getFullyQualifiedName(), parentTerm.getFullyQualifiedName()); + assertNotNull(result); + + // Query with non-matching parent — should return empty + String emptyResult = + getTermAssetsByName(client, childTerm.getFullyQualifiedName(), "nonexistent.glossary"); + assertNotNull(emptyResult); + } + + @Test + void get_termAssetsById_withoutParentFilter(TestNamespace ns) { + OpenMetadataClient client = SdkClients.adminClient(); + + CreateGlossary createGlossary = + new CreateGlossary() + .withName(ns.prefix("noparent_assets_glossary")) + .withDescription("Glossary for assets without parent test"); + Glossary glossary = client.glossaries().create(createGlossary); + + CreateGlossaryTerm termReq = + new CreateGlossaryTerm() + .withName(ns.prefix("noparent_term")) + .withGlossary(glossary.getFullyQualifiedName()) + .withDescription("Term for no-parent test"); + GlossaryTerm term = createEntity(termReq); + + // Without parent param — should return all assets for the term + String result = getTermAssetsById(client, term.getId().toString(), null); + assertNotNull(result); + } + + @Test + void get_termAssetsByName_withoutParentFilter(TestNamespace ns) { + OpenMetadataClient client = SdkClients.adminClient(); + + CreateGlossary createGlossary = + new CreateGlossary() + .withName(ns.prefix("noparent_name_glossary")) + .withDescription("Glossary for name assets without parent test"); + Glossary glossary = client.glossaries().create(createGlossary); + + CreateGlossaryTerm termReq = + new CreateGlossaryTerm() + .withName(ns.prefix("noparent_name_term")) + .withGlossary(glossary.getFullyQualifiedName()) + .withDescription("Term for no-parent name test"); + GlossaryTerm term = createEntity(termReq); + + // Without parent param — should return all assets for the term + String result = getTermAssetsByName(client, term.getFullyQualifiedName(), null); + assertNotNull(result); + } + + private String getAssetCounts(OpenMetadataClient client, String parent) { + RequestOptions.Builder optionsBuilder = RequestOptions.builder(); + if (parent != null) { + optionsBuilder.queryParam("parent", parent); + } + return client + .getHttpClient() + .executeForString( + HttpMethod.GET, "/v1/glossaryTerms/assets/counts", null, optionsBuilder.build()); + } + + private String getTermAssetsById(OpenMetadataClient client, String id, String parent) { + RequestOptions.Builder optionsBuilder = RequestOptions.builder(); + optionsBuilder.queryParam("limit", "10"); + optionsBuilder.queryParam("offset", "0"); + if (parent != null) { + optionsBuilder.queryParam("parent", parent); + } + return client + .getHttpClient() + .executeForString( + HttpMethod.GET, "/v1/glossaryTerms/" + id + "/assets", null, optionsBuilder.build()); + } + + private String getTermAssetsByName(OpenMetadataClient client, String fqn, String parent) { + RequestOptions.Builder optionsBuilder = RequestOptions.builder(); + optionsBuilder.queryParam("limit", "10"); + optionsBuilder.queryParam("offset", "0"); + if (parent != null) { + optionsBuilder.queryParam("parent", parent); + } + return client + .getHttpClient() + .executeForString( + HttpMethod.GET, + "/v1/glossaryTerms/name/" + fqn + "/assets", + null, + optionsBuilder.build()); + } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java index 26a3a37af3f7..10a1dc5ebfea 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java @@ -166,7 +166,7 @@ public GlossaryTermRepository() { } public ResultList getGlossaryTermAssets( - UUID glossaryTermId, int limit, int offset) { + UUID glossaryTermId, int limit, int offset, String parent) { GlossaryTerm term = get(null, glossaryTermId, getFields("id,fullyQualifiedName")); if (inheritedFieldEntitySearch == null) { @@ -174,16 +174,20 @@ public ResultList getGlossaryTermAssets( return new ResultList<>(new ArrayList<>(), null, null, 0); } + String fqn = term.getFullyQualifiedName(); + boolean useParentScope = + parent != null && !parent.isEmpty() && FullyQualifiedName.isParent(fqn, parent); + InheritedFieldQuery query = - InheritedFieldQuery.forGlossaryTerm(term.getFullyQualifiedName(), offset, limit); + useParentScope + ? InheritedFieldQuery.forGlossaryTermChildren(parent, offset, limit) + : InheritedFieldQuery.forGlossaryTerm(fqn, offset, limit); InheritedFieldResult result = inheritedFieldEntitySearch.getEntitiesForField( query, () -> { - LOG.warn( - "Search fallback for glossary term {} assets. Returning empty list.", - term.getFullyQualifiedName()); + LOG.warn("Search fallback for glossary term {} assets. Returning empty list.", fqn); return new InheritedFieldResult(new ArrayList<>(), 0); }); @@ -191,12 +195,12 @@ public ResultList getGlossaryTermAssets( } public ResultList getGlossaryTermAssetsByName( - String termName, int limit, int offset) { + String termName, int limit, int offset, String parent) { GlossaryTerm term = getByName(null, termName, getFields("id,fullyQualifiedName")); - return getGlossaryTermAssets(term.getId(), limit, offset); + return getGlossaryTermAssets(term.getId(), limit, offset, parent); } - public Map getAllGlossaryTermsWithAssetsCount() { + public Map getAllGlossaryTermsWithAssetsCount(String parent) { if (inheritedFieldEntitySearch == null) { LOG.warn("Search unavailable for glossary term asset counts"); return new HashMap<>(); @@ -204,6 +208,14 @@ public Map getAllGlossaryTermsWithAssetsCount() { List allGlossaryTerms = listAll(getFields("fullyQualifiedName"), new ListFilter(null)); + + if (parent != null && !parent.isEmpty()) { + allGlossaryTerms = + allGlossaryTerms.stream() + .filter(term -> FullyQualifiedName.isParent(term.getFullyQualifiedName(), parent)) + .collect(Collectors.toList()); + } + Map glossaryTermAssetCounts = new HashMap<>(); for (GlossaryTerm term : allGlossaryTerms) { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java index dc30e8474753..deefa2396f25 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java @@ -433,11 +433,18 @@ public Response getRelationTypeUsageCounts( content = @Content(mediaType = "application/json")) }) public Response getAllGlossaryTermsWithAssetsCount( - @Context UriInfo uriInfo, @Context SecurityContext securityContext) { + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter( + description = + "Filter by parent glossary or glossary term FQN. " + + "When provided, only returns asset counts for children whose FQN starts with this value.") + @QueryParam("parent") + String parent) { OperationContext operationContext = new OperationContext(entityType, MetadataOperation.VIEW_ALL); authorizer.authorize(securityContext, operationContext, getResourceContext()); - java.util.Map result = repository.getAllGlossaryTermsWithAssetsCount(); + java.util.Map result = repository.getAllGlossaryTermsWithAssetsCount(parent); return Response.ok(result).build(); } @@ -866,8 +873,14 @@ public Response listGlossaryTermAssets( @DefaultValue("0") @Min(0) @QueryParam("offset") - int offset) { - return Response.ok(repository.getGlossaryTermAssets(id, limit, offset)).build(); + int offset, + @Parameter( + description = + "Filter by parent glossary or glossary term FQN. " + + "When provided, only returns assets for children whose FQN starts with this value.") + @QueryParam("parent") + String parent) { + return Response.ok(repository.getGlossaryTermAssets(id, limit, offset, parent)).build(); } @GET @@ -908,8 +921,14 @@ public Response listGlossaryTermAssetsByName( @DefaultValue("0") @Min(0) @QueryParam("offset") - int offset) { - return Response.ok(repository.getGlossaryTermAssetsByName(fqn, limit, offset)).build(); + int offset, + @Parameter( + description = + "Filter by parent glossary or glossary term FQN. " + + "When provided, only returns assets for children whose FQN starts with this value.") + @QueryParam("parent") + String parent) { + return Response.ok(repository.getGlossaryTermAssetsByName(fqn, limit, offset, parent)).build(); } @PUT diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/InheritedFieldEntitySearch.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/InheritedFieldEntitySearch.java index dbaf7454ac9f..a7f121dae71e 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/InheritedFieldEntitySearch.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/InheritedFieldEntitySearch.java @@ -244,6 +244,19 @@ public static InheritedFieldQuery forGlossaryTerm( .build(); } + public static InheritedFieldQuery forGlossaryTermChildren( + String parentFqn, int offset, int limit) { + return builder() + .fieldPath("tags.tagFQN") + .fieldValue(parentFqn) + .supportsHierarchy(true) + .filterType(QueryFilterType.TAG_ASSETS) + .includeDeleted(true) + .from(offset) + .size(limit) + .build(); + } + public static InheritedFieldQuery forTeam(String teamId, int offset, int limit) { return builder() .fieldPath("owners.id") diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/QueryFilterBuilder.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/QueryFilterBuilder.java index 471c5a16f89b..b700ce6963f0 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/QueryFilterBuilder.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/QueryFilterBuilder.java @@ -135,7 +135,11 @@ public static String buildTagAssetsFilter(InheritedFieldQuery query) { ObjectNode boolNode = queryNode.putObject(BOOL_KEY); ArrayNode mustArray = boolNode.putArray(MUST_KEY); - addExactMatchCondition(mustArray, query.getFieldPath(), query.getFieldValue()); + if (query.isSupportsHierarchy()) { + addHierarchyCondition(mustArray, query.getFieldPath(), query.getFieldValue()); + } else { + addExactMatchCondition(mustArray, query.getFieldPath(), query.getFieldValue()); + } addCommonFilters(mustArray, query); return serializeQuery(queryFilter);