-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Support parent-scoped asset counts and assets listing for glossary terms #26801
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
| } | ||
|
Comment on lines
+2665
to
+2679
|
||
|
|
||
| @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); | ||
|
Comment on lines
+2706
to
+2715
|
||
| } | ||
|
|
||
| @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()); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -166,9 +166,15 @@ public GlossaryTermRepository() { | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| public ResultList<EntityReference> getGlossaryTermAssets( | ||||||||||||||||||||||||||||||
| UUID glossaryTermId, int limit, int offset) { | ||||||||||||||||||||||||||||||
| UUID glossaryTermId, int limit, int offset, String parent) { | ||||||||||||||||||||||||||||||
| GlossaryTerm term = get(null, glossaryTermId, getFields("id,fullyQualifiedName")); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (parent != null | ||||||||||||||||||||||||||||||
| && !parent.isEmpty() | ||||||||||||||||||||||||||||||
| && !FullyQualifiedName.isParent(term.getFullyQualifiedName(), parent)) { | ||||||||||||||||||||||||||||||
| return new ResultList<>(new ArrayList<>(), null, null, 0); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
sonika-shah marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (inheritedFieldEntitySearch == null) { | ||||||||||||||||||||||||||||||
| LOG.warn("Search is unavailable for glossary term assets. Returning empty list."); | ||||||||||||||||||||||||||||||
| return new ResultList<>(new ArrayList<>(), null, null, 0); | ||||||||||||||||||||||||||||||
|
|
@@ -191,19 +197,27 @@ public ResultList<EntityReference> getGlossaryTermAssets( | |||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| public ResultList<EntityReference> 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<String, Integer> getAllGlossaryTermsWithAssetsCount() { | ||||||||||||||||||||||||||||||
| public Map<String, Integer> getAllGlossaryTermsWithAssetsCount(String parent) { | ||||||||||||||||||||||||||||||
| if (inheritedFieldEntitySearch == null) { | ||||||||||||||||||||||||||||||
| LOG.warn("Search unavailable for glossary term asset counts"); | ||||||||||||||||||||||||||||||
| return new HashMap<>(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| List<GlossaryTerm> 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()); | ||||||||||||||||||||||||||||||
sonika-shah marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
209
to
+216
|
||||||||||||||||||||||||||||||
| List<GlossaryTerm> 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()); | |
| List<GlossaryTerm> allGlossaryTerms; | |
| if (parent != null && !parent.isEmpty()) { | |
| // Fetch only the nested terms under the given parent to avoid loading all glossary terms | |
| allGlossaryTerms = daoCollection.glossaryTermDAO().getNestedTerms(parent); | |
| } else { | |
| allGlossaryTerms = listAll(getFields("fullyQualifiedName"), new ListFilter(null)); |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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<String, Integer> result = repository.getAllGlossaryTermsWithAssetsCount(); | ||||||||||||||
| java.util.Map<String, Integer> 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.") | ||||||||||||||
|
Comment on lines
+879
to
+880
|
||||||||||||||
| "Filter by parent glossary or glossary term FQN. " | |
| + "When provided, only returns assets for children whose FQN starts with this value.") | |
| "Optional scope filter by parent glossary or glossary term FQN. " | |
| + "When provided, assets are returned only if the requested glossary term's FQN is within this parent hierarchy (for example, its FQN starts with the given value).") |
Copilot
AI
Mar 26, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parent query param description here says it "returns assets for children whose FQN starts with this value", but this endpoint returns assets for the requested term ({fqn}) and only uses parent as an additional scope filter. Please adjust the description to match the actual behavior so the OpenAPI docs are accurate.
| "Filter by parent glossary or glossary term FQN. " | |
| + "When provided, only returns assets for children whose FQN starts with this value.") | |
| "Optional scope filter by parent glossary or glossary term FQN. " | |
| + "Assets are always returned for the requested term `{fqn}`; when `parent` is provided, " | |
| + "only assets associated to that term within the specified parent hierarchy " | |
| + "(children whose FQN starts with this value) are included.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests don’t strongly validate the new
parentfiltering behavior. For example,countsWithParentis only asserted to not containterm3, which would also pass if the endpoint returned{}or an empty body; andterm1/term2are created but never asserted. Consider parsing the response JSON into a map and asserting that (a)term1andterm2keys are present, (b)term3is absent, and (c) the filtered response differs from the unfiltered response in the expected way.