packages feed

notion-client-0.8.0.0: src/Notion/V1/Views.hs

-- | @\/v1\/views@
--
-- Views represent saved configurations of database data (filters, sorts, layout)
-- across 10 view types: table, board, list, calendar, timeline, gallery, form,
-- chart, map, and dashboard.
module Notion.V1.Views
  ( -- * Main types
    ViewID,
    ViewObject (..),
    ViewType (..),
    CreateView (..),
    UpdateView (..),

    -- * Filters, sorts and placement
    ViewFilter (..),
    ViewSort (..),
    QuickFilter (..),
    ViewPropertySort (..),
    ViewPosition (..),
    WidgetPlacement (..),
    CreateDatabaseForView (..),
    Clearable (..),

    -- * Configuration
    module Notion.V1.ViewConfig,

    -- * View queries
    ViewQueryID,
    CreateViewQuery (..),
    ViewQuery (..),
    DeletedViewQuery (..),
    PartialPageObject (..),

    -- * Servant
    API,
  )
where

import Control.Applicative ((<|>))
import Data.Aeson ((.:), (.:?), (.=))
import Data.Aeson qualified as Aeson
import Notion.Prelude
import Notion.V1.Clearable (Clearable (..))
import Notion.V1.Common (ObjectType, Parent, UUID)
import Notion.V1.Filter (Filter, PropertyCondition, Sort, SortDirection)
import Notion.V1.ListOf (ListOf, RequestStatus)
import Notion.V1.Pages (PartialPageObject (..))
import Notion.V1.Users (UserReference)
import Notion.V1.ViewConfig
import Prelude hiding (id)

-- | View ID
type ViewID = UUID

-- | View types supported by the Notion API
data ViewType
  = TableView
  | BoardView
  | ListViewType
  | CalendarView
  | TimelineView
  | GalleryView
  | FormView
  | ChartView
  | MapView
  | DashboardView
  | -- | A view type this library does not know yet; holds the raw string.
    UnknownViewType Text
  deriving stock (Eq, Show, Generic)

instance FromJSON ViewType where
  parseJSON = Aeson.withText "ViewType" $ \case
    "table" -> pure TableView
    "board" -> pure BoardView
    "list" -> pure ListViewType
    "calendar" -> pure CalendarView
    "timeline" -> pure TimelineView
    "gallery" -> pure GalleryView
    "form" -> pure FormView
    "chart" -> pure ChartView
    "map" -> pure MapView
    "dashboard" -> pure DashboardView
    other -> pure (UnknownViewType other)

instance ToJSON ViewType where
  toJSON = \case
    TableView -> "table"
    BoardView -> "board"
    ListViewType -> "list"
    CalendarView -> "calendar"
    TimelineView -> "timeline"
    GalleryView -> "gallery"
    FormView -> "form"
    ChartView -> "chart"
    MapView -> "map"
    DashboardView -> "dashboard"
    UnknownViewType t -> String t

-- | A view's filter: typed when the 'Filter' DSL can express it, raw JSON otherwise.
data ViewFilter
  = ViewFilter Filter
  | RawViewFilter Value
  deriving stock (Eq, Show)

instance FromJSON ViewFilter where
  parseJSON v = (ViewFilter <$> parseJSON v) <|> pure (RawViewFilter v)

instance ToJSON ViewFilter where
  toJSON = \case
    ViewFilter f -> toJSON f
    RawViewFilter v -> v

-- | A view sort: typed property or timestamp sort, or raw JSON.
data ViewSort
  = ViewSort Sort
  | RawViewSort Value
  deriving stock (Eq, Show)

instance FromJSON ViewSort where
  parseJSON v = (ViewSort <$> parseJSON v) <|> pure (RawViewSort v)

instance ToJSON ViewSort where
  toJSON = \case
    ViewSort s -> toJSON s
    RawViewSort v -> v

-- | A quick filter condition (a property condition without the @property@
-- key, e.g. @{"select":{"equals":"High"}}@), or raw JSON.
data QuickFilter
  = QuickFilter PropertyCondition
  | RawQuickFilter Value
  deriving stock (Eq, Show)

instance FromJSON QuickFilter where
  parseJSON v = (QuickFilter <$> parseJSON v) <|> pure (RawQuickFilter v)

instance ToJSON QuickFilter where
  toJSON = \case
    QuickFilter c -> toJSON c
    RawQuickFilter v -> v

