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

Issue:
Is it possible to create nested compound filter?
Hi! 

Is it possible to create nested compound filters for QueryDatabase?

Example:
```json
{
    "filter": {
        "or": [
            {
                "property": "Description",
                "rich_text": {
                    "contains": "fish"
                }
            },
            {
                "and": [
                    {
                        "property": "Food group",
                        "select": {
                            "equals": "🥦Vegetable"
                        }
                    },
                    {
                        "property": "Is protein rich?",
                        "checkbox": {
                            "equals": true
                        }
                    }
                ]
            }
        ]
    }
}
```
I checked this for CompoundFilter:
https://github.com/seratch/notion-sdk-jvm/blob/v0.2.0/core/src/test/kotlin/integration_tests/DatabasesTest.kt#L131-L155

I managed to use the Compound Filter but could not find a way to have the nested compound.

Thank you for your help!

Pull request:
This pull request resolves #38

Patch:
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 f6be5b6..d585dd2 100644
--- a/core/src/main/kotlin/notion/api/v1/json/GsonSerializer.kt
+++ b/core/src/main/kotlin/notion/api/v1/json/GsonSerializer.kt
@@ -14,6 +14,7 @@ import notion.api.v1.model.common.Icon
 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.query.filter.CompoundFilterElement
 import notion.api.v1.model.error.Error
 import notion.api.v1.model.error.OAuthError
 import notion.api.v1.model.oauth.OAuthTokenResult
@@ -44,6 +45,9 @@ class GsonSerializer : NotionJsonSerializer {
             .registerTypeAdapter(Block::class.java, BlockParser(unknownPropertyDetection))
             .registerTypeAdapter(Icon::class.java, IconParser(unknownPropertyDetection))
             .registerTypeAdapter(Cover::class.java, CoverParser(unknownPropertyDetection))
+            .registerTypeAdapter(
+                CompoundFilterElement::class.java,
+                CompoundFilterElementParser(unknownPropertyDetection))
 
     gson =
         if (unknownPropertyDetection) {
diff --git a/core/src/main/kotlin/notion/api/v1/json/gson/CompoundFilterElementParser.kt b/core/src/main/kotlin/notion/api/v1/json/gson/CompoundFilterElementParser.kt
new file mode 100644
index 0000000..4b9ff03
--- /dev/null
+++ b/core/src/main/kotlin/notion/api/v1/json/gson/CompoundFilterElementParser.kt
@@ -0,0 +1,46 @@
+package notion.api.v1.json.gson
+
+import com.google.gson.*
+import java.lang.reflect.Type
+import notion.api.v1.model.databases.query.filter.CompoundFilter
+import notion.api.v1.model.databases.query.filter.CompoundFilterElement
+import notion.api.v1.model.databases.query.filter.PropertyFilter
+
+class CompoundFilterElementParser(private val unknownPropertyDetection: Boolean = false) :
+    JsonDeserializer<CompoundFilterElement>, JsonSerializer<CompoundFilterElement> {
+
+  override fun deserialize(
+      json: JsonElement,
+      typeOfT: Type,
+      context: JsonDeserializationContext
+  ): CompoundFilterElement? {
+    if (json == null) {
+      return null
+    }
+    if (json.asJsonObject.get("or") != null || json.asJsonObject.get("and") != null) {
+      return context.deserialize(json, CompoundFilter::class.java)
+    }
+    if (json.asJsonObject.get("property") != null) {
+      return context.deserialize(json, PropertyFilter::class.java)
+    }
+    if (unknownPropertyDetection) {
+      throw IllegalArgumentException("Unsupported compound filter item detected: $json")
+    } else {
+      return null
+    }
+  }
+
+  override fun serialize(
+      src: CompoundFilterElement,
+      typeOfSrc: Type,
+      context: JsonSerializationContext
+  ): JsonElement? {
+    if (src is CompoundFilter) {
+      return context.serialize(src, CompoundFilter::class.java)
+    }
+    if (src is PropertyFilter) {
+      return context.serialize(src, PropertyFilter::class.java)
+    }
+    return null
+  }
+}
diff --git a/core/src/main/kotlin/notion/api/v1/model/databases/query/filter/CompoundFilter.kt b/core/src/main/kotlin/notion/api/v1/model/databases/query/filter/CompoundFilter.kt
index 875c6ce..fe47230 100644
--- a/core/src/main/kotlin/notion/api/v1/model/databases/query/filter/CompoundFilter.kt
+++ b/core/src/main/kotlin/notion/api/v1/model/databases/query/filter/CompoundFilter.kt
@@ -3,6 +3,6 @@ package notion.api.v1.model.databases.query.filter
 open class CompoundFilter
 @JvmOverloads
 constructor(
-    var or: List<PropertyFilter>? = null,
-    var and: List<PropertyFilter>? = null,
-) : QueryTopLevelFilter
+    var or: List<CompoundFilterElement>? = null,
+    var and: List<CompoundFilterElement>? = null,
+) : QueryTopLevelFilter, CompoundFilterElement
diff --git a/core/src/main/kotlin/notion/api/v1/model/databases/query/filter/CompoundFilterElement.kt b/core/src/main/kotlin/notion/api/v1/model/databases/query/filter/CompoundFilterElement.kt
new file mode 100644
index 0000000..86b44ce
--- /dev/null
+++ b/core/src/main/kotlin/notion/api/v1/model/databases/query/filter/CompoundFilterElement.kt
@@ -0,0 +1,3 @@
+package notion.api.v1.model.databases.query.filter
+
+interface CompoundFilterElement
diff --git a/core/src/main/kotlin/notion/api/v1/model/databases/query/filter/PropertyFilter.kt b/core/src/main/kotlin/notion/api/v1/model/databases/query/filter/PropertyFilter.kt
index 076a431..66fa807 100644
--- a/core/src/main/kotlin/notion/api/v1/model/databases/query/filter/PropertyFilter.kt
+++ b/core/src/main/kotlin/notion/api/v1/model/databases/query/filter/PropertyFilter.kt
@@ -23,4 +23,4 @@ constructor(
     var file: FilesFilter? = null,
     var relation: RelationFilter? = null,
     var formula: FormulaFilter? = null,
-) : QueryTopLevelFilter
+) : QueryTopLevelFilter, CompoundFilterElement


