From 1b01490e0f03ee7e110251f23e7915c29526421a Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 17 Nov 2021 01:34:27 +0000 Subject: [PATCH] Prepare for release 0.0.4. --- CHANGELOG.md | 6 ++ README.md | 6 +- gradle.properties | 2 +- .../com/petersamokhin/notionsdk/Notion.kt | 11 ++-- .../com/petersamokhin/notionsdk/NotionImpl.kt | 15 +++-- .../data/mapper/DatabaseResultMapper.kt | 24 +++++-- .../{NotionDatabase.kt => NotionResults.kt} | 24 +++---- .../NotionResultsTypedSerializer.kt | 63 +++++++++++++++++++ .../markdown/NotionMarkdownExporterImpl.kt | 43 ++++++++----- 9 files changed, 149 insertions(+), 45 deletions(-) rename src/commonMain/kotlin/com/petersamokhin/notionsdk/data/model/result/{NotionDatabase.kt => NotionResults.kt} (91%) create mode 100644 src/commonMain/kotlin/com/petersamokhin/notionsdk/data/model/serializer/NotionResultsTypedSerializer.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 57c746b..39a0b9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 6c1c1ed..185ad8f 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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") @@ -99,7 +99,7 @@ val notion = Notion.fromToken( token = "token", httpClient = HttpClient(CIO) ) -val blocks: List = notion.retrieveBlockChildren("page-id") +val blocks: List = notion.retrieveBlockChildren("page-id").results val exporter = NotionMarkdownExporter.create() diff --git a/gradle.properties b/gradle.properties index 1f7aab2..05c20b1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/src/commonMain/kotlin/com/petersamokhin/notionsdk/Notion.kt b/src/commonMain/kotlin/com/petersamokhin/notionsdk/Notion.kt index 7d2321c..bce4fdd 100644 --- a/src/commonMain/kotlin/com/petersamokhin/notionsdk/Notion.kt +++ b/src/commonMain/kotlin/com/petersamokhin/notionsdk/Notion.kt @@ -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 @@ -20,7 +21,7 @@ public interface Notion : Closeable { databaseId: String, startCursor: String? = null, pageSize: Int? = null, - ): NotionDatabase + ): NotionResults /** * Notion API filter & sort params are too complicated to cover all the cases via strictly-typed models. @@ -31,7 +32,7 @@ public interface Notion : Closeable { public suspend fun queryDatabase( databaseId: String, jsonRequestBody: String, - ): NotionDatabase + ): NotionResults /** * @see Notion documentation @@ -54,7 +55,7 @@ public interface Notion : Closeable { blockId: String, startCursor: String? = null, pageSize: Int? = null, - ): List + ): NotionResults public companion object { public const val HEADER_VERSION: String = "Notion-Version" @@ -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) } diff --git a/src/commonMain/kotlin/com/petersamokhin/notionsdk/NotionImpl.kt b/src/commonMain/kotlin/com/petersamokhin/notionsdk/NotionImpl.kt index 7376f4e..ae781f7 100644 --- a/src/commonMain/kotlin/com/petersamokhin/notionsdk/NotionImpl.kt +++ b/src/commonMain/kotlin/com/petersamokhin/notionsdk/NotionImpl.kt @@ -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.* @@ -60,7 +61,7 @@ internal class NotionImpl( databaseId: String, startCursor: String?, pageSize: Int?, - ): NotionDatabase = + ): NotionResults = httpClient.post>("${Notion.API_BASE_URL}/${ENDPOINT_DATABASES}/$databaseId/$PATH_QUERY") { contentType(ContentType.Application.Json) body = QueryDatabaseRequest( @@ -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 = httpClient.post>("${Notion.API_BASE_URL}/${ENDPOINT_DATABASES}/$databaseId/$PATH_QUERY") { body = TextContent(jsonRequestBody, ContentType.Application.Json) }.toDomain() @@ -88,12 +92,11 @@ internal class NotionImpl( blockId: String, startCursor: String?, pageSize: Int?, - ): List = + ): NotionResults = httpClient.get>("${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" diff --git a/src/commonMain/kotlin/com/petersamokhin/notionsdk/data/mapper/DatabaseResultMapper.kt b/src/commonMain/kotlin/com/petersamokhin/notionsdk/data/mapper/DatabaseResultMapper.kt index 56b257e..fa265c4 100644 --- a/src/commonMain/kotlin/com/petersamokhin/notionsdk/data/mapper/DatabaseResultMapper.kt +++ b/src/commonMain/kotlin/com/petersamokhin/notionsdk/data/mapper/DatabaseResultMapper.kt @@ -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.toDomain(): NotionDatabase = - NotionDatabase( - rows = results.map(PageObject::toDomain), +@Suppress("UNCHECKED_CAST") +internal inline fun ResultsResponse.toDomain(): NotionResults = + NotionResults( + results = ( + when (T::class) { + PageObject::class -> when (R::class) { + NotionDatabaseRow::class -> results.filterIsInstance().map(PageObject::toDomain) + else -> null + } + Block::class -> when (R::class) { + NotionBlock::class -> results.filterIsInstance().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, nextCursor = nextCursor, hasMore = hasMore, ) \ No newline at end of file diff --git a/src/commonMain/kotlin/com/petersamokhin/notionsdk/data/model/result/NotionDatabase.kt b/src/commonMain/kotlin/com/petersamokhin/notionsdk/data/model/result/NotionResults.kt similarity index 91% rename from src/commonMain/kotlin/com/petersamokhin/notionsdk/data/model/result/NotionDatabase.kt rename to src/commonMain/kotlin/com/petersamokhin/notionsdk/data/model/result/NotionResults.kt index e094f91..a4888c9 100644 --- a/src/commonMain/kotlin/com/petersamokhin/notionsdk/data/model/result/NotionDatabase.kt +++ b/src/commonMain/kotlin/com/petersamokhin/notionsdk/data/model/result/NotionResults.kt @@ -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, +@Serializable(with = NotionResultsTypedSerializer::class) +public data class NotionResults( + val results: List, @SerialName("next_cursor") val nextCursor: String? = null, @@ -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) : NotionDatabaseProperty() { + public data class Text(override val id: String, val text: String, val parts: List) : + NotionDatabaseProperty() { @Serializable public data class Part(val text: String, val url: String?) } @@ -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( @@ -61,7 +63,7 @@ public sealed class NotionDatabaseProperty { @SerialName("multi_select") public data class MultiSelect( override val id: String, - val selected: List + val selected: List, ) : NotionDatabaseProperty() @Serializable @@ -81,7 +83,7 @@ public sealed class NotionDatabaseProperty { @SerialName("avatar_url") val avatarUrl: String?, val email: String, - ): Person() + ) : Person() @Serializable @SerialName("user") @@ -90,7 +92,7 @@ public sealed class NotionDatabaseProperty { val name: String, @SerialName("avatar_url") val avatarUrl: String? = null, - ): Person() + ) : Person() } } @@ -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 { @@ -195,6 +197,6 @@ public sealed class NotionDatabaseProperty { @Serializable @SerialName("rollup") public data class Rollup( - override val id: String + override val id: String, ) : NotionDatabaseProperty() } \ No newline at end of file diff --git a/src/commonMain/kotlin/com/petersamokhin/notionsdk/data/model/serializer/NotionResultsTypedSerializer.kt b/src/commonMain/kotlin/com/petersamokhin/notionsdk/data/model/serializer/NotionResultsTypedSerializer.kt new file mode 100644 index 0000000..cf8ddfd --- /dev/null +++ b/src/commonMain/kotlin/com/petersamokhin/notionsdk/data/model/serializer/NotionResultsTypedSerializer.kt @@ -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( + resultsItemSerializer: KSerializer, +) : KSerializer> { + private val resultsSerializer: KSerializer> = ListSerializer(resultsItemSerializer) + private val nextCursorSerializer: KSerializer = 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 { + val inp = decoder.beginStructure(descriptor) + var results: List? = 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) { + encoder.beginStructure(descriptor).apply { + encodeNullableSerializableElement(descriptor, 0, resultsSerializer, value.results) + encodeNullableSerializableElement(descriptor, 1, nextCursorSerializer, value.nextCursor) + encodeBooleanElement(descriptor, 2, value.hasMore) + endStructure(descriptor) + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/com/petersamokhin/notionsdk/markdown/NotionMarkdownExporterImpl.kt b/src/commonMain/kotlin/com/petersamokhin/notionsdk/markdown/NotionMarkdownExporterImpl.kt index 6589c12..d1168a3 100644 --- a/src/commonMain/kotlin/com/petersamokhin/notionsdk/markdown/NotionMarkdownExporterImpl.kt +++ b/src/commonMain/kotlin/com/petersamokhin/notionsdk/markdown/NotionMarkdownExporterImpl.kt @@ -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, settings: NotionMarkdownExporter.Settings, notion: Notion, - depthLevel: Int, - children: Boolean, + initialDepth: Int, + currentDepth: Int, ): String { var resultMarkdown = "" @@ -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' } } @@ -225,6 +226,18 @@ internal class NotionMarkdownExporterImpl : NotionMarkdownExporter { resultMarkdown } } + + private suspend fun NotionBlock.getAllChildren(notion: Notion): List { + 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 =