packages feed

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

-- | @\/v1\/data_sources@
--
-- Data sources represent the schema and content within a database.
-- A single database can contain multiple data sources (API version 2025-09-03+).
module Notion.V1.DataSources
  ( -- * Main types
    DataSourceID,
    DataSourceObject (..),
    CreateDataSource (..),
    UpdateDataSource (..),
    QueryDataSource (..),
    _QueryDataSource,
    QueryResultType (..),

    -- * Query and search results
    PageOrDataSource (..),
    PartialPageObject (..),
    PartialDataSourceObject (..),
    pageResults,
    dataSourceResults,
    resultId,
    resultCreatedTime,

    -- * Templates
    TemplateRef (..),
    ListTemplatesResponse (..),

    -- * Servant
    API,
  )
where

import Control.Applicative ((<|>))
import Data.Aeson ((.!=), (.:), (.:?), (.=))
import Data.Aeson qualified as Aeson
import Data.Aeson.KeyMap qualified as KeyMap
import Data.Aeson.Types (Parser)
import Data.Vector qualified as Vector
import Notion.Prelude
import Notion.V1.Common (Cover, Icon, ObjectType, Parent, UUID (..))
import Notion.V1.Databases (DatabaseType)
import Notion.V1.Filter (Filter, Sort)
import Notion.V1.ListOf (ListOf)
import Notion.V1.Pages (PageObject (..), PartialPageObject (..))
import Notion.V1.Properties (PropertySchema, PropertyUpdate)
import Notion.V1.RichText (RichText)
import Notion.V1.Users (UserReference)
import Servant.API (QueryParams)
import Prelude hiding (id)

-- | Data source ID
type DataSourceID = UUID

-- | Notion data source object
data DataSourceObject = DataSourceObject
  { id :: DataSourceID,
    createdTime :: POSIXTime,
    lastEditedTime :: POSIXTime,
    createdBy :: UserReference,
    lastEditedBy :: UserReference,
    title :: Vector RichText,
    description :: Vector RichText,
    properties :: Map Text PropertySchema,
    url :: Text,
    parent :: Parent,
    databaseParent :: Maybe Parent,
    isInline :: Maybe Bool,
    -- | The kind of typed database this data source belongs to, if any.
    databaseType :: Maybe DatabaseType,
    inTrash :: Maybe Bool,
    publicUrl :: Maybe Text,
    icon :: Maybe Icon,
    cover :: Maybe Cover,
    object :: ObjectType
  }
  deriving stock (Generic, Show)

instance FromJSON DataSourceObject 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"
      title <- o .: "title"
      description <- o .: "description"
      properties <- o .: "properties"
      url <- o .: "url"
      parent <- o .: "parent"
      databaseParent <- o .:? "database_parent"
      isInline <- o .:? "is_inline"
      databaseType <- o .:? "database_type"
      inTrash <- (fmap Just (o .: "in_trash")) <|> (fmap Just (o .: "is_archived")) <|> (fmap Just (o .: "archived")) <|> pure Nothing
      publicUrl <- o .:? "public_url"
      icon <- o .:? "icon"
      cover <- o .:? "cover"
      object <- o .: "object"
      return DataSourceObject {..}
    _ -> fail "Expected object for DataSourceObject"

-- | @{"object":"data_source","id":...,"properties":{...}}@
data PartialDataSourceObject = PartialDataSourceObject
  { id :: DataSourceID,
    properties :: Map Text PropertySchema
  }
  deriving stock (Generic, Show)

instance FromJSON PartialDataSourceObject where
  parseJSON = \case
    Object o -> PartialDataSourceObject <$> o .: "id" <*> (o .:? "properties" .!= mempty)
    _ -> fail "Expected object for PartialDataSourceObject"

-- | One result of a data source query or a search.
--
-- A page with a @url@ key, or a data source with a @title@ key, is full and must decode as
-- the full object; otherwise it is partial.
data PageOrDataSource
  = PageResult PageObject
  | PartialPageResult PartialPageObject
  | DataSourceResult DataSourceObject
  | PartialDataSourceResult PartialDataSourceObject
  | -- | An object type this client does not know; the raw JSON is kept.
    UnknownResult Value
  deriving stock (Generic, Show)

instance FromJSON PageOrDataSource where
  parseJSON v = case v of
    Object o -> do
      objectType <- o .:? "object" :: Parser (Maybe Text)
      case objectType of
        Just "page"
          | KeyMap.member "url" o -> PageResult <$> parseJSON v
          | otherwise -> PartialPageResult <$> parseJSON v
        Just "data_source"
          | KeyMap.member "title" o -> DataSourceResult <$> parseJSON v
          | otherwise -> PartialDataSourceResult <$> parseJSON v
        _ -> pure (UnknownResult v)
    _ -> pure (UnknownResult v)

-- | Full pages only.
pageResults :: Vector PageOrDataSource -> Vector PageObject
pageResults = Vector.mapMaybe $ \case
  PageResult p -> Just p
  _ -> Nothing

-- | Full data sources only.
dataSourceResults :: Vector PageOrDataSource -> Vector DataSourceObject
dataSourceResults = Vector.mapMaybe $ \case
  DataSourceResult d -> Just d
  _ -> Nothing

