From 8c0312a7d97dacf72f55ca1821b6a0d5f2bad4c8 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Tue, 31 Mar 2026 11:16:09 -0600 Subject: [PATCH 1/5] refactor: cleanup whitespace --- gradle.properties | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 5251aed..ec39fa2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,5 +14,4 @@ systemProp.sonar.tests=src/test/groovy systemProp.sonar.junit.reportPaths=build/test-results/test systemProp.sonar.coverage.jacoco.xmlReportPath=build/reports/jacoco/test/jacocoTestReport.xml systemProp.sonar.java.binaries=build/classes/java/main -systemProp.sonar.java.test.binaries=build/classes/groovy/test - +systemProp.sonar.java.test.binaries=build/classes/groovy/test \ No newline at end of file From 4095201f8583441cbf29c836ebe8f65e7312ab0b Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Tue, 31 Mar 2026 22:38:53 -0600 Subject: [PATCH 2/5] feat: introduce searchClient#list() --- src/main/resources/z4j.yaml | 154 +++++++++++++++++- .../pbu/z4j/client/SearchClientSpec.groovy | 49 ++++++ 2 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy diff --git a/src/main/resources/z4j.yaml b/src/main/resources/z4j.yaml index d7e3760..42715b5 100644 --- a/src/main/resources/z4j.yaml +++ b/src/main/resources/z4j.yaml @@ -575,7 +575,7 @@ paths: $ref: '#/components/schemas/Category' responses: "201": - description: OK Response + description: Created response headers: Location: description: The URL of the new created category @@ -782,12 +782,83 @@ paths: responses: "204": description:

No content

+ /api/v2/search: + get: + operationId: List + tags: + - Search + summary: List Search Results + description: |- +

Returns the search results. See Query syntax for details on the {@code query} parameter.

+

Use the ampersand character (&) to append the {@code sort_by} or {@code sort_order} parameters to the URL.

+

For examples, see Searching with Zendesk API.

+

This endpoint has its own rate limit. The rate limit counts towards the global API rate limit. See Limits.

+

Allowed For

+ + + parameters: + - name: query + in: query + description: Returns the search results. See Query syntax for details on the {@code query} parameter. For details on the query syntax, see the Zendesk Support search reference. + required: true + schema: + type: string + - name: sort_by + in: query + description: One of {@code updated_at}, {@code created_at}, {@code priority}, {@code status}, or {@code ticket_type}. Defaults to sorting by relevance + schema: + $ref: '#/components/schemas/SearchSortBy' + - name: sort_order + in: query + description: Defaults to descending + schema: + $ref: '#/components/schemas/SearchSortOrder' + - $ref: '#/components/parameters/SearchInclude' + responses: + "200": + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/SearchResponse' + "400": + description: Error response + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequestErrorResponse' components: schemas: + SearchSortBy: + type: string + enum: + - updated_at + - created_at + - priority + - status + - ticket_type + x-enum-name: SearchSortBy + x-enum-varnames: + - UPDATED_AT + - CREATED_AT + - PRIORITY + - STATUS + - TICKET_TYPE + SearchSortOrder: + type: string + enum: + - asc + - desc + x-enum-name: SearchSortOrder + x-enum-varnames: + - ASCENDING + - DESCENDING Attachment: type: object description: | -

A file represented as an Attachment object

+ A file represented as an Attachment object allOf: - $ref: '#/components/schemas/AttachmentBase' - $ref: '#/components/schemas/AttachmentThumbnails' @@ -801,7 +872,7 @@ components: content_url: type: string description: | -

A full URL where the attachment image file can be downloaded. The file may be hosted externally so take care not to inadvertently send Zendesk authentication credentials. See Working with url properties

