Repository: seratch/notion-sdk-jvm (kotlin)

Issue:
New database creation API support
https://developers.notion.com/v1/changelog/create-new-databases-with-post-databases

Pull request description:
This pull request fixes #12

Patch:
diff --git a/core/src/main/kotlin/notion/api/v1/endpoint/DatabasesSupport.kt b/core/src/main/kotlin/notion/api/v1/endpoint/DatabasesSupport.kt
index c11a47f..f4eb328 100644
--- a/core/src/main/kotlin/notion/api/v1/endpoint/DatabasesSupport.kt
+++ b/core/src/main/kotlin/notion/api/v1/endpoint/DatabasesSupport.kt
@@ -4,11 +4,10 @@ import notion.api.v1.exception.NotionAPIError
 import notion.api.v1.http.NotionHttpClient
 import notion.api.v1.json.NotionJsonSerializer
 import notion.api.v1.logging.NotionLogger
-import notion.api.v1.model.databases.Database
-import notion.api.v1.model.databases.Databases
-import notion.api.v1.model.databases.QueryResults
+import notion.api.v1.model.databases.*
 import notion.api.v1.model.databases.query.filter.QueryTopLevelFilter
 import notion.api.v1.model.databases.query.sort.QuerySort
+import notion.api.v1.request.databases.CreateDatabaseRequest
 import notion.api.v1.request.databases.ListDatabasesRequest
 import notion.api.v1.request.databases.QueryDatabaseRequest
 import notion.api.v1.request.databases.RetrieveDatabaseRequest
