Skip to content

Commit dd05046

Browse files
committed
fix(#27319): Add /search endpoint for Roles (#27335)
* fix(#27319): Add DB-backed /search endpoint for Roles and Teams Add generic search infrastructure that enables server-side name/displayName search via SQL LIKE queries, eliminating the need for large limit workarounds in role/team selection dropdowns. - EntityDAO: searchByNameAndDisplayName() with MySQL/Postgres support - EntityRepository: search() with offset pagination and field resolution - EntityResource: searchInternal() reusable by any entity resource - RoleResource: GET /v1/roles/search?q=&limit=&offset= - TeamResource: GET /v1/teams/search?q=&limit=&offset= Co-Authored-By: sonika-shah <sonika-shah@users.noreply.github.com> * test(#27319): Add integration tests for /search endpoint on Roles and Teams Tests cover: - Search by name (exact substring match) - Search by displayName - No results for non-matching query - Offset-based pagination (page1, page2, page3) - Empty query fallback to list Co-Authored-By: sonika-shah <sonika-shah@users.noreply.github.com> * test(#27319): Rewrite search integration tests as thorough end-to-end scenarios Replace shallow per-behavior unit tests with comprehensive integration tests that exercise the full search API in a single realistic flow: - Create roles/teams with distinct name vs displayName to verify both paths - Verify search finds matches by name AND displayName in one query - Verify result ordering (by name) - Verify no-match returns empty (not error) - Walk full pagination (pages of 2 across 6 results, verify no duplicates) - Verify fields param populates requested fields (policies, users) - Verify soft-deleted entities are excluded from results - Verify empty query falls back to listing Co-Authored-By: sonika-shah <sonika-shah@users.noreply.github.com> * test(#27319): Add case-insensitive search verification to integration tests Verify that UPPERCASE, lowercase, and MiXeD case queries all return the same results, ensuring the LOWER() SQL matching works correctly end-to-end. Co-Authored-By: sonika-shah <sonika-shah@users.noreply.github.com> * fix(#27319): Address review feedback — escape LIKE wildcards, remove empty-query branching 1. Escape SQL LIKE wildcards (%, _) in search term using ListFilter.escape() to prevent unintended wildcard matching from user input. 2. Remove if/else branching in searchInternal — null/empty query now flows through the same search() path (%% matches everything), so offset is always respected. No separate fallback to listAfter needed. Co-Authored-By: sonika-shah <sonika-shah@users.noreply.github.com> * fix(#27319): Address Copilot review — domain filter, ResourceContext, ORDER BY tie-breaker 1. Use filter.getResourceContext() instead of bare ResourceContext for consistent auth behavior with listInternal. 2. Add EntityUtil.addDomainQueryParam() so search respects domain restrictions. 3. Add id tie-breaker to ORDER BY (name, id) for deterministic pagination when entities share the same name. Co-Authored-By: sonika-shah <sonika-shah@users.noreply.github.com> * refactor(#27319): Use ListFilter nameFilter instead of custom DAO query Removes the custom searchByNameAndDisplayName SQL method from EntityDAO and the search() method from EntityRepository. Instead, adds a nameFilter condition to ListFilter.getCondition() — the LIKE clause flows through the existing listAfter SQL, same as every other filter. searchInternal now just sets filter.addQueryParam("nameFilter", query) and delegates to listWithOffset, following the same offset-based pagination pattern as GlossaryTermResource /search. Co-Authored-By: sonika-shah <sonika-shah@users.noreply.github.com> * fix(#27319): Remove duplicate addHref call in listWithOffset listWithOffset was calling withHref on each entity, but the caller (searchInternal) already calls addHref on the ResultList. Removed the redundant call from listWithOffset. Co-Authored-By: sonika-shah <sonika-shah@users.noreply.github.com> * fix(#27319): Use dao.listCount for real total instead of approximate knownTotal Same pattern as listAfter — one COUNT query per request gives the exact total matching the filter, consistent with all other list endpoints. Co-Authored-By: sonika-shah <sonika-shah@users.noreply.github.com> * fix(#27319): Use unqualified name and json columns in nameFilter condition Consistent with how other ListFilter conditions (getCreatedByCondition, getEntityStatusCondition, getAgentTypeCondition) reference columns — no table qualification needed for single-table queries. Co-Authored-By: sonika-shah <sonika-shah@users.noreply.github.com> * refactor: Extract shared listInternal in EntityRepository, fix GlossaryTerm offset pagination bug - Extract listInternal() from listAfter() to share deserialize + setFieldsInBulk + withHref logic between cursor-based and offset-based listing methods - Add listAfterWithOffset() using the shared listInternal() for offset-based pagination - Fix GlossaryTerm searchGlossaryTermsInternal: replace broken offset-to-cursor conversion (raw offset passed to base64 cursor-based listAfter would crash for offset > 0) with listAfterWithOffset - searchInternal in EntityResource delegates to listAfterWithOffset - Add integration test for GlossaryTerm /search with offset > 0 pagination Co-Authored-By: sonika-shah <sonika-shah@users.noreply.github.com> * fix: Use offset-based ResultList constructor and strengthen test assertions - listAfterWithOffset uses ResultList(data, offset, limit, total) instead of cursor-based constructor that was Base64-encoding offsets - Paging response now returns plain offset/limit/total integers, consistent with other offset-based APIs (e.g. listFromSearchWithOffset) - Tests assert paging.offset and paging.total instead of paging.after - TeamResourceIT creates team with users and policies to properly verify fields param populates requested fields Co-Authored-By: sonika-shah <sonika-shah@users.noreply.github.com> * scope: Remove teams /search endpoint — this PR is roles only Teams search will be added in a follow-up PR. Removed /search endpoint from TeamResource and all search-related tests from TeamResourceIT. Co-Authored-By: sonika-shah <sonika-shah@users.noreply.github.com> * fix: Remove unnecessary limit+1 fetch in listAfterWithOffset Since listCount already provides exact total, we don't need to fetch an extra row to detect has-more. Just fetch exactly limit rows. Co-Authored-By: sonika-shah <sonika-shah@users.noreply.github.com> --------- Co-authored-by: sonika-shah <sonika-shah@users.noreply.github.com> (cherry picked from commit e49b572)
1 parent 6a3af1e commit dd05046

7 files changed

Lines changed: 335 additions & 26 deletions

File tree

openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/GlossaryTermResourceIT.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2667,6 +2667,57 @@ private ResultList<GlossaryTerm> searchGlossaryTerms(
26672667
/** Result list type for deserializing glossary term search results. */
26682668
private static class GlossaryTermResultList extends ResultList<GlossaryTerm> {}
26692669

2670+
@Test
2671+
void test_searchGlossaryTermsWithOffsetPagination(TestNamespace ns) {
2672+
OpenMetadataClient client = SdkClients.adminClient();
2673+
2674+
// Create a dedicated glossary for this test
2675+
CreateGlossary createGlossary =
2676+
new CreateGlossary()
2677+
.withName(ns.prefix("offset_glossary"))
2678+
.withDescription("Glossary for offset pagination test");
2679+
Glossary glossary = client.glossaries().create(createGlossary);
2680+
2681+
// Create 5 terms
2682+
for (int i = 0; i < 5; i++) {
2683+
CreateGlossaryTerm create =
2684+
new CreateGlossaryTerm()
2685+
.withName(ns.prefix("offsetTerm" + i))
2686+
.withGlossary(glossary.getFullyQualifiedName())
2687+
.withDescription("Term for offset test");
2688+
createEntity(create);
2689+
}
2690+
2691+
// Search with no query (empty query path) — page 1
2692+
ResultList<GlossaryTerm> page1 =
2693+
searchGlossaryTerms(client, null, glossary.getFullyQualifiedName(), null, 2, 0);
2694+
assertNotNull(page1.getData());
2695+
assertEquals(2, page1.getData().size());
2696+
assertEquals(5, page1.getPaging().getTotal());
2697+
assertEquals(0, page1.getPaging().getOffset());
2698+
2699+
// Offset=2 skips first 2 rows — this was the bug: offset > 0 with empty query would crash
2700+
ResultList<GlossaryTerm> page2 =
2701+
searchGlossaryTerms(client, null, glossary.getFullyQualifiedName(), null, 2, 2);
2702+
assertNotNull(page2.getData());
2703+
assertEquals(2, page2.getData().size());
2704+
assertEquals(2, page2.getPaging().getOffset());
2705+
2706+
// Offset=4 skips first 4 rows — only 1 remaining
2707+
ResultList<GlossaryTerm> page3 =
2708+
searchGlossaryTerms(client, null, glossary.getFullyQualifiedName(), null, 2, 4);
2709+
assertNotNull(page3.getData());
2710+
assertEquals(1, page3.getData().size());
2711+
assertEquals(4, page3.getPaging().getOffset());
2712+
2713+
// Verify no duplicates across pages
2714+
List<UUID> allIds = new ArrayList<>();
2715+
page1.getData().forEach(t -> allIds.add(t.getId()));
2716+
page2.getData().forEach(t -> allIds.add(t.getId()));
2717+
page3.getData().forEach(t -> allIds.add(t.getId()));
2718+
assertEquals(5, new java.util.HashSet<>(allIds).size(), "No duplicates across pages");
2719+
}
2720+
26702721
@Test
26712722
void test_listGlossaryTermsWithEntityStatusFilter(TestNamespace ns) {
26722723
OpenMetadataClient client = SdkClients.adminClient();

openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/RoleResourceIT.java

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@
3030
import org.openmetadata.schema.entity.teams.Role;
3131
import org.openmetadata.schema.type.EntityHistory;
3232
import org.openmetadata.schema.type.EntityReference;
33+
import org.openmetadata.schema.utils.ResultList;
3334
import org.openmetadata.sdk.client.OpenMetadataClient;
3435
import org.openmetadata.sdk.models.ListParams;
3536
import org.openmetadata.sdk.models.ListResponse;
37+
import org.openmetadata.sdk.network.HttpMethod;
38+
import org.openmetadata.sdk.network.RequestOptions;
3639

3740
/**
3841
* Integration tests for Role entity operations.
@@ -370,6 +373,171 @@ void test_listRolesWithPoliciesField(TestNamespace ns) {
370373
}
371374
}
372375

376+
// ===================================================================
377+
// SEARCH ENDPOINT TESTS
378+
// ===================================================================
379+
380+
@Test
381+
void test_searchRolesEndpoint(TestNamespace ns) {
382+
OpenMetadataClient client = SdkClients.adminClient();
383+
384+
List<String> policyFqns =
385+
dataStewardRole().getPolicies().stream()
386+
.map(EntityReference::getFullyQualifiedName)
387+
.toList();
388+
389+
String uniqueToken = ns.prefix("srch");
390+
391+
// Create roles with distinct names and display names to test both search paths
392+
Role roleByName =
393+
createEntity(
394+
new CreateRole()
395+
.withName(uniqueToken + "ByNameOnly")
396+
.withPolicies(policyFqns)
397+
.withDescription("Role findable by name"));
398+
399+
Role roleByDisplay =
400+
createEntity(
401+
new CreateRole()
402+
.withName(ns.prefix("hiddenName"))
403+
.withPolicies(policyFqns)
404+
.withDisplayName(uniqueToken + " Visible Display")
405+
.withDescription("Role findable by display name, not by name token"));
406+
407+
// Create additional roles for pagination testing
408+
for (int i = 0; i < 4; i++) {
409+
createEntity(
410+
new CreateRole()
411+
.withName(uniqueToken + "Paged" + i)
412+
.withPolicies(policyFqns)
413+
.withDescription("Role for pagination"));
414+
}
415+
416+
// -- Search by shared token should return both name-match and displayName-match roles --
417+
ResultList<Role> allMatches = searchRoles(client, uniqueToken, 50, 0);
418+
assertNotNull(allMatches.getData());
419+
420+
// Should find roleByName (name contains token) AND roleByDisplay (displayName contains token)
421+
// plus the 4 paged roles = 6 total
422+
assertEquals(6, allMatches.getData().size(), "Should find all 6 roles matching the token");
423+
424+
assertTrue(
425+
allMatches.getData().stream().anyMatch(r -> r.getId().equals(roleByName.getId())),
426+
"Should find role matched by name");
427+
assertTrue(
428+
allMatches.getData().stream().anyMatch(r -> r.getId().equals(roleByDisplay.getId())),
429+
"Should find role matched by displayName");
430+
431+
// -- Verify results are ordered by name --
432+
List<String> names = allMatches.getData().stream().map(Role::getName).toList();
433+
List<String> sorted = names.stream().sorted().toList();
434+
assertEquals(sorted, names, "Search results should be ordered by name");
435+
436+
// -- Case-insensitive search: uppercase, lowercase, mixed case all return same results --
437+
ResultList<Role> upperCase = searchRoles(client, uniqueToken.toUpperCase(), 50, 0);
438+
assertEquals(
439+
allMatches.getData().size(),
440+
upperCase.getData().size(),
441+
"UPPERCASE query should return same results as original");
442+
443+
ResultList<Role> lowerCase = searchRoles(client, uniqueToken.toLowerCase(), 50, 0);
444+
assertEquals(
445+
allMatches.getData().size(),
446+
lowerCase.getData().size(),
447+
"lowercase query should return same results as original");
448+
449+
String mixedCase =
450+
uniqueToken.substring(0, 1).toUpperCase() + uniqueToken.substring(1).toLowerCase();
451+
ResultList<Role> mixedCaseResults = searchRoles(client, mixedCase, 50, 0);
452+
assertEquals(
453+
allMatches.getData().size(),
454+
mixedCaseResults.getData().size(),
455+
"MiXeD case query should return same results as original");
456+
457+
// -- Search with no matches returns empty, not an error --
458+
ResultList<Role> noMatches =
459+
searchRoles(client, "nonExistentRoleXyz" + System.nanoTime(), 50, 0);
460+
assertNotNull(noMatches.getData());
461+
assertEquals(0, noMatches.getData().size());
462+
463+
// -- Offset-based pagination: walk through all 6 results in pages of 2 --
464+
ResultList<Role> page1 = searchRoles(client, uniqueToken, 2, 0);
465+
assertEquals(2, page1.getData().size());
466+
assertEquals(6, page1.getPaging().getTotal());
467+
assertEquals(0, page1.getPaging().getOffset());
468+
469+
ResultList<Role> page2 = searchRoles(client, uniqueToken, 2, 2);
470+
assertEquals(2, page2.getData().size());
471+
assertEquals(6, page2.getPaging().getTotal());
472+
assertEquals(2, page2.getPaging().getOffset());
473+
474+
ResultList<Role> page3 = searchRoles(client, uniqueToken, 2, 4);
475+
assertEquals(2, page3.getData().size());
476+
assertEquals(4, page3.getPaging().getOffset());
477+
478+
// Verify no duplicates across pages
479+
List<UUID> allPagedIds = new java.util.ArrayList<>();
480+
page1.getData().forEach(r -> allPagedIds.add(r.getId()));
481+
page2.getData().forEach(r -> allPagedIds.add(r.getId()));
482+
page3.getData().forEach(r -> allPagedIds.add(r.getId()));
483+
assertEquals(6, new java.util.HashSet<>(allPagedIds).size(), "No duplicates across pages");
484+
485+
// -- Empty query falls back to listing all roles --
486+
ResultList<Role> emptyQuery = searchRoles(client, null, 10, 0);
487+
assertNotNull(emptyQuery.getData());
488+
assertTrue(emptyQuery.getData().size() > 0, "Empty query should return roles");
489+
490+
// -- Search with fields param returns requested fields --
491+
ResultList<Role> withPolicies = searchRoles(client, uniqueToken, 10, 0, "policies");
492+
assertNotNull(withPolicies.getData());
493+
assertFalse(withPolicies.getData().isEmpty());
494+
for (Role role : withPolicies.getData()) {
495+
assertNotNull(role.getPolicies(), "Policies field should be populated when requested");
496+
assertFalse(role.getPolicies().isEmpty());
497+
}
498+
499+
// -- Verify soft-deleted roles are excluded by default --
500+
deleteEntity(roleByName.getId().toString());
501+
502+
ResultList<Role> afterDelete = searchRoles(client, uniqueToken, 50, 0);
503+
assertFalse(
504+
afterDelete.getData().stream().anyMatch(r -> r.getId().equals(roleByName.getId())),
505+
"Soft-deleted role should not appear in search results");
506+
assertEquals(5, afterDelete.getData().size(), "Should have one fewer result after soft delete");
507+
508+
// Restore for cleanup
509+
restoreEntity(roleByName.getId().toString());
510+
}
511+
512+
private ResultList<Role> searchRoles(
513+
OpenMetadataClient client, String query, Integer limit, Integer offset) {
514+
return searchRoles(client, query, limit, offset, null);
515+
}
516+
517+
private ResultList<Role> searchRoles(
518+
OpenMetadataClient client, String query, Integer limit, Integer offset, String fields) {
519+
RequestOptions.Builder optionsBuilder = RequestOptions.builder();
520+
if (query != null) {
521+
optionsBuilder.queryParam("q", query);
522+
}
523+
if (limit != null) {
524+
optionsBuilder.queryParam("limit", limit.toString());
525+
}
526+
if (offset != null) {
527+
optionsBuilder.queryParam("offset", offset.toString());
528+
}
529+
if (fields != null) {
530+
optionsBuilder.queryParam("fields", fields);
531+
}
532+
533+
return client
534+
.getHttpClient()
535+
.execute(
536+
HttpMethod.GET, "/v1/roles/search", null, RoleResultList.class, optionsBuilder.build());
537+
}
538+
539+
private static class RoleResultList extends ResultList<Role> {}
540+
373541
// ===================================================================
374542
// VERSION HISTORY SUPPORT
375543
// ===================================================================

openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1981,16 +1981,7 @@ public ResultList<T> listAfter(
19811981
String afterId = cursorMap.get("id");
19821982
List<String> jsons = dao.listAfter(filter, limitParam + 1, afterName, afterId);
19831983

1984-
try (var ignored = phase("jsonDeserialize")) {
1985-
for (String json : jsons) {
1986-
T entity = JsonUtils.readValue(json, entityClass);
1987-
entities.add(entity);
1988-
}
1989-
}
1990-
try (var ignored = phase("setFieldsBulk")) {
1991-
setFieldsInBulk(fields, entities);
1992-
}
1993-
entities.forEach(entity -> withHref(uriInfo, entity));
1984+
entities = listInternal(jsons, fields, uriInfo);
19941985

19951986
String beforeCursor;
19961987
String afterCursor = null;
@@ -2007,6 +1998,28 @@ public ResultList<T> listAfter(
20071998
}
20081999
}
20092000

2001+
public ResultList<T> listAfterWithOffset(
2002+
UriInfo uriInfo, Fields fields, ListFilter filter, int limit, int offset) {
2003+
int total = dao.listCount(filter);
2004+
List<String> jsons = dao.listAfter(filter, limit, offset);
2005+
2006+
List<T> entities = listInternal(jsons, fields, uriInfo);
2007+
2008+
return new ResultList<>(entities, offset, limit, total);
2009+
}
2010+
2011+
private List<T> listInternal(List<String> jsons, Fields fields, UriInfo uriInfo) {
2012+
List<T> entities;
2013+
try (var ignored = phase("jsonDeserialize")) {
2014+
entities = JsonUtils.readObjects(jsons, entityClass);
2015+
}
2016+
try (var ignored = phase("setFieldsBulk")) {
2017+
setFieldsInBulk(fields, entities);
2018+
}
2019+
entities.forEach(entity -> withHref(uriInfo, entity));
2020+
return entities;
2021+
}
2022+
20102023
public ResultList<T> listAfterKeyset(
20112024
ListFilter filter,
20122025
int limitParam,

openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2486,7 +2486,7 @@ private ResultList<GlossaryTerm> searchGlossaryTermsInternal(
24862486
// Build the parent hash for filtering
24872487
String parentHash = parentFqn != null ? FullyQualifiedName.buildHash(parentFqn) + ".%" : "%";
24882488

2489-
// If no search query, use regular listing
2489+
// If no search query, use regular listing with offset-based pagination
24902490
if (query == null || query.trim().isEmpty()) {
24912491
ListFilter filter = new ListFilter(include);
24922492
if (parentFqn != null) {
@@ -2496,21 +2496,7 @@ private ResultList<GlossaryTerm> searchGlossaryTermsInternal(
24962496
filter.addQueryParam("entityStatus", entityStatus);
24972497
}
24982498

2499-
// Use cursor-based pagination with limit and convert offset to cursor
2500-
String afterCursor = offset > 0 ? String.valueOf(offset) : null;
2501-
ResultList<GlossaryTerm> result =
2502-
listAfter(null, getFields(fieldsParam), filter, limit, afterCursor);
2503-
2504-
// Convert pagination info
2505-
String before = offset > 0 ? String.valueOf(Math.max(0, offset - limit)) : null;
2506-
String after =
2507-
result.getPaging() != null && result.getPaging().getAfter() != null
2508-
? String.valueOf(offset + limit)
2509-
: null;
2510-
int total =
2511-
result.getPaging() != null ? result.getPaging().getTotal() : result.getData().size();
2512-
2513-
return new ResultList<>(result.getData(), before, after, total);
2499+
return listAfterWithOffset(null, getFields(fieldsParam), filter, limit, offset);
25142500
}
25152501

25162502
// For search queries, fetch limit+1 to determine if there are more pages

openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/ListFilter.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public String getCondition(String tableName) {
7373
conditions.add(getProviderCondition(tableName));
7474
conditions.add(getEntityStatusCondition(tableName));
7575
conditions.add(getServerIdCondition(tableName));
76+
conditions.add(getNameFilterCondition());
7677
String condition = addCondition(conditions);
7778
return condition.isEmpty() ? "WHERE TRUE" : "WHERE " + condition;
7879
}
@@ -755,6 +756,22 @@ protected String addCondition(List<String> conditions) {
755756
return condition.toString();
756757
}
757758

759+
private String getNameFilterCondition() {
760+
String nameFilter = queryParams.get("nameFilter");
761+
if (nullOrEmpty(nameFilter)) {
762+
return "";
763+
}
764+
String escaped = "%" + escape(nameFilter.trim()) + "%";
765+
queryParams.put("nameFilterParam", escaped);
766+
if (Boolean.TRUE.equals(DatasourceConfig.getInstance().isMySQL())) {
767+
return "(LOWER(name) LIKE LOWER(:nameFilterParam) "
768+
+ "OR LOWER(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(json, '$.displayName')), '')) LIKE LOWER(:nameFilterParam))";
769+
} else {
770+
return "(LOWER(name) LIKE LOWER(:nameFilterParam) "
771+
+ "OR LOWER(COALESCE(json->>'displayName', '')) LIKE LOWER(:nameFilterParam))";
772+
}
773+
}
774+
758775
public static String escapeApostrophe(String name) {
759776
// Escape string to be using in LIKE clause
760777
// "'" is used for indicated start and end of the string. Use "''" to escape it.

openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,30 @@ public ResultList<T> listInternal(
242242
return addHref(uriInfo, resultList);
243243
}
244244

245+
protected ResultList<T> searchInternal(
246+
UriInfo uriInfo,
247+
SecurityContext securityContext,
248+
String fieldsParam,
249+
ListFilter filter,
250+
String query,
251+
int limit,
252+
int offset) {
253+
Fields fields = getFields(fieldsParam);
254+
OperationContext operationContext = new OperationContext(entityType, getViewOperations(fields));
255+
ResourceContextInterface resourceContext = filter.getResourceContext(entityType);
256+
authorizer.authorize(securityContext, operationContext, resourceContext);
257+
258+
EntityUtil.addDomainQueryParam(securityContext, filter, entityType);
259+
260+
if (!nullOrEmpty(query)) {
261+
filter.addQueryParam("nameFilter", query);
262+
}
263+
264+
ResultList<T> resultList =
265+
repository.listAfterWithOffset(uriInfo, fields, filter, limit, offset);
266+
return addHref(uriInfo, resultList);
267+
}
268+
245269
public ResultList<T> listInternalFromSearch(
246270
UriInfo uriInfo,
247271
SecurityContext securityContext,

0 commit comments

Comments
 (0)