Test patch:
diff --git a/core/src/test/kotlin/integration_tests/DatabasesTest.kt b/core/src/test/kotlin/integration_tests/DatabasesTest.kt
index 686098b..ddafaad 100644
--- a/core/src/test/kotlin/integration_tests/DatabasesTest.kt
+++ b/core/src/test/kotlin/integration_tests/DatabasesTest.kt
@@ -165,6 +165,58 @@ class DatabasesTest {
       assertNotNull(queryResult)
       assertTrue { queryResult.results.isNotEmpty() }
       assertEquals(ObjectType.Page, queryResult.results[0].objectType)
+
+      val nestedCompoundQuery1 =
+          CompoundFilter(
+              and =
+                  listOf(PropertyFilter(property = "title", title = TextFilter(contains = "bug"))))
+      val nestedCompoundQuery2 =
+          CompoundFilter(
+              and =
+                  listOf(PropertyFilter(property = "title", title = TextFilter(contains = "test"))))
+
+      val nestedQueryResult =
+          client.queryDatabase(
+              databaseId = database.id,
+              filter = CompoundFilter(or = listOf(nestedCompoundQuery1, nestedCompoundQuery2)),
+              pageSize = 1,
+          )
+      assertNotNull(nestedQueryResult)
+      assertTrue { nestedQueryResult.results.isNotEmpty() }
+      assertEquals(ObjectType.Page, nestedQueryResult.results[0].objectType)
+    }
+  }
+
+  @Test
+  fun query_nested_compound() {
+    NotionClient(token = System.getenv("NOTION_TOKEN")).use { client ->
+      val database =
+          client
+              .search(
+                  query = "Test Database",
+                  filter = SearchRequest.SearchFilter("database", property = "object"))
+              .results
+              .find { it.asDatabase().properties.containsKey("Severity") }
+              ?.asDatabase()!!
+
+      val nestedCompoundQuery1 =
+          CompoundFilter(
+              and =
+                  listOf(PropertyFilter(property = "title", title = TextFilter(contains = "bug"))))
+      val nestedCompoundQuery2 =
+          CompoundFilter(
+              and =
+                  listOf(PropertyFilter(property = "title", title = TextFilter(contains = "test"))))
+
+      val nestedQueryResult =
+          client.queryDatabase(
+              databaseId = database.id,
+              filter = CompoundFilter(or = listOf(nestedCompoundQuery1, nestedCompoundQuery2)),
+              pageSize = 1,
+          )
+      assertNotNull(nestedQueryResult)
+      assertTrue { nestedQueryResult.results.isNotEmpty() }
+      assertEquals(ObjectType.Page, nestedQueryResult.results[0].objectType)
     }
   }
 }