Repository: jomei/notionapi
Pull request #86 (2022-05-30 22:46:51)

Supported multidimensional compound and timestamp filters
Added database query improvements:
* timestamp filter object https://developers.notion.com/reference/post-database-query-filter#timestamp-filter-object
* nested levels for compound filter object https://developers.notion.com/reference/post-database-query-filter#compound-filter-object

Thanks for your great library!

I'd like to propose the new different implementation of database query filter that uses sum types for different filter types. 
You can find the examples of usage in the unit tests.


Patch:
diff --git a/database.go b/database.go
index 117f336..8c2da7e 100644
--- a/database.go
+++ b/database.go
@@ -163,19 +163,26 @@ type DatabaseListResponse struct {
 }
 
 type DatabaseQueryRequest struct {
+	// Deprecated: use Filter instead
 	PropertyFilter *PropertyFilter
+	// Deprecated: use Filter instead
 	CompoundFilter *CompoundFilter
-	Sorts          []SortObject `json:"sorts,omitempty"`
-	StartCursor    Cursor       `json:"start_cursor,omitempty"`
-	PageSize       int          `json:"page_size,omitempty"`
+
+	Filter      Filter
+	Sorts       []SortObject `json:"sorts,omitempty"`
+	StartCursor Cursor       `json:"start_cursor,omitempty"`
+	PageSize    int          `json:"page_size,omitempty"`
 }
 
 func (qr *DatabaseQueryRequest) MarshalJSON() ([]byte, error) {
 	var filter interface{}
-	if qr.PropertyFilter != nil {
+	switch {
+	case qr.PropertyFilter != nil:
 		filter = qr.PropertyFilter
-	} else if qr.CompoundFilter != nil {
+	case qr.CompoundFilter != nil:
 		filter = qr.CompoundFilter
+	case qr.Filter != nil:
+		filter = qr.Filter
 	}
 	return json.Marshal(struct {
 		Sorts       []SortObject `json:"sorts,omitempty"`
diff --git a/filter.go b/filter.go
index 04381b0..b3ef8b6 100644
--- a/filter.go
+++ b/filter.go
@@ -1,11 +1,49 @@
 package notionapi
 
+import (
+	"encoding/json"
+)
+
 type FilterOperator string
 
+type Filter interface {
+	filter()
+}
+
 type CompoundFilter map[FilterOperator][]PropertyFilter
 
+type AndCompoundFilter []Filter
+type OrCompoundFilter []Filter
+
+func (f AndCompoundFilter) filter() {}
+func (f OrCompoundFilter) filter()  {}
+
+func (f AndCompoundFilter) MarshalJSON() ([]byte, error) {
+	return json.Marshal(struct {
+		And []Filter `json:"and"`
+	}{
+		And: f,
+	})
+}
+
+func (f OrCompoundFilter) MarshalJSON() ([]byte, error) {
+	return json.Marshal(struct {
+		Or []Filter `json:"or"`
+	}{
+		Or: f,
+	})
+}
+
 type Condition string
 
+type TimestampFilter struct {
+	Timestamp      TimestampType        `json:"timestamp"`
+	CreatedTime    *DateFilterCondition `json:"created_time,omitempty"`
+	LastEditedTime *DateFilterCondition `json:"last_edited_time,omitempty"`
+}
+
+func (f TimestampFilter) filter() {}
+
 type PropertyFilter struct {
 	Property string `json:"property"`
 	// DEPRECATED: doesn't for ver. 2022-02-22
@@ -22,6 +60,8 @@ type PropertyFilter struct {
 	Formula     *FormulaFilterCondition     `json:"formula,omitempty"`
 }
 
+func (f PropertyFilter) filter() {}
+
 type TextFilterCondition struct {
 	Equals         string `json:"equals,omitempty"`
 	DoesNotEqual   string `json:"does_not_equal,omitempty"`
diff --git a/object.go b/object.go
index b7a153b..325745e 100644
--- a/object.go
+++ b/object.go
@@ -111,7 +111,10 @@ func (d *Date) UnmarshalText(data []byte) error {
 type FileType string
 
 type File struct {
-	Name string `json:"name"`
+	Name     string      `json:"name"`
+	Type     FileType    `json:"type"`
+	File     *FileObject `json:"file,omitempty"`
+	External *FileObject `json:"external,omitempty"`
 }
 
 type FileObject struct {


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index f8da909..241b29d 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -23,3 +23,6 @@ jobs:
 
     - name: Test
       run: go test -v ./...
+
+    - name: golangci-lint
+      uses: golangci/golangci-lint-action@v3.2.0
diff --git a/database_test.go b/database_test.go
index 17daa4f..baf317e 100644
--- a/database_test.go
+++ b/database_test.go
@@ -440,6 +440,98 @@ func TestDatabaseQueryRequest_MarshalJSON(t *testing.T) {
 			},
 			want: []byte(`{"filter":{"property":"created_at","date":{"equals":"2021-05-10T02:43:42Z","past_week":{}}}}`),
 		},
+		{
+			name: "timestamp created",
+			req: &notionapi.DatabaseQueryRequest{
+				Filter: &notionapi.TimestampFilter{
+					Timestamp: notionapi.TimestampCreated,
+					CreatedTime: &notionapi.DateFilterCondition{
+						NextWeek: &struct{}{},
+					},
+				},
+			},
+			want: []byte(`{"filter":{"timestamp":"created_time","created_time":{"next_week":{}}}}`),
+		},
+		{
+			name: "timestamp last edited",
+			req: &notionapi.DatabaseQueryRequest{
+				Filter: &notionapi.TimestampFilter{
+					Timestamp: notionapi.TimestampLastEdited,
+					LastEditedTime: &notionapi.DateFilterCondition{
+						Before: &dateObj,
+					},
+				},
+			},
+			want: []byte(`{"filter":{"timestamp":"last_edited_time","last_edited_time":{"before":"2021-05-10T02:43:42Z"}}}`),
+		},
+		{
+			name: "or compound filter one level",
+			req: &notionapi.DatabaseQueryRequest{
+				Filter: notionapi.OrCompoundFilter{
+					notionapi.PropertyFilter{
+						Property: "Status",
+						Select: &notionapi.SelectFilterCondition{
+							Equals: "Reading",
+						},
+					},
+					notionapi.PropertyFilter{
+						Property: "Publisher",
+						Select: &notionapi.SelectFilterCondition{
+							Equals: "NYT",
+						},
+					},
+				},
+			},
+			want: []byte(`{"filter":{"or":[{"property":"Status","select":{"equals":"Reading"}},{"property":"Publisher","select":{"equals":"NYT"}}]}}`),
+		},
+		{
+			name: "and compound filter one level",
+			req: &notionapi.DatabaseQueryRequest{
+				Filter: notionapi.AndCompoundFilter{
+					notionapi.PropertyFilter{
+						Property: "Status",
+						Select: &notionapi.SelectFilterCondition{
+							Equals: "Reading",
+						},
+					},
+					notionapi.PropertyFilter{
+						Property: "Publisher",
+						Select: &notionapi.SelectFilterCondition{
+							Equals: "NYT",
+						},
+					},
+				},
+			},
+			want: []byte(`{"filter":{"and":[{"property":"Status","select":{"equals":"Reading"}},{"property":"Publisher","select":{"equals":"NYT"}}]}}`),
+		},
+		{
+			name: "compound filter two levels",
+			req: &notionapi.DatabaseQueryRequest{
+				Filter: notionapi.OrCompoundFilter{
+					notionapi.PropertyFilter{
+						Property: "Description",
+						RichText: &notionapi.TextFilterCondition{
+							Contains: "fish",
+						},
+					},
+					notionapi.AndCompoundFilter{
+						notionapi.PropertyFilter{
+							Property: "Food group",
+							Select: &notionapi.SelectFilterCondition{
+								Equals: "🥦Vegetable",
+							},
+						},
+						notionapi.PropertyFilter{
+							Property: "Is protein rich?",
+							Checkbox: &notionapi.CheckboxFilterCondition{
+								Equals: true,
+							},
+						},
+					},
+				},
+			},
+			want: []byte(`{"filter":{"or":[{"property":"Description","rich_text":{"contains":"fish"}},{"and":[{"property":"Food group","select":{"equals":"🥦Vegetable"}},{"property":"Is protein rich?","checkbox":{"equals":true}}]}]}}`),
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/page_test.go b/page_test.go
index 1ef5f72..d4bd23f 100644
--- a/page_test.go
+++ b/page_test.go
@@ -95,6 +95,19 @@ func TestPageClient(t *testing.T) {
 								},
 							},
 						},
+						"Some Files": &notionapi.FilesProperty{
+							ID:   "files",
+							Type: "files",
+							Files: []notionapi.File{
+								{
+									Name: "https://google.com",
+									Type: "external",
+									External: &notionapi.FileObject{
+										URL: "https://google.com",
+									},
+								},
+							},
+						},
 						"Name": &notionapi.TitleProperty{
 							ID:   "title",
 							Type: "title",
@@ -267,6 +280,25 @@ func TestPageClient(t *testing.T) {
 								},
 							},
 						},
+						"Important Files": notionapi.FilesProperty{
+							Type: "files",
+							Files: []notionapi.File{
+								{
+									Type: "external",
+									Name: "https://google.com",
+									External: &notionapi.FileObject{
+										URL: "https://google.com",
+									},
+								},
+								{
+									Type: "external",
+									Name: "https://123.com",
+									External: &notionapi.FileObject{
+										URL: "https://123.com",
+									},
+								},
+							},
+						},
 					},
 				},
 				want: &notionapi.Page{
diff --git a/testdata/page_get.json b/testdata/page_get.json
index beb4760..4c54c29 100644
--- a/testdata/page_get.json
+++ b/testdata/page_get.json
@@ -83,6 +83,19 @@
         "href": null
       }]
     },
+    "Some Files": {
+      "id": "files",
+      "type": "files",
+      "files": [
+        {
+          "name": "https://google.com",
+          "type": "external",
+          "external": {
+            "url": "https://google.com"
+          }
+        }
+      ]
+    },
     "RollupArray": {
       "id": "abcd",
       "type": "rollup",
diff --git a/testdata/page_update.json b/testdata/page_update.json
index 2380403..7e4b0b6 100644
--- a/testdata/page_update.json
+++ b/testdata/page_update.json
@@ -68,6 +68,19 @@
         }
       ]
     },
+    "Important Files": {
+      "id": "importantfiles!",
+      "type": "files",
+      "files": [
+        {
+          "name": "https://google.com",
+          "type": "external",
+          "external": {
+            "url": "https://google.com"
+          }
+        }
+      ]
+    },
     "Name": {
       "id": "title",
       "type": "title",