packages feed

notion-client-0.3.0.0: src/Notion/V1/Pages.hs

-- | @\/v1\/pages@
module Notion.V1.Pages
  ( -- * Main types
    PageID,
    PageObject (..),
    CreatePage (..),
    UpdatePage (..),
    PageProperties,
    PropertyValue (..),
    PropertyItem (..),
    PropertyValueType (..),
    SelectOption (..),
    mkCreatePage,
    mkUpdatePage,

    -- * Markdown
    PageMarkdown (..),

    -- * Servant
    API,
  )
where

import Control.Applicative ((<|>))
import Data.Aeson ((.:), (.:?), (.=))
import Data.Aeson qualified as Aeson
import Data.Aeson.Key qualified as Key
import Data.Aeson.KeyMap qualified as KeyMap
import Notion.Prelude hiding (Number)
import Notion.V1.Common (Cover, Icon, ObjectType (..), Parent, UUID)
import Notion.V1.Users (UserReference)

-- | Page ID
type PageID = UUID

-- | Notion page object
data PageObject = PageObject
  { id :: PageID,
    createdTime :: POSIXTime,
    lastEditedTime :: POSIXTime,
    createdBy :: UserReference,
    lastEditedBy :: UserReference,
    cover :: Maybe Cover,
    icon :: Maybe Icon,
    parent :: Parent,
    inTrash :: Bool,
    properties :: Map Text PropertyItem,
    url :: Text,
    object :: ObjectType
  }
  deriving stock (Generic, Show)

instance FromJSON PageObject where
  parseJSON = \case
    Object o -> do
      id <- o .: "id"
      createdTimeStr <- o .: "created_time"
      createdTime <- parseISO8601 createdTimeStr
      lastEditedTimeStr <- o .: "last_edited_time"
      lastEditedTime <- parseISO8601 lastEditedTimeStr
      createdBy <- o .: "created_by"
      lastEditedBy <- o .: "last_edited_by"
      cover <- o .:? "cover"
      icon <- o .:? "icon"
      parent <- o .: "parent"
      inTrash <- (o .: "in_trash") <|> (o .: "is_archived") <|> (o .: "archived") <|> pure False
      properties <- o .: "properties"
      url <- o .: "url"
      object <- o .: "object"
      return PageObject {..}
    _ -> fail "Expected object for PageObject"

instance ToJSON PageObject where
  toJSON PageObject {..} =
    Aeson.object
      [ "id" .= id,
        "created_time" .= posixToISO8601 createdTime,
        "last_edited_time" .= posixToISO8601 lastEditedTime,
        "created_by" .= createdBy,
        "last_edited_by" .= lastEditedBy,
        "cover" .= cover,
        "icon" .= icon,
        "parent" .= parent,
        "in_trash" .= inTrash,
        "properties" .= properties,
        "url" .= url,
        "object" .= object
      ]

-- | Create a page request
data CreatePage = CreatePage
  { parent :: Parent,
    properties :: PageProperties,
    children :: Maybe (Vector Value),
    icon :: Maybe Icon,
    cover :: Maybe Cover
  }
  deriving stock (Generic, Show)

instance ToJSON CreatePage where
  toJSON = genericToJSON aesonOptions

-- | Smart constructor for 'CreatePage' with required fields
mkCreatePage :: Parent -> PageProperties -> CreatePage
mkCreatePage parent properties =
  CreatePage
    { parent,
      properties,
      children = Nothing,
      icon = Nothing,
      cover = Nothing
    }

-- | Update a page request
data UpdatePage = UpdatePage
  { properties :: PageProperties,
    inTrash :: Maybe Bool,
    icon :: Maybe Icon,
    cover :: Maybe Cover
  }
  deriving stock (Generic, Show)

instance ToJSON UpdatePage where
  toJSON = genericToJSON aesonOptions

-- | Smart constructor for 'UpdatePage' with required fields
mkUpdatePage :: PageProperties -> UpdatePage
mkUpdatePage properties =
  UpdatePage
    { properties,
      inTrash = Nothing,
      icon = Nothing,
      cover = Nothing
    }

-- | Page properties map
type PageProperties = Map Text PropertyValue

-- | Property value type for creating or updating pages
data PropertyValue = PropertyValue
  { type_ :: PropertyValueType,
    value :: Maybe Value
  }
  deriving stock (Generic, Show)

