packages feed

notion-client-0.7.0.2: src/Notion/V1/Properties.hs

-- | Typed property schema definitions for Notion databases and data sources.
--
-- A property schema describes the "shape" of a database column — for example,
-- a select property schema defines which options are available (names and colors),
-- while a page's select property value is which option was chosen.
--
-- This module is used by both 'Notion.V1.Databases' and 'Notion.V1.DataSources'
-- for typed @properties@ fields.
module Notion.V1.Properties
  ( -- * Property schema
    PropertySchema (..),

    -- * Supporting types
    SelectColor (..),
    SelectOption (..),
    StatusGroup (..),
    NumberFormat (..),
    RollupFunction (..),
    RelationType (..),
  )
where

import Data.Aeson (object, (.:), (.:?), (.=))
import Data.Aeson qualified as Aeson
import Data.Aeson.Types (Parser)
import Notion.Prelude
import Notion.V1.Common (UUID)
import Prelude hiding (id)

-- | Colors available for select, multi-select, and status property options.
--
-- This is distinct from 'Notion.V1.Common.Color' which covers text annotation
-- colors including background variants.
data SelectColor
  = DefaultColor
  | Gray
  | Brown
  | Orange
  | Yellow
  | Green
  | Blue
  | Purple
  | Pink
  | Red
  deriving stock (Eq, Show, Generic)

instance FromJSON SelectColor where
  parseJSON = Aeson.withText "SelectColor" $ \case
    "default" -> pure DefaultColor
    "gray" -> pure Gray
    "brown" -> pure Brown
    "orange" -> pure Orange
    "yellow" -> pure Yellow
    "green" -> pure Green
    "blue" -> pure Blue
    "purple" -> pure Purple
    "pink" -> pure Pink
    "red" -> pure Red
    other -> fail $ "Unknown SelectColor: " <> unpack other

instance ToJSON SelectColor where
  toJSON DefaultColor = Aeson.String "default"
  toJSON Gray = Aeson.String "gray"
  toJSON Brown = Aeson.String "brown"
  toJSON Orange = Aeson.String "orange"
  toJSON Yellow = Aeson.String "yellow"
  toJSON Green = Aeson.String "green"
  toJSON Blue = Aeson.String "blue"
  toJSON Purple = Aeson.String "purple"
  toJSON Pink = Aeson.String "pink"
  toJSON Red = Aeson.String "red"

-- | A select or multi-select option in a property schema.
data SelectOption = SelectOption
  { id :: Maybe Text,
    name :: Text,
    color :: Maybe SelectColor
  }
  deriving stock (Eq, Show, Generic)

instance FromJSON SelectOption where
  parseJSON = genericParseJSON aesonOptions

instance ToJSON SelectOption where
  toJSON = genericToJSON aesonOptions

-- | A status group in a status property schema.
data StatusGroup = StatusGroup
  { id :: Maybe Text,
    name :: Text,
    color :: Maybe SelectColor,
    optionIds :: Vector Text
  }
  deriving stock (Eq, Show, Generic)

instance FromJSON StatusGroup where
  parseJSON = genericParseJSON aesonOptions

instance ToJSON StatusGroup where
  toJSON = genericToJSON aesonOptions

-- | Number format for number property schemas.
data NumberFormat
  = NumberPlain
  | NumberWithCommas
  | Percent
  | Dollar
  | CanadianDollar
  | Euro
  | Pound
  | Yen
  | Ruble
  | Rupee
  | Won
  | Yuan
  | Real
  | Lira
  | Rupiah
  | Franc
  | HongKongDollar
  | NewZealandDollar
  | Krona
  | NorwegianKrone
  | MexicanPeso
  | Rand
  | NewTaiwanDollar
  | DanishKrone
  | Zloty
  | Baht
  | Forint
  | Koruna
  | Shekel
  | ChileanPeso
  | PhilippinePeso
  | Dirham
  | ColombianPeso
  | Riyal
  | Ringgit
  | Leu
  | ArgentinePeso
  | UruguayanPeso
  | SingaporeDollar
  deriving stock (Eq, Show, Generic)