+ A full URL where the attachment image file can be downloaded. The file may be hosted externally so take care not to inadvertently send Zendesk authentication credentials. See Working with url properties readOnly: true deleted: type: boolean @@ -832,6 +903,17 @@ components: malware_scan_result: type: string description: 'The result of the malware scan. There is a delay between the time the attachment is uploaded and when the malware scan is completed. Usually the scan is done within a few seconds, but high load conditions can delay the scan results. Possible values: "malware_found", "malware_not_found", "failed_to_scan", "not_scanned"' + enum: + - malware_found + - malware_not_found + - failed_to_scan + - not_scanned + x-enum-name: MalwareScanResult + x-enum-varnames: + - MALWARE_FOUND + - MALWARE_NOT_FOUND + - FAILED_TO_SCAN + - NOT_SCANNED readOnly: true mapped_content_url: type: string @@ -3259,6 +3341,64 @@ components: type: array items: $ref: '#/components/schemas/Vote' + SearchResult: + type: object + properties: + created_at: + type: string + description: When the resource was created + default: + type: boolean + description: Flag to indicate whether this is the default resource + deleted: + type: boolean + description: Flag to indicate whether or not resource has been deleted + description: + type: string + description: The description of the resource + id: + type: integer + description: The ID of the resource + name: + type: string + description: The name of the resource + result_type: + type: string + description: The type of the resource + updated_at: + type: string + description: When the resource was last updated + url: + type: string + description: The url of the resource + SearchResponse: + type: object + properties: + count: + type: integer + description: The number of resources returned by the query corresponding to this page of results in the paginated response + readOnly: true + facets: + type: string + description: The facets corresponding to the search query + nullable: true + readOnly: true + next_page: + type: string + description: URL to the next page of results + nullable: true + readOnly: true + previous_page: + type: string + description: URL to the previous page of results + nullable: true + readOnly: true + results: + type: array + description: May consist of tickets, users, groups, or organizations, as specified by the `result_type` property in each result object + items: + $ref: '#/components/schemas/SearchResult' + readOnly: true parameters: ArticleAttachmentId: name: article_attachment_id @@ -3352,6 +3492,14 @@ components: type: string example: en-us example: en-us + SearchInclude: + name: include + in: query + description: | + Sideloads to include in the response. Accepts a comma-separated list of values. + The available sideloads depend on the search result types. + schema: + type: string SectionId: name: section_id in: path diff --git a/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy new file mode 100644 index 0000000..356c81d --- /dev/null +++ b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy @@ -0,0 +1,49 @@ +package lol.pbu.z4j.client + +import io.micronaut.http.client.exceptions.HttpClientResponseException +import lol.pbu.z4j.Z4jSpec +import lol.pbu.z4j.model.SearchSortBy +import lol.pbu.z4j.model.SearchSortOrder +import spock.lang.Shared +import spock.lang.Unroll + +class SearchClientSpec extends Z4jSpec { + @Shared + SearchClient adminSearchClient, agentSearchClient, userSearchClient + + def setupSpec() { + adminSearchClient = adminCtx.getBean(SearchClient.class) + agentSearchClient = agentCtx.getBean(SearchClient.class) + userSearchClient = userCtx.getBean(SearchClient.class) + } + + @Unroll("an #clientName user can run the list method with sortby: #sortBy, sortOrder: #sortOrder and include: #include") + void "can run the list method"(String clientName,SearchClient client, SearchSortBy sortBy, SearchSortOrder sortOrder, String include) { + when: + client.list(faker.bluey().quote(), sortBy, sortOrder, include).block() + + then: + noExceptionThrown() + + where: + [[client, clientName], sortBy, sortOrder, include] << [[[adminSearchClient, "admin"], [agentSearchClient, "agent"]], + [SearchSortBy.values(), null].flatten(), + [SearchSortOrder.values(), null].flatten(), + [null, faker.cat().name()]].combinations() + } + + @Unroll("a simple user querying the list method fails with #sortBy, #sortOrder and #include") + void "cannot run searchClient.list()"(SearchClient client, SearchSortBy sortBy, SearchSortOrder sortOrder, String include) { + when: + client.list(faker.bluey().quote(), sortBy, sortOrder, include).block() + + then: + thrown(HttpClientResponseException) + + where: + [client, sortBy, sortOrder, include] << [[userSearchClient], + [SearchSortBy.values(), null].flatten(), + [SearchSortOrder.values(), null].flatten(), + [null, faker.cat().name()]].combinations() + } +} From 455798123b1e64d2f5650a57f07bac8406427a7b Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Wed, 1 Apr 2026 23:09:57 -0600 Subject: [PATCH 3/5] feat: introduce Search#Count method --- src/main/resources/z4j.yaml | 34 +++++++++++++- .../pbu/z4j/client/SearchClientSpec.groovy | 46 +++++++++++++++---- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/main/resources/z4j.yaml b/src/main/resources/z4j.yaml index 42715b5..c6f5288 100644 --- a/src/main/resources/z4j.yaml +++ b/src/main/resources/z4j.yaml @@ -797,7 +797,6 @@ paths:
  • Agents
- parameters: - name: query in: query @@ -829,6 +828,39 @@ paths: application/json: schema: $ref: '#/components/schemas/BadRequestErrorResponse' + /api/v2/search/count: + get: + operationId: Count + tags: + - Search + summary: Show Search Results Count + description: | + Returns the number of items matching the query rather than returning the items. The search string works the same as a regular search. + +