instance ToJSON PropertyValue where
  -- Direct conversion - the important part is that we're NOT nesting under "type" or "value" fields
  toJSON PropertyValue {type_ = Title, value = Just v} =
    -- For title property, directly put the array into a "title" field
    case v of
      Object o ->
        if KeyMap.member "title" o
          then Aeson.Object o -- Use this object directly, just have to wrap it
          else Aeson.object ["title" .= v] -- Otherwise wrap it
      _ -> Aeson.object ["title" .= ([] :: [Value])]
  -- Handle other property types
  toJSON PropertyValue {type_ = t, value = Just v} =
    -- Just directly use the value as the property content
    case v of
      Object o -> Aeson.Object o -- Use the object directly, but wrap it
      _ -> Aeson.object [] -- Empty object as fallback

  -- Empty property values
  toJSON PropertyValue {type_} = Aeson.object []

-- | Property item returned by API
data PropertyItem = PropertyItem
  { id :: Text,
    type_ :: PropertyValueType,
    value :: Maybe Value
  }
  deriving stock (Generic, Show)

instance FromJSON PropertyItem where
  parseJSON = \case
    Object o -> do
      id <- o .: "id"
      type_ <- o .: "type"
      -- The Notion API stores the property value under a type-specific key
      -- (e.g., "title", "select", "rich_text"), not under a generic "value" key.
      let typeKey = Key.fromText (propertyTypeToKey type_)
          value = KeyMap.lookup typeKey o >>= \v -> Just v
      return PropertyItem {..}
    _ -> fail "Expected object for PropertyItem"

instance ToJSON PropertyItem where
  toJSON PropertyItem {..} =
    let base = ["id" .= id, "type" .= type_]
        typeKey = Key.fromText (propertyTypeToKey type_)
        valueField = case value of
          Just v -> [typeKey .= v]
          Nothing -> []
     in Aeson.object (base <> valueField)

-- | Map a PropertyValueType to the JSON key the Notion API uses for its value
propertyTypeToKey :: PropertyValueType -> Text
propertyTypeToKey = \case
  Title -> "title"
  RichText -> "rich_text"
  Number -> "number"
  Select -> "select"
  MultiSelect -> "multi_select"
  Date -> "date"
  People -> "people"
  Files -> "files"
  Checkbox -> "checkbox"
  Url -> "url"
  Email -> "email"
  PhoneNumber -> "phone_number"
  Formula -> "formula"
  Relation -> "relation"
  Rollup -> "rollup"
  CreatedTime -> "created_time"
  CreatedBy -> "created_by"
  LastEditedTime -> "last_edited_time"
  LastEditedBy -> "last_edited_by"
  Status -> "status"
  UniqueId -> "unique_id"
  Place -> "place"
  Button -> "button"
  Verification -> "verification"

-- | Property value types
data PropertyValueType
  = Title
  | RichText
  | Number
  | Select
  | MultiSelect
  | Date
  | People
  | Files
  | Checkbox
  | Url
  | Email
  | PhoneNumber
  | Formula
  | Relation
  | Rollup
  | CreatedTime
  | CreatedBy
  | LastEditedTime
  | LastEditedBy
  | Status
  | UniqueId
  | Place
  | Button
  | Verification
  deriving stock (Eq, Generic, Show)

instance FromJSON PropertyValueType where
  parseJSON = genericParseJSON aesonOptions

instance ToJSON PropertyValueType where
  toJSON = genericToJSON aesonOptions

-- | Select option
data SelectOption = SelectOption
  { id :: Maybe Text,
    name :: Text,
    color :: Maybe Text
  }
  deriving stock (Generic, Show)

instance FromJSON SelectOption where
  parseJSON = genericParseJSON aesonOptions

instance ToJSON SelectOption where
  toJSON = genericToJSON aesonOptions

-- | Response from @GET \/v1\/pages\/{page_id}\/markdown@
--
-- Contains the page content rendered as Notion-flavored enhanced markdown.
data PageMarkdown = PageMarkdown
  { id :: PageID,
    markdown :: Text,
    truncated :: Bool,
    unknownBlockIds :: Vector UUID
  }
  deriving stock (Generic, Show)

instance FromJSON PageMarkdown where
  parseJSON = genericParseJSON aesonOptions

instance ToJSON PageMarkdown where
  toJSON = genericToJSON aesonOptions

-- | Servant API
type API =
  "pages"
    :> ( Capture "page_id" PageID
           :> Get '[JSON] PageObject
           :<|> ReqBody '[JSON] CreatePage
           :> Post '[JSON] PageObject
           :<|> Capture "page_id" PageID
           :> ReqBody '[JSON] UpdatePage
           :> Patch '[JSON] PageObject
           :<|> Capture "page_id" PageID
           :> "markdown"
           :> QueryParam "include_transcript" Bool
           :> Get '[JSON] PageMarkdown
       )