Product Promotion
0x5a.live
for different kinds of informations and explorations.
GitHub - JetBrains/space-kotlin-sdk: Kotlin client for JetBrains Space HTTP API
Kotlin client for JetBrains Space HTTP API. Contribute to JetBrains/space-kotlin-sdk development by creating an account on GitHub.
Visit SiteGitHub - JetBrains/space-kotlin-sdk: Kotlin client for JetBrains Space HTTP API
Kotlin client for JetBrains Space HTTP API. Contribute to JetBrains/space-kotlin-sdk development by creating an account on GitHub.
Powered by 0x5a.live 💗
Space SDK
The Space SDK is a Kotlin library to work with the JetBrains Space API.
Overview
Let's have a look at how we can start working with the Space SDK.
Note: The beta version of the Space SDK is available from the following repository:
https://maven.pkg.jetbrains.space/public/p/space/maven
Getting Started
We will need to register an application to work with the Space API. There are various application types, all supporting different authentication flows.
For this example, we will use a Client application. Make sure to enable the Client Credentials Flow in your application's Authentication settings.
Create a Connection
After installing org.jetbrains:space-sdk
in our project, we can use the Client ID and Client Secret of our Space application to create a SpaceHttpClient
that can connect to our Space organization:
val spaceClient = SpaceClient(
SpaceAppInstance(
clientId, // from settings/secrets
clientSecret, // from settings/secrets
organizationUrl // i.e. "https://<organization>.jetbrains.space/"
),
SpaceAuth.ClientCredentials()
)
We can then use the Space HTTP client in spaceClient
to access the various Space API endpoints.
Note: Applications have access to a limited set of APIs when using the Client Credentials Flow. Many actions (such as posting an article draft) require user consent, and cannot be performed with client credentials. For actions that should be performed on behalf of the user, use other authorization flows, such as Resource Owner Password Credentials Flow.
Work with a Service Client
Clients for endpoints are exposed on the Space HTTP client, and map to the top level of endpoints we can find in the HTTP API Playground:
As an example, the top level Team Directory is exposed as spaceClient.teamDirectory
.
Tip: While not required to work with Space SDK, having the HTTP API Playground open will be useful to explore the available APIs and data types while developing.
Get Profile by Username
Let's fetch a user profile from the Team Directory:
val memberProfile = spaceClient.teamDirectory.profiles
.getProfile(ProfileIdentifier.Username("Heather.Stewart"))
The memberProfile
will expose top level properties, such as id
, username
, about
, and more.
To retrieve nested properties, check nested properties.
Space API client
The Space API client provides necessary types to connect to and interact with the Space API.
Authentication and Connection
Communication with Space is requires the Space organization URL and an access token to work. Some features also require app client ID and client secret.
SpaceAuth
defines several methods of authentication.
SpaceAuth.ClientCredentials
supports the Client Credentials Flow. This is typically used by a Space application that acts on behalf of itself. It requires the client to be created with client id and secret specified.SpaceAuth.RefreshToken
supports Refresh Token Flow (and, by the association, Authorization Code Flow). This is typically used by applications that act on behalf of a user. It also requires the client to be created with client id and secret specified. The refresh token can be retrieved usingSpace.exchangeAuthCodeForToken()
, which requires authorization code. Authorization code can be obtained by redirecting the user to a URL constructed withSpace.authCodeSpaceUrl()
SpaceAuth.Token
can be used with any flow. It doesn't require the client id and secret to be specified.
Scope
Scope is a mechanism in OAuth 2.0 to limit an application's access to a user's account.
When setting up the SpaceClient
with the SpaceAuth.ClientCredentials
or SpaceAuth.RefreshToken
auth method, or when creating a URL to request permissions from the user with Space.authCodeSpaceUrl()
, use the scope
parameter to specify the scope required by an application.
Scopes can be defined the following way:
val scope = PermissionScope.build(
PermissionScopeElement(
context = GlobalPermissionContextIdentifier,
permission = PermissionIdentifier.ViewMemberProfiles
),
PermissionScopeElement(
context = ProjectPermissionContextIdentifier(
project = ProjectIdentifier.Key("PROJ")
),
permission = PermissionIdentifier.ViewIssues
)
)
Alternatively, if you already have a scope string, you can use PermissionScope.fromString(scopeString)
.
Warning: By default, the Space API client uses the
PermissionScope.All
scope forSpaceAuth.ClientCredentials
, which requests all available scopes. It is recommended to limit the scope to just those permissions that are needed by your application.
Examples of available scopes are available in the Space documentation.
Endpoints
Clients for endpoints are exposed on the Space HTTP client, and map to the top level of endpoints we can find in the HTTP API Playground:
As an example, the top level Team Directory is exposed as spaceClient.teamDirectory
, with methods that correspond to endpoints seen in the HTTP API Playground.
Properties, Fields and Partials
In this section, we will cover partial requests and shaping response contents.
Background
For most requests, we can shape the results we want to retrieve from the API. In the HTTP API Playground, we can toggle checkboxes to include/exclude fields from the result.
When we want to retrieve a user's id
and about
description, we can set the $fields
parameter in an API request to $fields=id,about
. The API response will then only return those fields.
Fields can be primitive values (integers, strings, booleans, ...), and actual types. As an example, a user profile has a name
field of type TD_ProfileName
, which in turn has a firstName
and lastName
field. To request this hierarchy, we need to query $fields=name(firstName,lastName)
.
Being able to retrieve just the information our integration requires, helps in reducing payload size, and results in a better integration performance overall.
In Space API client, we will need to specify the properties we want to retrieve as well. Let's see how this is done.
Top-level Properties by Default
By default, Space API client will retrieve all top-level properties from the Space API. For example, retrieving a profile from the team directory will retrieve all top level properties, such as id
, username
, about
, and more:
val memberProfile = spaceClient.teamDirectory.profiles
.getProfile(ProfileIdentifier.Username("Heather.Stewart"))
A TD_MemberProfile
, which is the type returned by getProfile
, also has a managers
property. This property is a collection of nested TD_MemberProfile
, and is not retrieved by default.
Nested Properties
Nested properties have to be retrieved explicitly. For example, if we want to retrieve the managers
for a user profile, including the manager's name, we would have to request these properties by extending the default partial result:
val memberProfile = spaceClient.teamDirectory.profiles
.getProfile(ProfileIdentifier.Username("Heather.Stewart")) {
defaultPartial() // with all top level fields
managers { // include managers
id() // with their id
username() // and their username
name { // and their name
firstName() // with firstName
lastName() // and lastName
}
}
}
The Space API client will help with defining properties to include. Let's say we want to retrieve only the id
and username
properties for a profile:
val memberProfile = spaceClient.teamDirectory.profiles
.getProfile(ProfileIdentifier.Username("Heather.Stewart")) {
id()
username()
}
When we try to access the name
property for this profile, which we did not request, Space API client will throw an IllegalStateException
with additional information.
try {
// This will fail...
println("${memberProfile.name.firstName} ${memberProfile.name.lastName}")
} catch (e: IllegalStateException) {
// ...and we'll get a pointer about why it fails:
// Property 'name' was not requested. Reference chain: getAllProfiles->data->[0]->name
println("The Space API client tells us which partial query should be added to access the property:")
println(e.message)
}
Let's look at some other extension methods for retrieving partial responses.
Partial Methods
As demonstrated in the previous section, all builder methods (such as id()
) are methods to help build the partial response based on a property name of an object. There are some other methods and overloads that can be used when building partial responses.
Utility methods:
defaultPartial()
— Adds all fields at the current level (uses the*
field definition under the hood).
Utility overloads exist for retrieving recursive results. For example, if we want to retrieve a user profile and their managers with the same fields, we can use the managers(this)
overload:
val memberProfile = spaceClient.teamDirectory.profiles
.getProfile(ProfileIdentifier.Username("Heather.Stewart")) {
defaultPartial() // with all top level fields
managers(this) // and the same fields for all managers
}
Inheritance
The Space API may return polymorphic responses. In other words, there are several API endpoints that return subclasses.
One such example is spaceClient.projects.planning.issues.getAllIssues()
, where the createdBy
property can be a subclass of CPrincipalDetails
:
CAutomationTaskPrincipalDetails
, when the issue was created by an automation task.CBuiltInServicePrincipalDetails
, when the issue was created by Space itself.CExternalServicePrincipalDetails
, when the issue was created by an external service.CUserWithEmailPrincipalDetails
, when the issue was created by a user that has an e-mail address.CUserPrincipalDetails
, when the issue was created by a user.
The partial builder contains properties for all of these classes.
Here's an example retrieving issues from a project. For the createdBy
property, we are defining that the response should contain:
CUserPrincipalDetails
with theuser.id
property.CUserWithEmailPrincipalDetails
with thename
andemail
properties.
val issueStatuses = spaceClient.projects.planning.issues.statuses
.getAllIssueStatuses(ProjectIdentifier.Key("CRL")).map { it.id }
val issues = spaceClient.projects.planning.issues
.getAllIssues(ProjectIdentifier.Key("CRL"), sorting = IssuesSorting.UPDATED, descending = true, statuses = issueStatuses) {
defaultPartial()
creationTime()
createdBy {
details {
// Available on CUserPrincipalDetails
user {
id()
}
// Available on CUserWithEmailPrincipalDetails,
// CAutomationTaskPrincipalDetails, CBuiltInServicePrincipalDetails
name()
email()
}
}
status()
}.data.forEach { issue ->
when (issue.createdBy.details) {
is CUserPrincipalDetails -> {
// ...
}
is CUserWithEmailPrincipalDetails -> {
// ...
}
}
}
We can cast these types, use when
expressions on their type, and more.
Inaccessible Fields
When a given field can not be accessed, for example when our application does not have the required permissions, a PropertyValueInaccessibleException
may be thrown.
Let's say we try to retrieve the username
and logins
fields for a profile:
val memberProfile = spaceClient.teamDirectory.profiles
.getProfile(ProfileIdentifier.Username("Heather.Stewart")) {
username()
logins()
}
Accessing elements in the logins
field will throw a PropertyValueInaccessibleException
with additional information, in this case because logins
are typically not available to applications.
try {
// This will fail...
memberProfile.logins.forEach { login ->
println(login.identifier)
}
} catch (e: PropertyValueInaccessibleException) {
// ...and we'll get a pointer about why it fails:
// Could not access following fields: 'logins'.
println(e.message)
}
Batch
Many operations in the Space API return a collection of results. To guarantee performance, these responses will be paginated, and can be retrieved in batches.
A batch contains the data, and may return the total count of items that will be returned after fetching all pages:
class Batch<out T>(
val next: String,
val totalCount: Int?,
val data: List<T>)
We have to specify the properties of the data type we need. As an example, let's retrieve the current user's To-Do items for this week, with their id
and content
:
// Get all To-Do
var todoBatchInfo = BatchInfo("0", 100)
do {
val todoBatch = spaceClient.todoItems
.getAllTodoItems(from = LocalDate(2020, 1, 1), batchInfo = todoBatchInfo) {
id()
content()
}
todoBatch.data.forEach { todo ->
// ...
}
todoBatchInfo = BatchInfo(todoBatch.next, 100)
} while (todoBatch.hasNext())
Note: This code example makes use of an extension method
hasNext()
to determine whether more results need to be retrieved:
fun Batch<*>.hasNext() = data.isNotEmpty()
The resulting batch
will contain one page of results. To retrieve more To-Do items, we will have to make additional API calls.
Kotlin Resources
are all listed below.
Made with ❤️
to provide different kinds of informations and resources.