instance FromJSON NumberFormat where
  parseJSON = Aeson.withText "NumberFormat" $ \case
    "number" -> pure NumberPlain
    "number_with_commas" -> pure NumberWithCommas
    "percent" -> pure Percent
    "dollar" -> pure Dollar
    "canadian_dollar" -> pure CanadianDollar
    "euro" -> pure Euro
    "pound" -> pure Pound
    "yen" -> pure Yen
    "ruble" -> pure Ruble
    "rupee" -> pure Rupee
    "won" -> pure Won
    "yuan" -> pure Yuan
    "real" -> pure Real
    "lira" -> pure Lira
    "rupiah" -> pure Rupiah
    "franc" -> pure Franc
    "hong_kong_dollar" -> pure HongKongDollar
    "new_zealand_dollar" -> pure NewZealandDollar
    "krona" -> pure Krona
    "norwegian_krone" -> pure NorwegianKrone
    "mexican_peso" -> pure MexicanPeso
    "rand" -> pure Rand
    "new_taiwan_dollar" -> pure NewTaiwanDollar
    "danish_krone" -> pure DanishKrone
    "zloty" -> pure Zloty
    "baht" -> pure Baht
    "forint" -> pure Forint
    "koruna" -> pure Koruna
    "shekel" -> pure Shekel
    "chilean_peso" -> pure ChileanPeso
    "philippine_peso" -> pure PhilippinePeso
    "dirham" -> pure Dirham
    "colombian_peso" -> pure ColombianPeso
    "riyal" -> pure Riyal
    "ringgit" -> pure Ringgit
    "leu" -> pure Leu
    "argentine_peso" -> pure ArgentinePeso
    "uruguayan_peso" -> pure UruguayanPeso
    "singapore_dollar" -> pure SingaporeDollar
    other -> fail $ "Unknown NumberFormat: " <> unpack other

instance ToJSON NumberFormat where
  toJSON NumberPlain = Aeson.String "number"
  toJSON NumberWithCommas = Aeson.String "number_with_commas"
  toJSON Percent = Aeson.String "percent"
  toJSON Dollar = Aeson.String "dollar"
  toJSON CanadianDollar = Aeson.String "canadian_dollar"
  toJSON Euro = Aeson.String "euro"
  toJSON Pound = Aeson.String "pound"
  toJSON Yen = Aeson.String "yen"
  toJSON Ruble = Aeson.String "ruble"
  toJSON Rupee = Aeson.String "rupee"
  toJSON Won = Aeson.String "won"
  toJSON Yuan = Aeson.String "yuan"
  toJSON Real = Aeson.String "real"
  toJSON Lira = Aeson.String "lira"
  toJSON Rupiah = Aeson.String "rupiah"
  toJSON Franc = Aeson.String "franc"
  toJSON HongKongDollar = Aeson.String "hong_kong_dollar"
  toJSON NewZealandDollar = Aeson.String "new_zealand_dollar"
  toJSON Krona = Aeson.String "krona"
  toJSON NorwegianKrone = Aeson.String "norwegian_krone"
  toJSON MexicanPeso = Aeson.String "mexican_peso"
  toJSON Rand = Aeson.String "rand"
  toJSON NewTaiwanDollar = Aeson.String "new_taiwan_dollar"
  toJSON DanishKrone = Aeson.String "danish_krone"
  toJSON Zloty = Aeson.String "zloty"
  toJSON Baht = Aeson.String "baht"
  toJSON Forint = Aeson.String "forint"
  toJSON Koruna = Aeson.String "koruna"
  toJSON Shekel = Aeson.String "shekel"
  toJSON ChileanPeso = Aeson.String "chilean_peso"
  toJSON PhilippinePeso = Aeson.String "philippine_peso"
  toJSON Dirham = Aeson.String "dirham"
  toJSON ColombianPeso = Aeson.String "colombian_peso"
  toJSON Riyal = Aeson.String "riyal"
  toJSON Ringgit = Aeson.String "ringgit"
  toJSON Leu = Aeson.String "leu"
  toJSON ArgentinePeso = Aeson.String "argentine_peso"
  toJSON UruguayanPeso = Aeson.String "uruguayan_peso"
  toJSON SingaporeDollar = Aeson.String "singapore_dollar"

