diff --git a/kotlin-jira-client/kotlin-jira-client-api/src/main/kotlin/com/linkedplanet/kotlinjiraclient/api/interfaces/JiraIssueOperator.kt b/kotlin-jira-client/kotlin-jira-client-api/src/main/kotlin/com/linkedplanet/kotlinjiraclient/api/interfaces/JiraIssueOperator.kt index ed7aa423..f6c89279 100644 --- a/kotlin-jira-client/kotlin-jira-client-api/src/main/kotlin/com/linkedplanet/kotlinjiraclient/api/interfaces/JiraIssueOperator.kt +++ b/kotlin-jira-client/kotlin-jira-client-api/src/main/kotlin/com/linkedplanet/kotlinjiraclient/api/interfaces/JiraIssueOperator.kt @@ -24,6 +24,7 @@ import com.google.gson.JsonObject import com.linkedplanet.kotlinatlassianclientcore.common.api.Page import com.linkedplanet.kotlinjiraclient.api.error.JiraClientError import com.linkedplanet.kotlinjiraclient.api.model.JiraIssue +import com.linkedplanet.kotlinjiraclient.api.model.IssueQueryParams /** * Provides methods for working with Jira issues, including retrieving issues by JQL query, issue type, or key; creating and updating issues; and deleting issues. @@ -40,6 +41,7 @@ interface JiraIssueOperator { */ suspend fun getIssuesByJQL( jql: String, + queryParams: IssueQueryParams = IssueQueryParams(), parser: suspend (JsonObject, Map) -> Either ): Either> @@ -55,6 +57,7 @@ interface JiraIssueOperator { jql: String, pageIndex: Int = 0, pageSize: Int = RESULTS_PER_PAGE, + queryParams: IssueQueryParams = IssueQueryParams(), parser: suspend (JsonObject, Map) -> Either ): Either> @@ -66,6 +69,7 @@ interface JiraIssueOperator { */ suspend fun getIssueByJQL( jql: String, + queryParams: IssueQueryParams = IssueQueryParams(), parser: suspend (JsonObject, Map) -> Either ): Either @@ -79,6 +83,7 @@ interface JiraIssueOperator { suspend fun getIssuesByIssueType( projectId: Long, issueTypeId: Int, + queryParams: IssueQueryParams = IssueQueryParams(), parser: suspend (JsonObject, Map) -> Either ): Either> @@ -96,6 +101,7 @@ interface JiraIssueOperator { issueTypeId: Int, pageIndex: Int = 0, pageSize: Int = RESULTS_PER_PAGE, + queryParams: IssueQueryParams = IssueQueryParams(), parser: suspend (JsonObject, Map) -> Either ): Either> @@ -108,6 +114,7 @@ interface JiraIssueOperator { */ suspend fun getIssueByKey( key: String, + queryParams: IssueQueryParams = IssueQueryParams(), parser: suspend (JsonObject, Map) -> Either ): Either @@ -119,6 +126,7 @@ interface JiraIssueOperator { */ suspend fun getIssueById( id: Int, + queryParams: IssueQueryParams = IssueQueryParams(), parser: suspend (JsonObject, Map) -> Either ): Either diff --git a/kotlin-jira-client/kotlin-jira-client-api/src/main/kotlin/com/linkedplanet/kotlinjiraclient/api/model/ExpandedOption.kt b/kotlin-jira-client/kotlin-jira-client-api/src/main/kotlin/com/linkedplanet/kotlinjiraclient/api/model/ExpandedOption.kt new file mode 100644 index 00000000..66028d0d --- /dev/null +++ b/kotlin-jira-client/kotlin-jira-client-api/src/main/kotlin/com/linkedplanet/kotlinjiraclient/api/model/ExpandedOption.kt @@ -0,0 +1,41 @@ +/*- + * #%L + * kotlin-jira-client-api + * %% + * Copyright (C) 2022 - 2025 linked-planet GmbH + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.linkedplanet.kotlinjiraclient.api.model + +enum class ExpandedOption(val key: String) { + VERSIONED_REPRESENTATIONS("versionedRepresentations"), + RENDERED_FIELDS("renderedFields"), + NAMES("names"), + SCHEMA("schema"), + TRANSITIONS("transitions"), + OPERATIONS("operations"), + EDITMETA("editmeta"), + CHANGELOG("changelog"); + + companion object { + fun fromKey(key: String): ExpandedOption? { + return values().find { it.key == key } + } + } + + override fun toString(): String { + return this.key + } +} diff --git a/kotlin-jira-client/kotlin-jira-client-api/src/main/kotlin/com/linkedplanet/kotlinjiraclient/api/model/IssueQueryParams.kt b/kotlin-jira-client/kotlin-jira-client-api/src/main/kotlin/com/linkedplanet/kotlinjiraclient/api/model/IssueQueryParams.kt new file mode 100644 index 00000000..75c0c708 --- /dev/null +++ b/kotlin-jira-client/kotlin-jira-client-api/src/main/kotlin/com/linkedplanet/kotlinjiraclient/api/model/IssueQueryParams.kt @@ -0,0 +1,32 @@ +/*- + * #%L + * kotlin-jira-client-api + * %% + * Copyright (C) 2022 - 2025 linked-planet GmbH + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.linkedplanet.kotlinjiraclient.api.model + +import com.linkedplanet.kotlinjiraclient.api.model.ExpandedOption.NAMES +import com.linkedplanet.kotlinjiraclient.api.model.ExpandedOption.TRANSITIONS + +/** + * Controls expansion and other parameters of the returned Jira Issue. + * + * The following parameters are planed for the future: fields, fieldsByKeys, properties + */ +data class IssueQueryParams( + val expanded: List = listOf(NAMES, TRANSITIONS) +) diff --git a/kotlin-jira-client/kotlin-jira-client-http/src/main/kotlin/com/linkedplanet/kotlinjiraclient/http/HttpJiraIssueOperator.kt b/kotlin-jira-client/kotlin-jira-client-http/src/main/kotlin/com/linkedplanet/kotlinjiraclient/http/HttpJiraIssueOperator.kt index 8224cf51..a4428452 100644 --- a/kotlin-jira-client/kotlin-jira-client-http/src/main/kotlin/com/linkedplanet/kotlinjiraclient/http/HttpJiraIssueOperator.kt +++ b/kotlin-jira-client/kotlin-jira-client-http/src/main/kotlin/com/linkedplanet/kotlinjiraclient/http/HttpJiraIssueOperator.kt @@ -28,6 +28,7 @@ import com.linkedplanet.kotlinjiraclient.api.error.JiraClientError import com.linkedplanet.kotlinjiraclient.api.interfaces.JiraIssueOperator import com.linkedplanet.kotlinjiraclient.api.model.JiraIssue import com.linkedplanet.kotlinatlassianclientcore.common.api.Page +import com.linkedplanet.kotlinjiraclient.api.model.IssueQueryParams import com.linkedplanet.kotlinjiraclient.http.field.HttpJiraField import com.linkedplanet.kotlinjiraclient.http.model.HttpMappingField import com.linkedplanet.kotlinjiraclient.http.util.fromHttpDomainError @@ -41,6 +42,7 @@ class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIs override suspend fun getIssuesByJQL( jql: String, + queryParams: IssueQueryParams, parser: suspend (JsonObject, Map) -> Either ): Either> = either { recursiveRestCallPaginatedRaw { index, maxSize -> @@ -51,7 +53,7 @@ class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIs "jql" to jql, "startAt" to index.toString(), "maxResults" to maxSize.toString(), - "expand" to "names,transitions", + "expand" to queryParams.expanded.joinToString(","), ), null, "application/json", @@ -75,6 +77,7 @@ class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIs jql: String, pageIndex: Int, pageSize: Int, + queryParams: IssueQueryParams, parser: suspend (JsonObject, Map) -> Either ): Either> = either { val page = context.httpClient.executeGet( @@ -83,7 +86,7 @@ class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIs "jql" to jql, "startAt" to (pageIndex * pageSize).toString(), "maxResults" to pageSize.toString(), - "expand" to "names,transitions", + "expand" to queryParams.expanded.joinToString(","), ), object : TypeToken() {}.type ) @@ -111,17 +114,20 @@ class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIs override suspend fun getIssueByJQL( jql: String, + queryParams: IssueQueryParams, parser: suspend (JsonObject, Map) -> Either ): Either = either { - getIssuesByJQLPaginated(jql, 0, 1, parser).bind().items.firstOrNull() + getIssuesByJQLPaginated(jql, 0, 1, queryParams, parser).bind().items.firstOrNull() } override suspend fun getIssuesByIssueType( projectId: Long, issueTypeId: Int, + queryParams: IssueQueryParams, parser: suspend (JsonObject, Map) -> Either ): Either> = either { - getIssuesByJQL("project=$projectId AND issueType=$issueTypeId", parser).bind() + val jql = "project=$projectId AND issueType=$issueTypeId" + getIssuesByJQL(jql, queryParams, parser).bind() } override suspend fun getIssuesByTypePaginated( @@ -129,19 +135,22 @@ class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIs issueTypeId: Int, pageIndex: Int, pageSize: Int, + queryParams: IssueQueryParams, parser: suspend (JsonObject, Map) -> Either ): Either> = either { - getIssuesByJQLPaginated("project=$projectId AND issueType=$issueTypeId", pageIndex, pageSize, parser).bind() + val jql = "project=$projectId AND issueType=$issueTypeId" + getIssuesByJQLPaginated(jql, pageIndex, pageSize, queryParams, parser).bind() } override suspend fun getIssueByKey( key: String, + queryParams: IssueQueryParams, parser: suspend (JsonObject, Map) -> Either ): Either = either { val successResponse = context.httpClient.executeGetCall( "/rest/api/2/issue/${key}", mapOf( - "expand" to "names,transitions" + "expand" to queryParams.expanded.joinToString(","), ), ) .map { it.body } @@ -167,8 +176,9 @@ class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIs override suspend fun getIssueById( id: Int, + queryParams: IssueQueryParams, parser: suspend (JsonObject, Map) -> Either - ): Either = getIssueByKey(id.toString(), parser) + ): Either = getIssueByKey(id.toString(), queryParams, parser) override suspend fun createIssue( projectId: Long, diff --git a/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/SdkJiraIssueOperator.kt b/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/SdkJiraIssueOperator.kt index 4680f6c2..b905b0e5 100644 --- a/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/SdkJiraIssueOperator.kt +++ b/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/SdkJiraIssueOperator.kt @@ -45,6 +45,7 @@ import com.linkedplanet.kotlinatlassianclientcore.common.error.asEither import com.linkedplanet.kotlinjiraclient.api.error.JiraClientError import com.linkedplanet.kotlinjiraclient.api.interfaces.JiraIssueOperator import com.linkedplanet.kotlinjiraclient.api.model.JiraIssue +import com.linkedplanet.kotlinjiraclient.api.model.IssueQueryParams import com.linkedplanet.kotlinjiraclient.sdk.field.SdkJiraField import com.linkedplanet.kotlinjiraclient.sdk.util.IssueJsonConverter import com.linkedplanet.kotlinjiraclient.sdk.util.catchJiraClientError @@ -134,15 +135,17 @@ object SdkJiraIssueOperator : JiraIssueOperator { override suspend fun getIssueById( id: Int, + queryParams: IssueQueryParams, parser: suspend (JsonObject, Map) -> Either ): Either = - getIssueByKey(id.toString(), parser) + getIssueByKey(id.toString(), queryParams, parser) override suspend fun getIssueByJQL( jql: String, + queryParams: IssueQueryParams, parser: suspend (JsonObject, Map) -> Either ): Either = either { - val potentiallyMultipleIssues = getIssuesByJQLPaginated(jql, 0, 1, parser).bind() + val potentiallyMultipleIssues = getIssuesByJQLPaginated(jql, 0, 1, queryParams, parser).bind() if (potentiallyMultipleIssues.totalItems < 1) { JiraClientError("Issue not found", "No issue was found.").asEither().bind() } @@ -152,21 +155,28 @@ object SdkJiraIssueOperator : JiraIssueOperator { override suspend fun getIssuesByIssueType( projectId: Long, issueTypeId: Int, + queryParams: IssueQueryParams, parser: suspend (JsonObject, Map) -> Either - ): Either> = - getIssuesByJQL("project=$projectId AND issueType=$issueTypeId", parser) + ): Either> { + val jql = "project=$projectId AND issueType=$issueTypeId" + return getIssuesByJQL(jql, queryParams, parser) + } override suspend fun getIssuesByTypePaginated( projectId: Long, issueTypeId: Int, pageIndex: Int, pageSize: Int, + queryParams: IssueQueryParams, parser: suspend (JsonObject, Map) -> Either - ): Either> = - getIssuesByJQLPaginated("project=$projectId AND issueType=$issueTypeId", pageIndex, pageSize, parser) + ): Either> { + val jql = "project=$projectId AND issueType=$issueTypeId" + return getIssuesByJQLPaginated(jql, pageIndex, pageSize, queryParams, parser) + } override suspend fun getIssueByKey( key: String, + queryParams: IssueQueryParams, parser: suspend (JsonObject, Map) -> Either ): Either = either { Either.catchJiraClientError { @@ -176,15 +186,16 @@ object SdkJiraIssueOperator : JiraIssueOperator { } val issue = issueResult.toEither().bind().issue ?: return@catchJiraClientError null - issueToConcreteType(issue, parser).bind() + issueToConcreteType(issue, queryParams, parser).bind() }.bind() } override suspend fun getIssuesByJQL( jql: String, + queryParams: IssueQueryParams, parser: suspend (JsonObject, Map) -> Either ): Either> = either { - val issuePage = getIssuesByJqlWithPagerFilter(jql, PagerFilter.getUnlimitedFilter(), parser).bind() + val issuePage = getIssuesByJqlWithPagerFilter(jql, PagerFilter.getUnlimitedFilter(), queryParams, parser).bind() issuePage.items } @@ -192,15 +203,19 @@ object SdkJiraIssueOperator : JiraIssueOperator { jql: String, pageIndex: Int, pageSize: Int, + queryParams: IssueQueryParams, parser: suspend (JsonObject, Map) -> Either - ): Either> = - getIssuesByJqlWithPagerFilter(jql, PagerFilter.newPageAlignedFilter(pageIndex * pageSize, pageSize), parser) + ): Either> { + val pagerFilter = PagerFilter.newPageAlignedFilter(pageIndex * pageSize, pageSize) + return getIssuesByJqlWithPagerFilter(jql, pagerFilter, queryParams, parser) + } private suspend fun issueToConcreteType( issue: Issue, + queryParams: IssueQueryParams, parser: suspend (JsonObject, Map) -> Either ): Either = Either.catchJiraClientError { - val jsonIssue: JsonObject = issueJsonConverter.createJsonIssue(issue) + val jsonIssue: JsonObject = issueJsonConverter.createJsonIssue(issue, queryParams) val customFieldMap = customFieldManager.getCustomFieldObjects(issue).associate { it.name to it.id } return parser(jsonIssue, customFieldMap) } @@ -208,13 +223,14 @@ object SdkJiraIssueOperator : JiraIssueOperator { private suspend fun getIssuesByJqlWithPagerFilter( jql: String, pagerFilter: PagerFilter<*>?, + queryParams: IssueQueryParams, parser: suspend (JsonObject, Map) -> Either ): Either> = either { val user = userOrError().bind() val query = Either.catchJiraClientError { jqlParser.parseQuery(jql) }.bind() val search = Either.catchJiraClientError { searchService.search(user, query, pagerFilter) }.bind() val issues = search.results - .map { issue -> issueToConcreteType(issue, parser) } + .map { issue -> issueToConcreteType(issue, queryParams, parser) } .bindAll() val totalItems = search.total val pageSize = pagerFilter?.pageSize ?: 0 diff --git a/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/util/IssueJsonConverter.kt b/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/util/IssueJsonConverter.kt index 912326c0..afd607ad 100644 --- a/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/util/IssueJsonConverter.kt +++ b/kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/util/IssueJsonConverter.kt @@ -30,6 +30,7 @@ import com.atlassian.jira.rest.v2.issue.IncludedFields import com.atlassian.jira.rest.v2.issue.IssueBean import com.atlassian.jira.rest.v2.issue.builder.BeanBuilderFactory import com.google.gson.* +import com.linkedplanet.kotlinjiraclient.api.model.IssueQueryParams import com.linkedplanet.kotlinjiraclient.sdk.field.FieldAccessorImpl import org.slf4j.LoggerFactory import javax.ws.rs.core.UriBuilder @@ -66,10 +67,13 @@ class IssueJsonConverter { @Throws(FieldException::class) - fun createJsonIssue(issue: Issue): JsonObject { - val expand = "names,transitions" + fun createJsonIssue( + issue: Issue, + queryParams: IssueQueryParams, + ): JsonObject { + val expanded = queryParams.expanded.joinToString(",") val issueBean: IssueBean = beanBuilderFactory - .newIssueBeanBuilder2(IncludedFields.includeNavigableByDefault(null), expand, uriBuilder) + .newIssueBeanBuilder2(IncludedFields.includeNavigableByDefault(null), expanded, uriBuilder) .build(issue) this.addOrderableFieldsToBean(issueBean, issue) this.addAvailableNavigableFieldsToBean(issueBean, issue) diff --git a/kotlin-jira-client/kotlin-jira-client-test-base/src/main/kotlin/com/linkedplanet/kotlinjiraclient/JiraIssueOperatorTest.kt b/kotlin-jira-client/kotlin-jira-client-test-base/src/main/kotlin/com/linkedplanet/kotlinjiraclient/JiraIssueOperatorTest.kt index 017b7b29..668c560e 100644 --- a/kotlin-jira-client/kotlin-jira-client-test-base/src/main/kotlin/com/linkedplanet/kotlinjiraclient/JiraIssueOperatorTest.kt +++ b/kotlin-jira-client/kotlin-jira-client-test-base/src/main/kotlin/com/linkedplanet/kotlinjiraclient/JiraIssueOperatorTest.kt @@ -22,6 +22,8 @@ package com.linkedplanet.kotlinjiraclient import arrow.core.* import arrow.core.raise.either import com.linkedplanet.kotlinatlassianclientcore.common.api.Page +import com.linkedplanet.kotlinjiraclient.api.model.ExpandedOption +import com.linkedplanet.kotlinjiraclient.api.model.IssueQueryParams import com.linkedplanet.kotlinjiraclient.util.* import java.time.ZoneOffset import java.time.ZonedDateTime @@ -31,6 +33,7 @@ import org.hamcrest.CoreMatchers.* import org.hamcrest.MatcherAssert.assertThat import org.junit.Test +@Suppress("FunctionName") interface JiraIssueOperatorTest : BaseTestConfigProvider { @Test @@ -41,7 +44,7 @@ interface JiraIssueOperatorTest : BaseTestConfigProvider : BaseTestConfigProvider : BaseTestConfigProvider : BaseTestConfigProvider : BaseTestConfigProvider : BaseTestConfigProvider : BaseTestConfigProvider : BaseTestConfigProvider : BaseTestConfigProvider : BaseTestConfigProvider : BaseTestConfigProvider : BaseTestConfigProvider : BaseTestConfigProvider( private val epicIssueTypeId: Int, private val projectId: Long ) { - fun createDefaultIssue(vararg fields: JiraFieldType) = createIssue(issueTypeId, *fields) + fun createDefaultIssue(vararg fields: JiraFieldType): JiraIssue = createIssue(issueTypeId, *fields) - fun createEpic(vararg fields: JiraFieldType) = createIssue(epicIssueTypeId, *fields) + fun createEpic(vararg fields: JiraFieldType): JiraIssue = createIssue(epicIssueTypeId, *fields) - fun createIssue(jiraIssueTypeId: Int, vararg fields: JiraFieldType) = + fun createIssue(jiraIssueTypeId: Int, vararg fields: JiraFieldType): JiraIssue = runBlocking { val combinedFields = listOf( fieldFactory.jiraProjectField(projectId), @@ -53,8 +55,8 @@ class JiraIssueTestHelper( issueOperator.createIssue(projectId, jiraIssueTypeId, combinedFields) }.orFail() - fun getIssueByKey(key: String) = runBlocking { - issueOperator.getIssueByKey(key, ::issueParser) + fun getIssueByKey(key: String): Story = runBlocking { + issueOperator.getIssueByKey(key, IssueQueryParams(), ::issueParser) }.orFail() } @@ -79,7 +81,7 @@ data class Story( val transitions: List ) -suspend fun issueParser(jsonObject: JsonObject, map: Map): Either = +fun issueParser(jsonObject: JsonObject, map: Map): Either = either { val fields = jsonObject.get("fields").asJsonObject @@ -115,13 +117,14 @@ suspend fun issueParser(jsonObject: JsonObject, map: Map): Eithe ) val transitions = - jsonObject.get("transitions").asJsonArray - .map { + jsonObject.get("transitions")?.asJsonArray + ?.map { val transition = it.asJsonObject val name = transition.get("name").asString val id = transition.get("id").asString JiraTransition(id, name) } + ?: emptyList() val epicKey: String? = fieldByName("Epic Link")?.asString