Skip to content

Commit

Permalink
Prepare for release 0.0.4.
Browse files Browse the repository at this point in the history
  • Loading branch information
petersamokhin committed Nov 17, 2021
1 parent a9a9dbc commit 1b01490
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 45 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change log

Version 0.0.4 *(2021-11-16)*
----------------------------
* Fix missing pagination logic for block children endpoint
* Fix indentation for the child pages content recursively rendered to Markdown
* Unify list result response data model

Version 0.0.3 *(2021-11-16)*
----------------------------
* [`/databases/:id/query`](https://developers.notion.com/reference/retrieve-a-database) JSON query as a raw string support
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ implementation("com.petersamokhin.notionsdk:notionsdk:$latestVersion")
```
Library is published to the Maven Central repository.

Latest version: [![maven-central](https://img.shields.io/badge/Maven%20Central-0.0.3-yellowgreen?style=flat)](https://search.maven.org/search?q=g:com.petersamokhin.notionsdk)
Latest version: [![maven-central](https://img.shields.io/badge/Maven%20Central-0.0.4-yellowgreen?style=flat)](https://search.maven.org/search?q=g:com.petersamokhin.notionsdk)

### Supported endpoints
- [`/databases/:id/query`](https://developers.notion.com/reference/retrieve-a-database)
Expand All @@ -39,7 +39,7 @@ val notion = Notion.fromToken(
val schema = notion.retrieveDatabase("databaseId")
val database = notion.queryDatabase("databaseId")

val uncheckedRowSelectedOptionsIds = database.rows
val uncheckedRowSelectedOptionsIds = database.results
.first { row ->
val checkboxColumnSelected = (row.columns
.getValue("CheckboxColumn")
Expand Down Expand Up @@ -99,7 +99,7 @@ val notion = Notion.fromToken(
token = "token",
httpClient = HttpClient(CIO)
)
val blocks: List<NotionBlock> = notion.retrieveBlockChildren("page-id")
val blocks: List<NotionBlock> = notion.retrieveBlockChildren("page-id").results

val exporter = NotionMarkdownExporter.create()

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
GROUP=com.petersamokhin.notionsdk
VERSION_NAME=0.0.3
VERSION_NAME=0.0.4

POM_ARTIFACT_ID=notionsdk
POM_NAME=Notion SDK Kotlin Multiplatform
Expand Down
11 changes: 6 additions & 5 deletions src/commonMain/kotlin/com/petersamokhin/notionsdk/Notion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package com.petersamokhin.notionsdk

import com.petersamokhin.notionsdk.data.NotionApiVersion
import com.petersamokhin.notionsdk.data.model.result.NotionBlock
import com.petersamokhin.notionsdk.data.model.result.NotionDatabaseRow
import com.petersamokhin.notionsdk.data.model.result.NotionDatabaseSchema
import com.petersamokhin.notionsdk.data.model.result.NotionDatabase
import com.petersamokhin.notionsdk.data.model.result.NotionResults
import io.ktor.client.*
import io.ktor.utils.io.core.*
import kotlin.jvm.JvmStatic
Expand All @@ -20,7 +21,7 @@ public interface Notion : Closeable {
databaseId: String,
startCursor: String? = null,
pageSize: Int? = null,
): NotionDatabase
): NotionResults<NotionDatabaseRow>

/**
* Notion API filter & sort params are too complicated to cover all the cases via strictly-typed models.
Expand All @@ -31,7 +32,7 @@ public interface Notion : Closeable {
public suspend fun queryDatabase(
databaseId: String,
jsonRequestBody: String,
): NotionDatabase
): NotionResults<NotionDatabaseRow>

/**
* @see <a href="https://developers.notion.com/reference/retrieve-a-database">Notion documentation</a>
Expand All @@ -54,7 +55,7 @@ public interface Notion : Closeable {
blockId: String,
startCursor: String? = null,
pageSize: Int? = null,
): List<NotionBlock>
): NotionResults<NotionBlock>

public companion object {
public const val HEADER_VERSION: String = "Notion-Version"
Expand All @@ -64,7 +65,7 @@ public interface Notion : Closeable {
public fun fromToken(
token: String,
version: NotionApiVersion = NotionApiVersion.V_2021_08_16,
httpClient: HttpClient
httpClient: HttpClient,
): Notion =
NotionImpl(token, version, httpClient)
}
Expand Down
15 changes: 9 additions & 6 deletions src/commonMain/kotlin/com/petersamokhin/notionsdk/NotionImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import com.petersamokhin.notionsdk.data.model.internal.response.PageObject
import com.petersamokhin.notionsdk.data.model.internal.response.ResultsResponse
import com.petersamokhin.notionsdk.data.model.internal.response.RetrieveDatabaseResponse
import com.petersamokhin.notionsdk.data.model.result.NotionBlock
import com.petersamokhin.notionsdk.data.model.result.NotionDatabase
import com.petersamokhin.notionsdk.data.model.result.NotionDatabaseRow
import com.petersamokhin.notionsdk.data.model.result.NotionDatabaseSchema
import com.petersamokhin.notionsdk.data.model.result.NotionResults
import io.ktor.client.*
import io.ktor.client.features.*
import io.ktor.client.features.json.*
Expand Down Expand Up @@ -60,7 +61,7 @@ internal class NotionImpl(
databaseId: String,
startCursor: String?,
pageSize: Int?,
): NotionDatabase =
): NotionResults<NotionDatabaseRow> =
httpClient.post<ResultsResponse<PageObject>>("${Notion.API_BASE_URL}/${ENDPOINT_DATABASES}/$databaseId/$PATH_QUERY") {
contentType(ContentType.Application.Json)
body = QueryDatabaseRequest(
Expand All @@ -69,7 +70,10 @@ internal class NotionImpl(
)
}.toDomain()

override suspend fun queryDatabase(databaseId: String, jsonRequestBody: String): NotionDatabase =
override suspend fun queryDatabase(
databaseId: String,
jsonRequestBody: String,
): NotionResults<NotionDatabaseRow> =
httpClient.post<ResultsResponse<PageObject>>("${Notion.API_BASE_URL}/${ENDPOINT_DATABASES}/$databaseId/$PATH_QUERY") {
body = TextContent(jsonRequestBody, ContentType.Application.Json)
}.toDomain()
Expand All @@ -88,12 +92,11 @@ internal class NotionImpl(
blockId: String,
startCursor: String?,
pageSize: Int?,
): List<NotionBlock> =
): NotionResults<NotionBlock> =
httpClient.get<ResultsResponse<Block>>("${Notion.API_BASE_URL}/${ENDPOINT_BLOCKS}/$blockId/$PATH_CHILDREN") {
parameter(QUERY_PARAM_START_CURSOR, startCursor)
parameter(QUERY_PARAM_PAGE_SIZE, pageSize)
}.results
.map(Block::toDomain)
}.toDomain()

companion object {
private const val ENDPOINT_BLOCKS = "blocks"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
package com.petersamokhin.notionsdk.data.mapper

import com.petersamokhin.notionsdk.data.model.internal.obj.Block
import com.petersamokhin.notionsdk.data.model.internal.response.PageObject
import com.petersamokhin.notionsdk.data.model.internal.response.ResultsResponse
import com.petersamokhin.notionsdk.data.model.result.NotionDatabase
import com.petersamokhin.notionsdk.data.model.result.NotionBlock
import com.petersamokhin.notionsdk.data.model.result.NotionDatabaseRow
import com.petersamokhin.notionsdk.data.model.result.NotionResults

internal fun ResultsResponse<PageObject>.toDomain(): NotionDatabase =
NotionDatabase(
rows = results.map(PageObject::toDomain),
@Suppress("UNCHECKED_CAST")
internal inline fun <reified T : Any, reified R : Any> ResultsResponse<T>.toDomain(): NotionResults<R> =
NotionResults(
results = (
when (T::class) {
PageObject::class -> when (R::class) {
NotionDatabaseRow::class -> results.filterIsInstance<PageObject>().map(PageObject::toDomain)
else -> null
}
Block::class -> when (R::class) {
NotionBlock::class -> results.filterIsInstance<Block>().map(Block::toDomain)
else -> null
}
else -> error("${T::class} results response domain mapping is not supported")
} ?: error("${T::class} -> ${R::class} results response domain mapping is not supported")
) as List<R>,
nextCursor = nextCursor,
hasMore = hasMore,
)
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.petersamokhin.notionsdk.data.model.result

import com.petersamokhin.notionsdk.data.model.serializer.NotionResultsTypedSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
public data class NotionDatabase(
val rows: List<NotionDatabaseRow>,
@Serializable(with = NotionResultsTypedSerializer::class)
public data class NotionResults<T>(
val results: List<T>,

@SerialName("next_cursor")
val nextCursor: String? = null,
Expand Down Expand Up @@ -35,7 +36,8 @@ public sealed class NotionDatabaseProperty {

@Serializable
@SerialName("rich_text")
public data class Text(override val id: String, val text: String, val parts: List<Part>) : NotionDatabaseProperty() {
public data class Text(override val id: String, val text: String, val parts: List<Part>) :
NotionDatabaseProperty() {
@Serializable
public data class Part(val text: String, val url: String?)
}
Expand All @@ -48,7 +50,7 @@ public sealed class NotionDatabaseProperty {
@SerialName("select")
public data class Select(
override val id: String,
val selected: Option?
val selected: Option?,
) : NotionDatabaseProperty() {
@Serializable
public data class Option(
Expand All @@ -61,7 +63,7 @@ public sealed class NotionDatabaseProperty {
@SerialName("multi_select")
public data class MultiSelect(
override val id: String,
val selected: List<Select.Option>
val selected: List<Select.Option>,
) : NotionDatabaseProperty()

@Serializable
Expand All @@ -81,7 +83,7 @@ public sealed class NotionDatabaseProperty {
@SerialName("avatar_url")
val avatarUrl: String?,
val email: String,
): Person()
) : Person()

@Serializable
@SerialName("user")
Expand All @@ -90,7 +92,7 @@ public sealed class NotionDatabaseProperty {
val name: String,
@SerialName("avatar_url")
val avatarUrl: String? = null,
): Person()
) : Person()
}
}

Expand Down Expand Up @@ -127,13 +129,13 @@ public sealed class NotionDatabaseProperty {
public data class PhoneNumber(
override val id: String,
@SerialName("phone_number")
val phoneNumber: String? = null
val phoneNumber: String? = null,
) : NotionDatabaseProperty()

@Serializable
@SerialName("formula")
public data class Formula(
override val id: String, val formula: Item
override val id: String, val formula: Item,
) : NotionDatabaseProperty() {
@Serializable
public sealed class Item {
Expand Down Expand Up @@ -195,6 +197,6 @@ public sealed class NotionDatabaseProperty {
@Serializable
@SerialName("rollup")
public data class Rollup(
override val id: String
override val id: String,
) : NotionDatabaseProperty()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.petersamokhin.notionsdk.data.model.serializer

import com.petersamokhin.notionsdk.data.model.result.NotionResults
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.Serializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

@OptIn(ExperimentalSerializationApi::class)
@Serializer(forClass = NotionResults::class)
internal class NotionResultsTypedSerializer<T : Any>(
resultsItemSerializer: KSerializer<T>,
) : KSerializer<NotionResults<T>> {
private val resultsSerializer: KSerializer<List<T>> = ListSerializer(resultsItemSerializer)
private val nextCursorSerializer: KSerializer<String?> = String.serializer().nullable

override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ResultsResponseTypedSerializer") {
element("results", resultsSerializer.descriptor)
element("next_cursor", PrimitiveSerialDescriptor("next_cursor", PrimitiveKind.STRING))
element("has_more", PrimitiveSerialDescriptor("has_more", PrimitiveKind.BOOLEAN))
}

override fun deserialize(decoder: Decoder): NotionResults<T> {
val inp = decoder.beginStructure(descriptor)
var results: List<T>? = null
var nextCursor: String? = null
var hasMore: Boolean? = null
loop@ while (true) {
when (val i = inp.decodeElementIndex(descriptor)) {
CompositeDecoder.DECODE_DONE -> break@loop
0 -> results = inp.decodeSerializableElement(descriptor, i, resultsSerializer)
1 -> nextCursor = inp.decodeNullableSerializableElement(descriptor, i, nextCursorSerializer)
2 -> hasMore = inp.decodeBooleanElement(descriptor, i)
else -> throw SerializationException("Unknown index $i")
}
}
inp.endStructure(descriptor)
return NotionResults(
results = results ?: throw SerializationException("required field 'results' is null"),
nextCursor = nextCursor,
hasMore = hasMore ?: throw SerializationException("required field 'has_more' is null"),
)
}

override fun serialize(encoder: Encoder, value: NotionResults<T>) {
encoder.beginStructure(descriptor).apply {
encodeNullableSerializableElement(descriptor, 0, resultsSerializer, value.results)
encodeNullableSerializableElement(descriptor, 1, nextCursorSerializer, value.nextCursor)
encodeBooleanElement(descriptor, 2, value.hasMore)
endStructure(descriptor)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ internal class NotionMarkdownExporterImpl : NotionMarkdownExporter {
settings: NotionMarkdownExporter.Settings,
notion: Notion,
depthLevel: Int,
): String = exportRecursively(blocks, settings, notion, depthLevel, children = false)
): String = exportRecursively(blocks, settings, notion, depthLevel, depthLevel)

private suspend fun exportRecursively(
blocks: List<NotionBlock>,
settings: NotionMarkdownExporter.Settings,
notion: Notion,
depthLevel: Int,
children: Boolean,
initialDepth: Int,
currentDepth: Int,
): String {
var resultMarkdown = ""

Expand All @@ -68,19 +68,20 @@ internal class NotionMarkdownExporterImpl : NotionMarkdownExporter {
currentBlockMarkdown = "\n$currentBlockMarkdown"
}

val indentation = " " * (initialDepth - currentDepth)
lastBlockWasList = currentBlockIsList
resultMarkdown += currentBlockMarkdown?.let { md ->
(" " * depthLevel.coerceAtMost(1)).takeIf { children }.orEmpty() + md + '\n'
}.orEmpty()

if (block.hasChildren && depthLevel > 0) {
val blockChildren = notion.retrieveBlockChildren(block.id)

resultMarkdown += exportRecursively(blockChildren,
settings,
notion,
depthLevel - 1,
children = true) + '\n'
resultMarkdown += currentBlockMarkdown?.let { md -> indentation + md + '\n' }.orEmpty()

if (block.hasChildren && currentDepth > 0) {
val blockChildren = block.getAllChildren(notion)

resultMarkdown += exportRecursively(
blocks = blockChildren,
settings = settings,
notion = notion,
initialDepth = initialDepth,
currentDepth = currentDepth - 1
) + '\n'
}
}

Expand Down Expand Up @@ -225,6 +226,18 @@ internal class NotionMarkdownExporterImpl : NotionMarkdownExporter {
resultMarkdown
}
}

private suspend fun NotionBlock.getAllChildren(notion: Notion): List<NotionBlock> {
var lastResponse = notion.retrieveBlockChildren(id)
val result = lastResponse.results.toMutableList()

while (lastResponse.hasMore && lastResponse.nextCursor != null) {
lastResponse = notion.retrieveBlockChildren(id, startCursor = lastResponse.nextCursor)
result += lastResponse.results
}

return result
}
}

private operator fun String.times(count: Int): String =
Expand Down

0 comments on commit 1b01490

Please sign in to comment.