@@ -19,6 +18,36 @@ interface DatabasesSupport : EndpointsSupport {
   val logger: NotionLogger
   val baseUrl: String
 
+  // -----------------------------------------------
+  // createDatabase
+  // -----------------------------------------------
+
+  fun createDatabase(
+      parent: DatabaseParent,
+      title: List<DatabaseProperty.RichText>,
+      properties: Map<String, DatabasePropertySchema>,
+  ): Database {
+    return createDatabase(
+        CreateDatabaseRequest(parent = parent, title = title, properties = properties))
+  }
+
+  fun createDatabase(database: CreateDatabaseRequest): Database {
+    val httpResponse =
+        httpClient.postTextBody(
+            logger = logger,
+            url = "$baseUrl/databases",
+            body = jsonSerializer.toJsonString(database),
+            headers = buildRequestHeaders(contentTypeJson()))
+    if (httpResponse.status == 200) {
+      return jsonSerializer.toDatabase(httpResponse.body)
+    } else {
+      throw NotionAPIError(
+          error = jsonSerializer.toError(httpResponse.body),
+          httpResponse = httpResponse,
+      )
+    }
+  }
+
   // -----------------------------------------------
   // listDatabases
   // -----------------------------------------------
diff --git a/core/src/main/kotlin/notion/api/v1/json/GsonSerializer.kt b/core/src/main/kotlin/notion/api/v1/json/GsonSerializer.kt
index 21bc74b..430c585 100644
--- a/core/src/main/kotlin/notion/api/v1/json/GsonSerializer.kt
+++ b/core/src/main/kotlin/notion/api/v1/json/GsonSerializer.kt
@@ -19,6 +19,7 @@ import notion.api.v1.model.search.SearchResults
 import notion.api.v1.model.users.User
 import notion.api.v1.model.users.Users
 import notion.api.v1.request.blocks.AppendBlockChildrenRequest
+import notion.api.v1.request.databases.CreateDatabaseRequest
 import notion.api.v1.request.databases.QueryDatabaseRequest
 import notion.api.v1.request.pages.CreatePageRequest
 import notion.api.v1.request.pages.UpdatePagePropertiesRequest
@@ -57,6 +58,7 @@ class GsonSerializer : NotionJsonSerializer {
   override fun toUser(body: String): User = gson.fromJson(body, User::class.java)
   override fun toUsers(body: String): Users = gson.fromJson(body, Users::class.java)
 
+  override fun toJsonString(request: CreateDatabaseRequest): String = gson.toJson(request)
   override fun toJsonString(request: AppendBlockChildrenRequest): String = gson.toJson(request)
   override fun toJsonString(request: CreatePageRequest): String = gson.toJson(request)
   override fun toJsonString(request: SearchRequest): String = gson.toJson(request)
diff --git a/core/src/main/kotlin/notion/api/v1/json/NotionJsonSerializer.kt b/core/src/main/kotlin/notion/api/v1/json/NotionJsonSerializer.kt
index ac7b93b..9e4f43e 100644
--- a/core/src/main/kotlin/notion/api/v1/json/NotionJsonSerializer.kt
+++ b/core/src/main/kotlin/notion/api/v1/json/NotionJsonSerializer.kt
@@ -11,6 +11,7 @@ import notion.api.v1.model.search.SearchResults
 import notion.api.v1.model.users.User
 import notion.api.v1.model.users.Users
 import notion.api.v1.request.blocks.AppendBlockChildrenRequest
+import notion.api.v1.request.databases.CreateDatabaseRequest
 import notion.api.v1.request.databases.QueryDatabaseRequest
 import notion.api.v1.request.pages.CreatePageRequest
 import notion.api.v1.request.pages.UpdatePagePropertiesRequest
@@ -29,6 +30,7 @@ interface NotionJsonSerializer {
   fun toUser(body: String): User
   fun toUsers(body: String): Users
 
+  fun toJsonString(request: CreateDatabaseRequest): String
   fun toJsonString(request: AppendBlockChildrenRequest): String
   fun toJsonString(request: CreatePageRequest): String
   fun toJsonString(request: SearchRequest): String
diff --git a/core/src/main/kotlin/notion/api/v1/model/databases/DatabaseProperty.kt b/core/src/main/kotlin/notion/api/v1/model/databases/DatabaseProperty.kt
index de4a2c0..8d10ebf 100644
--- a/core/src/main/kotlin/notion/api/v1/model/databases/DatabaseProperty.kt
+++ b/core/src/main/kotlin/notion/api/v1/model/databases/DatabaseProperty.kt
@@ -1,12 +1,13 @@
 package notion.api.v1.model.databases
 
+import java.util.*
 import notion.api.v1.model.common.*
 import notion.api.v1.model.pages.Page
 import notion.api.v1.model.users.User
 
 open class DatabaseProperty(
     val type: PropertyType,
-    val id: String,
+    val id: String = UUID.randomUUID().toString(),
     val name: String? = null,
     var title: RichText? = null,
     var richText: RichText? = null,
@@ -30,7 +31,7 @@ open class DatabaseProperty(
 ) {
   constructor(
       type: PropertyType,
-      id: String,
+      id: String = UUID.randomUUID().toString(),
   ) : this(
       type,
       id,
diff --git a/core/src/main/kotlin/notion/api/v1/model/databases/DatabasePropertySchema.kt b/core/src/main/kotlin/notion/api/v1/model/databases/DatabasePropertySchema.kt
new file mode 100644
index 0000000..943615e
--- /dev/null
+++ b/core/src/main/kotlin/notion/api/v1/model/databases/DatabasePropertySchema.kt
@@ -0,0 +1,50 @@
+package notion.api.v1.model.databases
+
+import notion.api.v1.model.common.OptionColor
+
+interface DatabasePropertySchema
+
+open class TitlePropertySchema(val title: Map<String, Any> = emptyMap()) : DatabasePropertySchema
+
+open class RichTextPropertySchema(val richText: Map<String, Any> = emptyMap()) :
+    DatabasePropertySchema
+
+open class NumberPropertySchema(val number: Number = Number()) : DatabasePropertySchema {
+  open class Number(val format: String? = null)
+}
+
+open class SelectOptionSchema(val name: String, val color: OptionColor? = null)
+
+open class SelectPropertySchema(val select: List<SelectOptionSchema>? = null) :
+    DatabasePropertySchema
+
+open class MultiSelectPropertySchema(val multiSelect: List<SelectOptionSchema>? = null) :
+    DatabasePropertySchema
+
+class DatePropertySchema(val date: Map<String, Any> = emptyMap()) : DatabasePropertySchema
+
+open class PeoplePropertySchema(val people: Map<String, Any> = emptyMap()) : DatabasePropertySchema
+
+open class FilePropertySchema(val files: Map<String, Any> = emptyMap()) : DatabasePropertySchema
+
+open class CheckboxPropertySchema(val checkbox: Map<String, Any> = emptyMap()) :
+    DatabasePropertySchema
+
+open class URLPropertySchema(val url: Map<String, Any> = emptyMap()) : DatabasePropertySchema
+
+open class EmailPropertySchema(val email: Map<String, Any> = emptyMap()) : DatabasePropertySchema
+
+open class PhoneNumberPropertySchema(val phoneNumber: Map<String, Any> = emptyMap()) :
+    DatabasePropertySchema
+
+open class CreatedTimePropertySchema(val createdTime: Map<String, Any> = emptyMap()) :
+    DatabasePropertySchema
+
+open class CreatedByPropertySchema(val createdBy: Map<String, Any> = emptyMap()) :
+    DatabasePropertySchema
+
+open class LastEditedTimePropertySchema(val lastEditedTime: Map<String, Any> = emptyMap()) :
+    DatabasePropertySchema
+
+open class LastEditedByPropertySchema(val lastEditedBy: Map<String, Any> = emptyMap()) :
+    DatabasePropertySchema
diff --git a/core/src/main/kotlin/notion/api/v1/request/databases/CreateDatabaseRequest.kt b/core/src/main/kotlin/notion/api/v1/request/databases/CreateDatabaseRequest.kt
new file mode 100644
index 0000000..05b2e80
--- /dev/null
+++ b/core/src/main/kotlin/notion/api/v1/request/databases/CreateDatabaseRequest.kt
@@ -0,0 +1,11 @@
+package notion.api.v1.request.databases
+
+import notion.api.v1.model.databases.DatabaseParent
+import notion.api.v1.model.databases.DatabaseProperty
+import notion.api.v1.model.databases.DatabasePropertySchema
+
+data class CreateDatabaseRequest(
+    val parent: DatabaseParent,
+    val title: List<DatabaseProperty.RichText>,
+    val properties: Map<String, DatabasePropertySchema>,
+)


Test patch:
diff --git a/core/src/test/kotlin/integration_tests/DatabasesTest.kt b/core/src/test/kotlin/integration_tests/DatabasesTest.kt
index c64cda6..2347687 100644
--- a/core/src/test/kotlin/integration_tests/DatabasesTest.kt
+++ b/core/src/test/kotlin/integration_tests/DatabasesTest.kt
@@ -6,16 +6,51 @@ import kotlin.test.assertTrue
 import notion.api.v1.NotionClient
 import notion.api.v1.model.common.ObjectType
 import notion.api.v1.model.common.PropertyType
+import notion.api.v1.model.databases.DatabaseParent
+import notion.api.v1.model.databases.DatabaseProperty as prop
+import notion.api.v1.model.databases.RichTextPropertySchema
+import notion.api.v1.model.databases.TitlePropertySchema
 import notion.api.v1.model.databases.query.filter.CompoundFilter
 import notion.api.v1.model.databases.query.filter.PropertyFilter
 import notion.api.v1.model.databases.query.filter.condition.TextFilter
 import notion.api.v1.model.databases.query.sort.QuerySort
 import notion.api.v1.model.databases.query.sort.QuerySortDirection
 import notion.api.v1.model.databases.query.sort.QuerySortTimestamp
+import notion.api.v1.model.pages.PageParent
+import notion.api.v1.model.pages.PageProperty
 import org.junit.Test
 
 class DatabasesTest {
 
+  @Test
+  fun creation() {
+    NotionClient(token = System.getenv("NOTION_TOKEN")).use { client ->
+      val databases = client.listDatabases()
+      assertTrue { databases.results.isNotEmpty() }
+
+      val testDatabase =
+          databases.results.find { it.title.any { t -> t.plainText.contains("Test Database") } }!!
+
+      val page =
+          client.createPage(
+              // Use the "Test Database" as this page's parent
+              parent = PageParent.database(databaseId = testDatabase.id),
+              properties = mapOf("Link" to PageProperty(url = "https://www.example.com")))
+
+      val title = "Test database ${System.currentTimeMillis()}"
+      val database =
+          client.createDatabase(
+              parent = DatabaseParent.page(page.id),
+              title =
+                  listOf(
+                      prop.RichText(text = prop.RichText.Text(content = title), plainText = title)),
+              properties =
+                  mapOf(
+                      "Title" to TitlePropertySchema(), "Description" to RichTextPropertySchema()))
+      assertNotNull(database)
+    }
+  }
+
   @Test
   fun list() {
     NotionClient(token = System.getenv("NOTION_TOKEN")).use { client ->