-- | Rollup aggregation function.
data RollupFunction
  = CountAll
  | CountValues
  | CountUniqueValues
  | CountEmpty
  | CountNotEmpty
  | PercentEmpty
  | PercentNotEmpty
  | Sum
  | Average
  | Median
  | Min
  | Max
  | Range
  | ShowOriginal
  | Checked
  | Unchecked
  | PercentChecked
  | PercentUnchecked
  | DateRange
  | EarliestDate
  | LatestDate
  | ShowUnique
  | Count
  | Empty
  | NotEmpty
  | CountPerGroup
  | PercentPerGroup
  | Unique
  deriving stock (Eq, Show, Generic)

instance FromJSON RollupFunction where
  parseJSON = Aeson.withText "RollupFunction" $ \case
    "count_all" -> pure CountAll
    "count_values" -> pure CountValues
    "count_unique_values" -> pure CountUniqueValues
    "count_empty" -> pure CountEmpty
    "count_not_empty" -> pure CountNotEmpty
    "percent_empty" -> pure PercentEmpty
    "percent_not_empty" -> pure PercentNotEmpty
    "sum" -> pure Sum
    "average" -> pure Average
    "median" -> pure Median
    "min" -> pure Min
    "max" -> pure Max
    "range" -> pure Range
    "show_original" -> pure ShowOriginal
    "checked" -> pure Checked
    "unchecked" -> pure Unchecked
    "percent_checked" -> pure PercentChecked
    "percent_unchecked" -> pure PercentUnchecked
    "date_range" -> pure DateRange
    "earliest_date" -> pure EarliestDate
    "latest_date" -> pure LatestDate
    "show_unique" -> pure ShowUnique
    "count" -> pure Count
    "empty" -> pure Empty
    "not_empty" -> pure NotEmpty
    "count_per_group" -> pure CountPerGroup
    "percent_per_group" -> pure PercentPerGroup
    "unique" -> pure Unique
    other -> fail $ "Unknown RollupFunction: " <> unpack other

instance ToJSON RollupFunction where
  toJSON CountAll = Aeson.String "count_all"
  toJSON CountValues = Aeson.String "count_values"
  toJSON CountUniqueValues = Aeson.String "count_unique_values"
  toJSON CountEmpty = Aeson.String "count_empty"
  toJSON CountNotEmpty = Aeson.String "count_not_empty"
  toJSON PercentEmpty = Aeson.String "percent_empty"
  toJSON PercentNotEmpty = Aeson.String "percent_not_empty"
  toJSON Sum = Aeson.String "sum"
  toJSON Average = Aeson.String "average"
  toJSON Median = Aeson.String "median"
  toJSON Min = Aeson.String "min"
  toJSON Max = Aeson.String "max"
  toJSON Range = Aeson.String "range"
  toJSON ShowOriginal = Aeson.String "show_original"
  toJSON Checked = Aeson.String "checked"
  toJSON Unchecked = Aeson.String "unchecked"
  toJSON PercentChecked = Aeson.String "percent_checked"
  toJSON PercentUnchecked = Aeson.String "percent_unchecked"
  toJSON DateRange = Aeson.String "date_range"
  toJSON EarliestDate = Aeson.String "earliest_date"
  toJSON LatestDate = Aeson.String "latest_date"
  toJSON ShowUnique = Aeson.String "show_unique"
  toJSON Count = Aeson.String "count"
  toJSON Empty = Aeson.String "empty"
  toJSON NotEmpty = Aeson.String "not_empty"
  toJSON CountPerGroup = Aeson.String "count_per_group"
  toJSON PercentPerGroup = Aeson.String "percent_per_group"
  toJSON Unique = Aeson.String "unique"