-- | The id of a result, if it has one (unknown results are inspected for a string @id@).
resultId :: PageOrDataSource -> Maybe Text
resultId = \case
  PageResult PageObject {id = UUID t} -> Just t
  PartialPageResult PartialPageObject {id = UUID t} -> Just t
  DataSourceResult DataSourceObject {id = UUID t} -> Just t
  PartialDataSourceResult PartialDataSourceObject {id = UUID t} -> Just t
  UnknownResult (Object o) -> case KeyMap.lookup "id" o of
    Just (String t) -> Just t
    _ -> Nothing
  UnknownResult _ -> Nothing

-- | @created_time@ of a full page or full data source; 'Nothing' for partial and unknown results.
resultCreatedTime :: PageOrDataSource -> Maybe POSIXTime
resultCreatedTime = \case
  PageResult PageObject {createdTime} -> Just createdTime
  DataSourceResult DataSourceObject {createdTime} -> Just createdTime
  _ -> Nothing

-- | Create data source request
data CreateDataSource = CreateDataSource
  { parent :: Parent,
    properties :: Map Text PropertySchema,
    title :: Maybe (Vector RichText),
    -- | Not in Notion's published request schema; omitted when 'Nothing'.
    description :: Maybe (Vector RichText),
    icon :: Maybe Icon,
    -- | Not in Notion's published request schema; omitted when 'Nothing'.
    cover :: Maybe Cover
  }
  deriving stock (Generic, Show)

instance ToJSON CreateDataSource where
  toJSON = genericToJSON aesonOptions

-- | Update data source request.
--
-- Each entry of @properties@ is a 'PropertyUpdate': remove ('RemoveProperty', sent as @null@),
-- rename only, a full schema, or an option-list update. @Nothing@ leaves properties untouched.
data UpdateDataSource = UpdateDataSource
  { title :: Maybe (Vector RichText),
    icon :: Maybe Icon,
    properties :: Maybe (Map Text PropertyUpdate),
    inTrash :: Maybe Bool,
    parent :: Maybe Parent
  }
  deriving stock (Generic, Show)

instance ToJSON UpdateDataSource where
  toJSON UpdateDataSource {..} =
    Aeson.object $
      maybe [] (\t -> ["title" .= t]) title
        <> maybe [] (\i -> ["icon" .= i]) icon
        <> maybe [] (\p -> ["properties" .= p]) properties
        <> maybe [] (\t -> ["in_trash" .= t]) inTrash
        <> maybe [] (\p -> ["parent" .= p]) parent

-- | Query data source request
data QueryDataSource = QueryDataSource
  { filter :: Maybe Filter,
    sorts :: Maybe [Sort],
    startCursor :: Maybe Text,
    pageSize :: Maybe Natural,
    inTrash :: Maybe Bool,
    -- | Limit which properties are returned in the response.
    -- Each element is a property ID (not name).
    filterProperties :: Maybe [Text],
    -- | Return only pages or only data sources. Regular (non-wiki) data sources only
    -- contain pages.
    resultType :: Maybe QueryResultType
  }
  deriving stock (Generic, Show)

-- | A query with every optional field unset. Use record update to set fields.
_QueryDataSource :: QueryDataSource
_QueryDataSource =
  QueryDataSource
    { filter = Nothing,
      sorts = Nothing,
      startCursor = Nothing,
      pageSize = Nothing,
      inTrash = Nothing,
      filterProperties = Nothing,
      resultType = Nothing
    }

-- | Restrict a query to pages or to data sources.
data QueryResultType = ResultTypePage | ResultTypeDataSource
  deriving stock (Eq, Show, Generic)

instance ToJSON QueryResultType where
  toJSON ResultTypePage = Aeson.String "page"
  toJSON ResultTypeDataSource = Aeson.String "data_source"

-- | @filter_properties@ is a query parameter, not a body field; 'Notion.V1.makeMethods'
-- moves 'filterProperties' into the URL.
instance ToJSON QueryDataSource where
  toJSON q = case genericToJSON aesonOptions q of
    Object o -> Object (KeyMap.delete "filter_properties" o)
    other -> other

-- | A reference to a data source template
data TemplateRef = TemplateRef
  { id :: UUID,
    name :: Text,
    isDefault :: Bool
  }
  deriving stock (Generic, Show)

instance FromJSON TemplateRef where
  parseJSON = genericParseJSON aesonOptions

-- | Response from @GET \/v1\/data_sources\/{data_source_id}\/templates@
data ListTemplatesResponse = ListTemplatesResponse
  { templates :: Vector TemplateRef,
    hasMore :: Bool,
    nextCursor :: Maybe Text
  }
  deriving stock (Generic, Show)

instance FromJSON ListTemplatesResponse where
  parseJSON = genericParseJSON aesonOptions

-- | Servant API
type API =
  "data_sources"
    :> ( Capture "data_source_id" DataSourceID
           :> Get '[JSON] DataSourceObject
           :<|> ReqBody '[JSON] CreateDataSource
           :> Post '[JSON] DataSourceObject
           :<|> Capture "data_source_id" DataSourceID
           :> ReqBody '[JSON] UpdateDataSource
           :> Patch '[JSON] DataSourceObject
           :<|> Capture "data_source_id" DataSourceID
           :> "query"
           :> QueryParams "filter_properties" Text
           :> ReqBody '[JSON] QueryDataSource
           :> Post '[JSON] (ListOf PageOrDataSource)
           :<|> Capture "data_source_id" DataSourceID
           :> "templates"
           :> QueryParam "name" Text
           :> QueryParam "start_cursor" Text
           :> QueryParam "page_size" Natural
           :> Get '[JSON] ListTemplatesResponse
       )