-- | A property sort, the only kind 'UpdateView' accepts.
data ViewPropertySort = ViewPropertySort
  { property :: Text,
    direction :: SortDirection
  }
  deriving stock (Eq, Generic, Show)

instance ToJSON ViewPropertySort where
  toJSON = genericToJSON aesonOptions

-- | Where a new view tab goes in the database's tab bar.
data ViewPosition
  = ViewPositionStart
  | ViewPositionEnd
  | ViewPositionAfterView ViewID
  deriving stock (Eq, Show)

instance ToJSON ViewPosition where
  toJSON = \case
    ViewPositionStart -> Aeson.object ["type" .= ("start" :: Text)]
    ViewPositionEnd -> Aeson.object ["type" .= ("end" :: Text)]
    ViewPositionAfterView v -> Aeson.object ["type" .= ("after_view" :: Text), "view_id" .= v]

-- | Where a new widget goes inside a dashboard view (0-based row index).
data WidgetPlacement
  = NewRow (Maybe Natural)
  | ExistingRow Natural
  deriving stock (Eq, Show)

instance ToJSON WidgetPlacement where
  toJSON = \case
    NewRow Nothing -> Aeson.object ["type" .= ("new_row" :: Text)]
    NewRow (Just i) -> Aeson.object ["type" .= ("new_row" :: Text), "row_index" .= i]
    ExistingRow i -> Aeson.object ["type" .= ("existing_row" :: Text), "row_index" .= i]

-- | Create a new linked database block on a page and put the view in it.
data CreateDatabaseForView = CreateDatabaseForView
  { parentPageId :: UUID,
    afterBlockId :: Maybe UUID
  }
  deriving stock (Eq, Show)

instance ToJSON CreateDatabaseForView where
  toJSON CreateDatabaseForView {..} =
    Aeson.object $
      ["parent" .= Aeson.object ["type" .= ("page_id" :: Text), "page_id" .= parentPageId]]
        <> maybe [] (\b -> ["position" .= Aeson.object ["type" .= ("after_block" :: Text), "block_id" .= b]]) afterBlockId

-- | Notion view object
--
-- Many fields are 'Maybe' because the API returns partial or full view objects
-- depending on context (list endpoints return minimal objects with just id, parent, type).
data ViewObject = ViewObject
  { id :: ViewID,
    parent :: Maybe Parent,
    name :: Maybe Text,
    type_ :: Maybe ViewType,
    createdTime :: Maybe POSIXTime,
    lastEditedTime :: Maybe POSIXTime,
    url :: Maybe Text,
    dataSourceId :: Maybe UUID,
    createdBy :: Maybe UserReference,
    lastEditedBy :: Maybe UserReference,
    filter :: Maybe ViewFilter,
    sorts :: Maybe (Vector ViewSort),
    quickFilters :: Maybe (Map Text QuickFilter),
    configuration :: Maybe ViewConfig,
    dashboardViewId :: Maybe ViewID,
    object :: Maybe ObjectType
  }
  deriving stock (Generic, Show)

instance FromJSON ViewObject where
  parseJSON = \case
    Object o -> do
      id <- o .: "id"
      parent <- o .:? "parent"
      name <- o .:? "name"
      type_ <- o .:? "type"
      mCreatedTimeStr <- o .:? "created_time"
      createdTime <- traverse parseISO8601 mCreatedTimeStr
      mLastEditedTimeStr <- o .:? "last_edited_time"
      lastEditedTime <- traverse parseISO8601 mLastEditedTimeStr
      url <- o .:? "url"
      dataSourceId <- o .:? "data_source_id"
      createdBy <- o .:? "created_by"
      lastEditedBy <- o .:? "last_edited_by"
      filter <- o .:? "filter"
      sorts <- o .:? "sorts"
      quickFilters <- o .:? "quick_filters"
      configuration <- o .:? "configuration"
      dashboardViewId <- o .:? "dashboard_view_id"
      object <- o .:? "object"
      return ViewObject {..}
    _ -> fail "Expected object for ViewObject"