Allowed For

+
    +
  • Agents
  • +
+ parameters: + - name: query + in: query + description: Returns the search results. See Query syntax for details on the {@code query} parameter. For details on the query syntax, see the Zendesk Support search reference. + required: true + schema: + type: string + responses: + "200": + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/SearchResponse' + "400": + description: Error response + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequestErrorResponse' components: schemas: SearchSortBy: diff --git a/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy index 356c81d..09e03d7 100644 --- a/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy +++ b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy @@ -18,18 +18,35 @@ class SearchClientSpec extends Z4jSpec { } @Unroll("an #clientName user can run the list method with sortby: #sortBy, sortOrder: #sortOrder and include: #include") - void "can run the list method"(String clientName,SearchClient client, SearchSortBy sortBy, SearchSortOrder sortOrder, String include) { + void "can run the list method"(String clientName, SearchClient client, SearchSortBy sortBy, SearchSortOrder sortOrder, String include) { when: client.list(faker.bluey().quote(), sortBy, sortOrder, include).block() then: noExceptionThrown() + when: + client.count(faker.bluey().quote()).block() + + then: + noExceptionThrown() + where: [[client, clientName], sortBy, sortOrder, include] << [[[adminSearchClient, "admin"], [agentSearchClient, "agent"]], - [SearchSortBy.values(), null].flatten(), - [SearchSortOrder.values(), null].flatten(), - [null, faker.cat().name()]].combinations() + [SearchSortBy.values(), null].flatten(), + [SearchSortOrder.values(), null].flatten(), + [null, faker.cat().name()]].combinations() + } + + void "an #clientName user can run the count method"(String clientName, SearchClient client) { + when: + client.count(faker.bluey().quote()).block() + + then: + noExceptionThrown() + + where: + [[client, clientName]] << [[adminSearchClient, "admin"], [agentSearchClient, "agent"]] } @Unroll("a simple user querying the list method fails with #sortBy, #sortOrder and #include") @@ -41,9 +58,22 @@ class SearchClientSpec extends Z4jSpec { thrown(HttpClientResponseException) where: - [client, sortBy, sortOrder, include] << [[userSearchClient], - [SearchSortBy.values(), null].flatten(), - [SearchSortOrder.values(), null].flatten(), - [null, faker.cat().name()]].combinations() + [client, sortBy, sortOrder, include] << [[userSearchClient], + [SearchSortBy.values(), null].flatten(), + [SearchSortOrder.values(), null].flatten(), + [null, faker.cat().name()]].combinations() + } + + void "a normal user cannot run the count method"(SearchClient client) { + when: + client.count(faker.bluey().quote()).block() + + then: + thrown(HttpClientResponseException) + + where: + client | _ + userSearchClient | _ + } } From 2d83ac19eab3300e2b7ad5a4939f4b8dc17787bd Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Sat, 4 Apr 2026 19:58:45 -0600 Subject: [PATCH 4/5] feat: add SearchClient#export() --- src/main/resources/z4j.yaml | 92 ++++++++++++++++++- .../pbu/z4j/client/SearchClientSpec.groovy | 21 +++-- 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/src/main/resources/z4j.yaml b/src/main/resources/z4j.yaml index c6f5288..ec029e8 100644 --- a/src/main/resources/z4j.yaml +++ b/src/main/resources/z4j.yaml @@ -797,10 +797,18 @@ paths:
  • Agents
+

Pagination

+
    +
  • Offset pagination only
  • +
+

Offset pagination may result in duplicate results when paging. You can also use the + Export Search Results endpoint, which + uses cursor-based pagination and doesn't return duplicate results. See + Using cursor pagination for more information.

parameters: - name: query in: query - description: Returns the search results. See Query syntax for details on the {@code query} parameter. For details on the query syntax, see the Zendesk Support search reference. + description: Returns the search results. See Query syntax for details on the {@code query} parameter. For details on the query syntax, see the Zendesk Support search reference. required: true schema: type: string @@ -861,6 +869,75 @@ paths: application/json: schema: $ref: '#/components/schemas/BadRequestErrorResponse' + /api/v2/search/export: + get: + operationId: Export + tags: + - Search + summary: Export Search Results + description: |- +

Exports a set of results. See Query syntax for the syntax of the {@code query} parameter.

+

Use this endpoint for search queries that will return more than 1000 results. The result set is ordered only by the {@code created_at} attribute.

+