-- | Relation property type configuration.
data RelationType
  = SingleProperty
  | DualProperty
      { syncedPropertyId :: Text,
        syncedPropertyName :: Text
      }
  deriving stock (Eq, Show, Generic)

instance FromJSON RelationType where
  parseJSON = \case
    Object o -> do
      relType <- o .: "type"
      case relType of
        "single_property" -> pure SingleProperty
        "dual_property" -> do
          dp <- o .: "dual_property"
          syncedPropertyId <- dp .: "synced_property_id"
          syncedPropertyName <- dp .: "synced_property_name"
          pure DualProperty {..}
        other -> fail $ "Unknown RelationType: " <> unpack other
    _ -> fail "Expected object for RelationType"

instance ToJSON RelationType where
  toJSON SingleProperty =
    object ["type" .= ("single_property" :: Text)]
  toJSON DualProperty {..} =
    object
      [ "type" .= ("dual_property" :: Text),
        "dual_property"
          .= object
            [ "synced_property_id" .= syncedPropertyId,
              "synced_property_name" .= syncedPropertyName
            ]
      ]

-- | Typed property schema for a database or data source property.
--
-- Each constructor carries the common envelope fields (@schemaId@, @schemaName@)
-- plus any type-specific configuration. The JSON representation uses a @type@
-- discriminator with the configuration nested under a key matching the type name.
data PropertySchema
  = TitleSchema {schemaId :: Text, schemaName :: Text}
  | RichTextSchema {schemaId :: Text, schemaName :: Text}
  | NumberSchema {schemaId :: Text, schemaName :: Text, numberFormat :: NumberFormat}
  | SelectSchema {schemaId :: Text, schemaName :: Text, selectOptions :: Vector SelectOption}
  | MultiSelectSchema {schemaId :: Text, schemaName :: Text, multiSelectOptions :: Vector SelectOption}
  | DateSchema {schemaId :: Text, schemaName :: Text}
  | PeopleSchema {schemaId :: Text, schemaName :: Text}
  | FilesSchema {schemaId :: Text, schemaName :: Text}
  | CheckboxSchema {schemaId :: Text, schemaName :: Text}
  | UrlSchema {schemaId :: Text, schemaName :: Text}
  | EmailSchema {schemaId :: Text, schemaName :: Text}
  | PhoneNumberSchema {schemaId :: Text, schemaName :: Text}
  | FormulaSchema {schemaId :: Text, schemaName :: Text, formulaExpression :: Text}
  | RelationSchema {schemaId :: Text, schemaName :: Text, relationDataSourceId :: UUID, relationType :: RelationType}
  | RollupSchema
      { schemaId :: Text,
        schemaName :: Text,
        rollupFunction :: RollupFunction,
        rollupRelationPropertyName :: Maybe Text,
        rollupRelationPropertyId :: Maybe Text,
        rollupPropertyName :: Maybe Text,
        rollupPropertyId :: Maybe Text
      }
  | CreatedTimeSchema {schemaId :: Text, schemaName :: Text}
  | CreatedBySchema {schemaId :: Text, schemaName :: Text}
  | LastEditedTimeSchema {schemaId :: Text, schemaName :: Text}
  | LastEditedBySchema {schemaId :: Text, schemaName :: Text}
  | StatusSchema {schemaId :: Text, schemaName :: Text, statusOptions :: Vector SelectOption, statusGroups :: Vector StatusGroup}
  | UniqueIdSchema {schemaId :: Text, schemaName :: Text, uniqueIdPrefix :: Maybe Text}
  | PlaceSchema {schemaId :: Text, schemaName :: Text}
  | ButtonSchema {schemaId :: Text, schemaName :: Text}
  | VerificationSchema {schemaId :: Text, schemaName :: Text}
  deriving stock (Eq, Show, Generic)