-- | Create a view request
data CreateView = CreateView
  { dataSourceId :: UUID,
    name :: Text,
    type_ :: ViewType,
    -- | Mutually exclusive with 'viewId' and 'createDatabase_'
    databaseId :: Maybe UUID,
    -- | Dashboard view to add this view to as a widget
    viewId :: Maybe ViewID,
    filter :: Maybe ViewFilter,
    sorts :: Maybe (Vector ViewSort),
    -- | Keyed by property ID
    quickFilters :: Maybe (Map Text QuickFilter),
    -- | Wire name @create_database@
    createDatabase_ :: Maybe CreateDatabaseForView,
    configuration :: Maybe ViewConfig,
    position :: Maybe ViewPosition,
    placement :: Maybe WidgetPlacement
  }
  deriving stock (Generic, Show)

instance ToJSON CreateView where
  toJSON = genericToJSON aesonOptions

-- | Update a view request. 'Unset' leaves a field unchanged and 'Clear' sends
-- @null@ to clear it.
data UpdateView = UpdateView
  { name :: Maybe Text,
    filter :: Clearable ViewFilter,
    sorts :: Clearable (Vector ViewPropertySort),
    -- | A 'Nothing' value removes that quick filter; 'Clear' removes all of them
    quickFilters :: Clearable (Map Text (Maybe QuickFilter)),
    configuration :: Maybe ViewConfig
  }
  deriving stock (Generic, Show)

instance ToJSON UpdateView where
  toJSON = genericToJSON aesonOptions

-- | View query ID
type ViewQueryID = UUID

-- | Body of @POST views/{view_id}/queries@.
newtype CreateViewQuery = CreateViewQuery
  { -- | Results per page (max 100)
    pageSize :: Maybe Natural
  }
  deriving stock (Generic, Show)

instance ToJSON CreateViewQuery where
  toJSON = genericToJSON aesonOptions

-- | Response of @POST views/{view_id}/queries@: a cached server-side snapshot
-- of the rows the view matches, plus its first page of results.
data ViewQuery = ViewQuery
  { id :: ViewQueryID,
    viewId :: ViewID,
    -- | When the cached results expire
    expiresAt :: POSIXTime,
    totalCount :: Natural,
    results :: Vector PartialPageObject,
    nextCursor :: Maybe Text,
    hasMore :: Bool,
    requestStatus :: Maybe RequestStatus
  }
  deriving stock (Generic, Show)

instance FromJSON ViewQuery where
  parseJSON = \case
    Object o -> do
      id <- o .: "id"
      viewId <- o .: "view_id"
      expiresAt <- o .: "expires_at" >>= parseISO8601
      totalCount <- o .: "total_count"
      results <- o .: "results"
      nextCursor <- o .:? "next_cursor"
      hasMore <- o .: "has_more"
      requestStatus <- o .:? "request_status"
      pure ViewQuery {..}
    _ -> fail "Expected object for ViewQuery"

-- | Response of @DELETE views/{view_id}/queries/{query_id}@.
data DeletedViewQuery = DeletedViewQuery
  { id :: ViewQueryID,
    deleted :: Bool
  }
  deriving stock (Generic, Show)

instance FromJSON DeletedViewQuery where
  parseJSON = \case
    Object o -> DeletedViewQuery <$> o .: "id" <*> o .: "deleted"
    _ -> fail "Expected object for DeletedViewQuery"

-- | Servant API
type API =
  "views"
    :> ( ReqBody '[JSON] CreateView
           :> Post '[JSON] ViewObject
           :<|> Capture "view_id" ViewID
           :> Get '[JSON] ViewObject
           :<|> Capture "view_id" ViewID
           :> ReqBody '[JSON] UpdateView
           :> Patch '[JSON] ViewObject
           :<|> Capture "view_id" ViewID
           :> Delete '[JSON] ViewObject
           :<|> QueryParam "database_id" UUID
           :> QueryParam "data_source_id" UUID
           :> QueryParam "start_cursor" Text
           :> QueryParam "page_size" Natural
           :> Get '[JSON] (ListOf ViewObject)
           :<|> Capture "view_id" ViewID
           :> "queries"
           :> ReqBody '[JSON] CreateViewQuery
           :> Post '[JSON] ViewQuery
           :<|> Capture "view_id" ViewID
           :> "queries"
           :> Capture "query_id" ViewQueryID
           :> QueryParam "start_cursor" Text
           :> QueryParam "page_size" Natural
           :> Get '[JSON] (ListOf PartialPageObject)
           :<|> Capture "view_id" ViewID
           :> "queries"
           :> Capture "query_id" ViewQueryID
           :> Delete '[JSON] DeletedViewQuery
       )