The search only returns results of a single object type. The following object types are supported: ticket, organization, user, or group.

+

You must specify the type in the {@code filter[type]} parameter. Searches with type in the query string will result in an error.

+

Allowed For

+
    +
  • Agents
  • +
+

Pagination

+
    +
  • Cursor pagination
  • +
+

See Pagination.

+

Returns a maximum of 1000 records per page. The number of results shown in a page is determined by the {@code page[size]} parameter.

+

Note: You may experience a speed reduction or a timeout if you request 1000 results per page and you have many archived tickets in the results. Try reducing the number of results per page. We recommend 100 results per page.

+

The cursor specified by the {@code after_cursor} property in a response expires after one hour.

+

For more information on cursor-based pagination, see the following articles:

+ +

Export Search Results Limits

+

This API endpoint is rate-limited to 100 requests per minute per account. The limit also counts towards the global API rate limit.

+ parameters: + - name: query + in: query + description: Returns the search results. See Query syntax for details on the {@code query} parameter. For details on the query syntax, see the Zendesk Support search reference. + required: true + schema: + type: string + - name: page[size] + in: query + description: The number of results shown in a page. + required: true + schema: + type: integer + - name: page[after] + in: query + description: The cursor token for fetching the next page of results. + required: true + schema: + type: string + - name: filter[type] + in: query + description: The object type returned by the export query. Can be `ticket`, `organization`, `user`, or `group`. + required: true + schema: + $ref: '#/components/schemas/SearchExportType' + - $ref: '#/components/parameters/SearchInclude' + responses: + "200": + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/SearchResponse' + "400": + description: Error response + content: + application/json: + schema: + $ref: '#/components/schemas/BadRequestErrorResponse' components: schemas: SearchSortBy: @@ -887,6 +964,19 @@ components: x-enum-varnames: - ASCENDING - DESCENDING + SearchExportType: + type: string + enum: + - ticket + - organization + - user + - group + x-enum-name: SearchExportType + x-enum-varnames: + - TICKET + - ORGANIZATION + - USER + - GROUP Attachment: type: object description: | diff --git a/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy index 09e03d7..fc42953 100644 --- a/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy +++ b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy @@ -2,6 +2,7 @@ package lol.pbu.z4j.client import io.micronaut.http.client.exceptions.HttpClientResponseException import lol.pbu.z4j.Z4jSpec +import lol.pbu.z4j.model.SearchExportType import lol.pbu.z4j.model.SearchSortBy import lol.pbu.z4j.model.SearchSortOrder import spock.lang.Shared @@ -25,12 +26,6 @@ class SearchClientSpec extends Z4jSpec { then: noExceptionThrown() - when: - client.count(faker.bluey().quote()).block() - - then: - noExceptionThrown() - where: [[client, clientName], sortBy, sortOrder, include] << [[[adminSearchClient, "admin"], [agentSearchClient, "agent"]], [SearchSortBy.values(), null].flatten(), @@ -74,6 +69,20 @@ class SearchClientSpec extends Z4jSpec { where: client | _ userSearchClient | _ + } + void "an #clientName can call export method with pageSize: #pageSize, pageAfter: #pageAfter, filterType: #filterType and include: #include"(String clientName, SearchClient client, int pageSize, String pageAfter, SearchExportType filterType, String include) { + when: + client.export(faker.bluey().quote(), pageSize, pageAfter, filterType, include).block() + + then: + noExceptionThrown() + + where: + [[client, clientName], pageSize, pageAfter, filterType, include] << [[[adminSearchClient, "admin"], [agentSearchClient, "agent"]], + [100], + [faker.internet().uuid()], + SearchExportType.values(), + ["organizations"]].combinations() } } From c19ddb64aec70434d158105074d8bf3ab91708b6 Mon Sep 17 00:00:00 2001 From: jonathan zollinger Date: Sat, 4 Apr 2026 23:13:34 -0600 Subject: [PATCH 5/5] fix(test): correct where block syntax --- src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy index fc42953..b0f12e7 100644 --- a/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy +++ b/src/test/groovy/lol/pbu/z4j/client/SearchClientSpec.groovy @@ -41,7 +41,7 @@ class SearchClientSpec extends Z4jSpec { noExceptionThrown() where: - [[client, clientName]] << [[adminSearchClient, "admin"], [agentSearchClient, "agent"]] + [client, clientName] << [[adminSearchClient, "admin"], [agentSearchClient, "agent"]] } @Unroll("a simple user querying the list method fails with #sortBy, #sortOrder and #include")