instance FromJSON PropertySchema where
  parseJSON = \case
    Object o -> do
      sid <- o .: "id"
      sname <- o .: "name"
      propType <- o .: "type"
      parseByType sid sname propType o
    _ -> fail "Expected object for PropertySchema"
    where
      parseByType :: Text -> Text -> Text -> Aeson.Object -> Parser PropertySchema
      parseByType sid sname = \case
        "title" -> \_ -> pure TitleSchema {schemaId = sid, schemaName = sname}
        "rich_text" -> \_ -> pure RichTextSchema {schemaId = sid, schemaName = sname}
        "number" -> \o -> do
          cfg <- o .: "number"
          fmt <- cfg .: "format"
          pure NumberSchema {schemaId = sid, schemaName = sname, numberFormat = fmt}
        "select" -> \o -> do
          cfg <- o .: "select"
          opts <- cfg .: "options"
          pure SelectSchema {schemaId = sid, schemaName = sname, selectOptions = opts}
        "multi_select" -> \o -> do
          cfg <- o .: "multi_select"
          opts <- cfg .: "options"
          pure MultiSelectSchema {schemaId = sid, schemaName = sname, multiSelectOptions = opts}
        "date" -> \_ -> pure DateSchema {schemaId = sid, schemaName = sname}
        "people" -> \_ -> pure PeopleSchema {schemaId = sid, schemaName = sname}
        "files" -> \_ -> pure FilesSchema {schemaId = sid, schemaName = sname}
        "checkbox" -> \_ -> pure CheckboxSchema {schemaId = sid, schemaName = sname}
        "url" -> \_ -> pure UrlSchema {schemaId = sid, schemaName = sname}
        "email" -> \_ -> pure EmailSchema {schemaId = sid, schemaName = sname}
        "phone_number" -> \_ -> pure PhoneNumberSchema {schemaId = sid, schemaName = sname}
        "formula" -> \o -> do
          cfg <- o .: "formula"
          expr <- cfg .: "expression"
          pure FormulaSchema {schemaId = sid, schemaName = sname, formulaExpression = expr}
        "relation" -> \o -> do
          cfg <- o .: "relation"
          dsId <- cfg .: "data_source_id"
          relType <- Aeson.parseJSON (Object cfg)
          pure RelationSchema {schemaId = sid, schemaName = sname, relationDataSourceId = dsId, relationType = relType}
        "rollup" -> \o -> do
          cfg <- o .: "rollup"
          fn <- cfg .: "function"
          relPropName <- cfg .:? "relation_property_name"
          relPropId <- cfg .:? "relation_property_id"
          propName <- cfg .:? "rollup_property_name"
          propId <- cfg .:? "rollup_property_id"
          pure
            RollupSchema
              { schemaId = sid,
                schemaName = sname,
                rollupFunction = fn,
                rollupRelationPropertyName = relPropName,
                rollupRelationPropertyId = relPropId,
                rollupPropertyName = propName,
                rollupPropertyId = propId
              }
        "created_time" -> \_ -> pure CreatedTimeSchema {schemaId = sid, schemaName = sname}
        "created_by" -> \_ -> pure CreatedBySchema {schemaId = sid, schemaName = sname}
        "last_edited_time" -> \_ -> pure LastEditedTimeSchema {schemaId = sid, schemaName = sname}
        "last_edited_by" -> \_ -> pure LastEditedBySchema {schemaId = sid, schemaName = sname}
        "status" -> \o -> do
          cfg <- o .: "status"
          opts <- cfg .: "options"
          grps <- cfg .: "groups"
          pure StatusSchema {schemaId = sid, schemaName = sname, statusOptions = opts, statusGroups = grps}
        "unique_id" -> \o -> do
          cfg <- o .: "unique_id"
          prefix <- cfg .:? "prefix"
          pure UniqueIdSchema {schemaId = sid, schemaName = sname, uniqueIdPrefix = prefix}
        "place" -> \_ -> pure PlaceSchema {schemaId = sid, schemaName = sname}
        "button" -> \_ -> pure ButtonSchema {schemaId = sid, schemaName = sname}
        "verification" -> \_ -> pure VerificationSchema {schemaId = sid, schemaName = sname}
        other -> \_ -> fail $ "Unknown property type: " <> unpack other

