Skip to content
This repository has been archived by the owner on Nov 3, 2022. It is now read-only.

Commit

Permalink
Improve Readme and add simple JSON serialization, v1.0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
petersamokhin committed Nov 26, 2020
1 parent 1dabdf5 commit 68d76e4
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 81 deletions.
180 changes: 114 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,93 +17,141 @@ repositories {

```
dependencies {
implementation "com.github.petersamokhin:knotion-api:$kNotionApiVersion"
implementation "com.github.notionsdk:notion-sdk-kotlin:$latestVersion"
}
```

Latest version: https://github.com/petersamokhin/knotion-api/releases/latest
Latest version: https://github.com/notionsdk/notion-sdk-kotlin/releases

### Get authorization token
### Auth
#### If you have a password
Just use your email and password:

```kotlin
val notion = Notion.fromEmailAndPassword(
credentials = NotionCredentials(email = "test@test.com", password = "password"),
/* ... */
)
```

#### If you use a randomly generated passcode
- Open any [notion.so](https://notion.so) page in browser (e.g. in Google Chrome)
- Open debugging tools (you must be logged in)
- Open debugging tools (you must be logged in!)
- Obtain the `token_v2` cookie value of `https://www.notion.so/`

### Get page id
And then:
```kotlin
val notion = Notion.fromToken(
token = "your_token",
/* ... */
)
```

### Get page id
- Open any page
- Your link should look like `https://www.notion.so/<your_wokspace>/d822369169254cc4a1b2f5bbf3e8b87b`
- Last **path** part is page id (skip all query params), `d822369169254cc4a1b2f5bbf3e8b87b` for example above.

### Example: map collection to human-readable JSON

```kotlin
val token = "abcd..." // see above

val httpClient = HttpClient(CIO) {
// json feature is required to be installed to your client
install(JsonFeature) {
serializer = KotlinxSerializer(Json { ignoreUnknownKeys = true })
}
}

val notion = Notion(token, httpClient) // there is no client by default, you need to provide it

val pageId = "abcd...".dashifyId() // see above
val page = notion.loadPage(pageId)

val collectionId = page.recordMap.collectionsMap.keys.first()
val collectionViewId = page.recordMap.collectionViewsMap.keys.first()

val collectionResponse = notion.queryCollection(collectionId, collectionViewId)
val collection = collectionResponse.recordMap.collectionsMap[collectionId]

val title = collection?.title()
val description = collection?.description()

val jsonArray: JsonArray? = collectionResponse.mapCollectionToJsonArray()

// Then print mapped data:

println(title)
println(description)

println(jsonArray)
```
### Mapping

**Source collection:**

<img src="https://i.imgur.com/w24wLPW.png" data-canonical-src="https://i.imgur.com/w24wLPW.png" width="384" height="348" />

**Output:**

```
'Test table'
'Test description'
[{"value":"some_value_0","key":"some_key_0"},{"checked":true,"value":"another_value","key":"some_key_1"}]
```
<img src="https://i.imgur.com/I7n9Cx0.png" data-canonical-src="https://i.imgur.com/I7n9Cx0.png" width="367" height="328" />

So starting from here you can use the JSON to map your models.

### Mapping

You can also use the pre-defined mapping:
You can use the pre-defined mapping:

```kotlin
// ... see the previous example for the above steps
suspend fun main() {
// you need to provide your httpClient; JsonFeature must be installed!
val httpClient = HttpClient(CIO) {
Json { serializer = KotlinxSerializer(kotlinx.serialization.json.Json { ignoreUnknownKeys = true }) }
}
val notion = Notion.fromEmailAndPassword(
NotionCredentials("test@test.com", "password"),
httpClient // you need to provide your httpClient; JsonFeature must be installed!
)

// different instance for serialization
// because Notion's JSON is really weird for statically typed parsing
val json = Json {
ignoreUnknownKeys = true

// you need to use the discriminator if you want to serialize the table back to json
classDiscriminator = "object_type"
}
val table: NotionTable? = notion.getCollection(json, pageId = "73ef0bcb09424b00916e6e4f6759e310")

println(table)
// Output:
// NotionTable(
// title=Some test table,
// description=With test description,
// rows=[
// NotionRow(
// properties={
// checked=SingleValue(name=checked, type=Checkbox, value=NotionProperty(label=Yes, value=Checkbox(checked=true))),
// value=SingleValue(name=value, type=Text, value=NotionProperty(label=some_value_0, value=Text(text=some_value_0))),
// key=SingleValue(name=key, type=Title, value=NotionProperty(label=some_key_0, value=Title(text=some_key_0)))
// },
// metaInfo=MetaInfo(lastEditedBy=d6a033eb-82bb-49dd-975c-82ccb739f30e, lastEditedTime=1606352460000, createdBy=d6a033eb-82bb-49dd-975c-82ccb739f30e, createdTime=1606350145225)),
// NotionRow(
// properties={
// key=SingleValue(name=key, type=Title, value=NotionProperty(label=some_key_1, value=Title(text=some_key_1))),
// value=SingleValue(name=value, type=Text, value=NotionProperty(label=some_value_1, value=Text(text=some_value_1)))
// }
// metaInfo=MetaInfo(lastEditedBy=d6a033eb-82bb-49dd-975c-82ccb739f30e, lastEditedTime=1606350180000, createdBy=d6a033eb-82bb-49dd-975c-82ccb739f30e, createdTime=1606350145225))
// ],
// schema={
// key=Title(name=key, type=Title)
// checked=Checkbox(name=checked, type=Checkbox),
// value=Text(name=value, type=Text)
// }
// )

// now the json is much more readable!
println(json.encodeToString(table))

// works like a charm as well
println(json.decodeFromString<NotionTable>(json.encodeToString(table)))

// or even better:
val simpleJson: List<JsonElement>? = table.simpleJsonRows(json)

println(simpleJson)
// Output:
// [{"checked":true,"value":"some_value_0","key":"some_key_0"},{"value":"some_value_1","key":"some_key_1"}]

// map to your model:
val yourModels = simpleJson?.map { json.decodeFromJsonElement<SomeItem>(it) }

println(yourModels)
// Output:
// [SomeItem(key=some_key_0, value=some_value_0, checked=true), SomeItem(key=some_key_1, value=some_value_1, checked=false)]
}

val table: NotionTable<Map<String, NotionColumn<*>>>? = collectionResponse.mapTable()
@Serializable
data class SomeItem(
val key: String,
val value: String,
val checked: Boolean = false
)
```

// Table contains the (1) schema (map) of the collection:
println(table?.schema)
// {checked=Checkbox(name=checked, type=Checkbox), value=Text(name=value, type=Text), key=Title(name=key, type=Title)}
Supported types of columns:
- `Title`, `Text`, `Number`, `Checkbox`, `Select`, `MultiSelect`;
- `Person`, `Link`, `File`, `Email`, `PhoneNumber`
- `Date`

println(table)
// NotionTable(rows=[{value=NotionColumn(name=value, type=Text, value=Text(key=value, value=some_value_0)), key=NotionColumn(name=key, type=Title, value=Text(key=key, value=some_key_0))}, {checked=NotionColumn(name=checked, type=Checkbox, value=Bool(key=checked, value=true)), value=NotionColumn(name=value, type=Text, value=Text(key=value, value=another_value)), key=NotionColumn(name=key, type=Title, value=Text(key=key, value=some_key_1))}], schema={checked=Checkbox(name=checked, type=Checkbox), value=Text(name=value, type=Text), key=Title(name=key, type=Title)})
```
Unfortunately, Notion API is too dynamically typed to easy cover all the cases for the column types.
Also, some types, such as `Created by`, don't have the property value, but you can access it from the row meta info.
They added just because otherwise JSON deserialization will fail (they will appear only in row `schema`).

Unfortunately, Notion API is too dynamically typed to easy cover all the cases for the column types. Date, person,
email, URL etc. are not available in this library. Only simple text values, numbers, checkboxes, select/multiselect
values are supported.
**Note**:
- sometimes Notion can return almost empty response, so just make a request again;
- after several requests, auth endpoint will throw 429, limitations are unknown.

**Important note**: if you will have any other columns with these types, serializer will fail.
Actually the main purpose of the library is to retrieve the data from the Notion database (collection).
You can only authenticate and read, but not write or listen for the updates.
It uses the private APIs, reverse engineering helped here, so it may sometimes fail, or be even banned when Notion will release their paid APIs.
Unfortunately it is the only way to retrieve any info from Notion so far.
Use it at your own risk. For educational purposes only.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ repositories {
}

group = "com.petersamokhin.notionsdk"
version = "1.0.0"
version = "1.0.1"


dependencies {
Expand Down
26 changes: 26 additions & 0 deletions src/main/kotlin/com/petersamokhin/notionapi/Notion.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.petersamokhin.notionapi

import com.petersamokhin.notionapi.mapper.mapTable
import com.petersamokhin.notionapi.model.NotionCredentials
import com.petersamokhin.notionapi.model.NotionTable
import com.petersamokhin.notionapi.model.error.NotionAuthException
import com.petersamokhin.notionapi.model.request.LoadPageChunkRequestBody
import com.petersamokhin.notionapi.model.request.Loader
Expand All @@ -9,11 +11,21 @@ import com.petersamokhin.notionapi.model.response.NotionResponse
import com.petersamokhin.notionapi.request.LoadPageChunkRequest
import com.petersamokhin.notionapi.request.QueryNotionCollectionRequest
import com.petersamokhin.notionapi.request.base.NotionRequest
import com.petersamokhin.notionapi.utils.dashifyId
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.features.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.util.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement

class Notion internal constructor(token: String, private var httpClient: HttpClient) {
init {
Expand All @@ -24,6 +36,18 @@ class Notion internal constructor(token: String, private var httpClient: HttpCli
}
}

suspend fun getCollection(json: Json, pageId: String, sortColumns: Boolean = false): NotionTable? {
val normalPageId = pageId.dashifyId()
val page = loadPage(normalPageId)

val collectionId = page.recordMap.collectionsMap?.keys?.firstOrNull() ?: return null
val collectionViewId = page.recordMap.collectionViewsMap?.keys?.firstOrNull() ?: return null

val collectionResponse = queryCollection(collectionId, collectionViewId)

return collectionResponse.mapTable(json, sortColumns = sortColumns)
}

suspend fun loadPage(pageId: String, limit: Int = 50): NotionResponse {
return LoadPageChunkRequest(httpClient).execute(
LoadPageChunkRequestBody(pageId, limit, 0, false)
Expand Down Expand Up @@ -61,6 +85,8 @@ class Notion internal constructor(token: String, private var httpClient: HttpCli
body = credentials
}

println(response)

val token = response.headers.getAll(HttpHeaders.SetCookie)?.firstOrNull {
it.contains("$NOTION_TOKEN_COOKIE_KEY=", true)
}?.split("; ")?.firstOrNull {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,10 @@ fun NotionCollection.mapTable(

val schema = value.schema.mapKeys { it.value.name }

return NotionTable(rows, schema)
return NotionTable(
title(),
description(),
rows,
schema
)
}
Loading

0 comments on commit 68d76e4

Please sign in to comment.