notion-client-0.7.0.0: src/Notion/V1/Filter.hs
-- | Typed query filters and sorts for Notion database and data source queries.
--
-- This module provides a type-safe DSL for constructing filter and sort
-- conditions, replacing raw @Value@ construction.
--
-- Example usage:
--
-- @
-- let myFilter = And
-- [ PropertyFilter "Status" (SelectCondition (SelectEquals "Done"))
-- , PropertyFilter "Priority" (SelectCondition (SelectEquals "High"))
-- ]
-- mySorts = [PropertySort "Name" Ascending]
-- @
module Notion.V1.Filter
( -- * Filters
Filter (..),
PropertyCondition (..),
TimestampType (..),
-- * Filter conditions
TextCondition (..),
NumberCondition (..),
CheckboxCondition (..),
SelectCondition (..),
MultiSelectCondition (..),
DateCondition (..),
PeopleCondition (..),
FilesCondition (..),
RelationCondition (..),
StatusCondition (..),
UniqueIdCondition (..),
VerificationCondition (..),
FormulaCondition (..),
RollupCondition (..),
-- * Sorts
Sort (..),
SortDirection (..),
)
where
import Data.Aeson ((.=))
import Data.Aeson qualified as Aeson
import Data.Aeson.Key qualified as Key
import Data.Scientific (Scientific)
import Notion.Prelude
-- | Timestamp type for timestamp filters and sorts.
data TimestampType
= FilterCreatedTime
| FilterLastEditedTime
deriving stock (Eq, Show, Generic)
timestampTypeToText :: TimestampType -> Text
timestampTypeToText FilterCreatedTime = "created_time"
timestampTypeToText FilterLastEditedTime = "last_edited_time"
-- | Top-level filter type for querying databases and data sources.
--
-- Filters can be compound (@And@ / @Or@, nesting up to 2 levels per Notion API),
-- property filters (targeting a named property), or timestamp filters.
data Filter
= And [Filter]
| Or [Filter]
| PropertyFilter Text PropertyCondition
| TimestampFilter TimestampType DateCondition
deriving stock (Eq, Show, Generic)
instance ToJSON Filter where
toJSON (And filters) = Aeson.object ["and" .= filters]
toJSON (Or filters) = Aeson.object ["or" .= filters]
toJSON (PropertyFilter propName condition) =
let condObj = propertyConditionToObject condition
in Aeson.object $ ["property" .= propName] <> condObj
toJSON (TimestampFilter tsType condition) =
let tsKey = timestampTypeToText tsType
in Aeson.object
[ "timestamp" .= tsKey,
Key.fromText tsKey .= dateConditionToValue condition
]
-- | Property-type-specific filter condition.
--
-- Each constructor maps to the JSON key the Notion API expects
-- (e.g., 'TitleCondition' serializes under @\"title\"@).
data PropertyCondition
= TitleCondition TextCondition
| RichTextCondition TextCondition
| NumberCondition NumberCondition
| CheckboxCondition CheckboxCondition
| SelectCondition SelectCondition
| MultiSelectCondition MultiSelectCondition
| DateCondition DateCondition
| PeopleCondition PeopleCondition
| FilesCondition FilesCondition
| RelationCondition RelationCondition
| StatusCondition StatusCondition
| UniqueIdCondition UniqueIdCondition
| VerificationCondition VerificationCondition
| FormulaCondition FormulaCondition
| RollupCondition RollupCondition
| CreatedTimeCondition DateCondition
| CreatedByCondition PeopleCondition
| LastEditedTimeCondition DateCondition
| LastEditedByCondition PeopleCondition
| PhoneNumberCondition TextCondition
| UrlCondition TextCondition
| EmailCondition TextCondition
deriving stock (Eq, Show, Generic)
-- | Convert a PropertyCondition to key-value pairs for inclusion in a JSON object.
propertyConditionToObject :: PropertyCondition -> [(Aeson.Key, Aeson.Value)]
propertyConditionToObject = \case
TitleCondition c -> [("title", textConditionToValue c)]
RichTextCondition c -> [("rich_text", textConditionToValue c)]
NumberCondition c -> [("number", numberConditionToValue c)]
CheckboxCondition c -> [("checkbox", checkboxConditionToValue c)]
SelectCondition c -> [("select", selectConditionToValue c)]
MultiSelectCondition c -> [("multi_select", multiSelectConditionToValue c)]
DateCondition c -> [("date", dateConditionToValue c)]
PeopleCondition c -> [("people", peopleConditionToValue c)]
FilesCondition c -> [("files", filesConditionToValue c)]
RelationCondition c -> [("relation", relationConditionToValue c)]
StatusCondition c -> [("status", statusConditionToValue c)]
UniqueIdCondition c -> [("unique_id", uniqueIdConditionToValue c)]
VerificationCondition c -> [("verification", verificationConditionToValue c)]
FormulaCondition c -> [("formula", formulaConditionToValue c)]
RollupCondition c -> [("rollup", rollupConditionToValue c)]
CreatedTimeCondition c -> [("created_time", dateConditionToValue c)]
CreatedByCondition c -> [("created_by", peopleConditionToValue c)]
LastEditedTimeCondition c -> [("last_edited_time", dateConditionToValue c)]
LastEditedByCondition c -> [("last_edited_by", peopleConditionToValue c)]
PhoneNumberCondition c -> [("phone_number", textConditionToValue c)]
UrlCondition c -> [("url", textConditionToValue c)]
EmailCondition c -> [("email", textConditionToValue c)]
-- | Text filter conditions for title, rich_text, phone_number, url, and email properties.
data TextCondition
= TextEquals Text
| TextDoesNotEqual Text
| TextContains Text
| TextDoesNotContain Text
| TextStartsWith Text
| TextEndsWith Text
| TextIsEmpty
| TextIsNotEmpty
deriving stock (Eq, Show, Generic)
textConditionToValue :: TextCondition -> Aeson.Value
textConditionToValue = \case
TextEquals v -> Aeson.object ["equals" .= v]
TextDoesNotEqual v -> Aeson.object ["does_not_equal" .= v]
TextContains v -> Aeson.object ["contains" .= v]
TextDoesNotContain v -> Aeson.object ["does_not_contain" .= v]
TextStartsWith v -> Aeson.object ["starts_with" .= v]
TextEndsWith v -> Aeson.object ["ends_with" .= v]
TextIsEmpty -> Aeson.object ["is_empty" .= True]
TextIsNotEmpty -> Aeson.object ["is_not_empty" .= True]
-- | Number filter conditions.
data NumberCondition
= NumEquals Scientific
| NumDoesNotEqual Scientific
| NumGreaterThan Scientific
| NumGreaterThanOrEqualTo Scientific
| NumLessThan Scientific
| NumLessThanOrEqualTo Scientific
| NumIsEmpty
| NumIsNotEmpty
deriving stock (Eq, Show, Generic)
numberConditionToValue :: NumberCondition -> Aeson.Value
numberConditionToValue = \case
NumEquals v -> Aeson.object ["equals" .= v]
NumDoesNotEqual v -> Aeson.object ["does_not_equal" .= v]
NumGreaterThan v -> Aeson.object ["greater_than" .= v]
NumGreaterThanOrEqualTo v -> Aeson.object ["greater_than_or_equal_to" .= v]
NumLessThan v -> Aeson.object ["less_than" .= v]
NumLessThanOrEqualTo v -> Aeson.object ["less_than_or_equal_to" .= v]
NumIsEmpty -> Aeson.object ["is_empty" .= True]
NumIsNotEmpty -> Aeson.object ["is_not_empty" .= True]
-- | Checkbox filter conditions.
data CheckboxCondition
= CheckboxEquals Bool
| CheckboxDoesNotEqual Bool
deriving stock (Eq, Show, Generic)
checkboxConditionToValue :: CheckboxCondition -> Aeson.Value
checkboxConditionToValue = \case
CheckboxEquals v -> Aeson.object ["equals" .= v]
CheckboxDoesNotEqual v -> Aeson.object ["does_not_equal" .= v]
-- | Select filter conditions.
data SelectCondition
= SelectEquals Text
| SelectDoesNotEqual Text
| SelectIsEmpty
| SelectIsNotEmpty
deriving stock (Eq, Show, Generic)
selectConditionToValue :: SelectCondition -> Aeson.Value
selectConditionToValue = \case
SelectEquals v -> Aeson.object ["equals" .= v]
SelectDoesNotEqual v -> Aeson.object ["does_not_equal" .= v]
SelectIsEmpty -> Aeson.object ["is_empty" .= True]
SelectIsNotEmpty -> Aeson.object ["is_not_empty" .= True]
-- | Multi-select filter conditions.
data MultiSelectCondition
= MultiSelectContains Text
| MultiSelectDoesNotContain Text
| MultiSelectIsEmpty
| MultiSelectIsNotEmpty
deriving stock (Eq, Show, Generic)
multiSelectConditionToValue :: MultiSelectCondition -> Aeson.Value
multiSelectConditionToValue = \case
MultiSelectContains v -> Aeson.object ["contains" .= v]
MultiSelectDoesNotContain v -> Aeson.object ["does_not_contain" .= v]
MultiSelectIsEmpty -> Aeson.object ["is_empty" .= True]
MultiSelectIsNotEmpty -> Aeson.object ["is_not_empty" .= True]
-- | Date filter conditions. Also used for timestamp filters and created_time/last_edited_time.
--
-- Text values are ISO 8601 date strings (e.g., @\"2024-01-15\"@ or @\"2024-01-15T00:00:00Z\"@).
data DateCondition
= DateAfter Text
| DateBefore Text
| DateEquals Text
| DateOnOrAfter Text
| DateOnOrBefore Text
| DateIsEmpty
| DateIsNotEmpty
| DateNextWeek
| DateNextMonth
| DateNextYear
| DateThisWeek
| DateThisMonth
| DateThisYear
| DatePastWeek
| DatePastMonth
| DatePastYear
deriving stock (Eq, Show, Generic)
dateConditionToValue :: DateCondition -> Aeson.Value
dateConditionToValue = \case
DateAfter v -> Aeson.object ["after" .= v]
DateBefore v -> Aeson.object ["before" .= v]
DateEquals v -> Aeson.object ["equals" .= v]
DateOnOrAfter v -> Aeson.object ["on_or_after" .= v]
DateOnOrBefore v -> Aeson.object ["on_or_before" .= v]
DateIsEmpty -> Aeson.object ["is_empty" .= True]
DateIsNotEmpty -> Aeson.object ["is_not_empty" .= True]
DateNextWeek -> Aeson.object ["next_week" .= Aeson.object []]
DateNextMonth -> Aeson.object ["next_month" .= Aeson.object []]
DateNextYear -> Aeson.object ["next_year" .= Aeson.object []]
DateThisWeek -> Aeson.object ["this_week" .= Aeson.object []]
DateThisMonth -> Aeson.object ["this_month" .= Aeson.object []]
DateThisYear -> Aeson.object ["this_year" .= Aeson.object []]
DatePastWeek -> Aeson.object ["past_week" .= Aeson.object []]
DatePastMonth -> Aeson.object ["past_month" .= Aeson.object []]
DatePastYear -> Aeson.object ["past_year" .= Aeson.object []]
-- | People filter conditions. The Text value is a user UUID.
data PeopleCondition
= PeopleContains Text
| PeopleDoesNotContain Text
| PeopleIsEmpty
| PeopleIsNotEmpty
deriving stock (Eq, Show, Generic)
peopleConditionToValue :: PeopleCondition -> Aeson.Value
peopleConditionToValue = \case
PeopleContains v -> Aeson.object ["contains" .= v]
PeopleDoesNotContain v -> Aeson.object ["does_not_contain" .= v]
PeopleIsEmpty -> Aeson.object ["is_empty" .= True]
PeopleIsNotEmpty -> Aeson.object ["is_not_empty" .= True]
-- | Files filter conditions.
data FilesCondition
= FilesIsEmpty
| FilesIsNotEmpty
deriving stock (Eq, Show, Generic)
filesConditionToValue :: FilesCondition -> Aeson.Value
filesConditionToValue = \case
FilesIsEmpty -> Aeson.object ["is_empty" .= True]
FilesIsNotEmpty -> Aeson.object ["is_not_empty" .= True]
-- | Relation filter conditions. The Text value is a page UUID.
data RelationCondition
= RelationContains Text
| RelationDoesNotContain Text
| RelationIsEmpty
| RelationIsNotEmpty
deriving stock (Eq, Show, Generic)
relationConditionToValue :: RelationCondition -> Aeson.Value
relationConditionToValue = \case
RelationContains v -> Aeson.object ["contains" .= v]
RelationDoesNotContain v -> Aeson.object ["does_not_contain" .= v]
RelationIsEmpty -> Aeson.object ["is_empty" .= True]
RelationIsNotEmpty -> Aeson.object ["is_not_empty" .= True]
-- | Status filter conditions.
data StatusCondition
= StatusEquals Text
| StatusDoesNotEqual Text
| StatusIsEmpty
| StatusIsNotEmpty
deriving stock (Eq, Show, Generic)
statusConditionToValue :: StatusCondition -> Aeson.Value
statusConditionToValue = \case
StatusEquals v -> Aeson.object ["equals" .= v]
StatusDoesNotEqual v -> Aeson.object ["does_not_equal" .= v]
StatusIsEmpty -> Aeson.object ["is_empty" .= True]
StatusIsNotEmpty -> Aeson.object ["is_not_empty" .= True]
-- | Unique ID filter conditions.
data UniqueIdCondition
= UniqueIdEquals Natural
| UniqueIdDoesNotEqual Natural
| UniqueIdGreaterThan Natural
| UniqueIdGreaterThanOrEqualTo Natural
| UniqueIdLessThan Natural
| UniqueIdLessThanOrEqualTo Natural
deriving stock (Eq, Show, Generic)
uniqueIdConditionToValue :: UniqueIdCondition -> Aeson.Value
uniqueIdConditionToValue = \case
UniqueIdEquals v -> Aeson.object ["equals" .= v]
UniqueIdDoesNotEqual v -> Aeson.object ["does_not_equal" .= v]
UniqueIdGreaterThan v -> Aeson.object ["greater_than" .= v]
UniqueIdGreaterThanOrEqualTo v -> Aeson.object ["greater_than_or_equal_to" .= v]
UniqueIdLessThan v -> Aeson.object ["less_than" .= v]
UniqueIdLessThanOrEqualTo v -> Aeson.object ["less_than_or_equal_to" .= v]
-- | Verification filter condition.
-- The Text is one of @\"verified\"@, @\"expired\"@, or @\"none\"@.
data VerificationCondition
= VerificationStatus Text
deriving stock (Eq, Show, Generic)
verificationConditionToValue :: VerificationCondition -> Aeson.Value
verificationConditionToValue (VerificationStatus v) =
Aeson.object ["status" .= v]
-- | Formula filter condition, wrapping a condition by the formula's return type.
data FormulaCondition
= FormulaString TextCondition
| FormulaNumber NumberCondition
| FormulaDate DateCondition
| FormulaCheckbox CheckboxCondition
deriving stock (Eq, Show, Generic)
formulaConditionToValue :: FormulaCondition -> Aeson.Value
formulaConditionToValue = \case
FormulaString c -> Aeson.object ["string" .= textConditionToValue c]
FormulaNumber c -> Aeson.object ["number" .= numberConditionToValue c]
FormulaDate c -> Aeson.object ["date" .= dateConditionToValue c]
FormulaCheckbox c -> Aeson.object ["checkbox" .= checkboxConditionToValue c]
-- | Rollup filter condition.
data RollupCondition
= RollupAny PropertyCondition
| RollupEvery PropertyCondition
| RollupNone PropertyCondition
| RollupNumber NumberCondition
| RollupDate DateCondition
deriving stock (Eq, Show, Generic)
rollupConditionToValue :: RollupCondition -> Aeson.Value
rollupConditionToValue = \case
RollupAny c -> Aeson.object ["any" .= conditionInnerValue c]
RollupEvery c -> Aeson.object ["every" .= conditionInnerValue c]
RollupNone c -> Aeson.object ["none" .= conditionInnerValue c]
RollupNumber c -> Aeson.object ["number" .= numberConditionToValue c]
RollupDate c -> Aeson.object ["date" .= dateConditionToValue c]
where
conditionInnerValue :: PropertyCondition -> Aeson.Value
conditionInnerValue cond = Aeson.object (propertyConditionToObject cond)
-- =====================================================================
-- Sorts
-- =====================================================================
-- | Sort direction for query sorts.
data SortDirection
= Ascending
| Descending
deriving stock (Eq, Show, Generic)
instance ToJSON SortDirection where
toJSON Ascending = Aeson.String "ascending"
toJSON Descending = Aeson.String "descending"
-- | Sort specification for querying databases and data sources.
data Sort
= PropertySort Text SortDirection
| TimestampSort TimestampType SortDirection
deriving stock (Eq, Show, Generic)
instance ToJSON Sort where
toJSON (PropertySort propName dir) =
Aeson.object
[ "property" .= propName,
"direction" .= dir
]
toJSON (TimestampSort tsType dir) =
Aeson.object
[ "timestamp" .= timestampTypeToText tsType,
"direction" .= dir
]