instance ToJSON PropertySchema where
  toJSON schema =
    let (sid, sname, typeName, typeConfig) = schemaFields schema
     in object $
          [ "id" .= sid,
            "name" .= sname,
            "type" .= typeName
          ]
            <> [typeName .= typeConfig]

schemaFields :: PropertySchema -> (Text, Text, Aeson.Key, Value)
schemaFields = \case
  TitleSchema {..} -> (schemaId, schemaName, "title", object [])
  RichTextSchema {..} -> (schemaId, schemaName, "rich_text", object [])
  NumberSchema {..} -> (schemaId, schemaName, "number", object ["format" .= numberFormat])
  SelectSchema {..} -> (schemaId, schemaName, "select", object ["options" .= selectOptions])
  MultiSelectSchema {..} -> (schemaId, schemaName, "multi_select", object ["options" .= multiSelectOptions])
  DateSchema {..} -> (schemaId, schemaName, "date", object [])
  PeopleSchema {..} -> (schemaId, schemaName, "people", object [])
  FilesSchema {..} -> (schemaId, schemaName, "files", object [])
  CheckboxSchema {..} -> (schemaId, schemaName, "checkbox", object [])
  UrlSchema {..} -> (schemaId, schemaName, "url", object [])
  EmailSchema {..} -> (schemaId, schemaName, "email", object [])
  PhoneNumberSchema {..} -> (schemaId, schemaName, "phone_number", object [])
  FormulaSchema {..} -> (schemaId, schemaName, "formula", object ["expression" .= formulaExpression])
  RelationSchema {..} ->
    let relObj = case relationType of
          SingleProperty ->
            object
              [ "data_source_id" .= relationDataSourceId,
                "type" .= ("single_property" :: Text),
                "single_property" .= object []
              ]
          DualProperty {..} ->
            object
              [ "data_source_id" .= relationDataSourceId,
                "type" .= ("dual_property" :: Text),
                "dual_property" .= object ["synced_property_id" .= syncedPropertyId, "synced_property_name" .= syncedPropertyName]
              ]
     in (schemaId, schemaName, "relation", relObj)
  RollupSchema {..} ->
    ( schemaId,
      schemaName,
      "rollup",
      object $
        ["function" .= rollupFunction]
          <> maybe [] (\v -> ["relation_property_name" .= v]) rollupRelationPropertyName
          <> maybe [] (\v -> ["relation_property_id" .= v]) rollupRelationPropertyId
          <> maybe [] (\v -> ["rollup_property_name" .= v]) rollupPropertyName
          <> maybe [] (\v -> ["rollup_property_id" .= v]) rollupPropertyId
    )
  CreatedTimeSchema {..} -> (schemaId, schemaName, "created_time", object [])
  CreatedBySchema {..} -> (schemaId, schemaName, "created_by", object [])
  LastEditedTimeSchema {..} -> (schemaId, schemaName, "last_edited_time", object [])
  LastEditedBySchema {..} -> (schemaId, schemaName, "last_edited_by", object [])
  StatusSchema {..} -> (schemaId, schemaName, "status", object ["options" .= statusOptions, "groups" .= statusGroups])
  UniqueIdSchema {..} ->
    ( schemaId,
      schemaName,
      "unique_id",
      object $ maybe [] (\v -> ["prefix" .= v]) uniqueIdPrefix
    )
  PlaceSchema {..} -> (schemaId, schemaName, "place", object [])
  ButtonSchema {..} -> (schemaId, schemaName, "button", object [])
  VerificationSchema {..} -> (schemaId, schemaName, "verification", object [])